UncountablePythonSDK 0.0.188__py3-none-any.whl → 0.0.190__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.
Files changed (77) hide show
  1. docs/conf.py +3 -1
  2. docs/integration_examples/add_file_to_folder.md +13 -8
  3. examples/integration-server/jobs/materials_auto/example_scale_connection.py +1 -1
  4. examples/stream_dataset_measurements.py +3 -2
  5. pkgs/argument_parser/__init__.py +4 -0
  6. pkgs/argument_parser/field_name_resolution.py +35 -0
  7. pkgs/argument_parser/parser_inner.py +85 -67
  8. pkgs/filesystem_utils/_gdrive_session.py +1 -1
  9. pkgs/filesystem_utils/_sftp_connection.py +2 -0
  10. pkgs/filesystem_utils/file_type_utils.py +1 -2
  11. pkgs/serialization/__init__.py +3 -0
  12. pkgs/serialization/missing_sentry.py +2 -2
  13. pkgs/serialization/serial_union.py +20 -0
  14. pkgs/serialization/yaml.py +55 -4
  15. pkgs/serialization_util/__init__.py +4 -0
  16. pkgs/serialization_util/reuse_containers.py +88 -0
  17. pkgs/serialization_util/serialization_helpers.py +81 -32
  18. pkgs/serialization_util/serialize_for_api_encoding.py +70 -0
  19. pkgs/serialization_util/source_encoding_serializer.py +17 -0
  20. pkgs/type_spec/AGENTS.md +13 -0
  21. pkgs/type_spec/CLAUDE.md +5 -0
  22. pkgs/type_spec/builder.py +69 -19
  23. pkgs/type_spec/emit_open_api.py +4 -8
  24. pkgs/type_spec/emit_python.py +138 -18
  25. pkgs/type_spec/emit_typescript.py +17 -2
  26. pkgs/type_spec/emit_typescript_util.py +54 -11
  27. pkgs/type_spec/parts/base.py.prepart +11 -11
  28. pkgs/type_spec/parts/base.ts.prepart +6 -0
  29. pkgs/type_spec/type_info/emit_type_info.py +11 -15
  30. pkgs/type_spec/util.py +2 -2
  31. pkgs/type_spec/value_spec/__main__.py +1 -1
  32. pkgs/type_spec/value_spec/emit_python.py +7 -3
  33. uncountable/core/__init__.py +2 -1
  34. uncountable/core/client.py +8 -1
  35. uncountable/core/environment.py +4 -0
  36. uncountable/core/query/column.py +10 -1
  37. uncountable/core/types.py +6 -1
  38. uncountable/integration/db/session.py +2 -2
  39. uncountable/integration/executors/executors.py +2 -2
  40. uncountable/integration/queue_runner/command_server/command_client.py +1 -1
  41. uncountable/integration/queue_runner/command_server/command_server.py +1 -1
  42. uncountable/integration/queue_runner/job_scheduler.py +14 -3
  43. uncountable/integration/scheduler.py +19 -6
  44. uncountable/integration/telemetry.py +38 -13
  45. uncountable/integration/webhook_server/gunicorn_config.py +9 -0
  46. uncountable/types/api/file_folders/add_file_to_folder.py +1 -1
  47. uncountable/types/api/file_folders/modify_file_system.py +51 -2
  48. uncountable/types/api/files/download_file.py +17 -2
  49. uncountable/types/api/inputs/create_inputs.py +2 -2
  50. uncountable/types/api/material_families/update_entity_material_families.py +1 -1
  51. uncountable/types/api/ml/submit_async_job.py +3 -3
  52. uncountable/types/api/notebooks/add_notebook_content.py +2 -2
  53. uncountable/types/api/permissions/set_entity_permission.py +17 -2
  54. uncountable/types/api/recipe_links/create_recipe_link.py +1 -1
  55. uncountable/types/api/recipe_links/create_recipe_links.py +1 -1
  56. uncountable/types/api/recipe_links/remove_recipe_link.py +1 -1
  57. uncountable/types/api/recipes/edit_recipe_inputs.py +3 -3
  58. uncountable/types/api/recipes/upsert_step_relationships.py +1 -1
  59. uncountable/types/async_batch_processor.py +37 -0
  60. uncountable/types/async_batch_t.py +1 -0
  61. uncountable/types/base_t.py +11 -11
  62. uncountable/types/client_base.py +8 -8
  63. uncountable/types/curves_t.py +2 -2
  64. uncountable/types/dataset_measurements_t.py +1 -1
  65. uncountable/types/entity_t.py +122 -102
  66. uncountable/types/field_values.py +1 -0
  67. uncountable/types/field_values_t.py +15 -1
  68. uncountable/types/integration_session_t.py +1 -1
  69. uncountable/types/integrations_t.py +1 -1
  70. uncountable/types/listing.py +3 -0
  71. uncountable/types/listing_t.py +39 -3
  72. uncountable/types/recipe_workflow_step_types_t.py +1 -1
  73. uncountable/types/sockets_t.py +2 -2
  74. {uncountablepythonsdk-0.0.188.dist-info → uncountablepythonsdk-0.0.190.dist-info}/METADATA +2 -1
  75. {uncountablepythonsdk-0.0.188.dist-info → uncountablepythonsdk-0.0.190.dist-info}/RECORD +77 -70
  76. {uncountablepythonsdk-0.0.188.dist-info → uncountablepythonsdk-0.0.190.dist-info}/WHEEL +0 -0
  77. {uncountablepythonsdk-0.0.188.dist-info → uncountablepythonsdk-0.0.190.dist-info}/top_level.txt +0 -0
