UncountablePythonSDK 0.0.7__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 (311) 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.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
  4. {UncountablePythonSDK-0.0.7.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/opaque_key.py +1 -1
  49. pkgs/serialization/serial_alias.py +47 -0
  50. pkgs/serialization/serial_class.py +65 -50
  51. pkgs/serialization/serial_generic.py +16 -0
  52. pkgs/serialization/serial_union.py +84 -0
  53. pkgs/serialization/yaml.py +57 -0
  54. pkgs/serialization_util/__init__.py +7 -7
  55. pkgs/serialization_util/_get_type_for_serialization.py +1 -3
  56. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  57. pkgs/serialization_util/dataclasses.py +14 -0
  58. pkgs/serialization_util/serialization_helpers.py +118 -73
  59. pkgs/strenum_compat/strenum_compat.py +1 -9
  60. pkgs/type_spec/actions_registry/__init__.py +0 -0
  61. pkgs/type_spec/actions_registry/__main__.py +126 -0
  62. pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
  63. pkgs/type_spec/builder.py +475 -89
  64. pkgs/type_spec/config.py +24 -19
  65. pkgs/type_spec/emit_io_ts.py +5 -2
  66. pkgs/type_spec/emit_open_api.py +266 -32
  67. pkgs/type_spec/emit_open_api_util.py +32 -13
  68. pkgs/type_spec/emit_python.py +601 -150
  69. pkgs/type_spec/emit_typescript.py +74 -273
  70. pkgs/type_spec/emit_typescript_util.py +239 -5
  71. pkgs/type_spec/load_types.py +55 -10
  72. pkgs/type_spec/open_api_util.py +30 -41
  73. pkgs/type_spec/parts/base.py.prepart +4 -3
  74. pkgs/type_spec/type_info/emit_type_info.py +178 -16
  75. pkgs/type_spec/util.py +11 -11
  76. pkgs/type_spec/value_spec/__main__.py +3 -3
  77. pkgs/type_spec/value_spec/convert_type.py +8 -1
  78. pkgs/type_spec/value_spec/emit_python.py +13 -4
  79. uncountable/__init__.py +1 -2
  80. uncountable/core/__init__.py +12 -2
  81. uncountable/core/async_batch.py +37 -0
  82. uncountable/core/client.py +293 -43
  83. uncountable/core/environment.py +41 -0
  84. uncountable/core/file_upload.py +135 -0
  85. uncountable/core/types.py +17 -0
  86. uncountable/integration/__init__.py +0 -0
  87. uncountable/integration/cli.py +49 -0
  88. uncountable/integration/construct_client.py +51 -0
  89. uncountable/integration/cron.py +29 -0
  90. uncountable/integration/db/__init__.py +0 -0
  91. uncountable/integration/db/connect.py +18 -0
  92. uncountable/integration/db/session.py +25 -0
  93. uncountable/integration/entrypoint.py +13 -0
  94. uncountable/integration/executors/__init__.py +0 -0
  95. uncountable/integration/executors/executors.py +148 -0
  96. uncountable/integration/executors/generic_upload_executor.py +284 -0
  97. uncountable/integration/executors/script_executor.py +25 -0
  98. uncountable/integration/job.py +87 -0
  99. uncountable/integration/queue_runner/__init__.py +0 -0
  100. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  101. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  102. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  103. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  104. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  105. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  108. uncountable/integration/queue_runner/command_server/types.py +52 -0
  109. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  110. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  111. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  112. uncountable/integration/queue_runner/datastore/model.py +17 -0
  113. uncountable/integration/queue_runner/job_scheduler.py +163 -0
  114. uncountable/integration/queue_runner/queue_runner.py +26 -0
  115. uncountable/integration/queue_runner/types.py +7 -0
  116. uncountable/integration/queue_runner/worker.py +119 -0
  117. uncountable/integration/scan_profiles.py +67 -0
  118. uncountable/integration/scheduler.py +150 -0
  119. uncountable/integration/secret_retrieval/__init__.py +3 -0
  120. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  121. uncountable/integration/server.py +117 -0
  122. uncountable/integration/telemetry.py +209 -0
  123. uncountable/integration/webhook_server/entrypoint.py +170 -0
  124. uncountable/types/__init__.py +151 -5
  125. uncountable/types/api/batch/execute_batch.py +15 -7
  126. uncountable/types/api/batch/execute_batch_load_async.py +42 -0
  127. uncountable/types/api/chemical/__init__.py +1 -0
  128. uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
  129. uncountable/types/api/entity/create_entities.py +23 -10
  130. uncountable/types/api/entity/create_entity.py +21 -12
  131. uncountable/types/api/entity/get_entities_data.py +19 -29
  132. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  133. uncountable/types/api/entity/list_entities.py +28 -20
  134. uncountable/types/api/entity/lock_entity.py +45 -0
  135. uncountable/types/api/entity/resolve_entity_ids.py +19 -7
  136. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  137. uncountable/types/api/entity/set_values.py +13 -28
  138. uncountable/types/api/entity/transition_entity_phase.py +80 -0
  139. uncountable/types/api/entity/unlock_entity.py +44 -0
  140. uncountable/types/api/equipment/__init__.py +1 -0
  141. uncountable/types/api/equipment/associate_equipment_input.py +44 -0
  142. uncountable/types/api/field_options/__init__.py +1 -0
  143. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  144. uncountable/types/api/files/__init__.py +1 -0
  145. uncountable/types/api/files/download_file.py +77 -0
  146. uncountable/types/api/id_source/__init__.py +1 -0
  147. uncountable/types/api/id_source/list_id_source.py +56 -0
  148. uncountable/types/api/id_source/match_id_source.py +54 -0
  149. uncountable/types/api/input_groups/get_input_group_names.py +18 -7
  150. uncountable/types/api/inputs/create_inputs.py +25 -24
  151. uncountable/types/api/inputs/get_input_data.py +37 -31
  152. uncountable/types/api/inputs/get_input_names.py +20 -9
  153. uncountable/types/api/inputs/get_inputs_data.py +33 -27
  154. uncountable/types/api/inputs/set_input_attribute_values.py +18 -13
  155. uncountable/types/api/inputs/set_input_category.py +44 -0
  156. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  157. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  158. uncountable/types/api/material_families/__init__.py +1 -0
  159. uncountable/types/api/material_families/update_entity_material_families.py +48 -0
  160. uncountable/types/api/outputs/get_output_data.py +38 -29
  161. uncountable/types/api/outputs/get_output_names.py +20 -9
  162. uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
  163. uncountable/types/api/permissions/__init__.py +1 -0
  164. uncountable/types/api/permissions/set_core_permissions.py +105 -0
  165. uncountable/types/api/project/get_projects.py +23 -19
  166. uncountable/types/api/project/get_projects_data.py +26 -43
  167. uncountable/types/api/recipe_links/__init__.py +1 -0
  168. uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
  169. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  170. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +21 -10
  171. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  172. uncountable/types/api/recipes/archive_recipes.py +42 -0
  173. uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
  174. uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
  175. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  176. uncountable/types/api/recipes/create_recipe.py +51 -0
  177. uncountable/types/api/recipes/create_recipes.py +25 -24
  178. uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
  179. uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
  180. uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
  181. uncountable/types/api/recipes/get_curve.py +13 -27
  182. uncountable/types/api/recipes/get_recipe_calculations.py +21 -21
  183. uncountable/types/api/recipes/get_recipe_links.py +14 -6
  184. uncountable/types/api/recipes/get_recipe_names.py +18 -7
  185. uncountable/types/api/recipes/get_recipe_output_metadata.py +18 -19
  186. uncountable/types/api/recipes/get_recipes_data.py +83 -144
  187. uncountable/types/api/recipes/lock_recipes.py +63 -0
  188. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  189. uncountable/types/api/recipes/set_recipe_inputs.py +21 -11
  190. uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
  191. uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
  192. uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
  193. uncountable/types/api/recipes/set_recipe_outputs.py +28 -15
  194. uncountable/types/api/recipes/set_recipe_tags.py +109 -0
  195. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  196. uncountable/types/api/recipes/unlock_recipes.py +50 -0
  197. uncountable/types/api/triggers/__init__.py +1 -0
  198. uncountable/types/api/triggers/run_trigger.py +43 -0
  199. uncountable/types/api/uploader/__init__.py +1 -0
  200. uncountable/types/api/uploader/invoke_uploader.py +47 -0
  201. uncountable/types/async_batch.py +13 -0
  202. uncountable/types/async_batch_processor.py +384 -0
  203. uncountable/types/async_batch_t.py +97 -0
  204. uncountable/types/async_jobs.py +9 -0
  205. uncountable/types/async_jobs_t.py +53 -0
  206. uncountable/types/auth_retrieval.py +12 -0
  207. uncountable/types/auth_retrieval_t.py +75 -0
  208. uncountable/types/base.py +5 -78
  209. uncountable/types/base_t.py +85 -0
  210. uncountable/types/calculations.py +8 -0
  211. uncountable/types/calculations_t.py +27 -0
  212. uncountable/types/chemical_structure.py +8 -0
  213. uncountable/types/chemical_structure_t.py +28 -0
  214. uncountable/types/client_base.py +1115 -76
  215. uncountable/types/client_config.py +8 -0
  216. uncountable/types/client_config_t.py +26 -0
  217. uncountable/types/curves.py +10 -0
  218. uncountable/types/curves_t.py +51 -0
  219. uncountable/types/entity.py +8 -266
  220. uncountable/types/entity_t.py +393 -0
  221. uncountable/types/experiment_groups.py +8 -0
  222. uncountable/types/experiment_groups_t.py +27 -0
  223. uncountable/types/field_values.py +17 -23
  224. uncountable/types/field_values_t.py +204 -0
  225. uncountable/types/fields.py +8 -0
  226. uncountable/types/fields_t.py +28 -0
  227. uncountable/types/generic_upload.py +15 -0
  228. uncountable/types/generic_upload_t.py +119 -0
  229. uncountable/types/id_source.py +12 -0
  230. uncountable/types/id_source_t.py +68 -0
  231. uncountable/types/identifier.py +11 -0
  232. uncountable/types/identifier_t.py +63 -0
  233. uncountable/types/input_attributes.py +8 -0
  234. uncountable/types/input_attributes_t.py +30 -0
  235. uncountable/types/inputs.py +11 -0
  236. uncountable/types/inputs_t.py +83 -0
  237. uncountable/types/integration_server.py +9 -0
  238. uncountable/types/integration_server_t.py +42 -0
  239. uncountable/types/job_definition.py +27 -0
  240. uncountable/types/job_definition_t.py +260 -0
  241. uncountable/types/outputs.py +8 -0
  242. uncountable/types/outputs_t.py +30 -0
  243. uncountable/types/overrides.py +10 -0
  244. uncountable/types/overrides_t.py +49 -0
  245. uncountable/types/permissions.py +8 -0
  246. uncountable/types/permissions_t.py +46 -0
  247. uncountable/types/phases.py +8 -0
  248. uncountable/types/phases_t.py +27 -0
  249. uncountable/types/post_base.py +8 -0
  250. uncountable/types/post_base_t.py +30 -0
  251. uncountable/types/queued_job.py +16 -0
  252. uncountable/types/queued_job_t.py +123 -0
  253. uncountable/types/recipe_identifiers.py +12 -0
  254. uncountable/types/recipe_identifiers_t.py +76 -0
  255. uncountable/types/recipe_inputs.py +9 -0
  256. uncountable/types/recipe_inputs_t.py +30 -0
  257. uncountable/types/recipe_links.py +4 -44
  258. uncountable/types/recipe_links_t.py +54 -0
  259. uncountable/types/recipe_metadata.py +10 -0
  260. uncountable/types/recipe_metadata_t.py +58 -0
  261. uncountable/types/recipe_output_metadata.py +8 -0
  262. uncountable/types/recipe_output_metadata_t.py +28 -0
  263. uncountable/types/recipe_tags.py +8 -0
  264. uncountable/types/recipe_tags_t.py +27 -0
  265. uncountable/types/recipe_workflow_steps.py +14 -0
  266. uncountable/types/recipe_workflow_steps_t.py +95 -0
  267. uncountable/types/recipes.py +8 -0
  268. uncountable/types/recipes_t.py +25 -0
  269. uncountable/types/response.py +8 -0
  270. uncountable/types/response_t.py +26 -0
  271. uncountable/types/secret_retrieval.py +12 -0
  272. uncountable/types/secret_retrieval_t.py +75 -0
  273. uncountable/types/units.py +8 -0
  274. uncountable/types/units_t.py +27 -0
  275. uncountable/types/users.py +8 -0
  276. uncountable/types/users_t.py +28 -0
  277. uncountable/types/webhook_job.py +9 -0
  278. uncountable/types/webhook_job_t.py +37 -0
  279. uncountable/types/workflows.py +9 -0
  280. uncountable/types/workflows_t.py +39 -0
  281. UncountablePythonSDK-0.0.7.dist-info/METADATA +0 -27
  282. UncountablePythonSDK-0.0.7.dist-info/RECORD +0 -119
  283. examples/recipe-import/importer.py +0 -39
  284. type_spec/external/api/batch/execute_batch.yaml +0 -56
  285. type_spec/external/api/entity/create_entities.yaml +0 -33
  286. type_spec/external/api/entity/create_entity.yaml +0 -39
  287. type_spec/external/api/entity/get_entities_data.yaml +0 -55
  288. type_spec/external/api/entity/list_entities.yaml +0 -62
  289. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  290. type_spec/external/api/entity/set_values.yaml +0 -45
  291. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  292. type_spec/external/api/inputs/create_inputs.yaml +0 -61
  293. type_spec/external/api/inputs/get_input_data.yaml +0 -108
  294. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  295. type_spec/external/api/inputs/get_inputs_data.yaml +0 -95
  296. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -37
  297. type_spec/external/api/outputs/get_output_data.yaml +0 -103
  298. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  299. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  300. type_spec/external/api/project/get_projects.yaml +0 -52
  301. type_spec/external/api/project/get_projects_data.yaml +0 -86
  302. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  303. type_spec/external/api/recipes/create_recipes.yaml +0 -60
  304. type_spec/external/api/recipes/get_curve.yaml +0 -50
  305. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -49
  306. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  307. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  308. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -49
  309. type_spec/external/api/recipes/get_recipes_data.yaml +0 -372
  310. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
  311. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -56
@@ -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,75 +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
- stype: builder.SpecTypeDefn, name: str
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"
343
- assert (
344
- stype.base is None or stype.base.is_base
345
- ), f"Inheritance not supporterd in external api {name}"
411
+ assert isinstance(stype, builder.SpecTypeDefnObject), (
412
+ f"External api {name} must be an object"
413
+ )
414
+ if not supports_inheritance:
415
+ assert stype.base is None or stype.base.is_base, (
416
+ f"Inheritance not supported in external api {name}"
417
+ )
346
418
  return stype
347
419
 
348
420
 
349
- def _emit_endpoint_invocation_function(
350
- ctx: Context, namespace: builder.SpecNamespace
421
+ def _emit_endpoint_invocation_docstring(
422
+ ctx: Context,
423
+ endpoint: builder.SpecEndpoint,
424
+ properties: list[builder.SpecProperty],
351
425
  ) -> None:
352
- endpoint = namespace.endpoint
353
- if endpoint is None:
354
- return
355
- 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:
356
429
  return
357
430
 
358
- arguments_type = namespace.types["Arguments"]
359
- data_type = namespace.types["Data"]
431
+ FULL_INDENT = INDENT * 2
432
+ ctx.out.write(FULL_INDENT)
433
+ ctx.out.write('"""')
360
434
 
361
- arguments_type = _validate_supports_handler_generation(arguments_type, "arguments")
362
- data_type = _validate_supports_handler_generation(data_type, "response")
435
+ if endpoint.desc is not None and has_endpoint_desc:
436
+ ctx.out.write(f"{endpoint.desc}\n")
437
+ ctx.out.write("\n")
363
438
 
364
- endpoint_method_stype = builder.SpecTypeDefnObject(
365
- namespace=arguments_type.namespace, name=ENDPOINT_METHOD
366
- )
367
- endpoint_path_stype = builder.SpecTypeDefnObject(
368
- namespace=arguments_type.namespace, name=ENDPOINT_PATH
369
- )
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 [])
370
459
 
371
- has_arguments = (
372
- arguments_type.properties is not None
373
- and len(arguments_type.properties.values()) > 0
374
- )
375
460
  assert endpoint.function is not None
376
461
  function_name = endpoint.function.split(".")[-1]
377
- ctx.out.write("\n")
378
462
  ctx.out.write(
379
463
  f"""
380
464
  def {function_name}(
381
465
  self,\n"""
382
466
  )
383
- if has_arguments:
467
+ if len(all_arguments) > 0:
384
468
  ctx.out.write(f"{INDENT}{INDENT}*,\n")
385
- _emit_type_properties(
469
+ _emit_properties(
386
470
  ctx=ctx,
387
- stype=arguments_type,
471
+ properties=all_arguments,
388
472
  num_indent=2,
389
473
  separator=",\n",
390
474
  class_out=ctx.out,
391
475
  )
392
476
  ctx.out.write(f"{INDENT}) -> {refer_to(ctx=ctx, stype=data_type)}:")
393
-
394
477
  ctx.out.write("\n")
395
- ctx.out.write(f"{INDENT}{INDENT}args = {refer_to(ctx=ctx, stype=arguments_type)}(")
396
- if has_arguments:
397
- 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:
398
492
  ctx.out.write("\n")
399
- for prop in arguments_type.properties.values():
493
+ for prop in variable_type.properties.values():
400
494
  ctx.out.write(f"{INDENT}{INDENT}{INDENT}{prop.name}={prop.name},")
401
495
  ctx.out.write("\n")
402
496
  ctx.out.write(f"{INDENT}{INDENT})")
403
497
  else:
404
498
  ctx.out.write(")")
405
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
+
406
619
  ctx.out.write(
407
620
  f"""
408
621
  api_request = APIRequest(
@@ -412,7 +625,6 @@ def _emit_endpoint_invocation_function(
412
625
  )
413
626
  return self.do_request(api_request=api_request, return_type={refer_to(ctx=ctx, stype=data_type)})"""
414
627
  )
415
- ctx.out.write("\n")
416
628
 
417
629
 
418
630
  def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> None:
@@ -430,7 +642,9 @@ def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> No
430
642
  ctx.out.write(f"{INDENT}labels={{\n")
431
643
  for entry in stype.values.values():
432
644
  if entry.label is not None:
433
- 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
+ )
434
648
 
435
649
  ctx.out.write(f"{INDENT}}},\n")
436
650
  if need_deprecated:
@@ -461,12 +675,12 @@ def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> No
461
675
  )
