parseo 0.4.4__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.
Files changed (70) hide show
  1. parseo/__init__.py +53 -0
  2. parseo/_epsg_lookup.py +145 -0
  3. parseo/_field_mappings.py +219 -0
  4. parseo/_json.py +14 -0
  5. parseo/_tile_systems.py +51 -0
  6. parseo/assembler.py +225 -0
  7. parseo/cli.py +383 -0
  8. parseo/parser.py +784 -0
  9. parseo/schema_registry.py +246 -0
  10. parseo/schemas/copernicus/clms/clc/clc_filename_v1_0_0.json +48 -0
  11. parseo/schemas/copernicus/clms/clc/clc_filename_v1_1_0.json +65 -0
  12. parseo/schemas/copernicus/clms/clcplus/ras/clcplus_filename_v0_0_0.json +90 -0
  13. parseo/schemas/copernicus/clms/clcplus/ras/clcplus_filename_v0_0_1.json +80 -0
  14. parseo/schemas/copernicus/clms/egms/egms_gnss_model_filename_v0_0_0.json +46 -0
  15. parseo/schemas/copernicus/clms/egms/egms_l2a_product_filename_v0_0_0.json +71 -0
  16. parseo/schemas/copernicus/clms/egms/egms_l3_velocity_grid_filename_v0_0_0.json +64 -0
  17. parseo/schemas/copernicus/clms/euhydro/euhydro_filename_v0_0_0.json +115 -0
  18. parseo/schemas/copernicus/clms/euhydro/euhydro_filename_v0_0_1.json +103 -0
  19. parseo/schemas/copernicus/clms/hr-vpp/st_filename_v0_0_0.json +82 -0
  20. parseo/schemas/copernicus/clms/hr-vpp/vi_filename_v0_0_0.json +89 -0
  21. parseo/schemas/copernicus/clms/hr-vpp/vpp_filename_v0_0_0.json +109 -0
  22. parseo/schemas/copernicus/clms/hr-wsi/cc_filename_v0_0_0.json +84 -0
  23. parseo/schemas/copernicus/clms/hr-wsi/fsc_filename_v0_0_0.json +88 -0
  24. parseo/schemas/copernicus/clms/hr-wsi/gfsc_filename_v0_0_0.json +83 -0
  25. parseo/schemas/copernicus/clms/hr-wsi/icd_filename_v0_0_0.json +96 -0
  26. parseo/schemas/copernicus/clms/hr-wsi/sp_filename_v0_0_0.json +177 -0
  27. parseo/schemas/copernicus/clms/hr-wsi/sws_filename_v0_0_0.json +83 -0
  28. parseo/schemas/copernicus/clms/hr-wsi/wcd_filename_v0_0_0.json +96 -0
  29. parseo/schemas/copernicus/clms/hr-wsi/wds_filename_v0_0_0.json +83 -0
  30. parseo/schemas/copernicus/clms/hr-wsi/wic_comb_filename_v0_0_0.json +82 -0
  31. parseo/schemas/copernicus/clms/hr-wsi/wic_filename_v0_0_0.json +90 -0
  32. parseo/schemas/copernicus/clms/hrl/fty_filename_v0_0_0.json +48 -0
  33. parseo/schemas/copernicus/clms/hrl/gra_filename_v0_0_0.json +48 -0
  34. parseo/schemas/copernicus/clms/hrl/ibu_filename_v0_0_0.json +57 -0
  35. parseo/schemas/copernicus/clms/hrl/imp_filename_v0_0_0.json +48 -0
  36. parseo/schemas/copernicus/clms/hrl/nvlcc_filename_v0_0_0.json +73 -0
  37. parseo/schemas/copernicus/clms/hrl/nvlcc_filename_v0_0_1.json +89 -0
  38. parseo/schemas/copernicus/clms/hrl/nvlcc_filename_v0_0_2.json +89 -0
  39. parseo/schemas/copernicus/clms/hrl/swf_filename_v0_0_0.json +48 -0
  40. parseo/schemas/copernicus/clms/hrl/swf_filename_v0_0_1.json +49 -0
  41. parseo/schemas/copernicus/clms/hrl/swf_filename_v0_0_2.json +93 -0
  42. parseo/schemas/copernicus/clms/hrl/swf_filename_v0_0_3.json +99 -0
  43. parseo/schemas/copernicus/clms/hrl/tcd_filename_v0_0_0.json +48 -0
  44. parseo/schemas/copernicus/clms/hrl/vlcc_filename_v0_0_0.json +105 -0
  45. parseo/schemas/copernicus/clms/hrl/waw_filename_v0_0_0.json +48 -0
  46. parseo/schemas/copernicus/clms/n2k/n2k_filename_v1_0_0.json +43 -0
  47. parseo/schemas/copernicus/clms/pa/pa_filename_v1_0_0.json +99 -0
  48. parseo/schemas/copernicus/clms/riparian-zones/rpz_filename_v0_0_0.json +50 -0
  49. parseo/schemas/copernicus/clms/urban-atlas/ua_dhm_filename_v0_0_0.json +53 -0
  50. parseo/schemas/copernicus/clms/urban-atlas/ua_filename_v0_0_0.json +105 -0
  51. parseo/schemas/copernicus/clms/urban-atlas/ua_stl_filename_v0_0_0.json +89 -0
  52. parseo/schemas/copernicus/sentinel/s1_filename_v1_0_0.json +78 -0
  53. parseo/schemas/copernicus/sentinel/s2_filename_v1_0_0.json +62 -0
  54. parseo/schemas/copernicus/sentinel/s3_filename_v1_0_0.json +22 -0
  55. parseo/schemas/copernicus/sentinel/s4_filename_v1_0_0.json +21 -0
  56. parseo/schemas/copernicus/sentinel/s5p_filename_v1_0_0.json +21 -0
  57. parseo/schemas/copernicus/sentinel/s6_filename_v1_0_0.json +23 -0
  58. parseo/schemas/eumetsat/metop_filename_v1_0_0.json +68 -0
  59. parseo/schemas/eumetsat/mtg_filename_v1_0_0.json +65 -0
  60. parseo/schemas/nasa/modis_filename_v1_0_0.json +54 -0
  61. parseo/schemas/usgs/landsat/landsat_filename_v1_0_0.json +50 -0
  62. parseo/stac_http.py +267 -0
  63. parseo/stac_scraper.py +115 -0
  64. parseo/template.py +78 -0
  65. parseo-0.4.4.dist-info/METADATA +402 -0
  66. parseo-0.4.4.dist-info/RECORD +70 -0
  67. parseo-0.4.4.dist-info/WHEEL +5 -0
  68. parseo-0.4.4.dist-info/entry_points.txt +2 -0
  69. parseo-0.4.4.dist-info/licenses/LICENSE.txt +287 -0
  70. parseo-0.4.4.dist-info/top_level.txt +1 -0