docs/conf.py CHANGED
@@ -16,7 +16,9 @@ from sphinx.application import Sphinx # type: ignore[import-not-found]
16
16
  from sphinx.environment import BuildEnvironment # type: ignore[import-not-found]
17
17
 
18
18
  project = "Uncountable SDK"
19
- copyright = f"{datetime.datetime.now(tz=datetime.UTC).date().year}, Uncountable Inc"
19
+ project_copyright = (
20
+ f"{datetime.datetime.now(tz=datetime.UTC).date().year}, Uncountable Inc"
21
+ )
20
22
  author = "Uncountable Inc"
21
23
 
22
24
  # -- General configuration ---------------------------------------------------
@@ -1,7 +1,8 @@
1
1
  # Add a File to a Folder
2
2
 
3
3
  This is a two-step workflow: first upload the file using `upload_files`, then place it in a target
4
- folder using `add_file_to_folder`.
4
+ folder using `modify_file_system` with an `upsert_file` operation.
5
+ Each call creates a new document, so a retried call adds a duplicate file.
5
6
 
6
7
  ## Step 1: Upload the file
7
8
 
@@ -34,11 +35,13 @@ file_id = upload_file_response[0].file_id
34
35
  ## Step 2: Add the file to a folder
35
36
 
36
37
  ```{code-block} python
37
- from uncountable.types import identifier_t
38
+ from uncountable.types import identifier_t, modify_file_system_t
38
39
 
39
- response = client.add_file_to_folder(
40
- file_id=file_id,
41
- folder_key=identifier_t.IdentifierKeyId(id=<FOLDER_ID>),
40
+ response = client.modify_file_system(
41
+ operation=modify_file_system_t.UpsertFileOperation(
42
+ file_id=file_id,
43
+ folder_key=identifier_t.IdentifierKeyId(id=<FOLDER_ID>),
44
+ ),
42
45
  )
43
46
  ```
44
47
 
