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

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

Potentially problematic release.


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

Files changed (311) hide show
  1. UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
  2. UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
  3. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
  4. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
  5. docs/.gitignore +1 -0
  6. docs/conf.py +57 -0
  7. docs/index.md +13 -0
  8. docs/justfile +12 -0
  9. docs/quickstart.md +19 -0
  10. docs/requirements.txt +7 -0
  11. docs/static/favicons/android-chrome-192x192.png +0 -0
  12. docs/static/favicons/android-chrome-512x512.png +0 -0
  13. docs/static/favicons/apple-touch-icon.png +0 -0
  14. docs/static/favicons/browserconfig.xml +9 -0
  15. docs/static/favicons/favicon-16x16.png +0 -0
  16. docs/static/favicons/favicon-32x32.png +0 -0
  17. docs/static/favicons/manifest.json +18 -0
  18. docs/static/favicons/mstile-150x150.png +0 -0
  19. docs/static/favicons/safari-pinned-tab.svg +32 -0
  20. docs/static/logo_blue.png +0 -0
  21. examples/async_batch.py +35 -0
  22. examples/create_entity.py +22 -17
  23. examples/download_files.py +26 -0
  24. examples/edit_recipe_inputs.py +50 -0
  25. examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
  26. examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
  27. examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
  28. examples/integration-server/pyproject.toml +224 -0
  29. examples/invoke_uploader.py +26 -0
  30. examples/set_recipe_metadata_file.py +40 -0
  31. examples/set_recipe_output_file_sdk.py +26 -0
  32. examples/upload_files.py +18 -0
  33. pkgs/argument_parser/__init__.py +5 -0
  34. pkgs/argument_parser/_is_enum.py +1 -6
  35. pkgs/argument_parser/argument_parser.py +232 -76
  36. pkgs/argument_parser/case_convert.py +4 -3
  37. pkgs/filesystem_utils/__init__.py +20 -0
  38. pkgs/filesystem_utils/_blob_session.py +137 -0
  39. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  40. pkgs/filesystem_utils/_local_session.py +69 -0
  41. pkgs/filesystem_utils/_s3_session.py +117 -0
  42. pkgs/filesystem_utils/_sftp_session.py +147 -0
  43. pkgs/filesystem_utils/file_type_utils.py +91 -0
  44. pkgs/filesystem_utils/filesystem_session.py +39 -0
  45. pkgs/py.typed +0 -0
  46. pkgs/serialization/__init__.py +8 -1
  47. pkgs/serialization/annotation.py +64 -0
  48. pkgs/serialization/opaque_key.py +1 -1
  49. pkgs/serialization/serial_alias.py +47 -0
  50. pkgs/serialization/serial_class.py +65 -50
  51. pkgs/serialization/serial_generic.py +16 -0
  52. pkgs/serialization/serial_union.py +84 -0
  53. pkgs/serialization/yaml.py +57 -0
  54. pkgs/serialization_util/__init__.py +7 -7
  55. pkgs/serialization_util/_get_type_for_serialization.py +1 -3
  56. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  57. pkgs/serialization_util/dataclasses.py +14 -0
  58. pkgs/serialization_util/serialization_helpers.py +118 -73
  59. pkgs/strenum_compat/strenum_compat.py +1 -9
  60. pkgs/type_spec/actions_registry/__init__.py +0 -0
  61. pkgs/type_spec/actions_registry/__main__.py +126 -0
  62. pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
  63. pkgs/type_spec/builder.py +475 -89
  64. pkgs/type_spec/config.py +24 -19
  65. pkgs/type_spec/emit_io_ts.py +5 -2
  66. pkgs/type_spec/emit_open_api.py +266 -32
  67. pkgs/type_spec/emit_open_api_util.py +32 -13
  68. pkgs/type_spec/emit_python.py +601 -150
  69. pkgs/type_spec/emit_typescript.py +74 -273
  70. pkgs/type_spec/emit_typescript_util.py +239 -5
  71. pkgs/type_spec/load_types.py +55 -10
  72. pkgs/type_spec/open_api_util.py +30 -41
  73. pkgs/type_spec/parts/base.py.prepart +4 -3
  74. pkgs/type_spec/type_info/emit_type_info.py +178 -16
  75. pkgs/type_spec/util.py +11 -11
  76. pkgs/type_spec/value_spec/__main__.py +3 -3
  77. pkgs/type_spec/value_spec/convert_type.py +8 -1
  78. pkgs/type_spec/value_spec/emit_python.py +13 -4
  79. uncountable/__init__.py +1 -2
  80. uncountable/core/__init__.py +12 -2
  81. uncountable/core/async_batch.py +37 -0
  82. uncountable/core/client.py +293 -43
  83. uncountable/core/environment.py +41 -0
  84. uncountable/core/file_upload.py +135 -0
  85. uncountable/core/types.py +17 -0
  86. uncountable/integration/__init__.py +0 -0
  87. uncountable/integration/cli.py +49 -0
  88. uncountable/integration/construct_client.py +51 -0
  89. uncountable/integration/cron.py +29 -0
  90. uncountable/integration/db/__init__.py +0 -0
  91. uncountable/integration/db/connect.py +18 -0
  92. uncountable/integration/db/session.py +25 -0
  93. uncountable/integration/entrypoint.py +13 -0
  94. uncountable/integration/executors/__init__.py +0 -0
  95. uncountable/integration/executors/executors.py +148 -0
  96. uncountable/integration/executors/generic_upload_executor.py +284 -0
  97. uncountable/integration/executors/script_executor.py +25 -0
  98. uncountable/integration/job.py +87 -0
  99. uncountable/integration/queue_runner/__init__.py +0 -0
  100. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  101. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  102. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  103. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  104. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  105. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  108. uncountable/integration/queue_runner/command_server/types.py +52 -0
  109. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  110. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  111. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  112. uncountable/integration/queue_runner/datastore/model.py +17 -0
  113. uncountable/integration/queue_runner/job_scheduler.py +163 -0
  114. uncountable/integration/queue_runner/queue_runner.py +26 -0
  115. uncountable/integration/queue_runner/types.py +7 -0
  116. uncountable/integration/queue_runner/worker.py +119 -0
  117. uncountable/integration/scan_profiles.py +67 -0
  118. uncountable/integration/scheduler.py +150 -0
  119. uncountable/integration/secret_retrieval/__init__.py +3 -0
  120. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  121. uncountable/integration/server.py +117 -0
  122. uncountable/integration/telemetry.py +209 -0
  123. uncountable/integration/webhook_server/entrypoint.py +170 -0
  124. uncountable/types/__init__.py +151 -5
  125. uncountable/types/api/batch/execute_batch.py +15 -7
  126. uncountable/types/api/batch/execute_batch_load_async.py +42 -0
  127. uncountable/types/api/chemical/__init__.py +1 -0
  128. uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
  129. uncountable/types/api/entity/create_entities.py +23 -10
  130. uncountable/types/api/entity/create_entity.py +21 -12
  131. uncountable/types/api/entity/get_entities_data.py +19 -29
  132. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  133. uncountable/types/api/entity/list_entities.py +28 -20
  134. uncountable/types/api/entity/lock_entity.py +45 -0
  135. uncountable/types/api/entity/resolve_entity_ids.py +19 -7
  136. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  137. uncountable/types/api/entity/set_values.py +13 -28
  138. uncountable/types/api/entity/transition_entity_phase.py +80 -0
  139. uncountable/types/api/entity/unlock_entity.py +44 -0
  140. uncountable/types/api/equipment/__init__.py +1 -0
  141. uncountable/types/api/equipment/associate_equipment_input.py +44 -0
  142. uncountable/types/api/field_options/__init__.py +1 -0
  143. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  144. uncountable/types/api/files/__init__.py +1 -0
  145. uncountable/types/api/files/download_file.py +77 -0
  146. uncountable/types/api/id_source/__init__.py +1 -0
  147. uncountable/types/api/id_source/list_id_source.py +56 -0
  148. uncountable/types/api/id_source/match_id_source.py +54 -0
  149. uncountable/types/api/input_groups/get_input_group_names.py +18 -7
  150. uncountable/types/api/inputs/create_inputs.py +25 -24
  151. uncountable/types/api/inputs/get_input_data.py +37 -31
  152. uncountable/types/api/inputs/get_input_names.py +20 -9
  153. uncountable/types/api/inputs/get_inputs_data.py +33 -27
  154. uncountable/types/api/inputs/set_input_attribute_values.py +18 -13
  155. uncountable/types/api/inputs/set_input_category.py +44 -0
  156. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  157. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  158. uncountable/types/api/material_families/__init__.py +1 -0
  159. uncountable/types/api/material_families/update_entity_material_families.py +48 -0
  160. uncountable/types/api/outputs/get_output_data.py +38 -29
  161. uncountable/types/api/outputs/get_output_names.py +20 -9
  162. uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
  163. uncountable/types/api/permissions/__init__.py +1 -0
  164. uncountable/types/api/permissions/set_core_permissions.py +105 -0
  165. uncountable/types/api/project/get_projects.py +23 -19
  166. uncountable/types/api/project/get_projects_data.py +26 -43
  167. uncountable/types/api/recipe_links/__init__.py +1 -0
  168. uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
  169. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  170. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +21 -10
  171. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  172. uncountable/types/api/recipes/archive_recipes.py +42 -0
  173. uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
  174. uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
  175. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  176. uncountable/types/api/recipes/create_recipe.py +51 -0
  177. uncountable/types/api/recipes/create_recipes.py +25 -24
  178. uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
  179. uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
  180. uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
  181. uncountable/types/api/recipes/get_curve.py +13 -27
  182. uncountable/types/api/recipes/get_recipe_calculations.py +21 -21
  183. uncountable/types/api/recipes/get_recipe_links.py +14 -6
  184. uncountable/types/api/recipes/get_recipe_names.py +18 -7
  185. uncountable/types/api/recipes/get_recipe_output_metadata.py +18 -19
  186. uncountable/types/api/recipes/get_recipes_data.py +83 -144
  187. uncountable/types/api/recipes/lock_recipes.py +63 -0
  188. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  189. uncountable/types/api/recipes/set_recipe_inputs.py +21 -11
  190. uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
  191. uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
  192. uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
  193. uncountable/types/api/recipes/set_recipe_outputs.py +28 -15
  194. uncountable/types/api/recipes/set_recipe_tags.py +109 -0
  195. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  196. uncountable/types/api/recipes/unlock_recipes.py +50 -0
  197. uncountable/types/api/triggers/__init__.py +1 -0
  198. uncountable/types/api/triggers/run_trigger.py +43 -0
  199. uncountable/types/api/uploader/__init__.py +1 -0
  200. uncountable/types/api/uploader/invoke_uploader.py +47 -0
  201. uncountable/types/async_batch.py +13 -0
  202. uncountable/types/async_batch_processor.py +384 -0
  203. uncountable/types/async_batch_t.py +97 -0
  204. uncountable/types/async_jobs.py +9 -0
  205. uncountable/types/async_jobs_t.py +53 -0
  206. uncountable/types/auth_retrieval.py +12 -0
  207. uncountable/types/auth_retrieval_t.py +75 -0
  208. uncountable/types/base.py +5 -78
  209. uncountable/types/base_t.py +85 -0
  210. uncountable/types/calculations.py +8 -0
  211. uncountable/types/calculations_t.py +27 -0
  212. uncountable/types/chemical_structure.py +8 -0
  213. uncountable/types/chemical_structure_t.py +28 -0
  214. uncountable/types/client_base.py +1115 -76
  215. uncountable/types/client_config.py +8 -0
  216. uncountable/types/client_config_t.py +26 -0
  217. uncountable/types/curves.py +10 -0
  218. uncountable/types/curves_t.py +51 -0
  219. uncountable/types/entity.py +8 -266
  220. uncountable/types/entity_t.py +393 -0
  221. uncountable/types/experiment_groups.py +8 -0
  222. uncountable/types/experiment_groups_t.py +27 -0
  223. uncountable/types/field_values.py +17 -23
  224. uncountable/types/field_values_t.py +204 -0
  225. uncountable/types/fields.py +8 -0
  226. uncountable/types/fields_t.py +28 -0
  227. uncountable/types/generic_upload.py +15 -0
  228. uncountable/types/generic_upload_t.py +119 -0
  229. uncountable/types/id_source.py +12 -0
  230. uncountable/types/id_source_t.py +68 -0
  231. uncountable/types/identifier.py +11 -0
  232. uncountable/types/identifier_t.py +63 -0
  233. uncountable/types/input_attributes.py +8 -0
  234. uncountable/types/input_attributes_t.py +30 -0
  235. uncountable/types/inputs.py +11 -0
  236. uncountable/types/inputs_t.py +83 -0
  237. uncountable/types/integration_server.py +9 -0
  238. uncountable/types/integration_server_t.py +42 -0
  239. uncountable/types/job_definition.py +27 -0
  240. uncountable/types/job_definition_t.py +260 -0
  241. uncountable/types/outputs.py +8 -0
  242. uncountable/types/outputs_t.py +30 -0
  243. uncountable/types/overrides.py +10 -0
  244. uncountable/types/overrides_t.py +49 -0
  245. uncountable/types/permissions.py +8 -0
  246. uncountable/types/permissions_t.py +46 -0
  247. uncountable/types/phases.py +8 -0
  248. uncountable/types/phases_t.py +27 -0
  249. uncountable/types/post_base.py +8 -0
  250. uncountable/types/post_base_t.py +30 -0
  251. uncountable/types/queued_job.py +16 -0
  252. uncountable/types/queued_job_t.py +123 -0
  253. uncountable/types/recipe_identifiers.py +12 -0
  254. uncountable/types/recipe_identifiers_t.py +76 -0
  255. uncountable/types/recipe_inputs.py +9 -0
  256. uncountable/types/recipe_inputs_t.py +30 -0
  257. uncountable/types/recipe_links.py +4 -44
  258. uncountable/types/recipe_links_t.py +54 -0
  259. uncountable/types/recipe_metadata.py +10 -0
  260. uncountable/types/recipe_metadata_t.py +58 -0
  261. uncountable/types/recipe_output_metadata.py +8 -0
  262. uncountable/types/recipe_output_metadata_t.py +28 -0
  263. uncountable/types/recipe_tags.py +8 -0
  264. uncountable/types/recipe_tags_t.py +27 -0
  265. uncountable/types/recipe_workflow_steps.py +14 -0
  266. uncountable/types/recipe_workflow_steps_t.py +95 -0
  267. uncountable/types/recipes.py +8 -0
  268. uncountable/types/recipes_t.py +25 -0
  269. uncountable/types/response.py +8 -0
  270. uncountable/types/response_t.py +26 -0
  271. uncountable/types/secret_retrieval.py +12 -0
  272. uncountable/types/secret_retrieval_t.py +75 -0
  273. uncountable/types/units.py +8 -0
  274. uncountable/types/units_t.py +27 -0
  275. uncountable/types/users.py +8 -0
  276. uncountable/types/users_t.py +28 -0
  277. uncountable/types/webhook_job.py +9 -0
  278. uncountable/types/webhook_job_t.py +37 -0
  279. uncountable/types/workflows.py +9 -0
  280. uncountable/types/workflows_t.py +39 -0
  281. UncountablePythonSDK-0.0.7.dist-info/METADATA +0 -27
  282. UncountablePythonSDK-0.0.7.dist-info/RECORD +0 -119
  283. examples/recipe-import/importer.py +0 -39
  284. type_spec/external/api/batch/execute_batch.yaml +0 -56
  285. type_spec/external/api/entity/create_entities.yaml +0 -33
  286. type_spec/external/api/entity/create_entity.yaml +0 -39
  287. type_spec/external/api/entity/get_entities_data.yaml +0 -55
  288. type_spec/external/api/entity/list_entities.yaml +0 -62
  289. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  290. type_spec/external/api/entity/set_values.yaml +0 -45
  291. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  292. type_spec/external/api/inputs/create_inputs.yaml +0 -61
  293. type_spec/external/api/inputs/get_input_data.yaml +0 -108
  294. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  295. type_spec/external/api/inputs/get_inputs_data.yaml +0 -95
  296. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -37
  297. type_spec/external/api/outputs/get_output_data.yaml +0 -103
  298. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  299. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  300. type_spec/external/api/project/get_projects.yaml +0 -52
  301. type_spec/external/api/project/get_projects_data.yaml +0 -86
  302. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  303. type_spec/external/api/recipes/create_recipes.yaml +0 -60
  304. type_spec/external/api/recipes/get_curve.yaml +0 -50
  305. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -49
  306. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  307. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  308. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -49
  309. type_spec/external/api/recipes/get_recipes_data.yaml +0 -372
  310. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
  311. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -56
@@ -1,16 +1,24 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
4
+ import math
2
5
  import types
3
6
  import typing
4
7
  from collections import defaultdict
5
- from dataclasses import MISSING, dataclass
6
8
  from datetime import date, datetime
7
9
  from decimal import Decimal
10
+ from enum import Enum, auto
8
11
  from importlib import resources
9
12
 
10
13
  import dateutil.parser
11
- import yaml
12
14
 
13
- from pkgs.serialization import MissingSentryType, OpaqueKey, get_serial_class_data
15
+ from pkgs.serialization import (
16
+ MissingSentryType,
17
+ OpaqueKey,
18
+ get_serial_class_data,
19
+ get_serial_union_data,
20
+ yaml,
21
+ )
14
22
 
15
23
  from ._is_enum import is_string_enum_class
16
24
  from ._is_namedtuple import is_namedtuple_type
@@ -18,38 +26,78 @@ from .case_convert import camel_to_snake_case, snake_to_camel_case
18
26
 
19
27
  T = typing.TypeVar("T")
20
28
  ParserFunction = typing.Callable[[typing.Any], T]
21
- ParserCache = dict[typing.Type[typing.Any], ParserFunction[typing.Any]]
29
+ ParserCache = dict[type[typing.Any], ParserFunction[typing.Any]]
30
+
22
31
 
32
+ class SourceEncoding(Enum):
33
+ API = auto()
34
+ STORAGE = auto()
23
35
 
24
- @dataclass(frozen=True, eq=True)
36
+
37
+ @dataclasses.dataclass(frozen=True, eq=True)
25
38
  class ParserOptions:
26
- convert_to_snake_case: bool
39
+ encoding: SourceEncoding
40
+ strict_property_parsing: bool = False
41
+
42
+ @staticmethod
43
+ def Api(*, strict_property_parsing: bool = False) -> ParserOptions:
44
+ return ParserOptions(
45
+ encoding=SourceEncoding.API, strict_property_parsing=strict_property_parsing
46
+ )
47
+
48
+ @staticmethod
49
+ def Storage(*, strict_property_parsing: bool = False) -> ParserOptions:
50
+ return ParserOptions(
51
+ encoding=SourceEncoding.STORAGE,
52
+ strict_property_parsing=strict_property_parsing,
53
+ )
27
54
 
55
+ @property
56
+ def from_camel_case(self) -> bool:
57
+ return self.encoding == SourceEncoding.API
28
58
 
29
- @dataclass(frozen=True)
59
+ @property
60
+ def allow_direct_dataclass(self) -> bool:
61
+ return self.encoding == SourceEncoding.STORAGE
62
+
63
+
64
+ @dataclasses.dataclass(frozen=True)
30
65
  class ParserContext:
31
66
  options: ParserOptions
32
67
  cache: ParserCache
33
68
 
34
69
 
70
+ class ParserError(BaseException): ...
71
+
72
+
73
+ class ParserExtraFieldsError(ParserError):
74
+ extra_fields: set[str]
75
+
76
+ def __init__(self, extra_fields: set[str]) -> None:
77
+ self.extra_fields = extra_fields
78
+
79
+ def __str__(self) -> str:
80
+ return f"extra fields were provided: {', '.join(self.extra_fields)}"
81
+
82
+
83
+ def is_union(field_type: typing.Any) -> bool:
84
+ origin = typing.get_origin(field_type)
85
+ return origin is typing.Union or origin is types.UnionType
86
+
87
+
35
88
  def is_optional(field_type: typing.Any) -> bool:
36
- return typing.get_origin(field_type) is typing.Union and type(
37
- None
38
- ) in typing.get_args(field_type)
89
+ return is_union(field_type) and type(None) in typing.get_args(field_type)
39
90
 
40
91
 
41
92
  def is_missing(field_type: typing.Any) -> bool:
42
- origin = typing.get_origin(field_type)
43
- if origin is not typing.Union:
93
+ if not is_union(field_type):
44
94
  return False
45
95
  args = typing.get_args(field_type)
46
- if len(args) == 0 or args[0] is not MissingSentryType:
47
- return False
48
- return True
96
+ return not (len(args) == 0 or args[0] is not MissingSentryType)
49
97
 
50
98
 
