UncountablePythonSDK 0.0.7__py3-none-any.whl → 0.0.92__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (311) hide show
  1. UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
  2. UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
  3. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
  4. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
  5. docs/.gitignore +1 -0
  6. docs/conf.py +57 -0
  7. docs/index.md +13 -0
  8. docs/justfile +12 -0
  9. docs/quickstart.md +19 -0
  10. docs/requirements.txt +7 -0
  11. docs/static/favicons/android-chrome-192x192.png +0 -0
  12. docs/static/favicons/android-chrome-512x512.png +0 -0
  13. docs/static/favicons/apple-touch-icon.png +0 -0
  14. docs/static/favicons/browserconfig.xml +9 -0
  15. docs/static/favicons/favicon-16x16.png +0 -0
  16. docs/static/favicons/favicon-32x32.png +0 -0
  17. docs/static/favicons/manifest.json +18 -0
  18. docs/static/favicons/mstile-150x150.png +0 -0
  19. docs/static/favicons/safari-pinned-tab.svg +32 -0
  20. docs/static/logo_blue.png +0 -0
  21. examples/async_batch.py +35 -0
  22. examples/create_entity.py +22 -17
  23. examples/download_files.py +26 -0
  24. examples/edit_recipe_inputs.py +50 -0
  25. examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
  26. examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
  27. examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
  28. examples/integration-server/pyproject.toml +224 -0
  29. examples/invoke_uploader.py +26 -0
  30. examples/set_recipe_metadata_file.py +40 -0
  31. examples/set_recipe_output_file_sdk.py +26 -0
  32. examples/upload_files.py +18 -0
  33. pkgs/argument_parser/__init__.py +5 -0
  34. pkgs/argument_parser/_is_enum.py +1 -6
  35. pkgs/argument_parser/argument_parser.py +232 -76
  36. pkgs/argument_parser/case_convert.py +4 -3
  37. pkgs/filesystem_utils/__init__.py +20 -0
  38. pkgs/filesystem_utils/_blob_session.py +137 -0
  39. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  40. pkgs/filesystem_utils/_local_session.py +69 -0
  41. pkgs/filesystem_utils/_s3_session.py +117 -0
  42. pkgs/filesystem_utils/_sftp_session.py +147 -0
  43. pkgs/filesystem_utils/file_type_utils.py +91 -0
  44. pkgs/filesystem_utils/filesystem_session.py +39 -0
  45. pkgs/py.typed +0 -0
  46. pkgs/serialization/__init__.py +8 -1
  47. pkgs/serialization/annotation.py +64 -0
  48. pkgs/serialization/opaque_key.py +1 -1
  49. pkgs/serialization/serial_alias.py +47 -0
  50. pkgs/serialization/serial_class.py +65 -50
  51. pkgs/serialization/serial_generic.py +16 -0
  52. pkgs/serialization/serial_union.py +84 -0
  53. pkgs/serialization/yaml.py +57 -0
  54. pkgs/serialization_util/__init__.py +7 -7
  55. pkgs/serialization_util/_get_type_for_serialization.py +1 -3
  56. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  57. pkgs/serialization_util/dataclasses.py +14 -0
  58. pkgs/serialization_util/serialization_helpers.py +118 -73
  59. pkgs/strenum_compat/strenum_compat.py +1 -9
  60. pkgs/type_spec/actions_registry/__init__.py +0 -0
  61. pkgs/type_spec/actions_registry/__main__.py +126 -0
  62. pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
  63. pkgs/type_spec/builder.py +475 -89
  64. pkgs/type_spec/config.py +24 -19
  65. pkgs/type_spec/emit_io_ts.py +5 -2
  66. pkgs/type_spec/emit_open_api.py +266 -32
  67. pkgs/type_spec/emit_open_api_util.py +32 -13
  68. pkgs/type_spec/emit_python.py +601 -150
  69. pkgs/type_spec/emit_typescript.py +74 -273
  70. pkgs/type_spec/emit_typescript_util.py +239 -5
  71. pkgs/type_spec/load_types.py +55 -10
  72. pkgs/type_spec/open_api_util.py +30 -41
  73. pkgs/type_spec/parts/base.py.prepart +4 -3
  74. pkgs/type_spec/type_info/emit_type_info.py +178 -16
  75. pkgs/type_spec/util.py +11 -11
  76. pkgs/type_spec/value_spec/__main__.py +3 -3
  77. pkgs/type_spec/value_spec/convert_type.py +8 -1
  78. pkgs/type_spec/value_spec/emit_python.py +13 -4
  79. uncountable/__init__.py +1 -2
  80. uncountable/core/__init__.py +12 -2
  81. uncountable/core/async_batch.py +37 -0
  82. uncountable/core/client.py +293 -43
  83. uncountable/core/environment.py +41 -0
  84. uncountable/core/file_upload.py +135 -0
  85. uncountable/core/types.py +17 -0
  86. uncountable/integration/__init__.py +0 -0
  87. uncountable/integration/cli.py +49 -0
  88. uncountable/integration/construct_client.py +51 -0
  89. uncountable/integration/cron.py +29 -0
  90. uncountable/integration/db/__init__.py +0 -0
  91. uncountable/integration/db/connect.py +18 -0
  92. uncountable/integration/db/session.py +25 -0
  93. uncountable/integration/entrypoint.py +13 -0
  94. uncountable/integration/executors/__init__.py +0 -0
  95. uncountable/integration/executors/executors.py +148 -0
  96. uncountable/integration/executors/generic_upload_executor.py +284 -0
  97. uncountable/integration/executors/script_executor.py +25 -0
  98. uncountable/integration/job.py +87 -0
  99. uncountable/integration/queue_runner/__init__.py +0 -0
  100. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  101. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  102. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  103. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  104. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  105. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  108. uncountable/integration/queue_runner/command_server/types.py +52 -0
  109. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  110. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  111. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  112. uncountable/integration/queue_runner/datastore/model.py +17 -0
  113. uncountable/integration/queue_runner/job_scheduler.py +163 -0
  114. uncountable/integration/queue_runner/queue_runner.py +26 -0
  115. uncountable/integration/queue_runner/types.py +7 -0
  116. uncountable/integration/queue_runner/worker.py +119 -0
  117. uncountable/integration/scan_profiles.py +67 -0
  118. uncountable/integration/scheduler.py +150 -0
  119. uncountable/integration/secret_retrieval/__init__.py +3 -0
  120. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  121. uncountable/integration/server.py +117 -0
  122. uncountable/integration/telemetry.py +209 -0
  123. uncountable/integration/webhook_server/entrypoint.py +170 -0
  124. uncountable/types/__init__.py +151 -5
  125. uncountable/types/api/batch/execute_batch.py +15 -7
  126. uncountable/types/api/batch/execute_batch_load_async.py +42 -0
  127. uncountable/types/api/chemical/__init__.py +1 -0
  128. uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
  129. uncountable/types/api/entity/create_entities.py +23 -10
  130. uncountable/types/api/entity/create_entity.py +21 -12
  131. uncountable/types/api/entity/get_entities_data.py +19 -29
  132. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  133. uncountable/types/api/entity/list_entities.py +28 -20
  134. uncountable/types/api/entity/lock_entity.py +45 -0
  135. uncountable/types/api/entity/resolve_entity_ids.py +19 -7
  136. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  137. uncountable/types/api/entity/set_values.py +13 -28
  138. uncountable/types/api/entity/transition_entity_phase.py +80 -0
  139. uncountable/types/api/entity/unlock_entity.py +44 -0
  140. uncountable/types/api/equipment/__init__.py +1 -0
  141. uncountable/types/api/equipment/associate_equipment_input.py +44 -0
  142. uncountable/types/api/field_options/__init__.py +1 -0
  143. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  144. uncountable/types/api/files/__init__.py +1 -0
  145. uncountable/types/api/files/download_file.py +77 -0
  146. uncountable/types/api/id_source/__init__.py +1 -0
  147. uncountable/types/api/id_source/list_id_source.py +56 -0
  148. uncountable/types/api/id_source/match_id_source.py +54 -0
  149. uncountable/types/api/input_groups/get_input_group_names.py +18 -7
  150. uncountable/types/api/inputs/create_inputs.py +25 -24
  151. uncountable/types/api/inputs/get_input_data.py +37 -31
  152. uncountable/types/api/inputs/get_input_names.py +20 -9
  153. uncountable/types/api/inputs/get_inputs_data.py +33 -27
  154. uncountable/types/api/inputs/set_input_attribute_values.py +18 -13
  155. uncountable/types/api/inputs/set_input_category.py +44 -0
  156. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  157. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  158. uncountable/types/api/material_families/__init__.py +1 -0
  159. uncountable/types/api/material_families/update_entity_material_families.py +48 -0
  160. uncountable/types/api/outputs/get_output_data.py +38 -29
  161. uncountable/types/api/outputs/get_output_names.py +20 -9
  162. uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
  163. uncountable/types/api/permissions/__init__.py +1 -0
  164. uncountable/types/api/permissions/set_core_permissions.py +105 -0
  165. uncountable/types/api/project/get_projects.py +23 -19
  166. uncountable/types/api/project/get_projects_data.py +26 -43
  167. uncountable/types/api/recipe_links/__init__.py +1 -0
  168. uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
  169. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  170. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +21 -10
  171. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  172. uncountable/types/api/recipes/archive_recipes.py +42 -0
  173. uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
  174. uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
  175. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  176. uncountable/types/api/recipes/create_recipe.py +51 -0
  177. uncountable/types/api/recipes/create_recipes.py +25 -24
  178. uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
  179. uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
  180. uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
  181. uncountable/types/api/recipes/get_curve.py +13 -27
  182. uncountable/types/api/recipes/get_recipe_calculations.py +21 -21
  183. uncountable/types/api/recipes/get_recipe_links.py +14 -6
  184. uncountable/types/api/recipes/get_recipe_names.py +18 -7
  185. uncountable/types/api/recipes/get_recipe_output_metadata.py +18 -19
  186. uncountable/types/api/recipes/get_recipes_data.py +83 -144
  187. uncountable/types/api/recipes/lock_recipes.py +63 -0
  188. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  189. uncountable/types/api/recipes/set_recipe_inputs.py +21 -11
  190. uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
  191. uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
  192. uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
  193. uncountable/types/api/recipes/set_recipe_outputs.py +28 -15
  194. uncountable/types/api/recipes/set_recipe_tags.py +109 -0
  195. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  196. uncountable/types/api/recipes/unlock_recipes.py +50 -0
  197. uncountable/types/api/triggers/__init__.py +1 -0
  198. uncountable/types/api/triggers/run_trigger.py +43 -0
  199. uncountable/types/api/uploader/__init__.py +1 -0
  200. uncountable/types/api/uploader/invoke_uploader.py +47 -0
  201. uncountable/types/async_batch.py +13 -0
  202. uncountable/types/async_batch_processor.py +384 -0
  203. uncountable/types/async_batch_t.py +97 -0
  204. uncountable/types/async_jobs.py +9 -0
  205. uncountable/types/async_jobs_t.py +53 -0
  206. uncountable/types/auth_retrieval.py +12 -0
  207. uncountable/types/auth_retrieval_t.py +75 -0
  208. uncountable/types/base.py +5 -78
  209. uncountable/types/base_t.py +85 -0
  210. uncountable/types/calculations.py +8 -0
  211. uncountable/types/calculations_t.py +27 -0
  212. uncountable/types/chemical_structure.py +8 -0
  213. uncountable/types/chemical_structure_t.py +28 -0
  214. uncountable/types/client_base.py +1115 -76
  215. uncountable/types/client_config.py +8 -0
  216. uncountable/types/client_config_t.py +26 -0
  217. uncountable/types/curves.py +10 -0
  218. uncountable/types/curves_t.py +51 -0
  219. uncountable/types/entity.py +8 -266
  220. uncountable/types/entity_t.py +393 -0
  221. uncountable/types/experiment_groups.py +8 -0
  222. uncountable/types/experiment_groups_t.py +27 -0
  223. uncountable/types/field_values.py +17 -23
  224. uncountable/types/field_values_t.py +204 -0
  225. uncountable/types/fields.py +8 -0
  226. uncountable/types/fields_t.py +28 -0
  227. uncountable/types/generic_upload.py +15 -0
  228. uncountable/types/generic_upload_t.py +119 -0
  229. uncountable/types/id_source.py +12 -0
  230. uncountable/types/id_source_t.py +68 -0
  231. uncountable/types/identifier.py +11 -0
  232. uncountable/types/identifier_t.py +63 -0
  233. uncountable/types/input_attributes.py +8 -0
  234. uncountable/types/input_attributes_t.py +30 -0
  235. uncountable/types/inputs.py +11 -0
  236. uncountable/types/inputs_t.py +83 -0
  237. uncountable/types/integration_server.py +9 -0
  238. uncountable/types/integration_server_t.py +42 -0
  239. uncountable/types/job_definition.py +27 -0
  240. uncountable/types/job_definition_t.py +260 -0
  241. uncountable/types/outputs.py +8 -0
  242. uncountable/types/outputs_t.py +30 -0
  243. uncountable/types/overrides.py +10 -0
  244. uncountable/types/overrides_t.py +49 -0
  245. uncountable/types/permissions.py +8 -0
  246. uncountable/types/permissions_t.py +46 -0
  247. uncountable/types/phases.py +8 -0
  248. uncountable/types/phases_t.py +27 -0
  249. uncountable/types/post_base.py +8 -0
  250. uncountable/types/post_base_t.py +30 -0
  251. uncountable/types/queued_job.py +16 -0
  252. uncountable/types/queued_job_t.py +123 -0
  253. uncountable/types/recipe_identifiers.py +12 -0
  254. uncountable/types/recipe_identifiers_t.py +76 -0
  255. uncountable/types/recipe_inputs.py +9 -0
  256. uncountable/types/recipe_inputs_t.py +30 -0
  257. uncountable/types/recipe_links.py +4 -44
  258. uncountable/types/recipe_links_t.py +54 -0
  259. uncountable/types/recipe_metadata.py +10 -0
  260. uncountable/types/recipe_metadata_t.py +58 -0
  261. uncountable/types/recipe_output_metadata.py +8 -0
  262. uncountable/types/recipe_output_metadata_t.py +28 -0
  263. uncountable/types/recipe_tags.py +8 -0
  264. uncountable/types/recipe_tags_t.py +27 -0
  265. uncountable/types/recipe_workflow_steps.py +14 -0
  266. uncountable/types/recipe_workflow_steps_t.py +95 -0
  267. uncountable/types/recipes.py +8 -0
  268. uncountable/types/recipes_t.py +25 -0
  269. uncountable/types/response.py +8 -0
  270. uncountable/types/response_t.py +26 -0
  271. uncountable/types/secret_retrieval.py +12 -0
  272. uncountable/types/secret_retrieval_t.py +75 -0
  273. uncountable/types/units.py +8 -0
  274. uncountable/types/units_t.py +27 -0
  275. uncountable/types/users.py +8 -0
  276. uncountable/types/users_t.py +28 -0
  277. uncountable/types/webhook_job.py +9 -0
  278. uncountable/types/webhook_job_t.py +37 -0
  279. uncountable/types/workflows.py +9 -0
  280. uncountable/types/workflows_t.py +39 -0
  281. UncountablePythonSDK-0.0.7.dist-info/METADATA +0 -27
  282. UncountablePythonSDK-0.0.7.dist-info/RECORD +0 -119
  283. examples/recipe-import/importer.py +0 -39
  284. type_spec/external/api/batch/execute_batch.yaml +0 -56
  285. type_spec/external/api/entity/create_entities.yaml +0 -33
  286. type_spec/external/api/entity/create_entity.yaml +0 -39
  287. type_spec/external/api/entity/get_entities_data.yaml +0 -55
  288. type_spec/external/api/entity/list_entities.yaml +0 -62
  289. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  290. type_spec/external/api/entity/set_values.yaml +0 -45
  291. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  292. type_spec/external/api/inputs/create_inputs.yaml +0 -61
  293. type_spec/external/api/inputs/get_input_data.yaml +0 -108
  294. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  295. type_spec/external/api/inputs/get_inputs_data.yaml +0 -95
  296. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -37
  297. type_spec/external/api/outputs/get_output_data.yaml +0 -103
  298. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  299. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  300. type_spec/external/api/project/get_projects.yaml +0 -52
  301. type_spec/external/api/project/get_projects_data.yaml +0 -86
  302. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  303. type_spec/external/api/recipes/create_recipes.yaml +0 -60
  304. type_spec/external/api/recipes/get_curve.yaml +0 -50
  305. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -49
  306. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  307. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  308. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -49
  309. type_spec/external/api/recipes/get_recipes_data.yaml +0 -372
  310. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
  311. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -56
