UncountablePythonSDK 0.0.24__py3-none-any.whl → 0.0.131__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 (373) hide show
  1. docs/conf.py +60 -8
  2. docs/index.md +107 -4
  3. docs/integration_examples/create_ingredient.md +43 -0
  4. docs/integration_examples/create_output.md +56 -0
  5. docs/integration_examples/index.md +6 -0
  6. docs/justfile +2 -2
  7. docs/requirements.txt +7 -5
  8. examples/async_batch.py +5 -6
  9. examples/basic_auth.py +7 -0
  10. examples/create_entity.py +4 -6
  11. examples/create_ingredient_sdk.py +34 -0
  12. examples/download_files.py +26 -0
  13. examples/edit_recipe_inputs.py +50 -0
  14. examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
  15. examples/integration-server/jobs/materials_auto/example_cron.py +21 -0
  16. examples/integration-server/jobs/materials_auto/example_http.py +47 -0
  17. examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
  18. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  19. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  20. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
  21. examples/integration-server/jobs/materials_auto/example_wh.py +23 -0
  22. examples/integration-server/jobs/materials_auto/profile.yaml +104 -0
  23. examples/integration-server/pyproject.toml +224 -0
  24. examples/invoke_uploader.py +26 -0
  25. examples/oauth.py +7 -0
  26. examples/set_recipe_metadata_file.py +40 -0
  27. examples/set_recipe_output_file_sdk.py +26 -0
  28. examples/upload_files.py +2 -3
  29. pkgs/argument_parser/__init__.py +9 -0
  30. pkgs/argument_parser/_is_namedtuple.py +3 -0
  31. pkgs/argument_parser/argument_parser.py +295 -74
  32. pkgs/argument_parser/case_convert.py +4 -3
  33. pkgs/filesystem_utils/__init__.py +20 -0
  34. pkgs/filesystem_utils/_blob_session.py +144 -0
  35. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  36. pkgs/filesystem_utils/_local_session.py +69 -0
  37. pkgs/filesystem_utils/_s3_session.py +118 -0
  38. pkgs/filesystem_utils/_sftp_session.py +151 -0
  39. pkgs/filesystem_utils/file_type_utils.py +91 -0
  40. pkgs/filesystem_utils/filesystem_session.py +39 -0
  41. pkgs/py.typed +0 -0
  42. pkgs/serialization/__init__.py +8 -1
  43. pkgs/serialization/annotation.py +64 -0
  44. pkgs/serialization/missing_sentry.py +1 -1
  45. pkgs/serialization/opaque_key.py +1 -1
  46. pkgs/serialization/serial_alias.py +47 -0
  47. pkgs/serialization/serial_class.py +69 -54
  48. pkgs/serialization/serial_generic.py +16 -0
  49. pkgs/serialization/serial_union.py +84 -0
  50. pkgs/serialization/yaml.py +57 -0
  51. pkgs/serialization_util/__init__.py +7 -7
  52. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  53. pkgs/serialization_util/dataclasses.py +14 -0
  54. pkgs/serialization_util/serialization_helpers.py +117 -71
  55. pkgs/type_spec/actions_registry/__main__.py +0 -4
  56. pkgs/type_spec/actions_registry/emit_typescript.py +5 -5
  57. pkgs/type_spec/builder.py +438 -109
  58. pkgs/type_spec/builder_types.py +9 -0
  59. pkgs/type_spec/config.py +52 -24
  60. pkgs/type_spec/cross_output_links.py +99 -0
  61. pkgs/type_spec/emit_io_ts.py +1 -1
  62. pkgs/type_spec/emit_open_api.py +160 -41
  63. pkgs/type_spec/emit_open_api_util.py +13 -7
  64. pkgs/type_spec/emit_python.py +450 -136
  65. pkgs/type_spec/emit_typescript.py +117 -250
  66. pkgs/type_spec/emit_typescript_util.py +293 -4
  67. pkgs/type_spec/load_types.py +20 -5
  68. pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
  69. pkgs/type_spec/open_api_util.py +29 -4
  70. pkgs/type_spec/parts/base.py.prepart +13 -10
  71. pkgs/type_spec/parts/base.ts.prepart +4 -0
  72. pkgs/type_spec/type_info/__main__.py +3 -1
  73. pkgs/type_spec/type_info/emit_type_info.py +161 -32
  74. pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
  75. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
  76. pkgs/type_spec/util.py +4 -4
  77. pkgs/type_spec/value_spec/__main__.py +27 -10
  78. pkgs/type_spec/value_spec/convert_type.py +21 -1
  79. pkgs/type_spec/value_spec/emit_python.py +25 -7
  80. pkgs/type_spec/value_spec/types.py +1 -1
  81. uncountable/__init__.py +1 -2
  82. uncountable/core/__init__.py +11 -3
  83. uncountable/core/async_batch.py +16 -1
  84. uncountable/core/client.py +247 -52
  85. uncountable/core/environment.py +41 -0
  86. uncountable/core/file_upload.py +67 -22
  87. uncountable/core/types.py +8 -13
  88. uncountable/integration/cli.py +142 -0
  89. uncountable/integration/construct_client.py +43 -27
  90. uncountable/integration/cron.py +12 -11
  91. uncountable/integration/db/connect.py +12 -2
  92. uncountable/integration/db/session.py +25 -0
  93. uncountable/integration/entrypoint.py +4 -34
  94. uncountable/integration/executors/executors.py +147 -0
  95. uncountable/integration/executors/generic_upload_executor.py +336 -0
  96. uncountable/integration/executors/script_executor.py +15 -9
  97. uncountable/integration/http_server/__init__.py +5 -0
  98. uncountable/integration/http_server/types.py +69 -0
  99. uncountable/integration/job.py +246 -19
  100. uncountable/integration/queue_runner/__init__.py +0 -0
  101. uncountable/integration/queue_runner/command_server/__init__.py +28 -0
  102. uncountable/integration/queue_runner/command_server/command_client.py +133 -0
  103. uncountable/integration/queue_runner/command_server/command_server.py +142 -0
  104. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  105. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +58 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +57 -0
  108. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +114 -0
  109. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +264 -0
  110. uncountable/integration/queue_runner/command_server/types.py +75 -0
  111. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  112. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +250 -0
  113. uncountable/integration/queue_runner/datastore/interface.py +29 -0
  114. uncountable/integration/queue_runner/datastore/model.py +24 -0
  115. uncountable/integration/queue_runner/job_scheduler.py +200 -0
  116. uncountable/integration/queue_runner/queue_runner.py +34 -0
  117. uncountable/integration/queue_runner/types.py +7 -0
  118. uncountable/integration/queue_runner/worker.py +116 -0
  119. uncountable/integration/scan_profiles.py +67 -0
  120. uncountable/integration/scheduler.py +199 -0
  121. uncountable/integration/secret_retrieval/__init__.py +3 -0
  122. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  123. uncountable/integration/server.py +103 -54
  124. uncountable/integration/telemetry.py +251 -0
  125. uncountable/integration/webhook_server/entrypoint.py +97 -0
  126. uncountable/types/__init__.py +149 -30
  127. uncountable/types/api/batch/execute_batch.py +16 -9
  128. uncountable/types/api/batch/execute_batch_load_async.py +13 -7
  129. uncountable/types/api/chemical/convert_chemical_formats.py +20 -8
  130. uncountable/types/api/condition_parameters/__init__.py +1 -0
  131. uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
  132. uncountable/types/api/entity/create_entities.py +24 -12
  133. uncountable/types/api/entity/create_entity.py +22 -13
  134. uncountable/types/api/entity/create_or_update_entity.py +48 -0
  135. uncountable/types/api/entity/export_entities.py +59 -0
  136. uncountable/types/api/entity/get_entities_data.py +18 -9
  137. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  138. uncountable/types/api/entity/list_aggregate.py +79 -0
  139. uncountable/types/api/entity/list_entities.py +53 -14
  140. uncountable/types/api/entity/lock_entity.py +45 -0
  141. uncountable/types/api/entity/lookup_entity.py +116 -0
  142. uncountable/types/api/entity/resolve_entity_ids.py +19 -10
  143. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  144. uncountable/types/api/entity/set_values.py +15 -8
  145. uncountable/types/api/entity/transition_entity_phase.py +27 -12
  146. uncountable/types/api/entity/unlock_entity.py +44 -0
  147. uncountable/types/api/equipment/__init__.py +1 -0
  148. uncountable/types/api/equipment/associate_equipment_input.py +43 -0
  149. uncountable/types/api/field_options/__init__.py +1 -0
  150. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  151. uncountable/types/api/files/__init__.py +1 -0
  152. uncountable/types/api/files/download_file.py +77 -0
  153. uncountable/types/api/id_source/list_id_source.py +20 -11
  154. uncountable/types/api/id_source/match_id_source.py +15 -10
  155. uncountable/types/api/input_groups/get_input_group_names.py +16 -7
  156. uncountable/types/api/inputs/create_inputs.py +28 -14
  157. uncountable/types/api/inputs/get_input_data.py +34 -16
  158. uncountable/types/api/inputs/get_input_names.py +19 -10
  159. uncountable/types/api/inputs/get_inputs_data.py +29 -11
  160. uncountable/types/api/inputs/set_input_attribute_values.py +16 -10
  161. uncountable/types/api/inputs/set_input_category.py +44 -0
  162. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  163. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  164. uncountable/types/api/integrations/__init__.py +1 -0
  165. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  166. uncountable/types/api/integrations/push_notification.py +49 -0
  167. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  168. uncountable/types/api/listing/__init__.py +1 -0
  169. uncountable/types/api/listing/fetch_listing.py +58 -0
  170. uncountable/types/api/material_families/__init__.py +1 -0
  171. uncountable/types/api/material_families/update_entity_material_families.py +47 -0
  172. uncountable/types/api/notebooks/__init__.py +1 -0
  173. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  174. uncountable/types/api/outputs/get_output_data.py +32 -17
  175. uncountable/types/api/outputs/get_output_names.py +18 -9
  176. uncountable/types/api/outputs/get_output_organization.py +173 -0
  177. uncountable/types/api/outputs/resolve_output_conditions.py +23 -11
  178. uncountable/types/api/permissions/set_core_permissions.py +31 -15
  179. uncountable/types/api/project/get_projects.py +20 -11
  180. uncountable/types/api/project/get_projects_data.py +23 -14
  181. uncountable/types/api/recipe_links/create_recipe_link.py +17 -10
  182. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  183. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +19 -10
  184. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  185. uncountable/types/api/recipes/add_time_series_data.py +64 -0
  186. uncountable/types/api/recipes/archive_recipes.py +14 -7
  187. uncountable/types/api/recipes/associate_recipe_as_input.py +16 -8
  188. uncountable/types/api/recipes/associate_recipe_as_lot.py +14 -7
  189. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  190. uncountable/types/api/recipes/create_mix_order.py +44 -0
  191. uncountable/types/api/recipes/create_recipe.py +21 -14
  192. uncountable/types/api/recipes/create_recipes.py +25 -13
  193. uncountable/types/api/recipes/disassociate_recipe_as_input.py +14 -7
  194. uncountable/types/api/recipes/edit_recipe_inputs.py +208 -19
  195. uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
  196. uncountable/types/api/recipes/get_curve.py +15 -9
  197. uncountable/types/api/recipes/get_recipe_calculations.py +17 -11
  198. uncountable/types/api/recipes/get_recipe_links.py +14 -8
  199. uncountable/types/api/recipes/get_recipe_names.py +16 -7
  200. uncountable/types/api/recipes/get_recipe_output_metadata.py +16 -10
  201. uncountable/types/api/recipes/get_recipes_data.py +96 -45
  202. uncountable/types/api/recipes/lock_recipes.py +64 -0
  203. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  204. uncountable/types/api/recipes/set_recipe_inputs.py +19 -13
  205. uncountable/types/api/recipes/set_recipe_metadata.py +14 -7
  206. uncountable/types/api/recipes/set_recipe_output_annotations.py +114 -0
  207. uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
  208. uncountable/types/api/recipes/set_recipe_outputs.py +40 -15
  209. uncountable/types/api/recipes/set_recipe_tags.py +30 -13
  210. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  211. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  212. uncountable/types/api/recipes/unlock_recipes.py +51 -0
  213. uncountable/types/api/runsheet/__init__.py +1 -0
  214. uncountable/types/api/runsheet/complete_async_upload.py +41 -0
  215. uncountable/types/api/triggers/run_trigger.py +15 -8
  216. uncountable/types/api/uploader/__init__.py +1 -0
  217. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  218. uncountable/types/api/uploader/invoke_uploader.py +46 -0
  219. uncountable/types/api/user/__init__.py +1 -0
  220. uncountable/types/api/user/get_current_user_info.py +40 -0
  221. uncountable/types/async_batch.py +8 -52
  222. uncountable/types/async_batch_processor.py +694 -18
  223. uncountable/types/async_batch_t.py +108 -0
  224. uncountable/types/async_jobs.py +8 -0
  225. uncountable/types/async_jobs_t.py +52 -0
  226. uncountable/types/auth_retrieval.py +11 -0
  227. uncountable/types/auth_retrieval_t.py +75 -0
  228. uncountable/types/base.py +5 -80
  229. uncountable/types/base_t.py +87 -0
  230. uncountable/types/calculations.py +3 -19
  231. uncountable/types/calculations_t.py +26 -0
  232. uncountable/types/chemical_structure.py +3 -23
  233. uncountable/types/chemical_structure_t.py +28 -0
  234. uncountable/types/client_base.py +1170 -88
  235. uncountable/types/client_config.py +8 -0
  236. uncountable/types/client_config_t.py +36 -0
  237. uncountable/types/curves.py +5 -43
  238. uncountable/types/curves_t.py +50 -0
  239. uncountable/types/data.py +12 -0
  240. uncountable/types/data_t.py +103 -0
  241. uncountable/types/entity.py +8 -270
  242. uncountable/types/entity_t.py +446 -0
  243. uncountable/types/experiment_groups.py +3 -19
  244. uncountable/types/experiment_groups_t.py +26 -0
  245. uncountable/types/exports.py +8 -0
  246. uncountable/types/exports_t.py +34 -0
  247. uncountable/types/field_values.py +25 -61
  248. uncountable/types/field_values_t.py +302 -0
  249. uncountable/types/fields.py +3 -20
  250. uncountable/types/fields_t.py +27 -0
  251. uncountable/types/generic_upload.py +14 -0
  252. uncountable/types/generic_upload_t.py +119 -0
  253. uncountable/types/id_source.py +7 -45
  254. uncountable/types/id_source_t.py +68 -0
  255. uncountable/types/identifier.py +6 -50
  256. uncountable/types/identifier_t.py +62 -0
  257. uncountable/types/input_attributes.py +3 -25
  258. uncountable/types/input_attributes_t.py +29 -0
  259. uncountable/types/inputs.py +6 -57
  260. uncountable/types/inputs_t.py +82 -0
  261. uncountable/types/integration_server.py +8 -0
  262. uncountable/types/integration_server_t.py +46 -0
  263. uncountable/types/integration_session.py +10 -0
  264. uncountable/types/integration_session_t.py +60 -0
  265. uncountable/types/integrations.py +10 -0
  266. uncountable/types/integrations_t.py +62 -0
  267. uncountable/types/job_definition.py +28 -0
  268. uncountable/types/job_definition_t.py +285 -0
  269. uncountable/types/listing.py +9 -0
  270. uncountable/types/listing_t.py +51 -0
  271. uncountable/types/notices.py +8 -0
  272. uncountable/types/notices_t.py +37 -0
  273. uncountable/types/notifications.py +11 -0
  274. uncountable/types/notifications_t.py +74 -0
  275. uncountable/types/outputs.py +3 -22
  276. uncountable/types/outputs_t.py +29 -0
  277. uncountable/types/overrides.py +9 -0
  278. uncountable/types/overrides_t.py +49 -0
  279. uncountable/types/permissions.py +3 -42
  280. uncountable/types/permissions_t.py +45 -0
  281. uncountable/types/phases.py +3 -19
  282. uncountable/types/phases_t.py +26 -0
  283. uncountable/types/post_base.py +3 -26
  284. uncountable/types/post_base_t.py +29 -0
  285. uncountable/types/queued_job.py +17 -0
  286. uncountable/types/queued_job_t.py +140 -0
  287. uncountable/types/recipe_identifiers.py +7 -58
  288. uncountable/types/recipe_identifiers_t.py +75 -0
  289. uncountable/types/recipe_inputs.py +4 -26
  290. uncountable/types/recipe_inputs_t.py +29 -0
  291. uncountable/types/recipe_links.py +4 -46
  292. uncountable/types/recipe_links_t.py +53 -0
  293. uncountable/types/recipe_metadata.py +5 -48
  294. uncountable/types/recipe_metadata_t.py +57 -0
  295. uncountable/types/recipe_output_metadata.py +3 -20
  296. uncountable/types/recipe_output_metadata_t.py +27 -0
  297. uncountable/types/recipe_tags.py +3 -19
  298. uncountable/types/recipe_tags_t.py +26 -0
  299. uncountable/types/recipe_workflow_steps.py +9 -73
  300. uncountable/types/recipe_workflow_steps_t.py +95 -0
  301. uncountable/types/recipes.py +7 -0
  302. uncountable/types/recipes_t.py +25 -0
  303. uncountable/types/response.py +3 -21
  304. uncountable/types/response_t.py +26 -0
  305. uncountable/types/secret_retrieval.py +11 -0
  306. uncountable/types/secret_retrieval_t.py +75 -0
  307. uncountable/types/sockets.py +20 -0
  308. uncountable/types/sockets_t.py +169 -0
  309. uncountable/types/structured_filters.py +25 -0
  310. uncountable/types/structured_filters_t.py +248 -0
  311. uncountable/types/units.py +3 -19
  312. uncountable/types/units_t.py +26 -0
  313. uncountable/types/uploader.py +24 -0
  314. uncountable/types/uploader_t.py +222 -0
  315. uncountable/types/users.py +3 -20
  316. uncountable/types/users_t.py +27 -0
  317. uncountable/types/webhook_job.py +9 -0
  318. uncountable/types/webhook_job_t.py +48 -0
  319. uncountable/types/workflows.py +4 -28
  320. uncountable/types/workflows_t.py +38 -0
  321. uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
  322. uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
  323. {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
  324. {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -1
  325. UncountablePythonSDK-0.0.24.dist-info/METADATA +0 -47
  326. UncountablePythonSDK-0.0.24.dist-info/RECORD +0 -216
  327. docs/quickstart.md +0 -19
  328. examples/recipe-import/importer.py +0 -39
  329. type_spec/external/api/batch/execute_batch.yaml +0 -56
  330. type_spec/external/api/batch/execute_batch_load_async.yaml +0 -18
  331. type_spec/external/api/chemical/convert_chemical_formats.yaml +0 -33
  332. type_spec/external/api/entity/create_entities.yaml +0 -45
  333. type_spec/external/api/entity/create_entity.yaml +0 -51
  334. type_spec/external/api/entity/get_entities_data.yaml +0 -29
  335. type_spec/external/api/entity/list_entities.yaml +0 -52
  336. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  337. type_spec/external/api/entity/set_values.yaml +0 -18
  338. type_spec/external/api/entity/transition_entity_phase.yaml +0 -44
  339. type_spec/external/api/id_source/list_id_source.yaml +0 -35
  340. type_spec/external/api/id_source/match_id_source.yaml +0 -32
  341. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  342. type_spec/external/api/inputs/create_inputs.yaml +0 -48
  343. type_spec/external/api/inputs/get_input_data.yaml +0 -95
  344. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  345. type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
  346. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
  347. type_spec/external/api/outputs/get_output_data.yaml +0 -92
  348. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  349. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  350. type_spec/external/api/permissions/set_core_permissions.yaml +0 -69
  351. type_spec/external/api/project/get_projects.yaml +0 -42
  352. type_spec/external/api/project/get_projects_data.yaml +0 -50
  353. type_spec/external/api/recipe_links/create_recipe_link.yaml +0 -25
  354. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  355. type_spec/external/api/recipes/archive_recipes.yaml +0 -20
  356. type_spec/external/api/recipes/associate_recipe_as_input.yaml +0 -19
  357. type_spec/external/api/recipes/associate_recipe_as_lot.yaml +0 -19
  358. type_spec/external/api/recipes/create_recipe.yaml +0 -39
  359. type_spec/external/api/recipes/create_recipes.yaml +0 -47
  360. type_spec/external/api/recipes/disassociate_recipe_as_input.yaml +0 -16
  361. type_spec/external/api/recipes/edit_recipe_inputs.yaml +0 -85
  362. type_spec/external/api/recipes/get_curve.yaml +0 -21
  363. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
  364. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  365. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  366. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
  367. type_spec/external/api/recipes/get_recipes_data.yaml +0 -244
  368. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -42
  369. type_spec/external/api/recipes/set_recipe_metadata.yaml +0 -20
  370. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
  371. type_spec/external/api/recipes/set_recipe_tags.yaml +0 -62
  372. type_spec/external/api/triggers/run_trigger.yaml +0 -18
  373. uncountable/integration/types.py +0 -89
@@ -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
5
+ from typing import Any
6
6
 
7
7
  from . import builder, util
8
+ from .builder import EndpointEmitType, EndpointSpecificPath, base_namespace_name
8
9
  from .config import PythonConfig
10
+ from .cross_output_links import get_path_links
11
+ from .emit_open_api_util import EmitOpenAPIStabilityLevel
9
12
 
10
13
  INDENT = " "
11
14
  LINE_BREAK = "\n"
12
15
  MODIFY_NOTICE = "# DO NOT MODIFY -- This file is generated by type_spec\n"
13
16
  # 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"
17
+ LINT_HEADER = "# ruff: noqa: E402 Q003\n# fmt: off\n# isort: skip_file\n"
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,7 +24,7 @@ __all__: list[str] = [
21
24
  """
22
25
  END_ALL_EXPORTS = "]\n"
23
26
 
24
- ASYNC_BATCH_TYPE_NAMESPACE = builder.SpecNamespace(name="uncountable.types.async_batch")
27
+ ASYNC_BATCH_TYPE_NAMESPACE = builder.SpecNamespace(name="async_batch")
25
28
  ASYNC_BATCH_REQUEST_PATH_STYPE = builder.SpecTypeDefnStringEnum(
26
29
  namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="AsyncBatchRequestPath"
27
30
  )
@@ -32,29 +35,38 @@ QUEUED_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
32
35
  namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="QueuedAsyncBatchRequest"
33
36
  )
34
37
 
38
+ CLIENT_CONFIG_TYPE_NAMESPACE = builder.SpecNamespace(name="client_config")
39
+ REQUEST_OPTIONS_STYPE = builder.SpecTypeDefnObject(
40
+ namespace=CLIENT_CONFIG_TYPE_NAMESPACE, name="RequestOptions"
41
+ )
42
+
35
43
 
36
- @dataclass(kw_only=True)
44
+ @dataclasses.dataclass(kw_only=True)
37
45
  class TrackingContext:
38
- namespace: Optional[builder.SpecNamespace] = None
39
- namespaces: set[builder.SpecNamespace] = field(default_factory=set)
40
- names: set[str] = field(default_factory=set)
46
+ namespace: builder.SpecNamespace | None = None
47
+ namespaces: set[builder.SpecNamespace] = dataclasses.field(default_factory=set)
48
+ names: set[str] = dataclasses.field(default_factory=set)
41
49
 
42
50
  use_enum: bool = False
43
51
  use_serial_string_enum: bool = False
44
52
  use_dataclass: bool = False
45
- use_serial_class: bool = False
53
+ use_serial_union: bool = False
54
+ use_serial_alias: bool = False
46
55
  use_missing: bool = False
47
56
  use_opaque_key: bool = False
48
57
 
49
58
 
50
- @dataclass(kw_only=True)
59
+ @dataclasses.dataclass(kw_only=True)
51
60
  class Context(TrackingContext):
52
61
  out: io.StringIO
53
62
  namespace: builder.SpecNamespace
63
+ builder: builder.SpecBuilder
54
64
 
55
65
 
56
66
  def _resolve_namespace_name(namespace: builder.SpecNamespace) -> str:
57
- return namespace.name
67
+ if len(namespace.path) > 1:
68
+ return namespace.name
69
+ return f"{namespace.name}_t"
58
70
 
59
71
 
60
72
  def _resolve_namespace_ref(namespace: builder.SpecNamespace) -> str:
@@ -110,26 +122,36 @@ def _check_type_match(stype: builder.SpecType, value: Any) -> bool:
110
122
  raise Exception("invalid type", stype, value)
111
123
 
112
124
 
113
- def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> str:
125
+ def _emit_value(
126
+ ctx: TrackingContext, stype: builder.SpecType, value: Any, indent: int = 0
127
+ ) -> str:
114
128
  literal = builder.unwrap_literal_type(stype)
115
129
  if literal is not None:
116
130
  return _emit_value(ctx, literal.value_type, literal.value)
117
131
 
118
132
  if stype.is_base_type(builder.BaseTypeName.s_string):
119
- assert isinstance(value, str)
133
+ assert isinstance(value, str), (
134
+ f"Expected str value for {stype.name} but got {value}"
135
+ )
120
136
  return util.encode_common_string(value)
121
137
  elif stype.is_base_type(builder.BaseTypeName.s_integer):
122
- assert isinstance(value, int)
138
+ assert isinstance(value, int), (
139
+ f"Expected int value for {stype.name} but got {value}"
140
+ )
123
141
  return str(value)
124
142
  elif stype.is_base_type(builder.BaseTypeName.s_boolean):
125
- assert isinstance(value, bool)
143
+ assert isinstance(value, bool), (
144
+ f"Expected bool value for {stype.name} but got {value}"
145
+ )
126
146
  return "True" if value else "False"
127
147
  elif stype.is_base_type(builder.BaseTypeName.s_decimal) or stype.is_base_type(
128
148
  builder.BaseTypeName.s_lossy_decimal
129
149
  ):
130
150
  # Note that decimal requires the `!decimal 123.12` style notation in the YAML
131
151
  # file since PyYaml parses numbers as float, unfortuantely
132
- assert isinstance(value, (Decimal, int))
152
+ assert isinstance(value, (Decimal, int)), (
153
+ f"Expected decimal value for {stype.name} but got {value} (type: {type(value)})"
154
+ )
133
155
  if isinstance(value, int):
134
156
  # skip quotes for integers
135
157
  return f"Decimal({value})"
@@ -144,14 +166,14 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
144
166
  key_type = stype.parameters[0]
145
167
  value_type = stype.parameters[1]
146
168
  return (
147
- "{\n "
148
- + ",\n ".join(
169
+ f"{{\n{INDENT * (indent + 1)}"
170
+ + f",\n{INDENT * (indent + 1)}".join(
149
171
  _emit_value(ctx, key_type, dkey)
150
172
  + ": "
151
- + _emit_value(ctx, value_type, dvalue)
173
+ + _emit_value(ctx, value_type, dvalue, indent=indent + 1)
152
174
  for dkey, dvalue in value.items()
153
175
  )
154
- + "\n}"
176
+ + f"\n{INDENT * indent}}}"
155
177
  )
156
178
 
157
179
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
@@ -177,6 +199,34 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
177
199
  return f"{refer_to(ctx, stype)}.{_resolve_enum_name(value, stype.name_case)}"
178
200
  elif isinstance(stype, builder.SpecTypeDefnAlias):
179
201
  return _emit_value(ctx, stype.alias, value)
202
+ elif isinstance(stype, builder.SpecTypeDefnObject):
203
+ assert isinstance(value, dict), (
204
+ f"Expected dict value for {stype.name} but got {value}"
205
+ )
206
+ if not stype.is_hashable:
207
+ raise Exception("invalid constant object type, non-hashable", value, stype)
208
+ obj_out = f"{refer_to(ctx, stype)}("
209
+ emitted_fields: set[str] = set()
210
+ for prop_name, prop in (stype.properties or {}).items():
211
+ if prop_name not in value:
212
+ continue
213
+ else:
214
+ value_to_emit = value[prop_name]
215
+ emitted_fields.add(prop_name)
216
+ py_name = python_field_name(prop.name, prop.name_case)
217
+ obj_out += f"\n{INDENT * (indent + 1)}{py_name}={_emit_value(ctx, prop.spec_type, value_to_emit, indent=indent + 1)},"
218
+ whitespace = f"\n{INDENT * indent}" if len(emitted_fields) > 0 else ""
219
+ obj_out += f"{whitespace})"
220
+
221
+ if emitted_fields != set(value.keys()):
222
+ raise Exception(
223
+ "invalid object type, extra fields found:",
224
+ value,
225
+ stype,
226
+ set(value.keys()) - emitted_fields,
227
+ )
228
+
229
+ return obj_out
180
230
 
181
231
  raise Exception("invalid constant type", value, stype)
182
232
 
@@ -214,11 +264,14 @@ def _emit_types_imports(*, out: io.StringIO, ctx: Context) -> None:
214
264
  out.write("import datetime # noqa: F401\n")
215
265
  out.write("from decimal import Decimal # noqa: F401\n")
216
266
  if ctx.use_enum:
217
- out.write("from pkgs.strenum_compat import StrEnum\n")
267
+ out.write("from enum import StrEnum\n")
218
268
  if ctx.use_dataclass:
219
- out.write("from dataclasses import dataclass\n")
220
- if ctx.use_serial_class:
269
+ out.write("import dataclasses\n")
221
270
  out.write("from pkgs.serialization import serial_class\n")
271
+ if ctx.use_serial_union:
272
+ out.write("from pkgs.serialization import serial_union_annotation\n")
273
+ if ctx.use_serial_alias:
274
+ out.write("from pkgs.serialization import serial_alias_annotation\n")
222
275
  if ctx.use_serial_string_enum:
223
276
  out.write("from pkgs.serialization import serial_string_enum\n")
224
277
  if ctx.use_missing:
@@ -243,7 +296,7 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
243
296
  ):
244
297
  if (
245
298
  namespace.endpoint is not None
246
- and not namespace.endpoint.is_sdk
299
+ and namespace.endpoint.is_sdk == EndpointEmitType.EMIT_NOTHING
247
300
  and config.sdk_endpoints_only is True
248
301
  ):
249
302
  continue
@@ -251,6 +304,7 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
251
304
  ctx = Context(
252
305
  out=io.StringIO(),
253
306
  namespace=namespace,
307
+ builder=builder,
254
308
  )
255
309
 
256
310
  _emit_namespace(ctx, namespace)
@@ -289,17 +343,22 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
289
343
  full.write(f"# === END section from {namespace.name}.part.py ===\n")
290
344
 
291
345
  basename = "/".join(namespace.path)
292
- filename = f"{config.types_output}/{basename}.py"
346
+ filename = f"{config.types_output}/{basename}{'' if len(namespace.path) > 1 else '_t'}.py"
293
347
  util.rewrite_file(filename, full.getvalue())
294
348
 
349
+ # Deprecated SDK support
350
+ if config.all_named_type_exports and len(namespace.path) == 1:
351
+ compat_out = _create_sdk_compat_namespace(namespace)
352
+ compat_filename = f"{config.types_output}/{basename}.py"
353
+ util.rewrite_file(compat_filename, compat_out.getvalue())
354
+
295
355
  path_to = os.path.dirname(basename)
296
356
  while path_to != "":
297
357
  all_dirs.add(path_to)
298
358
  path_to = os.path.dirname(path_to)
299
359
 
300
- if len(namespace.path) == 1 or (
301
- config.all_named_type_exports and len(namespace.path) > 1
302
- ):
360
+ # Deprecated SDK support
361
+ if config.all_named_type_exports:
303
362
  index_out.write("from ")
304
363
  if len(namespace.path) == 1:
305
364
  index_out.write(".")
@@ -328,16 +387,44 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
328
387
 
329
388
  ENDPOINT_METHOD = "ENDPOINT_METHOD"
330
389
  ENDPOINT_PATH = "ENDPOINT_PATH"
390
+ # will be removed in Q1 2025 when ENDPOINT_PATH is made api_endpoint-agnostic
391
+ # is used when the API call has multiple endpoints for the one endpoint that isn't equal to the top_namespace
392
+ ENDPOINT_PATH_ALTERNATE = "ENDPOINT_PATH_ALTERNATE"
393
+
394
+
395
+ def _get_epf_root(endpoint_specific_path: EndpointSpecificPath) -> str:
396
+ return endpoint_specific_path.root
331
397
 
332
398
 
333
399
  def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
334
400
  endpoint = namespace.endpoint
335
401
  if endpoint is not None:
402
+ path_links = get_path_links(
403
+ ctx.builder.cross_output_paths,
404
+ namespace,
405
+ current_path_type="Python",
406
+ endpoint=endpoint,
407
+ )
408
+ if path_links != "":
409
+ ctx.out.write("\n")
410
+ ctx.out.write(path_links)
411
+
336
412
  ctx.out.write("\n")
337
413
  ctx.out.write(f'{ENDPOINT_METHOD} = "{endpoint.method.upper()}"\n')
338
- ctx.out.write(
339
- f'{ENDPOINT_PATH} = "{endpoint.path_root}/{endpoint.path_dirname}/{endpoint.path_basename}"\n'
340
- )
414
+ for endpoint_specific_path in sorted(
415
+ endpoint.path_per_api_endpoint.values(), key=_get_epf_root
416
+ ):
417
+ endpoint_path_name = ENDPOINT_PATH
418
+
419
+ if (
420
+ len(endpoint.path_per_api_endpoint.keys()) > 1
421
+ and endpoint_specific_path.root != ctx.builder.top_namespace
422
+ ):
423
+ endpoint_path_name = ENDPOINT_PATH_ALTERNATE
424
+ ctx.names.add(ENDPOINT_PATH_ALTERNATE)
425
+ ctx.out.write(
426
+ f'{endpoint_path_name} = "{endpoint_specific_path.path_root}/{endpoint_specific_path.path_dirname}/{endpoint_specific_path.path_basename}"\n'
427
+ )
341
428
 
342
429
  ctx.names.add(ENDPOINT_METHOD)
343
430
  ctx.names.add(ENDPOINT_PATH)
@@ -349,16 +436,42 @@ def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
349
436
  _emit_constant(ctx, sconst)
350
437
 
351
438
 
439
+ def _create_sdk_compat_namespace(namespace: builder.SpecNamespace) -> io.StringIO:
440
+ compat_out = io.StringIO()
441
+ compat_out.write(LINT_HEADER)
442
+ compat_out.write(MODIFY_NOTICE)
443
+ compat_out.write("# Kept only for SDK backwards compatibility\n")
444
+
445
+ # This mostly an prepart import, thus has no high-level knowledge. Since this onl
446
+ # needs backwards copmatibiltiy from when written we can just hardcode what was there
447
+ # (only those in __all__ assuming that worked, which it might not)
448
+ if namespace.name == "base":
449
+ compat_out.write("""
450
+ from .base_t import JsonScalar as JsonScalar
451
+ from .base_t import JsonValue as JsonValue
452
+ from .base_t import ObjectId as ObjectId
453
+ """)
454
+ else:
455
+ for stype in namespace.types.values():
456
+ compat_out.write(
457
+ f"from .{namespace.path[-1]}_t import {stype.name} as {stype.name}\n"
458
+ )
459
+
460
+ compat_out.write(MODIFY_NOTICE)
461
+
462
+ return compat_out
463
+
464
+
352
465
  def _validate_supports_handler_generation(
353
466
  stype: builder.SpecTypeDefn, name: str, supports_inheritance: bool = False
354
467
  ) -> builder.SpecTypeDefnObject:
355
- assert isinstance(
356
- stype, builder.SpecTypeDefnObject
357
- ), f"External api {name} must be an object"
468
+ assert isinstance(stype, builder.SpecTypeDefnObject), (
469
+ f"External api {name} must be an object"
470
+ )
358
471
  if not supports_inheritance:
359
- assert (
360
- stype.base is None or stype.base.is_base
361
- ), f"Inheritance not supported in external api {name}"
472
+ assert stype.base is None or stype.base.is_base, (
473
+ f"Inheritance not supported in external api {name}"
474
+ )
362
475
  return stype
363
476
 
364
477
 
@@ -401,8 +514,23 @@ def _emit_endpoint_invocation_function_signature(
401
514
  else []
402
515
  ) + (extra_params if extra_params is not None else [])
403
516
 
404
- assert endpoint.function is not None
405
- function_name = endpoint.function.split(".")[-1]
517
+ request_options_property = builder.SpecProperty(
518
+ name="_request_options",
519
+ label="_request_options",
520
+ spec_type=REQUEST_OPTIONS_STYPE,
521
+ extant=builder.PropertyExtant.optional,
522
+ convert_value=builder.PropertyConvertValue.auto,
523
+ name_case=builder.NameCase.convert,
524
+ default=None,
525
+ has_default=True,
526
+ desc=None,
527
+ )
528
+ all_arguments.append(request_options_property)
529
+
530
+ # All endpoints share a function name
531
+ function = endpoint.path_per_api_endpoint[endpoint.default_endpoint_key].function
532
+ assert function is not None
533
+ function_name = function.split(".")[-1]
406
534
  ctx.out.write(
407
535
  f"""
408
536
  def {function_name}(
@@ -448,7 +576,10 @@ def _emit_async_batch_invocation_function(
448
576
  endpoint = namespace.endpoint
449
577
  if endpoint is None:
450
578
  return
451
- if endpoint.async_batch_path is None or not endpoint.is_sdk:
579
+ if (
580
+ endpoint.async_batch_path is None
581
+ or endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
582
+ ):
452
583
  return
453
584
 
454
585
  ctx.out.write("\n")
@@ -530,7 +661,10 @@ def _emit_endpoint_invocation_function(
530
661
  endpoint = namespace.endpoint
531
662
  if endpoint is None:
532
663
  return
533
- if not endpoint.is_sdk or endpoint.is_beta:
664
+ if (
665
+ endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
666
+ or endpoint.stability_level == EmitOpenAPIStabilityLevel.draft
667
+ ):
534
668
  return
535
669
 
536
670
  ctx.out.write("\n")
@@ -563,6 +697,7 @@ def _emit_endpoint_invocation_function(
563
697
  method={refer_to(ctx=ctx, stype=endpoint_method_stype)},
564
698
  endpoint={refer_to(ctx=ctx, stype=endpoint_path_stype)},
565
699
  args=args,
700
+ request_options=_request_options,
566
701
  )
567
702
  return self.do_request(api_request=api_request, return_type={refer_to(ctx=ctx, stype=data_type)})"""
568
703
  )
@@ -583,7 +718,9 @@ def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> No
583
718
  ctx.out.write(f"{INDENT}labels={{\n")
584
719
  for entry in stype.values.values():
585
720
  if entry.label is not None:
586
- ctx.out.write(f'{INDENT}{INDENT}"{entry.value}": "{entry.label}",\n')
721
+ ctx.out.write(
722
+ f'{INDENT}{INDENT}"{entry.value}": "{entry.label}",\n'
723
+ )
587
724
 
588
725
  ctx.out.write(f"{INDENT}}},\n")
589
726
  if need_deprecated:
@@ -614,7 +751,7 @@ def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> No
614
751
  )
615
752
 
616
753
 
617
- @dataclass
754
+ @dataclasses.dataclass
618
755
  class EmittedPropertiesMetadata:
619
756
  unconverted_keys: set[str]
620
757
  unconverted_values: set[str]
@@ -655,33 +792,46 @@ def _emit_properties(
655
792
  if len(properties) > 0:
656
793
 
657
794
  def write_field(prop: builder.SpecProperty) -> None:
795
+ stype = prop.spec_type
658
796
  if prop.name_case == builder.NameCase.preserve:
659
797
  unconverted_keys.add(prop.name)
660
798
  py_name = python_field_name(prop.name, prop.name_case)
661
799
 
662
800
  if prop.convert_value == builder.PropertyConvertValue.no_convert:
663
801
  unconverted_values.add(py_name)
664
- elif not prop.spec_type.is_value_converted():
802
+ elif not stype.is_value_converted():
665
803
  assert prop.convert_value == builder.PropertyConvertValue.auto
666
804
  unconverted_values.add(py_name)
667
- if prop.spec_type.is_value_to_string():
805
+ if stype.is_value_to_string():
668
806
  to_string_values.add(py_name)
669
807
 
670
808
  if prop.parse_require:
671
809
  parse_require.add(py_name)
672
810
 
673
- ref_type = refer_to(ctx, prop.spec_type)
811
+ ref_type = refer_to(ctx, stype)
674
812
  default = None
675
813
  if prop.extant == builder.PropertyExtant.missing:
676
814
  ref_type = f"MissingType[{ref_type}]"
677
815
  default = "MISSING_SENTRY"
678
816
  ctx.use_missing = True
679
817
  elif prop.extant == builder.PropertyExtant.optional:
680
- ref_type = f"typing.Optional[{ref_type}]"
818
+ if isinstance(
819
+ stype, builder.SpecTypeInstance
820
+ ) and stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
821
+ pass # base type already adds the None union
822
+ elif ref_type == "None":
823
+ pass # no need to add a None union to a none type
824
+ else:
825
+ ref_type = f"{ref_type} | None"
681
826
  default = "None"
682
827
  elif prop.has_default:
683
- default = _emit_value(ctx, prop.spec_type, prop.default)
684
-
828
+ default = _emit_value(ctx, stype, prop.default)
829
+ if (
830
+ isinstance(stype, builder.SpecTypeInstance)
831
+ and (stype.defn_type.is_base_type(builder.BaseTypeName.s_list))
832
+ and default == "[]"
833
+ ):
834
+ default = "dataclasses.field(default_factory=list)"
685
835
  class_out.write(f"{INDENT * num_indent}{py_name}: {ref_type}")
686
836
  if default:
687
837
  class_out.write(f" = {default}")
@@ -704,6 +854,12 @@ def _emit_properties(
704
854
  )
705
855
 
706
856
 
857
+ def _named_type_path(ctx: Context, stype: builder.SpecTypeDefn) -> str:
858
+ parts = [] if stype.is_base else stype.namespace.path.copy()
859
+ parts.append(stype.name)
860
+ return f"{ctx.builder.top_namespace}.{'.'.join(parts)}"
861
+
862
+
707
863
  def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
708
864
  if not isinstance(stype, builder.SpecTypeDefn):
709
865
  return
@@ -724,7 +880,42 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
724
880
  return
725
881
 
726
882
  if isinstance(stype, builder.SpecTypeDefnAlias):
727
- ctx.out.write(f"{stype.name} = {refer_to(ctx, stype.alias)}\n")
883
+ ctx.use_serial_alias = True
884
+ ctx.out.write(f"{stype.name} = typing.Annotated[\n")
885
+ ctx.out.write(f"{INDENT}{refer_to(ctx, stype.alias)},\n")
886
+ ctx.out.write(f"{INDENT}serial_alias_annotation(\n")
887
+ ctx.out.write(
888
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
889
+ )
890
+ if stype.is_dynamic_allowed():
891
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
892
+ ctx.out.write(f"{INDENT}),\n")
893
+ ctx.out.write("]\n")
894
+ return
895
+
896
+ if isinstance(stype, builder.SpecTypeDefnUnion):
897
+ ctx.use_serial_union = True
898
+ ctx.out.write(f"{stype.name} = typing.Annotated[\n")
899
+ ctx.out.write(f"{INDENT}{refer_to(ctx, stype.get_backing_type())},\n")
900
+ ctx.out.write(f"{INDENT}serial_union_annotation(\n")
901
+ ctx.out.write(
902
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
903
+ )
904
+ if stype.is_dynamic_allowed():
905
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
906
+ if stype.discriminator is not None:
907
+ ctx.out.write(
908
+ f"{INDENT * 2}discriminator={util.encode_common_string(stype.discriminator)},\n"
909
+ )
910
+ if stype.discriminator_map is not None:
911
+ ctx.out.write(f"{INDENT * 2}discriminator_map={{\n")
912
+ for key, value in stype.discriminator_map.items():
913
+ ctx.out.write(
914
+ f"{INDENT * 3}{util.encode_common_string(key)}: {refer_to(ctx, value)},\n"
915
+ )
916
+ ctx.out.write(f"{INDENT * 2}}},\n")
917
+ ctx.out.write(f"{INDENT}),\n")
918
+ ctx.out.write("]\n")
728
919
  return
729
920
 
730
921
  if isinstance(stype, builder.SpecTypeDefnStringEnum):
@@ -736,11 +927,11 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
736
927
 
737
928
  class_out = io.StringIO()
738
929
  base_class = ""
739
- generic = stype.get_generic()
930
+ generics = stype.get_generics()
740
931
  if not stype.base.is_base:
741
932
  base_class = f"({refer_to(ctx, stype.base)})"
742
- elif generic is not None:
743
- base_class = f"(typing.Generic[{generic}])"
933
+ elif len(generics) > 0:
934
+ base_class = f"[{', '.join(generics)}]"
744
935
  class_out.write(f"class {stype.name}{base_class}:\n")
745
936
 
746
937
  emitted_properties_metadata = _emit_type_properties(
@@ -751,45 +942,51 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
751
942
  to_string_values = emitted_properties_metadata.to_string_values
752
943
  parse_require = emitted_properties_metadata.parse_require
753
944
 
754
- _emit_generic(ctx, stype.get_generic())
945
+ _emit_generics(ctx, generics)
755
946
 
756
- if (
757
- len(unconverted_values) > 0
758
- or len(to_string_values) > 0
759
- or len(unconverted_keys) > 0
760
- or len(parse_require) > 0
761
- ):
762
- ctx.use_serial_class = True
763
- ctx.out.write("@serial_class(\n")
764
-
765
- def write_values(key: str, values: set[str]) -> None:
766
- if len(values) == 0:
767
- return
768
- value_str = ", ".join([f'"{name}"' for name in sorted(values)])
769
- ctx.out.write(f"{INDENT}{key}={{{value_str}}},\n")
770
-
771
- write_values("unconverted_keys", unconverted_keys)
772
- write_values("unconverted_values", unconverted_values)
773
- write_values("to_string_values", to_string_values)
774
- write_values("parse_require", parse_require)
775
-
776
- ctx.out.write(")\n")
777
-
778
- dataclass = "@dataclass"
779
- dc_args = []
947
+ # Emit serial_class decorator
948
+ ctx.out.write("@serial_class(\n")
949
+ ctx.out.write(
950
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
951
+ )
952
+ if stype.is_dynamic_allowed():
953
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
954
+
955
+ def write_values(key: str, values: set[str]) -> None:
956
+ if len(values) == 0:
957
+ return
958
+ value_str = ", ".join([f'"{name}"' for name in sorted(values)])
959
+ ctx.out.write(f"{INDENT}{key}={{{value_str}}},\n")
960
+
961
+ write_values("unconverted_keys", unconverted_keys)
962
+ write_values("unconverted_values", unconverted_values)
963
+ write_values("to_string_values", to_string_values)
964
+ write_values("parse_require", parse_require)
965
+
966
+ ctx.out.write(")\n")
967
+
968
+ # Emit dataclass decorator
969
+ dataclass = "@dataclasses.dataclass"
970
+ refer_to(
971
+ ctx,
972
+ builder.SpecTypeDefnAlias(
973
+ namespace=ctx.builder.namespaces[base_namespace_name], name="ENABLE_SLOTS"
974
+ ),
975
+ )
976
+ dc_args = ["slots=base_t.ENABLE_SLOTS"]
780
977
  if stype.is_kw_only():
781
978
  dc_args.append("kw_only=True")
782
979
  if stype.is_hashable:
783
980
  dc_args.extend(["frozen=True", "eq=True"])
784
981
  if len(dc_args) > 0:
785
- dataclass += f'({", ".join(dc_args)})'
982
+ dataclass += f"({', '.join(dc_args)}) # type: ignore[literal-required]"
786
983
 
787
984
  ctx.out.write(f"{dataclass}\n")
788
985
  ctx.out.write(class_out.getvalue())
789
986
 
790
987
 
791
- def _emit_generic(ctx: Context, generic: Optional[str]) -> None:
792
- if generic is not None:
988
+ def _emit_generics(ctx: Context, generics: list[str]) -> None:
989
+ for generic in generics:
793
990
  ctx.out.write(f'{generic} = typing.TypeVar("{generic}")\n')
794
991
  ctx.out.write(f"{LINE_BREAK}{LINE_BREAK}")
795
992
 
@@ -827,13 +1024,22 @@ base_name_map = {
827
1024
 
828
1025
  def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
829
1026
  if isinstance(stype, builder.SpecTypeInstance):
830
- params = ", ".join([refer_to(ctx, p) for p in stype.parameters])
1027
+ params = [refer_to(ctx, p) for p in stype.parameters]
1028
+
1029
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_union):
1030
+ if len(stype.parameters) == 1:
1031
+ return f"typing.Union[{params[0]}]"
1032
+ return " | ".join(params)
831
1033
 
832
1034
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_readonly_array):
833
- assert len(stype.parameters) == 1, "Read Only Array takes one parameter"
834
- params = f"{params}, ..."
1035
+ assert len(params) == 1, "Read Only Array takes one parameter"
1036
+ return f"tuple[{params[0]}, ...]"
835
1037
 
836
- return f"{refer_to(ctx, stype.defn_type)}[{params}]"
1038
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
1039
+ assert len(params) == 1, "Optional only takes one parameter"
1040
+ return f"{params[0]} | None"
1041
+
1042
+ return f"{refer_to(ctx, stype.defn_type)}[{', '.join(params)}]"
837
1043
 
838
1044
  if isinstance(stype, builder.SpecTypeLiteralWrapper):
839
1045
  return _emit_value(ctx, stype.value_type, stype.value)
@@ -859,23 +1065,21 @@ def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
859
1065
  SpecEndpoint = builder.SpecEndpoint
860
1066
 
861
1067
 
862
- def _route_identifier(endpoint: builder.SpecEndpoint) -> tuple[str, str, str]:
863
- return (endpoint.path_dirname, endpoint.path_basename, endpoint.method)
864
-
865
-
866
1068
  def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
867
1069
  for endpoint_root in builder.api_endpoints:
868
1070
  endpoints: list[SpecEndpoint] = []
869
1071
  output = config.routes_output.get(endpoint_root)
870
1072
  if output is None:
871
1073
  continue
1074
+ last_endpoint: SpecEndpoint | None = None
872
1075
  for namespace in builder.namespaces.values():
873
1076
  endpoint = namespace.endpoint
1077
+ last_endpoint = endpoint
874
1078
  if endpoint is None:
875
1079
  continue
876
- if endpoint.root != endpoint_root:
1080
+ if endpoint_root not in endpoint.path_per_api_endpoint:
877
1081
  continue
878
- if endpoint.function is None:
1082
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
879
1083
  continue
880
1084
 
881
1085
  endpoints.append(endpoint)
@@ -888,25 +1092,38 @@ def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
888
1092
  from main.site.framework.types import StaticRouteType
889
1093
  """
890
1094
  )
1095
+
1096
+ def _route_identifier(endpoint: SpecEndpoint) -> tuple[str, str, str]:
1097
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1098
+ return (
1099
+ endpoint_specific_path.path_dirname,
1100
+ endpoint_specific_path.path_basename,
1101
+ endpoint.method,
1102
+ )
1103
+
891
1104
  sorted_endpoints = sorted(endpoints, key=_route_identifier)
892
1105
 
893
- assert len(endpoints) == len(
894
- set(map(_route_identifier, endpoints))
895
- ), "Endpoints are not unique"
1106
+ assert len(endpoints) == len(set(map(_route_identifier, endpoints))), (
1107
+ "Endpoints are not unique"
1108
+ )
896
1109
 
897
1110
  path_set = set()
898
1111
  for endpoint in sorted_endpoints:
899
- assert endpoint.function
900
- func_bits = endpoint.function.split(".")
1112
+ last_endpoint = endpoint
1113
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
1114
+ assert endpoint_function_path.function
1115
+ func_bits = endpoint_function_path.function.split(".")
901
1116
  path = ".".join(func_bits[:-1])
902
1117
  if path in path_set:
903
1118
  continue
904
1119
  path_set.add(path)
905
1120
  static_out.write(f"import {path}\n")
906
1121
 
1122
+ assert last_endpoint is not None
1123
+
907
1124
  static_out.write(
908
1125
  f"""
909
- ROUTE_PREFIX = "/{endpoint.path_root}"
1126
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
910
1127
 
911
1128
  ROUTES: list[StaticRouteType] = [
912
1129
  """
@@ -920,20 +1137,21 @@ ROUTES: list[StaticRouteType] = [
920
1137
 
921
1138
  from main.site.framework.types import DynamicRouteType
922
1139
 
923
- ROUTE_PREFIX = "/{endpoint.path_root}"
1140
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
924
1141
 
925
1142
  ROUTES: list[DynamicRouteType] = [
926
1143
  """
927
1144
  )
928
1145
 
929
1146
  for endpoint in sorted_endpoints:
1147
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
930
1148
  dynamic_out.write(
931
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", "{endpoint.function}", ["{endpoint.method.upper()}"]),\n'
1149
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", "{endpoint_function_path.function}", ["{endpoint.method.upper()}"]),\n'
932
1150
  )
933
1151
 
934
- assert endpoint.function
1152
+ assert endpoint_function_path.function
935
1153
  static_out.write(
936
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", {endpoint.function}, ["{endpoint.method.upper()}"]),\n'
1154
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", {endpoint_function_path.function}, ["{endpoint.method.upper()}"]),\n'
937
1155
  )
938
1156
 
939
1157
  dynamic_out.write(f"{MODIFY_NOTICE}]\n")
@@ -949,15 +1167,21 @@ def _emit_namespace_imports(
949
1167
  *,
950
1168
  out: io.StringIO,
951
1169
  namespaces: set[builder.SpecNamespace],
952
- from_namespace: Optional[builder.SpecNamespace],
1170
+ from_namespace: builder.SpecNamespace | None,
953
1171
  config: PythonConfig,
1172
+ skip_non_sdk: bool = False,
954
1173
  ) -> None:
955
1174
  for ns in sorted(
956
1175
  namespaces,
957
1176
  key=lambda name: _resolve_namespace_name(name),
958
1177
  ):
1178
+ if (
1179
+ skip_non_sdk
1180
+ and ns.endpoint is not None
1181
+ and ns.endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
1182
+ ):
1183
+ continue
959
1184
  resolved = _resolve_namespace_name(ns)
960
- ref = _resolve_namespace_ref(ns)
961
1185
  if ns.endpoint is not None:
962
1186
  import_alias = "_".join(ns.path[2:]) + "_t"
963
1187
  out.write(
@@ -969,7 +1193,7 @@ def _emit_namespace_imports(
969
1193
  else:
970
1194
  from_path = config.types_package
971
1195
 
972
- out.write(f"from {from_path} import {resolved} as {ref}\n")
1196
+ out.write(f"from {from_path} import {resolved}\n")
973
1197
 
974
1198
 
975
1199
  def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
@@ -981,8 +1205,8 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
981
1205
  return None
982
1206
  enum_out = io.StringIO()
983
1207
  enum_out.write(f"{LINT_HEADER}{MODIFY_NOTICE}\n")
984
- enum_out.write("from typing import Literal, Union\n")
985
- enum_out.write("from pkgs.strenum_compat import StrEnum\n")
1208
+ enum_out.write("import typing\n")
1209
+ enum_out.write("from enum import StrEnum\n")
986
1210
 
987
1211
  ctx = TrackingContext()
988
1212
  # In this context the propername
@@ -998,11 +1222,11 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
998
1222
  known_keys = []
999
1223
  enum_out.write("\nENUM_MAP: dict[str, type[StrEnum]] = {\n")
1000
1224
  for key in sorted(named_enums.keys()):
1001
- enum_out.write(f'"{key}": {named_enums[key]},\n')
1002
- known_keys.append(f'Literal["{key}"]')
1225
+ enum_out.write(f'{INDENT}"{key}": {named_enums[key]},\n')
1226
+ known_keys.append(f'"{key}"')
1003
1227
  enum_out.write(f"}}\n{MODIFY_NOTICE}\n")
1004
1228
 
1005
- enum_out.write(f"\nKnownEnumsType = Union[\n{INDENT}")
1229
+ enum_out.write(f"\nKnownEnumsType = typing.Literal[\n{INDENT}")
1006
1230
  enum_out.write(f",\n{INDENT}".join(known_keys))
1007
1231
  enum_out.write(f"\n]\n{MODIFY_NOTICE}\n")
1008
1232
 
@@ -1019,21 +1243,36 @@ def _emit_api_stubs(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1019
1243
 
1020
1244
  if endpoint is None:
1021
1245
  continue
1022
- if endpoint.root != endpoint_root:
1246
+ if endpoint_root not in endpoint.path_per_api_endpoint:
1023
1247
  continue
1024
- if endpoint.function is None:
1248
+
1249
+ endpoint_function = endpoint.path_per_api_endpoint[endpoint_root].function
1250
+ if endpoint_function is None:
1025
1251
  continue
1026
1252
 
1027
- module_dir, file_name, func_name = endpoint.function.rsplit(".", 2)
1253
+ module_dir, file_name, _func_name = endpoint_function.rsplit(".", 2)
1028
1254
  module_path = os.path.abspath(module_dir.replace(".", "/"))
1029
1255
  api_stub_file = f"{module_path}/{file_name}.py"
1030
1256
  if os.path.isfile(api_stub_file):
1031
1257
  continue
1032
- _create_api_stub(api_stub_file, file_name, endpoint, config)
1258
+ _create_api_stub(
1259
+ api_stub_file=api_stub_file,
1260
+ file_name=file_name,
1261
+ endpoint=endpoint,
1262
+ config=config,
1263
+ endpoint_root=endpoint_root,
1264
+ top_namespace=builder.top_namespace,
1265
+ )
1033
1266
 
1034
1267
 
1035
1268
  def _create_api_stub(
1036
- api_stub_file: str, file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1269
+ *,
1270
+ api_stub_file: str,
1271
+ file_name: str,
1272
+ endpoint: SpecEndpoint,
1273
+ config: PythonConfig,
1274
+ endpoint_root: str,
1275
+ top_namespace: str,
1037
1276
  ) -> None:
1038
1277
  assert (
1039
1278
  endpoint.method == builder.RouteMethod.post
@@ -1041,7 +1280,13 @@ def _create_api_stub(
1041
1280
  or endpoint.method == builder.RouteMethod.delete
1042
1281
  or endpoint.method == builder.RouteMethod.patch
1043
1282
  )
1044
- api_out = _create_api_function(file_name, endpoint, config)
1283
+ api_out = _create_api_function(
1284
+ file_name=file_name,
1285
+ endpoint=endpoint,
1286
+ config=config,
1287
+ endpoint_root=endpoint_root,
1288
+ top_namespace=top_namespace,
1289
+ )
1045
1290
  util.rewrite_file(api_stub_file, api_out.getvalue())
1046
1291
 
1047
1292
 
@@ -1050,15 +1295,22 @@ WRAP_ARGS_END = "\n"
1050
1295
 
1051
1296
 
1052
1297
  def _create_api_function(
1053
- file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1298
+ *,
1299
+ file_name: str,
1300
+ endpoint: SpecEndpoint,
1301
+ config: PythonConfig,
1302
+ endpoint_root: str,
1303
+ top_namespace: str,
1054
1304
  ) -> io.StringIO:
1305
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1306
+ assert endpoint_specific_path is not None
1055
1307
  api_out = io.StringIO()
1056
1308
  python_api_type_root = f"{config.types_package}.api"
1057
- dot_dirname = endpoint.path_dirname.replace("/", ".")
1309
+ dot_dirname = endpoint_specific_path.path_dirname.replace("/", ".")
1058
1310
  api_import = (
1059
1311
  f"{python_api_type_root}.{dot_dirname}.{file_name}"
1060
1312
  if dot_dirname != ""
1061
- else f"{python_api_type_root}.{endpoint.path_basename}"
1313
+ else f"{python_api_type_root}.{endpoint_specific_path.path_basename}"
1062
1314
  )
1063
1315
 
1064
1316
  if endpoint.method == builder.RouteMethod.post:
@@ -1070,7 +1322,20 @@ def _create_api_function(
1070
1322
  elif endpoint.method == builder.RouteMethod.patch:
1071
1323
  validated_method = "validated_patch"
1072
1324
 
1073
- ruff_requires_wrap = len(endpoint.path_basename) > 14
1325
+ ruff_requires_wrap = len(endpoint_specific_path.path_basename) > 14
1326
+
1327
+ account_type = (
1328
+ endpoint_specific_path.root
1329
+ if endpoint_specific_path.root not in ["external", "portal"]
1330
+ else "materials"
1331
+ )
1332
+
1333
+ endpoint_path_name = (
1334
+ ENDPOINT_PATH_ALTERNATE
1335
+ if len(endpoint.path_per_api_endpoint.keys()) > 1
1336
+ and endpoint_specific_path.root != top_namespace
1337
+ else ENDPOINT_PATH
1338
+ )
1074
1339
 
1075
1340
  api_out.write(
1076
1341
  f"""import {api_import} as api
@@ -1078,8 +1343,8 @@ from main.db.session import Session, SessionMaker
1078
1343
  from main.site.decorators import APIError, APIResponse, {validated_method}
1079
1344
 
1080
1345
 
1081
- @{validated_method}(api.ENDPOINT_PATH, "{endpoint.root}", api.Arguments)
1082
- 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]:
1346
+ @{validated_method}(api.{endpoint_path_name}, "{account_type}", api.Arguments)
1347
+ 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]:
1083
1348
  with Session(client_sm) as session:
1084
1349
  # return APIResponse(data=api.Data())
1085
1350
  pass
@@ -1101,7 +1366,7 @@ def _emit_api_argument_lookup(
1101
1366
  for endpoint_root in builder.api_endpoints:
1102
1367
  routes_output = config.routes_output[endpoint_root]
1103
1368
 
1104
- imports = []
1369
+ imports = ["import dataclasses"]
1105
1370
  mappings = []
1106
1371
  for namespace in sorted(
1107
1372
  builder.namespaces.values(),
@@ -1111,17 +1376,38 @@ def _emit_api_argument_lookup(
1111
1376
 
1112
1377
  if endpoint is None:
1113
1378
  continue
1114
- if endpoint.root != endpoint_root:
1379
+ if endpoint_root not in endpoint.path_per_api_endpoint:
1115
1380
  continue
1116
- if endpoint.function is None:
1381
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
1117
1382
  continue
1118
- if "Arguments" not in namespace.types:
1383
+ if "Arguments" not in namespace.types or "Data" not in namespace.types:
1119
1384
  continue
1120
1385
 
1121
1386
  import_alias = "_".join(namespace.path[1:])
1122
1387
  api_import = f"{config.types_package}.{'.'.join(namespace.path)}"
1123
1388
  imports.append(f"import {api_import} as {import_alias}")
1124
- mappings.append(f"{import_alias}.ENDPOINT_PATH: {import_alias}.Arguments")
1389
+
1390
+ route_group = (
1391
+ f'"{endpoint.route_group}"'
1392
+ if endpoint.route_group is not None
1393
+ else "None"
1394
+ )
1395
+ account_type = (
1396
+ f'"{endpoint.account_type}"'
1397
+ if endpoint.account_type is not None
1398
+ else "None"
1399
+ )
1400
+
1401
+ mapping = f"{INDENT}ApiEndpointKey(route={import_alias}.ENDPOINT_PATH, method={import_alias}.ENDPOINT_METHOD): ApiEndpointSpec(\n"
1402
+ mapping += f"{INDENT}{INDENT}arguments_type={import_alias}.Arguments,\n"
1403
+ mapping += f"{INDENT}{INDENT}data_type={import_alias}.Data,\n"
1404
+ mapping += f"{INDENT}{INDENT}route_group={route_group},\n"
1405
+ mapping += f"{INDENT}{INDENT}account_type={account_type},\n"
1406
+ mapping += f"{INDENT}{INDENT}route={import_alias}.ENDPOINT_PATH,\n"
1407
+ mapping += f'{INDENT}{INDENT}handler="{endpoint.path_per_api_endpoint[endpoint_root].function}",\n'
1408
+ mapping += f"{INDENT}{INDENT}method={import_alias}.ENDPOINT_METHOD,\n"
1409
+ mapping += f"{INDENT})"
1410
+ mappings.append(mapping)
1125
1411
 
1126
1412
  argument_lookup_out = io.StringIO()
1127
1413
  argument_lookup_out.write(MODIFY_NOTICE)
@@ -1129,8 +1415,26 @@ def _emit_api_argument_lookup(
1129
1415
  argument_lookup_out.write(
1130
1416
  f"""{LINE_BREAK.join(imports)}
1131
1417
 
1132
- {API_ARGUMENTS_NAME} = {{
1133
- {f",{LINE_BREAK}{INDENT}".join(mappings)},
1418
+
1419
+ @dataclasses.dataclass(kw_only=True, frozen=True)
1420
+ class ApiEndpointKey:
1421
+ method: str
1422
+ route: str
1423
+
1424
+
1425
+ @dataclasses.dataclass(kw_only=True)
1426
+ class ApiEndpointSpec[AT, DT]:
1427
+ route: str
1428
+ arguments_type: type[AT]
1429
+ data_type: type[DT]
1430
+ route_group: str | None
1431
+ account_type: str | None
1432
+ handler: str
1433
+ method: str
1434
+
1435
+
1436
+ {API_ARGUMENTS_NAME}: dict[ApiEndpointKey, ApiEndpointSpec] = {{
1437
+ {f",{LINE_BREAK}".join(mappings)},
1134
1438
  }}
1135
1439
 
1136
1440
  __all__ = ["{API_ARGUMENTS_NAME}"]
@@ -1146,14 +1450,13 @@ __all__ = ["{API_ARGUMENTS_NAME}"]
1146
1450
  CLIENT_CLASS_FILENAME = "client_base"
1147
1451
  CLIENT_CLASS_IMPORTS = [
1148
1452
  "from abc import ABC, abstractmethod",
1149
- "from dataclasses import dataclass",
1453
+ "import dataclasses",
1150
1454
  ]
1151
1455
  ASYNC_BATCH_PROCESSOR_FILENAME = "async_batch_processor"
1152
- ASYNC_BATCH_PROCESSOR_IMPORTS = [
1456
+ ASYNC_BATCH_PROCESSOR_BASE_IMPORTS = [
1153
1457
  "import uuid",
1154
1458
  "from abc import ABC, abstractmethod",
1155
- "from dataclasses import dataclass",
1156
- "from pkgs.serialization_util.serialization_helpers import serialize_for_api",
1459
+ "from pkgs.serialization_util import serialize_for_api",
1157
1460
  ]
1158
1461
 
1159
1462
 
@@ -1165,7 +1468,9 @@ def _emit_async_batch_processor(
1165
1468
 
1166
1469
  async_batch_processor_out = io.StringIO()
1167
1470
  ctx = Context(
1168
- out=io.StringIO(), namespace=builder.SpecNamespace("async_batch_processor")
1471
+ out=io.StringIO(),
1472
+ namespace=builder.SpecNamespace("async_batch_processor"),
1473
+ builder=spec_builder,
1169
1474
  )
1170
1475
 
1171
1476
  for namespace in sorted(
@@ -1186,8 +1491,11 @@ def _emit_async_batch_processor(
1186
1491
  config=config,
1187
1492
  )
1188
1493
 
1494
+ imports = ASYNC_BATCH_PROCESSOR_BASE_IMPORTS.copy()
1495
+ if ctx.use_dataclass:
1496
+ imports.append("import dataclasses")
1189
1497
  async_batch_processor_out.write(
1190
- f"""{LINE_BREAK.join(ASYNC_BATCH_PROCESSOR_IMPORTS)}
1498
+ f"""{LINE_BREAK.join(imports)}
1191
1499
 
1192
1500
 
1193
1501
  class AsyncBatchProcessorBase(ABC):
@@ -1215,7 +1523,11 @@ def _emit_client_class(
1215
1523
  return
1216
1524
 
1217
1525
  client_base_out = io.StringIO()
1218
- ctx = Context(out=io.StringIO(), namespace=builder.SpecNamespace("client_base"))
1526
+ ctx = Context(
1527
+ out=io.StringIO(),
1528
+ builder=spec_builder,
1529
+ namespace=builder.SpecNamespace("client_base"),
1530
+ )
1219
1531
  for namespace in sorted(
1220
1532
  spec_builder.namespaces.values(),
1221
1533
  key=lambda ns: _resolve_namespace_name(ns),
@@ -1232,6 +1544,7 @@ def _emit_client_class(
1232
1544
  namespaces=ctx.namespaces,
1233
1545
  from_namespace=None,
1234
1546
  config=config,
1547
+ skip_non_sdk=True,
1235
1548
  )
1236
1549
 
1237
1550
  client_base_out.write(
@@ -1240,11 +1553,12 @@ def _emit_client_class(
1240
1553
  DT = typing.TypeVar("DT")
1241
1554
 
1242
1555
 
1243
- @dataclass(kw_only=True)
1556
+ @dataclasses.dataclass(kw_only=True)
1244
1557
  class APIRequest:
1245
1558
  method: str
1246
1559
  endpoint: str
1247
1560
  args: typing.Any
1561
+ request_options: {refer_to(ctx=ctx, stype=REQUEST_OPTIONS_STYPE)} | None = None
1248
1562
 
1249
1563
 
1250
1564
  class ClientMethods(ABC):