parseo/cli.py ADDED
@@ -0,0 +1,383 @@
1
+ # src/parseo/cli.py
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from typing import Any
8
+ from typing import Dict
9
+ from typing import List
10
+ from typing import Union
11
+
12
+ from parseo import __version__
13
+ from parseo.assembler import assemble
14
+ from parseo.assembler import assemble_auto
15
+ from parseo.parser import ParseError
16
+ from parseo.parser import describe_schema # parser helpers
17
+ from parseo.parser import parse
18
+ from parseo.parser import parse_auto
19
+ from parseo.schema_registry import discover_families
20
+ from parseo.stac_http import StacClientError
21
+ from parseo.stac_http import list_collections_http
22
+ from parseo.stac_http import sample_collection_filenames
23
+
24
+
25
+ # ---------- small utilities ----------
26
+
27
+ def _build_arg_parser() -> argparse.ArgumentParser:
28
+ ap = argparse.ArgumentParser(prog="parseo", description="parsEO CLI")
29
+ ap.add_argument(
30
+ "--version",
31
+ action="version",
32
+ version=f"parseo version {__version__}",
33
+ help="Show the installed parseo version and exit",
34
+ )
35
+ sp = ap.add_subparsers(dest="cmd", required=True)
36
+
37
+ # parse
38
+ p_parse = sp.add_parser("parse", help="Parse a filename")
39
+ p_parse.add_argument("filename")
40
+ p_parse.add_argument(
41
+ "--output",
42
+ help="Write the JSON result to this file instead of stdout. Use '-' for stdout.",
43
+ )
44
+ p_parse.add_argument(
45
+ "--schema",
46
+ help="Path to a schema JSON file to parse with instead of auto-detection.",
47
+ )
48
+ p_parse.add_argument(
49
+ "--family",
50
+ help="Schema family to use for parsing (e.g. 'S2', 'LANDSAT'). When omitted, auto-detection is used.",
51
+ )
52
+ p_parse.add_argument(
53
+ "--ignore-case",
54
+ action="store_true",
55
+ help="Perform case-insensitive matching (default is case-sensitive).",
56
+ )
57
+
58
+ # list-schemas
59
+ p_list = sp.add_parser("list-schemas", help="List available schema families")
60
+ p_list.add_argument(
61
+ "--family",
62
+ help="Only include schemas belonging to this mission family (e.g. 'S2').",
63
+ )
64
+ p_list.add_argument(
65
+ "--status",
66
+ help="Only include schemas whose lifecycle status matches this value (case-insensitive).",
67
+ )
68
+
69
+ # schema-info
70
+ p_info = sp.add_parser("schema-info", help="Show details for a mission family")
71
+ p_info.add_argument("family", help="Mission family name, e.g. 'S2'")
72
+ p_info.add_argument(
73
+ "--version",
74
+ help="Inspect a specific schema version (defaults to the current version).",
75
+ )
76
+
77
+ # stac-sample
78
+ p_stac = sp.add_parser(
79
+ "stac-sample",
80
+ help="Print sample asset filenames from a STAC collection",
81
+ )
82
+ p_stac.add_argument("collection", help="STAC collection ID")
83
+ p_stac.add_argument(
84
+ "--samples", type=int, default=5, help="Number of filenames to list"
85
+ )
86
+ p_stac.add_argument(
87
+ "--stac-url",
88
+ required=True,
89
+ help="Base URL of the STAC API",
90
+ )
91
+ p_stac.add_argument(
92
+ "--asset-role",
93
+ help="Only include assets whose roles contain this value",
94
+ )
95
+
96
+ # list-stac-collections
97
+ p_stac_list = sp.add_parser(
98
+ "list-stac-collections",
99
+ help="List collection IDs available in a STAC API",
100
+ )
101
+ p_stac_list.add_argument(
102
+ "--stac-url",
103
+ required=True,
104
+ help="Base URL of the STAC API",
105
+ )
106
+ p_stac_list.add_argument(
107
+ "--deep",
108
+ action="store_true",
109
+ help="Recursively follow child catalogs to list nested collections",
110
+ )
111
+
112
+ # assemble
113
+ p_asm = sp.add_parser(
114
+ "assemble",
115
+ help=(
116
+ "Assemble a filename from fields. "
117
+ "Provide key=value pairs OR pipe a JSON object to stdin. "
118
+ "Schema is auto-selected using the schema's first compulsory field as defined by the template."
119
+ ),
120
+ )
121
+ p_asm.add_argument(
122
+ "fields",
123
+ nargs="*",
124
+ help="key=value pairs (optional if you pipe a JSON object to stdin).",
125
+ )
126
+ p_asm.add_argument(
127
+ "--fields-json",
128
+ help="JSON string with fields, or '-' to read JSON from stdin.",
129
+ )
130
+ p_asm.add_argument(
131
+ "--family",
132
+ help="Schema family to use when assembling the filename.",
133
+ )
134
+ p_asm.add_argument(
135
+ "--version",
136
+ help="Schema version to use (requires --family).",
137
+ )
138
+
139
+ return ap
140
+
141
+
142
+ def _kv_pairs_to_dict(pairs: List[str]) -> Dict[str, str]:
143
+ out: Dict[str, str] = {}
144
+ for p in pairs:
145
+ if "=" not in p:
146
+ raise SystemExit(f"Invalid field '{p}'. Use key=value.")
147
+ k, v = p.split("=", 1)
148
+ k = k.strip()
149
+ v = v.strip()
150
+ if not k:
151
+ raise SystemExit(f"Invalid field '{p}': empty key.")
152
+ if k in out:
153
+ raise SystemExit(f"Duplicate field '{k}'.")
154
+ out[k] = v
155
+ return out
156
+
157
+
158
+ def _stdin_text() -> str:
159
+ if sys.stdin and not sys.stdin.closed and not sys.stdin.isatty():
160
+ return sys.stdin.read()
161
+ return ""
162
+
163
+
164
+ def _normalize_fields_payload(payload: Any, *, source: str) -> Dict[str, Any]:
165
+ """Return a dictionary of fields from an arbitrary JSON payload."""
166
+ if isinstance(payload, dict):
167
+ if "fields" in payload:
168
+ fields = payload["fields"]
169
+ if not isinstance(fields, dict):
170
+ raise SystemExit(
171
+ f"{source} contains 'fields' but it is not a JSON object."
172
+ )
173
+ return fields
174
+ return payload
175
+ raise SystemExit(f"{source} must decode to a JSON object.")
176
+
177
+
178
+ def _resolve_fields(args) -> Dict[str, Any]:
179
+ """
180
+ Merge sources in priority:
181
+ 1) --fields-json if provided (string or '-')
182
+ 2) JSON from stdin if available and no positional fields
183
+ 3) positional key=value pairs
184
+ """
185
+ # 1) --fields-json
186
+ if args.fields_json:
187
+ if args.fields_json == "-":
188
+ raw = _stdin_text()
189
+ if not raw.strip():
190
+ raise SystemExit("--fields-json '-' set but stdin is empty.")
191
+ try:
192
+ payload = json.loads(raw)
193
+ except json.JSONDecodeError as e:
194
+ raise SystemExit(f"--fields-json '-' is not valid JSON: {e}")
195
+ return _normalize_fields_payload(payload, source="--fields-json '-'")
196
+ else:
197
+ try:
198
+ payload = json.loads(args.fields_json)
199
+ except json.JSONDecodeError as e:
200
+ raise SystemExit(f"--fields-json is not valid JSON: {e}")
201
+ return _normalize_fields_payload(payload, source="--fields-json")
202
+
203
+ # 2) stdin JSON (only if no positional fields were given)
204
+ if not args.fields:
205
+ raw = _stdin_text()
206
+ if raw.strip():
207
+ try:
208
+ payload = json.loads(raw)
209
+ except json.JSONDecodeError as e:
210
+ raise SystemExit(f"Stdin is not valid JSON: {e}")
211
+ return _normalize_fields_payload(payload, source="Stdin")
212
+
213
+ # 3) positional k=v pairs
214
+ if args.fields:
215
+ return _kv_pairs_to_dict(args.fields)
216
+
217
+ raise SystemExit(
218
+ "No fields provided. Supply key=value pairs, "
219
+ "or pass --fields-json, or pipe a JSON object via stdin."
220
+ )
221
+
222
+
223
+ # ---------- main ----------
224
+
225
+ def main(argv: Union[List[str], None] = None) -> int:
226
+ argv = argv if argv is not None else sys.argv[1:]
227
+ ap = _build_arg_parser()
228
+ args = ap.parse_args(argv)
229
+
230
+ if args.cmd == "parse":
231
+ try:
232
+ if args.schema:
233
+ res = parse(args.filename, schema_path=args.schema, ignore_case=args.ignore_case)
234
+ elif args.family:
235
+ res = parse(args.filename, family=args.family, ignore_case=args.ignore_case)
236
+ else:
237
+ res = parse_auto(args.filename, ignore_case=args.ignore_case)
238
+ except ParseError as exc:
239
+ hint = ""
240
+ family = getattr(exc, "match_family", None)
241
+ if family:
242
+ hint = (
243
+ f"\nHint: use `parseo schema-info {family}` to inspect the schema."
244
+ )
245
+ raise SystemExit(f"{exc}{hint}")
246
+ out = {
247
+ "valid": bool(getattr(res, "valid", False)),
248
+ "fields": getattr(res, "fields", None),
249
+ }
250
+ version = getattr(res, "version", None)
251
+ if version:
252
+ out["version"] = version
253
+ status = getattr(res, "status", None)
254
+ if status:
255
+ out["status"] = status
256
+ mfam = getattr(res, "match_family", None)
257
+ if mfam:
258
+ out["match_family"] = mfam
259
+ payload = json.dumps(out, indent=2, ensure_ascii=False)
260
+ if args.output and args.output != "-":
261
+ try:
262
+ with open(args.output, "w", encoding="utf-8") as fh:
263
+ fh.write(f"{payload}\n")
264
+ except OSError as exc:
265
+ raise SystemExit(f"Failed to write to '{args.output}': {exc}") from exc
266
+ else:
267
+ print(payload)
268
+ return 0
269
+
270
+ if args.cmd == "list-schemas":
271
+ rows: list[tuple[str, str, str, str]] = []
272
+ catalog = discover_families()
273
+ status_filter = args.status.lower() if args.status else None
274
+ prefix_filter: str | None = None
275
+ exact_family: str | None = None
276
+ if args.family:
277
+ if ":" in args.family:
278
+ prefix_filter = args.family.lower()
279
+ else:
280
+ fam_upper = args.family.upper()
281
+ if fam_upper in catalog:
282
+ exact_family = fam_upper
283
+ else:
284
+ prefix_filter = args.family.lower()
285
+ for fam in sorted(catalog):
286
+ meta = catalog[fam]
287
+ schema_id = meta.get("schema_id")
288
+ if not isinstance(schema_id, str):
289
+ continue
290
+ if prefix_filter and not schema_id.lower().startswith(prefix_filter):
291
+ continue
292
+ if exact_family and fam != exact_family:
293
+ continue
294
+ versions = meta.get("versions", {})
295
+ if not isinstance(versions, dict):
296
+ continue
297
+ for ver in sorted(versions):
298
+ data = versions[ver]
299
+ status = ""
300
+ path_str = ""
301
+ if isinstance(data, dict):
302
+ status = str(data.get("status") or "")
303
+ path = data.get("path")
304
+ path_str = str(path) if path is not None else ""
305
+ elif isinstance(data, tuple) and len(data) == 2:
306
+ path, status_val = data
307
+ status = status_val or ""
308
+ path_str = str(path)
309
+ if status_filter and status.lower() != status_filter:
310
+ continue
311
+ rows.append((schema_id, ver, status, path_str))
312
+ if rows:
313
+ rows.sort(key=lambda r: (r[0], r[1]))
314
+ headers = ("SCHEMA_ID", "VERSION", "STATUS", "FILE")
315
+ widths = [len(h) for h in headers]
316
+ for row in rows:
317
+ for i in range(3):
318
+ widths[i] = max(widths[i], len(row[i]))
319
+ line_fmt = f"{{:{widths[0]}}} {{:{widths[1]}}} {{:{widths[2]}}} {{}}"
320
+ print(line_fmt.format(*headers))
321
+ for row in rows:
322
+ print(line_fmt.format(*row))
323
+ else:
324
+ if args.family and not status_filter:
325
+ target = "family" if exact_family else "prefix"
326
+ print(f"No schemas found for {target} '{args.family}'.")
327
+ elif args.family and status_filter:
328
+ target = "family" if exact_family else "prefix"
329
+ print(
330
+ f"No schemas found for {target} '{args.family}' with status '{args.status}'."
331
+ )
332
+ elif status_filter:
333
+ print(f"No schemas found with status '{args.status}'.")
334
+ else:
335
+ print("No schemas found.")
336
+ return 0
337
+
338
+ if args.cmd == "schema-info":
339
+ try:
340
+ info = describe_schema(args.family, version=args.version)
341
+ except KeyError as e:
342
+ raise SystemExit(str(e))
343
+ print(json.dumps(info, indent=2, ensure_ascii=False))
344
+ return 0
345
+
346
+ if args.cmd == "list-stac-collections":
347
+ for cid in list_collections_http(base_url=args.stac_url, deep=args.deep):
348
+ print(cid)
349
+ return 0
350
+
351
+ if args.cmd == "stac-sample":
352
+ samples = sample_collection_filenames(
353
+ args.collection,
354
+ args.samples,
355
+ base_url=args.stac_url,
356
+ asset_role=args.asset_role,
357
+ )
358
+ for cid in sorted(samples):
359
+ print(f"{cid}:")
360
+ for fn in samples[cid]:
361
+ print(f" {fn}")
362
+ return 0
363
+
364
+ if args.cmd == "assemble":
365
+ fields = _resolve_fields(args)
366
+ if args.version and not args.family:
367
+ raise SystemExit("--version requires --family to be set.")
368
+ if args.family:
369
+ out = assemble(fields, family=args.family, version=args.version)
370
+ else:
371
+ out = assemble_auto(fields)
372
+ print(out)
373
+ return 0
374
+
375
+ ap.print_help()
376
+ return 1
377
+
378
+
379
+ if __name__ == "__main__":
380
+ try:
381
+ raise SystemExit(main())
382
+ except StacClientError as e:
383
+ raise SystemExit(str(e)) from e