jvspatial 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jvspatial-0.1.0/jvspatial.egg-info → jvspatial-0.2.0}/PKG-INFO +3 -2
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/__init__.py +22 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/__init__.py +22 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/annotations.py +3 -1
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/context.py +176 -145
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/node.py +8 -4
- jvspatial-0.2.0/jvspatial/core/graph_context_config.py +131 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/graph_expansion.py +107 -46
- jvspatial-0.2.0/jvspatial/core/graph_projection.py +1103 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/pager.py +73 -80
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/_cache.py +77 -7
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/_observable.py +143 -28
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/_postgres_translate.py +13 -5
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/_sqlite_translate.py +6 -2
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/database.py +261 -80
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/dynamodb.py +208 -129
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/jsondb.py +130 -138
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/mongodb.py +239 -58
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/postgres.py +278 -121
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/sqlite.py +198 -31
- jvspatial-0.2.0/jvspatial/storage/interfaces/_stream.py +55 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/interfaces/local.py +12 -14
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/interfaces/s3.py +11 -18
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/version.py +1 -1
- {jvspatial-0.1.0 → jvspatial-0.2.0/jvspatial.egg-info}/PKG-INFO +3 -2
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial.egg-info/SOURCES.txt +3 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial.egg-info/requires.txt +2 -1
- {jvspatial-0.1.0 → jvspatial-0.2.0}/pyproject.toml +6 -1
- {jvspatial-0.1.0 → jvspatial-0.2.0}/AUTHORS +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/LICENSE +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/README.md +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/_route_utils.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/_session_store.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/api_key_service.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/cleanup.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/config.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/enhanced.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/models.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/bridge.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/client_adapter.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/dcr.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/denylist.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/keys.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/metadata.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/models.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/refresh_store.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/requests.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/resource.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/revocation.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/routes.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/oauth/server.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/openapi_config.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/rbac.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/auth/service.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/app_builder.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/auth_configurator.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/auth_middleware.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/database_configurator.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/endpoint_auth_resolver.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/endpoint_manager.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/error_handler.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/logging_config.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/components/path_matcher.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/config.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/config_groups.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/constants.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/context.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/decorators/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/decorators/deferred_registry.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/decorators/field.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/decorators/function_wrappers.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/decorators/route.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/decorators/route_config.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/deferred_invoke_route.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/endpoints/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/endpoints/factory.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/endpoints/graph_visualization.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/endpoints/metadata.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/endpoints/registry.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/endpoints/response.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/endpoints/router.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/endpoints/walker_executor.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/exceptions.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/scheduler/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/scheduler/decorators.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/scheduler/middleware.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/scheduler/models.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/scheduler/scheduler.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/storage/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/storage/service.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/webhooks/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/webhooks/helpers.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/webhooks/middleware.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/webhooks/models.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/webhooks/utils.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/integrations/webhooks/webhook_auth.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/middleware/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/middleware/manager.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/middleware/rate_limit.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/middleware/rate_limit_backend.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/server.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/server_app_factory.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/server_configurator.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/server_lifecycle.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/server_registration.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/server_run.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/services/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/services/discovery.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/services/file_storage.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/services/lifecycle.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/utils/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/utils/path_utils.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/api/utils/reload.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/async_utils/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/cache/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/cache/base.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/cache/factory.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/cache/layered.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/cache/memory.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/cache/redis.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/cli.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/decorators.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/_visit_hooks.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/edge.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/node_query.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/object.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/root.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/walker.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/walker_components/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/walker_components/event_system.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/walker_components/protection.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/walker_components/trail_store.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/walker_components/walker_queue.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/entities/walker_components/walker_trail.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/events.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/graph.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/graph_payload.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/migrations.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/mixins/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/mixins/deferred_save.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/utils.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/core/validate.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/_atomic.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/_path_locks.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/factory.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/manager.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/query.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/transaction.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/db/work_claim.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/dev_tools/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/env.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/env_adapter.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/exceptions.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/logging/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/logging/config.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/logging/custom_levels.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/logging/endpoints.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/logging/filter_utils.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/logging/handler.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/logging/models.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/logging/service.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/memory/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/observability/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/observability/metrics.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/observability/otel.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/observability/tracing.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/profiling/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/py.typed +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/runtime/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/runtime/eventbridge_readiness.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/runtime/lwa.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/runtime/serverless.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/deferred_invoke.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/factory.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/tasks/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/tasks/aws_lambda.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/tasks/aws_sqs.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/tasks/base.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/tasks/stub.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/serverless/tasks/sync.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/exceptions.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/interfaces/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/interfaces/base.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/internal_markers.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/managers/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/managers/proxy.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/models.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/security/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/security/path_sanitizer.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/storage/security/validator.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/testing/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/__init__.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/context.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/decorators.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/deprecation.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/factory.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/normalization.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/retry.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/serialization.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/stability.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/types.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial/utils/validation.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial.egg-info/dependency_links.txt +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial.egg-info/entry_points.txt +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/jvspatial.egg-info/top_level.txt +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/setup.cfg +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/tests/test_config.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/tests/test_create_task.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/tests/test_env.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/tests/test_env_adapter_eventbridge_validation.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/tests/test_env_adapter_postgres.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/tests/test_node_operations.py +0 -0
- {jvspatial-0.1.0 → jvspatial-0.2.0}/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.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -29,7 +29,7 @@ Requires-Dist: starlette>=0.46.0
|
|
|
29
29
|
Requires-Dist: uvicorn<1.0.0,>=0.23.0
|
|
30
30
|
Requires-Dist: python-multipart>=0.0.6
|
|
31
31
|
Requires-Dist: motor>=3.0.0
|
|
32
|
-
Requires-Dist: pymongo>=4.
|
|
32
|
+
Requires-Dist: pymongo>=4.13.0
|
|
33
33
|
Requires-Dist: PyJWT>=2.0.0
|
|
34
34
|
Requires-Dist: cryptography>=42.0.0
|
|
35
35
|
Requires-Dist: authlib>=1.3
|
|
@@ -60,6 +60,7 @@ 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"
|
|
62
62
|
Requires-Dist: httpx>=0.24.0; extra == "test"
|
|
63
|
+
Requires-Dist: python-magic>=0.4.27; extra == "test"
|
|
63
64
|
Provides-Extra: scheduler
|
|
64
65
|
Requires-Dist: psutil>=5.9.0; extra == "scheduler"
|
|
65
66
|
Requires-Dist: python-dotenv>=1.0.0; extra == "scheduler"
|
|
@@ -65,6 +65,19 @@ from .core.context import GraphContext, TransactionUnavailable, graph_transactio
|
|
|
65
65
|
|
|
66
66
|
# Unified entity system
|
|
67
67
|
from .core.entities import Edge, Node, Object, Root, Walker
|
|
68
|
+
from .core.graph_context_config import (
|
|
69
|
+
ProjectionContextConfig,
|
|
70
|
+
ProjectionEvidence,
|
|
71
|
+
ProjectionField,
|
|
72
|
+
ProjectionRelation,
|
|
73
|
+
)
|
|
74
|
+
from .core.graph_projection import (
|
|
75
|
+
GraphProjection,
|
|
76
|
+
ProjectionAccess,
|
|
77
|
+
ProjectionRequest,
|
|
78
|
+
ProjectionScope,
|
|
79
|
+
ProjectionSemantics,
|
|
80
|
+
)
|
|
68
81
|
|
|
69
82
|
# Mixins
|
|
70
83
|
from .core.mixins import (
|
|
@@ -129,6 +142,15 @@ __all__ = [
|
|
|
129
142
|
"Walker",
|
|
130
143
|
"Root",
|
|
131
144
|
"GraphContext",
|
|
145
|
+
"GraphProjection",
|
|
146
|
+
"ProjectionRequest",
|
|
147
|
+
"ProjectionScope",
|
|
148
|
+
"ProjectionAccess",
|
|
149
|
+
"ProjectionContextConfig",
|
|
150
|
+
"ProjectionEvidence",
|
|
151
|
+
"ProjectionField",
|
|
152
|
+
"ProjectionRelation",
|
|
153
|
+
"ProjectionSemantics",
|
|
132
154
|
"graph_transaction",
|
|
133
155
|
"TransactionUnavailable",
|
|
134
156
|
# Mixins
|
|
@@ -37,7 +37,20 @@ from .graph import (
|
|
|
37
37
|
generate_graph_dot,
|
|
38
38
|
generate_graph_mermaid,
|
|
39
39
|
)
|
|
40
|
+
from .graph_context_config import (
|
|
41
|
+
ProjectionContextConfig,
|
|
42
|
+
ProjectionEvidence,
|
|
43
|
+
ProjectionField,
|
|
44
|
+
ProjectionRelation,
|
|
45
|
+
)
|
|
40
46
|
from .graph_expansion import expand_node, subgraph_bfs
|
|
47
|
+
from .graph_projection import (
|
|
48
|
+
GraphProjection,
|
|
49
|
+
ProjectionAccess,
|
|
50
|
+
ProjectionRequest,
|
|
51
|
+
ProjectionScope,
|
|
52
|
+
ProjectionSemantics,
|
|
53
|
+
)
|
|
41
54
|
from .mixins import (
|
|
42
55
|
DeferredSaveMixin,
|
|
43
56
|
deferred_saves_globally_allowed,
|
|
@@ -78,6 +91,15 @@ __all__ = [
|
|
|
78
91
|
"subgraph_bfs",
|
|
79
92
|
# Context (advanced usage)
|
|
80
93
|
"GraphContext",
|
|
94
|
+
"GraphProjection",
|
|
95
|
+
"ProjectionRequest",
|
|
96
|
+
"ProjectionScope",
|
|
97
|
+
"ProjectionAccess",
|
|
98
|
+
"ProjectionContextConfig",
|
|
99
|
+
"ProjectionEvidence",
|
|
100
|
+
"ProjectionField",
|
|
101
|
+
"ProjectionRelation",
|
|
102
|
+
"ProjectionSemantics",
|
|
81
103
|
"get_default_context",
|
|
82
104
|
"set_default_context",
|
|
83
105
|
"reset_default_context",
|
|
@@ -84,7 +84,9 @@ def attribute(
|
|
|
84
84
|
private: If True, creates a Pydantic private attribute (underscore field)
|
|
85
85
|
indexed: If True, create a single-field index on this field (maps to context.field_name)
|
|
86
86
|
index_unique: If True, create a unique index (requires indexed=True)
|
|
87
|
-
index_direction: Index direction for sorting (1=ascending, -1=descending,
|
|
87
|
+
index_direction: Index direction for sorting (1=ascending, -1=descending,
|
|
88
|
+
default=1). SQL sort indexes preserve direction; single-key MongoDB
|
|
89
|
+
and DynamoDB indexes reuse their canonical shape/name.
|
|
88
90
|
index_partial_filter_expression: MongoDB partialFilterExpression dict to scope the
|
|
89
91
|
index to a subset of documents. Field paths must use the full
|
|
90
92
|
``context.<field>`` DB path. Use this with ``index_unique=True`` to avoid
|
|
@@ -46,6 +46,14 @@ def end_request_identity_map(
|
|
|
46
46
|
|
|
47
47
|
if TYPE_CHECKING:
|
|
48
48
|
from .entities import Object
|
|
49
|
+
from .graph_projection import (
|
|
50
|
+
GraphProjection,
|
|
51
|
+
ProjectionAccess,
|
|
52
|
+
ProjectionContextConfig,
|
|
53
|
+
ProjectionRequest,
|
|
54
|
+
ProjectionScope,
|
|
55
|
+
ProjectionSemantics,
|
|
56
|
+
)
|
|
49
57
|
|
|
50
58
|
T = TypeVar("T", bound="Object")
|
|
51
59
|
|
|
@@ -56,17 +64,55 @@ logger = logging.getLogger(__name__)
|
|
|
56
64
|
_ensured_indexes: WeakKeyDictionary[Any, Set[str]] = WeakKeyDictionary()
|
|
57
65
|
|
|
58
66
|
|
|
67
|
+
class _SampleWindow(list):
|
|
68
|
+
"""List-compatible bounded diagnostic samples (statistics cover this window)."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, limit: int):
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.limit = limit
|
|
73
|
+
self.total_count = 0
|
|
74
|
+
self.total_duration = 0.0
|
|
75
|
+
self.total_bytes_read = 0
|
|
76
|
+
self.total_bytes_written = 0
|
|
77
|
+
|
|
78
|
+
def append(self, value):
|
|
79
|
+
self.total_count += 1
|
|
80
|
+
self.total_duration += value.get("duration", 0)
|
|
81
|
+
self.total_bytes_read += value.get("bytes_read") or 0
|
|
82
|
+
self.total_bytes_written += value.get("bytes_written") or 0
|
|
83
|
+
super().append(value)
|
|
84
|
+
if len(self) > self.limit:
|
|
85
|
+
del self[: len(self) - self.limit]
|
|
86
|
+
|
|
87
|
+
def clear(self):
|
|
88
|
+
super().clear()
|
|
89
|
+
self.total_count = 0
|
|
90
|
+
self.total_duration = 0.0
|
|
91
|
+
self.total_bytes_read = self.total_bytes_written = 0
|
|
92
|
+
|
|
93
|
+
def extend(self, values):
|
|
94
|
+
for value in values:
|
|
95
|
+
self.append(value)
|
|
96
|
+
|
|
97
|
+
|
|
59
98
|
# Simple performance monitor for tracking operations
|
|
60
99
|
class PerformanceMonitor:
|
|
61
100
|
"""Enhanced performance monitoring for database operations and general operations."""
|
|
62
101
|
|
|
63
|
-
def __init__(self) -> None:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
def __init__(self, sample_limit: int = 1000) -> None:
|
|
103
|
+
if (
|
|
104
|
+
isinstance(sample_limit, bool)
|
|
105
|
+
or not isinstance(sample_limit, int)
|
|
106
|
+
or sample_limit < 1
|
|
107
|
+
):
|
|
108
|
+
raise ValueError("sample_limit must be a positive integer")
|
|
109
|
+
self.sample_limit = sample_limit
|
|
110
|
+
self.db_operations: _SampleWindow = _SampleWindow(sample_limit)
|
|
111
|
+
self.hook_executions: _SampleWindow = _SampleWindow(sample_limit)
|
|
112
|
+
self.db_errors: _SampleWindow = _SampleWindow(sample_limit)
|
|
113
|
+
self.general_operations: _SampleWindow = _SampleWindow(sample_limit)
|
|
114
|
+
self.general_errors: _SampleWindow = _SampleWindow(sample_limit)
|
|
115
|
+
self.io_operations: _SampleWindow = _SampleWindow(sample_limit)
|
|
70
116
|
self.cache_stats: Dict[str, int] = {
|
|
71
117
|
"hits": 0,
|
|
72
118
|
"misses": 0,
|
|
@@ -206,10 +252,10 @@ class PerformanceMonitor:
|
|
|
206
252
|
return self.cache_stats["hits"] / total
|
|
207
253
|
|
|
208
254
|
async def get_stats(self) -> Dict[str, Any]:
|
|
209
|
-
"""
|
|
210
|
-
total_db_ops =
|
|
211
|
-
total_general_ops =
|
|
212
|
-
total_io_ops =
|
|
255
|
+
"""Lifetime counts/means and percentiles over retained sample windows."""
|
|
256
|
+
total_db_ops = self.db_operations.total_count
|
|
257
|
+
total_general_ops = self.general_operations.total_count
|
|
258
|
+
total_io_ops = self.io_operations.total_count
|
|
213
259
|
total_ops = total_db_ops + total_general_ops + total_io_ops
|
|
214
260
|
|
|
215
261
|
if total_ops == 0:
|
|
@@ -224,7 +270,7 @@ class PerformanceMonitor:
|
|
|
224
270
|
db_stats = {}
|
|
225
271
|
if total_db_ops > 0:
|
|
226
272
|
durations = [op["duration"] for op in self.db_operations]
|
|
227
|
-
avg_db_duration =
|
|
273
|
+
avg_db_duration = self.db_operations.total_duration / total_db_ops
|
|
228
274
|
# Calculate percentiles for database operations
|
|
229
275
|
sorted_durations = sorted(durations)
|
|
230
276
|
p50_idx = int(len(sorted_durations) * 0.5)
|
|
@@ -242,35 +288,32 @@ class PerformanceMonitor:
|
|
|
242
288
|
"p99_db_duration": (
|
|
243
289
|
sorted_durations[p99_idx] if sorted_durations else 0.0
|
|
244
290
|
),
|
|
245
|
-
"db_errors":
|
|
291
|
+
"db_errors": self.db_errors.total_count,
|
|
246
292
|
}
|
|
247
293
|
|
|
248
294
|
# Calculate general operation statistics
|
|
249
295
|
general_stats = {}
|
|
250
296
|
if total_general_ops > 0:
|
|
251
297
|
avg_general_duration = (
|
|
252
|
-
|
|
253
|
-
/ total_general_ops
|
|
298
|
+
self.general_operations.total_duration / total_general_ops
|
|
254
299
|
)
|
|
255
300
|
general_stats = {
|
|
256
301
|
"general_operations": total_general_ops,
|
|
257
302
|
"avg_general_duration": avg_general_duration,
|
|
258
|
-
"general_errors":
|
|
303
|
+
"general_errors": self.general_errors.total_count,
|
|
259
304
|
}
|
|
260
305
|
|
|
261
306
|
# Calculate I/O operation statistics
|
|
262
307
|
io_stats = {}
|
|
263
308
|
if total_io_ops > 0:
|
|
264
309
|
durations = [op["duration"] for op in self.io_operations]
|
|
265
|
-
avg_io_duration =
|
|
310
|
+
avg_io_duration = self.io_operations.total_duration / total_io_ops
|
|
266
311
|
sorted_io_durations = sorted(durations)
|
|
267
312
|
p50_idx = int(len(sorted_io_durations) * 0.5)
|
|
268
313
|
p95_idx = int(len(sorted_io_durations) * 0.95)
|
|
269
314
|
p99_idx = int(len(sorted_io_durations) * 0.99)
|
|
270
|
-
total_bytes_read =
|
|
271
|
-
total_bytes_written =
|
|
272
|
-
op.get("bytes_written", 0) for op in self.io_operations
|
|
273
|
-
)
|
|
315
|
+
total_bytes_read = self.io_operations.total_bytes_read
|
|
316
|
+
total_bytes_written = self.io_operations.total_bytes_written
|
|
274
317
|
io_stats = {
|
|
275
318
|
"io_operations": total_io_ops,
|
|
276
319
|
"avg_io_duration": avg_io_duration,
|
|
@@ -297,7 +340,7 @@ class PerformanceMonitor:
|
|
|
297
340
|
|
|
298
341
|
return {
|
|
299
342
|
"total_operations": total_ops,
|
|
300
|
-
"hook_executions":
|
|
343
|
+
"hook_executions": self.hook_executions.total_count,
|
|
301
344
|
**db_stats,
|
|
302
345
|
**general_stats,
|
|
303
346
|
**io_stats,
|
|
@@ -658,7 +701,7 @@ class GraphContext:
|
|
|
658
701
|
# Use the overridden create() method
|
|
659
702
|
entity = await entity_class.create(**kwargs)
|
|
660
703
|
entity._graph_context = self
|
|
661
|
-
return entity
|
|
704
|
+
return cast(T, entity)
|
|
662
705
|
|
|
663
706
|
# Default behavior: instantiate and save
|
|
664
707
|
entity = entity_class(**kwargs)
|
|
@@ -724,6 +767,7 @@ class GraphContext:
|
|
|
724
767
|
from .entities.object import Object as _Object
|
|
725
768
|
|
|
726
769
|
# Walk up to the appropriate hierarchy root
|
|
770
|
+
hierarchy_root: Type[_Object]
|
|
727
771
|
if issubclass(entity_class, _Node):
|
|
728
772
|
hierarchy_root = _Node
|
|
729
773
|
elif issubclass(entity_class, _Object):
|
|
@@ -755,15 +799,8 @@ class GraphContext:
|
|
|
755
799
|
if self._cache:
|
|
756
800
|
await self._cache.delete(entity_id)
|
|
757
801
|
|
|
758
|
-
async def
|
|
759
|
-
"""
|
|
760
|
-
|
|
761
|
-
Args:
|
|
762
|
-
entity: Entity instance to save
|
|
763
|
-
|
|
764
|
-
Returns:
|
|
765
|
-
The saved entity instance
|
|
766
|
-
"""
|
|
802
|
+
async def _prepare_save(self, entity):
|
|
803
|
+
"""Apply the same export/normalization/collection rules to every save."""
|
|
767
804
|
# Export entity - Node, Edge, and Walker return nested format
|
|
768
805
|
if hasattr(entity, "type_code"):
|
|
769
806
|
type_code = getattr(entity, "type_code", "")
|
|
@@ -835,17 +872,17 @@ class GraphContext:
|
|
|
835
872
|
else:
|
|
836
873
|
collection = type_code.lower()
|
|
837
874
|
|
|
838
|
-
db = self.database
|
|
839
875
|
is_node = (
|
|
840
876
|
hasattr(entity, "type_code") and getattr(entity, "type_code", "") == "n"
|
|
841
877
|
)
|
|
842
|
-
|
|
843
|
-
# Adjacency lives in the edge collection. Never write a legacy ``edges``
|
|
844
|
-
# array onto node rows (a leftover in-memory copy must not re-persist it).
|
|
845
878
|
if is_node:
|
|
846
879
|
record.pop("edges", None)
|
|
880
|
+
return collection, record
|
|
847
881
|
|
|
848
|
-
|
|
882
|
+
async def save(self, entity):
|
|
883
|
+
"""Save one entity with shared preparation and cache invalidation."""
|
|
884
|
+
collection, record = await self._prepare_save(entity)
|
|
885
|
+
await self.database.save(collection, record)
|
|
849
886
|
await self._add_to_cache(entity.id, entity)
|
|
850
887
|
return entity
|
|
851
888
|
|
|
@@ -963,6 +1000,31 @@ class GraphContext:
|
|
|
963
1000
|
self, format=format, output_file=output_file, **kwargs
|
|
964
1001
|
)
|
|
965
1002
|
|
|
1003
|
+
async def project_graph(
|
|
1004
|
+
self,
|
|
1005
|
+
request: "ProjectionRequest",
|
|
1006
|
+
*,
|
|
1007
|
+
scope: "ProjectionScope",
|
|
1008
|
+
access: Optional["ProjectionAccess"] = None,
|
|
1009
|
+
semantics: Optional["ProjectionSemantics"] = None,
|
|
1010
|
+
context_config: Optional["ProjectionContextConfig"] = None,
|
|
1011
|
+
) -> "GraphProjection":
|
|
1012
|
+
"""Capture scoped relationship context for language-model inference.
|
|
1013
|
+
|
|
1014
|
+
See ``ProjectionRequest`` for view, field, traversal, and budget controls.
|
|
1015
|
+
The application supplies explicit scope and optional current access rules.
|
|
1016
|
+
"""
|
|
1017
|
+
from .graph_projection import project_graph
|
|
1018
|
+
|
|
1019
|
+
return await project_graph(
|
|
1020
|
+
self,
|
|
1021
|
+
request,
|
|
1022
|
+
scope=scope,
|
|
1023
|
+
access=access,
|
|
1024
|
+
semantics=semantics,
|
|
1025
|
+
context_config=context_config,
|
|
1026
|
+
)
|
|
1027
|
+
|
|
966
1028
|
async def expand_node(
|
|
967
1029
|
self,
|
|
968
1030
|
node_id: str,
|
|
@@ -972,6 +1034,7 @@ class GraphContext:
|
|
|
972
1034
|
cursor: int = 0,
|
|
973
1035
|
after: Optional[str] = None,
|
|
974
1036
|
detail_level: str = "full",
|
|
1037
|
+
include_counts: bool = True,
|
|
975
1038
|
) -> Dict[str, Any]:
|
|
976
1039
|
"""Return a page of incident edges and neighbor summaries for progressive UIs.
|
|
977
1040
|
|
|
@@ -989,6 +1052,7 @@ class GraphContext:
|
|
|
989
1052
|
cursor=cursor,
|
|
990
1053
|
after=after,
|
|
991
1054
|
detail_level=detail_level, # type: ignore[arg-type]
|
|
1055
|
+
include_counts=include_counts,
|
|
992
1056
|
)
|
|
993
1057
|
|
|
994
1058
|
async def subgraph_bfs(
|
|
@@ -999,6 +1063,8 @@ class GraphContext:
|
|
|
999
1063
|
max_nodes: int = 100,
|
|
1000
1064
|
max_edges_per_node: int = 200,
|
|
1001
1065
|
detail_level: str = "full",
|
|
1066
|
+
include_counts: bool = True,
|
|
1067
|
+
priority_node_prefixes: Optional[List[str]] = None,
|
|
1002
1068
|
) -> Dict[str, Any]:
|
|
1003
1069
|
"""Return a bounded BFS subgraph from ``root_id``.
|
|
1004
1070
|
|
|
@@ -1015,6 +1081,8 @@ class GraphContext:
|
|
|
1015
1081
|
max_nodes=max_nodes,
|
|
1016
1082
|
max_edges_per_node=max_edges_per_node,
|
|
1017
1083
|
detail_level=detail_level, # type: ignore[arg-type]
|
|
1084
|
+
include_counts=include_counts,
|
|
1085
|
+
priority_node_prefixes=priority_node_prefixes,
|
|
1018
1086
|
)
|
|
1019
1087
|
|
|
1020
1088
|
def _get_collection_name(self, type_code: str) -> str:
|
|
@@ -1244,7 +1312,11 @@ class GraphContext:
|
|
|
1244
1312
|
page_limit = max(1, int(limit))
|
|
1245
1313
|
sort_fields = keyset_sort_fields(sort)
|
|
1246
1314
|
final_query: Dict[str, Any] = dict(query or {})
|
|
1247
|
-
after_cursor = keyset_filter(
|
|
1315
|
+
after_cursor = keyset_filter(
|
|
1316
|
+
sort_fields,
|
|
1317
|
+
decode_keyset_cursor(after),
|
|
1318
|
+
nulls_first=getattr(self.database, "_keyset_nulls_first", False),
|
|
1319
|
+
)
|
|
1248
1320
|
if after_cursor is not None:
|
|
1249
1321
|
final_query = (
|
|
1250
1322
|
{"$and": [final_query, after_cursor]} if final_query else after_cursor
|
|
@@ -1379,22 +1451,23 @@ class GraphContext:
|
|
|
1379
1451
|
if not neighbor_ids:
|
|
1380
1452
|
return out
|
|
1381
1453
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
{"id": {"$in": list(neighbor_ids)}}
|
|
1385
|
-
|
|
1454
|
+
node_collection = self._get_collection_name(self._get_entity_type_code(Node))
|
|
1455
|
+
if node_entities or node_filter:
|
|
1456
|
+
query = {"id": {"$in": list(neighbor_ids)}}
|
|
1457
|
+
if node_entities:
|
|
1458
|
+
query["entity"] = {"$in": node_entities}
|
|
1459
|
+
query.update({f"context.{k}": v for k, v in (node_filter or {}).items()})
|
|
1460
|
+
node_docs = await self.database.find(node_collection, query)
|
|
1461
|
+
else:
|
|
1462
|
+
node_docs = list(
|
|
1463
|
+
(
|
|
1464
|
+
await self.database.find_many(node_collection, list(neighbor_ids))
|
|
1465
|
+
).values()
|
|
1466
|
+
)
|
|
1386
1467
|
node_by_id: Dict[str, Any] = {}
|
|
1387
1468
|
for doc in node_docs:
|
|
1388
1469
|
if node_entities and doc.get("entity") not in node_entities:
|
|
1389
1470
|
continue
|
|
1390
|
-
if node_filter:
|
|
1391
|
-
fail = False
|
|
1392
|
-
for k, v in node_filter.items():
|
|
1393
|
-
if doc.get("context", {}).get(k) != v:
|
|
1394
|
-
fail = True
|
|
1395
|
-
break
|
|
1396
|
-
if fail:
|
|
1397
|
-
continue
|
|
1398
1471
|
obj = await self._deserialize_entity(Node, doc)
|
|
1399
1472
|
if obj is not None:
|
|
1400
1473
|
node_by_id[doc["id"]] = obj
|
|
@@ -1472,6 +1545,7 @@ class GraphContext:
|
|
|
1472
1545
|
# Postgres-only; other adapters receive the plain definition, and
|
|
1473
1546
|
# full-text definitions are skipped where ``$text`` runs in memory.
|
|
1474
1547
|
is_postgres = self._is_postgres(self.database)
|
|
1548
|
+
is_mongodb = self._is_mongodb(self.database)
|
|
1475
1549
|
scope_keys = ("per_class", "entity_leading", "partial_by_entity", "fulltext")
|
|
1476
1550
|
|
|
1477
1551
|
def _scoped(index_def: Dict[str, Any], extra: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -1497,9 +1571,19 @@ class GraphContext:
|
|
|
1497
1571
|
for k, v in index_def.items()
|
|
1498
1572
|
if k not in ("field", "unique", "direction", *scope_keys)
|
|
1499
1573
|
}
|
|
1574
|
+
# Preserve existing ascending names; list form carries a
|
|
1575
|
+
# descending declaration to adapters that support it.
|
|
1576
|
+
# Mongo single-key B-trees serve either direction; keep
|
|
1577
|
+
# annotation-created indexes in their canonical shape.
|
|
1578
|
+
direction = index_def.get("direction", 1)
|
|
1579
|
+
field_spec: Union[str, List[Tuple[str, int]]] = (
|
|
1580
|
+
index_def["field"]
|
|
1581
|
+
if direction == 1 or is_mongodb
|
|
1582
|
+
else [(index_def["field"], direction)]
|
|
1583
|
+
)
|
|
1500
1584
|
await self.database.create_index(
|
|
1501
1585
|
collection,
|
|
1502
|
-
|
|
1586
|
+
field_spec,
|
|
1503
1587
|
unique=index_def.get("unique", False),
|
|
1504
1588
|
**_scoped(index_def, extra),
|
|
1505
1589
|
)
|
|
@@ -1747,104 +1831,51 @@ class GraphContext:
|
|
|
1747
1831
|
|
|
1748
1832
|
# Batch operations for improved performance
|
|
1749
1833
|
async def save_batch(self, entities: List[Any]) -> List[Any]:
|
|
1750
|
-
"""Save
|
|
1751
|
-
|
|
1752
|
-
Persists each entity via the database adapter's ``save`` (or ``batch_write``
|
|
1753
|
-
where supported). There is no cross-collection atomicity unless the
|
|
1754
|
-
underlying :meth:`Database.begin_transaction` / ``commit_transaction``
|
|
1755
|
-
API is used and wired through callers separately.
|
|
1834
|
+
"""Save per-collection batches, returning only confirmed successes.
|
|
1756
1835
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
List of saved entity instances
|
|
1836
|
+
Uses the same preparation as context.save(), including normalization
|
|
1837
|
+
and stripping legacy adjacency. This context-level operation does not
|
|
1838
|
+
invoke overridden entity.save() methods, matching individual context
|
|
1839
|
+
saves. Batches are best-effort, not cross-collection transactions.
|
|
1762
1840
|
"""
|
|
1763
|
-
|
|
1764
|
-
return []
|
|
1765
|
-
|
|
1766
|
-
# Group entities by type for efficient batch operations
|
|
1767
|
-
entities_by_type: Dict[str, List[Any]] = {}
|
|
1841
|
+
grouped: Dict[str, List[Tuple[Any, Dict[str, Any]]]] = {}
|
|
1768
1842
|
for entity in entities:
|
|
1769
|
-
collection = self.
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
for collection, type_entities in entities_by_type.items():
|
|
1778
|
-
is_node_collection = collection == "node"
|
|
1779
|
-
if is_node_collection:
|
|
1780
|
-
for entity in type_entities:
|
|
1781
|
-
try:
|
|
1782
|
-
saved = await self.save(entity)
|
|
1783
|
-
saved_entities.append(saved)
|
|
1784
|
-
except Exception as e:
|
|
1785
|
-
logger.error(
|
|
1786
|
-
"Failed to save entity %s: %s",
|
|
1787
|
-
getattr(entity, "id", "unknown"),
|
|
1788
|
-
e,
|
|
1789
|
-
exc_info=True,
|
|
1790
|
-
)
|
|
1791
|
-
continue
|
|
1792
|
-
|
|
1793
|
-
# Export all entities of this type
|
|
1794
|
-
records = []
|
|
1795
|
-
for entity in type_entities:
|
|
1796
|
-
record = await entity.export()
|
|
1797
|
-
if getattr(entity, "type_code", "") == "n":
|
|
1798
|
-
record.pop("edges", None)
|
|
1799
|
-
records.append(record)
|
|
1800
|
-
|
|
1801
|
-
# Save all records of this type
|
|
1802
|
-
db = self.database
|
|
1803
|
-
|
|
1804
|
-
# Use batch_write if available (e.g., DynamoDB), otherwise fall back to sequential saves
|
|
1805
|
-
if hasattr(db, "batch_write"):
|
|
1843
|
+
collection, record = await self._prepare_save(entity)
|
|
1844
|
+
grouped.setdefault(collection, []).append((entity, record))
|
|
1845
|
+
saved = []
|
|
1846
|
+
for collection, items in grouped.items():
|
|
1847
|
+
for offset in range(0, len(items), 500):
|
|
1848
|
+
chunk = items[offset : offset + 500]
|
|
1849
|
+
# Every attempted identity is stale if the batch fails or is
|
|
1850
|
+
# cancelled, including when a backend outcome is uncertain.
|
|
1806
1851
|
try:
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
try:
|
|
1835
|
-
await db.save(collection, record)
|
|
1836
|
-
# Update cache with latest version
|
|
1837
|
-
await self._add_to_cache(record["id"], type_entities[i])
|
|
1838
|
-
saved_entities.append(type_entities[i])
|
|
1839
|
-
except Exception as e:
|
|
1840
|
-
# Log error but continue with other entities
|
|
1841
|
-
logger.error(
|
|
1842
|
-
f"Failed to save entity {type_entities[i].get('id', 'unknown')}: {e}",
|
|
1843
|
-
exc_info=True,
|
|
1844
|
-
)
|
|
1845
|
-
continue
|
|
1846
|
-
|
|
1847
|
-
return saved_entities
|
|
1852
|
+
bulk = getattr(self.database, "bulk_save_detailed", None)
|
|
1853
|
+
if callable(bulk):
|
|
1854
|
+
result = await bulk(collection, [record for _, record in chunk])
|
|
1855
|
+
else:
|
|
1856
|
+
from jvspatial.db.database import BulkSaveResult
|
|
1857
|
+
|
|
1858
|
+
for _, record in chunk:
|
|
1859
|
+
await self.database.save(collection, record)
|
|
1860
|
+
result = BulkSaveResult(len(chunk), len(chunk), [])
|
|
1861
|
+
except BaseException:
|
|
1862
|
+
for entity, _ in chunk:
|
|
1863
|
+
await self._evict_from_cache(entity.id)
|
|
1864
|
+
raise
|
|
1865
|
+
failed = set(result.failed_ids)
|
|
1866
|
+
if result.saved != len(chunk) and not failed:
|
|
1867
|
+
# Count-only custom adapters cannot identify successful IDs.
|
|
1868
|
+
for entity, _ in chunk:
|
|
1869
|
+
await self._evict_from_cache(entity.id)
|
|
1870
|
+
continue
|
|
1871
|
+
for entity, _ in chunk:
|
|
1872
|
+
if str(entity.id) in failed:
|
|
1873
|
+
await self._evict_from_cache(entity.id)
|
|
1874
|
+
else:
|
|
1875
|
+
await self._add_to_cache(entity.id, entity)
|
|
1876
|
+
saved.append(entity)
|
|
1877
|
+
confirmed = {id(entity) for entity in saved}
|
|
1878
|
+
return [entity for entity in entities if id(entity) in confirmed]
|
|
1848
1879
|
|
|
1849
1880
|
async def get_batch(self, entity_class: Type[T], ids: List[str]) -> List[T]:
|
|
1850
1881
|
"""Retrieve multiple entities by ID.
|
|
@@ -692,7 +692,11 @@ class Node(Object):
|
|
|
692
692
|
edge_entities, edge_query, node_entities, node_query = self._neighbor_spec(
|
|
693
693
|
node, edge, kwargs
|
|
694
694
|
)
|
|
695
|
-
after = keyset_filter(
|
|
695
|
+
after = keyset_filter(
|
|
696
|
+
sort_fields,
|
|
697
|
+
decode_keyset_cursor(cursor),
|
|
698
|
+
nulls_first=getattr(context.database, "_keyset_nulls_first", False),
|
|
699
|
+
)
|
|
696
700
|
if after is not None:
|
|
697
701
|
node_query = after if node_query is None else {"$and": [node_query, after]}
|
|
698
702
|
found = await self._neighbors(
|
|
@@ -1122,9 +1126,9 @@ class Node(Object):
|
|
|
1122
1126
|
except re.error:
|
|
1123
1127
|
return False
|
|
1124
1128
|
elif operator == "$exists":
|
|
1125
|
-
#
|
|
1126
|
-
#
|
|
1127
|
-
if
|
|
1129
|
+
# Match the in-memory query contract: null and missing are
|
|
1130
|
+
# absent; falsy non-null values remain present.
|
|
1131
|
+
if bool(criterion) != (value is not None):
|
|
1128
1132
|
return False
|
|
1129
1133
|
else:
|
|
1130
1134
|
# Unknown operator - ignore
|