disscube 0.4.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.
Files changed (43) hide show
  1. disscube/__init__.py +19 -0
  2. disscube/catalog/__init__.py +5 -0
  3. disscube/catalog/json_store.py +94 -0
  4. disscube/catalog/protocol.py +22 -0
  5. disscube/catalog/sqlite_store.py +143 -0
  6. disscube/cli.py +144 -0
  7. disscube/client.py +521 -0
  8. disscube/data/bdc_grids/BDC_LG_V2.zip +0 -0
  9. disscube/data/bdc_grids/BDC_MD_V2.zip +0 -0
  10. disscube/data/bdc_grids/BDC_SM_V2.zip +0 -0
  11. disscube/data/bdc_grids/README.md +51 -0
  12. disscube/export.py +168 -0
  13. disscube/models/__init__.py +28 -0
  14. disscube/models/derivation.py +186 -0
  15. disscube/models/grid.py +240 -0
  16. disscube/models/variable.py +129 -0
  17. disscube/operators/__init__.py +20 -0
  18. disscube/operators/base.py +115 -0
  19. disscube/operators/proximity.py +170 -0
  20. disscube/operators/zonal.py +522 -0
  21. disscube/pipeline/__init__.py +41 -0
  22. disscube/pipeline/aggregator.py +152 -0
  23. disscube/pipeline/aligner.py +353 -0
  24. disscube/pipeline/context.py +24 -0
  25. disscube/pipeline/normalizer.py +59 -0
  26. disscube/pipeline/runner.py +693 -0
  27. disscube/pipeline/schema.py +212 -0
  28. disscube/pipeline/writer.py +144 -0
  29. disscube/sources/__init__.py +43 -0
  30. disscube/sources/_categorical.py +89 -0
  31. disscube/sources/_raster.py +303 -0
  32. disscube/sources/bdc.py +286 -0
  33. disscube/sources/classified.py +80 -0
  34. disscube/sources/mapbiomas.py +162 -0
  35. disscube/sources/prodes.py +254 -0
  36. disscube/storage.py +32 -0
  37. disscube/utils.py +150 -0
  38. disscube-0.4.0.dist-info/METADATA +368 -0
  39. disscube-0.4.0.dist-info/RECORD +43 -0
  40. disscube-0.4.0.dist-info/WHEEL +5 -0
  41. disscube-0.4.0.dist-info/entry_points.txt +2 -0
  42. disscube-0.4.0.dist-info/licenses/LICENSE +21 -0
  43. disscube-0.4.0.dist-info/top_level.txt +1 -0
