GeneralManager 0.46.0__tar.gz → 0.47.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.
- {generalmanager-0.46.0/src/GeneralManager.egg-info → generalmanager-0.47.0}/PKG-INFO +1 -1
- {generalmanager-0.46.0 → generalmanager-0.47.0}/pyproject.toml +1 -1
- {generalmanager-0.46.0 → generalmanager-0.47.0/src/GeneralManager.egg-info}/PKG-INFO +1 -1
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/GeneralManager.egg-info/SOURCES.txt +2 -0
- generalmanager-0.47.0/src/general_manager/api/graphql_prefetch.py +182 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_resolvers.py +25 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_subscriptions.py +9 -51
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/cache_decorator.py +44 -30
- generalmanager-0.47.0/src/general_manager/cache/dependency_cache.py +154 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/dependency_publish.py +25 -23
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/run_context.py +22 -2
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/testing.py +25 -2
- {generalmanager-0.46.0 → generalmanager-0.47.0}/LICENSE +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/README.md +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/setup.cfg +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/GeneralManager.egg-info/dependency_links.txt +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/GeneralManager.egg-info/requires.txt +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/GeneralManager.egg-info/top_level.txt +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/api.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/bucket.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/cache.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/factory.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/general_manager.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/interface.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/manager.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/measurement.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/permission.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/rule.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/search.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/_types/utils.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_errors.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_mutations.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_search.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_subscription_consumer.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_view.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/mutation.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/property.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/registry.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/remote_api.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/remote_invalidation.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/remote_invalidation_client.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/apps.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/bootstrap.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/bucket/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/bucket/base_bucket.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/bucket/calculation_bucket.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/bucket/database_bucket.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/bucket/group_bucket.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/bucket/request_bucket.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/cache_tracker.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/dependency_index.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/model_dependency_collector.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/signals.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/conf.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/factory/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/factory/auto_factory.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/factory/factories.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/factory/factory_methods.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/base_interface.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/bundles/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/bundles/calculation.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/bundles/database.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/bundles/remote_manager.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/bundles/request.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/base.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/builtin.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/calculation/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/calculation/_compat.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/calculation/lifecycle.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/configuration.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/core/observability.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/core/utils.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/exceptions.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/existing_model/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/existing_model/_compat.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/existing_model/resolution.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/factory.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm/_compat.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm/history.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm/lifecycle.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm/mutations.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm/support.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm_utils/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm_utils/django_manager_utils.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm_utils/field_descriptors.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/orm_utils/payload_normalizer.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/read_only/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/read_only/_compat.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/read_only/lifecycle.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/read_only/management.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/registry.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/remote_manager.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/capabilities/request/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/infrastructure/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/infrastructure/startup_hooks.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/infrastructure/system_checks.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/interfaces/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/interfaces/calculation.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/interfaces/database.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/interfaces/existing_model.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/interfaces/read_only.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/interfaces/remote_manager.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/interfaces/request.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/manifests/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/manifests/capability_builder.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/manifests/capability_manifest.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/manifests/capability_models.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/orm_interface.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/requests.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/utils/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/utils/database_interface_protocols.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/utils/errors.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/interface/utils/models.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/logging.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/management/commands/search_index.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/management/commands/seed_manager_landscape.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/management/commands/shell.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/management/commands/workflow_drain_outbox.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/management/commands/workflow_replay_dead_letters.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/manager/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/manager/general_manager.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/manager/group_manager.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/manager/input.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/manager/meta.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/measurement/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/measurement/measurement.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/measurement/measurement_field.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/metrics/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/metrics/graphql.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/migrations/0001_initial.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/migrations/0002_workflow_outbox_scaling_indexes.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/migrations/0003_workflow_execution_correlation_constraint.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/migrations/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/models.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/audit.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/base_permission.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/graphql_capabilities.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/manager_based_permission.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/mutation_permission.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/permission_checks.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/permission_data_manager.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/permission/utils.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/public_api_registry.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/py.typed +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/rule/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/rule/handler.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/rule/rule.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/async_tasks.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/backend.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/backend_registry.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/backends/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/backends/dev.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/backends/meilisearch.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/backends/opensearch.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/backends/typesense.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/config.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/indexer.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/registry.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/search/utils.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/seeding/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/seeding/manager_landscape.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/args_to_kwargs.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/filter_parser.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/format_string.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/json_encoder.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/make_cache_key.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/none_to_zero.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/path_mapping.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/public_api.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/utils/type_checks.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/actions.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/backend_registry.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/backends/__init__.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/backends/celery.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/backends/local.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/backends/n8n.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/config.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/engine.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/event_registry.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/events.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/models.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/signal_bridge.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/tasks.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/workflow/telemetry.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/tests/test_settings.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/tests/test_urls.py +0 -0
- {generalmanager-0.46.0 → generalmanager-0.47.0}/tests/testing_asgi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GeneralManager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.47.0
|
|
4
4
|
Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
|
|
5
5
|
Author-email: Tim Kleindick <tkleindick@yahoo.de>
|
|
6
6
|
License: MIT License
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "GeneralManager"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.47.0"
|
|
8
8
|
description = "Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Tim Kleindick", email = "tkleindick@yahoo.de" }]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GeneralManager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.47.0
|
|
4
4
|
Summary: Modular Django-based data management framework with ORM, GraphQL, fine-grained permissions, rule validation, calculations and caching.
|
|
5
5
|
Author-email: Tim Kleindick <tkleindick@yahoo.de>
|
|
6
6
|
License: MIT License
|
|
@@ -31,6 +31,7 @@ src/general_manager/api/__init__.py
|
|
|
31
31
|
src/general_manager/api/graphql.py
|
|
32
32
|
src/general_manager/api/graphql_errors.py
|
|
33
33
|
src/general_manager/api/graphql_mutations.py
|
|
34
|
+
src/general_manager/api/graphql_prefetch.py
|
|
34
35
|
src/general_manager/api/graphql_resolvers.py
|
|
35
36
|
src/general_manager/api/graphql_search.py
|
|
36
37
|
src/general_manager/api/graphql_subscription_consumer.py
|
|
@@ -51,6 +52,7 @@ src/general_manager/bucket/request_bucket.py
|
|
|
51
52
|
src/general_manager/cache/__init__.py
|
|
52
53
|
src/general_manager/cache/cache_decorator.py
|
|
53
54
|
src/general_manager/cache/cache_tracker.py
|
|
55
|
+
src/general_manager/cache/dependency_cache.py
|
|
54
56
|
src/general_manager/cache/dependency_index.py
|
|
55
57
|
src/general_manager/cache/dependency_publish.py
|
|
56
58
|
src/general_manager/cache/model_dependency_collector.py
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Internal GraphQL helpers for dependency-cache prefetch planning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from django.core.cache import cache as django_cache
|
|
11
|
+
from graphql import GraphQLResolveInfo
|
|
12
|
+
from graphql.language.ast import (
|
|
13
|
+
FieldNode,
|
|
14
|
+
FragmentSpreadNode,
|
|
15
|
+
InlineFragmentNode,
|
|
16
|
+
SelectionSetNode,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from general_manager.api.property import GraphQLProperty
|
|
20
|
+
from general_manager.cache.dependency_cache import (
|
|
21
|
+
DependencyCacheBackend,
|
|
22
|
+
DependencyCacheHit,
|
|
23
|
+
read_many_dependency_cache_hits,
|
|
24
|
+
)
|
|
25
|
+
from general_manager.cache.run_context import current_calculation_run_context
|
|
26
|
+
from general_manager.manager.general_manager import GeneralManager
|
|
27
|
+
from general_manager.utils.make_cache_key import make_cache_key
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_graphql_name(name: str) -> str:
|
|
31
|
+
"""Convert a GraphQL field name to the Python attribute name."""
|
|
32
|
+
if "_" in name:
|
|
33
|
+
return name
|
|
34
|
+
snake = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
35
|
+
snake = re.sub("([a-z0-9])([A-Z])", r"\1_\2", snake)
|
|
36
|
+
return snake.lower()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def collect_selected_graphql_property_names(
|
|
40
|
+
info: GraphQLResolveInfo,
|
|
41
|
+
manager_class: type[Any],
|
|
42
|
+
*,
|
|
43
|
+
root_field: str,
|
|
44
|
+
normalize_name: Callable[[str], str] = normalize_graphql_name,
|
|
45
|
+
) -> set[str]:
|
|
46
|
+
"""Return GraphQLProperty names selected directly under root_field."""
|
|
47
|
+
interface_cls = getattr(manager_class, "Interface", None)
|
|
48
|
+
if interface_cls is None:
|
|
49
|
+
return set()
|
|
50
|
+
available_properties = set(interface_cls.get_graph_ql_properties().keys())
|
|
51
|
+
if not available_properties:
|
|
52
|
+
return set()
|
|
53
|
+
|
|
54
|
+
property_names: set[str] = set()
|
|
55
|
+
|
|
56
|
+
def collect_direct_fields(
|
|
57
|
+
selection_set: SelectionSetNode | None,
|
|
58
|
+
visited: frozenset[str],
|
|
59
|
+
) -> None:
|
|
60
|
+
if selection_set is None:
|
|
61
|
+
return
|
|
62
|
+
for selection in selection_set.selections:
|
|
63
|
+
if isinstance(selection, FieldNode):
|
|
64
|
+
normalized = normalize_name(selection.name.value)
|
|
65
|
+
if normalized in available_properties:
|
|
66
|
+
property_names.add(normalized)
|
|
67
|
+
elif isinstance(selection, InlineFragmentNode):
|
|
68
|
+
collect_direct_fields(selection.selection_set, visited)
|
|
69
|
+
elif isinstance(selection, FragmentSpreadNode):
|
|
70
|
+
fragment_name = selection.name.value
|
|
71
|
+
if fragment_name in visited:
|
|
72
|
+
continue
|
|
73
|
+
fragment = info.fragments.get(fragment_name)
|
|
74
|
+
if fragment is not None:
|
|
75
|
+
collect_direct_fields(
|
|
76
|
+
fragment.selection_set,
|
|
77
|
+
visited | frozenset((fragment_name,)),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def inspect_for_root(
|
|
81
|
+
selection_set: SelectionSetNode | None,
|
|
82
|
+
visited: frozenset[str],
|
|
83
|
+
) -> None:
|
|
84
|
+
if selection_set is None:
|
|
85
|
+
return
|
|
86
|
+
for selection in selection_set.selections:
|
|
87
|
+
if isinstance(selection, FieldNode):
|
|
88
|
+
if selection.name.value == root_field:
|
|
89
|
+
collect_direct_fields(selection.selection_set, visited)
|
|
90
|
+
else:
|
|
91
|
+
inspect_for_root(selection.selection_set, visited)
|
|
92
|
+
elif isinstance(selection, InlineFragmentNode):
|
|
93
|
+
inspect_for_root(selection.selection_set, visited)
|
|
94
|
+
elif isinstance(selection, FragmentSpreadNode):
|
|
95
|
+
fragment_name = selection.name.value
|
|
96
|
+
if fragment_name in visited:
|
|
97
|
+
continue
|
|
98
|
+
fragment = info.fragments.get(fragment_name)
|
|
99
|
+
if fragment is not None:
|
|
100
|
+
inspect_for_root(
|
|
101
|
+
fragment.selection_set,
|
|
102
|
+
visited | frozenset((fragment_name,)),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
for field_node in getattr(info, "field_nodes", ()):
|
|
106
|
+
inspect_for_root(field_node.selection_set, frozenset())
|
|
107
|
+
return property_names
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class DependencyCachePrefetchPlan:
|
|
112
|
+
"""A dependency-cache key planned for one GraphQL list item field."""
|
|
113
|
+
|
|
114
|
+
cache_key: str
|
|
115
|
+
instance: GeneralManager
|
|
116
|
+
property_name: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def plan_dependency_cache_prefetches(
|
|
120
|
+
instances: Iterable[GeneralManager],
|
|
121
|
+
manager_class: type[Any],
|
|
122
|
+
property_names: Iterable[str],
|
|
123
|
+
*,
|
|
124
|
+
can_read_field: Callable[[GeneralManager, str], bool],
|
|
125
|
+
) -> dict[str, DependencyCachePrefetchPlan]:
|
|
126
|
+
"""Build dependency-cache key plans for selected readable properties."""
|
|
127
|
+
interface_cls = getattr(manager_class, "Interface", None)
|
|
128
|
+
if interface_cls is None:
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
available_properties = interface_cls.get_graph_ql_properties()
|
|
132
|
+
instance_list = list(instances)
|
|
133
|
+
selected = set(property_names)
|
|
134
|
+
plans: dict[str, DependencyCachePrefetchPlan] = {}
|
|
135
|
+
|
|
136
|
+
for property_name in selected:
|
|
137
|
+
prop = available_properties.get(property_name)
|
|
138
|
+
if not isinstance(prop, GraphQLProperty):
|
|
139
|
+
continue
|
|
140
|
+
if prop.cache != "dependency":
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
cached_getter = prop._get_cached_fget()
|
|
144
|
+
for instance in instance_list:
|
|
145
|
+
if not can_read_field(instance, property_name):
|
|
146
|
+
continue
|
|
147
|
+
cache_key = make_cache_key(cached_getter, (instance,), {})
|
|
148
|
+
plans.setdefault(
|
|
149
|
+
cache_key,
|
|
150
|
+
DependencyCachePrefetchPlan(
|
|
151
|
+
cache_key=cache_key,
|
|
152
|
+
instance=instance,
|
|
153
|
+
property_name=property_name,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
return plans
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
DependencyCacheBulkReader = Callable[
|
|
160
|
+
[DependencyCacheBackend, Iterable[str]],
|
|
161
|
+
dict[str, DependencyCacheHit],
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def prefetch_dependency_cache_hits(
|
|
166
|
+
plans: Mapping[str, DependencyCachePrefetchPlan],
|
|
167
|
+
*,
|
|
168
|
+
cache_backend: DependencyCacheBackend = django_cache,
|
|
169
|
+
reader: DependencyCacheBulkReader | None = None,
|
|
170
|
+
) -> dict[str, DependencyCacheHit]:
|
|
171
|
+
"""Bulk-read planned dependency-cache hits into the active run context."""
|
|
172
|
+
context = current_calculation_run_context()
|
|
173
|
+
if context is None or not plans:
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
if reader is None:
|
|
177
|
+
reader = read_many_dependency_cache_hits
|
|
178
|
+
cache_keys = tuple(plans.keys())
|
|
179
|
+
hits = reader(cache_backend, cache_keys)
|
|
180
|
+
if hits:
|
|
181
|
+
context.set_dependency_cache_hits(hits)
|
|
182
|
+
return hits
|
{generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_resolvers.py
RENAMED
|
@@ -21,6 +21,11 @@ from general_manager.bucket.base_bucket import Bucket
|
|
|
21
21
|
from general_manager.manager.general_manager import GeneralManager
|
|
22
22
|
from general_manager.measurement.measurement import Measurement
|
|
23
23
|
from general_manager.api.graphql_errors import get_read_permission_filter
|
|
24
|
+
from general_manager.api.graphql_prefetch import (
|
|
25
|
+
collect_selected_graphql_property_names,
|
|
26
|
+
plan_dependency_cache_prefetches,
|
|
27
|
+
prefetch_dependency_cache_hits,
|
|
28
|
+
)
|
|
24
29
|
from general_manager.permission.graphql_capabilities import (
|
|
25
30
|
get_capability_context,
|
|
26
31
|
get_graphql_capabilities,
|
|
@@ -543,6 +548,26 @@ def create_list_resolver(
|
|
|
543
548
|
qs_paginated: Any = apply_pagination(qs_grouped, page, page_size)
|
|
544
549
|
if not hasattr(qs_paginated, "groups"):
|
|
545
550
|
qs_paginated = list(qs_paginated)
|
|
551
|
+
if isinstance(qs_paginated, list):
|
|
552
|
+
selected_property_names = collect_selected_graphql_property_names(
|
|
553
|
+
info,
|
|
554
|
+
manager_class,
|
|
555
|
+
root_field="items",
|
|
556
|
+
)
|
|
557
|
+
if selected_property_names:
|
|
558
|
+
prefetch_plans = plan_dependency_cache_prefetches(
|
|
559
|
+
qs_paginated,
|
|
560
|
+
manager_class,
|
|
561
|
+
selected_property_names,
|
|
562
|
+
can_read_field=lambda instance, property_name: (
|
|
563
|
+
check_read_permission(
|
|
564
|
+
instance,
|
|
565
|
+
info,
|
|
566
|
+
property_name,
|
|
567
|
+
)
|
|
568
|
+
),
|
|
569
|
+
)
|
|
570
|
+
prefetch_dependency_cache_hits(prefetch_plans)
|
|
546
571
|
if isinstance(qs_paginated, list) and selection_includes_path(
|
|
547
572
|
info, ("items", "capabilities")
|
|
548
573
|
):
|
{generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/api/graphql_subscriptions.py
RENAMED
|
@@ -16,19 +16,15 @@ from typing import Any, Callable, Iterable, TYPE_CHECKING, cast
|
|
|
16
16
|
|
|
17
17
|
from channels.layers import BaseChannelLayer, get_channel_layer # type: ignore[import]
|
|
18
18
|
|
|
19
|
-
from graphql.language.ast import (
|
|
20
|
-
FieldNode,
|
|
21
|
-
FragmentSpreadNode,
|
|
22
|
-
InlineFragmentNode,
|
|
23
|
-
SelectionSetNode,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
19
|
from general_manager.cache.cache_tracker import DependencyTracker
|
|
27
20
|
from general_manager.cache.dependency_index import (
|
|
28
21
|
Dependency,
|
|
29
22
|
parse_dependency_identifier,
|
|
30
23
|
serialize_dependency_identifier,
|
|
31
24
|
)
|
|
25
|
+
from general_manager.api.graphql_prefetch import (
|
|
26
|
+
collect_selected_graphql_property_names,
|
|
27
|
+
)
|
|
32
28
|
from general_manager.logging import get_logger
|
|
33
29
|
from general_manager.manager.general_manager import GeneralManager
|
|
34
30
|
from general_manager.api.graphql_errors import MissingChannelLayerError
|
|
@@ -334,50 +330,12 @@ def subscription_property_names(
|
|
|
334
330
|
Returns:
|
|
335
331
|
Set of selected ``GraphQLProperty`` names; empty set if none found.
|
|
336
332
|
"""
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
property_names: set[str] = set()
|
|
345
|
-
|
|
346
|
-
def collect_from_selection(selection_set: SelectionSetNode | None) -> None:
|
|
347
|
-
if selection_set is None:
|
|
348
|
-
return
|
|
349
|
-
for selection in selection_set.selections:
|
|
350
|
-
if isinstance(selection, FieldNode):
|
|
351
|
-
name = selection.name.value
|
|
352
|
-
normalized = normalize_graphql_name(name)
|
|
353
|
-
if normalized in available_properties:
|
|
354
|
-
property_names.add(normalized)
|
|
355
|
-
elif isinstance(selection, FragmentSpreadNode):
|
|
356
|
-
fragment = info.fragments.get(selection.name.value)
|
|
357
|
-
if fragment is not None:
|
|
358
|
-
collect_from_selection(fragment.selection_set)
|
|
359
|
-
elif isinstance(selection, InlineFragmentNode):
|
|
360
|
-
collect_from_selection(selection.selection_set)
|
|
361
|
-
|
|
362
|
-
def inspect_selection_set(selection_set: SelectionSetNode | None) -> None:
|
|
363
|
-
if selection_set is None:
|
|
364
|
-
return
|
|
365
|
-
for selection in selection_set.selections:
|
|
366
|
-
if isinstance(selection, FieldNode):
|
|
367
|
-
if selection.name.value == "item":
|
|
368
|
-
collect_from_selection(selection.selection_set)
|
|
369
|
-
else:
|
|
370
|
-
inspect_selection_set(selection.selection_set)
|
|
371
|
-
elif isinstance(selection, FragmentSpreadNode):
|
|
372
|
-
fragment = info.fragments.get(selection.name.value)
|
|
373
|
-
if fragment is not None:
|
|
374
|
-
inspect_selection_set(fragment.selection_set)
|
|
375
|
-
elif isinstance(selection, InlineFragmentNode):
|
|
376
|
-
inspect_selection_set(selection.selection_set)
|
|
377
|
-
|
|
378
|
-
for node in info.field_nodes:
|
|
379
|
-
inspect_selection_set(node.selection_set)
|
|
380
|
-
return property_names
|
|
333
|
+
return collect_selected_graphql_property_names(
|
|
334
|
+
info,
|
|
335
|
+
manager_class,
|
|
336
|
+
root_field="item",
|
|
337
|
+
normalize_name=normalize_graphql_name,
|
|
338
|
+
)
|
|
381
339
|
|
|
382
340
|
|
|
383
341
|
# ---------------------------------------------------------------------------
|
{generalmanager-0.46.0 → generalmanager-0.47.0}/src/general_manager/cache/cache_decorator.py
RENAMED
|
@@ -16,6 +16,11 @@ from typing import (
|
|
|
16
16
|
from django.core.cache import cache as django_cache
|
|
17
17
|
|
|
18
18
|
from general_manager.cache.cache_tracker import DependencyTracker
|
|
19
|
+
from general_manager.cache.dependency_cache import (
|
|
20
|
+
DependencyCacheHit,
|
|
21
|
+
read_dependency_cache_hit,
|
|
22
|
+
replay_dependency_cache_hit,
|
|
23
|
+
)
|
|
19
24
|
from general_manager.cache.dependency_index import (
|
|
20
25
|
Dependency,
|
|
21
26
|
get_dependency_generation,
|
|
@@ -26,9 +31,12 @@ from general_manager.cache.dependency_publish import (
|
|
|
26
31
|
acquire_compute_lease,
|
|
27
32
|
publish_dependency_cache_entry,
|
|
28
33
|
release_compute_lease,
|
|
29
|
-
|
|
34
|
+
wait_for_cached_dependency_hit,
|
|
35
|
+
)
|
|
36
|
+
from general_manager.cache.run_context import (
|
|
37
|
+
current_calculation_run_context,
|
|
38
|
+
ensure_calculation_run_context,
|
|
30
39
|
)
|
|
31
|
-
from general_manager.cache.run_context import ensure_calculation_run_context
|
|
32
40
|
from general_manager.cache.model_dependency_collector import ModelDependencyCollector
|
|
33
41
|
from general_manager.logging import get_logger
|
|
34
42
|
from general_manager.utils.make_cache_key import make_cache_key
|
|
@@ -179,52 +187,59 @@ def cached(
|
|
|
179
187
|
)
|
|
180
188
|
return result
|
|
181
189
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def track_cached_dependencies() -> None:
|
|
185
|
-
cached_deps = cache_backend.get(deps_key)
|
|
186
|
-
if cached_deps:
|
|
187
|
-
for class_name, operation, identifier in cached_deps:
|
|
188
|
-
DependencyTracker.track(class_name, operation, identifier)
|
|
189
|
-
|
|
190
|
-
cached_result = cache_backend.get(key, _SENTINEL)
|
|
191
|
-
if cached_result is not _SENTINEL:
|
|
192
|
-
track_cached_dependencies()
|
|
190
|
+
def return_cached_hit(hit: DependencyCacheHit, message: str) -> object:
|
|
191
|
+
replay_dependency_cache_hit(hit)
|
|
193
192
|
logger.debug(
|
|
194
|
-
|
|
193
|
+
message,
|
|
195
194
|
context={
|
|
196
195
|
"function": func.__qualname__,
|
|
197
196
|
"key": key,
|
|
198
197
|
"scope": scope,
|
|
199
198
|
},
|
|
200
199
|
)
|
|
201
|
-
return
|
|
200
|
+
return hit.value
|
|
201
|
+
|
|
202
|
+
prefetch_context = current_calculation_run_context()
|
|
203
|
+
if prefetch_context is not None:
|
|
204
|
+
prefetched_hit = prefetch_context.get_dependency_cache_hit(
|
|
205
|
+
key, _SENTINEL
|
|
206
|
+
)
|
|
207
|
+
if isinstance(prefetched_hit, DependencyCacheHit):
|
|
208
|
+
return return_cached_hit(
|
|
209
|
+
prefetched_hit,
|
|
210
|
+
"cache hit from dependency prefetch",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
cached_hit = read_dependency_cache_hit(
|
|
214
|
+
cache_backend,
|
|
215
|
+
key,
|
|
216
|
+
sentinel=_SENTINEL,
|
|
217
|
+
)
|
|
218
|
+
if cached_hit is not _SENTINEL:
|
|
219
|
+
return return_cached_hit(cached_hit, "cache hit")
|
|
202
220
|
|
|
203
221
|
lease = acquire_compute_lease(key)
|
|
204
222
|
while lease is None:
|
|
205
|
-
|
|
223
|
+
cached_hit = wait_for_cached_dependency_hit(
|
|
206
224
|
cache_backend,
|
|
207
225
|
key,
|
|
208
226
|
sentinel=_SENTINEL,
|
|
209
227
|
)
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
228
|
+
if cached_hit is not _SENTINEL:
|
|
229
|
+
return return_cached_hit(
|
|
230
|
+
cached_hit,
|
|
213
231
|
"cache hit after waiting for dependency publish",
|
|
214
|
-
context={
|
|
215
|
-
"function": func.__qualname__,
|
|
216
|
-
"key": key,
|
|
217
|
-
"scope": scope,
|
|
218
|
-
},
|
|
219
232
|
)
|
|
220
|
-
return cached_result
|
|
221
233
|
lease = acquire_compute_lease(key)
|
|
222
234
|
|
|
223
235
|
try:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
cached_hit = read_dependency_cache_hit(
|
|
237
|
+
cache_backend,
|
|
238
|
+
key,
|
|
239
|
+
sentinel=_SENTINEL,
|
|
240
|
+
)
|
|
241
|
+
if cached_hit is not _SENTINEL:
|
|
242
|
+
return return_cached_hit(cached_hit, "cache hit")
|
|
228
243
|
|
|
229
244
|
started_generation = get_dependency_generation()
|
|
230
245
|
with DependencyTracker() as dependencies:
|
|
@@ -240,7 +255,6 @@ def cached(
|
|
|
240
255
|
try:
|
|
241
256
|
publish_dependency_cache_entry(
|
|
242
257
|
cache_key=key,
|
|
243
|
-
deps_key=deps_key,
|
|
244
258
|
result=result,
|
|
245
259
|
dependencies=dependencies,
|
|
246
260
|
cache_backend=cache_backend,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Internal helpers for dependency-scoped cache entry reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Iterable, Mapping, Protocol, TypeGuard
|
|
7
|
+
|
|
8
|
+
from general_manager.cache.cache_tracker import DependencyTracker
|
|
9
|
+
from general_manager.cache.dependency_index import Dependency
|
|
10
|
+
|
|
11
|
+
DEPENDENCY_CACHE_ENTRY_VERSION = 1
|
|
12
|
+
_MISSING = object()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class DependencyCacheEntry:
|
|
17
|
+
"""Persisted dependency-cache payload stored at the main cache key."""
|
|
18
|
+
|
|
19
|
+
version: int
|
|
20
|
+
value: Any
|
|
21
|
+
dependencies: frozenset[Dependency]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class DependencyCacheHit:
|
|
26
|
+
"""In-memory representation of a dependency-cache hit."""
|
|
27
|
+
|
|
28
|
+
value: Any
|
|
29
|
+
dependencies: frozenset[Dependency]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DependencyCacheBackend(Protocol):
|
|
33
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
34
|
+
"""Return a cached value or *default* when absent."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def set(self, key: str, value: Any, timeout: int | None = None) -> None:
|
|
38
|
+
"""Store a cached value."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DependencyCacheGetManyBackend(DependencyCacheBackend, Protocol):
|
|
43
|
+
def get_many(self, keys: Iterable[str]) -> Mapping[str, Any]:
|
|
44
|
+
"""Return cached values for all found keys."""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_dependency_cache_entry(
|
|
49
|
+
value: Any,
|
|
50
|
+
dependencies: Iterable[Dependency],
|
|
51
|
+
) -> DependencyCacheEntry:
|
|
52
|
+
"""Build the current persisted dependency-cache payload."""
|
|
53
|
+
return DependencyCacheEntry(
|
|
54
|
+
version=DEPENDENCY_CACHE_ENTRY_VERSION,
|
|
55
|
+
value=value,
|
|
56
|
+
dependencies=frozenset(dependencies),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def read_dependency_cache_hit(
|
|
61
|
+
cache_backend: DependencyCacheBackend,
|
|
62
|
+
cache_key: str,
|
|
63
|
+
*,
|
|
64
|
+
sentinel: Any = _MISSING,
|
|
65
|
+
) -> DependencyCacheHit | Any:
|
|
66
|
+
"""Read one dependency-cache entry, including legacy split entries."""
|
|
67
|
+
payload = cache_backend.get(cache_key, sentinel)
|
|
68
|
+
if payload is sentinel:
|
|
69
|
+
return sentinel
|
|
70
|
+
combined_hit = _combined_payload_to_hit(payload)
|
|
71
|
+
if combined_hit is _MISSING:
|
|
72
|
+
return sentinel
|
|
73
|
+
if combined_hit is not None:
|
|
74
|
+
return combined_hit
|
|
75
|
+
dependency_payload = cache_backend.get(_legacy_deps_key(cache_key), ())
|
|
76
|
+
return DependencyCacheHit(
|
|
77
|
+
value=payload,
|
|
78
|
+
dependencies=frozenset(dependency_payload or ()),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def read_many_dependency_cache_hits(
|
|
83
|
+
cache_backend: DependencyCacheBackend,
|
|
84
|
+
cache_keys: Iterable[str],
|
|
85
|
+
) -> dict[str, DependencyCacheHit]:
|
|
86
|
+
"""Bulk-read dependency-cache hits for known keys."""
|
|
87
|
+
keys = tuple(dict.fromkeys(cache_keys))
|
|
88
|
+
if not keys:
|
|
89
|
+
return {}
|
|
90
|
+
if not _supports_get_many(cache_backend):
|
|
91
|
+
return _read_many_without_get_many(cache_backend, keys)
|
|
92
|
+
|
|
93
|
+
payloads = cache_backend.get_many(keys)
|
|
94
|
+
hits: dict[str, DependencyCacheHit] = {}
|
|
95
|
+
legacy_keys: list[str] = []
|
|
96
|
+
for key in keys:
|
|
97
|
+
if key not in payloads:
|
|
98
|
+
continue
|
|
99
|
+
combined_hit = _combined_payload_to_hit(payloads[key])
|
|
100
|
+
if combined_hit is _MISSING:
|
|
101
|
+
continue
|
|
102
|
+
if isinstance(combined_hit, DependencyCacheHit):
|
|
103
|
+
hits[key] = combined_hit
|
|
104
|
+
continue
|
|
105
|
+
legacy_keys.append(key)
|
|
106
|
+
|
|
107
|
+
if legacy_keys:
|
|
108
|
+
deps_keys = {_legacy_deps_key(key): key for key in legacy_keys}
|
|
109
|
+
legacy_deps = cache_backend.get_many(deps_keys.keys())
|
|
110
|
+
for deps_key, key in deps_keys.items():
|
|
111
|
+
hits[key] = DependencyCacheHit(
|
|
112
|
+
value=payloads[key],
|
|
113
|
+
dependencies=frozenset(legacy_deps.get(deps_key, ()) or ()),
|
|
114
|
+
)
|
|
115
|
+
return hits
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def replay_dependency_cache_hit(hit: DependencyCacheHit) -> None:
|
|
119
|
+
"""Replay cached dependencies into active dependency tracking scopes."""
|
|
120
|
+
for class_name, operation, identifier in hit.dependencies:
|
|
121
|
+
DependencyTracker.track(class_name, operation, identifier)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _legacy_deps_key(cache_key: str) -> str:
|
|
125
|
+
return f"{cache_key}:deps"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _combined_payload_to_hit(payload: Any) -> DependencyCacheHit | None | object:
|
|
129
|
+
if not isinstance(payload, DependencyCacheEntry):
|
|
130
|
+
return None
|
|
131
|
+
if payload.version != DEPENDENCY_CACHE_ENTRY_VERSION:
|
|
132
|
+
return _MISSING
|
|
133
|
+
return DependencyCacheHit(
|
|
134
|
+
value=payload.value,
|
|
135
|
+
dependencies=payload.dependencies,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _supports_get_many(
|
|
140
|
+
cache_backend: DependencyCacheBackend,
|
|
141
|
+
) -> TypeGuard[DependencyCacheGetManyBackend]:
|
|
142
|
+
return callable(getattr(cache_backend, "get_many", None))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _read_many_without_get_many(
|
|
146
|
+
cache_backend: DependencyCacheBackend,
|
|
147
|
+
cache_keys: tuple[str, ...],
|
|
148
|
+
) -> dict[str, DependencyCacheHit]:
|
|
149
|
+
hits: dict[str, DependencyCacheHit] = {}
|
|
150
|
+
for key in cache_keys:
|
|
151
|
+
hit = read_dependency_cache_hit(cache_backend, key, sentinel=_MISSING)
|
|
152
|
+
if hit is not _MISSING:
|
|
153
|
+
hits[key] = hit
|
|
154
|
+
return hits
|