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.
Files changed (212) hide show
  1. {generalmanager-0.55.0/src/GeneralManager.egg-info → generalmanager-0.56.0}/PKG-INFO +1 -1
  2. {generalmanager-0.55.0 → generalmanager-0.56.0}/pyproject.toml +1 -1
  3. {generalmanager-0.55.0 → generalmanager-0.56.0/src/GeneralManager.egg-info}/PKG-INFO +1 -1
  4. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/GeneralManager.egg-info/SOURCES.txt +5 -0
  5. generalmanager-0.56.0/src/general_manager/api/graphql_warmup.py +302 -0
  6. generalmanager-0.56.0/src/general_manager/api/graphql_warmup_registry.py +315 -0
  7. generalmanager-0.56.0/src/general_manager/api/graphql_warmup_tasks.py +189 -0
  8. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/property.py +64 -5
  9. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/apps.py +6 -0
  10. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_index.py +52 -9
  11. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/signals.py +24 -8
  12. generalmanager-0.56.0/src/general_manager/management/commands/graphql_warmup.py +77 -0
  13. generalmanager-0.56.0/src/general_manager/management/commands/graphql_warmup_refresh_due.py +32 -0
  14. {generalmanager-0.55.0 → generalmanager-0.56.0}/LICENSE +0 -0
  15. {generalmanager-0.55.0 → generalmanager-0.56.0}/README.md +0 -0
  16. {generalmanager-0.55.0 → generalmanager-0.56.0}/setup.cfg +0 -0
  17. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/GeneralManager.egg-info/dependency_links.txt +0 -0
  18. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/GeneralManager.egg-info/requires.txt +0 -0
  19. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/GeneralManager.egg-info/top_level.txt +0 -0
  20. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/__init__.py +0 -0
  21. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/__init__.py +0 -0
  22. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/api.py +0 -0
  23. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/bucket.py +0 -0
  24. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/cache.py +0 -0
  25. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/factory.py +0 -0
  26. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/general_manager.py +0 -0
  27. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/interface.py +0 -0
  28. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/manager.py +0 -0
  29. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/measurement.py +0 -0
  30. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/permission.py +0 -0
  31. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/rule.py +0 -0
  32. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/search.py +0 -0
  33. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/_types/utils.py +0 -0
  34. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/__init__.py +0 -0
  35. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql.py +0 -0
  36. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_errors.py +0 -0
  37. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_mutations.py +0 -0
  38. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_prefetch.py +0 -0
  39. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_resolvers.py +0 -0
  40. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_search.py +0 -0
  41. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_subscription_consumer.py +0 -0
  42. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_subscriptions.py +0 -0
  43. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/graphql_view.py +0 -0
  44. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/mutation.py +0 -0
  45. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/registry.py +0 -0
  46. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/remote_api.py +0 -0
  47. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/remote_invalidation.py +0 -0
  48. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/api/remote_invalidation_client.py +0 -0
  49. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bootstrap.py +0 -0
  50. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/__init__.py +0 -0
  51. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/base_bucket.py +0 -0
  52. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/calculation_bucket.py +0 -0
  53. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/database_bucket.py +0 -0
  54. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/group_bucket.py +0 -0
  55. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/indexing.py +0 -0
  56. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/bucket/request_bucket.py +0 -0
  57. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/__init__.py +0 -0
  58. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/cache_decorator.py +0 -0
  59. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/cache_tracker.py +0 -0
  60. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_cache.py +0 -0
  61. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_matching.py +0 -0
  62. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_publish.py +0 -0
  63. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/dependency_shards.py +0 -0
  64. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/model_dependency_collector.py +0 -0
  65. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/cache/run_context.py +0 -0
  66. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/conf.py +0 -0
  67. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/factory/__init__.py +0 -0
  68. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/factory/auto_factory.py +0 -0
  69. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/factory/factories.py +0 -0
  70. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/factory/factory_methods.py +0 -0
  71. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/__init__.py +0 -0
  72. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/base_interface.py +0 -0
  73. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/__init__.py +0 -0
  74. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/calculation.py +0 -0
  75. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/database.py +0 -0
  76. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/remote_manager.py +0 -0
  77. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/bundles/request.py +0 -0
  78. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/__init__.py +0 -0
  79. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/base.py +0 -0
  80. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/builtin.py +0 -0
  81. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/calculation/__init__.py +0 -0
  82. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/calculation/_compat.py +0 -0
  83. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/calculation/lifecycle.py +0 -0
  84. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/configuration.py +0 -0
  85. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/core/observability.py +0 -0
  86. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/core/utils.py +0 -0
  87. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/exceptions.py +0 -0
  88. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/existing_model/__init__.py +0 -0
  89. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/existing_model/_compat.py +0 -0
  90. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/existing_model/resolution.py +0 -0
  91. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/factory.py +0 -0
  92. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/__init__.py +0 -0
  93. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/_compat.py +0 -0
  94. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/history.py +0 -0
  95. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/lifecycle.py +0 -0
  96. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/mutations.py +0 -0
  97. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm/support.py +0 -0
  98. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm_utils/__init__.py +0 -0
  99. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm_utils/django_manager_utils.py +0 -0
  100. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm_utils/field_descriptors.py +0 -0
  101. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/orm_utils/payload_normalizer.py +0 -0
  102. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/read_only/__init__.py +0 -0
  103. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/read_only/_compat.py +0 -0
  104. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/read_only/lifecycle.py +0 -0
  105. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/read_only/management.py +0 -0
  106. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/registry.py +0 -0
  107. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/remote_manager.py +0 -0
  108. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/capabilities/request/__init__.py +0 -0
  109. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/infrastructure/__init__.py +0 -0
  110. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/infrastructure/startup_hooks.py +0 -0
  111. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/infrastructure/system_checks.py +0 -0
  112. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/__init__.py +0 -0
  113. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/calculation.py +0 -0
  114. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/database.py +0 -0
  115. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/existing_model.py +0 -0
  116. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/read_only.py +0 -0
  117. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/remote_manager.py +0 -0
  118. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/interfaces/request.py +0 -0
  119. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/manifests/__init__.py +0 -0
  120. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/manifests/capability_builder.py +0 -0
  121. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/manifests/capability_manifest.py +0 -0
  122. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/manifests/capability_models.py +0 -0
  123. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/orm_interface.py +0 -0
  124. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/requests.py +0 -0
  125. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/utils/__init__.py +0 -0
  126. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/utils/database_interface_protocols.py +0 -0
  127. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/utils/errors.py +0 -0
  128. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/interface/utils/models.py +0 -0
  129. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/logging.py +0 -0
  130. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/search_index.py +0 -0
  131. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/search_reconcile.py +0 -0
  132. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/seed_manager_landscape.py +0 -0
  133. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/shell.py +0 -0
  134. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/workflow_drain_outbox.py +0 -0
  135. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/management/commands/workflow_replay_dead_letters.py +0 -0
  136. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/__init__.py +0 -0
  137. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/general_manager.py +0 -0
  138. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/group_manager.py +0 -0
  139. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/input.py +0 -0
  140. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/manager/meta.py +0 -0
  141. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/measurement/__init__.py +0 -0
  142. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/measurement/measurement.py +0 -0
  143. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/measurement/measurement_field.py +0 -0
  144. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/metrics/__init__.py +0 -0
  145. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/metrics/graphql.py +0 -0
  146. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/0001_initial.py +0 -0
  147. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/0002_workflow_outbox_scaling_indexes.py +0 -0
  148. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/0003_workflow_execution_correlation_constraint.py +0 -0
  149. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/0004_search_index_state.py +0 -0
  150. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/migrations/__init__.py +0 -0
  151. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/models.py +0 -0
  152. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/__init__.py +0 -0
  153. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/audit.py +0 -0
  154. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/base_permission.py +0 -0
  155. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/graphql_capabilities.py +0 -0
  156. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/manager_based_permission.py +0 -0
  157. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/mutation_permission.py +0 -0
  158. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/permission_checks.py +0 -0
  159. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/permission_data_manager.py +0 -0
  160. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/permission/utils.py +0 -0
  161. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/public_api_registry.py +0 -0
  162. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/py.typed +0 -0
  163. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/rule/__init__.py +0 -0
  164. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/rule/handler.py +0 -0
  165. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/rule/rule.py +0 -0
  166. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/__init__.py +0 -0
  167. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/async_tasks.py +0 -0
  168. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backend.py +0 -0
  169. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backend_registry.py +0 -0
  170. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/__init__.py +0 -0
  171. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/dev.py +0 -0
  172. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/meilisearch.py +0 -0
  173. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/opensearch.py +0 -0
  174. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/backends/typesense.py +0 -0
  175. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/config.py +0 -0
  176. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/indexer.py +0 -0
  177. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/models.py +0 -0
  178. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/reconciliation.py +0 -0
  179. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/registry.py +0 -0
  180. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/tasks.py +0 -0
  181. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/search/utils.py +0 -0
  182. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/seeding/__init__.py +0 -0
  183. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/seeding/manager_landscape.py +0 -0
  184. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/__init__.py +0 -0
  185. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/args_to_kwargs.py +0 -0
  186. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/filter_parser.py +0 -0
  187. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/format_string.py +0 -0
  188. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/json_encoder.py +0 -0
  189. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/make_cache_key.py +0 -0
  190. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/none_to_zero.py +0 -0
  191. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/path_mapping.py +0 -0
  192. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/public_api.py +0 -0
  193. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/testing.py +0 -0
  194. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/utils/type_checks.py +0 -0
  195. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/__init__.py +0 -0
  196. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/actions.py +0 -0
  197. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backend_registry.py +0 -0
  198. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backends/__init__.py +0 -0
  199. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backends/celery.py +0 -0
  200. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backends/local.py +0 -0
  201. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/backends/n8n.py +0 -0
  202. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/config.py +0 -0
  203. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/engine.py +0 -0
  204. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/event_registry.py +0 -0
  205. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/events.py +0 -0
  206. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/models.py +0 -0
  207. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/signal_bridge.py +0 -0
  208. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/tasks.py +0 -0
  209. {generalmanager-0.55.0 → generalmanager-0.56.0}/src/general_manager/workflow/telemetry.py +0 -0
  210. {generalmanager-0.55.0 → generalmanager-0.56.0}/tests/test_settings.py +0 -0
  211. {generalmanager-0.55.0 → generalmanager-0.56.0}/tests/test_urls.py +0 -0
  212. {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.55.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.55.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.55.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())