pkgs/type_spec/builder.py CHANGED
@@ -9,22 +9,34 @@ import os
9
9
  import re
10
10
  from collections import defaultdict
11
11
  from dataclasses import MISSING, dataclass
12
- from enum import Enum, auto
13
- from typing import Any, Dict, Optional
12
+ from enum import Enum, StrEnum, auto
13
+ from typing import Any, Self
14
14
 
15
15
  from . import util
16
16
  from .util import parse_type_str, unused
17
17
 
18
- RawDict = Dict[Any, Any]
18
+ RawDict = dict[Any, Any]
19
+ EndpointKey = str
19
20
 
20
21
 
21
- class PropertyExtant(str, Enum):
22
+ class StabilityLevel(StrEnum):
23
+ """These are currently used for open api,
24
+ see: https://github.com/Tufin/oasdiff/blob/main/docs/STABILITY.md
25
+ """
26
+
27
+ draft = "draft"
28
+ alpha = "alpha"
29
+ beta = "beta"
30
+ stable = "stable"
31
+
32
+
33
+ class PropertyExtant(StrEnum):
22
34
  required = "required"
23
35
  optional = "optional"
24
36
  missing = "missing"
25
37
 
26
38
 
27
- class PropertyConvertValue(str, Enum):
39
+ class PropertyConvertValue(StrEnum):
28
40
  # Base conversion on underlying types
