GeneralManager 0.55.0__tar.gz → 0.56.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.55.0/src/GeneralManager.egg-info → generalmanager-0.56.0}/PKG-INFO +1 -1
- {generalmanager-0.55.0 → generalmanager-0.56.0}/pyproject.toml +1 -1
- {generalmanager-0.55.0 → generalmanager-0.56.0/src/GeneralManager.egg-info}/PKG-INFO +1 -1
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/GeneralManager.egg-info/SOURCES.txt +5 -0
- generalmanager-0.56.0/src/general_manager/api/graphql_warmup.py +302 -0
- generalmanager-0.56.0/src/general_manager/api/graphql_warmup_registry.py +315 -0
- generalmanager-0.56.0/src/general_manager/api/graphql_warmup_tasks.py +189 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/property.py +64 -5
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/apps.py +6 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_index.py +52 -9
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/signals.py +24 -8
- generalmanager-0.56.0/src/general_manager/management/commands/graphql_warmup.py +77 -0
- generalmanager-0.56.0/src/general_manager/management/commands/graphql_warmup_refresh_due.py +32 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/LICENSE +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/README.md +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/setup.cfg +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/GeneralManager.egg-info/dependency_links.txt +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/GeneralManager.egg-info/requires.txt +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/GeneralManager.egg-info/top_level.txt +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/api.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/bucket.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/cache.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/factory.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/general_manager.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/interface.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/manager.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/measurement.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/permission.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/rule.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/search.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/utils.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_errors.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_mutations.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_prefetch.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_resolvers.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_search.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_subscription_consumer.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_subscriptions.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_view.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/mutation.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/registry.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/remote_api.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/remote_invalidation.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/remote_invalidation_client.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bootstrap.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/base_bucket.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/calculation_bucket.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/database_bucket.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/group_bucket.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/indexing.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/request_bucket.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/cache_decorator.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/cache_tracker.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_cache.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_matching.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_publish.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_shards.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/model_dependency_collector.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/run_context.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/conf.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/factory/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/factory/auto_factory.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/factory/factories.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/factory/factory_methods.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/base_interface.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/calculation.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/database.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/remote_manager.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/request.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/base.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/builtin.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/calculation/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/calculation/_compat.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/calculation/lifecycle.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/configuration.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/core/observability.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/core/utils.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/exceptions.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/existing_model/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/existing_model/_compat.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/existing_model/resolution.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/factory.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/_compat.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/history.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/lifecycle.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/mutations.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/support.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm_utils/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm_utils/django_manager_utils.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm_utils/field_descriptors.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm_utils/payload_normalizer.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/read_only/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/read_only/_compat.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/read_only/lifecycle.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/read_only/management.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/registry.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/remote_manager.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/request/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/infrastructure/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/infrastructure/startup_hooks.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/infrastructure/system_checks.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/calculation.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/database.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/existing_model.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/read_only.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/remote_manager.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/request.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/manifests/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/manifests/capability_builder.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/manifests/capability_manifest.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/manifests/capability_models.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/orm_interface.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/requests.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/utils/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/utils/database_interface_protocols.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/utils/errors.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/utils/models.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/logging.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/search_index.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/search_reconcile.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/seed_manager_landscape.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/shell.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/workflow_drain_outbox.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/workflow_replay_dead_letters.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/general_manager.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/group_manager.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/input.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/meta.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/measurement/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/measurement/measurement.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/measurement/measurement_field.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/metrics/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/metrics/graphql.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/0001_initial.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/0002_workflow_outbox_scaling_indexes.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/0003_workflow_execution_correlation_constraint.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/0004_search_index_state.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/models.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/audit.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/base_permission.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/graphql_capabilities.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/manager_based_permission.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/mutation_permission.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/permission_checks.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/permission_data_manager.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/utils.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/public_api_registry.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/py.typed +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/rule/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/rule/handler.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/rule/rule.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/async_tasks.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backend.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backend_registry.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/dev.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/meilisearch.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/opensearch.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/typesense.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/config.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/indexer.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/models.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/reconciliation.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/registry.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/tasks.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/utils.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/seeding/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/seeding/manager_landscape.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/args_to_kwargs.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/filter_parser.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/format_string.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/json_encoder.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/make_cache_key.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/none_to_zero.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/path_mapping.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/public_api.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/testing.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/type_checks.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/actions.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backend_registry.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backends/__init__.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backends/celery.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backends/local.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backends/n8n.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/config.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/engine.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/event_registry.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/events.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/models.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/signal_bridge.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/tasks.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/telemetry.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/tests/test_settings.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.0}/tests/test_urls.py +0 -0
- {generalmanager-0.55.0 → generalmanager-0.56.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.56.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.56.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.56.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
|
|
@@ -37,6 +37,9 @@ src/general_manager/api/graphql_search.py
|
|
|
37
37
|
src/general_manager/api/graphql_subscription_consumer.py
|
|
38
38
|
src/general_manager/api/graphql_subscriptions.py
|
|
39
39
|
src/general_manager/api/graphql_view.py
|
|
40
|
+
src/general_manager/api/graphql_warmup.py
|
|
41
|
+
src/general_manager/api/graphql_warmup_registry.py
|
|
42
|
+
src/general_manager/api/graphql_warmup_tasks.py
|
|
40
43
|
src/general_manager/api/mutation.py
|
|
41
44
|
src/general_manager/api/property.py
|
|
42
45
|
src/general_manager/api/registry.py
|
|
@@ -123,6 +126,8 @@ src/general_manager/interface/utils/__init__.py
|
|
|
123
126
|
src/general_manager/interface/utils/database_interface_protocols.py
|
|
124
127
|
src/general_manager/interface/utils/errors.py
|
|
125
128
|
src/general_manager/interface/utils/models.py
|
|
129
|
+
src/general_manager/management/commands/graphql_warmup.py
|
|
130
|
+
src/general_manager/management/commands/graphql_warmup_refresh_due.py
|
|
126
131
|
src/general_manager/management/commands/search_index.py
|
|
127
132
|
src/general_manager/management/commands/search_reconcile.py
|
|
128
133
|
src/general_manager/management/commands/seed_manager_landscape.py
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""GraphQL property warm-up execution helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from django.core.cache import cache as django_cache
|
|
11
|
+
from django.utils import timezone
|
|
12
|
+
from django.utils.module_loading import import_string
|
|
13
|
+
|
|
14
|
+
from general_manager.api.property import GraphQLProperty
|
|
15
|
+
from general_manager.cache.run_context import CalculationRunContext
|
|
16
|
+
from general_manager.conf import get_setting
|
|
17
|
+
from general_manager.logging import get_logger
|
|
18
|
+
from general_manager.utils.make_cache_key import make_cache_key
|
|
19
|
+
from general_manager.api.graphql_warmup_registry import (
|
|
20
|
+
GraphQLWarmUpRecipe,
|
|
21
|
+
acquire_graphql_warmup_recipe_lock,
|
|
22
|
+
due_timeout_graphql_warmup_recipe_keys,
|
|
23
|
+
get_graphql_warmup_recipe,
|
|
24
|
+
register_graphql_warmup_recipe,
|
|
25
|
+
release_graphql_warmup_recipe_lock,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = get_logger("api.graphql_warmup")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class GraphQLWarmUpSummary:
|
|
33
|
+
"""Counts from one warm-up execution."""
|
|
34
|
+
|
|
35
|
+
evaluated: int = 0
|
|
36
|
+
failed: int = 0
|
|
37
|
+
recipes: int = 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def warmable_graphql_properties(
|
|
41
|
+
manager_class: type[Any],
|
|
42
|
+
property_names: Iterable[str] | None = None,
|
|
43
|
+
) -> dict[str, GraphQLProperty]:
|
|
44
|
+
"""Return warm-up-eligible GraphQL properties for a manager class."""
|
|
45
|
+
interface_cls = getattr(manager_class, "Interface", None)
|
|
46
|
+
if interface_cls is None:
|
|
47
|
+
return {}
|
|
48
|
+
available_properties = interface_cls.get_graph_ql_properties()
|
|
49
|
+
selected = set(property_names) if property_names is not None else None
|
|
50
|
+
return {
|
|
51
|
+
name: prop
|
|
52
|
+
for name, prop in available_properties.items()
|
|
53
|
+
if isinstance(prop, GraphQLProperty)
|
|
54
|
+
and prop.warm_up
|
|
55
|
+
and prop.cache in {"dependency", "timeout"}
|
|
56
|
+
and (selected is None or name in selected)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def warm_up_graphql_properties(
|
|
61
|
+
manager_classes: Iterable[type[Any]] | None = None,
|
|
62
|
+
property_names: Iterable[str] | None = None,
|
|
63
|
+
) -> GraphQLWarmUpSummary:
|
|
64
|
+
"""Warm opted-in GraphQL properties by enumerating each manager's ``.all()``."""
|
|
65
|
+
if not graphql_warmup_enabled():
|
|
66
|
+
return GraphQLWarmUpSummary()
|
|
67
|
+
classes = tuple(manager_classes) if manager_classes is not None else _all_managers()
|
|
68
|
+
batch_size = _positive_int_setting("GRAPHQL_WARMUP_BATCH_SIZE", 100)
|
|
69
|
+
warning_threshold = _positive_int_setting(
|
|
70
|
+
"GRAPHQL_WARMUP_WARNING_ITEMS_PER_MANAGER",
|
|
71
|
+
1000,
|
|
72
|
+
)
|
|
73
|
+
total_evaluated = 0
|
|
74
|
+
total_failed = 0
|
|
75
|
+
total_recipes = 0
|
|
76
|
+
|
|
77
|
+
for manager_class in classes:
|
|
78
|
+
properties = warmable_graphql_properties(manager_class, property_names)
|
|
79
|
+
if not properties:
|
|
80
|
+
continue
|
|
81
|
+
manager_path = _manager_path(manager_class)
|
|
82
|
+
instances_seen = 0
|
|
83
|
+
warned = False
|
|
84
|
+
batch: list[Any] = []
|
|
85
|
+
for instance in manager_class.all():
|
|
86
|
+
instances_seen += 1
|
|
87
|
+
if not warned and instances_seen > warning_threshold:
|
|
88
|
+
warned = True
|
|
89
|
+
logger.warning(
|
|
90
|
+
"graphql warm-up manager crossed warning threshold",
|
|
91
|
+
context={
|
|
92
|
+
"manager": manager_class.__name__,
|
|
93
|
+
"threshold": warning_threshold,
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
batch.append(instance)
|
|
97
|
+
if len(batch) >= batch_size:
|
|
98
|
+
evaluated, failed, recipes = _warm_batch(
|
|
99
|
+
batch,
|
|
100
|
+
properties,
|
|
101
|
+
manager_path,
|
|
102
|
+
)
|
|
103
|
+
total_evaluated += evaluated
|
|
104
|
+
total_failed += failed
|
|
105
|
+
total_recipes += recipes
|
|
106
|
+
batch = []
|
|
107
|
+
if batch:
|
|
108
|
+
evaluated, failed, recipes = _warm_batch(batch, properties, manager_path)
|
|
109
|
+
total_evaluated += evaluated
|
|
110
|
+
total_failed += failed
|
|
111
|
+
total_recipes += recipes
|
|
112
|
+
|
|
113
|
+
return GraphQLWarmUpSummary(
|
|
114
|
+
evaluated=total_evaluated,
|
|
115
|
+
failed=total_failed,
|
|
116
|
+
recipes=total_recipes,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def warm_up_graphql_recipe(cache_key: str) -> bool:
|
|
121
|
+
"""Reconstruct and warm one recipe-backed GraphQL property."""
|
|
122
|
+
if not graphql_warmup_enabled():
|
|
123
|
+
return False
|
|
124
|
+
recipe = get_graphql_warmup_recipe(cache_key)
|
|
125
|
+
if recipe is None:
|
|
126
|
+
return False
|
|
127
|
+
lock = acquire_graphql_warmup_recipe_lock(cache_key)
|
|
128
|
+
if lock is None:
|
|
129
|
+
return False
|
|
130
|
+
warmed = False
|
|
131
|
+
try:
|
|
132
|
+
manager_class = import_string(recipe.manager_path)
|
|
133
|
+
instance = manager_class(**recipe.identification)
|
|
134
|
+
interface_cls = getattr(manager_class, "Interface", None)
|
|
135
|
+
if interface_cls is not None:
|
|
136
|
+
prop = interface_cls.get_graph_ql_properties().get(recipe.property_name)
|
|
137
|
+
if isinstance(prop, GraphQLProperty):
|
|
138
|
+
if recipe.cache == "timeout":
|
|
139
|
+
warmed = _refresh_timeout_recipe(instance, prop, recipe)
|
|
140
|
+
else:
|
|
141
|
+
with CalculationRunContext():
|
|
142
|
+
getattr(instance, recipe.property_name)
|
|
143
|
+
register_graphql_warmup_recipe(
|
|
144
|
+
_recipe_for(instance, prop, recipe.property_name)
|
|
145
|
+
)
|
|
146
|
+
warmed = True
|
|
147
|
+
except Exception:
|
|
148
|
+
logger.exception(
|
|
149
|
+
"graphql warm-up recipe failed",
|
|
150
|
+
context={"cache_key": cache_key},
|
|
151
|
+
)
|
|
152
|
+
finally:
|
|
153
|
+
release_graphql_warmup_recipe_lock(lock)
|
|
154
|
+
return warmed
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def refresh_due_graphql_warmup_recipes(limit: int | None = None) -> int:
|
|
158
|
+
"""Refresh due timeout recipes and return the number refreshed."""
|
|
159
|
+
if not graphql_warmup_enabled():
|
|
160
|
+
return 0
|
|
161
|
+
refreshed = 0
|
|
162
|
+
for cache_key in due_timeout_graphql_warmup_recipe_keys(limit=limit):
|
|
163
|
+
if warm_up_graphql_recipe(cache_key):
|
|
164
|
+
refreshed += 1
|
|
165
|
+
return refreshed
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def enqueue_graphql_warmup(
|
|
169
|
+
manager_classes: Iterable[type[Any]] | None = None,
|
|
170
|
+
) -> bool:
|
|
171
|
+
"""Enqueue or run all-entry warm-up through the built-in task adapter."""
|
|
172
|
+
if not graphql_warmup_enabled():
|
|
173
|
+
return False
|
|
174
|
+
from general_manager.api.graphql_warmup_tasks import dispatch_graphql_warmup
|
|
175
|
+
|
|
176
|
+
return dispatch_graphql_warmup(manager_classes)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def enqueue_graphql_recipe_warmup(cache_keys: Iterable[str]) -> bool:
|
|
180
|
+
"""Enqueue recipe re-warm through the built-in task adapter."""
|
|
181
|
+
if not (
|
|
182
|
+
graphql_warmup_enabled()
|
|
183
|
+
and bool(get_setting("GRAPHQL_WARMUP_REWARM_AFTER_INVALIDATION", True))
|
|
184
|
+
):
|
|
185
|
+
return False
|
|
186
|
+
keys = tuple(dict.fromkeys(cache_keys))
|
|
187
|
+
if not keys:
|
|
188
|
+
return False
|
|
189
|
+
from general_manager.api.graphql_warmup_tasks import dispatch_graphql_recipe_warmup
|
|
190
|
+
|
|
191
|
+
return dispatch_graphql_recipe_warmup(keys)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def graphql_warmup_enabled() -> bool:
|
|
195
|
+
"""Return whether framework-owned GraphQL warm-up behavior is enabled."""
|
|
196
|
+
return bool(get_setting("GRAPHQL_WARMUP_ENABLED", False))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _warm_batch(
|
|
200
|
+
instances: list[Any],
|
|
201
|
+
properties: dict[str, GraphQLProperty],
|
|
202
|
+
manager_path: str | None,
|
|
203
|
+
) -> tuple[int, int, int]:
|
|
204
|
+
"""Evaluate one batch of instances and persist successful recipes."""
|
|
205
|
+
candidates: list[GraphQLWarmUpRecipe] = []
|
|
206
|
+
evaluated = 0
|
|
207
|
+
failed = 0
|
|
208
|
+
with CalculationRunContext():
|
|
209
|
+
for instance in instances:
|
|
210
|
+
for property_name, prop in properties.items():
|
|
211
|
+
try:
|
|
212
|
+
getattr(instance, property_name)
|
|
213
|
+
except Exception:
|
|
214
|
+
failed += 1
|
|
215
|
+
logger.exception(
|
|
216
|
+
"graphql property warm-up failed",
|
|
217
|
+
context={
|
|
218
|
+
"manager": instance.__class__.__name__,
|
|
219
|
+
"property": property_name,
|
|
220
|
+
"identification": getattr(
|
|
221
|
+
instance,
|
|
222
|
+
"identification",
|
|
223
|
+
None,
|
|
224
|
+
),
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
continue
|
|
228
|
+
evaluated += 1
|
|
229
|
+
if manager_path is not None:
|
|
230
|
+
candidates.append(_recipe_for(instance, prop, property_name))
|
|
231
|
+
for recipe in candidates:
|
|
232
|
+
register_graphql_warmup_recipe(recipe)
|
|
233
|
+
return evaluated, failed, len(candidates)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _refresh_timeout_recipe(
|
|
237
|
+
instance: Any,
|
|
238
|
+
prop: GraphQLProperty,
|
|
239
|
+
recipe: GraphQLWarmUpRecipe,
|
|
240
|
+
) -> bool:
|
|
241
|
+
"""Refresh one timeout-backed recipe without evicting the old value first."""
|
|
242
|
+
result = prop._raw_fget(instance)
|
|
243
|
+
django_cache.set(recipe.cache_key, result, recipe.timeout)
|
|
244
|
+
register_graphql_warmup_recipe(_recipe_for(instance, prop, recipe.property_name))
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _recipe_for(
|
|
249
|
+
instance: Any,
|
|
250
|
+
prop: GraphQLProperty,
|
|
251
|
+
property_name: str,
|
|
252
|
+
) -> GraphQLWarmUpRecipe:
|
|
253
|
+
"""Build the registry payload for a warmed instance/property pair."""
|
|
254
|
+
cache_key = make_cache_key(prop._get_cached_fget(), (instance,), {})
|
|
255
|
+
timeout = prop.timeout if prop.cache == "timeout" else None
|
|
256
|
+
refresh_at = None
|
|
257
|
+
if prop.cache == "timeout" and timeout is not None:
|
|
258
|
+
refresh_at = timezone.now() + timedelta(
|
|
259
|
+
seconds=timeout * _timeout_refresh_ratio()
|
|
260
|
+
)
|
|
261
|
+
return GraphQLWarmUpRecipe(
|
|
262
|
+
cache_key=cache_key,
|
|
263
|
+
manager_path=_manager_path(instance.__class__) or "",
|
|
264
|
+
property_name=property_name,
|
|
265
|
+
identification=dict(getattr(instance, "identification", {})),
|
|
266
|
+
cache="timeout" if prop.cache == "timeout" else "dependency",
|
|
267
|
+
timeout=timeout,
|
|
268
|
+
refresh_at=refresh_at,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _manager_path(manager_class: type[Any]) -> str | None:
|
|
273
|
+
"""Return an import path for reconstructable manager classes."""
|
|
274
|
+
qualname = getattr(manager_class, "__qualname__", manager_class.__name__)
|
|
275
|
+
if "<locals>" in qualname:
|
|
276
|
+
return None
|
|
277
|
+
return f"{manager_class.__module__}.{qualname}"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _all_managers() -> tuple[type[Any], ...]:
|
|
281
|
+
"""Return all manager classes registered with the GraphQL integration."""
|
|
282
|
+
from general_manager.api.graphql import GraphQL
|
|
283
|
+
|
|
284
|
+
return tuple(GraphQL.manager_registry.values())
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _positive_int_setting(key: str, default: int) -> int:
|
|
288
|
+
"""Read a positive integer warm-up setting with a fallback."""
|
|
289
|
+
raw = get_setting(key, default)
|
|
290
|
+
try:
|
|
291
|
+
return max(1, int(raw))
|
|
292
|
+
except (TypeError, ValueError):
|
|
293
|
+
return default
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _timeout_refresh_ratio() -> float:
|
|
297
|
+
"""Read and clamp the timeout refresh ratio setting."""
|
|
298
|
+
raw = get_setting("GRAPHQL_WARMUP_TIMEOUT_REFRESH_RATIO", 0.8)
|
|
299
|
+
try:
|
|
300
|
+
return max(0.0, min(1.0, float(raw)))
|
|
301
|
+
except (TypeError, ValueError):
|
|
302
|
+
return 0.8
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Cache-backed recipe registry for GraphQL property warm-up."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
from django.core.cache import cache as django_cache
|
|
15
|
+
|
|
16
|
+
GraphQLWarmUpCacheScope = Literal["dependency", "timeout"]
|
|
17
|
+
|
|
18
|
+
RECIPE_VERSION = 1
|
|
19
|
+
KEY_PREFIX = "general_manager:graphql_warmup"
|
|
20
|
+
RECIPE_INDEX_KEY = f"{KEY_PREFIX}:recipes"
|
|
21
|
+
TIMEOUT_RECIPE_INDEX_KEY = f"{KEY_PREFIX}:timeout_recipes"
|
|
22
|
+
LOCK_PREFIX = f"{KEY_PREFIX}:lock"
|
|
23
|
+
INDEX_LOCK_PREFIX = f"{LOCK_PREFIX}:index"
|
|
24
|
+
DEFAULT_RECIPE_LOCK_TIMEOUT = 300
|
|
25
|
+
DEFAULT_INDEX_LOCK_TIMEOUT = 30
|
|
26
|
+
DEFAULT_INDEX_LOCK_WAIT_SECONDS = 1.0
|
|
27
|
+
_LOCAL_LOCKS_GUARD = threading.Lock()
|
|
28
|
+
_LOCAL_LOCKS: dict[str, threading.Lock] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class GraphQLWarmUpRecipe:
|
|
33
|
+
"""Information required to reconstruct one warmed GraphQL property entry."""
|
|
34
|
+
|
|
35
|
+
cache_key: str
|
|
36
|
+
manager_path: str
|
|
37
|
+
property_name: str
|
|
38
|
+
identification: dict[str, Any]
|
|
39
|
+
cache: GraphQLWarmUpCacheScope
|
|
40
|
+
timeout: int | None
|
|
41
|
+
refresh_at: datetime | None
|
|
42
|
+
version: int = RECIPE_VERSION
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class GraphQLWarmUpRecipeLock:
|
|
47
|
+
"""Token proving ownership of one recipe warm-up attempt."""
|
|
48
|
+
|
|
49
|
+
key: str
|
|
50
|
+
token: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GraphQLWarmUpRecipeLockTimeoutError(TimeoutError):
|
|
54
|
+
"""Raised when an index update lock cannot be acquired quickly."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, lock_key: str) -> None:
|
|
57
|
+
"""Build an error message for a cache lock acquisition timeout."""
|
|
58
|
+
super().__init__(f"Timed out acquiring GraphQL warm-up lock: {lock_key}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def register_graphql_warmup_recipe(
|
|
62
|
+
recipe: GraphQLWarmUpRecipe,
|
|
63
|
+
*,
|
|
64
|
+
cache_backend: Any = django_cache,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Persist one warm-up recipe and update registry indexes."""
|
|
67
|
+
cache_backend.set(_recipe_key(recipe.cache_key), recipe, None)
|
|
68
|
+
_add_index_member(RECIPE_INDEX_KEY, recipe.cache_key, cache_backend=cache_backend)
|
|
69
|
+
if recipe.cache == "timeout" and recipe.refresh_at is not None:
|
|
70
|
+
_add_index_member(
|
|
71
|
+
TIMEOUT_RECIPE_INDEX_KEY,
|
|
72
|
+
recipe.cache_key,
|
|
73
|
+
cache_backend=cache_backend,
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
_remove_index_member(
|
|
77
|
+
TIMEOUT_RECIPE_INDEX_KEY,
|
|
78
|
+
recipe.cache_key,
|
|
79
|
+
cache_backend=cache_backend,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_graphql_warmup_recipe(
|
|
84
|
+
cache_key: str,
|
|
85
|
+
*,
|
|
86
|
+
cache_backend: Any = django_cache,
|
|
87
|
+
) -> GraphQLWarmUpRecipe | None:
|
|
88
|
+
"""Return the recipe for *cache_key*, if it exists and matches this version."""
|
|
89
|
+
recipe = cache_backend.get(_recipe_key(cache_key))
|
|
90
|
+
if not isinstance(recipe, GraphQLWarmUpRecipe):
|
|
91
|
+
return None
|
|
92
|
+
if recipe.version != RECIPE_VERSION:
|
|
93
|
+
return None
|
|
94
|
+
return recipe
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_graphql_warmup_recipes(
|
|
98
|
+
cache_keys: Iterable[str],
|
|
99
|
+
*,
|
|
100
|
+
cache_backend: Any = django_cache,
|
|
101
|
+
) -> dict[str, GraphQLWarmUpRecipe]:
|
|
102
|
+
"""Return existing recipes for *cache_keys* keyed by cache key."""
|
|
103
|
+
recipes: dict[str, GraphQLWarmUpRecipe] = {}
|
|
104
|
+
for cache_key in tuple(dict.fromkeys(cache_keys)):
|
|
105
|
+
recipe = get_graphql_warmup_recipe(cache_key, cache_backend=cache_backend)
|
|
106
|
+
if recipe is not None:
|
|
107
|
+
recipes[cache_key] = recipe
|
|
108
|
+
return recipes
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def graphql_warmup_recipe_keys(
|
|
112
|
+
*,
|
|
113
|
+
cache_backend: Any = django_cache,
|
|
114
|
+
) -> tuple[str, ...]:
|
|
115
|
+
"""Return known recipe cache keys in deterministic order."""
|
|
116
|
+
return tuple(sorted(_read_index(RECIPE_INDEX_KEY, cache_backend=cache_backend)))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def due_timeout_graphql_warmup_recipe_keys(
|
|
120
|
+
*,
|
|
121
|
+
now: datetime | None = None,
|
|
122
|
+
limit: int | None = None,
|
|
123
|
+
cache_backend: Any = django_cache,
|
|
124
|
+
) -> tuple[str, ...]:
|
|
125
|
+
"""Return timeout recipe keys whose refresh time has arrived."""
|
|
126
|
+
current_time = now or datetime.now(UTC)
|
|
127
|
+
due: list[tuple[datetime, str]] = []
|
|
128
|
+
for cache_key in _read_index(TIMEOUT_RECIPE_INDEX_KEY, cache_backend=cache_backend):
|
|
129
|
+
recipe = get_graphql_warmup_recipe(cache_key, cache_backend=cache_backend)
|
|
130
|
+
if recipe is None or recipe.cache != "timeout" or recipe.refresh_at is None:
|
|
131
|
+
_remove_index_member(
|
|
132
|
+
TIMEOUT_RECIPE_INDEX_KEY,
|
|
133
|
+
cache_key,
|
|
134
|
+
cache_backend=cache_backend,
|
|
135
|
+
)
|
|
136
|
+
continue
|
|
137
|
+
if recipe.refresh_at <= current_time:
|
|
138
|
+
due.append((recipe.refresh_at, cache_key))
|
|
139
|
+
ordered = tuple(cache_key for _refresh_at, cache_key in sorted(due))
|
|
140
|
+
if limit is None:
|
|
141
|
+
return ordered
|
|
142
|
+
return ordered[: max(0, limit)]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def delete_graphql_warmup_recipe(
|
|
146
|
+
cache_key: str,
|
|
147
|
+
*,
|
|
148
|
+
cache_backend: Any = django_cache,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Remove one recipe and all index references to it."""
|
|
151
|
+
cache_backend.delete(_recipe_key(cache_key))
|
|
152
|
+
_remove_index_member(RECIPE_INDEX_KEY, cache_key, cache_backend=cache_backend)
|
|
153
|
+
_remove_index_member(
|
|
154
|
+
TIMEOUT_RECIPE_INDEX_KEY,
|
|
155
|
+
cache_key,
|
|
156
|
+
cache_backend=cache_backend,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def acquire_graphql_warmup_recipe_lock(
|
|
161
|
+
cache_key: str,
|
|
162
|
+
*,
|
|
163
|
+
timeout: int = DEFAULT_RECIPE_LOCK_TIMEOUT,
|
|
164
|
+
cache_backend: Any = django_cache,
|
|
165
|
+
) -> GraphQLWarmUpRecipeLock | None:
|
|
166
|
+
"""Acquire a best-effort per-recipe execution lock."""
|
|
167
|
+
lock_key = _lock_key(cache_key)
|
|
168
|
+
token = uuid.uuid4().hex
|
|
169
|
+
if not cache_backend.add(lock_key, token, timeout):
|
|
170
|
+
return None
|
|
171
|
+
return GraphQLWarmUpRecipeLock(key=lock_key, token=token)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def release_graphql_warmup_recipe_lock(
|
|
175
|
+
lock: GraphQLWarmUpRecipeLock,
|
|
176
|
+
*,
|
|
177
|
+
cache_backend: Any = django_cache,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Release a recipe lock without deleting another worker's newer lock."""
|
|
180
|
+
_delete_lock_if_owned(lock, cache_backend=cache_backend)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _recipe_key(cache_key: str) -> str:
|
|
184
|
+
"""Return the cache key used for one recipe payload."""
|
|
185
|
+
return f"{KEY_PREFIX}:recipe:{cache_key}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _lock_key(cache_key: str) -> str:
|
|
189
|
+
"""Return the cache key used for one recipe lock."""
|
|
190
|
+
return f"{LOCK_PREFIX}:{cache_key}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _read_index(index_key: str, *, cache_backend: Any) -> frozenset[str]:
|
|
194
|
+
"""Read an index payload, ignoring malformed values."""
|
|
195
|
+
value = cache_backend.get(index_key, frozenset())
|
|
196
|
+
if not isinstance(value, frozenset):
|
|
197
|
+
return frozenset()
|
|
198
|
+
return value
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _write_index(
|
|
202
|
+
index_key: str,
|
|
203
|
+
values: Iterable[str],
|
|
204
|
+
*,
|
|
205
|
+
cache_backend: Any,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Write an index payload as an immutable set."""
|
|
208
|
+
cache_backend.set(index_key, frozenset(values), None)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _add_index_member(
|
|
212
|
+
index_key: str,
|
|
213
|
+
member: str,
|
|
214
|
+
*,
|
|
215
|
+
cache_backend: Any,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Add one member to an index under an index-level cache lock."""
|
|
218
|
+
with _locked_index_update(index_key, cache_backend=cache_backend):
|
|
219
|
+
_write_index(
|
|
220
|
+
index_key,
|
|
221
|
+
_read_index(index_key, cache_backend=cache_backend) | frozenset((member,)),
|
|
222
|
+
cache_backend=cache_backend,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _remove_index_member(
|
|
227
|
+
index_key: str,
|
|
228
|
+
member: str,
|
|
229
|
+
*,
|
|
230
|
+
cache_backend: Any,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Remove one member from an index under an index-level cache lock."""
|
|
233
|
+
with _locked_index_update(index_key, cache_backend=cache_backend):
|
|
234
|
+
_write_index(
|
|
235
|
+
index_key,
|
|
236
|
+
_read_index(index_key, cache_backend=cache_backend) - frozenset((member,)),
|
|
237
|
+
cache_backend=cache_backend,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@contextmanager
|
|
242
|
+
def _locked_index_update(index_key: str, *, cache_backend: Any):
|
|
243
|
+
"""Acquire and release the cache lock guarding one index update."""
|
|
244
|
+
lock = _acquire_cache_lock(
|
|
245
|
+
f"{INDEX_LOCK_PREFIX}:{index_key}",
|
|
246
|
+
timeout=DEFAULT_INDEX_LOCK_TIMEOUT,
|
|
247
|
+
wait_seconds=DEFAULT_INDEX_LOCK_WAIT_SECONDS,
|
|
248
|
+
cache_backend=cache_backend,
|
|
249
|
+
)
|
|
250
|
+
try:
|
|
251
|
+
yield
|
|
252
|
+
finally:
|
|
253
|
+
release_graphql_warmup_recipe_lock(lock, cache_backend=cache_backend)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _acquire_cache_lock(
|
|
257
|
+
lock_key: str,
|
|
258
|
+
*,
|
|
259
|
+
timeout: int,
|
|
260
|
+
wait_seconds: float,
|
|
261
|
+
cache_backend: Any,
|
|
262
|
+
) -> GraphQLWarmUpRecipeLock:
|
|
263
|
+
"""Acquire a cache-backed lock, waiting briefly for contention to clear."""
|
|
264
|
+
token = uuid.uuid4().hex
|
|
265
|
+
deadline = time.monotonic() + wait_seconds
|
|
266
|
+
while True:
|
|
267
|
+
if cache_backend.add(lock_key, token, timeout):
|
|
268
|
+
return GraphQLWarmUpRecipeLock(key=lock_key, token=token)
|
|
269
|
+
if time.monotonic() >= deadline:
|
|
270
|
+
raise GraphQLWarmUpRecipeLockTimeoutError(lock_key)
|
|
271
|
+
time.sleep(0.01)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _delete_lock_if_owned(
|
|
275
|
+
lock: GraphQLWarmUpRecipeLock,
|
|
276
|
+
*,
|
|
277
|
+
cache_backend: Any,
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Delete a lock only when the backend still stores the caller's token."""
|
|
280
|
+
if _delete_redis_lock_if_owned(lock, cache_backend=cache_backend):
|
|
281
|
+
return
|
|
282
|
+
with _local_lock_for(lock.key):
|
|
283
|
+
if cache_backend.get(lock.key) != lock.token:
|
|
284
|
+
return
|
|
285
|
+
if cache_backend.get(lock.key) != lock.token:
|
|
286
|
+
return
|
|
287
|
+
cache_backend.delete(lock.key)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _delete_redis_lock_if_owned(
|
|
291
|
+
lock: GraphQLWarmUpRecipeLock,
|
|
292
|
+
*,
|
|
293
|
+
cache_backend: Any,
|
|
294
|
+
) -> bool:
|
|
295
|
+
"""Use Redis compare-and-delete when the cache backend exposes a client."""
|
|
296
|
+
client_holder = getattr(cache_backend, "client", None)
|
|
297
|
+
get_client = getattr(client_holder, "get_client", None)
|
|
298
|
+
if get_client is None:
|
|
299
|
+
return False
|
|
300
|
+
make_key = getattr(cache_backend, "make_key", lambda key: key)
|
|
301
|
+
script = (
|
|
302
|
+
"if redis.call('get', KEYS[1]) == ARGV[1] "
|
|
303
|
+
"then return redis.call('del', KEYS[1]) else return 0 end"
|
|
304
|
+
)
|
|
305
|
+
try:
|
|
306
|
+
get_client(write=True).eval(script, 1, make_key(lock.key), lock.token)
|
|
307
|
+
except (AttributeError, OSError, TypeError, ValueError):
|
|
308
|
+
return False
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _local_lock_for(lock_key: str) -> threading.Lock:
|
|
313
|
+
"""Return a process-local fallback mutex for non-Redis cache backends."""
|
|
314
|
+
with _LOCAL_LOCKS_GUARD:
|
|
315
|
+
return _LOCAL_LOCKS.setdefault(lock_key, threading.Lock())
|