@@ -50,8 +53,10 @@ Data(modification_made=True, result_id=4521, entity=Entity(id=892, type=<EntityT
50
53
  To add the file to the root folder instead of a specific folder:
51
54
 
52
55
  ```{code-block} python
53
- response = client.add_file_to_folder(
54
- file_id=file_id,
55
- folder_key=identifier_t.IdentifierKeyRefName(ref_name="root"),
56
+ response = client.modify_file_system(
57
+ operation=modify_file_system_t.UpsertFileOperation(
58
+ file_id=file_id,
59
+ folder_key=identifier_t.IdentifierKeyRefName(ref_name="root"),
60
+ ),
56
61
  )
57
62
  ```
@@ -65,7 +65,7 @@ def _resolve_equipment_id(*, client: Client, equipment_name: str) -> ObjectId:
65
65
  )
66
66
  equipment_id = results[0].column_values[0]
67
67
  if not isinstance(equipment_id, int):
68
- raise Exception(
68
+ raise TypeError(
69
69
  f"expected an integer id for equipment named {equipment_name}, found {equipment_id!r}"
70
70
  )
71
71
  return ObjectId(equipment_id)
@@ -2,9 +2,9 @@ import argparse
2
2
  import datetime
3
3
  import json
4
4
  import os
5
- import random
6
5
  import time
7
6
  from decimal import Decimal
7
+ from random import SystemRandom
8
8
 
9
9
  import requests
10
10
  from uncountable.types import dataset_measurements_t, identifier_t
@@ -17,11 +17,12 @@ API_SECRET_KEY_ENV_VAR = "UNC_API_SECRET_KEY"
17
17
  MEASUREMENTS_PER_SECOND = 500
18
18
  ONE_SECOND = datetime.timedelta(seconds=1)
19
19
  VALUE_QUANTUM = Decimal("0.0001")
20
+ RNG = SystemRandom()
20
21
 
21
22
 
22
23
  def _random_value(*, min_value: Decimal, max_value: Decimal) -> Decimal:
23
24
  span = max_value - min_value
24
- sampled = (min_value + span * Decimal(str(random.random()))).quantize(VALUE_QUANTUM)
25
+ sampled = (min_value + span * Decimal(str(RNG.random()))).quantize(VALUE_QUANTUM)
25
26
  return max(min_value, min(max_value, sampled))
26
27
 
27
28
 
@@ -19,6 +19,7 @@ __all__ = [
19
19
  "is_string_enum_class",
20
20
  "is_union",
21
21
  "kebab_to_pascal_case",
22
+ "resolve_serialized_field_name",
22
23
  "snake_to_camel_case",
23
24
  ]
24
25
 
@@ -32,6 +33,9 @@ from .case_convert import (
32
33
  kebab_to_pascal_case as kebab_to_pascal_case,
33
34
  snake_to_camel_case as snake_to_camel_case,
34
35
  )
36
+ from .field_name_resolution import (
37
+ resolve_serialized_field_name as resolve_serialized_field_name,
38
+ )
35
39
  from .parser_base import ParserBase as ParserBase
36
40
  from .parser_builder import build_parser as build_parser
37
41
  from .parser_error import (
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TypeVar, assert_never
4
+
5
+ from pkgs.serialization import SerialClassDataInspector
6
+
7
+ from .case_convert import snake_to_camel_case
8
+ from .parser_options import SourceEncoding
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ def resolve_serialized_field_name(
14
+ *,
15
+ serial_class_data: SerialClassDataInspector[T],
16
+ field_name: str,
17
+ source_encoding: SourceEncoding,
18
+ ) -> str:
19
+ match source_encoding:
20
+ case SourceEncoding.API:
21
+ emit_api_override = serial_class_data.get_emit_api_override(field_name)
22
+ if emit_api_override is not None:
23
+ assert not serial_class_data.has_unconverted_key(field_name), (
24
+ f"serial class field {field_name!r} appears in both "
25
+ "emit_api_overrides and unconverted_keys; the features cannot "
26
+ "be combined"
27
+ )
28
+ return emit_api_override
29
+ if not serial_class_data.has_unconverted_key(field_name):
30
+ return snake_to_camel_case(field_name)
31
+ return field_name
32
+ case SourceEncoding.STORAGE:
33
+ return field_name
34
+ case _ as unreachable:
35
+ assert_never(unreachable)
@@ -21,6 +21,7 @@ from pkgs.serialization import (
21
21
  )
22
22
 
23
23
  from .case_convert import camel_to_snake_case, snake_to_camel_case
24
+ from .field_name_resolution import resolve_serialized_field_name
24
25
  from .parser_error import (
25
26
  ParserEnumError,
26
27
  ParserError,
@@ -36,7 +37,11 @@ from .type_predicates import (
36
37
  is_string_enum_class,
37
38
  )
38
39
 
40
+ if typing.TYPE_CHECKING:
41
+ from _typeshed import DataclassInstance
42
+
39
43
  T = typing.TypeVar("T")
44
+ DataclassT = typing.TypeVar("DataclassT", bound="DataclassInstance")
40
45
 
41
46
 
42
47
  def _has_field_default(field: dataclasses.Field[typing.Any]) -> bool:
@@ -250,6 +255,40 @@ def _build_parser_for_type_alias(
250
255
  return build_parser_inner(alias.__value__, context, merged_alias_map)
251
256
 
252
257
 
258
+ def _build_parser_for_collection(
259
+ *,
260
+ parsed_type: type[T]
261
+ | typing.TypeVar
262
+ | typing.TypeAliasType
263
+ | typing.NewType
264
+ | types.UnionType,
265
+ origin: type,
266
+ type_args: tuple[typing.Any, ...],
267
+ context: ParserContext,
268
+ type_var_map: dict[typing.TypeVar, type],
269
+ ) -> ParserFunction[T]:
270
+ if len(type_args) != 1:
271
+ raise ParserError(
272
+ "List types only support one argument", expected_type=parsed_type
273
+ )
274
+ arg_parser = build_parser_inner(type_args[0], context, type_var_map)
275
+
276
+ def parse_element(value: typing.Any) -> typing.Any:
277
+ try:
278
+ return arg_parser(value)
279
+ except Exception as e:
280
+ raise ParserError("Failed to parse element", value=value) from e
281
+
282
+ def parse(value: typing.Any) -> typing.Any:
283
+ if not isinstance(value, list):
284
+ raise ParserError(
285
+ "value is not a list", value=value, expected_type=parsed_type
286
+ )
287
+ return origin(parse_element(x) for x in value)
288
+
289
+ return parse
290
+
291
+
253
292
  def build_parser_inner(
254
293
  parsed_type: type[T]
255
294
  | typing.TypeVar
@@ -323,7 +362,7 @@ def build_parser_inner(
323
362
  type_var_map=merged_map,
324
363
  )
325
364
 
326
- return _build_parser_dataclass(origin, context, merged_map) # type: ignore[arg-type]
365
+ return _build_parser_dataclass(origin, context, merged_map) # type: ignore[return-value]
327
366
 
328
367
  # namedtuple support
329
368
  if is_namedtuple_type(parsed_type):
@@ -383,27 +422,13 @@ def build_parser_inner(
383
422
  return lambda value: value
384
423
 
385
424
  if origin in (list, set):
386
- args = typing.get_args(parsed_type)
387
- if len(args) != 1:
388
- raise ParserError(
389
- "List types only support one argument", expected_type=parsed_type
390
- )
391
- arg_parser = build_parser_inner(args[0], context, type_var_map)
392
-
393
- def parse_element(value: typing.Any) -> typing.Any:
394
- try:
395
- return arg_parser(value)
396
- except Exception as e:
397
- raise ParserError("Failed to parse element", value=value) from e
398
-
399
- def parse(value: typing.Any) -> typing.Any:
400
- if not isinstance(value, list):
401
- raise ParserError(
402
- "value is not a list", value=value, expected_type=parsed_type
403
- )
404
- return origin(parse_element(x) for x in value)
405
-
406
- return parse
425
+ return _build_parser_for_collection(
426
+ parsed_type=parsed_type,
427
+ origin=origin,
428
+ type_args=type_args,
429
+ context=context,
430
+ type_var_map=type_var_map,
431
+ )
407
432
 
408
433
  if origin is dict:
409
434
  args = typing.get_args(parsed_type)
@@ -445,7 +470,7 @@ def build_parser_inner(
445
470
  # first parse ints to decimal to allow scientific notation and decimals
446
471
  # e.g. (1) 1e4 => 1000, (2) 3.0 => 3
447
472
 
448
- def parse_int(value: typing.Any) -> T:
473
+ def parse_int(value: typing.Any) -> int:
449
474
  if isinstance(value, str):
450
475
  assert "_" not in value, (
451
476
  "numbers with underscores not considered integers"
@@ -456,54 +481,57 @@ def build_parser_inner(
456
481
  assert int_value == dec_value, (
457
482
  f"value ({value}) cannot be parsed to int without discarding precision"
458
483
  )
459
- return int_value # type: ignore
484
+ return int_value
460
485
 
461
- return parse_int
486
+ return parse_int # type: ignore[return-value]
462
487
 
463
488
  if parsed_type is datetime.datetime:
464
489
 
465
- def parse_datetime(value: typing.Any) -> T:
490
+ def parse_datetime(value: typing.Any) -> datetime.datetime:
466
491
  if context.options.allow_direct_type and isinstance(
467
492
  value, datetime.datetime
468
493
  ):
469
- return value # type: ignore
470
- return dateutil.parser.isoparse(value) # type:ignore
494
+ return value
495
+ return dateutil.parser.isoparse(value)
471
496
 
472
- return parse_datetime
497
+ return parse_datetime # type: ignore[return-value]
473
498
 
474
499
  if parsed_type is date:
475
500
 
476
- def parse_date(value: typing.Any) -> T:
501
+ def parse_date(value: typing.Any) -> date:
477
502
  if context.options.allow_direct_type and isinstance(value, date):
478
503
  if isinstance(value, datetime.datetime):
479
504
  assert value.time() == datetime.time.min
480
- return value.date() # type:ignore
481
- return value # type:ignore
482
- return date.fromisoformat(value) # type:ignore
505
+ return value.date()
506
+ return value
507
+ return date.fromisoformat(value)
483
508
 
484
- return parse_date
509
+ return parse_date # type: ignore[return-value]
485
510
 
486
511
  # MyPy: It's unclear why `parsed_type in (str, OpaqueKey)` is flagged as invalid
487
512
  # Thus an or statement is used instead, which isn't flagged as invalid.
488
513
  if parsed_type is str or parsed_type is OpaqueKey:
489
514
 
490
- def parse_str(value: typing.Any) -> T:
515
+ def parse_str(value: typing.Any) -> str:
491
516
  if isinstance(value, str):
492
- return value # type: ignore
493
- if isinstance(value, (float, int)):
494
- return str(value) # type: ignore
517
+ return value
518
+ if isinstance(value, (Decimal, float, int)):
519
+ return str(value)
495
520
  raise ParserError(
496
521
  f"Invalid string value: {type(value)}: {value}",
497
522
  value=value,
498
523
  expected_type=str,
499
524
  )
500
525
 
501
- return parse_str
526
+ return parse_str # type: ignore[return-value]
502
527
 
503
528
  if parsed_type in (float, Decimal):
529
+ numeric_type: type[Decimal | float] = (
530
+ Decimal if parsed_type is Decimal else float
531
+ )
504
532
 
505
- def parse_as_numeric_type(value: typing.Any) -> T:
506
- numeric_value: Decimal | float = parsed_type(value) # type: ignore
533
+ def parse_as_numeric_type(value: typing.Any) -> Decimal | float:
534
+ numeric_value = numeric_type(value)
507
535
  if math.isnan(numeric_value):
508
536
  raise ParserError(
509
537
  f"Invalid numeric value: {numeric_value}",
@@ -511,12 +539,12 @@ def build_parser_inner(
511
539
  expected_type=parsed_type,
512
540
  )
513
541
 
514
- return numeric_value # type: ignore
542
+ return numeric_value
515
543
 
516
- return parse_as_numeric_type
544
+ return parse_as_numeric_type # type: ignore[return-value]
517
545
 
518
546
  if parsed_type in (dict, bool):
519
- return lambda value: parsed_type(value) # type: ignore
547
+ return lambda value: parsed_type(value) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments]
520
548
 
521
549
  if is_string_enum_class(parsed_type):
522
550
  assert issubclass(parsed_type, Enum)
@@ -550,7 +578,7 @@ def build_parser_inner(
550
578
  # will also be annotated, but have a special form
551
579
  if typing.get_origin(parsed_type) is typing.Annotated:
552
580
  return build_parser_inner(
553
- parsed_type.__origin__, # type: ignore[attr-defined]
581
+ typing.get_args(parsed_type)[0],
554
582
  context,
555
583
  type_var_map,
556
584
  )
@@ -562,10 +590,10 @@ def build_parser_inner(
562
590
 
563
591
  # Take in map of parameter name to materialized type
564
592
  def _build_parser_dataclass(
565
- parsed_type: type[T],
593
+ parsed_type: type[DataclassT],
566
594
  context: ParserContext,
567
595
  type_var_map: dict[typing.TypeVar, type],
568
- ) -> ParserFunction[T]:
596
+ ) -> ParserFunction[DataclassT]:
569
597
  """
570
598
  Use the cache so that recursion involve dataclasses is supported. This
571
599
  requires the build order is a bit inverted: the dataclass parser is added
@@ -609,33 +637,27 @@ def _build_parser_dataclass(
609
637
 
610
638
  serial_class_data = get_serial_class_data(parsed_type)
611
639
 
612
- def resolve_serialized_field_name(*, field_name: str) -> str:
613
- if context.options.is_api_encoding:
614
- emit_api_override = serial_class_data.get_emit_api_override(field_name)
615
- if emit_api_override is not None:
616
- assert not serial_class_data.has_unconverted_key(field_name), (
617
- f"serial class field {field_name!r} appears in both "
618
- "emit_api_overrides and unconverted_keys; the features cannot "
619
- "be combined"
620
- )
621
- return emit_api_override
622
- if not serial_class_data.has_unconverted_key(field_name):
623
- return snake_to_camel_case(field_name)
624
- return field_name
625
-
626
640
  def parse(value: typing.Any) -> typing.Any:
627
641
  # Use an exact type match to prevent base/derived class mismatches
628
642
  if context.options.allow_direct_type and type(value) is parsed_type:
629
643
  return value
630
644
 
631
645
  data: dict[typing.Any, typing.Any] = {}
646
+ all_allowed_field_names: set[str] = set()
632
647
  for field, field_type, field_parser in dc_field_parsers:
633
648
  field_raw_value = None
634
649
  try:
650
+ serialized_field_name = resolve_serialized_field_name(
651
+ serial_class_data=serial_class_data,
652
+ field_name=field.name,
653
+ source_encoding=context.options.encoding,
654
+ )
635
655
  field_raw_value = value.get(
636
- resolve_serialized_field_name(field_name=field.name),
656
+ serialized_field_name,
637
657
  dataclasses.MISSING,
638
658
  )
659
+ if context.options.strict_property_parsing:
660
+ all_allowed_field_names.add(serialized_field_name)
639
661
  field_value: typing.Any
640
662
  if field_raw_value == dataclasses.MISSING:
641
663
  if serial_class_data.has_parse_require(field.name):
@@ -679,10 +701,6 @@ def _build_parser_dataclass(
679
701
  ) from e
680
702
 
681
703
  if context.options.strict_property_parsing:
682
- all_allowed_field_names = set(
683
- resolve_serialized_field_name(field_name=field.name)
684
- for (field, _, _) in dc_field_parsers
685
- )
686
704
  passed_field_names = set(value.keys())
687
705
  disallowed_field_names = passed_field_names.difference(
688
706
  all_allowed_field_names
@@ -697,7 +715,7 @@ def _build_parser_dataclass(
697
715
  context.cache.put(cache_key, parser_function)
698
716
 
699
717
  dc_field_parsers = []
700
- for field in dataclasses.fields(parsed_type): # type:ignore[arg-type]
718
+ for field in dataclasses.fields(parsed_type):
701
719
  field_type_hint = type_hints[field.name]
702
720
  if isinstance(field_type_hint, typing.TypeVar):
703
721
  field_type_hint = type_var_map[field_type_hint]
@@ -187,7 +187,7 @@ class GDriveSession(FileSystemSession):
187
187
  self.service_account_json_path = service_account_json_path
188
188
 
189
189
  def start(self) -> None:
190
- credentials = service_account.Credentials.from_service_account_file( # type: ignore[no-untyped-call]
190
+ credentials = service_account.Credentials.from_service_account_file(
191
191
  self.service_account_json_path
192
192
  )
193
193
  gdrive_connection = build_gdrive_connection(
@@ -21,6 +21,8 @@ def _open_ssh_and_sftp(
21
21
  if private_key is None and password is None:
22
22
  raise SFTPCredentialError("Must specify either a private key or a password.")
23
23
  ssh = paramiko.SSHClient()
24
+ # ruff: ignore[ssh-no-host-key-verification]
25
+ # The platform does not hold host keys for dynamic customer SFTP endpoints.
24
26
  ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
25
27
  try:
26
28
  if private_key is not None:
@@ -1,7 +1,6 @@
1
1
  import os
2
2
  from dataclasses import dataclass
3
3
  from io import BytesIO
4
- from typing import Union
5
4
 
6
5
  import paramiko
7
6
  from azure.core.credentials import (
@@ -46,7 +45,7 @@ class RemoteObjectReference:
46
45
  return "folder" in self.mime_type
47
46
 
48
47
 
49
- FileSystemObject = Union[FileSystemFileReference, RemoteObjectReference, FileObjectData]
48
+ FileSystemObject = FileSystemFileReference | RemoteObjectReference | FileObjectData
50
49
 
51
50
 
52
51
  FileTransfer = tuple[FileSystemObject, FileSystemObject]
@@ -20,7 +20,10 @@ from .serial_class import (
20
20
  )
21
21
  from .serial_generic import get_serial_data as get_serial_data
22
22
  from .serial_union import (
23
+ IdentityHashWrapper as IdentityHashWrapper,
23
24
  get_serial_union_data as get_serial_union_data,
25
+ get_serial_union_discriminator_map as get_serial_union_discriminator_map,
26
+ rebuild_serial_union_metadata as rebuild_serial_union_metadata,
24
27
  serial_union_annotation as serial_union_annotation,
25
28
  )
26
29
  from .unknown import UnknownBrand as UnknownBrand
@@ -1,4 +1,4 @@
1
- from typing import Optional, TypeVar, Union
1
+ from typing import Optional, TypeAlias, TypeVar
2
2
 
3
3
  ClassT = TypeVar("ClassT")
4
4
 
@@ -23,7 +23,7 @@ class MissingSentryType:
23
23
  MISSING_SENTRY = MissingSentryType()
24
24
 
25
25
  # The MissingSentryType is expected to come first for serialization parsing
26
- MissingType = Union[MissingSentryType, ClassT]
26
+ MissingType: TypeAlias = MissingSentryType | ClassT
27
27
 
28
28
 
29
29
  def coalesce_missing_sentry(value: MissingType[ClassT]) -> ClassT | None:
@@ -55,6 +55,26 @@ def serial_union_annotation(
55
55
  )
56
56
 
57
57
 
58
+ def rebuild_serial_union_metadata(
59
+ serial_union_metadata: SerialBase,
60
+ *,
61
+ discriminator_map: IdentityHashWrapper[dict[str, type]],
62
+ ) -> SerialBase:
63
+ assert isinstance(serial_union_metadata, _SerialUnion)
64
+ return dataclasses.replace(
65
+ serial_union_metadata,
66
+ discriminator_map=discriminator_map,
67
+ )
68
+
69
+
70
+ def get_serial_union_discriminator_map(
71
+ metadata: object,
72
+ ) -> IdentityHashWrapper[dict[str, type]] | None:
73
+ if not isinstance(metadata, _SerialUnion):
74
+ return None
75
+ return metadata.discriminator_map
76
+
77
+
58
78
  def _get_serial_union(parsed_type: SerialType[T]) -> _SerialUnion | None:
59
79
  serial = get_serial_annotation(parsed_type)
60
80
  if not isinstance(serial, _SerialUnion):
@@ -1,4 +1,6 @@
1
+ from collections.abc import Iterable
1
2
  from decimal import Decimal
3
+ from enum import StrEnum
2
4
  from typing import TYPE_CHECKING, Any
3
5
 
4
6
  import yaml
@@ -29,12 +31,45 @@ class YAMLError(BaseException):
29
31
  pass
30
32
 
31
33
 
34
+ def _represent_str_enum(
35
+ dumper: yaml.representer.SafeRepresenter, value: StrEnum
36
+ ) -> yaml.ScalarNode:
37
+ return dumper.represent_str(value.value)
38
+
39
+
40
+ def _represent_decimal(
41
+ dumper: yaml.representer.SafeRepresenter, value: Decimal
42
+ ) -> yaml.ScalarNode:
43
+ return dumper.represent_str(str(value))
44
+
45
+
46
+ class _StorageFormatDumper(yaml.Dumper):
47
+ pass
48
+
49
+
50
+ class _StorageFormatSafeDumper(yaml.SafeDumper):
51
+ pass
52
+
53
+
54
+ _StorageFormatDumper.add_multi_representer(StrEnum, _represent_str_enum)
55
+ _StorageFormatSafeDumper.add_multi_representer(StrEnum, _represent_str_enum)
56
+ _StorageFormatSafeDumper.add_representer(Decimal, _represent_decimal)
57
+
58
+
32
59
  def dumps(obj: Any, sort_keys: bool = False) -> str:
33
- return yaml.dump(obj, sort_keys=sort_keys)
60
+ return yaml.dump(obj, Dumper=_StorageFormatDumper, sort_keys=sort_keys)
34
61
 
35
62
 
36
- def dump(obj: Any, f: SupportsWrite, sort_keys: bool = False) -> None:
37
- yaml.dump(obj, f, sort_keys=sort_keys)
63
+ def dump(
64
+ obj: Any, f: SupportsWrite, sort_keys: bool = False, *, allow_unicode: bool = False
65
+ ) -> None:
66
+ yaml.dump(
67
+ obj,
68
+ f,
69
+ Dumper=_StorageFormatDumper,
70
+ sort_keys=sort_keys,
71
+ allow_unicode=allow_unicode,
72
+ )
38
73
 
39
74
 
40
75
  def safe_load(src: str | bytes | SupportsRead) -> Any:
@@ -44,10 +79,26 @@ def safe_load(src: str | bytes | SupportsRead) -> Any:
44
79
  raise YAMLError() from e
45
80
 
46
81
 
82
+ def safe_load_all(src: str | bytes | SupportsRead) -> list[object]:
83
+ return list(yaml.load_all(src, Loader=yaml.CSafeLoader))
84
+
85
+
86
+ def safe_dump_all(documents: Iterable[object], sort_keys: bool = False) -> str:
87
+ return yaml.dump_all(
88
+ documents, Dumper=_StorageFormatSafeDumper, sort_keys=sort_keys
89
+ )
90
+
91
+
47
92
  def safe_dump(
48
93
  obj: Any,
49
94
  sort_keys: bool = False,
50
95
  indent: int | None = None,
51
96
  width: int | None = None,
52
97
  ) -> str:
53
- return yaml.safe_dump(obj, sort_keys=sort_keys, indent=indent, width=width)
98
+ return yaml.dump(
99
+ obj,
100
+ Dumper=_StorageFormatSafeDumper,
101
+ sort_keys=sort_keys,
102
+ indent=indent,
103
+ width=width,
104
+ )
@@ -9,11 +9,15 @@ from .serialization_helpers import (
9
9
  serialize_for_storage,
10
10
  serialize_for_storage_dict,
11
11
  )
12
+ from .serialize_for_api_encoding import serialize_for_api_encoding_reusing_input
13
+ from .source_encoding_serializer import serialize_for_source_encoding
12
14
 
13
15
  __all__: list[str] = [
14
16
  "convert_decimals_to_strings",
15
17
  "convert_dict_to_snake_case",
16
18
  "serialize_for_api",
19
+ "serialize_for_api_encoding_reusing_input",
20
+ "serialize_for_source_encoding",
17
21
  "serialize_for_storage",
18
22
  "serialize_for_storage_dict",
19
23
  "iterate_fields",