GeneralManager 0.57.4__tar.gz → 0.59.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 (255) hide show
  1. {generalmanager-0.57.4/src/GeneralManager.egg-info → generalmanager-0.59.0}/PKG-INFO +1 -1
  2. {generalmanager-0.57.4 → generalmanager-0.59.0}/pyproject.toml +1 -1
  3. {generalmanager-0.57.4 → generalmanager-0.59.0/src/GeneralManager.egg-info}/PKG-INFO +1 -1
  4. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_search.py +162 -61
  5. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm/mutations.py +34 -8
  6. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm_utils/payload_normalizer.py +34 -1
  7. {generalmanager-0.57.4 → generalmanager-0.59.0}/LICENSE +0 -0
  8. {generalmanager-0.57.4 → generalmanager-0.59.0}/README.md +0 -0
  9. {generalmanager-0.57.4 → generalmanager-0.59.0}/setup.cfg +0 -0
  10. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/GeneralManager.egg-info/SOURCES.txt +0 -0
  11. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/GeneralManager.egg-info/dependency_links.txt +0 -0
  12. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/GeneralManager.egg-info/requires.txt +0 -0
  13. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/GeneralManager.egg-info/top_level.txt +0 -0
  14. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/__init__.py +0 -0
  15. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/__init__.py +0 -0
  16. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/api.py +0 -0
  17. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/bucket.py +0 -0
  18. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/cache.py +0 -0
  19. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/factory.py +0 -0
  20. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/general_manager.py +0 -0
  21. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/interface.py +0 -0
  22. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/manager.py +0 -0
  23. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/measurement.py +0 -0
  24. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/permission.py +0 -0
  25. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/rule.py +0 -0
  26. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/search.py +0 -0
  27. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/_types/utils.py +0 -0
  28. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/__init__.py +0 -0
  29. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql.py +0 -0
  30. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_errors.py +0 -0
  31. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_mutations.py +0 -0
  32. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_prefetch.py +0 -0
  33. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_resolvers.py +0 -0
  34. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_subscription_consumer.py +0 -0
  35. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_subscriptions.py +0 -0
  36. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_view.py +0 -0
  37. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_warmup.py +0 -0
  38. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_warmup_registry.py +0 -0
  39. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/graphql_warmup_tasks.py +0 -0
  40. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/mutation.py +0 -0
  41. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/property.py +0 -0
  42. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/registry.py +0 -0
  43. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/remote_api.py +0 -0
  44. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/remote_invalidation.py +0 -0
  45. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/api/remote_invalidation_client.py +0 -0
  46. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/apps.py +0 -0
  47. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/bootstrap.py +0 -0
  48. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/bucket/__init__.py +0 -0
  49. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/bucket/base_bucket.py +0 -0
  50. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/bucket/calculation_bucket.py +0 -0
  51. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/bucket/database_bucket.py +0 -0
  52. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/bucket/group_bucket.py +0 -0
  53. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/bucket/indexing.py +0 -0
  54. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/bucket/request_bucket.py +0 -0
  55. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/__init__.py +0 -0
  56. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/cache_decorator.py +0 -0
  57. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/cache_tracker.py +0 -0
  58. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/dependency_cache.py +0 -0
  59. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/dependency_index.py +0 -0
  60. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/dependency_matching.py +0 -0
  61. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/dependency_publish.py +0 -0
  62. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/dependency_shards.py +0 -0
  63. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/model_dependency_collector.py +0 -0
  64. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/run_context.py +0 -0
  65. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/cache/signals.py +0 -0
  66. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/__init__.py +0 -0
  67. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/audit.py +0 -0
  68. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/bootstrap.py +0 -0
  69. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/checks.py +0 -0
  70. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/consumer.py +0 -0
  71. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/errors.py +0 -0
  72. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/__init__.py +0 -0
  73. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/__main__.py +0 -0
  74. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/baseline.py +0 -0
  75. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/diagnostics.py +0 -0
  76. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/fingerprints.py +0 -0
  77. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/fixtures.py +0 -0
  78. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/judges/__init__.py +0 -0
  79. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/judges/answer_quality.py +0 -0
  80. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/judges/contract.py +0 -0
  81. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/judges/result_accuracy.py +0 -0
  82. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/judges/tool_sequence.py +0 -0
  83. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/readiness.py +0 -0
  84. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/runner.py +0 -0
  85. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/evals/traces.py +0 -0
  86. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/grounding.py +0 -0
  87. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/models.py +0 -0
  88. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/providers/__init__.py +0 -0
  89. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/providers/_shared.py +0 -0
  90. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/providers/anthropic.py +0 -0
  91. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/providers/base.py +0 -0
  92. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/providers/google.py +0 -0
  93. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/providers/ollama.py +0 -0
  94. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/providers/openai.py +0 -0
  95. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/rate_limits.py +0 -0
  96. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/routing.py +0 -0
  97. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/schema_index.py +0 -0
  98. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/settings.py +0 -0
  99. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/signals.py +0 -0
  100. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/system_prompt.py +0 -0
  101. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/tool_metadata.py +0 -0
  102. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/tools.py +0 -0
  103. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/chat/views.py +0 -0
  104. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/conf.py +0 -0
  105. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/factory/__init__.py +0 -0
  106. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/factory/auto_factory.py +0 -0
  107. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/factory/factories.py +0 -0
  108. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/factory/factory_methods.py +0 -0
  109. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/__init__.py +0 -0
  110. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/base_interface.py +0 -0
  111. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/bundles/__init__.py +0 -0
  112. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/bundles/calculation.py +0 -0
  113. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/bundles/database.py +0 -0
  114. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/bundles/remote_manager.py +0 -0
  115. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/bundles/request.py +0 -0
  116. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/__init__.py +0 -0
  117. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/base.py +0 -0
  118. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/builtin.py +0 -0
  119. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/calculation/__init__.py +0 -0
  120. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/calculation/_compat.py +0 -0
  121. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/calculation/lifecycle.py +0 -0
  122. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/configuration.py +0 -0
  123. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/core/observability.py +0 -0
  124. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/core/utils.py +0 -0
  125. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/exceptions.py +0 -0
  126. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/existing_model/__init__.py +0 -0
  127. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/existing_model/_compat.py +0 -0
  128. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/existing_model/resolution.py +0 -0
  129. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/factory.py +0 -0
  130. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm/__init__.py +0 -0
  131. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm/_compat.py +0 -0
  132. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm/history.py +0 -0
  133. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm/lifecycle.py +0 -0
  134. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm/support.py +0 -0
  135. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm_utils/__init__.py +0 -0
  136. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm_utils/django_manager_utils.py +0 -0
  137. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/orm_utils/field_descriptors.py +0 -0
  138. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/read_only/__init__.py +0 -0
  139. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/read_only/_compat.py +0 -0
  140. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/read_only/_types.py +0 -0
  141. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/read_only/lifecycle.py +0 -0
  142. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/read_only/management.py +0 -0
  143. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/registry.py +0 -0
  144. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/remote_manager.py +0 -0
  145. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/capabilities/request/__init__.py +0 -0
  146. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/infrastructure/__init__.py +0 -0
  147. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/infrastructure/startup_hooks.py +0 -0
  148. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/infrastructure/system_checks.py +0 -0
  149. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/interfaces/__init__.py +0 -0
  150. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/interfaces/calculation.py +0 -0
  151. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/interfaces/database.py +0 -0
  152. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/interfaces/existing_model.py +0 -0
  153. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/interfaces/read_only.py +0 -0
  154. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/interfaces/remote_manager.py +0 -0
  155. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/interfaces/request.py +0 -0
  156. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/manifests/__init__.py +0 -0
  157. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/manifests/capability_builder.py +0 -0
  158. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/manifests/capability_manifest.py +0 -0
  159. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/manifests/capability_models.py +0 -0
  160. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/orm_interface.py +0 -0
  161. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/requests.py +0 -0
  162. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/utils/__init__.py +0 -0
  163. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/utils/database_interface_protocols.py +0 -0
  164. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/utils/errors.py +0 -0
  165. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/interface/utils/models.py +0 -0
  166. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/logging.py +0 -0
  167. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/chat_cleanup.py +0 -0
  168. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/graphql_warmup.py +0 -0
  169. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/graphql_warmup_refresh_due.py +0 -0
  170. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/search_index.py +0 -0
  171. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/search_reconcile.py +0 -0
  172. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/seed_manager_landscape.py +0 -0
  173. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/shell.py +0 -0
  174. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/workflow_drain_outbox.py +0 -0
  175. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/management/commands/workflow_replay_dead_letters.py +0 -0
  176. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/manager/__init__.py +0 -0
  177. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/manager/general_manager.py +0 -0
  178. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/manager/group_manager.py +0 -0
  179. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/manager/input.py +0 -0
  180. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/manager/meta.py +0 -0
  181. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/measurement/__init__.py +0 -0
  182. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/measurement/measurement.py +0 -0
  183. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/measurement/measurement_field.py +0 -0
  184. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/metrics/__init__.py +0 -0
  185. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/metrics/graphql.py +0 -0
  186. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/migrations/0001_initial.py +0 -0
  187. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/migrations/0002_workflow_outbox_scaling_indexes.py +0 -0
  188. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/migrations/0003_workflow_execution_correlation_constraint.py +0 -0
  189. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/migrations/0004_chat_persistence_models.py +0 -0
  190. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/migrations/0004_search_index_state.py +0 -0
  191. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/migrations/0005_merge_chat_search.py +0 -0
  192. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/migrations/0006_chat_pending_confirmation_scoped_ids.py +0 -0
  193. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/migrations/__init__.py +0 -0
  194. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/models.py +0 -0
  195. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/__init__.py +0 -0
  196. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/audit.py +0 -0
  197. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/base_permission.py +0 -0
  198. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/graphql_capabilities.py +0 -0
  199. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/manager_based_permission.py +0 -0
  200. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/mutation_permission.py +0 -0
  201. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/permission_checks.py +0 -0
  202. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/permission_data_manager.py +0 -0
  203. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/permission/utils.py +0 -0
  204. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/public_api_registry.py +0 -0
  205. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/py.typed +0 -0
  206. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/rule/__init__.py +0 -0
  207. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/rule/handler.py +0 -0
  208. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/rule/rule.py +0 -0
  209. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/__init__.py +0 -0
  210. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/async_tasks.py +0 -0
  211. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/backend.py +0 -0
  212. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/backend_registry.py +0 -0
  213. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/backends/__init__.py +0 -0
  214. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/backends/dev.py +0 -0
  215. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/backends/meilisearch.py +0 -0
  216. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/backends/opensearch.py +0 -0
  217. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/backends/typesense.py +0 -0
  218. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/config.py +0 -0
  219. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/indexer.py +0 -0
  220. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/models.py +0 -0
  221. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/reconciliation.py +0 -0
  222. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/registry.py +0 -0
  223. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/tasks.py +0 -0
  224. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/search/utils.py +0 -0
  225. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/seeding/__init__.py +0 -0
  226. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/seeding/manager_landscape.py +0 -0
  227. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/__init__.py +0 -0
  228. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/args_to_kwargs.py +0 -0
  229. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/filter_parser.py +0 -0
  230. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/format_string.py +0 -0
  231. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/json_encoder.py +0 -0
  232. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/make_cache_key.py +0 -0
  233. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/none_to_zero.py +0 -0
  234. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/path_mapping.py +0 -0
  235. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/public_api.py +0 -0
  236. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/testing.py +0 -0
  237. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/utils/type_checks.py +0 -0
  238. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/__init__.py +0 -0
  239. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/actions.py +0 -0
  240. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/backend_registry.py +0 -0
  241. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/backends/__init__.py +0 -0
  242. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/backends/celery.py +0 -0
  243. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/backends/local.py +0 -0
  244. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/backends/n8n.py +0 -0
  245. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/config.py +0 -0
  246. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/engine.py +0 -0
  247. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/event_registry.py +0 -0
  248. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/events.py +0 -0
  249. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/models.py +0 -0
  250. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/signal_bridge.py +0 -0
  251. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/tasks.py +0 -0
  252. {generalmanager-0.57.4 → generalmanager-0.59.0}/src/general_manager/workflow/telemetry.py +0 -0
  253. {generalmanager-0.57.4 → generalmanager-0.59.0}/tests/test_settings.py +0 -0
  254. {generalmanager-0.57.4 → generalmanager-0.59.0}/tests/test_urls.py +0 -0
  255. {generalmanager-0.57.4 → generalmanager-0.59.0}/tests/testing_asgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeneralManager
3
- Version: 0.57.4
3
+ Version: 0.59.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.57.4"
7
+ version = "0.59.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.57.4
3
+ Version: 0.59.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
@@ -14,6 +14,7 @@ from datetime import date, datetime
14
14
  from decimal import Decimal
15
15
  from typing import (
16
16
  TYPE_CHECKING,
17
+ Literal,
17
18
  cast,
18
19
  get_args,
19
20
  )
@@ -23,6 +24,7 @@ from graphql import GraphQLError
23
24
 
24
25
  from django.utils import timezone
25
26
 
27
+ from general_manager.conf import get_setting
26
28
  from general_manager.logging import get_logger
27
29
  from general_manager.manager.general_manager import GeneralManager
28
30
  from general_manager.measurement.measurement import Measurement
@@ -69,9 +71,20 @@ GrapheneReadMapper = Callable[
69
71
  SearchResolverPayload = dict[str, object]
70
72
  SearchHitEntry = tuple[float | None, SearchHit, GeneralManager]
71
73
  SortableValue = float | datetime | str | None
74
+ SearchTotalMode = Literal["exact", "bounded"]
72
75
  _BAD_USER_INPUT_CODE = "BAD_USER_INPUT"
73
76
  _PAGE_MUST_BE_POSITIVE = "page must be a positive integer."
74
77
  _PAGE_SIZE_MUST_BE_POSITIVE = "pageSize must be a positive integer."
78
+ DEFAULT_GRAPHQL_SEARCH_TOTAL_SCAN_LIMIT = 1000
79
+ SEARCH_TOTAL_MODE_ERROR = "must be one of: exact, bounded."
80
+ SEARCH_TOTAL_SCAN_LIMIT_ERROR = (
81
+ "GRAPHQL_SEARCH_TOTAL_SCAN_LIMIT must be a positive integer."
82
+ )
83
+
84
+
85
+ def _bad_user_input_error(message: str) -> GraphQLError:
86
+ """Build a GraphQL user-input error with the shared extension code."""
87
+ return GraphQLError(message, extensions={"code": _BAD_USER_INPUT_CODE})
75
88
 
76
89
 
77
90
  def _resolve_search_pagination(
@@ -96,6 +109,106 @@ def _resolve_search_pagination(
96
109
  return current_page, limit
97
110
 
98
111
 
112
+ def _invalid_search_total_mode_error(setting_label: str) -> GraphQLError:
113
+ """Build the validation error for an invalid total-mode value."""
114
+ message = f"{setting_label} {SEARCH_TOTAL_MODE_ERROR}"
115
+ return _bad_user_input_error(message)
116
+
117
+
118
+ def normalize_search_total_mode(total_mode: str | None = None) -> SearchTotalMode:
119
+ """Resolve and validate the requested GraphQL search total-count mode."""
120
+ if total_mode is None:
121
+ raw_mode = get_setting("GRAPHQL_SEARCH_TOTAL_MODE", "exact")
122
+ setting_label = 'GENERAL_MANAGER["GRAPHQL_SEARCH_TOTAL_MODE"]'
123
+ else:
124
+ raw_mode = total_mode
125
+ setting_label = "totalMode"
126
+
127
+ if not isinstance(raw_mode, str):
128
+ raise _invalid_search_total_mode_error(setting_label)
129
+
130
+ normalized = raw_mode.strip().lower()
131
+ if normalized not in {"exact", "bounded"}:
132
+ raise _invalid_search_total_mode_error(setting_label)
133
+ return cast(SearchTotalMode, normalized)
134
+
135
+
136
+ def get_graphql_search_total_scan_limit() -> int:
137
+ """Return the configured positive scan limit for bounded total counts."""
138
+ raw_limit = get_setting(
139
+ "GRAPHQL_SEARCH_TOTAL_SCAN_LIMIT",
140
+ DEFAULT_GRAPHQL_SEARCH_TOTAL_SCAN_LIMIT,
141
+ )
142
+ if not isinstance(raw_limit, int) or isinstance(raw_limit, bool) or raw_limit <= 0:
143
+ raise _bad_user_input_error(SEARCH_TOTAL_SCAN_LIMIT_ERROR)
144
+ return raw_limit
145
+
146
+
147
+ def normalize_search_sort_value(value: object) -> SortableValue:
148
+ """Normalise a search sort value to a comparable type."""
149
+ if value is None:
150
+ return None
151
+ if isinstance(value, (int, float, Decimal)):
152
+ return float(value)
153
+ if isinstance(value, datetime):
154
+ if timezone.is_naive(value):
155
+ return timezone.make_aware(value)
156
+ return value
157
+ if isinstance(value, date):
158
+ return timezone.make_aware(datetime.combine(value, datetime.min.time()))
159
+ if isinstance(value, str):
160
+ try:
161
+ parsed = datetime.fromisoformat(value)
162
+ except ValueError:
163
+ return value
164
+ if timezone.is_naive(parsed):
165
+ parsed = timezone.make_aware(parsed)
166
+ return parsed
167
+ return str(value)
168
+
169
+
170
+ def sort_search_hit_entries(
171
+ entries: list[SearchHitEntry],
172
+ *,
173
+ sort_by: str | None,
174
+ sort_desc: bool,
175
+ ) -> None:
176
+ """Sort retained search entries using GraphQL search response semantics."""
177
+ if sort_by:
178
+
179
+ def _sort_key(
180
+ item: SearchHitEntry,
181
+ ) -> tuple[bool, SortableValue]:
182
+ """Return a null-last comparable key for an indexed search hit."""
183
+ value = item[1].data.get(sort_by) if item[1].data else None
184
+ normalized = normalize_search_sort_value(value)
185
+ return (normalized is None, normalized)
186
+
187
+ entries.sort(key=_sort_key)
188
+ if sort_desc:
189
+ null_start = next(
190
+ (index for index, item in enumerate(entries) if _sort_key(item)[0]),
191
+ len(entries),
192
+ )
193
+ entries[:null_start] = reversed(entries[:null_start])
194
+ else:
195
+ entries.sort(key=lambda item: item[0] or 0, reverse=True)
196
+
197
+
198
+ def trim_search_hit_entries_to_window(
199
+ entries: list[SearchHitEntry],
200
+ *,
201
+ requested_count: int,
202
+ sort_by: str | None,
203
+ sort_desc: bool,
204
+ ) -> None:
205
+ """Keep only entries that can still appear in the requested global page."""
206
+ if len(entries) <= requested_count:
207
+ return
208
+ sort_search_hit_entries(entries, sort_by=sort_by, sort_desc=sort_desc)
209
+ del entries[requested_count:]
210
+
211
+
99
212
  # ---------------------------------------------------------------------------
100
213
  # Filter normalisation
101
214
  # ---------------------------------------------------------------------------
@@ -343,7 +456,7 @@ def create_search_result_type(
343
456
 
344
457
  Returns:
345
458
  A ``SearchResult`` Graphene ObjectType with ``results``, ``total``,
346
- ``took_ms``, and ``raw`` fields.
459
+ ``total_is_exact``, ``took_ms``, and ``raw`` fields.
347
460
  """
348
461
  return type(
349
462
  "SearchResult",
@@ -351,6 +464,7 @@ def create_search_result_type(
351
464
  {
352
465
  "results": graphene.List(union_type),
353
466
  "total": graphene.Int(),
467
+ "total_is_exact": graphene.Boolean(),
354
468
  "took_ms": graphene.Int(),
355
469
  "raw": graphene.JSONString(),
356
470
  },
@@ -767,13 +881,15 @@ def register_search_query(
767
881
 
768
882
  Manager search order follows ``manager_registry.values()`` filtered to
769
883
  managers with search configuration, unless the resolver receives ``types``;
770
- unknown names in ``types`` are ignored. ``total`` in the resolver payload is
771
- the post-permission authorized total, not the backend raw total. Sorting
772
- uses hit ``data[sort_by]`` when available and does not validate the sort
773
- field ahead of comparison. Configured filter-key validation runs on the
774
- parsed top-level search filters before permission filters or relation
775
- normalizers are applied; exclude-derived permission keys are not validated
776
- by this search helper.
884
+ unknown names in ``types`` are ignored. In exact total mode, ``total`` in
885
+ the resolver payload is the post-permission authorized total, not the
886
+ backend raw total. In bounded mode, ``total`` is the authorized count found
887
+ before the per-manager scan cap and ``total_is_exact`` tells callers whether
888
+ that count is exact. Sorting uses hit ``data[sort_by]`` when available and
889
+ does not validate the sort field ahead of comparison. Configured filter-key
890
+ validation runs on the parsed top-level search filters before permission
891
+ filters or relation normalizers are applied; exclude-derived permission keys
892
+ are not validated by this search helper.
777
893
 
778
894
  Parameters:
779
895
  query_fields: Mutable dict of registered query fields; will be updated.
@@ -819,6 +935,7 @@ def register_search_query(
819
935
  filters: GraphQLSearchFilterInput = None,
820
936
  sort_by: str | None = None,
821
937
  sort_desc: bool = False,
938
+ total_mode: str | None = None,
822
939
  page: int | None = None,
823
940
  page_size: int | None = None,
824
941
  ) -> SearchResolverPayload:
@@ -855,10 +972,13 @@ def register_search_query(
855
972
  raw: list[object] = []
856
973
  requested_count = offset + limit
857
974
  fetch_limit = max(requested_count, limit)
858
- # Tracks how many authorized hits have been appended globally so far.
859
- # This enforces a single cross-manager cap on collected results while
860
- # still letting the loop continue counting for an accurate ``total``.
861
- global_appended_hits = 0
975
+ resolved_total_mode = normalize_search_total_mode(total_mode)
976
+ total_scan_limit = (
977
+ max(get_graphql_search_total_scan_limit(), requested_count)
978
+ if resolved_total_mode == "bounded"
979
+ else None
980
+ )
981
+ total_is_exact = True
862
982
 
863
983
  for manager_class in manager_classes:
864
984
  type_label = manager_class.__name__
@@ -872,17 +992,24 @@ def register_search_query(
872
992
  parsed_filters,
873
993
  permission_plan.filters,
874
994
  )
875
- authorized_hits: list[SearchHitEntry] = []
876
995
  total_hits_for_manager = 0
877
996
  candidate_hits_for_manager = 0
878
- appended_hits_for_manager = 0
879
997
  offset_cursor = 0
998
+ scanned_hits_for_manager = 0
999
+ manager_total_is_exact = True
880
1000
  while True:
1001
+ query_limit = fetch_limit
1002
+ if total_scan_limit is not None:
1003
+ remaining_scan_budget = total_scan_limit - scanned_hits_for_manager
1004
+ if remaining_scan_budget <= 0:
1005
+ manager_total_is_exact = False
1006
+ break
1007
+ query_limit = min(fetch_limit, remaining_scan_budget)
881
1008
  result = backend.search(
882
1009
  index_name,
883
1010
  query,
884
1011
  filters=filter_groups,
885
- limit=fetch_limit,
1012
+ limit=query_limit,
886
1013
  offset=offset_cursor,
887
1014
  types=[type_label],
888
1015
  sort_by=sort_by,
@@ -896,6 +1023,7 @@ def register_search_query(
896
1023
  raw.append(result.raw)
897
1024
  if not result.hits:
898
1025
  break
1026
+ scanned_hits_for_manager += len(result.hits)
899
1027
  offset_cursor += len(result.hits)
900
1028
  for hit in result.hits:
901
1029
  try:
@@ -918,17 +1046,24 @@ def register_search_query(
918
1046
  ):
919
1047
  continue
920
1048
  total_hits_for_manager += 1
921
- # Apply a single global cap across all managers so that
922
- # ``authorized_hits`` never grows beyond ``requested_count``
923
- # items, regardless of how many managers are searched.
924
- if global_appended_hits < requested_count:
925
- authorized_hits.append((hit.score, hit, instance))
926
- appended_hits_for_manager += 1
927
- global_appended_hits += 1
928
- if len(result.hits) < fetch_limit:
1049
+ hits.append((hit.score, hit, instance))
1050
+ trim_search_hit_entries_to_window(
1051
+ hits,
1052
+ requested_count=requested_count,
1053
+ sort_by=sort_by,
1054
+ sort_desc=sort_desc,
1055
+ )
1056
+ if len(result.hits) < query_limit:
1057
+ break
1058
+ if (
1059
+ total_scan_limit is not None
1060
+ and scanned_hits_for_manager >= total_scan_limit
1061
+ ):
1062
+ manager_total_is_exact = False
929
1063
  break
930
1064
  total += total_hits_for_manager
931
- hits.extend(authorized_hits)
1065
+ if not manager_total_is_exact:
1066
+ total_is_exact = False
932
1067
  if permission_plan.requires_instance_check:
933
1068
  logger.info(
934
1069
  "graphql read authorization summary",
@@ -947,43 +1082,7 @@ def register_search_query(
947
1082
  },
948
1083
  )
949
1084
 
950
- if sort_by:
951
-
952
- def _normalize_sort_value(value: object) -> SortableValue:
953
- """Normalise a sort value to a comparable type."""
954
- if value is None:
955
- return None
956
- if isinstance(value, (int, float, Decimal)):
957
- return float(value)
958
- if isinstance(value, datetime):
959
- if timezone.is_naive(value):
960
- return timezone.make_aware(value)
961
- return value
962
- if isinstance(value, date):
963
- return timezone.make_aware(
964
- datetime.combine(value, datetime.min.time())
965
- )
966
- if isinstance(value, str):
967
- try:
968
- parsed = datetime.fromisoformat(value)
969
- except ValueError:
970
- return value
971
- if timezone.is_naive(parsed):
972
- parsed = timezone.make_aware(parsed)
973
- return parsed
974
- return str(value)
975
-
976
- def _sort_key(
977
- item: SearchHitEntry,
978
- ) -> tuple[bool, SortableValue]:
979
- """Sort key placing None-valued items last."""
980
- value = item[1].data.get(sort_by) if item[1].data else None
981
- normalized = _normalize_sort_value(value)
982
- return (normalized is None, normalized)
983
-
984
- hits.sort(key=_sort_key, reverse=sort_desc)
985
- else:
986
- hits.sort(key=lambda item: item[0] or 0, reverse=True)
1085
+ sort_search_hit_entries(hits, sort_by=sort_by, sort_desc=sort_desc)
987
1086
 
988
1087
  items: list[GeneralManager] = []
989
1088
  for _, _hit, instance in hits[offset : offset + limit]:
@@ -992,6 +1091,7 @@ def register_search_query(
992
1091
  return {
993
1092
  "results": items,
994
1093
  "total": total,
1094
+ "total_is_exact": total_is_exact,
995
1095
  "took_ms": took_ms,
996
1096
  "raw": raw,
997
1097
  }
@@ -1004,6 +1104,7 @@ def register_search_query(
1004
1104
  filters=graphene.JSONString(),
1005
1105
  sort_by=graphene.String(),
1006
1106
  sort_desc=graphene.Boolean(),
1107
+ total_mode=graphene.String(),
1007
1108
  page=graphene.Int(),
1008
1109
  page_size=graphene.Int(),
1009
1110
  resolver=resolver,
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, ClassVar, cast
5
+ from typing import TYPE_CHECKING, Any, ClassVar, cast
6
6
 
7
7
  from django.contrib.auth import get_user_model
8
8
  from django.db import models, transaction
@@ -10,6 +10,9 @@ from django.db.models import NOT_PROVIDED
10
10
 
11
11
  from general_manager.interface.capabilities.base import CapabilityName
12
12
  from general_manager.interface.capabilities.builtin import BaseCapability
13
+ from general_manager.interface.capabilities.orm_utils.payload_normalizer import (
14
+ PayloadNormalizer,
15
+ )
13
16
  from general_manager.interface.utils.database_interface_protocols import (
14
17
  SupportsActivation,
15
18
  )
@@ -581,10 +584,8 @@ class OrmValidationCapability(BaseCapability):
581
584
  """
582
585
  support = get_support_capability(interface_cls)
583
586
  normalizer = support.get_payload_normalizer(interface_cls)
584
- payload_copy = dict(payload)
585
- normalizer.validate_keys(payload_copy)
586
- simple_kwargs, many_to_many_kwargs = normalizer.split_many_to_many(
587
- payload_copy
587
+ simple_kwargs, many_to_many_kwargs = _validate_and_split_payload(
588
+ normalizer, payload
588
589
  )
589
590
  normalized_simple = normalizer.normalize_simple_values(simple_kwargs)
590
591
  normalized_many = normalizer.normalize_many_values(many_to_many_kwargs)
@@ -629,9 +630,9 @@ def _normalize_payload(
629
630
  )
630
631
  support = get_support_capability(interface_cls)
631
632
  normalizer = support.get_payload_normalizer(interface_cls)
632
- payload_copy = dict(payload)
633
- normalizer.validate_keys(payload_copy)
634
- simple_kwargs, many_to_many_kwargs = normalizer.split_many_to_many(payload_copy)
633
+ simple_kwargs, many_to_many_kwargs = _validate_and_split_payload(
634
+ normalizer, payload
635
+ )
635
636
  normalized_simple = normalizer.normalize_simple_values(simple_kwargs)
636
637
  normalized_many = normalizer.normalize_many_values(many_to_many_kwargs)
637
638
  return dict(normalized_simple), {
@@ -639,6 +640,31 @@ def _normalize_payload(
639
640
  }
640
641
 
641
642
 
643
+ def _validate_and_split_payload(
644
+ normalizer: Any,
645
+ payload: MutationPayload,
646
+ ) -> tuple[MutationPayload, MutationPayload]:
647
+ """Validate and split payloads without exposing caller mappings to mutation."""
648
+ normalizer_payload = (
649
+ payload if type(normalizer) is PayloadNormalizer else dict(payload)
650
+ )
651
+ normalizer.validate_keys(normalizer_payload)
652
+ return _split_many_to_many_payload(normalizer, normalizer_payload)
653
+
654
+
655
+ def _split_many_to_many_payload(
656
+ normalizer: Any,
657
+ payload: MutationPayload,
658
+ ) -> tuple[MutationPayload, MutationPayload]:
659
+ """Split concrete normalizer payloads without mutation; copy for custom normalizers."""
660
+ if type(normalizer) is PayloadNormalizer:
661
+ return normalizer.split_many_to_many_non_mutating(payload)
662
+ return cast(
663
+ tuple[MutationPayload, MutationPayload],
664
+ normalizer.split_many_to_many(payload),
665
+ )
666
+
667
+
642
668
  def _assign_history_actor(
643
669
  instance: models.Model,
644
670
  *,
@@ -88,7 +88,7 @@ class PayloadNormalizer:
88
88
  self, kwargs: PayloadMapping
89
89
  ) -> tuple[PayloadMapping, PayloadMapping]:
90
90
  """
91
- Separate many-to-many related entries from a kwargs mapping.
91
+ Mutate a kwargs mapping by moving many-to-many related entries out of it.
92
92
 
93
93
  Parameters:
94
94
  kwargs: Mapping of field lookups where keys for many-to-many relations are expected to end with `_id_list`.
@@ -106,6 +106,39 @@ class PayloadNormalizer:
106
106
  many_kwargs[f"{base_key}_id_list"] = kwargs.pop(key)
107
107
  return kwargs, many_kwargs
108
108
 
109
+ def split_many_to_many_non_mutating(
110
+ self, kwargs: PayloadMapping
111
+ ) -> tuple[PayloadMapping, PayloadMapping]:
112
+ """
113
+ Return separated many-to-many entries without mutating the input mapping.
114
+
115
+ Parameters:
116
+ kwargs: Mapping of field lookups where keys for many-to-many
117
+ relations may use either `<relation>_list` or
118
+ `<relation>_id_list`.
119
+
120
+ Returns:
121
+ tuple: A pair `(remaining_kwargs, many_kwargs)` where both mappings
122
+ are new dictionaries. `remaining_kwargs` contains non-matching
123
+ entries from `kwargs`, and `many_kwargs` contains matching
124
+ many-to-many entries under canonical `<relation>_id_list` keys.
125
+
126
+ Notes:
127
+ This is the non-mutating counterpart to `split_many_to_many()`. If
128
+ both `<relation>_list` and `<relation>_id_list` are present,
129
+ iteration order decides the last canonical value stored in
130
+ `many_kwargs`.
131
+ """
132
+ remaining_kwargs: PayloadMapping = {}
133
+ many_kwargs: PayloadMapping = {}
134
+ for key, value in kwargs.items():
135
+ base_key = self._m2m_alias_base_key(key)
136
+ if base_key in self._many_to_many_fields:
137
+ many_kwargs[f"{base_key}_id_list"] = value
138
+ else:
139
+ remaining_kwargs[key] = value
140
+ return remaining_kwargs, many_kwargs
141
+
109
142
  def normalize_simple_values(self, kwargs: PayloadMapping) -> PayloadMapping:
110
143
  """
111
144
  Normalize simple (single-valued) payload entries by converting general-manager objects to their identifier form and adjusting keys.
File without changes