29
41
  auto = "auto"
30
42
  # Always convert the value (Not needed yet, thus not supported)
@@ -36,7 +48,7 @@ class PropertyConvertValue(str, Enum):
36
48
  @dataclass
37
49
  class SpecProperty:
38
50
  name: str
39
- label: Optional[str]
51
+ label: str | None
40
52
  spec_type: SpecType
41
53
  extant: PropertyExtant
42
54
  convert_value: PropertyConvertValue
@@ -52,7 +64,7 @@ class SpecProperty:
52
64
  ext_info: Any = None
53
65
 
54
66
 
55
- class NameCase(str, Enum):
67
+ class NameCase(StrEnum):
56
68
  convert = "convert"
57
69
  preserve = "preserve"
58
70
  # Upper-case in JavaScript, convert otherwise. This is a compatibilty
@@ -60,7 +72,7 @@ class NameCase(str, Enum):
60
72
  js_upper = "js_upper"
61
73
 
62
74
 
63
- class BaseTypeName(str, Enum):
75
+ class BaseTypeName(StrEnum):
64
76
  """
65
77
  Base types that are supported.
66
78
  """
@@ -86,13 +98,14 @@ class BaseTypeName(str, Enum):
86
98
  s_optional = "Optional"
87
99
  s_string = "String"
88
100
  s_tuple = "Tuple"
101
+ s_readonly_array = "ReadonlyArray"
89
102
  s_union = "Union"
90
103
 
91
104
  # For a root class that defines properties
92
105
  s_object = "Object"
93
106
 
94
107
 
95
- class DefnTypeName(str, Enum):
108
+ class DefnTypeName(StrEnum):
96
109
  # Type is a named alias of another type
97
110
  s_alias = "Alias"
98
111
  # Type is imported from an external source (opaque to type_spec)
@@ -101,6 +114,8 @@ class DefnTypeName(str, Enum):
101
114
  s_string_enum = "StringEnum"
102
115
  # a particular literal value
103
116
  s_string_literal = "_StringLiteral"
117
+ # A union of several other types
118
+ s_union = "Union"
104
119
 
105
120
 
106
121
  base_namespace_name = "base"
@@ -184,6 +199,35 @@ class SpecTypeInstance(SpecType):
184
199
  return defn_type + self.parameters
185
200
 
186
201
 
