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
pkgs/type_spec/builder.py CHANGED
@@ -10,12 +10,36 @@ import re
10
10
  from collections import defaultdict
11
11
  from dataclasses import MISSING, dataclass
12
12
  from enum import Enum, StrEnum, auto
13
- from typing import Any, Optional, Self
13
+ from typing import Any, Self
14
14
 
15
15
  from . import util
16
- from .util import parse_type_str, unused
16
+ from .builder_types import CrossOutputPaths
17
+ from .non_discriminated_union_exceptions import NON_DISCRIMINATED_UNION_EXCEPTIONS
18
+ from .util import parse_type_str
17
19
 
18
20
  RawDict = dict[Any, Any]
21
+ EndpointKey = str
22
+
23
+
24
+ class PathMapping(StrEnum):
25
+ NO_MAPPING = "no_mapping"
26
+ DEFAULT_MAPPING = "default_mapping"
27
+
28
+
29
+ @dataclass(kw_only=True)
30
+ class APIEndpointInfo:
31
+ root_path: str
32
+ path_mapping: PathMapping
33
+
34
+
35
+ class StabilityLevel(StrEnum):
36
+ """These are currently used for open api,
37
+ see: https://github.com/Tufin/oasdiff/blob/main/docs/STABILITY.md
38
+ """
39
+
40
+ draft = "draft"
41
+ beta = "beta"
42
+ stable = "stable"
19
43
 
20
44
 
21
45
  class PropertyExtant(StrEnum):
@@ -36,7 +60,7 @@ class PropertyConvertValue(StrEnum):
36
60
  @dataclass
37
61
  class SpecProperty:
38
62
  name: str
39
- label: Optional[str]
63
+ label: str | None
40
64
  spec_type: SpecType
41
65
  extant: PropertyExtant
42
66
  convert_value: PropertyConvertValue
@@ -50,6 +74,7 @@ class SpecProperty:
50
74
  # Holds extra information that will be emitted along with type_info. The builder knows nothing
51
75
  # about the contents of this information.
52
76
  ext_info: Any = None
77
+ explicit_default: bool = False
53
78
 
54
79
 
55
80
  class NameCase(StrEnum):
@@ -102,6 +127,8 @@ class DefnTypeName(StrEnum):
102
127
  s_string_enum = "StringEnum"
103
128
  # a particular literal value
104
129
  s_string_literal = "_StringLiteral"
130
+ # A union of several other types
131
+ s_union = "Union"
105
132
 
106
133
 
107
134
  base_namespace_name = "base"
@@ -195,6 +222,7 @@ class SpecEndpointExample:
195
222
 
196
223
  @dataclass(kw_only=True)
197
224
  class SpecGuide:
225
+ ref_name: str
198
226
  title: str
199
227
  markdown_content: str
200
228
  html_content: str
@@ -241,7 +269,7 @@ class SpecTypeLiteralWrapper(SpecType):
241
269
  return [self.value_type]
242
270
 
243
271
 
244
- def unwrap_literal_type(stype: SpecType) -> Optional[SpecTypeLiteralWrapper]:
272
+ def unwrap_literal_type(stype: SpecType) -> SpecTypeLiteralWrapper | None:
245
273
  if isinstance(stype, SpecTypeInstance) and stype.defn_type.is_base_type(
246
274
  BaseTypeName.s_literal
247
275
  ):
@@ -269,7 +297,7 @@ class SpecTypeDefn(SpecType):
269
297
  ) -> None:
270
298
  self.namespace = namespace
271
299
  self.name = name
272
- self.label: Optional[str] = None
300
+ self.label: str | None = None
273
301
 
274
302
  self.is_predefined = is_predefined
275
303
  self.name_case = NameCase.convert
@@ -279,6 +307,8 @@ class SpecTypeDefn(SpecType):
279
307
  self._is_value_converted = _is_value_converted
280
308
  self._is_value_to_string = False
281
309
  self._is_valid_parameter = True
310
+ self._is_dynamic_allowed = False
311
+ self._default_extant: PropertyExtant | None = None
282
312
  self.ext_info: Any = None
283
313
 
284
314
  def is_value_converted(self) -> bool:
@@ -290,20 +320,43 @@ class SpecTypeDefn(SpecType):
290
320
  def is_valid_parameter(self) -> bool:
291
321
  return self._is_valid_parameter
292
322
 
323
+ def is_dynamic_allowed(self) -> bool:
324
+ return self._is_dynamic_allowed
325
+
293
326
  def is_base_type(self, type_: BaseTypeName) -> bool:
294
327
  return self.is_base and self.name == type_
295
328
 
329
+ def can_process(self, builder: SpecBuilder, data: RawDict) -> bool:
330
+ return True
331
+
296
332
  @abc.abstractmethod
297
333
  def process(self, builder: SpecBuilder, data: RawDict) -> None: ...
298
334
 
299
335
  def base_process(
300
336
  self, builder: SpecBuilder, data: RawDict, extra_names: list[str]
301
337
  ) -> None:
302
- util.check_fields(data, ["ext_info", "label"] + extra_names)
338
+ util.check_fields(
339
+ data,
340
+ [
341
+ "ext_info",
342
+ "label",
343
+ "is_dynamic_allowed",
344
+ "default_extant",
345
+ ]
346
+ + extra_names,
347
+ )
303
348
 
304
349
  self.ext_info = data.get("ext_info")
