gispulse 2.0.0__tar.gz → 2.2.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.
- {gispulse-2.0.0/src/gispulse.egg-info → gispulse-2.2.0}/PKG-INFO +3 -2
- {gispulse-2.0.0 → gispulse-2.2.0}/pyproject.toml +3 -2
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/__init__.py +11 -3
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/_compat.py +43 -9
- gispulse-2.2.0/src/gispulse/adapters/rest/__init__.py +15 -0
- gispulse-2.2.0/src/gispulse/adapters/rest/rest_table_fetcher.py +364 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/app.py +131 -1
- gispulse-2.2.0/src/gispulse/capabilities/_attribute_sql.py +379 -0
- gispulse-2.2.0/src/gispulse/capabilities/_geometry_sql.py +598 -0
- gispulse-2.2.0/src/gispulse/capabilities/clustering.py +612 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/network.py +288 -85
- gispulse-2.2.0/src/gispulse/capabilities/network_components.py +211 -0
- gispulse-2.2.0/src/gispulse/capabilities/network_graph.py +289 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/network_topology.py +315 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/overlay.py +23 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/registry.py +2 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/schema.py +50 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/selection.py +24 -0
- gispulse-2.2.0/src/gispulse/capabilities/sql_pushdown.py +333 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/temporal.py +31 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/__init__.py +6 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/boundary.py +10 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/calculate.py +14 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/centroid_area.py +11 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/concave_hull.py +14 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/diff.py +19 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/dissolve.py +17 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/nearest.py +15 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/reproject.py +12 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/shape_ops_basic.py +15 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/simplify.py +14 -0
- gispulse-2.2.0/src/gispulse/capabilities/vector/snap_points.py +185 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/spatial_join.py +15 -0
- gispulse-2.2.0/src/gispulse/capabilities/vector/split_lines.py +207 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/union.py +10 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli.py +127 -0
- gispulse-2.2.0/src/gispulse/core/assertions.py +304 -0
- gispulse-2.2.0/src/gispulse/core/bulk_ingest.py +189 -0
- gispulse-2.2.0/src/gispulse/core/bulk_runner.py +1114 -0
- gispulse-2.2.0/src/gispulse/core/dag.py +91 -0
- gispulse-2.2.0/src/gispulse/core/explain.py +377 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/__init__.py +22 -16
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/base.py +23 -1
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/geoparquet_s3.py +29 -14
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/http_file.py +32 -7
- gispulse-2.2.0/src/gispulse/core/fetchers/table_file.py +158 -0
- gispulse-2.2.0/src/gispulse/core/manifest_v3.py +738 -0
- gispulse-2.2.0/src/gispulse/core/network_graph_handle.py +175 -0
- gispulse-2.2.0/src/gispulse/core/pipeline_schema.py +572 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/plugin_model.py +3 -1
- gispulse-2.2.0/src/gispulse/core/spatial_index.py +132 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/graph_executor.py +21 -19
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/__init__.py +11 -0
- gispulse-2.2.0/src/gispulse/persistence/datamart.py +184 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/duckdb_engine.py +70 -0
- gispulse-2.2.0/src/gispulse/persistence/duckdb_relations.py +271 -0
- gispulse-2.2.0/src/gispulse/persistence/geonode.py +287 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/io.py +140 -10
- gispulse-2.2.0/src/gispulse/persistence/loader.py +433 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/sql_dialect.py +131 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/storage.py +16 -4
- gispulse-2.2.0/src/gispulse/runtime/manifest_runner.py +369 -0
- {gispulse-2.0.0 → gispulse-2.2.0/src/gispulse.egg-info}/PKG-INFO +3 -2
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/SOURCES.txt +22 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/requires.txt +2 -1
- gispulse-2.0.0/src/gispulse/adapters/rest/__init__.py +0 -13
- gispulse-2.0.0/src/gispulse/capabilities/clustering.py +0 -306
- gispulse-2.0.0/src/gispulse/core/pipeline_schema.py +0 -298
- {gispulse-2.0.0 → gispulse-2.2.0}/LICENSE +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/LICENSE-COMMERCIAL.md +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/README.md +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/setup.cfg +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/_pyogrio_warnings.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/apicarto.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/action_dispatcher.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/bus_message.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/circuit_breaker.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/dlq.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/enums.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/event_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/pg_notify.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/pool.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/predicate_evaluator.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/state_store.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/trigger_manager.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/workers/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/workers/base_worker.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/workers/dispatch_worker.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/workers/identify_worker.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/app.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/auth.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/dataset_ops.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/dependencies.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/error_handlers.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/event_hub.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/layer_utils.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/middleware/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/middleware/audit_middleware.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/middleware/metrics_middleware.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/middleware/read_only.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/portal_app.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/rate_limit.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/_upload_utils.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/auth_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/capabilities_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/catalog_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/datasets_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/esb_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/examples_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/filter_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/jobs_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/marketplace_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/ogc_features_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/pipelines_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_datasets_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_features_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_sql_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_upload_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/projects_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/relations_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/rules_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/scenarios_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/schedules_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/sessions_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/system_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/templates_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/tiles_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/triggers_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/viewer_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/watchers_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/ws_router.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/schemas.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/serve_app.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/mcp/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/mcp/dryrun.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/mcp/server.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/mcp/workdir.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/metrics.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/auth.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/loader.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/wfs_client.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/wfs_fetcher.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/rest/rest_fetcher.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/stac/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/stac/stac_fetcher.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/webhooks/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/webhooks/http_client.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/base.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/classification.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/density.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/palettes.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/pointcloud.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/polygon_topology.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/postgis_sql.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/raster.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/relation_detector.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/spatial_stats.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/strategy.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/transforms.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/validation.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/aggregate.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/assign_projection.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/buffer.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/chaikin.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/classify.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/clip.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/extract_holes.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/extract_ops.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/filter.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/force_geometry_type.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/intersects.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/line_merge.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/line_ops.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/merge.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/offset_curve.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/parts.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/polygonize.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/shape_ops_advanced.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/snap_grid.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/voronoi.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/data/basemaps.json +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/data/epsg_common.json +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/models.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/base.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/basemaps.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/flux_ign.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/flux_osm.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/opendata_datagouv.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/opendata_hub.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/opendata_ign.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/projections.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/stac_client.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/registry.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/source_bridge.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_mcp.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_portal.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_track.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_triggers.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_triggers_watch.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_watch.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/config.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/cache.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/capability_params.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/conditions.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/config.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/crs.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/data/worldwide_catalog.yml +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/data_pack_signature.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/dispatcher.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/enums.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/ogc_client.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/ogc_features.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/stac.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/cache.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/chain.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/expression.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/expression_converter.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/result.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/service.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/types.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/graph.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/io/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/io/geoparquet.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/licence_format.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/logging.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/models.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/observability.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/pipeline.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/plugin_contracts.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/plugin_hub.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/predicates.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/pricing_catalog.yml +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/registry.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/regulatory_zoning_entry.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/relations.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/session.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/sources.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/sql_safety.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/ssrf.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/zoning_normalizer.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/diagnostics/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/diagnostics/system.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/dsl/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/dsl/expression_parser.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/dsl/geom_fcts.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/capability_executor.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/job_queue.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/job_queue_factory.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/metering.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/pipeline_executor.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/runner.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/scenario_runner.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/scheduler.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/session_manager.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/trigger_bridge.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/worker.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/audit.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/auth_models.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/auth_repository.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/bridge.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/change_log_watcher.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/changelog_doctor.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/changelog_reader.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/duckdb_change_detector.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/duckdb_diff_engine.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/duckdb_engine_adapter.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/engine.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/engine_factory.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/file_blob_cdc.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_connection.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_engine.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_repository.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_schema.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_spatial.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/licence.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/postgis.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/project_io.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/raster_io.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/repository.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/schedule_repository.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/schema.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/session_provisioner.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/sld_converter.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/source_watcher.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/spatial_queries.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/spatialite_engine.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/spatialite_session.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/sql_guardrails.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/sqlite_repository.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/style_converter.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/style_sidecar.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/tier.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/virtual_dataset.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/watcher_registry.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/api.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/datagouv_refresh.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/mcp_pilot.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/pipeline.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/sources.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/spatial.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/worldwide_source.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/engine.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/loader.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/operation_executor.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/predicates.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/trigger_evaluator.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/validation.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/__init__.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/config_loader.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/dialect_scanner.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/duckdb_engine.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/engine_inference.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/headless_runtime.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/layer_registry.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/predicate_dsl.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/source_watch.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/sqlite_retry.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/validation_runner.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/telemetry.py +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/dependency_links.txt +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/entry_points.txt +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/top_level.txt +0 -0
- {gispulse-2.0.0 → gispulse-2.2.0}/tests/test_read_only_middleware.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gispulse
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Modular geospatial engine with business rules, triggers, and dual operating modes (DuckDB/PostGIS)
|
|
5
5
|
Author-email: ImagoData <contact@imagodata.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -34,6 +34,7 @@ Requires-Dist: pydantic<3.0,>=2.0
|
|
|
34
34
|
Requires-Dist: pydantic-settings<3.0,>=2.0
|
|
35
35
|
Requires-Dist: PyYAML<7.0,>=6.0
|
|
36
36
|
Requires-Dist: httpx<1.0,>=0.24
|
|
37
|
+
Requires-Dist: openpyxl<4.0,>=3.1
|
|
37
38
|
Provides-Extra: postgis
|
|
38
39
|
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == "postgis"
|
|
39
40
|
Requires-Dist: geoalchemy2<1.0,>=0.14; extra == "postgis"
|
|
@@ -60,7 +61,7 @@ Requires-Dist: mapclassify<3.0,>=2.5; extra == "classification"
|
|
|
60
61
|
Provides-Extra: pointcloud
|
|
61
62
|
Requires-Dist: laspy<3.0,>=2.5; extra == "pointcloud"
|
|
62
63
|
Provides-Extra: redis
|
|
63
|
-
Requires-Dist: redis<
|
|
64
|
+
Requires-Dist: redis<9.0,>=5.0; extra == "redis"
|
|
64
65
|
Provides-Extra: s3
|
|
65
66
|
Requires-Dist: boto3<2.0,>=1.28; extra == "s3"
|
|
66
67
|
Provides-Extra: scheduling
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gispulse"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Modular geospatial engine with business rules, triggers, and dual operating modes (DuckDB/PostGIS)"
|
|
9
9
|
license = "AGPL-3.0-or-later"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -36,6 +36,7 @@ dependencies = [
|
|
|
36
36
|
"pydantic-settings>=2.0,<3.0",
|
|
37
37
|
"PyYAML>=6.0,<7.0",
|
|
38
38
|
"httpx>=0.24,<1.0",
|
|
39
|
+
"openpyxl>=3.1,<4.0",
|
|
39
40
|
]
|
|
40
41
|
|
|
41
42
|
[project.scripts]
|
|
@@ -61,7 +62,7 @@ raster = ["rasterio>=1.3,<2.0", "rasterstats>=0.19,<1.0"]
|
|
|
61
62
|
network = ["networkx>=3.0,<4.0"]
|
|
62
63
|
classification = ["mapclassify>=2.5,<3.0"]
|
|
63
64
|
pointcloud = ["laspy>=2.5,<3.0"]
|
|
64
|
-
redis = ["redis>=5.0,<
|
|
65
|
+
redis = ["redis>=5.0,<9.0"]
|
|
65
66
|
s3 = ["boto3>=1.28,<2.0"]
|
|
66
67
|
scheduling = ["croniter>=1.3,<7.0"]
|
|
67
68
|
sso = ["PyJWT[crypto]>=2.8,<3.0", "httpx>=0.24,<1.0"]
|
|
@@ -31,10 +31,18 @@ from gispulse import _compat as _compat
|
|
|
31
31
|
|
|
32
32
|
_compat.install()
|
|
33
33
|
|
|
34
|
-
__all__ = [
|
|
34
|
+
__all__ = [
|
|
35
|
+
"__version__",
|
|
36
|
+
"GISPulseApp",
|
|
37
|
+
"get_app",
|
|
38
|
+
"apply",
|
|
39
|
+
"load",
|
|
40
|
+
"publish",
|
|
41
|
+
"run",
|
|
42
|
+
]
|
|
35
43
|
|
|
36
44
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
37
|
-
from gispulse.app import GISPulseApp, apply, get_app, run
|
|
45
|
+
from gispulse.app import GISPulseApp, apply, get_app, load, publish, run
|
|
38
46
|
|
|
39
47
|
|
|
40
48
|
def __getattr__(name: str):
|
|
@@ -43,7 +51,7 @@ def __getattr__(name: str):
|
|
|
43
51
|
Keeps ``import gispulse`` free of heavy imports — ``gispulse.app`` and
|
|
44
52
|
its subsystems are pulled only when the façade is first accessed.
|
|
45
53
|
"""
|
|
46
|
-
if name in {"GISPulseApp", "get_app", "apply", "run"}:
|
|
54
|
+
if name in {"GISPulseApp", "get_app", "apply", "load", "publish", "run"}:
|
|
47
55
|
from gispulse import app as _app
|
|
48
56
|
|
|
49
57
|
return getattr(_app, name)
|
|
@@ -36,17 +36,39 @@ _warned: set[str] = set()
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class _AliasLoader(Loader):
|
|
39
|
-
"""Loader that
|
|
39
|
+
"""Loader that aliases a legacy name to its ``gispulse.*`` module.
|
|
40
|
+
|
|
41
|
+
CRITICAL — class identity (see imagodata/gispulse#333): legacy and
|
|
42
|
+
canonical names MUST point to the *same* module object so that
|
|
43
|
+
``from persistence.storage import StorageError`` yields the same class
|
|
44
|
+
as ``from gispulse.persistence.storage import StorageError``. Without
|
|
45
|
+
this, ``isinstance(err, StorageError)`` and ``pytest.raises(...)``
|
|
46
|
+
silently fail across the namespace boundary.
|
|
47
|
+
|
|
48
|
+
Implementation: ``exec_module`` overwrites ``sys.modules[spec.name]``
|
|
49
|
+
with the canonical module *after* Python's import machinery has placed
|
|
50
|
+
its freshly-created (empty) module there. This is the only point at
|
|
51
|
+
which the override sticks — doing it in ``create_module`` is reverted
|
|
52
|
+
by the import system because the returned module's ``__name__`` is the
|
|
53
|
+
canonical one, not ``spec.name``.
|
|
54
|
+
"""
|
|
40
55
|
|
|
41
56
|
def __init__(self, target: str) -> None:
|
|
42
57
|
self._target = target
|
|
43
58
|
|
|
44
|
-
def create_module(self, spec: importlib.machinery.ModuleSpec) -> ModuleType:
|
|
45
|
-
|
|
59
|
+
def create_module(self, spec: importlib.machinery.ModuleSpec) -> ModuleType | None:
|
|
60
|
+
# Returning None lets Python allocate a default empty module under
|
|
61
|
+
# ``spec.name``; we replace it in ``exec_module``.
|
|
62
|
+
return None
|
|
46
63
|
|
|
47
64
|
def exec_module(self, module: ModuleType) -> None:
|
|
48
|
-
#
|
|
49
|
-
|
|
65
|
+
# Resolve the canonical module (may already be cached) and
|
|
66
|
+
# overwrite the sys.modules entry the import machinery just
|
|
67
|
+
# installed. From this point on, ``import <legacy>`` and
|
|
68
|
+
# ``import gispulse.<legacy>`` resolve to the same object, so
|
|
69
|
+
# class identity holds across both namespaces.
|
|
70
|
+
target = importlib.import_module(self._target)
|
|
71
|
+
sys.modules[module.__name__] = target
|
|
50
72
|
|
|
51
73
|
|
|
52
74
|
class _LegacyTopLevelFinder(MetaPathFinder):
|
|
@@ -76,9 +98,21 @@ class _LegacyTopLevelFinder(MetaPathFinder):
|
|
|
76
98
|
|
|
77
99
|
|
|
78
100
|
def install() -> None:
|
|
79
|
-
"""
|
|
101
|
+
"""Insert the legacy finder at the front of ``sys.meta_path`` (idempotent).
|
|
102
|
+
|
|
103
|
+
The finder MUST run before ``PathFinder``: once we alias
|
|
104
|
+
``sys.modules['persistence'] = gispulse.persistence``, the legacy
|
|
105
|
+
package inherits ``__path__`` from the canonical one. If a standard
|
|
106
|
+
``PathFinder`` reached a sub-import (``persistence.storage``) before
|
|
107
|
+
our shim, it would discover ``gispulse/persistence/storage.py`` via
|
|
108
|
+
the parent's ``__path__`` and execute it a *second* time under the
|
|
109
|
+
legacy name — duplicating every class (StorageError, …) and breaking
|
|
110
|
+
``isinstance()`` checks (imagodata/gispulse#333).
|
|
111
|
+
|
|
112
|
+
Prepending guarantees the alias hook intercepts every legacy import
|
|
113
|
+
before any path-based finder runs, so ``sys.modules`` ends up with
|
|
114
|
+
one shared module object per (root, sub) tuple.
|
|
115
|
+
"""
|
|
80
116
|
if any(isinstance(finder, _LegacyTopLevelFinder) for finder in sys.meta_path):
|
|
81
117
|
return
|
|
82
|
-
|
|
83
|
-
# that no genuine finder could resolve.
|
|
84
|
-
sys.meta_path.append(_LegacyTopLevelFinder())
|
|
118
|
+
sys.meta_path.insert(0, _LegacyTopLevelFinder())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""REST service adapters — GeoJSON and tabular-JSON over HTTP.
|
|
2
|
+
|
|
3
|
+
Importing this package self-registers the core REST transport adapters in
|
|
4
|
+
:data:`core.sources.PROTOCOLS`, so the ETL fetch path has real fetchers to
|
|
5
|
+
dispatch to: ``rest-api`` (GeoJSON FeatureCollection, #192) and
|
|
6
|
+
``rest-table`` (paginated tabular JSON, #196).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
# Side-effect imports: each fetcher registers itself on import.
|
|
12
|
+
from gispulse.adapters.rest import rest_fetcher # noqa: F401
|
|
13
|
+
from gispulse.adapters.rest import rest_table_fetcher # noqa: F401
|
|
14
|
+
|
|
15
|
+
__all__ = ["rest_fetcher", "rest_table_fetcher"]
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Paginated tabular-JSON REST fetcher — ``AccessProtocol.REST_TABLE``.
|
|
2
|
+
|
|
3
|
+
Issue #196. Sibling of :class:`RestGeoJsonFetcher` (#192): where that
|
|
4
|
+
adapter reads a GeoJSON ``FeatureCollection``, this one reads a tabular
|
|
5
|
+
JSON REST API that answers ``{"data": [...], "next": ...}`` — the shape
|
|
6
|
+
served by Géorisques (``/api/v1/...``), BAN and RNB.
|
|
7
|
+
|
|
8
|
+
The fetcher is **materialize-only**: a paginated REST API has no
|
|
9
|
+
zero-copy DuckDB scan, so :attr:`~core.plugin_model.FetchMode.REFERENCE`
|
|
10
|
+
raises. Rows are streamed to newline-delimited JSON (JSONL): local file
|
|
11
|
+
by default, or S3/Garage when ``s3_uri`` / ``s3_key`` is supplied.
|
|
12
|
+
|
|
13
|
+
Like :class:`RestGeoJsonFetcher`, importing this module self-registers
|
|
14
|
+
the fetcher in the process-wide :data:`core.sources.PROTOCOLS` registry
|
|
15
|
+
(idempotent), so the ETL fetch path has a real ``rest-table`` adapter to
|
|
16
|
+
dispatch to.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from os import PathLike
|
|
25
|
+
import tempfile
|
|
26
|
+
import time
|
|
27
|
+
from typing import Any, BinaryIO
|
|
28
|
+
from urllib.parse import urlencode, urljoin, urlsplit
|
|
29
|
+
|
|
30
|
+
from gispulse.core.config import settings
|
|
31
|
+
from gispulse.core.logging import get_logger
|
|
32
|
+
from gispulse.core.plugin_model import (
|
|
33
|
+
AccessProtocol,
|
|
34
|
+
AccessSpec,
|
|
35
|
+
FetchMode,
|
|
36
|
+
Payload,
|
|
37
|
+
SourceResult,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
log = get_logger(__name__)
|
|
41
|
+
|
|
42
|
+
_DEFAULT_TIMEOUT_S = 20.0
|
|
43
|
+
#: Hard ceiling on pages followed when the AccessSpec does not set one —
|
|
44
|
+
#: a tabular API with a runaway ``next`` must never loop unbounded.
|
|
45
|
+
_DEFAULT_MAX_PAGES = 1000
|
|
46
|
+
_ROW_SOURCE_KEY = "key"
|
|
47
|
+
_ROW_SOURCE_BODY = "body"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _same_origin(a: str, b: str) -> bool:
|
|
51
|
+
"""True if ``a`` and ``b`` share scheme + host + port.
|
|
52
|
+
|
|
53
|
+
A paginated ``next`` link must not steer the fetch to another host —
|
|
54
|
+
a malicious catalogue entry could otherwise exfiltrate the request or
|
|
55
|
+
pivot to an internal service across pages.
|
|
56
|
+
"""
|
|
57
|
+
sa, sb = urlsplit(a), urlsplit(b)
|
|
58
|
+
return (sa.scheme, sa.netloc) == (sb.scheme, sb.netloc)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_json(url: str, timeout: float) -> dict[str, Any]:
|
|
62
|
+
"""GET ``url`` and return the parsed JSON body."""
|
|
63
|
+
import httpx
|
|
64
|
+
|
|
65
|
+
# follow_redirects is OFF: httpx would otherwise chase a 3xx to an
|
|
66
|
+
# arbitrary host *after* the SSRF/same-origin guard has cleared the
|
|
67
|
+
# original URL, re-opening the very hole the guard closes (#199).
|
|
68
|
+
resp = httpx.get(
|
|
69
|
+
url,
|
|
70
|
+
timeout=timeout,
|
|
71
|
+
follow_redirects=False,
|
|
72
|
+
headers={"Accept": "application/json"},
|
|
73
|
+
)
|
|
74
|
+
resp.raise_for_status()
|
|
75
|
+
return resp.json()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _normalize_row_source(value: Any) -> str:
|
|
79
|
+
if value in {_ROW_SOURCE_BODY, "object"}:
|
|
80
|
+
return _ROW_SOURCE_BODY
|
|
81
|
+
return _ROW_SOURCE_KEY
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class PaginationSpec:
|
|
86
|
+
"""Typed REST_TABLE pagination recipe compiled from ``AccessSpec.params``."""
|
|
87
|
+
|
|
88
|
+
data_key: Any = "data"
|
|
89
|
+
next_key: Any | None = None
|
|
90
|
+
row_source: str = _ROW_SOURCE_KEY
|
|
91
|
+
empty_statuses: frozenset[Any] = field(default_factory=frozenset)
|
|
92
|
+
empty_body_is_empty: bool = False
|
|
93
|
+
max_pages: int = _DEFAULT_MAX_PAGES
|
|
94
|
+
max_rows: int | None = None
|
|
95
|
+
max_total_seconds: float | None = None
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_params(cls, params: dict[str, Any]) -> PaginationSpec:
|
|
99
|
+
pagination = dict(params.get("pagination") or {})
|
|
100
|
+
row_source = pagination.get("row_source", pagination.get("row_shape"))
|
|
101
|
+
max_rows = pagination.get("max_rows")
|
|
102
|
+
max_total_seconds = pagination.get("max_total_seconds")
|
|
103
|
+
return cls(
|
|
104
|
+
data_key=pagination.get("data_key", "data"),
|
|
105
|
+
next_key=pagination.get("next_key"),
|
|
106
|
+
row_source=_normalize_row_source(row_source),
|
|
107
|
+
empty_statuses=frozenset(pagination.get("empty_statuses") or []),
|
|
108
|
+
empty_body_is_empty=bool(pagination.get("empty_body_is_empty", False)),
|
|
109
|
+
max_pages=int(pagination.get("max_pages", _DEFAULT_MAX_PAGES)),
|
|
110
|
+
max_rows=int(max_rows) if max_rows is not None else None,
|
|
111
|
+
max_total_seconds=(
|
|
112
|
+
float(max_total_seconds) if max_total_seconds is not None else None
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _rows_from_body(body: dict[str, Any], spec: PaginationSpec) -> list[Any]:
|
|
118
|
+
if spec.row_source == _ROW_SOURCE_BODY:
|
|
119
|
+
return [body]
|
|
120
|
+
page_rows = body.get(spec.data_key)
|
|
121
|
+
if isinstance(page_rows, list): # ignore a non-list data_key
|
|
122
|
+
return page_rows
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _next_page_url(
|
|
127
|
+
body: dict[str, Any],
|
|
128
|
+
current_url: str,
|
|
129
|
+
origin: str,
|
|
130
|
+
seen: set[str],
|
|
131
|
+
spec: PaginationSpec,
|
|
132
|
+
) -> str | None:
|
|
133
|
+
nxt = body.get(spec.next_key) if spec.next_key else None
|
|
134
|
+
if not nxt:
|
|
135
|
+
return None
|
|
136
|
+
# Resolve a relative ``next`` ("?page=2") against the current page URL,
|
|
137
|
+
# then re-guard the absolute result before the next request.
|
|
138
|
+
candidate = urljoin(current_url, str(nxt))
|
|
139
|
+
if candidate not in seen and _same_origin(origin, candidate):
|
|
140
|
+
return candidate
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _write_jsonl(
|
|
145
|
+
rows: list[Any],
|
|
146
|
+
local_path: str | PathLike[str] | None,
|
|
147
|
+
) -> tuple[str | PathLike[str], str, int]:
|
|
148
|
+
if not local_path:
|
|
149
|
+
handle = tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False)
|
|
150
|
+
handle.close()
|
|
151
|
+
local_path = handle.name
|
|
152
|
+
digest = hashlib.sha256()
|
|
153
|
+
row_count = 0
|
|
154
|
+
with open(local_path, "wb") as fh:
|
|
155
|
+
for row in rows:
|
|
156
|
+
line = (json.dumps(row, separators=(",", ":")) + "\n").encode("utf-8")
|
|
157
|
+
digest.update(line)
|
|
158
|
+
fh.write(line)
|
|
159
|
+
row_count += 1
|
|
160
|
+
return local_path, digest.hexdigest(), row_count
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _resolve_s3_materialize_uri(params: dict[str, Any]) -> str | None:
|
|
164
|
+
s3_uri = str(params.get("s3_uri", "") or "").strip()
|
|
165
|
+
if s3_uri:
|
|
166
|
+
return s3_uri
|
|
167
|
+
|
|
168
|
+
s3_key = str(params.get("s3_key", "") or "").strip().lstrip("/")
|
|
169
|
+
if not s3_key:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
bucket = str(params.get("s3_bucket", "") or "").strip() or settings.s3.bucket
|
|
173
|
+
return f"s3://{bucket}/{s3_key}"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _write_jsonl_row(row: Any, fh: BinaryIO, digest: Any) -> int:
|
|
177
|
+
line = (json.dumps(row, separators=(",", ":")) + "\n").encode("utf-8")
|
|
178
|
+
digest.update(line)
|
|
179
|
+
fh.write(line)
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _run_async(coro: Any) -> Any:
|
|
184
|
+
import asyncio
|
|
185
|
+
import threading
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
asyncio.get_running_loop()
|
|
189
|
+
except RuntimeError:
|
|
190
|
+
return asyncio.run(coro)
|
|
191
|
+
|
|
192
|
+
result: dict[str, Any] = {}
|
|
193
|
+
|
|
194
|
+
def runner() -> None:
|
|
195
|
+
try:
|
|
196
|
+
result["value"] = asyncio.run(coro)
|
|
197
|
+
except BaseException as exc: # noqa: BLE001 - propagate from worker
|
|
198
|
+
result["error"] = exc
|
|
199
|
+
|
|
200
|
+
thread = threading.Thread(target=runner)
|
|
201
|
+
thread.start()
|
|
202
|
+
thread.join()
|
|
203
|
+
if "error" in result:
|
|
204
|
+
raise result["error"]
|
|
205
|
+
return result.get("value")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _upload_jsonl_to_s3(s3_uri: str, body: BinaryIO) -> None:
|
|
209
|
+
parsed = urlsplit(s3_uri)
|
|
210
|
+
bucket = parsed.netloc
|
|
211
|
+
key = parsed.path.lstrip("/")
|
|
212
|
+
if parsed.scheme != "s3" or not bucket or not key:
|
|
213
|
+
raise ValueError(f"Invalid REST_TABLE S3 destination: {s3_uri!r}")
|
|
214
|
+
if not settings.s3.endpoint:
|
|
215
|
+
raise ValueError("REST_TABLE S3 materialization requires GISPULSE_S3_ENDPOINT")
|
|
216
|
+
|
|
217
|
+
from gispulse.persistence.storage import S3Storage
|
|
218
|
+
from gispulse.persistence.tier import check_tier
|
|
219
|
+
|
|
220
|
+
check_tier("pro")
|
|
221
|
+
storage = S3Storage(
|
|
222
|
+
endpoint_url=settings.s3.endpoint,
|
|
223
|
+
bucket=bucket,
|
|
224
|
+
access_key=settings.s3.access_key,
|
|
225
|
+
secret_key=settings.s3.secret_key,
|
|
226
|
+
region=settings.s3.region,
|
|
227
|
+
)
|
|
228
|
+
_run_async(storage.upload(key, body, content_type="application/x-ndjson"))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class RestTableFetcher:
|
|
232
|
+
""":class:`~core.sources.Fetcher` for ``AccessProtocol.REST_TABLE``."""
|
|
233
|
+
|
|
234
|
+
protocol = AccessProtocol.REST_TABLE
|
|
235
|
+
payload = Payload.TABLE
|
|
236
|
+
|
|
237
|
+
def fetch(
|
|
238
|
+
self,
|
|
239
|
+
access: AccessSpec,
|
|
240
|
+
*,
|
|
241
|
+
extent: Any | None = None,
|
|
242
|
+
mode: FetchMode = FetchMode.MATERIALIZE,
|
|
243
|
+
) -> SourceResult:
|
|
244
|
+
if mode is FetchMode.REFERENCE:
|
|
245
|
+
raise NotImplementedError("REST_TABLE is materialize-only")
|
|
246
|
+
|
|
247
|
+
params = dict(access.params or {})
|
|
248
|
+
timeout = float(params.get("timeout", _DEFAULT_TIMEOUT_S))
|
|
249
|
+
pagination = PaginationSpec.from_params(params)
|
|
250
|
+
deadline = (
|
|
251
|
+
time.monotonic() + pagination.max_total_seconds
|
|
252
|
+
if pagination.max_total_seconds is not None
|
|
253
|
+
else None
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
import httpx
|
|
257
|
+
|
|
258
|
+
from gispulse.core.ssrf import guard_outbound_url
|
|
259
|
+
|
|
260
|
+
query = dict(params.get("query") or {})
|
|
261
|
+
origin = access.endpoint
|
|
262
|
+
if query:
|
|
263
|
+
sep = "&" if "?" in origin else "?"
|
|
264
|
+
origin = f"{origin}{sep}{urlencode(query)}"
|
|
265
|
+
s3_uri = _resolve_s3_materialize_uri(params)
|
|
266
|
+
local_path = params.get("local_path")
|
|
267
|
+
if s3_uri:
|
|
268
|
+
fh: BinaryIO = tempfile.SpooledTemporaryFile(
|
|
269
|
+
max_size=64 * 1024 * 1024,
|
|
270
|
+
mode="w+b",
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
if not local_path:
|
|
274
|
+
handle = tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False)
|
|
275
|
+
handle.close()
|
|
276
|
+
local_path = handle.name
|
|
277
|
+
fh = open(local_path, "wb")
|
|
278
|
+
|
|
279
|
+
digest = hashlib.sha256()
|
|
280
|
+
row_count = 0
|
|
281
|
+
page_count = 0
|
|
282
|
+
seen: set[str] = set()
|
|
283
|
+
url: str | None = origin
|
|
284
|
+
try:
|
|
285
|
+
while url:
|
|
286
|
+
guard_outbound_url(url)
|
|
287
|
+
seen.add(url)
|
|
288
|
+
try:
|
|
289
|
+
body = _get_json(url, timeout)
|
|
290
|
+
except httpx.HTTPStatusError as exc:
|
|
291
|
+
if exc.response.status_code in pagination.empty_statuses:
|
|
292
|
+
break # "no data here" — leave the result empty
|
|
293
|
+
raise
|
|
294
|
+
except json.JSONDecodeError:
|
|
295
|
+
if pagination.empty_body_is_empty:
|
|
296
|
+
break # empty/malformed body configured as "no data here"
|
|
297
|
+
raise
|
|
298
|
+
page_count += 1
|
|
299
|
+
for row in _rows_from_body(body, pagination):
|
|
300
|
+
if (
|
|
301
|
+
pagination.max_rows is not None
|
|
302
|
+
and row_count >= pagination.max_rows
|
|
303
|
+
):
|
|
304
|
+
break
|
|
305
|
+
row_count += _write_jsonl_row(row, fh, digest)
|
|
306
|
+
if (
|
|
307
|
+
pagination.max_rows is not None
|
|
308
|
+
and row_count >= pagination.max_rows
|
|
309
|
+
):
|
|
310
|
+
break
|
|
311
|
+
if page_count >= pagination.max_pages:
|
|
312
|
+
break
|
|
313
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
314
|
+
break
|
|
315
|
+
url = _next_page_url(body, url, origin, seen, pagination)
|
|
316
|
+
|
|
317
|
+
if s3_uri:
|
|
318
|
+
fh.seek(0)
|
|
319
|
+
_upload_jsonl_to_s3(s3_uri, fh)
|
|
320
|
+
finally:
|
|
321
|
+
fh.close()
|
|
322
|
+
|
|
323
|
+
log.info(
|
|
324
|
+
"rest_table_materialized",
|
|
325
|
+
endpoint=access.endpoint,
|
|
326
|
+
row_count=row_count,
|
|
327
|
+
page_count=page_count,
|
|
328
|
+
)
|
|
329
|
+
data = s3_uri if s3_uri else local_path
|
|
330
|
+
metadata = {
|
|
331
|
+
"row_count": row_count,
|
|
332
|
+
"page_count": page_count,
|
|
333
|
+
"source_url": access.endpoint,
|
|
334
|
+
"sha256": digest.hexdigest(),
|
|
335
|
+
}
|
|
336
|
+
if s3_uri:
|
|
337
|
+
metadata["s3_uri"] = s3_uri
|
|
338
|
+
return SourceResult(
|
|
339
|
+
payload=Payload.TABLE,
|
|
340
|
+
mode=FetchMode.MATERIALIZE,
|
|
341
|
+
data=data,
|
|
342
|
+
reference=s3_uri,
|
|
343
|
+
metadata=metadata,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def register_rest_table_fetcher(registry: Any | None = None) -> None:
|
|
348
|
+
"""Register a :class:`RestTableFetcher` under ``REST_TABLE`` — idempotent."""
|
|
349
|
+
from gispulse.core.sources import PROTOCOLS, ProtocolNotSupported
|
|
350
|
+
|
|
351
|
+
target = registry if registry is not None else PROTOCOLS
|
|
352
|
+
try:
|
|
353
|
+
target.get_fetcher(AccessProtocol.REST_TABLE)
|
|
354
|
+
return # already registered
|
|
355
|
+
except ProtocolNotSupported:
|
|
356
|
+
pass
|
|
357
|
+
target.register(RestTableFetcher())
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# Importing this module wires the fetcher into the global registry (#192 pattern).
|
|
361
|
+
register_rest_table_fetcher()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
__all__ = ["RestTableFetcher", "register_rest_table_fetcher"]
|
|
@@ -37,6 +37,8 @@ if TYPE_CHECKING: # pragma: no cover - typing only
|
|
|
37
37
|
import geopandas as gpd
|
|
38
38
|
|
|
39
39
|
from gispulse.catalog.models import CatalogEntry
|
|
40
|
+
from gispulse.core.explain import ManifestExplanation
|
|
41
|
+
from gispulse.core.manifest_v3 import ManifestV3
|
|
40
42
|
from gispulse.core.pipeline import PipelineSpec
|
|
41
43
|
from gispulse.core.plugin_model import PluginRecord
|
|
42
44
|
from gispulse.runtime.headless_runtime import HeadlessRuntime
|
|
@@ -148,6 +150,78 @@ class GISPulseApp:
|
|
|
148
150
|
# ------------------------------------------------------------------
|
|
149
151
|
# Datasets
|
|
150
152
|
# ------------------------------------------------------------------
|
|
153
|
+
def load(
|
|
154
|
+
self,
|
|
155
|
+
source: "str | Path | Any",
|
|
156
|
+
*,
|
|
157
|
+
bbox: "tuple[float, float, float, float] | None" = None,
|
|
158
|
+
layer: str | None = None,
|
|
159
|
+
lazy: bool = False,
|
|
160
|
+
crs: str | None = None,
|
|
161
|
+
**opts: Any,
|
|
162
|
+
) -> "gpd.GeoDataFrame | Any":
|
|
163
|
+
"""Load any supported source into a GeoDataFrame.
|
|
164
|
+
|
|
165
|
+
The unified entry point over every reader GISPulse owns — local
|
|
166
|
+
files (GeoPackage, GeoJSON, GeoParquet, Shapefile, …) and remote
|
|
167
|
+
protocol sources (GeoParquet on S3/HTTP, OGC API Features, WFS,
|
|
168
|
+
STAC, …). The result is a plain GeoDataFrame, so any capability
|
|
169
|
+
runs on it directly: ``app.apply(verb, app.load(src), ...)``.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
source: A path, a ``s3://`` / ``https://`` / named-protocol
|
|
173
|
+
URI, an :class:`~gispulse.core.plugin_model.AccessSpec`,
|
|
174
|
+
or an access-descriptor ``dict``.
|
|
175
|
+
bbox: Spatial filter ``(minx, miny, maxx, maxy)`` pushed
|
|
176
|
+
down to the reader/fetcher when supported.
|
|
177
|
+
layer: Layer name for multi-layer file formats.
|
|
178
|
+
lazy: Return a :class:`~gispulse.persistence.loader.LazyDataset`
|
|
179
|
+
handle for remote sources instead of materialising.
|
|
180
|
+
crs: Force this CRS when the source declares none.
|
|
181
|
+
**opts: Extra options forwarded to the underlying reader.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
A GeoDataFrame, or a ``LazyDataset`` when ``lazy=True`` on a
|
|
185
|
+
remote source.
|
|
186
|
+
"""
|
|
187
|
+
from gispulse.persistence.loader import load as _load
|
|
188
|
+
|
|
189
|
+
return _load(source, bbox=bbox, layer=layer, lazy=lazy, crs=crs, **opts)
|
|
190
|
+
|
|
191
|
+
def publish(
|
|
192
|
+
self,
|
|
193
|
+
gdf: "gpd.GeoDataFrame",
|
|
194
|
+
target: str,
|
|
195
|
+
*,
|
|
196
|
+
auth: str | None = None,
|
|
197
|
+
overwrite: bool = True,
|
|
198
|
+
**opts: Any,
|
|
199
|
+
) -> dict:
|
|
200
|
+
"""Publish a GeoDataFrame to a writable destination.
|
|
201
|
+
|
|
202
|
+
Currently supports GeoNode (``geonode://<instance>/<dataset>``),
|
|
203
|
+
the write counterpart of :meth:`load`. The frame is packaged and
|
|
204
|
+
uploaded through the destination's native publish API.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
gdf: The GeoDataFrame to publish.
|
|
208
|
+
target: Destination URI (``geonode://…``).
|
|
209
|
+
auth: Credential/token; falls back to the instance config.
|
|
210
|
+
overwrite: Replace an existing dataset of the same name.
|
|
211
|
+
**opts: Extra options forwarded to the writer.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
The writer's response payload.
|
|
215
|
+
"""
|
|
216
|
+
from gispulse.persistence.geonode import GEONODE_SCHEME, publish as _publish
|
|
217
|
+
|
|
218
|
+
if str(target).partition("://")[0].lower() == GEONODE_SCHEME:
|
|
219
|
+
return _publish(gdf, target, auth=auth, overwrite=overwrite, **opts)
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"unsupported publish target {target!r}; "
|
|
222
|
+
"supported schemes: geonode://"
|
|
223
|
+
)
|
|
224
|
+
|
|
151
225
|
def load_dataset(
|
|
152
226
|
self, path: "str | Path", layer: str | None = None
|
|
153
227
|
) -> "gpd.GeoDataFrame":
|
|
@@ -480,6 +554,33 @@ class GISPulseApp:
|
|
|
480
554
|
|
|
481
555
|
return build_runtime(gpkg_path, triggers, **kwargs)
|
|
482
556
|
|
|
557
|
+
# ------------------------------------------------------------------
|
|
558
|
+
# ELT — manifest v3 introspection (Lot 4E / #251)
|
|
559
|
+
# ------------------------------------------------------------------
|
|
560
|
+
def explain(
|
|
561
|
+
self,
|
|
562
|
+
manifest: "str | Path | ManifestV3",
|
|
563
|
+
*,
|
|
564
|
+
engine: str | None = None,
|
|
565
|
+
) -> "ManifestExplanation":
|
|
566
|
+
"""Return a structured explanation of a v3 manifest.
|
|
567
|
+
|
|
568
|
+
Walks the manifest's compiled DAG and, for each step, surfaces
|
|
569
|
+
which :class:`ExecutionStrategy` ``select_strategy()`` would pick
|
|
570
|
+
under the configured engine. Capabilities with no SQL strategy
|
|
571
|
+
are flagged ``etl_strict``. See
|
|
572
|
+
:func:`gispulse.core.explain.explain_manifest` for the report
|
|
573
|
+
shape and :func:`format_explanation_text` to render it.
|
|
574
|
+
"""
|
|
575
|
+
from gispulse.core.explain import explain_manifest
|
|
576
|
+
from gispulse.core.manifest_v3 import ManifestV3, load_manifest_v3
|
|
577
|
+
|
|
578
|
+
if isinstance(manifest, ManifestV3):
|
|
579
|
+
mf = manifest
|
|
580
|
+
else:
|
|
581
|
+
mf = load_manifest_v3(manifest)
|
|
582
|
+
return explain_manifest(mf, engine=engine)
|
|
583
|
+
|
|
483
584
|
|
|
484
585
|
@lru_cache(maxsize=1)
|
|
485
586
|
def get_app() -> GISPulseApp:
|
|
@@ -506,4 +607,33 @@ def run(
|
|
|
506
607
|
return get_app().run_pipeline(spec, inputs, params)
|
|
507
608
|
|
|
508
609
|
|
|
509
|
-
|
|
610
|
+
def load(
|
|
611
|
+
source: "str | Path | Any",
|
|
612
|
+
*,
|
|
613
|
+
bbox: "tuple[float, float, float, float] | None" = None,
|
|
614
|
+
layer: str | None = None,
|
|
615
|
+
lazy: bool = False,
|
|
616
|
+
crs: str | None = None,
|
|
617
|
+
**opts: Any,
|
|
618
|
+
) -> "gpd.GeoDataFrame | Any":
|
|
619
|
+
"""Load any supported source — shortcut for ``get_app().load``."""
|
|
620
|
+
return get_app().load(
|
|
621
|
+
source, bbox=bbox, layer=layer, lazy=lazy, crs=crs, **opts
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def publish(
|
|
626
|
+
gdf: "gpd.GeoDataFrame",
|
|
627
|
+
target: str,
|
|
628
|
+
*,
|
|
629
|
+
auth: str | None = None,
|
|
630
|
+
overwrite: bool = True,
|
|
631
|
+
**opts: Any,
|
|
632
|
+
) -> dict:
|
|
633
|
+
"""Publish a GeoDataFrame — shortcut for ``get_app().publish``."""
|
|
634
|
+
return get_app().publish(
|
|
635
|
+
gdf, target, auth=auth, overwrite=overwrite, **opts
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
__all__ = ["GISPulseApp", "get_app", "apply", "load", "publish", "run"]
|