202
+ @dataclass(kw_only=True)
203
+ class SpecEndpointExample:
204
+ summary: str
205
+ description: str
206
+ arguments: dict[str, object]
207
+ data: dict[str, object]
208
+
209
+
210
+ @dataclass(kw_only=True)
211
+ class SpecGuide:
212
+ ref_name: str
213
+ title: str
214
+ markdown_content: str
215
+ html_content: str
216
+
217
+
218
+ @dataclass(kw_only=True, frozen=True)
219
+ class RootGuideKey:
220
+ pass
221
+
222
+
223
+ @dataclass(kw_only=True, frozen=True)
224
+ class EndpointGuideKey:
225
+ path: str
226
+
227
+
228
+ SpecGuideKey = RootGuideKey | EndpointGuideKey
229
+
230
+
187
231
  class SpecTypeLiteralWrapper(SpecType):
188
232
  def __init__(
189
233
  self,
@@ -212,7 +256,7 @@ class SpecTypeLiteralWrapper(SpecType):
212
256
  return [self.value_type]
213
257
 
214
258
 
215
- def unwrap_literal_type(stype: SpecType) -> Optional[SpecTypeLiteralWrapper]:
259
+ def unwrap_literal_type(stype: SpecType) -> SpecTypeLiteralWrapper | None:
216
260
  if isinstance(stype, SpecTypeInstance) and stype.defn_type.is_base_type(
217
261
  BaseTypeName.s_literal
218
262
  ):
@@ -240,7 +284,7 @@ class SpecTypeDefn(SpecType):
240
284
  ) -> None:
241
285
  self.namespace = namespace
242
286
  self.name = name
243
- self.label: Optional[str] = None
287
+ self.label: str | None = None
244
288
 
245
289
  self.is_predefined = is_predefined
246
290
  self.name_case = NameCase.convert
@@ -250,6 +294,7 @@ class SpecTypeDefn(SpecType):
250
294
  self._is_value_converted = _is_value_converted
251
295
  self._is_value_to_string = False
252
296
  self._is_valid_parameter = True
297
+ self._is_dynamic_allowed = False
253
298
  self.ext_info: Any = None
254
299
 
255
300
  def is_value_converted(self) -> bool:
@@ -261,20 +306,38 @@ class SpecTypeDefn(SpecType):
261
306
  def is_valid_parameter(self) -> bool:
262
307
  return self._is_valid_parameter
263
308
 
309
+ def is_dynamic_allowed(self) -> bool:
310
+ return self._is_dynamic_allowed
311
+
264
312
  def is_base_type(self, type_: BaseTypeName) -> bool:
265
313
  return self.is_base and self.name == type_
266
314
 
315
+ def can_process(self, builder: SpecBuilder, data: RawDict) -> bool:
316
+ return True
317
+
267
318
  @abc.abstractmethod
268
319
  def process(self, builder: SpecBuilder, data: RawDict) -> None: ...
269
320
 
270
321
  def base_process(
271
322
  self, builder: SpecBuilder, data: RawDict, extra_names: list[str]
272
323
  ) -> None:
273
- util.check_fields(data, ["ext_info", "label"] + extra_names)
324
+ util.check_fields(
325
+ data,
326
+ [
327
+ "ext_info",
328
+ "label",
329
+ "is_dynamic_allowed",
330
+ ]
331
+ + extra_names,
332
+ )
274
333
 
275
334
  self.ext_info = data.get("ext_info")
276
335
  self.label = data.get("label")
277
336
 
337
+ is_dynamic_allowed = data.get("is_dynamic_allowed", False)
338
+ assert isinstance(is_dynamic_allowed, bool)
339
+ self._is_dynamic_allowed = is_dynamic_allowed
340
+
278
341
  def _process_property(
279
342
  self, builder: SpecBuilder, spec_name: str, data: RawDict
280
343
  ) -> SpecProperty:
@@ -313,9 +376,9 @@ class SpecTypeDefn(SpecType):
313
376
  property_name_case = NameCase(name_case_raw)
314
377
 
315
378
  if property_name_case != NameCase.preserve:
316
- assert util.is_valid_property_name(
317
- name
318
- ), f"{name} is not a valid property name"
379
+ assert util.is_valid_property_name(name), (
380
+ f"{name} is not a valid property name"
381
+ )
319
382
 
320
383
  data_type = data.get("type")
321
384
  builder.ensure(data_type is not None, "missing `type` entry")
@@ -393,7 +456,7 @@ class SpecTypeGenericParameter(SpecType):
393
456
 
394
457
 
395
458
  class SpecTypeDefnObject(SpecTypeDefn):
396
- base: Optional[SpecTypeDefnObject]
459
+ base: SpecTypeDefnObject | None
397
460
  parameters: list[str]
398
461
 
