gispulse 2.2.0__tar.gz → 2.2.2__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 (336) hide show
  1. {gispulse-2.2.0/src/gispulse.egg-info → gispulse-2.2.2}/PKG-INFO +1 -1
  2. {gispulse-2.2.0 → gispulse-2.2.2}/pyproject.toml +1 -1
  3. gispulse-2.2.2/src/gispulse/capabilities/vector/snap_points.py +259 -0
  4. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/datamart.py +61 -14
  5. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/loader.py +84 -4
  6. {gispulse-2.2.0 → gispulse-2.2.2/src/gispulse.egg-info}/PKG-INFO +1 -1
  7. gispulse-2.2.0/src/gispulse/capabilities/vector/snap_points.py +0 -185
  8. {gispulse-2.2.0 → gispulse-2.2.2}/LICENSE +0 -0
  9. {gispulse-2.2.0 → gispulse-2.2.2}/LICENSE-COMMERCIAL.md +0 -0
  10. {gispulse-2.2.0 → gispulse-2.2.2}/README.md +0 -0
  11. {gispulse-2.2.0 → gispulse-2.2.2}/setup.cfg +0 -0
  12. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/__init__.py +0 -0
  13. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/_compat.py +0 -0
  14. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/_pyogrio_warnings.py +0 -0
  15. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/__init__.py +0 -0
  16. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/apicarto.py +0 -0
  17. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/__init__.py +0 -0
  18. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/action_dispatcher.py +0 -0
  19. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/bus_message.py +0 -0
  20. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/circuit_breaker.py +0 -0
  21. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/dlq.py +0 -0
  22. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/enums.py +0 -0
  23. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/event_router.py +0 -0
  24. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/pg_notify.py +0 -0
  25. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/pool.py +0 -0
  26. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/predicate_evaluator.py +0 -0
  27. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/state_store.py +0 -0
  28. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/trigger_manager.py +0 -0
  29. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/workers/__init__.py +0 -0
  30. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/workers/base_worker.py +0 -0
  31. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/workers/dispatch_worker.py +0 -0
  32. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/workers/identify_worker.py +0 -0
  33. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/__init__.py +0 -0
  34. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/app.py +0 -0
  35. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/auth.py +0 -0
  36. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/dataset_ops.py +0 -0
  37. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/dependencies.py +0 -0
  38. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/error_handlers.py +0 -0
  39. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/event_hub.py +0 -0
  40. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/layer_utils.py +0 -0
  41. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/middleware/__init__.py +0 -0
  42. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/middleware/audit_middleware.py +0 -0
  43. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/middleware/metrics_middleware.py +0 -0
  44. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/middleware/read_only.py +0 -0
  45. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/portal_app.py +0 -0
  46. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/rate_limit.py +0 -0
  47. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/__init__.py +0 -0
  48. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/_upload_utils.py +0 -0
  49. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/auth_router.py +0 -0
  50. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/capabilities_router.py +0 -0
  51. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/catalog_router.py +0 -0
  52. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/datasets_router.py +0 -0
  53. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/esb_router.py +0 -0
  54. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/examples_router.py +0 -0
  55. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/filter_router.py +0 -0
  56. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/jobs_router.py +0 -0
  57. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/marketplace_router.py +0 -0
  58. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/ogc_features_router.py +0 -0
  59. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/pipelines_router.py +0 -0
  60. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_datasets_router.py +0 -0
  61. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_features_router.py +0 -0
  62. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_router.py +0 -0
  63. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_sql_router.py +0 -0
  64. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_upload_router.py +0 -0
  65. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/projects_router.py +0 -0
  66. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/relations_router.py +0 -0
  67. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/rules_router.py +0 -0
  68. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/scenarios_router.py +0 -0
  69. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/schedules_router.py +0 -0
  70. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/sessions_router.py +0 -0
  71. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/system_router.py +0 -0
  72. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/templates_router.py +0 -0
  73. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/tiles_router.py +0 -0
  74. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/triggers_router.py +0 -0
  75. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/viewer_router.py +0 -0
  76. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/watchers_router.py +0 -0
  77. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/ws_router.py +0 -0
  78. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/schemas.py +0 -0
  79. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/serve_app.py +0 -0
  80. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/mcp/__init__.py +0 -0
  81. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/mcp/dryrun.py +0 -0
  82. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/mcp/server.py +0 -0
  83. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/mcp/workdir.py +0 -0
  84. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/metrics.py +0 -0
  85. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/__init__.py +0 -0
  86. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/auth.py +0 -0
  87. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/loader.py +0 -0
  88. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/wfs_client.py +0 -0
  89. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/wfs_fetcher.py +0 -0
  90. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/rest/__init__.py +0 -0
  91. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/rest/rest_fetcher.py +0 -0
  92. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/rest/rest_table_fetcher.py +0 -0
  93. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/stac/__init__.py +0 -0
  94. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/stac/stac_fetcher.py +0 -0
  95. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/webhooks/__init__.py +0 -0
  96. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/webhooks/http_client.py +0 -0
  97. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/app.py +0 -0
  98. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/__init__.py +0 -0
  99. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/_attribute_sql.py +0 -0
  100. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/_geometry_sql.py +0 -0
  101. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/base.py +0 -0
  102. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/classification.py +0 -0
  103. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/clustering.py +0 -0
  104. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/density.py +0 -0
  105. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/network.py +0 -0
  106. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/network_components.py +0 -0
  107. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/network_graph.py +0 -0
  108. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/network_topology.py +0 -0
  109. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/overlay.py +0 -0
  110. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/palettes.py +0 -0
  111. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/pointcloud.py +0 -0
  112. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/polygon_topology.py +0 -0
  113. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/postgis_sql.py +0 -0
  114. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/raster.py +0 -0
  115. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/registry.py +0 -0
  116. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/relation_detector.py +0 -0
  117. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/schema.py +0 -0
  118. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/selection.py +0 -0
  119. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/spatial_stats.py +0 -0
  120. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/sql_pushdown.py +0 -0
  121. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/strategy.py +0 -0
  122. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/temporal.py +0 -0
  123. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/transforms.py +0 -0
  124. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/validation.py +0 -0
  125. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/__init__.py +0 -0
  126. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/aggregate.py +0 -0
  127. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/assign_projection.py +0 -0
  128. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/boundary.py +0 -0
  129. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/buffer.py +0 -0
  130. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/calculate.py +0 -0
  131. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/centroid_area.py +0 -0
  132. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/chaikin.py +0 -0
  133. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/classify.py +0 -0
  134. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/clip.py +0 -0
  135. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/concave_hull.py +0 -0
  136. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/diff.py +0 -0
  137. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/dissolve.py +0 -0
  138. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/extract_holes.py +0 -0
  139. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/extract_ops.py +0 -0
  140. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/filter.py +0 -0
  141. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/force_geometry_type.py +0 -0
  142. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/intersects.py +0 -0
  143. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/line_merge.py +0 -0
  144. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/line_ops.py +0 -0
  145. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/merge.py +0 -0
  146. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/nearest.py +0 -0
  147. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/offset_curve.py +0 -0
  148. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/parts.py +0 -0
  149. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/polygonize.py +0 -0
  150. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/reproject.py +0 -0
  151. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/shape_ops_advanced.py +0 -0
  152. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/shape_ops_basic.py +0 -0
  153. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/simplify.py +0 -0
  154. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/snap_grid.py +0 -0
  155. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/spatial_join.py +0 -0
  156. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/split_lines.py +0 -0
  157. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/union.py +0 -0
  158. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/voronoi.py +0 -0
  159. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/__init__.py +0 -0
  160. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/data/basemaps.json +0 -0
  161. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/data/epsg_common.json +0 -0
  162. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/models.py +0 -0
  163. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/__init__.py +0 -0
  164. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/base.py +0 -0
  165. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/basemaps.py +0 -0
  166. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/flux_ign.py +0 -0
  167. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/flux_osm.py +0 -0
  168. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/opendata_datagouv.py +0 -0
  169. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/opendata_hub.py +0 -0
  170. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/opendata_ign.py +0 -0
  171. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/projections.py +0 -0
  172. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/stac_client.py +0 -0
  173. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/registry.py +0 -0
  174. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/source_bridge.py +0 -0
  175. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli.py +0 -0
  176. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_mcp.py +0 -0
  177. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_portal.py +0 -0
  178. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_track.py +0 -0
  179. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_triggers.py +0 -0
  180. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_triggers_watch.py +0 -0
  181. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_watch.py +0 -0
  182. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/config.py +0 -0
  183. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/__init__.py +0 -0
  184. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/assertions.py +0 -0
  185. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/bulk_ingest.py +0 -0
  186. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/bulk_runner.py +0 -0
  187. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/cache.py +0 -0
  188. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/capability_params.py +0 -0
  189. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/conditions.py +0 -0
  190. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/config.py +0 -0
  191. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/crs.py +0 -0
  192. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/dag.py +0 -0
  193. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/data/worldwide_catalog.yml +0 -0
  194. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/data_pack_signature.py +0 -0
  195. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/dispatcher.py +0 -0
  196. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/enums.py +0 -0
  197. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/explain.py +0 -0
  198. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/__init__.py +0 -0
  199. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/base.py +0 -0
  200. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/geoparquet_s3.py +0 -0
  201. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/http_file.py +0 -0
  202. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/ogc_client.py +0 -0
  203. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/ogc_features.py +0 -0
  204. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/stac.py +0 -0
  205. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/table_file.py +0 -0
  206. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/__init__.py +0 -0
  207. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/cache.py +0 -0
  208. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/chain.py +0 -0
  209. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/expression.py +0 -0
  210. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/expression_converter.py +0 -0
  211. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/result.py +0 -0
  212. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/service.py +0 -0
  213. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/types.py +0 -0
  214. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/graph.py +0 -0
  215. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/io/__init__.py +0 -0
  216. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/io/geoparquet.py +0 -0
  217. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/licence_format.py +0 -0
  218. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/logging.py +0 -0
  219. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/manifest_v3.py +0 -0
  220. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/models.py +0 -0
  221. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/network_graph_handle.py +0 -0
  222. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/observability.py +0 -0
  223. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/pipeline.py +0 -0
  224. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/pipeline_schema.py +0 -0
  225. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/plugin_contracts.py +0 -0
  226. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/plugin_hub.py +0 -0
  227. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/plugin_model.py +0 -0
  228. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/predicates.py +0 -0
  229. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/pricing_catalog.yml +0 -0
  230. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/registry.py +0 -0
  231. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/regulatory_zoning_entry.py +0 -0
  232. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/relations.py +0 -0
  233. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/session.py +0 -0
  234. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/sources.py +0 -0
  235. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/spatial_index.py +0 -0
  236. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/sql_safety.py +0 -0
  237. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/ssrf.py +0 -0
  238. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/zoning_normalizer.py +0 -0
  239. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/diagnostics/__init__.py +0 -0
  240. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/diagnostics/system.py +0 -0
  241. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/dsl/__init__.py +0 -0
  242. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/dsl/expression_parser.py +0 -0
  243. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/dsl/geom_fcts.py +0 -0
  244. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/__init__.py +0 -0
  245. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/capability_executor.py +0 -0
  246. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/graph_executor.py +0 -0
  247. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/job_queue.py +0 -0
  248. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/job_queue_factory.py +0 -0
  249. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/metering.py +0 -0
  250. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/pipeline_executor.py +0 -0
  251. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/runner.py +0 -0
  252. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/scenario_runner.py +0 -0
  253. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/scheduler.py +0 -0
  254. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/session_manager.py +0 -0
  255. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/trigger_bridge.py +0 -0
  256. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/worker.py +0 -0
  257. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/__init__.py +0 -0
  258. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/audit.py +0 -0
  259. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/auth_models.py +0 -0
  260. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/auth_repository.py +0 -0
  261. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/bridge.py +0 -0
  262. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/change_log_watcher.py +0 -0
  263. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/changelog_doctor.py +0 -0
  264. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/changelog_reader.py +0 -0
  265. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_change_detector.py +0 -0
  266. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_diff_engine.py +0 -0
  267. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_engine.py +0 -0
  268. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_engine_adapter.py +0 -0
  269. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_relations.py +0 -0
  270. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/engine.py +0 -0
  271. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/engine_factory.py +0 -0
  272. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/file_blob_cdc.py +0 -0
  273. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/geonode.py +0 -0
  274. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg.py +0 -0
  275. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_connection.py +0 -0
  276. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_engine.py +0 -0
  277. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_repository.py +0 -0
  278. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_schema.py +0 -0
  279. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_spatial.py +0 -0
  280. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/io.py +0 -0
  281. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/licence.py +0 -0
  282. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/postgis.py +0 -0
  283. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/project_io.py +0 -0
  284. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/raster_io.py +0 -0
  285. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/repository.py +0 -0
  286. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/schedule_repository.py +0 -0
  287. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/schema.py +0 -0
  288. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/session_provisioner.py +0 -0
  289. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/sld_converter.py +0 -0
  290. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/source_watcher.py +0 -0
  291. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/spatial_queries.py +0 -0
  292. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/spatialite_engine.py +0 -0
  293. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/spatialite_session.py +0 -0
  294. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/sql_dialect.py +0 -0
  295. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/sql_guardrails.py +0 -0
  296. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/sqlite_repository.py +0 -0
  297. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/storage.py +0 -0
  298. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/style_converter.py +0 -0
  299. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/style_sidecar.py +0 -0
  300. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/tier.py +0 -0
  301. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/virtual_dataset.py +0 -0
  302. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/watcher_registry.py +0 -0
  303. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/__init__.py +0 -0
  304. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/api.py +0 -0
  305. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/datagouv_refresh.py +0 -0
  306. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/mcp_pilot.py +0 -0
  307. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/pipeline.py +0 -0
  308. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/sources.py +0 -0
  309. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/spatial.py +0 -0
  310. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/worldwide_source.py +0 -0
  311. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/__init__.py +0 -0
  312. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/engine.py +0 -0
  313. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/loader.py +0 -0
  314. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/operation_executor.py +0 -0
  315. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/predicates.py +0 -0
  316. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/trigger_evaluator.py +0 -0
  317. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/validation.py +0 -0
  318. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/__init__.py +0 -0
  319. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/config_loader.py +0 -0
  320. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/dialect_scanner.py +0 -0
  321. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/duckdb_engine.py +0 -0
  322. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/engine_inference.py +0 -0
  323. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/headless_runtime.py +0 -0
  324. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/layer_registry.py +0 -0
  325. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/manifest_runner.py +0 -0
  326. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/predicate_dsl.py +0 -0
  327. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/source_watch.py +0 -0
  328. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/sqlite_retry.py +0 -0
  329. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/validation_runner.py +0 -0
  330. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/telemetry.py +0 -0
  331. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/SOURCES.txt +0 -0
  332. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/dependency_links.txt +0 -0
  333. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/entry_points.txt +0 -0
  334. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/requires.txt +0 -0
  335. {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/top_level.txt +0 -0
  336. {gispulse-2.2.0 → gispulse-2.2.2}/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.2.0
3
+ Version: 2.2.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gispulse"
7
- version = "2.2.0"
7
+ version = "2.2.2"
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"
@@ -0,0 +1,259 @@
1
+ """Snap points to a line network with stable edge ids (capability B).
2
+
3
+ Where ``line_locate_point`` returns only a measure plus a *positional*
4
+ ``ref_index``, this capability returns everything needed to attach an event
5
+ to a routable edge:
6
+
7
+ * ``edge_id`` — the matched line's id (from ``ref_id_col``), stable
8
+ across runs (not a row position);
9
+ * ``measure`` — distance along the matched line, in meters, from its
10
+ start (in ``[0, line_length]``); null when unsnapped;
11
+ * ``offset_distance`` — perpendicular distance from the point to its nearest
12
+ line (always reported, snapped or not);
13
+ * ``snapped`` — ``True`` when ``offset_distance <= max_distance_m``
14
+ (or ``max_distance_m is None``), ``False`` otherwise;
15
+ * ``geometry`` — for snapped rows, the **projected** point on the
16
+ nearest line; for unsnapped rows the original point is kept.
17
+
18
+ A point beyond ``max_distance_m`` is reported with ``snapped=False``,
19
+ ``edge_id``/``measure`` null and its original geometry — only
20
+ ``offset_distance`` records how far its nearest line lies, so the consumer
21
+ can decide what to do. A null/empty input geometry has no nearest line and
22
+ stays ``edge_id=None``, ``snapped=False`` with its geometry untouched.
23
+
24
+ Candidate lines are found through a :class:`~gispulse.core.spatial_index.SpatialIndex`
25
+ (STRtree, no O(n·m) scan); each point projects directly onto its segment.
26
+ Ties — several lines exactly equidistant from one point — break
27
+ deterministically on the smallest ``edge_id`` so the same inputs always
28
+ produce the same outputs.
29
+
30
+ All metric quantities (``measure``, ``offset_distance``, ``max_distance_m``)
31
+ are computed in ``crs_meters`` (a projected CRS); inputs in another CRS are
32
+ reprojected in and the result is reprojected back to the points' original
33
+ CRS. ``crs_meters`` must be a metric/projected CRS — never feed it degrees.
34
+
35
+ This capability is deliberately **generic**: it carries no business notion
36
+ (site, fibre edge, river reach…). Accidents on a road network, meters on a
37
+ utility line and discharges on a watercourse are all the same operation —
38
+ project points onto identified lines — and none is special-cased here.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import geopandas as gpd
44
+ import numpy as np
45
+
46
+ from gispulse.capabilities.base import Capability
47
+ from gispulse.capabilities.registry import register
48
+ from gispulse.core.spatial_index import SpatialIndex
49
+
50
+ # How many nearest lines to inspect per point when breaking ties. Genuine
51
+ # geometric ties (several lines exactly equidistant from one point) are rare;
52
+ # 8 candidates is ample headroom while keeping each lookup O(k·log n) — far
53
+ # cheaper (and bounded) than a radius query whose buffer could match the whole
54
+ # network for a far-off point.
55
+ _TIE_CANDIDATES = 8
56
+ # Distances within this many CRS units (meters) are treated as equal for the
57
+ # purpose of tie-breaking on edge_id.
58
+ _TIE_TOL_M = 1e-9
59
+
60
+
61
+ def _id_sort_key(value: object) -> "tuple[int, object]":
62
+ """Deterministic, total ordering key for tie-breaking on edge ids.
63
+
64
+ Real ids in a single ``ref_id_col`` are homogeneous (all ints or all
65
+ strings), where this reduces to natural order. The two-level key only
66
+ guards the pathological mixed-type column so ``min`` never raises and the
67
+ outcome stays reproducible.
68
+ """
69
+ if isinstance(value, bool): # bool is an int subclass — keep it on the str side
70
+ return (1, str(value))
71
+ if isinstance(value, (int, float)):
72
+ return (0, value)
73
+ return (1, str(value))
74
+
75
+
76
+ @register
77
+ class SnapPointsToLinesCapability(Capability):
78
+ """Projects points onto a line network, tagging each with its edge id."""
79
+
80
+ name = "snap_points_to_lines"
81
+ description = (
82
+ "Snaps each point to the nearest line, adding edge_id (from "
83
+ "ref_id_col), measure, offset_distance and a snapped flag; geometry "
84
+ "becomes the projected point on the nearest line."
85
+ )
86
+
87
+ def execute(
88
+ self,
89
+ gdf: gpd.GeoDataFrame,
90
+ ref_gdf: gpd.GeoDataFrame | None = None,
91
+ ref_id_col: str | None = None,
92
+ max_distance_m: float | None = None,
93
+ crs_meters: str = "EPSG:3857",
94
+ edge_id_col: str = "edge_id",
95
+ measure_col: str = "measure",
96
+ offset_col: str = "offset_distance",
97
+ snapped_col: str = "snapped",
98
+ **_,
99
+ ) -> gpd.GeoDataFrame:
100
+ """Project each point onto its nearest reference line.
101
+
102
+ Args:
103
+ gdf: Input points (non-points use their centroid). All
104
+ input columns are preserved on the output.
105
+ ref_gdf: Reference line layer (injected via ``ref_layer``).
106
+ ref_id_col: Column on ``ref_gdf`` providing the stable
107
+ ``edge_id``. **Required** — its absence raises a
108
+ clear ``ValueError`` rather than silently falling
109
+ back to a positional index.
110
+ max_distance_m: Snap threshold in meters. A point whose nearest
111
+ line is farther than this is left unsnapped:
112
+ ``snapped=False``, ``edge_id``/``measure`` null
113
+ and the original geometry kept, with only
114
+ ``offset_distance`` reporting the true nearest
115
+ distance. ``None`` snaps every point to its
116
+ nearest line.
117
+ crs_meters: Metric (projected) CRS used for all distances.
118
+ edge_id_col: Output column for the matched edge id.
119
+ measure_col: Output column for the along-line measure (m).
120
+ offset_col: Output column for the perpendicular offset (m).
121
+ snapped_col: Output boolean column.
122
+
123
+ Returns:
124
+ Copy of ``gdf`` (input columns intact) with the four columns added
125
+ and ``geometry`` replaced by the projected point, in the input CRS.
126
+
127
+ Raises:
128
+ ValueError: if ``ref_gdf`` is missing/empty, or if ``ref_id_col``
129
+ is not a column of ``ref_gdf``.
130
+ """
131
+ if ref_gdf is None or ref_gdf.empty:
132
+ raise ValueError(
133
+ "snap_points_to_lines requires a non-empty reference line layer "
134
+ "(ref_gdf). Pass the lines to project the points onto."
135
+ )
136
+ if ref_id_col is None or ref_id_col not in ref_gdf.columns:
137
+ raise ValueError(
138
+ "snap_points_to_lines: ref_id_col="
139
+ f"{ref_id_col!r} is not a column of ref_gdf "
140
+ f"(available columns: {list(ref_gdf.columns)}). "
141
+ "Pass ref_id_col=<the stable id column on your lines> so every "
142
+ "snapped point can carry a durable edge_id."
143
+ )
144
+
145
+ original_crs = gdf.crs
146
+ left = gdf.to_crs(crs_meters) if original_crs is not None else gdf.copy()
147
+ right = (
148
+ ref_gdf.to_crs(crs_meters)
149
+ if ref_gdf.crs is not None and str(ref_gdf.crs) != str(crs_meters)
150
+ else ref_gdf.copy()
151
+ )
152
+ right = right.reset_index(drop=True)
153
+
154
+ line_geoms = list(right.geometry)
155
+ ref_ids = list(right[ref_id_col])
156
+ index = SpatialIndex(line_geoms)
157
+
158
+ n = len(left)
159
+ edge_ids: list[object] = [None] * n
160
+ measures = np.full(n, np.nan, dtype=float)
161
+ offsets = np.full(n, np.nan, dtype=float)
162
+ snapped = np.zeros(n, dtype=bool)
163
+ new_geoms = list(left.geometry)
164
+
165
+ for row_i, geom in enumerate(left.geometry):
166
+ if geom is None or geom.is_empty:
167
+ continue
168
+ pt = geom if geom.geom_type == "Point" else geom.centroid
169
+
170
+ pos = self._nearest_line(index, line_geoms, ref_ids, pt)
171
+ if pos is None: # ref layer had no usable geometry
172
+ continue
173
+ line = line_geoms[pos]
174
+ offset = line.distance(pt)
175
+ offsets[row_i] = float(offset) # nearest distance is always reported
176
+ if max_distance_m is not None and offset > max_distance_m:
177
+ # Beyond the threshold: leave edge_id/measure null and the
178
+ # original geometry in place (snapped stays False).
179
+ continue
180
+ measure = line.project(pt)
181
+ measures[row_i] = float(measure)
182
+ edge_ids[row_i] = ref_ids[pos]
183
+ new_geoms[row_i] = line.interpolate(measure)
184
+ snapped[row_i] = True
185
+
186
+ out = left.copy()
187
+ out[edge_id_col] = edge_ids
188
+ out[measure_col] = measures
189
+ out[offset_col] = offsets
190
+ out[snapped_col] = snapped
191
+ out = out.set_geometry(
192
+ gpd.GeoSeries(new_geoms, index=out.index, crs=crs_meters)
193
+ )
194
+ if original_crs is not None:
195
+ out = out.to_crs(original_crs)
196
+ return out.reset_index(drop=True)
197
+
198
+ @staticmethod
199
+ def _nearest_line(
200
+ index: SpatialIndex,
201
+ line_geoms: list,
202
+ ref_ids: list,
203
+ pt,
204
+ ) -> "int | None":
205
+ """Position of the nearest line to ``pt``, ties broken by smallest id.
206
+
207
+ Inspects the ``_TIE_CANDIDATES`` nearest lines (O(k·log n)), keeps
208
+ those within ``_TIE_TOL_M`` of the minimum distance, and returns the
209
+ one with the smallest ``edge_id`` — a stable, deterministic choice.
210
+ """
211
+ near = index.nearest(pt, k=min(_TIE_CANDIDATES, len(line_geoms)))
212
+ if not near:
213
+ return None
214
+ dists = {pos: line_geoms[pos].distance(pt) for pos in near}
215
+ best_dist = min(dists.values())
216
+ tied = [pos for pos, d in dists.items() if d <= best_dist + _TIE_TOL_M]
217
+ return min(tied, key=lambda pos: _id_sort_key(ref_ids[pos]))
218
+
219
+ def get_schema(self) -> dict:
220
+ return {
221
+ "type": "object",
222
+ "properties": {
223
+ "ref_layer": {
224
+ "type": "string",
225
+ "description": "Reference line layer to snap onto.",
226
+ },
227
+ "ref_id_col": {
228
+ "type": "string",
229
+ "description": (
230
+ "Required. Column on the lines providing the stable "
231
+ "edge_id; its absence raises a ValueError."
232
+ ),
233
+ },
234
+ "max_distance_m": {
235
+ "type": ["number", "null"],
236
+ "description": (
237
+ "Snap threshold (m). Beyond it, snapped=False with "
238
+ "edge_id/measure null and the original geometry kept "
239
+ "(only offset_distance reported). None snaps every "
240
+ "point to its nearest line."
241
+ ),
242
+ },
243
+ "crs_meters": {"type": "string", "default": "EPSG:3857"},
244
+ "edge_id_col": {"type": "string", "default": "edge_id"},
245
+ "measure_col": {"type": "string", "default": "measure"},
246
+ "offset_col": {"type": "string", "default": "offset_distance"},
247
+ "snapped_col": {"type": "string", "default": "snapped"},
248
+ },
249
+ # ``ref_id_col`` is genuinely required and is not plumbing, so it
250
+ # can validate early. ``ref_layer`` must NOT appear in ``required``:
251
+ # it is pipeline plumbing (resolved to ``ref_gdf`` and stripped
252
+ # before schema validation), so requiring it would fail every v2
253
+ # call before the capability runs. The runtime still raises a clear
254
+ # ValueError when ``ref_gdf`` itself is missing.
255
+ "required": ["ref_id_col"],
256
+ }
257
+
258
+
259
+ __all__ = ["SnapPointsToLinesCapability"]
@@ -19,8 +19,14 @@ from the ``GISPULSE_DATAMARTS`` environment variable (a JSON object), e.g.::
19
19
 
20
20
  GISPULSE_DATAMARTS='{
21
21
  "referentiel": {"location": "s3://bucket/marts/referentiel"},
22
- "local": {"location": "/data/marts/local", "table_pattern": "{table}.parquet"}
22
+ "local": {"location": "/data/marts/local", "table_pattern": "{table}.parquet"},
23
+ "warehouse": {"location": "/data/marts/warehouse.duckdb", "kind": "duckdb"}
23
24
  }'
25
+
26
+ A ``kind: "duckdb"`` mart points :attr:`Datamart.location` at a single
27
+ DuckDB database file; ``datamart://warehouse/<table>`` then reads the table
28
+ of that name from inside the file (attached read-only, bbox push-down still
29
+ applies via an ``ST_Intersects`` predicate on a ``geom`` column).
24
30
  """
25
31
 
26
32
  from __future__ import annotations
@@ -42,17 +48,33 @@ DATAMARTS_ENV = "GISPULSE_DATAMARTS"
42
48
  DATAMART_SCHEME = "datamart"
43
49
 
44
50
 
51
+ #: Supported datamart backends.
52
+ DATAMART_KINDS = ("parquet", "duckdb")
53
+
54
+
45
55
  @dataclass(frozen=True)
46
56
  class Datamart:
47
57
  """Declaration of one curated table store.
48
58
 
59
+ Two backends are supported, selected by :attr:`kind`:
60
+
61
+ * ``"parquet"`` (default) — *location* is a directory / ``s3://`` /
62
+ ``https://`` prefix holding **one file per table**; the file is
63
+ addressed by joining *location* with *table_pattern* and read as an
64
+ ordinary source (DuckDB-scannable, bbox push-down applies).
65
+ * ``"duckdb"`` — *location* is the path to a **single DuckDB database
66
+ file** (``.duckdb``); a table is a relation *inside* that file, read by
67
+ attaching the database read-only and selecting the table.
68
+
49
69
  Args:
50
70
  name: Logical mart name used in ``datamart://<name>/…``.
51
- location: Base location of the tables a directory path or
52
- an ``s3://`` / ``https://`` prefix. Joined with
53
- ``table_pattern`` to address a single table.
54
- table_pattern: Filename pattern for a table, ``{table}`` being the
55
- table name. Defaults to ``"{table}.parquet"``.
71
+ location: Base location: a directory / ``s3://`` / ``https://``
72
+ prefix (``parquet``), or the path to a ``.duckdb``
73
+ file (``duckdb``).
74
+ table_pattern: Filename pattern for a ``parquet`` table, ``{table}``
75
+ being the table name. Defaults to ``"{table}.parquet"``.
76
+ Ignored for ``duckdb`` marts.
77
+ kind: Backend, ``"parquet"`` (default) or ``"duckdb"``.
56
78
  crs: Optional CRS to force on tables that declare none.
57
79
  metadata: Free-form descriptive metadata.
58
80
  """
@@ -60,13 +82,31 @@ class Datamart:
60
82
  name: str
61
83
  location: str
62
84
  table_pattern: str = "{table}.parquet"
85
+ kind: str = "parquet"
63
86
  crs: str | None = None
64
87
  metadata: dict[str, Any] = field(default_factory=dict)
65
88
 
89
+ def __post_init__(self) -> None:
90
+ if self.kind not in DATAMART_KINDS:
91
+ raise ValueError(
92
+ f"datamart '{self.name}': unknown kind {self.kind!r} "
93
+ f"(expected one of {DATAMART_KINDS})"
94
+ )
95
+
66
96
  def uri_for(self, table: str) -> str:
67
- """Return the concrete source URI for *table* in this mart."""
97
+ """Return the concrete source URI for *table* (``parquet`` marts).
98
+
99
+ Raises:
100
+ ValueError: for a ``duckdb`` mart — a table there is not a file
101
+ URI; resolve it via the loader's DuckDB-table path instead.
102
+ """
68
103
  if not table:
69
104
  raise ValueError(f"datamart '{self.name}': empty table name")
105
+ if self.kind != "parquet":
106
+ raise ValueError(
107
+ f"datamart '{self.name}': uri_for is only valid for parquet "
108
+ f"marts, not kind={self.kind!r}"
109
+ )
70
110
  filename = self.table_pattern.format(table=table)
71
111
  base = self.location.rstrip("/")
72
112
  return f"{base}/{filename}"
@@ -141,13 +181,19 @@ class DatamartRegistry:
141
181
  if not isinstance(spec, dict) or "location" not in spec:
142
182
  log.warning("datamart_env_bad_decl", name=name)
143
183
  continue
144
- self._marts[name] = Datamart(
145
- name=name,
146
- location=str(spec["location"]),
147
- table_pattern=str(spec.get("table_pattern", "{table}.parquet")),
148
- crs=spec.get("crs"),
149
- metadata=dict(spec.get("metadata") or {}),
150
- )
184
+ try:
185
+ self._marts[name] = Datamart(
186
+ name=name,
187
+ location=str(spec["location"]),
188
+ table_pattern=str(
189
+ spec.get("table_pattern", "{table}.parquet")
190
+ ),
191
+ kind=str(spec.get("kind", "parquet")),
192
+ crs=spec.get("crs"),
193
+ metadata=dict(spec.get("metadata") or {}),
194
+ )
195
+ except ValueError as exc:
196
+ log.warning("datamart_env_bad_decl", name=name, error=str(exc))
151
197
 
152
198
 
153
199
  def parse_datamart_uri(uri: str) -> tuple[str, str]:
@@ -177,6 +223,7 @@ DATAMARTS = DatamartRegistry()
177
223
  __all__ = [
178
224
  "DATAMARTS",
179
225
  "DATAMARTS_ENV",
226
+ "DATAMART_KINDS",
180
227
  "DATAMART_SCHEME",
181
228
  "Datamart",
182
229
  "DatamartRegistry",
@@ -119,12 +119,30 @@ class LazyDataset:
119
119
  return _scan_to_gdf(self.scan_sql, crs=self.crs, bbox=bbox)
120
120
 
121
121
 
122
+ @dataclass(frozen=True)
123
+ class _DuckDBTableRef:
124
+ """Reference to a table living inside a single DuckDB database file.
125
+
126
+ Produced by :func:`resolve_source` for a ``duckdb``-kind datamart
127
+ (``datamart://<mart>/<table>`` where the mart's location is a
128
+ ``.duckdb`` file). Unlike a Parquet table it is not a file URI the
129
+ loader can hand to ``read_vector`` / a remote-table scan, so the loader
130
+ reads it via :func:`_duckdb_table_to_gdf` (attach read-only, select).
131
+ """
132
+
133
+ db_path: str
134
+ table: str
135
+ crs: str | None = None
136
+
137
+
122
138
  # ---------------------------------------------------------------------------
123
139
  # Source resolution
124
140
  # ---------------------------------------------------------------------------
125
141
 
126
142
 
127
- def resolve_source(source: "str | Path | AccessSpec | dict[str, Any]") -> "Path | AccessSpec":
143
+ def resolve_source(
144
+ source: "str | Path | AccessSpec | dict[str, Any]",
145
+ ) -> "Path | AccessSpec | _DuckDBTableRef":
128
146
  """Resolve a user-supplied source into a local path or an access spec.
129
147
 
130
148
  Accepted forms:
@@ -162,12 +180,19 @@ def resolve_source(source: "str | Path | AccessSpec | dict[str, Any]") -> "Path
162
180
  scheme = head.lower()
163
181
 
164
182
  # Datamart indirection: resolve datamart://<mart>/<table> to its
165
- # concrete source URI, then resolve that recursively.
183
+ # concrete source. A "parquet" mart yields a file URI resolved
184
+ # recursively; a "duckdb" mart yields a table reference inside a
185
+ # database file, read via _duckdb_table_to_gdf.
166
186
  if scheme == DATAMART_SCHEME:
167
187
  from gispulse.persistence.datamart import DATAMARTS, parse_datamart_uri
168
188
 
169
- mart, table = parse_datamart_uri(raw)
170
- return resolve_source(DATAMARTS.resolve(mart, table))
189
+ mart_name, table = parse_datamart_uri(raw)
190
+ mart = DATAMARTS.get(mart_name)
191
+ if mart.kind == "duckdb":
192
+ if not table:
193
+ raise ValueError(f"datamart '{mart_name}': empty table name")
194
+ return _DuckDBTableRef(db_path=mart.location, table=table, crs=mart.crs)
195
+ return resolve_source(mart.uri_for(table))
171
196
 
172
197
  # GeoNode: geonode://<instance>/<dataset> reads via the instance's
173
198
  # GeoServer WFS endpoint (reuses the OGC fetcher).
@@ -330,6 +355,52 @@ def _scan_to_gdf(
330
355
  return gdf
331
356
 
332
357
 
358
+ def _duckdb_table_to_gdf(
359
+ db_path: str, table: str, *, crs: str | None, bbox: BBox | None
360
+ ) -> gpd.GeoDataFrame:
361
+ """Read a table from a DuckDB database file into a GeoDataFrame.
362
+
363
+ Attaches *db_path* **read-only** (so concurrent readers don't contend on
364
+ a write lock) onto an ephemeral in-memory session, then selects *table*.
365
+ When *bbox* is given an ``ST_Intersects`` envelope predicate is pushed
366
+ into the query against a ``geom`` column (matching the convention of
367
+ :func:`_scan_to_gdf`).
368
+ """
369
+ from gispulse.persistence.duckdb_engine import DuckDBSession
370
+
371
+ # ATTACH cannot be wrapped in the SELECT sub-query DuckDBSession.sql
372
+ # builds, so run it separately on the connection, then SELECT the table
373
+ # — that SELECT *is* wrappable, so geometry/CRS decoding still works.
374
+ db_lit = db_path.replace("'", "''")
375
+ rel = f"_mart.{_quote_ident(table)}"
376
+ query = f"SELECT * FROM {rel}"
377
+ if bbox is not None:
378
+ minx, miny, maxx, maxy = bbox
379
+ query = (
380
+ f"SELECT * FROM {rel} "
381
+ f"WHERE ST_Intersects(geom, ST_MakeEnvelope("
382
+ f"{minx}, {miny}, {maxx}, {maxy}))"
383
+ )
384
+
385
+ session = DuckDBSession()
386
+ session.open()
387
+ try:
388
+ session.conn.execute(f"ATTACH '{db_lit}' AS _mart (READ_ONLY)")
389
+ gdf = session.sql(query)
390
+ finally:
391
+ session.close()
392
+
393
+ if crs and gdf.crs is None:
394
+ gdf = gdf.set_crs(crs)
395
+ return gdf
396
+
397
+
398
+ def _quote_ident(identifier: str) -> str:
399
+ """Quote a SQL identifier (double-quote, doubling embedded quotes)."""
400
+ escaped = identifier.replace('"', '""')
401
+ return f'"{escaped}"'
402
+
403
+
333
404
  # ---------------------------------------------------------------------------
334
405
  # Public entry point
335
406
  # ---------------------------------------------------------------------------
@@ -380,6 +451,15 @@ def load(
380
451
 
381
452
  resolved = resolve_source(source)
382
453
 
454
+ # --- DuckDB-file datamart table ---------------------------------------
455
+ if isinstance(resolved, _DuckDBTableRef):
456
+ if lazy:
457
+ log.debug("loader_lazy_ignored_for_duckdb_table", table=resolved.table)
458
+ gdf = _duckdb_table_to_gdf(
459
+ resolved.db_path, resolved.table, crs=crs or resolved.crs, bbox=bbox
460
+ )
461
+ return gdf
462
+
383
463
  # --- Local file path ---------------------------------------------------
384
464
  if isinstance(resolved, Path):
385
465
  if lazy:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gispulse
3
- Version: 2.2.0
3
+ Version: 2.2.2
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