scse-spatial 1.0.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. scse_spatial-1.0.0rc1/PKG-INFO +136 -0
  2. scse_spatial-1.0.0rc1/README.md +92 -0
  3. scse_spatial-1.0.0rc1/pyproject.toml +182 -0
  4. scse_spatial-1.0.0rc1/spatial/__init__.py +7 -0
  5. scse_spatial-1.0.0rc1/spatial/__main__.py +4 -0
  6. scse_spatial-1.0.0rc1/spatial/_version.py +2 -0
  7. scse_spatial-1.0.0rc1/spatial/cli/__init__.py +19 -0
  8. scse_spatial-1.0.0rc1/spatial/cli/alembic.py +85 -0
  9. scse_spatial-1.0.0rc1/spatial/cli/imports.py +89 -0
  10. scse_spatial-1.0.0rc1/spatial/cli/manager.py +161 -0
  11. scse_spatial-1.0.0rc1/spatial/cli/server.py +24 -0
  12. scse_spatial-1.0.0rc1/spatial/cli/workflow.py +367 -0
  13. scse_spatial-1.0.0rc1/spatial/conf/__init__.py +2 -0
  14. scse_spatial-1.0.0rc1/spatial/conf/credentials.py +73 -0
  15. scse_spatial-1.0.0rc1/spatial/conf/logging.json +44 -0
  16. scse_spatial-1.0.0rc1/spatial/conf/platforms.py +91 -0
  17. scse_spatial-1.0.0rc1/spatial/conf/settings.py +397 -0
  18. scse_spatial-1.0.0rc1/spatial/data/base/exceptions.py +24 -0
  19. scse_spatial-1.0.0rc1/spatial/data/base/filters.py +65 -0
  20. scse_spatial-1.0.0rc1/spatial/data/base/hierarchy.py +267 -0
  21. scse_spatial-1.0.0rc1/spatial/data/base/pagination.py +76 -0
  22. scse_spatial-1.0.0rc1/spatial/data/base/service.py +173 -0
  23. scse_spatial-1.0.0rc1/spatial/data/features/__init__.py +2 -0
  24. scse_spatial-1.0.0rc1/spatial/data/features/controller.py +113 -0
  25. scse_spatial-1.0.0rc1/spatial/data/features/db.py +136 -0
  26. scse_spatial-1.0.0rc1/spatial/data/features/dto.py +66 -0
  27. scse_spatial-1.0.0rc1/spatial/data/features/service.py +765 -0
  28. scse_spatial-1.0.0rc1/spatial/data/features/tiling.py +123 -0
  29. scse_spatial-1.0.0rc1/spatial/data/file_storage.py +362 -0
  30. scse_spatial-1.0.0rc1/spatial/data/hashing.py +30 -0
  31. scse_spatial-1.0.0rc1/spatial/data/manifest/__init__.py +7 -0
  32. scse_spatial-1.0.0rc1/spatial/data/manifest/db.py +127 -0
  33. scse_spatial-1.0.0rc1/spatial/data/rasters/__init__.py +4 -0
  34. scse_spatial-1.0.0rc1/spatial/data/rasters/controller.py +145 -0
  35. scse_spatial-1.0.0rc1/spatial/data/rasters/db.py +214 -0
  36. scse_spatial-1.0.0rc1/spatial/data/rasters/dto.py +86 -0
  37. scse_spatial-1.0.0rc1/spatial/data/rasters/service.py +2150 -0
  38. scse_spatial-1.0.0rc1/spatial/data/rasters/tile.py +35 -0
  39. scse_spatial-1.0.0rc1/spatial/data/stac/__init__.py +1 -0
  40. scse_spatial-1.0.0rc1/spatial/data/stac/io.py +81 -0
  41. scse_spatial-1.0.0rc1/spatial/data/stac/service.py +401 -0
  42. scse_spatial-1.0.0rc1/spatial/db/__init__.py +23 -0
  43. scse_spatial-1.0.0rc1/spatial/db/base.py +33 -0
  44. scse_spatial-1.0.0rc1/spatial/db/migrations/__init__.py +1 -0
  45. scse_spatial-1.0.0rc1/spatial/db/migrations/env.py +109 -0
  46. scse_spatial-1.0.0rc1/spatial/db/migrations/script.py.mako +26 -0
  47. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/64901b988c97_initial.py +219 -0
  48. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/7f3e2a1b9c4d_rename_url_columns_to_path.py +87 -0
  49. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/8a2b3c4d5e6f_store_tile_params_in_metadata.py +45 -0
  50. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/__init__.py +0 -0
  51. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/a1b2c3d4e5f6_drop_raster_step_columns.py +35 -0
  52. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/b1c3d5e7f9a1_add_raster_hierarchy_projection_epsg.py +32 -0
  53. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/c5f3a4b8d9e7_drop_zarr_variable_and_feature_crs.py +34 -0
  54. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/d2e4f6a8b0c2_add_jsonb_gin_indexes.py +48 -0
  55. scse_spatial-1.0.0rc1/spatial/db/migrations/versions/e3f5a7c9d1b2_add_actor_attribution.py +49 -0
  56. scse_spatial-1.0.0rc1/spatial/db/models.py +15 -0
  57. scse_spatial-1.0.0rc1/spatial/projections.py +287 -0
  58. scse_spatial-1.0.0rc1/spatial/py.typed +1 -0
  59. scse_spatial-1.0.0rc1/spatial/server/__init__.py +8 -0
  60. scse_spatial-1.0.0rc1/spatial/server/algorithm.py +54 -0
  61. scse_spatial-1.0.0rc1/spatial/server/app.py +203 -0
  62. scse_spatial-1.0.0rc1/spatial/server/data_serve.py +135 -0
  63. scse_spatial-1.0.0rc1/spatial/server/dependencies.py +103 -0
  64. scse_spatial-1.0.0rc1/spatial/server/exceptions.py +44 -0
  65. scse_spatial-1.0.0rc1/spatial/server/mounts.py +76 -0
  66. scse_spatial-1.0.0rc1/spatial/server/root.py +60 -0
  67. scse_spatial-1.0.0rc1/spatial/server/schemas.py +29 -0
  68. scse_spatial-1.0.0rc1/spatial/server/stac.py +98 -0
  69. scse_spatial-1.0.0rc1/spatial/server/titiler.py +323 -0
  70. scse_spatial-1.0.0rc1/spatial/spatial.py +282 -0
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: scse-spatial
3
+ Version: 1.0.0rc1
4
+ Summary: A registry enabling the indexing and querying of spatial data resources on other services.
5
+ Author: Max Wolschlager
6
+ Author-email: rxtx@ungut.at
7
+ Requires-Python: >=3.11,<3.15
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: alembic (>=1.12.0,<2.0.0)
14
+ Requires-Dist: dask (>=2024.0.0)
15
+ Requires-Dist: geoalchemy2 (>=0.20.0,<0.21.0)
16
+ Requires-Dist: geopandas (>=1.0,<2.0)
17
+ Requires-Dist: jsonschema (>=4.26.0,<5.0.0)
18
+ Requires-Dist: litestar (>=2.0.0,<3.0.0)
19
+ Requires-Dist: mapbox-vector-tile (>=2,<3)
20
+ Requires-Dist: morecantile (>=7.0.3,<8.0)
21
+ Requires-Dist: netcdf4 (>=1.7.3,<2.0.0)
22
+ Requires-Dist: pmtiles (>=3.7,<4)
23
+ Requires-Dist: psycopg[binary] (>=3.1.12,<4.0.0)
24
+ Requires-Dist: pydantic (>=2.4.2,<3.0.0)
25
+ Requires-Dist: pydantic-settings (>=2.0.3,<3.0.0)
26
+ Requires-Dist: pyproj (>=3.1,<4.0)
27
+ Requires-Dist: pystac[validation] (>=1.12,<1.13)
28
+ Requires-Dist: rasterio (>=1.4.3,<2.0.0)
29
+ Requires-Dist: rich (>=13.9.4,<14.0.0)
30
+ Requires-Dist: rioxarray (>=0.19,<1.0)
31
+ Requires-Dist: s3fs (>=2024.10.0,<2027.0.0)
32
+ Requires-Dist: scse-toolkit (>=2,<3)
33
+ Requires-Dist: sqlalchemy (>=2.0.21,<3)
34
+ Requires-Dist: tippecanoe (>=2.45)
35
+ Requires-Dist: titiler-core (>=2.0,<3.0)
36
+ Requires-Dist: titiler-xarray[fs] (>=2.0,<3.0)
37
+ Requires-Dist: typer (>=0.24.1,<0.25.0)
38
+ Requires-Dist: uvicorn (>=0.52.1,<0.53.0)
39
+ Requires-Dist: xarray (>=2025.1.0,<2026)
40
+ Requires-Dist: xxhash (>=3.4.1,<4.0.0)
41
+ Requires-Dist: zarr (>=3.0.0,<4.0)
42
+ Description-Content-Type: text/markdown
43
+
44
+ # scse-spatial
45
+
46
+ A registry that indexes and queries spatial data resources on other services.
47
+
48
+ ## Overview
49
+
50
+ scse-spatial is the spatial data extension to a scenario services web
51
+ application stack. It turns geodata files into fast, web-optimized resources
52
+ and serves them to any map app.
53
+
54
+ A workflow loads data. A web app reads it. This repository provides both
55
+ sides: the ingest path and the HTTP API.
56
+
57
+ ### Feeding it
58
+
59
+ Workflows push data in with the CLI or a few lines of Python. The `Spatial`
60
+ object connects to a PostGIS database and to object storage. It exposes
61
+ services for rasters, features, and a STAC catalog.
62
+
63
+ ```python
64
+ with Spatial("<platform>", settings=Settings()) as spatial:
65
+ spatial.rasters.sync_hierarchy(
66
+ "emissions",
67
+ [path],
68
+ parameters_class=EmissionParameters,
69
+ metadata_class=EmissionMetadata,
70
+ extract_meta=extract_em_meta,
71
+ )
72
+ spatial.stac.export_catalog()
73
+ ```
74
+
75
+ The platform converts each source file into optimized formats, uploads them,
76
+ and indexes them. A re-run skips files whose content did not change.
77
+
78
+ Workflows live in their own repositories. See `WORKFLOWS.md` for more information.
79
+
80
+ ### Connecting to a platform
81
+
82
+ In multi-tenant deployments the connection details come from the manager
83
+ API. Staff log in with their user account once; the credentials are cached
84
+ locally and used in place of the shared service secret.
85
+
86
+ ```bash
87
+ spatial login # prompts for username and password, caches them
88
+ spatial whoami # show the identity the CLI would use
89
+ spatial platforms # list the platforms you can access
90
+ ```
91
+
92
+ The manager DSN's secret material stays in the local environment through
93
+ `{env:...}` placeholders. Imports are attributed to the logged-in user.
94
+
95
+ ### HTTP API
96
+
97
+ Web apps can read data over HTTP. A Litestar server lists hierarchies and
98
+ layers, renders map tiles through TiTiler, and streams vector tiles and
99
+ GeoJSON from object storage.
100
+
101
+ ```bash
102
+ curl -X PATCH http://<scse-spatial>/v1/{platform}/rasters/ \
103
+ -H "Content-Type: application/json" \
104
+ -d '{"hierarchy_id": 3, "parameters__contains": {"sector": "power"}}'
105
+ ```
106
+
107
+ Point an scse-app at the `/v1` or `/titiler` endpoints, or open `/docs` and
108
+ `/titiler/docs` for interactive API docs. See `HTTP_API.md` for more information.
109
+
110
+ ## Development Setup
111
+
112
+ ```bash
113
+ # Install Poetry, minimum version >=1.2 required
114
+ pipx install poetry
115
+
116
+ # You may have to reinitialize your shell at this point.
117
+ source ~/.bashrc
118
+
119
+ # Activate in-project virtualenvs
120
+ poetry config virtualenvs.in-project true
121
+
122
+ # Add dynamic versioning plugin
123
+ poetry self add "poetry-dynamic-versioning[plugin]"
124
+
125
+ # Install dependencies
126
+ # (using "--with dev" if dev dependencies should be installed as well)
127
+ poetry install --with dev
128
+
129
+ # Activate virtual environment
130
+ poetry shell
131
+
132
+ # Copy the template environment configuration
133
+ cp template.env .env
134
+
135
+ ```
136
+
@@ -0,0 +1,92 @@
1
+ # scse-spatial
2
+
3
+ A registry that indexes and queries spatial data resources on other services.
4
+
5
+ ## Overview
6
+
7
+ scse-spatial is the spatial data extension to a scenario services web
8
+ application stack. It turns geodata files into fast, web-optimized resources
9
+ and serves them to any map app.
10
+
11
+ A workflow loads data. A web app reads it. This repository provides both
12
+ sides: the ingest path and the HTTP API.
13
+
14
+ ### Feeding it
15
+
16
+ Workflows push data in with the CLI or a few lines of Python. The `Spatial`
17
+ object connects to a PostGIS database and to object storage. It exposes
18
+ services for rasters, features, and a STAC catalog.
19
+
20
+ ```python
21
+ with Spatial("<platform>", settings=Settings()) as spatial:
22
+ spatial.rasters.sync_hierarchy(
23
+ "emissions",
24
+ [path],
25
+ parameters_class=EmissionParameters,
26
+ metadata_class=EmissionMetadata,
27
+ extract_meta=extract_em_meta,
28
+ )
29
+ spatial.stac.export_catalog()
30
+ ```
31
+
32
+ The platform converts each source file into optimized formats, uploads them,
33
+ and indexes them. A re-run skips files whose content did not change.
34
+
35
+ Workflows live in their own repositories. See `WORKFLOWS.md` for more information.
36
+
37
+ ### Connecting to a platform
38
+
39
+ In multi-tenant deployments the connection details come from the manager
40
+ API. Staff log in with their user account once; the credentials are cached
41
+ locally and used in place of the shared service secret.
42
+
43
+ ```bash
44
+ spatial login # prompts for username and password, caches them
45
+ spatial whoami # show the identity the CLI would use
46
+ spatial platforms # list the platforms you can access
47
+ ```
48
+
49
+ The manager DSN's secret material stays in the local environment through
50
+ `{env:...}` placeholders. Imports are attributed to the logged-in user.
51
+
52
+ ### HTTP API
53
+
54
+ Web apps can read data over HTTP. A Litestar server lists hierarchies and
55
+ layers, renders map tiles through TiTiler, and streams vector tiles and
56
+ GeoJSON from object storage.
57
+
58
+ ```bash
59
+ curl -X PATCH http://<scse-spatial>/v1/{platform}/rasters/ \
60
+ -H "Content-Type: application/json" \
61
+ -d '{"hierarchy_id": 3, "parameters__contains": {"sector": "power"}}'
62
+ ```
63
+
64
+ Point an scse-app at the `/v1` or `/titiler` endpoints, or open `/docs` and
65
+ `/titiler/docs` for interactive API docs. See `HTTP_API.md` for more information.
66
+
67
+ ## Development Setup
68
+
69
+ ```bash
70
+ # Install Poetry, minimum version >=1.2 required
71
+ pipx install poetry
72
+
73
+ # You may have to reinitialize your shell at this point.
74
+ source ~/.bashrc
75
+
76
+ # Activate in-project virtualenvs
77
+ poetry config virtualenvs.in-project true
78
+
79
+ # Add dynamic versioning plugin
80
+ poetry self add "poetry-dynamic-versioning[plugin]"
81
+
82
+ # Install dependencies
83
+ # (using "--with dev" if dev dependencies should be installed as well)
84
+ poetry install --with dev
85
+
86
+ # Activate virtual environment
87
+ poetry shell
88
+
89
+ # Copy the template environment configuration
90
+ cp template.env .env
91
+
92
+ ```
@@ -0,0 +1,182 @@
1
+ [tool.poetry]
2
+ name = "scse-spatial"
3
+ version = "1.0.0rc1"
4
+ description = "A registry enabling the indexing and querying of spatial data resources on other services."
5
+ authors = ["Max Wolschlager <rxtx@ungut.at>"]
6
+ readme = "README.md"
7
+ packages = [{ include = "spatial" }]
8
+ requires-poetry = ">=2.0,<3.0"
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.11, <3.15"
12
+ alembic = "^1.12.0"
13
+ sqlalchemy = ">=2.0.21,<3"
14
+ psycopg = { extras = ["binary"], version = "^3.1.12" }
15
+ geoalchemy2 = "^0.20.0"
16
+
17
+ pydantic = "^2.4.2"
18
+ litestar = "^2.0.0"
19
+ uvicorn = "^0.52.1"
20
+
21
+ pydantic-settings = "^2.0.3"
22
+ xxhash = "^3.4.1"
23
+
24
+ typer = "^0.24.1"
25
+ geopandas = "^1.0"
26
+ rasterio = "^1.4.3"
27
+ pyproj = ">=3.1,<4.0"
28
+ morecantile = ">=7.0.3,<8.0"
29
+ rioxarray = ">=0.19,<1.0"
30
+ xarray = ">=2025.1.0,<2026"
31
+ dask = ">=2024.0.0"
32
+ netcdf4 = "^1.7.3"
33
+ scse-toolkit = ">=2,<3"
34
+ titiler-core = "^2.0"
35
+ titiler-xarray = { version = "^2.0", extras = ["fs"] }
36
+ zarr = ">=3.0.0,<4.0"
37
+ s3fs = ">=2024.10.0,<2027.0.0"
38
+ pystac = { version = ">=1.12,<1.13", extras = ["validation"] }
39
+ pmtiles = ">=3.7,<4"
40
+ mapbox-vector-tile = ">=2,<3"
41
+ tippecanoe = ">=2.45"
42
+ rich = "^13.9.4"
43
+ jsonschema = "^4.26.0"
44
+
45
+
46
+ [tool.poetry.group.dev]
47
+ optional = true
48
+
49
+ [tool.poetry.group.dev.dependencies]
50
+ pytest = "^7.4.2"
51
+ ruff = ">=0.5.3"
52
+ mypy = "^1.6.0"
53
+ pytest-cov = "^4.1.0"
54
+ httpx = "^0.27.2"
55
+ pandas-stubs = "^3.0.0.260204"
56
+
57
+ [tool.poetry.scripts]
58
+ spatial = "spatial.__main__:app"
59
+
60
+ [build-system]
61
+ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
62
+ build-backend = "poetry_dynamic_versioning.backend"
63
+
64
+ [tool.ruff]
65
+ # Exclude a variety of commonly ignored directories.
66
+ exclude = [
67
+ ".eggs",
68
+ ".git",
69
+ ".git-rewrite",
70
+ ".mypy_cache",
71
+ ".pytype",
72
+ ".ruff_cache",
73
+ ".venv",
74
+ "__pypackages__",
75
+ "build",
76
+ ]
77
+
78
+ # Same as Black.
79
+ line-length = 88
80
+
81
+ [tool.ruff.lint]
82
+ # Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default.
83
+ # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
84
+ # McCabe complexity (`C901`) by default.
85
+ select = ["E", "F"]
86
+ ignore = []
87
+
88
+ fixable = ["ALL"]
89
+ unfixable = []
90
+
91
+ [tool.ruff.lint.per-file-ignores]
92
+ "__init__.py" = ["E402", "F401", "F403"]
93
+
94
+ [tool.pytest.ini_options]
95
+ testpaths = ["tests"]
96
+ markers = [
97
+ "unit: heavily mocked unit tests",
98
+ "server: server endpoint tests",
99
+ "data: integration tests on a live database",
100
+ ]
101
+
102
+
103
+ [tool.mypy]
104
+ exclude = []
105
+ disable_error_code = ['override']
106
+ show_error_codes = true
107
+ plugins = ['pydantic.mypy']
108
+ # The following are equivalent to --strict mypy as seen in
109
+ # https://mypy.readthedocs.io/en/stable/existing_code.html#introduce-stricter-options
110
+ warn_unused_configs = true
111
+ warn_redundant_casts = true
112
+ warn_unused_ignores = true
113
+ strict_equality = true
114
+ extra_checks = true
115
+ check_untyped_defs = true
116
+ disallow_subclassing_any = true
117
+ disallow_untyped_decorators = true
118
+ disallow_any_generics = true
119
+ disallow_untyped_calls = true
120
+ disallow_incomplete_defs = true
121
+ disallow_untyped_defs = true
122
+ no_implicit_reexport = true
123
+ warn_return_any = true
124
+ # These are bonus, it seems:
125
+ disallow_any_unimported = true
126
+ no_implicit_optional = true
127
+ warn_unreachable = true
128
+
129
+ [[tool.mypy.overrides]]
130
+ module = "geopandas.*"
131
+ ignore_missing_imports = true
132
+
133
+ [[tool.mypy.overrides]]
134
+ module = "rasterio.*"
135
+ ignore_missing_imports = true
136
+
137
+ [[tool.mypy.overrides]]
138
+ module = "pystac.*"
139
+ ignore_missing_imports = true
140
+
141
+ [[tool.mypy.overrides]]
142
+ module = "tippecanoe.*"
143
+ ignore_missing_imports = true
144
+
145
+ [[tool.mypy.overrides]]
146
+ module = "pmtiles.*"
147
+ follow_imports = "skip"
148
+ ignore_missing_imports = true
149
+
150
+ [[tool.mypy.overrides]]
151
+ module = "mapbox_vector_tile.*"
152
+ ignore_missing_imports = true
153
+
154
+ [[tool.mypy.overrides]]
155
+ module = "shapely.*"
156
+ ignore_missing_imports = true
157
+
158
+
159
+ [[tool.mypy.overrides]]
160
+ module = "fsspec.*"
161
+ ignore_missing_imports = true
162
+
163
+
164
+ [[tool.mypy.overrides]]
165
+ module = "botocore.*"
166
+ ignore_missing_imports = true
167
+
168
+
169
+ [[tool.mypy.overrides]]
170
+ module = "boto3.*"
171
+ ignore_missing_imports = true
172
+
173
+
174
+ [[tool.mypy.overrides]]
175
+ module = "jsonschema.*"
176
+ ignore_missing_imports = true
177
+
178
+
179
+ [tool.poetry-dynamic-versioning]
180
+ enable = false
181
+ style = "pep440"
182
+ vcs = "git"
@@ -0,0 +1,7 @@
1
+ from ._version import __version__ as __version__
2
+ from ._version import __version_tuple__ as __version_tuple__
3
+ from .spatial import Spatial as Spatial
4
+ from .data.base.exceptions import Skip as Skip
5
+ from .data.features import Feature as Feature
6
+ from .data.rasters import RasterLayer as RasterLayer
7
+ from .data.rasters import TileParams as TileParams
@@ -0,0 +1,4 @@
1
+ from spatial.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,2 @@
1
+ __version__ = "1.0.0rc1"
2
+ __version_tuple__ = (1, 0, 0, "rc1")
@@ -0,0 +1,19 @@
1
+ """Command line interface of the spatial data platform.
2
+
3
+ See the CLI help for usage information:
4
+
5
+ .. code:: bash
6
+
7
+ spatial --help
8
+ spatial server --help
9
+ """
10
+
11
+ import typer
12
+ from . import server, alembic, workflow, manager
13
+
14
+ app = typer.Typer()
15
+ app.add_typer(server.app, name="server")
16
+ app.add_typer(alembic.app, name="alembic")
17
+ app.add_typer(workflow.app)
18
+ app.add_typer(manager.app)
19
+ app.add_typer(manager.platforms_app, name="platforms")
@@ -0,0 +1,85 @@
1
+ import typer
2
+ from toolkit.db.alembic import AlembicCli
3
+ from typing_extensions import Annotated
4
+
5
+ from spatial.conf import Settings
6
+ from spatial.conf.platforms import (
7
+ resolve_dsn_env_tokens,
8
+ resolve_spatial_dsn,
9
+ )
10
+ from spatial.db import get_alembic_controller
11
+
12
+ app = AlembicCli(
13
+ help="Manages alembic migrations on selected platforms via subcommands."
14
+ )
15
+
16
+
17
+ def collect_platforms(
18
+ settings: Settings,
19
+ *,
20
+ platform: list[str] | None,
21
+ manager: bool,
22
+ ) -> list[str]:
23
+ """Collect the DSNs of the selected command targets."""
24
+ candidates: list[str] = []
25
+ if settings.manager_url is not None and (platform or manager):
26
+ try:
27
+ platforms = settings.get_manager_platforms()
28
+ except ValueError as exc:
29
+ raise typer.BadParameter(str(exc)) from exc
30
+ if manager:
31
+ for p in platforms.list_spatial_platforms():
32
+ try:
33
+ dsn = resolve_spatial_dsn(p)
34
+ except ValueError:
35
+ continue # skip platforms with unresolvable dsn
36
+ candidates.append(dsn)
37
+
38
+ if platform is not None:
39
+ for name in platform:
40
+ platform_obj = platforms.get_spatial_platform(name)
41
+ dsn = resolve_spatial_dsn(platform_obj)
42
+ candidates.append(dsn)
43
+
44
+ if not candidates and settings.postgres_dsn is not None:
45
+ candidates.append(resolve_dsn_env_tokens(settings.postgres_dsn))
46
+
47
+ return candidates
48
+
49
+
50
+ @app.callback()
51
+ def alembic(
52
+ ctx: typer.Context,
53
+ platform: Annotated[
54
+ list[str] | None,
55
+ typer.Option(
56
+ "--platform",
57
+ "-p",
58
+ help="Use (a) platform name(s) as an alembic command target.",
59
+ ),
60
+ ] = None,
61
+ manager: Annotated[
62
+ bool,
63
+ typer.Option(
64
+ help="Use platforms from the manager api as alembic targets.",
65
+ ),
66
+ ] = False,
67
+ ) -> None:
68
+ """Run alembic commands on platform databases.
69
+ Use '--platform/-p' or '--manager' to select the
70
+ target databases. Falls back to
71
+ `SPATIAL_POSTGRES_DSN` if no platform is specified."""
72
+ settings = Settings()
73
+ settings.configure_logging()
74
+
75
+ dsns = collect_platforms(settings, platform=platform, manager=manager)
76
+
77
+ if len(dsns) == 0:
78
+ raise typer.BadParameter(
79
+ "No platforms found. Use '--platform/-p', '--manager', or set "
80
+ "`SPATIAL_POSTGRES_DSN`."
81
+ )
82
+
83
+ controllers = [get_alembic_controller(dsn) for dsn in dsns]
84
+ ctx.ensure_object(dict)
85
+ ctx.obj["controllers"] = controllers
@@ -0,0 +1,89 @@
1
+ """Resolve ``<module>:<name>`` references passed on the command line.
2
+
3
+ Workflows live in their own repositories, so the objects they hand to the
4
+ CLI (Pydantic models, extractor callables) cannot be imported by the CLI
5
+ itself. They are passed as ``<module>:<name>`` strings, resolved against
6
+ the working directory, and validated against an expected kind.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from importlib import import_module
13
+ from typing import Any, Callable, cast
14
+
15
+ import pydantic as pyd
16
+ import typer
17
+
18
+ ModelClassParser = Callable[[str], type[pyd.BaseModel]]
19
+ CallableParser = Callable[[str], Callable[..., Any]]
20
+
21
+
22
+ def import_member(reference: str) -> tuple[Any, str, str]:
23
+ """Import the member named by ``<module>:<name>``.
24
+
25
+ The working directory is added to ``sys.path`` so a workflow repository
26
+ can point at its own modules without being installed.
27
+
28
+ Returns
29
+ -------
30
+ tuple[Any, str, str]
31
+ The imported object, its name, and its module path.
32
+
33
+ Raises
34
+ ------
35
+ typer.BadParameter
36
+ If *reference* is malformed or the module cannot be imported.
37
+ """
38
+ if ":" not in reference:
39
+ raise typer.BadParameter(
40
+ f"'{reference}' is not a '<module>:<name>' reference."
41
+ )
42
+ module_name, name = reference.rsplit(":", 1)
43
+ if not module_name or not name:
44
+ raise typer.BadParameter(
45
+ f"'{reference}' is not a '<module>:<name>' reference."
46
+ )
47
+
48
+ cwd = "."
49
+ if cwd not in sys.path:
50
+ sys.path.insert(0, cwd)
51
+
52
+ try:
53
+ module = import_module(module_name)
54
+ except ImportError as exc:
55
+ raise typer.BadParameter(
56
+ f"Cannot import module '{module_name}' for reference '{reference}': {exc}"
57
+ ) from exc
58
+
59
+ member = getattr(module, name, None)
60
+ if member is None:
61
+ raise typer.BadParameter(
62
+ f"'{name}' does not exist in module '{module_name}'."
63
+ )
64
+ return member, name, module_name
65
+
66
+
67
+ def parse_model_class(reference: str) -> type[pyd.BaseModel]:
68
+ """Parse ``<module>:<name>`` into a Pydantic model class."""
69
+ member, name, module_name = import_member(reference)
70
+ if not isinstance(member, type) or not issubclass(member, pyd.BaseModel):
71
+ raise typer.BadParameter(
72
+ f"'{name}' in module '{module_name}' is not a Pydantic model."
73
+ )
74
+ return member
75
+
76
+
77
+ def parse_callable(reference: str) -> Callable[..., Any]:
78
+ """Parse ``<module>:<name>`` into a callable."""
79
+ member, name, module_name = import_member(reference)
80
+ if not callable(member):
81
+ raise typer.BadParameter(
82
+ f"'{name}' in module '{module_name}' is not callable."
83
+ )
84
+ return cast(Callable[..., Any], member)
85
+
86
+
87
+ def parse_csv_paths(value: str) -> list[str]:
88
+ """Parse a comma-separated list of paths, dropping empty entries."""
89
+ return [item.strip() for item in value.split(",") if item.strip()]