399
462
  def __init__(
@@ -401,7 +464,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
401
464
  namespace: SpecNamespace,
402
465
  name: str,
403
466
  *,
404
- parameters: Optional[list[str]] = None,
467
+ parameters: list[str] | None = None,
405
468
  is_base: bool = False,
406
469
  is_predefined: bool = False,
407
470
  is_hashable: bool = False,
@@ -418,7 +481,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
418
481
  self.parameters = parameters if parameters is not None else []
419
482
  self.is_hashable = is_hashable
420
483
  self.base = None
421
- self.properties: Optional[dict[str, SpecProperty]] = None
484
+ self.properties: dict[str, SpecProperty] | None = None
422
485
  self._kw_only: bool = True
423
486
  self.desc: str | None = None
424
487
 
@@ -489,13 +552,8 @@ class SpecTypeDefnObject(SpecTypeDefn):
489
552
  base_type: list[SpecType] = [self.base] if self.base is not None else []
490
553
  return base_type + prop_types
491
554
 
492
- def get_generic(self) -> Optional[str]:
493
- if len(self.parameters) > 0:
494
- assert (
495
- len(self.parameters) == 1
496
- ), "Only single generic parameters current supported"
497
- return self.parameters[0]
498
- return None
555
+ def get_generics(self) -> list[str]:
556
+ return self.parameters
499
557
 
500
558
 
501
559
  class SpecTypeDefnAlias(SpecTypeDefn):
@@ -517,13 +575,65 @@ class SpecTypeDefnAlias(SpecTypeDefn):
517
575
  super().base_process(builder, data, ["type", "desc", "alias", "discriminator"])
518
576
  self.alias = builder.parse_type(self.namespace, data["alias"])
519
577
  self.desc = data.get("desc", None)
520
- # Should be limited to Union type aliases
521
578
  self.discriminator = data.get("discriminator", None)
522
579
 
523
580
  def get_referenced_types(self) -> list[SpecType]:
524
581
  return [self.alias]
525
582
 
526
583
 
584
+ class SpecTypeDefnUnion(SpecTypeDefn):
585
+ def __init__(self, namespace: SpecNamespace, name: str) -> None:
586
+ super().__init__(namespace, name)
587
+ self.discriminator: str | None = None
588
+ self.types: list[SpecType] = []
589
+ self._alias_type: SpecType | None = None
590
+ self.discriminator_map: dict[str, SpecType] | None = None
591
+ self.desc: str | None = None
592
+
593
+ def process(self, builder: SpecBuilder, data: RawDict) -> None:
594
+ super().base_process(builder, data, ["type", "desc", "types", "discriminator"])
595
+
596
+ self.desc = data.get("desc", None)
597
+ self.discriminator = data.get("discriminator", None)
598
+
599
+ for sub_type_str in data["types"]:
600
+ sub_type = builder.parse_type(self.namespace, sub_type_str)
601
+ self.types.append(sub_type)
602
+
603
+ base_type = builder.namespaces[base_namespace_name].types[BaseTypeName.s_union]
604
+ self._backing_type = SpecTypeInstance(base_type, self.types)
605
+
606
+ if self.discriminator is not None:
607
+ self.discriminator_map = {}
608
+ for sub_type in self.types:
609
+ builder.push_where(sub_type.name)
610
+ assert isinstance(sub_type, SpecTypeDefnObject), (
611
+ "union-type-must-be-object"
612
+ )
613
+ assert sub_type.properties is not None
614
+ discriminator_type = sub_type.properties.get(self.discriminator)
615
+ assert discriminator_type is not None, (
616
+ f"missing-discriminator-field: {sub_type}"
617
+ )
618
+ prop_type = unwrap_literal_type(discriminator_type.spec_type)
619
+ assert prop_type is not None
620
+ assert prop_type.is_value_to_string()
621
+ discriminant = str(prop_type.value)
622
+ assert discriminant not in self.discriminator_map, (
623
+ f"duplicated-discriminant, {discriminant} in {sub_type}"
624
+ )
625
+ self.discriminator_map[discriminant] = sub_type
626
+
627
+ builder.pop_where()
628
+
629
+ def get_referenced_types(self) -> list[SpecType]:
630
+ return self.types
631
+
632
+ def get_backing_type(self) -> SpecType:
633
+ assert self._backing_type is not None
634
+ return self._backing_type
635
+
636
+
527
637
  class SpecTypeDefnExternal(SpecTypeDefn):
528
638
  external_map: dict[str, str]
529
639
 
@@ -553,7 +663,7 @@ class SpecTypeDefnExternal(SpecTypeDefn):
553
663
  class StringEnumEntry:
554
664
  name: str
555
665
  value: str
556
- label: Optional[str] = None
666
+ label: str | None = None
557
667
  deprecated: bool = False
558
668
 
559
669
 
@@ -569,17 +679,32 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
569
679
  )
570
680
  self.values: dict[str, StringEnumEntry] = {}
571
681
  self.desc: str | None = None
572
- self.sql_type_name: Optional[str] = None
682
+ self.sql_type_name: str | None = None
573
683
  self.emit_id_source = False
684
+ self.source_enums: list[SpecType] = []
685
+
686
+ def can_process(self, builder: SpecBuilder, data: dict[Any, Any]) -> bool:
687
+ source_enums = data.get("source_enums")
688
+ try:
689
+ for sub_type_str in source_enums or []:
690
+ sub_type = builder.parse_type(self.namespace, sub_type_str)
691
+ assert isinstance(sub_type, SpecTypeDefnStringEnum)
692
+ assert len(sub_type.values) > 0
693
+ except AssertionError:
694
+ return False
695
+ return super().can_process(builder, data)
574
696
 
575
697
  def process(self, builder: SpecBuilder, data: RawDict) -> None:
576
698
  super().base_process(
577
- builder, data, ["type", "desc", "values", "name_case", "sql", "emit"]
699
+ builder,
700
+ data,
701
+ ["type", "desc", "values", "name_case", "sql", "emit", "source_enums"],
578
702
  )
579
703
  self.name_case = NameCase(data.get("name_case", "convert"))
580
704
  self.values = {}
581
- data_values = data["values"]
705
+ data_values = data.get("values")
582
706
  self.desc = data.get("desc", None)
707
+ source_enums = data.get("source_enums", None)
583
708
  if isinstance(data_values, dict):
584
709
  for name, value in data_values.items():
585
710
  builder.push_where(name)
@@ -617,10 +742,13 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
617
742
  elif isinstance(data_values, list):
618
743
  for value in data_values:
619
744
  if value in self.values:
620
- raise Exception("duplicate value in typespec enum", self.name, value)
745
+ raise Exception(
746
+ "duplicate value in typespec enum", self.name, value
747
+ )
621
748
  self.values[value] = StringEnumEntry(name=value, value=value)
622
749
  else:
623
- raise Exception("unsupported values type")
750
+ if source_enums is None or data_values is not None:
751
+ raise Exception("unsupported values type")
624
752
 
625
753
  sql_data = data.get("sql")
626
754
  if sql_data is not None:
@@ -642,9 +770,18 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
642
770
  builder.ensure(
643
771
  entry.label is not None, f"need-label-for-id-source:{entry.name}"
644
772
  )
773
+ for sub_type_str in source_enums or []:
774
+ sub_type = builder.parse_type(self.namespace, sub_type_str)
775
+ self.source_enums.append(sub_type)
776
+
777
+ for sub_type in self.source_enums:
778
+ builder.push_where(sub_type.name)
779
+ if isinstance(sub_type, SpecTypeDefnStringEnum):
780
+ self.values.update(sub_type.values)
781
+ builder.pop_where()
645
782
 
646
783
  def get_referenced_types(self) -> list[SpecType]:
647
- return []
784
+ return self.source_enums
648
785
 
649
786
 
650
787
  TOKEN_ENDPOINT = "$endpoint"
@@ -656,7 +793,7 @@ TOKEN_EMIT_TYPE_INFO = "$emit_type_info"
656
793
  TOKEN_IMPORT = "$import"
657
794
 
658
795
 
659
- class RouteMethod(str, Enum):
796
+ class RouteMethod(StrEnum):
660
797
  post = "post"
661
798
  get = "get"
662
799
  delete = "delete"
@@ -664,7 +801,7 @@ class RouteMethod(str, Enum):
664
801
  put = "put"
665
802
 
666
803
 
667
- class ResultType(str, Enum):
804
+ class ResultType(StrEnum):
668
805
  json = "json"
669
806
  binary = "binary"
670
807
 
@@ -672,20 +809,108 @@ class ResultType(str, Enum):
672
809
  RE_ENDPOINT_ROOT = re.compile(r"\${([_a-z]+)}")
673
810
 
674
811
 
