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
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(kw_only=True, frozen=True)
5
+ class CrossOutputPaths:
6
+ python_types_output: str
7
+ typescript_types_output: str
8
+ typescript_routes_output_by_endpoint: dict[str, str]
9
+ typespec_files_input: list[str]
pkgs/type_spec/config.py CHANGED
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import Self, TypeVar
5
5
 
6
6
  from pkgs.serialization import yaml
7
+ from pkgs.type_spec.builder import APIEndpointInfo, EndpointKey
7
8
 
8
9
  ConfigValueType = str | None | Mapping[str, str | None] | list[str]
9
10
 
@@ -19,6 +20,22 @@ def _parse_string_lookup(
19
20
  }
20
21
 
21
22
 
23
+ VT = TypeVar("VT")
24
+
25
+
26
+ def _parse_data_lookup(
27
+ key: str,
28
+ raw_value: ConfigValueType,
29
+ conv_func: type[VT],
30
+ ) -> dict[str, VT]:
31
+ assert isinstance(raw_value, dict), f"{key} must be key/values"
32
+ return {
33
+ k: conv_func(**v)
34
+ for k, v in raw_value.items()
35
+ if v is not None and isinstance(v, dict)
36
+ }
37
+
38
+
22
39
  @dataclass(kw_only=True)
23
40
  class BaseLanguageConfig:
