GeneralManager 0.68.3__tar.gz → 0.69.1__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 (298) hide show
  1. {generalmanager-0.68.3/src/GeneralManager.egg-info → generalmanager-0.69.1}/PKG-INFO +1 -1
  2. {generalmanager-0.68.3 → generalmanager-0.69.1}/pyproject.toml +1 -1
  3. {generalmanager-0.68.3 → generalmanager-0.69.1/src/GeneralManager.egg-info}/PKG-INFO +1 -1
  4. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql.py +37 -29
  5. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/remote_invalidation.py +25 -17
  6. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/data_change_context.py +77 -6
  7. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/dependency_index.py +32 -21
  8. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/run_context.py +127 -7
  9. generalmanager-0.69.1/src/general_manager/cache/run_context_lru.py +793 -0
  10. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/signals.py +88 -14
  11. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/invalidation.py +50 -44
  12. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/signal_bridge.py +22 -13
  13. generalmanager-0.68.3/src/general_manager/cache/run_context_lru.py +0 -460
  14. {generalmanager-0.68.3 → generalmanager-0.69.1}/LICENSE +0 -0
  15. {generalmanager-0.68.3 → generalmanager-0.69.1}/README.md +0 -0
  16. {generalmanager-0.68.3 → generalmanager-0.69.1}/setup.cfg +0 -0
  17. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/GeneralManager.egg-info/SOURCES.txt +0 -0
  18. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/GeneralManager.egg-info/dependency_links.txt +0 -0
  19. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/GeneralManager.egg-info/requires.txt +0 -0
  20. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/GeneralManager.egg-info/top_level.txt +0 -0
  21. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/__init__.py +0 -0
  22. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/__init__.py +0 -0
  23. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/api.py +0 -0
  24. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/bucket.py +0 -0
  25. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/cache.py +0 -0
  26. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/dataframes.py +0 -0
  27. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/factory.py +0 -0
  28. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/general_manager.py +0 -0
  29. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/interface.py +0 -0
  30. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/manager.py +0 -0
  31. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/measurement.py +0 -0
  32. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/permission.py +0 -0
  33. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/rule.py +0 -0
  34. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/search.py +0 -0
  35. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/_types/utils.py +0 -0
  36. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/__init__.py +0 -0
  37. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_as_of.py +0 -0
  38. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_errors.py +0 -0
  39. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_mutations.py +0 -0
  40. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_prefetch.py +0 -0
  41. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_relations.py +0 -0
  42. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_resolvers.py +0 -0
  43. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_search.py +0 -0
  44. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_subscription_consumer.py +0 -0
  45. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_subscriptions.py +0 -0
  46. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_view.py +0 -0
  47. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_warmup.py +0 -0
  48. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_warmup_registry.py +0 -0
  49. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/graphql_warmup_tasks.py +0 -0
  50. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/mutation.py +0 -0
  51. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/notification_batching.py +0 -0
  52. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/property.py +0 -0
  53. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/registry.py +0 -0
  54. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/remote_api.py +0 -0
  55. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/api/remote_invalidation_client.py +0 -0
  56. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/apps.py +0 -0
  57. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/as_of.py +0 -0
  58. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/bootstrap.py +0 -0
  59. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/bucket/__init__.py +0 -0
  60. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/bucket/base_bucket.py +0 -0
  61. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/bucket/calculation_bucket.py +0 -0
  62. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/bucket/database_bucket.py +0 -0
  63. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/bucket/group_bucket.py +0 -0
  64. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/bucket/indexing.py +0 -0
  65. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/bucket/request_bucket.py +0 -0
  66. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/__init__.py +0 -0
  67. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/cache_decorator.py +0 -0
  68. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/cache_tracker.py +0 -0
  69. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/dependency_cache.py +0 -0
  70. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/dependency_matching.py +0 -0
  71. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/dependency_publish.py +0 -0
  72. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/dependency_shards.py +0 -0
  73. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/cache/model_dependency_collector.py +0 -0
  74. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/__init__.py +0 -0
  75. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/audit.py +0 -0
  76. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/bootstrap.py +0 -0
  77. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/checks.py +0 -0
  78. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/consumer.py +0 -0
  79. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/errors.py +0 -0
  80. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/__init__.py +0 -0
  81. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/__main__.py +0 -0
  82. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/baseline.py +0 -0
  83. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/datasets/basic_queries.yaml +0 -0
  84. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/datasets/demo_readiness.yaml +0 -0
  85. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/datasets/edge_cases.yaml +0 -0
  86. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/datasets/follow_ups.yaml +0 -0
  87. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/datasets/large_schema.yaml +0 -0
  88. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/datasets/multi_hop.yaml +0 -0
  89. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/diagnostics.py +0 -0
  90. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/fingerprints.py +0 -0
  91. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/fixtures.py +0 -0
  92. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/judges/__init__.py +0 -0
  93. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/judges/answer_quality.py +0 -0
  94. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/judges/contract.py +0 -0
  95. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/judges/result_accuracy.py +0 -0
  96. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/judges/tool_sequence.py +0 -0
  97. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/readiness.py +0 -0
  98. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/runner.py +0 -0
  99. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/evals/traces.py +0 -0
  100. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/grounding.py +0 -0
  101. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/models.py +0 -0
  102. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/providers/__init__.py +0 -0
  103. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/providers/_shared.py +0 -0
  104. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/providers/anthropic.py +0 -0
  105. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/providers/base.py +0 -0
  106. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/providers/google.py +0 -0
  107. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/providers/ollama.py +0 -0
  108. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/providers/openai.py +0 -0
  109. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/rate_limits.py +0 -0
  110. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/routing.py +0 -0
  111. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/schema_index.py +0 -0
  112. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/settings.py +0 -0
  113. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/signals.py +0 -0
  114. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/system_prompt.py +0 -0
  115. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/tool_metadata.py +0 -0
  116. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/tools.py +0 -0
  117. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/chat/views.py +0 -0
  118. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/conf.py +0 -0
  119. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/dataframes/__init__.py +0 -0
  120. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/dataframes/measurements.py +0 -0
  121. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/factory/__init__.py +0 -0
  122. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/factory/auto_factory.py +0 -0
  123. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/factory/factories.py +0 -0
  124. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/factory/factory_methods.py +0 -0
  125. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/__init__.py +0 -0
  126. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/base_interface.py +0 -0
  127. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/bundles/__init__.py +0 -0
  128. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/bundles/calculation.py +0 -0
  129. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/bundles/database.py +0 -0
  130. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/bundles/remote_manager.py +0 -0
  131. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/bundles/request.py +0 -0
  132. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/__init__.py +0 -0
  133. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/base.py +0 -0
  134. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/builtin.py +0 -0
  135. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/calculation/__init__.py +0 -0
  136. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/calculation/_compat.py +0 -0
  137. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/calculation/lifecycle.py +0 -0
  138. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/configuration.py +0 -0
  139. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/core/observability.py +0 -0
  140. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/core/utils.py +0 -0
  141. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/exceptions.py +0 -0
  142. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/existing_model/__init__.py +0 -0
  143. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/existing_model/_compat.py +0 -0
  144. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/existing_model/resolution.py +0 -0
  145. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/factory.py +0 -0
  146. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm/__init__.py +0 -0
  147. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm/_compat.py +0 -0
  148. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm/history.py +0 -0
  149. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm/lifecycle.py +0 -0
  150. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm/mutations.py +0 -0
  151. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm/support.py +0 -0
  152. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm_utils/__init__.py +0 -0
  153. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm_utils/django_manager_utils.py +0 -0
  154. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm_utils/field_descriptors.py +0 -0
  155. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/orm_utils/payload_normalizer.py +0 -0
  156. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/read_only/__init__.py +0 -0
  157. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/read_only/_compat.py +0 -0
  158. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/read_only/_types.py +0 -0
  159. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/read_only/lifecycle.py +0 -0
  160. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/read_only/management.py +0 -0
  161. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/registry.py +0 -0
  162. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/remote_manager.py +0 -0
  163. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/capabilities/request/__init__.py +0 -0
  164. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/infrastructure/__init__.py +0 -0
  165. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/infrastructure/startup_hooks.py +0 -0
  166. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/infrastructure/system_checks.py +0 -0
  167. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/interfaces/__init__.py +0 -0
  168. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/interfaces/calculation.py +0 -0
  169. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/interfaces/database.py +0 -0
  170. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/interfaces/existing_model.py +0 -0
  171. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/interfaces/read_only.py +0 -0
  172. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/interfaces/remote_manager.py +0 -0
  173. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/interfaces/request.py +0 -0
  174. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/manifests/__init__.py +0 -0
  175. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/manifests/capability_builder.py +0 -0
  176. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/manifests/capability_manifest.py +0 -0
  177. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/manifests/capability_models.py +0 -0
  178. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/orm_interface.py +0 -0
  179. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/requests.py +0 -0
  180. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/utils/__init__.py +0 -0
  181. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/utils/database_interface_protocols.py +0 -0
  182. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/utils/errors.py +0 -0
  183. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/utils/history.py +0 -0
  184. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/interface/utils/models.py +0 -0
  185. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/logging.py +0 -0
  186. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/chat_cleanup.py +0 -0
  187. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/cleanup_upload_intents.py +0 -0
  188. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/graphql_warmup.py +0 -0
  189. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/graphql_warmup_refresh_due.py +0 -0
  190. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/search_index.py +0 -0
  191. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/search_reconcile.py +0 -0
  192. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/seed_manager_landscape.py +0 -0
  193. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/shell.py +0 -0
  194. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/workflow_drain_outbox.py +0 -0
  195. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/management/commands/workflow_replay_dead_letters.py +0 -0
  196. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/manager/__init__.py +0 -0
  197. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/manager/general_manager.py +0 -0
  198. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/manager/group_manager.py +0 -0
  199. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/manager/input.py +0 -0
  200. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/manager/meta.py +0 -0
  201. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/measurement/__init__.py +0 -0
  202. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/measurement/measurement.py +0 -0
  203. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/measurement/measurement_field.py +0 -0
  204. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/metrics/__init__.py +0 -0
  205. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/metrics/graphql.py +0 -0
  206. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0001_initial.py +0 -0
  207. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0002_workflow_outbox_scaling_indexes.py +0 -0
  208. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0003_workflow_execution_correlation_constraint.py +0 -0
  209. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0004_chat_persistence_models.py +0 -0
  210. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0004_search_index_state.py +0 -0
  211. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0005_merge_chat_search.py +0 -0
  212. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0006_chat_pending_confirmation_scoped_ids.py +0 -0
  213. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0007_upload_intent.py +0 -0
  214. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0008_upload_finalization_versions.py +0 -0
  215. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0009_upload_cleanup_state.py +0 -0
  216. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0010_chat_pending_confirmation_portable_uniqueness.py +0 -0
  217. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/0011_search_index_state_dirty_generation.py +0 -0
  218. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/migrations/__init__.py +0 -0
  219. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/models.py +0 -0
  220. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/__init__.py +0 -0
  221. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/audit.py +0 -0
  222. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/base_permission.py +0 -0
  223. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/graphql_capabilities.py +0 -0
  224. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/manager_based_permission.py +0 -0
  225. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/mutation_permission.py +0 -0
  226. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/permission_checks.py +0 -0
  227. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/permission_data_manager.py +0 -0
  228. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/permission/utils.py +0 -0
  229. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/public_api_registry.py +0 -0
  230. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/py.typed +0 -0
  231. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/rule/__init__.py +0 -0
  232. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/rule/handler.py +0 -0
  233. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/rule/rule.py +0 -0
  234. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/__init__.py +0 -0
  235. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/async_tasks.py +0 -0
  236. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/backend.py +0 -0
  237. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/backend_registry.py +0 -0
  238. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/backends/__init__.py +0 -0
  239. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/backends/dev.py +0 -0
  240. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/backends/meilisearch.py +0 -0
  241. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/backends/opensearch.py +0 -0
  242. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/backends/typesense.py +0 -0
  243. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/checks.py +0 -0
  244. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/config.py +0 -0
  245. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/indexer.py +0 -0
  246. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/m2m_invalidation.py +0 -0
  247. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/models.py +0 -0
  248. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/reconciliation.py +0 -0
  249. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/registry.py +0 -0
  250. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/tasks.py +0 -0
  251. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/search/utils.py +0 -0
  252. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/seeding/__init__.py +0 -0
  253. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/seeding/manager_landscape.py +0 -0
  254. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/__init__.py +0 -0
  255. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/adapters.py +0 -0
  256. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/checks.py +0 -0
  257. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/config.py +0 -0
  258. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/errors.py +0 -0
  259. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/finalization.py +0 -0
  260. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/graphql.py +0 -0
  261. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/graphql_types.py +0 -0
  262. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/metrics.py +0 -0
  263. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/models.py +0 -0
  264. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/public.py +0 -0
  265. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/s3.py +0 -0
  266. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/services.py +0 -0
  267. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/tokens.py +0 -0
  268. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/types.py +0 -0
  269. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/urls.py +0 -0
  270. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/uploads/views.py +0 -0
  271. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/__init__.py +0 -0
  272. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/args_to_kwargs.py +0 -0
  273. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/filter_parser.py +0 -0
  274. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/format_string.py +0 -0
  275. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/json_encoder.py +0 -0
  276. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/make_cache_key.py +0 -0
  277. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/none_to_zero.py +0 -0
  278. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/path_mapping.py +0 -0
  279. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/public_api.py +0 -0
  280. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/testing.py +0 -0
  281. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/utils/type_checks.py +0 -0
  282. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/__init__.py +0 -0
  283. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/actions.py +0 -0
  284. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/backend_registry.py +0 -0
  285. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/backends/__init__.py +0 -0
  286. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/backends/celery.py +0 -0
  287. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/backends/local.py +0 -0
  288. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/backends/n8n.py +0 -0
  289. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/config.py +0 -0
  290. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/engine.py +0 -0
  291. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/event_registry.py +0 -0
  292. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/events.py +0 -0
  293. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/models.py +0 -0
  294. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/tasks.py +0 -0
  295. {generalmanager-0.68.3 → generalmanager-0.69.1}/src/general_manager/workflow/telemetry.py +0 -0
  296. {generalmanager-0.68.3 → generalmanager-0.69.1}/tests/test_settings.py +0 -0
  297. {generalmanager-0.68.3 → generalmanager-0.69.1}/tests/test_urls.py +0 -0
  298. {generalmanager-0.68.3 → generalmanager-0.69.1}/tests/testing_asgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.68.3
