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
@@ -2,18 +2,19 @@ import dataclasses
2
2
  import io
3
3
  import os
4
4
  from decimal import Decimal
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  from . import builder, util
8
+ from .builder import EndpointEmitType, EndpointSpecificPath, base_namespace_name
8
9
  from .config import PythonConfig
10
+ from .cross_output_links import get_path_links
11
+ from .emit_open_api_util import EmitOpenAPIStabilityLevel
9
12
 
10
13
  INDENT = " "
11
14
  LINE_BREAK = "\n"
12
15
  MODIFY_NOTICE = "# DO NOT MODIFY -- This file is generated by type_spec\n"
13
16
  # Turn excess line length warning and turn off ruff formatting
14
- LINT_HEADER = (
15
- "# flake8: noqa: F821\n# ruff: noqa: E402 Q003\n# fmt: off\n# isort: skip_file\n"
16
- )
17
+ LINT_HEADER = "# ruff: noqa: E402 Q003\n# fmt: off\n# isort: skip_file\n"
17
18
  LINT_FOOTER = "# fmt: on\n"
18
19
  ROUTE_NOTICE = """# Routes are generated from $endpoint specifications in the
19
20
  # type_spec API YAML files. Refer to the section on endpoints in the type_spec/README"""
@@ -34,18 +35,23 @@ QUEUED_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
34
35
  namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="QueuedAsyncBatchRequest"
35
36
  )
36
37
 
38
+ CLIENT_CONFIG_TYPE_NAMESPACE = builder.SpecNamespace(name="client_config")
39
+ REQUEST_OPTIONS_STYPE = builder.SpecTypeDefnObject(
40
+ namespace=CLIENT_CONFIG_TYPE_NAMESPACE, name="RequestOptions"
41
+ )
42
+
37
43
 
38
44
  @dataclasses.dataclass(kw_only=True)
39
45
  class TrackingContext:
40
- namespace: Optional[builder.SpecNamespace] = None
46
+ namespace: builder.SpecNamespace | None = None
41
47
  namespaces: set[builder.SpecNamespace] = dataclasses.field(default_factory=set)
42
48
  names: set[str] = dataclasses.field(default_factory=set)
43
49
 
44
50
  use_enum: bool = False
45
51
  use_serial_string_enum: bool = False
46
52
  use_dataclass: bool = False
47
- use_serial_class: bool = False
48
53
  use_serial_union: bool = False
54
+ use_serial_alias: bool = False
49
55
  use_missing: bool = False
50
56
  use_opaque_key: bool = False
51
57
 
@@ -54,6 +60,7 @@ class TrackingContext:
54
60
  class Context(TrackingContext):
55
61
  out: io.StringIO
56
62
  namespace: builder.SpecNamespace
63
+ builder: builder.SpecBuilder
57
64
 
58
65
 
59
66
  def _resolve_namespace_name(namespace: builder.SpecNamespace) -> str:
@@ -115,26 +122,36 @@ def _check_type_match(stype: builder.SpecType, value: Any) -> bool:
115
122
  raise Exception("invalid type", stype, value)
116
123
 
117
124
 
118
- def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> str:
125
+ def _emit_value(
126
+ ctx: TrackingContext, stype: builder.SpecType, value: Any, indent: int = 0
127
+ ) -> str:
119
128
  literal = builder.unwrap_literal_type(stype)
120
129
  if literal is not None:
121
130
  return _emit_value(ctx, literal.value_type, literal.value)
122
131
 
123
132
  if stype.is_base_type(builder.BaseTypeName.s_string):
124
- assert isinstance(value, str)
133
+ assert isinstance(value, str), (
134
+ f"Expected str value for {stype.name} but got {value}"
135
+ )
125
136
  return util.encode_common_string(value)
126
137
  elif stype.is_base_type(builder.BaseTypeName.s_integer):
127
- assert isinstance(value, int)
138
+ assert isinstance(value, int), (
139
+ f"Expected int value for {stype.name} but got {value}"
140
+ )
128
141
  return str(value)
129
142
  elif stype.is_base_type(builder.BaseTypeName.s_boolean):
130
- assert isinstance(value, bool)
143
+ assert isinstance(value, bool), (
144
+ f"Expected bool value for {stype.name} but got {value}"
145
+ )
131
146
  return "True" if value else "False"
132
147
  elif stype.is_base_type(builder.BaseTypeName.s_decimal) or stype.is_base_type(
133
148
  builder.BaseTypeName.s_lossy_decimal
134
149
  ):
135
150
  # Note that decimal requires the `!decimal 123.12` style notation in the YAML
136
151
  # file since PyYaml parses numbers as float, unfortuantely
137
- assert isinstance(value, (Decimal, int))
152
+ assert isinstance(value, (Decimal, int)), (
153
+ f"Expected decimal value for {stype.name} but got {value} (type: {type(value)})"
154
+ )
138
155
  if isinstance(value, int):
139
156
  # skip quotes for integers
140
157
  return f"Decimal({value})"
@@ -149,14 +166,14 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
149
166
  key_type = stype.parameters[0]
150
167
  value_type = stype.parameters[1]
151
168
  return (
152
- "{\n "
153
- + ",\n ".join(
169
+ f"{{\n{INDENT * (indent + 1)}"
170
+ + f",\n{INDENT * (indent + 1)}".join(
154
171
  _emit_value(ctx, key_type, dkey)
155
172
  + ": "
156
- + _emit_value(ctx, value_type, dvalue)
173
+ + _emit_value(ctx, value_type, dvalue, indent=indent + 1)
157
174
  for dkey, dvalue in value.items()
158
175
  )
159
- + "\n}"
176
+ + f"\n{INDENT * indent}}}"
160
177
  )
161
178
 
162
179
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
@@ -182,6 +199,34 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
182
199
  return f"{refer_to(ctx, stype)}.{_resolve_enum_name(value, stype.name_case)}"
183
200
  elif isinstance(stype, builder.SpecTypeDefnAlias):
184
201
  return _emit_value(ctx, stype.alias, value)
202
+ elif isinstance(stype, builder.SpecTypeDefnObject):
203
+ assert isinstance(value, dict), (
204
+ f"Expected dict value for {stype.name} but got {value}"
205
+ )
206
+ if not stype.is_hashable:
207
+ raise Exception("invalid constant object type, non-hashable", value, stype)
208
+ obj_out = f"{refer_to(ctx, stype)}("
209
+ emitted_fields: set[str] = set()
210
+ for prop_name, prop in (stype.properties or {}).items():
211
+ if prop_name not in value:
212
+ continue
213
+ else:
214
+ value_to_emit = value[prop_name]
215
+ emitted_fields.add(prop_name)
216
+ py_name = python_field_name(prop.name, prop.name_case)
217
+ obj_out += f"\n{INDENT * (indent + 1)}{py_name}={_emit_value(ctx, prop.spec_type, value_to_emit, indent=indent + 1)},"
218
+ whitespace = f"\n{INDENT * indent}" if len(emitted_fields) > 0 else ""
219
+ obj_out += f"{whitespace})"
220
+
221
+ if emitted_fields != set(value.keys()):
222
+ raise Exception(
223
+ "invalid object type, extra fields found:",
224
+ value,
225
+ stype,
226
+ set(value.keys()) - emitted_fields,
227
+ )
228
+
229
+ return obj_out
185
230
 
186
231
  raise Exception("invalid constant type", value, stype)
187
232
 
@@ -219,13 +264,14 @@ def _emit_types_imports(*, out: io.StringIO, ctx: Context) -> None:
219
264
  out.write("import datetime # noqa: F401\n")
220
265
  out.write("from decimal import Decimal # noqa: F401\n")
221
266
  if ctx.use_enum:
222
- out.write("from pkgs.strenum_compat import StrEnum\n")
267
+ out.write("from enum import StrEnum\n")
223
268
  if ctx.use_dataclass:
224
269
  out.write("import dataclasses\n")
225
- if ctx.use_serial_class:
226
270
  out.write("from pkgs.serialization import serial_class\n")
227
271
  if ctx.use_serial_union:
228
272
  out.write("from pkgs.serialization import serial_union_annotation\n")
273
+ if ctx.use_serial_alias:
274
+ out.write("from pkgs.serialization import serial_alias_annotation\n")
229
275
  if ctx.use_serial_string_enum:
230
276
  out.write("from pkgs.serialization import serial_string_enum\n")
231
277
  if ctx.use_missing:
@@ -250,7 +296,7 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
250
296
  ):
251
297
  if (
252
298
  namespace.endpoint is not None
253
- and not namespace.endpoint.is_sdk
299
+ and namespace.endpoint.is_sdk == EndpointEmitType.EMIT_NOTHING
254
300
  and config.sdk_endpoints_only is True
255
301
  ):
256
302
  continue
@@ -258,6 +304,7 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
258
304
  ctx = Context(
259
305
  out=io.StringIO(),
260
306
  namespace=namespace,
307
+ builder=builder,
261
308
  )
262
309
 
263
310
  _emit_namespace(ctx, namespace)
@@ -340,16 +387,44 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
340
387
 
341
388
  ENDPOINT_METHOD = "ENDPOINT_METHOD"
342
389
  ENDPOINT_PATH = "ENDPOINT_PATH"
390
+ # will be removed in Q1 2025 when ENDPOINT_PATH is made api_endpoint-agnostic
391
+ # is used when the API call has multiple endpoints for the one endpoint that isn't equal to the top_namespace
392
+ ENDPOINT_PATH_ALTERNATE = "ENDPOINT_PATH_ALTERNATE"
393
+
394
+
395
+ def _get_epf_root(endpoint_specific_path: EndpointSpecificPath) -> str:
396
+ return endpoint_specific_path.root
343
397
 
344
398
 
345
399
  def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
346
400
  endpoint = namespace.endpoint
347
401
  if endpoint is not None:
402
+ path_links = get_path_links(
403
+ ctx.builder.cross_output_paths,
404
+ namespace,
405
+ current_path_type="Python",
406
+ endpoint=endpoint,
407
+ )
408
+ if path_links != "":
409
+ ctx.out.write("\n")
410
+ ctx.out.write(path_links)
411
+
348
412
  ctx.out.write("\n")
349
413
  ctx.out.write(f'{ENDPOINT_METHOD} = "{endpoint.method.upper()}"\n')
350
- ctx.out.write(
351
- f'{ENDPOINT_PATH} = "{endpoint.path_root}/{endpoint.path_dirname}/{endpoint.path_basename}"\n'
352
- )
414
+ for endpoint_specific_path in sorted(
415
+ endpoint.path_per_api_endpoint.values(), key=_get_epf_root
416
+ ):
417
+ endpoint_path_name = ENDPOINT_PATH
418
+
419
+ if (
420
+ len(endpoint.path_per_api_endpoint.keys()) > 1
421
+ and endpoint_specific_path.root != ctx.builder.top_namespace
422
+ ):
423
+ endpoint_path_name = ENDPOINT_PATH_ALTERNATE
424
+ ctx.names.add(ENDPOINT_PATH_ALTERNATE)
425
+ ctx.out.write(
426
+ f'{endpoint_path_name} = "{endpoint_specific_path.path_root}/{endpoint_specific_path.path_dirname}/{endpoint_specific_path.path_basename}"\n'
427
+ )
353
428
 
354
429
  ctx.names.add(ENDPOINT_METHOD)
355
430
  ctx.names.add(ENDPOINT_PATH)
@@ -390,13 +465,13 @@ from .base_t import ObjectId as ObjectId
390
465
  def _validate_supports_handler_generation(
391
466
  stype: builder.SpecTypeDefn, name: str, supports_inheritance: bool = False
392
467
  ) -> builder.SpecTypeDefnObject:
393
- assert isinstance(
394
- stype, builder.SpecTypeDefnObject
395
- ), f"External api {name} must be an object"
468
+ assert isinstance(stype, builder.SpecTypeDefnObject), (
469
+ f"External api {name} must be an object"
470
+ )
396
471
  if not supports_inheritance:
397
- assert (
398
- stype.base is None or stype.base.is_base
399
- ), f"Inheritance not supported in external api {name}"
472
+ assert stype.base is None or stype.base.is_base, (
473
+ f"Inheritance not supported in external api {name}"
474
+ )
400
475
  return stype
401
476
 
402
477
 
@@ -439,8 +514,23 @@ def _emit_endpoint_invocation_function_signature(
439
514
  else []
440
515
  ) + (extra_params if extra_params is not None else [])
441
516
 
442
- assert endpoint.function is not None
443
- function_name = endpoint.function.split(".")[-1]
517
+ request_options_property = builder.SpecProperty(
518
+ name="_request_options",
519
+ label="_request_options",
520
+ spec_type=REQUEST_OPTIONS_STYPE,
521
+ extant=builder.PropertyExtant.optional,
522
+ convert_value=builder.PropertyConvertValue.auto,
523
+ name_case=builder.NameCase.convert,
524
+ default=None,
525
+ has_default=True,
526
+ desc=None,
527
+ )
528
+ all_arguments.append(request_options_property)
529
+
530
+ # All endpoints share a function name
531
+ function = endpoint.path_per_api_endpoint[endpoint.default_endpoint_key].function
532
+ assert function is not None
533
+ function_name = function.split(".")[-1]
444
534
  ctx.out.write(
445
535
  f"""
446
536
  def {function_name}(
@@ -486,7 +576,10 @@ def _emit_async_batch_invocation_function(
486
576
  endpoint = namespace.endpoint
487
577
  if endpoint is None:
488
578
  return
489
- if endpoint.async_batch_path is None or not endpoint.is_sdk:
579
+ if (
580
+ endpoint.async_batch_path is None
581
+ or endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
582
+ ):
490
583
  return
491
584
 
492
585
  ctx.out.write("\n")
@@ -568,7 +661,10 @@ def _emit_endpoint_invocation_function(
568
661
  endpoint = namespace.endpoint
569
662
  if endpoint is None:
570
663
  return
571
- if not endpoint.is_sdk or endpoint.is_beta:
664
+ if (
665
+ endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
666
+ or endpoint.stability_level == EmitOpenAPIStabilityLevel.draft
667
+ ):
572
668
  return
573
669
 
574
670
  ctx.out.write("\n")
@@ -601,6 +697,7 @@ def _emit_endpoint_invocation_function(
601
697
  method={refer_to(ctx=ctx, stype=endpoint_method_stype)},
602
698
  endpoint={refer_to(ctx=ctx, stype=endpoint_path_stype)},
603
699
  args=args,
700
+ request_options=_request_options,
604
701
  )
605
702
  return self.do_request(api_request=api_request, return_type={refer_to(ctx=ctx, stype=data_type)})"""
606
703
  )
@@ -621,7 +718,9 @@ def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> No
621
718
  ctx.out.write(f"{INDENT}labels={{\n")
622
719
  for entry in stype.values.values():
623
720
  if entry.label is not None:
624
- ctx.out.write(f'{INDENT}{INDENT}"{entry.value}": "{entry.label}",\n')
721
+ ctx.out.write(
722
+ f'{INDENT}{INDENT}"{entry.value}": "{entry.label}",\n'
723
+ )
625
724
 
626
725
  ctx.out.write(f"{INDENT}}},\n")
627
726
  if need_deprecated:
@@ -693,37 +792,43 @@ def _emit_properties(
693
792
  if len(properties) > 0:
694
793
 
695
794
  def write_field(prop: builder.SpecProperty) -> None:
795
+ stype = prop.spec_type
696
796
  if prop.name_case == builder.NameCase.preserve:
697
797
  unconverted_keys.add(prop.name)
698
798
  py_name = python_field_name(prop.name, prop.name_case)
699
799
 
700
800
  if prop.convert_value == builder.PropertyConvertValue.no_convert:
701
801
  unconverted_values.add(py_name)
702
- elif not prop.spec_type.is_value_converted():
802
+ elif not stype.is_value_converted():
703
803
  assert prop.convert_value == builder.PropertyConvertValue.auto
704
804
  unconverted_values.add(py_name)
705
- if prop.spec_type.is_value_to_string():
805
+ if stype.is_value_to_string():
706
806
  to_string_values.add(py_name)
707
807
 
708
808
  if prop.parse_require:
709
809
  parse_require.add(py_name)
710
810
 
711
- ref_type = refer_to(ctx, prop.spec_type)
811
+ ref_type = refer_to(ctx, stype)
712
812
  default = None
713
813
  if prop.extant == builder.PropertyExtant.missing:
714
814
  ref_type = f"MissingType[{ref_type}]"
715
815
  default = "MISSING_SENTRY"
716
816
  ctx.use_missing = True
717
817
  elif prop.extant == builder.PropertyExtant.optional:
718
- ref_type = f"typing.Optional[{ref_type}]"
818
+ if isinstance(
819
+ stype, builder.SpecTypeInstance
820
+ ) and stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
821
+ pass # base type already adds the None union
822
+ elif ref_type == "None":
823
+ pass # no need to add a None union to a none type
824
+ else:
825
+ ref_type = f"{ref_type} | None"
719
826
  default = "None"
720
827
  elif prop.has_default:
721
- default = _emit_value(ctx, prop.spec_type, prop.default)
828
+ default = _emit_value(ctx, stype, prop.default)
722
829
  if (
723
- isinstance(prop.spec_type, builder.SpecTypeInstance)
724
- and (
725
- prop.spec_type.defn_type.is_base_type(builder.BaseTypeName.s_list)
726
- )
830
+ isinstance(stype, builder.SpecTypeInstance)
831
+ and (stype.defn_type.is_base_type(builder.BaseTypeName.s_list))
727
832
  and default == "[]"
728
833
  ):
729
834
  default = "dataclasses.field(default_factory=list)"
@@ -749,6 +854,12 @@ def _emit_properties(
749
854
  )
750
855
 
751
856
 
857
+ def _named_type_path(ctx: Context, stype: builder.SpecTypeDefn) -> str:
858
+ parts = [] if stype.is_base else stype.namespace.path.copy()
859
+ parts.append(stype.name)
860
+ return f"{ctx.builder.top_namespace}.{'.'.join(parts)}"
861
+
862
+
752
863
  def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
753
864
  if not isinstance(stype, builder.SpecTypeDefn):
754
865
  return
@@ -769,7 +880,17 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
769
880
  return
770
881
 
771
882
  if isinstance(stype, builder.SpecTypeDefnAlias):
772
- ctx.out.write(f"{stype.name} = {refer_to(ctx, stype.alias)}\n")
883
+ ctx.use_serial_alias = True
884
+ ctx.out.write(f"{stype.name} = typing.Annotated[\n")
885
+ ctx.out.write(f"{INDENT}{refer_to(ctx, stype.alias)},\n")
886
+ ctx.out.write(f"{INDENT}serial_alias_annotation(\n")
887
+ ctx.out.write(
888
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
889
+ )
890
+ if stype.is_dynamic_allowed():
891
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
892
+ ctx.out.write(f"{INDENT}),\n")
893
+ ctx.out.write("]\n")
773
894
  return
774
895
 
775
896
  if isinstance(stype, builder.SpecTypeDefnUnion):
@@ -777,6 +898,11 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
777
898
  ctx.out.write(f"{stype.name} = typing.Annotated[\n")
778
899
  ctx.out.write(f"{INDENT}{refer_to(ctx, stype.get_backing_type())},\n")
779
900
  ctx.out.write(f"{INDENT}serial_union_annotation(\n")
901
+ ctx.out.write(
902
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
903
+ )
904
+ if stype.is_dynamic_allowed():
905
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
780
906
  if stype.discriminator is not None:
781
907
  ctx.out.write(
782
908
  f"{INDENT * 2}discriminator={util.encode_common_string(stype.discriminator)},\n"
@@ -801,11 +927,11 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
801
927
 
802
928
  class_out = io.StringIO()
803
929
  base_class = ""
804
- generic = stype.get_generic()
930
+ generics = stype.get_generics()
805
931
  if not stype.base.is_base:
806
932
  base_class = f"({refer_to(ctx, stype.base)})"
807
- elif generic is not None:
808
- base_class = f"(typing.Generic[{generic}])"
933
+ elif len(generics) > 0:
934
+ base_class = f"[{', '.join(generics)}]"
809
935
  class_out.write(f"class {stype.name}{base_class}:\n")
810
936
 
811
937
  emitted_properties_metadata = _emit_type_properties(
@@ -816,45 +942,51 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
816
942
  to_string_values = emitted_properties_metadata.to_string_values
817
943
  parse_require = emitted_properties_metadata.parse_require
818
944
 
819
- _emit_generic(ctx, stype.get_generic())
945
+ _emit_generics(ctx, generics)
820
946
 
821
- if (
822
- len(unconverted_values) > 0
823
- or len(to_string_values) > 0
824
- or len(unconverted_keys) > 0
825
- or len(parse_require) > 0
826
- ):
827
- ctx.use_serial_class = True
828
- ctx.out.write("@serial_class(\n")
947
+ # Emit serial_class decorator
948
+ ctx.out.write("@serial_class(\n")
949
+ ctx.out.write(
950
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
951
+ )
952
+ if stype.is_dynamic_allowed():
953
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
829
954
 
830
- def write_values(key: str, values: set[str]) -> None:
831
- if len(values) == 0:
832
- return
833
- value_str = ", ".join([f'"{name}"' for name in sorted(values)])
834
- ctx.out.write(f"{INDENT}{key}={{{value_str}}},\n")
955
+ def write_values(key: str, values: set[str]) -> None:
956
+ if len(values) == 0:
957
+ return
958
+ value_str = ", ".join([f'"{name}"' for name in sorted(values)])
959
+ ctx.out.write(f"{INDENT}{key}={{{value_str}}},\n")
835
960
 
836
- write_values("unconverted_keys", unconverted_keys)
837
- write_values("unconverted_values", unconverted_values)
838
- write_values("to_string_values", to_string_values)
839
- write_values("parse_require", parse_require)
961
+ write_values("unconverted_keys", unconverted_keys)
962
+ write_values("unconverted_values", unconverted_values)
963
+ write_values("to_string_values", to_string_values)
964
+ write_values("parse_require", parse_require)
840
965
 
841
- ctx.out.write(")\n")
966
+ ctx.out.write(")\n")
842
967
 
968
+ # Emit dataclass decorator
843
969
  dataclass = "@dataclasses.dataclass"
844
- dc_args = []
970
+ refer_to(
971
+ ctx,
972
+ builder.SpecTypeDefnAlias(
973
+ namespace=ctx.builder.namespaces[base_namespace_name], name="ENABLE_SLOTS"
974
+ ),
975
+ )
976
+ dc_args = ["slots=base_t.ENABLE_SLOTS"]
845
977
  if stype.is_kw_only():
846
978
  dc_args.append("kw_only=True")
847
979
  if stype.is_hashable:
848
980
  dc_args.extend(["frozen=True", "eq=True"])
849
981
  if len(dc_args) > 0:
850
- dataclass += f'({", ".join(dc_args)})'
982
+ dataclass += f"({', '.join(dc_args)}) # type: ignore[literal-required]"
851
983
 
852
984
  ctx.out.write(f"{dataclass}\n")
853
985
  ctx.out.write(class_out.getvalue())
854
986
 
855
987
 
856
- def _emit_generic(ctx: Context, generic: Optional[str]) -> None:
857
- if generic is not None:
988
+ def _emit_generics(ctx: Context, generics: list[str]) -> None:
989
+ for generic in generics:
858
990
  ctx.out.write(f'{generic} = typing.TypeVar("{generic}")\n')
859
991
  ctx.out.write(f"{LINE_BREAK}{LINE_BREAK}")
860
992
 
@@ -892,13 +1024,22 @@ base_name_map = {
892
1024
 
893
1025
  def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
894
1026
  if isinstance(stype, builder.SpecTypeInstance):
895
- params = ", ".join([refer_to(ctx, p) for p in stype.parameters])
1027
+ params = [refer_to(ctx, p) for p in stype.parameters]
1028
+
1029
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_union):
1030
+ if len(stype.parameters) == 1:
1031
+ return f"typing.Union[{params[0]}]"
1032
+ return " | ".join(params)
896
1033
 
897
1034
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_readonly_array):
898
- assert len(stype.parameters) == 1, "Read Only Array takes one parameter"
899
- params = f"{params}, ..."
1035
+ assert len(params) == 1, "Read Only Array takes one parameter"
1036
+ return f"tuple[{params[0]}, ...]"
1037
+
1038
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
1039
+ assert len(params) == 1, "Optional only takes one parameter"
1040
+ return f"{params[0]} | None"
900
1041
 
901
- return f"{refer_to(ctx, stype.defn_type)}[{params}]"
1042
+ return f"{refer_to(ctx, stype.defn_type)}[{', '.join(params)}]"
902
1043
 
903
1044
  if isinstance(stype, builder.SpecTypeLiteralWrapper):
904
1045
  return _emit_value(ctx, stype.value_type, stype.value)
@@ -924,23 +1065,21 @@ def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
924
1065
  SpecEndpoint = builder.SpecEndpoint
925
1066
 
926
1067
 
927
- def _route_identifier(endpoint: builder.SpecEndpoint) -> tuple[str, str, str]:
928
- return (endpoint.path_dirname, endpoint.path_basename, endpoint.method)
929
-
930
-
931
1068
  def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
932
1069
  for endpoint_root in builder.api_endpoints:
933
1070
  endpoints: list[SpecEndpoint] = []
934
1071
  output = config.routes_output.get(endpoint_root)
935
1072
  if output is None:
936
1073
  continue
1074
+ last_endpoint: SpecEndpoint | None = None
937
1075
  for namespace in builder.namespaces.values():
938
1076
  endpoint = namespace.endpoint
1077
+ last_endpoint = endpoint
939
1078
  if endpoint is None:
940
1079
  continue
941
- if endpoint.root != endpoint_root:
1080
+ if endpoint_root not in endpoint.path_per_api_endpoint:
942
1081
  continue
943
- if endpoint.function is None:
1082
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
944
1083
  continue
945
1084
 
946
1085
  endpoints.append(endpoint)
@@ -953,25 +1092,38 @@ def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
953
1092
  from main.site.framework.types import StaticRouteType
954
1093
  """
955
1094
  )
1095
+
1096
+ def _route_identifier(endpoint: SpecEndpoint) -> tuple[str, str, str]:
1097
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1098
+ return (
1099
+ endpoint_specific_path.path_dirname,
1100
+ endpoint_specific_path.path_basename,
1101
+ endpoint.method,
1102
+ )
1103
+
956
1104
  sorted_endpoints = sorted(endpoints, key=_route_identifier)
957
1105
 
958
- assert len(endpoints) == len(
959
- set(map(_route_identifier, endpoints))
960
- ), "Endpoints are not unique"
1106
+ assert len(endpoints) == len(set(map(_route_identifier, endpoints))), (
1107
+ "Endpoints are not unique"
1108
+ )
961
1109
 
962
1110
  path_set = set()
963
1111
  for endpoint in sorted_endpoints:
964
- assert endpoint.function
965
- func_bits = endpoint.function.split(".")
1112
+ last_endpoint = endpoint
1113
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
1114
+ assert endpoint_function_path.function
1115
+ func_bits = endpoint_function_path.function.split(".")
966
1116
  path = ".".join(func_bits[:-1])
967
1117
  if path in path_set:
968
1118
  continue
969
1119
  path_set.add(path)
970
1120
  static_out.write(f"import {path}\n")
971
1121
 
1122
+ assert last_endpoint is not None
1123
+
972
1124
  static_out.write(
973
1125
  f"""
974
- ROUTE_PREFIX = "/{endpoint.path_root}"
1126
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
975
1127
 
976
1128
  ROUTES: list[StaticRouteType] = [
977
1129
  """
@@ -985,20 +1137,21 @@ ROUTES: list[StaticRouteType] = [
985
1137
 
986
1138
  from main.site.framework.types import DynamicRouteType
987
1139
 
988
- ROUTE_PREFIX = "/{endpoint.path_root}"
1140
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
989
1141
 
990
1142
  ROUTES: list[DynamicRouteType] = [
991
1143
  """
992
1144
  )
993
1145
 
994
1146
  for endpoint in sorted_endpoints:
1147
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
995
1148
  dynamic_out.write(
996
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", "{endpoint.function}", ["{endpoint.method.upper()}"]),\n'
1149
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", "{endpoint_function_path.function}", ["{endpoint.method.upper()}"]),\n'
997
1150
  )
998
1151
 
999
- assert endpoint.function
1152
+ assert endpoint_function_path.function
1000
1153
  static_out.write(
1001
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", {endpoint.function}, ["{endpoint.method.upper()}"]),\n'
1154
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", {endpoint_function_path.function}, ["{endpoint.method.upper()}"]),\n'
1002
1155
  )
1003
1156
 
1004
1157
  dynamic_out.write(f"{MODIFY_NOTICE}]\n")
@@ -1014,15 +1167,21 @@ def _emit_namespace_imports(
1014
1167
  *,
1015
1168
  out: io.StringIO,
1016
1169
  namespaces: set[builder.SpecNamespace],
1017
- from_namespace: Optional[builder.SpecNamespace],
1170
+ from_namespace: builder.SpecNamespace | None,
1018
1171
  config: PythonConfig,
1172
+ skip_non_sdk: bool = False,
1019
1173
  ) -> None:
1020
1174
  for ns in sorted(
1021
1175
  namespaces,
1022
1176
  key=lambda name: _resolve_namespace_name(name),
1023
1177
  ):
1178
+ if (
1179
+ skip_non_sdk
1180
+ and ns.endpoint is not None
1181
+ and ns.endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
1182
+ ):
1183
+ continue
1024
1184
  resolved = _resolve_namespace_name(ns)
1025
- ref = _resolve_namespace_ref(ns)
1026
1185
  if ns.endpoint is not None:
1027
1186
  import_alias = "_".join(ns.path[2:]) + "_t"
1028
1187
  out.write(
@@ -1046,8 +1205,8 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1046
1205
  return None
1047
1206
  enum_out = io.StringIO()
1048
1207
  enum_out.write(f"{LINT_HEADER}{MODIFY_NOTICE}\n")
1049
- enum_out.write("from typing import Literal, Union\n")
1050
- enum_out.write("from pkgs.strenum_compat import StrEnum\n")
1208
+ enum_out.write("import typing\n")
1209
+ enum_out.write("from enum import StrEnum\n")
1051
1210
 
1052
1211
  ctx = TrackingContext()
1053
1212
  # In this context the propername
@@ -1063,11 +1222,11 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1063
1222
  known_keys = []
1064
1223
  enum_out.write("\nENUM_MAP: dict[str, type[StrEnum]] = {\n")
1065
1224
  for key in sorted(named_enums.keys()):
1066
- enum_out.write(f'"{key}": {named_enums[key]},\n')
1067
- known_keys.append(f'Literal["{key}"]')
1225
+ enum_out.write(f'{INDENT}"{key}": {named_enums[key]},\n')
1226
+ known_keys.append(f'"{key}"')
1068
1227
  enum_out.write(f"}}\n{MODIFY_NOTICE}\n")
1069
1228
 
1070
- enum_out.write(f"\nKnownEnumsType = Union[\n{INDENT}")
1229
+ enum_out.write(f"\nKnownEnumsType = typing.Literal[\n{INDENT}")
1071
1230
  enum_out.write(f",\n{INDENT}".join(known_keys))
1072
1231
  enum_out.write(f"\n]\n{MODIFY_NOTICE}\n")
1073
1232
 
@@ -1084,21 +1243,36 @@ def _emit_api_stubs(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1084
1243
 
1085
1244
  if endpoint is None:
1086
1245
  continue
1087
- if endpoint.root != endpoint_root:
1246
+ if endpoint_root not in endpoint.path_per_api_endpoint:
1088
1247
  continue
1089
- if endpoint.function is None:
1248
+
1249
+ endpoint_function = endpoint.path_per_api_endpoint[endpoint_root].function
1250
+ if endpoint_function is None:
1090
1251
  continue
1091
1252
 
1092
- module_dir, file_name, func_name = endpoint.function.rsplit(".", 2)
1253
+ module_dir, file_name, _func_name = endpoint_function.rsplit(".", 2)
1093
1254
  module_path = os.path.abspath(module_dir.replace(".", "/"))
1094
1255
  api_stub_file = f"{module_path}/{file_name}.py"
1095
1256
  if os.path.isfile(api_stub_file):
1096
1257
  continue
1097
- _create_api_stub(api_stub_file, file_name, endpoint, config)
1258
+ _create_api_stub(
1259
+ api_stub_file=api_stub_file,
1260
+ file_name=file_name,
1261
+ endpoint=endpoint,
1262
+ config=config,
1263
+ endpoint_root=endpoint_root,
1264
+ top_namespace=builder.top_namespace,
1265
+ )
1098
1266
 
1099
1267
 
1100
1268
  def _create_api_stub(
1101
- api_stub_file: str, file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1269
+ *,
1270
+ api_stub_file: str,
1271
+ file_name: str,
1272
+ endpoint: SpecEndpoint,
1273
+ config: PythonConfig,
1274
+ endpoint_root: str,
1275
+ top_namespace: str,
1102
1276
  ) -> None:
1103
1277
  assert (
1104
1278
  endpoint.method == builder.RouteMethod.post
@@ -1106,7 +1280,13 @@ def _create_api_stub(
1106
1280
  or endpoint.method == builder.RouteMethod.delete
1107
1281
  or endpoint.method == builder.RouteMethod.patch
1108
1282
  )
1109
- api_out = _create_api_function(file_name, endpoint, config)
1283
+ api_out = _create_api_function(
1284
+ file_name=file_name,
1285
+ endpoint=endpoint,
1286
+ config=config,
1287
+ endpoint_root=endpoint_root,
1288
+ top_namespace=top_namespace,
1289
+ )
1110
1290
  util.rewrite_file(api_stub_file, api_out.getvalue())
1111
1291
 
1112
1292
 
@@ -1115,15 +1295,22 @@ WRAP_ARGS_END = "\n"
1115
1295
 
1116
1296
 
1117
1297
  def _create_api_function(
1118
- file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1298
+ *,
1299
+ file_name: str,
1300
+ endpoint: SpecEndpoint,
1301
+ config: PythonConfig,
1302
+ endpoint_root: str,
1303
+ top_namespace: str,
1119
1304
  ) -> io.StringIO:
1305
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1306
+ assert endpoint_specific_path is not None
1120
1307
  api_out = io.StringIO()
1121
1308
  python_api_type_root = f"{config.types_package}.api"
1122
- dot_dirname = endpoint.path_dirname.replace("/", ".")
1309
+ dot_dirname = endpoint_specific_path.path_dirname.replace("/", ".")
1123
1310
  api_import = (
1124
1311
  f"{python_api_type_root}.{dot_dirname}.{file_name}"
1125
1312
  if dot_dirname != ""
1126
- else f"{python_api_type_root}.{endpoint.path_basename}"
1313
+ else f"{python_api_type_root}.{endpoint_specific_path.path_basename}"
1127
1314
  )
1128
1315
 
1129
1316
  if endpoint.method == builder.RouteMethod.post:
@@ -1135,7 +1322,20 @@ def _create_api_function(
1135
1322
  elif endpoint.method == builder.RouteMethod.patch:
1136
1323
  validated_method = "validated_patch"
1137
1324
 
1138
- ruff_requires_wrap = len(endpoint.path_basename) > 14
1325
+ ruff_requires_wrap = len(endpoint_specific_path.path_basename) > 14
1326
+
1327
+ account_type = (
1328
+ endpoint_specific_path.root
1329
+ if endpoint_specific_path.root not in ["external", "portal"]
1330
+ else "materials"
1331
+ )
1332
+
1333
+ endpoint_path_name = (
1334
+ ENDPOINT_PATH_ALTERNATE
1335
+ if len(endpoint.path_per_api_endpoint.keys()) > 1
1336
+ and endpoint_specific_path.root != top_namespace
1337
+ else ENDPOINT_PATH
1338
+ )
1139
1339
 
1140
1340
  api_out.write(
1141
1341
  f"""import {api_import} as api
@@ -1143,8 +1343,8 @@ from main.db.session import Session, SessionMaker
1143
1343
  from main.site.decorators import APIError, APIResponse, {validated_method}
1144
1344
 
1145
1345
 
1146
- @{validated_method}(api.ENDPOINT_PATH, "{endpoint.root}", api.Arguments)
1147
- def {endpoint.path_basename}({WRAP_ARGS_START if ruff_requires_wrap else ""}args: api.Arguments, client_sm: SessionMaker{WRAP_ARGS_END if ruff_requires_wrap else ""}) -> APIResponse[api.Data]:
1346
+ @{validated_method}(api.{endpoint_path_name}, "{account_type}", api.Arguments)
1347
+ def {endpoint_specific_path.path_basename}({WRAP_ARGS_START if ruff_requires_wrap else ""}args: api.Arguments, client_sm: SessionMaker{WRAP_ARGS_END if ruff_requires_wrap else ""}) -> APIResponse[api.Data]:
1148
1348
  with Session(client_sm) as session:
1149
1349
  # return APIResponse(data=api.Data())
1150
1350
  pass
@@ -1166,7 +1366,7 @@ def _emit_api_argument_lookup(
1166
1366
  for endpoint_root in builder.api_endpoints:
1167
1367
  routes_output = config.routes_output[endpoint_root]
1168
1368
 
1169
- imports = ["import typing", "import dataclasses"]
1369
+ imports = ["import dataclasses"]
1170
1370
  mappings = []
1171
1371
  for namespace in sorted(
1172
1372
  builder.namespaces.values(),
@@ -1176,11 +1376,11 @@ def _emit_api_argument_lookup(
1176
1376
 
1177
1377
  if endpoint is None:
1178
1378
  continue
1179
- if endpoint.root != endpoint_root:
1379
+ if endpoint_root not in endpoint.path_per_api_endpoint:
1180
1380
  continue
1181
- if endpoint.function is None:
1381
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
1182
1382
  continue
1183
- if "Arguments" not in namespace.types:
1383
+ if "Arguments" not in namespace.types or "Data" not in namespace.types:
1184
1384
  continue
1185
1385
 
1186
1386
  import_alias = "_".join(namespace.path[1:])
@@ -1204,7 +1404,7 @@ def _emit_api_argument_lookup(
1204
1404
  mapping += f"{INDENT}{INDENT}route_group={route_group},\n"
1205
1405
  mapping += f"{INDENT}{INDENT}account_type={account_type},\n"
1206
1406
  mapping += f"{INDENT}{INDENT}route={import_alias}.ENDPOINT_PATH,\n"
1207
- mapping += f'{INDENT}{INDENT}handler="{endpoint.function}",\n'
1407
+ mapping += f'{INDENT}{INDENT}handler="{endpoint.path_per_api_endpoint[endpoint_root].function}",\n'
1208
1408
  mapping += f"{INDENT}{INDENT}method={import_alias}.ENDPOINT_METHOD,\n"
1209
1409
  mapping += f"{INDENT})"
1210
1410
  mappings.append(mapping)
@@ -1215,9 +1415,6 @@ def _emit_api_argument_lookup(
1215
1415
  argument_lookup_out.write(
1216
1416
  f"""{LINE_BREAK.join(imports)}
1217
1417
 
1218
- AT = typing.TypeVar("AT")
1219
- DT = typing.TypeVar("DT")
1220
-
1221
1418
 
1222
1419
  @dataclasses.dataclass(kw_only=True, frozen=True)
1223
1420
  class ApiEndpointKey:
@@ -1226,7 +1423,7 @@ class ApiEndpointKey:
1226
1423
 
1227
1424
 
1228
1425
  @dataclasses.dataclass(kw_only=True)
1229
- class ApiEndpointSpec(typing.Generic[AT, DT]):
1426
+ class ApiEndpointSpec[AT, DT]:
1230
1427
  route: str
1231
1428
  arguments_type: type[AT]
1232
1429
  data_type: type[DT]
@@ -1256,10 +1453,10 @@ CLIENT_CLASS_IMPORTS = [
1256
1453
  "import dataclasses",
1257
1454
  ]
1258
1455
  ASYNC_BATCH_PROCESSOR_FILENAME = "async_batch_processor"
1259
- ASYNC_BATCH_PROCESSOR_IMPORTS = [
1456
+ ASYNC_BATCH_PROCESSOR_BASE_IMPORTS = [
1260
1457
  "import uuid",
1261
1458
  "from abc import ABC, abstractmethod",
1262
- "from pkgs.serialization_util.serialization_helpers import serialize_for_api",
1459
+ "from pkgs.serialization_util import serialize_for_api",
1263
1460
  ]
1264
1461
 
1265
1462
 
@@ -1271,7 +1468,9 @@ def _emit_async_batch_processor(
1271
1468
 
1272
1469
  async_batch_processor_out = io.StringIO()
1273
1470
  ctx = Context(
1274
- out=io.StringIO(), namespace=builder.SpecNamespace("async_batch_processor")
1471
+ out=io.StringIO(),
1472
+ namespace=builder.SpecNamespace("async_batch_processor"),
1473
+ builder=spec_builder,
1275
1474
  )
1276
1475
 
1277
1476
  for namespace in sorted(
@@ -1292,8 +1491,11 @@ def _emit_async_batch_processor(
1292
1491
  config=config,
1293
1492
  )
1294
1493
 
1494
+ imports = ASYNC_BATCH_PROCESSOR_BASE_IMPORTS.copy()
1495
+ if ctx.use_dataclass:
1496
+ imports.append("import dataclasses")
1295
1497
  async_batch_processor_out.write(
1296
- f"""{LINE_BREAK.join(ASYNC_BATCH_PROCESSOR_IMPORTS)}
1498
+ f"""{LINE_BREAK.join(imports)}
1297
1499
 
1298
1500
 
1299
1501
  class AsyncBatchProcessorBase(ABC):
@@ -1321,7 +1523,11 @@ def _emit_client_class(
1321
1523
  return
1322
1524
 
1323
1525
  client_base_out = io.StringIO()
1324
- ctx = Context(out=io.StringIO(), namespace=builder.SpecNamespace("client_base"))
1526
+ ctx = Context(
1527
+ out=io.StringIO(),
1528
+ builder=spec_builder,
1529
+ namespace=builder.SpecNamespace("client_base"),
1530
+ )
1325
1531
  for namespace in sorted(
1326
1532
  spec_builder.namespaces.values(),
1327
1533
  key=lambda ns: _resolve_namespace_name(ns),
@@ -1338,6 +1544,7 @@ def _emit_client_class(
1338
1544
  namespaces=ctx.namespaces,
1339
1545
  from_namespace=None,
1340
1546
  config=config,
1547
+ skip_non_sdk=True,
1341
1548
  )
1342
1549
 
1343
1550
  client_base_out.write(
@@ -1351,6 +1558,7 @@ class APIRequest:
1351
1558
  method: str
1352
1559
  endpoint: str
1353
1560
  args: typing.Any
1561
+ request_options: {refer_to(ctx=ctx, stype=REQUEST_OPTIONS_STYPE)} | None = None
1354
1562
 
1355
1563
 
1356
1564
  class ClientMethods(ABC):