GeneralManager 0.63.2__tar.gz → 0.64.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 (287) hide show
  1. {generalmanager-0.63.2/src/GeneralManager.egg-info → generalmanager-0.64.0}/PKG-INFO +1 -1
  2. {generalmanager-0.63.2 → generalmanager-0.64.0}/pyproject.toml +1 -1
  3. {generalmanager-0.63.2 → generalmanager-0.64.0/src/GeneralManager.egg-info}/PKG-INFO +1 -1
  4. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/GeneralManager.egg-info/SOURCES.txt +1 -0
  5. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/api.py +2 -0
  6. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql.py +214 -70
  7. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_subscriptions.py +50 -0
  8. generalmanager-0.64.0/src/general_manager/api/notification_batching.py +121 -0
  9. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/remote_invalidation.py +51 -16
  10. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/bootstrap.py +1 -1
  11. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/dependency_index.py +1 -1
  12. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0007_upload_intent.py +5 -5
  13. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/public_api_registry.py +4 -0
  14. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/backends/meilisearch.py +1 -1
  15. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/finalization.py +4 -4
  16. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/models.py +5 -5
  17. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/filter_parser.py +1 -1
  18. {generalmanager-0.63.2 → generalmanager-0.64.0}/LICENSE +0 -0
  19. {generalmanager-0.63.2 → generalmanager-0.64.0}/README.md +0 -0
  20. {generalmanager-0.63.2 → generalmanager-0.64.0}/setup.cfg +0 -0
  21. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/GeneralManager.egg-info/dependency_links.txt +0 -0
  22. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/GeneralManager.egg-info/requires.txt +0 -0
  23. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/GeneralManager.egg-info/top_level.txt +0 -0
  24. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/__init__.py +0 -0
  25. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/__init__.py +0 -0
  26. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/bucket.py +0 -0
  27. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/cache.py +0 -0
  28. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/dataframes.py +0 -0
  29. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/factory.py +0 -0
  30. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/general_manager.py +0 -0
  31. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/interface.py +0 -0
  32. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/manager.py +0 -0
  33. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/measurement.py +0 -0
  34. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/permission.py +0 -0
  35. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/rule.py +0 -0
  36. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/search.py +0 -0
  37. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/_types/utils.py +0 -0
  38. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/__init__.py +0 -0
  39. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_errors.py +0 -0
  40. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_mutations.py +0 -0
  41. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_prefetch.py +0 -0
  42. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_resolvers.py +0 -0
  43. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_search.py +0 -0
  44. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_subscription_consumer.py +0 -0
  45. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_view.py +0 -0
  46. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_warmup.py +0 -0
  47. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_warmup_registry.py +0 -0
  48. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/graphql_warmup_tasks.py +0 -0
  49. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/mutation.py +0 -0
  50. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/property.py +0 -0
  51. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/registry.py +0 -0
  52. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/remote_api.py +0 -0
  53. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/api/remote_invalidation_client.py +0 -0
  54. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/apps.py +0 -0
  55. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/bucket/__init__.py +0 -0
  56. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/bucket/base_bucket.py +0 -0
  57. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/bucket/calculation_bucket.py +0 -0
  58. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/bucket/database_bucket.py +0 -0
  59. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/bucket/group_bucket.py +0 -0
  60. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/bucket/indexing.py +0 -0
  61. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/bucket/request_bucket.py +0 -0
  62. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/__init__.py +0 -0
  63. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/cache_decorator.py +0 -0
  64. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/cache_tracker.py +0 -0
  65. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/dependency_cache.py +0 -0
  66. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/dependency_matching.py +0 -0
  67. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/dependency_publish.py +0 -0
  68. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/dependency_shards.py +0 -0
  69. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/model_dependency_collector.py +0 -0
  70. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/run_context.py +0 -0
  71. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/cache/signals.py +0 -0
  72. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/__init__.py +0 -0
  73. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/audit.py +0 -0
  74. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/bootstrap.py +0 -0
  75. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/checks.py +0 -0
  76. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/consumer.py +0 -0
  77. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/errors.py +0 -0
  78. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/__init__.py +0 -0
  79. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/__main__.py +0 -0
  80. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/baseline.py +0 -0
  81. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/datasets/basic_queries.yaml +0 -0
  82. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/datasets/demo_readiness.yaml +0 -0
  83. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/datasets/edge_cases.yaml +0 -0
  84. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/datasets/follow_ups.yaml +0 -0
  85. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/datasets/large_schema.yaml +0 -0
  86. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/datasets/multi_hop.yaml +0 -0
  87. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/diagnostics.py +0 -0
  88. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/fingerprints.py +0 -0
  89. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/fixtures.py +0 -0
  90. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/judges/__init__.py +0 -0
  91. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/judges/answer_quality.py +0 -0
  92. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/judges/contract.py +0 -0
  93. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/judges/result_accuracy.py +0 -0
  94. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/judges/tool_sequence.py +0 -0
  95. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/readiness.py +0 -0
  96. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/runner.py +0 -0
  97. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/evals/traces.py +0 -0
  98. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/grounding.py +0 -0
  99. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/models.py +0 -0
  100. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/providers/__init__.py +0 -0
  101. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/providers/_shared.py +0 -0
  102. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/providers/anthropic.py +0 -0
  103. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/providers/base.py +0 -0
  104. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/providers/google.py +0 -0
  105. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/providers/ollama.py +0 -0
  106. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/providers/openai.py +0 -0
  107. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/rate_limits.py +0 -0
  108. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/routing.py +0 -0
  109. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/schema_index.py +0 -0
  110. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/settings.py +0 -0
  111. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/signals.py +0 -0
  112. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/system_prompt.py +0 -0
  113. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/tool_metadata.py +0 -0
  114. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/tools.py +0 -0
  115. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/chat/views.py +0 -0
  116. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/conf.py +0 -0
  117. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/dataframes/__init__.py +0 -0
  118. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/dataframes/measurements.py +0 -0
  119. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/factory/__init__.py +0 -0
  120. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/factory/auto_factory.py +0 -0
  121. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/factory/factories.py +0 -0
  122. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/factory/factory_methods.py +0 -0
  123. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/__init__.py +0 -0
  124. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/base_interface.py +0 -0
  125. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/bundles/__init__.py +0 -0
  126. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/bundles/calculation.py +0 -0
  127. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/bundles/database.py +0 -0
  128. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/bundles/remote_manager.py +0 -0
  129. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/bundles/request.py +0 -0
  130. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/__init__.py +0 -0
  131. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/base.py +0 -0
  132. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/builtin.py +0 -0
  133. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/calculation/__init__.py +0 -0
  134. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/calculation/_compat.py +0 -0
  135. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/calculation/lifecycle.py +0 -0
  136. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/configuration.py +0 -0
  137. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/core/observability.py +0 -0
  138. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/core/utils.py +0 -0
  139. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/exceptions.py +0 -0
  140. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/existing_model/__init__.py +0 -0
  141. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/existing_model/_compat.py +0 -0
  142. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/existing_model/resolution.py +0 -0
  143. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/factory.py +0 -0
  144. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm/__init__.py +0 -0
  145. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm/_compat.py +0 -0
  146. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm/history.py +0 -0
  147. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm/lifecycle.py +0 -0
  148. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm/mutations.py +0 -0
  149. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm/support.py +0 -0
  150. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm_utils/__init__.py +0 -0
  151. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm_utils/django_manager_utils.py +0 -0
  152. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm_utils/field_descriptors.py +0 -0
  153. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/orm_utils/payload_normalizer.py +0 -0
  154. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/read_only/__init__.py +0 -0
  155. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/read_only/_compat.py +0 -0
  156. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/read_only/_types.py +0 -0
  157. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/read_only/lifecycle.py +0 -0
  158. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/read_only/management.py +0 -0
  159. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/registry.py +0 -0
  160. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/remote_manager.py +0 -0
  161. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/capabilities/request/__init__.py +0 -0
  162. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/infrastructure/__init__.py +0 -0
  163. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/infrastructure/startup_hooks.py +0 -0
  164. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/infrastructure/system_checks.py +0 -0
  165. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/interfaces/__init__.py +0 -0
  166. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/interfaces/calculation.py +0 -0
  167. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/interfaces/database.py +0 -0
  168. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/interfaces/existing_model.py +0 -0
  169. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/interfaces/read_only.py +0 -0
  170. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/interfaces/remote_manager.py +0 -0
  171. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/interfaces/request.py +0 -0
  172. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/manifests/__init__.py +0 -0
  173. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/manifests/capability_builder.py +0 -0
  174. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/manifests/capability_manifest.py +0 -0
  175. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/manifests/capability_models.py +0 -0
  176. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/orm_interface.py +0 -0
  177. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/requests.py +0 -0
  178. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/utils/__init__.py +0 -0
  179. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/utils/database_interface_protocols.py +0 -0
  180. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/utils/errors.py +0 -0
  181. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/utils/history.py +0 -0
  182. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/interface/utils/models.py +0 -0
  183. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/logging.py +0 -0
  184. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/chat_cleanup.py +0 -0
  185. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/cleanup_upload_intents.py +0 -0
  186. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/graphql_warmup.py +0 -0
  187. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/graphql_warmup_refresh_due.py +0 -0
  188. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/search_index.py +0 -0
  189. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/search_reconcile.py +0 -0
  190. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/seed_manager_landscape.py +0 -0
  191. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/shell.py +0 -0
  192. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/workflow_drain_outbox.py +0 -0
  193. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/management/commands/workflow_replay_dead_letters.py +0 -0
  194. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/manager/__init__.py +0 -0
  195. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/manager/general_manager.py +0 -0
  196. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/manager/group_manager.py +0 -0
  197. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/manager/input.py +0 -0
  198. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/manager/meta.py +0 -0
  199. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/measurement/__init__.py +0 -0
  200. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/measurement/measurement.py +0 -0
  201. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/measurement/measurement_field.py +0 -0
  202. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/metrics/__init__.py +0 -0
  203. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/metrics/graphql.py +0 -0
  204. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0001_initial.py +0 -0
  205. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0002_workflow_outbox_scaling_indexes.py +0 -0
  206. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0003_workflow_execution_correlation_constraint.py +0 -0
  207. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0004_chat_persistence_models.py +0 -0
  208. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0004_search_index_state.py +0 -0
  209. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0005_merge_chat_search.py +0 -0
  210. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0006_chat_pending_confirmation_scoped_ids.py +0 -0
  211. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0008_upload_finalization_versions.py +0 -0
  212. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/0009_upload_cleanup_state.py +0 -0
  213. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/migrations/__init__.py +0 -0
  214. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/models.py +0 -0
  215. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/__init__.py +0 -0
  216. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/audit.py +0 -0
  217. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/base_permission.py +0 -0
  218. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/graphql_capabilities.py +0 -0
  219. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/manager_based_permission.py +0 -0
  220. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/mutation_permission.py +0 -0
  221. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/permission_checks.py +0 -0
  222. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/permission_data_manager.py +0 -0
  223. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/permission/utils.py +0 -0
  224. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/py.typed +0 -0
  225. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/rule/__init__.py +0 -0
  226. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/rule/handler.py +0 -0
  227. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/rule/rule.py +0 -0
  228. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/__init__.py +0 -0
  229. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/async_tasks.py +0 -0
  230. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/backend.py +0 -0
  231. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/backend_registry.py +0 -0
  232. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/backends/__init__.py +0 -0
  233. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/backends/dev.py +0 -0
  234. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/backends/opensearch.py +0 -0
  235. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/backends/typesense.py +0 -0
  236. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/config.py +0 -0
  237. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/indexer.py +0 -0
  238. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/models.py +0 -0
  239. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/reconciliation.py +0 -0
  240. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/registry.py +0 -0
  241. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/tasks.py +0 -0
  242. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/search/utils.py +0 -0
  243. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/seeding/__init__.py +0 -0
  244. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/seeding/manager_landscape.py +0 -0
  245. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/__init__.py +0 -0
  246. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/adapters.py +0 -0
  247. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/checks.py +0 -0
  248. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/config.py +0 -0
  249. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/errors.py +0 -0
  250. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/graphql.py +0 -0
  251. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/graphql_types.py +0 -0
  252. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/metrics.py +0 -0
  253. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/public.py +0 -0
  254. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/s3.py +0 -0
  255. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/services.py +0 -0
  256. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/tokens.py +0 -0
  257. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/types.py +0 -0
  258. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/urls.py +0 -0
  259. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/uploads/views.py +0 -0
  260. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/__init__.py +0 -0
  261. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/args_to_kwargs.py +0 -0
  262. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/format_string.py +0 -0
  263. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/json_encoder.py +0 -0
  264. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/make_cache_key.py +0 -0
  265. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/none_to_zero.py +0 -0
  266. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/path_mapping.py +0 -0
  267. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/public_api.py +0 -0
  268. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/testing.py +0 -0
  269. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/utils/type_checks.py +0 -0
  270. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/__init__.py +0 -0
  271. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/actions.py +0 -0
  272. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/backend_registry.py +0 -0
  273. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/backends/__init__.py +0 -0
  274. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/backends/celery.py +0 -0
  275. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/backends/local.py +0 -0
  276. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/backends/n8n.py +0 -0
  277. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/config.py +0 -0
  278. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/engine.py +0 -0
  279. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/event_registry.py +0 -0
  280. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/events.py +0 -0
  281. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/models.py +0 -0
  282. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/signal_bridge.py +0 -0
  283. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/tasks.py +0 -0
  284. {generalmanager-0.63.2 → generalmanager-0.64.0}/src/general_manager/workflow/telemetry.py +0 -0
  285. {generalmanager-0.63.2 → generalmanager-0.64.0}/tests/test_settings.py +0 -0
  286. {generalmanager-0.63.2 → generalmanager-0.64.0}/tests/test_urls.py +0 -0
  287. {generalmanager-0.63.2 → generalmanager-0.64.0}/tests/testing_asgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.63.2