disscube/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ from disscube.client import CubeClient
2
+ from disscube.models import (
3
+ Derivation,
4
+ DerivedVariable,
5
+ GridSpec,
6
+ SpatialDerivation,
7
+ SpatialSource,
8
+ Variable,
9
+ )
10
+
11
+ __all__ = [
12
+ "CubeClient",
13
+ "Derivation",
14
+ "DerivedVariable",
15
+ "GridSpec",
16
+ "SpatialDerivation",
17
+ "SpatialSource",
18
+ "Variable",
19
+ ]
@@ -0,0 +1,5 @@
1
+ from .json_store import JsonCatalogStore
2
+ from .protocol import CatalogStore
3
+ from .sqlite_store import SqliteCatalogStore
4
+
5
+ __all__ = ["CatalogStore", "JsonCatalogStore", "SqliteCatalogStore"]
@@ -0,0 +1,94 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from disscube.models import DerivedVariable, GridSpec, SpatialRelation, SpatialSource
6
+
7
+
8
+ class JsonCatalogStore:
9
+ def __init__(self, path: str | Path):
10
+ self.path = Path(path)
11
+ self._data: dict[str, Any] = {"grids": {}, "sources": {}, "derived": {}, "relations": []}
12
+ if self.path.exists():
13
+ with open(self.path, "r") as f:
14
+ self._data = json.load(f)
15
+ if "relations" not in self._data:
16
+ self._data["relations"] = []
17
+
18
+ def _save(self):
19
+ with open(self.path, "w") as f:
20
+ json.dump(self._data, f, indent=2)
21
+
22
+ def save_grid(self, grid: GridSpec) -> None:
23
+ self._data["grids"][grid.id] = grid.model_dump()
24
+ self._save()
25
+
26
+ def get_grid(self, grid_id: str) -> GridSpec | None:
27
+ data = self._data["grids"].get(grid_id)
28
+ return GridSpec(**data) if data else None
29
+
30
+ def list_grids(self) -> list[GridSpec]:
31
+ return [GridSpec(**g) for g in self._data["grids"].values()]
32
+
33
+ def save_spatial_source(self, source: SpatialSource) -> None:
34
+ self._data["sources"][source.id] = source.model_dump()
35
+ self._save()
36
+
37
+ def get_spatial_source(self, source_id: str) -> SpatialSource | None:
38
+ data = self._data["sources"].get(source_id)
39
+ return SpatialSource(**data) if data else None
40
+
41
+ def list_spatial_sources(self) -> list[SpatialSource]:
42
+ return [SpatialSource(**s) for s in self._data["sources"].values()]
43
+
44
+ def save_derived(self, derived: DerivedVariable) -> None:
45
+ self._data["derived"][derived.id] = derived.model_dump()
46
+ self._save()
47
+
48
+ def delete_derived(self, derived_id: str) -> None:
49
+ self._data["derived"].pop(derived_id, None)
50
+ self._save()
51
+
52
+ def search_derived_variables(self, grid_id: str | None = None, role: str | None = None, tile_id: str | None = None) -> list[DerivedVariable]:
53
+ results = []
54
+ for d in self._data["derived"].values():
55
+ if grid_id and d["grid_id"] != grid_id:
56
+ continue
57
+ if role and d["role"] != role:
58
+ continue
59
+ if tile_id and d.get("tile_id") != tile_id:
60
+ continue
61
+ results.append(DerivedVariable(**d))
62
+ return results
63
+
64
+ def get_derived_by_hash(self, spec_hash: str) -> DerivedVariable | None:
65
+ for d in self._data["derived"].values():
66
+ if d["spec_hash"] == spec_hash:
67
+ return DerivedVariable(**d)
68
+ return None
69
+
70
+ def save_relation(self, relation: SpatialRelation) -> None:
71
+ # Check if already exists to avoid duplicates
72
+ existing = self.get_relation(relation.source_grid_id, relation.target_grid_id)
73
+ if existing:
74
+ # Update
75
+ for i, r in enumerate(self._data["relations"]):
76
+ if r["source_grid_id"] == relation.source_grid_id and r["target_grid_id"] == relation.target_grid_id:
77
+ self._data["relations"][i] = relation.model_dump()
78
+ break
79
+ else:
80
+ self._data["relations"].append(relation.model_dump())
81
+ self._save()
82
+
83
+ def get_relations(self, grid_id: str) -> list[SpatialRelation]:
84
+ """Busca bidirecional: source_grid_id OU target_grid_id."""
85
+ return [
86
+ SpatialRelation(**r) for r in self._data["relations"]
87
+ if r["source_grid_id"] == grid_id or r["target_grid_id"] == grid_id
88
+ ]
89
+
90
+ def get_relation(self, source_id: str, target_id: str) -> SpatialRelation | None:
91
+ for r in self._data["relations"]:
92
+ if r["source_grid_id"] == source_id and r["target_grid_id"] == target_id:
93
+ return SpatialRelation(**r)
94
+ return None
@@ -0,0 +1,22 @@
1
+ from typing import Protocol
2
+
3
+ from disscube.models import DerivedVariable, GridSpec, SpatialRelation, SpatialSource
4
+
5
+
6
+ class CatalogStore(Protocol):
7
+ def save_grid(self, grid: GridSpec) -> None: ...
8
+ def get_grid(self, grid_id: str) -> GridSpec | None: ...
9
+ def list_grids(self) -> list[GridSpec]: ...
10
+
11
+ def save_spatial_source(self, source: SpatialSource) -> None: ...
12
+ def get_spatial_source(self, source_id: str) -> SpatialSource | None: ...
13
+ def list_spatial_sources(self) -> list[SpatialSource]: ...
14
+
15
+ def save_derived(self, derived: DerivedVariable) -> None: ...
16
+ def delete_derived(self, derived_id: str) -> None: ...
17
+ def search_derived_variables(self, grid_id: str | None = None, role: str | None = None, tile_id: str | None = None) -> list[DerivedVariable]: ...
18
+ def get_derived_by_hash(self, spec_hash: str) -> DerivedVariable | None: ...
19
+
20
+ def save_relation(self, relation: SpatialRelation) -> None: ...
21
+ def get_relations(self, grid_id: str) -> list[SpatialRelation]: ...
22
+ def get_relation(self, source_id: str, target_id: str) -> SpatialRelation | None: ...
@@ -0,0 +1,143 @@
1
+ import json
2
+ import sqlite3
3
+ from pathlib import Path
4
+
5
+ from disscube.models import DerivedVariable, GridSpec, SpatialRelation, SpatialSource
6
+
7
+
8
+ class SqliteCatalogStore:
9
+ def __init__(self, path: str | Path):
10
+ self.path = Path(path)
11
+ self._init_db()
12
+
13
+ def _get_connection(self):
14
+ conn = sqlite3.connect(self.path)
15
+ conn.row_factory = sqlite3.Row
16
+ return conn
17
+
18
+ def _init_db(self):
19
+ with self._get_connection() as conn:
20
+ conn.execute("""
21
+ CREATE TABLE IF NOT EXISTS grids (
22
+ id TEXT PRIMARY KEY,
23
+ data TEXT NOT NULL
24
+ )
25
+ """)
26
+ conn.execute("""
27
+ CREATE TABLE IF NOT EXISTS sources (
28
+ id TEXT PRIMARY KEY,
29
+ data TEXT NOT NULL
30
+ )
31
+ """)
32
+ conn.execute("""
33
+ CREATE TABLE IF NOT EXISTS derived (
34
+ id TEXT PRIMARY KEY,
35
+ grid_id TEXT,
36
+ spec_hash TEXT,
37
+ tile_id TEXT,
38
+ role TEXT,
39
+ data TEXT NOT NULL
40
+ )
41
+ """)
42
+ conn.execute("""
43
+ CREATE TABLE IF NOT EXISTS relations (
44
+ source_grid_id TEXT,
45
+ target_grid_id TEXT,
46
+ data TEXT NOT NULL,
47
+ PRIMARY KEY (source_grid_id, target_grid_id)
48
+ )
49
+ """)
50
+ # Indexes for performance
51
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_derived_grid ON derived(grid_id)")
52
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_derived_hash ON derived(spec_hash)")
53
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_derived_tile ON derived(tile_id)")
54
+
55
+ def save_grid(self, grid: GridSpec) -> None:
56
+ with self._get_connection() as conn:
57
+ conn.execute(
58
+ "INSERT OR REPLACE INTO grids (id, data) VALUES (?, ?)",
59
+ (grid.id, grid.model_dump_json())
60
+ )
61
+
62
+ def get_grid(self, grid_id: str) -> GridSpec | None:
63
+ with self._get_connection() as conn:
64
+ row = conn.execute("SELECT data FROM grids WHERE id = ?", (grid_id,)).fetchone()
65
+ return GridSpec(**json.loads(row["data"])) if row else None
66
+
67
+ def list_grids(self) -> list[GridSpec]:
68
+ with self._get_connection() as conn:
69
+ rows = conn.execute("SELECT data FROM grids").fetchall()
70
+ return [GridSpec(**json.loads(r["data"])) for r in rows]
71
+
72
+ def save_spatial_source(self, source: SpatialSource) -> None:
73
+ with self._get_connection() as conn:
74
+ conn.execute(
75
+ "INSERT OR REPLACE INTO sources (id, data) VALUES (?, ?)",
76
+ (source.id, source.model_dump_json())
77
+ )
78
+
79
+ def get_spatial_source(self, source_id: str) -> SpatialSource | None:
80
+ with self._get_connection() as conn:
81
+ row = conn.execute("SELECT data FROM sources WHERE id = ?", (source_id,)).fetchone()
82
+ return SpatialSource(**json.loads(row["data"])) if row else None
83
+
84
+ def list_spatial_sources(self) -> list[SpatialSource]:
85
+ with self._get_connection() as conn:
86
+ rows = conn.execute("SELECT data FROM sources").fetchall()
87
+ return [SpatialSource(**json.loads(r["data"])) for r in rows]
88
+
89
+ def save_derived(self, derived: DerivedVariable) -> None:
90
+ with self._get_connection() as conn:
91
+ conn.execute(
92
+ "INSERT OR REPLACE INTO derived (id, grid_id, spec_hash, tile_id, role, data) VALUES (?, ?, ?, ?, ?, ?)",
93
+ (derived.id, derived.grid_id, derived.spec_hash, derived.tile_id, derived.role, derived.model_dump_json())
94
+ )
95
+
96
+ def delete_derived(self, derived_id: str) -> None:
97
+ with self._get_connection() as conn:
98
+ conn.execute("DELETE FROM derived WHERE id = ?", (derived_id,))
99
+
100
+ def search_derived_variables(self, grid_id: str | None = None, role: str | None = None, tile_id: str | None = None) -> list[DerivedVariable]:
101
+ query = "SELECT data FROM derived WHERE 1=1"
102
+ params = []
103
+ if grid_id:
104
+ query += " AND grid_id = ?"
105
+ params.append(grid_id)
106
+ if role:
107
+ query += " AND role = ?"
108
+ params.append(role)
109
+ if tile_id:
110
+ query += " AND tile_id = ?"
111
+ params.append(tile_id)
112
+
113
+ with self._get_connection() as conn:
114
+ rows = conn.execute(query, params).fetchall()
115
+ return [DerivedVariable(**json.loads(r["data"])) for r in rows]
116
+
117
+ def get_derived_by_hash(self, spec_hash: str) -> DerivedVariable | None:
118
+ with self._get_connection() as conn:
119
+ row = conn.execute("SELECT data FROM derived WHERE spec_hash = ?", (spec_hash,)).fetchone()
120
+ return DerivedVariable(**json.loads(row["data"])) if row else None
121
+
122
+ def save_relation(self, relation: SpatialRelation) -> None:
123
+ with self._get_connection() as conn:
124
+ conn.execute(
125
+ "INSERT OR REPLACE INTO relations (source_grid_id, target_grid_id, data) VALUES (?, ?, ?)",
126
+ (relation.source_grid_id, relation.target_grid_id, relation.model_dump_json())
127
+ )
128
+
129
+ def get_relations(self, grid_id: str) -> list[SpatialRelation]:
130
+ with self._get_connection() as conn:
131
+ rows = conn.execute(
132
+ "SELECT data FROM relations WHERE source_grid_id = ? OR target_grid_id = ?",
133
+ (grid_id, grid_id)
134
+ )
135
+ return [SpatialRelation(**json.loads(r["data"])) for r in rows]
136
+
137
+ def get_relation(self, source_id: str, target_id: str) -> SpatialRelation | None:
138
+ with self._get_connection() as conn:
139
+ row = conn.execute(
140
+ "SELECT data FROM relations WHERE source_grid_id = ? AND target_grid_id = ?",
141
+ (source_id, target_id)
142
+ ).fetchone()
143
+ return SpatialRelation(**json.loads(row["data"])) if row else None
disscube/cli.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Command line: run DisSCube pipeline files.
3
+
4
+ disscube validate pipeline.toml [--json]
5
+ disscube fetch pipeline.toml
6
+ disscube run pipeline.toml [--workspace DIR] [--output OUT.tif] [--dry-run] [--json] [-v]
7
+ disscube export pipeline.toml --output OUT.tif [--workspace DIR] [--variables ...] [--json] [-v]
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import logging
14
+ import sys
15
+
16
+
17
+ def main(argv: list[str] | None = None) -> int:
18
+ parser = argparse.ArgumentParser(prog="disscube", description="Run DisSCube pipeline files.")
19
+ sub = parser.add_subparsers(dest="command", required=True)
20
+
21
+ p_val = sub.add_parser("validate", help="check a pipeline file without fetching anything")
22
+ p_val.add_argument("file")
23
+ p_val.add_argument("--json", action="store_true", help="output validation result as JSON")
24
+
25
+ p_fetch = sub.add_parser("fetch", help="download and verify remote file sources declared in the pipeline")
26
+ p_fetch.add_argument("file")
27
+
28
+ p_run = sub.add_parser("run", help="fetch the sources and derive the variables")
29
+ p_run.add_argument("file")
30
+ p_run.add_argument("--workspace", help="output folder (default: the file's 'workspace', "
31
+ "else a folder named after the file)")
32
+ p_run.add_argument("--output", "-o", help="export derived variables (GeoTIFF; netCDF if the file ends in .nc)")
33
+ p_run.add_argument("--dry-run", action="store_true", help="simulate plan execution without downloading or computing")
34
+ p_run.add_argument("--json", action="store_true", help="output execution report as JSON")
35
+ p_run.add_argument("-v", "--verbose", action="store_true", help="log each step")
36
+
37
+ p_exp = sub.add_parser("export", help="export derived variables from an existing data cube to GeoTIFF or netCDF")
38
+ p_exp.add_argument("file", help="pipeline TOML file")
39
+ p_exp.add_argument("--output", "-o", required=True, help="output file: GeoTIFF, or netCDF if it ends in .nc (e.g. data/cellspace.tif)")
40
+ p_exp.add_argument("--workspace", help="workspace folder (default: data/cube or from pipeline)")
41
+ p_exp.add_argument("--variables", nargs="*", help="specific variables to export (default: all derived)")
42
+ p_exp.add_argument("--json", action="store_true", help="output export result as JSON")
43
+ p_exp.add_argument("-v", "--verbose", action="store_true", help="log each step")
44
+
45
+ args = parser.parse_args(argv)
46
+ from disscube.pipeline import PipelineError, plan, run
47
+
48
+ logging.basicConfig(level=logging.INFO if getattr(args, "verbose", False) else logging.WARNING,
49
+ format="%(levelname)s %(name)s: %(message)s")
50
+ is_json = getattr(args, "json", False)
51
+ try:
52
+ p = plan(args.file)
53
+ if not is_json:
54
+ print(p.summary())
55
+ if args.command == "fetch":
56
+ from disscube.pipeline.runner import _fetch_file_source, _local_file, _raw_cache_dir, _resolve
57
+ from disscube.pipeline.schema import FileSource
58
+ raw = _raw_cache_dir()
59
+ raw.mkdir(parents=True, exist_ok=True)
60
+ fetched = 0
61
+ for s in p.sources:
62
+ if isinstance(s.config, FileSource) and getattr(s.config, 'url', None):
63
+ target = _local_file(_resolve(raw, s.config.path))
64
+ if target is not None:
65
+ print(f"Fetching {s.id} -> {target.name}...")
66
+ _fetch_file_source(s.config, target)
67
+ fetched += 1
68
+ print(f"Fetch completed: {fetched} remote sources verified.")
69
+ return 0
70
+ if args.command == "validate":
71
+ if is_json:
72
+ import json
73
+ print(json.dumps({
74
+ "status": "ok",
75
+ "file": str(p.file.path),
76
+ "name": p.file.config.name,
77
+ "grid": p.grid.name if p.grid else None,
78
+ "sources": [s.id for s in p.sources],
79
+ "derives": [d.target for d in p.derives],
80
+ }, indent=2))
81
+ else:
82
+ print("OK")
83
+ return 0
84
+ if getattr(args, "dry_run", False):
85
+ if is_json:
86
+ import json
87
+ print(json.dumps({
88
+ "status": "ok",
89
+ "dry_run": True,
90
+ "file": str(p.file.path),
91
+ "grid": p.grid.name if p.grid else None,
92
+ "sources": [{"id": s.id, "type": s.config.type} for s in p.sources],
93
+ "derives": [{"target": d.target, "source": d.source, "operator": d.operator} for d in p.derives],
94
+ }, indent=2))
95
+ else:
96
+ print(f"[dry-run] Plan is valid. Would process {len(p.sources)} sources and derive {len(p.derives)} variables.")
97
+ return 0
98
+ if args.command == "export":
99
+ from disscube.pipeline import export_cube
100
+ exp = export_cube(p, output=args.output, workspace=args.workspace, variables=args.variables)
101
+ if is_json:
102
+ import json
103
+ print(json.dumps({
104
+ "status": "ok",
105
+ "workspace": str(exp.workspace),
106
+ "grid_id": exp.grid_id,
107
+ "variables": exp.variables,
108
+ "output": str(exp.output),
109
+ }, indent=2))
110
+ else:
111
+ print(f"workspace : {exp.workspace}")
112
+ print(f"exported : {len(exp.variables)} variables on {exp.grid_id}")
113
+ print(f"output : {exp.output}")
114
+ return 0
115
+ report = run(p, workspace=args.workspace, export_geotiff=getattr(args, "output", None))
116
+ if is_json:
117
+ import json
118
+ print(json.dumps({
119
+ "status": "ok",
120
+ "workspace": str(report.workspace),
121
+ "grid_id": report.grid_id,
122
+ "sources": report.sources,
123
+ "derived": report.derived,
124
+ "exported": str(report.exported) if report.exported else None,
125
+ "record": str(report.record),
126
+ }, indent=2, default=str))
127
+ return 0
128
+ except PipelineError as exc:
129
+ if is_json:
130
+ import json
131
+ print(json.dumps({"status": "error", "error": str(exc)}, indent=2), file=sys.stderr)
132
+ else:
133
+ print(f"error: {exc}", file=sys.stderr)
134
+ return 2
135
+ print(f"workspace : {report.workspace}")
136
+ print(f"derived : {len(report.derived)} products on {report.grid_id}")
137
+ if report.exported:
138
+ print(f"exported : {report.exported}")
139
+ print(f"record : {report.record}")
140
+ return 0
141
+
142
+
143
+ if __name__ == "__main__": # pragma: no cover
144
+ sys.exit(main())