geodeploy 1.3.0__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 (53) hide show
  1. geodeploy-1.3.0/LICENSE +202 -0
  2. geodeploy-1.3.0/MANIFEST.in +6 -0
  3. geodeploy-1.3.0/NOTICE +11 -0
  4. geodeploy-1.3.0/PKG-INFO +102 -0
  5. geodeploy-1.3.0/PYPI.md +68 -0
  6. geodeploy-1.3.0/README.md +196 -0
  7. geodeploy-1.3.0/geodeploy/__init__.py +52 -0
  8. geodeploy-1.3.0/geodeploy/__main__.py +13 -0
  9. geodeploy-1.3.0/geodeploy/admin.py +191 -0
  10. geodeploy-1.3.0/geodeploy/catalog.py +96 -0
  11. geodeploy-1.3.0/geodeploy/cli/__init__.py +7 -0
  12. geodeploy-1.3.0/geodeploy/cli/commands/__init__.py +2 -0
  13. geodeploy-1.3.0/geodeploy/cli/commands/_common.py +233 -0
  14. geodeploy-1.3.0/geodeploy/cli/commands/admin.py +317 -0
  15. geodeploy-1.3.0/geodeploy/cli/commands/auth.py +280 -0
  16. geodeploy-1.3.0/geodeploy/cli/commands/browse.py +224 -0
  17. geodeploy-1.3.0/geodeploy/cli/commands/catalog.py +107 -0
  18. geodeploy-1.3.0/geodeploy/cli/commands/imports.py +125 -0
  19. geodeploy-1.3.0/geodeploy/cli/commands/jobs.py +38 -0
  20. geodeploy-1.3.0/geodeploy/cli/commands/layers.py +536 -0
  21. geodeploy-1.3.0/geodeploy/cli/commands/portals.py +515 -0
  22. geodeploy-1.3.0/geodeploy/cli/commands/sources.py +97 -0
  23. geodeploy-1.3.0/geodeploy/cli/commands/upload.py +154 -0
  24. geodeploy-1.3.0/geodeploy/cli/main.py +263 -0
  25. geodeploy-1.3.0/geodeploy/cli/output.py +320 -0
  26. geodeploy-1.3.0/geodeploy/client.py +327 -0
  27. geodeploy-1.3.0/geodeploy/config.py +438 -0
  28. geodeploy-1.3.0/geodeploy/errors.py +93 -0
  29. geodeploy-1.3.0/geodeploy/imports.py +65 -0
  30. geodeploy-1.3.0/geodeploy/jobs.py +73 -0
  31. geodeploy-1.3.0/geodeploy/layers.py +433 -0
  32. geodeploy-1.3.0/geodeploy/portals.py +280 -0
  33. geodeploy-1.3.0/geodeploy/py.typed +0 -0
  34. geodeploy-1.3.0/geodeploy/sources.py +70 -0
  35. geodeploy-1.3.0/geodeploy/styles.py +467 -0
  36. geodeploy-1.3.0/geodeploy/transport.py +355 -0
  37. geodeploy-1.3.0/geodeploy/uploads.py +474 -0
  38. geodeploy-1.3.0/geodeploy.egg-info/PKG-INFO +102 -0
  39. geodeploy-1.3.0/geodeploy.egg-info/SOURCES.txt +51 -0
  40. geodeploy-1.3.0/geodeploy.egg-info/dependency_links.txt +1 -0
  41. geodeploy-1.3.0/geodeploy.egg-info/entry_points.txt +2 -0
  42. geodeploy-1.3.0/geodeploy.egg-info/requires.txt +3 -0
  43. geodeploy-1.3.0/geodeploy.egg-info/top_level.txt +1 -0
  44. geodeploy-1.3.0/pyproject.toml +80 -0
  45. geodeploy-1.3.0/setup.cfg +4 -0
  46. geodeploy-1.3.0/tests/conftest.py +686 -0
  47. geodeploy-1.3.0/tests/test_cli.py +640 -0
  48. geodeploy-1.3.0/tests/test_config.py +222 -0
  49. geodeploy-1.3.0/tests/test_layers_portals.py +218 -0
  50. geodeploy-1.3.0/tests/test_output.py +178 -0
  51. geodeploy-1.3.0/tests/test_styles_jobs.py +285 -0
  52. geodeploy-1.3.0/tests/test_transport.py +216 -0
  53. geodeploy-1.3.0/tests/test_uploads.py +299 -0
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Koffi Dodji Noumonvi
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,6 @@
1
+ # The sdist should be enough to run the tests from, so a packager or a distro can verify the
2
+ # release rather than take the wheel on trust. LICENSE, NOTICE and PYPI.md come in automatically
3
+ # from the metadata; the tests do not.
4
+ recursive-include tests *.py
5
+ include README.md
6
+ global-exclude __pycache__/* *.py[cod]
geodeploy-1.3.0/NOTICE ADDED
@@ -0,0 +1,11 @@
1
+ GeoDeploy
2
+ Copyright 2026 Koffi Dodji Noumonvi
3
+
4
+ This product includes software developed for the GeoDeploy project
5
+ (https://github.com/bravemaster3/GeoDeploy).
6
+
7
+ Licensed under the Apache License, Version 2.0. See LICENSE.
8
+
9
+ GeoDeploy runs alongside third-party services (PostGIS, MinIO, Martin, TiTiler,
10
+ Redis, nginx) and bundles third-party libraries, each under its own licence.
11
+ Those services run as separate processes; their licences are unaffected by this one.
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: geodeploy
3
+ Version: 1.3.0
4
+ Summary: Command-line client and Python API for GeoDeploy — upload spatial data, build and publish portals.
5
+ Author: Koffi Dodji Noumonvi
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/bravemaster3/GeoDeploy
8
+ Project-URL: Documentation, https://docs-geodeploy.kndev.org/cli/
9
+ Project-URL: Repository, https://github.com/bravemaster3/GeoDeploy
10
+ Project-URL: Issues, https://github.com/bravemaster3/GeoDeploy/issues
11
+ Project-URL: Changelog, https://github.com/bravemaster3/GeoDeploy/blob/main/CHANGELOG.md
12
+ Keywords: gis,geospatial,postgis,geoparquet,cog,maplibre,qgis,stac,ogc-api-features,geodeploy
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: GIS
25
+ Classifier: Topic :: Utilities
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ License-File: NOTICE
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # geodeploy
36
+
37
+ **Command-line client and Python API for [GeoDeploy](https://github.com/bravemaster3/GeoDeploy)** —
38
+ the self-hosted spatial data platform and geoportal builder.
39
+
40
+ Upload data, style it, build a portal and publish it, without opening a browser:
41
+
42
+ ```bash
43
+ pip install geodeploy
44
+
45
+ geodeploy login https://geodeploy.example.org --token gdp_…
46
+ geodeploy upload roads.gpkg sites.csv dem.tif --wait
47
+ geodeploy portals create "Field sites 2026" --publish
48
+ ```
49
+
50
+ **No dependencies. Python 3.9+.** Every request goes through the standard library, so installing
51
+ this pulls in nothing else — and a QGIS plugin can vendor the same client without asking anyone to
52
+ pip-install into QGIS.
53
+
54
+ If your shell cannot find `geodeploy` after installing, pip's scripts directory is not on your
55
+ `PATH` — `python -m geodeploy` works regardless, and `pipx install geodeploy` or a virtual
56
+ environment avoids it.
57
+
58
+ ## What it covers
59
+
60
+ - **Uploads** — shapefile, GeoPackage, GeoJSON, CSV (X/Y or WKT), GeoParquet, GeoTIFF. Many files
61
+ in one command; the route is chosen per file, and anything over 48 MB goes direct to object
62
+ storage in parallel presigned parts, so a multi-gigabyte upload survives a proxy that caps
63
+ request bodies.
64
+ - **Layers** — list, inspect, rename, share (STAC / OGC API - Features opt-in), share links per
65
+ tool, download, delete, restart a stalled ingest.
66
+ - **Symbology** — colour, opacity, outlines, dashes, markers, raster colormaps and stretches, plus
67
+ data-driven styling: `--color-field pop --classify jenks --classes 6 --ramp magma`, proportional
68
+ size, and attribute-driven 3D. Classification is computed by the instance, with the same code the
69
+ portal editor uses, so the CLI and the editor can never disagree about a class.
70
+ - **Portals** — create (web map, story map or catalog), arrange and style layers, folders, About
71
+ page, assets, access tiers, publish, and a whole-configuration JSON round trip for version
72
+ control.
73
+ - **Everything else** — external WMS/XYZ/WFS services, registering data already in PostGIS or the
74
+ bucket, the public STAC and OGC API - Features catalog, ingest jobs, users, and instance
75
+ administration (health, services, updates, backups, activity log).
76
+
77
+ Every command takes `--json`, where stdout is exactly one JSON document, and exit codes distinguish
78
+ authentication (3) from network (4) from server (5) so a scheduled job can alert on the right thing.
79
+
80
+ ## As a library
81
+
82
+ ```python
83
+ from geodeploy import Client
84
+
85
+ gd = Client("https://geodeploy.example.org", token="gdp_…")
86
+ result = gd.uploads.upload("roads.gpkg", wait=True)
87
+ portal = gd.portals.create("Roads")
88
+ gd.portals.add_layer(portal["id"], result.layer_id, "vector", {"color": "#e11d48"})
89
+ gd.portals.publish(portal["id"])
90
+ ```
91
+
92
+ Typed exceptions (`AuthError`, `PermissionError_`, `NotFoundError`, `ValidationError`,
93
+ `ServerError`, `TransportError`, `JobFailed`), progress callbacks and cancellation on uploads, and a
94
+ swappable transport so a desktop application can route requests through its own network stack.
95
+
96
+ ## Documentation
97
+
98
+ Full guide: **<https://docs-geodeploy.kndev.org/cli/>**
99
+
100
+ Source and issues: **<https://github.com/bravemaster3/GeoDeploy>**
101
+
102
+ Licensed under the Apache License 2.0.
@@ -0,0 +1,68 @@
1
+ # geodeploy
2
+
3
+ **Command-line client and Python API for [GeoDeploy](https://github.com/bravemaster3/GeoDeploy)** —
4
+ the self-hosted spatial data platform and geoportal builder.
5
+
6
+ Upload data, style it, build a portal and publish it, without opening a browser:
7
+
8
+ ```bash
9
+ pip install geodeploy
10
+
11
+ geodeploy login https://geodeploy.example.org --token gdp_…
12
+ geodeploy upload roads.gpkg sites.csv dem.tif --wait
13
+ geodeploy portals create "Field sites 2026" --publish
14
+ ```
15
+
16
+ **No dependencies. Python 3.9+.** Every request goes through the standard library, so installing
17
+ this pulls in nothing else — and a QGIS plugin can vendor the same client without asking anyone to
18
+ pip-install into QGIS.
19
+
20
+ If your shell cannot find `geodeploy` after installing, pip's scripts directory is not on your
21
+ `PATH` — `python -m geodeploy` works regardless, and `pipx install geodeploy` or a virtual
22
+ environment avoids it.
23
+
24
+ ## What it covers
25
+
26
+ - **Uploads** — shapefile, GeoPackage, GeoJSON, CSV (X/Y or WKT), GeoParquet, GeoTIFF. Many files
27
+ in one command; the route is chosen per file, and anything over 48 MB goes direct to object
28
+ storage in parallel presigned parts, so a multi-gigabyte upload survives a proxy that caps
29
+ request bodies.
30
+ - **Layers** — list, inspect, rename, share (STAC / OGC API - Features opt-in), share links per
31
+ tool, download, delete, restart a stalled ingest.
32
+ - **Symbology** — colour, opacity, outlines, dashes, markers, raster colormaps and stretches, plus
33
+ data-driven styling: `--color-field pop --classify jenks --classes 6 --ramp magma`, proportional
34
+ size, and attribute-driven 3D. Classification is computed by the instance, with the same code the
35
+ portal editor uses, so the CLI and the editor can never disagree about a class.
36
+ - **Portals** — create (web map, story map or catalog), arrange and style layers, folders, About
37
+ page, assets, access tiers, publish, and a whole-configuration JSON round trip for version
38
+ control.
39
+ - **Everything else** — external WMS/XYZ/WFS services, registering data already in PostGIS or the
40
+ bucket, the public STAC and OGC API - Features catalog, ingest jobs, users, and instance
41
+ administration (health, services, updates, backups, activity log).
42
+
43
+ Every command takes `--json`, where stdout is exactly one JSON document, and exit codes distinguish
44
+ authentication (3) from network (4) from server (5) so a scheduled job can alert on the right thing.
45
+
46
+ ## As a library
47
+
48
+ ```python
49
+ from geodeploy import Client
50
+
51
+ gd = Client("https://geodeploy.example.org", token="gdp_…")
52
+ result = gd.uploads.upload("roads.gpkg", wait=True)
53
+ portal = gd.portals.create("Roads")
54
+ gd.portals.add_layer(portal["id"], result.layer_id, "vector", {"color": "#e11d48"})
55
+ gd.portals.publish(portal["id"])
56
+ ```
57
+
58
+ Typed exceptions (`AuthError`, `PermissionError_`, `NotFoundError`, `ValidationError`,
59
+ `ServerError`, `TransportError`, `JobFailed`), progress callbacks and cancellation on uploads, and a
60
+ swappable transport so a desktop application can route requests through its own network stack.
61
+
62
+ ## Documentation
63
+
64
+ Full guide: **<https://docs-geodeploy.kndev.org/cli/>**
65
+
66
+ Source and issues: **<https://github.com/bravemaster3/GeoDeploy>**
67
+
68
+ Licensed under the Apache License 2.0.
@@ -0,0 +1,196 @@
1
+ # cli/
2
+
3
+ ## Purpose
4
+ The packaged **`geodeploy`** command-line client **and** the Python API client it is built on — the
5
+ headless half of the product (roadmap v1.3). Two audiences, one package: a person or CI job at a
6
+ shell, and the **QGIS plugin** (`E-05`), which imports `geodeploy.client` directly rather than
7
+ re-implementing HTTP against the same endpoints.
8
+
9
+ User documentation is `docs/cli.md`; this file is the technical note.
10
+
11
+ ## Contents
12
+
13
+ **The library** (`geodeploy/`) — never prints, never exits, raises typed errors:
14
+ - `client.py` — `Client`: URL/auth resolution, request plumbing, error mapping, and the namespaces
15
+ (`gd.vector`, `gd.raster`, `gd.layers`, `gd.portals`, `gd.sources`, `gd.imports`, `gd.jobs`,
16
+ `gd.uploads`, `gd.admin`, `gd.users`, `gd.catalog`). `absolute()` resolves the **relative
17
+ `/s3/…` presigned URLs** a managed MinIO returns.
18
+ - `transport.py` — stdlib HTTP. `UrllibTransport` (retries only connection failures and 502/503/504,
19
+ and **never** a streamed body — the file object is already partly consumed), `MultipartBody`
20
+ (form-data streamed from disk with a real Content-Length), `ProgressReader`. Anything with
21
+ `send(Request) -> Response` can replace it; that seam is for QGIS's `QgsNetworkAccessManager`.
22
+ - `config.py` — profiles + credentials. **Tokens never enter `config.json`**: OS keyring when there
23
+ is one, else `credentials.json` at 0600 in a 0700 directory, written atomically. `normalize_url`
24
+ collapses `…/api`, trailing slashes and a bare host to one origin, so a credential saved under one
25
+ spelling is found under another. `split_portal_url` does the same job for a published portal's
26
+ URL → `(origin, slug)`, so a link copied out of the address bar is a valid portal reference
27
+ anywhere a slug is (`browse --portal`, and `Portals.get`, which every portal command goes
28
+ through). `resolve()` = flag → env → profile, for URL and token separately.
29
+ - `uploads.py` — the routing table (below) plus both transports: multipart-through-the-API, and
30
+ presign/chunked direct-to-storage with per-part retry and abort-on-failure.
31
+ - `layers.py` — the two layer kinds, and `Layers.resolve` (id | uid | `vector-3` | name; ambiguity
32
+ raises rather than guessing — including a **bare integer that exists in both kinds**, since
33
+ vector and raster ids are separate sequences and "1" is routinely two different layers).
34
+ `download_dataset()` pulls a prepared GeoParquet layer's manifest + every partition straight from
35
+ storage: complete, lossless and **not subject to the export row cap**, which is why
36
+ `layers download` prefers it over queueing an export for those layers.
37
+ - `portals.py` — portal CRUD and `layer_configs` surgery. `layer_configs[0]` = top of the list =
38
+ drawn on top. `editable_config()` drops server-owned fields before a round-trip PUT.
39
+ - `styles.py` — the style vocabulary of `api/geodeploy/services/symbology.py`, in both directions.
40
+ `build_style()` **assembles** it from plain arguments; `parse()`/`Style` **reads** one back
41
+ (mode, field, classes, categories, size, extrusion, rescale) so a consumer — the QGIS plugin —
42
+ does not re-decide what `color_mode: "graduated"` implies. `Style` is a reader, not a schema: it
43
+ never rejects and keeps `.raw`, and `to_dict()` makes build → parse → build lossless.
44
+ **Classification maths is NOT reimplemented**: `classify()` reads
45
+ `GET /data/vector/{ref}/field-stats`, so the CLI cannot disagree with the editor about which class
46
+ a feature is in. Same rule for legends — `vector.legend()` asks the instance, and
47
+ `Style.legend()` is the local twin for a style that has no URL yet (an unsaved edit), pinned
48
+ against the server's exact labels in `test_styles_jobs`.
49
+ - `catalog.py` — the public surfaces, including `public()` (the instance index behind
50
+ `geodeploy browse`) and `portal_style()`, which reads a published portal's own `style.json`.
51
+ - `jobs.py`, `sources.py`, `imports.py`, `admin.py`, `errors.py`.
52
+
53
+ `Layers.resolve_public()` is the anonymous twin of `resolve()`: with no credential a layer is looked
54
+ up in the public index, so `geodeploy --url … layers download roads` works by NAME for someone with
55
+ no account. `_LayerBase.export_to_file()` drives the queue-poll-download of a built export.
56
+
57
+ **The CLI** (`geodeploy/cli/`) — the only part allowed to print:
58
+ - `main.py` — argparse root, `Context` (formatter + lazy client), and the single place exceptions
59
+ become exit codes.
60
+ - `output.py` — `Formatter` (stdout = the answer, stderr = commentary, `--json` = one document),
61
+ tables, progress bar, `EXIT_*` constants.
62
+ - `commands/` — one module per group (`auth`, `upload`, `layers`, `portals`, `sources`, `imports`,
63
+ `jobs`, `catalog`, `admin`, `browse`), each with `register(subparsers)`. `_common.py` holds the
64
+ styling flags, shared by the three commands that take them, and `resolve_layer(..., public_ok=)`
65
+ which decides between the authenticated list and the public index.
66
+
67
+ `tests/` — 317 tests against a real HTTP server (`conftest.FakeInstance`) that records what arrived
68
+ on the wire. `pyproject.toml` — packaging; console script `geodeploy`.
69
+
70
+ ## Dependencies / relationships
71
+ - **Zero runtime dependencies, Python 3.9+.** Both constraints exist for the QGIS plugin, which
72
+ vendors this package and cannot pip-install anything on a user's machine. The floor is set by the
73
+ **oldest QGIS anyone still runs**, not the current one — institutions pin a release for years —
74
+ so it reaches back through several LTR lines rather than tracking today's. Current QGIS ships a
75
+ much newer Python; that is not the constraint. Do not add a dependency here without moving it
76
+ into an extra.
77
+ - Consumes `api/geodeploy/routers/` — see that folder's README for the permission model. Nothing in
78
+ the API imports this.
79
+ - **GeoLibre does not use this package**: its plugin is browser TypeScript
80
+ (`integrations/geolibre-plugin/`) hitting `/api/interop/geolibre/*`. What the two share is the
81
+ HTTP contract and the style vocabulary, not code.
82
+ - Parity to keep: the upload routing mirrors `ui/src/composables/useUpload.js`, and the style keys
83
+ mirror `services/symbology.py` + `ui/src/lib/symbology.js`.
84
+
85
+ ## The upload routing table (and why 48 MB)
86
+ `.gpkg/.geojson/.json/.zip` < 48 MB → `POST /data/vector/upload`; `.csv` < 48 MB → `/upload-csv`
87
+ (PostGIS); either at/over 48 MB → presigned direct-to-storage → `/large/complete` (converted to
88
+ GeoParquet); `.parquet` at any size → `/geoparquet/{presign,complete}`; `.tif` small/large →
89
+ `/data/raster/upload` or chunked → `/data/raster/large/complete`. Over 48 MB the upload is chunked
90
+ into 48 MB presigned parts, four in parallel.
91
+
92
+ **48 MB is not the API's 2 GB cap.** The binding limit is the request-body cap of whatever proxy
93
+ sits in front of the instance (Cloudflare's free tier: 100 MB), and exceeding it produces *no server
94
+ log at all* — the request never arrives. `LARGE_UPLOAD_THRESHOLD` must stay equal to the UI's.
95
+
96
+ ## Packaging and release
97
+
98
+ `pyproject.toml` follows PEP 639: `license = "Apache-2.0"` as an SPDX expression plus
99
+ `license-files`, and **no `License ::` classifier** — setuptools ≥ 77 refuses a build that declares
100
+ both. The version is single-sourced from `geodeploy/__init__.py` (`dynamic = ["version"]`), so
101
+ `--version`, the user agent and the package metadata cannot disagree.
102
+
103
+ **`PYPI.md`, not `README.md`, is the project page.** This file is the folder's technical note (the
104
+ repo convention in CLAUDE.md), which is not what someone landing on PyPI wants to read; `PYPI.md` is
105
+ the user-facing front page. Keep both current.
106
+
107
+ The name `geodeploy` was unregistered on PyPI as of 2026-08-13 (re-check immediately before the
108
+ first upload — anyone can claim it).
109
+
110
+ **Releases go through `.github/workflows/publish-cli.yml`, not from a laptop.** It uses PyPI's
111
+ Trusted Publishing: no API token exists to leak or rotate, and the workflow gates the upload on the
112
+ test suite, on `twine check`, on a clean `--no-deps` install, and on the **tag matching the packaged
113
+ version**. Cutting a release is therefore:
114
+
115
+ ```bash
116
+ # 1. bump the one line in geodeploy/__init__.py, commit, merge
117
+ # 2. tag it — the prefix is `cli-v`, not the platform's `v1.3`
118
+ git tag cli-v1.3.0b1 && git push origin cli-v1.3.0b1
119
+ ```
120
+
121
+ Use the workflow's manual run (`Actions → Publish CLI → Run workflow → testpypi`) to rehearse
122
+ against TestPyPI first. Building by hand is still the way to CHECK a change without releasing it:
123
+
124
+ ```bash
125
+ cd cli
126
+ python -m build # sdist + wheel into dist/
127
+ python -m twine check dist/* # must pass before anything is uploaded
128
+ ```
129
+
130
+ `dist/` and `build/` are git-ignored. The sdist carries the tests, so the release can be verified
131
+ from source rather than trusted.
132
+
133
+ **Versioning.** The CLI's number tracks the GeoDeploy release it ships with, so it must not claim a
134
+ release that has not happened. `1.3.0b1` went up first for exactly that reason — a version on PyPI
135
+ can never be re-uploaded, and deleting it does not free the number, so a pre-release is the only way
136
+ to test the real index without spending the one that matters. `1.3.0` followed when v1.3 tagged.
137
+ Do the same next time: `1.4.0bN` while v1.4 is unreleased, then the final.
138
+
139
+ ## The row cap, and the two ways around it
140
+
141
+ A **built** export (`gpkg`/`csv`/`geojson`, or anything clipped) stops at `FULL_EXPORT_CAP`
142
+ (1,000,000 features, env-tunable) for a whole layer and `FEATURE_CAP` (50,000) for a bbox clip,
143
+ because the worker assembles the archive in memory. The failure mode this creates is not the cap —
144
+ it is that a truncated export has the same names and formats as a complete one. So:
145
+
146
+ - the task records the row count of every file it writes, marks the ones that reached the cap in
147
+ `MANIFEST.txt` (with the uncapped alternatives spelled out), and writes `{job_id}.json` beside
148
+ the zip;
149
+ - `GET …/export-status/{job_id}` reports `truncated: [{file, rows, cap}]` from that file;
150
+ - `layers download` prints `INCOMPLETE`, names the alternative, and **exits non-zero**.
151
+
152
+ The uncapped paths, which the CLI prefers automatically where it can: a prepared **GeoParquet**
153
+ layer's own partition files (`download_dataset`, no worker involved), and **OGC API - Features**
154
+ paging for PostGIS (`ogr2ogr -f GPKG out.gpkg "OAPIF:<instance>/api/ogc" <layer>` — not wrapped by
155
+ the CLI yet; see below).
156
+
157
+ ## Current status & known issues
158
+ - `geodeploy browse` and `layers download` depend on API endpoints added in the same branch
159
+ (`/api/public`, `/data/{kind}/{ref}/export`). An older instance answers 404 for both; the browse
160
+ command says so, but `layers download` for a built format will simply fail against one.
161
+ - Built 2026-08-12 on branch `feat/cli`; **not yet exercised against a live instance** — every test
162
+ runs against the in-repo fake. First real run should be `upload --dry-run`, then a small upload,
163
+ then a portal round trip.
164
+ - Not published to PyPI yet. `pip install -e cli/` is the current install.
165
+ - `geodeploy admin update --watch` polls through the API restart the update itself causes; a failed
166
+ poll is treated as progress, which is right but means a genuinely dead instance looks like a slow
167
+ update until the poll loop is interrupted.
168
+ - No declarative `apply` (a manifest describing layers + portal, applied idempotently). The
169
+ `portals export/import` round trip is the seam it would build on. Deliberately deferred.
170
+ - No shell completion.
171
+ - The QGIS plugin does not exist yet; the seams it needs (pluggable transport, progress callbacks,
172
+ `cancel`, typed errors, no printing, and `styles.Style` for reading a style back) are in place and
173
+ tested.
174
+ **Which URL it should hand QGIS is two questions, not one.** For DISPLAY of a heavy layer,
175
+ **PMTiles** is the fastest thing we serve — pre-tiled, range-requested, no per-pan query — and it
176
+ is what `layers links` should offer first for a big GeoParquet layer. But PMTiles is a *rendering*
177
+ format: generalised geometry, tile-clipped features, attributes trimmed to what the tiles carry.
178
+ For DATA — full attributes, exact geometry, analysis, editing — the answer is OGC API - Features,
179
+ the GeoParquet partitions, or a built export. A plugin that offers only one of the two will be
180
+ wrong half the time, so the layer-add dialog needs both, labelled for what they are.
181
+ Caveat: reading PMTiles needs a GDAL with the support (3.8+), which rules out the oldest QGIS the
182
+ Python floor above deliberately still supports — so the display path must degrade to OAPIF rather
183
+ than assume it.
184
+ **Measured on the maintainer's two installs (2026-08-13)**, which is why this is a rule and not a
185
+ worry: one reports Python **3.9.5** / GDAL **3.7.2** (no PMTiles — and the exact Python the floor
186
+ above exists for), the other Python **3.12.12** / GDAL **3.12.1** (PMTiles fine). Both are in
187
+ daily use. So `metadata.txt` should say `qgisMinimumVersion=3.28` (the oldest QGIS whose Python
188
+ is ≥ 3.9) and the plugin should branch on `gdal.VersionInfo()` at RUNTIME rather than refusing to
189
+ install — blocking the old one would lock out precisely the pinned installs this package's
190
+ constraints were chosen to serve.
191
+ Develop and demo on the NEWEST QGIS; that is a different decision from what the plugin refuses to
192
+ run on, and only the second one is visible to a user with an old install.
193
+
194
+ ## Last updated
195
+ 2026-08-12 (created — packaged CLI + Python client, replacing `examples/geodeploy_cli.py`; then
196
+ `browse` + anonymous layer download on top of the new `/api/public` and per-layer export endpoints)
@@ -0,0 +1,52 @@
1
+ """GeoDeploy — command-line client and Python API.
2
+
3
+ from geodeploy import Client
4
+ gd = Client("https://geodeploy.example.org", token="gdp_…")
5
+ layer = gd.uploads.upload("roads.gpkg", wait=True)
6
+ portal = gd.portals.create("Roads")
7
+ gd.portals.add_layer(portal["id"], layer.layer_id, "vector", {"color": "#e11d48"})
8
+ gd.portals.publish(portal["id"])
9
+
10
+ Zero runtime dependencies, Python 3.9+, so the QGIS plugin can vendor this package as-is.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ # The CLI's version tracks the GeoDeploy release it ships with. A PyPI version can never be
15
+ # re-uploaded, so a number is only spent once the release it names exists: 1.3.0b1 proved the
16
+ # packaging against the real index, and this is the release it was rehearsing for.
17
+ __version__ = "1.3.0"
18
+
19
+ from .client import Client # noqa: E402 (after __version__ — the user agent reads it)
20
+ from .errors import ( # noqa: E402
21
+ APIError,
22
+ AuthError,
23
+ ConfigError,
24
+ ConflictError,
25
+ GeoDeployError,
26
+ NotFoundError,
27
+ PermissionError_,
28
+ ServerError,
29
+ TransportError,
30
+ ValidationError,
31
+ )
32
+ from .jobs import JobFailed, JobTimeout # noqa: E402
33
+ from .styles import Style, parse_style # noqa: E402
34
+
35
+ __all__ = [
36
+ "Client",
37
+ "Style",
38
+ "parse_style",
39
+ "GeoDeployError",
40
+ "APIError",
41
+ "AuthError",
42
+ "ConfigError",
43
+ "ConflictError",
44
+ "NotFoundError",
45
+ "PermissionError_",
46
+ "ServerError",
47
+ "TransportError",
48
+ "ValidationError",
49
+ "JobFailed",
50
+ "JobTimeout",
51
+ "__version__",
52
+ ]
@@ -0,0 +1,13 @@
1
+ """`python -m geodeploy` — the same entry point as the `geodeploy` console script.
2
+
3
+ Worth having: inside QGIS's bundled Python, or a virtualenv whose `Scripts/` is not on PATH, the
4
+ module form is the one that works.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ from .cli.main import main
11
+
12
+ if __name__ == "__main__":
13
+ sys.exit(main())