51
99
  def _invoke_tuple_parsers(
52
- tuple_type: typing.Type[T],
100
+ tuple_type: type[T],
53
101
  arg_parsers: typing.Sequence[typing.Callable[[typing.Any], object]],
54
102
  has_ellipsis: bool,
55
103
  value: typing.Any,
@@ -68,22 +116,25 @@ def _invoke_tuple_parsers(
68
116
 
69
117
 
70
118
  def _invoke_fallback_parsers(
71
- original_type: typing.Type[T],
119
+ original_type: type[T],
72
120
  arg_parsers: typing.Sequence[typing.Callable[[typing.Any], T]],
73
121
  value: typing.Any,
74
122
  ) -> T:
123
+ exceptions = []
124
+
75
125
  for parser in arg_parsers:
76
126
  try:
77
127
  return parser(value)
78
- except Exception:
128
+ except Exception as e:
129
+ exceptions.append(e)
79
130
  continue
80
131
  raise ValueError(
81
132
  f"Unhandled value {value} cannot be cast to a member of {original_type}"
82
- )
133
+ ) from ExceptionGroup("Fallback Parser Exception", exceptions)
83
134
 
84
135
 
85
136
  def _invoke_membership_parser(
86
- expected_values: typing.Set[T],
137
+ expected_values: set[T],
87
138
  value: typing.Any,
88
139
  ) -> T:
89
140
  """
@@ -97,24 +148,54 @@ def _invoke_membership_parser(
97
148
  raise ValueError(f"Expected value from {expected_values} but got value {value}")
98
149
 
99
150
 
151
+ def _build_parser_discriminated_union(
152
+ context: ParserContext,
153
+ discriminator: str,
154
+ discriminator_map: dict[str, ParserFunction[T]],
155
+ ) -> ParserFunction[T]:
156
+ def parse(value: typing.Any) -> typing.Any:
157
+ if context.options.allow_direct_dataclass and dataclasses.is_dataclass(value):
158
+ discriminant = getattr(value, discriminator)
159
+ else:
160
+ discriminant = value.get(discriminator)
161
+ if discriminant is None:
162
+ raise ValueError("missing-union-discriminant")
163
+ if not isinstance(discriminant, str):
164
+ raise ValueError("union-discriminant-is-not-string")
165
+ parser = discriminator_map.get(discriminant)
166
+ if parser is None:
167
+ raise ValueError("missing-type-for-union-discriminant", discriminant)
168
+ return parser(value)
169
+
170
+ return parse
171
+
172
+
100
173
  def _build_parser_inner(
101
- parsed_type: typing.Type[T],
174
+ parsed_type: type[T],
102
175
  context: ParserContext,
103
- *,
104
- convert_string_to_snake_case: bool = False,
105
176
  ) -> ParserFunction[T]:
106
177
  """
107
- convert_to_snake_case - internal flag
108
- if convert_to_snake_case is True, and parsed_type is str,
109
- then the generated parser will convert camel to snake case case
110
- should only be True for cases like dictionary keys
111
- should only be True if options.convert_to_snake_case is True
112
-
113
- NOTE: This argument makes caching at this level difficult, as the cache-map
114
- would need to vary based on this argument. For this reason only dataclasses
115
- are cached now, as they don't use the argument, and they're known to be safe.
116
- This is also enough to support some recursion.
178
+ IMPROVE: We can now cache at this level, to avoid producing redundant
179
+ internal parsers.
117
180
  """
181
+
182
+ serial_union = get_serial_union_data(parsed_type)
183
+ if serial_union is not None:
184
+ discriminator = serial_union.discriminator
185
+ discriminator_map = serial_union.discriminator_map
186
+ if discriminator is None or discriminator_map is None:
187
+ # fallback to standard union parsing
188
+ parsed_type = serial_union.get_union_underlying()
189
+ else:
190
+ return _build_parser_discriminated_union(
191
+ context,
192
+ discriminator,
193
+ {
194
+ key: _build_parser_inner(value, context)
195
+ for key, value in discriminator_map.items()
196
+ },
197
+ )
198
+
118
199
  if dataclasses.is_dataclass(parsed_type):
119
200
  return _build_parser_dataclass(parsed_type, context) # type: ignore[arg-type]
120
201
 
@@ -129,7 +210,7 @@ def _build_parser_inner(
129
210
  field_name: field_parser(
130
211
  value.get(
131
212
  snake_to_camel_case(field_name)
132
- if context.options.convert_to_snake_case
213
+ if context.options.from_camel_case
133
214
  else field_name
134
215
  )
135
216
  )
@@ -142,7 +223,7 @@ def _build_parser_inner(
142
223
  origin = typing.get_origin(parsed_type)
143
224
  if origin is tuple:
144
225
  args = typing.get_args(parsed_type)
145
- element_parsers: typing.List[typing.Callable[[typing.Any], object]] = []
226
+ element_parsers: list[typing.Callable[[typing.Any], object]] = []
146
227
  has_ellipsis = False
147
228
  for arg in args:
148
229
  assert not has_ellipsis
@@ -164,7 +245,7 @@ def _build_parser_inner(
164
245
  arg_parsers = [_build_parser_inner(arg, context) for arg in sorted_args]
165
246
  return lambda value: _invoke_fallback_parsers(parsed_type, arg_parsers, value)
166
247
 
167
- if parsed_type is typing.Any:
248
+ if parsed_type is typing.Any: # type: ignore[comparison-overlap]
168
249
  return lambda value: value
169
250
 
170
251
  if origin in (list, set):
@@ -173,10 +254,16 @@ def _build_parser_inner(
173
254
  raise ValueError("List types only support one argument")
174
255
  arg_parser = _build_parser_inner(args[0], context)
175
256
 
257
+ def parse_element(value: typing.Any) -> typing.Any:
258
+ try:
259
+ return arg_parser(value)
260
+ except Exception as e:
261
+ raise ValueError("Failed to parse element", value) from e
262
+
176
263
  def parse(value: typing.Any) -> typing.Any:
177
264
  if not isinstance(value, list):
178
265
  raise ValueError("value is not a list", parsed_type)
179
- return origin(arg_parser(x) for x in value)
266
+ return origin(parse_element(x) for x in value)
180
267
 
181
268
  return parse
182
269
 
@@ -184,36 +271,47 @@ def _build_parser_inner(
184
271
  args = typing.get_args(parsed_type)
185
272
  if len(args) != 2:
186
273
  raise ValueError("Dict types only support two arguments for now")
187
- k_parser = _build_parser_inner(
274
+ k_inner_parser = _build_parser_inner(
188
275
  args[0],
189
276
  context,
190
- convert_string_to_snake_case=context.options.convert_to_snake_case,
191
277
  )
278
+
279
+ def key_parser(value: typing.Any) -> object:
280
+ inner = k_inner_parser(value)
281
+ if (
282
+ isinstance(inner, str)
283
+ # enum keys and OpaqueData's would also have string value types,
284
+ # but their explicit type is not a string, thus shouldn't be converted
285
+ and args[0] is str
286
+ and context.options.from_camel_case
287
+ ):
288
+ return camel_to_snake_case(value)
289
+ return inner
290
+
192
291
  v_parser = _build_parser_inner(args[1], context)
193
- return lambda value: origin((k_parser(k), v_parser(v)) for k, v in value.items())
292
+ return lambda value: origin(
293
+ (key_parser(k), v_parser(v)) for k, v in value.items()
294
+ )
194
295
 
195
296
  if origin == typing.Literal:
196
- valid_values: typing.Set[T] = set(typing.get_args(parsed_type))
297
+ valid_values: set[T] = set(typing.get_args(parsed_type))
197
298
  return lambda value: _invoke_membership_parser(valid_values, value)
198
299
 
199
- if parsed_type is str and convert_string_to_snake_case:
200
- return lambda value: camel_to_snake_case(value) # type: ignore
201
-
202
300
  if parsed_type is int:
203
301
  # first parse ints to decimal to allow scientific notation and decimals
204
302
  # e.g. (1) 1e4 => 1000, (2) 3.0 => 3
205
303
 
206
304
  def parse_int(value: typing.Any) -> T:
207
305
  if isinstance(value, str):
208
- assert (
209
- "_" not in value
210
- ), "numbers with underscores not considered integers"
306
+ assert "_" not in value, (
307
+ "numbers with underscores not considered integers"
308
+ )
211
309
 
212
310
  dec_value = Decimal(value)
213
311
  int_value = int(dec_value)
214
- assert (
215
- int_value == dec_value
216
- ), f"value ({value}) cannot be parsed to int without discarding precision"
312
+ assert int_value == dec_value, (
313
+ f"value ({value}) cannot be parsed to int without discarding precision"
314
+ )
217
315
  return int_value # type: ignore
218
316
 
219
317
  return parse_int
@@ -237,7 +335,18 @@ def _build_parser_inner(
237
335
 
238
336
  return parse_str
239
337
 
240
- if parsed_type in (float, dict, bool, Decimal) or is_string_enum_class(parsed_type):
338
+ if parsed_type in (float, Decimal):
339
+
340
+ def parse_as_numeric_type(value: typing.Any) -> T:
341
+ numeric_value: Decimal | float = parsed_type(value) # type: ignore
342
+ if math.isnan(numeric_value):
343
+ raise ValueError(f"Invalid numeric value: {numeric_value}")
344
+
345
+ return numeric_value # type: ignore
346
+
347
+ return parse_as_numeric_type
348
+
349
+ if parsed_type in (dict, bool) or is_string_enum_class(parsed_type):
241
350
  return lambda value: parsed_type(value) # type: ignore
242
351
 
243
352
  if parsed_type is MissingSentryType:
@@ -246,11 +355,21 @@ def _build_parser_inner(
246
355
  raise ValueError("Missing type cannot be parsed directly")
247
356
 
248
357
  return error
358
+
359
+ # Check last for generic annotated types and process them unwrapped
360
+ # this must be last, since some of the expected types, like Unions,
361
+ # will also be annotated, but have a special form
362
+ if typing.get_origin(parsed_type) is typing.Annotated:
363
+ return _build_parser_inner(
364
+ parsed_type.__origin__, # type: ignore[attr-defined]
365
+ context,
366
+ )
367
+
249
368
  raise ValueError(f"Unhandled type {parsed_type}")
250
369
 
251
370
 
252
371
  def _build_parser_dataclass(
253
- parsed_type: typing.Type[T],
372
+ parsed_type: type[T],
254
373
  context: ParserContext,
255
374
  ) -> ParserFunction[T]:
256
375
  """
@@ -264,36 +383,45 @@ def _build_parser_dataclass(
264
383
 
265
384
  type_hints = typing.get_type_hints(parsed_type)
266
385
  dc_field_parsers: list[
267
- typing.Tuple[
386
+ tuple[
268
387
  dataclasses.Field[typing.Any],
269
- typing.Type[typing.Any],
388
+ type[typing.Any],
270
389
  ParserFunction[typing.Any],
271
390
  ]
272
391
  ] = []
273
392
 
274
393
  serial_class_data = get_serial_class_data(parsed_type)
275
394
 
395
+ def resolve_serialized_field_name(*, field_name: str) -> str:
396
+ return (
397
+ snake_to_camel_case(field_name)
398
+ if (
399
+ context.options.from_camel_case
400
+ and not serial_class_data.has_unconverted_key(field_name)
401
+ )
402
+ else field_name
403
+ )
404
+
276
405
  def parse(value: typing.Any) -> typing.Any:
277
- data: typing.Dict[typing.Any, typing.Any] = {}
406
+ # Use an exact type match to prevent base/derived class mismatches
407
+ if context.options.allow_direct_dataclass and type(value) is parsed_type:
408
+ return value
409
+
410
+ data: dict[typing.Any, typing.Any] = {}
278
411
  for field, field_type, field_parser in dc_field_parsers:
279
412
  field_raw_value = None
280
413
  try:
281
414
  field_raw_value = value.get(
282
- snake_to_camel_case(field.name)
283
- if (
284
- context.options.convert_to_snake_case
285
- and not serial_class_data.has_unconverted_key(field.name)
286
- )
287
- else field.name,
288
- MISSING,
415
+ resolve_serialized_field_name(field_name=field.name),
416
+ dataclasses.MISSING,
289
417
  )
290
418
  field_value: typing.Any
291
- if field_raw_value == MISSING:
419
+ if field_raw_value == dataclasses.MISSING:
292
420
  if serial_class_data.has_parse_require(field.name):
293
421
  raise ValueError("missing-required-field", field.name)
294
- if field.default != MISSING:
422
+ if field.default != dataclasses.MISSING:
295
423
  field_value = field.default
296
- elif field.default_factory != MISSING:
424
+ elif field.default_factory != dataclasses.MISSING:
297
425
  field_value = field.default_factory()
298
426
  elif is_missing(field_type):
299
427
  field_value = MissingSentryType()
@@ -315,9 +443,21 @@ def _build_parser_dataclass(
315
443
 
316
444
  except Exception as e:
317
445
  raise ValueError(
318
- f"unable to parse field:{field.name}", field_raw_value
446
+ f"unable-to-parse-field:{field.name}", field_raw_value
319
447
  ) from e
320
448
 
449
+ if context.options.strict_property_parsing:
450
+ all_allowed_field_names = set(
451
+ resolve_serialized_field_name(field_name=field.name)
452
+ for (field, _, _) in dc_field_parsers
453
+ )
454
+ passed_field_names = set(value.keys())
455
+ disallowed_field_names = passed_field_names.difference(
456
+ all_allowed_field_names
457
+ )
458
+ if len(disallowed_field_names) > 0:
459
+ raise ParserExtraFieldsError(disallowed_field_names)
460
+
321
461
  return parsed_type(**data)
322
462
 
323
463
  # Add to cache before building inner types, to support recursion
@@ -340,7 +480,7 @@ _CACHE_MAP: dict[ParserOptions, ParserCache] = defaultdict(ParserCache)
340
480
 
341
481
 
342
482
  def build_parser(
343
- parsed_type: typing.Type[T],
483
+ parsed_type: type[T],
344
484
  options: ParserOptions,
345
485
  ) -> ParserFunction[T]:
346
486
  """
@@ -364,11 +504,27 @@ def build_parser(
364
504
  class CachedParser(typing.Generic[T]):
365
505
  def __init__(
366
506
  self,
367
- args: typing.Type[T],
507
+ args: type[T],
508
+ strict_property_parsing: bool = False,
368
509
  ):
369
510
  self.arguments = args
370
- self.parser_api: typing.Optional[ParserFunction[T]] = None
371
- self.parser_storage: typing.Optional[ParserFunction[T]] = None
511
+ self.parser_api: ParserFunction[T] | None = None
512
+ self.parser_storage: ParserFunction[T] | None = None
513
+ self.strict_property_parsing = strict_property_parsing
514
+
515
+ def parse_from_encoding(
516
+ self,
517
+ args: typing.Any,
518
+ *,
519
+ source_encoding: SourceEncoding,
520
+ ) -> T:
521
+ match source_encoding:
522
+ case SourceEncoding.API:
523
+ return self.parse_api(args)
524
+ case SourceEncoding.STORAGE:
525
+ return self.parse_storage(args)
526
+ case _:
527
+ typing.assert_never(source_encoding)
372
528
 
373
529
  def parse_api(self, args: typing.Any) -> T:
374
530
  """
@@ -381,8 +537,8 @@ class CachedParser(typing.Generic[T]):
381
537
  if self.parser_api is None:
382
538
  self.parser_api = build_parser(
383
539
  self.arguments,
384
- ParserOptions(
385
- convert_to_snake_case=True,
540
+ ParserOptions.Api(
541
+ strict_property_parsing=self.strict_property_parsing,
386
542
  ),
387
543
  )
388
544
  assert self.parser_api is not None
@@ -395,8 +551,8 @@ class CachedParser(typing.Generic[T]):
395
551
  if self.parser_storage is None:
396
552
  self.parser_storage = build_parser(
397
553
  self.arguments,
398
- ParserOptions(
399
- convert_to_snake_case=False,
554
+ ParserOptions.Storage(
555
+ strict_property_parsing=self.strict_property_parsing,
400
556
  ),
401
557
  )
402
558
  assert self.parser_storage is not None
@@ -407,5 +563,5 @@ class CachedParser(typing.Generic[T]):
407
563
  return self.parse_storage(yaml.safe_load(data_in))
408
564
 
409
565
  def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
410
- raw_data = resources.read_text(package, resource)
411
- return self.parse_storage(yaml.safe_load(raw_data))
566
+ with resources.open_text(package, resource) as fp:
567
+ return self.parse_storage(yaml.safe_load(fp))
@@ -4,9 +4,10 @@ import re
4
4
 
5
5
  @functools.lru_cache(maxsize=500000)
6
6
  def snake_to_camel_case(o: str) -> str:
7
- return "".join([
8
- part.title() if i > 0 else part for i, part in enumerate(o.split("_"))
9
- ])
7
+ tokens = o.split("_")
8
+ if len(tokens) < 2:
9
+ return o
10
+ return "".join([part.title() if i > 0 else part for i, part in enumerate(tokens)])
10
11
 
11
12
 
12
13
  def kebab_to_pascal_case(o: str) -> str:
@@ -0,0 +1,20 @@
1
+ from ._gdrive_session import GDriveSession as GDriveSession
2
+ from ._gdrive_session import delete_gdrive_file as delete_gdrive_file
3
+ from ._gdrive_session import download_gdrive_file as download_gdrive_file
4
+ from ._gdrive_session import list_gdrive_files as list_gdrive_files
5
+ from ._gdrive_session import move_gdrive_file as move_gdrive_file
6
+ from ._gdrive_session import upload_file_gdrive as upload_file_gdrive
7
+ from ._local_session import LocalSession as LocalSession
8
+ from ._s3_session import S3Session as S3Session
9
+ from ._sftp_session import SFTPSession as SFTPSession
10
+ from ._sftp_session import list_sftp_files as list_sftp_files
11
+ from ._sftp_session import move_sftp_files as move_sftp_files
12
+ from .file_type_utils import FileObjectData as FileObjectData
13
+ from .file_type_utils import FileSystemFileReference as FileSystemFileReference
14
+ from .file_type_utils import FileSystemObject as FileSystemObject
15
+ from .file_type_utils import FileSystemS3Config as FileSystemS3Config
16
+ from .file_type_utils import FileSystemSFTPConfig as FileSystemSFTPConfig
17
+ from .file_type_utils import FileTransfer as FileTransfer
18
+ from .file_type_utils import IncompatibleFileReference as IncompatibleFileReference
19
+ from .file_type_utils import RemoteObjectReference as RemoteObjectReference
20
+ from .filesystem_session import FileSystemSession as FileSystemSession
@@ -0,0 +1,137 @@
1
+ from io import BytesIO
2
+
3
+ from azure.storage.blob import BlobServiceClient, ContainerClient
4
+
5
+ from pkgs.filesystem_utils.file_type_utils import (
6
+ FileObjectData,
7
+ FileSystemBlobConfig,
8
+ FileSystemFileReference,
9
+ FileSystemObject,
10
+ FileTransfer,
11
+ IncompatibleFileReference,
12
+ )
13
+
14
+ from .filesystem_session import FileSystemSession
15
+
16
+
17
+ def _add_slash(prefix: str) -> str:
18
+ if len(prefix) > 0 and prefix[-1] != "/":
19
+ prefix = prefix + "/"
20
+ return prefix
21
+
22
+
23
+ class BlobSession(FileSystemSession):
24
+ config: FileSystemBlobConfig
25
+
26
+ def __init__(self, blob_config: FileSystemBlobConfig) -> None:
27
+ super().__init__()
28
+ self.config = blob_config
29
+
30
+ def start(self) -> None:
31
+ self.service_client: BlobServiceClient | None = BlobServiceClient(
32
+ self.config.account_url, credential=self.config.credential
33
+ )
34
+ self.container_client: ContainerClient | None = (
35
+ self.service_client.get_container_client(self.config.container)
36
+ )
37
+
38
+ def __enter__(self) -> "BlobSession":
39
+ self.start()
40
+ return self
41
+
42
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
43
+ self.service_client = None
44
+ self.container_client = None
45
+
46
+ def list_files(
47
+ self,
48
+ dir_path: FileSystemObject,
49
+ *,
50
+ recursive: bool = False,
51
+ valid_extensions: list[str] | None = None,
52
+ ) -> list[FileSystemObject]:
53
+ if not isinstance(dir_path, FileSystemFileReference):
54
+ raise IncompatibleFileReference()
55
+
56
+ assert self.service_client is not None and self.container_client is not None, (
57
+ "call to list_files on uninitialized blob session"
58
+ )
59
+
60
+ filesystem_file_references: list[FileSystemObject] = []
61
+ prefix = _add_slash(dir_path.filepath)
62
+ for blob in self.container_client.list_blobs(name_starts_with=prefix):
63
+ if not recursive and (
64
+ blob.name == prefix or "/" in blob.name[len(prefix) :]
65
+ ):
66
+ continue
67
+ if valid_extensions is None or any(
68
+ blob.name.endswith(valid_extension)
69
+ for valid_extension in valid_extensions
70
+ ):
71
+ filesystem_file_references.append(
72
+ FileSystemFileReference(
73
+ filepath=blob.name,
74
+ )
75
+ )
76
+
77
+ return filesystem_file_references
78
+
79
+ def download_files(
80
+ self,
81
+ filepaths: list[FileSystemObject],
82
+ ) -> list[FileObjectData]:
83
+ downloaded_files: list[FileObjectData] = []
84
+ assert self.service_client is not None and self.container_client is not None, (
85
+ "call to download_files on uninitialized blob session"
86
+ )
87
+
88
+ for file_object in filepaths:
89
+ if (
90
+ not isinstance(file_object, FileSystemFileReference)
91
+ or file_object.filename is None
92
+ ):
93
+ raise IncompatibleFileReference()
94
+
95
+ blob_client = self.container_client.get_blob_client(file_object.filepath)
96
+ download_stream = blob_client.download_blob()
97
+ file_data = download_stream.readall()
98
+ downloaded_files.append(
99
+ FileObjectData(
100
+ file_data=file_data,
101
+ file_IO=BytesIO(file_data),
102
+ filename=file_object.filename,
103
+ filepath=file_object.filepath,
104
+ )
105
+ )
106
+
107
+ return downloaded_files
108
+
109
+ def move_files(self, file_mappings: list[FileTransfer]) -> None:
110
+ assert self.service_client is not None and self.container_client is not None, (
111
+ "call to move_files on uninitialized blob session"
112
+ )
113
+
114
+ for src_file, dest_file in file_mappings:
115
+ if not isinstance(src_file, FileSystemFileReference) or not isinstance(
116
+ dest_file, FileSystemFileReference
117
+ ):
118
+ raise IncompatibleFileReference()
119
+
120
+ source_blob_client = self.container_client.get_blob_client(
121
+ src_file.filepath
122
+ )
123
+ dest_blob_client = self.container_client.get_blob_client(dest_file.filepath)
124
+
125
+ dest_blob_client.start_copy_from_url(source_blob_client.url)
126
+ source_blob_client.delete_blob()
127
+
128
+ def delete_files(self, filepaths: list[FileSystemObject]) -> None:
129
+ assert self.service_client is not None and self.container_client is not None, (
130
+ "call to delete_files on uninitialized blob session"
131
+ )
132
+ for file_object in filepaths:
133
+ if not isinstance(file_object, FileSystemFileReference):
134
+ raise IncompatibleFileReference()
135
+
136
+ blob_client = self.container_client.get_blob_client(file_object.filepath)
137
+ blob_client.delete_blob()