gatewaysdk 0.3.2__py3-none-any.whl

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 (376) hide show
  1. gatewaysdk/__init__.py +540 -0
  2. gatewaysdk/adapter/__init__.py +15 -0
  3. gatewaysdk/adapter/base.py +94 -0
  4. gatewaysdk/adapter/messages.py +270 -0
  5. gatewaysdk/adapter/triplet.py +1026 -0
  6. gatewaysdk/algorithm/__init__.py +39 -0
  7. gatewaysdk/algorithm/apo/__init__.py +5 -0
  8. gatewaysdk/algorithm/apo/apo.py +898 -0
  9. gatewaysdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
  10. gatewaysdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
  11. gatewaysdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
  12. gatewaysdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
  13. gatewaysdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
  14. gatewaysdk/algorithm/base.py +262 -0
  15. gatewaysdk/algorithm/decorator.py +264 -0
  16. gatewaysdk/algorithm/evals/__init__.py +7 -0
  17. gatewaysdk/algorithm/evals/evals.py +217 -0
  18. gatewaysdk/algorithm/fast.py +250 -0
  19. gatewaysdk/algorithm/gepa/__init__.py +61 -0
  20. gatewaysdk/algorithm/gepa/adapter.py +495 -0
  21. gatewaysdk/algorithm/gepa/gepa.py +570 -0
  22. gatewaysdk/algorithm/gepa/lib/__init__.py +18 -0
  23. gatewaysdk/algorithm/gepa/lib/adapters/README.md +12 -0
  24. gatewaysdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
  25. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
  26. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
  27. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
  28. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
  29. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
  30. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
  31. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
  32. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
  33. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
  34. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
  35. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
  36. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
  37. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +268 -0
  38. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
  39. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
  40. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
  41. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
  42. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
  43. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
  44. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
  45. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
  46. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
  47. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
  48. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
  49. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
  50. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
  51. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
  52. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +699 -0
  53. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
  54. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
  55. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
  56. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
  57. gatewaysdk/algorithm/gepa/lib/api.py +382 -0
  58. gatewaysdk/algorithm/gepa/lib/core/__init__.py +0 -0
  59. gatewaysdk/algorithm/gepa/lib/core/adapter.py +180 -0
  60. gatewaysdk/algorithm/gepa/lib/core/data_loader.py +74 -0
  61. gatewaysdk/algorithm/gepa/lib/core/engine.py +379 -0
  62. gatewaysdk/algorithm/gepa/lib/core/result.py +233 -0
  63. gatewaysdk/algorithm/gepa/lib/core/state.py +636 -0
  64. gatewaysdk/algorithm/gepa/lib/examples/__init__.py +0 -0
  65. gatewaysdk/algorithm/gepa/lib/examples/aime.py +24 -0
  66. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
  67. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
  68. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
  69. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
  70. gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
  71. gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
  72. gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
  73. gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +456 -0
  74. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
  75. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
  76. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +820 -0
  77. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
  78. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
  79. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
  80. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
  81. gatewaysdk/algorithm/gepa/lib/gepa_utils.py +117 -0
  82. gatewaysdk/algorithm/gepa/lib/logging/__init__.py +0 -0
  83. gatewaysdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
  84. gatewaysdk/algorithm/gepa/lib/logging/logger.py +75 -0
  85. gatewaysdk/algorithm/gepa/lib/logging/utils.py +103 -0
  86. gatewaysdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
  87. gatewaysdk/algorithm/gepa/lib/proposer/base.py +31 -0
  88. gatewaysdk/algorithm/gepa/lib/proposer/merge.py +357 -0
  89. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
  90. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
  91. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
  92. gatewaysdk/algorithm/gepa/lib/py.typed +0 -0
  93. gatewaysdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
  94. gatewaysdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
  95. gatewaysdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
  96. gatewaysdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
  97. gatewaysdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
  98. gatewaysdk/algorithm/gepa/lib/strategies/instruction_proposal.py +126 -0
  99. gatewaysdk/algorithm/gepa/lib/utils/__init__.py +10 -0
  100. gatewaysdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
  101. gatewaysdk/algorithm/gepa/tracing.py +105 -0
  102. gatewaysdk/algorithm/utils.py +177 -0
  103. gatewaysdk/algorithm/verl/__init__.py +5 -0
  104. gatewaysdk/algorithm/verl/interface.py +202 -0
  105. gatewaysdk/automations.py +111 -0
  106. gatewaysdk/benchmark_hub/README.md +88 -0
  107. gatewaysdk/benchmark_hub/__init__.py +119 -0
  108. gatewaysdk/benchmark_hub/_archive.py +189 -0
  109. gatewaysdk/benchmark_hub/_tar.py +6 -0
  110. gatewaysdk/benchmark_hub/client.py +992 -0
  111. gatewaysdk/benchmark_hub/dispatch_shard.py +34 -0
  112. gatewaysdk/benchmark_hub/eval_config.py +20 -0
  113. gatewaysdk/benchmark_hub/evals.py +351 -0
  114. gatewaysdk/benchmark_hub/harbor_adapter.py +673 -0
  115. gatewaysdk/benchmark_hub/path_utils.py +81 -0
  116. gatewaysdk/benchmark_hub/save_utils.py +101 -0
  117. gatewaysdk/benchmark_hub/verifiers_adapter.py +327 -0
  118. gatewaysdk/benchmark_hub/versioning.py +72 -0
  119. gatewaysdk/build.py +515 -0
  120. gatewaysdk/cli/__init__.py +58 -0
  121. gatewaysdk/cli/agent_runner.py +132 -0
  122. gatewaysdk/cli/http_client.py +115 -0
  123. gatewaysdk/cli/platform.py +4086 -0
  124. gatewaysdk/cli/prometheus.py +115 -0
  125. gatewaysdk/cli/release_gate.py +215 -0
  126. gatewaysdk/cli/store.py +131 -0
  127. gatewaysdk/cli/vllm.py +29 -0
  128. gatewaysdk/client.py +406 -0
  129. gatewaysdk/config.py +348 -0
  130. gatewaysdk/connectors/__init__.py +25 -0
  131. gatewaysdk/connectors/client.py +203 -0
  132. gatewaysdk/connectors/skill.py +25 -0
  133. gatewaysdk/connectors/template.py +106 -0
  134. gatewaysdk/context.py +606 -0
  135. gatewaysdk/emitter/__init__.py +43 -0
  136. gatewaysdk/emitter/annotation.py +370 -0
  137. gatewaysdk/emitter/exception.py +54 -0
  138. gatewaysdk/emitter/message.py +61 -0
  139. gatewaysdk/emitter/object.py +117 -0
  140. gatewaysdk/emitter/reward.py +320 -0
  141. gatewaysdk/env_var.py +156 -0
  142. gatewaysdk/environment/__init__.py +108 -0
  143. gatewaysdk/environment/_bundle.py +281 -0
  144. gatewaysdk/environment/_harbor.py +276 -0
  145. gatewaysdk/environment/_materialize.py +97 -0
  146. gatewaysdk/environment/_tar.py +33 -0
  147. gatewaysdk/environment/_world.py +346 -0
  148. gatewaysdk/environment/core.py +527 -0
  149. gatewaysdk/environment/errors.py +58 -0
  150. gatewaysdk/environment/runtime.py +150 -0
  151. gatewaysdk/environment/schema/__init__.py +35 -0
  152. gatewaysdk/environment/schema/__main__.py +225 -0
  153. gatewaysdk/environment/schema/_slack_fidelity.py +116 -0
  154. gatewaysdk/environment/schema/_slack_scenario.py +183 -0
  155. gatewaysdk/environment/schema/api.py +1628 -0
  156. gatewaysdk/environment/schema/batch.py +755 -0
  157. gatewaysdk/environment/schema/compiler.py +615 -0
  158. gatewaysdk/environment/schema/conform.py +428 -0
  159. gatewaysdk/environment/schema/connector.py +777 -0
  160. gatewaysdk/environment/schema/connectors/apple-business-manager/handlers.py +17 -0
  161. gatewaysdk/environment/schema/connectors/apple-business-manager/parity.json +28 -0
  162. gatewaysdk/environment/schema/connectors/apple-business-manager/provenance.json +233 -0
  163. gatewaysdk/environment/schema/connectors/apple-business-manager/scope.toml +112 -0
  164. gatewaysdk/environment/schema/connectors/apple-business-manager/world.json +1380 -0
  165. gatewaysdk/environment/schema/connectors/base.json +28 -0
  166. gatewaysdk/environment/schema/connectors/custom/handlers.py +18 -0
  167. gatewaysdk/environment/schema/connectors/custom/world.json +123 -0
  168. gatewaysdk/environment/schema/connectors/github/handlers.py +81 -0
  169. gatewaysdk/environment/schema/connectors/github/parity.json +37 -0
  170. gatewaysdk/environment/schema/connectors/github/provenance.json +134 -0
  171. gatewaysdk/environment/schema/connectors/github/scope.toml +158 -0
  172. gatewaysdk/environment/schema/connectors/github/world.json +5869 -0
  173. gatewaysdk/environment/schema/connectors/google-calendar/handlers.py +23 -0
  174. gatewaysdk/environment/schema/connectors/google-calendar/world.json +209 -0
  175. gatewaysdk/environment/schema/connectors/google-drive/handlers.py +18 -0
  176. gatewaysdk/environment/schema/connectors/google-drive/world.json +286 -0
  177. gatewaysdk/environment/schema/connectors/jamf/handlers.py +34 -0
  178. gatewaysdk/environment/schema/connectors/jamf/parity.json +49 -0
  179. gatewaysdk/environment/schema/connectors/jamf/provenance.json +181 -0
  180. gatewaysdk/environment/schema/connectors/jamf/scope.toml +128 -0
  181. gatewaysdk/environment/schema/connectors/jamf/world.json +2218 -0
  182. gatewaysdk/environment/schema/connectors/jira/handlers.py +28 -0
  183. gatewaysdk/environment/schema/connectors/jira/world.json +389 -0
  184. gatewaysdk/environment/schema/connectors/kandji/handlers.py +38 -0
  185. gatewaysdk/environment/schema/connectors/kandji/parity.json +29 -0
  186. gatewaysdk/environment/schema/connectors/kandji/provenance.json +152 -0
  187. gatewaysdk/environment/schema/connectors/kandji/scope.toml +120 -0
  188. gatewaysdk/environment/schema/connectors/kandji/world.json +765 -0
  189. gatewaysdk/environment/schema/connectors/linear/handlers.py +20 -0
  190. gatewaysdk/environment/schema/connectors/linear/world.json +363 -0
  191. gatewaysdk/environment/schema/connectors/microsoft-teams/fidelity.json +45 -0
  192. gatewaysdk/environment/schema/connectors/microsoft-teams/handlers.py +1 -0
  193. gatewaysdk/environment/schema/connectors/microsoft-teams/provenance.json +851 -0
  194. gatewaysdk/environment/schema/connectors/microsoft-teams/scope.md +231 -0
  195. gatewaysdk/environment/schema/connectors/microsoft-teams/teams-types.json +1710 -0
  196. gatewaysdk/environment/schema/connectors/microsoft-teams/world.json +858 -0
  197. gatewaysdk/environment/schema/connectors/netsuite/handlers.py +32 -0
  198. gatewaysdk/environment/schema/connectors/netsuite/parity.json +50 -0
  199. gatewaysdk/environment/schema/connectors/netsuite/provenance.json +137 -0
  200. gatewaysdk/environment/schema/connectors/netsuite/scope.toml +78 -0
  201. gatewaysdk/environment/schema/connectors/netsuite/world.json +16243 -0
  202. gatewaysdk/environment/schema/connectors/salesforce/handlers.py +806 -0
  203. gatewaysdk/environment/schema/connectors/salesforce/parity.json +46 -0
  204. gatewaysdk/environment/schema/connectors/salesforce/provenance.json +143 -0
  205. gatewaysdk/environment/schema/connectors/salesforce/scope.toml +174 -0
  206. gatewaysdk/environment/schema/connectors/salesforce/world.json +4695 -0
  207. gatewaysdk/environment/schema/connectors/slack/MODEL.md +236 -0
  208. gatewaysdk/environment/schema/connectors/slack/capabilities.json +1039 -0
  209. gatewaysdk/environment/schema/connectors/slack/handlers.py +762 -0
  210. gatewaysdk/environment/schema/connectors/slack/parity.json +65 -0
  211. gatewaysdk/environment/schema/connectors/slack/provenance.json +294 -0
  212. gatewaysdk/environment/schema/connectors/slack/scope.toml +44 -0
  213. gatewaysdk/environment/schema/connectors/slack/world.json +4993 -0
  214. gatewaysdk/environment/schema/connectors/workday/handlers.py +75 -0
  215. gatewaysdk/environment/schema/connectors/workday/parity.json +36 -0
  216. gatewaysdk/environment/schema/connectors/workday/provenance.json +179 -0
  217. gatewaysdk/environment/schema/connectors/workday/scope.toml +180 -0
  218. gatewaysdk/environment/schema/connectors/workday/world.json +806 -0
  219. gatewaysdk/environment/schema/data.py +593 -0
  220. gatewaysdk/environment/schema/extract.py +263 -0
  221. gatewaysdk/environment/schema/host.py +2437 -0
  222. gatewaysdk/environment/schema/host_surfaces.py +275 -0
  223. gatewaysdk/environment/schema/platform.py +1272 -0
  224. gatewaysdk/environment/schema/scaffold.py +250 -0
  225. gatewaysdk/environment/schema/skill.py +118 -0
  226. gatewaysdk/environment/schema/skills/agents/connector-template-author.md +202 -0
  227. gatewaysdk/environment/schema/skills/connector-schema-authoring/CONNECTORS.md +331 -0
  228. gatewaysdk/environment/schema/skills/connector-schema-authoring/CONVENTIONS.md +100 -0
  229. gatewaysdk/environment/schema/skills/connector-schema-authoring/SKILL.md +313 -0
  230. gatewaysdk/environment/schema/skills/world-data-ingestion/ROWS.md +223 -0
  231. gatewaysdk/environment/schema/skills/world-data-ingestion/SKILL.md +293 -0
  232. gatewaysdk/environment/schema/skills/worlds-getting-started/SKILL.md +390 -0
  233. gatewaysdk/environment/schema/slack.py +757 -0
  234. gatewaysdk/environment/schema/snapshot.py +479 -0
  235. gatewaysdk/environment/schema/store.py +904 -0
  236. gatewaysdk/environment/schema/tasks.py +525 -0
  237. gatewaysdk/environment/schema/tools_world.py +322 -0
  238. gatewaysdk/environment/schema/validation.py +577 -0
  239. gatewaysdk/environment/schema/worker.py +78 -0
  240. gatewaysdk/environment/schema/workers.py +500 -0
  241. gatewaysdk/environment/schema/world_tests.py +336 -0
  242. gatewaysdk/environments/__init__.py +24 -0
  243. gatewaysdk/environments/client.py +486 -0
  244. gatewaysdk/environments/types.py +318 -0
  245. gatewaysdk/execution/__init__.py +15 -0
  246. gatewaysdk/execution/base.py +64 -0
  247. gatewaysdk/execution/client_server.py +443 -0
  248. gatewaysdk/execution/events.py +69 -0
  249. gatewaysdk/execution/inter_process.py +16 -0
  250. gatewaysdk/execution/shared_memory.py +282 -0
  251. gatewaysdk/experiments/__init__.py +90 -0
  252. gatewaysdk/experiments/assignment.py +177 -0
  253. gatewaysdk/experiments/client.py +877 -0
  254. gatewaysdk/experiments/exposure.py +222 -0
  255. gatewaysdk/experiments/types.py +81 -0
  256. gatewaysdk/importers/__init__.py +49 -0
  257. gatewaysdk/importers/_normalize.py +91 -0
  258. gatewaysdk/importers/builder.py +173 -0
  259. gatewaysdk/importers/client.py +95 -0
  260. gatewaysdk/importers/langsmith.py +196 -0
  261. gatewaysdk/importers/recipes.py +249 -0
  262. gatewaysdk/instrumentation/__init__.py +300 -0
  263. gatewaysdk/instrumentation/agentops.py +314 -0
  264. gatewaysdk/instrumentation/agentops_langchain.py +45 -0
  265. gatewaysdk/instrumentation/base.py +119 -0
  266. gatewaysdk/instrumentation/litellm.py +83 -0
  267. gatewaysdk/instrumentation/registry.py +273 -0
  268. gatewaysdk/instrumentation/vllm.py +81 -0
  269. gatewaysdk/instrumentation/weave.py +500 -0
  270. gatewaysdk/integrations/__init__.py +15 -0
  271. gatewaysdk/integrations/gateway/__init__.py +11 -0
  272. gatewaysdk/integrations/gateway/client.py +171 -0
  273. gatewaysdk/integrations/tool_access.py +549 -0
  274. gatewaysdk/litagent/__init__.py +11 -0
  275. gatewaysdk/litagent/decorator.py +536 -0
  276. gatewaysdk/litagent/litagent.py +252 -0
  277. gatewaysdk/llm_proxy.py +1742 -0
  278. gatewaysdk/logging.py +370 -0
  279. gatewaysdk/memory.py +278 -0
  280. gatewaysdk/personas.py +186 -0
  281. gatewaysdk/platform/__init__.py +17 -0
  282. gatewaysdk/platform/builder.py +221 -0
  283. gatewaysdk/platform/compatibility.py +67 -0
  284. gatewaysdk/platform/manifest.py +185 -0
  285. gatewaysdk/platform/orchestrator.py +901 -0
  286. gatewaysdk/platform/registry.py +122 -0
  287. gatewaysdk/platform/worker.py +864 -0
  288. gatewaysdk/replay/__init__.py +1032 -0
  289. gatewaysdk/replay/pytest.py +56 -0
  290. gatewaysdk/reward.py +7 -0
  291. gatewaysdk/run.py +1781 -0
  292. gatewaysdk/runner/__init__.py +11 -0
  293. gatewaysdk/runner/agent.py +878 -0
  294. gatewaysdk/runner/base.py +182 -0
  295. gatewaysdk/runner/legacy.py +309 -0
  296. gatewaysdk/security.py +700 -0
  297. gatewaysdk/semconv.py +170 -0
  298. gatewaysdk/server.py +399 -0
  299. gatewaysdk/sessions.py +282 -0
  300. gatewaysdk/store/__init__.py +45 -0
  301. gatewaysdk/store/base.py +908 -0
  302. gatewaysdk/store/client_server.py +2093 -0
  303. gatewaysdk/store/collection/__init__.py +30 -0
  304. gatewaysdk/store/collection/base.py +587 -0
  305. gatewaysdk/store/collection/memory.py +970 -0
  306. gatewaysdk/store/collection/mongo.py +1412 -0
  307. gatewaysdk/store/collection_based.py +1823 -0
  308. gatewaysdk/store/gateway.py +983 -0
  309. gatewaysdk/store/gateway_listener.py +465 -0
  310. gatewaysdk/store/listener.py +58 -0
  311. gatewaysdk/store/memory.py +396 -0
  312. gatewaysdk/store/mongo.py +165 -0
  313. gatewaysdk/store/redis_stream.py +517 -0
  314. gatewaysdk/store/sqlite.py +3 -0
  315. gatewaysdk/store/threading.py +370 -0
  316. gatewaysdk/store/utils.py +142 -0
  317. gatewaysdk/tracer/__init__.py +14 -0
  318. gatewaysdk/tracer/base.py +286 -0
  319. gatewaysdk/tracer/dummy.py +106 -0
  320. gatewaysdk/tracer/otel.py +559 -0
  321. gatewaysdk/tracing/__init__.py +110 -0
  322. gatewaysdk/tracing/api.py +808 -0
  323. gatewaysdk/tracing/attributes.py +9 -0
  324. gatewaysdk/tracing/context.py +272 -0
  325. gatewaysdk/tracing/exporters/__init__.py +10 -0
  326. gatewaysdk/tracing/exporters/gateway.py +228 -0
  327. gatewaysdk/tracing/identity.py +288 -0
  328. gatewaysdk/tracing/init.py +620 -0
  329. gatewaysdk/tracing/instrumentors/__init__.py +15 -0
  330. gatewaysdk/tracing/instrumentors/claude_agent_sdk.py +766 -0
  331. gatewaysdk/tracing/instrumentors/instrumentation_principles.md +294 -0
  332. gatewaysdk/tracing/instrumentors/registry.py +352 -0
  333. gatewaysdk/tracing/mapping.py +729 -0
  334. gatewaysdk/tracing/processors.py +393 -0
  335. gatewaysdk/tracing/push.py +617 -0
  336. gatewaysdk/tracing/push_models.py +247 -0
  337. gatewaysdk/tracing/semconv.py +294 -0
  338. gatewaysdk/tracing/span_builder.py +356 -0
  339. gatewaysdk/trainer/__init__.py +6 -0
  340. gatewaysdk/trainer/init_utils.py +263 -0
  341. gatewaysdk/trainer/legacy.py +359 -0
  342. gatewaysdk/trainer/registry.py +12 -0
  343. gatewaysdk/trainer/trainer.py +638 -0
  344. gatewaysdk/types/__init__.py +63 -0
  345. gatewaysdk/types/core.py +556 -0
  346. gatewaysdk/types/resources.py +204 -0
  347. gatewaysdk/types/tracer.py +515 -0
  348. gatewaysdk/types/tracing.py +162 -0
  349. gatewaysdk/users.py +251 -0
  350. gatewaysdk/utils/__init__.py +1 -0
  351. gatewaysdk/utils/id.py +18 -0
  352. gatewaysdk/utils/metrics.py +1025 -0
  353. gatewaysdk/utils/otel.py +550 -0
  354. gatewaysdk/utils/otlp.py +556 -0
  355. gatewaysdk/utils/redact.py +22 -0
  356. gatewaysdk/utils/server_launcher.py +1045 -0
  357. gatewaysdk/utils/system_snapshot.py +90 -0
  358. gatewaysdk/verl/__init__.py +8 -0
  359. gatewaysdk/verl/__main__.py +6 -0
  360. gatewaysdk/verl/async_server.py +46 -0
  361. gatewaysdk/verl/config.yaml +27 -0
  362. gatewaysdk/verl/daemon.py +1154 -0
  363. gatewaysdk/verl/dataset.py +44 -0
  364. gatewaysdk/verl/entrypoint.py +248 -0
  365. gatewaysdk/verl/trainer.py +549 -0
  366. gatewaysdk/world_browser.py +748 -0
  367. gatewaysdk/world_data.py +453 -0
  368. gatewaysdk/world_sessions.py +978 -0
  369. gatewaysdk/world_tasks.py +285 -0
  370. gatewaysdk/world_tools.py +115 -0
  371. gatewaysdk/worlds.py +639 -0
  372. gatewaysdk-0.3.2.dist-info/METADATA +236 -0
  373. gatewaysdk-0.3.2.dist-info/RECORD +376 -0
  374. gatewaysdk-0.3.2.dist-info/WHEEL +4 -0
  375. gatewaysdk-0.3.2.dist-info/entry_points.txt +5 -0
  376. gatewaysdk-0.3.2.dist-info/licenses/LICENSE +19 -0