462
676
 
463
677
 
464
- @dataclass
678
+ @dataclasses.dataclass
465
679
  class EmittedPropertiesMetadata:
466
- unconverted_keys: Set[str]
467
- unconverted_values: Set[str]
468
- to_string_values: Set[str]
469
- 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]
470
684
 
471
685
 
472
686
  def _emit_type_properties(
@@ -477,16 +691,31 @@ def _emit_type_properties(
477
691
  num_indent: int = 1,
478
692
  separator: str = "\n",
479
693
  ) -> EmittedPropertiesMetadata:
480
- unconverted_keys: Set[str] = set()
481
- unconverted_values: Set[str] = set()
482
- to_string_values: Set[str] = set()
483
- 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()
484
715
 
485
- if stype.properties is not None and len(stype.properties) > 0:
716
+ if len(properties) > 0:
486
717
 
487
718
  def write_field(prop: builder.SpecProperty) -> None:
488
- # Checked in outer function, MyPy doens't track the check inside here
489
- assert isinstance(stype, builder.SpecTypeDefn)
490
719
  if prop.name_case == builder.NameCase.preserve:
491
720
  unconverted_keys.add(prop.name)
492
721
  py_name = python_field_name(prop.name, prop.name_case)
@@ -509,20 +738,30 @@ def _emit_type_properties(
509
738
  default = "MISSING_SENTRY"
510
739
  ctx.use_missing = True
511
740
  elif prop.extant == builder.PropertyExtant.optional:
512
- ref_type = f"typing.Optional[{ref_type}]"
741
+ if ref_type != "None":
742
+ ref_type = f"{ref_type} | None"
513
743
  default = "None"
514
744
  elif prop.has_default:
515
745
  default = _emit_value(ctx, prop.spec_type, prop.default)
516
-
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)"
517
756
  class_out.write(f"{INDENT * num_indent}{py_name}: {ref_type}")
518
757
  if default:
519
758
  class_out.write(f" = {default}")
520
759
  class_out.write(separator)
521
760
 
522
- for prop in stype.properties.values():
761
+ for prop in properties:
523
762
  if prop.extant == builder.PropertyExtant.required:
524
763
  write_field(prop)
525
- for prop in stype.properties.values():
764
+ for prop in properties:
526
765
  if prop.extant != builder.PropertyExtant.required:
527
766
  write_field(prop)
528
767
  else:
@@ -536,6 +775,12 @@ def _emit_type_properties(
536
775
  )
537
776
 
538
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
+
539
784
  def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
540
785
  if not isinstance(stype, builder.SpecTypeDefn):
541
786
  return
@@ -556,7 +801,42 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
556
801
  return
557
802
 
558
803
  if isinstance(stype, builder.SpecTypeDefnAlias):
559
- 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")
560
840
  return
561
841
 
562
842
  if isinstance(stype, builder.SpecTypeDefnStringEnum):
@@ -568,11 +848,11 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
568
848
 
569
849
  class_out = io.StringIO()
570
850
  base_class = ""
571
- generic = stype.get_generic()
851
+ generics = stype.get_generics()
572
852
  if not stype.base.is_base:
573
853
  base_class = f"({refer_to(ctx, stype.base)})"
574
- elif generic is not None:
575
- base_class = f"(typing.Generic[{generic}])"
854
+ elif len(generics) > 0:
855
+ base_class = f"(typing.Generic[{', '.join(generics)}])"
576
856
  class_out.write(f"class {stype.name}{base_class}:\n")
577
857
 
578
858
  emitted_properties_metadata = _emit_type_properties(
@@ -583,45 +863,45 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
583
863
  to_string_values = emitted_properties_metadata.to_string_values
584
864
  parse_require = emitted_properties_metadata.parse_require
585
865
 
586
- _emit_generic(ctx, stype.get_generic())
866
+ _emit_generics(ctx, generics)
587
867
 
588
- if (
589
- len(unconverted_values) > 0
590
- or len(to_string_values) > 0
591
- or len(unconverted_keys) > 0
592
- or len(parse_require) > 0
593
- ):
594
- ctx.use_serial_class = True
595
- 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")
596
875
 
597
- def write_values(key: str, values: set[str]) -> None:
598
- if len(values) == 0:
599
- return
600
- value_str = ", ".join([f'"{name}"' for name in sorted(values)])
601
- 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")
602
881
 
603
- write_values("unconverted_keys", unconverted_keys)
604
- write_values("unconverted_values", unconverted_values)
605
- write_values("to_string_values", to_string_values)
606
- 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)
607
886
 
608
- ctx.out.write(")\n")
887
+ ctx.out.write(")\n")
609
888
 
610
- dataclass = "@dataclass"
889
+ # Emit dataclass decorator
890
+ dataclass = "@dataclasses.dataclass"
611
891
  dc_args = []
612
892
  if stype.is_kw_only():
613
893
  dc_args.append("kw_only=True")
614
894
  if stype.is_hashable:
615
895
  dc_args.extend(["frozen=True", "eq=True"])
616
896
  if len(dc_args) > 0:
617
- dataclass += f'({", ".join(dc_args)})'
897
+ dataclass += f"({', '.join(dc_args)})"
618
898
 
619
899
  ctx.out.write(f"{dataclass}\n")
620
900
  ctx.out.write(class_out.getvalue())
621
901
 
622
902
 
623
- def _emit_generic(ctx: Context, generic: Optional[str]) -> None:
624
- if generic is not None:
903
+ def _emit_generics(ctx: Context, generics: list[str]) -> None:
904
+ for generic in generics:
625
905
  ctx.out.write(f'{generic} = typing.TypeVar("{generic}")\n')
626
906
  ctx.out.write(f"{LINE_BREAK}{LINE_BREAK}")
627
907
 
@@ -651,6 +931,7 @@ base_name_map = {
651
931
  builder.BaseTypeName.s_opaque_key: "OpaqueKey",
652
932
  builder.BaseTypeName.s_string: "str",
653
933
  builder.BaseTypeName.s_tuple: "tuple",
934
+ builder.BaseTypeName.s_readonly_array: "tuple",
654
935
  builder.BaseTypeName.s_union: "typing.Union",
655
936
  builder.BaseTypeName.s_literal: "typing.Literal",
656
937
  }
@@ -659,6 +940,11 @@ base_name_map = {
659
940
  def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
660
941
  if isinstance(stype, builder.SpecTypeInstance):
661
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
+
662
948
  return f"{refer_to(ctx, stype.defn_type)}[{params}]"
663
949
 
664
950
  if isinstance(stype, builder.SpecTypeLiteralWrapper):
@@ -685,23 +971,21 @@ def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
685
971
  SpecEndpoint = builder.SpecEndpoint
686
972
 
687
973
 
688
- def _route_identifier(endpoint: builder.SpecEndpoint) -> tuple[str, str, str]:
689
- return (endpoint.path_dirname, endpoint.path_basename, endpoint.method)
690
-
691
-
692
974
  def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
693
975
  for endpoint_root in builder.api_endpoints:
694
976
  endpoints: list[SpecEndpoint] = []
695
977
  output = config.routes_output.get(endpoint_root)
696
978
  if output is None:
697
979
  continue
980
+ last_endpoint: SpecEndpoint | None = None
698
981
  for namespace in builder.namespaces.values():
699
982
  endpoint = namespace.endpoint
983
+ last_endpoint = endpoint
700
984
  if endpoint is None:
701
985
  continue
702
- if endpoint.root != endpoint_root:
986
+ if endpoint_root not in endpoint.path_per_api_endpoint:
703
987
  continue
704
- if endpoint.function is None:
988
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
705
989
  continue
706
990
 
707
991
  endpoints.append(endpoint)
@@ -714,25 +998,38 @@ def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
714
998
  from main.site.framework.types import StaticRouteType
715
999
  """
716
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
+
717
1010
  sorted_endpoints = sorted(endpoints, key=_route_identifier)
718
1011
 
719
- assert len(endpoints) == len(
720
- set(map(_route_identifier, endpoints))
721
- ), "Endpoints are not unique"
1012
+ assert len(endpoints) == len(set(map(_route_identifier, endpoints))), (
1013
+ "Endpoints are not unique"
1014
+ )
722
1015
 
723
1016
  path_set = set()
724
1017
  for endpoint in sorted_endpoints:
725
- assert endpoint.function
726
- 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(".")
727
1022
  path = ".".join(func_bits[:-1])
728
1023
  if path in path_set:
729
1024
  continue
730
1025
  path_set.add(path)
731
1026
  static_out.write(f"import {path}\n")
732
1027
 
1028
+ assert last_endpoint is not None
1029
+
733
1030
  static_out.write(
734
1031
  f"""
735
- ROUTE_PREFIX = "/{endpoint.path_root}"
1032
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
736
1033
 
737
1034
  ROUTES: list[StaticRouteType] = [
738
1035
  """
@@ -746,20 +1043,21 @@ ROUTES: list[StaticRouteType] = [
746
1043
 
747
1044
  from main.site.framework.types import DynamicRouteType
748
1045
 
749
- ROUTE_PREFIX = "/{endpoint.path_root}"
1046
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
750
1047
 
751
1048
  ROUTES: list[DynamicRouteType] = [
752
1049
  """
753
1050
  )
754
1051
 
755
1052
  for endpoint in sorted_endpoints:
1053
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
756
1054
  dynamic_out.write(
757
- 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'
758
1056
  )
759
1057
 
760
- assert endpoint.function
1058
+ assert endpoint_function_path.function
761
1059
  static_out.write(
762
- 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'
763
1061
  )
764
1062
 
765
1063
  dynamic_out.write(f"{MODIFY_NOTICE}]\n")
@@ -774,16 +1072,22 @@ ROUTES: list[DynamicRouteType] = [
774
1072
  def _emit_namespace_imports(
775
1073
  *,
776
1074
  out: io.StringIO,
777
- namespaces: Set[builder.SpecNamespace],
778
- from_namespace: Optional[builder.SpecNamespace],
1075
+ namespaces: set[builder.SpecNamespace],
1076
+ from_namespace: builder.SpecNamespace | None,
779
1077
  config: PythonConfig,
1078
+ skip_non_sdk: bool = False,
780
1079
  ) -> None:
781
1080
  for ns in sorted(
782
1081
  namespaces,
783
1082
  key=lambda name: _resolve_namespace_name(name),
784
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
785
1090
  resolved = _resolve_namespace_name(ns)
786
- ref = _resolve_namespace_ref(ns)
787
1091
  if ns.endpoint is not None:
788
1092
  import_alias = "_".join(ns.path[2:]) + "_t"
789
1093
  out.write(
@@ -795,7 +1099,7 @@ def _emit_namespace_imports(
795
1099
  else:
796
1100
  from_path = config.types_package
797
1101
 
798
- out.write(f"from {from_path} import {resolved} as {ref}\n")
1102
+ out.write(f"from {from_path} import {resolved}\n")
799
1103
 
800
1104
 
801
1105
  def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
@@ -807,7 +1111,7 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
807
1111
  return None
808
1112
  enum_out = io.StringIO()
809
1113
  enum_out.write(f"{LINT_HEADER}{MODIFY_NOTICE}\n")
810
- enum_out.write("from typing import Literal, Type, Union\n")
1114
+ enum_out.write("from typing import Literal, Union\n")
811
1115
  enum_out.write("from pkgs.strenum_compat import StrEnum\n")
812
1116
 
813
1117
  ctx = TrackingContext()
@@ -822,13 +1126,13 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
822
1126
  )
823
1127
 
824
1128
  known_keys = []
825
- enum_out.write("\nENUM_MAP: dict[str, Type[StrEnum]] = {\n")
1129
+ enum_out.write("\nENUM_MAP: dict[str, type[StrEnum]] = {\n")
826
1130
  for key in sorted(named_enums.keys()):
827
1131
  enum_out.write(f'"{key}": {named_enums[key]},\n')
828
1132
  known_keys.append(f'Literal["{key}"]')
829
1133
  enum_out.write(f"}}\n{MODIFY_NOTICE}\n")
830
1134
 
831
- enum_out.write(f"\nKnownEnumsType = Union[\\\n{INDENT}")
1135
+ enum_out.write(f"\nKnownEnumsType = Union[\n{INDENT}")
832
1136
  enum_out.write(f",\n{INDENT}".join(known_keys))
833
1137
  enum_out.write(f"\n]\n{MODIFY_NOTICE}\n")
834
1138
 
@@ -845,21 +1149,36 @@ def _emit_api_stubs(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
845
1149
 
846
1150
  if endpoint is None:
847
1151
  continue
848
- if endpoint.root != endpoint_root:
1152
+ if endpoint_root not in endpoint.path_per_api_endpoint:
849
1153
  continue
850
- if endpoint.function is None:
1154
+
1155
+ endpoint_function = endpoint.path_per_api_endpoint[endpoint_root].function
1156
+ if endpoint_function is None:
851
1157
  continue
852
1158
 
853
- module_dir, file_name, func_name = endpoint.function.rsplit(".", 2)
1159
+ module_dir, file_name, func_name = endpoint_function.rsplit(".", 2)
854
1160
  module_path = os.path.abspath(module_dir.replace(".", "/"))
855
1161
  api_stub_file = f"{module_path}/{file_name}.py"
856
1162
  if os.path.isfile(api_stub_file):
857
1163
  continue
858
- _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
+ )
859
1172
 
860
1173
 
861
1174
  def _create_api_stub(
862
- 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,
863
1182
  ) -> None:
864
1183
  assert (
865
1184
  endpoint.method == builder.RouteMethod.post
@@ -867,7 +1186,13 @@ def _create_api_stub(
867
1186
  or endpoint.method == builder.RouteMethod.delete
868
1187
  or endpoint.method == builder.RouteMethod.patch
869
1188
  )
870
- 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
+ )
871
1196
  util.rewrite_file(api_stub_file, api_out.getvalue())
872
1197
 
873
1198
 
@@ -876,15 +1201,22 @@ WRAP_ARGS_END = "\n"
876
1201
 
877
1202
 
878
1203
  def _create_api_function(
879
- 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,
880
1210
  ) -> io.StringIO:
1211
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1212
+ assert endpoint_specific_path is not None
881
1213
  api_out = io.StringIO()
882
1214
  python_api_type_root = f"{config.types_package}.api"
883
- dot_dirname = endpoint.path_dirname.replace("/", ".")
1215
+ dot_dirname = endpoint_specific_path.path_dirname.replace("/", ".")
884
1216
  api_import = (
885
1217
  f"{python_api_type_root}.{dot_dirname}.{file_name}"
886
1218
  if dot_dirname != ""
887
- else f"{python_api_type_root}.{endpoint.path_basename}"
1219
+ else f"{python_api_type_root}.{endpoint_specific_path.path_basename}"
888
1220
  )
889
1221
 
890
1222
  if endpoint.method == builder.RouteMethod.post:
@@ -896,7 +1228,20 @@ def _create_api_function(
896
1228
  elif endpoint.method == builder.RouteMethod.patch:
897
1229
  validated_method = "validated_patch"
898
1230
 
899
- 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
+ )
900
1245
 
901
1246
  api_out.write(
902
1247
  f"""import {api_import} as api
@@ -904,8 +1249,8 @@ from main.db.session import Session, SessionMaker
904
1249
  from main.site.decorators import APIError, APIResponse, {validated_method}
905
1250
 
906
1251
 
907
- @{validated_method}(api.ENDPOINT_PATH, "{endpoint.root}", api.Arguments)
908
- 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]:
909
1254
  with Session(client_sm) as session:
910
1255
  # return APIResponse(data=api.Data())
911
1256
  pass
@@ -927,7 +1272,7 @@ def _emit_api_argument_lookup(
927
1272
  for endpoint_root in builder.api_endpoints:
928
1273
  routes_output = config.routes_output[endpoint_root]
929
1274
 
930
- imports = []
1275
+ imports = ["import typing", "import dataclasses"]
931
1276
  mappings = []
932
1277
  for namespace in sorted(
933
1278
  builder.namespaces.values(),
@@ -937,17 +1282,38 @@ def _emit_api_argument_lookup(
937
1282
 
938
1283
  if endpoint is None:
939
1284
  continue
940
- if endpoint.root != endpoint_root:
1285
+ if endpoint_root not in endpoint.path_per_api_endpoint:
941
1286
  continue
942
- if endpoint.function is None:
1287
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
943
1288
  continue
944
- if "Arguments" not in namespace.types:
1289
+ if "Arguments" not in namespace.types or "Data" not in namespace.types:
945
1290
  continue
946
1291
 
947
1292
  import_alias = "_".join(namespace.path[1:])
948
1293
  api_import = f"{config.types_package}.{'.'.join(namespace.path)}"
949
1294
  imports.append(f"import {api_import} as {import_alias}")
950
- 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)
951
1317
 
952
1318
  argument_lookup_out = io.StringIO()
953
1319
  argument_lookup_out.write(MODIFY_NOTICE)
@@ -955,8 +1321,29 @@ def _emit_api_argument_lookup(
955
1321
  argument_lookup_out.write(
956
1322
  f"""{LINE_BREAK.join(imports)}
957
1323
 
958
- {API_ARGUMENTS_NAME} = {{
959
- {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)},
960
1347
  }}
961
1348
 
962
1349
  __all__ = ["{API_ARGUMENTS_NAME}"]
@@ -972,10 +1359,69 @@ __all__ = ["{API_ARGUMENTS_NAME}"]
972
1359
  CLIENT_CLASS_FILENAME = "client_base"
973
1360
  CLIENT_CLASS_IMPORTS = [
974
1361
  "from abc import ABC, abstractmethod",
975
- "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",
976
1369
  ]
977
1370
 
978
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
+
979
1425
  def _emit_client_class(
980
1426
  *, spec_builder: builder.SpecBuilder, config: PythonConfig
981
1427
  ) -> None:
@@ -983,7 +1429,11 @@ def _emit_client_class(
983
1429
  return
984
1430
 
985
1431
  client_base_out = io.StringIO()
986
- 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
+ )
987
1437
  for namespace in sorted(
988
1438
  spec_builder.namespaces.values(),
989
1439
  key=lambda ns: _resolve_namespace_name(ns),
@@ -998,8 +1448,9 @@ def _emit_client_class(
998
1448
  _emit_namespace_imports(
999
1449
  out=client_base_out,
1000
1450
  namespaces=ctx.namespaces,
1001
- from_namespace=builder.SpecNamespace("client_base"),
1451
+ from_namespace=None,
1002
1452
  config=config,
1453
+ skip_non_sdk=True,
1003
1454
  )
1004
1455
 
1005
1456
  client_base_out.write(
@@ -1008,7 +1459,7 @@ def _emit_client_class(
1008
1459
  DT = typing.TypeVar("DT")
1009
1460
 
1010
1461
 
1011
- @dataclass(kw_only=True)
1462
+ @dataclasses.dataclass(kw_only=True)
1012
1463
  class APIRequest:
1013
1464
  method: str
1014
1465
  endpoint: str
@@ -1019,10 +1470,10 @@ class ClientMethods(ABC):
1019
1470
 
1020
1471
  @abstractmethod
1021
1472
  def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
1022
- ...
1023
- """
1473
+ ..."""
1024
1474
  )
1025
1475
  client_base_out.write(ctx.out.getvalue())
1476
+ client_base_out.write("\n")
1026
1477
 
1027
1478
  util.rewrite_file(
1028
1479
  f"{config.types_output}/{CLIENT_CLASS_FILENAME}.py", client_base_out.getvalue()