3
+ Version: 0.69.1
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.68.3"
7
+ version = "0.69.1"
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.68.3
3
+ Version: 0.69.1
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
@@ -6,6 +6,7 @@ import asyncio
6
6
  from contextlib import asynccontextmanager
7
7
  from copy import deepcopy
8
8
  import re
9
+ from time import perf_counter
9
10
  from types import UnionType
10
11
  from typing import (
11
12
  AsyncIterator,
@@ -37,6 +38,7 @@ from general_manager.bucket.group_bucket import GroupBucket
37
38
  from general_manager.cache.dependency_index import (
38
39
  Dependency,
39
40
  )
41
+ from general_manager.cache.data_change_context import record_data_change_phase
40
42
  from general_manager.cache.signals import post_data_change
41
43
  from general_manager.conf import get_setting
42
44
  from general_manager.interface.base_interface import InterfaceBase
@@ -1784,38 +1786,44 @@ class GraphQL:
1784
1786
  instance (GeneralManager | None): The GeneralManager instance that changed.
1785
1787
  action (str): A string describing the change action (e.g., "created", "updated", "deleted").
1786
1788
  """
1787
- event_instance = instance if instance is not None else previous_instance
1788
- if event_instance is None or not isinstance(event_instance, GeneralManager):
1789
- return
1789
+ started = perf_counter()
1790
+ try:
1791
+ event_instance = instance if instance is not None else previous_instance
1792
+ if event_instance is None or not isinstance(event_instance, GeneralManager):
1793
+ return
1790
1794
 
1791
- if isinstance(sender, type) and issubclass(sender, GeneralManager):
1792
- manager_class: type[GeneralManager] = sender
1793
- else:
1794
- manager_class = event_instance.__class__
1795
+ if isinstance(sender, type) and issubclass(sender, GeneralManager):
1796
+ manager_class: type[GeneralManager] = sender
1797
+ else:
1798
+ manager_class = event_instance.__class__
1795
1799
 
1796
- if manager_class.__name__ not in cls.manager_registry:
1797
- logger.debug(
1798
- "skipping subscription event for unregistered manager",
1799
- context={
1800
- "manager": manager_class.__name__,
1801
- "action": action,
1802
- },
1803
- )
1804
- return
1800
+ if manager_class.__name__ not in cls.manager_registry:
1801
+ logger.debug(
1802
+ "skipping subscription event for unregistered manager",
1803
+ context={
1804
+ "manager": manager_class.__name__,
1805
+ "action": action,
1806
+ },
1807
+ )
1808
+ return
1805
1809
 
1806
- event_identification = deepcopy(
1807
- identification
1808
- if identification is not None
1809
- else event_instance.identification
1810
- )
1811
- transaction.on_commit(
1812
- lambda: cls._publish_data_change(
1813
- manager_class,
1814
- action,
1815
- event_identification,
1816
- ),
1817
- using=database_alias,
1818
- )
1810
+ event_identification = deepcopy(
1811
+ identification
1812
+ if identification is not None
1813
+ else event_instance.identification
1814
+ )
1815
+ transaction.on_commit(
1816
+ lambda: cls._publish_data_change(
1817
+ manager_class,
1818
+ action,
1819
+ event_identification,
1820
+ ),
1821
+ using=database_alias,
1822
+ )
1823
+ finally:
1824
+ record_data_change_phase(
1825
+ "subscription", perf_counter() - started, database_alias
1826
+ )
1819
1827
 
1820
1828
  @classmethod
1821
1829
  def _publish_data_change(
@@ -8,6 +8,7 @@ from datetime import date, datetime
8
8
  import json
9
9
  from importlib import import_module
10
10
  import re
11
+ from time import perf_counter
11
12
  from uuid import UUID, uuid4
12
13
  from collections.abc import Callable, Coroutine, Mapping, MutableSequence, Sequence
13
14
  from types import ModuleType
@@ -25,6 +26,7 @@ from general_manager.api.remote_api import (
25
26
  get_remote_api_config,
26
27
  )
27
28
  from general_manager.api.notification_batching import _queue_notification
29
+ from general_manager.cache.data_change_context import record_data_change_phase
28
30
  from general_manager.cache.signals import post_data_change
29
31
  from general_manager.logging import get_logger
30
32
 
@@ -200,24 +202,30 @@ def emit_remote_invalidation(
200
202
  originating database alias and delegates channel lookup, batching, payload
201
203
  creation, and delivery to `_publish_remote_invalidation` after commit.
202
204
  """
203
- config = get_remote_api_config(sender)
204
- if config is None or not config.websocket_invalidation:
205
- return
205
+ started = perf_counter()
206
+ try:
207
+ config = get_remote_api_config(sender)
208
+ if config is None or not config.websocket_invalidation:
209
+ return
206
210
 
207
- event_identification = identification
208
- if event_identification is None and instance is not None:
209
- event_identification = dict(instance.identification)
210
- captured_identification = (
211
- deepcopy(event_identification) if event_identification is not None else None
212
- )
213
- transaction.on_commit(
214
- lambda: _publish_remote_invalidation(
215
- config,
216
- action,
217
- captured_identification,
218
- ),
219
- using=database_alias,
220
- )
211
+ event_identification = identification
212
+ if event_identification is None and instance is not None:
213
+ event_identification = dict(instance.identification)
214
+ captured_identification = (
215
+ deepcopy(event_identification) if event_identification is not None else None
216
+ )
217
+ transaction.on_commit(
218
+ lambda: _publish_remote_invalidation(
219
+ config,
220
+ action,
221
+ captured_identification,
222
+ ),
223
+ using=database_alias,
224
+ )
225
+ finally:
226
+ record_data_change_phase(
227
+ "subscription", perf_counter() - started, database_alias
228
+ )
221
229
 
222
230
 
223
231
  post_data_change.connect(emit_remote_invalidation, weak=False)
@@ -5,17 +5,45 @@ from __future__ import annotations
5
5
  from collections.abc import Iterator
6
6
  from contextlib import contextmanager
7
7
  from contextvars import ContextVar
8
- from dataclasses import dataclass
8
+ from dataclasses import dataclass, field
9
+ from math import isfinite
10
+ from typing import Literal
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class DataChangeTransactionContext:
15
+ """Mutable state owned by one framework-managed data-change transaction."""
16
+
17
+ database_alias: str
18
+ caller_in_atomic_block: bool
19
+ changed_classes: set[str] = field(default_factory=set)
20
+ metadata: dict[str, object] = field(default_factory=dict)
21
+ phase_seconds: dict[str, float] = field(default_factory=dict)
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class DataChangeTransactionScope:
26
+ """Public view of a context-manager entry's transaction state."""
27
+
28
+ transaction: DataChangeTransactionContext
29
+ is_outermost: bool
9
30
 
10
31
 
11
32
  @dataclass
12
33
  class _DataChangeTransaction:
13
34
  """Live marker for one framework-owned transaction envelope."""
14
35
 
15
- database_alias: str
16
- caller_in_atomic_block: bool
36
+ context: DataChangeTransactionContext
17
37
  active: bool = True
18
38
 
39
+ @property
40
+ def database_alias(self) -> str:
41
+ return self.context.database_alias
42
+
43
+ @property
44
+ def caller_in_atomic_block(self) -> bool:
45
+ return self.context.caller_in_atomic_block
46
+
19
47
 
20
48
  _owned_transactions: ContextVar[tuple[_DataChangeTransaction, ...]] = ContextVar(
21
49
  "owned_data_change_transactions",
@@ -34,25 +62,68 @@ def _current_owner(database_alias: str) -> _DataChangeTransaction | None:
34
62
  return None
35
63
 
36
64
 
65
+ def current_data_change_transaction(
66
+ database_alias: str,
67
+ ) -> DataChangeTransactionContext | None:
68
+ """Return the live framework-owned context for a database alias, if any."""
69
+ owner = _current_owner(database_alias)
70
+ return owner.context if owner is not None else None
71
+
72
+
37
73
  @contextmanager
38
74
  def own_data_change_transaction(
39
75
  database_alias: str,
40
76
  *,
41
77
  caller_in_atomic_block: bool,
42
- ) -> Iterator[None]:
78
+ ) -> Iterator[DataChangeTransactionScope]:
43
79
  """Record a live framework envelope without inspecting Django internals."""
44
- owner = _DataChangeTransaction(
80
+ existing_owner = _current_owner(database_alias)
81
+ if existing_owner is not None:
82
+ yield DataChangeTransactionScope(
83
+ transaction=existing_owner.context,
84
+ is_outermost=False,
85
+ )
86
+ return
87
+
88
+ context = DataChangeTransactionContext(
45
89
  database_alias=database_alias,
46
90
  caller_in_atomic_block=caller_in_atomic_block,
47
91
  )
92
+ owner = _DataChangeTransaction(
93
+ context=context,
94
+ )
48
95
  token = _owned_transactions.set((*_owned_transactions.get(), owner))
49
96
  try:
50
- yield
97
+ yield DataChangeTransactionScope(transaction=context, is_outermost=True)
51
98
  finally:
52
99
  owner.active = False
53
100
  _owned_transactions.reset(token)
54
101
 
55
102
 
103
+ def register_data_change_class(class_name: str, database_alias: str) -> bool:
104
+ """Record a changed class when the framework owns the live transaction."""
105
+ context = current_data_change_transaction(database_alias)
106
+ if context is None or context.caller_in_atomic_block:
107
+ return False
108
+ context.changed_classes.add(class_name)
109
+ return True
110
+
111
+
112
+ def record_data_change_phase(
113
+ phase: Literal["database", "invalidation", "subscription", "search", "workflow"],
114
+ duration_seconds: float,
115
+ database_alias: str,
116
+ ) -> None:
117
+ """Accumulate a non-negative duration for a phase in the live context."""
118
+ if not isfinite(duration_seconds):
119
+ return
120
+ context = current_data_change_transaction(database_alias)
121
+ if context is not None:
122
+ context.phase_seconds[phase] = context.phase_seconds.get(phase, 0.0) + max(
123
+ duration_seconds, 0.0
124
+ )
125
+
126
+
56
127
  @contextmanager
57
128
  def authorize_data_change_operation(database_alias: str) -> Iterator[None]:
58
129
  """Authorize only the decorated manager method to reuse its envelope."""
@@ -8,9 +8,11 @@ import time
8
8
  import uuid
9
9
  from contextvars import ContextVar
10
10
  from datetime import date, datetime
11
+ from time import perf_counter
11
12
  from typing import TYPE_CHECKING, Callable, Iterable, Literal, Tuple, Type, cast
12
13
 
13
14
  from django.core.cache import cache
15
+ from django.db import DEFAULT_DB_ALIAS
14
16
  from django.dispatch import receiver
15
17
 
16
18
  from general_manager.cache.dependency_matching import (
@@ -34,6 +36,7 @@ from general_manager.cache.dependency_shards import (
34
36
  reverse_memberships,
35
37
  tracked_lookup_names,
36
38
  )
39
+ from general_manager.cache.data_change_context import record_data_change_phase
37
40
  from general_manager.cache.signals import post_data_change, pre_data_change
38
41
  from general_manager.logging import get_logger
39
42
 
@@ -1384,6 +1387,7 @@ def generic_cache_invalidation(
1384
1387
  sender: type[GeneralManager],
1385
1388
  instance: GeneralManager,
1386
1389
  old_relevant_values: dict[str, object],
1390
+ database_alias: str = DEFAULT_DB_ALIAS,
1387
1391
  **kwargs: object,
1388
1392
  ) -> None:
1389
1393
  """
@@ -1395,27 +1399,34 @@ def generic_cache_invalidation(
1395
1399
  sender (type[GeneralManager]): Manager class that emitted the signal.
1396
1400
  instance (GeneralManager): The manager instance that was changed.
1397
1401
  old_relevant_values (dict[str, object]): Mapping of lookup paths (joined by "__") to their values as captured before the change; used to compare old vs. new values for invalidation decisions.
1402
+ database_alias (str): Database alias used to attribute invalidation timing.
1398
1403
  """
1399
- manager_name = sender.__name__
1400
- invalidated_cache_keys: set[str] = set()
1401
- lock_token = acquire_lock_with_retry("generic_cache_invalidation")
1404
+ started = perf_counter()
1402
1405
  try:
1403
- if legacy_dependency_index_exists():
1404
- idx = get_full_index()
1405
- invalidated_cache_keys = _generic_cache_invalidation_locked(
1406
- idx,
1407
- manager_name,
1408
- instance,
1409
- old_relevant_values,
1410
- )
1411
- set_full_index(idx)
1412
- else:
1413
- invalidated_cache_keys = _generic_cache_invalidation_from_shards(
1414
- manager_name,
1415
- instance,
1416
- old_relevant_values,
1417
- )
1406
+ manager_name = sender.__name__
1407
+ invalidated_cache_keys: set[str] = set()
1408
+ lock_token = acquire_lock_with_retry("generic_cache_invalidation")
1409
+ try:
1410
+ if legacy_dependency_index_exists():
1411
+ idx = get_full_index()
1412
+ invalidated_cache_keys = _generic_cache_invalidation_locked(
1413
+ idx,
1414
+ manager_name,
1415
+ instance,
1416
+ old_relevant_values,
1417
+ )
1418
+ set_full_index(idx)
1419
+ else:
1420
+ invalidated_cache_keys = _generic_cache_invalidation_from_shards(
1421
+ manager_name,
1422
+ instance,
1423
+ old_relevant_values,
1424
+ )
1425
+ finally:
1426
+ release_lock(lock_token)
1427
+ if invalidated_cache_keys:
1428
+ record_invalidated_cache_keys_for_graphql_rewarm(invalidated_cache_keys)
1418
1429
  finally:
1419
- release_lock(lock_token)
1420
- if invalidated_cache_keys:
1421
- record_invalidated_cache_keys_for_graphql_rewarm(invalidated_cache_keys)
1430
+ record_data_change_phase(
1431
+ "invalidation", perf_counter() - started, database_alias
1432
+ )
@@ -6,6 +6,7 @@ from collections import defaultdict
6
6
  from collections.abc import Callable, Hashable, Iterable, Mapping
7
7
  from contextvars import ContextVar, Token
8
8
  from dataclasses import dataclass
9
+ from threading import RLock, get_ident
9
10
  from types import TracebackType
10
11
  from typing import TYPE_CHECKING, Optional, TypeVar, cast
11
12
 
@@ -43,8 +44,10 @@ ORM_BUCKET_EXISTS_PREFIX = "orm_bucket_exists"
43
44
  BUCKET_INDEX_PREFIX = "bucket_index"
44
45
  TRUSTED_ORM_MANAGER_PREFIX = "trusted_orm_manager"
45
46
  DEFAULT_DEPENDENCY_CACHE_PUBLISH_BATCH_SIZE = 1000
47
+ RUN_CONTEXT_TOUCH_BATCH_SIZE = 64
46
48
  logger = get_logger("cache.run_context")
47
49
  OrmModelRowKey = tuple[Hashable, Hashable | None]
50
+ PendingRunCacheTouch = tuple[RunCacheNamespace, Hashable]
48
51
 
49
52
 
50
53
  @dataclass(frozen=True)
@@ -104,9 +107,16 @@ class CalculationRunContext:
104
107
  ] = {}
105
108
  self._dependency_cache_publish_batch_size = dependency_cache_publish_batch_size
106
109
  self._tokens: list[Token[CalculationRunContext | None]] = []
110
+ self._run_cache_touch_lock = RLock()
111
+ self._pending_run_cache_touches: dict[PendingRunCacheTouch, None] = {}
112
+ self._last_pending_run_cache_touch: PendingRunCacheTouch | None = None
113
+ self._run_cache_touch_count = 0
114
+ self._run_cache_touch_thread_id: int | None = None
115
+ max_bytes = resolve_run_context_cache_max_bytes()
116
+ self._run_cache_budget_enabled = max_bytes is not None
107
117
  run_context_cache_budget.register(
108
118
  self,
109
- resolve_run_context_cache_max_bytes(),
119
+ max_bytes,
110
120
  )
111
121
 
112
122
  def _iter_run_cache_entries(
@@ -119,31 +129,45 @@ class CalculationRunContext:
119
129
  if key not in self._dependency_cache_pending_publications:
120
130
  yield "dependency_hits", key, hit
121
131
 
132
+ def _set_run_cache_budget_enabled(self, enabled: bool) -> None:
133
+ """Refresh the owner hint after a coordinator transition."""
134
+ with self._run_cache_touch_lock:
135
+ self._run_cache_budget_enabled = enabled
136
+ if not enabled:
137
+ self._pending_run_cache_touches.clear()
138
+ self._last_pending_run_cache_touch = None
139
+ self._run_cache_touch_count = 0
140
+
122
141
  def _evict_run_cache_entry(
123
142
  self,
124
143
  namespace: RunCacheNamespace,
125
144
  key: Hashable,
126
145
  ) -> None:
127
146
  """Remove one entry selected by the process-wide coordinator."""
147
+ self._discard_run_cache_touch((namespace, key))
128
148
  if namespace == "values":
129
149
  self._values.pop(key, None)
130
150
  elif key not in self._dependency_cache_pending_publications:
131
151
  self._dependency_cache_hits.pop(cast(str, key), None)
132
152
 
133
153
  def _store_run_value(self, scoped_key: Hashable, value: object) -> None:
154
+ self._flush_run_cache_touches()
134
155
  self._values[scoped_key] = value
135
156
  run_context_cache_budget.track(self, "values", scoped_key, value)
136
157
 
137
158
  def _remove_run_value(self, scoped_key: Hashable) -> None:
159
+ self._discard_run_cache_touch(("values", scoped_key))
138
160
  if scoped_key in self._values:
139
161
  del self._values[scoped_key]
140
162
  run_context_cache_budget.remove(self, "values", scoped_key)
141
163
 
142
164
  def _refresh_run_value(self, scoped_key: Hashable) -> None:
165
+ self._discard_run_cache_touch(("values", scoped_key))
143
166
  try:
144
167
  value = self._values[scoped_key]
145
168
  except KeyError:
146
169
  return
170
+ self._flush_run_cache_touches()
147
171
  run_context_cache_budget.refresh(self, "values", scoped_key, value)
148
172
 
149
173
  def _store_dependency_cache_hit(
@@ -151,26 +175,109 @@ class CalculationRunContext:
151
175
  key: str,
152
176
  hit: DependencyCacheHit,
153
177
  ) -> None:
178
+ is_pending_publication = key in self._dependency_cache_pending_publications
179
+ if not is_pending_publication:
180
+ self._flush_run_cache_touches()
154
181
  self._dependency_cache_hits[key] = hit
155
- if key in self._dependency_cache_pending_publications:
182
+ if is_pending_publication:
183
+ self._discard_run_cache_touch(("dependency_hits", key))
156
184
  run_context_cache_budget.remove(self, "dependency_hits", key)
157
185
  else:
158
186
  run_context_cache_budget.track(self, "dependency_hits", key, hit)
159
187
 
160
188
  def _remove_dependency_cache_hit(self, key: str) -> None:
189
+ self._discard_run_cache_touch(("dependency_hits", key))
161
190
  if key in self._dependency_cache_hits:
162
191
  del self._dependency_cache_hits[key]
163
192
  run_context_cache_budget.remove(self, "dependency_hits", key)
164
193
 
165
194
  def _refresh_dependency_cache_hit(self, key: str) -> None:
195
+ self._discard_run_cache_touch(("dependency_hits", key))
166
196
  if key in self._dependency_cache_pending_publications:
167
197
  return
168
198
  try:
169
199
  hit = self._dependency_cache_hits[key]
170
200
  except KeyError:
171
201
  return
202
+ self._flush_run_cache_touches()
172
203
  run_context_cache_budget.refresh(self, "dependency_hits", key, hit)
173
204
 
205
+ def _queue_run_cache_touch(
206
+ self,
207
+ namespace: RunCacheNamespace,
208
+ key: Hashable,
209
+ ) -> None:
210
+ pending_key = (namespace, key)
211
+ with self._run_cache_touch_lock:
212
+ if not self._run_cache_budget_enabled:
213
+ return
214
+ if pending_key != self._last_pending_run_cache_touch:
215
+ self._pending_run_cache_touches.pop(pending_key, None)
216
+ self._pending_run_cache_touches[pending_key] = None
217
+ self._last_pending_run_cache_touch = pending_key
218
+ self._queue_repeated_run_cache_touch(pending_key)
219
+
220
+ def _queue_repeated_run_cache_touch(
221
+ self,
222
+ pending_key: PendingRunCacheTouch,
223
+ ) -> bool:
224
+ touch_immediately = False
225
+ touches_to_flush: tuple[PendingRunCacheTouch, ...] = ()
226
+ with self._run_cache_touch_lock:
227
+ if not self._run_cache_budget_enabled:
228
+ return False
229
+ if self._run_cache_touch_thread_id != get_ident():
230
+ touch_immediately = True
231
+ elif pending_key != self._last_pending_run_cache_touch:
232
+ return False
233
+ else:
234
+ count = self._run_cache_touch_count + 1
235
+ if count < RUN_CONTEXT_TOUCH_BATCH_SIZE:
236
+ self._run_cache_touch_count = count
237
+ return True
238
+ touches_to_flush = self._take_run_cache_touches_locked()
239
+ if touch_immediately:
240
+ run_context_cache_budget.touch(self, *pending_key)
241
+ return True
242
+ run_context_cache_budget.touch_many(self, touches_to_flush)
243
+ return True
244
+
245
+ def _touch_run_cache_entry(
246
+ self,
247
+ namespace: RunCacheNamespace,
248
+ key: Hashable,
249
+ ) -> None:
250
+ pending_key: PendingRunCacheTouch = (namespace, key)
251
+ if not self._queue_repeated_run_cache_touch(pending_key):
252
+ self._queue_run_cache_touch(namespace, key)
253
+
254
+ def _discard_run_cache_touch(self, pending_key: PendingRunCacheTouch) -> None:
255
+ with self._run_cache_touch_lock:
256
+ self._pending_run_cache_touches.pop(pending_key, None)
257
+ if pending_key == self._last_pending_run_cache_touch:
258
+ self._last_pending_run_cache_touch = next(
259
+ reversed(self._pending_run_cache_touches),
260
+ None,
261
+ )
262
+
263
+ def _take_run_cache_touches_locked(
264
+ self,
265
+ ) -> tuple[PendingRunCacheTouch, ...]:
266
+ touches = tuple(self._pending_run_cache_touches)
267
+ self._pending_run_cache_touches.clear()
268
+ self._last_pending_run_cache_touch = None
269
+ self._run_cache_touch_count = 0
270
+ return touches
271
+
272
+ def _flush_run_cache_touches(self) -> None:
273
+ with self._run_cache_touch_lock:
274
+ if not self._pending_run_cache_touches:
275
+ self._last_pending_run_cache_touch = None
276
+ self._run_cache_touch_count = 0
277
+ return
278
+ touches = self._take_run_cache_touches_locked()
279
+ run_context_cache_budget.touch_many(self, touches)
280
+
174
281
  @staticmethod
175
282
  def _scoped_key(key: Hashable) -> Hashable:
176
283
  """Namespace a run value by the active historical snapshot."""
@@ -182,9 +289,16 @@ class CalculationRunContext:
182
289
  def __enter__(self) -> "CalculationRunContext":
183
290
  """Activate this context and return it for the `with` block."""
184
291
  if not self._tokens:
292
+ max_bytes = resolve_run_context_cache_max_bytes()
293
+ with self._run_cache_touch_lock:
294
+ self._pending_run_cache_touches.clear()
295
+ self._last_pending_run_cache_touch = None
296
+ self._run_cache_touch_count = 0
297
+ self._run_cache_touch_thread_id = get_ident()
298
+ self._run_cache_budget_enabled = max_bytes is not None
185
299
  run_context_cache_budget.register(
186
300
  self,
187
- resolve_run_context_cache_max_bytes(),
301
+ max_bytes,
188
302
  )
189
303
  self._tokens.append(_active_context.set(self))
190
304
  return self
@@ -219,6 +333,12 @@ class CalculationRunContext:
219
333
 
220
334
  token = self._tokens.pop()
221
335
  is_outermost_exit = not self._tokens
336
+ if is_outermost_exit:
337
+ with self._run_cache_touch_lock:
338
+ self._pending_run_cache_touches.clear()
339
+ self._last_pending_run_cache_touch = None
340
+ self._run_cache_touch_count = 0
341
+ self._run_cache_touch_thread_id = None
222
342
  try:
223
343
  if is_outermost_exit:
224
344
  if exc_type is None:
@@ -253,7 +373,7 @@ class CalculationRunContext:
253
373
  value = loader()
254
374
  self._store_run_value(scoped_key, value)
255
375
  return value
256
- run_context_cache_budget.touch(self, "values", scoped_key)
376
+ self._touch_run_cache_entry("values", scoped_key)
257
377
  return cast(T, value)
258
378
 
259
379
  def get(self, key: Hashable, default: object = None) -> object:
@@ -263,7 +383,7 @@ class CalculationRunContext:
263
383
  value = self._values[scoped_key]
264
384
  except KeyError:
265
385
  return default
266
- run_context_cache_budget.touch(self, "values", scoped_key)
386
+ self._touch_run_cache_entry("values", scoped_key)
267
387
  return value
268
388
 
269
389
  def set(self, key: Hashable, value: object) -> None:
@@ -295,7 +415,7 @@ class CalculationRunContext:
295
415
  except KeyError:
296
416
  return default
297
417
  if key not in self._dependency_cache_pending_publications:
298
- run_context_cache_budget.touch(self, "dependency_hits", key)
418
+ self._touch_run_cache_entry("dependency_hits", key)
299
419
  return hit
300
420
 
301
421
  def buffer_dependency_cache_publication(
@@ -718,7 +838,7 @@ class CalculationRunContext:
718
838
  scoped_key = self._scoped_key(key)
719
839
  if scoped_key not in self._values:
720
840
  return False
721
- run_context_cache_budget.touch(self, "values", scoped_key)
841
+ self._touch_run_cache_entry("values", scoped_key)
722
842
  return True
723
843
 
724
844
  def __contains__(self, key: Hashable) -> bool: