UncountablePythonSDK 0.0.180__py3-none-any.whl → 0.0.181__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.
- examples/integration-server/pyproject.toml +2 -2
- pkgs/argument_parser/parser_inner.py +15 -1
- pkgs/type_spec/__main__.py +4 -0
- pkgs/type_spec/actions_registry/__main__.py +1 -0
- pkgs/type_spec/actions_registry/emit_python.py +9 -0
- pkgs/type_spec/actions_registry/emit_typescript.py +12 -0
- pkgs/type_spec/builder.py +106 -24
- pkgs/type_spec/config.py +40 -0
- pkgs/type_spec/emit_python.py +10 -1
- pkgs/type_spec/emit_scss.py +42 -0
- pkgs/type_spec/emit_typescript_util.py +23 -6
- pkgs/type_spec/type_info/emit_type_info.py +5 -1
- uncountable/core/environment.py +4 -0
- uncountable/integration/http_server/types.py +12 -0
- uncountable/integration/queue_runner/job_scheduler.py +1 -1
- uncountable/integration/webhook_server/admin_auth.py +38 -0
- uncountable/integration/webhook_server/admin_routes.py +49 -0
- uncountable/integration/webhook_server/entrypoint.py +2 -0
- uncountable/types/__init__.py +8 -0
- uncountable/types/api/ml/__init__.py +1 -0
- uncountable/types/api/ml/get_analyze_results.py +56 -0
- uncountable/types/api/ml/get_suggest_results.py +69 -0
- uncountable/types/api/ml/submit_async_job.py +231 -0
- uncountable/types/client_base.py +64 -0
- uncountable/types/entity_t.py +9 -1
- uncountable/types/ml_reports.py +15 -0
- uncountable/types/ml_reports_t.py +138 -0
- uncountable/types/specs.py +4 -0
- uncountable/types/specs_t.py +50 -0
- {uncountablepythonsdk-0.0.180.dist-info → uncountablepythonsdk-0.0.181.dist-info}/METADATA +1 -1
- {uncountablepythonsdk-0.0.180.dist-info → uncountablepythonsdk-0.0.181.dist-info}/RECORD +33 -24
- {uncountablepythonsdk-0.0.180.dist-info → uncountablepythonsdk-0.0.181.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.180.dist-info → uncountablepythonsdk-0.0.181.dist-info}/top_level.txt +0 -0
|
@@ -9,7 +9,7 @@ dependencies = [
|
|
|
9
9
|
"ruff == 0.*",
|
|
10
10
|
"openpyxl == 3.*",
|
|
11
11
|
"more_itertools ==11.*",
|
|
12
|
-
"types-paramiko ==5.0.0.
|
|
12
|
+
"types-paramiko ==5.0.0.20260724",
|
|
13
13
|
"types-openpyxl == 3.*",
|
|
14
14
|
"types-pytz ==2026.*",
|
|
15
15
|
"types-requests == 2.*",
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
"pandas-stubs",
|
|
18
18
|
"xlrd == 2.*",
|
|
19
19
|
"msgspec ==0.21.*",
|
|
20
|
-
"websockets==
|
|
20
|
+
"websockets==17.0",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[tool.mypy]
|
|
@@ -560,9 +560,23 @@ def _build_parser_dataclass(
|
|
|
560
560
|
type_params = getattr(parsed_type, "__parameters__", None)
|
|
561
561
|
cache_key: typing.Any = parsed_type
|
|
562
562
|
if type_params is not None and type_var_map:
|
|
563
|
+
# WARNING: Do not widen this back to the whole `type_var_map`. Only a class's
|
|
564
|
+
# own parameters can appear in its field annotations, so only their bindings
|
|
565
|
+
# distinguish one instantiation's parser from another; the map additionally
|
|
566
|
+
# carries every enclosing generic's TypeVars, which are irrelevant here.
|
|
567
|
+
#
|
|
568
|
+
# Widening it does not merely lose cache hits. Each generic class declares its
|
|
569
|
+
# own TypeVar, so a set of mutually recursive generics contributes a distinct
|
|
570
|
+
# one per level as the build descends. Keyed on the whole map, no key ever
|
|
571
|
+
# repeats, nothing is ever reused, and parser construction grows exponentially
|
|
572
|
+
# in the number of participating generics rather than terminating.
|
|
563
573
|
cache_key = (
|
|
564
574
|
parsed_type,
|
|
565
|
-
tuple(
|
|
575
|
+
tuple(
|
|
576
|
+
(param, type_var_map[param])
|
|
577
|
+
for param in type_params
|
|
578
|
+
if param in type_var_map
|
|
579
|
+
),
|
|
566
580
|
)
|
|
567
581
|
|
|
568
582
|
cur_parser = context.cache.get(cache_key)
|
pkgs/type_spec/__main__.py
CHANGED
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
from .config import parse_yaml_config
|
|
5
5
|
from .emit_open_api import emit_open_api
|
|
6
6
|
from .emit_python import emit_python
|
|
7
|
+
from .emit_scss import emit_scss
|
|
7
8
|
from .emit_typescript import emit_typescript
|
|
8
9
|
from .load_types import load_types
|
|
9
10
|
|
|
@@ -34,6 +35,9 @@ def main() -> bool:
|
|
|
34
35
|
if config.typescript is not None:
|
|
35
36
|
emit_typescript(builder, config.typescript)
|
|
36
37
|
|
|
38
|
+
if config.scss is not None:
|
|
39
|
+
emit_scss(builder, config.scss)
|
|
40
|
+
|
|
37
41
|
return True
|
|
38
42
|
|
|
39
43
|
|
|
@@ -22,6 +22,11 @@ def _module_enum_member(module: str) -> str:
|
|
|
22
22
|
return module.upper()
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _emit_search_aliases(search_aliases: list[str]) -> str:
|
|
26
|
+
items = ", ".join(_py_str(alias) for alias in search_aliases)
|
|
27
|
+
return f"[{items}]"
|
|
28
|
+
|
|
29
|
+
|
|
25
30
|
def _emit_visibility_scope(
|
|
26
31
|
visibility_scope: list[actions_registry_t.ActionDefinitionVisibilityScope],
|
|
27
32
|
) -> str:
|
|
@@ -58,6 +63,10 @@ def _emit_action_definition(
|
|
|
58
63
|
out.write(
|
|
59
64
|
f"{sub_indent}kind=actions_registry_t.ActionDefinitionKind.{action.kind.name},\n"
|
|
60
65
|
)
|
|
66
|
+
if action.search_aliases is not None and len(action.search_aliases) > 0:
|
|
67
|
+
out.write(
|
|
68
|
+
f"{sub_indent}search_aliases={_emit_search_aliases(action.search_aliases)},\n"
|
|
69
|
+
)
|
|
61
70
|
if action.arguments is not None:
|
|
62
71
|
out.write(f"{sub_indent}arguments={_py_str(action.arguments)},\n")
|
|
63
72
|
out.write(f"{indent}),\n")
|
|
@@ -151,6 +151,13 @@ def _emit_action_definition(
|
|
|
151
151
|
out.write(
|
|
152
152
|
f"{sub_indent}kind: ActionsRegistryT.ActionDefinitionKind.{ts_name(action_definition.kind, name_case=builder.NameCase.convert)},\n"
|
|
153
153
|
)
|
|
154
|
+
if (
|
|
155
|
+
action_definition.search_aliases is not None
|
|
156
|
+
and len(action_definition.search_aliases) > 0
|
|
157
|
+
):
|
|
158
|
+
out.write(
|
|
159
|
+
f"{sub_indent}searchAliases: {_emit_search_aliases(action_definition.search_aliases)},\n"
|
|
160
|
+
)
|
|
154
161
|
out.write(f"{indent}}} as ActionsRegistryT.ActionDefinition{argument_type},\n")
|
|
155
162
|
return out.getvalue()
|
|
156
163
|
|
|
@@ -174,6 +181,11 @@ def _emit_action_definition_types(modules: list[str], indent: str) -> str:
|
|
|
174
181
|
return out.getvalue()
|
|
175
182
|
|
|
176
183
|
|
|
184
|
+
def _emit_search_aliases(search_aliases: list[str]) -> str:
|
|
185
|
+
aliases = ",".join(encode_common_string(alias) for alias in search_aliases)
|
|
186
|
+
return f"[ {aliases} ]"
|
|
187
|
+
|
|
188
|
+
|
|
177
189
|
def _emit_visibility_scope(
|
|
178
190
|
visibility_scope: list[actions_registry_t.ActionDefinitionVisibilityScope],
|
|
179
191
|
) -> str:
|
pkgs/type_spec/builder.py
CHANGED
|
@@ -517,7 +517,9 @@ class SpecTypeDefn(SpecType):
|
|
|
517
517
|
class SpecTypeGenericParameter(SpecType):
|
|
518
518
|
def __init__(
|
|
519
519
|
self,
|
|
520
|
-
spec_type_definition: SpecTypeDefnObject
|
|
520
|
+
spec_type_definition: SpecTypeDefnObject
|
|
521
|
+
| SpecTypeDefnUnion
|
|
522
|
+
| SpecTypeDefnAlias,
|
|
521
523
|
name: str,
|
|
522
524
|
) -> None:
|
|
523
525
|
self.spec_type_definition = spec_type_definition
|
|
@@ -546,6 +548,8 @@ class SpecTypeDefnObject(SpecTypeDefn):
|
|
|
546
548
|
base: SpecTypeDefnObject | None
|
|
547
549
|
parameters: list[str]
|
|
548
550
|
base_type_instance: SpecTypeInstance | None
|
|
551
|
+
# Set by _mark_recursive_instantiations; see is_eagerly_self_referential.
|
|
552
|
+
needs_lazy_emission: bool = False
|
|
549
553
|
|
|
550
554
|
def __init__(
|
|
551
555
|
self,
|
|
@@ -652,22 +656,26 @@ class SpecTypeDefnObject(SpecTypeDefn):
|
|
|
652
656
|
|
|
653
657
|
class SpecTypeDefnAlias(SpecTypeDefn):
|
|
654
658
|
alias: SpecType
|
|
659
|
+
parameters: list[str]
|
|
655
660
|
|
|
656
661
|
def __init__(
|
|
657
662
|
self,
|
|
658
663
|
namespace: SpecNamespace,
|
|
659
664
|
name: str,
|
|
665
|
+
*,
|
|
666
|
+
parameters: list[str] | None = None,
|
|
660
667
|
) -> None:
|
|
661
668
|
super().__init__(
|
|
662
669
|
namespace,
|
|
663
670
|
name,
|
|
664
671
|
)
|
|
672
|
+
self.parameters = parameters if parameters is not None else []
|
|
665
673
|
self.desc: str | None = None
|
|
666
674
|
self.discriminator: str | None = None
|
|
667
675
|
|
|
668
676
|
def process(self, builder: SpecBuilder, data: RawDict) -> None:
|
|
669
677
|
super().base_process(builder, data, ["type", "desc", "alias", "discriminator"])
|
|
670
|
-
self.alias = builder.parse_type(self.namespace, data["alias"])
|
|
678
|
+
self.alias = builder.parse_type(self.namespace, data["alias"], scope=self)
|
|
671
679
|
self.desc = data.get("desc", None)
|
|
672
680
|
self.discriminator = data.get("discriminator", None)
|
|
673
681
|
|
|
@@ -1447,19 +1455,19 @@ class SpecNamespace:
|
|
|
1447
1455
|
defn_type = defn.get("type")
|
|
1448
1456
|
assert isinstance(defn_type, str), f"{name} requires a string type"
|
|
1449
1457
|
spec_type: SpecTypeDefn
|
|
1458
|
+
named_parameters = (
|
|
1459
|
+
[
|
|
1460
|
+
parameter.name
|
|
1461
|
+
for name_parameters in parsed_name.parameters
|
|
1462
|
+
for parameter in name_parameters
|
|
1463
|
+
]
|
|
1464
|
+
if parsed_name.parameters is not None
|
|
1465
|
+
else None
|
|
1466
|
+
)
|
|
1450
1467
|
if defn_type == DefnTypeName.s_alias:
|
|
1451
|
-
spec_type = SpecTypeDefnAlias(self, name)
|
|
1468
|
+
spec_type = SpecTypeDefnAlias(self, name, parameters=named_parameters)
|
|
1452
1469
|
elif defn_type == DefnTypeName.s_union:
|
|
1453
|
-
|
|
1454
|
-
[
|
|
1455
|
-
parameter.name
|
|
1456
|
-
for name_parameters in parsed_name.parameters
|
|
1457
|
-
for parameter in name_parameters
|
|
1458
|
-
]
|
|
1459
|
-
if parsed_name.parameters is not None
|
|
1460
|
-
else None
|
|
1461
|
-
)
|
|
1462
|
-
spec_type = SpecTypeDefnUnion(self, name, parameters=union_parameters)
|
|
1470
|
+
spec_type = SpecTypeDefnUnion(self, name, parameters=named_parameters)
|
|
1463
1471
|
elif defn_type == DefnTypeName.s_external:
|
|
1464
1472
|
spec_type = SpecTypeDefnExternal(self, name)
|
|
1465
1473
|
elif defn_type == DefnTypeName.s_new_type:
|
|
@@ -1467,19 +1475,10 @@ class SpecNamespace:
|
|
|
1467
1475
|
elif defn_type == DefnTypeName.s_string_enum:
|
|
1468
1476
|
spec_type = SpecTypeDefnStringEnum(self, name)
|
|
1469
1477
|
else:
|
|
1470
|
-
parameters = (
|
|
1471
|
-
[
|
|
1472
|
-
parameter.name
|
|
1473
|
-
for name_parameters in parsed_name.parameters
|
|
1474
|
-
for parameter in name_parameters
|
|
1475
|
-
]
|
|
1476
|
-
if parsed_name.parameters is not None
|
|
1477
|
-
else None
|
|
1478
|
-
)
|
|
1479
1478
|
spec_type = SpecTypeDefnObject(
|
|
1480
1479
|
self,
|
|
1481
1480
|
name,
|
|
1482
|
-
parameters=
|
|
1481
|
+
parameters=named_parameters,
|
|
1483
1482
|
)
|
|
1484
1483
|
self.types[name] = spec_type
|
|
1485
1484
|
|
|
@@ -1549,6 +1548,62 @@ class NameDataPair:
|
|
|
1549
1548
|
data: RawDict
|
|
1550
1549
|
|
|
1551
1550
|
|
|
1551
|
+
def is_eagerly_self_referential(target: SpecTypeDefn, current: SpecType) -> bool:
|
|
1552
|
+
"""
|
|
1553
|
+
Does rendering `current` name `target` in a position evaluated eagerly.
|
|
1554
|
+
|
|
1555
|
+
A generic instantiation renders its parameters inline (`Container[Target]`), so a
|
|
1556
|
+
definition reachable that way is resolved at assignment time. Object properties are
|
|
1557
|
+
deferred in both emitted languages, so this stops at object definitions rather than
|
|
1558
|
+
descending into them.
|
|
1559
|
+
"""
|
|
1560
|
+
if current is target:
|
|
1561
|
+
return True
|
|
1562
|
+
if isinstance(current, SpecTypeInstance):
|
|
1563
|
+
return is_eagerly_self_referential(target, current.defn_type) or any(
|
|
1564
|
+
is_eagerly_self_referential(target, parameter)
|
|
1565
|
+
for parameter in current.parameters
|
|
1566
|
+
)
|
|
1567
|
+
return False
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def _collect_objects_on_cycle(
|
|
1571
|
+
target: SpecTypeDefn,
|
|
1572
|
+
current: SpecType,
|
|
1573
|
+
found: set[SpecTypeDefnObject],
|
|
1574
|
+
seen: set[int],
|
|
1575
|
+
) -> None:
|
|
1576
|
+
"""
|
|
1577
|
+
Collect the objects whose instantiation is what closes the cycle back on `target`.
|
|
1578
|
+
|
|
1579
|
+
Only those need lazy emission. An instantiation that appears alongside them but
|
|
1580
|
+
does not name `target` — `Container<String>` in the same union — is not on the
|
|
1581
|
+
cycle and emits as a type alias like any other generic. The synthetic backing
|
|
1582
|
+
objects of unions and lists are skipped for the same reason: they are never
|
|
1583
|
+
emitted as objects.
|
|
1584
|
+
"""
|
|
1585
|
+
if id(current) in seen:
|
|
1586
|
+
return
|
|
1587
|
+
seen.add(id(current))
|
|
1588
|
+
if isinstance(current, SpecTypeInstance):
|
|
1589
|
+
if (
|
|
1590
|
+
isinstance(current.defn_type, SpecTypeDefnObject)
|
|
1591
|
+
and not current.defn_type.is_base
|
|
1592
|
+
and current.parameters
|
|
1593
|
+
and is_eagerly_self_referential(target, current)
|
|
1594
|
+
):
|
|
1595
|
+
found.add(current.defn_type)
|
|
1596
|
+
_collect_objects_on_cycle(target, current.defn_type, found, seen)
|
|
1597
|
+
for parameter in current.parameters:
|
|
1598
|
+
_collect_objects_on_cycle(target, parameter, found, seen)
|
|
1599
|
+
return
|
|
1600
|
+
if isinstance(current, SpecTypeDefnAlias):
|
|
1601
|
+
_collect_objects_on_cycle(target, current.alias, found, seen)
|
|
1602
|
+
return
|
|
1603
|
+
if isinstance(current, SpecTypeDefnUnion):
|
|
1604
|
+
_collect_objects_on_cycle(target, current.get_backing_type(), found, seen)
|
|
1605
|
+
|
|
1606
|
+
|
|
1552
1607
|
class SpecBuilder:
|
|
1553
1608
|
def __init__(
|
|
1554
1609
|
self,
|
|
@@ -1651,8 +1706,33 @@ class SpecBuilder:
|
|
|
1651
1706
|
print(self.where)
|
|
1652
1707
|
raise
|
|
1653
1708
|
|
|
1709
|
+
self._mark_recursive_instantiations()
|
|
1654
1710
|
return True
|
|
1655
1711
|
|
|
1712
|
+
def _mark_recursive_instantiations(self) -> None:
|
|
1713
|
+
"""
|
|
1714
|
+
Flag objects instantiated inside a self-referential alias or union.
|
|
1715
|
+
|
|
1716
|
+
TypeScript expands a generic type alias eagerly, so such an instantiation is
|
|
1717
|
+
rejected as circular unless the instantiated object emits as an interface,
|
|
1718
|
+
whose members resolve lazily. Only objects on that cycle need it, so the
|
|
1719
|
+
remaining generics keep their existing emission.
|
|
1720
|
+
"""
|
|
1721
|
+
for namespace in self.namespaces.values():
|
|
1722
|
+
for defn in namespace.types.values():
|
|
1723
|
+
if isinstance(defn, SpecTypeDefnAlias):
|
|
1724
|
+
expression: SpecType = defn.alias
|
|
1725
|
+
elif isinstance(defn, SpecTypeDefnUnion):
|
|
1726
|
+
expression = defn.get_backing_type()
|
|
1727
|
+
else:
|
|
1728
|
+
continue
|
|
1729
|
+
if not is_eagerly_self_referential(defn, expression):
|
|
1730
|
+
continue
|
|
1731
|
+
found: set[SpecTypeDefnObject] = set()
|
|
1732
|
+
_collect_objects_on_cycle(defn, expression, found, set())
|
|
1733
|
+
for obj in found:
|
|
1734
|
+
obj.needs_lazy_emission = True
|
|
1735
|
+
|
|
1656
1736
|
def get_type_of_literal(self, value: util.LiteralTypeValue) -> SpecType:
|
|
1657
1737
|
if isinstance(value, str):
|
|
1658
1738
|
return self.namespaces[base_namespace_name].types[BaseTypeName.s_string]
|
|
@@ -1708,7 +1788,9 @@ class SpecBuilder:
|
|
|
1708
1788
|
if (
|
|
1709
1789
|
defn_type is None
|
|
1710
1790
|
and scope is not None
|
|
1711
|
-
and isinstance(
|
|
1791
|
+
and isinstance(
|
|
1792
|
+
scope, (SpecTypeDefnObject, SpecTypeDefnUnion, SpecTypeDefnAlias)
|
|
1793
|
+
)
|
|
1712
1794
|
):
|
|
1713
1795
|
if path[0].name in scope.parameters:
|
|
1714
1796
|
return SpecTypeGenericParameter(
|
pkgs/type_spec/config.py
CHANGED
|
@@ -115,6 +115,36 @@ class OpenAPIConfig(BaseLanguageConfig):
|
|
|
115
115
|
self.types_output = os.path.abspath(self.types_output)
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
@dataclass(kw_only=True)
|
|
119
|
+
class ScssConstantOutput:
|
|
120
|
+
namespace: str # namespace holding the constant to emit
|
|
121
|
+
constant: str # nested Dict constant to flatten into $prefix-<group>-<field> vars
|
|
122
|
+
output: str # scss file to write the flattened variables to
|
|
123
|
+
prefix: str # variable name prefix, e.g. "unc" for $unc-<group>-<field>
|
|
124
|
+
|
|
125
|
+
def __post_init__(self: Self) -> None:
|
|
126
|
+
self.output = os.path.abspath(self.output)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _parse_scss_output(entry: object) -> ScssConstantOutput:
|
|
130
|
+
assert isinstance(entry, dict), (
|
|
131
|
+
f"scss config entry must be a mapping, got {entry!r}"
|
|
132
|
+
)
|
|
133
|
+
fields: dict[str, str] = {}
|
|
134
|
+
for key in ("namespace", "constant", "output", "prefix"):
|
|
135
|
+
value = entry.get(key)
|
|
136
|
+
assert isinstance(value, str), (
|
|
137
|
+
f"scss config entry requires string '{key}', got {value!r}"
|
|
138
|
+
)
|
|
139
|
+
fields[key] = value
|
|
140
|
+
return ScssConstantOutput(
|
|
141
|
+
namespace=fields["namespace"],
|
|
142
|
+
constant=fields["constant"],
|
|
143
|
+
output=fields["output"],
|
|
144
|
+
prefix=fields["prefix"],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
118
148
|
@dataclass(kw_only=True)
|
|
119
149
|
class Config:
|
|
120
150
|
top_namespace: str
|
|
@@ -124,6 +154,7 @@ class Config:
|
|
|
124
154
|
typescript: TypeScriptConfig | None
|
|
125
155
|
python: PythonConfig
|
|
126
156
|
open_api: OpenAPIConfig | None
|
|
157
|
+
scss: list[ScssConstantOutput] | None
|
|
127
158
|
|
|
128
159
|
|
|
129
160
|
T = TypeVar("T")
|
|
@@ -164,6 +195,14 @@ def parse_yaml_config(config_file: str) -> Config:
|
|
|
164
195
|
else None
|
|
165
196
|
)
|
|
166
197
|
|
|
198
|
+
raw_scss = raw_config.get("scss")
|
|
199
|
+
scss = None
|
|
200
|
+
if raw_scss is not None:
|
|
201
|
+
assert isinstance(raw_scss, list), (
|
|
202
|
+
f"scss config must be a list, got {raw_scss!r}"
|
|
203
|
+
)
|
|
204
|
+
scss = [_parse_scss_output(entry) for entry in raw_scss]
|
|
205
|
+
|
|
167
206
|
top_namespace = raw_config["top_namespace"]
|
|
168
207
|
assert isinstance(top_namespace, str)
|
|
169
208
|
|
|
@@ -174,4 +213,5 @@ def parse_yaml_config(config_file: str) -> Config:
|
|
|
174
213
|
typescript=typescript,
|
|
175
214
|
python=python,
|
|
176
215
|
open_api=open_api,
|
|
216
|
+
scss=scss,
|
|
177
217
|
)
|
pkgs/type_spec/emit_python.py
CHANGED
|
@@ -947,7 +947,14 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
|
|
|
947
947
|
|
|
948
948
|
if isinstance(stype, builder.SpecTypeDefnAlias):
|
|
949
949
|
ctx.use_serial_alias = True
|
|
950
|
-
|
|
950
|
+
if len(stype.parameters) > 0:
|
|
951
|
+
type_params = f"[{', '.join(stype.parameters)}]"
|
|
952
|
+
assignment = f"type {stype.name}{type_params}"
|
|
953
|
+
elif builder.is_eagerly_self_referential(stype, stype.alias):
|
|
954
|
+
assignment = f"type {stype.name}"
|
|
955
|
+
else:
|
|
956
|
+
assignment = stype.name
|
|
957
|
+
ctx.out.write(f"{assignment} = typing.Annotated[\n")
|
|
951
958
|
ctx.out.write(f"{INDENT}{refer_to(ctx, stype.alias)},\n")
|
|
952
959
|
ctx.out.write(f"{INDENT}serial_alias_annotation(\n")
|
|
953
960
|
ctx.out.write(
|
|
@@ -964,6 +971,8 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
|
|
|
964
971
|
if len(stype.parameters) > 0:
|
|
965
972
|
type_params = f"[{', '.join(stype.parameters)}]"
|
|
966
973
|
ctx.out.write(f"type {stype.name}{type_params} = typing.Annotated[\n")
|
|
974
|
+
elif builder.is_eagerly_self_referential(stype, stype.get_backing_type()):
|
|
975
|
+
ctx.out.write(f"type {stype.name} = typing.Annotated[\n")
|
|
967
976
|
else:
|
|
968
977
|
ctx.out.write(f"{stype.name} = typing.Annotated[\n")
|
|
969
978
|
ctx.out.write(f"{INDENT}{refer_to(ctx, stype.get_backing_type())},\n")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
from . import builder, util
|
|
4
|
+
from .config import ScssConstantOutput
|
|
5
|
+
|
|
6
|
+
_HEADER = (
|
|
7
|
+
"// DO NOT MODIFY -- This file is generated by type_spec\n"
|
|
8
|
+
"// Edit the source constant in main/type_spec/ and run `just typespec regen-types`\n"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def emit_scss(
|
|
13
|
+
spec_builder: builder.SpecBuilder, configs: list[ScssConstantOutput]
|
|
14
|
+
) -> None:
|
|
15
|
+
blocks_by_output: dict[str, list[str]] = defaultdict(list)
|
|
16
|
+
for config in configs:
|
|
17
|
+
namespace = spec_builder.namespaces[config.namespace]
|
|
18
|
+
sconst = namespace.constants[config.constant]
|
|
19
|
+
blocks_by_output[config.output].append(_render_constant(sconst, config.prefix))
|
|
20
|
+
for output, blocks in blocks_by_output.items():
|
|
21
|
+
util.rewrite_file(output, _HEADER + "\n" + "\n\n".join(blocks) + "\n")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _render_constant(sconst: builder.SpecConstant, prefix: str) -> str:
|
|
25
|
+
value = sconst.value
|
|
26
|
+
assert isinstance(value, dict), "scss emit expects a nested Dict constant"
|
|
27
|
+
|
|
28
|
+
blocks: list[str] = []
|
|
29
|
+
for group, shades in value.items():
|
|
30
|
+
assert isinstance(shades, dict), (
|
|
31
|
+
"scss emit expects Dict<Key, Dict|Object> values"
|
|
32
|
+
)
|
|
33
|
+
lines = [
|
|
34
|
+
f"${_scss_name(prefix, str(group), str(field))}: {color};"
|
|
35
|
+
for field, color in shades.items()
|
|
36
|
+
]
|
|
37
|
+
blocks.append("\n".join(lines))
|
|
38
|
+
return "\n\n".join(blocks)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _scss_name(prefix: str, group: str, field: str) -> str:
|
|
42
|
+
return f"{prefix}-{group}-{field}".replace("_", "-")
|
|
@@ -193,7 +193,11 @@ def emit_type_ts(ctx: EmitTypescriptContext, stype: builder.SpecType) -> None:
|
|
|
193
193
|
return
|
|
194
194
|
|
|
195
195
|
if isinstance(stype, builder.SpecTypeDefnAlias):
|
|
196
|
-
|
|
196
|
+
if len(stype.parameters) > 0:
|
|
197
|
+
full_type_name = f"{stype.name}<{', '.join(stype.parameters)}>"
|
|
198
|
+
else:
|
|
199
|
+
full_type_name = stype.name
|
|
200
|
+
ctx.out.write(f"export type {full_type_name} = {refer_to(ctx, stype.alias)}\n")
|
|
197
201
|
return
|
|
198
202
|
|
|
199
203
|
if isinstance(stype, builder.SpecTypeDefnUnion):
|
|
@@ -219,23 +223,36 @@ def emit_type_ts(ctx: EmitTypescriptContext, stype: builder.SpecType) -> None:
|
|
|
219
223
|
assert isinstance(stype, builder.SpecTypeDefnObject)
|
|
220
224
|
assert stype.base is not None
|
|
221
225
|
|
|
222
|
-
|
|
226
|
+
base_ref = ""
|
|
227
|
+
parenthesized_base = ""
|
|
223
228
|
if not stype.base.is_base:
|
|
224
229
|
if stype.base_type_instance is not None:
|
|
225
|
-
|
|
230
|
+
base_ref = refer_to(ctx, stype.base_type_instance)
|
|
231
|
+
parenthesized_base = f"({base_ref})"
|
|
226
232
|
else:
|
|
227
|
-
|
|
233
|
+
base_ref = refer_to(ctx, stype.base)
|
|
234
|
+
parenthesized_base = base_ref
|
|
235
|
+
base_type = f"{parenthesized_base} & " if base_ref else ""
|
|
228
236
|
|
|
229
237
|
if stype.properties is None and base_type == "":
|
|
230
238
|
ctx.out.write(f"export type {stype.name} = TEmpty\n")
|
|
231
239
|
elif stype.properties is None:
|
|
232
240
|
ctx.out.write(f"export type {stype.name} = {base_type}{{}}\n")
|
|
233
241
|
else:
|
|
234
|
-
if
|
|
242
|
+
if len(stype.parameters) > 0:
|
|
235
243
|
full_type_name = f"{stype.name}<{', '.join(stype.parameters)}>"
|
|
236
244
|
else:
|
|
237
245
|
full_type_name = stype.name
|
|
238
|
-
|
|
246
|
+
if stype.needs_lazy_emission:
|
|
247
|
+
# WARNING: Must not go back to a type alias. TypeScript expands a
|
|
248
|
+
# generic type alias eagerly and rejects the cycle with TS2456;
|
|
249
|
+
# interface members resolve lazily. Kept to on-cycle objects because
|
|
250
|
+
# an interface has no implicit index signature, so it is not
|
|
251
|
+
# assignable where a type alias is.
|
|
252
|
+
extends = f" extends {base_ref}" if base_ref else ""
|
|
253
|
+
ctx.out.write(f"export interface {full_type_name}{extends} {{")
|
|
254
|
+
else:
|
|
255
|
+
ctx.out.write(f"export type {full_type_name} = {base_type}{{")
|
|
239
256
|
ctx.out.write("\n")
|
|
240
257
|
for prop in stype.properties.values():
|
|
241
258
|
ref_type = refer_to(ctx, prop.spec_type)
|
|
@@ -149,7 +149,9 @@ def emit_type_info_python(build: builder.SpecBuilder, output: str) -> None:
|
|
|
149
149
|
|
|
150
150
|
for namespace, data in type_map.namespaces.items():
|
|
151
151
|
serial = serialize_for_storage(asdict_for_yaml_dump(data))
|
|
152
|
-
yaml_content = yaml.dump(
|
|
152
|
+
yaml_content = yaml.dump(
|
|
153
|
+
serial, default_flow_style=False, sort_keys=True, Dumper=yaml.CDumper
|
|
154
|
+
)
|
|
153
155
|
util.rewrite_file(f"{output}/namespace_{namespace}.yaml", yaml_content)
|
|
154
156
|
|
|
155
157
|
|
|
@@ -451,6 +453,7 @@ def _build_map_type(
|
|
|
451
453
|
|
|
452
454
|
if isinstance(stype, builder.SpecTypeDefnAlias):
|
|
453
455
|
ext_info = _parse_ext_info(stype.ext_info)
|
|
456
|
+
alias_params = stype.parameters if len(stype.parameters) > 0 else None
|
|
454
457
|
return MapTypeAlias(
|
|
455
458
|
type_name=stype.name,
|
|
456
459
|
label=stype.label,
|
|
@@ -458,6 +461,7 @@ def _build_map_type(
|
|
|
458
461
|
alias_type_path=type_path_of(stype.alias),
|
|
459
462
|
ext_info=_parse_ext_info(stype.ext_info),
|
|
460
463
|
discriminator=stype.discriminator,
|
|
464
|
+
parameters=alias_params,
|
|
461
465
|
)
|
|
462
466
|
|
|
463
467
|
if isinstance(stype, builder.SpecTypeDefnUnion):
|
uncountable/core/environment.py
CHANGED
|
@@ -26,6 +26,10 @@ def get_local_admin_server_port() -> int:
|
|
|
26
26
|
return int(os.environ.get("UNC_ADMIN_SERVER_PORT", "50051"))
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def get_integration_admin_token() -> str | None:
|
|
30
|
+
return os.environ.get("UNC_INTEGRATION_ADMIN_TOKEN")
|
|
31
|
+
|
|
32
|
+
|
|
29
33
|
def get_otel_enabled() -> bool:
|
|
30
34
|
return os.environ.get("UNC_OTEL_ENABLED") == "true"
|
|
31
35
|
|
|
@@ -28,6 +28,18 @@ class HttpException(Exception):
|
|
|
28
28
|
def body_parse_error() -> "HttpException":
|
|
29
29
|
return HttpException(error_code=400, message="body parse error")
|
|
30
30
|
|
|
31
|
+
@staticmethod
|
|
32
|
+
def unauthorized() -> "HttpException":
|
|
33
|
+
return HttpException(error_code=401, message="unauthorized")
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def service_unavailable() -> "HttpException":
|
|
37
|
+
return HttpException(error_code=503, message="service unavailable")
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def bad_gateway() -> "HttpException":
|
|
41
|
+
return HttpException(error_code=502, message="bad gateway")
|
|
42
|
+
|
|
31
43
|
@staticmethod
|
|
32
44
|
def unknown_error() -> "HttpException":
|
|
33
45
|
return HttpException(error_code=500, message="internal server error")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import hmac
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
import flask
|
|
6
|
+
from flask.typing import ResponseReturnValue
|
|
7
|
+
from uncountable.core.environment import get_integration_admin_token
|
|
8
|
+
from uncountable.integration.http_server import HttpException
|
|
9
|
+
|
|
10
|
+
_BEARER_PREFIX = "Bearer "
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _extract_bearer_token(authorization_header: str | None) -> str | None:
|
|
14
|
+
if authorization_header is None:
|
|
15
|
+
return None
|
|
16
|
+
if not authorization_header.startswith(_BEARER_PREFIX):
|
|
17
|
+
return None
|
|
18
|
+
return authorization_header[len(_BEARER_PREFIX) :]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def require_admin_auth(
|
|
22
|
+
view: Callable[..., ResponseReturnValue],
|
|
23
|
+
) -> Callable[..., ResponseReturnValue]:
|
|
24
|
+
@functools.wraps(view)
|
|
25
|
+
def wrapper(*args: object, **kwargs: object) -> ResponseReturnValue:
|
|
26
|
+
admin_token = get_integration_admin_token()
|
|
27
|
+
if admin_token is None:
|
|
28
|
+
raise HttpException.service_unavailable()
|
|
29
|
+
provided_token = _extract_bearer_token(
|
|
30
|
+
flask.request.headers.get("Authorization")
|
|
31
|
+
)
|
|
32
|
+
if provided_token is None or not hmac.compare_digest(
|
|
33
|
+
provided_token, admin_token
|
|
34
|
+
):
|
|
35
|
+
raise HttpException.unauthorized()
|
|
36
|
+
return view(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
return wrapper
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import flask
|
|
2
|
+
from flask.typing import ResponseReturnValue
|
|
3
|
+
from opentelemetry.trace import get_current_span
|
|
4
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
5
|
+
from uncountable.integration.http_server import HttpException
|
|
6
|
+
from uncountable.integration.queue_runner.command_server.command_client import (
|
|
7
|
+
check_health,
|
|
8
|
+
)
|
|
9
|
+
from uncountable.integration.queue_runner.command_server.types import (
|
|
10
|
+
CommandServerException,
|
|
11
|
+
)
|
|
12
|
+
from uncountable.integration.telemetry import Logger
|
|
13
|
+
from uncountable.integration.webhook_server.admin_auth import require_admin_auth
|
|
14
|
+
|
|
15
|
+
_ADMIN_HEALTH_ROUTE = "/admin/health"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register_admin_routes(app: flask.Flask) -> None:
|
|
19
|
+
server_logger = Logger(get_current_span())
|
|
20
|
+
|
|
21
|
+
@require_admin_auth
|
|
22
|
+
def handle_admin_health() -> ResponseReturnValue:
|
|
23
|
+
healthy = check_health(port=get_local_admin_server_port())
|
|
24
|
+
return flask.jsonify({"healthy": healthy})
|
|
25
|
+
|
|
26
|
+
def admin_health() -> ResponseReturnValue:
|
|
27
|
+
with server_logger.push_scope(_ADMIN_HEALTH_ROUTE):
|
|
28
|
+
try:
|
|
29
|
+
return handle_admin_health()
|
|
30
|
+
except HttpException as e:
|
|
31
|
+
return e.make_error_response()
|
|
32
|
+
except CommandServerException as e:
|
|
33
|
+
server_logger.log_exception(
|
|
34
|
+
e,
|
|
35
|
+
message="admin health check could not reach the command server",
|
|
36
|
+
)
|
|
37
|
+
return HttpException.bad_gateway().make_error_response()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
server_logger.log_exception(
|
|
40
|
+
e, message=f"unexpected error on GET {_ADMIN_HEALTH_ROUTE}"
|
|
41
|
+
)
|
|
42
|
+
return HttpException.unknown_error().make_error_response()
|
|
43
|
+
|
|
44
|
+
app.add_url_rule(
|
|
45
|
+
_ADMIN_HEALTH_ROUTE,
|
|
46
|
+
endpoint="admin_health",
|
|
47
|
+
view_func=admin_health,
|
|
48
|
+
methods=["GET"],
|
|
49
|
+
)
|