UncountablePythonSDK 0.0.191__py3-none-any.whl → 0.0.193__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.
- pkgs/argument_parser/parse_fallback.py +26 -0
- pkgs/argument_parser/parser_inner.py +116 -54
- pkgs/codeowners.py +157 -0
- pkgs/serialization/__init__.py +1 -0
- pkgs/serialization/serial_class.py +26 -1
- pkgs/type_spec/builder.py +93 -19
- pkgs/type_spec/builder_types.py +6 -0
- pkgs/type_spec/emit_io_ts.py +1 -1
- pkgs/type_spec/emit_open_api.py +29 -32
- pkgs/type_spec/emit_open_api_examples.py +221 -0
- pkgs/type_spec/emit_python.py +27 -81
- pkgs/type_spec/emit_typescript_util.py +10 -4
- uncountable/core/client.py +7 -4
- uncountable/core/file_upload.py +5 -3
- uncountable/core/tls.py +34 -0
- uncountable/core/types.py +4 -4
- uncountable/integration/queue_runner/command_handlers.py +144 -0
- uncountable/integration/queue_runner/job_dispatcher.py +44 -0
- uncountable/integration/queue_runner/job_scheduler.py +51 -172
- uncountable/integration/queue_runner/queue_runner.py +5 -1
- uncountable/integration/telemetry.py +13 -3
- uncountable/types/__init__.py +2 -0
- uncountable/types/api/files/download_file.py +18 -2
- uncountable/types/api/listing/fetch_listing.py +2 -1
- uncountable/types/api/notebooks/get_notebook_content.py +15 -1
- uncountable/types/client_base.py +5 -2
- uncountable/types/listing.py +2 -0
- uncountable/types/listing_t.py +18 -0
- uncountable/types/notebook_files.py +10 -0
- uncountable/types/notebook_files_t.py +64 -0
- {uncountablepythonsdk-0.0.191.dist-info → uncountablepythonsdk-0.0.193.dist-info}/METADATA +1 -1
- {uncountablepythonsdk-0.0.191.dist-info → uncountablepythonsdk-0.0.193.dist-info}/RECORD +34 -26
- {uncountablepythonsdk-0.0.191.dist-info → uncountablepythonsdk-0.0.193.dist-info}/WHEEL +1 -1
- {uncountablepythonsdk-0.0.191.dist-info → uncountablepythonsdk-0.0.193.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import inspect
|
|
3
|
+
import typing
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
|
|
6
|
+
from .parser_function_type import ParserFunction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
10
|
+
class DataclassParseFallback:
|
|
11
|
+
legacy_key: str
|
|
12
|
+
legacy_parser: ParserFunction[object]
|
|
13
|
+
convert: Callable[[typing.Any], object]
|
|
14
|
+
|
|
15
|
+
def applies(self, *, value: Mapping[str, object], field_raw_value: object) -> bool:
|
|
16
|
+
return (
|
|
17
|
+
field_raw_value is dataclasses.MISSING or field_raw_value is None
|
|
18
|
+
) and value.get(self.legacy_key) is not None
|
|
19
|
+
|
|
20
|
+
def parse(self, value: Mapping[str, object]) -> object:
|
|
21
|
+
return self.convert(self.legacy_parser(value[self.legacy_key]))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_fallback_legacy_type(convert: Callable[..., object]) -> typing.Any:
|
|
25
|
+
legacy_parameter = next(iter(inspect.signature(convert).parameters))
|
|
26
|
+
return typing.get_type_hints(convert, include_extras=True)[legacy_parameter]
|
|
@@ -15,6 +15,7 @@ import dateutil.parser
|
|
|
15
15
|
from pkgs.serialization import (
|
|
16
16
|
MissingSentryType,
|
|
17
17
|
OpaqueKey,
|
|
18
|
+
SerialClassDataInspector,
|
|
18
19
|
UnknownBrand,
|
|
19
20
|
get_serial_class_data,
|
|
20
21
|
get_serial_union_data,
|
|
@@ -22,6 +23,7 @@ from pkgs.serialization import (
|
|
|
22
23
|
|
|
23
24
|
from .case_convert import camel_to_snake_case, snake_to_camel_case
|
|
24
25
|
from .field_name_resolution import resolve_serialized_field_name
|
|
26
|
+
from .parse_fallback import DataclassParseFallback, parse_fallback_legacy_type
|
|
25
27
|
from .parser_error import (
|
|
26
28
|
ParserEnumError,
|
|
27
29
|
ParserError,
|
|
@@ -255,6 +257,39 @@ def _build_parser_for_type_alias(
|
|
|
255
257
|
return build_parser_inner(alias.__value__, context, merged_alias_map)
|
|
256
258
|
|
|
257
259
|
|
|
260
|
+
def _build_parser_for_dict(
|
|
261
|
+
*,
|
|
262
|
+
key_type: typing.Any,
|
|
263
|
+
value_type: typing.Any,
|
|
264
|
+
context: ParserContext,
|
|
265
|
+
type_var_map: dict[typing.TypeVar, type],
|
|
266
|
+
) -> ParserFunction[T]:
|
|
267
|
+
k_inner_parser = build_parser_inner(
|
|
268
|
+
key_type,
|
|
269
|
+
context,
|
|
270
|
+
type_var_map,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def key_parser(value: typing.Any) -> object:
|
|
274
|
+
inner = k_inner_parser(value)
|
|
275
|
+
if (
|
|
276
|
+
isinstance(inner, str)
|
|
277
|
+
# enum keys and OpaqueData's would also have string value types,
|
|
278
|
+
# but their explicit type is not a string, thus shouldn't be converted
|
|
279
|
+
and key_type is str
|
|
280
|
+
and context.options.is_api_encoding
|
|
281
|
+
):
|
|
282
|
+
return camel_to_snake_case(value)
|
|
283
|
+
return inner
|
|
284
|
+
|
|
285
|
+
v_parser = build_parser_inner(value_type, context, type_var_map)
|
|
286
|
+
|
|
287
|
+
def parse_dict(value: typing.Any) -> typing.Any:
|
|
288
|
+
return {key_parser(k): v_parser(v) for k, v in value.items()}
|
|
289
|
+
|
|
290
|
+
return parse_dict
|
|
291
|
+
|
|
292
|
+
|
|
258
293
|
def _build_parser_for_collection(
|
|
259
294
|
*,
|
|
260
295
|
parsed_type: type[T]
|
|
@@ -289,6 +324,32 @@ def _build_parser_for_collection(
|
|
|
289
324
|
return parse
|
|
290
325
|
|
|
291
326
|
|
|
327
|
+
# first parse ints to decimal to allow scientific notation and decimals
|
|
328
|
+
# e.g. (1) 1e4 => 1000, (2) 3.0 => 3
|
|
329
|
+
def _parse_int(value: typing.Any) -> int:
|
|
330
|
+
if isinstance(value, str):
|
|
331
|
+
assert "_" not in value, "numbers with underscores not considered integers"
|
|
332
|
+
|
|
333
|
+
dec_value = Decimal(value)
|
|
334
|
+
int_value = int(dec_value)
|
|
335
|
+
assert int_value == dec_value, (
|
|
336
|
+
f"value ({value}) cannot be parsed to int without discarding precision"
|
|
337
|
+
)
|
|
338
|
+
return int_value
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _parse_str(value: typing.Any) -> str:
|
|
342
|
+
if isinstance(value, str):
|
|
343
|
+
return value
|
|
344
|
+
if isinstance(value, (Decimal, float, int)):
|
|
345
|
+
return str(value)
|
|
346
|
+
raise ParserError(
|
|
347
|
+
f"Invalid string value: {type(value)}: {value}",
|
|
348
|
+
value=value,
|
|
349
|
+
expected_type=str,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
292
353
|
def build_parser_inner(
|
|
293
354
|
parsed_type: type[T]
|
|
294
355
|
| typing.TypeVar
|
|
@@ -437,53 +498,20 @@ def build_parser_inner(
|
|
|
437
498
|
"Dict types only support two arguments for now",
|
|
438
499
|
expected_type=parsed_type,
|
|
439
500
|
)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
501
|
+
key_type, value_type = args
|
|
502
|
+
return _build_parser_for_dict(
|
|
503
|
+
key_type=key_type,
|
|
504
|
+
value_type=value_type,
|
|
505
|
+
context=context,
|
|
506
|
+
type_var_map=type_var_map,
|
|
444
507
|
)
|
|
445
508
|
|
|
446
|
-
def key_parser(value: typing.Any) -> object:
|
|
447
|
-
inner = k_inner_parser(value)
|
|
448
|
-
if (
|
|
449
|
-
isinstance(inner, str)
|
|
450
|
-
# enum keys and OpaqueData's would also have string value types,
|
|
451
|
-
# but their explicit type is not a string, thus shouldn't be converted
|
|
452
|
-
and args[0] is str
|
|
453
|
-
and context.options.is_api_encoding
|
|
454
|
-
):
|
|
455
|
-
return camel_to_snake_case(value)
|
|
456
|
-
return inner
|
|
457
|
-
|
|
458
|
-
v_parser = build_parser_inner(args[1], context, type_var_map)
|
|
459
|
-
|
|
460
|
-
def parse_dict(value: typing.Any) -> typing.Any:
|
|
461
|
-
return origin((key_parser(k), v_parser(v)) for k, v in value.items())
|
|
462
|
-
|
|
463
|
-
return parse_dict
|
|
464
|
-
|
|
465
509
|
if origin == typing.Literal:
|
|
466
510
|
valid_values: set[T] = set(typing.get_args(parsed_type))
|
|
467
511
|
return lambda value: _invoke_membership_parser(valid_values, value)
|
|
468
512
|
|
|
469
513
|
if parsed_type is int:
|
|
470
|
-
#
|
|
471
|
-
# e.g. (1) 1e4 => 1000, (2) 3.0 => 3
|
|
472
|
-
|
|
473
|
-
def parse_int(value: typing.Any) -> int:
|
|
474
|
-
if isinstance(value, str):
|
|
475
|
-
assert "_" not in value, (
|
|
476
|
-
"numbers with underscores not considered integers"
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
dec_value = Decimal(value)
|
|
480
|
-
int_value = int(dec_value)
|
|
481
|
-
assert int_value == dec_value, (
|
|
482
|
-
f"value ({value}) cannot be parsed to int without discarding precision"
|
|
483
|
-
)
|
|
484
|
-
return int_value
|
|
485
|
-
|
|
486
|
-
return parse_int # type: ignore[return-value]
|
|
514
|
+
return _parse_int # type: ignore[return-value]
|
|
487
515
|
|
|
488
516
|
if parsed_type is datetime.datetime:
|
|
489
517
|
|
|
@@ -511,19 +539,7 @@ def build_parser_inner(
|
|
|
511
539
|
# MyPy: It's unclear why `parsed_type in (str, OpaqueKey)` is flagged as invalid
|
|
512
540
|
# Thus an or statement is used instead, which isn't flagged as invalid.
|
|
513
541
|
if parsed_type is str or parsed_type is OpaqueKey:
|
|
514
|
-
|
|
515
|
-
def parse_str(value: typing.Any) -> str:
|
|
516
|
-
if isinstance(value, str):
|
|
517
|
-
return value
|
|
518
|
-
if isinstance(value, (Decimal, float, int)):
|
|
519
|
-
return str(value)
|
|
520
|
-
raise ParserError(
|
|
521
|
-
f"Invalid string value: {type(value)}: {value}",
|
|
522
|
-
value=value,
|
|
523
|
-
expected_type=str,
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
return parse_str # type: ignore[return-value]
|
|
542
|
+
return _parse_str # type: ignore[return-value]
|
|
527
543
|
|
|
528
544
|
if parsed_type in (float, Decimal):
|
|
529
545
|
numeric_type: type[Decimal | float] = (
|
|
@@ -636,6 +652,7 @@ def _build_parser_dataclass(
|
|
|
636
652
|
] = []
|
|
637
653
|
|
|
638
654
|
serial_class_data = get_serial_class_data(parsed_type)
|
|
655
|
+
dc_parse_fallbacks: dict[str, DataclassParseFallback] = {}
|
|
639
656
|
|
|
640
657
|
def parse(value: typing.Any) -> typing.Any:
|
|
641
658
|
# Use an exact type match to prevent base/derived class mismatches
|
|
@@ -656,10 +673,17 @@ def _build_parser_dataclass(
|
|
|
656
673
|
serialized_field_name,
|
|
657
674
|
dataclasses.MISSING,
|
|
658
675
|
)
|
|
676
|
+
parse_fallback = dc_parse_fallbacks.get(field.name)
|
|
659
677
|
if context.options.strict_property_parsing:
|
|
660
678
|
all_allowed_field_names.add(serialized_field_name)
|
|
679
|
+
if parse_fallback is not None:
|
|
680
|
+
all_allowed_field_names.add(parse_fallback.legacy_key)
|
|
661
681
|
field_value: typing.Any
|
|
662
|
-
if
|
|
682
|
+
if parse_fallback is not None and parse_fallback.applies(
|
|
683
|
+
value=value, field_raw_value=field_raw_value
|
|
684
|
+
):
|
|
685
|
+
field_value = parse_fallback.parse(value)
|
|
686
|
+
elif field_raw_value == dataclasses.MISSING:
|
|
663
687
|
if serial_class_data.has_parse_require(field.name):
|
|
664
688
|
raise ParserError(
|
|
665
689
|
"missing-required-field", field_name=field.name
|
|
@@ -726,4 +750,42 @@ def _build_parser_dataclass(
|
|
|
726
750
|
build_parser_inner(field_type_hint, context, type_var_map),
|
|
727
751
|
))
|
|
728
752
|
|
|
753
|
+
dc_parse_fallbacks.update(
|
|
754
|
+
_build_dataclass_parse_fallbacks(
|
|
755
|
+
parsed_type=parsed_type,
|
|
756
|
+
serial_class_data=serial_class_data,
|
|
757
|
+
context=context,
|
|
758
|
+
type_var_map=type_var_map,
|
|
759
|
+
)
|
|
760
|
+
)
|
|
761
|
+
|
|
729
762
|
return parser_function
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _build_dataclass_parse_fallbacks(
|
|
766
|
+
*,
|
|
767
|
+
parsed_type: type[DataclassT],
|
|
768
|
+
serial_class_data: SerialClassDataInspector[DataclassT],
|
|
769
|
+
context: ParserContext,
|
|
770
|
+
type_var_map: dict[typing.TypeVar, type],
|
|
771
|
+
) -> dict[str, DataclassParseFallback]:
|
|
772
|
+
field_names = {field.name for field in dataclasses.fields(parsed_type)}
|
|
773
|
+
dc_parse_fallbacks: dict[str, DataclassParseFallback] = {}
|
|
774
|
+
for field_name, parse_fallback in serial_class_data.get_parse_fallbacks().items():
|
|
775
|
+
assert field_name in field_names, (
|
|
776
|
+
f"parse fallback for {field_name!r}, which is not a field of {parsed_type}"
|
|
777
|
+
)
|
|
778
|
+
dc_parse_fallbacks[field_name] = DataclassParseFallback(
|
|
779
|
+
legacy_key=resolve_serialized_field_name(
|
|
780
|
+
serial_class_data=serial_class_data,
|
|
781
|
+
field_name=parse_fallback.legacy_field,
|
|
782
|
+
source_encoding=context.options.encoding,
|
|
783
|
+
),
|
|
784
|
+
legacy_parser=build_parser_inner(
|
|
785
|
+
parse_fallback_legacy_type(parse_fallback.convert),
|
|
786
|
+
context,
|
|
787
|
+
type_var_map,
|
|
788
|
+
),
|
|
789
|
+
convert=parse_fallback.convert,
|
|
790
|
+
)
|
|
791
|
+
return dc_parse_fallbacks
|
pkgs/codeowners.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Iterable, Mapping
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from itertools import chain
|
|
6
|
+
|
|
7
|
+
GLOB_CHARS = "*?["
|
|
8
|
+
TEAM_OWNER_PREFIX = "@uncountableinc/"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_team_username(team_slug: str) -> str:
|
|
12
|
+
return f"{TEAM_OWNER_PREFIX}{team_slug}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _glob_regex(pattern: str) -> re.Pattern[str]:
|
|
16
|
+
directory_pattern = pattern.endswith("/**")
|
|
17
|
+
normalized_pattern = pattern[:-3] if directory_pattern else pattern
|
|
18
|
+
regex_parts: list[str] = []
|
|
19
|
+
index = 0
|
|
20
|
+
while index < len(normalized_pattern):
|
|
21
|
+
dotfile_guard = (
|
|
22
|
+
"(?!\\.)" if index == 0 or normalized_pattern[index - 1] == "/" else ""
|
|
23
|
+
)
|
|
24
|
+
if normalized_pattern.startswith("**/", index):
|
|
25
|
+
regex_parts.append("(?:(?!\\.)[^/]+/)*")
|
|
26
|
+
index += 3
|
|
27
|
+
elif normalized_pattern.startswith("**", index):
|
|
28
|
+
regex_parts.append(".*")
|
|
29
|
+
index += 2
|
|
30
|
+
elif normalized_pattern[index] == "*":
|
|
31
|
+
regex_parts.append(f"{dotfile_guard}[^/]*")
|
|
32
|
+
index += 1
|
|
33
|
+
elif normalized_pattern[index] == "?":
|
|
34
|
+
regex_parts.append(f"{dotfile_guard}[^/]")
|
|
35
|
+
index += 1
|
|
36
|
+
elif normalized_pattern[index] == "[":
|
|
37
|
+
closing_index = normalized_pattern.find("]", index + 1)
|
|
38
|
+
if closing_index == -1:
|
|
39
|
+
regex_parts.append(r"\[")
|
|
40
|
+
index += 1
|
|
41
|
+
else:
|
|
42
|
+
character_class = normalized_pattern[index + 1 : closing_index]
|
|
43
|
+
if character_class.startswith("!"):
|
|
44
|
+
character_class = f"^{character_class[1:]}"
|
|
45
|
+
regex_parts.append(f"{dotfile_guard}[{character_class}]")
|
|
46
|
+
index = closing_index + 1
|
|
47
|
+
else:
|
|
48
|
+
regex_parts.append(re.escape(normalized_pattern[index]))
|
|
49
|
+
index += 1
|
|
50
|
+
suffix = "(?:/(?!\\.)[^/]+)*" if directory_pattern else ""
|
|
51
|
+
return re.compile(f"^{''.join(regex_parts)}{suffix}$")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(kw_only=True)
|
|
55
|
+
class CodeownerRule:
|
|
56
|
+
owners: list[str]
|
|
57
|
+
glob: str
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def normalized_globs(self) -> list[str]:
|
|
61
|
+
normalized_glob = self.glob.strip("/")
|
|
62
|
+
|
|
63
|
+
# /path/to/file should the file /path/to/file
|
|
64
|
+
globs = [normalized_glob]
|
|
65
|
+
|
|
66
|
+
# /path/to/file should also match files in subdirectory
|
|
67
|
+
# /path/to/file/foo
|
|
68
|
+
# heuristic: treat as directory if no wildcard and last path part has no dot
|
|
69
|
+
last_part = normalized_glob.split("/")[-1]
|
|
70
|
+
if (
|
|
71
|
+
"*" not in normalized_glob
|
|
72
|
+
and "?" not in normalized_glob
|
|
73
|
+
and "." not in last_part
|
|
74
|
+
):
|
|
75
|
+
globs.append(f"{normalized_glob}/**")
|
|
76
|
+
|
|
77
|
+
return globs
|
|
78
|
+
|
|
79
|
+
@cached_property
|
|
80
|
+
def matches_everything(self) -> bool:
|
|
81
|
+
return "*" in self.normalized_globs
|
|
82
|
+
|
|
83
|
+
@cached_property
|
|
84
|
+
def _compiled_matchers(self) -> list[re.Pattern[str]]:
|
|
85
|
+
return [_glob_regex(pattern) for pattern in self.normalized_globs]
|
|
86
|
+
|
|
87
|
+
def check_path(self, path: str) -> bool:
|
|
88
|
+
if self.matches_everything:
|
|
89
|
+
return True
|
|
90
|
+
return any(pattern.match(path) for pattern in self._compiled_matchers)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def clean_line(line: str) -> str:
|
|
94
|
+
return line.strip().split("#", 1)[0]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _rules_from_lines(lines: Iterable[str]) -> list[CodeownerRule]:
|
|
98
|
+
rules: list[CodeownerRule] = []
|
|
99
|
+
for raw_line in lines:
|
|
100
|
+
line = clean_line(raw_line)
|
|
101
|
+
parts = line.split()
|
|
102
|
+
if len(parts) > 1:
|
|
103
|
+
rules.append(CodeownerRule(glob=parts[0], owners=parts[1:]))
|
|
104
|
+
return rules
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_codeowner_rules(codeowners_path: str) -> list[CodeownerRule]:
|
|
108
|
+
with open(codeowners_path, encoding="utf-8") as codeowners_file:
|
|
109
|
+
return _rules_from_lines(codeowners_file)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_codeowner_rules_from_text(text: str) -> list[CodeownerRule]:
|
|
113
|
+
return _rules_from_lines(text.splitlines())
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def last_matching_rule(rules: list[CodeownerRule], path: str) -> CodeownerRule | None:
|
|
117
|
+
"""The rule that owns `path` under CODEOWNERS last-match-wins precedence."""
|
|
118
|
+
matching_rules = [rule for rule in rules if rule.check_path(path)]
|
|
119
|
+
return matching_rules[-1] if matching_rules else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_owners_for_file(rules: list[CodeownerRule], path: str) -> list[str]:
|
|
123
|
+
rule = last_matching_rule(rules, path)
|
|
124
|
+
return rule.owners if rule is not None else []
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def evaluate_codeowner_rules(rules: list[CodeownerRule], files: list[str]) -> set[str]:
|
|
128
|
+
return set(chain.from_iterable(get_owners_for_file(rules, file) for file in files))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_owners(codeowners_path: str, files: list[str]) -> set[str]:
|
|
132
|
+
return evaluate_codeowner_rules(get_codeowner_rules(codeowners_path), files)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_codeowner_team_slugs(*, rules: list[CodeownerRule]) -> set[str]:
|
|
136
|
+
return {
|
|
137
|
+
owner.removeprefix(TEAM_OWNER_PREFIX)
|
|
138
|
+
for rule in rules
|
|
139
|
+
for owner in rule.owners
|
|
140
|
+
if owner.startswith(TEAM_OWNER_PREFIX)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_codeowner_team_slugs_for_files(
|
|
145
|
+
*,
|
|
146
|
+
files: list[str],
|
|
147
|
+
codeowner_rules: list[CodeownerRule],
|
|
148
|
+
all_teams_lookup: Mapping[str, object],
|
|
149
|
+
) -> list[str]:
|
|
150
|
+
if len(files) == 0:
|
|
151
|
+
return []
|
|
152
|
+
codeowners = evaluate_codeowner_rules(codeowner_rules, files)
|
|
153
|
+
return [
|
|
154
|
+
slug
|
|
155
|
+
for slug in all_teams_lookup
|
|
156
|
+
if generate_team_username(team_slug=slug) in codeowners
|
|
157
|
+
]
|
pkgs/serialization/__init__.py
CHANGED
|
@@ -12,6 +12,7 @@ from .serial_alias import (
|
|
|
12
12
|
serial_alias_annotation as serial_alias_annotation,
|
|
13
13
|
)
|
|
14
14
|
from .serial_class import (
|
|
15
|
+
ParseFallback as ParseFallback,
|
|
15
16
|
SerialClassDataInspector as SerialClassDataInspector,
|
|
16
17
|
get_serial_class_data as get_serial_class_data,
|
|
17
18
|
get_serial_string_enum_data as get_serial_string_enum_data,
|
|
@@ -3,11 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from enum import StrEnum
|
|
6
|
-
from typing import Any, TypeVar, cast
|
|
6
|
+
from typing import Any, Generic, TypeVar, cast
|
|
7
7
|
|
|
8
8
|
from .annotation import SerialBase, SerialInspector, SerialType
|
|
9
9
|
|
|
10
10
|
ClassT = TypeVar("ClassT")
|
|
11
|
+
LegacyT = TypeVar("LegacyT")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
15
|
+
class ParseFallback(Generic[LegacyT]):
|
|
16
|
+
legacy_field: str
|
|
17
|
+
convert: Callable[[LegacyT], object]
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
@dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
|
|
@@ -17,6 +24,9 @@ class _SerialClassData(SerialBase):
|
|
|
17
24
|
to_string_values: set[str] = dataclasses.field(default_factory=set)
|
|
18
25
|
parse_require: set[str] = dataclasses.field(default_factory=set)
|
|
19
26
|
emit_api_overrides: dict[str, str] = dataclasses.field(default_factory=dict)
|
|
27
|
+
parse_fallbacks: dict[str, ParseFallback[Any]] = dataclasses.field(
|
|
28
|
+
default_factory=dict
|
|
29
|
+
)
|
|
20
30
|
named_type_path: str | None = None
|
|
21
31
|
|
|
22
32
|
|
|
@@ -30,6 +40,7 @@ def serial_class(
|
|
|
30
40
|
to_string_values: set[str] | None = None,
|
|
31
41
|
parse_require: set[str] | None = None,
|
|
32
42
|
emit_api_overrides: dict[str, str] | None = None,
|
|
43
|
+
parse_fallbacks: dict[str, ParseFallback[Any]] | None = None,
|
|
33
44
|
named_type_path: str | None = None,
|
|
34
45
|
is_dynamic_allowed: bool = False,
|
|
35
46
|
) -> Callable[[ClassT], ClassT]:
|
|
@@ -59,6 +70,12 @@ def serial_class(
|
|
|
59
70
|
storage encoding are unaffected. Replaces the default camelCase
|
|
60
71
|
conversion. Cannot be combined with `unconverted_keys` for the
|
|
61
72
|
same field; doing so raises at parse and serialize time.
|
|
73
|
+
@param parse_fallbacks
|
|
74
|
+
Maps Python field name -> a legacy field that the parser reads when
|
|
75
|
+
the input omits the field or sets it to null. The parser parses the
|
|
76
|
+
legacy value with the type of the first parameter of `convert`, and
|
|
77
|
+
stores the result of `convert` in the field. The legacy field is not
|
|
78
|
+
part of the type, so serialization never writes it.
|
|
62
79
|
@param named_type_path
|
|
63
80
|
The type_spec type-path to this type. This applies only to named types.
|
|
64
81
|
"""
|
|
@@ -70,6 +87,7 @@ def serial_class(
|
|
|
70
87
|
to_string_values=to_string_values or set(),
|
|
71
88
|
parse_require=parse_require or set(),
|
|
72
89
|
emit_api_overrides=emit_api_overrides or {},
|
|
90
|
+
parse_fallbacks=parse_fallbacks or {},
|
|
73
91
|
named_type_path=named_type_path,
|
|
74
92
|
from_decorator=True,
|
|
75
93
|
is_dynamic_allowed=is_dynamic_allowed,
|
|
@@ -103,6 +121,9 @@ class SerialClassDataInspector(SerialInspector[ClassT]):
|
|
|
103
121
|
def get_emit_api_override(self, key: str) -> str | None:
|
|
104
122
|
return self.current.emit_api_overrides.get(key)
|
|
105
123
|
|
|
124
|
+
def get_parse_fallbacks(self) -> dict[str, ParseFallback[Any]]:
|
|
125
|
+
return self.current.parse_fallbacks
|
|
126
|
+
|
|
106
127
|
|
|
107
128
|
def get_merged_serial_class_data(
|
|
108
129
|
type_class: SerialType[Any],
|
|
@@ -136,6 +157,10 @@ def get_merged_serial_class_data(
|
|
|
136
157
|
**curr_base_class_data.emit_api_overrides,
|
|
137
158
|
**base_class_data.emit_api_overrides,
|
|
138
159
|
},
|
|
160
|
+
parse_fallbacks={
|
|
161
|
+
**curr_base_class_data.parse_fallbacks,
|
|
162
|
+
**base_class_data.parse_fallbacks,
|
|
163
|
+
},
|
|
139
164
|
)
|
|
140
165
|
return base_class_data
|
|
141
166
|
|