305
350
  self.label = data.get("label")
306
351
 
352
+ is_dynamic_allowed = data.get("is_dynamic_allowed", False)
353
+ assert isinstance(is_dynamic_allowed, bool)
354
+ self._is_dynamic_allowed = is_dynamic_allowed
355
+
356
+ default_extant = data.get("default_extant")
357
+ if default_extant is not None:
358
+ self._default_extant = PropertyExtant(default_extant)
359
+
307
360
  def _process_property(
308
361
  self, builder: SpecBuilder, spec_name: str, data: RawDict
309
362
  ) -> SpecProperty:
@@ -322,18 +375,18 @@ class SpecTypeDefn(SpecType):
322
375
  ],
323
376
  )
324
377
  try:
325
- extant_type = data.get("extant")
378
+ extant_type_str = data.get("extant")
379
+ extant_type = (
380
+ PropertyExtant(extant_type_str) if extant_type_str is not None else None
381
+ )
382
+ extant = extant_type or self._default_extant
326
383
  if spec_name.endswith("?"):
327
- if extant_type is not None:
384
+ if extant is not None:
328
385
  raise Exception("cannot specify extant with ?")
329
386
  extant = PropertyExtant.optional
330
387
  name = spec_name[:-1]
331
388
  else:
332
- extant = (
333
- PropertyExtant.required
334
- if extant_type is None
335
- else PropertyExtant(extant_type)
336
- )
389
+ extant = extant or PropertyExtant.required
337
390
  name = spec_name
338
391
 
339
392
  property_name_case = self.name_case
@@ -342,9 +395,9 @@ class SpecTypeDefn(SpecType):
342
395
  property_name_case = NameCase(name_case_raw)
343
396
 
344
397
  if property_name_case != NameCase.preserve:
345
- assert util.is_valid_property_name(
346
- name
347
- ), f"{name} is not a valid property name"
398
+ assert util.is_valid_property_name(name), (
399
+ f"{name} is not a valid property name"
400
+ )
348
401
 
349
402
  data_type = data.get("type")
350
403
  builder.ensure(data_type is not None, "missing `type` entry")
@@ -355,6 +408,7 @@ class SpecTypeDefn(SpecType):
355
408
  ptype = builder.parse_type(self.namespace, data_type, scope=self)
356
409
 
357
410
  default_spec = data.get("default", MISSING)
411
+ explicit_default = default_spec != MISSING
358
412
  if default_spec == MISSING:
359
413
  has_default = False
360
414
  default = None
@@ -362,7 +416,10 @@ class SpecTypeDefn(SpecType):
362
416
  has_default = True
363
417
  # IMPROVE: check the type against the ptype
364
418
  default = default_spec
365
-
419
+ if extant == PropertyExtant.missing and explicit_default:
420
+ raise Exception(
421
+ f"cannot have extant missing and default for property {name}"
422
+ )
366
423
  parse_require = False
367
424
  literal = unwrap_literal_type(ptype)
368
425
  if literal is not None:
@@ -385,6 +442,7 @@ class SpecTypeDefn(SpecType):
385
442
  parse_require=parse_require,
386
443
  desc=data.get("desc", None),
387
444
  ext_info=ext_info,
445
+ explicit_default=explicit_default,
388
446
  )
389
447
  finally:
390
448
  builder.pop_where()
@@ -422,7 +480,7 @@ class SpecTypeGenericParameter(SpecType):
422
480
 
423
481
 
424
482
  class SpecTypeDefnObject(SpecTypeDefn):
425
- base: Optional[SpecTypeDefnObject]
483
+ base: SpecTypeDefnObject | None
426
484
  parameters: list[str]
427
485
 
