jvspatial 0.2.3__tar.gz → 0.2.4__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 (223) hide show
  1. {jvspatial-0.2.3/jvspatial.egg-info → jvspatial-0.2.4}/PKG-INFO +1 -1
  2. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/__init__.py +9 -1
  3. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/object.py +9 -2
  4. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/walker.py +30 -5
  5. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/__init__.py +3 -0
  6. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/_cache.py +34 -0
  7. jvspatial-0.2.4/jvspatial/db/_graph_binding_postgres.py +120 -0
  8. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/_observable.py +48 -0
  9. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/database.py +88 -0
  10. jvspatial-0.2.4/jvspatial/db/graph_bindings.py +70 -0
  11. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/postgres.py +137 -0
  12. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/transaction.py +84 -6
  13. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/version.py +1 -1
  14. {jvspatial-0.2.3 → jvspatial-0.2.4/jvspatial.egg-info}/PKG-INFO +1 -1
  15. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial.egg-info/SOURCES.txt +2 -0
  16. {jvspatial-0.2.3 → jvspatial-0.2.4}/AUTHORS +0 -0
  17. {jvspatial-0.2.3 → jvspatial-0.2.4}/LICENSE +0 -0
  18. {jvspatial-0.2.3 → jvspatial-0.2.4}/README.md +0 -0
  19. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/__init__.py +0 -0
  20. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/_route_utils.py +0 -0
  21. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/_session_store.py +0 -0
  22. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/api_key_service.py +0 -0
  23. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/cleanup.py +0 -0
  24. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/config.py +0 -0
  25. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/enhanced.py +0 -0
  26. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/models.py +0 -0
  27. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/__init__.py +0 -0
  28. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/bridge.py +0 -0
  29. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/client_adapter.py +0 -0
  30. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/dcr.py +0 -0
  31. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/denylist.py +0 -0
  32. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/keys.py +0 -0
  33. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/metadata.py +0 -0
  34. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/models.py +0 -0
  35. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/refresh_store.py +0 -0
  36. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/requests.py +0 -0
  37. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/resource.py +0 -0
  38. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/revocation.py +0 -0
  39. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/routes.py +0 -0
  40. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/server.py +0 -0
  41. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/openapi_config.py +0 -0
  42. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/rbac.py +0 -0
  43. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/auth/service.py +0 -0
  44. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/__init__.py +0 -0
  45. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/app_builder.py +0 -0
  46. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/auth_configurator.py +0 -0
  47. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/auth_middleware.py +0 -0
  48. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/database_configurator.py +0 -0
  49. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/endpoint_auth_resolver.py +0 -0
  50. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/endpoint_manager.py +0 -0
  51. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/error_handler.py +0 -0
  52. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/logging_config.py +0 -0
  53. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/components/path_matcher.py +0 -0
  54. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/config.py +0 -0
  55. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/config_groups.py +0 -0
  56. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/constants.py +0 -0
  57. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/context.py +0 -0
  58. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/decorators/__init__.py +0 -0
  59. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/decorators/deferred_registry.py +0 -0
  60. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/decorators/field.py +0 -0
  61. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/decorators/function_wrappers.py +0 -0
  62. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/decorators/route.py +0 -0
  63. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/decorators/route_config.py +0 -0
  64. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/deferred_invoke_route.py +0 -0
  65. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/endpoints/__init__.py +0 -0
  66. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/endpoints/factory.py +0 -0
  67. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/endpoints/graph_visualization.py +0 -0
  68. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/endpoints/metadata.py +0 -0
  69. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/endpoints/registry.py +0 -0
  70. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/endpoints/response.py +0 -0
  71. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/endpoints/router.py +0 -0
  72. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/endpoints/walker_executor.py +0 -0
  73. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/exceptions.py +0 -0
  74. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/__init__.py +0 -0
  75. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/__init__.py +0 -0
  76. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/decorators.py +0 -0
  77. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/middleware.py +0 -0
  78. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/models.py +0 -0
  79. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/scheduler.py +0 -0
  80. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/storage/__init__.py +0 -0
  81. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/storage/service.py +0 -0
  82. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/__init__.py +0 -0
  83. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/helpers.py +0 -0
  84. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/middleware.py +0 -0
  85. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/models.py +0 -0
  86. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/utils.py +0 -0
  87. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/webhook_auth.py +0 -0
  88. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/middleware/__init__.py +0 -0
  89. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/middleware/manager.py +0 -0
  90. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/middleware/rate_limit.py +0 -0
  91. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/middleware/rate_limit_backend.py +0 -0
  92. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/server.py +0 -0
  93. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/server_app_factory.py +0 -0
  94. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/server_configurator.py +0 -0
  95. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/server_lifecycle.py +0 -0
  96. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/server_registration.py +0 -0
  97. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/server_run.py +0 -0
  98. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/services/__init__.py +0 -0
  99. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/services/discovery.py +0 -0
  100. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/services/file_storage.py +0 -0
  101. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/services/lifecycle.py +0 -0
  102. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/utils/__init__.py +0 -0
  103. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/utils/path_utils.py +0 -0
  104. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/api/utils/reload.py +0 -0
  105. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/async_utils/__init__.py +0 -0
  106. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/cache/__init__.py +0 -0
  107. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/cache/base.py +0 -0
  108. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/cache/factory.py +0 -0
  109. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/cache/layered.py +0 -0
  110. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/cache/memory.py +0 -0
  111. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/cache/redis.py +0 -0
  112. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/cli.py +0 -0
  113. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/__init__.py +0 -0
  114. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/annotations.py +0 -0
  115. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/context.py +0 -0
  116. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/decorators.py +0 -0
  117. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/__init__.py +0 -0
  118. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/_visit_hooks.py +0 -0
  119. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/edge.py +0 -0
  120. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/node.py +0 -0
  121. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/node_query.py +0 -0
  122. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/root.py +0 -0
  123. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/__init__.py +0 -0
  124. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/event_system.py +0 -0
  125. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/protection.py +0 -0
  126. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/trail_store.py +0 -0
  127. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/walker_queue.py +0 -0
  128. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/walker_trail.py +0 -0
  129. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/events.py +0 -0
  130. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/graph.py +0 -0
  131. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/graph_context_config.py +0 -0
  132. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/graph_expansion.py +0 -0
  133. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/graph_payload.py +0 -0
  134. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/graph_projection.py +0 -0
  135. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/migrations.py +0 -0
  136. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/mixins/__init__.py +0 -0
  137. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/mixins/deferred_save.py +0 -0
  138. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/pager.py +0 -0
  139. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/utils.py +0 -0
  140. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/core/validate.py +0 -0
  141. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/_atomic.py +0 -0
  142. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/_path_locks.py +0 -0
  143. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/_postgres_translate.py +0 -0
  144. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/_sqlite_translate.py +0 -0
  145. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/dynamodb.py +0 -0
  146. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/factory.py +0 -0
  147. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/jsondb.py +0 -0
  148. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/manager.py +0 -0
  149. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/mongodb.py +0 -0
  150. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/query.py +0 -0
  151. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/sqlite.py +0 -0
  152. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/db/work_claim.py +0 -0
  153. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/dev_tools/__init__.py +0 -0
  154. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/env.py +0 -0
  155. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/env_adapter.py +0 -0
  156. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/exceptions.py +0 -0
  157. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/logging/__init__.py +0 -0
  158. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/logging/config.py +0 -0
  159. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/logging/custom_levels.py +0 -0
  160. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/logging/endpoints.py +0 -0
  161. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/logging/filter_utils.py +0 -0
  162. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/logging/handler.py +0 -0
  163. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/logging/models.py +0 -0
  164. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/logging/service.py +0 -0
  165. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/memory/__init__.py +0 -0
  166. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/observability/__init__.py +0 -0
  167. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/observability/metrics.py +0 -0
  168. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/observability/otel.py +0 -0
  169. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/observability/tracing.py +0 -0
  170. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/profiling/__init__.py +0 -0
  171. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/py.typed +0 -0
  172. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/runtime/__init__.py +0 -0
  173. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/runtime/eventbridge_readiness.py +0 -0
  174. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/runtime/lwa.py +0 -0
  175. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/runtime/serverless.py +0 -0
  176. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/__init__.py +0 -0
  177. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/deferred_invoke.py +0 -0
  178. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/factory.py +0 -0
  179. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/tasks/__init__.py +0 -0
  180. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/tasks/aws_lambda.py +0 -0
  181. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/tasks/aws_sqs.py +0 -0
  182. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/tasks/base.py +0 -0
  183. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/tasks/stub.py +0 -0
  184. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/serverless/tasks/sync.py +0 -0
  185. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/__init__.py +0 -0
  186. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/exceptions.py +0 -0
  187. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/interfaces/__init__.py +0 -0
  188. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/interfaces/_stream.py +0 -0
  189. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/interfaces/base.py +0 -0
  190. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/interfaces/local.py +0 -0
  191. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/interfaces/s3.py +0 -0
  192. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/internal_markers.py +0 -0
  193. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/managers/__init__.py +0 -0
  194. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/managers/proxy.py +0 -0
  195. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/models.py +0 -0
  196. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/security/__init__.py +0 -0
  197. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/security/path_sanitizer.py +0 -0
  198. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/storage/security/validator.py +0 -0
  199. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/testing/__init__.py +0 -0
  200. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/__init__.py +0 -0
  201. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/context.py +0 -0
  202. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/decorators.py +0 -0
  203. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/deprecation.py +0 -0
  204. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/factory.py +0 -0
  205. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/normalization.py +0 -0
  206. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/retry.py +0 -0
  207. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/serialization.py +0 -0
  208. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/stability.py +0 -0
  209. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/types.py +0 -0
  210. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial/utils/validation.py +0 -0
  211. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial.egg-info/dependency_links.txt +0 -0
  212. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial.egg-info/entry_points.txt +0 -0
  213. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial.egg-info/requires.txt +0 -0
  214. {jvspatial-0.2.3 → jvspatial-0.2.4}/jvspatial.egg-info/top_level.txt +0 -0
  215. {jvspatial-0.2.3 → jvspatial-0.2.4}/pyproject.toml +0 -0
  216. {jvspatial-0.2.3 → jvspatial-0.2.4}/setup.cfg +0 -0
  217. {jvspatial-0.2.3 → jvspatial-0.2.4}/tests/test_config.py +0 -0
  218. {jvspatial-0.2.3 → jvspatial-0.2.4}/tests/test_create_task.py +0 -0
  219. {jvspatial-0.2.3 → jvspatial-0.2.4}/tests/test_env.py +0 -0
  220. {jvspatial-0.2.3 → jvspatial-0.2.4}/tests/test_env_adapter_eventbridge_validation.py +0 -0
  221. {jvspatial-0.2.3 → jvspatial-0.2.4}/tests/test_env_adapter_postgres.py +0 -0
  222. {jvspatial-0.2.3 → jvspatial-0.2.4}/tests/test_node_operations.py +0 -0
  223. {jvspatial-0.2.3 → jvspatial-0.2.4}/tests/test_normalize_optional_secret.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jvspatial
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: An asynchronous object-spatial Python library for persistence and business logic application layers.
5
5
  Author-email: "TrueSelph Inc." <adminh@trueselph.com>
