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.
- docs/conf.py +3 -1
- docs/integration_examples/add_file_to_folder.md +13 -8
- examples/integration-server/jobs/materials_auto/example_scale_connection.py +1 -1
- examples/stream_dataset_measurements.py +3 -2
- pkgs/argument_parser/__init__.py +4 -0
- pkgs/argument_parser/field_name_resolution.py +35 -0
- pkgs/argument_parser/parser_inner.py +85 -67
- pkgs/filesystem_utils/_gdrive_session.py +1 -1
- pkgs/filesystem_utils/_sftp_connection.py +2 -0
- pkgs/filesystem_utils/file_type_utils.py +1 -2
- pkgs/serialization/__init__.py +3 -0
- pkgs/serialization/missing_sentry.py +2 -2
- pkgs/serialization/serial_union.py +20 -0
- pkgs/serialization/yaml.py +55 -4
- pkgs/serialization_util/__init__.py +4 -0
- pkgs/serialization_util/reuse_containers.py +88 -0
- pkgs/serialization_util/serialization_helpers.py +81 -32
- pkgs/serialization_util/serialize_for_api_encoding.py +70 -0
- pkgs/serialization_util/source_encoding_serializer.py +17 -0
- pkgs/type_spec/AGENTS.md +13 -0
- pkgs/type_spec/CLAUDE.md +5 -0
- pkgs/type_spec/builder.py +69 -19
- pkgs/type_spec/emit_open_api.py +4 -8
- pkgs/type_spec/emit_python.py +138 -18
- pkgs/type_spec/emit_typescript.py +17 -2
- pkgs/type_spec/emit_typescript_util.py +54 -11
- pkgs/type_spec/parts/base.py.prepart +11 -11
- pkgs/type_spec/parts/base.ts.prepart +6 -0
- pkgs/type_spec/type_info/emit_type_info.py +11 -15
- pkgs/type_spec/util.py +2 -2
- pkgs/type_spec/value_spec/__main__.py +1 -1
- pkgs/type_spec/value_spec/emit_python.py +7 -3
- uncountable/core/__init__.py +2 -1
- uncountable/core/client.py +8 -1
- uncountable/core/environment.py +4 -0
- uncountable/core/query/column.py +10 -1
- uncountable/core/types.py +6 -1
- uncountable/integration/db/session.py +2 -2
- uncountable/integration/executors/executors.py +2 -2
- uncountable/integration/queue_runner/command_server/command_client.py +1 -1
- uncountable/integration/queue_runner/command_server/command_server.py +1 -1
- uncountable/integration/queue_runner/job_scheduler.py +14 -3
- uncountable/integration/scheduler.py +19 -6
- uncountable/integration/telemetry.py +38 -13
- uncountable/integration/webhook_server/gunicorn_config.py +9 -0
- uncountable/types/api/file_folders/add_file_to_folder.py +1 -1
- uncountable/types/api/file_folders/modify_file_system.py +51 -2
- uncountable/types/api/files/download_file.py +17 -2
- uncountable/types/api/inputs/create_inputs.py +2 -2
- uncountable/types/api/material_families/update_entity_material_families.py +1 -1
- uncountable/types/api/ml/submit_async_job.py +3 -3
- uncountable/types/api/notebooks/add_notebook_content.py +2 -2
- uncountable/types/api/permissions/set_entity_permission.py +17 -2
- uncountable/types/api/recipe_links/create_recipe_link.py +1 -1
- uncountable/types/api/recipe_links/create_recipe_links.py +1 -1
- uncountable/types/api/recipe_links/remove_recipe_link.py +1 -1
- uncountable/types/api/recipes/edit_recipe_inputs.py +3 -3
- uncountable/types/api/recipes/upsert_step_relationships.py +1 -1
- uncountable/types/async_batch_processor.py +37 -0
- uncountable/types/async_batch_t.py +1 -0
- uncountable/types/base_t.py +11 -11
- uncountable/types/client_base.py +8 -8
- uncountable/types/curves_t.py +2 -2
- uncountable/types/dataset_measurements_t.py +1 -1
- uncountable/types/entity_t.py +122 -102
- uncountable/types/field_values.py +1 -0
- uncountable/types/field_values_t.py +15 -1
- uncountable/types/integration_session_t.py +1 -1
- uncountable/types/integrations_t.py +1 -1
- uncountable/types/listing.py +3 -0
- uncountable/types/listing_t.py +39 -3
- uncountable/types/recipe_workflow_step_types_t.py +1 -1
- uncountable/types/sockets_t.py +2 -2
- {uncountablepythonsdk-0.0.188.dist-info → uncountablepythonsdk-0.0.190.dist-info}/METADATA +2 -1
- {uncountablepythonsdk-0.0.188.dist-info → uncountablepythonsdk-0.0.190.dist-info}/RECORD +77 -70
- {uncountablepythonsdk-0.0.188.dist-info → uncountablepythonsdk-0.0.190.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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 `
|
|
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.
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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(
|
|
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
|
|
pkgs/argument_parser/__init__.py
CHANGED
|
@@ -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[
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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
|
|
470
|
-
return dateutil.parser.isoparse(value)
|
|
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) ->
|
|
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()
|
|
481
|
-
return value
|
|
482
|
-
return date.fromisoformat(value)
|
|
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) ->
|
|
515
|
+
def parse_str(value: typing.Any) -> str:
|
|
491
516
|
if isinstance(value, str):
|
|
492
|
-
return value
|
|
493
|
-
if isinstance(value, (float, int)):
|
|
494
|
-
return str(value)
|
|
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) ->
|
|
506
|
-
numeric_value
|
|
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
|
|
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
|
|
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[
|
|
593
|
+
parsed_type: type[DataclassT],
|
|
566
594
|
context: ParserContext,
|
|
567
595
|
type_var_map: dict[typing.TypeVar, type],
|
|
568
|
-
) -> ParserFunction[
|
|
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
|
-
|
|
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):
|
|
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(
|
|
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 =
|
|
48
|
+
FileSystemObject = FileSystemFileReference | RemoteObjectReference | FileObjectData
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
FileTransfer = tuple[FileSystemObject, FileSystemObject]
|
pkgs/serialization/__init__.py
CHANGED
|
@@ -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,
|
|
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 =
|
|
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):
|
pkgs/serialization/yaml.py
CHANGED
|
@@ -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(
|
|
37
|
-
|
|
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.
|
|
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",
|