675
- class SpecEndpoint:
676
- method: RouteMethod
677
- root: str
812
+ @dataclass(kw_only=True, frozen=True)
813
+ class _EndpointPathDetails:
814
+ root: EndpointKey
815
+ root_path: str
816
+ resolved_path: str
817
+
818
+
819
+ def _resolve_endpoint_path(
820
+ path: str, api_endpoints: dict[EndpointKey, str]
821
+ ) -> _EndpointPathDetails:
822
+ root_path_source = path.split("/")[0]
823
+ root_match = RE_ENDPOINT_ROOT.fullmatch(root_path_source)
824
+ if root_match is None:
825
+ raise Exception(f"invalid-api-path-root:{root_path_source}")
826
+
827
+ root_var = root_match.group(1)
828
+ root_path = api_endpoints[root_var]
829
+
830
+ _, *rest_path = path.split("/", 1)
831
+ resolved_path = "/".join([root_path] + rest_path)
832
+
833
+ return _EndpointPathDetails(
834
+ root=root_var, root_path=root_path, resolved_path=resolved_path
835
+ )
836
+
837
+
838
+ class EndpointEmitType(StrEnum):
839
+ EMIT_ENDPOINT = "emit_endpoint"
840
+ EMIT_TYPES = "emit_types"
841
+ EMIT_NOTHING = "emit_nothing"
842
+
843
+
844
+ @dataclass(kw_only=True, frozen=True)
845
+ class EndpointSpecificPath:
846
+ root: EndpointKey
678
847
  path_root: str
679
848
  path_dirname: str
680
849
  path_basename: str
850
+ function: str | None
851
+
852
+
853
+ def parse_endpoint_specific_path(
854
+ builder: SpecBuilder,
855
+ data_per_endpoint: RawDict | None,
856
+ ) -> EndpointSpecificPath | None:
857
+ if data_per_endpoint is None:
858
+ return None
859
+ util.check_fields(
860
+ data_per_endpoint,
861
+ [
862
+ "path",
863
+ "function",
864
+ ],
865
+ )
866
+
867
+ if "path" not in data_per_endpoint or data_per_endpoint["path"] is None:
868
+ return None
869
+
870
+ path = data_per_endpoint["path"].split("/")
871
+
872
+ assert len(path) > 1, "invalid-endpoint-path"
873
+
874
+ path_details = _resolve_endpoint_path(
875
+ data_per_endpoint["path"], builder.api_endpoints
876
+ )
877
+
878
+ result = EndpointSpecificPath(
879
+ function=data_per_endpoint.get("function"),
880
+ path_dirname="/".join(path[1:-1]),
881
+ path_basename=path[-1],
882
+ root=path_details.root,
883
+ path_root=path_details.root_path,
884
+ )
885
+
886
+ return result
887
+
888
+
889
+ class SpecEndpoint:
890
+ method: RouteMethod
681
891
  data_loader: bool
682
- is_sdk: bool
892
+ is_sdk: EndpointEmitType
893
+ is_beta: bool
894
+ stability_level: StabilityLevel | None
683
895
  # Don't emit TypeScript endpoint code
684
896
  suppress_ts: bool
685
- function: Optional[str]
897
+ async_batch_path: str | None = None
686
898
  result_type: ResultType = ResultType.json
687
899
  has_attachment: bool = False
688
900
  desc: str | None = None
901
+ account_type: str | None
902
+ route_group: str | None
903
+
904
+ # to be deprecated in favor of path_per_api_endpoint:
905
+ # default function, path details
906
+ function: str | None
907
+ root: EndpointKey
908
+ path_root: str
909
+ path_dirname: str
910
+ path_basename: str
911
+
912
+ # function, path details per api endpoint
913
+ path_per_api_endpoint: dict[str, EndpointSpecificPath]
689
914
 
690
915
  is_external: bool = False
691
916
 
@@ -701,51 +926,122 @@ class SpecEndpoint:
701
926
  "path",
702
927
  "data_loader",
703
928
  "is_sdk",
929
+ "is_beta",
930
+ "stability_level",
931
+ "async_batch_path",
704
932
  "function",
705
933
  "suppress_ts",
706
934
  "desc",
707
935
  "deprecated",
708
936
  "result_type",
709
937
  "has_attachment",
710
- ],
938
+ "account_type",
939
+ "route_group",
940
+ ]
941
+ + list(builder.api_endpoints.keys()),
711
942
  )
712
943
  self.method = RouteMethod(data["method"])
713
944
 
714
- path = data["path"].split("/")
715
-
716
- assert len(path) > 1, "invalid-endpoint-path"
717
-
718
- # handle ${external} in the same way we handle ${materials} for now
719
- self.path_dirname = "/".join(path[1:-1])
720
- self.path_basename = path[-1]
721
-
722
945
  data_loader = data.get("data_loader", False)
723
946
  assert isinstance(data_loader, bool)
724
947
  self.data_loader = data_loader
725
948
 
726
- is_sdk = data.get("is_sdk", False)
727
- assert isinstance(is_sdk, bool)
949
+ is_sdk = data.get("is_sdk", EndpointEmitType.EMIT_NOTHING)
950
+
951
+ # backwards compatibility
952
+ if isinstance(is_sdk, bool):
953
+ if is_sdk is True:
954
+ is_sdk = EndpointEmitType.EMIT_ENDPOINT
955
+ else:
956
+ is_sdk = EndpointEmitType.EMIT_NOTHING
957
+ elif isinstance(is_sdk, str):
958
+ try:
959
+ is_sdk = EndpointEmitType(is_sdk)
960
+ except ValueError:
961
+ raise ValueError(f"Invalid value for is_sdk: {is_sdk}")
962
+
963
+ assert isinstance(is_sdk, EndpointEmitType)
964
+
728
965
  self.is_sdk = is_sdk
729
966
 
730
- self.function = data.get("function")
967
+ route_group = data.get("route_group")
968
+ assert route_group is None or isinstance(route_group, str)
969
+ self.route_group = route_group
970
+
971
+ account_type = data.get("account_type")
972
+ assert account_type is None or isinstance(account_type, str)
973
+ self.account_type = account_type
974
+
975
+ is_beta = data.get("is_beta", False)
976
+ assert isinstance(is_beta, bool)
977
+ self.is_beta = is_beta
978
+
979
+ stability_level_raw = data.get("stability_level")
980
+ assert stability_level_raw is None or isinstance(stability_level_raw, str)
981
+ self.stability_level = (
982
+ StabilityLevel(stability_level_raw)
983
+ if stability_level_raw is not None
984
+ else None
985
+ )
986
+
987
+ async_batch_path = data.get("async_batch_path")
988
+ if async_batch_path is not None:
989
+ assert isinstance(async_batch_path, str)
990
+ self.async_batch_path = async_batch_path
731
991
 
732
992
  suppress_ts = data.get("suppress_ts", False)
733
993
  assert isinstance(suppress_ts, bool)
734
994
  self.suppress_ts = suppress_ts
735
995
 
736
996
  self.result_type = ResultType(data.get("result_type", ResultType.json.value))
737
-
997
+ self.has_attachment = data.get("has_attachment", False)
738
998
  self.desc = data.get("desc")
739
999
 
