GeneralManager 0.68.2__tar.gz → 0.69.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 (297) hide show
  1. {generalmanager-0.68.2/src/GeneralManager.egg-info → generalmanager-0.69.0}/PKG-INFO +1 -1
  2. {generalmanager-0.68.2 → generalmanager-0.69.0}/pyproject.toml +1 -1
  3. {generalmanager-0.68.2 → generalmanager-0.69.0/src/GeneralManager.egg-info}/PKG-INFO +1 -1
  4. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql.py +37 -29
  5. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_resolvers.py +4 -0
  6. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/remote_invalidation.py +25 -17
  7. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/data_change_context.py +77 -6
  8. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/dependency_index.py +32 -21
  9. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/signals.py +88 -14
  10. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/invalidation.py +50 -44
  11. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/signal_bridge.py +22 -13
  12. {generalmanager-0.68.2 → generalmanager-0.69.0}/LICENSE +0 -0
  13. {generalmanager-0.68.2 → generalmanager-0.69.0}/README.md +0 -0
  14. {generalmanager-0.68.2 → generalmanager-0.69.0}/setup.cfg +0 -0
  15. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/GeneralManager.egg-info/SOURCES.txt +0 -0
  16. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/GeneralManager.egg-info/dependency_links.txt +0 -0
  17. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/GeneralManager.egg-info/requires.txt +0 -0
  18. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/GeneralManager.egg-info/top_level.txt +0 -0
  19. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/__init__.py +0 -0
  20. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/__init__.py +0 -0
  21. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/api.py +0 -0
  22. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/bucket.py +0 -0
  23. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/cache.py +0 -0
  24. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/dataframes.py +0 -0
  25. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/factory.py +0 -0
  26. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/general_manager.py +0 -0
  27. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/interface.py +0 -0
  28. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/manager.py +0 -0
  29. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/measurement.py +0 -0
  30. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/permission.py +0 -0
  31. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/rule.py +0 -0
  32. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/search.py +0 -0
  33. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/_types/utils.py +0 -0
  34. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/__init__.py +0 -0
  35. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_as_of.py +0 -0
  36. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_errors.py +0 -0
  37. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_mutations.py +0 -0
  38. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_prefetch.py +0 -0
  39. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_relations.py +0 -0
  40. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_search.py +0 -0
  41. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_subscription_consumer.py +0 -0
  42. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_subscriptions.py +0 -0
  43. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_view.py +0 -0
  44. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_warmup.py +0 -0
  45. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_warmup_registry.py +0 -0
  46. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/graphql_warmup_tasks.py +0 -0
  47. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/mutation.py +0 -0
  48. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/notification_batching.py +0 -0
  49. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/property.py +0 -0
  50. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/registry.py +0 -0
  51. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/remote_api.py +0 -0
  52. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/api/remote_invalidation_client.py +0 -0
  53. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/apps.py +0 -0
  54. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/as_of.py +0 -0
  55. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/bootstrap.py +0 -0
  56. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/bucket/__init__.py +0 -0
  57. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/bucket/base_bucket.py +0 -0
  58. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/bucket/calculation_bucket.py +0 -0
  59. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/bucket/database_bucket.py +0 -0
  60. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/bucket/group_bucket.py +0 -0
  61. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/bucket/indexing.py +0 -0
  62. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/bucket/request_bucket.py +0 -0
  63. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/__init__.py +0 -0
  64. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/cache_decorator.py +0 -0
  65. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/cache_tracker.py +0 -0
  66. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/dependency_cache.py +0 -0
  67. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/dependency_matching.py +0 -0
  68. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/dependency_publish.py +0 -0
  69. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/dependency_shards.py +0 -0
  70. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/model_dependency_collector.py +0 -0
  71. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/run_context.py +0 -0
  72. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/cache/run_context_lru.py +0 -0
  73. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/__init__.py +0 -0
  74. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/audit.py +0 -0
  75. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/bootstrap.py +0 -0
  76. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/checks.py +0 -0
  77. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/consumer.py +0 -0
  78. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/errors.py +0 -0
  79. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/__init__.py +0 -0
  80. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/__main__.py +0 -0
  81. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/baseline.py +0 -0
  82. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/datasets/basic_queries.yaml +0 -0
  83. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/datasets/demo_readiness.yaml +0 -0
  84. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/datasets/edge_cases.yaml +0 -0
  85. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/datasets/follow_ups.yaml +0 -0
  86. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/datasets/large_schema.yaml +0 -0
  87. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/datasets/multi_hop.yaml +0 -0
  88. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/diagnostics.py +0 -0
  89. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/fingerprints.py +0 -0
  90. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/fixtures.py +0 -0
  91. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/judges/__init__.py +0 -0
  92. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/judges/answer_quality.py +0 -0
  93. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/judges/contract.py +0 -0
  94. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/judges/result_accuracy.py +0 -0
  95. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/judges/tool_sequence.py +0 -0
  96. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/readiness.py +0 -0
  97. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/runner.py +0 -0
  98. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/evals/traces.py +0 -0
  99. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/grounding.py +0 -0
  100. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/models.py +0 -0
  101. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/providers/__init__.py +0 -0
  102. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/providers/_shared.py +0 -0
  103. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/providers/anthropic.py +0 -0
  104. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/providers/base.py +0 -0
  105. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/providers/google.py +0 -0
  106. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/providers/ollama.py +0 -0
  107. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/providers/openai.py +0 -0
  108. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/rate_limits.py +0 -0
  109. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/routing.py +0 -0
  110. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/schema_index.py +0 -0
  111. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/settings.py +0 -0
  112. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/signals.py +0 -0
  113. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/system_prompt.py +0 -0
  114. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/tool_metadata.py +0 -0
  115. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/tools.py +0 -0
  116. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/chat/views.py +0 -0
  117. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/conf.py +0 -0
  118. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/dataframes/__init__.py +0 -0
  119. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/dataframes/measurements.py +0 -0
  120. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/factory/__init__.py +0 -0
  121. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/factory/auto_factory.py +0 -0
  122. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/factory/factories.py +0 -0
  123. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/factory/factory_methods.py +0 -0
  124. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/__init__.py +0 -0
  125. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/base_interface.py +0 -0
  126. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/bundles/__init__.py +0 -0
  127. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/bundles/calculation.py +0 -0
  128. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/bundles/database.py +0 -0
  129. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/bundles/remote_manager.py +0 -0
  130. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/bundles/request.py +0 -0
  131. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/__init__.py +0 -0
  132. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/base.py +0 -0
  133. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/builtin.py +0 -0
  134. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/calculation/__init__.py +0 -0
  135. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/calculation/_compat.py +0 -0
  136. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/calculation/lifecycle.py +0 -0
  137. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/configuration.py +0 -0
  138. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/core/observability.py +0 -0
  139. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/core/utils.py +0 -0
  140. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/exceptions.py +0 -0
  141. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/existing_model/__init__.py +0 -0
  142. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/existing_model/_compat.py +0 -0
  143. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/existing_model/resolution.py +0 -0
  144. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/factory.py +0 -0
  145. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm/__init__.py +0 -0
  146. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm/_compat.py +0 -0
  147. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm/history.py +0 -0
  148. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm/lifecycle.py +0 -0
  149. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm/mutations.py +0 -0
  150. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm/support.py +0 -0
  151. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm_utils/__init__.py +0 -0
  152. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm_utils/django_manager_utils.py +0 -0
  153. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm_utils/field_descriptors.py +0 -0
  154. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/orm_utils/payload_normalizer.py +0 -0
  155. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/read_only/__init__.py +0 -0
  156. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/read_only/_compat.py +0 -0
  157. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/read_only/_types.py +0 -0
  158. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/read_only/lifecycle.py +0 -0
  159. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/read_only/management.py +0 -0
  160. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/registry.py +0 -0
  161. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/remote_manager.py +0 -0
  162. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/capabilities/request/__init__.py +0 -0
  163. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/infrastructure/__init__.py +0 -0
  164. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/infrastructure/startup_hooks.py +0 -0
  165. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/infrastructure/system_checks.py +0 -0
  166. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/interfaces/__init__.py +0 -0
  167. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/interfaces/calculation.py +0 -0
  168. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/interfaces/database.py +0 -0
  169. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/interfaces/existing_model.py +0 -0
  170. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/interfaces/read_only.py +0 -0
  171. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/interfaces/remote_manager.py +0 -0
  172. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/interfaces/request.py +0 -0
  173. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/manifests/__init__.py +0 -0
  174. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/manifests/capability_builder.py +0 -0
  175. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/manifests/capability_manifest.py +0 -0
  176. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/manifests/capability_models.py +0 -0
  177. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/orm_interface.py +0 -0
  178. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/requests.py +0 -0
  179. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/utils/__init__.py +0 -0
  180. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/utils/database_interface_protocols.py +0 -0
  181. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/utils/errors.py +0 -0
  182. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/utils/history.py +0 -0
  183. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/interface/utils/models.py +0 -0
  184. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/logging.py +0 -0
  185. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/chat_cleanup.py +0 -0
  186. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/cleanup_upload_intents.py +0 -0
  187. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/graphql_warmup.py +0 -0
  188. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/graphql_warmup_refresh_due.py +0 -0
  189. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/search_index.py +0 -0
  190. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/search_reconcile.py +0 -0
  191. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/seed_manager_landscape.py +0 -0
  192. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/shell.py +0 -0
  193. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/workflow_drain_outbox.py +0 -0
  194. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/management/commands/workflow_replay_dead_letters.py +0 -0
  195. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/manager/__init__.py +0 -0
  196. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/manager/general_manager.py +0 -0
  197. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/manager/group_manager.py +0 -0
  198. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/manager/input.py +0 -0
  199. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/manager/meta.py +0 -0
  200. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/measurement/__init__.py +0 -0
  201. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/measurement/measurement.py +0 -0
  202. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/measurement/measurement_field.py +0 -0
  203. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/metrics/__init__.py +0 -0
  204. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/metrics/graphql.py +0 -0
  205. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0001_initial.py +0 -0
  206. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0002_workflow_outbox_scaling_indexes.py +0 -0
  207. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0003_workflow_execution_correlation_constraint.py +0 -0
  208. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0004_chat_persistence_models.py +0 -0
  209. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0004_search_index_state.py +0 -0
  210. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0005_merge_chat_search.py +0 -0
  211. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0006_chat_pending_confirmation_scoped_ids.py +0 -0
  212. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0007_upload_intent.py +0 -0
  213. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0008_upload_finalization_versions.py +0 -0
  214. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0009_upload_cleanup_state.py +0 -0
  215. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0010_chat_pending_confirmation_portable_uniqueness.py +0 -0
  216. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/0011_search_index_state_dirty_generation.py +0 -0
  217. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/migrations/__init__.py +0 -0
  218. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/models.py +0 -0
  219. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/__init__.py +0 -0
  220. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/audit.py +0 -0
  221. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/base_permission.py +0 -0
  222. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/graphql_capabilities.py +0 -0
  223. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/manager_based_permission.py +0 -0
  224. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/mutation_permission.py +0 -0
  225. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/permission_checks.py +0 -0
  226. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/permission_data_manager.py +0 -0
  227. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/permission/utils.py +0 -0
  228. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/public_api_registry.py +0 -0
  229. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/py.typed +0 -0
  230. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/rule/__init__.py +0 -0
  231. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/rule/handler.py +0 -0
  232. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/rule/rule.py +0 -0
  233. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/__init__.py +0 -0
  234. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/async_tasks.py +0 -0
  235. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/backend.py +0 -0
  236. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/backend_registry.py +0 -0
  237. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/backends/__init__.py +0 -0
  238. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/backends/dev.py +0 -0
  239. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/backends/meilisearch.py +0 -0
  240. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/backends/opensearch.py +0 -0
  241. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/backends/typesense.py +0 -0
  242. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/checks.py +0 -0
  243. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/config.py +0 -0
  244. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/indexer.py +0 -0
  245. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/m2m_invalidation.py +0 -0
  246. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/models.py +0 -0
  247. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/reconciliation.py +0 -0
  248. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/registry.py +0 -0
  249. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/tasks.py +0 -0
  250. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/search/utils.py +0 -0
  251. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/seeding/__init__.py +0 -0
  252. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/seeding/manager_landscape.py +0 -0
  253. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/__init__.py +0 -0
  254. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/adapters.py +0 -0
  255. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/checks.py +0 -0
  256. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/config.py +0 -0
  257. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/errors.py +0 -0
  258. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/finalization.py +0 -0
  259. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/graphql.py +0 -0
  260. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/graphql_types.py +0 -0
  261. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/metrics.py +0 -0
  262. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/models.py +0 -0
  263. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/public.py +0 -0
  264. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/s3.py +0 -0
  265. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/services.py +0 -0
  266. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/tokens.py +0 -0
  267. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/types.py +0 -0
  268. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/urls.py +0 -0
  269. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/uploads/views.py +0 -0
  270. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/__init__.py +0 -0
  271. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/args_to_kwargs.py +0 -0
  272. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/filter_parser.py +0 -0
  273. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/format_string.py +0 -0
  274. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/json_encoder.py +0 -0
  275. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/make_cache_key.py +0 -0
  276. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/none_to_zero.py +0 -0
  277. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/path_mapping.py +0 -0
  278. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/public_api.py +0 -0
  279. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/testing.py +0 -0
  280. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/utils/type_checks.py +0 -0
  281. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/__init__.py +0 -0
  282. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/actions.py +0 -0
  283. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/backend_registry.py +0 -0
  284. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/backends/__init__.py +0 -0
  285. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/backends/celery.py +0 -0
  286. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/backends/local.py +0 -0
  287. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/backends/n8n.py +0 -0
  288. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/config.py +0 -0
  289. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/engine.py +0 -0
  290. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/event_registry.py +0 -0
  291. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/events.py +0 -0
  292. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/models.py +0 -0
  293. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/tasks.py +0 -0
  294. {generalmanager-0.68.2 → generalmanager-0.69.0}/src/general_manager/workflow/telemetry.py +0 -0
  295. {generalmanager-0.68.2 → generalmanager-0.69.0}/tests/test_settings.py +0 -0
  296. {generalmanager-0.68.2 → generalmanager-0.69.0}/tests/test_urls.py +0 -0
  297. {generalmanager-0.68.2 → generalmanager-0.69.0}/tests/testing_asgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.68.2
