diff --git a/.github/workflows/siibra-testing.yml b/.github/workflows/siibra-testing.yml index 1a0917ebd..07bd0fe04 100644 --- a/.github/workflows/siibra-testing.yml +++ b/.github/workflows/siibra-testing.yml @@ -89,6 +89,30 @@ jobs: fi pytest -rx e2e + ctrl-sxplr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: "Install siibra" + run: pip install -e . + - name: "Install ctrl-sxplr dep" + run: | + pip install -r siibra/explorer/requirements.txt + python -m playwright install chromium + - name: "Run test" + run: | + python siibra/explorer/sample_playwrite.py --video + - uses: actions/upload-artifact@v4 + with: + name: video-artefact + path: | + video + retention-days: 3 + check-importable: runs-on: ubuntu-22.04 env: diff --git a/siibra/explorer/README.md b/siibra/explorer/README.md new file mode 100644 index 000000000..b3e9ef732 --- /dev/null +++ b/siibra/explorer/README.md @@ -0,0 +1,56 @@ +# (experimental) Explorer extension + +Control siibra-explorer with python + +```python +from siibra.explorer.plugin import Explorer +explorer = Explorer() +explorer.start() + +# Open the link specified in the console +# And click "OK" to open the companion plugin + +explorer.navigate(position=(1e7,1e7,1e7)) # in nm + +explorer.overlay(url="nifti://https://data-proxy.ebrains.eu/api/v1/public/buckets/d-d69b70e2-3002-4eaf-9c61-9c56f019bbc8/probabilistic_maps_pmaps_175areas/Area-hOc1/Area-hOc1_pmap_l_N10_nlin2ICBM152asym2009c_4.2_public_258e8c1d846f92be76922b20287344ae.nii.gz") # TODO a bit buggy, and does not yet work + +from siibra.locations import PointSet +import siibra + +ptst = PointSet([ + [1,2,3], + [2,3,4] +], space=siibra.spaces['mni152']) + +explorer.annotate(points=ptst) # maximize perspective view for best effect + +``` + +## Automated tests + +use [playwrite](https://playwright.dev/python/) to automate user interactions. + +n.b. this functionality will depend largely on network speed, and geographical closeness to the siibra data centers. + +note: despite stdout, do not interact with + +### Installation + +```sh +pip install -r siibra/explorer/requirements.txt +playwright install chromium +``` + +### Example + +see + +```sh +$ python siibra/explorer/sample_playwrite.py + +start Point in MNI Colin 27 [0.0,0.0,0.0] +navigated Point in MNI Colin 27 [10.0,10.0,10.0] +space_specced Point in MNI 152 ICBM 2009c Nonlinear Asymmetric [9.323640000000012,11.565300000000008,9.66219000000001] +returned Point in MNI Colin 27 [9.995550000000009,9.996489999999994,10.009999999999991] + +``` \ No newline at end of file diff --git a/siibra/explorer/api/broadcast/allRegions/request/__init__.py b/siibra/explorer/api/broadcast/allRegions/request/__init__.py new file mode 100644 index 000000000..8251e8a6e --- /dev/null +++ b/siibra/explorer/api/broadcast/allRegions/request/__init__.py @@ -0,0 +1,118 @@ +# generated by datamodel-codegen: +# filename: sxplr.on.allRegions__fromSxplr__request.json +# timestamp: 2023-12-07T15:40:35+00:00 + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, List, Optional, Union + + +@dataclass +class VocabModel: + field_vocab: str + + +@dataclass +class QuantitativeOverlapItem: + value: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + typeOfUncertainty: Optional[Any] = None + uncertainty: Optional[List[float]] = None + unit: Optional[Any] = None + + +@dataclass +class QuantitativeOverlapItem1: + maxValue: float + minValue: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + maxValueUnit: Optional[Any] = None + minValueUnit: Optional[Any] = None + + +@dataclass +class ApiModelsOpenmindsSANDSV3AtlasParcellationEntityVersionCoordinates: + value: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + typeOfUncertainty: Optional[Any] = None + uncertainty: Optional[List[float]] = None + unit: Optional[Any] = None + + +@dataclass +class RelationAssessmentItem: + inRelationTo: Any + qualitativeOverlap: Any + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + criteria: Optional[Any] = None + + +@dataclass +class RelationAssessmentItem1: + inRelationTo: Any + quantitativeOverlap: Union[QuantitativeOverlapItem, QuantitativeOverlapItem1] + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + criteria: Optional[Any] = None + + +@dataclass +class BestViewPoint: + coordinateSpace: Any + coordinates: List[ + ApiModelsOpenmindsSANDSV3AtlasParcellationEntityVersionCoordinates + ] + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + + +@dataclass +class HasAnnotation: + criteriaQualityType: Any + internalIdentifier: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + bestViewPoint: Optional[BestViewPoint] = None + criteria: Optional[Any] = None + displayColor: Optional[str] = None + inspiredBy: Optional[List] = None + laterality: Optional[List] = None + visualizedIn: Optional[Any] = None + + +@dataclass +class PTRegion: + field_type: str + field_id: str + versionIdentifier: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + hasAnnotation: Optional[HasAnnotation] = None + hasParent: Optional[List] = None + lookupLabel: Optional[str] = None + name: Optional[str] = None + ontologyIdentifier: Optional[List[str]] = None + relationAssessment: Optional[ + Union[RelationAssessmentItem, RelationAssessmentItem1] + ] = None + versionInnovation: Optional[str] = None + + +@dataclass +class Model: + jsonrpc: str = "2.0" + method: str = "sxplr.on.allRegions" + params: Optional[List[PTRegion]] = None diff --git a/siibra/explorer/api/broadcast/atlasSelected/request/__init__.py b/siibra/explorer/api/broadcast/atlasSelected/request/__init__.py new file mode 100644 index 000000000..67e37a515 --- /dev/null +++ b/siibra/explorer/api/broadcast/atlasSelected/request/__init__.py @@ -0,0 +1,30 @@ +# generated by datamodel-codegen: +# filename: sxplr.on.atlasSelected__fromSxplr__request.json +# timestamp: 2023-12-07T15:40:36+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class SiibraAtIdModel: + field_id: str + + +@dataclass +class PTAtlas: + field_type: str + field_id: str + name: str + spaces: List[SiibraAtIdModel] + parcellations: List[SiibraAtIdModel] + species: str + + +@dataclass +class Model: + jsonrpc: str = "2.0" + method: str = "sxplr.on.atlasSelected" + params: Optional[PTAtlas] = None diff --git a/siibra/explorer/api/broadcast/parcellationSelected/request/__init__.py b/siibra/explorer/api/broadcast/parcellationSelected/request/__init__.py new file mode 100644 index 000000000..b736a92bf --- /dev/null +++ b/siibra/explorer/api/broadcast/parcellationSelected/request/__init__.py @@ -0,0 +1,139 @@ +# generated by datamodel-codegen: +# filename: sxplr.on.parcellationSelected__fromSxplr__request.json +# timestamp: 2023-12-07T15:40:36+00:00 + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + + +@dataclass +class EbrainsDsPerson: + field_type: str + field_id: str + schema_org_shortName: str + identifier: str + shortName: str + name: str + + +@dataclass +class EbrainsDsUrl: + field_type: str + url: str + + +@dataclass +class SiibraAtIdModel: + field_id: str + + +@dataclass +class VocabModel: + field_vocab: str + + +@dataclass +class EbrainsDatasetModel: + field_type: str + field_id: str + name: str + urls: List[EbrainsDsUrl] + contributors: List[EbrainsDsPerson] + custodians: List[EbrainsDsPerson] + description: Optional[str] = None + ebrains_page: Optional[str] = None + + +@dataclass +class SiibraParcellationVersionModel: + field_type: str + name: str + deprecated: Optional[bool] = None + prev: Optional[SiibraAtIdModel] = None + next: Optional[SiibraAtIdModel] = None + + +@dataclass +class Copyright: + holder: List + year: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + + +@dataclass +class HasTerminologyVersion: + hasEntityVersion: List + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + definedIn: Optional[List] = None + ontologyIdentifier: Optional[List[str]] = None + + +@dataclass +class OtherContribution: + contributionType: List + contributor: Any + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + + +@dataclass +class BrainAtlasVersionModel: + field_type: str + field_id: str + accessibility: Dict[str, Any] + coordinateSpace: Dict[str, Any] + fullDocumentation: Dict[str, Any] + hasTerminologyVersion: HasTerminologyVersion + license: Dict[str, Any] + releaseDate: str + shortName: str + versionIdentifier: str + versionInnovation: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + abbreviation: Optional[str] = None + atlasType: Optional[Dict[str, Any]] = None + author: Optional[List] = None + copyright: Optional[Copyright] = None + custodian: Optional[List] = None + description: Optional[str] = None + digitalIdentifier: Optional[Dict[str, Any]] = None + fullName: Optional[str] = None + funding: Optional[List] = None + homepage: Optional[Dict[str, Any]] = None + howToCite: Optional[str] = None + isAlternativeVersionOf: Optional[List] = None + isNewVersionOf: Optional[Dict[str, Any]] = None + keyword: Optional[List] = None + ontologyIdentifier: Optional[List[str]] = None + otherContribution: Optional[OtherContribution] = None + relatedPublication: Optional[List] = None + repository: Optional[Dict[str, Any]] = None + supportChannel: Optional[List[str]] = None + + +@dataclass +class PTParcellation: + field_type: str + field_id: str + name: str + datasets: List[EbrainsDatasetModel] + brainAtlasVersions: List[BrainAtlasVersionModel] + modality: Optional[str] = None + version: Optional[SiibraParcellationVersionModel] = None + shortname: Optional[str] = None + + +@dataclass +class Model: + jsonrpc: str = "2.0" + method: str = "sxplr.on.parcellationSelected" + params: Optional[PTParcellation] = None diff --git a/siibra/explorer/api/broadcast/regionsSelected/request/__init__.py b/siibra/explorer/api/broadcast/regionsSelected/request/__init__.py new file mode 100644 index 000000000..cf6d177e1 --- /dev/null +++ b/siibra/explorer/api/broadcast/regionsSelected/request/__init__.py @@ -0,0 +1,118 @@ +# generated by datamodel-codegen: +# filename: sxplr.on.regionsSelected__fromSxplr__request.json +# timestamp: 2023-12-07T15:40:36+00:00 + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, List, Optional, Union + + +@dataclass +class VocabModel: + field_vocab: str + + +@dataclass +class QuantitativeOverlapItem: + value: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + typeOfUncertainty: Optional[Any] = None + uncertainty: Optional[List[float]] = None + unit: Optional[Any] = None + + +@dataclass +class QuantitativeOverlapItem1: + maxValue: float + minValue: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + maxValueUnit: Optional[Any] = None + minValueUnit: Optional[Any] = None + + +@dataclass +class ApiModelsOpenmindsSANDSV3AtlasParcellationEntityVersionCoordinates: + value: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + typeOfUncertainty: Optional[Any] = None + uncertainty: Optional[List[float]] = None + unit: Optional[Any] = None + + +@dataclass +class RelationAssessmentItem: + inRelationTo: Any + qualitativeOverlap: Any + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + criteria: Optional[Any] = None + + +@dataclass +class RelationAssessmentItem1: + inRelationTo: Any + quantitativeOverlap: Union[QuantitativeOverlapItem, QuantitativeOverlapItem1] + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + criteria: Optional[Any] = None + + +@dataclass +class BestViewPoint: + coordinateSpace: Any + coordinates: List[ + ApiModelsOpenmindsSANDSV3AtlasParcellationEntityVersionCoordinates + ] + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + + +@dataclass +class HasAnnotation: + criteriaQualityType: Any + internalIdentifier: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + bestViewPoint: Optional[BestViewPoint] = None + criteria: Optional[Any] = None + displayColor: Optional[str] = None + inspiredBy: Optional[List] = None + laterality: Optional[List] = None + visualizedIn: Optional[Any] = None + + +@dataclass +class PTRegion: + field_type: str + field_id: str + versionIdentifier: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + hasAnnotation: Optional[HasAnnotation] = None + hasParent: Optional[List] = None + lookupLabel: Optional[str] = None + name: Optional[str] = None + ontologyIdentifier: Optional[List[str]] = None + relationAssessment: Optional[ + Union[RelationAssessmentItem, RelationAssessmentItem1] + ] = None + versionInnovation: Optional[str] = None + + +@dataclass +class Model: + jsonrpc: str = "2.0" + method: str = "sxplr.on.regionsSelected" + params: Optional[List[PTRegion]] = None diff --git a/siibra/explorer/api/broadcast/templateSelected/request/__init__.py b/siibra/explorer/api/broadcast/templateSelected/request/__init__.py new file mode 100644 index 000000000..31535ba55 --- /dev/null +++ b/siibra/explorer/api/broadcast/templateSelected/request/__init__.py @@ -0,0 +1,100 @@ +# generated by datamodel-codegen: +# filename: sxplr.on.templateSelected__fromSxplr__request.json +# timestamp: 2023-12-07T15:40:37+00:00 + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Union + + +@dataclass +class VocabModel: + field_vocab: str + + +@dataclass +class EbrainsDsPerson: + field_type: str + field_id: str + schema_org_shortName: str + identifier: str + shortName: str + name: str + + +@dataclass +class EbrainsDsUrl: + field_type: str + url: str + + +@dataclass +class SiibraAtIdModel: + field_id: str + + +@dataclass +class AxesOrigin: + value: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + typeOfUncertainty: Optional[Any] = None + uncertainty: Optional[List[float]] = None + unit: Optional[Any] = None + + +@dataclass +class EbrainsDatasetModel: + field_type: str + field_id: str + name: str + urls: List[EbrainsDsUrl] + contributors: List[EbrainsDsPerson] + custodians: List[EbrainsDsPerson] + description: Optional[str] = None + ebrains_page: Optional[str] = None + + +@dataclass +class VolumeModel: + field_type: str + name: str + formats: List[str] + providesMesh: bool + providesImage: bool + fragments: Dict[str, List[str]] + providedVolumes: Dict[str, Union[str, Dict[str, str]]] + space: SiibraAtIdModel + datasets: List[EbrainsDatasetModel] + variant: Optional[str] = None + + +@dataclass +class PTSpace: + field_type: str + field_id: str + anatomicalAxesOrientation: Dict[str, Any] + axesOrigin: List[AxesOrigin] + fullName: str + nativeUnit: Dict[str, Any] + releaseDate: str + shortName: str + versionIdentifier: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + defaultImage: Optional[List[VolumeModel]] = None + digitalIdentifier: Optional[Dict[str, Any]] = None + homepage: Optional[Dict[str, Any]] = None + howToCite: Optional[str] = None + ontologyIdentifier: Optional[List[str]] = None + datasets: Optional[List[EbrainsDatasetModel]] = None + + +@dataclass +class Model: + jsonrpc: str = "2.0" + method: str = "sxplr.on.templateSelected" + params: Optional[PTSpace] = None diff --git a/siibra/explorer/api/handshake/init/request/__init__.py b/siibra/explorer/api/handshake/init/request/__init__.py new file mode 100644 index 000000000..2d1e81b4f --- /dev/null +++ b/siibra/explorer/api/handshake/init/request/__init__.py @@ -0,0 +1,20 @@ +# generated by datamodel-codegen: +# filename: sxplr.init__fromSxplr__request.json +# timestamp: 2023-12-07T15:40:35+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Result: + name: Optional[str] = None + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: Optional[Result] = None diff --git a/siibra/explorer/api/request/addAnnotations/request/__init__.py b/siibra/explorer/api/request/addAnnotations/request/__init__.py new file mode 100644 index 000000000..960aefb94 --- /dev/null +++ b/siibra/explorer/api/request/addAnnotations/request/__init__.py @@ -0,0 +1,60 @@ +# generated by datamodel-codegen: +# filename: sxplr.addAnnotations__toSxplr__request.json +# timestamp: 2023-12-08T16:22:30+00:00 + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + + +@dataclass +class AtId: + field_id: Optional[str] = None + + +@dataclass +class VocabModel: + field_vocab: str + + +@dataclass +class ApiModelsOpenmindsSANDSV3MiscellaneousCoordinatePointCoordinates: + value: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + typeOfUncertainty: Optional[Any] = None + uncertainty: Optional[List[float]] = None + unit: Optional[Any] = None + + +@dataclass +class CoordinatePointModel: + field_type: str + field_id: str + coordinateSpace: Dict[str, Any] + coordinates: List[ApiModelsOpenmindsSANDSV3MiscellaneousCoordinatePointCoordinates] + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + + +@dataclass +class SxplrCoordinatePointExtension(AtId): + name: Optional[str] = None + color: Optional[str] = None + openminds: Optional[CoordinatePointModel] = None + + +@dataclass +class Params: + annotations: Optional[List[SxplrCoordinatePointExtension]] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.addAnnotations" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/addAnnotations/response/__init__.py b/siibra/explorer/api/request/addAnnotations/response/__init__.py new file mode 100644 index 000000000..0c110dcf5 --- /dev/null +++ b/siibra/explorer/api/request/addAnnotations/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.addAnnotations__toSxplr__response.json +# timestamp: 2023-12-07T15:40:42+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/cancelRequest/request/__init__.py b/siibra/explorer/api/request/cancelRequest/request/__init__.py new file mode 100644 index 000000000..f10a0d139 --- /dev/null +++ b/siibra/explorer/api/request/cancelRequest/request/__init__.py @@ -0,0 +1,21 @@ +# generated by datamodel-codegen: +# filename: sxplr.cancelRequest__toSxplr__request.json +# timestamp: 2023-12-07T15:40:41+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Params: + id: Optional[str] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.cancelRequest" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/cancelRequest/response/__init__.py b/siibra/explorer/api/request/cancelRequest/response/__init__.py new file mode 100644 index 000000000..aeb12287f --- /dev/null +++ b/siibra/explorer/api/request/cancelRequest/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.cancelRequest__toSxplr__response.json +# timestamp: 2023-12-07T15:40:44+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/exit/request/__init__.py b/siibra/explorer/api/request/exit/request/__init__.py new file mode 100644 index 000000000..c74687c71 --- /dev/null +++ b/siibra/explorer/api/request/exit/request/__init__.py @@ -0,0 +1,26 @@ +# generated by datamodel-codegen: +# filename: sxplr.exit__toSxplr__request.json +# timestamp: 2023-12-07T15:40:44+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class JRPCRequest: + id: Optional[str] = None + + +@dataclass +class Params: + requests: Optional[List[JRPCRequest]] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.exit" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/exit/response/__init__.py b/siibra/explorer/api/request/exit/response/__init__.py new file mode 100644 index 000000000..af9bfacbf --- /dev/null +++ b/siibra/explorer/api/request/exit/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.exit__toSxplr__response.json +# timestamp: 2023-12-07T15:40:43+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/getAllAtlases/request/__init__.py b/siibra/explorer/api/request/getAllAtlases/request/__init__.py new file mode 100644 index 000000000..f89dad89f --- /dev/null +++ b/siibra/explorer/api/request/getAllAtlases/request/__init__.py @@ -0,0 +1,16 @@ +# generated by datamodel-codegen: +# filename: sxplr.getAllAtlases__toSxplr__request.json +# timestamp: 2023-12-07T15:40:41+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.getAllAtlases" + params: Any = None diff --git a/siibra/explorer/api/request/getAllAtlases/response/__init__.py b/siibra/explorer/api/request/getAllAtlases/response/__init__.py new file mode 100644 index 000000000..241fa3838 --- /dev/null +++ b/siibra/explorer/api/request/getAllAtlases/response/__init__.py @@ -0,0 +1,30 @@ +# generated by datamodel-codegen: +# filename: sxplr.getAllAtlases__toSxplr__response.json +# timestamp: 2023-12-07T15:40:42+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class SiibraAtIdModel: + field_id: str + + +@dataclass +class PTAtlas: + field_type: str + field_id: str + name: str + spaces: List[SiibraAtIdModel] + parcellations: List[SiibraAtIdModel] + species: str + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: Optional[List[PTAtlas]] = None diff --git a/siibra/explorer/api/request/getSupportedParcellations/request/__init__.py b/siibra/explorer/api/request/getSupportedParcellations/request/__init__.py new file mode 100644 index 000000000..b7e6b6d0b --- /dev/null +++ b/siibra/explorer/api/request/getSupportedParcellations/request/__init__.py @@ -0,0 +1,16 @@ +# generated by datamodel-codegen: +# filename: sxplr.getSupportedParcellations__toSxplr__request.json +# timestamp: 2023-12-07T15:40:40+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.getSupportedParcellations" + params: Any = None diff --git a/siibra/explorer/api/request/getSupportedParcellations/response/__init__.py b/siibra/explorer/api/request/getSupportedParcellations/response/__init__.py new file mode 100644 index 000000000..ff578b334 --- /dev/null +++ b/siibra/explorer/api/request/getSupportedParcellations/response/__init__.py @@ -0,0 +1,139 @@ +# generated by datamodel-codegen: +# filename: sxplr.getSupportedParcellations__toSxplr__response.json +# timestamp: 2023-12-07T15:40:41+00:00 + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + + +@dataclass +class EbrainsDsPerson: + field_type: str + field_id: str + schema_org_shortName: str + identifier: str + shortName: str + name: str + + +@dataclass +class EbrainsDsUrl: + field_type: str + url: str + + +@dataclass +class SiibraAtIdModel: + field_id: str + + +@dataclass +class VocabModel: + field_vocab: str + + +@dataclass +class EbrainsDatasetModel: + field_type: str + field_id: str + name: str + urls: List[EbrainsDsUrl] + contributors: List[EbrainsDsPerson] + custodians: List[EbrainsDsPerson] + description: Optional[str] = None + ebrains_page: Optional[str] = None + + +@dataclass +class SiibraParcellationVersionModel: + field_type: str + name: str + deprecated: Optional[bool] = None + prev: Optional[SiibraAtIdModel] = None + next: Optional[SiibraAtIdModel] = None + + +@dataclass +class Copyright: + holder: List + year: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + + +@dataclass +class HasTerminologyVersion: + hasEntityVersion: List + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + definedIn: Optional[List] = None + ontologyIdentifier: Optional[List[str]] = None + + +@dataclass +class OtherContribution: + contributionType: List + contributor: Any + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + + +@dataclass +class BrainAtlasVersionModel: + field_type: str + field_id: str + accessibility: Dict[str, Any] + coordinateSpace: Dict[str, Any] + fullDocumentation: Dict[str, Any] + hasTerminologyVersion: HasTerminologyVersion + license: Dict[str, Any] + releaseDate: str + shortName: str + versionIdentifier: str + versionInnovation: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + abbreviation: Optional[str] = None + atlasType: Optional[Dict[str, Any]] = None + author: Optional[List] = None + copyright: Optional[Copyright] = None + custodian: Optional[List] = None + description: Optional[str] = None + digitalIdentifier: Optional[Dict[str, Any]] = None + fullName: Optional[str] = None + funding: Optional[List] = None + homepage: Optional[Dict[str, Any]] = None + howToCite: Optional[str] = None + isAlternativeVersionOf: Optional[List] = None + isNewVersionOf: Optional[Dict[str, Any]] = None + keyword: Optional[List] = None + ontologyIdentifier: Optional[List[str]] = None + otherContribution: Optional[OtherContribution] = None + relatedPublication: Optional[List] = None + repository: Optional[Dict[str, Any]] = None + supportChannel: Optional[List[str]] = None + + +@dataclass +class PTParcellation: + field_type: str + field_id: str + name: str + datasets: List[EbrainsDatasetModel] + brainAtlasVersions: List[BrainAtlasVersionModel] + modality: Optional[str] = None + version: Optional[SiibraParcellationVersionModel] = None + shortname: Optional[str] = None + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: Optional[List[PTParcellation]] = None diff --git a/siibra/explorer/api/request/getSupportedTemplates/request/__init__.py b/siibra/explorer/api/request/getSupportedTemplates/request/__init__.py new file mode 100644 index 000000000..eef89ab92 --- /dev/null +++ b/siibra/explorer/api/request/getSupportedTemplates/request/__init__.py @@ -0,0 +1,16 @@ +# generated by datamodel-codegen: +# filename: sxplr.getSupportedTemplates__toSxplr__request.json +# timestamp: 2023-12-07T15:40:43+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.getSupportedTemplates" + params: Any = None diff --git a/siibra/explorer/api/request/getSupportedTemplates/response/__init__.py b/siibra/explorer/api/request/getSupportedTemplates/response/__init__.py new file mode 100644 index 000000000..27d8a045b --- /dev/null +++ b/siibra/explorer/api/request/getSupportedTemplates/response/__init__.py @@ -0,0 +1,100 @@ +# generated by datamodel-codegen: +# filename: sxplr.getSupportedTemplates__toSxplr__response.json +# timestamp: 2023-12-07T15:40:37+00:00 + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Union + + +@dataclass +class VocabModel: + field_vocab: str + + +@dataclass +class EbrainsDsPerson: + field_type: str + field_id: str + schema_org_shortName: str + identifier: str + shortName: str + name: str + + +@dataclass +class EbrainsDsUrl: + field_type: str + url: str + + +@dataclass +class SiibraAtIdModel: + field_id: str + + +@dataclass +class AxesOrigin: + value: float + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + typeOfUncertainty: Optional[Any] = None + uncertainty: Optional[List[float]] = None + unit: Optional[Any] = None + + +@dataclass +class EbrainsDatasetModel: + field_type: str + field_id: str + name: str + urls: List[EbrainsDsUrl] + contributors: List[EbrainsDsPerson] + custodians: List[EbrainsDsPerson] + description: Optional[str] = None + ebrains_page: Optional[str] = None + + +@dataclass +class VolumeModel: + field_type: str + name: str + formats: List[str] + providesMesh: bool + providesImage: bool + fragments: Dict[str, List[str]] + providedVolumes: Dict[str, Union[str, Dict[str, str]]] + space: SiibraAtIdModel + datasets: List[EbrainsDatasetModel] + variant: Optional[str] = None + + +@dataclass +class PTSpace: + field_type: str + field_id: str + anatomicalAxesOrientation: Dict[str, Any] + axesOrigin: List[AxesOrigin] + fullName: str + nativeUnit: Dict[str, Any] + releaseDate: str + shortName: str + versionIdentifier: str + field_context: Optional[VocabModel] = field( + default_factory=lambda: {"@vocab": "https://openminds.ebrains.eu/vocab/"} + ) + defaultImage: Optional[List[VolumeModel]] = None + digitalIdentifier: Optional[Dict[str, Any]] = None + homepage: Optional[Dict[str, Any]] = None + howToCite: Optional[str] = None + ontologyIdentifier: Optional[List[str]] = None + datasets: Optional[List[EbrainsDatasetModel]] = None + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: Optional[List[PTSpace]] = None diff --git a/siibra/explorer/api/request/getUserToSelectARoi/request/__init__.py b/siibra/explorer/api/request/getUserToSelectARoi/request/__init__.py new file mode 100644 index 000000000..b1fbd39f9 --- /dev/null +++ b/siibra/explorer/api/request/getUserToSelectARoi/request/__init__.py @@ -0,0 +1,22 @@ +# generated by datamodel-codegen: +# filename: sxplr.getUserToSelectARoi__toSxplr__request.json +# timestamp: 2023-12-07T15:40:40+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass +class Params: + type: Optional[Any] = None + message: Optional[str] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.getUserToSelectARoi" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/getUserToSelectARoi/response/__init__.py b/siibra/explorer/api/request/getUserToSelectARoi/response/__init__.py new file mode 100644 index 000000000..f2964a83a --- /dev/null +++ b/siibra/explorer/api/request/getUserToSelectARoi/response/__init__.py @@ -0,0 +1,29 @@ +# generated by datamodel-codegen: +# filename: sxplr.getUserToSelectARoi__toSxplr__response.json +# timestamp: 2023-12-07T15:40:45+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, List, Optional, Union + + +@dataclass +class Space: + id: Optional[str] = None + + +@dataclass +class Point: + loc: Optional[List[float]] = None + space: Optional[Space] = None + + +PTRegion = Any + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: Optional[Union[PTRegion, Point]] = None diff --git a/siibra/explorer/api/request/loadLayers/request/__init__.py b/siibra/explorer/api/request/loadLayers/request/__init__.py new file mode 100644 index 000000000..2d0ba4156 --- /dev/null +++ b/siibra/explorer/api/request/loadLayers/request/__init__.py @@ -0,0 +1,35 @@ +# generated by datamodel-codegen: +# filename: sxplr.loadLayers__toSxplr__request.json +# timestamp: 2023-12-07T15:40:45+00:00 + +from __future__ import annotations + +from uuid import uuid4 +from dataclasses import dataclass, field +from typing import List, Optional + +Len4num = List[float] + + +@dataclass +class AddableLayer: + # TODO fix siibra-explorer API doc, requiring clType and id + id: str = field(default_factory=lambda: str(uuid4())) + type: str = "image" + clType: str = "customlayer/nglayer" + source: Optional[str] = None + shader: Optional[str] = None + transform: Optional[List[Len4num]] = None + + +@dataclass +class Params: + layers: Optional[List[AddableLayer]] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.loadLayers" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/loadLayers/response/__init__.py b/siibra/explorer/api/request/loadLayers/response/__init__.py new file mode 100644 index 000000000..414414a78 --- /dev/null +++ b/siibra/explorer/api/request/loadLayers/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.loadLayers__toSxplr__response.json +# timestamp: 2023-12-07T15:40:39+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/navigateTo/request/MainState___state.py b/siibra/explorer/api/request/navigateTo/request/MainState___state.py new file mode 100644 index 000000000..d9e86213d --- /dev/null +++ b/siibra/explorer/api/request/navigateTo/request/MainState___state.py @@ -0,0 +1,23 @@ +# generated by datamodel-codegen: +# filename: sxplr.navigateTo__toSxplr__request.json +# timestamp: 2023-12-07T15:40:46+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class AtlasSelectionNavigation: + position: Optional[Len3num] = None + orientation: Optional[Len4num] = None + zoom: Optional[float] = None + perspectiveOrientation: Optional[Len4num] = None + perspectiveZoom: Optional[float] = None + + +Len3num = List[float] + + +Len4num = List[float] diff --git a/siibra/explorer/api/request/navigateTo/request/__init__.py b/siibra/explorer/api/request/navigateTo/request/__init__.py new file mode 100644 index 000000000..0181c2fb7 --- /dev/null +++ b/siibra/explorer/api/request/navigateTo/request/__init__.py @@ -0,0 +1,23 @@ +# generated by datamodel-codegen: +# filename: sxplr.navigateTo__toSxplr__request.json +# timestamp: 2023-12-07T15:40:46+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + +from .MainState___state import AtlasSelectionNavigation + + +@dataclass +class Params(AtlasSelectionNavigation): + animate: Optional[bool] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.navigateTo" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/navigateTo/response/__init__.py b/siibra/explorer/api/request/navigateTo/response/__init__.py new file mode 100644 index 000000000..5a467e908 --- /dev/null +++ b/siibra/explorer/api/request/navigateTo/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.navigateTo__toSxplr__response.json +# timestamp: 2023-12-07T15:40:45+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/removeLayers/request/__init__.py b/siibra/explorer/api/request/removeLayers/request/__init__.py new file mode 100644 index 000000000..5f8b56c97 --- /dev/null +++ b/siibra/explorer/api/request/removeLayers/request/__init__.py @@ -0,0 +1,26 @@ +# generated by datamodel-codegen: +# filename: sxplr.removeLayers__toSxplr__request.json +# timestamp: 2023-12-07T15:40:38+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class Layer: + id: Optional[str] = None + + +@dataclass +class Params: + layers: Optional[List[Layer]] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.removeLayers" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/removeLayers/response/__init__.py b/siibra/explorer/api/request/removeLayers/response/__init__.py new file mode 100644 index 000000000..b0e24560a --- /dev/null +++ b/siibra/explorer/api/request/removeLayers/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.removeLayers__toSxplr__response.json +# timestamp: 2023-12-07T15:40:39+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/rmAnnotations/request/__init__.py b/siibra/explorer/api/request/rmAnnotations/request/__init__.py new file mode 100644 index 000000000..23ab4c1ae --- /dev/null +++ b/siibra/explorer/api/request/rmAnnotations/request/__init__.py @@ -0,0 +1,26 @@ +# generated by datamodel-codegen: +# filename: sxplr.rmAnnotations__toSxplr__request.json +# timestamp: 2023-12-07T15:40:39+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class AtId: + field_id: Optional[str] = None + + +@dataclass +class Params: + annotations: Optional[List[AtId]] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.rmAnnotations" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/rmAnnotations/response/__init__.py b/siibra/explorer/api/request/rmAnnotations/response/__init__.py new file mode 100644 index 000000000..15504cac2 --- /dev/null +++ b/siibra/explorer/api/request/rmAnnotations/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.rmAnnotations__toSxplr__response.json +# timestamp: 2023-12-07T15:40:38+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/selectAtlas/request/__init__.py b/siibra/explorer/api/request/selectAtlas/request/__init__.py new file mode 100644 index 000000000..2fd88a7ed --- /dev/null +++ b/siibra/explorer/api/request/selectAtlas/request/__init__.py @@ -0,0 +1,21 @@ +# generated by datamodel-codegen: +# filename: sxplr.selectAtlas__toSxplr__request.json +# timestamp: 2023-12-07T15:40:42+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class AtId: + field_id: Optional[str] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.selectAtlas" + params: Optional[AtId] = None diff --git a/siibra/explorer/api/request/selectAtlas/response/__init__.py b/siibra/explorer/api/request/selectAtlas/response/__init__.py new file mode 100644 index 000000000..0126b3b90 --- /dev/null +++ b/siibra/explorer/api/request/selectAtlas/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.selectAtlas__toSxplr__response.json +# timestamp: 2023-12-07T15:40:44+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/selectParcellation/request/__init__.py b/siibra/explorer/api/request/selectParcellation/request/__init__.py new file mode 100644 index 000000000..bc3a1fd6a --- /dev/null +++ b/siibra/explorer/api/request/selectParcellation/request/__init__.py @@ -0,0 +1,21 @@ +# generated by datamodel-codegen: +# filename: sxplr.selectParcellation__toSxplr__request.json +# timestamp: 2023-12-07T15:40:41+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class AtId: + field_id: Optional[str] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.selectParcellation" + params: Optional[AtId] = None diff --git a/siibra/explorer/api/request/selectParcellation/response/__init__.py b/siibra/explorer/api/request/selectParcellation/response/__init__.py new file mode 100644 index 000000000..f2e8687b5 --- /dev/null +++ b/siibra/explorer/api/request/selectParcellation/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.selectParcellation__toSxplr__response.json +# timestamp: 2023-12-07T15:40:45+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/selectTemplate/request/__init__.py b/siibra/explorer/api/request/selectTemplate/request/__init__.py new file mode 100644 index 000000000..82a93439d --- /dev/null +++ b/siibra/explorer/api/request/selectTemplate/request/__init__.py @@ -0,0 +1,21 @@ +# generated by datamodel-codegen: +# filename: sxplr.selectTemplate__toSxplr__request.json +# timestamp: 2023-12-07T15:40:39+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class AtId: + field_id: Optional[str] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.selectTemplate" + params: Optional[AtId] = None diff --git a/siibra/explorer/api/request/selectTemplate/response/__init__.py b/siibra/explorer/api/request/selectTemplate/response/__init__.py new file mode 100644 index 000000000..9d08359d8 --- /dev/null +++ b/siibra/explorer/api/request/selectTemplate/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.selectTemplate__toSxplr__response.json +# timestamp: 2023-12-07T15:40:37+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/api/request/updateLayers/request/__init__.py b/siibra/explorer/api/request/updateLayers/request/__init__.py new file mode 100644 index 000000000..ef0c5b277 --- /dev/null +++ b/siibra/explorer/api/request/updateLayers/request/__init__.py @@ -0,0 +1,30 @@ +# generated by datamodel-codegen: +# filename: sxplr.updateLayers__toSxplr__request.json +# timestamp: 2023-12-07T15:40:40+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + +Len4num = List[float] + + +@dataclass +class AddableLayer: + source: Optional[str] = None + shader: Optional[str] = None + transform: Optional[List[Len4num]] = None + + +@dataclass +class Params: + layers: Optional[List[AddableLayer]] = None + + +@dataclass +class Model: + id: Optional[str] = None + jsonrpc: str = "2.0" + method: str = "sxplr.updateLayers" + params: Optional[Params] = None diff --git a/siibra/explorer/api/request/updateLayers/response/__init__.py b/siibra/explorer/api/request/updateLayers/response/__init__.py new file mode 100644 index 000000000..bf4bbcfe6 --- /dev/null +++ b/siibra/explorer/api/request/updateLayers/response/__init__.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: sxplr.updateLayers__toSxplr__response.json +# timestamp: 2023-12-07T15:40:43+00:00 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class Model: + jsonrpc: str = "2.0" + id: Optional[str] = None + result: str = "OK" diff --git a/siibra/explorer/generate_dataclass.sh b/siibra/explorer/generate_dataclass.sh new file mode 100755 index 000000000..10177b5d7 --- /dev/null +++ b/siibra/explorer/generate_dataclass.sh @@ -0,0 +1,66 @@ +#! /bin/bash + +# Populate datamodels based on jsonschema generated by siibra-explorer +# require +# pip install datamodel-code-generator +# which is not a part of requirements +# Usage: +# ./generate_dataclass.sh \ +# $PATH_SIIBRA_EXPLORER_ROOT_DIR \ +# $PATH_TO_SIIBRA_PYTHON_EXPLORERAPI_DIR +# +# $PATH_TO_SIIBRA_PYTHON_EXPLORERAPI_DIR is usually ~/siibra/explorer/api + +if [[ -z "$1" ]] +then + echo "Path to siibra-explorer is required to populate" + exit 1 +fi + +if [[ -z "$2" ]] +then + echo "Output path is required" + exit 1 +fi + +for f in $(find $1/src/api -type f -name '*.json') +do + echo "Processing $f" + dst_file=${f#$1/src/api/} + dst_file=${dst_file%.json} + dst_name=$(basename $dst_file) + dst_dir=$(dirname $dst_file) + + subdir="other" + if [[ $dst_name == *"request"* ]] + then + subdir="request" + fi + if [[ $dst_name == *"response"* ]] + then + subdir="response" + fi + + dst_name=${dst_name%__request} + dst_name=${dst_name%__response} + dst_name=${dst_name%__fromSxplr} + dst_name=${dst_name%__toSxplr} + dst_name=${dst_name#sxplr.} + dst_name=${dst_name#on.} + + dst=$2/$dst_dir/$dst_name/$subdir + + mkdir -p $dst + datamodel-codegen --input $f \ + --input-file-type jsonschema \ + --output-model-type dataclasses.dataclass \ + --output $dst/__init__.py + + echo "=============================" +done + +datamodel-codegen --input $1/src/api/request/sxplr.navigateTo__toSxplr__request.json \ + --input-file-type jsonschema \ + --output-model-type dataclasses.dataclass \ + --output $2/request/navigateTo/request + diff --git a/siibra/explorer/plugin.py b/siibra/explorer/plugin.py new file mode 100644 index 000000000..2860800e7 --- /dev/null +++ b/siibra/explorer/plugin.py @@ -0,0 +1,291 @@ +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +import json +from dataclasses import is_dataclass, asdict as _asdict +from threading import Thread +from typing import Any, TYPE_CHECKING, Union +import sys +from uuid import uuid4 + +if TYPE_CHECKING: + from siibra.locations import Point, PointSet + +IDENTITY = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], +] + +key_mapping = {"field_id": "@id"} + + +def custom_dict_factory(list_of_tuples): + updated = [(key_mapping.get(n, n), v) for n, v in list_of_tuples] + return dict(updated) + + +def asdict(val_in): + if is_dataclass(val_in): + val_in = _asdict(val_in, dict_factory=custom_dict_factory) + if isinstance(val_in, (str, int, float)): + return val_in + if isinstance(val_in, (list, tuple)): + return [asdict(v) for v in val_in if v is not None] + if isinstance(val_in, dict): + return { + key: asdict(value) for key, value in val_in.items() if value is not None + } + raise Exception(f"Cannot deal with type {type(val_in)}") + + +TEMPLATE: str = None + +LOG_FLAG = False + + +class ReqHndl(BaseHTTPRequestHandler): + sxplr_requests = [] + + @property + def tmpl(self): + with open(Path(__file__).parent / "template.html", "r") as fp: + return fp.read() + + def queue_sxplr_request(self, req) -> None: + self.sxplr_requests.append(req) + + def log_message(self, format: str, *args: Any) -> None: + if LOG_FLAG: + return super().log_message(format, *args) + + def do_GET(self): + if self.path == "/template.html": + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + + self.wfile.write(bytes(self.tmpl, "utf-8")) + return + if self.path == "/ping": + self.send_response(200) + + self.send_header("Content-Type", "application/json") + self.end_headers() + ReqHndl.sxplr_requests + result = json.dumps([asdict(req) for req in ReqHndl.sxplr_requests]) + ReqHndl.sxplr_requests = [] + self.wfile.write(bytes(result, "utf-8")) + return + self.send_error(404, f"{self.path} Not Found") + + def do_POST(self): + if self.path.endswith("data"): + self.send_response(200) + self.end_headers() + self.wfile.write(bytes("OK", "utf-8")) + return + self.send_error(500, f"{self.path} is not supported") + + +class ThreadedController(Thread): + + # exits on main thread exits + daemon = True + + def __init__(self, port=7099, *, debug=False): + super().__init__() + global LOG_FLAG + if debug: + LOG_FLAG = True + self.port = port + self.server = ThreadingHTTPServer(("localhost", self.port), ReqHndl) + + def run(self) -> None: + print("!!!NOT FOR PRODUCTION USE!!!") + print(f"Listening on {self.port}") + self.server.serve_forever() + + def stop(self): + self.server.shutdown() + + +class Explorer: + """Start a controller for explorer. + If used as a context manager, will start the server automatically. + If used normally, user should call start() to start the server, and call stop() when done. + + Args: + root_url: str The viewer URL that will be launched + port: int Port plugin is running + """ + + def __init__( + self, root_url="https://atlases.ebrains.eu/viewer/", port: int = 7099 + ) -> None: + self.controller = ThreadedController(port) + self.root_url = root_url + + def __enter__(self): + self.start() + return self + + def __exit__(self): + self.stop() + + def start( + self, + *, + atlas_spec: str = "human", + space_spec: str = "mni 152", + parcellation_spec: str = "julich 3", + ): + from . import encode_url + import siibra + + atlas = siibra.atlases[atlas_spec] + space = siibra.spaces[space_spec] + parc = siibra.parcellations[parcellation_spec] + self.goto_url = encode_url( + atlas, + space, + parc, + root_url=self.root_url, + query_params={"pl": '["http://localhost:7099/template.html"]'}, + ) + print(f"Go to {self.goto_url}", file=sys.stderr) + self.controller.start() + return self.goto_url + + def stop(self): + self.controller.stop() + + def _check_alive(self): + if not self.controller.is_alive(): + raise Exception("controller is not yet live") + + def navigate( + self, + *, + position=None, + orientation=None, + zoom=None, + perspective_orientation=None, + perspective_zoom=None, + ): + self._check_alive() + + from .api.request.navigateTo.request import Model, Params + + ReqHndl.sxplr_requests.append( + Model( + id=str(uuid4()), + params=Params( + position=position, + orientation=orientation, + perspectiveOrientation=perspective_orientation, + perspectiveZoom=perspective_zoom, + zoom=zoom, + animate=True, + ), + ) + ) + + def overlay(self, *, url: str, type="image", transform=IDENTITY): + self._check_alive() + from .api.request.loadLayers.request import Model, Params, AddableLayer + + ReqHndl.sxplr_requests.append( + Model( + id=str(uuid4()), + params=Params(layers=[AddableLayer(source=url, type=type, transform=transform)]), + ) + ) + + @staticmethod + def _point_to_coords(point: "Point", name="Untitled", color: str = "#ffffff"): + from .api.request.addAnnotations.request import ( + SxplrCoordinatePointExtension, + CoordinatePointModel, + ApiModelsOpenmindsSANDSV3MiscellaneousCoordinatePointCoordinates as Coord, + ) + + return SxplrCoordinatePointExtension( + color=color, + name=name, + openminds=CoordinatePointModel( + field_type="", + field_id="", + coordinateSpace={"@id": point.space.id}, + coordinates=[Coord(value=v) for v in point], + ), + ) + + @staticmethod + def _pointset_to_coords( + pointset: "PointSet", name="Untitled", color: str = "#ff0000" + ): + return [Explorer._point_to_coords(point, name, color) for point in pointset] + + def annotate(self, *, points: Union["Point", "PointSet"]): + self._check_alive() + from .api.request.addAnnotations.request import Model, Params + from siibra.locations import Point, PointSet + + append_points = [] + + if isinstance(points, PointSet): + append_points.extend(Explorer._pointset_to_coords(points)) + if isinstance(points, Point): + append_points.append(Explorer._point_to_coords(points)) + ReqHndl.sxplr_requests.append( + Model(id=str(uuid4()), params=Params(annotations=append_points)) + ) + + def select( + self, + *, + atlas_spec: str = None, + template_spec: str = None, + parcellation_spec: str = None, + ): + assert ( + bool(atlas_spec) + bool(template_spec) + bool(parcellation_spec) == 1 + ), f"""Expected one and only one of {atlas_spec}, {template_spec}, and {parcellation_spec} to be set""" + + import siibra + + if bool(atlas_spec): + from .api.request.selectAtlas.request import Model, AtId + + atlas = siibra.atlases[atlas_spec] + if atlas is None: + raise Exception(f"{atlas_spec} did not resolve to any atlas") + ReqHndl.sxplr_requests.append( + Model(id=str(uuid4()), params=AtId(field_id=atlas.id)) + ) + return + + if bool(template_spec): + from .api.request.selectTemplate.request import Model, AtId + + space = siibra.spaces[template_spec] + if space is None: + raise Exception(f"{template_spec} did not resolve to any space") + ReqHndl.sxplr_requests.append( + Model(id=str(uuid4()), params=AtId(field_id=space.id)) + ) + return + + if bool(parcellation_spec): + from .api.request.selectParcellation.request import Model, AtId + + parcellation = siibra.parcellations[parcellation_spec] + if parcellation is None: + raise Exception( + f"{parcellation_spec} did not resolve to any parcellation" + ) + ReqHndl.sxplr_requests.append( + Model(id=str(uuid4()), params=AtId(field_id=parcellation.id)) + ) + return diff --git a/siibra/explorer/requirements.txt b/siibra/explorer/requirements.txt new file mode 100644 index 000000000..62b829ea1 --- /dev/null +++ b/siibra/explorer/requirements.txt @@ -0,0 +1 @@ +pytest-playwright \ No newline at end of file diff --git a/siibra/explorer/sample_playwrite.py b/siibra/explorer/sample_playwrite.py new file mode 100644 index 000000000..22d501805 --- /dev/null +++ b/siibra/explorer/sample_playwrite.py @@ -0,0 +1,115 @@ +import sys +from pathlib import Path +from typing import Callable + +from siibra.locations import BoundingBox +from siibra.explorer.plugin import Explorer +from siibra.explorer.url import decode_url, DecodeNavigationException, DecodedUrl +from playwright.sync_api import sync_playwright + +dismiss_preamble = [ + ("Welcome to ebrains siibra explorer", "Dismiss"), + ("Privacy Policy", "Ok"), +] + +wanted_dialog = [ + ("http://localhost:7099/template.html", "OK"), +] + + +def access_region(space_spec: str, parc_spec: str, region_spec: str, *, to_space_spec: str, record_video_dir: str = None): + import siibra + region = siibra.get_region(parc_spec, region_spec) + bbox = region.get_bounding_box(space_spec) + return assess_roundtrip(bbox, to_space_spec, record_video_dir) + + +VIEWPORT_SIZE_WIDTH = 1600 +VIEWPORT_SIZE_HEIGHT = 800 + +def assess_roundtrip(bbox: BoundingBox, space_spec: str, record_video_dir: str = None): + + def try_decode_url(predicate: Callable[[DecodedUrl], bool]=lambda *args: True): + while True: + try: + deocded_url = decode_url(page.url, VIEWPORT_SIZE_WIDTH) + assert predicate(deocded_url) + return deocded_url + + except (DecodeNavigationException, AssertionError): + page.wait_for_timeout(1000) + + explorer = Explorer(root_url="https://atlases.ebrains.eu/viewer-staging/") + url = explorer.start(space_spec=bbox.space) + with sync_playwright() as playwrite: + chromium = playwrite.chromium + browser = chromium.launch() + context = browser.new_context(record_video_dir=record_video_dir) if record_video_dir else browser.new_context() + page = context.new_page() + page.set_viewport_size({ + 'width': VIEWPORT_SIZE_WIDTH, + 'height': VIEWPORT_SIZE_HEIGHT, + }) + + page.goto(url) + page.wait_for_timeout(1000) + + for text, click in dismiss_preamble: + dialog = page.get_by_role("dialog").filter(has_text=text) + btn = dialog.locator("//button").filter(has_text=click) + btn.dispatch_event("click") + + for text, click in wanted_dialog: + dialog = page.get_by_role("dialog").filter(has_text=text) + btn = dialog.locator("//button").filter(has_text=click) + btn.click() + + bbox_size = bbox.maxpoint - bbox.minpoint + max_dimen = max([p for p in bbox_size]) + max_viewport_dimen = max(VIEWPORT_SIZE_WIDTH, VIEWPORT_SIZE_HEIGHT) / 2 # viewport is quatered, so div by 2 + + # convert to nm + zoom = max_dimen * 1e6 / max_viewport_dimen + + start_decoded = try_decode_url() + explorer.navigate(position=[coord * 1e6 for coord in bbox.center], zoom=zoom) # in nm + page.wait_for_timeout(10000) + navigated_decoded = try_decode_url(lambda b: b.bounding_box.center != start_decoded.bounding_box.center) + + explorer.select(template_spec=space_spec) + page.wait_for_timeout(10000) + space_seld_decoded = try_decode_url(lambda b: b.bounding_box.space.id != navigated_decoded.bounding_box.space.id) + + explorer.select(template_spec=bbox.space) + page.wait_for_timeout(10000) + + returned_decoded = try_decode_url(lambda b: b.bounding_box.space.id != space_seld_decoded.bounding_box.space.id) + + print_result = zip( + ("start", "navigated", "space_specced", "returned"), + ( + start_decoded.bounding_box, + navigated_decoded.bounding_box, + space_seld_decoded.bounding_box, + returned_decoded.bounding_box), + ) + + for name, bbox in print_result: + print(name, bbox) + context.close() + + +if __name__ == "__main__": + video_flag = "--video" in sys.argv[1:] + if video_flag: + Path("./video").mkdir(exist_ok=True, parents=True) + access_region("mni 152", + "julich brain 3.1", + "hoc1 left", + to_space_spec="colin 27", + record_video_dir="./video") + else: + access_region("mni 152", + "julich brain 3.1", + "hoc1 left", + to_space_spec="colin 27") diff --git a/siibra/explorer/template.html b/siibra/explorer/template.html new file mode 100644 index 000000000..57e928d70 --- /dev/null +++ b/siibra/explorer/template.html @@ -0,0 +1,86 @@ + + + + + + Siibra Explorer Plugin + + + This is a companion plugin for siibra-python. + You can safely minimize (NOT close!) this plugin, and control it via python. + + + \ No newline at end of file diff --git a/siibra/explorer/url.py b/siibra/explorer/url.py index 4486e1087..9fa2a288d 100644 --- a/siibra/explorer/url.py +++ b/siibra/explorer/url.py @@ -11,122 +11,162 @@ if TYPE_CHECKING: from siibra.core.atlas import Atlas from siibra.core.space import Space - from siibra.locations import BoundingBox, Point + from siibra.locations import BoundingBox from siibra.core.parcellation import Parcellation from siibra.core.region import Region from siibra.features.feature import Feature -class DecodeNavigationException(Exception): pass -min_int32=-2_147_483_648 -max_int32=2_147_483_647 +class DecodeNavigationException(Exception): + pass -default_root_url='https://atlases.ebrains.eu/viewer/' +min_int32 = -2_147_483_648 +max_int32 = 2_147_483_647 + + +default_root_url = "https://atlases.ebrains.eu/viewer/" + def sanitize_id(id: str): - return id.replace('/', ':') + return id.replace("/", ":") + -def get_perspective_zoom(atlas: "Atlas", space: "Space", parc: "Parcellation", region: Optional["Region"]): +def get_perspective_zoom( + atlas: "Atlas", space: "Space", parc: "Parcellation", region: Optional["Region"] +): import siibra - if atlas is siibra.atlases['rat'] or atlas is siibra.atlases['mouse']: + + if atlas is siibra.atlases["rat"] or atlas is siibra.atlases["mouse"]: return 200000 return 2000000 -def get_zoom(atlas: "Atlas", space: "Space", parc: "Parcellation", region: Optional["Region"]): + +def get_zoom( + atlas: "Atlas", space: "Space", parc: "Parcellation", region: Optional["Region"] +): import siibra - if atlas is siibra.atlases['rat'] or atlas is siibra.atlases['mouse']: + + if atlas is siibra.atlases["rat"] or atlas is siibra.atlases["mouse"]: return 35000 return 350000 -supported_prefix = ( - "nifti://", - "swc://", - "precomputed://", - "deepzoom://" -) + +supported_prefix = ("nifti://", "swc://", "precomputed://", "deepzoom://") + def append_query_params(url: str, *args, query_params={}, **kwargs): - query_str = "&".join([f"{key}={quote_plus(value)}" for key, value in query_params.items()]) + query_str = "&".join( + [f"{key}={quote_plus(value)}" for key, value in query_params.items()] + ) if len(query_str) > 0: query_str = "?" + query_str return url + query_str + @post_process(append_query_params) -def encode_url(atlas: "Atlas", space: "Space", parc: "Parcellation", region: Optional["Region"]=None, *, root_url=default_root_url, external_url:str=None, feature: "Feature"=None, ignore_warning=False, query_params={}): - +def encode_url( + atlas: "Atlas", + space: "Space", + parc: "Parcellation", + region: Optional["Region"] = None, + *, + root_url=default_root_url, + external_url: str = None, + feature: "Feature" = None, + ignore_warning=False, + query_params={}, +): + overlay_url = None if external_url: - assert any([external_url.startswith(prefix) for prefix in supported_prefix]), f"url needs to start with {(' , '.join(supported_prefix))}" - overlay_url = '/x-overlay-layer:{url}'.format( + assert any( + [external_url.startswith(prefix) for prefix in supported_prefix] + ), f"url needs to start with {(' , '.join(supported_prefix))}" + overlay_url = "/x-overlay-layer:{url}".format( url=external_url.replace("/", "%2F") ) zoom = get_zoom(atlas, space, parc, region) pzoom = get_perspective_zoom(atlas, space, parc, region) - + zoom_kwargs = { "encoded_pzoom": encode_number(pzoom, False), - "encoded_zoom": encode_number(zoom, False) + "encoded_zoom": encode_number(zoom, False), } - nav_string='/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..{encoded_pzoom}..{encoded_nav}..{encoded_zoom}' - - return_url='{root_url}#/a:{atlas_id}/t:{template_id}/p:{parc_id}{overlay_url}'.format( - root_url = root_url, - atlas_id = sanitize_id(atlas.id), - template_id = sanitize_id(space.id), - parc_id = sanitize_id(parc.id), - overlay_url = overlay_url if overlay_url else "", + nav_string = "/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..{encoded_pzoom}..{encoded_nav}..{encoded_zoom}" + + return_url = ( + "{root_url}#/a:{atlas_id}/t:{template_id}/p:{parc_id}{overlay_url}".format( + root_url=root_url, + atlas_id=sanitize_id(atlas.id), + template_id=sanitize_id(space.id), + parc_id=sanitize_id(parc.id), + overlay_url=overlay_url if overlay_url else "", + ) ) if feature is not None: return_url = return_url + f"/f:{sanitize_id(feature.id)}" if region is None: - return return_url + nav_string.format(encoded_nav='0.0.0', **zoom_kwargs) - - return_url=f'{return_url}/rn:{get_hash(region.name)}' + return return_url + nav_string.format(encoded_nav="0.0.0", **zoom_kwargs) + + return_url = f"{return_url}/rn:{get_hash(region.name)}" try: - result_props=region.spatial_props(space, maptype='labelled') + result_props = region.spatial_props(space, maptype="labelled") if len(result_props.components) == 0: - return return_url + nav_string.format(encoded_nav='0.0.0', **zoom_kwargs) + return return_url + nav_string.format(encoded_nav="0.0.0", **zoom_kwargs) except Exception as e: - print(f'Cannot get_spatial_props {str(e)}') + print(f"Cannot get_spatial_props {str(e)}") if not ignore_warning: raise e - return return_url + nav_string.format(encoded_nav='0.0.0', **zoom_kwargs) + return return_url + nav_string.format(encoded_nav="0.0.0", **zoom_kwargs) - centroid=result_props.components[0].centroid + centroid = result_props.components[0].centroid - encoded_centroid=separator.join([ encode_number(math.floor(val * 1e6)) for val in centroid ]) - return_url=return_url + nav_string.format(encoded_nav=encoded_centroid, **zoom_kwargs) + encoded_centroid = separator.join( + [encode_number(math.floor(val * 1e6)) for val in centroid] + ) + return_url = return_url + nav_string.format( + encoded_nav=encoded_centroid, **zoom_kwargs + ) return return_url + @dataclass class DecodedUrl: bounding_box: "BoundingBox" + def decode_url(url: str, vp_length=1000): import siibra + from siibra.locations import Point, BoundingBox + try: - space_match = re.search(r'/t:(?P[^/]+)', url) + space_match = re.search(r"/t:(?P[^/]+)", url) space_id = space_match.group("space_id") space_id = space_id.replace(":", "/") space = siibra.spaces[space_id] except Exception as e: raise DecodeNavigationException from e - nav_match = re.search(r'/@:(?P.+)/?', url) + nav_match = re.search(r"/@:(?P[^/?]+)[/?]?", url) navigation_str = nav_match.group("navigation_str") for char in navigation_str: - assert char in cipher or char in [neg, separator], f"char {char} not in cipher, nor separator/neg" - + assert char in cipher or char in [ + neg, + separator, + ], f"char {char} not in cipher, nor separator/neg: {navigation_str}" + try: - ori_enc, pers_ori_enc, pers_zoom_enc, pos_enc, zoomm_enc = navigation_str.split(f"{separator}{separator}") + ori_enc, pers_ori_enc, pers_zoom_enc, pos_enc, zoomm_enc = navigation_str.split( + f"{separator}{separator}" + ) except Exception as e: raise DecodeNavigationException from e - + try: x_enc, y_enc, z_enc = pos_enc.split(separator) pos = [decode_number(val) for val in [x_enc, y_enc, z_enc]] @@ -135,7 +175,7 @@ def decode_url(url: str, vp_length=1000): # zoom = nm/pixel pt1 = [(coord - (zoom * vp_length / 2)) / 1e6 for coord in pos] pt1 = Point(pt1, space) - + pt2 = [(coord + (zoom * vp_length / 2)) / 1e6 for coord in pos] pt2 = Point(pt2, space) @@ -144,17 +184,18 @@ def decode_url(url: str, vp_length=1000): bbx = BoundingBox(pt1, pt2, space) return DecodedUrl(bounding_box=bbx) - + + def get_hash(full_string: str): - return_val=0 + return_val = 0 with np.errstate(over="ignore"): for char in full_string: # overflowing is expected and in fact the whole reason why convert number to int32 - + # in windows, int32((0 - min_int32) << 5), rather than overflow to wraper around, raises OverflowError shifted_5 = int32( - (return_val - min_int32) if return_val > max_int32 else return_val - << 5) + (return_val - min_int32) if return_val > max_int32 else return_val << 5 + ) return_val = int32(shifted_5 - return_val + ord(char)) return_val = return_val & return_val diff --git a/siibra/explorer/util.py b/siibra/explorer/util.py index b0d873cdd..f6bf03efa 100644 --- a/siibra/explorer/util.py +++ b/siibra/explorer/util.py @@ -3,29 +3,32 @@ from functools import wraps from typing import Callable -cipher = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-' -separator = '.' -neg = '~' +cipher = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-" +separator = "." +neg = "~" + + def encode_number(n, float_flag=False): if float_flag: - b=struct.pack('f', n) - new_n=struct.unpack('i',b) + b = struct.pack("f", n) + new_n = struct.unpack("i", b) return encode_int(new_n[0]) else: return encode_int(n) + def encode_int(n): if not isinstance(n, int): - raise ValueError('Cannot encode int') + raise ValueError("Cannot encode int") - residual=None - result='' + residual = None + result = "" if n < 0: result += neg residual = n * -1 else: residual = n - + while True: result = cipher[residual % 64] + result residual = math.floor(residual / 64) @@ -34,6 +37,7 @@ def encode_int(n): break return result + def decode_int(n): neg_flag = False if n[-1] == neg: @@ -49,6 +53,7 @@ def decode_int(n): result = result * -1 return result + def decode_number(n, float_flag=False): if float_flag: raise NotImplementedError @@ -61,5 +66,7 @@ def outer(fn): def inner(*args, **kwargs): val = fn(*args, **kwargs) return post_process(val, *args, **kwargs) + return inner + return outer