740
- root_match = RE_ENDPOINT_ROOT.fullmatch(path[0])
741
- if root_match is None:
742
- raise Exception(f"invalid-api-path-root:{path[0]}")
1000
+ default_endpoint_path = parse_endpoint_specific_path(
1001
+ builder,
1002
+ {"path": data.get("path"), "function": data.get("function")},
1003
+ )
1004
+ if default_endpoint_path is not None:
1005
+ self.root = default_endpoint_path.root
1006
+ self.path_per_api_endpoint = {
1007
+ self.root: default_endpoint_path,
1008
+ }
1009
+ else:
1010
+ self.path_per_api_endpoint = {}
1011
+ shared_function_name = None
1012
+ for endpoint_key in builder.api_endpoints:
1013
+ endpoint_specific_path = parse_endpoint_specific_path(
1014
+ builder,
1015
+ data.get(endpoint_key),
1016
+ )
1017
+ if endpoint_specific_path is not None:
1018
+ self.path_per_api_endpoint[endpoint_key] = endpoint_specific_path
1019
+ if endpoint_specific_path.function is not None:
1020
+ fn_name = endpoint_specific_path.function.split(".")[-1]
1021
+ if shared_function_name is None:
1022
+ shared_function_name = fn_name
1023
+ assert shared_function_name == fn_name
1024
+ assert self.path_per_api_endpoint != {}
1025
+
1026
+ assert builder.top_namespace in self.path_per_api_endpoint
1027
+ self.root = builder.top_namespace
1028
+
1029
+ default_endpoint_path = self.path_per_api_endpoint[self.root]
1030
+ self.function = default_endpoint_path.function
1031
+ self.path_dirname = default_endpoint_path.path_dirname
1032
+ self.path_basename = default_endpoint_path.path_basename
1033
+ self.path_root = default_endpoint_path.path_root
743
1034
 
744
- self.root = root_match.group(1)
745
- self.path_root = builder.api_endpoints[self.root]
746
1035
  # IMPROVE: remove need for is_external flag
747
1036
  self.is_external = self.path_root == "api/external"
748
- self.has_attachment = data.get("has_attachment", False)
1037
+
1038
+ assert self.is_sdk != EndpointEmitType.EMIT_ENDPOINT or self.desc is not None, (
1039
+ f"Endpoint description required for SDK endpoints, missing: {self.path_dirname}/{self.path_basename}"
1040
+ )
1041
+
1042
+ @property
1043
+ def resolved_path(self: Self) -> str:
1044
+ return f"{self.path_root}/{self.path_dirname}/{self.path_basename}"
749
1045
 
750
1046
 
751
1047
  def _parse_const(
@@ -812,7 +1108,9 @@ def _parse_const(
812
1108
  return value
813
1109
 
814
1110
  if const_type.name == BaseTypeName.s_boolean:
815
- builder.ensure(isinstance(value, bool), "invalid value for boolean constant")
1111
+ builder.ensure(
1112
+ isinstance(value, bool), "invalid value for boolean constant"
1113
+ )
816
1114
  return value
817
1115
 
818
1116
  raise Exception("unsupported-const-scalar-type", const_type)
@@ -844,7 +1142,9 @@ class SpecConstant:
844
1142
  assert isinstance(self.value, dict)
845
1143
  # the parsing checks that the values are correct, so a simple length check
846
1144
  # should be enough to check completeness
847
- builder.ensure(len(key_type.values) == len(self.value), "incomplete-enum-map")
1145
+ builder.ensure(
1146
+ len(key_type.values) == len(self.value), "incomplete-enum-map"
1147
+ )
848
1148
 
849
1149
 
850
1150
  class SpecNamespace:
@@ -854,14 +1154,14 @@ class SpecNamespace:
854
1154
  ):
855
1155
  self.types: dict[str, SpecTypeDefn] = {}
856
1156
  self.constants: dict[str, SpecConstant] = {}
857
- self.endpoint: Optional[SpecEndpoint] = None
1157
+ self.endpoint: SpecEndpoint | None = None
858
1158
  self.emit_io_ts = False
859
1159
  self.emit_type_info = False
860
1160
  self.derive_types_from_io_ts = False
861
- self._imports: Optional[list[str]] = None
1161
+ self._imports: list[str] | None = None
862
1162
  self.path = name.split(".")
863
1163
  self.name = self.path[-1]
864
- self._order: Optional[int] = None
1164
+ self._order: int | None = None
865
1165
 
866
1166
  def _update_order(self, builder: SpecBuilder, recurse: int = 0) -> int:
867
1167
  if self._order is not None:
@@ -919,9 +1219,9 @@ class SpecNamespace:
919
1219
  continue
920
1220
 
921
1221
  if "value" in defn:
922
- assert util.is_valid_property_name(
923
- name
924
- ), f"{name} is not a valid constant name"
1222
+ assert util.is_valid_property_name(name), (
1223
+ f"{name} is not a valid constant name"
1224
+ )
925
1225
  spec_constant = SpecConstant(self, name)
926
1226
  self.constants[name] = spec_constant
927
1227
  continue
@@ -932,13 +1232,19 @@ class SpecNamespace:
932
1232
  spec_type: SpecTypeDefn
933
1233
  if defn_type == DefnTypeName.s_alias:
934
1234
  spec_type = SpecTypeDefnAlias(self, name)
1235
+ elif defn_type == DefnTypeName.s_union:
1236
+ spec_type = SpecTypeDefnUnion(self, name)
935
1237
  elif defn_type == DefnTypeName.s_external:
936
1238
  spec_type = SpecTypeDefnExternal(self, name)
937
1239
  elif defn_type == DefnTypeName.s_string_enum:
938
1240
  spec_type = SpecTypeDefnStringEnum(self, name)
939
1241
  else:
940
1242
  parameters = (
941
- [parameter.name for parameter in parsed_name.parameters[0]]
1243
+ [
1244
+ parameter.name
1245
+ for name_parameters in parsed_name.parameters
1246
+ for parameter in name_parameters
1247
+ ]
942
1248
  if parsed_name.parameters is not None
943
1249
  else None
944
1250
  )
@@ -954,28 +1260,41 @@ class SpecNamespace:
954
1260
  Complete the definition of each type.
955
1261
  """
956
1262
  builder.push_where(self.name)
957
- for full_name, defn in data.items():
958
- parsed_name = parse_type_str(full_name)[0]
959
- name = parsed_name.name
1263
+ items_to_process: list[NameDataPair] = [
1264
+ NameDataPair(full_name=full_name, data=defn)
1265
+ for full_name, defn in data.items()
1266
+ ]
1267
+ while len(items_to_process) > 0:
1268
+ deferred_items: list[NameDataPair] = []
1269
+ for item in items_to_process:
1270
+ full_name = item.full_name
1271
+ defn = item.data
1272
+ parsed_name = parse_type_str(full_name)[0]
1273
+ name = parsed_name.name
1274
+
1275
+ if name in [TOKEN_EMIT_IO_TS, TOKEN_EMIT_TYPE_INFO, TOKEN_IMPORT]:
1276
+ continue
960
1277
 
961
- if name in [TOKEN_EMIT_IO_TS, TOKEN_EMIT_TYPE_INFO, TOKEN_IMPORT]:
962
- continue
1278
+ builder.push_where(name)
963
1279
 
964
- builder.push_where(name)
1280
+ if "value" in defn:
1281
+ spec_constant = self.constants[name]
1282
+ spec_constant.process(builder, defn)
965
1283
 
966
- if "value" in defn:
967
- spec_constant = self.constants[name]
968
- spec_constant.process(builder, defn)
1284
+ elif name == TOKEN_ENDPOINT:
1285
+ assert self.endpoint
1286
+ self.endpoint.process(builder, defn)
969
1287
 
970
- elif name == TOKEN_ENDPOINT:
971
- assert self.endpoint
972
- self.endpoint.process(builder, defn)
973
-
974
- else:
975
- spec_type = self.types[name]
976
- spec_type.process(builder, defn)
1288
+ else:
1289
+ spec_type = self.types[name]
1290
+ if spec_type.can_process(builder, defn):
1291
+ spec_type.process(builder, defn)
1292
+ else:
1293
+ deferred_items.append(item)
977
1294
 
978
- builder.pop_where()
1295
+ builder.pop_where()
1296
+ assert len(deferred_items) < len(items_to_process)
1297
+ items_to_process = [deferred for deferred in deferred_items]
979
1298
 
980
1299
  builder.pop_where()
981
1300
 
@@ -990,13 +1309,24 @@ class NamespaceDataPair:
990
1309
  data: RawDict
991
1310
 
992
1311
 
1312
+ @dataclass(kw_only=True)
1313
+ class NameDataPair:
1314
+ full_name: str
1315
+ data: RawDict
1316
+
1317
+
993
1318
  class SpecBuilder:
994
- def __init__(self, *, api_endpoints: dict[str, str]) -> None:
1319
+ def __init__(
1320
+ self, *, api_endpoints: dict[EndpointKey, str], top_namespace: str
1321
+ ) -> None:
1322
+ self.top_namespace = top_namespace
995
1323
  self.where: list[str] = []
996
1324
  self.namespaces = {}
997
1325
  self.pending: list[NamespaceDataPair] = []
998
1326
  self.parts: dict[str, dict[str, str]] = defaultdict(dict)
999
1327
  self.preparts: dict[str, dict[str, str]] = defaultdict(dict)
1328
+ self.examples: dict[str, list[SpecEndpointExample]] = defaultdict(list)
1329
+ self.guides: dict[SpecGuideKey, list[SpecGuide]] = defaultdict(list)
1000
1330
  self.api_endpoints = api_endpoints
1001
1331
  base_namespace = SpecNamespace(name=base_namespace_name)
1002
1332
  for base_type in BaseTypeName:
@@ -1086,7 +1416,7 @@ class SpecBuilder:
1086
1416
  self,
1087
1417
  path: util.ParsedTypePath,
1088
1418
  namespace: SpecNamespace,
1089
- scope: Optional[SpecTypeDefn] = None,
1419
+ scope: SpecTypeDefn | None = None,
1090
1420
  top: bool = False,
1091
1421
  ) -> SpecType:
1092
1422
  """
@@ -1167,11 +1497,13 @@ class SpecBuilder:
1167
1497
  )
