UncountablePythonSDK 0.0.8__py3-none-any.whl → 0.0.92__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.

Potentially problematic release.


This version of UncountablePythonSDK might be problematic. Click here for more details.

Files changed (312) hide show
  1. UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
  2. UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
  3. {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
  4. {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
  5. docs/.gitignore +1 -0
  6. docs/conf.py +57 -0
  7. docs/index.md +13 -0
  8. docs/justfile +12 -0
  9. docs/quickstart.md +19 -0
  10. docs/requirements.txt +7 -0
  11. docs/static/favicons/android-chrome-192x192.png +0 -0
  12. docs/static/favicons/android-chrome-512x512.png +0 -0
  13. docs/static/favicons/apple-touch-icon.png +0 -0
  14. docs/static/favicons/browserconfig.xml +9 -0
  15. docs/static/favicons/favicon-16x16.png +0 -0
  16. docs/static/favicons/favicon-32x32.png +0 -0
  17. docs/static/favicons/manifest.json +18 -0
  18. docs/static/favicons/mstile-150x150.png +0 -0
  19. docs/static/favicons/safari-pinned-tab.svg +32 -0
  20. docs/static/logo_blue.png +0 -0
  21. examples/async_batch.py +35 -0
  22. examples/create_entity.py +22 -17
  23. examples/download_files.py +26 -0
  24. examples/edit_recipe_inputs.py +50 -0
  25. examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
  26. examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
  27. examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
  28. examples/integration-server/pyproject.toml +224 -0
  29. examples/invoke_uploader.py +26 -0
  30. examples/set_recipe_metadata_file.py +40 -0
  31. examples/set_recipe_output_file_sdk.py +26 -0
  32. examples/upload_files.py +18 -0
  33. pkgs/argument_parser/__init__.py +5 -0
  34. pkgs/argument_parser/_is_enum.py +1 -6
  35. pkgs/argument_parser/argument_parser.py +232 -76
  36. pkgs/argument_parser/case_convert.py +4 -3
  37. pkgs/filesystem_utils/__init__.py +20 -0
  38. pkgs/filesystem_utils/_blob_session.py +137 -0
  39. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  40. pkgs/filesystem_utils/_local_session.py +69 -0
  41. pkgs/filesystem_utils/_s3_session.py +117 -0
  42. pkgs/filesystem_utils/_sftp_session.py +147 -0
  43. pkgs/filesystem_utils/file_type_utils.py +91 -0
  44. pkgs/filesystem_utils/filesystem_session.py +39 -0
  45. pkgs/py.typed +0 -0
  46. pkgs/serialization/__init__.py +8 -1
  47. pkgs/serialization/annotation.py +64 -0
  48. pkgs/serialization/missing_sentry.py +1 -1
  49. pkgs/serialization/opaque_key.py +1 -1
  50. pkgs/serialization/serial_alias.py +47 -0
  51. pkgs/serialization/serial_class.py +65 -50
  52. pkgs/serialization/serial_generic.py +16 -0
  53. pkgs/serialization/serial_union.py +84 -0
  54. pkgs/serialization/yaml.py +57 -0
  55. pkgs/serialization_util/__init__.py +7 -7
  56. pkgs/serialization_util/_get_type_for_serialization.py +1 -3
  57. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  58. pkgs/serialization_util/dataclasses.py +14 -0
  59. pkgs/serialization_util/serialization_helpers.py +116 -74
  60. pkgs/strenum_compat/strenum_compat.py +1 -9
  61. pkgs/type_spec/actions_registry/__init__.py +0 -0
  62. pkgs/type_spec/actions_registry/__main__.py +126 -0
  63. pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
  64. pkgs/type_spec/builder.py +475 -89
  65. pkgs/type_spec/config.py +24 -19
  66. pkgs/type_spec/emit_io_ts.py +5 -2
  67. pkgs/type_spec/emit_open_api.py +266 -32
  68. pkgs/type_spec/emit_open_api_util.py +32 -13
  69. pkgs/type_spec/emit_python.py +599 -151
  70. pkgs/type_spec/emit_typescript.py +74 -273
  71. pkgs/type_spec/emit_typescript_util.py +239 -5
  72. pkgs/type_spec/load_types.py +55 -10
  73. pkgs/type_spec/open_api_util.py +30 -41
  74. pkgs/type_spec/parts/base.py.prepart +4 -3
  75. pkgs/type_spec/type_info/emit_type_info.py +178 -16
  76. pkgs/type_spec/util.py +11 -11
  77. pkgs/type_spec/value_spec/__main__.py +3 -3
  78. pkgs/type_spec/value_spec/convert_type.py +8 -1
  79. pkgs/type_spec/value_spec/emit_python.py +13 -4
  80. uncountable/__init__.py +1 -2
  81. uncountable/core/__init__.py +12 -2
  82. uncountable/core/async_batch.py +37 -0
  83. uncountable/core/client.py +293 -43
  84. uncountable/core/environment.py +41 -0
  85. uncountable/core/file_upload.py +135 -0
  86. uncountable/core/types.py +17 -0
  87. uncountable/integration/__init__.py +0 -0
  88. uncountable/integration/cli.py +49 -0
  89. uncountable/integration/construct_client.py +51 -0
  90. uncountable/integration/cron.py +29 -0
  91. uncountable/integration/db/__init__.py +0 -0
  92. uncountable/integration/db/connect.py +18 -0
  93. uncountable/integration/db/session.py +25 -0
  94. uncountable/integration/entrypoint.py +13 -0
  95. uncountable/integration/executors/__init__.py +0 -0
  96. uncountable/integration/executors/executors.py +148 -0
  97. uncountable/integration/executors/generic_upload_executor.py +284 -0
  98. uncountable/integration/executors/script_executor.py +25 -0
  99. uncountable/integration/job.py +87 -0
  100. uncountable/integration/queue_runner/__init__.py +0 -0
  101. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  102. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  103. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  104. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  105. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  108. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  109. uncountable/integration/queue_runner/command_server/types.py +52 -0
  110. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  111. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  112. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  113. uncountable/integration/queue_runner/datastore/model.py +17 -0
  114. uncountable/integration/queue_runner/job_scheduler.py +163 -0
  115. uncountable/integration/queue_runner/queue_runner.py +26 -0
  116. uncountable/integration/queue_runner/types.py +7 -0
  117. uncountable/integration/queue_runner/worker.py +119 -0
  118. uncountable/integration/scan_profiles.py +67 -0
  119. uncountable/integration/scheduler.py +150 -0
  120. uncountable/integration/secret_retrieval/__init__.py +3 -0
  121. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  122. uncountable/integration/server.py +117 -0
  123. uncountable/integration/telemetry.py +209 -0
  124. uncountable/integration/webhook_server/entrypoint.py +170 -0
  125. uncountable/types/__init__.py +136 -20
  126. uncountable/types/api/batch/execute_batch.py +15 -7
  127. uncountable/types/api/batch/execute_batch_load_async.py +42 -0
  128. uncountable/types/api/chemical/__init__.py +1 -0
  129. uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
  130. uncountable/types/api/entity/create_entities.py +23 -11
  131. uncountable/types/api/entity/create_entity.py +21 -12
  132. uncountable/types/api/entity/get_entities_data.py +18 -8
  133. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  134. uncountable/types/api/entity/list_entities.py +27 -12
  135. uncountable/types/api/entity/lock_entity.py +45 -0
  136. uncountable/types/api/entity/resolve_entity_ids.py +17 -7
  137. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  138. uncountable/types/api/entity/set_values.py +14 -7
  139. uncountable/types/api/entity/transition_entity_phase.py +80 -0
  140. uncountable/types/api/entity/unlock_entity.py +44 -0
  141. uncountable/types/api/equipment/__init__.py +1 -0
  142. uncountable/types/api/equipment/associate_equipment_input.py +44 -0
  143. uncountable/types/api/field_options/__init__.py +1 -0
  144. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  145. uncountable/types/api/files/__init__.py +1 -0
  146. uncountable/types/api/files/download_file.py +77 -0
  147. uncountable/types/api/id_source/__init__.py +1 -0
  148. uncountable/types/api/id_source/list_id_source.py +56 -0
  149. uncountable/types/api/id_source/match_id_source.py +54 -0
  150. uncountable/types/api/input_groups/get_input_group_names.py +16 -6
  151. uncountable/types/api/inputs/create_inputs.py +24 -11
  152. uncountable/types/api/inputs/get_input_data.py +32 -13
  153. uncountable/types/api/inputs/get_input_names.py +18 -8
  154. uncountable/types/api/inputs/get_inputs_data.py +29 -10
  155. uncountable/types/api/inputs/set_input_attribute_values.py +16 -9
  156. uncountable/types/api/inputs/set_input_category.py +44 -0
  157. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  158. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  159. uncountable/types/api/material_families/__init__.py +1 -0
  160. uncountable/types/api/material_families/update_entity_material_families.py +48 -0
  161. uncountable/types/api/outputs/get_output_data.py +32 -16
  162. uncountable/types/api/outputs/get_output_names.py +18 -8
  163. uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
  164. uncountable/types/api/permissions/__init__.py +1 -0
  165. uncountable/types/api/permissions/set_core_permissions.py +105 -0
  166. uncountable/types/api/project/get_projects.py +17 -7
  167. uncountable/types/api/project/get_projects_data.py +21 -11
  168. uncountable/types/api/recipe_links/__init__.py +1 -0
  169. uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
  170. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  171. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +18 -8
  172. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  173. uncountable/types/api/recipes/archive_recipes.py +42 -0
  174. uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
  175. uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
  176. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  177. uncountable/types/api/recipes/create_recipe.py +51 -0
  178. uncountable/types/api/recipes/create_recipes.py +25 -12
  179. uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
  180. uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
  181. uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
  182. uncountable/types/api/recipes/get_curve.py +15 -7
  183. uncountable/types/api/recipes/get_recipe_calculations.py +17 -10
  184. uncountable/types/api/recipes/get_recipe_links.py +13 -6
  185. uncountable/types/api/recipes/get_recipe_names.py +16 -6
  186. uncountable/types/api/recipes/get_recipe_output_metadata.py +14 -7
  187. uncountable/types/api/recipes/get_recipes_data.py +63 -38
  188. uncountable/types/api/recipes/lock_recipes.py +63 -0
  189. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  190. uncountable/types/api/recipes/set_recipe_inputs.py +19 -10
  191. uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
  192. uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
  193. uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
  194. uncountable/types/api/recipes/set_recipe_outputs.py +26 -12
  195. uncountable/types/api/recipes/set_recipe_tags.py +109 -0
  196. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  197. uncountable/types/api/recipes/unlock_recipes.py +50 -0
  198. uncountable/types/api/triggers/__init__.py +1 -0
  199. uncountable/types/api/triggers/run_trigger.py +43 -0
  200. uncountable/types/api/uploader/__init__.py +1 -0
  201. uncountable/types/api/uploader/invoke_uploader.py +47 -0
  202. uncountable/types/async_batch.py +13 -0
  203. uncountable/types/async_batch_processor.py +384 -0
  204. uncountable/types/async_batch_t.py +97 -0
  205. uncountable/types/async_jobs.py +9 -0
  206. uncountable/types/async_jobs_t.py +53 -0
  207. uncountable/types/auth_retrieval.py +12 -0
  208. uncountable/types/auth_retrieval_t.py +75 -0
  209. uncountable/types/base.py +5 -78
  210. uncountable/types/base_t.py +85 -0
  211. uncountable/types/calculations.py +3 -18
  212. uncountable/types/calculations_t.py +27 -0
  213. uncountable/types/chemical_structure.py +8 -0
  214. uncountable/types/chemical_structure_t.py +28 -0
  215. uncountable/types/client_base.py +1093 -54
  216. uncountable/types/client_config.py +8 -0
  217. uncountable/types/client_config_t.py +26 -0
  218. uncountable/types/curves.py +5 -42
  219. uncountable/types/curves_t.py +51 -0
  220. uncountable/types/entity.py +8 -269
  221. uncountable/types/entity_t.py +393 -0
  222. uncountable/types/experiment_groups.py +3 -18
  223. uncountable/types/experiment_groups_t.py +27 -0
  224. uncountable/types/field_values.py +17 -60
  225. uncountable/types/field_values_t.py +204 -0
  226. uncountable/types/fields.py +3 -19
  227. uncountable/types/fields_t.py +28 -0
  228. uncountable/types/generic_upload.py +15 -0
  229. uncountable/types/generic_upload_t.py +119 -0
  230. uncountable/types/id_source.py +12 -0
  231. uncountable/types/id_source_t.py +68 -0
  232. uncountable/types/identifier.py +11 -0
  233. uncountable/types/identifier_t.py +63 -0
  234. uncountable/types/input_attributes.py +3 -24
  235. uncountable/types/input_attributes_t.py +30 -0
  236. uncountable/types/inputs.py +6 -56
  237. uncountable/types/inputs_t.py +83 -0
  238. uncountable/types/integration_server.py +9 -0
  239. uncountable/types/integration_server_t.py +42 -0
  240. uncountable/types/job_definition.py +27 -0
  241. uncountable/types/job_definition_t.py +260 -0
  242. uncountable/types/outputs.py +3 -21
  243. uncountable/types/outputs_t.py +30 -0
  244. uncountable/types/overrides.py +10 -0
  245. uncountable/types/overrides_t.py +49 -0
  246. uncountable/types/permissions.py +8 -0
  247. uncountable/types/permissions_t.py +46 -0
  248. uncountable/types/phases.py +3 -18
  249. uncountable/types/phases_t.py +27 -0
  250. uncountable/types/post_base.py +8 -0
  251. uncountable/types/post_base_t.py +30 -0
  252. uncountable/types/queued_job.py +16 -0
  253. uncountable/types/queued_job_t.py +123 -0
  254. uncountable/types/recipe_identifiers.py +12 -0
  255. uncountable/types/recipe_identifiers_t.py +76 -0
  256. uncountable/types/recipe_inputs.py +9 -0
  257. uncountable/types/recipe_inputs_t.py +30 -0
  258. uncountable/types/recipe_links.py +4 -45
  259. uncountable/types/recipe_links_t.py +54 -0
  260. uncountable/types/recipe_metadata.py +5 -45
  261. uncountable/types/recipe_metadata_t.py +58 -0
  262. uncountable/types/recipe_output_metadata.py +3 -19
  263. uncountable/types/recipe_output_metadata_t.py +28 -0
  264. uncountable/types/recipe_tags.py +3 -18
  265. uncountable/types/recipe_tags_t.py +27 -0
  266. uncountable/types/recipe_workflow_steps.py +14 -0
  267. uncountable/types/recipe_workflow_steps_t.py +95 -0
  268. uncountable/types/recipes.py +8 -0
  269. uncountable/types/recipes_t.py +25 -0
  270. uncountable/types/response.py +3 -20
  271. uncountable/types/response_t.py +26 -0
  272. uncountable/types/secret_retrieval.py +12 -0
  273. uncountable/types/secret_retrieval_t.py +75 -0
  274. uncountable/types/units.py +3 -18
  275. uncountable/types/units_t.py +27 -0
  276. uncountable/types/users.py +3 -19
  277. uncountable/types/users_t.py +28 -0
  278. uncountable/types/webhook_job.py +9 -0
  279. uncountable/types/webhook_job_t.py +37 -0
  280. uncountable/types/workflows.py +4 -27
  281. uncountable/types/workflows_t.py +39 -0
  282. UncountablePythonSDK-0.0.8.dist-info/METADATA +0 -27
  283. UncountablePythonSDK-0.0.8.dist-info/RECORD +0 -134
  284. examples/recipe-import/importer.py +0 -39
  285. type_spec/external/api/batch/execute_batch.yaml +0 -56
  286. type_spec/external/api/entity/create_entities.yaml +0 -33
  287. type_spec/external/api/entity/create_entity.yaml +0 -39
  288. type_spec/external/api/entity/get_entities_data.yaml +0 -29
  289. type_spec/external/api/entity/list_entities.yaml +0 -52
  290. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  291. type_spec/external/api/entity/set_values.yaml +0 -18
  292. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  293. type_spec/external/api/inputs/create_inputs.yaml +0 -48
  294. type_spec/external/api/inputs/get_input_data.yaml +0 -95
  295. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  296. type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
  297. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
  298. type_spec/external/api/outputs/get_output_data.yaml +0 -92
  299. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  300. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  301. type_spec/external/api/project/get_projects.yaml +0 -42
  302. type_spec/external/api/project/get_projects_data.yaml +0 -50
  303. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  304. type_spec/external/api/recipes/create_recipes.yaml +0 -47
  305. type_spec/external/api/recipes/get_curve.yaml +0 -18
  306. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
  307. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  308. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  309. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
  310. type_spec/external/api/recipes/get_recipes_data.yaml +0 -238
  311. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
  312. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
@@ -1,17 +1,20 @@
1
+ import dataclasses
1
2
  import io
2
3
  import os
3
- from dataclasses import dataclass, field
4
4
  from decimal import Decimal
5
- from typing import Any, Optional, Set
5
+ from typing import Any
6
6
 
7
7
  from . import builder, util
8
+ from .builder import EndpointEmitType
8
9
  from .config import PythonConfig
9
10
 
10
11
  INDENT = " "
11
12
  LINE_BREAK = "\n"
12
13
  MODIFY_NOTICE = "# DO NOT MODIFY -- This file is generated by type_spec\n"
13
14
  # Turn excess line length warning and turn off ruff formatting
14
- LINT_HEADER = "# flake8: noqa: F821\n# ruff: noqa: E402\n# fmt: off\n# isort: skip_file\n"
15
+ LINT_HEADER = (
16
+ "# flake8: noqa: F821\n# ruff: noqa: E402 Q003\n# fmt: off\n# isort: skip_file\n"
17
+ )
15
18
  LINT_FOOTER = "# fmt: on\n"
16
19
  ROUTE_NOTICE = """# Routes are generated from $endpoint specifications in the
17
20
  # type_spec API YAML files. Refer to the section on endpoints in the type_spec/README"""
@@ -21,29 +24,44 @@ __all__: list[str] = [
21
24
  """
22
25
  END_ALL_EXPORTS = "]\n"
23
26
 
27
+ ASYNC_BATCH_TYPE_NAMESPACE = builder.SpecNamespace(name="async_batch")
28
+ ASYNC_BATCH_REQUEST_PATH_STYPE = builder.SpecTypeDefnStringEnum(
29
+ namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="AsyncBatchRequestPath"
30
+ )
31
+ ASYNC_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
32
+ namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="AsyncBatchRequest"
33
+ )
34
+ QUEUED_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
35
+ namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="QueuedAsyncBatchRequest"
36
+ )
24
37
 
25
- @dataclass(kw_only=True)
38
+
39
+ @dataclasses.dataclass(kw_only=True)
26
40
  class TrackingContext:
27
- namespace: Optional[builder.SpecNamespace] = None
28
- namespaces: Set[builder.SpecNamespace] = field(default_factory=set)
29
- names: Set[str] = field(default_factory=set)
41
+ namespace: builder.SpecNamespace | None = None
42
+ namespaces: set[builder.SpecNamespace] = dataclasses.field(default_factory=set)
43
+ names: set[str] = dataclasses.field(default_factory=set)
30
44
 
31
45
  use_enum: bool = False
32
46
  use_serial_string_enum: bool = False
33
47
  use_dataclass: bool = False
34
- use_serial_class: bool = False
48
+ use_serial_union: bool = False
49
+ use_serial_alias: bool = False
35
50
  use_missing: bool = False
36
51
  use_opaque_key: bool = False
37
52
 
38
53
 
39
- @dataclass(kw_only=True)
54
+ @dataclasses.dataclass(kw_only=True)
40
55
  class Context(TrackingContext):
41
56
  out: io.StringIO
42
57
  namespace: builder.SpecNamespace
58
+ builder: builder.SpecBuilder
43
59
 
44
60
 
45
61
  def _resolve_namespace_name(namespace: builder.SpecNamespace) -> str:
46
- return namespace.name
62
+ if len(namespace.path) > 1:
63
+ return namespace.name
64
+ return f"{namespace.name}_t"
47
65
 
48
66
 
49
67
  def _resolve_namespace_ref(namespace: builder.SpecNamespace) -> str:
@@ -119,6 +137,9 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
119
137
  # Note that decimal requires the `!decimal 123.12` style notation in the YAML
120
138
  # file since PyYaml parses numbers as float, unfortuantely
121
139
  assert isinstance(value, (Decimal, int))
140
+ if isinstance(value, int):
141
+ # skip quotes for integers
142
+ return f"Decimal({value})"
122
143
  return f'Decimal("{value}")'
123
144
  elif isinstance(stype, builder.SpecTypeInstance):
124
145
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_list):
@@ -191,6 +212,7 @@ def emit_python(builder: builder.SpecBuilder, *, config: PythonConfig) -> None:
191
212
  _emit_api_stubs(builder=builder, config=config)
192
213
  _emit_api_argument_lookup(builder=builder, config=config)
193
214
  _emit_client_class(spec_builder=builder, config=config)
215
+ _emit_async_batch_processor(spec_builder=builder, config=config)
194
216
 
195
217
 
196
218
  def _emit_types_imports(*, out: io.StringIO, ctx: Context) -> None:
@@ -201,9 +223,12 @@ def _emit_types_imports(*, out: io.StringIO, ctx: Context) -> None:
201
223
  if ctx.use_enum:
202
224
  out.write("from pkgs.strenum_compat import StrEnum\n")
203
225
  if ctx.use_dataclass:
204
- out.write("from dataclasses import dataclass\n")
205
- if ctx.use_serial_class:
226
+ out.write("import dataclasses\n")
206
227
  out.write("from pkgs.serialization import serial_class\n")
228
+ if ctx.use_serial_union:
229
+ out.write("from pkgs.serialization import serial_union_annotation\n")
230
+ if ctx.use_serial_alias:
231
+ out.write("from pkgs.serialization import serial_alias_annotation\n")
207
232
  if ctx.use_serial_string_enum:
208
233
  out.write("from pkgs.serialization import serial_string_enum\n")
209
234
  if ctx.use_missing:
@@ -220,7 +245,7 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
220
245
  exports_out = io.StringIO()
221
246
  exports_out.write(START_ALL_EXPORTS)
222
247
 
223
- all_dirs: Set[str] = set()
248
+ all_dirs: set[str] = set()
224
249
 
225
250
  for namespace in sorted(
226
251
  builder.namespaces.values(),
@@ -228,7 +253,7 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
228
253
  ):
229
254
  if (
230
255
  namespace.endpoint is not None
231
- and not namespace.endpoint.is_sdk
256
+ and namespace.endpoint.is_sdk == EndpointEmitType.EMIT_NOTHING
232
257
  and config.sdk_endpoints_only is True
233
258
  ):
234
259
  continue
@@ -236,6 +261,7 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
236
261
  ctx = Context(
237
262
  out=io.StringIO(),
238
263
  namespace=namespace,
264
+ builder=builder,
239
265
  )
240
266
 
241
267
  _emit_namespace(ctx, namespace)
@@ -274,17 +300,22 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
274
300
  full.write(f"# === END section from {namespace.name}.part.py ===\n")
275
301
 
276
302
  basename = "/".join(namespace.path)
277
- filename = f"{config.types_output}/{basename}.py"
303
+ filename = f"{config.types_output}/{basename}{'' if len(namespace.path) > 1 else '_t'}.py"
278
304
  util.rewrite_file(filename, full.getvalue())
279
305
 
306
+ # Deprecated SDK support
307
+ if config.all_named_type_exports and len(namespace.path) == 1:
308
+ compat_out = _create_sdk_compat_namespace(namespace)
309
+ compat_filename = f"{config.types_output}/{basename}.py"
310
+ util.rewrite_file(compat_filename, compat_out.getvalue())
311
+
280
312
  path_to = os.path.dirname(basename)
281
313
  while path_to != "":
282
314
  all_dirs.add(path_to)
283
315
  path_to = os.path.dirname(path_to)
284
316
 
285
- if len(namespace.path) == 1 or (
286
- config.all_named_type_exports and len(namespace.path) > 1
287
- ):
317
+ # Deprecated SDK support
318
+ if config.all_named_type_exports:
288
319
  index_out.write("from ")
289
320
  if len(namespace.path) == 1:
290
321
  index_out.write(".")
@@ -313,6 +344,9 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
313
344
 
314
345
  ENDPOINT_METHOD = "ENDPOINT_METHOD"
315
346
  ENDPOINT_PATH = "ENDPOINT_PATH"
347
+ # will be removed in Q1 2025 when ENDPOINT_PATH is made api_endpoint-agnostic
348
+ # is used when the API call has multiple endpoints for the one endpoint that isn't equal to the top_namespace
349
+ ENDPOINT_PATH_ALTERNATE = "ENDPOINT_PATH_ALTERNATE"
316
350
 
317
351
 
318
352
  def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
@@ -320,9 +354,20 @@ def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
320
354
  if endpoint is not None:
321
355
  ctx.out.write("\n")
322
356
  ctx.out.write(f'{ENDPOINT_METHOD} = "{endpoint.method.upper()}"\n')
323
- ctx.out.write(
324
- f'{ENDPOINT_PATH} = "{endpoint.path_root}/{endpoint.path_dirname}/{endpoint.path_basename}"\n'
325
- )
357
+ for endpoint_specific_path in sorted(
358
+ endpoint.path_per_api_endpoint.values(), key=lambda epf: epf.root
359
+ ):
360
+ endpoint_path_name = ENDPOINT_PATH
361
+
362
+ if (
363
+ len(endpoint.path_per_api_endpoint.keys()) > 1
364
+ and endpoint_specific_path.root != ctx.builder.top_namespace
365
+ ):
366
+ endpoint_path_name = ENDPOINT_PATH_ALTERNATE
367
+ ctx.names.add(ENDPOINT_PATH_ALTERNATE)
368
+ ctx.out.write(
369
+ f'{endpoint_path_name} = "{endpoint_specific_path.path_root}/{endpoint_specific_path.path_dirname}/{endpoint_specific_path.path_basename}"\n'
370
+ )
326
371
 
327
372
  ctx.names.add(ENDPOINT_METHOD)
328
373
  ctx.names.add(ENDPOINT_PATH)
@@ -334,78 +379,243 @@ def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
334
379
  _emit_constant(ctx, sconst)
335
380
 
336
381
 
382
+ def _create_sdk_compat_namespace(namespace: builder.SpecNamespace) -> io.StringIO:
383
+ compat_out = io.StringIO()
384
+ compat_out.write(LINT_HEADER)
385
+ compat_out.write(MODIFY_NOTICE)
386
+ compat_out.write("# Kept only for SDK backwards compatibility\n")
387
+
388
+ # This mostly an prepart import, thus has no high-level knowledge. Since this onl
389
+ # needs backwards copmatibiltiy from when written we can just hardcode what was there
390
+ # (only those in __all__ assuming that worked, which it might not)
391
+ if namespace.name == "base":
392
+ compat_out.write("""
393
+ from .base_t import JsonScalar as JsonScalar
394
+ from .base_t import JsonValue as JsonValue
395
+ from .base_t import ObjectId as ObjectId
396
+ """)
397
+ else:
398
+ for stype in namespace.types.values():
399
+ compat_out.write(
400
+ f"from .{namespace.path[-1]}_t import {stype.name} as {stype.name}\n"
401
+ )
402
+
403
+ compat_out.write(MODIFY_NOTICE)
404
+
405
+ return compat_out
406
+
407
+
337
408
  def _validate_supports_handler_generation(
338
409
  stype: builder.SpecTypeDefn, name: str, supports_inheritance: bool = False
339
410
  ) -> builder.SpecTypeDefnObject:
340
- assert isinstance(
341
- stype, builder.SpecTypeDefnObject
342
- ), f"External api {name} must be an object"
411
+ assert isinstance(stype, builder.SpecTypeDefnObject), (
412
+ f"External api {name} must be an object"
413
+ )
343
414
  if not supports_inheritance:
344
- assert (
345
- stype.base is None or stype.base.is_base
346
- ), f"Inheritance not supported in external api {name}"
415
+ assert stype.base is None or stype.base.is_base, (
416
+ f"Inheritance not supported in external api {name}"
417
+ )
347
418
  return stype
348
419
 
349
420
 
350
- def _emit_endpoint_invocation_function(
351
- ctx: Context, namespace: builder.SpecNamespace
421
+ def _emit_endpoint_invocation_docstring(
422
+ ctx: Context,
423
+ endpoint: builder.SpecEndpoint,
424
+ properties: list[builder.SpecProperty],
352
425
  ) -> None:
353
- endpoint = namespace.endpoint
354
- if endpoint is None:
355
- return
356
- if not endpoint.is_sdk:
426
+ has_argument_desc = any(prop.desc is not None for prop in properties)
427
+ has_endpoint_desc = endpoint.desc
428
+ if not has_argument_desc and not has_endpoint_desc:
357
429
  return
358
430
 
359
- arguments_type = namespace.types["Arguments"]
360
- data_type = namespace.types["Data"]
431
+ FULL_INDENT = INDENT * 2
432
+ ctx.out.write(FULL_INDENT)
433
+ ctx.out.write('"""')
361
434
 
362
- arguments_type = _validate_supports_handler_generation(arguments_type, "arguments")
363
- data_type = _validate_supports_handler_generation(
364
- data_type, "response", supports_inheritance=True
365
- )
435
+ if endpoint.desc is not None and has_endpoint_desc:
436
+ ctx.out.write(f"{endpoint.desc}\n")
437
+ ctx.out.write("\n")
366
438
 
367
- endpoint_method_stype = builder.SpecTypeDefnObject(
368
- namespace=arguments_type.namespace, name=ENDPOINT_METHOD
369
- )
370
- endpoint_path_stype = builder.SpecTypeDefnObject(
371
- namespace=arguments_type.namespace, name=ENDPOINT_PATH
372
- )
439
+ if has_argument_desc:
440
+ for prop in properties:
441
+ if prop.desc:
442
+ ctx.out.write(f"{FULL_INDENT}:param {prop.name}: {prop.desc}\n")
443
+
444
+ ctx.out.write(f'{FULL_INDENT}"""\n')
445
+
446
+
447
+ def _emit_endpoint_invocation_function_signature(
448
+ ctx: Context,
449
+ endpoint: builder.SpecEndpoint,
450
+ arguments_type: builder.SpecTypeDefnObject,
451
+ data_type: builder.SpecTypeDefnObject,
452
+ extra_params: list[builder.SpecProperty] | None = None,
453
+ ) -> None:
454
+ all_arguments = (
455
+ list(arguments_type.properties.values())
456
+ if arguments_type.properties is not None
457
+ else []
458
+ ) + (extra_params if extra_params is not None else [])
373
459
 
374
- has_arguments = (
375
- arguments_type.properties is not None
376
- and len(arguments_type.properties.values()) > 0
377
- )
378
460
  assert endpoint.function is not None
379
461
  function_name = endpoint.function.split(".")[-1]
380
- ctx.out.write("\n")
381
462
  ctx.out.write(
382
463
  f"""
383
464
  def {function_name}(
384
465
  self,\n"""
385
466
  )
386
- if has_arguments:
467
+ if len(all_arguments) > 0:
387
468
  ctx.out.write(f"{INDENT}{INDENT}*,\n")
388
- _emit_type_properties(
469
+ _emit_properties(
389
470
  ctx=ctx,
390
- stype=arguments_type,
471
+ properties=all_arguments,
391
472
  num_indent=2,
392
473
  separator=",\n",
393
474
  class_out=ctx.out,
394
475
  )
395
476
  ctx.out.write(f"{INDENT}) -> {refer_to(ctx=ctx, stype=data_type)}:")
396
-
397
477
  ctx.out.write("\n")
398
- ctx.out.write(f"{INDENT}{INDENT}args = {refer_to(ctx=ctx, stype=arguments_type)}(")
399
- if has_arguments:
400
- assert arguments_type.properties is not None
478
+
479
+ if len(all_arguments) > 0:
480
+ _emit_endpoint_invocation_docstring(
481
+ ctx=ctx, endpoint=endpoint, properties=all_arguments
482
+ )
483
+
484
+
485
+ def _emit_instantiate_type_from_locals(
486
+ ctx: Context, variable_name: str, variable_type: builder.SpecTypeDefnObject
487
+ ) -> None:
488
+ ctx.out.write(
489
+ f"{INDENT}{INDENT}{variable_name} = {refer_to(ctx=ctx, stype=variable_type)}("
490
+ )
491
+ if variable_type.properties is not None and len(variable_type.properties) > 0:
401
492
  ctx.out.write("\n")
402
- for prop in arguments_type.properties.values():
493
+ for prop in variable_type.properties.values():
403
494
  ctx.out.write(f"{INDENT}{INDENT}{INDENT}{prop.name}={prop.name},")
404
495
  ctx.out.write("\n")
405
496
  ctx.out.write(f"{INDENT}{INDENT})")
406
497
  else:
407
498
  ctx.out.write(")")
408
499
 
500
+
501
+ def _emit_async_batch_invocation_function(
502
+ ctx: Context, namespace: builder.SpecNamespace
503
+ ) -> None:
504
+ endpoint = namespace.endpoint
505
+ if endpoint is None:
506
+ return
507
+ if (
508
+ endpoint.async_batch_path is None
509
+ or endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
510
+ ):
511
+ return
512
+
513
+ ctx.out.write("\n")
514
+ arguments_type = namespace.types["Arguments"]
515
+ arguments_type = _validate_supports_handler_generation(arguments_type, "arguments")
516
+ data_type = QUEUED_BATCH_REQUEST_STYPE
517
+
518
+ list_str_params: list[builder.SpecType] = []
519
+ list_str_params.append(
520
+ builder.SpecTypeGenericParameter(
521
+ name="str",
522
+ spec_type_definition=builder.SpecTypeDefnObject(
523
+ namespace=namespace, name=builder.BaseTypeName.s_string, is_base=True
524
+ ),
525
+ )
526
+ )
527
+
528
+ _emit_endpoint_invocation_function_signature(
529
+ ctx=ctx,
530
+ endpoint=endpoint,
531
+ arguments_type=arguments_type,
532
+ data_type=data_type,
533
+ extra_params=[
534
+ builder.SpecProperty(
535
+ name="depends_on",
536
+ extant=builder.PropertyExtant.optional,
537
+ convert_value=builder.PropertyConvertValue.auto,
538
+ name_case=builder.NameCase.convert,
539
+ label="depends_on",
540
+ desc="A list of batch reference keys to process before processing this request",
541
+ spec_type=builder.SpecTypeInstance(
542
+ defn_type=builder.SpecTypeDefnObject(
543
+ name=builder.BaseTypeName.s_list,
544
+ is_base=True,
545
+ namespace=namespace,
546
+ ),
547
+ parameters=list_str_params,
548
+ ),
549
+ )
550
+ ],
551
+ )
552
+
553
+ # Emit function body
554
+ _emit_instantiate_type_from_locals(
555
+ ctx=ctx, variable_name="args", variable_type=arguments_type
556
+ )
557
+
558
+ path = _emit_value(
559
+ ctx=ctx,
560
+ stype=ASYNC_BATCH_REQUEST_PATH_STYPE,
561
+ value=endpoint.async_batch_path,
562
+ )
563
+
564
+ ctx.out.write(
565
+ f"""
566
+ json_data = serialize_for_api(args)
567
+
568
+ batch_reference = str(uuid.uuid4())
569
+
570
+ req = {refer_to(ctx=ctx, stype=ASYNC_BATCH_REQUEST_STYPE)}(
571
+ path={path},
572
+ data=json_data,
573
+ depends_on=depends_on,
574
+ batch_reference=batch_reference,
575
+ )
576
+
577
+ self._enqueue(req)
578
+
579
+ return {refer_to(ctx=ctx, stype=data_type)}(
580
+ path=req.path,
581
+ batch_reference=req.batch_reference,
582
+ )"""
583
+ )
584
+
585
+
586
+ def _emit_endpoint_invocation_function(
587
+ ctx: Context, namespace: builder.SpecNamespace
588
+ ) -> None:
589
+ endpoint = namespace.endpoint
590
+ if endpoint is None:
591
+ return
592
+ if endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT or endpoint.is_beta:
593
+ return
594
+
595
+ ctx.out.write("\n")
596
+ arguments_type = namespace.types["Arguments"]
597
+ data_type = namespace.types["Data"]
598
+ arguments_type = _validate_supports_handler_generation(arguments_type, "arguments")
599
+ data_type = _validate_supports_handler_generation(
600
+ data_type, "response", supports_inheritance=True
601
+ )
602
+
603
+ _emit_endpoint_invocation_function_signature(
604
+ ctx=ctx, endpoint=endpoint, arguments_type=arguments_type, data_type=data_type
605
+ )
606
+
607
+ endpoint_method_stype = builder.SpecTypeDefnObject(
608
+ namespace=arguments_type.namespace, name=ENDPOINT_METHOD
609
+ )
610
+ endpoint_path_stype = builder.SpecTypeDefnObject(
611
+ namespace=arguments_type.namespace, name=ENDPOINT_PATH
612
+ )
613
+
614
+ # Emit function body
615
+ _emit_instantiate_type_from_locals(
616
+ ctx=ctx, variable_name="args", variable_type=arguments_type
617
+ )
618
+
409
619
  ctx.out.write(
410
620
  f"""
411
621
  api_request = APIRequest(
@@ -415,7 +625,6 @@ def _emit_endpoint_invocation_function(
415
625
  )
416
626
  return self.do_request(api_request=api_request, return_type={refer_to(ctx=ctx, stype=data_type)})"""
417
627
  )
418
- ctx.out.write("\n")
419
628
 
420
629
 
421
630
  def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> None:
@@ -433,7 +642,9 @@ def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> No
433
642
  ctx.out.write(f"{INDENT}labels={{\n")
434
643
  for entry in stype.values.values():
435
644
  if entry.label is not None:
436
- ctx.out.write(f'{INDENT}{INDENT}"{entry.value}": "{entry.label}",\n')
645
+ ctx.out.write(
646
+ f'{INDENT}{INDENT}"{entry.value}": "{entry.label}",\n'
647
+ )
437
648
 
438
649
  ctx.out.write(f"{INDENT}}},\n")
439
650
  if need_deprecated:
@@ -464,12 +675,12 @@ def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> No
464
675
  )
465
676
 
466
677
 
467
- @dataclass
678
+ @dataclasses.dataclass
468
679
  class EmittedPropertiesMetadata:
469
- unconverted_keys: Set[str]
470
- unconverted_values: Set[str]
471
- to_string_values: Set[str]
472
- parse_require: Set[str]
680
+ unconverted_keys: set[str]
681
+ unconverted_values: set[str]
682
+ to_string_values: set[str]
683
+ parse_require: set[str]
473
684
 
474
685
 
475
686
  def _emit_type_properties(
@@ -480,16 +691,31 @@ def _emit_type_properties(
480
691
  num_indent: int = 1,
481
692
  separator: str = "\n",
482
693
  ) -> EmittedPropertiesMetadata:
483
- unconverted_keys: Set[str] = set()
484
- unconverted_values: Set[str] = set()
485
- to_string_values: Set[str] = set()
486
- parse_require: Set[str] = set()
694
+ return _emit_properties(
695
+ ctx=ctx,
696
+ class_out=class_out,
697
+ properties=list((stype.properties or {}).values()),
698
+ num_indent=num_indent,
699
+ separator=separator,
700
+ )
701
+
702
+
703
+ def _emit_properties(
704
+ *,
705
+ ctx: Context,
706
+ class_out: io.StringIO,
707
+ properties: list[builder.SpecProperty],
708
+ num_indent: int = 1,
709
+ separator: str = "\n",
710
+ ) -> EmittedPropertiesMetadata:
711
+ unconverted_keys: set[str] = set()
712
+ unconverted_values: set[str] = set()
713
+ to_string_values: set[str] = set()
714
+ parse_require: set[str] = set()
487
715
 
488
- if stype.properties is not None and len(stype.properties) > 0:
716
+ if len(properties) > 0:
489
717
 
490
718
  def write_field(prop: builder.SpecProperty) -> None:
491
- # Checked in outer function, MyPy doens't track the check inside here
492
- assert isinstance(stype, builder.SpecTypeDefn)
493
719
  if prop.name_case == builder.NameCase.preserve:
494
720
  unconverted_keys.add(prop.name)
495
721
  py_name = python_field_name(prop.name, prop.name_case)
@@ -512,20 +738,30 @@ def _emit_type_properties(
512
738
  default = "MISSING_SENTRY"
513
739
  ctx.use_missing = True
514
740
  elif prop.extant == builder.PropertyExtant.optional:
515
- ref_type = f"typing.Optional[{ref_type}]"
741
+ if ref_type != "None":
742
+ ref_type = f"{ref_type} | None"
516
743
  default = "None"
517
744
  elif prop.has_default:
518
745
  default = _emit_value(ctx, prop.spec_type, prop.default)
519
-
746
+ if (
747
+ isinstance(prop.spec_type, builder.SpecTypeInstance)
748
+ and (
749
+ prop.spec_type.defn_type.is_base_type(
750
+ builder.BaseTypeName.s_list
751
+ )
752
+ )
753
+ and default == "[]"
754
+ ):
755
+ default = "dataclasses.field(default_factory=list)"
520
756
  class_out.write(f"{INDENT * num_indent}{py_name}: {ref_type}")
521
757
  if default:
522
758
  class_out.write(f" = {default}")
523
759
  class_out.write(separator)
524
760
 
525
- for prop in stype.properties.values():
761
+ for prop in properties:
526
762
  if prop.extant == builder.PropertyExtant.required:
527
763
  write_field(prop)
528
- for prop in stype.properties.values():
764
+ for prop in properties:
529
765
  if prop.extant != builder.PropertyExtant.required:
530
766
  write_field(prop)
531
767
  else:
@@ -539,6 +775,12 @@ def _emit_type_properties(
539
775
  )
540
776
 
541
777
 
778
+ def _named_type_path(ctx: Context, stype: builder.SpecTypeDefn) -> str:
779
+ parts = [] if stype.is_base else stype.namespace.path.copy()
780
+ parts.append(stype.name)
781
+ return f"{ctx.builder.top_namespace}.{'.'.join(parts)}"
782
+
783
+
542
784
  def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
543
785
  if not isinstance(stype, builder.SpecTypeDefn):
544
786
  return
@@ -559,7 +801,42 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
559
801
  return
560
802
 
561
803
  if isinstance(stype, builder.SpecTypeDefnAlias):
562
- ctx.out.write(f"{stype.name} = {refer_to(ctx, stype.alias)}\n")
804
+ ctx.use_serial_alias = True
805
+ ctx.out.write(f"{stype.name} = typing.Annotated[\n")
806
+ ctx.out.write(f"{INDENT}{refer_to(ctx, stype.alias)},\n")
807
+ ctx.out.write(f"{INDENT}serial_alias_annotation(\n")
808
+ ctx.out.write(
809
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
810
+ )
811
+ if stype.is_dynamic_allowed():
812
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
813
+ ctx.out.write(f"{INDENT}),\n")
814
+ ctx.out.write("]\n")
815
+ return
816
+
817
+ if isinstance(stype, builder.SpecTypeDefnUnion):
818
+ ctx.use_serial_union = True
819
+ ctx.out.write(f"{stype.name} = typing.Annotated[\n")
820
+ ctx.out.write(f"{INDENT}{refer_to(ctx, stype.get_backing_type())},\n")
821
+ ctx.out.write(f"{INDENT}serial_union_annotation(\n")
822
+ ctx.out.write(
823
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
824
+ )
825
+ if stype.is_dynamic_allowed():
826
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
827
+ if stype.discriminator is not None:
828
+ ctx.out.write(
829
+ f"{INDENT * 2}discriminator={util.encode_common_string(stype.discriminator)},\n"
830
+ )
831
+ if stype.discriminator_map is not None:
832
+ ctx.out.write(f"{INDENT * 2}discriminator_map={{\n")
833
+ for key, value in stype.discriminator_map.items():
834
+ ctx.out.write(
835
+ f"{INDENT * 3}{util.encode_common_string(key)}: {refer_to(ctx, value)},\n"
836
+ )
837
+ ctx.out.write(f"{INDENT * 2}}},\n")
838
+ ctx.out.write(f"{INDENT}),\n")
839
+ ctx.out.write("]\n")
563
840
  return
564
841
 
565
842
  if isinstance(stype, builder.SpecTypeDefnStringEnum):
@@ -571,11 +848,11 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
571
848
 
572
849
  class_out = io.StringIO()
573
850
  base_class = ""
574
- generic = stype.get_generic()
851
+ generics = stype.get_generics()
575
852
  if not stype.base.is_base:
576
853
  base_class = f"({refer_to(ctx, stype.base)})"
577
- elif generic is not None:
578
- base_class = f"(typing.Generic[{generic}])"
854
+ elif len(generics) > 0:
855
+ base_class = f"(typing.Generic[{', '.join(generics)}])"
579
856
  class_out.write(f"class {stype.name}{base_class}:\n")
580
857
 
581
858
  emitted_properties_metadata = _emit_type_properties(
@@ -586,45 +863,45 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
586
863
  to_string_values = emitted_properties_metadata.to_string_values
587
864
  parse_require = emitted_properties_metadata.parse_require
588
865
 
589
- _emit_generic(ctx, stype.get_generic())
866
+ _emit_generics(ctx, generics)
590
867
 
591
- if (
592
- len(unconverted_values) > 0
593
- or len(to_string_values) > 0
594
- or len(unconverted_keys) > 0
595
- or len(parse_require) > 0
596
- ):
597
- ctx.use_serial_class = True
598
- ctx.out.write("@serial_class(\n")
868
+ # Emit serial_class decorator
869
+ ctx.out.write("@serial_class(\n")
870
+ ctx.out.write(
871
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
872
+ )
873
+ if stype.is_dynamic_allowed():
874
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
599
875
 
600
- def write_values(key: str, values: set[str]) -> None:
601
- if len(values) == 0:
602
- return
603
- value_str = ", ".join([f'"{name}"' for name in sorted(values)])
604
- ctx.out.write(f"{INDENT}{key}={{{value_str}}},\n")
876
+ def write_values(key: str, values: set[str]) -> None:
877
+ if len(values) == 0:
878
+ return
879
+ value_str = ", ".join([f'"{name}"' for name in sorted(values)])
880
+ ctx.out.write(f"{INDENT}{key}={{{value_str}}},\n")
605
881
 
606
- write_values("unconverted_keys", unconverted_keys)
607
- write_values("unconverted_values", unconverted_values)
608
- write_values("to_string_values", to_string_values)
609
- write_values("parse_require", parse_require)
882
+ write_values("unconverted_keys", unconverted_keys)
883
+ write_values("unconverted_values", unconverted_values)
884
+ write_values("to_string_values", to_string_values)
885
+ write_values("parse_require", parse_require)
610
886
 
611
- ctx.out.write(")\n")
887
+ ctx.out.write(")\n")
612
888
 
613
- dataclass = "@dataclass"
889
+ # Emit dataclass decorator
890
+ dataclass = "@dataclasses.dataclass"
614
891
  dc_args = []
615
892
  if stype.is_kw_only():
616
893
  dc_args.append("kw_only=True")
617
894
  if stype.is_hashable:
618
895
  dc_args.extend(["frozen=True", "eq=True"])
619
896
  if len(dc_args) > 0:
620
- dataclass += f'({", ".join(dc_args)})'
897
+ dataclass += f"({', '.join(dc_args)})"
621
898
 
622
899
  ctx.out.write(f"{dataclass}\n")
623
900
  ctx.out.write(class_out.getvalue())
624
901
 
625
902
 
626
- def _emit_generic(ctx: Context, generic: Optional[str]) -> None:
627
- if generic is not None:
903
+ def _emit_generics(ctx: Context, generics: list[str]) -> None:
904
+ for generic in generics:
628
905
  ctx.out.write(f'{generic} = typing.TypeVar("{generic}")\n')
629
906
  ctx.out.write(f"{LINE_BREAK}{LINE_BREAK}")
630
907
 
@@ -654,6 +931,7 @@ base_name_map = {
654
931
  builder.BaseTypeName.s_opaque_key: "OpaqueKey",
655
932
  builder.BaseTypeName.s_string: "str",
656
933
  builder.BaseTypeName.s_tuple: "tuple",
934
+ builder.BaseTypeName.s_readonly_array: "tuple",
657
935
  builder.BaseTypeName.s_union: "typing.Union",
658
936
  builder.BaseTypeName.s_literal: "typing.Literal",
659
937
  }
@@ -662,6 +940,11 @@ base_name_map = {
662
940
  def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
663
941
  if isinstance(stype, builder.SpecTypeInstance):
664
942
  params = ", ".join([refer_to(ctx, p) for p in stype.parameters])
943
+
944
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_readonly_array):
945
+ assert len(stype.parameters) == 1, "Read Only Array takes one parameter"
946
+ params = f"{params}, ..."
947
+
665
948
  return f"{refer_to(ctx, stype.defn_type)}[{params}]"
666
949
 
667
950
  if isinstance(stype, builder.SpecTypeLiteralWrapper):
@@ -688,23 +971,21 @@ def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
688
971
  SpecEndpoint = builder.SpecEndpoint
689
972
 
690
973
 
691
- def _route_identifier(endpoint: builder.SpecEndpoint) -> tuple[str, str, str]:
692
- return (endpoint.path_dirname, endpoint.path_basename, endpoint.method)
693
-
694
-
695
974
  def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
696
975
  for endpoint_root in builder.api_endpoints:
697
976
  endpoints: list[SpecEndpoint] = []
698
977
  output = config.routes_output.get(endpoint_root)
699
978
  if output is None:
700
979
  continue
980
+ last_endpoint: SpecEndpoint | None = None
701
981
  for namespace in builder.namespaces.values():
702
982
  endpoint = namespace.endpoint
983
+ last_endpoint = endpoint
703
984
  if endpoint is None:
704
985
  continue
705
- if endpoint.root != endpoint_root:
986
+ if endpoint_root not in endpoint.path_per_api_endpoint:
706
987
  continue
707
- if endpoint.function is None:
988
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
708
989
  continue
709
990
 
710
991
  endpoints.append(endpoint)
@@ -717,25 +998,38 @@ def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
717
998
  from main.site.framework.types import StaticRouteType
718
999
  """
719
1000
  )
1001
+
1002
+ def _route_identifier(endpoint: SpecEndpoint) -> tuple[str, str, str]:
1003
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1004
+ return (
1005
+ endpoint_specific_path.path_dirname,
1006
+ endpoint_specific_path.path_basename,
1007
+ endpoint.method,
1008
+ )
1009
+
720
1010
  sorted_endpoints = sorted(endpoints, key=_route_identifier)
721
1011
 
722
- assert len(endpoints) == len(
723
- set(map(_route_identifier, endpoints))
724
- ), "Endpoints are not unique"
1012
+ assert len(endpoints) == len(set(map(_route_identifier, endpoints))), (
1013
+ "Endpoints are not unique"
1014
+ )
725
1015
 
726
1016
  path_set = set()
727
1017
  for endpoint in sorted_endpoints:
728
- assert endpoint.function
729
- func_bits = endpoint.function.split(".")
1018
+ last_endpoint = endpoint
1019
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
1020
+ assert endpoint_function_path.function
1021
+ func_bits = endpoint_function_path.function.split(".")
730
1022
  path = ".".join(func_bits[:-1])
731
1023
  if path in path_set:
732
1024
  continue
733
1025
  path_set.add(path)
734
1026
  static_out.write(f"import {path}\n")
735
1027
 
1028
+ assert last_endpoint is not None
1029
+
736
1030
  static_out.write(
737
1031
  f"""
738
- ROUTE_PREFIX = "/{endpoint.path_root}"
1032
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
739
1033
 
740
1034
  ROUTES: list[StaticRouteType] = [
741
1035
  """
@@ -749,20 +1043,21 @@ ROUTES: list[StaticRouteType] = [
749
1043
 
750
1044
  from main.site.framework.types import DynamicRouteType
751
1045
 
752
- ROUTE_PREFIX = "/{endpoint.path_root}"
1046
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
753
1047
 
754
1048
  ROUTES: list[DynamicRouteType] = [
755
1049
  """
756
1050
  )
757
1051
 
758
1052
  for endpoint in sorted_endpoints:
1053
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
759
1054
  dynamic_out.write(
760
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", "{endpoint.function}", ["{endpoint.method.upper()}"]),\n'
1055
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", "{endpoint_function_path.function}", ["{endpoint.method.upper()}"]),\n'
761
1056
  )
762
1057
 
763
- assert endpoint.function
1058
+ assert endpoint_function_path.function
764
1059
  static_out.write(
765
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", {endpoint.function}, ["{endpoint.method.upper()}"]),\n'
1060
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", {endpoint_function_path.function}, ["{endpoint.method.upper()}"]),\n'
766
1061
  )
767
1062
 
768
1063
  dynamic_out.write(f"{MODIFY_NOTICE}]\n")
@@ -777,16 +1072,22 @@ ROUTES: list[DynamicRouteType] = [
777
1072
  def _emit_namespace_imports(
778
1073
  *,
779
1074
  out: io.StringIO,
780
- namespaces: Set[builder.SpecNamespace],
781
- from_namespace: Optional[builder.SpecNamespace],
1075
+ namespaces: set[builder.SpecNamespace],
1076
+ from_namespace: builder.SpecNamespace | None,
782
1077
  config: PythonConfig,
1078
+ skip_non_sdk: bool = False,
783
1079
  ) -> None:
784
1080
  for ns in sorted(
785
1081
  namespaces,
786
1082
  key=lambda name: _resolve_namespace_name(name),
787
1083
  ):
1084
+ if (
1085
+ skip_non_sdk
1086
+ and ns.endpoint is not None
1087
+ and ns.endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
1088
+ ):
1089
+ continue
788
1090
  resolved = _resolve_namespace_name(ns)
789
- ref = _resolve_namespace_ref(ns)
790
1091
  if ns.endpoint is not None:
791
1092
  import_alias = "_".join(ns.path[2:]) + "_t"
792
1093
  out.write(
@@ -798,7 +1099,7 @@ def _emit_namespace_imports(
798
1099
  else:
799
1100
  from_path = config.types_package
800
1101
 
801
- out.write(f"from {from_path} import {resolved} as {ref}\n")
1102
+ out.write(f"from {from_path} import {resolved}\n")
802
1103
 
803
1104
 
804
1105
  def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
@@ -810,7 +1111,7 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
810
1111
  return None
811
1112
  enum_out = io.StringIO()
812
1113
  enum_out.write(f"{LINT_HEADER}{MODIFY_NOTICE}\n")
813
- enum_out.write("from typing import Literal, Type, Union\n")
1114
+ enum_out.write("from typing import Literal, Union\n")
814
1115
  enum_out.write("from pkgs.strenum_compat import StrEnum\n")
815
1116
 
816
1117
  ctx = TrackingContext()
@@ -825,13 +1126,13 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
825
1126
  )
826
1127
 
827
1128
  known_keys = []
828
- enum_out.write("\nENUM_MAP: dict[str, Type[StrEnum]] = {\n")
1129
+ enum_out.write("\nENUM_MAP: dict[str, type[StrEnum]] = {\n")
829
1130
  for key in sorted(named_enums.keys()):
830
1131
  enum_out.write(f'"{key}": {named_enums[key]},\n')
831
1132
  known_keys.append(f'Literal["{key}"]')
832
1133
  enum_out.write(f"}}\n{MODIFY_NOTICE}\n")
833
1134
 
834
- enum_out.write(f"\nKnownEnumsType = Union[\\\n{INDENT}")
1135
+ enum_out.write(f"\nKnownEnumsType = Union[\n{INDENT}")
835
1136
  enum_out.write(f",\n{INDENT}".join(known_keys))
836
1137
  enum_out.write(f"\n]\n{MODIFY_NOTICE}\n")
837
1138
 
@@ -848,21 +1149,36 @@ def _emit_api_stubs(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
848
1149
 
849
1150
  if endpoint is None:
850
1151
  continue
851
- if endpoint.root != endpoint_root:
1152
+ if endpoint_root not in endpoint.path_per_api_endpoint:
852
1153
  continue
853
- if endpoint.function is None:
1154
+
1155
+ endpoint_function = endpoint.path_per_api_endpoint[endpoint_root].function
1156
+ if endpoint_function is None:
854
1157
  continue
855
1158
 
856
- module_dir, file_name, func_name = endpoint.function.rsplit(".", 2)
1159
+ module_dir, file_name, func_name = endpoint_function.rsplit(".", 2)
857
1160
  module_path = os.path.abspath(module_dir.replace(".", "/"))
858
1161
  api_stub_file = f"{module_path}/{file_name}.py"
859
1162
  if os.path.isfile(api_stub_file):
860
1163
  continue
861
- _create_api_stub(api_stub_file, file_name, endpoint, config)
1164
+ _create_api_stub(
1165
+ api_stub_file=api_stub_file,
1166
+ file_name=file_name,
1167
+ endpoint=endpoint,
1168
+ config=config,
1169
+ endpoint_root=endpoint_root,
1170
+ top_namespace=builder.top_namespace,
1171
+ )
862
1172
 
863
1173
 
864
1174
  def _create_api_stub(
865
- api_stub_file: str, file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1175
+ *,
1176
+ api_stub_file: str,
1177
+ file_name: str,
1178
+ endpoint: SpecEndpoint,
1179
+ config: PythonConfig,
1180
+ endpoint_root: str,
1181
+ top_namespace: str,
866
1182
  ) -> None:
867
1183
  assert (
868
1184
  endpoint.method == builder.RouteMethod.post
@@ -870,7 +1186,13 @@ def _create_api_stub(
870
1186
  or endpoint.method == builder.RouteMethod.delete
871
1187
  or endpoint.method == builder.RouteMethod.patch
872
1188
  )
873
- api_out = _create_api_function(file_name, endpoint, config)
1189
+ api_out = _create_api_function(
1190
+ file_name=file_name,
1191
+ endpoint=endpoint,
1192
+ config=config,
1193
+ endpoint_root=endpoint_root,
1194
+ top_namespace=top_namespace,
1195
+ )
874
1196
  util.rewrite_file(api_stub_file, api_out.getvalue())
875
1197
 
876
1198
 
@@ -879,15 +1201,22 @@ WRAP_ARGS_END = "\n"
879
1201
 
880
1202
 
881
1203
  def _create_api_function(
882
- file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1204
+ *,
1205
+ file_name: str,
1206
+ endpoint: SpecEndpoint,
1207
+ config: PythonConfig,
1208
+ endpoint_root: str,
1209
+ top_namespace: str,
883
1210
  ) -> io.StringIO:
1211
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1212
+ assert endpoint_specific_path is not None
884
1213
  api_out = io.StringIO()
885
1214
  python_api_type_root = f"{config.types_package}.api"
886
- dot_dirname = endpoint.path_dirname.replace("/", ".")
1215
+ dot_dirname = endpoint_specific_path.path_dirname.replace("/", ".")
887
1216
  api_import = (
888
1217
  f"{python_api_type_root}.{dot_dirname}.{file_name}"
889
1218
  if dot_dirname != ""
890
- else f"{python_api_type_root}.{endpoint.path_basename}"
1219
+ else f"{python_api_type_root}.{endpoint_specific_path.path_basename}"
891
1220
  )
892
1221
 
893
1222
  if endpoint.method == builder.RouteMethod.post:
@@ -899,7 +1228,20 @@ def _create_api_function(
899
1228
  elif endpoint.method == builder.RouteMethod.patch:
900
1229
  validated_method = "validated_patch"
901
1230
 
902
- ruff_requires_wrap = len(endpoint.path_basename) > 14
1231
+ ruff_requires_wrap = len(endpoint_specific_path.path_basename) > 14
1232
+
1233
+ account_type = (
1234
+ endpoint_specific_path.root
1235
+ if endpoint_specific_path.root not in ["external", "portal"]
1236
+ else "materials"
1237
+ )
1238
+
1239
+ endpoint_path_name = (
1240
+ ENDPOINT_PATH_ALTERNATE
1241
+ if len(endpoint.path_per_api_endpoint.keys()) > 1
1242
+ and endpoint_specific_path.root != top_namespace
1243
+ else ENDPOINT_PATH
1244
+ )
903
1245
 
904
1246
  api_out.write(
905
1247
  f"""import {api_import} as api
@@ -907,8 +1249,8 @@ from main.db.session import Session, SessionMaker
907
1249
  from main.site.decorators import APIError, APIResponse, {validated_method}
908
1250
 
909
1251
 
910
- @{validated_method}(api.ENDPOINT_PATH, "{endpoint.root}", api.Arguments)
911
- def {endpoint.path_basename}({WRAP_ARGS_START if ruff_requires_wrap else ""}args: api.Arguments, client_sm: SessionMaker{WRAP_ARGS_END if ruff_requires_wrap else ""}) -> APIResponse[api.Data]:
1252
+ @{validated_method}(api.{endpoint_path_name}, "{account_type}", api.Arguments)
1253
+ def {endpoint_specific_path.path_basename}({WRAP_ARGS_START if ruff_requires_wrap else ""}args: api.Arguments, client_sm: SessionMaker{WRAP_ARGS_END if ruff_requires_wrap else ""}) -> APIResponse[api.Data]:
912
1254
  with Session(client_sm) as session:
913
1255
  # return APIResponse(data=api.Data())
914
1256
  pass
@@ -930,7 +1272,7 @@ def _emit_api_argument_lookup(
930
1272
  for endpoint_root in builder.api_endpoints:
931
1273
  routes_output = config.routes_output[endpoint_root]
932
1274
 
933
- imports = []
1275
+ imports = ["import typing", "import dataclasses"]
934
1276
  mappings = []
935
1277
  for namespace in sorted(
936
1278
  builder.namespaces.values(),
@@ -940,17 +1282,38 @@ def _emit_api_argument_lookup(
940
1282
 
941
1283
  if endpoint is None:
942
1284
  continue
943
- if endpoint.root != endpoint_root:
1285
+ if endpoint_root not in endpoint.path_per_api_endpoint:
944
1286
  continue
945
- if endpoint.function is None:
1287
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
946
1288
  continue
947
- if "Arguments" not in namespace.types:
1289
+ if "Arguments" not in namespace.types or "Data" not in namespace.types:
948
1290
  continue
949
1291
 
950
1292
  import_alias = "_".join(namespace.path[1:])
951
1293
  api_import = f"{config.types_package}.{'.'.join(namespace.path)}"
952
1294
  imports.append(f"import {api_import} as {import_alias}")
953
- mappings.append(f"{import_alias}.ENDPOINT_PATH: {import_alias}.Arguments")
1295
+
1296
+ route_group = (
1297
+ f'"{endpoint.route_group}"'
1298
+ if endpoint.route_group is not None
1299
+ else "None"
1300
+ )
1301
+ account_type = (
1302
+ f'"{endpoint.account_type}"'
1303
+ if endpoint.account_type is not None
1304
+ else "None"
1305
+ )
1306
+
1307
+ mapping = f"{INDENT}ApiEndpointKey(route={import_alias}.ENDPOINT_PATH, method={import_alias}.ENDPOINT_METHOD): ApiEndpointSpec(\n"
1308
+ mapping += f"{INDENT}{INDENT}arguments_type={import_alias}.Arguments,\n"
1309
+ mapping += f"{INDENT}{INDENT}data_type={import_alias}.Data,\n"
1310
+ mapping += f"{INDENT}{INDENT}route_group={route_group},\n"
1311
+ mapping += f"{INDENT}{INDENT}account_type={account_type},\n"
1312
+ mapping += f"{INDENT}{INDENT}route={import_alias}.ENDPOINT_PATH,\n"
1313
+ mapping += f'{INDENT}{INDENT}handler="{endpoint.path_per_api_endpoint[endpoint_root].function}",\n'
1314
+ mapping += f"{INDENT}{INDENT}method={import_alias}.ENDPOINT_METHOD,\n"
1315
+ mapping += f"{INDENT})"
1316
+ mappings.append(mapping)
954
1317
 
955
1318
  argument_lookup_out = io.StringIO()
956
1319
  argument_lookup_out.write(MODIFY_NOTICE)
@@ -958,8 +1321,29 @@ def _emit_api_argument_lookup(
958
1321
  argument_lookup_out.write(
959
1322
  f"""{LINE_BREAK.join(imports)}
960
1323
 
961
- {API_ARGUMENTS_NAME} = {{
962
- {f",{LINE_BREAK}{INDENT}".join(mappings)},
1324
+ AT = typing.TypeVar("AT")
1325
+ DT = typing.TypeVar("DT")
1326
+
1327
+
1328
+ @dataclasses.dataclass(kw_only=True, frozen=True)
1329
+ class ApiEndpointKey:
1330
+ method: str
1331
+ route: str
1332
+
1333
+
1334
+ @dataclasses.dataclass(kw_only=True)
1335
+ class ApiEndpointSpec(typing.Generic[AT, DT]):
1336
+ route: str
1337
+ arguments_type: type[AT]
1338
+ data_type: type[DT]
1339
+ route_group: str | None
1340
+ account_type: str | None
1341
+ handler: str
1342
+ method: str
1343
+
1344
+
1345
+ {API_ARGUMENTS_NAME}: dict[ApiEndpointKey, ApiEndpointSpec] = {{
1346
+ {f",{LINE_BREAK}".join(mappings)},
963
1347
  }}
964
1348
 
965
1349
  __all__ = ["{API_ARGUMENTS_NAME}"]
@@ -975,10 +1359,69 @@ __all__ = ["{API_ARGUMENTS_NAME}"]
975
1359
  CLIENT_CLASS_FILENAME = "client_base"
976
1360
  CLIENT_CLASS_IMPORTS = [
977
1361
  "from abc import ABC, abstractmethod",
978
- "from dataclasses import dataclass",
1362
+ "import dataclasses",
1363
+ ]
1364
+ ASYNC_BATCH_PROCESSOR_FILENAME = "async_batch_processor"
1365
+ ASYNC_BATCH_PROCESSOR_IMPORTS = [
1366
+ "import uuid",
1367
+ "from abc import ABC, abstractmethod",
1368
+ "from pkgs.serialization_util import serialize_for_api",
979
1369
  ]
980
1370
 
981
1371
 
1372
+ def _emit_async_batch_processor(
1373
+ *, spec_builder: builder.SpecBuilder, config: PythonConfig
1374
+ ) -> None:
1375
+ if not config.emit_async_batch_processor:
1376
+ return
1377
+
1378
+ async_batch_processor_out = io.StringIO()
1379
+ ctx = Context(
1380
+ out=io.StringIO(),
1381
+ namespace=builder.SpecNamespace("async_batch_processor"),
1382
+ builder=spec_builder,
1383
+ )
1384
+
1385
+ for namespace in sorted(
1386
+ spec_builder.namespaces.values(),
1387
+ key=lambda ns: _resolve_namespace_name(ns),
1388
+ ):
1389
+ _emit_async_batch_invocation_function(ctx=ctx, namespace=namespace)
1390
+
1391
+ async_batch_processor_out.write(MODIFY_NOTICE)
1392
+ async_batch_processor_out.write(LINT_HEADER)
1393
+ async_batch_processor_out.write("# ruff: noqa: PLR0904\n")
1394
+
1395
+ _emit_types_imports(out=async_batch_processor_out, ctx=ctx)
1396
+ _emit_namespace_imports(
1397
+ out=async_batch_processor_out,
1398
+ namespaces=ctx.namespaces,
1399
+ from_namespace=None,
1400
+ config=config,
1401
+ )
1402
+
1403
+ async_batch_processor_out.write(
1404
+ f"""{LINE_BREAK.join(ASYNC_BATCH_PROCESSOR_IMPORTS)}
1405
+
1406
+
1407
+ class AsyncBatchProcessorBase(ABC):
1408
+ @abstractmethod
1409
+ def _enqueue(self, req: {refer_to(ctx=ctx, stype=ASYNC_BATCH_REQUEST_STYPE)}) -> None:
1410
+ ...
1411
+
1412
+ @abstractmethod
1413
+ def send(self) -> base_t.ObjectId:
1414
+ ..."""
1415
+ )
1416
+ async_batch_processor_out.write(ctx.out.getvalue())
1417
+ async_batch_processor_out.write("\n")
1418
+
1419
+ util.rewrite_file(
1420
+ f"{config.types_output}/{ASYNC_BATCH_PROCESSOR_FILENAME}.py",
1421
+ async_batch_processor_out.getvalue(),
1422
+ )
1423
+
1424
+
982
1425
  def _emit_client_class(
983
1426
  *, spec_builder: builder.SpecBuilder, config: PythonConfig
984
1427
  ) -> None:
@@ -986,7 +1429,11 @@ def _emit_client_class(
986
1429
  return
987
1430
 
988
1431
  client_base_out = io.StringIO()
989
- ctx = Context(out=io.StringIO(), namespace=builder.SpecNamespace("client_class"))
1432
+ ctx = Context(
1433
+ out=io.StringIO(),
1434
+ builder=spec_builder,
1435
+ namespace=builder.SpecNamespace("client_base"),
1436
+ )
990
1437
  for namespace in sorted(
991
1438
  spec_builder.namespaces.values(),
992
1439
  key=lambda ns: _resolve_namespace_name(ns),
@@ -1001,8 +1448,9 @@ def _emit_client_class(
1001
1448
  _emit_namespace_imports(
1002
1449
  out=client_base_out,
1003
1450
  namespaces=ctx.namespaces,
1004
- from_namespace=builder.SpecNamespace("client_base"),
1451
+ from_namespace=None,
1005
1452
  config=config,
1453
+ skip_non_sdk=True,
1006
1454
  )
1007
1455
 
1008
1456
  client_base_out.write(
@@ -1011,7 +1459,7 @@ def _emit_client_class(
1011
1459
  DT = typing.TypeVar("DT")
1012
1460
 
1013
1461
 
1014
- @dataclass(kw_only=True)
1462
+ @dataclasses.dataclass(kw_only=True)
1015
1463
  class APIRequest:
1016
1464
  method: str
1017
1465
  endpoint: str
@@ -1022,10 +1470,10 @@ class ClientMethods(ABC):
1022
1470
 
1023
1471
  @abstractmethod
1024
1472
  def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
1025
- ...
1026
- """
1473
+ ..."""
1027
1474
  )
1028
1475
  client_base_out.write(ctx.out.getvalue())
1476
+ client_base_out.write("\n")
1029
1477
 
1030
1478
  util.rewrite_file(
1031
1479
  f"{config.types_output}/{CLIENT_CLASS_FILENAME}.py", client_base_out.getvalue()