scse-spatial 1.0.0__py3-none-any.whl
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.
- scse_spatial-1.0.0.dist-info/METADATA +136 -0
- scse_spatial-1.0.0.dist-info/RECORD +71 -0
- scse_spatial-1.0.0.dist-info/WHEEL +4 -0
- scse_spatial-1.0.0.dist-info/entry_points.txt +3 -0
- spatial/__init__.py +7 -0
- spatial/__main__.py +4 -0
- spatial/_version.py +2 -0
- spatial/cli/__init__.py +19 -0
- spatial/cli/alembic.py +85 -0
- spatial/cli/imports.py +89 -0
- spatial/cli/manager.py +161 -0
- spatial/cli/server.py +24 -0
- spatial/cli/workflow.py +367 -0
- spatial/conf/__init__.py +2 -0
- spatial/conf/credentials.py +73 -0
- spatial/conf/logging.json +44 -0
- spatial/conf/platforms.py +91 -0
- spatial/conf/settings.py +397 -0
- spatial/data/base/exceptions.py +24 -0
- spatial/data/base/filters.py +65 -0
- spatial/data/base/hierarchy.py +267 -0
- spatial/data/base/pagination.py +76 -0
- spatial/data/base/service.py +173 -0
- spatial/data/features/__init__.py +2 -0
- spatial/data/features/controller.py +113 -0
- spatial/data/features/db.py +136 -0
- spatial/data/features/dto.py +66 -0
- spatial/data/features/service.py +765 -0
- spatial/data/features/tiling.py +123 -0
- spatial/data/file_storage.py +362 -0
- spatial/data/hashing.py +30 -0
- spatial/data/manifest/__init__.py +7 -0
- spatial/data/manifest/db.py +127 -0
- spatial/data/rasters/__init__.py +4 -0
- spatial/data/rasters/controller.py +145 -0
- spatial/data/rasters/db.py +214 -0
- spatial/data/rasters/dto.py +86 -0
- spatial/data/rasters/service.py +2150 -0
- spatial/data/rasters/tile.py +35 -0
- spatial/data/stac/__init__.py +1 -0
- spatial/data/stac/io.py +81 -0
- spatial/data/stac/service.py +401 -0
- spatial/db/__init__.py +23 -0
- spatial/db/base.py +33 -0
- spatial/db/migrations/__init__.py +1 -0
- spatial/db/migrations/env.py +109 -0
- spatial/db/migrations/script.py.mako +26 -0
- spatial/db/migrations/versions/64901b988c97_initial.py +219 -0
- spatial/db/migrations/versions/7f3e2a1b9c4d_rename_url_columns_to_path.py +87 -0
- spatial/db/migrations/versions/8a2b3c4d5e6f_store_tile_params_in_metadata.py +45 -0
- spatial/db/migrations/versions/__init__.py +0 -0
- spatial/db/migrations/versions/a1b2c3d4e5f6_drop_raster_step_columns.py +35 -0
- spatial/db/migrations/versions/b1c3d5e7f9a1_add_raster_hierarchy_projection_epsg.py +32 -0
- spatial/db/migrations/versions/c5f3a4b8d9e7_drop_zarr_variable_and_feature_crs.py +34 -0
- spatial/db/migrations/versions/d2e4f6a8b0c2_add_jsonb_gin_indexes.py +48 -0
- spatial/db/migrations/versions/e3f5a7c9d1b2_add_actor_attribution.py +49 -0
- spatial/db/models.py +15 -0
- spatial/projections.py +287 -0
- spatial/py.typed +1 -0
- spatial/server/__init__.py +8 -0
- spatial/server/algorithm.py +54 -0
- spatial/server/app.py +203 -0
- spatial/server/data_serve.py +135 -0
- spatial/server/dependencies.py +103 -0
- spatial/server/exceptions.py +44 -0
- spatial/server/mounts.py +76 -0
- spatial/server/root.py +60 -0
- spatial/server/schemas.py +29 -0
- spatial/server/stac.py +98 -0
- spatial/server/titiler.py +323 -0
- spatial/spatial.py +282 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scse-spatial
|
|
3
|
+
Version: 1.0.0
|
|
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,71 @@
|
|
|
1
|
+
spatial/__init__.py,sha256=O5fnfu4pycUsZ2OxD75O44Z2oYe7ypc-ErLOzxo3-iE,347
|
|
2
|
+
spatial/__main__.py,sha256=XkhyIe8pfOPor5k9qmd3xaLk0NX9k84P8552WluOVgs,66
|
|
3
|
+
spatial/_version.py,sha256=cRA43y3Jnp27uBqDtCORV6hlW2Ee4vFj6IKmlZcGaq0,52
|
|
4
|
+
spatial/cli/__init__.py,sha256=MoAVw81l00fM4iz5H23MDw8RTglGHE-lI6AZNaA7Fyc,441
|
|
5
|
+
spatial/cli/alembic.py,sha256=8_kD3luu-fZDj_-xE45m2B8mdfGtnoOmsGYUisK98WU,2580
|
|
6
|
+
spatial/cli/imports.py,sha256=8gP3XSnxxnPBavsabl_dwcZQ-uBG3oVPqzwYJ-GlltA,2908
|
|
7
|
+
spatial/cli/manager.py,sha256=esKbUoVHBECB5FPq74gJnJEP4h_xEcRDrQH-Y4_aWdk,5220
|
|
8
|
+
spatial/cli/server.py,sha256=Pds1WNRmZz4ZATMZdmHjzxhRkL9RrUxca1ob101byWw,452
|
|
9
|
+
spatial/cli/workflow.py,sha256=vaQTE9Mz07GKUV0Z8aFIV97p2TI-zOwEoydBXrFwV8U,11766
|
|
10
|
+
spatial/conf/__init__.py,sha256=l8BKzRD687hZpeXRDt-cYkNDMB6eiyL6a1vHPmIQPN0,132
|
|
11
|
+
spatial/conf/credentials.py,sha256=I92EOIJ9tbrCbxOdIoOmnGk5HtEx4sO7U5z8sRO1TW4,2312
|
|
12
|
+
spatial/conf/logging.json,sha256=OnlLx791TO87yj-n2Mh1fZp0vDeprgsPEpKg_bRZAfo,821
|
|
13
|
+
spatial/conf/platforms.py,sha256=3ctns1jl1jHZSGA6QXOclUyD1b0rWv6Fv9dx90eShuA,3052
|
|
14
|
+
spatial/conf/settings.py,sha256=feraDkHM_qvs503DnkMz7DustS4GR_KyKlg6pzMzqHc,14845
|
|
15
|
+
spatial/data/base/exceptions.py,sha256=mLE5tadlyFXu2LMqJEkUxKRhBaifseze3b8Rtsbxq54,913
|
|
16
|
+
spatial/data/base/filters.py,sha256=md1XJ-jdrKPBSEEZpTMqaZeeFpm5qOBcbRNiHXyKAv8,2052
|
|
17
|
+
spatial/data/base/hierarchy.py,sha256=8kPwVS0n9vQZwwtg0v99RLHq6Sdrd9OdB8LjvAu9Ue4,10584
|
|
18
|
+
spatial/data/base/pagination.py,sha256=dDFQX1eu2DuPp7mTd8rV_2grtIxl1UnBSvYgYAIEPO0,2300
|
|
19
|
+
spatial/data/base/service.py,sha256=WU3u6jR7Kax4GiTETHUszOQwhSgZeyIoQt99uK0Vo9o,5889
|
|
20
|
+
spatial/data/features/__init__.py,sha256=d8kqJ2X5CVaDU6oKUCvmaOWRPJp3bjcgqHZ2fxtet5Y,89
|
|
21
|
+
spatial/data/features/controller.py,sha256=P_r9HXSrvwwoT0jFbsGQbrK0oYqyHLyQtrjfP3n4u4g,3806
|
|
22
|
+
spatial/data/features/db.py,sha256=NRtHKg5jbvqUo_cQVw1Umrmdp3a_eN2oGcFy78MNUKI,4589
|
|
23
|
+
spatial/data/features/dto.py,sha256=A7ka9olxocceIBy9UgI1kBI0Xw8WhoxVwGUtsxY7rYk,2086
|
|
24
|
+
spatial/data/features/service.py,sha256=-KsBI3025Ivt2syvGkWmbU6o63HP9ZC-Y1CRNZUQnZw,26852
|
|
25
|
+
spatial/data/features/tiling.py,sha256=4Jcgfh3Q795-X7_hHo7uIO8PJWP2ixHvWnzDrRzbbOU,3397
|
|
26
|
+
spatial/data/file_storage.py,sha256=GFyfKE4G2RkOCsKOzSa0IKSl_3Ak55mI299ZiKXMirU,13259
|
|
27
|
+
spatial/data/hashing.py,sha256=mTLyANp2UDwP4E1hKD1h5T-sa_615OM-aEnM5WnZaDY,795
|
|
28
|
+
spatial/data/manifest/__init__.py,sha256=tpnbfmyLrF6rx8PuUKWkBTLv_hmqrrt0VT2_9vAU4po,187
|
|
29
|
+
spatial/data/manifest/db.py,sha256=ZeYNapN3lQM6Jh-e_uoFoblhOrr_HFXIxu6el0MsKMM,4041
|
|
30
|
+
spatial/data/rasters/__init__.py,sha256=Y_KQHT2XPQ8XcwV3l2yDQXHTyqEj-Qz-sw3znYc5HwY,192
|
|
31
|
+
spatial/data/rasters/controller.py,sha256=rVmyD_D7S5GcqhOYT_h4tAIXm_zl5nHDXEnIZlxlvV4,4970
|
|
32
|
+
spatial/data/rasters/db.py,sha256=hhVFkn99T7qFaFLHonbTa1fpXaEg-fRinNam23svgZY,7603
|
|
33
|
+
spatial/data/rasters/dto.py,sha256=JTdpX8V_UrffgsbKRF3pcaoygPKKKyGtxN2p1sZN6TQ,2882
|
|
34
|
+
spatial/data/rasters/service.py,sha256=l5xASgVkkDiZc77rYX3O14g3tmkGdvD32ZGLtFzTf-c,81385
|
|
35
|
+
spatial/data/rasters/tile.py,sha256=UuXL1O8pw_elU1a-xyiYvi-FI-VdUDehsy4zdBwFY7A,1265
|
|
36
|
+
spatial/data/stac/__init__.py,sha256=9TOLTj8K1JqNWYij4IlDIhr6UsskgXBnlr51KQqGois,64
|
|
37
|
+
spatial/data/stac/io.py,sha256=P_B8jWed7twigufVeooPQ0NjwbdeteiQhV9gi3AAa8A,2782
|
|
38
|
+
spatial/data/stac/service.py,sha256=MQR8MQaM2_KsdqSgDphHp_mHTKkWm1bCs2aqgFzh1oc,13417
|
|
39
|
+
spatial/db/__init__.py,sha256=Wib0RZ1o0mLdfJEshRaDhQv6LMTjG3MGDv9cxtBcnhQ,652
|
|
40
|
+
spatial/db/base.py,sha256=M2b3apFLfWjRj_rB8OXpbZTUcdrfEX1ZV-dYm82OeZw,981
|
|
41
|
+
spatial/db/migrations/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
42
|
+
spatial/db/migrations/env.py,sha256=Xwg6_Bf5rb-c161bzpyb0_FqgKlZzucB3jqiOz1VNYA,2695
|
|
43
|
+
spatial/db/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
|
44
|
+
spatial/db/migrations/versions/64901b988c97_initial.py,sha256=3C8i4gRa8re2_TMXQykrKGJXGn7ur-RRbivc9iLJMPk,7979
|
|
45
|
+
spatial/db/migrations/versions/7f3e2a1b9c4d_rename_url_columns_to_path.py,sha256=RGXR5LjSiisvTel_gA_2mLGAzo5EbxEvvMeh-Xg0jgg,3810
|
|
46
|
+
spatial/db/migrations/versions/8a2b3c4d5e6f_store_tile_params_in_metadata.py,sha256=8_N9X3GjKjZzl_R_tnGed3pWzMORnsxZcGE-HrBa-ZI,1332
|
|
47
|
+
spatial/db/migrations/versions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
|
+
spatial/db/migrations/versions/a1b2c3d4e5f6_drop_raster_step_columns.py,sha256=304_GfhcbmiSDUs6fahOnbHJm_hzDpPmI0vYPMzJRm0,1037
|
|
49
|
+
spatial/db/migrations/versions/b1c3d5e7f9a1_add_raster_hierarchy_projection_epsg.py,sha256=hLgbgoLzKVq6OQnG8Vxez3keYvZK12duHWuievgTFp8,896
|
|
50
|
+
spatial/db/migrations/versions/c5f3a4b8d9e7_drop_zarr_variable_and_feature_crs.py,sha256=l9BFVy6JceZkzwuTWnwsntPXorgbCjZr6rXY6EFqh3E,988
|
|
51
|
+
spatial/db/migrations/versions/d2e4f6a8b0c2_add_jsonb_gin_indexes.py,sha256=-dbQy0qbsdv84bdEGQ7rV29G3i9bLehi9sdujh_GWlA,1450
|
|
52
|
+
spatial/db/migrations/versions/e3f5a7c9d1b2_add_actor_attribution.py,sha256=r6TIiPnxZsCWEzBcnQj5qwm6vq7jtV6pBUCrmyc8EX0,1390
|
|
53
|
+
spatial/db/models.py,sha256=qYHUO1_3SNkzv7e0_lv9cOfDtOcGD_Dpvsr_9x6N_Zs,420
|
|
54
|
+
spatial/projections.py,sha256=uAr0vidrwo70ktE0bUKxNITid93rzOgw1LHhfV3lEzk,9896
|
|
55
|
+
spatial/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
56
|
+
spatial/server/__init__.py,sha256=W9gAiMQESiDlnfhcbU00PY4PL0gIun0Tb13KBlFMWEE,223
|
|
57
|
+
spatial/server/algorithm.py,sha256=tkJewk_DJEep0NjHJcVWqR1EbK5V6OBrkpDczrPZNec,1810
|
|
58
|
+
spatial/server/app.py,sha256=oZm3DWgfBJ9RntURG_R3DhJrOcae1jb1jZH6IiqDo64,6678
|
|
59
|
+
spatial/server/data_serve.py,sha256=ZkvKsR01CXYT-gOoITKJ0Fr55ZlqdmFwzkHVf0veUTA,4486
|
|
60
|
+
spatial/server/dependencies.py,sha256=z7eM94jwLqp6OShiA3NJE7NjK1Z-k6x7uRw5Coa41us,3208
|
|
61
|
+
spatial/server/exceptions.py,sha256=3l_MhJhpWPmeQC5_2CgGonhIzetgb0VS3JfHDsvxsas,1483
|
|
62
|
+
spatial/server/mounts.py,sha256=CcAdrWjq5x5KdQYtdLUXfsMMKeyqTf7DBT-cLets_18,2636
|
|
63
|
+
spatial/server/root.py,sha256=Z3MVLzY9tN39HbhvrqtbSq1jo86Mv-XLFolKELD_Su4,2062
|
|
64
|
+
spatial/server/schemas.py,sha256=EzAnUih-tkkStVL_a3UsNwzfI68UW69Msbutlyg_GS8,963
|
|
65
|
+
spatial/server/stac.py,sha256=1R9kyb4zirV_Y37fpMu0b4UafXAijO1DRxdNXfQR_kA,3199
|
|
66
|
+
spatial/server/titiler.py,sha256=2ZwCbiBnGSZNmYZ2K1BZ5jUDnITleO-D6vjgv0ZonLs,11820
|
|
67
|
+
spatial/spatial.py,sha256=bSqEq6zC98dl4T-Aky7vEzmO9WQbwtbrGPESqsOWFwk,10047
|
|
68
|
+
scse_spatial-1.0.0.dist-info/METADATA,sha256=HNT214BHTmQVQlXMFX00bCsRhFpDxOnCsLxovPZrtqI,4583
|
|
69
|
+
scse_spatial-1.0.0.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
70
|
+
scse_spatial-1.0.0.dist-info/entry_points.txt,sha256=k3HIUh-3rmsb-ExarlxTsuIt6Qc7JvQiFSWzAJtixiw,48
|
|
71
|
+
scse_spatial-1.0.0.dist-info/RECORD,,
|
spatial/__init__.py
ADDED
|
@@ -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
|
spatial/__main__.py
ADDED
spatial/_version.py
ADDED
spatial/cli/__init__.py
ADDED
|
@@ -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")
|
spatial/cli/alembic.py
ADDED
|
@@ -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
|
spatial/cli/imports.py
ADDED
|
@@ -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()]
|
spatial/cli/manager.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Manager authentication and platform inspection commands.
|
|
2
|
+
|
|
3
|
+
Staff log in with their user account (``spatial login``); the credentials
|
|
4
|
+
are cached locally and used by the other commands instead of the shared
|
|
5
|
+
service secret. The connection details (the platform's spatial DSN) are
|
|
6
|
+
fetched from the manager; secret material inside it stays in the local
|
|
7
|
+
environment via ``{env:...}`` placeholders.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Annotated, Optional
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from toolkit.client.auth import ManagerAuth
|
|
19
|
+
from toolkit.exceptions import InvalidCredentials
|
|
20
|
+
|
|
21
|
+
from spatial.conf import Settings
|
|
22
|
+
from spatial.conf.credentials import Credential, CredentialStore
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help="Manager authentication and platform inspection.")
|
|
27
|
+
platforms_app = typer.Typer(
|
|
28
|
+
help="List platforms on the manager.", invoke_without_command=True
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_USERINFO = re.compile(r"//(?P<user>[^:/@]+)(?::[^@]*)?@")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _settings() -> Settings:
|
|
35
|
+
settings = Settings()
|
|
36
|
+
settings.configure_logging()
|
|
37
|
+
return settings
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _mask_dsn(dsn: str) -> str:
|
|
41
|
+
"""Hide the password in a DSN, keeping the scheme, user and host."""
|
|
42
|
+
return _USERINFO.sub(r"//\g<user>:***@", dsn)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _require_manager_url(settings: Settings) -> str:
|
|
46
|
+
if settings.manager_url is None:
|
|
47
|
+
raise typer.BadParameter("`SPATIAL_MANAGER_URL` must be set.")
|
|
48
|
+
return str(settings.manager_url)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command("login")
|
|
52
|
+
def login(
|
|
53
|
+
username: Annotated[
|
|
54
|
+
Optional[str],
|
|
55
|
+
typer.Option("--username", "-u", help="Your manager username."),
|
|
56
|
+
] = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Log in to the manager api and cache the credentials locally."""
|
|
59
|
+
settings = _settings()
|
|
60
|
+
url = _require_manager_url(settings)
|
|
61
|
+
if username is None:
|
|
62
|
+
username = typer.prompt("Username")
|
|
63
|
+
password = typer.prompt("Password", hide_input=True)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
ManagerAuth(username, password, url)
|
|
67
|
+
except InvalidCredentials:
|
|
68
|
+
raise typer.BadParameter("The credentials are not valid.")
|
|
69
|
+
|
|
70
|
+
store = CredentialStore(settings.auth_file)
|
|
71
|
+
store.save(Credential(manager_url=url, username=username, password=password))
|
|
72
|
+
console.print(
|
|
73
|
+
f"Logged in as [bold]{username}[/bold] on {url}. "
|
|
74
|
+
f"Credentials saved to {settings.auth_file}."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("logout")
|
|
79
|
+
def logout() -> None:
|
|
80
|
+
"""Delete the locally cached manager credentials."""
|
|
81
|
+
settings = _settings()
|
|
82
|
+
store = CredentialStore(settings.auth_file)
|
|
83
|
+
if store.load() is None:
|
|
84
|
+
console.print("No cached credentials.")
|
|
85
|
+
return
|
|
86
|
+
if typer.confirm("Delete the cached credentials?"):
|
|
87
|
+
store.clear()
|
|
88
|
+
console.print("Logged out.")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command("whoami")
|
|
92
|
+
def whoami() -> None:
|
|
93
|
+
"""Show the manager identity the CLI would use."""
|
|
94
|
+
settings = _settings()
|
|
95
|
+
credential = CredentialStore(settings.auth_file).load()
|
|
96
|
+
if credential is not None:
|
|
97
|
+
console.print(f"User: {credential.username}")
|
|
98
|
+
console.print(f"Manager: {credential.manager_url}")
|
|
99
|
+
console.print("Auth: cached user credentials")
|
|
100
|
+
return
|
|
101
|
+
if settings.secret_hs256 is not None:
|
|
102
|
+
console.print("User: <service account>")
|
|
103
|
+
console.print(f"Manager: {settings.manager_url}")
|
|
104
|
+
console.print("Auth: self-signed service secret")
|
|
105
|
+
return
|
|
106
|
+
console.print("Not logged in. Run `spatial login`, or set `SPATIAL_SECRET_HS256`.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _list_platforms(settings: Settings) -> None:
|
|
110
|
+
rows = settings.get_manager_platforms().ixmp4.cached_list()
|
|
111
|
+
table = Table(title=f"Platforms on {settings.manager_url}")
|
|
112
|
+
table.add_column("slug")
|
|
113
|
+
table.add_column("name")
|
|
114
|
+
table.add_column("accessibility")
|
|
115
|
+
table.add_column("spatial")
|
|
116
|
+
table.add_column("dsn")
|
|
117
|
+
for platform in rows:
|
|
118
|
+
has_spatial = bool(getattr(platform, "spatial_dsn", None))
|
|
119
|
+
dsn_access = (
|
|
120
|
+
"visible" if getattr(platform, "is_mutable", False) else "hidden"
|
|
121
|
+
)
|
|
122
|
+
table.add_row(
|
|
123
|
+
platform.slug,
|
|
124
|
+
platform.name,
|
|
125
|
+
str(platform.accessibility),
|
|
126
|
+
"yes" if has_spatial else "no",
|
|
127
|
+
dsn_access if has_spatial else "-",
|
|
128
|
+
)
|
|
129
|
+
console.print(table)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@platforms_app.callback()
|
|
133
|
+
def platforms(
|
|
134
|
+
ctx: typer.Context,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""List the platforms you can access on the manager."""
|
|
137
|
+
if ctx.invoked_subcommand is not None:
|
|
138
|
+
return
|
|
139
|
+
_list_platforms(_settings())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@platforms_app.command("show")
|
|
143
|
+
def platforms_show(
|
|
144
|
+
slug: Annotated[str, typer.Argument(help="Platform slug.")],
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Show the connection details of one platform."""
|
|
147
|
+
settings = _settings()
|
|
148
|
+
platform = settings.get_manager_platforms().get_spatial_platform(slug)
|
|
149
|
+
|
|
150
|
+
console.print(f"[bold]{platform.name}[/bold] ({platform.slug})")
|
|
151
|
+
console.print(f"accessibility: {platform.accessibility}")
|
|
152
|
+
if getattr(platform, "is_mutable", False):
|
|
153
|
+
console.print(f"spatial DSN: {_mask_dsn(str(platform.spatial_dsn))}")
|
|
154
|
+
else:
|
|
155
|
+
console.print(
|
|
156
|
+
"spatial DSN: hidden (you are not a manager of this platform)"
|
|
157
|
+
)
|
|
158
|
+
console.print(f"spatial URL: {platform.spatial_dsn}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
__all__ = ["app", "platforms_app"]
|
spatial/cli/server.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import uvicorn
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(help="CLI for starting the spatial server.")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@app.command()
|
|
9
|
+
def start(
|
|
10
|
+
host: str = typer.Option(
|
|
11
|
+
"127.0.0.1",
|
|
12
|
+
help="The hostname to bind to.",
|
|
13
|
+
),
|
|
14
|
+
port: int = typer.Option(
|
|
15
|
+
9000,
|
|
16
|
+
help="Requested server port.",
|
|
17
|
+
),
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Start the spatial server."""
|
|
20
|
+
uvicorn.run(
|
|
21
|
+
"spatial.server.app:app",
|
|
22
|
+
host=host,
|
|
23
|
+
port=port,
|
|
24
|
+
)
|