@@ -0,0 +1,755 @@
1
+ """Rows in bulk: streamed from files, cut into chunks, checked and applied as a batch.
2
+
3
+ The runtime side of world data ingestion at scale. :func:`iter_rows` streams the
4
+ three rows-file shapes (JSON object of entity to rows, tagged JSONL, bare rows with
5
+ an entity) without loading a file; :func:`chunk_rows` cuts a row stream into gzipped
6
+ JSONL chunks on row boundaries; :func:`validate_chunks` checks every row's shape
7
+ against the contract; :func:`build_batch` applies chunks onto a parent snapshot in
8
+ dependency order and saves the new snapshot. Refusals everywhere are the one record
9
+ of the contract (section 2)::
10
+
11
+ {"chunk": 0, "index": 17, "entity": "users", "primaryKey": {"id": "u17"},
12
+ "field": "email", "pointer": "/users/17/email", "detail": "..."}
13
+
14
+ Standard library only, so the bridge and the api bundle can load it.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import gzip
20
+ import hashlib
21
+ import io
22
+ import json
23
+ import sys
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import IO, Any, Iterable, Iterator
27
+
28
+ from .compiler import Contract, quote
29
+ from .store import SQLiteStore
30
+ from .validation import ContractError, fail, validate
31
+
32
+ CHUNK_BYTES = 64 * 1024 * 1024
33
+ REPORT_CAP = 10_000
34
+ _READ = 256 * 1024
35
+ #: A first line longer than this is a one-line document, never a JSONL row.
36
+ _LINE_CAP = 1024 * 1024
37
+
38
+
39
+ def encode_row(value: Any) -> str:
40
+ """One JSONL line, the exact serialisation every client must match: compact
41
+ separators, non-ASCII kept as is, key order kept as given."""
42
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
43
+
44
+
45
+ # ---------------------------------------------------------------- streaming rows
46
+
47
+
48
+ class _Reader:
49
+ """A text stream with a growing buffer, for decoding JSON values one at a time.
50
+
51
+ The buffer only shrinks on ``compact()``, so a caller may note a position,
52
+ read ahead, and come back to it."""
53
+
54
+ def __init__(self, stream: IO[str]):
55
+ self._stream = stream
56
+ self.buffer = ""
57
+ self.pos = 0
58
+ self.eof = False
59
+
60
+ def fill(self) -> bool:
61
+ if self.eof:
62
+ return False
63
+ more = self._stream.read(_READ)
64
+ if not more:
65
+ self.eof = True
66
+ return False
67
+ self.buffer += more
68
+ return True
69
+
70
+ def compact(self) -> None:
71
+ self.buffer = self.buffer[self.pos :]
72
+ self.pos = 0
73
+
74
+ def peek(self) -> str | None:
75
+ """The next non-whitespace character, or None at the end."""
76
+ while True:
77
+ while self.pos < len(self.buffer) and self.buffer[self.pos] in " \t\r\n\ufeff":
78
+ self.pos += 1
79
+ if self.pos < len(self.buffer):
80
+ return self.buffer[self.pos]
81
+ if not self.fill():
82
+ return None
83
+
84
+ def take(self, char: str, where: str) -> None:
85
+ if self.peek() != char:
86
+ fail("rows", f"{where}: expected {char!r} in the JSON document", where)
87
+ self.pos += 1
88
+
89
+ def value(self, where: str) -> Any:
90
+ """Decode the next JSON value, reading more of the stream as needed."""
91
+ decoder = json.JSONDecoder()
92
+ self.peek()
93
+ while True:
94
+ try:
95
+ parsed, end = decoder.raw_decode(self.buffer, self.pos)
96
+ except ValueError:
97
+ if self.fill():
98
+ continue
99
+ fail("rows", f"{where}: not valid JSON", where)
100
+ if end == len(self.buffer) and self.fill():
101
+ # The value may continue (a number, a literal) past the buffer's edge.
102
+ continue
103
+ self.pos = end
104
+ return parsed
105
+
106
+ def line(self, cap: int | None = None) -> str | None:
107
+ """The next line without its newline, or None at the end. With ``cap``, a line
108
+ longer than it is handed back unconsumed (the caller treats it as a document)."""
109
+ while True:
110
+ rest = self.buffer[self.pos :]
111
+ newline = rest.find("\n")
112
+ if newline >= 0:
113
+ self.pos += newline + 1
114
+ return rest[:newline]
115
+ if cap is not None and len(rest) > cap:
116
+ return rest
117
+ if not self.fill():
118
+ break
119
+ rest = self.buffer[self.pos :]
120
+ if not rest:
121
+ return None
122
+ self.pos = len(self.buffer)
123
+ return rest
124
+
125
+
126
+ def _open_text(source: str | Path) -> IO[str]:
127
+ if str(source) == "-":
128
+ return io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8")
129
+ path = Path(source)
130
+ if path.suffix == ".gz":
131
+ return io.TextIOWrapper(gzip.open(path, "rb"), encoding="utf-8")
132
+ return open(path, "r", encoding="utf-8")
133
+
134
+
135
+ def _row(value: Any, where: str) -> dict:
136
+ if not isinstance(value, dict):
137
+ fail("rows", f"{where}: a row must be a JSON object", where)
138
+ return value
139
+
140
+
141
+ def _stream_array(reader: _Reader, where: str) -> Iterator[dict]:
142
+ reader.take("[", where)
143
+ index = 0
144
+ while True:
145
+ if reader.peek() == "]":
146
+ reader.pos += 1
147
+ return
148
+ yield _row(reader.value(f"{where}[{index}]"), f"{where}[{index}]")
149
+ reader.compact()
150
+ index += 1
151
+ if reader.peek() == ",":
152
+ reader.pos += 1
153
+ elif reader.peek() != "]":
154
+ fail("rows", f"{where}: expected ',' or ']' after row {index - 1}", where)
155
+
156
+
157
+ def _stream_document(
158
+ reader: _Reader, where: str, entity: str | None, seen: dict[str, int] | None = None
159
+ ) -> Iterator[tuple[str, dict]]:
160
+ """``{"<entity>": [row, ...], ...}`` streamed one row at a time. ``seen`` collects
161
+ every entity the document names, an empty array included (a replace names it)."""
162
+ reader.take("{", where)
163
+ while True:
164
+ if reader.peek() == "}":
165
+ reader.pos += 1
166
+ return
167
+ name = reader.value(where)
168
+ if not isinstance(name, str):
169
+ fail("rows", f"{where}: object keys must be entity names", where)
170
+ reader.take(":", where)
171
+ if reader.peek() != "[":
172
+ fail("rows", f'{where}: {name} must map to an array of rows (JSONL lines are {{"entity", "row"}})', where)
173
+ if entity is not None and name != entity:
174
+ fail("rows", f"{where} maps entities; drop --entity or make the file a bare array", where)
175
+ if seen is not None:
176
+ seen.setdefault(name, 0)
177
+ for row in _stream_array(reader, f"{where}:{name}"):
178
+ if seen is not None:
179
+ seen[name] += 1
180
+ yield name, row
181
+ if reader.peek() == ",":
182
+ reader.pos += 1
183
+ elif reader.peek() != "}":
184
+ fail("rows", f"{where}: expected ',' or '}}' after {name}", where)
185
+
186
+
187
+ def _stream_jsonl(reader: _Reader, first: str, where: str, entity: str | None) -> Iterator[tuple[str, dict]]:
188
+ number = 0
189
+ line: str | None = first
190
+ while line is not None:
191
+ number += 1
192
+ if line.strip():
193
+ try:
194
+ item = json.loads(line)
195
+ except ValueError:
196
+ fail("rows", f"{where}:{number} is not JSON", where)
197
+ if entity is not None:
198
+ yield entity, _row(item, f"{where}:{number}")
199
+ elif not isinstance(item, dict) or set(item) != {"entity", "row"} or not isinstance(item["entity"], str):
200
+ fail(
201
+ "rows",
202
+ f'{where}:{number}: expected {{"entity": name, "row": {{…}}}} (or pass --entity for bare rows)',
203
+ where,
204
+ )
205
+ else:
206
+ yield item["entity"], _row(item["row"], f"{where}:{number}")
207
+ reader.compact()
208
+ line = reader.line()
209
+
210
+
211
+ def iter_file_rows(
212
+ stream: IO[str], where: str, *, entity: str | None = None, seen: dict[str, int] | None = None
213
+ ) -> Iterator[tuple[str, dict]]:
214
+ """Stream ``(entity, row)`` from one rows file of any of the three shapes. ``seen``,
215
+ when given, collects entity names to row counts, an entity with no rows included."""
216
+ reader = _Reader(stream)
217
+ head = reader.peek()
218
+ if head is None:
219
+ return
220
+ if head == "[":
221
+ if entity is None:
222
+ fail("rows", f"{where} is a bare array; say which entity it holds with --entity", where)
223
+ if seen is not None:
224
+ seen.setdefault(entity, 0)
225
+ for row in _stream_array(reader, where):
226
+ if seen is not None:
227
+ seen[entity] += 1
228
+ yield entity, row
229
+ return
230
+ if head != "{":
231
+ fail("rows", f"{where} is neither a JSON object of entity -> rows nor an array of rows", where)
232
+ start = reader.pos
233
+ first = reader.line(cap=_LINE_CAP)
234
+ parsed: Any = None
235
+ complete = False
236
+ if first is not None and len(first) <= _LINE_CAP:
237
+ try:
238
+ parsed = json.loads(first)
239
+ complete = True
240
+ except ValueError:
241
+ complete = False
242
+ if complete and isinstance(parsed, dict):
243
+ is_row = (entity is None and set(parsed) == {"entity", "row"}) or (
244
+ entity is not None and not (set(parsed) == {entity} and isinstance(parsed[entity], list))
245
+ )
246
+ if is_row:
247
+ for name, row in _stream_jsonl(reader, first, where, entity):
248
+ if seen is not None:
249
+ seen[name] = seen.get(name, 0) + 1
250
+ yield name, row
251
+ return
252
+ # A document: rewind to its first character and stream it.
253
+ reader.pos = start
254
+ yield from _stream_document(reader, where, entity, seen)
255
+
256
+
257
+ def iter_rows(
258
+ sources: Iterable[str | Path], *, entity: str | None = None, seen: dict[str, int] | None = None
259
+ ) -> Iterator[tuple[str, dict]]:
260
+ """Stream ``(entity, row)`` from rows files (``-`` is stdin, ``.gz`` is gunzipped)
261
+ in order, never holding a whole file."""
262
+ for source in sources:
263
+ where = str(source)
264
+ try:
265
+ stream = _open_text(source)
266
+ except OSError:
267
+ fail("rows", f"Cannot read {where}", where)
268
+ with stream:
269
+ try:
270
+ yield from iter_file_rows(stream, where, entity=entity, seen=seen)
271
+ except UnicodeError:
272
+ fail("rows", f"Cannot read {where} as UTF-8 text", where)
273
+
274
+
275
+ def count_rows(sources: Iterable[str | Path], *, entity: str | None = None, cap: int | None = None) -> int:
276
+ """How many rows the files hold, stopping early past ``cap``."""
277
+ total = 0
278
+ for _ in iter_rows(sources, entity=entity):
279
+ total += 1
280
+ if cap is not None and total > cap:
281
+ break
282
+ return total
283
+
284
+
285
+ # ---------------------------------------------------------------- chunks
286
+
287
+
288
+ @dataclass
289
+ class Chunk:
290
+ ordinal: int
291
+ path: Path
292
+ digest: str # sha256 of the gzipped bytes: the chunk's address
293
+ bytes: int # gzipped size
294
+ rows: int
295
+ entity: str | None
296
+ content_bytes: int # the JSONL before gzip
297
+ content_sha256: str
298
+ first_row: int # index of the chunk's first row in the input stream
299
+
300
+ def manifest_entry(self) -> dict:
301
+ return {
302
+ "ordinal": self.ordinal,
303
+ "digest": self.digest,
304
+ "bytes": self.bytes,
305
+ "rows": self.rows,
306
+ "entity": self.entity,
307
+ "chunkId": None,
308
+ "uploaded": False,
309
+ }
310
+
311
+
312
+ class _ChunkWriter:
313
+ def __init__(self, path: Path, ordinal: int, entity: str | None, first_row: int):
314
+ self.path = path
315
+ self.ordinal = ordinal
316
+ self.entity = entity
317
+ self.first_row = first_row
318
+ self.rows = 0
319
+ self.content_bytes = 0
320
+ self._hash = hashlib.sha256()
321
+ self._raw = open(path, "wb")
322
+ self._gz = gzip.GzipFile(fileobj=self._raw, mode="wb", mtime=0)
323
+
324
+ def write(self, line: bytes) -> None:
325
+ self._gz.write(line)
326
+ self._hash.update(line)
327
+ self.rows += 1
328
+ self.content_bytes += len(line)
329
+
330
+ def close(self) -> Chunk:
331
+ self._gz.close()
332
+ self._raw.close()
333
+ digest = hashlib.sha256()
334
+ with open(self.path, "rb") as stream:
335
+ for block in iter(lambda: stream.read(_READ), b""):
336
+ digest.update(block)
337
+ return Chunk(
338
+ ordinal=self.ordinal,
339
+ path=self.path,
340
+ digest=digest.hexdigest(),
341
+ bytes=self.path.stat().st_size,
342
+ rows=self.rows,
343
+ entity=self.entity,
344
+ content_bytes=self.content_bytes,
345
+ content_sha256=self._hash.hexdigest(),
346
+ first_row=self.first_row,
347
+ )
348
+
349
+
350
+ def chunk_rows(
351
+ rows: Iterable[tuple[str, dict]],
352
+ workdir: str | Path,
353
+ *,
354
+ chunk_bytes: int = CHUNK_BYTES,
355
+ chunk_rows: int | None = None,
356
+ entity: str | None = None,
357
+ ) -> Iterator[Chunk]:
358
+ """Cut a row stream into gzipped JSONL chunk files under ``workdir``.
359
+
360
+ Lines are tagged ``{"entity", "row"}`` unless ``entity`` names the one entity
361
+ every row belongs to, in which case they are bare rows. A chunk closes when the
362
+ next line would take it past ``chunk_bytes`` of JSONL (never mid-row; a single
363
+ row larger than the bound is a chunk of its own) or when it holds ``chunk_rows``
364
+ rows. Chunks are yielded as they close, so one chunk is on disk at a time."""
365
+ workdir = Path(workdir)
366
+ workdir.mkdir(parents=True, exist_ok=True)
367
+ if chunk_bytes < 1 or (chunk_rows is not None and chunk_rows < 1):
368
+ fail("chunk", "chunk bounds must be positive", "/chunk")
369
+ writer: _ChunkWriter | None = None
370
+ ordinal = 0
371
+ seen = 0
372
+ for name, row in rows:
373
+ if entity is not None and name != entity:
374
+ fail("rows", f"Row {seen} belongs to {name}; a chunk declared for {entity} holds only its rows", "/rows")
375
+ line = (encode_row(row) if entity is not None else encode_row({"entity": name, "row": row})) + "\n"
376
+ encoded = line.encode("utf-8")
377
+ if writer is not None and (
378
+ (chunk_rows is not None and writer.rows >= chunk_rows)
379
+ or (writer.content_bytes > 0 and writer.content_bytes + len(encoded) > chunk_bytes)
380
+ ):
381
+ yield writer.close()
382
+ writer = None
383
+ if writer is None:
384
+ writer = _ChunkWriter(workdir / f"chunk-{ordinal:05d}.jsonl.gz", ordinal, entity, seen)
385
+ ordinal += 1
386
+ writer.write(encoded)
387
+ seen += 1
388
+ if writer is not None:
389
+ yield writer.close()
390
+
391
+
392
+ def iter_chunk_rows(path: str | Path, entity: str | None = None) -> Iterator[tuple[str, dict]]:
393
+ """Stream ``(entity, row)`` from one chunk file (gzip over JSONL; a plain file
394
+ is read as is), tagged lines or bare rows of the declared entity."""
395
+ path = Path(path)
396
+ with open(path, "rb") as probe:
397
+ compressed = probe.read(2) == b"\x1f\x8b"
398
+ opener = gzip.open if compressed else open
399
+ with opener(path, "rt", encoding="utf-8") as stream: # type: ignore[call-overload]
400
+ reader = _Reader(stream)
401
+ first = reader.line()
402
+ if first is not None:
403
+ yield from _stream_jsonl(reader, first, str(path), entity)
404
+
405
+
406
+ # ---------------------------------------------------------------- refusals
407
+
408
+
409
+ def _field_of(error: ContractError) -> str | None:
410
+ path = error.detail.get("path") or ""
411
+ return path[1:] if path.startswith("/") and len(path) > 1 else None
412
+
413
+
414
+ def refusal(
415
+ chunk: int | None,
416
+ index: int | None,
417
+ entity: str | None,
418
+ row: Any,
419
+ field_name: str | None,
420
+ detail: str,
421
+ contract: Contract | None = None,
422
+ ) -> dict:
423
+ """One refusal record. ``primaryKey`` is the row's key fields when it carries
424
+ them all, ``field`` the property refused (None for a row-level refusal)."""
425
+ key = None
426
+ if contract is not None and isinstance(row, dict) and entity in contract.entities:
427
+ fields = contract.entities[entity]["primary_key"]
428
+ if all(name in row for name in fields):
429
+ key = {name: row[name] for name in fields}
430
+ pointer = "/" + (entity or "")
431
+ if index is not None:
432
+ pointer += f"/{index}"
433
+ if field_name:
434
+ pointer += f"/{field_name}"
435
+ return {
436
+ "chunk": chunk,
437
+ "index": index,
438
+ "entity": entity,
439
+ "primaryKey": key,
440
+ "field": field_name,
441
+ "pointer": pointer,
442
+ "detail": detail,
443
+ }
444
+
445
+
446
+ def shape_refusal(contract: Contract, chunk: int, index: int, entity: str, row: Any) -> dict | None:
447
+ """A row's shape against the contract: type, bounds, required, unknown property
448
+ and primary key present. No reference checks. None when the row conforms."""
449
+ if entity not in contract.entities:
450
+ return refusal(
451
+ chunk,
452
+ index,
453
+ entity,
454
+ row,
455
+ None,
456
+ f"Unknown entity {entity}; the world has {', '.join(sorted(contract.entities))}",
457
+ )
458
+ if not isinstance(row, dict):
459
+ return refusal(chunk, index, entity, row, None, "A row must be an object")
460
+ spec = contract.entities[entity]
461
+ for name in spec["primary_key"]:
462
+ if name not in row:
463
+ return refusal(chunk, index, entity, row, name, f"Row is missing primary key field {name!r}", contract)
464
+ try:
465
+ validate(spec["schema"], row, location=f"{spec['location']}/schema")
466
+ except ContractError as error:
467
+ return refusal(chunk, index, entity, row, _field_of(error), error.detail["message"], contract)
468
+ return None
469
+
470
+
471
+ def write_report(records: Iterable[dict], destination: IO[str], *, cap: int = REPORT_CAP) -> int:
472
+ """Refusal records as JSONL, at most ``cap`` of them followed by
473
+ ``{"truncated": true, "total": N}`` when there were more. Returns the total."""
474
+ total = 0
475
+ for record in records:
476
+ total += 1
477
+ if total <= cap:
478
+ destination.write(encode_row(record) + "\n")
479
+ if total > cap:
480
+ destination.write(encode_row({"truncated": True, "total": total}) + "\n")
481
+ return total
482
+
483
+
484
+ # ---------------------------------------------------------------- batches
485
+
486
+
487
+ @dataclass
488
+ class ChunkSpec:
489
+ ordinal: int
490
+ path: Path
491
+ entity: str | None = None
492
+
493
+
494
+ @dataclass
495
+ class _Staged:
496
+ """The valid rows of a batch after the in-batch merge, keyed the way the store keys."""
497
+
498
+ rows: dict[str, dict[str, dict]] = field(default_factory=dict) # entity -> key -> row
499
+ origin: dict[str, dict[str, tuple[int, int]]] = field(default_factory=dict) # entity -> key -> (chunk, index)
500
+ refusals: list[dict] = field(default_factory=list)
501
+ read: int = 0
502
+
503
+
504
+ def dependency_order(contract: Contract) -> list[str]:
505
+ """Entities with the targets of relationships first; a cycle keeps name order."""
506
+ remaining = dict(sorted(contract.entities.items()))
507
+ ordered: list[str] = []
508
+ while remaining:
509
+ ready = [
510
+ name
511
+ for name, entity in remaining.items()
512
+ if all(rel["target"] == name or rel["target"] not in remaining for rel in entity["relationships"])
513
+ ]
514
+ if not ready:
515
+ ready = [next(iter(remaining))]
516
+ for name in ready:
517
+ ordered.append(name)
518
+ del remaining[name]
519
+ return ordered
520
+
521
+
522
+ def _stage(
523
+ contract: Contract,
524
+ store: SQLiteStore,
525
+ chunks: Iterable[ChunkSpec],
526
+ merge: str,
527
+ staged: _Staged | None = None,
528
+ ) -> _Staged:
529
+ """Shape-check every row of the chunks and merge them by primary key."""
530
+ staged = staged or _Staged()
531
+ for chunk in chunks:
532
+ for index, (entity, row) in enumerate(iter_chunk_rows(chunk.path, chunk.entity)):
533
+ staged.read += 1
534
+ refused = shape_refusal(contract, chunk.ordinal, index, entity, row)
535
+ if refused is not None:
536
+ staged.refusals.append(refused)
537
+ continue
538
+ key = store._key(entity, row)
539
+ held = staged.rows.setdefault(entity, {})
540
+ origin = staged.origin.setdefault(entity, {})
541
+ if key in held and merge == "fill":
542
+ first = held[key]
543
+ for name, value in row.items():
544
+ if name not in first:
545
+ first[name] = value
546
+ continue
547
+ held[key] = row
548
+ origin[key] = (chunk.ordinal, index)
549
+ return staged
550
+
551
+
552
+ def _reference_refusals(contract: Contract, store: SQLiteStore, staged: _Staged, mode: str) -> list[dict]:
553
+ """Every row whose relationship points at a key neither the parent snapshot
554
+ (minus replaced entities) nor the batch holds."""
555
+ refusals: list[dict] = []
556
+ known: dict[str, set[str]] = {}
557
+
558
+ def keys_of(target: str) -> set[str]:
559
+ if target not in known:
560
+ held = set(staged.rows.get(target, {}))
561
+ if not (mode == "replace" and target in staged.rows):
562
+ with store._trusted():
563
+ held |= {row[0] for row in store._conn.execute(f"SELECT _key FROM {quote(target)}")}
564
+ known[target] = held
565
+ return known[target]
566
+
567
+ for entity, rows in staged.rows.items():
568
+ relationships = contract.entities[entity]["relationships"]
569
+ if not relationships:
570
+ continue
571
+ dropped = []
572
+ for key, row in rows.items():
573
+ for rel in relationships:
574
+ values = [row.get(name) for name in rel["fields"]]
575
+ if all(value is None for value in values):
576
+ continue
577
+ chunk, index = staged.origin[entity][key]
578
+ if any(value is None for value in values):
579
+ refusals.append(
580
+ refusal(
581
+ chunk,
582
+ index,
583
+ entity,
584
+ row,
585
+ rel["fields"][0],
586
+ "Composite references must be wholly present or wholly absent/null",
587
+ contract,
588
+ )
589
+ )
590
+ dropped.append(key)
591
+ break
592
+ target = rel["target"]
593
+ try:
594
+ wanted = store._key(target, dict(zip(contract.entities[target]["primary_key"], values)))
595
+ except ContractError as error:
596
+ refusals.append(
597
+ refusal(chunk, index, entity, row, rel["fields"][0], error.detail["message"], contract)
598
+ )
599
+ dropped.append(key)
600
+ break
601
+ if wanted not in keys_of(target):
602
+ refusals.append(
603
+ refusal(
604
+ chunk,
605
+ index,
606
+ entity,
607
+ row,
608
+ rel["fields"][0],
609
+ f"Relationship target does not exist in {target}",
610
+ contract,
611
+ )
612
+ )
613
+ dropped.append(key)
614
+ break
615
+ for key in dropped:
616
+ del rows[key]
617
+ return refusals
618
+
619
+
620
+ def _apply(
621
+ contract: Contract, store: SQLiteStore, staged: _Staged, mode: str, batch_id: str
622
+ ) -> tuple[int, dict | None]:
623
+ """Apply the staged rows as one batch; the count applied, or the store's refusal."""
624
+ from .data import seed_batch
625
+
626
+ ordered = {name: list(staged.rows[name].values()) for name in dependency_order(contract) if name in staged.rows}
627
+ batch = seed_batch(store, contract, ordered, mode, batch_id=batch_id)
628
+ origins = {id(row): staged.origin[entity][key] for entity, rows in staged.rows.items() for key, row in rows.items()}
629
+ try:
630
+ store.apply_batch(batch)
631
+ except ContractError as error:
632
+ detail = error.detail
633
+ mutation = None
634
+ index = detail.get("index")
635
+ if isinstance(index, int) and 0 <= index < len(batch["mutations"]):
636
+ mutation = batch["mutations"][index]
637
+ origin = origins.get(id(mutation.get("value"))) if mutation else None
638
+ entity = detail.get("entity") if isinstance(detail.get("entity"), str) else None
639
+ row = mutation.get("value") if mutation else None
640
+ chunk, position = origin if origin else (None, None)
641
+ return 0, refusal(chunk, position, entity, row, _field_of(error), detail["message"], contract)
642
+ applied = sum(1 for mutation in batch["mutations"] if mutation["op"] != "delete")
643
+ return applied, None
644
+
645
+
646
+ def validate_chunks(contract: Contract, chunks: list[ChunkSpec]) -> dict:
647
+ """``validate-rows``: shape-only, every row of every chunk."""
648
+ rows = 0
649
+ refusals: list[dict] = []
650
+ entities: dict[str, int] = {}
651
+ for chunk in chunks:
652
+ for index, (entity, row) in enumerate(iter_chunk_rows(chunk.path, chunk.entity)):
653
+ rows += 1
654
+ refused = shape_refusal(contract, chunk.ordinal, index, entity, row)
655
+ if refused is not None:
656
+ refusals.append(refused)
657
+ continue
658
+ entities[entity] = entities.get(entity, 0) + 1
659
+ return {"rows": rows, "refusals": refusals, "entities": entities}
660
+
661
+
662
+ def build_batch(
663
+ contract: Contract,
664
+ parent: Path,
665
+ chunks: list[ChunkSpec],
666
+ *,
667
+ mode: str = "append",
668
+ merge: str = "upsert",
669
+ atomic: bool = True,
670
+ batch_id: str,
671
+ parent_digest: str,
672
+ dry_run: bool = False,
673
+ ) -> dict:
674
+ """Apply ``chunks`` onto the snapshot at ``parent`` (a private copy: it is written
675
+ in place, so the caller passes the file the result should be) and describe the
676
+ result. Atomic: one transaction, any refusal means nothing applied and every
677
+ refusal found. Per chunk: one transaction per chunk, a refused chunk is left out
678
+ and the rest applied. ``dry_run`` finds the refusals and applies nothing."""
679
+ from .snapshot import batch_digest, file_sha256, write_sidecar, contract_hash
680
+
681
+ if mode not in ("append", "replace"):
682
+ fail("mode", "mode must be append or replace", "/mode")
683
+ if merge not in ("upsert", "fill"):
684
+ fail("merge", "merge must be upsert or fill", "/merge")
685
+ refusals: list[dict] = []
686
+ applied_rows = 0
687
+ applied_chunks: list[int] = []
688
+ applied_digests: list[str] = []
689
+ counts: dict[str, int] = {}
690
+ with SQLiteStore.open(parent, contract, audit=False) as store:
691
+ if atomic:
692
+ staged = _stage(contract, store, chunks, merge)
693
+ refusals.extend(staged.refusals)
694
+ refusals.extend(_reference_refusals(contract, store, staged, mode))
695
+ applied, refused = _apply(contract, store, staged, mode, batch_id)
696
+ if refused is not None:
697
+ refusals.append(refused)
698
+ if not refusals and not dry_run:
699
+ applied_rows = applied
700
+ applied_chunks = [chunk.ordinal for chunk in chunks]
701
+ applied_digests = [file_sha256(chunk.path) for chunk in chunks]
702
+ else:
703
+ for chunk in chunks:
704
+ staged = _stage(contract, store, [chunk], merge)
705
+ found = list(staged.refusals) + _reference_refusals(contract, store, staged, mode)
706
+ applied, refused = _apply(contract, store, staged, mode, f"{batch_id}/{chunk.ordinal}")
707
+ if refused is not None:
708
+ found.append(refused)
709
+ refusals.extend(found)
710
+ if found and not dry_run:
711
+ # The store rolled the chunk's transaction back; the rows it did
712
+ # accept are gone with it, so the chunk is refused as a whole.
713
+ continue
714
+ if not dry_run:
715
+ applied_rows += applied
716
+ applied_chunks.append(chunk.ordinal)
717
+ applied_digests.append(file_sha256(chunk.path))
718
+ counts = store.counts()
719
+ digest = batch_digest(parent_digest, batch_id, applied_digests)
720
+ result: dict[str, Any] = {
721
+ "contractHash": contract_hash(contract),
722
+ "batchDigest": digest,
723
+ "entityCounts": counts,
724
+ "appliedRows": applied_rows,
725
+ "refusals": refusals[:REPORT_CAP],
726
+ "refusalCount": len(refusals),
727
+ "appliedChunks": applied_chunks,
728
+ }
729
+ if dry_run or (atomic and refusals):
730
+ parent.unlink(missing_ok=True)
731
+ return result
732
+ write_sidecar(parent, result["contractHash"], digest, counts)
733
+ result["snapshot"] = parent
734
+ result["fileSha256"] = file_sha256(parent)
735
+ return result
736
+
737
+
738
+ __all__ = [
739
+ "CHUNK_BYTES",
740
+ "REPORT_CAP",
741
+ "Chunk",
742
+ "ChunkSpec",
743
+ "build_batch",
744
+ "chunk_rows",
745
+ "count_rows",
746
+ "dependency_order",
747
+ "encode_row",
748
+ "iter_chunk_rows",
749
+ "iter_file_rows",
750
+ "iter_rows",
751
+ "refusal",
752
+ "shape_refusal",
753
+ "validate_chunks",
754
+ "write_report",
755
+ ]