428
486
  def __init__(
@@ -430,7 +488,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
430
488
  namespace: SpecNamespace,
431
489
  name: str,
432
490
  *,
433
- parameters: Optional[list[str]] = None,
491
+ parameters: list[str] | None = None,
434
492
  is_base: bool = False,
435
493
  is_predefined: bool = False,
436
494
  is_hashable: bool = False,
@@ -447,7 +505,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
447
505
  self.parameters = parameters if parameters is not None else []
448
506
  self.is_hashable = is_hashable
449
507
  self.base = None
450
- self.properties: Optional[dict[str, SpecProperty]] = None
508
+ self.properties: dict[str, SpecProperty] | None = None
451
509
  self._kw_only: bool = True
452
510
  self.desc: str | None = None
453
511
 
@@ -518,13 +576,8 @@ class SpecTypeDefnObject(SpecTypeDefn):
518
576
  base_type: list[SpecType] = [self.base] if self.base is not None else []
519
577
  return base_type + prop_types
520
578
 
521
- def get_generic(self) -> Optional[str]:
522
- if len(self.parameters) > 0:
523
- assert (
524
- len(self.parameters) == 1
525
- ), "Only single generic parameters current supported"
526
- return self.parameters[0]
527
- return None
579
+ def get_generics(self) -> list[str]:
580
+ return self.parameters
528
581
 
529
582
 
530
583
  class SpecTypeDefnAlias(SpecTypeDefn):
@@ -546,13 +599,75 @@ class SpecTypeDefnAlias(SpecTypeDefn):
546
599
  super().base_process(builder, data, ["type", "desc", "alias", "discriminator"])
547
600
  self.alias = builder.parse_type(self.namespace, data["alias"])
548
601
  self.desc = data.get("desc", None)
549
- # Should be limited to Union type aliases
550
602
  self.discriminator = data.get("discriminator", None)
551
603
 
552
604
  def get_referenced_types(self) -> list[SpecType]:
553
605
  return [self.alias]
554
606
 
555
607
 
608
+ class SpecTypeDefnUnion(SpecTypeDefn):
609
+ def __init__(self, namespace: SpecNamespace, name: str) -> None:
610
+ super().__init__(namespace, name)
611
+ self.discriminator: str | None = None
612
+ self.types: list[SpecType] = []
613
+ self._alias_type: SpecType | None = None
614
+ self.discriminator_map: dict[str, SpecType] | None = None
615
+ self.desc: str | None = None
616
+
617
+ def process(self, builder: SpecBuilder, data: RawDict) -> None:
618
+ super().base_process(builder, data, ["type", "desc", "types", "discriminator"])
619
+
620
+ self.desc = data.get("desc", None)
621
+ self.discriminator = data.get("discriminator", None)
622
+
623
+ for sub_type_str in data["types"]:
624
+ sub_type = builder.parse_type(self.namespace, sub_type_str)
625
+ self.types.append(sub_type)
626
+
627
+ base_type = builder.namespaces[base_namespace_name].types[BaseTypeName.s_union]
628
+ self._backing_type = SpecTypeInstance(base_type, self.types)
629
+
630
+ if self.discriminator is not None:
631
+ self.discriminator_map = {}
632
+ for sub_type in self.types:
633
+ builder.push_where(sub_type.name)
634
+ assert isinstance(sub_type, SpecTypeDefnObject), (
635
+ "union-type-must-be-object"
636
+ )
637
+ assert sub_type.properties is not None
638
+ discriminator_type = sub_type.properties.get(self.discriminator)
639
+ assert discriminator_type is not None, (
640
+ f"missing-discriminator-field: {sub_type}"
641
+ )
642
+ prop_type = unwrap_literal_type(discriminator_type.spec_type)
643
+ assert prop_type is not None
644
+ assert prop_type.is_value_to_string()
645
+ value_type = prop_type.value_type
646
+ if isinstance(value_type, SpecTypeDefnStringEnum):
647
+ assert isinstance(prop_type.value, str)
648
+ discriminant = value_type.values[prop_type.value].value
649
+ else:
650
+ discriminant = str(prop_type.value)
651
+ assert discriminant not in self.discriminator_map, (
652
+ f"duplicated-discriminant, {discriminant} in {sub_type}"
653
+ )
654
+ self.discriminator_map[discriminant] = sub_type
655
+
656
+ builder.pop_where()
657
+ elif (
658
+ f"{self.namespace.name}.{self.name}"
659
+ not in NON_DISCRIMINATED_UNION_EXCEPTIONS
660
+ ):
661
+ raise Exception(f"union requires a discriminator: {self.name}")
662
+
663
+ def get_referenced_types(self) -> list[SpecType]:
664
+ return self.types
665
+
666
+ def get_backing_type(self) -> SpecType:
667
+ assert self._backing_type is not None
668
+ return self._backing_type
669
+
670
+
556
671
  class SpecTypeDefnExternal(SpecTypeDefn):
557
672
  external_map: dict[str, str]
558
673
 
@@ -582,7 +697,7 @@ class SpecTypeDefnExternal(SpecTypeDefn):
582
697
  class StringEnumEntry:
583
698
  name: str
584
699
  value: str
585
- label: Optional[str] = None
700
+ label: str | None = None
586
701
  deprecated: bool = False
587
702
 
588
703
 
@@ -598,17 +713,32 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
598
713
  )
599
714
  self.values: dict[str, StringEnumEntry] = {}
600
715
  self.desc: str | None = None
601
- self.sql_type_name: Optional[str] = None
716
+ self.sql_type_name: str | None = None
602
717
  self.emit_id_source = False
718
+ self.source_enums: list[SpecType] = []
719
+
720
+ def can_process(self, builder: SpecBuilder, data: dict[Any, Any]) -> bool:
721
+ source_enums = data.get("source_enums")
722
+ try:
723
+ for sub_type_str in source_enums or []:
724
+ sub_type = builder.parse_type(self.namespace, sub_type_str)
725
+ assert isinstance(sub_type, SpecTypeDefnStringEnum)
726
+ assert len(sub_type.values) > 0
727
+ except AssertionError:
728
+ return False
729
+ return super().can_process(builder, data)
603
730
 
604
731
  def process(self, builder: SpecBuilder, data: RawDict) -> None:
605
732
  super().base_process(
606
- builder, data, ["type", "desc", "values", "name_case", "sql", "emit"]
733
+ builder,
734
+ data,
735
+ ["type", "desc", "values", "name_case", "sql", "emit", "source_enums"],
607
736
  )
608
737
  self.name_case = NameCase(data.get("name_case", "convert"))
609
738
  self.values = {}
610
- data_values = data["values"]
739
+ data_values = data.get("values")
611
740
  self.desc = data.get("desc", None)
741
+ source_enums = data.get("source_enums", None)
612
742
  if isinstance(data_values, dict):
613
743
  for name, value in data_values.items():
614
744
  builder.push_where(name)
@@ -621,6 +751,8 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
621
751
  builder.ensure(
622
752
  isinstance(enum_value, str), "enum value should be string"
623
753
  )