1168
1498
 
1169
1499
  def parse_type(
1170
- self, namespace: SpecNamespace, spec: str, scope: Optional[SpecTypeDefn] = None
1500
+ self, namespace: SpecNamespace, spec: str, scope: SpecTypeDefn | None = None
1171
1501
  ) -> SpecType:
1172
1502
  self.push_where(spec)
1173
1503
  parsed_type = util.parse_type_str(spec)
1174
- result = self._convert_parsed_type(parsed_type, namespace, top=True, scope=scope)
1504
+ result = self._convert_parsed_type(
1505
+ parsed_type, namespace, top=True, scope=scope
1506
+ )
1175
1507
  self.pop_where()
1176
1508
  return result
1177
1509
 
@@ -1181,5 +1513,59 @@ class SpecBuilder:
1181
1513
  def add_prepart_file(self, target: str, name: str, data: str) -> None:
1182
1514
  self.preparts[target][name] = data
1183
1515
 
1516
+ def add_example_file(self, data: dict[str, object]) -> None:
1517
+ path_details = _resolve_endpoint_path(str(data["path"]), self.api_endpoints)
1518
+
1519
+ examples_data = data["examples"]
1520
+ if not isinstance(examples_data, list):
1521
+ raise Exception(
1522
+ f"'examples' in example files are expected to be a list, endpoint_path={path_details.resolved_path}"
1523
+ )
1524
+ for example in examples_data:
1525
+ arguments = example["arguments"]
1526
+ data_example = example["data"]
1527
+ if not isinstance(arguments, dict) or not isinstance(data_example, dict):
1528
+ raise Exception(
1529
+ f"'arguments' and 'data' fields must be dictionaries for each endpoint example, endpoint={path_details.resolved_path}"
1530
+ )
1531
+ self.examples[path_details.resolved_path].append(
1532
+ SpecEndpointExample(
1533
+ summary=str(example["summary"]),
1534
+ description=str(example["description"]),
1535
+ arguments=arguments,
1536
+ data=data_example,
1537
+ )
1538
+ )
1539
+
1540
+ def add_guide_file(self, file_content: str) -> None:
1541
+ import markdown
1542
+
1543
+ md = markdown.Markdown(extensions=["meta"])
1544
+ html = md.convert(file_content)
1545
+ meta: dict[str, list[str]] = md.Meta # type: ignore[attr-defined]
1546
+ title_meta: list[str] | None = meta.get("title")
1547
+ if title_meta is None:
1548
+ raise Exception("guides require a title in the meta section")
1549
+ id_meta: list[str] | None = meta.get("id")
1550
+ if id_meta is None:
1551
+ raise Exception("guides require an id in the meta section")
1552
+
1553
+ path_meta: list[str] | None = meta.get("path")
1554
+ guide_key: SpecGuideKey = RootGuideKey()
1555
+ if path_meta is not None:
1556
+ path_details = _resolve_endpoint_path(
1557
+ "".join(path_meta), self.api_endpoints
1558
+ )
1559
+ guide_key = EndpointGuideKey(path=path_details.resolved_path)
1560
+
1561
+ self.guides[guide_key].append(
1562
+ SpecGuide(
1563
+ ref_name="".join(id_meta),
1564
+ title="".join(title_meta),
1565
+ html_content=html,
1566
+ markdown_content=file_content,
1567
+ )
1568
+ )
1569
+
1184
1570
  def resolve_proper_name(self, stype: SpecTypeDefn) -> str:
1185
1571
  return f"{'.'.join(stype.namespace.path)}.{stype.name}"