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
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  from io import BytesIO
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
 
5
5
  from google.oauth2 import service_account
6
6
  from googleapiclient.discovery import build as build_gdrive_connection
@@ -30,7 +30,7 @@ def download_gdrive_file(
30
30
  mime_type: str,
31
31
  *,
32
32
  verbose: bool = False,
33
- ) -> Optional[FileObjectData]:
33
+ ) -> FileObjectData | None:
34
34
  if "folder" in mime_type:
35
35
  if verbose:
36
36
  print(f"{filename} is a folder and will not be downloaded.")
@@ -63,7 +63,7 @@ def download_gdrive_file(
63
63
  downloader = MediaIoBaseDownload(file_handler, file_request)
64
64
  download_complete = False
65
65
  while not download_complete:
66
- status, download_complete = downloader.next_chunk()
66
+ _status, download_complete = downloader.next_chunk()
67
67
 
68
68
  file_handler.seek(0)
69
69
  file_data = file_handler.read()
@@ -148,7 +148,7 @@ def move_gdrive_file(
148
148
  src_file_id: str,
149
149
  dest_folder_id: str,
150
150
  *,
151
- dest_filename: Optional[str] = None,
151
+ dest_filename: str | None = None,
152
152
  ) -> None:
153
153
  # Retrieve the existing parents to remove
154
154
  file = (
@@ -197,7 +197,7 @@ class GDriveSession(FileSystemSession):
197
197
  dir_path: FileSystemObject,
198
198
  *,
199
199
  recursive: bool = False,
200
- valid_file_extensions: Optional[tuple[str, ...]] = None,
200
+ valid_file_extensions: tuple[str, ...] | None = None,
201
201
  ) -> list[FileSystemObject]:
202
202
  if not isinstance(dir_path, RemoteObjectReference):
203
203
  raise IncompatibleFileReference(
@@ -212,7 +212,8 @@ class GDriveSession(FileSystemSession):
212
212
  for file_context in files:
213
213
  if (
214
214
  valid_file_extensions is not None
215
- and os.path.splitext(file_context["name"])[1] not in valid_file_extensions
215
+ and os.path.splitext(file_context["name"])[1]
216
+ not in valid_file_extensions
216
217
  ):
217
218
  continue
218
219
  gdrive_files.append(
@@ -256,7 +257,9 @@ class GDriveSession(FileSystemSession):
256
257
  dest_filename=new_filename,
257
258
  )
258
259
  else:
259
- move_gdrive_file(self.connection, src_file.file_id, dest_file.file_id)
260
+ move_gdrive_file(
261
+ self.connection, src_file.file_id, dest_file.file_id
262
+ )
260
263
  elif isinstance(src_file, FileObjectData):
261
264
  if src_file.mime_type is None:
262
265
  raise IncompatibleFileReference(
@@ -1,6 +1,7 @@
1
1
  from io import BytesIO
2
2
 
3
3
  from boto3.session import Session
4
+ from mypy_boto3_s3.service_resource import Bucket
4
5
 
5
6
  from pkgs.filesystem_utils.file_type_utils import (
6
7
  FileObjectData,
@@ -37,7 +38,7 @@ class S3Session(FileSystemSession):
37
38
  aws_session_token=self.config.session_token,
38
39
  )
39
40
 
40
- self.bucket = s3_resource.Bucket(self.config.bucket_name)
41
+ self.bucket: Bucket | None = s3_resource.Bucket(self.config.bucket_name)
41
42
 
42
43
  def __enter__(self) -> "S3Session":
43
44
  self.start()
@@ -53,18 +54,19 @@ class S3Session(FileSystemSession):
53
54
  recursive: bool = False,
54
55
  valid_extensions: list[str] | None = None,
55
56
  ) -> list[FileSystemObject]:
56
- if recursive:
57
- raise NotImplementedError("recursive file listings not implemented for s3")
58
-
59
57
  if not isinstance(dir_path, FileSystemFileReference):
60
58
  raise IncompatibleFileReference()
61
59
 
62
60
  assert self.bucket is not None, "call to list_files on uninitialized s3 session"
63
61
 
64
62
  filesystem_references: list[FileSystemObject] = []
65
- for obj in self.bucket.objects.filter(Prefix=dir_path.filepath):
63
+ prefix = _add_slash(dir_path.filepath)
64
+ for obj in self.bucket.objects.filter(Prefix=prefix):
65
+ if not recursive and (obj.key == prefix or "/" in obj.key[len(prefix) :]):
66
+ continue
66
67
  if valid_extensions is None or any(
67
- obj.key.endswith(valid_extension) for valid_extension in valid_extensions
68
+ obj.key.endswith(valid_extension)
69
+ for valid_extension in valid_extensions
68
70
  ):
69
71
  filesystem_references.append(FileSystemFileReference(obj.key))
70
72
 
@@ -75,9 +77,9 @@ class S3Session(FileSystemSession):
75
77
  filepaths: list[FileSystemObject],
76
78
  ) -> list[FileObjectData]:
77
79
  downloaded_files: list[FileObjectData] = []
78
- assert (
79
- self.bucket is not None
80
- ), "call to download_files on uninitialized s3 session"
80
+ assert self.bucket is not None, (
81
+ "call to download_files on uninitialized s3 session"
82
+ )
81
83
 
82
84
  for file_object in filepaths:
83
85
  if (
@@ -85,7 +87,7 @@ class S3Session(FileSystemSession):
85
87
  or file_object.filename is None
86
88
  ):
87
89
  raise IncompatibleFileReference()
88
- s3_file_obj = self.bucket.Object(_add_slash(file_object.filepath))
90
+ s3_file_obj = self.bucket.Object(file_object.filepath)
89
91
  response = s3_file_obj.get()
90
92
  file_obj_bytes = response["Body"].read()
91
93
  downloaded_files.append(
@@ -107,10 +109,10 @@ class S3Session(FileSystemSession):
107
109
  dest_file, FileSystemFileReference
108
110
  ):
109
111
  raise IncompatibleFileReference()
110
- self.bucket.Object(_add_slash(dest_file.filepath)).copy_from(
112
+ self.bucket.Object(dest_file.filepath).copy_from(
111
113
  CopySource={
112
114
  "Bucket": self.bucket.name,
113
- "Key": _add_slash(src_file.filepath),
115
+ "Key": src_file.filepath,
114
116
  }
115
117
  )
116
- self.bucket.Object(_add_slash(src_file.filepath)).delete()
118
+ self.bucket.Object(src_file.filepath).delete()
@@ -1,7 +1,6 @@
1
1
  import os
2
2
  from collections.abc import Iterable
3
3
  from io import BytesIO
4
- from typing import Optional
5
4
 
6
5
  import paramiko
7
6
  import pysftp
@@ -30,8 +29,8 @@ def list_sftp_files(
30
29
  connection: pysftp.Connection,
31
30
  dir_path: str,
32
31
  *,
33
- valid_extensions: Optional[Iterable[str]] = None,
34
- parent_dir_path: Optional[str] = None,
32
+ valid_extensions: Iterable[str] | None = None,
33
+ parent_dir_path: str | None = None,
35
34
  recursive: bool = True,
36
35
  ) -> list[str]:
37
36
  file_paths: list[str] = []
@@ -42,7 +41,8 @@ def list_sftp_files(
42
41
 
43
42
  def _add_file(path: str) -> None:
44
43
  if (
45
- valid_extensions is None or os.path.splitext(path)[1] in valid_extensions
44
+ valid_extensions is None
45
+ or os.path.splitext(path)[1] in valid_extensions
46
46
  ) and (parent_dir_path is None or os.path.dirname(path) == parent_dir_path):
47
47
  file_paths.append(path)
48
48
 
@@ -54,6 +54,10 @@ def list_sftp_files(
54
54
  os.path.join(dir_path, file)
55
55
  for file in connection.listdir(dir_path)
56
56
  if connection.isfile(os.path.join(dir_path, file))
57
+ and (
58
+ valid_extensions is None
59
+ or os.path.splitext(file)[1] in valid_extensions
60
+ )
57
61
  ])
58
62
  return file_paths
59
63
 
@@ -106,9 +110,9 @@ class SFTPSession(FileSystemSession):
106
110
  recursive: bool = True,
107
111
  valid_extensions: list[str] | None = None,
108
112
  ) -> list[FileSystemObject]:
109
- if not isinstance(dir_path, FileSystemFileReference) or not self.connection.isdir(
110
- dir_path.filepath
111
- ):
113
+ if not isinstance(
114
+ dir_path, FileSystemFileReference
115
+ ) or not self.connection.isdir(dir_path.filepath):
112
116
  raise IncompatibleFileReference()
113
117
 
114
118
  return [
@@ -1,9 +1,15 @@
1
1
  import os
2
2
  from dataclasses import dataclass
3
3
  from io import BytesIO
4
- from typing import Optional, Union
4
+ from typing import Union
5
5
 
6
6
  import paramiko
7
+ from azure.core.credentials import (
8
+ AzureNamedKeyCredential,
9
+ AzureSasCredential,
10
+ TokenCredential,
11
+ )
12
+ from azure.storage.blob import ContainerProperties
7
13
 
8
14
 
9
15
  @dataclass
@@ -11,9 +17,9 @@ class FileObjectData:
11
17
  file_data: bytes
12
18
  file_IO: BytesIO
13
19
  filename: str
14
- filepath: Optional[str] = None
15
- mime_type: Optional[str] = None
16
- metadata: Optional[dict[str, str]] = None
20
+ filepath: str | None = None
21
+ mime_type: str | None = None
22
+ metadata: dict[str, str] | None = None
17
23
 
18
24
 
19
25
  @dataclass
@@ -33,7 +39,7 @@ class FileSystemFileReference:
33
39
  class RemoteObjectReference:
34
40
  file_id: str
35
41
  mime_type: str
36
- filename: Optional[str] = None
42
+ filename: str | None = None
37
43
 
38
44
  @property
39
45
  def is_dir(self) -> bool:
@@ -57,7 +63,7 @@ class FileSystemSFTPConfig:
57
63
  pem_path: str | None
58
64
  pem_key: paramiko.RSAKey | None = None
59
65
  password: str | None = None
60
- valid_extensions: Optional[tuple[str]] = None
66
+ valid_extensions: tuple[str] | None = None
61
67
  recursive: bool = True
62
68
 
63
69
 
@@ -65,7 +71,21 @@ class FileSystemSFTPConfig:
65
71
  class FileSystemS3Config:
66
72
  endpoint_url: str
67
73
  bucket_name: str
68
- region_name: Optional[str]
69
- access_key_id: Optional[str]
70
- secret_access_key: Optional[str]
71
- session_token: Optional[str]
74
+ region_name: str | None
75
+ access_key_id: str | None
76
+ secret_access_key: str | None
77
+ session_token: str | None
78
+
79
+
80
+ @dataclass(kw_only=True)
81
+ class FileSystemBlobConfig:
82
+ account_url: str
83
+ credential: (
84
+ str
85
+ | dict[str, str]
86
+ | AzureNamedKeyCredential
87
+ | AzureSasCredential
88
+ | TokenCredential
89
+ | None
90
+ )
91
+ container: ContainerProperties | str
pkgs/py.typed ADDED
File without changes
@@ -1,12 +1,17 @@
1
- # flake8:noqa
1
+ from .annotation import unwrap_annotated as unwrap_annotated
2
2
  from .missing_sentry import MISSING_SENTRY as MISSING_SENTRY
3
3
  from .missing_sentry import MissingSentryType as MissingSentryType
4
4
  from .missing_sentry import MissingType as MissingType
5
5
  from .missing_sentry import coalesce_missing_sentry as coalesce_missing_sentry
6
6
  from .opaque_key import OpaqueKey as OpaqueKey
7
+ from .serial_alias import SerialAliasInspector as SerialAliasInspector
8
+ from .serial_alias import get_serial_alias_data as get_serial_alias_data
9
+ from .serial_alias import serial_alias_annotation as serial_alias_annotation
10
+ from .serial_class import SerialClassDataInspector as SerialClassDataInspector
7
11
  from .serial_class import get_serial_class_data as get_serial_class_data
8
12
  from .serial_class import get_serial_string_enum_data as get_serial_string_enum_data
9
13
  from .serial_class import serial_class as serial_class
10
14
  from .serial_class import serial_string_enum as serial_string_enum
11
- from .serial_union import serial_union_annotation as serial_union_annotation
15
+ from .serial_generic import get_serial_data as get_serial_data
12
16
  from .serial_union import get_serial_union_data as get_serial_union_data
17
+ from .serial_union import serial_union_annotation as serial_union_annotation
@@ -0,0 +1,64 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ T = typing.TypeVar("T")
5
+
6
+
7
+ @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
8
+ class SerialBase:
9
+ named_type_path: str | None = None
10
+ # Indicates this type is allowed in dynamic lookups, such as via a named_type_path
11
+ # This isn't meant to be limting, but to catalog all the types where we need it
12
+ is_dynamic_allowed: bool = False
13
+ # Tracks if this data was provided as a decorator to the type.
14
+ # This is used to track "proper types" which are appropriate
15
+ # for serialization and/or dynamic discovery
16
+ from_decorator: bool = False
17
+
18
+
19
+ def get_serial_annotation(parsed_type: type[T]) -> SerialBase | None:
20
+ if not hasattr(parsed_type, "__metadata__"):
21
+ return None
22
+ metadata = parsed_type.__metadata__ # type:ignore[attr-defined]
23
+ if not isinstance(metadata, tuple) or len(metadata) != 1:
24
+ return None
25
+ serial = metadata[0]
26
+ if not isinstance(serial, SerialBase):
27
+ return None
28
+ return serial
29
+
30
+
31
+ class SerialInspector(typing.Generic[T]):
32
+ def __init__(self, parsed_type: type[T], serial_base: SerialBase) -> None:
33
+ self._parsed_type = parsed_type
34
+ self._serial_base = serial_base
35
+
36
+ @property
37
+ def named_type_path(self) -> str | None:
38
+ return self._serial_base.named_type_path
39
+
40
+ @property
41
+ def from_decorator(self) -> bool:
42
+ return self._serial_base.from_decorator
43
+
44
+ @property
45
+ def is_field_proper(self) -> bool:
46
+ return (
47
+ self._serial_base.from_decorator
48
+ and self._serial_base.named_type_path is not None
49
+ )
50
+
51
+ @property
52
+ def is_dynamic_allowed(self) -> bool:
53
+ return self._serial_base.is_dynamic_allowed
54
+
55
+
56
+ def unwrap_annotated(parsed_type: type[T]) -> type[T]:
57
+ """
58
+ If the type is an annotated type then return the origin of it.
59
+ Otherwise return the original type.
60
+ """
61
+ if typing.get_origin(parsed_type) is typing.Annotated:
62
+ # It's unclear if there's anyway to type this correctly
63
+ return parsed_type.__origin__ # type:ignore[attr-defined, no-any-return]
64
+ return parsed_type
@@ -26,5 +26,5 @@ MISSING_SENTRY = MissingSentryType()
26
26
  MissingType = Union[MissingSentryType, ClassT]
27
27
 
28
28
 
29
- def coalesce_missing_sentry(value: MissingType[ClassT]) -> Optional[ClassT]:
29
+ def coalesce_missing_sentry(value: MissingType[ClassT]) -> ClassT | None:
30
30
  return None if isinstance(value, MissingSentryType) else value
@@ -1,4 +1,4 @@
1
1
  # Blocks a string key value from being interpreted for case conversion
2
- class OpaqueKey(str):
2
+ class OpaqueKey(str): # noqa: FURB189
3
3
  def __new__(cls, key: str) -> "OpaqueKey":
4
4
  return str.__new__(cls, key)
@@ -0,0 +1,47 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from .annotation import SerialBase, SerialInspector, get_serial_annotation
5
+
6
+ T = typing.TypeVar("T")
7
+
8
+
9
+ @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
10
+ class _SerialAlias(SerialBase):
11
+ """
12
+ This class is to be kept private, to provide flexibility in registration/lookup.
13
+ Places that need the data should access it via help classes/methods.
14
+ """
15
+
16
+
17
+ def serial_alias_annotation(
18
+ *,
19
+ named_type_path: str | None = None,
20
+ is_dynamic_allowed: bool = False,
21
+ ) -> _SerialAlias:
22
+ return _SerialAlias(
23
+ named_type_path=named_type_path,
24
+ from_decorator=True,
25
+ is_dynamic_allowed=is_dynamic_allowed,
26
+ )
27
+
28
+
29
+ def _get_serial_alias(parsed_type: type[T]) -> _SerialAlias | None:
30
+ serial = get_serial_annotation(parsed_type)
31
+ if not isinstance(serial, _SerialAlias):
32
+ return None
33
+ return serial
34
+
35
+
36
+ class SerialAliasInspector(SerialInspector[T]):
37
+ def __init__(self, parsed_type: type[T], serial_alias: _SerialAlias) -> None:
38
+ super().__init__(parsed_type, serial_alias)
39
+ self._serial_alias = serial_alias
40
+
41
+
42
+ def get_serial_alias_data(parsed_type: type[T]) -> SerialAliasInspector[T] | None:
43
+ serial = _get_serial_alias(parsed_type)
44
+ if serial is None:
45
+ return None
46
+
47
+ return SerialAliasInspector(parsed_type, serial)
@@ -3,17 +3,20 @@ from __future__ import annotations
3
3
  import dataclasses
4
4
  from collections.abc import Callable
5
5
  from enum import StrEnum
6
- from typing import Any, Optional, TypeVar, cast
6
+ from typing import Any, TypeVar, cast
7
7
 
8
- _ClassT = TypeVar("_ClassT")
8
+ from .annotation import SerialBase, SerialInspector
9
9
 
10
+ ClassT = TypeVar("ClassT")
10
11
 
11
- @dataclasses.dataclass
12
- class _SerialClassData:
12
+
13
+ @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
14
+ class _SerialClassData(SerialBase):
13
15
  unconverted_keys: set[str] = dataclasses.field(default_factory=set)
14
16
  unconverted_values: set[str] = dataclasses.field(default_factory=set)
15
17
  to_string_values: set[str] = dataclasses.field(default_factory=set)
16
18
  parse_require: set[str] = dataclasses.field(default_factory=set)
19
+ named_type_path: str | None = None
17
20
 
18
21
 
19
22
  EMPTY_SERIAL_CLASS_DATA = _SerialClassData()
@@ -21,11 +24,13 @@ EMPTY_SERIAL_CLASS_DATA = _SerialClassData()
21
24
 
22
25
  def serial_class(
23
26
  *,
24
- unconverted_keys: Optional[set[str]] = None,
25
- unconverted_values: Optional[set[str]] = None,
26
- to_string_values: Optional[set[str]] = None,
27
- parse_require: Optional[set[str]] = None,
28
- ) -> Callable[[_ClassT], _ClassT]:
27
+ unconverted_keys: set[str] | None = None,
28
+ unconverted_values: set[str] | None = None,
29
+ to_string_values: set[str] | None = None,
30
+ parse_require: set[str] | None = None,
31
+ named_type_path: str | None = None,
32
+ is_dynamic_allowed: bool = False,
33
+ ) -> Callable[[ClassT], ClassT]:
29
34
  """
30
35
  An additional decorator to a dataclass that specifies serialization options.
31
36
 
@@ -45,25 +50,32 @@ def serial_class(
45
50
  This field is always required while parsing, even if it has a default in the definition.
46
51
  This allows supporting literal type defaults for Python instantiation, but
47
52
  requiring them for the API input.
53
+ @param named_type_path
54
+ The type_spec type-path to this type. This applies only to named types.
48
55
  """
49
56
 
50
- def decorate(orig_class: _ClassT) -> _ClassT:
57
+ def decorate(orig_class: ClassT) -> ClassT:
51
58
  cast(Any, orig_class).__unc_serial_data = _SerialClassData(
52
59
  unconverted_keys=unconverted_keys or set(),
53
60
  unconverted_values=unconverted_values or set(),
54
61
  to_string_values=to_string_values or set(),
55
62
  parse_require=parse_require or set(),
63
+ named_type_path=named_type_path,
64
+ from_decorator=True,
65
+ is_dynamic_allowed=is_dynamic_allowed,
56
66
  )
57
67
  return orig_class
58
68
 
59
69
  return decorate
60
70
 
61
71
 
62
- class SerialClassDataInspector:
72
+ class SerialClassDataInspector(SerialInspector[ClassT]):
63
73
  def __init__(
64
74
  self,
75
+ parsed_type: type[ClassT],
65
76
  current: _SerialClassData,
66
77
  ) -> None:
78
+ super().__init__(parsed_type, current)
67
79
  self.current = current
68
80
 
69
81
  def has_unconverted_key(self, key: str) -> bool:
@@ -79,30 +91,39 @@ class SerialClassDataInspector:
79
91
  return key in self.current.parse_require
80
92
 
81
93
 
82
- def _get_merged_serial_class_data(type_class: type[Any]) -> _SerialClassData | None:
94
+ def get_merged_serial_class_data(type_class: type[Any]) -> _SerialClassData | None:
83
95
  base_class_data = (
84
96
  cast(_SerialClassData, type_class.__unc_serial_data)
85
97
  if hasattr(type_class, "__unc_serial_data")
86
98
  else None
87
99
  )
100
+ if base_class_data is None:
101
+ return None
102
+
103
+ # IMPROVE: We should cache this result on the type
88
104
  if type_class.__bases__ is not None:
89
105
  for base in type_class.__bases__:
90
- curr_base_class_data = _get_merged_serial_class_data(base)
106
+ curr_base_class_data = get_merged_serial_class_data(base)
91
107
  if curr_base_class_data is not None:
92
- if base_class_data is None:
93
- base_class_data = _SerialClassData()
94
- base_class_data.unconverted_keys |= curr_base_class_data.unconverted_keys
95
- base_class_data.unconverted_values |= (
96
- curr_base_class_data.unconverted_values
108
+ base_class_data = dataclasses.replace(
109
+ base_class_data,
110
+ unconverted_keys=base_class_data.unconverted_keys
111
+ | curr_base_class_data.unconverted_keys,
112
+ unconverted_values=base_class_data.unconverted_values
113
+ | curr_base_class_data.unconverted_values,
114
+ to_string_values=base_class_data.to_string_values
115
+ | curr_base_class_data.to_string_values,
116
+ parse_require=base_class_data.parse_require
117
+ | curr_base_class_data.parse_require,
97
118
  )
98
- base_class_data.to_string_values |= curr_base_class_data.to_string_values
99
- base_class_data.parse_require |= curr_base_class_data.parse_require
100
119
  return base_class_data
101
120
 
102
121
 
103
- def get_serial_class_data(type_class: type[Any]) -> SerialClassDataInspector:
122
+ def get_serial_class_data(
123
+ type_class: type[ClassT],
124
+ ) -> SerialClassDataInspector[ClassT]:
104
125
  return SerialClassDataInspector(
105
- _get_merged_serial_class_data(type_class) or EMPTY_SERIAL_CLASS_DATA
126
+ type_class, get_merged_serial_class_data(type_class) or EMPTY_SERIAL_CLASS_DATA
106
127
  )
107
128
 
108
129
 
@@ -113,13 +134,13 @@ class _SerialStringEnumData:
113
134
 
114
135
 
115
136
  def serial_string_enum(
116
- *, labels: Optional[dict[str, str]] = None, deprecated: Optional[set[str]] = None
117
- ) -> Callable[[_ClassT], _ClassT]:
137
+ *, labels: dict[str, str] | None = None, deprecated: set[str] | None = None
138
+ ) -> Callable[[ClassT], ClassT]:
118
139
  """
119
140
  A decorator for enums to provide serialization data, including labels.
120
141
  """
121
142
 
122
- def decorate(orig_class: _ClassT) -> _ClassT:
143
+ def decorate(orig_class: ClassT) -> ClassT:
123
144
  cast(Any, orig_class).__unc_serial_string_enum_data = _SerialStringEnumData(
124
145
  labels=labels or {}, deprecated=deprecated or set()
125
146
  )
@@ -132,7 +153,7 @@ class SerialStringEnumInspector:
132
153
  def __init__(self, current: _SerialStringEnumData) -> None:
133
154
  self.current = current
134
155
 
135
- def get_label(self, value: str) -> Optional[str]:
156
+ def get_label(self, value: str) -> str | None:
136
157
  return self.current.labels.get(value)
137
158
 
138
159
  def get_deprecated(self, value: str) -> bool:
@@ -0,0 +1,16 @@
1
+ import typing
2
+
3
+ from .annotation import SerialInspector, get_serial_annotation
4
+ from .serial_class import get_merged_serial_class_data
5
+
6
+ T = typing.TypeVar("T")
7
+
8
+
9
+ def get_serial_data(parsed_type: type[T]) -> SerialInspector[T] | None:
10
+ serial = get_serial_annotation(parsed_type)
11
+ if serial is None:
12
+ serial = get_merged_serial_class_data(parsed_type)
13
+
14
+ if serial is not None:
15
+ return SerialInspector(parsed_type, serial)
16
+ return None
@@ -1,6 +1,8 @@
1
1
  import dataclasses
2
2
  import typing
3
3
 
4
+ from .annotation import SerialBase, SerialInspector, get_serial_annotation
5
+
4
6
  T = typing.TypeVar("T")
5
7
 
6
8
 
@@ -17,7 +19,7 @@ class IdentityHashWrapper(typing.Generic[T]):
17
19
 
18
20
 
19
21
  @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
20
- class _SerialUnion:
22
+ class _SerialUnion(SerialBase):
21
23
  """
22
24
  This class is to be kept private, to provide flexibility in registration/lookup.
23
25
  Places that need the data should access it via help classes/methods.
@@ -25,37 +27,38 @@ class _SerialUnion:
25
27
 
26
28
  # If specified, indicates the Union has a discriminator which should be used to
27
29
  # determine which type to parse.
28
- discriminator: typing.Optional[str] = None
29
- discriminator_map: typing.Optional[IdentityHashWrapper[dict[str, type]]] = None
30
+ discriminator: str | None = None
31
+ discriminator_map: IdentityHashWrapper[dict[str, type]] | None = None
30
32
 
31
33
 
32
34
  def serial_union_annotation(
33
35
  *,
34
- discriminator: typing.Optional[str] = None,
35
- discriminator_map: typing.Optional[dict[str, type]] = None,
36
+ discriminator: str | None = None,
37
+ discriminator_map: dict[str, type] | None = None,
38
+ named_type_path: str | None = None,
39
+ is_dynamic_allowed: bool = False,
36
40
  ) -> _SerialUnion:
37
41
  return _SerialUnion(
38
42
  discriminator=discriminator,
39
43
  discriminator_map=IdentityHashWrapper(discriminator_map)
40
44
  if discriminator_map is not None
41
45
  else None,
46
+ named_type_path=named_type_path,
47
+ from_decorator=True,
48
+ is_dynamic_allowed=is_dynamic_allowed,
42
49
  )
43
50
 
44
51
 
45
52
  def _get_serial_union(parsed_type: type[T]) -> _SerialUnion | None:
46
- if not hasattr(parsed_type, "__metadata__"):
47
- return None
48
- metadata = parsed_type.__metadata__ # type:ignore[attr-defined]
49
- if not isinstance(metadata, tuple) or len(metadata) != 1:
50
- return None
51
- serial = metadata[0]
53
+ serial = get_serial_annotation(parsed_type)
52
54
  if not isinstance(serial, _SerialUnion):
53
55
  return None
54
56
  return serial
55
57
 
56
58
 
57
- class SerialClassInspector(typing.Generic[T]):
59
+ class SerialClassInspector(SerialInspector[T]):
58
60
  def __init__(self, parsed_type: type[T], serial_union: _SerialUnion) -> None:
61
+ super().__init__(parsed_type, serial_union)
59
62
  self._parsed_type = parsed_type
60
63
  self._serial_union = serial_union
61
64
 
@@ -63,11 +66,11 @@ class SerialClassInspector(typing.Generic[T]):
63
66
  return typing.get_args(self._parsed_type)[0] # type:ignore[no-any-return]
64
67
 
65
68
  @property
66
- def discriminator(self) -> typing.Optional[str]:
69
+ def discriminator(self) -> str | None:
67
70
  return self._serial_union.discriminator
68
71
 
69
72
  @property
70
- def discriminator_map(self) -> typing.Optional[dict[str, type]]:
73
+ def discriminator_map(self) -> dict[str, type] | None:
71
74
  if self._serial_union.discriminator_map is None:
72
75
  return None
73
76
  return self._serial_union.discriminator_map.inner