754
+ assert isinstance(enum_value, str)
755
+
624
756
  deprecated = value.get("deprecated", False)
625
757
  builder.ensure(
626
758
  isinstance(deprecated, bool),
@@ -646,10 +778,13 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
646
778
  elif isinstance(data_values, list):
647
779
  for value in data_values:
648
780
  if value in self.values:
649
- raise Exception("duplicate value in typespec enum", self.name, value)
781
+ raise Exception(
782
+ "duplicate value in typespec enum", self.name, value
783
+ )
650
784
  self.values[value] = StringEnumEntry(name=value, value=value)
651
785
  else:
652
- raise Exception("unsupported values type")
786
+ if source_enums is None or data_values is not None:
787
+ raise Exception("unsupported values type")
653
788
 
654
789
  sql_data = data.get("sql")
655
790
  if sql_data is not None:
@@ -671,14 +806,24 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
671
806
  builder.ensure(
672
807
  entry.label is not None, f"need-label-for-id-source:{entry.name}"
673
808
  )
809
+ for sub_type_str in source_enums or []:
810
+ sub_type = builder.parse_type(self.namespace, sub_type_str)
811
+ self.source_enums.append(sub_type)
812
+
813
+ for sub_type in self.source_enums:
814
+ builder.push_where(sub_type.name)
815
+ if isinstance(sub_type, SpecTypeDefnStringEnum):
816
+ self.values.update(sub_type.values)
817
+ builder.pop_where()
674
818
 
675
819
  def get_referenced_types(self) -> list[SpecType]:
676
- return []
820
+ return self.source_enums
677
821
 
678
822
 
679
823
  TOKEN_ENDPOINT = "$endpoint"
680
824
  TOKEN_EMIT_IO_TS = "$emit_io_ts"
681
825
  TOKEN_EMIT_TYPE_INFO = "$emit_type_info"
826
+ TOKEN_EMIT_TYPE_INFO_PYTHON = "$emit_type_info_python"
682
827
  # The import token is only for explicit ordering of the files, to process constants
683
828
  # and enums correctly. It does not impact the final generation of files, or the
684
829
  # language imports. Those are still auto-resolved.
@@ -703,13 +848,13 @@ RE_ENDPOINT_ROOT = re.compile(r"\${([_a-z]+)}")
703
848
 
704
849
  @dataclass(kw_only=True, frozen=True)
705
850
  class _EndpointPathDetails:
706
- root: str
851
+ root: EndpointKey
707
852
  root_path: str
708
853
  resolved_path: str
709
854
 
710
855
 
711
856
  def _resolve_endpoint_path(
712
- path: str, api_endpoints: dict[str, str]
857
+ path: str, api_endpoints: dict[EndpointKey, APIEndpointInfo]
713
858
  ) -> _EndpointPathDetails:
714
859
  root_path_source = path.split("/")[0]
715
860
  root_match = RE_ENDPOINT_ROOT.fullmatch(root_path_source)
@@ -717,7 +862,7 @@ def _resolve_endpoint_path(
717
862
  raise Exception(f"invalid-api-path-root:{root_path_source}")
718
863
 
719
864
  root_var = root_match.group(1)
720
- root_path = api_endpoints[root_var]
865
+ root_path = api_endpoints[root_var].root_path
721
866
 
722
867
  _, *rest_path = path.split("/", 1)
723
868
  resolved_path = "/".join([root_path] + rest_path)
@@ -727,22 +872,75 @@ def _resolve_endpoint_path(
727
872
  )
728
873
 
729
874
 
730
- class SpecEndpoint:
731
- method: RouteMethod
732
- root: str
875
+ class EndpointEmitType(StrEnum):
876
+ EMIT_ENDPOINT = "emit_endpoint"
877
+ EMIT_TYPES = "emit_types"
878
+ EMIT_NOTHING = "emit_nothing"
879
+
880
+
881
+ @dataclass(kw_only=True, frozen=True)
882
+ class EndpointSpecificPath:
883
+ root: EndpointKey
733
884
  path_root: str
734
885
  path_dirname: str
735
886
  path_basename: str
887
+ function: str | None
888
+
889
+
890
+ def parse_endpoint_specific_path(
891
+ builder: SpecBuilder,
892
+ data_per_endpoint: RawDict | None,
893
+ ) -> EndpointSpecificPath | None:
894
+ if data_per_endpoint is None:
895
+ return None
896
+ util.check_fields(
897
+ data_per_endpoint,
898
+ [
899
+ "path",
900
+ "function",
901
+ ],
902
+ )
903
+
904
+ if "path" not in data_per_endpoint or data_per_endpoint["path"] is None:
905
+ return None
906
+
907
+ path = data_per_endpoint["path"].split("/")
908
+
909
+ assert len(path) > 1, "invalid-endpoint-path"
910
+
911
+ path_details = _resolve_endpoint_path(
912
+ data_per_endpoint["path"], builder.api_endpoints
913
+ )
914
+
915
+ result = EndpointSpecificPath(
916
+ function=data_per_endpoint.get("function"),
917
+ path_dirname="/".join(path[1:-1]),
918
+ path_basename=path[-1],
919
+ root=path_details.root,
920
+ path_root=path_details.root_path,
921
+ )
922
+
923
+ return result
924
+
925
+
926
+ class SpecEndpoint:
927
+ method: RouteMethod
736
928
  data_loader: bool
737
- is_sdk: bool
738
- is_beta: bool
929
+ is_sdk: EndpointEmitType
930
+ stability_level: StabilityLevel | None
739
931
  # Don't emit TypeScript endpoint code
740
932
  suppress_ts: bool
741
- function: Optional[str]
933
+ deprecated: bool = False
742
934
  async_batch_path: str | None = None
743
935
  result_type: ResultType = ResultType.json
744
936
  has_attachment: bool = False
745
937
  desc: str | None = None
938
+ account_type: str | None
939
+ route_group: str | None
940
+
941
+ # function, path details per api endpoint
942
+ path_per_api_endpoint: dict[str, EndpointSpecificPath]
943
+ default_endpoint_key: EndpointKey
746
944
 
747
945
  is_external: bool = False
748
946
 
@@ -750,15 +948,15 @@ class SpecEndpoint:
750
948
  pass
751
949
 
752
950
  def process(self, builder: SpecBuilder, data: RawDict) -> None:
753
- unused(builder)
754
951
  util.check_fields(
755
952
  data,
756
953
  [
757
954
  "method",
758
955
  "path",
759
956
  "data_loader",
957
+ "deprecated",
760
958
  "is_sdk",
761
- "is_beta",
959
+ "stability_level",
762
960
  "async_batch_path",
763
961
  "function",
764
962
  "suppress_ts",
@@ -766,58 +964,121 @@ class SpecEndpoint:
766
964
  "deprecated",
767
965
  "result_type",
768
966
  "has_attachment",
769
- ],
967
+ "account_type",
968
+ "route_group",
969
+ ]
970
+ + list(builder.api_endpoints.keys()),
770
971
  )
771
972
  self.method = RouteMethod(data["method"])
772
973
 
773
- path = data["path"].split("/")
774
-
775
- assert len(path) > 1, "invalid-endpoint-path"
776
-
777
- # handle ${external} in the same way we handle ${materials} for now
778
- self.path_dirname = "/".join(path[1:-1])
779
- self.path_basename = path[-1]
780
-
781
974
  data_loader = data.get("data_loader", False)
782
975
  assert isinstance(data_loader, bool)
783
976
  self.data_loader = data_loader
977
+ self.deprecated = data.get("deprecated", False)
978
+
979
+ is_sdk = data.get("is_sdk", EndpointEmitType.EMIT_NOTHING)
980
+
981
+ # backwards compatibility
982
+ if isinstance(is_sdk, bool):
983
+ if is_sdk is True:
984
+ is_sdk = EndpointEmitType.EMIT_ENDPOINT
985
+ else:
986
+ is_sdk = EndpointEmitType.EMIT_NOTHING
987
+ elif isinstance(is_sdk, str):
988
+ try:
989
+ is_sdk = EndpointEmitType(is_sdk)
990
+ except ValueError as e:
991
+ raise ValueError(f"Invalid value for is_sdk: {is_sdk}") from e
992
+
993
+ assert isinstance(is_sdk, EndpointEmitType)
784
994
 
785
- is_sdk = data.get("is_sdk", False)
786
- assert isinstance(is_sdk, bool)
787
995
  self.is_sdk = is_sdk
788
996
 
789
- is_beta = data.get("is_beta", False)
790
- assert isinstance(is_beta, bool)
791
- self.is_beta = is_beta
997
+ route_group = data.get("route_group")
998
+ assert route_group is None or isinstance(route_group, str)
999
+ self.route_group = route_group
1000
+
1001
+ account_type = data.get("account_type")
1002
+ assert account_type is None or isinstance(account_type, str)
1003
+ self.account_type = account_type
1004
+
1005
+ stability_level_raw = data.get("stability_level")
1006
+ assert stability_level_raw is None or isinstance(stability_level_raw, str)
1007
+ self.stability_level = (
1008
+ StabilityLevel(stability_level_raw)
1009
+ if stability_level_raw is not None
1010
+ else None
1011
+ )
792
1012
 
793
1013
  async_batch_path = data.get("async_batch_path")
794
1014
  if async_batch_path is not None:
795
1015
  assert isinstance(async_batch_path, str)
796
1016
  self.async_batch_path = async_batch_path
797
1017
 
798
- self.function = data.get("function")
799
-
800
1018
  suppress_ts = data.get("suppress_ts", False)
801
1019
  assert isinstance(suppress_ts, bool)
802
1020
  self.suppress_ts = suppress_ts
803
1021
 
804
1022
  self.result_type = ResultType(data.get("result_type", ResultType.json.value))
805
-
806
- path_details = _resolve_endpoint_path(data["path"], builder.api_endpoints)
807
- self.root = path_details.root
808
- self.path_root = path_details.root_path
1023
+ self.has_attachment = data.get("has_attachment", False)
809
1024
  self.desc = data.get("desc")
1025
+
1026
+ # compatibility with single-endpoint files
1027
+ default_endpoint_path = parse_endpoint_specific_path(
1028
+ builder,
1029
+ {"path": data.get("path"), "function": data.get("function")},
1030
+ )
1031
+ if default_endpoint_path is not None:
1032
+ assert default_endpoint_path.root in builder.api_endpoints, (
1033
+ "Default endpoint is not a valid API endpoint"
1034
+ )
1035
+ self.default_endpoint_key = default_endpoint_path.root
1036
+ self.path_per_api_endpoint = {
1037
+ self.default_endpoint_key: default_endpoint_path,
1038
+ }
1039
+ else:
1040
+ self.path_per_api_endpoint = {}
1041
+ shared_function_name = None
1042
+ for endpoint_key in builder.api_endpoints:
1043
+ endpoint_specific_path = parse_endpoint_specific_path(
1044
+ builder,
1045
+ data.get(endpoint_key),
1046
+ )
1047
+ if endpoint_specific_path is not None:
1048
+ self.path_per_api_endpoint[endpoint_key] = endpoint_specific_path
1049
+ if endpoint_specific_path.function is not None:
1050
+ fn_name = endpoint_specific_path.function.split(".")[-1]
1051
+ if shared_function_name is None:
1052
+ shared_function_name = fn_name
1053
+ assert shared_function_name == fn_name
1054
+
1055
+ if builder.top_namespace in self.path_per_api_endpoint:
1056
+ self.default_endpoint_key = builder.top_namespace
1057
+ elif len(self.path_per_api_endpoint) == 1:
1058
+ self.default_endpoint_key = next(
1059
+ iter(self.path_per_api_endpoint.keys())
1060
+ )
1061
+ else:
1062
+ raise RuntimeError("no clear default endpoint")
1063
+
1064
+ assert len(self.path_per_api_endpoint) > 0, (
1065
+ "Missing API endpoint path and function definitions for API call"
1066
+ )
1067
+
810
1068
  # IMPROVE: remove need for is_external flag
811
- self.is_external = self.path_root == "api/external"
812
- self.has_attachment = data.get("has_attachment", False)
1069
+ self.is_external = (
1070
+ self.path_per_api_endpoint[self.default_endpoint_key].path_root
1071
+ == "api/external"
1072
+ )
813
1073
 
814
- assert (
815
- not is_sdk or self.desc is not None
816
- ), f"Endpoint description required for SDK endpoints, missing: {path}"
1074
+ assert self.is_sdk != EndpointEmitType.EMIT_ENDPOINT or self.desc is not None, (
1075
+ f"Endpoint description required for SDK endpoints, missing: {self.resolved_path}"
1076
+ )
817
1077
 
818
1078
  @property
819
1079
  def resolved_path(self: Self) -> str:
820
- return f"{self.path_root}/{self.path_dirname}/{self.path_basename}"
1080
+ default_endpoint_path = self.path_per_api_endpoint[self.default_endpoint_key]
1081
+ return f"{default_endpoint_path.path_root}/{default_endpoint_path.path_dirname}/{default_endpoint_path.path_basename}"
821
1082
 
822
1083
 
823
1084
  def _parse_const(
@@ -840,7 +1101,7 @@ def _parse_const(
840
1101
  elif const_type.defn_type.name == BaseTypeName.s_dict:
841
1102
  assert isinstance(value, dict)
842
1103
  builder.ensure(
843
- len(const_type.parameters) == 2, "constant-dict-expects-one-type"
1104
+ len(const_type.parameters) == 2, "constant-dict-expects-two-types"
844
1105
  )
845
1106
  key_type = const_type.parameters[0]
846
1107
  value_type = const_type.parameters[1]
@@ -884,7 +1145,14 @@ def _parse_const(
884
1145
  return value
885
1146
 
886
1147
  if const_type.name == BaseTypeName.s_boolean:
887
- builder.ensure(isinstance(value, bool), "invalid value for boolean constant")
1148
+ builder.ensure(
1149
+ isinstance(value, bool), "invalid value for boolean constant"
1150
+ )
1151
+ return value
1152
+
1153
+ if not const_type.is_base:
1154
+ # IMPROVE: validate the object type properties before emission stage
1155
+ builder.ensure(isinstance(value, dict), "invalid value for object constant")
888
1156
  return value
889
1157
 
890
1158
  raise Exception("unsupported-const-scalar-type", const_type)
@@ -916,7 +1184,9 @@ class SpecConstant:
916
1184
  assert isinstance(self.value, dict)
917
1185
  # the parsing checks that the values are correct, so a simple length check
918
1186
  # should be enough to check completeness
919
- builder.ensure(len(key_type.values) == len(self.value), "incomplete-enum-map")
1187
+ builder.ensure(
1188
+ len(key_type.values) == len(self.value), "incomplete-enum-map"
1189
+ )
920
1190
 
921
1191
 
922
1192
  class SpecNamespace:
@@ -926,14 +1196,15 @@ class SpecNamespace:
926
1196
  ):
927
1197
  self.types: dict[str, SpecTypeDefn] = {}
928
1198
  self.constants: dict[str, SpecConstant] = {}
929
- self.endpoint: Optional[SpecEndpoint] = None
1199
+ self.endpoint: SpecEndpoint | None = None
930
1200
  self.emit_io_ts = False
931
1201
  self.emit_type_info = False
1202
+ self.emit_type_info_python = False
932
1203
  self.derive_types_from_io_ts = False
933
- self._imports: Optional[list[str]] = None
1204
+ self._imports: list[str] | None = None
934
1205
  self.path = name.split(".")
935
1206
  self.name = self.path[-1]
936
- self._order: Optional[int] = None
1207
+ self._order: int | None = None
937
1208
 
938
1209
  def _update_order(self, builder: SpecBuilder, recurse: int = 0) -> int:
939
1210
  if self._order is not None:
@@ -983,6 +1254,11 @@ class SpecNamespace:
983
1254
  self.emit_type_info = defn
984
1255
  continue
985
1256
 
1257
+ if name == TOKEN_EMIT_TYPE_INFO_PYTHON:
1258
+ assert defn in (True, False)
1259
+ self.emit_type_info_python = defn
1260
+ continue
1261
+
986
1262
  if name == TOKEN_IMPORT:
987
1263
  assert self._imports is None
988
1264
  imports = [defn] if isinstance(defn, str) else defn
@@ -991,26 +1267,33 @@ class SpecNamespace:
991
1267
  continue
992
1268
 
993
1269
  if "value" in defn:
994
- assert util.is_valid_property_name(
995
- name
996
- ), f"{name} is not a valid constant name"
1270
+ assert util.is_valid_property_name(name), (
1271
+ f"{name} is not a valid constant name"
1272
+ )
997
1273
  spec_constant = SpecConstant(self, name)
998
1274
  self.constants[name] = spec_constant
999
1275
  continue
1000
1276
 
1001
1277
  assert util.is_valid_type_name(name), f"{name} is not a valid type name"
1002
1278
  assert name not in self.types, f"{name} is duplicate"
1003
- defn_type = defn["type"]
1279
+ defn_type = defn.get("type")
1280
+ assert isinstance(defn_type, str), f"{name} requires a string type"
1004
1281
  spec_type: SpecTypeDefn
1005
1282
  if defn_type == DefnTypeName.s_alias:
1006
1283
  spec_type = SpecTypeDefnAlias(self, name)
1284
+ elif defn_type == DefnTypeName.s_union:
1285
+ spec_type = SpecTypeDefnUnion(self, name)
1007
1286
  elif defn_type == DefnTypeName.s_external:
1008
1287
  spec_type = SpecTypeDefnExternal(self, name)
1009
1288
  elif defn_type == DefnTypeName.s_string_enum:
1010
1289
  spec_type = SpecTypeDefnStringEnum(self, name)
1011
1290
  else:
1012
1291
  parameters = (
1013
- [parameter.name for parameter in parsed_name.parameters[0]]
1292
+ [
1293
+ parameter.name
1294
+ for name_parameters in parsed_name.parameters
1295
+ for parameter in name_parameters
1296
+ ]
1014
1297
  if parsed_name.parameters is not None
1015
1298
  else None
1016
1299
  )
@@ -1026,28 +1309,46 @@ class SpecNamespace:
1026
1309
  Complete the definition of each type.
1027
1310
  """
1028
1311
  builder.push_where(self.name)
1029
- for full_name, defn in data.items():
1030
- parsed_name = parse_type_str(full_name)[0]
1031
- name = parsed_name.name
1312
+ items_to_process: list[NameDataPair] = [
1313
+ NameDataPair(full_name=full_name, data=defn)
1314
+ for full_name, defn in data.items()
1315
+ ]
1316
+ while len(items_to_process) > 0:
1317
+ deferred_items: list[NameDataPair] = []
1318
+ for item in items_to_process:
1319
+ full_name = item.full_name
1320
+ defn = item.data
1321
+ parsed_name = parse_type_str(full_name)[0]
1322
+ name = parsed_name.name
1323
+
1324
+ if name in [
1325
+ TOKEN_EMIT_IO_TS,
1326
+ TOKEN_EMIT_TYPE_INFO,
1327
+ TOKEN_IMPORT,
1328
+ TOKEN_EMIT_TYPE_INFO_PYTHON,
1329
+ ]:
1330
+ continue
1032
1331
 
1033
- if name in [TOKEN_EMIT_IO_TS, TOKEN_EMIT_TYPE_INFO, TOKEN_IMPORT]:
1034
- continue
1332
+ builder.push_where(name)
1035
1333
 
1036
- builder.push_where(name)
1334
+ if "value" in defn:
1335
+ spec_constant = self.constants[name]
1336
+ spec_constant.process(builder, defn)
1037
1337
 
1038
- if "value" in defn:
1039
- spec_constant = self.constants[name]
1040
- spec_constant.process(builder, defn)
1338
+ elif name == TOKEN_ENDPOINT:
1339
+ assert self.endpoint
1340
+ self.endpoint.process(builder, defn)
1041
1341
 
1042
- elif name == TOKEN_ENDPOINT:
1043
- assert self.endpoint
1044
- self.endpoint.process(builder, defn)
1045
-
1046
- else:
1047
- spec_type = self.types[name]
1048
- spec_type.process(builder, defn)
1342
+ else:
1343
+ spec_type = self.types[name]
1344
+ if spec_type.can_process(builder, defn):
1345
+ spec_type.process(builder, defn)
1346
+ else:
1347
+ deferred_items.append(item)
1049
1348
 
1050
- builder.pop_where()
1349
+ builder.pop_where()
1350
+ assert len(deferred_items) < len(items_to_process)
1351
+ items_to_process = [deferred for deferred in deferred_items]
1051
1352
 
1052
1353
  builder.pop_where()
1053
1354
 
@@ -1062,8 +1363,21 @@ class NamespaceDataPair:
1062
1363
  data: RawDict
1063
1364
 
1064
1365
 
1366
+ @dataclass(kw_only=True)
1367
+ class NameDataPair:
1368
+ full_name: str
1369
+ data: RawDict
1370
+
1371
+
1065
1372
  class SpecBuilder:
1066
- def __init__(self, *, api_endpoints: dict[str, str]) -> None:
1373
+ def __init__(
1374
+ self,
1375
+ *,
1376
+ api_endpoints: dict[EndpointKey, APIEndpointInfo],
1377
+ top_namespace: str,
1378
+ cross_output_paths: CrossOutputPaths | None,
1379
+ ) -> None:
1380
+ self.top_namespace = top_namespace
1067
1381
  self.where: list[str] = []
1068
1382
  self.namespaces = {}
1069
1383
  self.pending: list[NamespaceDataPair] = []
@@ -1072,6 +1386,7 @@ class SpecBuilder:
1072
1386
  self.examples: dict[str, list[SpecEndpointExample]] = defaultdict(list)
1073
1387
  self.guides: dict[SpecGuideKey, list[SpecGuide]] = defaultdict(list)
1074
1388
  self.api_endpoints = api_endpoints
1389
+ self.cross_output_paths = cross_output_paths
1075
1390
  base_namespace = SpecNamespace(name=base_namespace_name)
1076
1391
  for base_type in BaseTypeName:
1077
1392
  defn = SpecTypeDefnObject(base_namespace, base_type, is_base=True)
@@ -1089,9 +1404,13 @@ class SpecBuilder:
1089
1404
  self.emit_id_source_enums: set[SpecTypeDefnStringEnum] = set()
1090
1405
 
1091
1406
  this_dir = os.path.dirname(os.path.realpath(__file__))
1092
- with open(f"{this_dir}/parts/base.py.prepart") as py_base_part:
1407
+ with open(
1408
+ f"{this_dir}/parts/base.py.prepart", encoding="utf-8"
1409
+ ) as py_base_part:
1093
1410
  self.preparts["python"][base_namespace_name] = py_base_part.read()
1094
- with open(f"{this_dir}/parts/base.ts.prepart") as ts_base_part:
1411
+ with open(
1412
+ f"{this_dir}/parts/base.ts.prepart", encoding="utf-8"
1413
+ ) as ts_base_part:
1095
1414
  self.preparts["typescript"][base_namespace_name] = ts_base_part.read()
1096
1415
 
1097
1416
  base_namespace.types["ObjectId"] = SpecTypeDefnObject(
@@ -1160,7 +1479,7 @@ class SpecBuilder:
1160
1479
  self,
1161
1480
  path: util.ParsedTypePath,
1162
1481
  namespace: SpecNamespace,
1163
- scope: Optional[SpecTypeDefn] = None,
1482
+ scope: SpecTypeDefn | None = None,
1164
1483
  top: bool = False,
1165
1484
  ) -> SpecType:
1166
1485
  """
@@ -1218,8 +1537,10 @@ class SpecBuilder:
1218
1537
  if len(path) == 2:
1219
1538
  if isinstance(defn_type, SpecTypeDefnStringEnum):
1220
1539
  assert path[1].parameters is None
1540
+ statement = f"$import: [{defn_type.namespace.name}]"
1221
1541
  self.ensure(
1222
- path[1].name in defn_type.values, f"missing-enum-value: {path}"
1542
+ path[1].name in defn_type.values,
1543
+ f"missing-enum-value: {path} have you specified the dependency in an import statement: {statement}",
1223
1544
  )
1224
1545
  return SpecTypeLiteralWrapper(
1225
1546
  value=path[1].name,
@@ -1241,11 +1562,13 @@ class SpecBuilder:
1241
1562
  )
1242
1563
 
1243
1564
  def parse_type(
1244
- self, namespace: SpecNamespace, spec: str, scope: Optional[SpecTypeDefn] = None
1565
+ self, namespace: SpecNamespace, spec: str, scope: SpecTypeDefn | None = None
1245
1566
  ) -> SpecType:
1246
1567
  self.push_where(spec)
1247
1568
  parsed_type = util.parse_type_str(spec)
1248
- result = self._convert_parsed_type(parsed_type, namespace, top=True, scope=scope)
1569
+ result = self._convert_parsed_type(
1570
+ parsed_type, namespace, top=True, scope=scope
1571
+ )
1249
1572
  self.pop_where()
1250
1573
  return result
1251
1574
 
@@ -1287,16 +1610,22 @@ class SpecBuilder:
1287
1610
  meta: dict[str, list[str]] = md.Meta # type: ignore[attr-defined]
1288
1611
  title_meta: list[str] | None = meta.get("title")
1289
1612
  if title_meta is None:
1290
- raise Exception("guides requier a title in the meta section")
1613
+ raise Exception("guides require a title in the meta section")
1614
+ id_meta: list[str] | None = meta.get("id")
1615
+ if id_meta is None:
1616
+ raise Exception("guides require an id in the meta section")
1291
1617
 
1292
1618
  path_meta: list[str] | None = meta.get("path")
1293
1619
  guide_key: SpecGuideKey = RootGuideKey()
1294
1620
  if path_meta is not None:
1295
- path_details = _resolve_endpoint_path("".join(path_meta), self.api_endpoints)
1621
+ path_details = _resolve_endpoint_path(
1622
+ "".join(path_meta), self.api_endpoints
1623
+ )
1296
1624
  guide_key = EndpointGuideKey(path=path_details.resolved_path)
1297
1625
 
1298
1626
  self.guides[guide_key].append(
1299
1627
  SpecGuide(
1628
+ ref_name="".join(id_meta),
1300
1629
  title="".join(title_meta),
1301
1630
  html_content=html,
1302
1631
  markdown_content=file_content,