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.
Files changed (338) hide show
  1. {gispulse-2.0.0/src/gispulse.egg-info → gispulse-2.2.0}/PKG-INFO +3 -2
  2. {gispulse-2.0.0 → gispulse-2.2.0}/pyproject.toml +3 -2
  3. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/__init__.py +11 -3
  4. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/_compat.py +43 -9
  5. gispulse-2.2.0/src/gispulse/adapters/rest/__init__.py +15 -0
  6. gispulse-2.2.0/src/gispulse/adapters/rest/rest_table_fetcher.py +364 -0
  7. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/app.py +131 -1
  8. gispulse-2.2.0/src/gispulse/capabilities/_attribute_sql.py +379 -0
  9. gispulse-2.2.0/src/gispulse/capabilities/_geometry_sql.py +598 -0
  10. gispulse-2.2.0/src/gispulse/capabilities/clustering.py +612 -0
  11. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/network.py +288 -85
  12. gispulse-2.2.0/src/gispulse/capabilities/network_components.py +211 -0
  13. gispulse-2.2.0/src/gispulse/capabilities/network_graph.py +289 -0
  14. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/network_topology.py +315 -0
  15. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/overlay.py +23 -0
  16. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/registry.py +2 -0
  17. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/schema.py +50 -0
  18. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/selection.py +24 -0
  19. gispulse-2.2.0/src/gispulse/capabilities/sql_pushdown.py +333 -0
  20. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/temporal.py +31 -0
  21. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/__init__.py +6 -0
  22. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/boundary.py +10 -0
  23. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/calculate.py +14 -0
  24. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/centroid_area.py +11 -0
  25. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/concave_hull.py +14 -0
  26. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/diff.py +19 -0
  27. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/dissolve.py +17 -0
  28. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/nearest.py +15 -0
  29. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/reproject.py +12 -0
  30. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/shape_ops_basic.py +15 -0
  31. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/simplify.py +14 -0
  32. gispulse-2.2.0/src/gispulse/capabilities/vector/snap_points.py +185 -0
  33. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/spatial_join.py +15 -0
  34. gispulse-2.2.0/src/gispulse/capabilities/vector/split_lines.py +207 -0
  35. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/union.py +10 -0
  36. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli.py +127 -0
  37. gispulse-2.2.0/src/gispulse/core/assertions.py +304 -0
  38. gispulse-2.2.0/src/gispulse/core/bulk_ingest.py +189 -0
  39. gispulse-2.2.0/src/gispulse/core/bulk_runner.py +1114 -0
  40. gispulse-2.2.0/src/gispulse/core/dag.py +91 -0
  41. gispulse-2.2.0/src/gispulse/core/explain.py +377 -0
  42. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/__init__.py +22 -16
  43. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/base.py +23 -1
  44. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/geoparquet_s3.py +29 -14
  45. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/http_file.py +32 -7
  46. gispulse-2.2.0/src/gispulse/core/fetchers/table_file.py +158 -0
  47. gispulse-2.2.0/src/gispulse/core/manifest_v3.py +738 -0
  48. gispulse-2.2.0/src/gispulse/core/network_graph_handle.py +175 -0
  49. gispulse-2.2.0/src/gispulse/core/pipeline_schema.py +572 -0
  50. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/plugin_model.py +3 -1
  51. gispulse-2.2.0/src/gispulse/core/spatial_index.py +132 -0
  52. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/graph_executor.py +21 -19
  53. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/__init__.py +11 -0
  54. gispulse-2.2.0/src/gispulse/persistence/datamart.py +184 -0
  55. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/duckdb_engine.py +70 -0
  56. gispulse-2.2.0/src/gispulse/persistence/duckdb_relations.py +271 -0
  57. gispulse-2.2.0/src/gispulse/persistence/geonode.py +287 -0
  58. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/io.py +140 -10
  59. gispulse-2.2.0/src/gispulse/persistence/loader.py +433 -0
  60. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/sql_dialect.py +131 -0
  61. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/storage.py +16 -4
  62. gispulse-2.2.0/src/gispulse/runtime/manifest_runner.py +369 -0
  63. {gispulse-2.0.0 → gispulse-2.2.0/src/gispulse.egg-info}/PKG-INFO +3 -2
  64. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/SOURCES.txt +22 -0
  65. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/requires.txt +2 -1
  66. gispulse-2.0.0/src/gispulse/adapters/rest/__init__.py +0 -13
  67. gispulse-2.0.0/src/gispulse/capabilities/clustering.py +0 -306
  68. gispulse-2.0.0/src/gispulse/core/pipeline_schema.py +0 -298
  69. {gispulse-2.0.0 → gispulse-2.2.0}/LICENSE +0 -0
  70. {gispulse-2.0.0 → gispulse-2.2.0}/LICENSE-COMMERCIAL.md +0 -0
  71. {gispulse-2.0.0 → gispulse-2.2.0}/README.md +0 -0
  72. {gispulse-2.0.0 → gispulse-2.2.0}/setup.cfg +0 -0
  73. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/_pyogrio_warnings.py +0 -0
  74. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/__init__.py +0 -0
  75. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/apicarto.py +0 -0
  76. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/__init__.py +0 -0
  77. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/action_dispatcher.py +0 -0
  78. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/bus_message.py +0 -0
  79. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/circuit_breaker.py +0 -0
  80. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/dlq.py +0 -0
  81. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/enums.py +0 -0
  82. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/event_router.py +0 -0
  83. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/pg_notify.py +0 -0
  84. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/pool.py +0 -0
  85. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/predicate_evaluator.py +0 -0
  86. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/state_store.py +0 -0
  87. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/trigger_manager.py +0 -0
  88. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/workers/__init__.py +0 -0
  89. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/workers/base_worker.py +0 -0
  90. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/workers/dispatch_worker.py +0 -0
  91. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/esb/workers/identify_worker.py +0 -0
  92. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/__init__.py +0 -0
  93. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/app.py +0 -0
  94. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/auth.py +0 -0
  95. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/dataset_ops.py +0 -0
  96. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/dependencies.py +0 -0
  97. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/error_handlers.py +0 -0
  98. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/event_hub.py +0 -0
  99. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/layer_utils.py +0 -0
  100. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/middleware/__init__.py +0 -0
  101. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/middleware/audit_middleware.py +0 -0
  102. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/middleware/metrics_middleware.py +0 -0
  103. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/middleware/read_only.py +0 -0
  104. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/portal_app.py +0 -0
  105. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/rate_limit.py +0 -0
  106. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/__init__.py +0 -0
  107. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/_upload_utils.py +0 -0
  108. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/auth_router.py +0 -0
  109. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/capabilities_router.py +0 -0
  110. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/catalog_router.py +0 -0
  111. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/datasets_router.py +0 -0
  112. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/esb_router.py +0 -0
  113. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/examples_router.py +0 -0
  114. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/filter_router.py +0 -0
  115. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/jobs_router.py +0 -0
  116. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/marketplace_router.py +0 -0
  117. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/ogc_features_router.py +0 -0
  118. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/pipelines_router.py +0 -0
  119. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_datasets_router.py +0 -0
  120. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_features_router.py +0 -0
  121. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_router.py +0 -0
  122. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_sql_router.py +0 -0
  123. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/portal_upload_router.py +0 -0
  124. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/projects_router.py +0 -0
  125. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/relations_router.py +0 -0
  126. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/rules_router.py +0 -0
  127. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/scenarios_router.py +0 -0
  128. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/schedules_router.py +0 -0
  129. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/sessions_router.py +0 -0
  130. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/system_router.py +0 -0
  131. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/templates_router.py +0 -0
  132. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/tiles_router.py +0 -0
  133. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/triggers_router.py +0 -0
  134. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/viewer_router.py +0 -0
  135. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/watchers_router.py +0 -0
  136. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/routers/ws_router.py +0 -0
  137. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/schemas.py +0 -0
  138. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/http/serve_app.py +0 -0
  139. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/mcp/__init__.py +0 -0
  140. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/mcp/dryrun.py +0 -0
  141. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/mcp/server.py +0 -0
  142. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/mcp/workdir.py +0 -0
  143. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/metrics.py +0 -0
  144. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/__init__.py +0 -0
  145. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/auth.py +0 -0
  146. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/loader.py +0 -0
  147. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/wfs_client.py +0 -0
  148. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/ogc/wfs_fetcher.py +0 -0
  149. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/rest/rest_fetcher.py +0 -0
  150. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/stac/__init__.py +0 -0
  151. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/stac/stac_fetcher.py +0 -0
  152. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/webhooks/__init__.py +0 -0
  153. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/adapters/webhooks/http_client.py +0 -0
  154. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/__init__.py +0 -0
  155. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/base.py +0 -0
  156. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/classification.py +0 -0
  157. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/density.py +0 -0
  158. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/palettes.py +0 -0
  159. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/pointcloud.py +0 -0
  160. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/polygon_topology.py +0 -0
  161. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/postgis_sql.py +0 -0
  162. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/raster.py +0 -0
  163. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/relation_detector.py +0 -0
  164. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/spatial_stats.py +0 -0
  165. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/strategy.py +0 -0
  166. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/transforms.py +0 -0
  167. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/validation.py +0 -0
  168. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/aggregate.py +0 -0
  169. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/assign_projection.py +0 -0
  170. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/buffer.py +0 -0
  171. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/chaikin.py +0 -0
  172. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/classify.py +0 -0
  173. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/clip.py +0 -0
  174. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/extract_holes.py +0 -0
  175. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/extract_ops.py +0 -0
  176. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/filter.py +0 -0
  177. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/force_geometry_type.py +0 -0
  178. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/intersects.py +0 -0
  179. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/line_merge.py +0 -0
  180. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/line_ops.py +0 -0
  181. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/merge.py +0 -0
  182. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/offset_curve.py +0 -0
  183. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/parts.py +0 -0
  184. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/polygonize.py +0 -0
  185. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/shape_ops_advanced.py +0 -0
  186. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/snap_grid.py +0 -0
  187. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/capabilities/vector/voronoi.py +0 -0
  188. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/__init__.py +0 -0
  189. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/data/basemaps.json +0 -0
  190. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/data/epsg_common.json +0 -0
  191. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/models.py +0 -0
  192. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/__init__.py +0 -0
  193. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/base.py +0 -0
  194. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/basemaps.py +0 -0
  195. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/flux_ign.py +0 -0
  196. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/flux_osm.py +0 -0
  197. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/opendata_datagouv.py +0 -0
  198. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/opendata_hub.py +0 -0
  199. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/opendata_ign.py +0 -0
  200. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/projections.py +0 -0
  201. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/providers/stac_client.py +0 -0
  202. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/registry.py +0 -0
  203. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/catalog/source_bridge.py +0 -0
  204. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_mcp.py +0 -0
  205. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_portal.py +0 -0
  206. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_track.py +0 -0
  207. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_triggers.py +0 -0
  208. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_triggers_watch.py +0 -0
  209. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/cli_watch.py +0 -0
  210. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/config.py +0 -0
  211. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/__init__.py +0 -0
  212. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/cache.py +0 -0
  213. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/capability_params.py +0 -0
  214. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/conditions.py +0 -0
  215. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/config.py +0 -0
  216. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/crs.py +0 -0
  217. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/data/worldwide_catalog.yml +0 -0
  218. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/data_pack_signature.py +0 -0
  219. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/dispatcher.py +0 -0
  220. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/enums.py +0 -0
  221. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/ogc_client.py +0 -0
  222. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/ogc_features.py +0 -0
  223. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/fetchers/stac.py +0 -0
  224. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/__init__.py +0 -0
  225. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/cache.py +0 -0
  226. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/chain.py +0 -0
  227. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/expression.py +0 -0
  228. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/expression_converter.py +0 -0
  229. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/result.py +0 -0
  230. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/service.py +0 -0
  231. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/filter/types.py +0 -0
  232. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/graph.py +0 -0
  233. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/io/__init__.py +0 -0
  234. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/io/geoparquet.py +0 -0
  235. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/licence_format.py +0 -0
  236. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/logging.py +0 -0
  237. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/models.py +0 -0
  238. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/observability.py +0 -0
  239. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/pipeline.py +0 -0
  240. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/plugin_contracts.py +0 -0
  241. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/plugin_hub.py +0 -0
  242. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/predicates.py +0 -0
  243. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/pricing_catalog.yml +0 -0
  244. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/registry.py +0 -0
  245. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/regulatory_zoning_entry.py +0 -0
  246. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/relations.py +0 -0
  247. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/session.py +0 -0
  248. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/sources.py +0 -0
  249. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/sql_safety.py +0 -0
  250. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/ssrf.py +0 -0
  251. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/core/zoning_normalizer.py +0 -0
  252. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/diagnostics/__init__.py +0 -0
  253. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/diagnostics/system.py +0 -0
  254. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/dsl/__init__.py +0 -0
  255. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/dsl/expression_parser.py +0 -0
  256. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/dsl/geom_fcts.py +0 -0
  257. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/__init__.py +0 -0
  258. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/capability_executor.py +0 -0
  259. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/job_queue.py +0 -0
  260. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/job_queue_factory.py +0 -0
  261. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/metering.py +0 -0
  262. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/pipeline_executor.py +0 -0
  263. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/runner.py +0 -0
  264. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/scenario_runner.py +0 -0
  265. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/scheduler.py +0 -0
  266. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/session_manager.py +0 -0
  267. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/trigger_bridge.py +0 -0
  268. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/orchestration/worker.py +0 -0
  269. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/audit.py +0 -0
  270. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/auth_models.py +0 -0
  271. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/auth_repository.py +0 -0
  272. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/bridge.py +0 -0
  273. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/change_log_watcher.py +0 -0
  274. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/changelog_doctor.py +0 -0
  275. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/changelog_reader.py +0 -0
  276. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/duckdb_change_detector.py +0 -0
  277. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/duckdb_diff_engine.py +0 -0
  278. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/duckdb_engine_adapter.py +0 -0
  279. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/engine.py +0 -0
  280. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/engine_factory.py +0 -0
  281. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/file_blob_cdc.py +0 -0
  282. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg.py +0 -0
  283. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_connection.py +0 -0
  284. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_engine.py +0 -0
  285. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_repository.py +0 -0
  286. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_schema.py +0 -0
  287. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/gpkg_spatial.py +0 -0
  288. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/licence.py +0 -0
  289. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/postgis.py +0 -0
  290. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/project_io.py +0 -0
  291. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/raster_io.py +0 -0
  292. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/repository.py +0 -0
  293. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/schedule_repository.py +0 -0
  294. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/schema.py +0 -0
  295. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/session_provisioner.py +0 -0
  296. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/sld_converter.py +0 -0
  297. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/source_watcher.py +0 -0
  298. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/spatial_queries.py +0 -0
  299. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/spatialite_engine.py +0 -0
  300. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/spatialite_session.py +0 -0
  301. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/sql_guardrails.py +0 -0
  302. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/sqlite_repository.py +0 -0
  303. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/style_converter.py +0 -0
  304. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/style_sidecar.py +0 -0
  305. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/tier.py +0 -0
  306. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/virtual_dataset.py +0 -0
  307. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/persistence/watcher_registry.py +0 -0
  308. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/__init__.py +0 -0
  309. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/api.py +0 -0
  310. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/datagouv_refresh.py +0 -0
  311. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/mcp_pilot.py +0 -0
  312. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/pipeline.py +0 -0
  313. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/sources.py +0 -0
  314. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/spatial.py +0 -0
  315. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/plugins/worldwide_source.py +0 -0
  316. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/__init__.py +0 -0
  317. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/engine.py +0 -0
  318. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/loader.py +0 -0
  319. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/operation_executor.py +0 -0
  320. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/predicates.py +0 -0
  321. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/trigger_evaluator.py +0 -0
  322. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/rules/validation.py +0 -0
  323. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/__init__.py +0 -0
  324. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/config_loader.py +0 -0
  325. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/dialect_scanner.py +0 -0
  326. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/duckdb_engine.py +0 -0
  327. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/engine_inference.py +0 -0
  328. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/headless_runtime.py +0 -0
  329. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/layer_registry.py +0 -0
  330. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/predicate_dsl.py +0 -0
  331. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/source_watch.py +0 -0
  332. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/sqlite_retry.py +0 -0
  333. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/runtime/validation_runner.py +0 -0
  334. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse/telemetry.py +0 -0
  335. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/dependency_links.txt +0 -0
  336. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/entry_points.txt +0 -0
  337. {gispulse-2.0.0 → gispulse-2.2.0}/src/gispulse.egg-info/top_level.txt +0 -0
  338. {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.0.0
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<8.0,>=5.0; extra == "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.0.0"
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,<8.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__ = ["__version__", "GISPulseApp", "get_app", "apply", "run"]
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 resolves a legacy name to its ``gispulse.*`` module."""
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
- return importlib.import_module(self._target)
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
- # ``import_module`` in ``create_module`` already executed the module.
49
- return None
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
- """Append the legacy finder to ``sys.meta_path`` (idempotent)."""
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
- # Appended last: real packages always win; the shim only catches names
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
- __all__ = ["GISPulseApp", "get_app", "apply", "run"]
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"]