6
6
  License: MIT
@@ -87,7 +87,13 @@ from .core.mixins import (
87
87
  )
88
88
 
89
89
  # Simplified database and cache
90
- from .db import Database, InsertIfAbsentResult, create_database
90
+ from .db import (
91
+ Database,
92
+ GraphBindingSummary,
93
+ InsertIfAbsentResult,
94
+ LiteralBindingFact,
95
+ create_database,
96
+ )
91
97
  from .db.work_claim import claim_record, delete_claimed_record, release_claim
92
98
 
93
99
  # Observability primitives
@@ -165,6 +171,8 @@ __all__ = [
165
171
  # Database & Cache
166
172
  "Database",
167
173
  "InsertIfAbsentResult",
174
+ "GraphBindingSummary",
175
+ "LiteralBindingFact",
168
176
  "create_database",
169
177
  "create_cache",
170
178
  # Observability
@@ -950,9 +950,16 @@ class Object(AttributeMixin, BaseModel):
950
950
  # Get compound indexes from class decorators
951
951
  compound_indexes = get_compound_indexes(cls)
952
952
  for comp_index in compound_indexes:
953
- # Map field names to context.field_name
953
+ # Identity/discriminator live at the record root, not in context.
954
954
  mapped_fields = [
955
- (f"context.{field_name}", direction)
955
+ (
956
+ (
957
+ field_name
958
+ if field_name in {"id", "entity"}
959
+ else f"context.{field_name}"
960
+ ),
961
+ direction,
962
+ )
956
963
  for field_name, direction in comp_index["fields"]
957
964
  ]
958
965
  entry: Dict[str, Any] = {
@@ -30,6 +30,26 @@ from .walker_components.walker_queue import WalkerQueue
30
30
  from .walker_components.walker_trail import WalkerTrail
31
31
 
32
32
 
33
+ def _class_functions(cls: type):
34
+ """Match function-member discovery without evaluating class descriptors.
35
+
36
+ Resolve the MRO on each call so runtime method replacements remain visible.
37
+ Unlike inspect.getmembers this does not force Pydantic's lazy signature
38
+ descriptor while looking for event/exit hook metadata.
39
+ """
40
+ members = {}
41
+ for base in cls.__mro__:
42
+ for name, value in vars(base).items():
43
+ if name not in members:
44
+ members[name] = value
45
+ for name in sorted(members):
46
+ value = members[name]
47
+ if isinstance(value, staticmethod):
48
+ value = value.__func__
49
+ if inspect.isfunction(value):
50
+ yield name, value
51
+
52
+
33
53
  class WalkerVisitingContext:
34
54
  """Context manager for visiting a node with a walker."""
35
55
 
@@ -133,12 +153,15 @@ class Walker(AttributeMixin, BaseModel):
133
153
  protected=True, transient=True, description="Unique identifier for the walker"
134
154
  )
135
155
 
156
+ # Python factories avoid repeated builtin-signature parsing by Pydantic
157
+ # during private-state initialization, especially on Python 3.14. Each
158
+ # invocation still allocates fresh mutable state.
136
159
  # Reporting system
137
- _report: List[Any] = attribute(private=True, default_factory=list)
160
+ _report: List[Any] = attribute(private=True, default_factory=lambda: [])
138
161
 
139
162
  # Event system
140
163
  _event_handlers: Dict[str, List[Callable]] = attribute(
141
- private=True, default_factory=dict
164
+ private=True, default_factory=lambda: {}
142
165
  )
143
166
 
144
167
  # Walker core attributes
@@ -153,7 +176,9 @@ class Walker(AttributeMixin, BaseModel):
153
176
  # Trail tracking. Default factory creates an unbounded trail; Walker
154
177
  # ``__init__`` rebuilds with ``max_trail_length`` from kwargs/env so
155
178
  # SPEC §6.4 ``max_trail_length`` is actually wired (audit §2.3).
156
- _trail_tracker: WalkerTrail = attribute(private=True, default_factory=WalkerTrail)
179
+ _trail_tracker: WalkerTrail = attribute(
180
+ private=True, default_factory=lambda: WalkerTrail()
181
+ )
157
182
 
158
183
  # Trail-related methods (sync for pure computations, async for I/O)
159
184
  def get_trail(self) -> List[str]:
@@ -502,7 +527,7 @@ class Walker(AttributeMixin, BaseModel):
502
527
 
503
528
  def _register_event_handlers(self):
504
529
  """Register all @on_emit methods for event handling."""
505
- for _name, method in inspect.getmembers(self.__class__, inspect.isfunction):
530
+ for _name, method in _class_functions(self.__class__):
506
531
  if hasattr(method, "_is_event_handler"):
507
532
  event_types = getattr(method, "_event_types", [])
508
533
  for event_type in event_types:
@@ -1033,7 +1058,7 @@ class Walker(AttributeMixin, BaseModel):
1033
1058
 
1034
1059
  async def _execute_exit_hooks(self) -> None:
1035
1060
  """Execute all @on_exit decorated methods."""
1036
- for _name, method in inspect.getmembers(self.__class__, inspect.isfunction):
1061
+ for _name, method in _class_functions(self.__class__):
1037
1062
  if hasattr(method, "_on_exit") and method._on_exit:
1038
1063
  try:
1039
1064
  if asyncio.iscoroutinefunction(method):
@@ -22,6 +22,7 @@ from .factory import (
22
22
  unregister_database,
23
23
  unregister_database_type,
24
24
  )
25
+ from .graph_bindings import GraphBindingSummary, LiteralBindingFact
25
26
  from .jsondb import JsonDB
26
27
  from .manager import DatabaseManager, get_database_manager, set_database_manager
27
28
  from .query import escape_regex
@@ -60,6 +61,8 @@ except ImportError: # pragma: no cover - postgres optional
60
61
 
61
62
  __all__ = [
62
63
  "Database",
64
+ "GraphBindingSummary",
65
+ "LiteralBindingFact",
63
66
  "DatabaseError",
64
67
  "VersionConflictError",
65
68
  "InsertIfAbsentResult",
@@ -214,6 +214,40 @@ class CachingDatabase(Database):
214
214
  # find() is intentionally NOT cached. See module docstring.
215
215
  return await self.inner.find(collection, query, limit=limit, sort=sort)
216
216
 
217
+ async def summarize_graph_bindings(self, query, **kwargs):
218
+ """Forward current graph facts without caching a summary as authority."""
219
+ reader = getattr(self.inner, "summarize_graph_bindings", None)
220
+ return await reader(query, **kwargs) if callable(reader) else None
221
+
222
+ @property
223
+ def supports_native_graph_binding_summary(self) -> bool:
224
+ """Expose the current underlying adapter's optional aggregate support."""
225
+ return bool(getattr(self.inner, "supports_native_graph_binding_summary", False))
226
+
227
+ async def find_projected(
228
+ self,
229
+ collection: str,
230
+ query: Dict[str, Any],
231
+ *,
232
+ fields: List[str],
233
+ limit: Optional[int] = None,
234
+ sort: Optional[List[Tuple[str, int]]] = None,
235
+ ) -> List[Dict[str, Any]]:
236
+ """Forward current projections; never cache incomplete entity records."""
237
+ reader = getattr(self.inner, "find_projected", None)
238
+ if callable(reader):
239
+ return await reader(
240
+ collection, query, fields=fields, limit=limit, sort=sort
241
+ )
242
+ return await Database.find_projected(
243
+ self.inner, collection, query, fields=fields, limit=limit, sort=sort
244
+ )
245
+
246
+ @property
247
+ def supports_native_projected_reads(self) -> bool:
248
+ """Expose native projection support without promising a complete entity."""
249
+ return bool(getattr(self.inner, "supports_native_projected_reads", False))
250
+
217
251
  async def graph_edge_page(
218
252
  self, node_id: str, *, after: Optional[str] = None, limit: int = 50
219
253
  ) -> List[Dict[str, Any]]:
@@ -0,0 +1,120 @@
1
+ """Native one-hop binding summary; policy belongs to the consuming host."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Optional, Tuple
5
+
6
+ from jvspatial.db.graph_bindings import GraphBindingSummary, LiteralBindingFact
7
+
8
+
9
+ async def summarize(
10
+ database: Any,
11
+ query: Dict[str, Any],
12
+ *,
13
+ record_entity: str,
14
+ edge_entity: str,
15
+ parent_id: str,
16
+ facts: Tuple[LiteralBindingFact, ...],
17
+ max_records: int = 100000,
18
+ max_edges: int = 400000,
19
+ ) -> Optional[GraphBindingSummary]:
20
+ """Aggregate requested storage facts in one bounded PostgreSQL statement."""
21
+ from jvspatial.db._postgres_translate import translate_query
22
+ from jvspatial.db.postgres import _SAFE_IDENT_RE, _safe_collection
23
+
24
+ if (
25
+ not all(
26
+ isinstance(value, str) and value
27
+ for value in (record_entity, edge_entity, parent_id)
28
+ )
29
+ or not isinstance(facts, tuple)
30
+ or not all(isinstance(fact, LiteralBindingFact) for fact in facts)
31
+ or any(
32
+ type(value) is not int or value <= 0 for value in (max_records, max_edges)
33
+ )
34
+ ):
35
+ raise ValueError("Invalid graph binding summary request")
36
+ translated = translate_query(query)
37
+ if translated is None:
38
+ return None
39
+ where, values = translated
40
+ params = list(values)
41
+
42
+ def bind(value: Any) -> str:
43
+ params.append(value)
44
+ return f"${len(params)}"
45
+
46
+ record_param, edge_param, parent_param = (
47
+ bind(record_entity),
48
+ bind(edge_entity),
49
+ bind(parent_id),
50
+ )
51
+ constraints = [
52
+ "jsonb_typeof(data->'context')='object'",
53
+ "data->'id'=to_jsonb(id)",
54
+ "id<>''",
55
+ f"data->'entity'=to_jsonb({record_param}::text)",
56
+ ]
57
+ for fact in facts:
58
+ paths = [fact.path] + ([fact.legacy_path] if fact.legacy_path else [])
59
+ if any(
60
+ not _SAFE_IDENT_RE.fullmatch(segment)
61
+ for path in paths
62
+ for segment in path.split(".")
63
+ ):
64
+ return None
65
+ canonical = "(data#>'{" + ",".join(fact.path.split(".")) + "}')"
66
+ legacy = (
67
+ ("(data#>'{" + ",".join(fact.legacy_path.split(".")) + "}')")
68
+ if fact.legacy_path
69
+ else None
70
+ )
71
+ chosen = f"COALESCE({canonical},{legacy})" if legacy else canonical
72
+ allowed = bind([json.dumps(value) for value in fact.allowed_values])
73
+ valid = f"({chosen}=ANY({allowed}::jsonb[]))"
74
+ if legacy:
75
+ valid = f"({valid} AND ({canonical} IS NULL OR {legacy} IS NULL OR {canonical}={legacy}))"
76
+ if not fact.required:
77
+ missing = (
78
+ f"({canonical} IS NULL AND {legacy} IS NULL)"
79
+ if legacy
80
+ else f"{canonical} IS NULL"
81
+ )
82
+ valid = f"({missing} OR {valid})"
83
+ constraints.append(valid)
84
+ limit_records, limit_edges = bind(max_records + 1), bind(max_edges + 1)
85
+ schema = _safe_collection(database.schema_name)
86
+ await database._bootstrap_collection("node")
87
+ await database._bootstrap_collection("edge")
88
+ node_where = f"entity={record_param}" + (f" AND ({where})" if where else "")
89
+ sql = f"""
90
+ WITH candidates AS MATERIALIZED (
91
+ SELECT id, COALESCE({' AND '.join(constraints)},FALSE) AS facts_valid
92
+ FROM {schema}.node WHERE {node_where} LIMIT {limit_records}
93
+ ), links AS NOT MATERIALIZED (
94
+ SELECT (e.data#>>'{{target}}') AS target, e.data->>'source'={parent_param} AS expected,
95
+ COALESCE(jsonb_typeof(e.data->'source')='string' AND e.data->>'source'<>''
96
+ AND jsonb_typeof(e.data->'target')='string'
97
+ AND jsonb_typeof(e.data->'entity')='string'
98
+ AND e.data->>'entity'={edge_param}, FALSE) AS valid
99
+ FROM {schema}.edge e JOIN candidates c ON (e.data#>>'{{target}}')=c.id
100
+ WHERE e.entity={edge_param} LIMIT {limit_edges}
101
+ ), parents AS (
102
+ SELECT target, count(*) AS cardinality, bool_and(expected) AS expected,
103
+ bool_or(NOT valid) AS invalid_edge FROM links GROUP BY target
104
+ )
105
+ SELECT count(*) AS matched,
106
+ count(*) FILTER(WHERE NOT facts_valid) AS invalid_facts,
107
+ count(*) FILTER(WHERE invalid_edge) AS invalid_edges,
108
+ count(*) FILTER(WHERE facts_valid AND NOT invalid_edge AND cardinality=1 AND expected) AS bound,
109
+ COALESCE(sum(cardinality),0) AS edge_count
110
+ FROM candidates LEFT JOIN parents ON parents.target=candidates.id
111
+ """
112
+ async with database._acquire_conn() as connection:
113
+ row = await connection.fetchrow(sql, *params)
114
+ return GraphBindingSummary(
115
+ matched=row["matched"],
116
+ bound=row["bound"],
117
+ invalid_facts=row["invalid_facts"],
118
+ invalid_edges=row["invalid_edges"],
119
+ complete=(row["matched"] <= max_records and row["edge_count"] <= max_edges),
120
+ )
@@ -309,6 +309,54 @@ class ObservableDatabase(Database):
309
309
  result_count_extractor=lambda r: len(r) if isinstance(r, list) else 0,
310
310
  )
311
311
 
312
+ async def summarize_graph_bindings(self, query, **kwargs):
313
+ """Observe one logical graph fact read, preserving upstream recording."""
314
+ reader = getattr(self.inner, "summarize_graph_bindings", None)
315
+ if not callable(reader):
316
+ return None
317
+ return await self._instrument(
318
+ "summarize_graph_bindings",
319
+ "node",
320
+ lambda: reader(query, **kwargs),
321
+ result_count_extractor=lambda result: (
322
+ result.bound if result is not None else 0
323
+ ),
324
+ )
325
+
326
+ @property
327
+ def supports_native_graph_binding_summary(self) -> bool:
328
+ """Expose the current underlying adapter's optional aggregate support."""
329
+ return bool(getattr(self.inner, "supports_native_graph_binding_summary", False))
330
+
331
+ async def find_projected(
332
+ self,
333
+ collection: str,
334
+ query: Dict[str, Any],
335
+ *,
336
+ fields: List[str],
337
+ limit: Optional[int] = None,
338
+ sort: Optional[List[Tuple[str, int]]] = None,
339
+ ) -> List[Dict[str, Any]]:
340
+ """Instrument a projection read without replacing upstream metrics."""
341
+ reader = getattr(self.inner, "find_projected", None)
342
+ return await self._instrument(
343
+ "find_projected",
344
+ collection,
345
+ lambda: (
346
+ reader(collection, query, fields=fields, limit=limit, sort=sort)
347
+ if callable(reader)
348
+ else Database.find_projected(
349
+ self.inner, collection, query, fields=fields, limit=limit, sort=sort
350
+ )
351
+ ),
352
+ result_count_extractor=lambda rows: len(rows),
353
+ )
354
+
355
+ @property
356
+ def supports_native_projected_reads(self) -> bool:
357
+ """Expose native projection support without promising a complete entity."""
358
+ return bool(getattr(self.inner, "supports_native_projected_reads", False))
359
+
312
360
  async def graph_edge_page(
313
361
  self, node_id: str, *, after: Optional[str] = None, limit: int = 50
314
362
  ) -> List[Dict[str, Any]]:
@@ -9,12 +9,48 @@ import json
9
9
  import logging
10
10
  from abc import ABC, abstractmethod
11
11
  from contextvars import ContextVar
12
+ from copy import deepcopy
12
13
  from dataclasses import dataclass, field
13
14
  from functools import partial
14
15
  from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, Union
15
16
 
17
+ from jvspatial.db.graph_bindings import GraphBindingSummary, LiteralBindingFact
16
18
  from jvspatial.db.query import QueryEngine
17
19
 
20
+
21
+ def projection_fields(fields: Any) -> List[str]:
22
+ """Validate and deduplicate literal dotted JSON paths."""
23
+ if not isinstance(fields, (list, tuple)) or not fields:
24
+ raise ValueError("projection fields must be a nonempty list or tuple")
25
+ if any(
26
+ not isinstance(path, str)
27
+ or not path
28
+ or any(not part for part in path.split("."))
29
+ for path in fields
30
+ ):
31
+ raise ValueError("projection paths must contain nonempty segments")
32
+ return list(dict.fromkeys(fields))
33
+
34
+
35
+ def project_record_fields(record: Dict[str, Any], fields: List[str]) -> Dict[str, Any]:
36
+ """Return flat path/value pairs, preserving explicit null versus missing."""
37
+ result = {}
38
+ for path in fields:
39
+ value = record
40
+ for part in path.split("."):
41
+ if isinstance(value, dict) and part in value:
42
+ value = value[part]
43
+ elif (
44
+ isinstance(value, list) and part.isdecimal() and int(part) < len(value)
45
+ ):
46
+ value = value[int(part)]
47
+ else:
48
+ break
49
+ else:
50
+ result[path] = deepcopy(value) if isinstance(value, (dict, list)) else value
51
+ return result
52
+
53
+
18
54
  # ---- cursor encoding -------------------------------------------------------
19
55
 
20
56
  # Opaque bytes that callers pass back to ``find_iter(cursor=...)`` to
@@ -370,6 +406,16 @@ class Database(ABC):
370
406
  supports_transactions: bool = False
371
407
  supports_native_graph_paging: bool = False
372
408
 
409
+ @property
410
+ def supports_native_projected_reads(self) -> bool:
411
+ """Whether projection is performed natively by the current adapter."""
412
+ return False
413
+
414
+ @property
415
+ def supports_native_graph_binding_summary(self) -> bool:
416
+ """Whether the current adapter provides native graph fact summaries."""
417
+ return False
418
+
373
419
  async def graph_node_batch(self, ids: List[str]) -> Dict[str, Dict[str, Any]]:
374
420
  """Fetch raw projection nodes; wrappers bypass stale record caches."""
375
421
  return await self.find_many("node", ids)
@@ -592,6 +638,48 @@ class Database(ABC):
592
638
  results = await self.find(collection, query)
593
639
  return len(results)
594
640
 
641
+ async def summarize_graph_bindings(
642
+ self,
643
+ query: Dict[str, Any],
644
+ *,
645
+ record_entity: str,
646
+ edge_entity: str,
647
+ parent_id: str,
648
+ facts: Tuple[LiteralBindingFact, ...],
649
+ max_records: int = 100000,
650
+ max_edges: int = 400000,
651
+ ) -> Optional[GraphBindingSummary]:
652
+ """Optional native fact aggregate; None means use the caller's fallback.
653
+
654
+ This does not authorize any read. A consuming host must prove its
655
+ principal, current scope and rooted parent independently. It must never
656
+ use an incomplete result as an exact count, or silently discard invalid
657
+ authority facts. Unsupported adapters retain their established executor.
658
+ """
659
+ return None
660
+
661
+ async def find_projected(
662
+ self,
663
+ collection: str,
664
+ query: Dict[str, Any],
665
+ *,
666
+ fields: List[str],
667
+ limit: Optional[int] = None,
668
+ sort: Optional[List[Tuple[str, int]]] = None,
669
+ ) -> List[Dict[str, Any]]:
670
+ """Read flat dotted-path projections after complete filtering/ordering.
671
+
672
+ Missing paths are omitted; explicit nulls remain. Returned maps are
673
+ projections, not complete persistence records or hydrated entities.
674
+ Adapters may push projection down only when filtering and ordering
675
+ semantics are preserved. The portable fallback projects after find().
676
+ """
677
+ names = projection_fields(fields)
678
+ return [
679
+ project_record_fields(row, names)
680
+ for row in await self.find(collection, query, limit=limit, sort=sort)
681
+ ]
682
+
595
683
  async def find_iter(
596
684
  self,
597
685
  collection: str,
@@ -0,0 +1,70 @@
1
+ """Read-only one-hop graph binding facts, without application access policy."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Tuple
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class LiteralBindingFact:
9
+ """An exact persisted literal with optional conflict-checked legacy storage.
10
+
11
+ Missing and JSON null differ. A present value must belong to allowed_values;
12
+ when both paths exist their JSON values must agree. This is a storage fact,
13
+ never an access decision or a hydrated-model default.
14
+ """
15
+
16
+ path: str
17
+ allowed_values: Tuple[str, ...]
18
+ legacy_path: Optional[str] = None
19
+ required: bool = True
20
+
21
+ def __post_init__(self) -> None:
22
+ """Reject ambiguous paths and mutable or malformed literal declarations."""
23
+ if not isinstance(self.path, str) or not self.path:
24
+ raise ValueError("A canonical fact path is required")
25
+ for path in (self.path, self.legacy_path):
26
+ if path is not None and (
27
+ not isinstance(path, str)
28
+ or not path
29
+ or any(not part for part in path.split("."))
30
+ ):
31
+ raise ValueError("Fact paths must contain nonempty segments")
32
+ if (
33
+ not isinstance(self.allowed_values, tuple)
34
+ or not self.allowed_values
35
+ or any(
36
+ not isinstance(value, str) or not value for value in self.allowed_values
37
+ )
38
+ ):
39
+ raise ValueError(
40
+ "allowed_values must be a nonempty tuple of nonempty strings"
41
+ )
42
+ if type(self.required) is not bool:
43
+ raise ValueError("required must be a bool")
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class GraphBindingSummary:
48
+ """Current matching records and unique-parent bindings in one store read.
49
+
50
+ Only complete results describe the full matching audience. Invalid facts or
51
+ edges remain explicit; callers decide whether to fail closed. No principal,
52
+ tenancy admission, rooted-ancestor proof or permission lease is supplied.
53
+ """
54
+
55
+ matched: int
56
+ bound: int
57
+ invalid_facts: int
58
+ invalid_edges: int
59
+ complete: bool
60
+
61
+ def __post_init__(self) -> None:
62
+ """Keep count and completeness types literal and cardinalities coherent."""
63
+ values = (self.matched, self.bound, self.invalid_facts, self.invalid_edges)
64
+ if any(type(value) is not int or value < 0 for value in values):
65
+ raise ValueError("Summary counts must be nonnegative integers")
66
+ if (
67
+ any(value > self.matched for value in values[1:])
68
+ or type(self.complete) is not bool
69
+ ):
70
+ raise ValueError("Invalid graph binding summary cardinality")