honua-admin 0.2.0__tar.gz → 0.2.2__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.
- {honua_admin-0.2.0 → honua_admin-0.2.2}/CHANGELOG.md +20 -0
- {honua_admin-0.2.0 → honua_admin-0.2.2}/PKG-INFO +15 -4
- {honua_admin-0.2.0 → honua_admin-0.2.2}/README.md +13 -2
- {honua_admin-0.2.0 → honua_admin-0.2.2}/honua_admin/__init__.py +5 -0
- {honua_admin-0.2.0 → honua_admin-0.2.2}/honua_admin/_async_client.py +73 -75
- honua_admin-0.2.2/honua_admin/_catalog.py +102 -0
- {honua_admin-0.2.0 → honua_admin-0.2.2}/honua_admin/_client.py +73 -75
- honua_admin-0.2.2/honua_admin/_endpoints.py +119 -0
- {honua_admin-0.2.0 → honua_admin-0.2.2}/honua_admin/_models.py +2 -12
- {honua_admin-0.2.0 → honua_admin-0.2.2}/pyproject.toml +2 -2
- honua_admin-0.2.0/honua_admin/_endpoints.py +0 -45
- {honua_admin-0.2.0 → honua_admin-0.2.2}/.gitignore +0 -0
- {honua_admin-0.2.0 → honua_admin-0.2.2}/LICENSE +0 -0
- {honua_admin-0.2.0 → honua_admin-0.2.2}/honua_admin/_redaction.py +0 -0
- {honua_admin-0.2.0 → honua_admin-0.2.2}/honua_admin/cli.py +0 -0
- {honua_admin-0.2.0 → honua_admin-0.2.2}/honua_admin/py.typed +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2](https://github.com/honua-io/honua-sdk-python/compare/python-admin-v0.2.1...python-admin-v0.2.2) (2026-11-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* page the admin service and layer lists ([#362](https://github.com/honua-io/honua-sdk-python/issues/362)) ([eee2d3e](https://github.com/honua-io/honua-sdk-python/commit/eee2d3e4a898bdb53753bdd6d45ef471193f46b2))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* neutral wording in honua-gp docstrings and docs ([#356](https://github.com/honua-io/honua-sdk-python/issues/356)) ([08d4113](https://github.com/honua-io/honua-sdk-python/commit/08d4113a801ce22b60463ae021dc843a4dc21caa))
|
|
14
|
+
* neutralize Python client and geometry terminology ([#367](https://github.com/honua-io/honua-sdk-python/issues/367)) ([5f8b245](https://github.com/honua-io/honua-sdk-python/commit/5f8b245179550d1799a09a0efa1154604291e86a))
|
|
15
|
+
|
|
16
|
+
## [0.2.1](https://github.com/honua-io/honua-sdk-python/compare/python-admin-v0.2.0...python-admin-v0.2.1) (2026-11-04)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* show how to verify a release's build provenance ([#354](https://github.com/honua-io/honua-sdk-python/issues/354)) ([712beb1](https://github.com/honua-io/honua-sdk-python/commit/712beb11be58d4f0378b424dff3903dab56b2eaf))
|
|
22
|
+
|
|
3
23
|
## [0.2.0](https://github.com/honua-io/honua-sdk-python/compare/python-admin-v0.1.13...python-admin-v0.2.0) (2026-11-04)
|
|
4
24
|
|
|
5
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: honua-admin
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Admin / control-plane client for Honua geospatial server
|
|
5
5
|
Project-URL: Homepage, https://honua.io
|
|
6
6
|
Project-URL: Documentation, https://honua-io.github.io/honua-sdk-python/latest/
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.11
|
|
24
|
-
Requires-Dist: honua-sdk<0.3,>=0.2.
|
|
24
|
+
Requires-Dist: honua-sdk<0.3,>=0.2.2
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
27
27
|
# honua-admin
|
|
@@ -46,9 +46,9 @@ and manage server state declaratively through manifests. Sync
|
|
|
46
46
|
contract, so callers can gate control-plane calls on what the connected
|
|
47
47
|
server actually supports (`manifest_apply`, `manifest_dry_run`,
|
|
48
48
|
`metadata_resources`, ...).
|
|
49
|
-
- **Services**: `list_services()`, `get_service_settings()`,
|
|
49
|
+
- **Services**: `list_services()`, `list_services_page()`, `get_service_settings()`,
|
|
50
50
|
`update_protocols()`, `update_mapserver_settings()`.
|
|
51
|
-
- **Layers and data**: `list_layers()`, `publish_layer()`,
|
|
51
|
+
- **Layers and data**: `list_layers()`, `list_layers_page()`, `publish_layer()`,
|
|
52
52
|
`set_layer_enabled()` / `set_service_layers_enabled()`, and
|
|
53
53
|
`discover_tables()` for source-database table discovery.
|
|
54
54
|
- **Connections**: typed CRUD plus `test_connection()` /
|
|
@@ -136,6 +136,17 @@ Related: [honua-sdk](https://github.com/honua-io/honua-sdk-python/tree/trunk/pac
|
|
|
136
136
|
[honua-sdk-js](https://github.com/honua-io/honua-sdk-js) (JS/TS SDKs + MCP server),
|
|
137
137
|
[honua-sdk-dotnet](https://github.com/honua-io/honua-sdk-dotnet) (.NET SDKs).
|
|
138
138
|
|
|
139
|
+
## Verify a release
|
|
140
|
+
|
|
141
|
+
From 0.2.1, each PyPI release is built by this repository's publication workflow, which attaches
|
|
142
|
+
the same wheel and sdist to the matching GitHub release and records a build-provenance attestation
|
|
143
|
+
for them. 0.2.0 was published without an attestation; use 0.2.1 or later when you need to verify
|
|
144
|
+
provenance. To check a downloaded file:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
gh attestation verify honua_admin-<version>-py3-none-any.whl --repo honua-io/honua-sdk-python
|
|
148
|
+
```
|
|
149
|
+
|
|
139
150
|
## License
|
|
140
151
|
|
|
141
152
|
Apache-2.0
|
|
@@ -20,9 +20,9 @@ and manage server state declaratively through manifests. Sync
|
|
|
20
20
|
contract, so callers can gate control-plane calls on what the connected
|
|
21
21
|
server actually supports (`manifest_apply`, `manifest_dry_run`,
|
|
22
22
|
`metadata_resources`, ...).
|
|
23
|
-
- **Services**: `list_services()`, `get_service_settings()`,
|
|
23
|
+
- **Services**: `list_services()`, `list_services_page()`, `get_service_settings()`,
|
|
24
24
|
`update_protocols()`, `update_mapserver_settings()`.
|
|
25
|
-
- **Layers and data**: `list_layers()`, `publish_layer()`,
|
|
25
|
+
- **Layers and data**: `list_layers()`, `list_layers_page()`, `publish_layer()`,
|
|
26
26
|
`set_layer_enabled()` / `set_service_layers_enabled()`, and
|
|
27
27
|
`discover_tables()` for source-database table discovery.
|
|
28
28
|
- **Connections**: typed CRUD plus `test_connection()` /
|
|
@@ -110,6 +110,17 @@ Related: [honua-sdk](https://github.com/honua-io/honua-sdk-python/tree/trunk/pac
|
|
|
110
110
|
[honua-sdk-js](https://github.com/honua-io/honua-sdk-js) (JS/TS SDKs + MCP server),
|
|
111
111
|
[honua-sdk-dotnet](https://github.com/honua-io/honua-sdk-dotnet) (.NET SDKs).
|
|
112
112
|
|
|
113
|
+
## Verify a release
|
|
114
|
+
|
|
115
|
+
From 0.2.1, each PyPI release is built by this repository's publication workflow, which attaches
|
|
116
|
+
the same wheel and sdist to the matching GitHub release and records a build-provenance attestation
|
|
117
|
+
for them. 0.2.0 was published without an attestation; use 0.2.1 or later when you need to verify
|
|
118
|
+
provenance. To check a downloaded file:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
gh attestation verify honua_admin-<version>-py3-none-any.whl --repo honua-io/honua-sdk-python
|
|
122
|
+
```
|
|
123
|
+
|
|
113
124
|
## License
|
|
114
125
|
|
|
115
126
|
Apache-2.0
|
|
@@ -13,6 +13,7 @@ except Exception: # pragma: no cover -- editable / not-installed fallback
|
|
|
13
13
|
__version__ = "0.0.0.dev0"
|
|
14
14
|
|
|
15
15
|
from ._async_client import AsyncHonuaAdminClient
|
|
16
|
+
from ._catalog import LayerListQuery, PublishedLayerPage, ServiceListPage, ServiceListQuery
|
|
16
17
|
from ._client import HonuaAdminClient
|
|
17
18
|
from ._models import (
|
|
18
19
|
MINIMUM_SUPPORTED_CONTROL_PLANE_API_MAJOR,
|
|
@@ -132,6 +133,7 @@ __all__ = [
|
|
|
132
133
|
"HonuaAdminClient",
|
|
133
134
|
"ImportResult",
|
|
134
135
|
"KeyRotationResult",
|
|
136
|
+
"LayerListQuery",
|
|
135
137
|
"LayerStyleResponse",
|
|
136
138
|
"LayerStyleUpdateRequest",
|
|
137
139
|
"ManifestApplyEntry",
|
|
@@ -176,10 +178,13 @@ __all__ = [
|
|
|
176
178
|
"OperationProposalDetail",
|
|
177
179
|
"OperationProposalSummary",
|
|
178
180
|
"PublishLayerRequest",
|
|
181
|
+
"PublishedLayerPage",
|
|
179
182
|
"PublishedLayerSummary",
|
|
180
183
|
"ResourceMetadata",
|
|
181
184
|
"SecureConnectionDetail",
|
|
182
185
|
"SecureConnectionSummary",
|
|
186
|
+
"ServiceListPage",
|
|
187
|
+
"ServiceListQuery",
|
|
183
188
|
"ServiceSettingsResponse",
|
|
184
189
|
"ServiceSummary",
|
|
185
190
|
"StyleEncoding",
|
|
@@ -31,6 +31,7 @@ from honua_sdk.http import (
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
from . import _endpoints
|
|
34
|
+
from ._catalog import LayerListQuery, PublishedLayerPage, ServiceListPage, ServiceListQuery
|
|
34
35
|
from ._models import (
|
|
35
36
|
DEFAULT_STYLE_ENCODING as _DEFAULT_STYLE_ENCODING,
|
|
36
37
|
)
|
|
@@ -512,9 +513,7 @@ class AsyncHonuaAdminClient:
|
|
|
512
513
|
)
|
|
513
514
|
return _endpoints.unwrap_envelope(response)
|
|
514
515
|
|
|
515
|
-
# ======================================================================
|
|
516
516
|
# File imports
|
|
517
|
-
# ======================================================================
|
|
518
517
|
|
|
519
518
|
async def upload_file(
|
|
520
519
|
self,
|
|
@@ -568,9 +567,7 @@ class AsyncHonuaAdminClient:
|
|
|
568
567
|
return BackgroundImportResponse.from_dict(payload)
|
|
569
568
|
return ImportResult.from_dict(payload)
|
|
570
569
|
|
|
571
|
-
# ======================================================================
|
|
572
570
|
# Services
|
|
573
|
-
# ======================================================================
|
|
574
571
|
|
|
575
572
|
async def list_services(
|
|
576
573
|
self,
|
|
@@ -578,30 +575,41 @@ class AsyncHonuaAdminClient:
|
|
|
578
575
|
timeout: float | httpx.Timeout | None = None,
|
|
579
576
|
extra_headers: Mapping[str, str] | None = None,
|
|
580
577
|
) -> list[ServiceSummary]:
|
|
581
|
-
"""
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
578
|
+
"""Read every page of the admin service list."""
|
|
579
|
+
collected: list[ServiceSummary] = []
|
|
580
|
+
page_number = _endpoints.DEFAULT_CATALOG_PAGE
|
|
581
|
+
while True:
|
|
582
|
+
page = await self.list_services_page(
|
|
583
|
+
ServiceListQuery(page=page_number), timeout=timeout, extra_headers=extra_headers
|
|
584
|
+
)
|
|
585
|
+
advanced = _endpoints._advance_catalog_page(collected, page)
|
|
586
|
+
if advanced is None:
|
|
587
|
+
return collected
|
|
588
|
+
page_number = advanced
|
|
590
589
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
590
|
+
async def list_services_page(
|
|
591
|
+
self,
|
|
592
|
+
query: ServiceListQuery | None = None,
|
|
593
|
+
*,
|
|
594
|
+
timeout: float | httpx.Timeout | None = None,
|
|
595
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
596
|
+
) -> ServiceListPage:
|
|
597
|
+
"""Return one page of admin services and the count of every match."""
|
|
598
|
+
selected = query or ServiceListQuery()
|
|
599
|
+
return ServiceListPage.from_dict(
|
|
600
|
+
await self._request_json(
|
|
601
|
+
"GET",
|
|
602
|
+
"/api/v1/admin/services/",
|
|
603
|
+
params=_endpoints.admin_catalog_params(
|
|
604
|
+
page=selected.page,
|
|
605
|
+
page_size=selected.page_size,
|
|
606
|
+
name_query=selected.name_query,
|
|
607
|
+
extra=(("folderId", selected.folder_id),),
|
|
608
|
+
),
|
|
609
|
+
timeout=timeout,
|
|
610
|
+
extra_headers=extra_headers,
|
|
611
|
+
)
|
|
601
612
|
)
|
|
602
|
-
if not isinstance(data, list):
|
|
603
|
-
return []
|
|
604
|
-
return [ServiceSummary.from_dict(s) for s in data]
|
|
605
613
|
|
|
606
614
|
async def get_service_settings(
|
|
607
615
|
self,
|
|
@@ -786,9 +794,7 @@ class AsyncHonuaAdminClient:
|
|
|
786
794
|
)
|
|
787
795
|
return ServiceSettingsResponse.from_dict(data)
|
|
788
796
|
|
|
789
|
-
# ======================================================================
|
|
790
797
|
# Metadata Resources
|
|
791
|
-
# ======================================================================
|
|
792
798
|
|
|
793
799
|
async def list_metadata_resources(
|
|
794
800
|
self,
|
|
@@ -1004,9 +1010,7 @@ class AsyncHonuaAdminClient:
|
|
|
1004
1010
|
idempotency_key=idempotency_key,
|
|
1005
1011
|
)
|
|
1006
1012
|
|
|
1007
|
-
# ======================================================================
|
|
1008
1013
|
# Manifests
|
|
1009
|
-
# ======================================================================
|
|
1010
1014
|
|
|
1011
1015
|
async def get_version(
|
|
1012
1016
|
self,
|
|
@@ -1198,9 +1202,7 @@ class AsyncHonuaAdminClient:
|
|
|
1198
1202
|
)
|
|
1199
1203
|
return ManifestApplyResult.from_dict(data)
|
|
1200
1204
|
|
|
1201
|
-
# ======================================================================
|
|
1202
1205
|
# Migration Toolkit
|
|
1203
|
-
# ======================================================================
|
|
1204
1206
|
|
|
1205
1207
|
async def scan_migration_source(
|
|
1206
1208
|
self,
|
|
@@ -1234,12 +1236,12 @@ class AsyncHonuaAdminClient:
|
|
|
1234
1236
|
) -> ToolboxTranslationReport:
|
|
1235
1237
|
"""POST /api/v1/admin/import/toolbox/translation/validate
|
|
1236
1238
|
|
|
1237
|
-
Validate an SDK-translated
|
|
1239
|
+
Validate an SDK-translated source toolbox manifest against the server's
|
|
1238
1240
|
canonical geoprocessing process catalog, and return the
|
|
1239
1241
|
server-authoritative per-tool classification.
|
|
1240
1242
|
|
|
1241
1243
|
The server owns the round-trip proof: it never parses toolbox sources
|
|
1242
|
-
and never emulates
|
|
1244
|
+
and never emulates the scripting API. The SDK proposes a mapping; the catalog
|
|
1243
1245
|
decides whether that mapping is ``translated``,
|
|
1244
1246
|
``partially-translated``, or ``unsupported``, and returns the specific
|
|
1245
1247
|
reasons a tool cannot be fully translated.
|
|
@@ -1270,9 +1272,7 @@ class AsyncHonuaAdminClient:
|
|
|
1270
1272
|
)
|
|
1271
1273
|
return ToolboxTranslationReport.from_dict(data)
|
|
1272
1274
|
|
|
1273
|
-
# ======================================================================
|
|
1274
1275
|
# Connections
|
|
1275
|
-
# ======================================================================
|
|
1276
1276
|
|
|
1277
1277
|
async def list_connections(
|
|
1278
1278
|
self,
|
|
@@ -1561,9 +1561,7 @@ class AsyncHonuaAdminClient:
|
|
|
1561
1561
|
)
|
|
1562
1562
|
return KeyRotationResult.from_dict(data)
|
|
1563
1563
|
|
|
1564
|
-
# ======================================================================
|
|
1565
1564
|
# Layers
|
|
1566
|
-
# ======================================================================
|
|
1567
1565
|
|
|
1568
1566
|
async def list_layers(
|
|
1569
1567
|
self,
|
|
@@ -1573,37 +1571,45 @@ class AsyncHonuaAdminClient:
|
|
|
1573
1571
|
timeout: float | httpx.Timeout | None = None,
|
|
1574
1572
|
extra_headers: Mapping[str, str] | None = None,
|
|
1575
1573
|
) -> list[PublishedLayerSummary]:
|
|
1576
|
-
"""
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1574
|
+
"""Read every page of layers published on a connection."""
|
|
1575
|
+
collected: list[PublishedLayerSummary] = []
|
|
1576
|
+
page_number = _endpoints.DEFAULT_CATALOG_PAGE
|
|
1577
|
+
while True:
|
|
1578
|
+
page = await self.list_layers_page(
|
|
1579
|
+
conn_id,
|
|
1580
|
+
LayerListQuery(page=page_number, service_name=service_name),
|
|
1581
|
+
timeout=timeout,
|
|
1582
|
+
extra_headers=extra_headers,
|
|
1583
|
+
)
|
|
1584
|
+
advanced = _endpoints._advance_catalog_page(collected, page)
|
|
1585
|
+
if advanced is None:
|
|
1586
|
+
return collected
|
|
1587
|
+
page_number = advanced
|
|
1588
1588
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1589
|
+
async def list_layers_page(
|
|
1590
|
+
self,
|
|
1591
|
+
conn_id: str,
|
|
1592
|
+
query: LayerListQuery | None = None,
|
|
1593
|
+
*,
|
|
1594
|
+
timeout: float | httpx.Timeout | None = None,
|
|
1595
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
1596
|
+
) -> PublishedLayerPage:
|
|
1597
|
+
"""Return one page of layers published on a connection."""
|
|
1598
|
+
selected = query or LayerListQuery()
|
|
1599
|
+
return PublishedLayerPage.from_dict(
|
|
1600
|
+
await self._request_json(
|
|
1601
|
+
"GET",
|
|
1602
|
+
f"/api/v1/admin/connections/{encode_path_segment(conn_id)}/layers",
|
|
1603
|
+
params=_endpoints.admin_catalog_params(
|
|
1604
|
+
page=selected.page,
|
|
1605
|
+
page_size=selected.page_size,
|
|
1606
|
+
name_query=selected.name_query,
|
|
1607
|
+
extra=(("serviceName", selected.service_name),),
|
|
1608
|
+
),
|
|
1609
|
+
timeout=timeout,
|
|
1610
|
+
extra_headers=extra_headers,
|
|
1611
|
+
)
|
|
1603
1612
|
)
|
|
1604
|
-
if not isinstance(data, list):
|
|
1605
|
-
return []
|
|
1606
|
-
return [PublishedLayerSummary.from_dict(item) for item in data]
|
|
1607
1613
|
|
|
1608
1614
|
async def publish_layer(
|
|
1609
1615
|
self,
|
|
@@ -1736,9 +1742,7 @@ class AsyncHonuaAdminClient:
|
|
|
1736
1742
|
return []
|
|
1737
1743
|
return [PublishedLayerSummary.from_dict(item) for item in data]
|
|
1738
1744
|
|
|
1739
|
-
# ======================================================================
|
|
1740
1745
|
# Discovery
|
|
1741
|
-
# ======================================================================
|
|
1742
1746
|
|
|
1743
1747
|
async def discover_tables(
|
|
1744
1748
|
self,
|
|
@@ -1771,9 +1775,7 @@ class AsyncHonuaAdminClient:
|
|
|
1771
1775
|
)
|
|
1772
1776
|
return TableDiscoveryResponse.from_dict(data)
|
|
1773
1777
|
|
|
1774
|
-
# ======================================================================
|
|
1775
1778
|
# Styles
|
|
1776
|
-
# ======================================================================
|
|
1777
1779
|
|
|
1778
1780
|
async def list_styles(
|
|
1779
1781
|
self,
|
|
@@ -1997,9 +1999,7 @@ class AsyncHonuaAdminClient:
|
|
|
1997
1999
|
)
|
|
1998
2000
|
return LayerStyleResponse.from_dict(data)
|
|
1999
2001
|
|
|
2000
|
-
# ======================================================================
|
|
2001
2002
|
# Operation proposals
|
|
2002
|
-
# ======================================================================
|
|
2003
2003
|
|
|
2004
2004
|
async def list_proposals(
|
|
2005
2005
|
self,
|
|
@@ -2155,9 +2155,7 @@ class AsyncHonuaAdminClient:
|
|
|
2155
2155
|
)
|
|
2156
2156
|
return OperationProposalDetail.from_dict(data)
|
|
2157
2157
|
|
|
2158
|
-
# ======================================================================
|
|
2159
2158
|
# Config
|
|
2160
|
-
# ======================================================================
|
|
2161
2159
|
|
|
2162
2160
|
async def get_config(
|
|
2163
2161
|
self,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""One page of the admin service list or the published-layer list."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ._models import AdminServiceSummary, PublishedLayerSummary, _snake_keys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _page_int(value: Any, default: int) -> int:
|
|
12
|
+
if value is None:
|
|
13
|
+
return default
|
|
14
|
+
return int(value)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _require_page(data: Any, kind: str) -> dict[str, Any]:
|
|
18
|
+
if data is None:
|
|
19
|
+
return {}
|
|
20
|
+
if not isinstance(data, dict):
|
|
21
|
+
raise ValueError(f"{kind} data must be a page object.")
|
|
22
|
+
return _snake_keys(data)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _page_items(raw: Any, parse: Any, kind: str) -> list[Any]:
|
|
26
|
+
if raw is None:
|
|
27
|
+
return []
|
|
28
|
+
if not isinstance(raw, list) or any(not isinstance(item, dict) for item in raw):
|
|
29
|
+
raise ValueError(f"{kind} items must be a list of objects.")
|
|
30
|
+
return [parse(item) for item in raw]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _page_counts(parsed: dict[str, Any]) -> tuple[int, int, int]:
|
|
34
|
+
return (
|
|
35
|
+
_page_int(parsed.get("page"), 1),
|
|
36
|
+
_page_int(parsed.get("page_size"), 100),
|
|
37
|
+
_page_int(parsed.get("total"), 0),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class ServiceListQuery:
|
|
43
|
+
"""Selects one page of the admin service list."""
|
|
44
|
+
|
|
45
|
+
page: int = 1
|
|
46
|
+
page_size: int = 100
|
|
47
|
+
name_query: str | None = None
|
|
48
|
+
folder_id: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class ServiceListPage:
|
|
53
|
+
"""One page of admin services and the count of every match."""
|
|
54
|
+
|
|
55
|
+
items: list[AdminServiceSummary]
|
|
56
|
+
page: int
|
|
57
|
+
page_size: int
|
|
58
|
+
total: int
|
|
59
|
+
layer_total: int
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_dict(cls, data: dict[str, Any] | None) -> ServiceListPage:
|
|
63
|
+
parsed = _require_page(data, "Service list")
|
|
64
|
+
page, page_size, total = _page_counts(parsed)
|
|
65
|
+
return cls(
|
|
66
|
+
items=_page_items(parsed.get("items"), AdminServiceSummary.from_dict, "Service list"),
|
|
67
|
+
page=page,
|
|
68
|
+
page_size=page_size,
|
|
69
|
+
total=total,
|
|
70
|
+
layer_total=_page_int(parsed.get("layer_total"), 0),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class LayerListQuery:
|
|
76
|
+
"""Selects one page of layers published on a connection."""
|
|
77
|
+
|
|
78
|
+
page: int = 1
|
|
79
|
+
page_size: int = 100
|
|
80
|
+
name_query: str | None = None
|
|
81
|
+
service_name: str | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class PublishedLayerPage:
|
|
86
|
+
"""One page of published layers and the count of every match."""
|
|
87
|
+
|
|
88
|
+
items: list[PublishedLayerSummary]
|
|
89
|
+
page: int
|
|
90
|
+
page_size: int
|
|
91
|
+
total: int
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: dict[str, Any] | None) -> PublishedLayerPage:
|
|
95
|
+
parsed = _require_page(data, "Layer list")
|
|
96
|
+
page, page_size, total = _page_counts(parsed)
|
|
97
|
+
return cls(
|
|
98
|
+
items=_page_items(parsed.get("items"), PublishedLayerSummary.from_dict, "Layer list"),
|
|
99
|
+
page=page,
|
|
100
|
+
page_size=page_size,
|
|
101
|
+
total=total,
|
|
102
|
+
)
|
|
@@ -33,6 +33,7 @@ from honua_sdk.http import (
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
from . import _endpoints
|
|
36
|
+
from ._catalog import LayerListQuery, PublishedLayerPage, ServiceListPage, ServiceListQuery
|
|
36
37
|
from ._models import (
|
|
37
38
|
DEFAULT_STYLE_ENCODING as _DEFAULT_STYLE_ENCODING,
|
|
38
39
|
)
|
|
@@ -513,9 +514,7 @@ class HonuaAdminClient:
|
|
|
513
514
|
)
|
|
514
515
|
return _endpoints.unwrap_envelope(response)
|
|
515
516
|
|
|
516
|
-
# ======================================================================
|
|
517
517
|
# File imports
|
|
518
|
-
# ======================================================================
|
|
519
518
|
|
|
520
519
|
def upload_file(
|
|
521
520
|
self,
|
|
@@ -569,9 +568,7 @@ class HonuaAdminClient:
|
|
|
569
568
|
return BackgroundImportResponse.from_dict(payload)
|
|
570
569
|
return ImportResult.from_dict(payload)
|
|
571
570
|
|
|
572
|
-
# ======================================================================
|
|
573
571
|
# Services
|
|
574
|
-
# ======================================================================
|
|
575
572
|
|
|
576
573
|
def list_services(
|
|
577
574
|
self,
|
|
@@ -579,30 +576,41 @@ class HonuaAdminClient:
|
|
|
579
576
|
timeout: float | httpx.Timeout | None = None,
|
|
580
577
|
extra_headers: Mapping[str, str] | None = None,
|
|
581
578
|
) -> list[ServiceSummary]:
|
|
582
|
-
"""
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
579
|
+
"""Read every page of the admin service list."""
|
|
580
|
+
collected: list[ServiceSummary] = []
|
|
581
|
+
page_number = _endpoints.DEFAULT_CATALOG_PAGE
|
|
582
|
+
while True:
|
|
583
|
+
page = self.list_services_page(
|
|
584
|
+
ServiceListQuery(page=page_number), timeout=timeout, extra_headers=extra_headers
|
|
585
|
+
)
|
|
586
|
+
advanced = _endpoints._advance_catalog_page(collected, page)
|
|
587
|
+
if advanced is None:
|
|
588
|
+
return collected
|
|
589
|
+
page_number = advanced
|
|
591
590
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
591
|
+
def list_services_page(
|
|
592
|
+
self,
|
|
593
|
+
query: ServiceListQuery | None = None,
|
|
594
|
+
*,
|
|
595
|
+
timeout: float | httpx.Timeout | None = None,
|
|
596
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
597
|
+
) -> ServiceListPage:
|
|
598
|
+
"""Return one page of admin services and the count of every match."""
|
|
599
|
+
selected = query or ServiceListQuery()
|
|
600
|
+
return ServiceListPage.from_dict(
|
|
601
|
+
self._request_json(
|
|
602
|
+
"GET",
|
|
603
|
+
"/api/v1/admin/services/",
|
|
604
|
+
params=_endpoints.admin_catalog_params(
|
|
605
|
+
page=selected.page,
|
|
606
|
+
page_size=selected.page_size,
|
|
607
|
+
name_query=selected.name_query,
|
|
608
|
+
extra=(("folderId", selected.folder_id),),
|
|
609
|
+
),
|
|
610
|
+
timeout=timeout,
|
|
611
|
+
extra_headers=extra_headers,
|
|
612
|
+
)
|
|
602
613
|
)
|
|
603
|
-
if not isinstance(data, list):
|
|
604
|
-
return []
|
|
605
|
-
return [ServiceSummary.from_dict(s) for s in data]
|
|
606
614
|
|
|
607
615
|
def get_service_settings(
|
|
608
616
|
self,
|
|
@@ -787,9 +795,7 @@ class HonuaAdminClient:
|
|
|
787
795
|
)
|
|
788
796
|
return ServiceSettingsResponse.from_dict(data)
|
|
789
797
|
|
|
790
|
-
# ======================================================================
|
|
791
798
|
# Metadata Resources
|
|
792
|
-
# ======================================================================
|
|
793
799
|
|
|
794
800
|
def list_metadata_resources(
|
|
795
801
|
self,
|
|
@@ -1005,9 +1011,7 @@ class HonuaAdminClient:
|
|
|
1005
1011
|
idempotency_key=idempotency_key,
|
|
1006
1012
|
)
|
|
1007
1013
|
|
|
1008
|
-
# ======================================================================
|
|
1009
1014
|
# Manifests
|
|
1010
|
-
# ======================================================================
|
|
1011
1015
|
|
|
1012
1016
|
def get_version(
|
|
1013
1017
|
self,
|
|
@@ -1199,9 +1203,7 @@ class HonuaAdminClient:
|
|
|
1199
1203
|
)
|
|
1200
1204
|
return ManifestApplyResult.from_dict(data)
|
|
1201
1205
|
|
|
1202
|
-
# ======================================================================
|
|
1203
1206
|
# Migration Toolkit
|
|
1204
|
-
# ======================================================================
|
|
1205
1207
|
|
|
1206
1208
|
def scan_migration_source(
|
|
1207
1209
|
self,
|
|
@@ -1235,12 +1237,12 @@ class HonuaAdminClient:
|
|
|
1235
1237
|
) -> ToolboxTranslationReport:
|
|
1236
1238
|
"""POST /api/v1/admin/import/toolbox/translation/validate
|
|
1237
1239
|
|
|
1238
|
-
Validate an SDK-translated
|
|
1240
|
+
Validate an SDK-translated source toolbox manifest against the server's
|
|
1239
1241
|
canonical geoprocessing process catalog, and return the
|
|
1240
1242
|
server-authoritative per-tool classification.
|
|
1241
1243
|
|
|
1242
1244
|
The server owns the round-trip proof: it never parses toolbox sources
|
|
1243
|
-
and never emulates
|
|
1245
|
+
and never emulates the scripting API. The SDK proposes a mapping; the catalog
|
|
1244
1246
|
decides whether that mapping is ``translated``,
|
|
1245
1247
|
``partially-translated``, or ``unsupported``, and returns the specific
|
|
1246
1248
|
reasons a tool cannot be fully translated.
|
|
@@ -1271,9 +1273,7 @@ class HonuaAdminClient:
|
|
|
1271
1273
|
)
|
|
1272
1274
|
return ToolboxTranslationReport.from_dict(data)
|
|
1273
1275
|
|
|
1274
|
-
# ======================================================================
|
|
1275
1276
|
# Connections
|
|
1276
|
-
# ======================================================================
|
|
1277
1277
|
|
|
1278
1278
|
def list_connections(
|
|
1279
1279
|
self,
|
|
@@ -1562,9 +1562,7 @@ class HonuaAdminClient:
|
|
|
1562
1562
|
)
|
|
1563
1563
|
return KeyRotationResult.from_dict(data)
|
|
1564
1564
|
|
|
1565
|
-
# ======================================================================
|
|
1566
1565
|
# Layers
|
|
1567
|
-
# ======================================================================
|
|
1568
1566
|
|
|
1569
1567
|
def list_layers(
|
|
1570
1568
|
self,
|
|
@@ -1574,37 +1572,45 @@ class HonuaAdminClient:
|
|
|
1574
1572
|
timeout: float | httpx.Timeout | None = None,
|
|
1575
1573
|
extra_headers: Mapping[str, str] | None = None,
|
|
1576
1574
|
) -> list[PublishedLayerSummary]:
|
|
1577
|
-
"""
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1575
|
+
"""Read every page of layers published on a connection."""
|
|
1576
|
+
collected: list[PublishedLayerSummary] = []
|
|
1577
|
+
page_number = _endpoints.DEFAULT_CATALOG_PAGE
|
|
1578
|
+
while True:
|
|
1579
|
+
page = self.list_layers_page(
|
|
1580
|
+
conn_id,
|
|
1581
|
+
LayerListQuery(page=page_number, service_name=service_name),
|
|
1582
|
+
timeout=timeout,
|
|
1583
|
+
extra_headers=extra_headers,
|
|
1584
|
+
)
|
|
1585
|
+
advanced = _endpoints._advance_catalog_page(collected, page)
|
|
1586
|
+
if advanced is None:
|
|
1587
|
+
return collected
|
|
1588
|
+
page_number = advanced
|
|
1589
1589
|
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1590
|
+
def list_layers_page(
|
|
1591
|
+
self,
|
|
1592
|
+
conn_id: str,
|
|
1593
|
+
query: LayerListQuery | None = None,
|
|
1594
|
+
*,
|
|
1595
|
+
timeout: float | httpx.Timeout | None = None,
|
|
1596
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
1597
|
+
) -> PublishedLayerPage:
|
|
1598
|
+
"""Return one page of layers published on a connection."""
|
|
1599
|
+
selected = query or LayerListQuery()
|
|
1600
|
+
return PublishedLayerPage.from_dict(
|
|
1601
|
+
self._request_json(
|
|
1602
|
+
"GET",
|
|
1603
|
+
f"/api/v1/admin/connections/{encode_path_segment(conn_id)}/layers",
|
|
1604
|
+
params=_endpoints.admin_catalog_params(
|
|
1605
|
+
page=selected.page,
|
|
1606
|
+
page_size=selected.page_size,
|
|
1607
|
+
name_query=selected.name_query,
|
|
1608
|
+
extra=(("serviceName", selected.service_name),),
|
|
1609
|
+
),
|
|
1610
|
+
timeout=timeout,
|
|
1611
|
+
extra_headers=extra_headers,
|
|
1612
|
+
)
|
|
1604
1613
|
)
|
|
1605
|
-
if not isinstance(data, list):
|
|
1606
|
-
return []
|
|
1607
|
-
return [PublishedLayerSummary.from_dict(item) for item in data]
|
|
1608
1614
|
|
|
1609
1615
|
def publish_layer(
|
|
1610
1616
|
self,
|
|
@@ -1737,9 +1743,7 @@ class HonuaAdminClient:
|
|
|
1737
1743
|
return []
|
|
1738
1744
|
return [PublishedLayerSummary.from_dict(item) for item in data]
|
|
1739
1745
|
|
|
1740
|
-
# ======================================================================
|
|
1741
1746
|
# Discovery
|
|
1742
|
-
# ======================================================================
|
|
1743
1747
|
|
|
1744
1748
|
def discover_tables(
|
|
1745
1749
|
self,
|
|
@@ -1772,9 +1776,7 @@ class HonuaAdminClient:
|
|
|
1772
1776
|
)
|
|
1773
1777
|
return TableDiscoveryResponse.from_dict(data)
|
|
1774
1778
|
|
|
1775
|
-
# ======================================================================
|
|
1776
1779
|
# Styles
|
|
1777
|
-
# ======================================================================
|
|
1778
1780
|
|
|
1779
1781
|
def list_styles(
|
|
1780
1782
|
self,
|
|
@@ -1998,9 +2000,7 @@ class HonuaAdminClient:
|
|
|
1998
2000
|
)
|
|
1999
2001
|
return LayerStyleResponse.from_dict(data)
|
|
2000
2002
|
|
|
2001
|
-
# ======================================================================
|
|
2002
2003
|
# Operation proposals
|
|
2003
|
-
# ======================================================================
|
|
2004
2004
|
|
|
2005
2005
|
def list_proposals(
|
|
2006
2006
|
self,
|
|
@@ -2156,9 +2156,7 @@ class HonuaAdminClient:
|
|
|
2156
2156
|
)
|
|
2157
2157
|
return OperationProposalDetail.from_dict(data)
|
|
2158
2158
|
|
|
2159
|
-
# ======================================================================
|
|
2160
2159
|
# Config
|
|
2161
|
-
# ======================================================================
|
|
2162
2160
|
|
|
2163
2161
|
def get_config(
|
|
2164
2162
|
self,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Pure response-parse and header helpers shared by the admin clients.
|
|
2
|
+
|
|
3
|
+
The admin sync (:class:`HonuaAdminClient`) and async
|
|
4
|
+
(:class:`AsyncHonuaAdminClient`) clients duplicate the
|
|
5
|
+
``ApiResponse``-unwrap logic and the ``Idempotency-Key`` header policy
|
|
6
|
+
character-for-character. Both modules pull those pieces from this
|
|
7
|
+
module so the only differences left are the unavoidable ``await``
|
|
8
|
+
keywords and the sync/async ``def`` declarations.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping, Sequence
|
|
14
|
+
from typing import Any, Protocol, TypeVar
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from honua_sdk.http import build_idempotency_headers
|
|
18
|
+
|
|
19
|
+
IMPORT_UPLOAD_PATH = "/api/v1/admin/import/upload"
|
|
20
|
+
SERVICE_ACCESS_POLICY_PATH = "/api/v1/admin/services/{name}/access-policy"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_CATALOG_PAGE = 1
|
|
24
|
+
DEFAULT_CATALOG_PAGE_SIZE = 100
|
|
25
|
+
MAX_CATALOG_PAGE_SIZE = 100
|
|
26
|
+
MAX_CATALOG_NAME_QUERY_LENGTH = 200
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def admin_catalog_params(
|
|
30
|
+
*,
|
|
31
|
+
page: int = DEFAULT_CATALOG_PAGE,
|
|
32
|
+
page_size: int = DEFAULT_CATALOG_PAGE_SIZE,
|
|
33
|
+
name_query: str | None = None,
|
|
34
|
+
extra: tuple[tuple[str, str | None], ...] = (),
|
|
35
|
+
) -> dict[str, str]:
|
|
36
|
+
"""Query string for one page of an admin catalog list."""
|
|
37
|
+
if page < 1:
|
|
38
|
+
raise ValueError("page must be at least 1.")
|
|
39
|
+
if page_size < 1 or page_size > MAX_CATALOG_PAGE_SIZE:
|
|
40
|
+
raise ValueError("page_size must be from 1 to 100.")
|
|
41
|
+
name = _blank_to_none(name_query)
|
|
42
|
+
if name is not None and len(name) > MAX_CATALOG_NAME_QUERY_LENGTH:
|
|
43
|
+
raise ValueError("name query is longer than 200 characters.")
|
|
44
|
+
params = {"page": str(page), "pageSize": str(page_size)}
|
|
45
|
+
if name is not None:
|
|
46
|
+
params["q"] = name
|
|
47
|
+
for key, value in extra:
|
|
48
|
+
trimmed = _blank_to_none(value)
|
|
49
|
+
if trimmed is not None:
|
|
50
|
+
params[key] = trimmed
|
|
51
|
+
return params
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_last_catalog_page(item_count: int, page_size: int, total: int, collected: int) -> bool:
|
|
55
|
+
"""A short page, an empty page, or a full total ends the walk."""
|
|
56
|
+
return item_count == 0 or collected >= total or item_count < page_size
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_ItemT_co = TypeVar("_ItemT_co", covariant=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _CatalogPage(Protocol[_ItemT_co]):
|
|
63
|
+
@property
|
|
64
|
+
def items(self) -> Sequence[_ItemT_co]: ...
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def page(self) -> int: ...
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def page_size(self) -> int: ...
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def total(self) -> int: ...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _advance_catalog_page(collected: list[_ItemT_co], page: _CatalogPage[_ItemT_co]) -> int | None:
|
|
77
|
+
"""Append this page and return the next page number, or None when the walk is done."""
|
|
78
|
+
collected.extend(page.items)
|
|
79
|
+
if is_last_catalog_page(len(page.items), page.page_size, page.total, len(collected)):
|
|
80
|
+
return None
|
|
81
|
+
return page.page + 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _blank_to_none(value: str | None) -> str | None:
|
|
85
|
+
if value is None:
|
|
86
|
+
return None
|
|
87
|
+
trimmed = value.strip()
|
|
88
|
+
return trimmed or None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def unwrap_envelope(response: httpx.Response) -> Any:
|
|
92
|
+
"""Strip the ``{"success": true, "data": ...}`` envelope, if present.
|
|
93
|
+
|
|
94
|
+
* Empty bodies return ``None``.
|
|
95
|
+
* Bodies that are not JSON return ``response.text`` as a fallback.
|
|
96
|
+
* JSON objects with a ``data`` key return the inner value.
|
|
97
|
+
* Anything else returns the parsed payload verbatim.
|
|
98
|
+
"""
|
|
99
|
+
if not response.content:
|
|
100
|
+
return None
|
|
101
|
+
try:
|
|
102
|
+
payload = response.json()
|
|
103
|
+
except ValueError:
|
|
104
|
+
return response.text
|
|
105
|
+
if isinstance(payload, Mapping) and "data" in payload:
|
|
106
|
+
return payload["data"]
|
|
107
|
+
return payload
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"DEFAULT_CATALOG_PAGE",
|
|
112
|
+
"DEFAULT_CATALOG_PAGE_SIZE",
|
|
113
|
+
"MAX_CATALOG_NAME_QUERY_LENGTH",
|
|
114
|
+
"MAX_CATALOG_PAGE_SIZE",
|
|
115
|
+
"admin_catalog_params",
|
|
116
|
+
"build_idempotency_headers",
|
|
117
|
+
"is_last_catalog_page",
|
|
118
|
+
"unwrap_envelope",
|
|
119
|
+
]
|
|
@@ -14,7 +14,7 @@ from typing import Any, Literal, TypeAlias
|
|
|
14
14
|
# release line. Honua Server versions are ``<year>.<release>.<patch>`` SemVer triples
|
|
15
15
|
# (2026.1.1); the major tracks the year, not compatibility, so the control-plane API
|
|
16
16
|
# major carries the breaking-change signal. Builds that predate the server's version
|
|
17
|
-
# identity
|
|
17
|
+
# identity report ``1.0.0+<sha>`` and fall below the floor.
|
|
18
18
|
# tests/admin/test_server_floor_baseline.py fails when this and the baseline disagree;
|
|
19
19
|
# the JS and .NET SDKs carry the same value.
|
|
20
20
|
MINIMUM_SUPPORTED_SERVER_VERSION = "2026.1.0-0"
|
|
@@ -102,9 +102,7 @@ def _parse_version_components(version: str | None) -> tuple[int, int, int] | Non
|
|
|
102
102
|
return (parts[0], parts[1], parts[2])
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
# ---------------------------------------------------------------------------
|
|
106
105
|
# Response models (frozen)
|
|
107
|
-
# ---------------------------------------------------------------------------
|
|
108
106
|
|
|
109
107
|
|
|
110
108
|
@dataclass(frozen=True, slots=True)
|
|
@@ -845,9 +843,7 @@ class LayerStyleResponse:
|
|
|
845
843
|
return cls(**_extract_fields(cls, d))
|
|
846
844
|
|
|
847
845
|
|
|
848
|
-
# ---------------------------------------------------------------------------
|
|
849
846
|
# OGC API - Styles (styleId-keyed; ADR-0048)
|
|
850
|
-
# ---------------------------------------------------------------------------
|
|
851
847
|
|
|
852
848
|
# Stylesheet encoding identifiers accepted by the styleId-keyed client. These
|
|
853
849
|
# map to the ``Accept`` media types the OGC API - Styles surface negotiates on
|
|
@@ -977,9 +973,7 @@ class OgcStylesheet:
|
|
|
977
973
|
return parsed
|
|
978
974
|
|
|
979
975
|
|
|
980
|
-
# ---------------------------------------------------------------------------
|
|
981
976
|
# Request models (mutable)
|
|
982
|
-
# ---------------------------------------------------------------------------
|
|
983
977
|
|
|
984
978
|
|
|
985
979
|
@dataclass
|
|
@@ -1081,9 +1075,7 @@ class LayerStyleUpdateRequest:
|
|
|
1081
1075
|
return d
|
|
1082
1076
|
|
|
1083
1077
|
|
|
1084
|
-
# ---------------------------------------------------------------------------
|
|
1085
1078
|
# Migration toolkit artifact models
|
|
1086
|
-
# ---------------------------------------------------------------------------
|
|
1087
1079
|
|
|
1088
1080
|
|
|
1089
1081
|
@dataclass(frozen=True, slots=True)
|
|
@@ -1881,9 +1873,7 @@ class ToolboxTranslationReport:
|
|
|
1881
1873
|
return {**identity, **d}
|
|
1882
1874
|
|
|
1883
1875
|
|
|
1884
|
-
# ---------------------------------------------------------------------------
|
|
1885
1876
|
# Request models (mutable)
|
|
1886
|
-
# ---------------------------------------------------------------------------
|
|
1887
1877
|
|
|
1888
1878
|
|
|
1889
1879
|
@dataclass
|
|
@@ -1920,7 +1910,7 @@ class ToolboxToolDescriptor:
|
|
|
1920
1910
|
|
|
1921
1911
|
@dataclass
|
|
1922
1912
|
class ToolboxTranslationManifest:
|
|
1923
|
-
"""Client-proposed translation manifest for one
|
|
1913
|
+
"""Client-proposed translation manifest for one source toolbox.
|
|
1924
1914
|
|
|
1925
1915
|
``source_format`` is the source toolbox format: ``pyt``, ``atbx``, or
|
|
1926
1916
|
``tbx``. Redact local paths and credentials out of ``source_label`` before
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "honua-admin"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Admin / control-plane client for Honua geospatial server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
|
27
27
|
dependencies = [
|
|
28
28
|
# The floor tracks the honua-sdk release (release-please generic updater on the line
|
|
29
29
|
# below) so honua-admin always requires the SDK it was released with.
|
|
30
|
-
"honua-sdk>=0.2.
|
|
30
|
+
"honua-sdk>=0.2.2,<0.3", # x-release-please-version
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.scripts]
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"""Pure response-parse and header helpers shared by the admin clients.
|
|
2
|
-
|
|
3
|
-
The admin sync (:class:`HonuaAdminClient`) and async
|
|
4
|
-
(:class:`AsyncHonuaAdminClient`) clients duplicate the
|
|
5
|
-
``ApiResponse``-unwrap logic and the ``Idempotency-Key`` header policy
|
|
6
|
-
character-for-character. Both modules pull those pieces from this
|
|
7
|
-
module so the only differences left are the unavoidable ``await``
|
|
8
|
-
keywords and the sync/async ``def`` declarations.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
from collections.abc import Mapping
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
|
-
import httpx
|
|
17
|
-
from honua_sdk.http import build_idempotency_headers
|
|
18
|
-
|
|
19
|
-
IMPORT_UPLOAD_PATH = "/api/v1/admin/import/upload"
|
|
20
|
-
SERVICE_ACCESS_POLICY_PATH = "/api/v1/admin/services/{name}/access-policy"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def unwrap_envelope(response: httpx.Response) -> Any:
|
|
24
|
-
"""Strip the ``{"success": true, "data": ...}`` envelope, if present.
|
|
25
|
-
|
|
26
|
-
* Empty bodies return ``None``.
|
|
27
|
-
* Bodies that are not JSON return ``response.text`` as a fallback.
|
|
28
|
-
* JSON objects with a ``data`` key return the inner value.
|
|
29
|
-
* Anything else returns the parsed payload verbatim.
|
|
30
|
-
"""
|
|
31
|
-
if not response.content:
|
|
32
|
-
return None
|
|
33
|
-
try:
|
|
34
|
-
payload = response.json()
|
|
35
|
-
except ValueError:
|
|
36
|
-
return response.text
|
|
37
|
-
if isinstance(payload, Mapping) and "data" in payload:
|
|
38
|
-
return payload["data"]
|
|
39
|
-
return payload
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
__all__ = [
|
|
43
|
-
"build_idempotency_headers",
|
|
44
|
-
"unwrap_envelope",
|
|
45
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|