jvspatial 0.2.2__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.
- {jvspatial-0.2.2/jvspatial.egg-info → jvspatial-0.2.4}/PKG-INFO +3 -3
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/__init__.py +9 -1
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/node.py +3 -3
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/object.py +9 -2
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/walker.py +30 -5
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/__init__.py +3 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/_cache.py +34 -0
- jvspatial-0.2.4/jvspatial/db/_graph_binding_postgres.py +120 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/_observable.py +48 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/database.py +88 -0
- jvspatial-0.2.4/jvspatial/db/graph_bindings.py +70 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/postgres.py +137 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/transaction.py +84 -6
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/version.py +1 -1
- {jvspatial-0.2.2 → jvspatial-0.2.4/jvspatial.egg-info}/PKG-INFO +3 -3
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial.egg-info/SOURCES.txt +2 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial.egg-info/requires.txt +2 -2
- {jvspatial-0.2.2 → jvspatial-0.2.4}/pyproject.toml +2 -2
- {jvspatial-0.2.2 → jvspatial-0.2.4}/AUTHORS +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/LICENSE +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/README.md +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/_route_utils.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/_session_store.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/api_key_service.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/cleanup.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/config.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/enhanced.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/models.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/bridge.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/client_adapter.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/dcr.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/denylist.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/keys.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/metadata.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/models.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/refresh_store.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/requests.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/resource.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/revocation.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/routes.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/oauth/server.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/openapi_config.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/rbac.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/auth/service.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/app_builder.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/auth_configurator.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/auth_middleware.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/database_configurator.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/endpoint_auth_resolver.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/endpoint_manager.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/error_handler.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/logging_config.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/components/path_matcher.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/config.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/config_groups.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/constants.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/context.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/decorators/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/decorators/deferred_registry.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/decorators/field.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/decorators/function_wrappers.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/decorators/route.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/decorators/route_config.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/deferred_invoke_route.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/endpoints/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/endpoints/factory.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/endpoints/graph_visualization.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/endpoints/metadata.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/endpoints/registry.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/endpoints/response.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/endpoints/router.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/endpoints/walker_executor.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/exceptions.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/decorators.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/middleware.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/models.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/scheduler/scheduler.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/storage/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/storage/service.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/helpers.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/middleware.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/models.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/utils.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/integrations/webhooks/webhook_auth.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/middleware/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/middleware/manager.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/middleware/rate_limit.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/middleware/rate_limit_backend.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/server.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/server_app_factory.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/server_configurator.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/server_lifecycle.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/server_registration.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/server_run.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/services/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/services/discovery.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/services/file_storage.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/services/lifecycle.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/utils/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/utils/path_utils.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/api/utils/reload.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/async_utils/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/cache/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/cache/base.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/cache/factory.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/cache/layered.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/cache/memory.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/cache/redis.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/cli.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/annotations.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/context.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/decorators.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/_visit_hooks.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/edge.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/node_query.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/root.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/event_system.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/protection.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/trail_store.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/walker_queue.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/entities/walker_components/walker_trail.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/events.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/graph.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/graph_context_config.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/graph_expansion.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/graph_payload.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/graph_projection.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/migrations.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/mixins/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/mixins/deferred_save.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/pager.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/utils.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/core/validate.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/_atomic.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/_path_locks.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/_postgres_translate.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/_sqlite_translate.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/dynamodb.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/factory.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/jsondb.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/manager.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/mongodb.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/query.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/sqlite.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/db/work_claim.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/dev_tools/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/env.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/env_adapter.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/exceptions.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/logging/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/logging/config.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/logging/custom_levels.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/logging/endpoints.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/logging/filter_utils.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/logging/handler.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/logging/models.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/logging/service.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/memory/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/observability/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/observability/metrics.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/observability/otel.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/observability/tracing.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/profiling/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/py.typed +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/runtime/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/runtime/eventbridge_readiness.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/runtime/lwa.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/runtime/serverless.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/deferred_invoke.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/factory.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/tasks/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/tasks/aws_lambda.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/tasks/aws_sqs.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/tasks/base.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/tasks/stub.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/serverless/tasks/sync.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/exceptions.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/interfaces/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/interfaces/_stream.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/interfaces/base.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/interfaces/local.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/interfaces/s3.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/internal_markers.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/managers/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/managers/proxy.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/models.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/security/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/security/path_sanitizer.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/storage/security/validator.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/testing/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/__init__.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/context.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/decorators.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/deprecation.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/factory.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/normalization.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/retry.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/serialization.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/stability.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/types.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial/utils/validation.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial.egg-info/dependency_links.txt +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial.egg-info/entry_points.txt +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/jvspatial.egg-info/top_level.txt +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/setup.cfg +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/tests/test_config.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/tests/test_create_task.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/tests/test_env.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/tests/test_env_adapter_eventbridge_validation.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/tests/test_env_adapter_postgres.py +0 -0
- {jvspatial-0.2.2 → jvspatial-0.2.4}/tests/test_node_operations.py +0 -0
- {jvspatial-0.2.2 → 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
|
+
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
|
|
@@ -48,14 +48,14 @@ Provides-Extra: pgvector
|
|
|
48
48
|
Requires-Dist: asyncpg>=0.29.0; extra == "pgvector"
|
|
49
49
|
Requires-Dist: pgvector>=0.2.5; extra == "pgvector"
|
|
50
50
|
Provides-Extra: dev
|
|
51
|
-
Requires-Dist: pytest
|
|
51
|
+
Requires-Dist: pytest<9.2,>=7.0; extra == "dev"
|
|
52
52
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
53
53
|
Requires-Dist: pytest-benchmark>=4.0.0; extra == "dev"
|
|
54
54
|
Requires-Dist: httpx>=0.24.0; extra == "dev"
|
|
55
55
|
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
56
56
|
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
|
|
57
57
|
Provides-Extra: test
|
|
58
|
-
Requires-Dist: pytest
|
|
58
|
+
Requires-Dist: pytest<9.2,>=7.0; extra == "test"
|
|
59
59
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
60
60
|
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
61
61
|
Requires-Dist: pytest-benchmark>=4.0.0; extra == "test"
|
|
@@ -87,7 +87,13 @@ from .core.mixins import (
|
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
# Simplified database and cache
|
|
90
|
-
from .db import
|
|
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
|
|
@@ -1360,10 +1360,10 @@ class Node(Object):
|
|
|
1360
1360
|
# Get outgoing edges to find nodes reachable FROM this node
|
|
1361
1361
|
outgoing_edges = await context.find_edges_between(source_id=self.id)
|
|
1362
1362
|
|
|
1363
|
-
#
|
|
1364
|
-
#
|
|
1363
|
+
# Reachability is needed only for cascading. A shallow delete must
|
|
1364
|
+
# inspect incident edges without exploring shared neighbours' graphs.
|
|
1365
1365
|
reachable_node_ids = set()
|
|
1366
|
-
nodes_to_explore = {self.id}
|
|
1366
|
+
nodes_to_explore = {self.id} if cascade else set()
|
|
1367
1367
|
explored = set()
|
|
1368
1368
|
|
|
1369
1369
|
while nodes_to_explore:
|
|
@@ -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
|
-
#
|
|
953
|
+
# Identity/discriminator live at the record root, not in context.
|
|
954
954
|
mapped_fields = [
|
|
955
|
-
(
|
|
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=
|
|
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=
|
|
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(
|
|
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
|
|
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
|
|
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")
|