3
+ Version: 0.64.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.63.2"
7
+ version = "0.64.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.63.2
3
+ Version: 0.64.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
@@ -42,6 +42,7 @@ src/general_manager/api/graphql_warmup.py
42
42
  src/general_manager/api/graphql_warmup_registry.py
43
43
  src/general_manager/api/graphql_warmup_tasks.py
44
44
  src/general_manager/api/mutation.py
45
+ src/general_manager/api/notification_batching.py
45
46
  src/general_manager/api/property.py
46
47
  src/general_manager/api/registry.py
47
48
  src/general_manager/api/remote_api.py
@@ -56,6 +56,7 @@ __all__ = [
56
56
  "UploadTokenInvalidError",
57
57
  "UploadTransferConflictError",
58
58
  "UploadTransport",
59
+ "bulk_data_change_notifications",
59
60
  "graph_ql_mutation",
60
61
  "graph_ql_property",
61
62
  "register_upload_adapter",
@@ -115,5 +116,6 @@ from general_manager.uploads.errors import UploadTokenInvalidError
115
116
  from general_manager.uploads.errors import UploadTransferConflictError
116
117
  from general_manager.uploads.types import UploadTransport
117
118
  from general_manager.api.mutation import graph_ql_mutation
119
+ from general_manager.api.notification_batching import bulk_data_change_notifications
118
120
  from general_manager.api.property import graph_ql_property
119
121
  from general_manager.uploads.public import register_upload_adapter
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from contextlib import suppress
6
+ from contextlib import asynccontextmanager
7
7
  from copy import deepcopy
8
8
  import re
9
9
  from types import UnionType
@@ -113,10 +113,13 @@ from general_manager.api.graphql_search import (
113
113
  create_filter_options as _create_filter_options_fn,
114
114
  normalize_filter_input as _normalize_filter_input_fn,
115
115
  )
116
+ from general_manager.api.notification_batching import _queue_notification
116
117
  from general_manager.api.graphql_subscriptions import (
117
118
  get_channel_layer_safe as _get_channel_layer_fn,
118
119
  group_name as _group_name_fn,
119
120
  class_group_name as _class_group_name_fn,
121
+ refresh_group_name as _refresh_group_name_fn,
122
+ dispatch_subscription_event as _dispatch_subscription_event_fn,
120
123
  channel_listener as _channel_listener_fn,
121
124
  channel_message_listener as _channel_message_listener_fn,
122
125
  prime_graphql_properties as _prime_graphql_properties_fn,
@@ -155,6 +158,101 @@ GraphQLFilterNormalizer = Callable[
155
158
  dict[str, GraphQLFilterMapping],
156
159
  ]
157
160
  GraphQLMutationMap = dict[str, type[graphene.Mutation]]
161
+ _SUBSCRIPTION_CLEANUP_FAILURE_MESSAGE = "subscription cleanup failed"
162
+ _SUBSCRIPTION_ROLLBACK_FAILURE_MESSAGE = "subscription setup and rollback failed"
163
+ _SUBSCRIPTION_STREAM_CLEANUP_FAILURE_MESSAGE = "subscription stream and cleanup failed"
164
+
165
+
166
+ async def _cleanup_subscription_resources(
167
+ channel_layer: BaseChannelLayer,
168
+ channel_name: str,
169
+ joined_groups: Iterable[str],
170
+ listener_task: asyncio.Task[None] | None = None,
171
+ ) -> None:
172
+ """Stop a listener and attempt to discard every successfully joined group."""
173
+ cleanup_errors: list[BaseException] = []
174
+ if listener_task is not None:
175
+ listener_task.cancel()
176
+ try:
177
+ await listener_task
178
+ except asyncio.CancelledError:
179
+ pass
180
+ except BaseException as exc: # noqa: BLE001
181
+ cleanup_errors.append(exc)
182
+
183
+ for group in joined_groups:
184
+ try:
185
+ await channel_layer.group_discard(group, channel_name)
186
+ except BaseException as exc: # noqa: BLE001
187
+ cleanup_errors.append(exc)
188
+
189
+ if len(cleanup_errors) == 1:
190
+ raise cleanup_errors[0]
191
+ if cleanup_errors:
192
+ raise BaseExceptionGroup(
193
+ _SUBSCRIPTION_CLEANUP_FAILURE_MESSAGE,
194
+ cleanup_errors,
195
+ )
196
+
197
+
198
+ async def _join_subscription_groups(
199
+ channel_layer: BaseChannelLayer,
200
+ channel_name: str,
201
+ group_names: Iterable[str],
202
+ ) -> list[str]:
203
+ """Join groups and roll back every successful join if a later join fails."""
204
+ joined_groups: list[str] = []
205
+ try:
206
+ for group in group_names:
207
+ await channel_layer.group_add(group, channel_name)
208
+ joined_groups.append(group)
209
+ except BaseException as setup_error:
210
+ try:
211
+ await _cleanup_subscription_resources(
212
+ channel_layer,
213
+ channel_name,
214
+ joined_groups,
215
+ )
216
+ except BaseException as cleanup_error: # noqa: BLE001
217
+ raise BaseExceptionGroup(
218
+ _SUBSCRIPTION_ROLLBACK_FAILURE_MESSAGE,
219
+ [setup_error, cleanup_error],
220
+ ) from None
221
+ raise
222
+ return joined_groups
223
+
224
+
225
+ @asynccontextmanager
226
+ async def _subscription_cleanup_scope(
227
+ channel_layer: BaseChannelLayer,
228
+ channel_name: str,
229
+ joined_groups: Iterable[str],
230
+ listener_task: asyncio.Task[None],
231
+ ) -> AsyncIterator[None]:
232
+ """Clean subscription resources without masking an active stream failure."""
233
+ try:
234
+ yield
235
+ except BaseException as stream_error:
236
+ try:
237
+ await _cleanup_subscription_resources(
238
+ channel_layer,
239
+ channel_name,
240
+ joined_groups,
241
+ listener_task,
242
+ )
243
+ except BaseException as cleanup_error: # noqa: BLE001
244
+ raise BaseExceptionGroup(
245
+ _SUBSCRIPTION_STREAM_CLEANUP_FAILURE_MESSAGE,
246
+ [stream_error, cleanup_error],
247
+ ) from None
248
+ raise
249
+ else:
250
+ await _cleanup_subscription_resources(
251
+ channel_layer,
252
+ channel_name,
253
+ joined_groups,
254
+ listener_task,
255
+ )
158
256
 
159
257
 
160
258
  class GraphQL:
@@ -281,6 +379,15 @@ class GraphQL:
281
379
  """
282
380
  return _class_group_name_fn(manager_class)
283
381
 
382
+ @staticmethod
383
+ def _refresh_group_name(manager_class: type[GeneralManager]) -> str:
384
+ """
385
+ Compatibility wrapper for ``graphql_subscriptions.refresh_group_name(manager_class)``.
386
+
387
+ Returns the deterministic manager-wide refresh group name.
388
+ """
389
+ return _refresh_group_name_fn(manager_class)
390
+
284
391
  @staticmethod
285
392
  async def _channel_listener(
286
393
  channel_layer: BaseChannelLayer,
@@ -1352,7 +1459,7 @@ class GraphQL:
1352
1459
  """
1353
1460
  Stream subscription events for a specific manager instance identified by the provided arguments.
1354
1461
 
1355
- Yields an initial `SubscriptionEvent` with `action` set to `"snapshot"` containing the current manager instance, then yields `SubscriptionEvent`s for each subsequent action. For update events, `item` will be the re-instantiated manager instance or `None` if instantiation fails. The subscriber is registered on the manager's channel groups (including dependent managers' groups) and the channel subscriptions and background listener are cleaned up when the iterator is closed or cancelled.
1462
+ Yields an initial `SubscriptionEvent` with `action` set to `"snapshot"` containing the current manager instance, then yields `SubscriptionEvent`s for each subsequent action. For subsequent events, `item` will be the re-instantiated manager instance or `None` if instantiation fails. The subscriber is registered on the manager's channel groups (including dependent managers' groups) and the channel subscriptions and background listener are cleaned up when the iterator is closed or cancelled.
1356
1463
 
