geodeploy 1.3.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.
- geodeploy/__init__.py +52 -0
- geodeploy/__main__.py +13 -0
- geodeploy/admin.py +191 -0
- geodeploy/catalog.py +96 -0
- geodeploy/cli/__init__.py +7 -0
- geodeploy/cli/commands/__init__.py +2 -0
- geodeploy/cli/commands/_common.py +233 -0
- geodeploy/cli/commands/admin.py +317 -0
- geodeploy/cli/commands/auth.py +280 -0
- geodeploy/cli/commands/browse.py +224 -0
- geodeploy/cli/commands/catalog.py +107 -0
- geodeploy/cli/commands/imports.py +125 -0
- geodeploy/cli/commands/jobs.py +38 -0
- geodeploy/cli/commands/layers.py +536 -0
- geodeploy/cli/commands/portals.py +515 -0
- geodeploy/cli/commands/sources.py +97 -0
- geodeploy/cli/commands/upload.py +154 -0
- geodeploy/cli/main.py +263 -0
- geodeploy/cli/output.py +320 -0
- geodeploy/client.py +327 -0
- geodeploy/config.py +438 -0
- geodeploy/errors.py +93 -0
- geodeploy/imports.py +65 -0
- geodeploy/jobs.py +73 -0
- geodeploy/layers.py +433 -0
- geodeploy/portals.py +280 -0
- geodeploy/py.typed +0 -0
- geodeploy/sources.py +70 -0
- geodeploy/styles.py +467 -0
- geodeploy/transport.py +355 -0
- geodeploy/uploads.py +474 -0
- geodeploy-1.3.0.dist-info/METADATA +102 -0
- geodeploy-1.3.0.dist-info/RECORD +38 -0
- geodeploy-1.3.0.dist-info/WHEEL +5 -0
- geodeploy-1.3.0.dist-info/entry_points.txt +2 -0
- geodeploy-1.3.0.dist-info/licenses/LICENSE +202 -0
- geodeploy-1.3.0.dist-info/licenses/NOTICE +11 -0
- geodeploy-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""`geodeploy browse` — point at an instance and see what it offers.
|
|
2
|
+
|
|
3
|
+
The one command that needs no account: give it a URL and it prints the public portals and the
|
|
4
|
+
public layers, grouped by how they are stored, with the URL each one is best consumed by. With a
|
|
5
|
+
token it also shows what that token can see — which is the same two-mode behaviour a desktop
|
|
6
|
+
plugin needs, and the reason this exists in the client rather than only in a plugin.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ...config import split_portal_url
|
|
11
|
+
from ...errors import GeoDeployError, NotFoundError
|
|
12
|
+
from ..main import add_command
|
|
13
|
+
from ..output import EXIT_GENERIC, EXIT_OK
|
|
14
|
+
|
|
15
|
+
KIND_LABEL = {"postgis": "Vector (PostGIS)", "geoparquet": "Vector (GeoParquet)",
|
|
16
|
+
"raster": "Raster (COG)"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register(subparsers) -> None:
|
|
20
|
+
parser = add_command(
|
|
21
|
+
subparsers, "browse", cmd_browse,
|
|
22
|
+
"see what an instance publishes — no account needed",
|
|
23
|
+
epilog="""\
|
|
24
|
+
examples:
|
|
25
|
+
geodeploy browse https://geodeploy.example.org anyone can run this
|
|
26
|
+
geodeploy browse the instance you are logged in to
|
|
27
|
+
geodeploy browse --portal field-sites-2026 what one portal contains
|
|
28
|
+
geodeploy browse https://gd.example.org/portals/5b5c627cfd/ a portal URL, pasted whole
|
|
29
|
+
geodeploy browse --json | jq '.layers.raster' machine-readable
|
|
30
|
+
|
|
31
|
+
With a token stored (or --token), it also reports what that token can see beyond the public
|
|
32
|
+
surface, which is usually the point of having one.
|
|
33
|
+
""")
|
|
34
|
+
parser.add_argument("instance", nargs="?",
|
|
35
|
+
help="instance URL, or a published portal's URL "
|
|
36
|
+
"(default: the active profile)")
|
|
37
|
+
parser.add_argument("--portal", metavar="SLUG_OR_URL",
|
|
38
|
+
help="show one portal's layers, read from its published style")
|
|
39
|
+
parser.add_argument("--kind", choices=["raster", "postgis", "geoparquet"],
|
|
40
|
+
help="only this kind of layer")
|
|
41
|
+
parser.add_argument("--links", action="store_true",
|
|
42
|
+
help="print every access URL for each layer, not just a summary")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cmd_browse(ctx, args) -> int:
|
|
46
|
+
instance = args.instance
|
|
47
|
+
if instance and not args.portal and "/portals/" in instance:
|
|
48
|
+
# A portal's own URL, pasted whole. It names both the instance and the portal, so it does
|
|
49
|
+
# not need --portal and must not be read as an instance URL.
|
|
50
|
+
instance, args.portal = split_portal_url(instance)
|
|
51
|
+
elif args.portal:
|
|
52
|
+
origin, args.portal = split_portal_url(args.portal)
|
|
53
|
+
instance = instance or origin # a full portal URL brings its instance with it
|
|
54
|
+
if instance:
|
|
55
|
+
args.url = instance # the positional is just a friendlier --url
|
|
56
|
+
ctx._resolved = None # re-resolve against it
|
|
57
|
+
client = ctx.client(auth_required=False)
|
|
58
|
+
out = ctx.out
|
|
59
|
+
|
|
60
|
+
if args.portal:
|
|
61
|
+
return _one_portal(ctx, client, args)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
index = client.catalog.public()
|
|
65
|
+
except NotFoundError:
|
|
66
|
+
out.error("{0} does not publish a public index.".format(client.url),
|
|
67
|
+
hint="An admin can turn it on in Settings, or you can list what your token can "
|
|
68
|
+
"see with `geodeploy layers list` and `geodeploy portals list`.")
|
|
69
|
+
return EXIT_GENERIC
|
|
70
|
+
|
|
71
|
+
if out.json_mode:
|
|
72
|
+
payload = dict(index)
|
|
73
|
+
private = _private_view(ctx)
|
|
74
|
+
if private:
|
|
75
|
+
payload["with_your_token"] = private
|
|
76
|
+
out.json(payload)
|
|
77
|
+
return EXIT_OK
|
|
78
|
+
|
|
79
|
+
counts = index.get("counts") or {}
|
|
80
|
+
out.out(out.bold("{0}".format(client.url)))
|
|
81
|
+
out.out(out.dim("{0} public portal(s), {1} public layer(s)".format(
|
|
82
|
+
counts.get("portals", 0),
|
|
83
|
+
sum(v for k, v in counts.items() if k != "portals"))))
|
|
84
|
+
|
|
85
|
+
portals = index.get("portals") or []
|
|
86
|
+
if portals:
|
|
87
|
+
out.out("")
|
|
88
|
+
out.out(out.bold("Portals"))
|
|
89
|
+
out.table([{"slug": p.get("slug"), "title": p.get("title"),
|
|
90
|
+
"experience": p.get("experience"), "layers": p.get("layer_count"),
|
|
91
|
+
"url": p.get("url")} for p in portals],
|
|
92
|
+
["slug", "title", "experience", "layers", "url"])
|
|
93
|
+
|
|
94
|
+
layers = index.get("layers") or {}
|
|
95
|
+
for kind in ("postgis", "geoparquet", "raster"):
|
|
96
|
+
rows = layers.get(kind) or []
|
|
97
|
+
if not rows or (args.kind and args.kind != kind):
|
|
98
|
+
continue
|
|
99
|
+
out.out("")
|
|
100
|
+
out.out(out.bold("{0} — {1}".format(KIND_LABEL[kind], len(rows))))
|
|
101
|
+
out.table([{"id": r.get("id"), "name": r.get("name"),
|
|
102
|
+
"geometry": r.get("geometry_type") or ("{0} band(s)".format(r.get("band_count"))
|
|
103
|
+
if r.get("band_count") else None),
|
|
104
|
+
"features": r.get("feature_count"), "crs": r.get("crs"),
|
|
105
|
+
"licence": r.get("license")} for r in rows],
|
|
106
|
+
["id", "name", "geometry", "features", "crs", "licence"])
|
|
107
|
+
if args.links:
|
|
108
|
+
for row in rows:
|
|
109
|
+
out.out("")
|
|
110
|
+
out.out(" " + out.bold(row.get("name") or "?"))
|
|
111
|
+
for link in row.get("links") or []:
|
|
112
|
+
out.out(" {0} {1}".format(link.get("label"), link.get("url")))
|
|
113
|
+
|
|
114
|
+
catalogs = index.get("catalogs") or {}
|
|
115
|
+
out.out("")
|
|
116
|
+
out.info("Standards: OGC API - Features {0} · STAC {1}".format(
|
|
117
|
+
catalogs.get("ogc_features"), catalogs.get("stac")))
|
|
118
|
+
|
|
119
|
+
private = _private_view(ctx)
|
|
120
|
+
if private:
|
|
121
|
+
out.info("With your token: {0} layer(s) and {1} portal(s) in total — "
|
|
122
|
+
"`geodeploy layers list` shows them.".format(private["layers"], private["portals"]))
|
|
123
|
+
elif not ctx.resolved.token:
|
|
124
|
+
out.info("No token in use: this is the anonymous view. `geodeploy login <url> --token …` "
|
|
125
|
+
"to see everything you have access to.")
|
|
126
|
+
return EXIT_OK
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _private_view(ctx):
|
|
130
|
+
"""What the caller's own credential can see, when there is one. Best-effort by design:
|
|
131
|
+
browsing must not fail because a token expired."""
|
|
132
|
+
if not (ctx.resolved.token or ctx.resolved.jwt):
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
client = ctx.client(auth_required=False)
|
|
136
|
+
return {"layers": len(client.layers.list()), "portals": len(client.portals.list())}
|
|
137
|
+
except GeoDeployError:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _one_portal(ctx, client, args) -> int:
|
|
142
|
+
"""Read a published portal's own style.json — the whole portal, anonymously."""
|
|
143
|
+
out = ctx.out
|
|
144
|
+
slug = args.portal
|
|
145
|
+
style_url = "{0}/portals/{1}/style.json".format(client.url, slug)
|
|
146
|
+
try:
|
|
147
|
+
style = client.catalog.portal_style(style_url)
|
|
148
|
+
except GeoDeployError as exc:
|
|
149
|
+
out.error("Could not read portal {0!r}: {1}".format(slug, exc),
|
|
150
|
+
hint="Public portals only — a password or members-only portal will not open "
|
|
151
|
+
"without a session.")
|
|
152
|
+
return EXIT_GENERIC
|
|
153
|
+
|
|
154
|
+
meta = style.get("geodeploy") or {}
|
|
155
|
+
if out.json_mode:
|
|
156
|
+
out.json(style)
|
|
157
|
+
return EXIT_OK
|
|
158
|
+
|
|
159
|
+
out.out(out.bold(meta.get("title") or slug))
|
|
160
|
+
out.out(out.dim("{0}/portals/{1}/".format(client.url, slug)))
|
|
161
|
+
out.out("")
|
|
162
|
+
|
|
163
|
+
sources = style.get("sources") or {}
|
|
164
|
+
rows, urls = [], {}
|
|
165
|
+
for layer in _data_layers(style):
|
|
166
|
+
lmeta = layer.get("metadata") or {}
|
|
167
|
+
source = layer.get("source")
|
|
168
|
+
row = {"name": lmeta.get("geodeploy:name") or layer.get("id"),
|
|
169
|
+
"kind": lmeta.get("geodeploy:geometry") or layer.get("type"),
|
|
170
|
+
"id": layer.get("id"),
|
|
171
|
+
"visible": "no" if _hidden(layer) else "yes"}
|
|
172
|
+
rows.append(row)
|
|
173
|
+
urls[row["id"]] = _source_url(sources.get(source) or {})
|
|
174
|
+
for deck in meta.get("deckLayers") or []:
|
|
175
|
+
ident = deck.get("id") or deck.get("name")
|
|
176
|
+
rows.append({"name": deck.get("name") or ident, "kind": "GeoParquet (deck.gl)",
|
|
177
|
+
"id": ident, "visible": "no" if deck.get("visible") is False else "yes"})
|
|
178
|
+
urls[ident] = deck.get("url") or deck.get("manifest") or ""
|
|
179
|
+
|
|
180
|
+
columns = ["name", "kind", "id"]
|
|
181
|
+
if any(r["visible"] == "no" for r in rows):
|
|
182
|
+
columns.append("visible") # only worth a column when something is actually off
|
|
183
|
+
out.table(rows, columns)
|
|
184
|
+
|
|
185
|
+
if args.links:
|
|
186
|
+
out.out("")
|
|
187
|
+
for row in rows:
|
|
188
|
+
if urls.get(row["id"]):
|
|
189
|
+
out.out(" {0} {1}".format(out.bold(row["name"]), urls[row["id"]]))
|
|
190
|
+
if meta.get("bounds"):
|
|
191
|
+
out.info("Extent: {0}".format(meta["bounds"]))
|
|
192
|
+
if not args.links:
|
|
193
|
+
out.info("`--links` adds the data URL behind each layer.")
|
|
194
|
+
return EXIT_OK
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _data_layers(style):
|
|
198
|
+
"""The author's layers, without the basemap's and without counting one layer twice.
|
|
199
|
+
|
|
200
|
+
A layer can be drawn by several MapLibre layers (a fill plus its outline, a polygon plus its
|
|
201
|
+
3D pillars); the generator flags the extras `geodeploy:part` and puts the metadata on the
|
|
202
|
+
first, which is exactly the rule the portal's own layer switcher follows.
|
|
203
|
+
"""
|
|
204
|
+
layers = style.get("layers") or []
|
|
205
|
+
tagged = [l for l in layers
|
|
206
|
+
if (l.get("metadata") or {}).get("geodeploy:layer_id") is not None
|
|
207
|
+
and not (l.get("metadata") or {}).get("geodeploy:part")]
|
|
208
|
+
if tagged:
|
|
209
|
+
return tagged
|
|
210
|
+
# An older bundle, published before the metadata existed: fall back to "not the basemap".
|
|
211
|
+
return [l for l in layers
|
|
212
|
+
if l.get("source") and l.get("source") not in ("basemap", "composite")]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _hidden(layer):
|
|
216
|
+
return ((layer.get("layout") or {}).get("visibility")) == "none"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _source_url(source):
|
|
220
|
+
"""Where a MapLibre source actually reads from — a tile template, a TileJSON or inline data."""
|
|
221
|
+
tiles = source.get("tiles")
|
|
222
|
+
if tiles:
|
|
223
|
+
return tiles[0]
|
|
224
|
+
return source.get("url") or (source.get("data") if isinstance(source.get("data"), str) else "")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""`geodeploy catalog …` — the public surfaces: OGC API - Features, STAC, templates, basemaps.
|
|
2
|
+
|
|
3
|
+
These need no credentials, which makes this group the honest way to check what you have actually
|
|
4
|
+
published: if a layer is not listed here, nobody outside your organisation can see it either.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from ..main import add_command, group_parser
|
|
11
|
+
from ..output import EXIT_OK
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register(subparsers) -> None:
|
|
15
|
+
group = group_parser(subparsers, "catalog",
|
|
16
|
+
"what this instance publishes: OGC API - Features, STAC, templates")
|
|
17
|
+
|
|
18
|
+
add_command(group, "collections", cmd_collections,
|
|
19
|
+
"OGC API - Features collections (one per public vector layer)")
|
|
20
|
+
|
|
21
|
+
items = add_command(group, "items", cmd_items, "features from a collection")
|
|
22
|
+
items.add_argument("collection", help="collection id, e.g. vector-a1b2c3d4e5f6")
|
|
23
|
+
items.add_argument("--bbox", help="minx,miny,maxx,maxy")
|
|
24
|
+
items.add_argument("--limit", type=int, default=10)
|
|
25
|
+
items.add_argument("--offset", type=int, default=0)
|
|
26
|
+
|
|
27
|
+
add_command(group, "conformance", cmd_conformance,
|
|
28
|
+
"what the OGC endpoint claims to support")
|
|
29
|
+
|
|
30
|
+
stac = add_command(group, "stac", cmd_stac, "the STAC catalog root and its collections")
|
|
31
|
+
stac.add_argument("--collection", help="list items in this collection (vectors | rasters)")
|
|
32
|
+
stac.add_argument("--limit", type=int, default=20)
|
|
33
|
+
|
|
34
|
+
search = add_command(group, "search", cmd_search, "STAC search across public layers")
|
|
35
|
+
search.add_argument("--bbox")
|
|
36
|
+
search.add_argument("--collections")
|
|
37
|
+
search.add_argument("--datetime", dest="datetime_", help="instant or start/end (.. is open)")
|
|
38
|
+
search.add_argument("--ids")
|
|
39
|
+
search.add_argument("--limit", type=int, default=20)
|
|
40
|
+
|
|
41
|
+
add_command(group, "templates", cmd_templates, "portal templates and the experiences they suit")
|
|
42
|
+
add_command(group, "basemaps", cmd_basemaps, "basemaps available to portals")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _client(ctx):
|
|
46
|
+
# Public endpoints: a URL is enough, no credential required.
|
|
47
|
+
return ctx.client(auth_required=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_collections(ctx, args) -> int:
|
|
51
|
+
rows = _client(ctx).catalog.collections()
|
|
52
|
+
ctx.out.render([{"id": c.get("id"), "title": c.get("title"),
|
|
53
|
+
"extent": ((c.get("extent") or {}).get("spatial") or {}).get("bbox")}
|
|
54
|
+
for c in rows], ["id", "title", "extent"],
|
|
55
|
+
empty="No public collections — share a vector layer publicly to add one.")
|
|
56
|
+
return EXIT_OK
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_items(ctx, args) -> int:
|
|
60
|
+
data = _client(ctx).catalog.items(args.collection, bbox=args.bbox, limit=args.limit,
|
|
61
|
+
offset=args.offset)
|
|
62
|
+
if ctx.out.json_mode:
|
|
63
|
+
ctx.out.json(data)
|
|
64
|
+
return EXIT_OK
|
|
65
|
+
ctx.out.info("{0} returned, {1} matched.".format(data.get("numberReturned"),
|
|
66
|
+
data.get("numberMatched", "unknown")))
|
|
67
|
+
ctx.out.out(json.dumps(data, indent=2, ensure_ascii=False)[:20000])
|
|
68
|
+
return EXIT_OK
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_conformance(ctx, args) -> int:
|
|
72
|
+
ctx.out.render((_client(ctx).catalog.conformance() or {}).get("conformsTo") or [])
|
|
73
|
+
return EXIT_OK
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_stac(ctx, args) -> int:
|
|
77
|
+
catalog = _client(ctx).catalog
|
|
78
|
+
if args.collection:
|
|
79
|
+
ctx.out.render(catalog.stac_items(args.collection, limit=args.limit))
|
|
80
|
+
return EXIT_OK
|
|
81
|
+
ctx.out.render([{"id": c.get("id"), "title": c.get("title"), "description": c.get("description")}
|
|
82
|
+
for c in catalog.stac_collections()], ["id", "title", "description"])
|
|
83
|
+
return EXIT_OK
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmd_search(ctx, args) -> int:
|
|
87
|
+
data = _client(ctx).catalog.search(bbox=args.bbox, collections=args.collections,
|
|
88
|
+
datetime=args.datetime_, ids=args.ids, limit=args.limit)
|
|
89
|
+
if ctx.out.json_mode:
|
|
90
|
+
ctx.out.json(data)
|
|
91
|
+
return EXIT_OK
|
|
92
|
+
features = data.get("features") or []
|
|
93
|
+
ctx.out.table([{"id": f.get("id"), "collection": f.get("collection"),
|
|
94
|
+
"assets": ", ".join(sorted((f.get("assets") or {}).keys()))}
|
|
95
|
+
for f in features], ["id", "collection", "assets"])
|
|
96
|
+
return EXIT_OK
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cmd_templates(ctx, args) -> int:
|
|
100
|
+
ctx.out.render(_client(ctx).catalog.templates(),
|
|
101
|
+
["id", "name", "archetypes", "archetype", "tags", "is_official", "version"])
|
|
102
|
+
return EXIT_OK
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cmd_basemaps(ctx, args) -> int:
|
|
106
|
+
ctx.out.render(_client(ctx).catalog.basemaps(), ["id", "name", "type", "attribution"])
|
|
107
|
+
return EXIT_OK
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""`geodeploy import …` — register data that is already in the database or the bucket.
|
|
2
|
+
|
|
3
|
+
Nothing is copied and nothing is moved: a PostGIS table is introspected and registered where it
|
|
4
|
+
stands, and an object in storage is attached by key. This is how you point a fresh instance at
|
|
5
|
+
data that is already on the server, without a round trip through your laptop.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
|
+
|
|
11
|
+
from ..main import add_command, group_parser
|
|
12
|
+
from ..output import EXIT_GENERIC, EXIT_OK
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register(subparsers) -> None:
|
|
16
|
+
group = group_parser(subparsers, "import", "register existing PostGIS tables or storage objects",
|
|
17
|
+
aliases=["discover"])
|
|
18
|
+
|
|
19
|
+
db_list = add_command(group, "db-list", cmd_db_list,
|
|
20
|
+
"spatial tables in the database, and whether they are registered")
|
|
21
|
+
db_list.add_argument("--all", action="store_true", help="include already-imported tables")
|
|
22
|
+
|
|
23
|
+
db_add = add_command(group, "db-add", cmd_db_add, "register PostGIS tables as layers",
|
|
24
|
+
epilog="examples:\n geodeploy import db-add public.roads public.sites\n")
|
|
25
|
+
db_add.add_argument("tables", nargs="+", help="schema.table (as listed by db-list)")
|
|
26
|
+
db_add.add_argument("--name", help="display name (single table only)")
|
|
27
|
+
|
|
28
|
+
st_list = add_command(group, "storage-list", cmd_storage_list,
|
|
29
|
+
"GeoTIFF / GeoParquet / CSV objects in the bucket")
|
|
30
|
+
st_list.add_argument("--kind", choices=["raster", "geoparquet", "csv"])
|
|
31
|
+
|
|
32
|
+
st_add = add_command(group, "storage-add", cmd_storage_add,
|
|
33
|
+
"attach storage objects as layers (GeoParquet is inspected + prepared)")
|
|
34
|
+
st_add.add_argument("keys", nargs="+", help="object keys as listed by storage-list")
|
|
35
|
+
st_add.add_argument("--name", help="display name (single object only)")
|
|
36
|
+
st_add.add_argument("--wait", action="store_true", help="wait for any queued jobs")
|
|
37
|
+
|
|
38
|
+
csv_cols = add_command(group, "csv-columns", cmd_csv_columns, "header of a CSV in the bucket")
|
|
39
|
+
csv_cols.add_argument("key")
|
|
40
|
+
csv_cols.add_argument("--delimiter", default="comma",
|
|
41
|
+
choices=["comma", "semicolon", "tab", "pipe", "space"])
|
|
42
|
+
|
|
43
|
+
csv_add = add_command(group, "csv", cmd_csv, "build a PostGIS layer from a CSV in the bucket")
|
|
44
|
+
csv_add.add_argument("key")
|
|
45
|
+
csv_add.add_argument("--name")
|
|
46
|
+
csv_add.add_argument("--x", dest="x_column")
|
|
47
|
+
csv_add.add_argument("--y", dest="y_column")
|
|
48
|
+
csv_add.add_argument("--wkt", dest="wkt_column")
|
|
49
|
+
csv_add.add_argument("--srid", type=int, default=4326)
|
|
50
|
+
csv_add.add_argument("--delimiter", default="comma",
|
|
51
|
+
choices=["comma", "semicolon", "tab", "pipe", "space"])
|
|
52
|
+
csv_add.add_argument("--wait", action="store_true")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_db_list(ctx, args) -> int:
|
|
56
|
+
rows = ctx.client().imports.database_tables()
|
|
57
|
+
if not args.all:
|
|
58
|
+
rows = [r for r in rows if not r.get("imported")]
|
|
59
|
+
ctx.out.render(rows, ["schema_name", "table_name", "geometry_column", "geometry_type", "srid",
|
|
60
|
+
"imported"],
|
|
61
|
+
empty="No spatial tables found (or all of them are already registered).")
|
|
62
|
+
return EXIT_OK
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_db_add(ctx, args) -> int:
|
|
66
|
+
available = {"{0}.{1}".format(r.get("schema_name"), r.get("table_name")): r
|
|
67
|
+
for r in ctx.client().imports.database_tables()}
|
|
68
|
+
tables = [] # type: List[Dict[str, Any]]
|
|
69
|
+
for ref in args.tables:
|
|
70
|
+
found = available.get(ref)
|
|
71
|
+
if not found:
|
|
72
|
+
ctx.out.error("No spatial table {0!r}. `geodeploy import db-list` shows what there is."
|
|
73
|
+
.format(ref))
|
|
74
|
+
return EXIT_GENERIC
|
|
75
|
+
entry = {"schema_name": found["schema_name"], "table_name": found["table_name"],
|
|
76
|
+
"geometry_column": found.get("geometry_column"),
|
|
77
|
+
"srid": found.get("srid") or 0,
|
|
78
|
+
"geometry_type": found.get("geometry_type")}
|
|
79
|
+
if args.name and len(args.tables) == 1:
|
|
80
|
+
entry["name"] = args.name
|
|
81
|
+
tables.append(entry)
|
|
82
|
+
result = ctx.client().imports.database(tables)
|
|
83
|
+
ctx.out.render(result)
|
|
84
|
+
if not ctx.out.json_mode:
|
|
85
|
+
ctx.out.success("Registered {0} table(s).".format(len(tables)))
|
|
86
|
+
return EXIT_OK
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_storage_list(ctx, args) -> int:
|
|
90
|
+
ctx.out.render(ctx.client().imports.storage_objects(args.kind),
|
|
91
|
+
["key", "kind", "size", "imported"], empty="Nothing importable in the bucket.")
|
|
92
|
+
return EXIT_OK
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_storage_add(ctx, args) -> int:
|
|
96
|
+
items = [{"key": key} for key in args.keys]
|
|
97
|
+
if args.name and len(items) == 1:
|
|
98
|
+
items[0]["name"] = args.name
|
|
99
|
+
result = ctx.client().imports.storage(items)
|
|
100
|
+
jobs = (result or {}).get("jobs") or []
|
|
101
|
+
ctx.out.render(result)
|
|
102
|
+
if args.wait and jobs:
|
|
103
|
+
for job in jobs:
|
|
104
|
+
ctx.client().jobs.wait(job.get("id"), "vector",
|
|
105
|
+
on_progress=lambda st: ctx.out.info(" {0:3d}% {1}".format(
|
|
106
|
+
st.get("progress") or 0, st.get("current_step") or "")))
|
|
107
|
+
ctx.out.success("All queued imports finished.")
|
|
108
|
+
return EXIT_OK
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_csv_columns(ctx, args) -> int:
|
|
112
|
+
ctx.out.render(ctx.client().imports.csv_columns(args.key, args.delimiter))
|
|
113
|
+
return EXIT_OK
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_csv(ctx, args) -> int:
|
|
117
|
+
job = ctx.client().imports.csv(args.key, name=args.name, x_column=args.x_column,
|
|
118
|
+
y_column=args.y_column, wkt_column=args.wkt_column,
|
|
119
|
+
srid=args.srid, delimiter=args.delimiter)
|
|
120
|
+
if args.wait and job.get("id"):
|
|
121
|
+
job = ctx.client().jobs.wait(job["id"], "vector",
|
|
122
|
+
on_progress=lambda st: ctx.out.info(" {0:3d}% {1}".format(
|
|
123
|
+
st.get("progress") or 0, st.get("current_step") or "")))
|
|
124
|
+
ctx.out.render(job)
|
|
125
|
+
return EXIT_OK
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""`geodeploy jobs …` — ingest jobs, and waiting for one."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from ..main import add_command, group_parser
|
|
5
|
+
from ..output import EXIT_OK
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register(subparsers) -> None:
|
|
9
|
+
group = group_parser(subparsers, "jobs", "ingest job status")
|
|
10
|
+
|
|
11
|
+
show = add_command(group, "show", cmd_show, "the current state of a job")
|
|
12
|
+
show.add_argument("job_id")
|
|
13
|
+
show.add_argument("--type", dest="layer_type", choices=["vector", "raster"], default="vector")
|
|
14
|
+
|
|
15
|
+
watch = add_command(group, "watch", cmd_watch, "follow a job until it finishes")
|
|
16
|
+
watch.add_argument("job_id")
|
|
17
|
+
watch.add_argument("--type", dest="layer_type", choices=["vector", "raster"], default="vector")
|
|
18
|
+
watch.add_argument("--interval", type=float, default=2.0)
|
|
19
|
+
watch.add_argument("--max-wait", dest="max_wait", type=float, default=3600.0,
|
|
20
|
+
help="give up after this many seconds (the job keeps running server-side)")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cmd_show(ctx, args) -> int:
|
|
24
|
+
ctx.out.render(ctx.client().jobs.get(args.job_id, args.layer_type),
|
|
25
|
+
["id", "layer_id", "layer_type", "status", "progress", "current_step",
|
|
26
|
+
"error_message"])
|
|
27
|
+
return EXIT_OK
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cmd_watch(ctx, args) -> int:
|
|
31
|
+
final = ctx.client().jobs.wait(
|
|
32
|
+
args.job_id, args.layer_type, interval=args.interval, timeout=args.max_wait,
|
|
33
|
+
on_progress=lambda st: ctx.out.info(" {0:3d}% {1}".format(
|
|
34
|
+
st.get("progress") or 0, st.get("current_step") or st.get("status") or "")))
|
|
35
|
+
ctx.out.render(final, ["id", "layer_id", "layer_type", "status", "progress", "current_step"])
|
|
36
|
+
if not ctx.out.json_mode:
|
|
37
|
+
ctx.out.success("Job {0} finished.".format(args.job_id))
|
|
38
|
+
return EXIT_OK
|