3
+ Version: 0.69.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.68.2"
7
+ version = "0.69.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.68.2
3
+ Version: 0.69.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
@@ -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(
@@ -499,6 +499,8 @@ def apply_pagination(
499
499
  Negative ``page`` or ``page_size`` values raise ``ValueError`` before
500
500
  slicing. Falsey explicit values such as ``page=0`` or ``page_size=0`` also
501
501
  fall back through those defaults for slicing.
502
+ Already-empty grouped buckets are returned unchanged when pagination is
503
+ requested, preserving their shape without invoking an invalid empty slice.
502
504
  The returned object keeps the same bucket/group-bucket shape as the slice
503
505
  operation exposes. Slice errors from the bucket implementation propagate
504
506
  unchanged.
@@ -508,6 +510,8 @@ def apply_pagination(
508
510
  if page_size is not None and page_size < 0:
509
511
  raise InvalidPaginationValueError
510
512
  if page is not None or page_size is not None:
513
+ if isinstance(queryset, GroupBucket) and len(queryset) == 0:
514
+ return queryset
511
515
  page = page or 1
512
516
  page_size = page_size or 10
513
517
  offset = (page - 1) * page_size
@@ -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
+ )
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from contextlib import nullcontext
6
6
  from copy import deepcopy
7
7
  from functools import wraps
8
+ from time import perf_counter
8
9
  from typing import Callable, ParamSpec, TypeVar, cast, overload
9
10
 
10
11
  from django.db import DEFAULT_DB_ALIAS, connections, transaction
@@ -12,8 +13,10 @@ from django.dispatch import Signal
12
13
  from django.utils.connection import ConnectionDoesNotExist
13
14
 
14
15
  from general_manager.cache.data_change_context import (
16
+ DataChangeTransactionScope,
15
17
  authorize_data_change_operation,
16
18
  own_data_change_transaction,
19
+ record_data_change_phase,
17
20
  )
18
21
  from general_manager.as_of import reject_historical_mutation
19
22
  from general_manager.logging import get_logger
@@ -22,6 +25,12 @@ post_data_change = Signal()
22
25
 
23
26
  pre_data_change = Signal()
24
27
 
28
+ data_change_transaction_started = Signal()
29
+
30
+ data_change_transaction_finishing = Signal()
31
+
32
+ data_change_transaction_finished = Signal()
33
+
25
34
  P = ParamSpec("P")
26
35
  R = TypeVar("R")
27
36
 
@@ -64,6 +73,12 @@ def data_change(
64
73
  data-change barrier has closed; failed mutations drain pending keys but do
65
74
  not enqueue rewarm work.
66
75
 
76
+ ORM-backed outermost mutations also emit transaction lifecycle signals.
77
+ A ``committed`` finished outcome means this GeneralManager atomic block or
78
+ savepoint completed. When ``caller_in_atomic_block`` is true, consumers
79
+ requiring the caller's durable commit must register work with
80
+ ``transaction.on_commit()``.
81
+
67
82
  Parameters:
68
83
  func: Function that performs a data mutation. Methods named `create`
69
84
  are treated as class-level creates; every other name is treated as
@@ -134,6 +149,8 @@ def data_change(
134
149
 
135
150
  primary_exc: BaseException | None = None
136
151
  completed = False
152
+ transaction_scope: DataChangeTransactionScope | None = None
153
+ transaction_outcome = "rolled_back"
137
154
  begin_dependency_data_change()
138
155
  context = current_calculation_run_context()
139
156
  if context is not None:
@@ -157,7 +174,30 @@ def data_change(
157
174
  if is_orm_backed
158
175
  else nullcontext()
159
176
  )
160
- with transaction_context, ownership_context:
177
+ with transaction_context, ownership_context as entered_scope:
178
+ if is_orm_backed:
179
+ assert isinstance(entered_scope, DataChangeTransactionScope)
180
+ transaction_scope = entered_scope
181
+ lifecycle_action = (
182
+ action if action in {"create", "update", "delete"} else "other"
183
+ )
184
+ lifecycle_kwargs = (
185
+ {
186
+ "transaction_context": transaction_scope.transaction,
187
+ "database_alias": database_alias,
188
+ "caller_in_atomic_block": (
189
+ transaction_scope.transaction.caller_in_atomic_block
190
+ ),
191
+ "action": lifecycle_action,
192
+ }
193
+ if transaction_scope is not None
194
+ else {}
195
+ )
196
+ if transaction_scope is not None and transaction_scope.is_outermost:
197
+ data_change_transaction_started.send(
198
+ sender=sender,
199
+ **lifecycle_kwargs,
200
+ )
161
201
  pre_data_change.send(
162
202
  sender=sender,
163
203
  instance=instance_before,
@@ -174,11 +214,22 @@ def data_change(
174
214
  else nullcontext()
175
215
  )
176
216
  with operation_context:
177
- if isinstance(func, classmethod):
178
- inner = cast(Callable[P, R], func.__func__)
179
- result = inner(*args, **kwargs)
180
- else:
181
- result = func(*args, **kwargs)
217
+ mutation_started = (
218
+ perf_counter() if transaction_scope is not None else None
219
+ )
220
+ try:
221
+ if isinstance(func, classmethod):
222
+ inner = cast(Callable[P, R], func.__func__)
223
+ result = inner(*args, **kwargs)
224
+ else:
225
+ result = func(*args, **kwargs)
226
+ finally:
227
+ if mutation_started is not None:
228
+ record_data_change_phase(
229
+ "database",
230
+ perf_counter() - mutation_started,
231
+ database_alias,
232
+ )
182
233
 
183
234
  context = current_calculation_run_context()
184
235
  if context is not None:
@@ -205,6 +256,13 @@ def data_change(
205
256
  delattr(instance_before, "_old_values")
206
257
  except AttributeError:
207
258
  pass
259
+ if transaction_scope is not None and transaction_scope.is_outermost:
260
+ data_change_transaction_finishing.send(
261
+ sender=sender,
262
+ **lifecycle_kwargs,
263
+ )
264
+ if transaction_scope is not None and transaction_scope.is_outermost:
265
+ transaction_outcome = "committed"
208
266
  completed = True
209
267
  except BaseException as error:
210
268
  primary_exc = error
@@ -215,15 +273,31 @@ def data_change(
215
273
  cache_keys: tuple[str, ...] = ()
216
274
  try:
217
275
  try:
276
+ if transaction_scope is not None and transaction_scope.is_outermost:
277
+ try:
278
+ lifecycle_kwargs["outcome"] = transaction_outcome
279
+ data_change_transaction_finished.send(
280
+ sender=sender,
281
+ **lifecycle_kwargs,
282
+ )
283
+ except Exception:
284
+ if primary_exc is not None:
285
+ logger.exception(
286
+ "Data-change transaction finished receiver failed "
287
+ "while handling another exception."
288
+ )
289
+ else:
290
+ raise
291
+ finally:
218
292
  end_dependency_data_change()
219
- except Exception:
220
- if primary_exc is not None:
221
- logger.exception(
222
- "Dependency data-change cleanup failed while handling "
223
- "another exception."
224
- )
225
- else:
226
- raise
293
+ except Exception:
294
+ if primary_exc is not None:
295
+ logger.exception(
296
+ "Dependency data-change cleanup failed while handling "
297
+ "another exception."
298
+ )
299
+ else:
300
+ raise
227
301
  finally:
228
302
  try:
229
303
  if not is_dependency_data_change_active():