1357
1464
  Parameters:
1358
1465
  identification: Identification fields required to locate the manager instance (maps to the manager's identification signature).
@@ -1379,14 +1486,13 @@ class GraphQL:
1379
1486
  )
1380
1487
  except RuntimeError as exc:
1381
1488
  raise GraphQLError(str(exc)) from exc
1382
- channel_name = cast(str, await channel_layer.new_channel())
1383
- queue: asyncio.Queue[str] = asyncio.Queue[str]()
1384
1489
 
1385
1490
  group_names = {
1386
1491
  cls._group_name(
1387
1492
  generalManagerClass,
1388
1493
  instance.identification,
1389
- )
1494
+ ),
1495
+ cls._refresh_group_name(generalManagerClass),
1390
1496
  }
1391
1497
  dependencies = cls._resolve_subscription_dependencies(
1392
1498
  generalManagerClass,
@@ -1397,13 +1503,7 @@ class GraphQL:
1397
1503
  group_names.add(
1398
1504
  cls._group_name(dependency_class, dependency_identification)
1399
1505
  )
1400
-
1401
- for group in group_names:
1402
- await channel_layer.group_add(group, channel_name)
1403
-
1404
- listener_task = asyncio.create_task(
1405
- cls._channel_listener(channel_layer, channel_name, queue)
1406
- )
1506
+ group_names.add(cls._refresh_group_name(dependency_class))
1407
1507
 
1408
1508
  async def event_stream() -> AsyncIterator[SubscriptionEvent]:
1409
1509
  """
@@ -1415,7 +1515,22 @@ class GraphQL:
1415
1515
  Notes:
1416
1516
  When the iterator is closed or exits, the background listener task is cancelled and the subscription's channel group memberships are discarded.
1417
1517
  """
1418
- try:
1518
+ channel_name = cast(str, await channel_layer.new_channel())
1519
+ queue: asyncio.Queue[str] = asyncio.Queue[str]()
1520
+ joined_groups = await _join_subscription_groups(
1521
+ channel_layer,
1522
+ channel_name,
1523
+ group_names,
1524
+ )
1525
+ listener_task = asyncio.create_task(
1526
+ cls._channel_listener(channel_layer, channel_name, queue)
1527
+ )
1528
+ async with _subscription_cleanup_scope(
1529
+ channel_layer,
1530
+ channel_name,
1531
+ joined_groups,
1532
+ listener_task,
1533
+ ):
1419
1534
  clear_capability_context(info)
1420
1535
  yield SubscriptionEvent(item=instance, action="snapshot")
1421
1536
  while True:
@@ -1431,12 +1546,6 @@ class GraphQL:
1431
1546
  item = None
1432
1547
  clear_capability_context(info)
1433
1548
  yield SubscriptionEvent(item=item, action=action)
1434
- finally:
1435
- listener_task.cancel()
1436
- with suppress(asyncio.CancelledError):
1437
- await listener_task
1438
- for group in group_names:
1439
- await channel_layer.group_discard(group, channel_name)
1440
1549
 
1441
1550
  return event_stream()
1442
1551
 
@@ -1466,9 +1575,10 @@ class GraphQL:
1466
1575
  Stream future authorized change events for any instance of a manager class.
1467
1576
 
1468
1577
  Unlike single-entity subscriptions, class-wide subscriptions do not
1469
- yield an initial snapshot. Each channel event is hydrated and
1470
- checked against the request user's object-level read permission
1471
- before it is yielded.
1578
+ yield an initial snapshot. Aggregate refresh events without an
1579
+ identification carry no item; identified row events, including
1580
+ row-level refresh actions, are hydrated and checked against the
1581
+ request user's object-level read permission before they are yielded.
1472
1582
  """
1473
1583
  try:
1474
1584
  channel_layer = cast(
@@ -1476,25 +1586,41 @@ class GraphQL:
1476
1586
  )
1477
1587
  except RuntimeError as exc:
1478
1588
  raise GraphQLError(str(exc)) from exc
1479
- channel_name = cast(str, await channel_layer.new_channel())
1480
- queue: asyncio.Queue[GraphQLFieldMap] = asyncio.Queue()
1481
- group_name = cls._class_group_name(generalManagerClass)
1482
- await channel_layer.group_add(group_name, channel_name)
1483
- listener_task = asyncio.create_task(
1484
- cls._channel_message_listener(channel_layer, channel_name, queue)
1485
- )
1589
+ group_names = {
1590
+ cls._class_group_name(generalManagerClass),
1591
+ cls._refresh_group_name(generalManagerClass),
1592
+ }
1486
1593
 
1487
1594
  async def event_stream() -> AsyncIterator[SubscriptionEvent]:
1488
- try:
1595
+ channel_name = cast(str, await channel_layer.new_channel())
1596
+ queue: asyncio.Queue[GraphQLFieldMap] = asyncio.Queue()
1597
+ joined_groups = await _join_subscription_groups(
1598
+ channel_layer,
1599
+ channel_name,
1600
+ group_names,
1601
+ )
1602
+ listener_task = asyncio.create_task(
1603
+ cls._channel_message_listener(channel_layer, channel_name, queue)
1604
+ )
1605
+ async with _subscription_cleanup_scope(
1606
+ channel_layer,
1607
+ channel_name,
1608
+ joined_groups,
1609
+ listener_task,
1610
+ ):
1489
1611
  while True:
1490
1612
  message = await queue.get()
1491
1613
  if message.get("manager") != generalManagerClass.__name__:
1492
1614
  continue
1493
1615
  action = message.get("action")
1616
+ if not isinstance(action, str):
1617
+ continue
1494
1618
  identification = message.get("identification")
1495
- if not isinstance(action, str) or not isinstance(
1496
- identification, dict
1497
- ):
1619
+ if action == "refresh" and identification is None:
1620
+ clear_capability_context(info)
1621
+ yield SubscriptionEvent(item=None, action=action)
1622
+ continue
1623
+ if not isinstance(identification, dict):
1498
1624
  continue
1499
1625
  identification_copy = deepcopy(identification)
1500
1626
  try:
@@ -1509,11 +1635,6 @@ class GraphQL:
1509
1635
  continue
1510
1636
  clear_capability_context(info)
1511
1637
  yield SubscriptionEvent(item=item, action=action)
1512
- finally:
1513
- listener_task.cancel()
1514
- with suppress(asyncio.CancelledError):
1515
- await listener_task
1516
- await channel_layer.group_discard(group_name, channel_name)
1517
1638
 
1518
1639
  return event_stream()
1519
1640
 
@@ -1595,14 +1716,15 @@ class GraphQL:
1595
1716
  Send a "gm.subscription.event" message to the channel group corresponding to a changed GeneralManager instance.
1596
1717
 
1597
1718
  If the provided instance is a registered GeneralManager and a channel
1598
- layer is configured, publish a message containing the given action to the
1599
- instance channel group and the class-wide channel group. Identification
1600
- comes from the provided `identification` mapping when supplied,
1601
- otherwise from the changed instance, and is deep-copied before dispatch.
1602
- If the instance is `None`, the manager type is not registered, or no
1603
- channel layer is available, the function returns without dispatching.
1604
- Group dispatch failures are logged and do not stop attempts for the
1605
- remaining target groups.
1719
+ layer is configured, queue one manager-wide refresh while notification
1720
+ batching is active. Outside a batch, publish the row-level message to
1721
+ the instance and class-wide groups through one async bridge.
1722
+ Identification comes from the provided `identification` mapping when
1723
+ supplied, otherwise from the changed instance, and is deep-copied
1724
+ before dispatch. If the instance is `None`, the manager type is not
1725
+ registered, or no channel layer is available, the function returns
1726
+ without dispatching. Ordinary group dispatch failures are logged and
1727
+ do not stop attempts for remaining groups.
1606
1728
 
1607
1729
  Parameters:
1608
1730
  sender (type[GeneralManager] | GeneralManager): The signal sender; either a GeneralManager subclass or an instance.
@@ -1646,35 +1768,57 @@ class GraphQL:
1646
1768
  )
1647
1769
  group_name = cls._group_name(manager_class, event_identification)
1648
1770
  class_group_name = cls._class_group_name(manager_class)
1649
- message = {
1771
+ message: dict[str, object] = {
1650
1772
  "type": "gm.subscription.event",
1651
1773
  "action": action,
1652
1774
  "manager": manager_class.__name__,
1653
1775
  "identification": event_identification,
1654
1776
  }
1655
- group_send = async_to_sync(channel_layer.group_send)
1656
- for target_group_name in (group_name, class_group_name):
1657
- try:
1658
- group_send(target_group_name, message)
1659
- except Exception as exc: # noqa: BLE001
1660
- logger.warning(
1661
- "failed to dispatch subscription event",
1662
- context={
1663
- "manager": manager_class.__name__,
1664
- "action": action,
1665
- "group": target_group_name,
1666
- "message": message,
1667
- },
1668
- exc_info=exc,
1669
- )
1670
- logger.debug(
1671
- "dispatched subscription event",
1672
- context={
1673
- "manager": manager_class.__name__,
1674
- "action": action,
1675
- "groups": [group_name, class_group_name],
1676
- },
1677
- )
1777
+ refresh_group_name = cls._refresh_group_name(manager_class)
1778
+ refresh_message: dict[str, object] = {
1779
+ "type": "gm.subscription.event",
1780
+ "action": "refresh",
1781
+ "manager": manager_class.__name__,
1782
+ }
1783
+ if _queue_notification(
1784
+ key=("graphql", refresh_group_name),
1785
+ group_send=channel_layer.group_send,
1786
+ group=refresh_group_name,
1787
+ message=refresh_message,
1788
+ ):
1789
+ return
1790
+
1791
+ target_groups = (group_name, class_group_name)
1792
+ try:
1793
+ dispatched = async_to_sync(_dispatch_subscription_event_fn)(
1794
+ channel_layer,
1795
+ target_groups,
1796
+ message,
1797
+ )
1798
+ except MemoryError:
1799
+ raise
1800
+ except Exception as exc: # noqa: BLE001
1801
+ logger.warning(
1802
+ "failed to dispatch subscription event",
1803
+ context={
1804
+ "manager": manager_class.__name__,
1805
+ "action": action,
1806
+ "target_groups": list(target_groups),
1807
+ "message": message,
1808
+ },
1809
+ exc_info=exc,
1810
+ )
1811
+ return
1812
+ if dispatched > 0:
1813
+ logger.debug(
1814
+ "dispatched subscription event",
1815
+ context={
1816
+ "manager": manager_class.__name__,
1817
+ "action": action,
1818
+ "target_groups": list(target_groups),
1819
+ "successful_group_count": dispatched,
1820
+ },
1821
+ )
1678
1822
 
1679
1823
 
1680
1824
  post_data_change.connect(GraphQL._handle_data_change, weak=False)
@@ -110,6 +110,56 @@ def class_group_name(manager_class: type[GeneralManager]) -> str:
110
110
  return f"gm_subscriptions.{manager_class.__name__}.__class__"
111
111
 
112
112
 
113
+ def refresh_group_name(manager_class: type[GeneralManager]) -> str:
114
+ """
115
+ Build the channel-group name for batched refreshes of a manager class.
116
+
117
+ Parameters:
118
+ manager_class: Manager class used to namespace the refresh group.
119
+
120
+ Returns:
121
+ A stable group identifier for aggregate manager refresh events.
122
+ """
123
+ return f"gm_subscriptions.{manager_class.__name__}.__refresh__"
124
+
125
+
126
+ async def dispatch_subscription_event(
127
+ channel_layer: BaseChannelLayer,
128
+ group_names: Iterable[str],
129
+ message: SubscriptionMessage,
130
+ ) -> int:
131
+ """
132
+ Send one subscription event sequentially to each requested group.
133
+
134
+ Ordinary dispatch failures are logged for their target and do not prevent
135
+ later sends. ``MemoryError`` propagates immediately because continuing
136
+ dispatch during memory exhaustion is unsafe.
137
+
138
+ Returns:
139
+ The number of groups that accepted the event successfully.
140
+ """
141
+ dispatched = 0
142
+ for target_group_name in group_names:
143
+ try:
144
+ await channel_layer.group_send(target_group_name, message)
145
+ except MemoryError:
146
+ raise
147
+ except Exception as exc: # noqa: BLE001
148
+ logger.warning(
149
+ "failed to dispatch subscription event",
150
+ context={
151
+ "group": target_group_name,
152
+ "event_type": message.get("type"),
153
+ "action": message.get("action"),
154
+ "manager": message.get("manager"),
155
+ },
156
+ exc_info=exc,
157
+ )
158
+ else:
159
+ dispatched += 1
160
+ return dispatched
161
+
162
+
113
163
  async def channel_listener(
114
164
  channel_layer: BaseChannelLayer,
115
165
  channel_name: str,
@@ -0,0 +1,121 @@
1
+ """Batch data-change notifications across synchronous bulk operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from _thread import LockType
7
+ from collections.abc import Awaitable, Callable, Iterator, Mapping
8
+ from contextlib import contextmanager
9
+ from contextvars import ContextVar
10
+ from dataclasses import dataclass, field
11
+
12
+ from asgiref.sync import async_to_sync
13
+
14
+ from general_manager.logging import get_logger
15
+
16
+ type NotificationKey = tuple[str, ...]
17
+ type NotificationMessage = Mapping[str, object]
18
+ type GroupSend = Callable[[str, dict[str, object]], Awaitable[None]]
19
+
20
+ logger = get_logger("api.notification_batching")
21
+ _COMBINED_FAILURE_MESSAGE = "bulk data change and notification flush failed"
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class _PendingNotification:
26
+ key: NotificationKey
27
+ group_send: GroupSend = field(repr=False)
28
+ group: str
29
+ message: dict[str, object]
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class _BatchState:
34
+ targets: dict[NotificationKey, _PendingNotification] = field(default_factory=dict)
35
+ accepting: bool = True
36
+ lock: LockType = field(default_factory=threading.Lock, repr=False)
37
+
38
+
39
+ _active_batch: ContextVar[_BatchState | None] = ContextVar(
40
+ "general_manager_notification_batch",
41
+ default=None,
42
+ )
43
+
44
+
45
+ def _queue_notification(
46
+ *,
47
+ key: NotificationKey,
48
+ group_send: GroupSend,
49
+ group: str,
50
+ message: NotificationMessage,
51
+ ) -> bool:
52
+ """Queue a target when batching is active."""
53
+ state = _active_batch.get()
54
+ if state is None:
55
+ return False
56
+ with state.lock:
57
+ if not state.accepting:
58
+ return False
59
+ if key in state.targets:
60
+ return True
61
+ state.targets[key] = _PendingNotification(
62
+ key,
63
+ group_send,
64
+ group,
65
+ dict(message),
66
+ )
67
+ return True
68
+
69
+
70
+ def _close_batch(state: _BatchState) -> None:
71
+ with state.lock:
72
+ state.accepting = False
73
+
74
+
75
+ async def _flush_notifications(state: _BatchState) -> None:
76
+ for key in sorted(state.targets):
77
+ target = state.targets[key]
78
+ try:
79
+ await target.group_send(target.group, target.message)
80
+ except MemoryError:
81
+ raise
82
+ except Exception as exc: # noqa: BLE001
83
+ logger.warning(
84
+ "failed to dispatch batched notification",
85
+ context={"key": key, "group": target.group},
86
+ exc_info=exc,
87
+ )
88
+
89
+
90
+ def _flush_sync(state: _BatchState) -> None:
91
+ if state.targets:
92
+ async_to_sync(_flush_notifications)(state)
93
+
94
+
95
+ @contextmanager
96
+ def bulk_data_change_notifications() -> Iterator[None]:
97
+ """Collect and flush data-change notifications for a bulk operation."""
98
+ current = _active_batch.get()
99
+ if current is not None:
100
+ yield
101
+ return
102
+
103
+ state = _BatchState()
104
+ token = _active_batch.set(state)
105
+ try:
106
+ yield
107
+ except BaseException as body_error:
108
+ _close_batch(state)
109
+ _active_batch.reset(token)
110
+ try:
111
+ _flush_sync(state)
112
+ except BaseException as flush_error: # noqa: BLE001
113
+ raise BaseExceptionGroup(
114
+ _COMBINED_FAILURE_MESSAGE,
115
+ [body_error, flush_error],
116
+ ) from None
117
+ raise
118
+ else:
119
+ _close_batch(state)
120
+ _active_batch.reset(token)
121
+ _flush_sync(state)