UncountablePythonSDK 0.0.52__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 (316) hide show
  1. docs/conf.py +54 -7
  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 +6 -4
  8. examples/async_batch.py +3 -3
  9. examples/basic_auth.py +7 -0
  10. examples/create_entity.py +3 -1
  11. examples/create_ingredient_sdk.py +34 -0
  12. examples/download_files.py +26 -0
  13. examples/edit_recipe_inputs.py +4 -2
  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 +4 -1
  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 +1 -2
  29. pkgs/argument_parser/__init__.py +9 -0
  30. pkgs/argument_parser/_is_namedtuple.py +3 -0
  31. pkgs/argument_parser/argument_parser.py +217 -70
  32. pkgs/filesystem_utils/__init__.py +1 -0
  33. pkgs/filesystem_utils/_blob_session.py +144 -0
  34. pkgs/filesystem_utils/_gdrive_session.py +10 -7
  35. pkgs/filesystem_utils/_s3_session.py +15 -13
  36. pkgs/filesystem_utils/_sftp_session.py +11 -7
  37. pkgs/filesystem_utils/file_type_utils.py +30 -10
  38. pkgs/py.typed +0 -0
  39. pkgs/serialization/__init__.py +7 -2
  40. pkgs/serialization/annotation.py +64 -0
  41. pkgs/serialization/missing_sentry.py +1 -1
  42. pkgs/serialization/opaque_key.py +1 -1
  43. pkgs/serialization/serial_alias.py +47 -0
  44. pkgs/serialization/serial_class.py +47 -26
  45. pkgs/serialization/serial_generic.py +16 -0
  46. pkgs/serialization/serial_union.py +17 -14
  47. pkgs/serialization/yaml.py +4 -1
  48. pkgs/serialization_util/__init__.py +6 -0
  49. pkgs/serialization_util/dataclasses.py +14 -0
  50. pkgs/serialization_util/serialization_helpers.py +15 -5
  51. pkgs/type_spec/actions_registry/__main__.py +0 -4
  52. pkgs/type_spec/actions_registry/emit_typescript.py +5 -5
  53. pkgs/type_spec/builder.py +354 -119
  54. pkgs/type_spec/builder_types.py +9 -0
  55. pkgs/type_spec/config.py +51 -11
  56. pkgs/type_spec/cross_output_links.py +99 -0
  57. pkgs/type_spec/emit_io_ts.py +1 -1
  58. pkgs/type_spec/emit_open_api.py +127 -36
  59. pkgs/type_spec/emit_open_api_util.py +5 -6
  60. pkgs/type_spec/emit_python.py +329 -121
  61. pkgs/type_spec/emit_typescript.py +117 -256
  62. pkgs/type_spec/emit_typescript_util.py +291 -2
  63. pkgs/type_spec/load_types.py +18 -4
  64. pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
  65. pkgs/type_spec/open_api_util.py +29 -4
  66. pkgs/type_spec/parts/base.py.prepart +13 -10
  67. pkgs/type_spec/parts/base.ts.prepart +4 -0
  68. pkgs/type_spec/type_info/__main__.py +3 -1
  69. pkgs/type_spec/type_info/emit_type_info.py +124 -29
  70. pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
  71. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
  72. pkgs/type_spec/util.py +4 -4
  73. pkgs/type_spec/value_spec/__main__.py +26 -9
  74. pkgs/type_spec/value_spec/convert_type.py +21 -1
  75. pkgs/type_spec/value_spec/emit_python.py +25 -7
  76. pkgs/type_spec/value_spec/types.py +1 -1
  77. uncountable/core/async_batch.py +1 -1
  78. uncountable/core/client.py +142 -39
  79. uncountable/core/environment.py +41 -0
  80. uncountable/core/file_upload.py +52 -18
  81. uncountable/integration/cli.py +142 -0
  82. uncountable/integration/construct_client.py +8 -8
  83. uncountable/integration/cron.py +11 -37
  84. uncountable/integration/db/connect.py +12 -2
  85. uncountable/integration/db/session.py +25 -0
  86. uncountable/integration/entrypoint.py +8 -37
  87. uncountable/integration/executors/executors.py +125 -2
  88. uncountable/integration/executors/generic_upload_executor.py +87 -29
  89. uncountable/integration/executors/script_executor.py +3 -3
  90. uncountable/integration/http_server/__init__.py +5 -0
  91. uncountable/integration/http_server/types.py +69 -0
  92. uncountable/integration/job.py +242 -12
  93. uncountable/integration/queue_runner/__init__.py +0 -0
  94. uncountable/integration/queue_runner/command_server/__init__.py +28 -0
  95. uncountable/integration/queue_runner/command_server/command_client.py +133 -0
  96. uncountable/integration/queue_runner/command_server/command_server.py +142 -0
  97. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  98. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  99. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +58 -0
  100. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +57 -0
  101. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +114 -0
  102. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +264 -0
  103. uncountable/integration/queue_runner/command_server/types.py +75 -0
  104. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  105. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +250 -0
  106. uncountable/integration/queue_runner/datastore/interface.py +29 -0
  107. uncountable/integration/queue_runner/datastore/model.py +24 -0
  108. uncountable/integration/queue_runner/job_scheduler.py +200 -0
  109. uncountable/integration/queue_runner/queue_runner.py +34 -0
  110. uncountable/integration/queue_runner/types.py +7 -0
  111. uncountable/integration/queue_runner/worker.py +116 -0
  112. uncountable/integration/scan_profiles.py +67 -0
  113. uncountable/integration/scheduler.py +199 -0
  114. uncountable/integration/secret_retrieval/retrieve_secret.py +26 -4
  115. uncountable/integration/server.py +94 -69
  116. uncountable/integration/telemetry.py +150 -34
  117. uncountable/integration/webhook_server/entrypoint.py +97 -0
  118. uncountable/types/__init__.py +78 -1
  119. uncountable/types/api/batch/execute_batch.py +13 -6
  120. uncountable/types/api/batch/execute_batch_load_async.py +9 -3
  121. uncountable/types/api/chemical/convert_chemical_formats.py +17 -5
  122. uncountable/types/api/condition_parameters/__init__.py +1 -0
  123. uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
  124. uncountable/types/api/entity/create_entities.py +19 -7
  125. uncountable/types/api/entity/create_entity.py +17 -8
  126. uncountable/types/api/entity/create_or_update_entity.py +48 -0
  127. uncountable/types/api/entity/export_entities.py +59 -0
  128. uncountable/types/api/entity/get_entities_data.py +13 -4
  129. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  130. uncountable/types/api/entity/list_aggregate.py +79 -0
  131. uncountable/types/api/entity/list_entities.py +42 -10
  132. uncountable/types/api/entity/lock_entity.py +11 -4
  133. uncountable/types/api/entity/lookup_entity.py +116 -0
  134. uncountable/types/api/entity/resolve_entity_ids.py +15 -6
  135. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  136. uncountable/types/api/entity/set_values.py +10 -3
  137. uncountable/types/api/entity/transition_entity_phase.py +22 -7
  138. uncountable/types/api/entity/unlock_entity.py +10 -3
  139. uncountable/types/api/equipment/associate_equipment_input.py +9 -3
  140. uncountable/types/api/field_options/upsert_field_options.py +17 -7
  141. uncountable/types/api/files/__init__.py +1 -0
  142. uncountable/types/api/files/download_file.py +77 -0
  143. uncountable/types/api/id_source/list_id_source.py +16 -7
  144. uncountable/types/api/id_source/match_id_source.py +14 -5
  145. uncountable/types/api/input_groups/get_input_group_names.py +13 -4
  146. uncountable/types/api/inputs/create_inputs.py +23 -9
  147. uncountable/types/api/inputs/get_input_data.py +30 -12
  148. uncountable/types/api/inputs/get_input_names.py +16 -7
  149. uncountable/types/api/inputs/get_inputs_data.py +25 -7
  150. uncountable/types/api/inputs/set_input_attribute_values.py +12 -6
  151. uncountable/types/api/inputs/set_input_category.py +12 -5
  152. uncountable/types/api/inputs/set_input_subcategories.py +10 -3
  153. uncountable/types/api/inputs/set_intermediate_type.py +11 -4
  154. uncountable/types/api/integrations/__init__.py +1 -0
  155. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  156. uncountable/types/api/integrations/push_notification.py +49 -0
  157. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  158. uncountable/types/api/listing/__init__.py +1 -0
  159. uncountable/types/api/listing/fetch_listing.py +58 -0
  160. uncountable/types/api/material_families/update_entity_material_families.py +10 -4
  161. uncountable/types/api/notebooks/__init__.py +1 -0
  162. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  163. uncountable/types/api/outputs/get_output_data.py +28 -13
  164. uncountable/types/api/outputs/get_output_names.py +15 -6
  165. uncountable/types/api/outputs/get_output_organization.py +173 -0
  166. uncountable/types/api/outputs/resolve_output_conditions.py +20 -8
  167. uncountable/types/api/permissions/set_core_permissions.py +26 -10
  168. uncountable/types/api/project/get_projects.py +16 -7
  169. uncountable/types/api/project/get_projects_data.py +17 -8
  170. uncountable/types/api/recipe_links/create_recipe_link.py +12 -5
  171. uncountable/types/api/recipe_links/remove_recipe_link.py +11 -4
  172. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +16 -7
  173. uncountable/types/api/recipes/add_recipe_to_project.py +10 -3
  174. uncountable/types/api/recipes/add_time_series_data.py +64 -0
  175. uncountable/types/api/recipes/archive_recipes.py +11 -4
  176. uncountable/types/api/recipes/associate_recipe_as_input.py +12 -5
  177. uncountable/types/api/recipes/associate_recipe_as_lot.py +10 -3
  178. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  179. uncountable/types/api/recipes/create_mix_order.py +44 -0
  180. uncountable/types/api/recipes/create_recipe.py +15 -9
  181. uncountable/types/api/recipes/create_recipes.py +21 -9
  182. uncountable/types/api/recipes/disassociate_recipe_as_input.py +10 -3
  183. uncountable/types/api/recipes/edit_recipe_inputs.py +134 -22
  184. uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
  185. uncountable/types/api/recipes/get_curve.py +11 -5
  186. uncountable/types/api/recipes/get_recipe_calculations.py +13 -7
  187. uncountable/types/api/recipes/get_recipe_links.py +10 -4
  188. uncountable/types/api/recipes/get_recipe_names.py +13 -4
  189. uncountable/types/api/recipes/get_recipe_output_metadata.py +12 -6
  190. uncountable/types/api/recipes/get_recipes_data.py +87 -33
  191. uncountable/types/api/recipes/lock_recipes.py +19 -8
  192. uncountable/types/api/recipes/remove_recipe_from_project.py +10 -3
  193. uncountable/types/api/recipes/set_recipe_inputs.py +16 -10
  194. uncountable/types/api/recipes/set_recipe_metadata.py +10 -3
  195. uncountable/types/api/recipes/set_recipe_output_annotations.py +24 -12
  196. uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
  197. uncountable/types/api/recipes/set_recipe_outputs.py +35 -12
  198. uncountable/types/api/recipes/set_recipe_tags.py +26 -9
  199. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  200. uncountable/types/api/recipes/unarchive_recipes.py +10 -3
  201. uncountable/types/api/recipes/unlock_recipes.py +14 -6
  202. uncountable/types/api/runsheet/__init__.py +1 -0
  203. uncountable/types/api/runsheet/complete_async_upload.py +41 -0
  204. uncountable/types/api/triggers/run_trigger.py +11 -4
  205. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  206. uncountable/types/api/uploader/invoke_uploader.py +13 -6
  207. uncountable/types/api/user/__init__.py +1 -0
  208. uncountable/types/api/user/get_current_user_info.py +40 -0
  209. uncountable/types/async_batch.py +2 -1
  210. uncountable/types/async_batch_processor.py +618 -18
  211. uncountable/types/async_batch_t.py +54 -7
  212. uncountable/types/async_jobs.py +8 -0
  213. uncountable/types/async_jobs_t.py +52 -0
  214. uncountable/types/auth_retrieval.py +11 -0
  215. uncountable/types/auth_retrieval_t.py +75 -0
  216. uncountable/types/base.py +0 -1
  217. uncountable/types/base_t.py +13 -11
  218. uncountable/types/calculations.py +0 -1
  219. uncountable/types/calculations_t.py +5 -2
  220. uncountable/types/chemical_structure.py +0 -1
  221. uncountable/types/chemical_structure_t.py +6 -5
  222. uncountable/types/client_base.py +751 -70
  223. uncountable/types/client_config.py +1 -1
  224. uncountable/types/client_config_t.py +17 -3
  225. uncountable/types/curves.py +0 -1
  226. uncountable/types/curves_t.py +10 -7
  227. uncountable/types/data.py +12 -0
  228. uncountable/types/data_t.py +103 -0
  229. uncountable/types/entity.py +4 -1
  230. uncountable/types/entity_t.py +125 -7
  231. uncountable/types/experiment_groups.py +0 -1
  232. uncountable/types/experiment_groups_t.py +5 -2
  233. uncountable/types/exports.py +8 -0
  234. uncountable/types/exports_t.py +34 -0
  235. uncountable/types/field_values.py +19 -1
  236. uncountable/types/field_values_t.py +246 -9
  237. uncountable/types/fields.py +0 -1
  238. uncountable/types/fields_t.py +5 -2
  239. uncountable/types/generic_upload.py +6 -1
  240. uncountable/types/generic_upload_t.py +88 -9
  241. uncountable/types/id_source.py +0 -1
  242. uncountable/types/id_source_t.py +26 -7
  243. uncountable/types/identifier.py +0 -1
  244. uncountable/types/identifier_t.py +13 -5
  245. uncountable/types/input_attributes.py +0 -1
  246. uncountable/types/input_attributes_t.py +4 -4
  247. uncountable/types/inputs.py +1 -1
  248. uncountable/types/inputs_t.py +24 -4
  249. uncountable/types/integration_server.py +8 -0
  250. uncountable/types/integration_server_t.py +46 -0
  251. uncountable/types/integration_session.py +10 -0
  252. uncountable/types/integration_session_t.py +60 -0
  253. uncountable/types/integrations.py +10 -0
  254. uncountable/types/integrations_t.py +62 -0
  255. uncountable/types/job_definition.py +4 -6
  256. uncountable/types/job_definition_t.py +96 -65
  257. uncountable/types/listing.py +9 -0
  258. uncountable/types/listing_t.py +51 -0
  259. uncountable/types/notices.py +8 -0
  260. uncountable/types/notices_t.py +37 -0
  261. uncountable/types/notifications.py +11 -0
  262. uncountable/types/notifications_t.py +74 -0
  263. uncountable/types/outputs.py +0 -1
  264. uncountable/types/outputs_t.py +6 -3
  265. uncountable/types/overrides.py +9 -0
  266. uncountable/types/overrides_t.py +49 -0
  267. uncountable/types/permissions.py +0 -1
  268. uncountable/types/permissions_t.py +1 -2
  269. uncountable/types/phases.py +0 -1
  270. uncountable/types/phases_t.py +5 -2
  271. uncountable/types/post_base.py +0 -1
  272. uncountable/types/post_base_t.py +1 -2
  273. uncountable/types/queued_job.py +17 -0
  274. uncountable/types/queued_job_t.py +140 -0
  275. uncountable/types/recipe_identifiers.py +0 -1
  276. uncountable/types/recipe_identifiers_t.py +21 -8
  277. uncountable/types/recipe_inputs.py +0 -1
  278. uncountable/types/recipe_inputs_t.py +1 -2
  279. uncountable/types/recipe_links.py +0 -1
  280. uncountable/types/recipe_links_t.py +7 -4
  281. uncountable/types/recipe_metadata.py +0 -1
  282. uncountable/types/recipe_metadata_t.py +14 -9
  283. uncountable/types/recipe_output_metadata.py +0 -1
  284. uncountable/types/recipe_output_metadata_t.py +5 -2
  285. uncountable/types/recipe_tags.py +0 -1
  286. uncountable/types/recipe_tags_t.py +5 -2
  287. uncountable/types/recipe_workflow_steps.py +0 -1
  288. uncountable/types/recipe_workflow_steps_t.py +14 -7
  289. uncountable/types/recipes.py +0 -1
  290. uncountable/types/recipes_t.py +6 -2
  291. uncountable/types/response.py +0 -1
  292. uncountable/types/response_t.py +3 -2
  293. uncountable/types/secret_retrieval.py +0 -1
  294. uncountable/types/secret_retrieval_t.py +13 -7
  295. uncountable/types/sockets.py +20 -0
  296. uncountable/types/sockets_t.py +169 -0
  297. uncountable/types/structured_filters.py +25 -0
  298. uncountable/types/structured_filters_t.py +248 -0
  299. uncountable/types/units.py +0 -1
  300. uncountable/types/units_t.py +5 -2
  301. uncountable/types/uploader.py +24 -0
  302. uncountable/types/uploader_t.py +222 -0
  303. uncountable/types/users.py +0 -1
  304. uncountable/types/users_t.py +5 -2
  305. uncountable/types/webhook_job.py +9 -0
  306. uncountable/types/webhook_job_t.py +48 -0
  307. uncountable/types/workflows.py +0 -1
  308. uncountable/types/workflows_t.py +10 -4
  309. uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
  310. uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
  311. {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
  312. UncountablePythonSDK-0.0.52.dist-info/METADATA +0 -56
  313. UncountablePythonSDK-0.0.52.dist-info/RECORD +0 -246
  314. docs/quickstart.md +0 -19
  315. uncountable/core/version.py +0 -11
  316. {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -0
pkgs/type_spec/builder.py CHANGED
@@ -10,12 +10,26 @@ 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
19
33
 
20
34
 
21
35
  class StabilityLevel(StrEnum):
@@ -24,7 +38,6 @@ class StabilityLevel(StrEnum):
24
38
  """
25
39
 
26
40
  draft = "draft"
27
- alpha = "alpha"
28
41
  beta = "beta"
29
42
  stable = "stable"
30
43
 
@@ -47,7 +60,7 @@ class PropertyConvertValue(StrEnum):
47
60
  @dataclass
48
61
  class SpecProperty:
49
62
  name: str
50
- label: Optional[str]
63
+ label: str | None
51
64
  spec_type: SpecType
52
65
  extant: PropertyExtant
53
66
  convert_value: PropertyConvertValue
@@ -61,6 +74,7 @@ class SpecProperty:
61
74
  # Holds extra information that will be emitted along with type_info. The builder knows nothing
62
75
  # about the contents of this information.
63
76
  ext_info: Any = None
77
+ explicit_default: bool = False
64
78
 
65
79
 
66
80
  class NameCase(StrEnum):
@@ -255,7 +269,7 @@ class SpecTypeLiteralWrapper(SpecType):
255
269
  return [self.value_type]
256
270
 
257
271
 
258
- def unwrap_literal_type(stype: SpecType) -> Optional[SpecTypeLiteralWrapper]:
272
+ def unwrap_literal_type(stype: SpecType) -> SpecTypeLiteralWrapper | None:
259
273
  if isinstance(stype, SpecTypeInstance) and stype.defn_type.is_base_type(
260
274
  BaseTypeName.s_literal
261
275
  ):
@@ -283,7 +297,7 @@ class SpecTypeDefn(SpecType):
283
297
  ) -> None:
284
298
  self.namespace = namespace
285
299
  self.name = name
286
- self.label: Optional[str] = None
300
+ self.label: str | None = None
287
301
 
288
302
  self.is_predefined = is_predefined
289
303
  self.name_case = NameCase.convert
@@ -293,6 +307,8 @@ class SpecTypeDefn(SpecType):
293
307
  self._is_value_converted = _is_value_converted
294
308
  self._is_value_to_string = False
295
309
  self._is_valid_parameter = True
310
+ self._is_dynamic_allowed = False
311
+ self._default_extant: PropertyExtant | None = None
296
312
  self.ext_info: Any = None
297
313
 
298
314
  def is_value_converted(self) -> bool:
@@ -304,20 +320,43 @@ class SpecTypeDefn(SpecType):
304
320
  def is_valid_parameter(self) -> bool:
305
321
  return self._is_valid_parameter
306
322
 
323
+ def is_dynamic_allowed(self) -> bool:
324
+ return self._is_dynamic_allowed
325
+
307
326
  def is_base_type(self, type_: BaseTypeName) -> bool:
308
327
  return self.is_base and self.name == type_
309
328
 
329
+ def can_process(self, builder: SpecBuilder, data: RawDict) -> bool:
330
+ return True
331
+
310
332
  @abc.abstractmethod
311
333
  def process(self, builder: SpecBuilder, data: RawDict) -> None: ...
312
334
 
313
335
  def base_process(
314
336
  self, builder: SpecBuilder, data: RawDict, extra_names: list[str]
315
337
  ) -> None:
316
- 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
+ )
317
348
 
318
349
  self.ext_info = data.get("ext_info")
319
350
  self.label = data.get("label")
320
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
+
321
360
  def _process_property(
322
361
  self, builder: SpecBuilder, spec_name: str, data: RawDict
323
362
  ) -> SpecProperty:
@@ -336,18 +375,18 @@ class SpecTypeDefn(SpecType):
336
375
  ],
337
376
  )
338
377
  try:
339
- 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
340
383
  if spec_name.endswith("?"):
341
- if extant_type is not None:
384
+ if extant is not None:
342
385
  raise Exception("cannot specify extant with ?")
343
386
  extant = PropertyExtant.optional
344
387
  name = spec_name[:-1]
345
388
  else:
346
- extant = (
347
- PropertyExtant.required
348
- if extant_type is None
349
- else PropertyExtant(extant_type)
350
- )
389
+ extant = extant or PropertyExtant.required
351
390
  name = spec_name
352
391
 
353
392
  property_name_case = self.name_case
@@ -356,9 +395,9 @@ class SpecTypeDefn(SpecType):
356
395
  property_name_case = NameCase(name_case_raw)
357
396
 
358
397
  if property_name_case != NameCase.preserve:
359
- assert util.is_valid_property_name(
360
- name
361
- ), 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
+ )
362
401
 
363
402
  data_type = data.get("type")
364
403
  builder.ensure(data_type is not None, "missing `type` entry")
@@ -369,6 +408,7 @@ class SpecTypeDefn(SpecType):
369
408
  ptype = builder.parse_type(self.namespace, data_type, scope=self)
370
409
 
371
410
  default_spec = data.get("default", MISSING)
411
+ explicit_default = default_spec != MISSING
372
412
  if default_spec == MISSING:
373
413
  has_default = False
374
414
  default = None
@@ -376,7 +416,10 @@ class SpecTypeDefn(SpecType):
376
416
  has_default = True
377
417
  # IMPROVE: check the type against the ptype
378
418
  default = default_spec
379
-
419
+ if extant == PropertyExtant.missing and explicit_default:
420
+ raise Exception(
421
+ f"cannot have extant missing and default for property {name}"
422
+ )
380
423
  parse_require = False
381
424
  literal = unwrap_literal_type(ptype)
382
425
  if literal is not None:
@@ -399,6 +442,7 @@ class SpecTypeDefn(SpecType):
399
442
  parse_require=parse_require,
400
443
  desc=data.get("desc", None),
401
444
  ext_info=ext_info,
445
+ explicit_default=explicit_default,
402
446
  )
403
447
  finally:
404
448
  builder.pop_where()
@@ -436,7 +480,7 @@ class SpecTypeGenericParameter(SpecType):
436
480
 
437
481
 
438
482
  class SpecTypeDefnObject(SpecTypeDefn):
439
- base: Optional[SpecTypeDefnObject]
483
+ base: SpecTypeDefnObject | None
440
484
  parameters: list[str]
441
485
 
442
486
  def __init__(
@@ -444,7 +488,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
444
488
  namespace: SpecNamespace,
445
489
  name: str,
446
490
  *,
447
- parameters: Optional[list[str]] = None,
491
+ parameters: list[str] | None = None,
448
492
  is_base: bool = False,
449
493
  is_predefined: bool = False,
450
494
  is_hashable: bool = False,
@@ -461,7 +505,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
461
505
  self.parameters = parameters if parameters is not None else []
462
506
  self.is_hashable = is_hashable
463
507
  self.base = None
464
- self.properties: Optional[dict[str, SpecProperty]] = None
508
+ self.properties: dict[str, SpecProperty] | None = None
465
509
  self._kw_only: bool = True
466
510
  self.desc: str | None = None
467
511
 
@@ -532,13 +576,8 @@ class SpecTypeDefnObject(SpecTypeDefn):
532
576
  base_type: list[SpecType] = [self.base] if self.base is not None else []
533
577
  return base_type + prop_types
534
578
 
535
- def get_generic(self) -> Optional[str]:
536
- if len(self.parameters) > 0:
537
- assert (
538
- len(self.parameters) == 1
539
- ), "Only single generic parameters current supported"
540
- return self.parameters[0]
541
- return None
579
+ def get_generics(self) -> list[str]:
580
+ return self.parameters
542
581
 
543
582
 
544
583
  class SpecTypeDefnAlias(SpecTypeDefn):
@@ -592,24 +631,34 @@ class SpecTypeDefnUnion(SpecTypeDefn):
592
631
  self.discriminator_map = {}
593
632
  for sub_type in self.types:
594
633
  builder.push_where(sub_type.name)
595
- assert isinstance(
596
- sub_type, SpecTypeDefnObject
597
- ), "union-type-must-be-object"
634
+ assert isinstance(sub_type, SpecTypeDefnObject), (
635
+ "union-type-must-be-object"
636
+ )
598
637
  assert sub_type.properties is not None
599
638
  discriminator_type = sub_type.properties.get(self.discriminator)
600
- assert (
601
- discriminator_type is not None
602
- ), f"missing-discriminator-field: {sub_type}"
639
+ assert discriminator_type is not None, (
640
+ f"missing-discriminator-field: {sub_type}"
641
+ )
603
642
  prop_type = unwrap_literal_type(discriminator_type.spec_type)
604
643
  assert prop_type is not None
605
644
  assert prop_type.is_value_to_string()
606
- discriminant = str(prop_type.value)
607
- assert (
608
- discriminant not in self.discriminator_map
609
- ), f"duplicated-discriminant, {discriminant} in {sub_type}"
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
+ )
610
654
  self.discriminator_map[discriminant] = sub_type
611
655
 
612
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}")
613
662
 
614
663
  def get_referenced_types(self) -> list[SpecType]:
615
664
  return self.types
@@ -648,7 +697,7 @@ class SpecTypeDefnExternal(SpecTypeDefn):
648
697
  class StringEnumEntry:
649
698
  name: str
650
699
  value: str
651
- label: Optional[str] = None
700
+ label: str | None = None
652
701
  deprecated: bool = False
653
702
 
654
703
 
@@ -664,17 +713,32 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
664
713
  )
665
714
  self.values: dict[str, StringEnumEntry] = {}
666
715
  self.desc: str | None = None
667
- self.sql_type_name: Optional[str] = None
716
+ self.sql_type_name: str | None = None
668
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)
669
730
 
670
731
  def process(self, builder: SpecBuilder, data: RawDict) -> None:
671
732
  super().base_process(
672
- builder, data, ["type", "desc", "values", "name_case", "sql", "emit"]
733
+ builder,
734
+ data,
735
+ ["type", "desc", "values", "name_case", "sql", "emit", "source_enums"],
673
736
  )
674
737
  self.name_case = NameCase(data.get("name_case", "convert"))
675
738
  self.values = {}
676
- data_values = data["values"]
739
+ data_values = data.get("values")
677
740
  self.desc = data.get("desc", None)
741
+ source_enums = data.get("source_enums", None)
678
742
  if isinstance(data_values, dict):
679
743
  for name, value in data_values.items():
680
744
  builder.push_where(name)
@@ -687,6 +751,8 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
687
751
  builder.ensure(
688
752
  isinstance(enum_value, str), "enum value should be string"
689
753
  )
754
+ assert isinstance(enum_value, str)
755
+
690
756
  deprecated = value.get("deprecated", False)
691
757
  builder.ensure(
692
758
  isinstance(deprecated, bool),
@@ -712,10 +778,13 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
712
778
  elif isinstance(data_values, list):
713
779
  for value in data_values:
714
780
  if value in self.values:
715
- raise Exception("duplicate value in typespec enum", self.name, value)
781
+ raise Exception(
782
+ "duplicate value in typespec enum", self.name, value
783
+ )
716
784
  self.values[value] = StringEnumEntry(name=value, value=value)
717
785
  else:
718
- raise Exception("unsupported values type")
786
+ if source_enums is None or data_values is not None:
787
+ raise Exception("unsupported values type")
719
788
 
720
789
  sql_data = data.get("sql")
721
790
  if sql_data is not None:
@@ -737,14 +806,24 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
737
806
  builder.ensure(
738
807
  entry.label is not None, f"need-label-for-id-source:{entry.name}"
739
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()
740
818
 
741
819
  def get_referenced_types(self) -> list[SpecType]:
742
- return []
820
+ return self.source_enums
743
821
 
744
822
 
745
823
  TOKEN_ENDPOINT = "$endpoint"
746
824
  TOKEN_EMIT_IO_TS = "$emit_io_ts"
747
825
  TOKEN_EMIT_TYPE_INFO = "$emit_type_info"
826
+ TOKEN_EMIT_TYPE_INFO_PYTHON = "$emit_type_info_python"
748
827
  # The import token is only for explicit ordering of the files, to process constants
749
828
  # and enums correctly. It does not impact the final generation of files, or the
750
829
  # language imports. Those are still auto-resolved.
@@ -769,13 +848,13 @@ RE_ENDPOINT_ROOT = re.compile(r"\${([_a-z]+)}")
769
848
 
770
849
  @dataclass(kw_only=True, frozen=True)
771
850
  class _EndpointPathDetails:
772
- root: str
851
+ root: EndpointKey
773
852
  root_path: str
774
853
  resolved_path: str
775
854
 
776
855
 
777
856
  def _resolve_endpoint_path(
778
- path: str, api_endpoints: dict[str, str]
857
+ path: str, api_endpoints: dict[EndpointKey, APIEndpointInfo]
779
858
  ) -> _EndpointPathDetails:
780
859
  root_path_source = path.split("/")[0]
781
860
  root_match = RE_ENDPOINT_ROOT.fullmatch(root_path_source)
@@ -783,7 +862,7 @@ def _resolve_endpoint_path(
783
862
  raise Exception(f"invalid-api-path-root:{root_path_source}")
784
863
 
785
864
  root_var = root_match.group(1)
786
- root_path = api_endpoints[root_var]
865
+ root_path = api_endpoints[root_var].root_path
787
866
 
788
867
  _, *rest_path = path.split("/", 1)
789
868
  resolved_path = "/".join([root_path] + rest_path)
@@ -793,19 +872,65 @@ def _resolve_endpoint_path(
793
872
  )
794
873
 
795
874
 
796
- class SpecEndpoint:
797
- method: RouteMethod
798
- 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
799
884
  path_root: str
800
885
  path_dirname: str
801
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
802
928
  data_loader: bool
803
- is_sdk: bool
804
- is_beta: bool
929
+ is_sdk: EndpointEmitType
805
930
  stability_level: StabilityLevel | None
806
931
  # Don't emit TypeScript endpoint code
807
932
  suppress_ts: bool
808
- function: Optional[str]
933
+ deprecated: bool = False
809
934
  async_batch_path: str | None = None
810
935
  result_type: ResultType = ResultType.json
811
936
  has_attachment: bool = False
@@ -813,21 +938,24 @@ class SpecEndpoint:
813
938
  account_type: str | None
814
939
  route_group: str | None
815
940
 
941
+ # function, path details per api endpoint
942
+ path_per_api_endpoint: dict[str, EndpointSpecificPath]
943
+ default_endpoint_key: EndpointKey
944
+
816
945
  is_external: bool = False
817
946
 
818
947
  def __init__(self) -> None:
819
948
  pass
820
949
 
821
950
  def process(self, builder: SpecBuilder, data: RawDict) -> None:
822
- unused(builder)
823
951
  util.check_fields(
824
952
  data,
825
953
  [
826
954
  "method",
827
955
  "path",
828
956
  "data_loader",
957
+ "deprecated",
829
958
  "is_sdk",
830
- "is_beta",
831
959
  "stability_level",
832
960
  "async_batch_path",
833
961
  "function",
@@ -838,24 +966,32 @@ class SpecEndpoint:
838
966
  "has_attachment",
839
967
  "account_type",
840
968
  "route_group",
841
- ],
969
+ ]
970
+ + list(builder.api_endpoints.keys()),
842
971
  )
843
972
  self.method = RouteMethod(data["method"])
844
973
 
845
- path = data["path"].split("/")
846
-
847
- assert len(path) > 1, "invalid-endpoint-path"
848
-
849
- # handle ${external} in the same way we handle ${materials} for now
850
- self.path_dirname = "/".join(path[1:-1])
851
- self.path_basename = path[-1]
852
-
853
974
  data_loader = data.get("data_loader", False)
854
975
  assert isinstance(data_loader, bool)
855
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)
856
994
 
857
- is_sdk = data.get("is_sdk", False)
858
- assert isinstance(is_sdk, bool)
859
995
  self.is_sdk = is_sdk
860
996
 
861
997
  route_group = data.get("route_group")
@@ -866,10 +1002,6 @@ class SpecEndpoint:
866
1002
  assert account_type is None or isinstance(account_type, str)
867
1003
  self.account_type = account_type
868
1004
 
869
- is_beta = data.get("is_beta", False)
870
- assert isinstance(is_beta, bool)
871
- self.is_beta = is_beta
872
-
873
1005
  stability_level_raw = data.get("stability_level")
874
1006
  assert stability_level_raw is None or isinstance(stability_level_raw, str)
875
1007
  self.stability_level = (
@@ -883,29 +1015,70 @@ class SpecEndpoint:
883
1015
  assert isinstance(async_batch_path, str)
884
1016
  self.async_batch_path = async_batch_path
885
1017
 
886
- self.function = data.get("function")
887
-
888
1018
  suppress_ts = data.get("suppress_ts", False)
889
1019
  assert isinstance(suppress_ts, bool)
890
1020
  self.suppress_ts = suppress_ts
891
1021
 
892
1022
  self.result_type = ResultType(data.get("result_type", ResultType.json.value))
893
-
894
- path_details = _resolve_endpoint_path(data["path"], builder.api_endpoints)
895
- self.root = path_details.root
896
- self.path_root = path_details.root_path
1023
+ self.has_attachment = data.get("has_attachment", False)
897
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
+
898
1068
  # IMPROVE: remove need for is_external flag
899
- self.is_external = self.path_root == "api/external"
900
- 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
+ )
901
1073
 
902
- assert (
903
- not is_sdk or self.desc is not None
904
- ), 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
+ )
905
1077
 
906
1078
  @property
907
1079
  def resolved_path(self: Self) -> str:
908
- 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}"
909
1082
 
910
1083
 
911
1084
  def _parse_const(
@@ -928,7 +1101,7 @@ def _parse_const(
928
1101
  elif const_type.defn_type.name == BaseTypeName.s_dict:
929
1102
  assert isinstance(value, dict)
930
1103
  builder.ensure(
931
- len(const_type.parameters) == 2, "constant-dict-expects-one-type"
1104
+ len(const_type.parameters) == 2, "constant-dict-expects-two-types"
932
1105
  )
933
1106
  key_type = const_type.parameters[0]
934
1107
  value_type = const_type.parameters[1]
@@ -972,7 +1145,14 @@ def _parse_const(
972
1145
  return value
973
1146
 
974
1147
  if const_type.name == BaseTypeName.s_boolean:
975
- 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")
976
1156
  return value
977
1157
 
978
1158
  raise Exception("unsupported-const-scalar-type", const_type)
@@ -1004,7 +1184,9 @@ class SpecConstant:
1004
1184
  assert isinstance(self.value, dict)
1005
1185
  # the parsing checks that the values are correct, so a simple length check
1006
1186
  # should be enough to check completeness
1007
- 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
+ )
1008
1190
 
1009
1191
 
1010
1192
  class SpecNamespace:
@@ -1014,14 +1196,15 @@ class SpecNamespace:
1014
1196
  ):
1015
1197
  self.types: dict[str, SpecTypeDefn] = {}
1016
1198
  self.constants: dict[str, SpecConstant] = {}
1017
- self.endpoint: Optional[SpecEndpoint] = None
1199
+ self.endpoint: SpecEndpoint | None = None
1018
1200
  self.emit_io_ts = False
1019
1201
  self.emit_type_info = False
1202
+ self.emit_type_info_python = False
1020
1203
  self.derive_types_from_io_ts = False
1021
- self._imports: Optional[list[str]] = None
1204
+ self._imports: list[str] | None = None
1022
1205
  self.path = name.split(".")
1023
1206
  self.name = self.path[-1]
1024
- self._order: Optional[int] = None
1207
+ self._order: int | None = None
1025
1208
 
1026
1209
  def _update_order(self, builder: SpecBuilder, recurse: int = 0) -> int:
1027
1210
  if self._order is not None:
@@ -1071,6 +1254,11 @@ class SpecNamespace:
1071
1254
  self.emit_type_info = defn
1072
1255
  continue
1073
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
+
1074
1262
  if name == TOKEN_IMPORT:
1075
1263
  assert self._imports is None
1076
1264
  imports = [defn] if isinstance(defn, str) else defn
@@ -1079,16 +1267,17 @@ class SpecNamespace:
1079
1267
  continue
1080
1268
 
1081
1269
  if "value" in defn:
1082
- assert util.is_valid_property_name(
1083
- name
1084
- ), 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
+ )
1085
1273
  spec_constant = SpecConstant(self, name)
1086
1274
  self.constants[name] = spec_constant
1087
1275
  continue
1088
1276
 
1089
1277
  assert util.is_valid_type_name(name), f"{name} is not a valid type name"
1090
1278
  assert name not in self.types, f"{name} is duplicate"
1091
- defn_type = defn["type"]
1279
+ defn_type = defn.get("type")
1280
+ assert isinstance(defn_type, str), f"{name} requires a string type"
1092
1281
  spec_type: SpecTypeDefn
1093
1282
  if defn_type == DefnTypeName.s_alias:
1094
1283
  spec_type = SpecTypeDefnAlias(self, name)
@@ -1100,7 +1289,11 @@ class SpecNamespace:
1100
1289
  spec_type = SpecTypeDefnStringEnum(self, name)
1101
1290
  else:
1102
1291
  parameters = (
1103
- [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
+ ]
1104
1297
  if parsed_name.parameters is not None
1105
1298
  else None
1106
1299
  )
@@ -1116,28 +1309,46 @@ class SpecNamespace:
1116
1309
  Complete the definition of each type.
1117
1310
  """
1118
1311
  builder.push_where(self.name)
1119
- for full_name, defn in data.items():
1120
- parsed_name = parse_type_str(full_name)[0]
1121
- 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
1122
1331
 
1123
- if name in [TOKEN_EMIT_IO_TS, TOKEN_EMIT_TYPE_INFO, TOKEN_IMPORT]:
1124
- continue
1332
+ builder.push_where(name)
1125
1333
 
1126
- builder.push_where(name)
1334
+ if "value" in defn:
1335
+ spec_constant = self.constants[name]
1336
+ spec_constant.process(builder, defn)
1127
1337
 
1128
- if "value" in defn:
1129
- spec_constant = self.constants[name]
1130
- spec_constant.process(builder, defn)
1338
+ elif name == TOKEN_ENDPOINT:
1339
+ assert self.endpoint
1340
+ self.endpoint.process(builder, defn)
1131
1341
 
1132
- elif name == TOKEN_ENDPOINT:
1133
- assert self.endpoint
1134
- self.endpoint.process(builder, defn)
1135
-
1136
- else:
1137
- spec_type = self.types[name]
1138
- 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)
1139
1348
 
1140
- 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]
1141
1352
 
1142
1353
  builder.pop_where()
1143
1354
 
@@ -1152,8 +1363,21 @@ class NamespaceDataPair:
1152
1363
  data: RawDict
1153
1364
 
1154
1365
 
1366
+ @dataclass(kw_only=True)
1367
+ class NameDataPair:
1368
+ full_name: str
1369
+ data: RawDict
1370
+
1371
+
1155
1372
  class SpecBuilder:
1156
- 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
1157
1381
  self.where: list[str] = []
1158
1382
  self.namespaces = {}
1159
1383
  self.pending: list[NamespaceDataPair] = []
@@ -1162,6 +1386,7 @@ class SpecBuilder:
1162
1386
  self.examples: dict[str, list[SpecEndpointExample]] = defaultdict(list)
1163
1387
  self.guides: dict[SpecGuideKey, list[SpecGuide]] = defaultdict(list)
1164
1388
  self.api_endpoints = api_endpoints
1389
+ self.cross_output_paths = cross_output_paths
1165
1390
  base_namespace = SpecNamespace(name=base_namespace_name)
1166
1391
  for base_type in BaseTypeName:
1167
1392
  defn = SpecTypeDefnObject(base_namespace, base_type, is_base=True)
@@ -1179,9 +1404,13 @@ class SpecBuilder:
1179
1404
  self.emit_id_source_enums: set[SpecTypeDefnStringEnum] = set()
1180
1405
 
1181
1406
  this_dir = os.path.dirname(os.path.realpath(__file__))
1182
- 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:
1183
1410
  self.preparts["python"][base_namespace_name] = py_base_part.read()
1184
- 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:
1185
1414
  self.preparts["typescript"][base_namespace_name] = ts_base_part.read()
1186
1415
 
1187
1416
  base_namespace.types["ObjectId"] = SpecTypeDefnObject(
@@ -1250,7 +1479,7 @@ class SpecBuilder:
1250
1479
  self,
1251
1480
  path: util.ParsedTypePath,
1252
1481
  namespace: SpecNamespace,
1253
- scope: Optional[SpecTypeDefn] = None,
1482
+ scope: SpecTypeDefn | None = None,
1254
1483
  top: bool = False,
1255
1484
  ) -> SpecType:
1256
1485
  """
@@ -1308,8 +1537,10 @@ class SpecBuilder:
1308
1537
  if len(path) == 2:
1309
1538
  if isinstance(defn_type, SpecTypeDefnStringEnum):
1310
1539
  assert path[1].parameters is None
1540
+ statement = f"$import: [{defn_type.namespace.name}]"
1311
1541
  self.ensure(
1312
- 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}",
1313
1544
  )
1314
1545
  return SpecTypeLiteralWrapper(
1315
1546
  value=path[1].name,
@@ -1331,11 +1562,13 @@ class SpecBuilder:
1331
1562
  )
1332
1563
 
1333
1564
  def parse_type(
1334
- self, namespace: SpecNamespace, spec: str, scope: Optional[SpecTypeDefn] = None
1565
+ self, namespace: SpecNamespace, spec: str, scope: SpecTypeDefn | None = None
1335
1566
  ) -> SpecType:
1336
1567
  self.push_where(spec)
1337
1568
  parsed_type = util.parse_type_str(spec)
1338
- 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
+ )
1339
1572
  self.pop_where()
1340
1573
  return result
1341
1574
 
@@ -1385,7 +1618,9 @@ class SpecBuilder:
1385
1618
  path_meta: list[str] | None = meta.get("path")
1386
1619
  guide_key: SpecGuideKey = RootGuideKey()
1387
1620
  if path_meta is not None:
1388
- 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
+ )
1389
1624
  guide_key = EndpointGuideKey(path=path_details.resolved_path)
1390
1625
 
1391
1626
  self.guides[guide_key].append(