24
41
  types_output: (
@@ -31,18 +48,28 @@ class BaseLanguageConfig:
31
48
 
32
49
  @dataclass(kw_only=True)
33
50
  class TypeScriptConfig(BaseLanguageConfig):
34
- routes_output: str # folder for generate route files will be located.
51
+ endpoint_to_routes_output: dict[
52
+ EndpointKey, str
53
+ ] # folder for generate route files will be located.
35
54
  type_info_output: str # folder for generated type info files
36
55
  id_source_output: str | None = None # folder for emitted id source maps.
56
+ endpoint_to_frontend_app_type: dict[
57
+ str, str
58
+ ] # map from api_endpoint to frontend app type
37
59
 
38
60
  def __post_init__(self: Self) -> None:
39
- self.routes_output = self.routes_output
61
+ self.endpoint_to_routes_output = self.endpoint_to_routes_output
40
62
  self.type_info_output = os.path.abspath(self.type_info_output)
41
63
  self.id_source_output = (
42
64
  os.path.abspath(self.id_source_output)
43
65
  if self.id_source_output is not None
44
66
  else None
45
67
  )
68
+ self.endpoint_to_frontend_app_type = _parse_string_lookup(
69
+ "typescript_endpoint_to_frontend_app_type",
70
+ self.endpoint_to_frontend_app_type,
71
+ lambda x: x,
72
+ )
46
73
 
47
74
 
48
75
  @dataclass(kw_only=True)
@@ -59,6 +86,7 @@ class PythonConfig(BaseLanguageConfig):
59
86
  emit_client_class: bool = False # emit the base class for the api client
60
87
  all_named_type_exports: bool = False # emit __all__ for all named type exports
61
88
  sdk_endpoints_only: bool = False # only emit is_sdk endpoints
89
+ type_info_output: str | None = None # folder for generated type info files
62
90
 
63
91
  def __post_init__(self: Self) -> None:
64
92
  self.routes_output = _parse_string_lookup(
@@ -70,6 +98,9 @@ class PythonConfig(BaseLanguageConfig):
70
98
  else None
71
99
  )
72
100
 
101
+ if self.type_info_output is not None:
102
+ self.type_info_output = os.path.abspath(self.type_info_output)
103
+
73
104
 
74
105
  @dataclass(kw_only=True)
75
106
  class OpenAPIConfig(BaseLanguageConfig):
@@ -86,18 +117,19 @@ class OpenAPIConfig(BaseLanguageConfig):
86
117
 
87
118
  @dataclass(kw_only=True)
88
119
  class Config:
120
+ top_namespace: str
89
121
  type_spec_types: list[str] # folders containing the yaml type spec definitions
90
- api_endpoint: dict[str, str]
122
+ api_endpoint: dict[str, APIEndpointInfo]
91
123
  # languages
92
124
  typescript: TypeScriptConfig | None
93
125
  python: PythonConfig
94
126
  open_api: OpenAPIConfig | None
95
127
 
96
128
 
97
- _T = TypeVar("_T")
129
+ T = TypeVar("T")
98
130
 
99
131
 
100
- def _parse_language(config_class: type[_T], raw_value: ConfigValueType) -> _T:
132
+ def _parse_language(config_class: type[T], raw_value: ConfigValueType) -> T:
101
133
  assert isinstance(raw_value, dict), "expecting language config to have key/values."
102
134
  return config_class(**raw_value)
103
135
 
@@ -107,13 +139,15 @@ def parse_yaml_config(config_file: str) -> Config:
107
139
  raw_config: dict[str, ConfigValueType] = yaml.safe_load(input)
108
140
 
109
141
  raw_type_spec_types = raw_config["type_spec_types"]
110
- assert isinstance(
111
- raw_type_spec_types, list
112
- ), "type_spec_types, must be a list of folders"
142
+ assert isinstance(raw_type_spec_types, list), (
143
+ "type_spec_types, must be a list of folders"
144
+ )
113
145
  type_spec_types = [os.path.abspath(folder) for folder in raw_type_spec_types]
114
146
 
115
- api_endpoint = _parse_string_lookup(
116
- "api_endpoint", raw_config.get("api_endpoint", {}), lambda x: x
147
+ api_endpoint = _parse_data_lookup(
148
+ "api_endpoint",
149
+ raw_config.get("api_endpoint", {}),
150
+ APIEndpointInfo,
117
151
  )
118
152
 
119
153
  raw_typescript = raw_config.get("typescript")
@@ -125,10 +159,16 @@ def parse_yaml_config(config_file: str) -> Config:
125
159
  python = _parse_language(PythonConfig, raw_config["python"])
126
160
  raw_open_api = raw_config.get("open_api")
127
161
  open_api = (
128
- _parse_language(OpenAPIConfig, raw_open_api) if raw_open_api is not None else None
162
+ _parse_language(OpenAPIConfig, raw_open_api)
163
+ if raw_open_api is not None
164
+ else None
129
165
  )
130
166
 
167
+ top_namespace = raw_config["top_namespace"]
168
+ assert isinstance(top_namespace, str)
169
+
131
170
  return Config(
171
+ top_namespace=top_namespace,
132
172
  type_spec_types=type_spec_types,
133
173
  api_endpoint=api_endpoint,
134
174
  typescript=typescript,
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from . import builder
6
+ from .builder_types import CrossOutputPaths
7
+
8
+
9
+ def get_python_stub_file_path(
10
+ function_name: str | None,
11
+ ) -> str | None:
12
+ if function_name is None:
13
+ return None
14
+ module_dir, file_name, _func_name = function_name.rsplit(".", 2)
15
+ module_path = os.path.relpath(module_dir.replace(".", "/"))
16
+ api_stub_file = f"{module_path}/{file_name}.py"
17
+ return api_stub_file
18
+
19
+
20
+ def get_python_api_file_path(
21
+ cross_output_paths: CrossOutputPaths,
22
+ namespace: builder.SpecNamespace,
23
+ ) -> str:
24
+ return f"{cross_output_paths.python_types_output}/{'/'.join(namespace.path)}{'' if len(namespace.path) > 1 else '_t'}.py"
25
+
26
+
27
+ def get_typescript_api_file_path(
28
+ cross_output_paths: CrossOutputPaths,
29
+ namespace: builder.SpecNamespace,
30
+ endpoint_key: builder.EndpointKey,
31
+ ) -> str:
32
+ return f"{cross_output_paths.typescript_routes_output_by_endpoint[endpoint_key]}/{'/'.join(namespace.path)}.tsx"
33
+
34
+
35
+ def get_yaml_api_file_path(
36
+ cross_output_paths: CrossOutputPaths,
37
+ namespace: builder.SpecNamespace,
38
+ ) -> str:
39
+ abs_path = next(
40
+ (
41
+ path
42
+ for path in cross_output_paths.typespec_files_input
43
+ if (
44
+ namespace.endpoint is None
45
+ or namespace.endpoint.default_endpoint_key in path
46
+ )
47
+ ),
48
+ cross_output_paths.typespec_files_input[0],
49
+ )
50
+ return f"{os.path.relpath(abs_path)}/{'/'.join(namespace.path)}.yaml"
51
+
52
+
53
+ def get_return_to_root_path(path: str) -> str:
54
+ return "../" * (path.count("/"))
55
+
56
+
57
+ def get_path_links(
58
+ cross_output_paths: CrossOutputPaths | None,
59
+ namespace: builder.SpecNamespace,
60
+ *,
61
+ current_path_type: str,
62
+ endpoint: builder.SpecEndpoint,
63
+ ) -> str:
64
+ if cross_output_paths is None:
65
+ return ""
66
+
67
+ api_paths = {
68
+ "Python": get_python_api_file_path(cross_output_paths, namespace),
69
+ "TypeScript": get_typescript_api_file_path(
70
+ cross_output_paths, namespace, endpoint.default_endpoint_key
71
+ ),
72
+ "YAML": get_yaml_api_file_path(cross_output_paths, namespace),
73
+ }
74
+
75
+ assert current_path_type in api_paths
76
+
77
+ comment_prefix = "#"
78
+ if current_path_type == "TypeScript":
79
+ comment_prefix = "//"
80
+
81
+ return_to_root_path = get_return_to_root_path(api_paths[current_path_type])
82
+ del api_paths[current_path_type]
83
+
84
+ paths_string = ""
85
+ for path_name, path in api_paths.items():
86
+ paths_string += (
87
+ f"{comment_prefix} {path_name}: file://./{return_to_root_path}{path}\n"
88
+ )
89
+
90
+ if namespace.endpoint is not None:
91
+ for (
92
+ endpoint_key,
93
+ path_specific_endpoint,
94
+ ) in namespace.endpoint.path_per_api_endpoint.items():
95
+ path_from_root = get_python_stub_file_path(path_specific_endpoint.function)
96
+ if path_from_root is None:
97
+ continue
98
+ paths_string += f"{comment_prefix} Implementation for {endpoint_key}: file://./{return_to_root_path}{path_from_root}\n"
99
+ return paths_string
@@ -125,7 +125,7 @@ def refer_to_io_ts(
125
125
  spec = refer_to_io_ts(ctx, stype.parameters[0])
126
126
  return f"IO.array({spec})"
127
127
  if stype.defn_type.name == builder.BaseTypeName.s_union:
128
- return f'IO.union([{", ".join([refer_to_io_ts(ctx, p) for p in stype.parameters])}])'
128
+ return f"IO.union([{', '.join([refer_to_io_ts(ctx, p) for p in stype.parameters])}])"
129
129
  if stype.defn_type.name == builder.BaseTypeName.s_optional:
130
130
  return f"IO.optional({refer_to_io_ts(ctx, stype.parameters[0])})"
131
131
  if stype.defn_type.name == builder.BaseTypeName.s_tuple:
@@ -7,10 +7,11 @@ WORK-IN-PROGRESS, DON'T USE!
7
7
  import dataclasses
8
8
  import json
9
9
  import re
10
- from typing import Collection, cast
10
+ from enum import StrEnum
11
+ from typing import Collection, assert_never, cast
11
12
 
12
13
  from pkgs.serialization import yaml
13
- from pkgs.serialization_util.serialization_helpers import serialize_for_api
14
+ from pkgs.serialization_util import serialize_for_api
14
15
 
15
16
  from . import builder, util
16
17
  from .builder import EndpointGuideKey, RootGuideKey
@@ -62,6 +63,10 @@ base_name_map = {
62
63
  }
63
64
 
64
65
 
66
+ class OpenAPIDefaultBehavior(StrEnum):
67
+ OPTIONAL_WITH_DEFAULT = "optional_with_default"
68
+
69
+
65
70
  def _rewrite_with_notice(
66
71
  file_path: str, file_content: str, *, notice: str = MODIFY_NOTICE
67
72
  ) -> bool:
@@ -126,7 +131,11 @@ def emit_open_api(builder: builder.SpecBuilder, *, config: OpenAPIConfig) -> Non
126
131
  for namespace in sorted(builder.namespaces.values(), key=lambda ns: ns.name):
127
132
  ctx = EmitOpenAPIContext(namespace=namespace)
128
133
 
129
- if ctx.namespace.endpoint is not None and ctx.namespace.endpoint.is_beta:
134
+ if (
135
+ ctx.namespace.endpoint is not None
136
+ and ctx.namespace.endpoint.stability_level
137
+ == EmitOpenAPIStabilityLevel.draft
138
+ ):
130
139
  continue
131
140
 
132
141
  if ctx.namespace.name == "base":
@@ -252,18 +261,35 @@ def _emit_endpoint_parameters(
252
261
  } | _emit_endpoint_parameter_examples(examples)
253
262
 
254
263
 
255
- def _emit_is_beta(is_beta: bool) -> DictApiSchema:
256
- if is_beta:
257
- return {"x-beta": True}
258
- return {}
264
+ def _emit_endpoint_deprecated(deprecated: bool) -> DictApiSchema:
265
+ return {"deprecated": True} if deprecated else {}
259
266
 
260
267
 
261
268
  def _emit_stability_level(
262
269
  stability_level: EmitOpenAPIStabilityLevel | None,
263
270
  ) -> DictApiSchema:
264
- if stability_level is not None:
265
- return {"x-stability-level": str(stability_level)}
266
- return {}
271
+ stability_info: dict[str, ApiSchema] = {}
272
+ resolved_stability_level = (
273
+ stability_level
274
+ if stability_level is not None
275
+ else EmitOpenAPIStabilityLevel.stable
276
+ )
277
+ stability_info["x-stability-level"] = str(resolved_stability_level)
278
+ match resolved_stability_level:
279
+ case EmitOpenAPIStabilityLevel.draft:
280
+ stability_info["x-beta"] = True
281
+ case EmitOpenAPIStabilityLevel.beta:
282
+ stability_info["x-badges"] = [
283
+ {
284
+ "name": "Beta",
285
+ "color": "DarkOrange",
286
+ }
287
+ ]
288
+ case EmitOpenAPIStabilityLevel.stable:
289
+ pass
290
+ case _:
291
+ assert_never(stability_level)
292
+ return stability_info
267
293
 
268
294
 
269
295
  def _emit_endpoint_request_body(
@@ -286,7 +312,9 @@ def _emit_endpoint_request_body(
286
312
  "type": "object",
287
313
  "title": "Body",
288
314
  "required": ["data"],
289
- "properties": {"data": {"$ref": "#/components/schema/Arguments"}},
315
+ "properties": {
316
+ "data": {"$ref": "#/components/schema/Arguments"}
317
+ },
290
318
  }
291
319
  }
292
320
  | _emit_endpoint_argument_examples(examples)
@@ -311,18 +339,57 @@ def _emit_endpoint_response_examples(
311
339
  return {"examples": response_examples}
312
340
 
313
341
 
342
+ def _create_warning_banner(api_type: str, message: str) -> str:
343
+ return (
344
+ f'<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; '
345
+ f'border-radius: 4px; padding: 12px; margin-bottom: 16px;">'
346
+ f"<strong>⚠️ {api_type} API:</strong> {message}"
347
+ f"</div>"
348
+ )
349
+
350
+
351
+ def _get_stability_warning(
352
+ stability_level: EmitOpenAPIStabilityLevel | None,
353
+ ) -> str:
354
+ resolved_stability_level = (
355
+ stability_level
356
+ if stability_level is not None
357
+ else EmitOpenAPIStabilityLevel.stable
358
+ )
359
+
360
+ match resolved_stability_level:
361
+ case EmitOpenAPIStabilityLevel.draft:
362
+ return _create_warning_banner(
363
+ "Draft",
364
+ "This endpoint is in draft status and may change significantly. Not recommended for production use.",
365
+ )
366
+ case EmitOpenAPIStabilityLevel.beta:
367
+ return _create_warning_banner(
368
+ "Beta",
369
+ "This endpoint is in beta and its required parameters may change. Use with caution in production environments.",
370
+ )
371
+ case EmitOpenAPIStabilityLevel.stable:
372
+ return ""
373
+
374
+
314
375
  def _emit_endpoint_description(
315
- description: str, guides: list[EmitOpenAPIGuide]
376
+ description: str,
377
+ guides: list[EmitOpenAPIGuide],
378
+ stability_level: EmitOpenAPIStabilityLevel | None = None,
316
379
  ) -> dict[str, str]:
380
+ stability_warning = _get_stability_warning(stability_level)
381
+
317
382
  full_guides = "<br/>".join([
318
383
  _write_guide_as_html(guide, is_open=False)
319
384
  for guide in sorted(guides, key=lambda g: g.ref_name)
320
385
  ])
321
- return {
322
- "description": description
323
- if len(guides) == 0
324
- else f"{description}<br/>{full_guides}"
325
- }
386
+
387
+ full_description_parts = [
388
+ part for part in [stability_warning, description, full_guides] if part
389
+ ]
390
+ full_description = "<br/>".join(full_description_parts)
391
+
392
+ return {"description": full_description}
326
393
 
327
394
 
328
395
  def _emit_namespace(
@@ -357,11 +424,15 @@ def _emit_namespace(
357
424
  "tags": endpoint.tags,
358
425
  "summary": endpoint.summary,
359
426
  }
360
- | _emit_endpoint_description(endpoint.description, ctx.endpoint.guides)
361
- | _emit_is_beta(endpoint.is_beta)
427
+ | _emit_endpoint_deprecated(endpoint.deprecated)
428
+ | _emit_endpoint_description(
429
+ endpoint.description, ctx.endpoint.guides, endpoint.stability_level
430
+ )
362
431
  | _emit_stability_level(endpoint.stability_level)
363
432
  | _emit_endpoint_parameters(endpoint, argument_type, ctx.endpoint.examples)
364
- | _emit_endpoint_request_body(endpoint, argument_type, ctx.endpoint.examples)
433
+ | _emit_endpoint_request_body(
434
+ endpoint, argument_type, ctx.endpoint.examples
435
+ )
365
436
  | {
366
437
  "responses": {
367
438
  "200": {
@@ -417,6 +488,18 @@ def _emit_namespace(
417
488
  _rewrite_with_notice(path, yaml.dumps(oa_namespace, sort_keys=False))
418
489
 
419
490
 
491
+ def _get_openapi_default_behavior(
492
+ prop: builder.SpecProperty,
493
+ ) -> OpenAPIDefaultBehavior | None:
494
+ if prop.ext_info is None or prop.ext_info.get("open_api") is None:
495
+ return None
496
+ value_passed = prop.ext_info["open_api"].get("default_required_behavior")
497
+ if value_passed is None:
498
+ return None
499
+ assert isinstance(value_passed, str)
500
+ return OpenAPIDefaultBehavior(value_passed)
501
+
502
+
420
503
  def _emit_type(
421
504
  ctx: EmitOpenAPIContext,
422
505
  stype: builder.SpecType,
@@ -442,8 +525,18 @@ def _emit_type(
442
525
  return
443
526
 
444
527
  if isinstance(stype, builder.SpecTypeDefnUnion):
445
- ctx.types[stype.name] = open_api_type(
446
- ctx, stype.get_backing_type(), config=config
528
+ converted_discriminator_map: dict[str, OpenAPIRefType] = dict()
529
+ if stype.discriminator_map is not None:
530
+ for discriminator_value, base_type in stype.discriminator_map.items():
531
+ converted_base_type = open_api_type(ctx, base_type, config=config)
532
+ assert isinstance(converted_base_type, OpenAPIRefType)
533
+ converted_discriminator_map[discriminator_value] = converted_base_type
534
+ ctx.types[stype.name] = OpenAPIUnionType(
535
+ [open_api_type(ctx, p, config=config) for p in stype.types],
536
+ discriminator=stype.discriminator,
537
+ discriminator_map=converted_discriminator_map
538
+ if stype.discriminator_map is not None
539
+ else None,
447
540
  )
448
541
  return
449
542
 
@@ -480,6 +573,16 @@ def _emit_type(
480
573
  # arguments, thus treat like extant==missing
481
574
  # IMPROVE: if we can decide they are meant as output instead, then
482
575
  # they should be marked as required
576
+ openapi_default_beahvior = _get_openapi_default_behavior(prop)
577
+ match openapi_default_beahvior:
578
+ case None:
579
+ pass
580
+ case OpenAPIDefaultBehavior.OPTIONAL_WITH_DEFAULT:
581
+ ref_type.nullable = True
582
+ assert prop.default is not None, (
583
+ "optional_with_default requires default"
584
+ )
585
+ ref_type.default = prop.default
483
586
  properties[prop_name] = ref_type
484
587
  elif prop.extant == builder.PropertyExtant.missing:
485
588
  # Unlike optional below, missing does not imply null is possible. They
@@ -507,18 +610,6 @@ def _emit_type(
507
610
  ctx.types[stype.name] = final_type
508
611
 
509
612
 
510
- def _emit_constant(ctx: EmitOpenAPIContext, sconst: builder.SpecConstant) -> None:
511
- if sconst.value_type.is_base_type(builder.BaseTypeName.s_string):
512
- value = util.encode_common_string(cast(str, sconst.value))
513
- elif sconst.value_type.is_base_type(builder.BaseTypeName.s_integer):
514
- value = str(sconst.value)
515
- else:
516
- raise Exception("invalid constant type", sconst.name)
517
-
518
- const_name = sconst.name.upper()
519
- print("_emit_constant", value, const_name)
520
-
521
-
522
613
  def _emit_endpoint(
523
614
  gctx: EmitOpenAPIGlobalContext,
524
615
  ctx: EmitOpenAPIContext,
@@ -559,7 +650,7 @@ def _emit_endpoint(
559
650
  ep = namespace.endpoint
560
651
  gctx.paths.append(
561
652
  EmitOpenAPIPath(
562
- path=f"/{ep.path_root}/{ep.path_dirname}/{ep.path_basename}",
653
+ path=f"/{ep.resolved_path}",
563
654
  ref=ref_path,
564
655
  )
565
656
  )
@@ -575,7 +666,7 @@ def _emit_endpoint(
575
666
  tags=[tag_name],
576
667
  summary=f"{'/'.join(namespace.path[path_cutoff:])}",
577
668
  description=description,
578
- is_beta=namespace.endpoint.is_beta,
669
+ deprecated=namespace.endpoint.deprecated,
579
670
  stability_level=namespace.endpoint.stability_level,
580
671
  examples=[
581
672
  EmitOpenAPIEndpointExample(
@@ -6,18 +6,17 @@ WORK-IN-PROGRESS, DON'T USE!
6
6
 
7
7
  from collections import defaultdict
8
8
  from dataclasses import dataclass, field
9
- from typing import TypeAlias
10
9
 
11
- from pkgs.serialization_util.serialization_helpers import JsonValue
10
+ from pkgs.serialization_util import JsonValue
12
11
 
13
12
  from . import builder
14
13
  from .open_api_util import OpenAPIType
15
14
 
16
15
  MODIFY_NOTICE = "# DO NOT MODIFY -- This file is generated by type_spec"
17
16
 
18
- GlobalContextInfo: TypeAlias = dict[str, str | dict[str, str]]
19
- TagGroupToNamedTags: TypeAlias = dict[str, str | list[str]]
20
- TagPathsToRef: TypeAlias = dict[str, dict[str, str]]
17
+ GlobalContextInfo = dict[str, str | dict[str, str]]
18
+ TagGroupToNamedTags = dict[str, str | list[str]]
19
+ TagPathsToRef = dict[str, dict[str, str]]
21
20
 
22
21
 
23
22
  @dataclass
@@ -83,7 +82,7 @@ class EmitOpenAPIEndpoint:
83
82
  tags: list[str]
84
83
  summary: str
85
84
  description: str
86
- is_beta: bool
85
+ deprecated: bool
87
86
  stability_level: EmitOpenAPIStabilityLevel | None
88
87
  examples: list[EmitOpenAPIEndpointExample]
89
88
  guides: list[EmitOpenAPIGuide]