tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__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.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +72 -0
- tigrbl/api/api_spec.py +30 -0
- tigrbl/api/mro_collect.py +43 -0
- tigrbl/api/shortcuts.py +56 -0
- tigrbl/api/tigrbl_api.py +286 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +61 -0
- tigrbl/app/app_spec.py +42 -0
- tigrbl/app/mro_collect.py +67 -0
- tigrbl/app/shortcuts.py +65 -0
- tigrbl/app/tigrbl_app.py +314 -0
- tigrbl/bindings/__init__.py +73 -0
- tigrbl/bindings/api/__init__.py +12 -0
- tigrbl/bindings/api/common.py +109 -0
- tigrbl/bindings/api/include.py +256 -0
- tigrbl/bindings/api/resource_proxy.py +149 -0
- tigrbl/bindings/api/rpc.py +111 -0
- tigrbl/bindings/columns.py +49 -0
- tigrbl/bindings/handlers/__init__.py +11 -0
- tigrbl/bindings/handlers/builder.py +119 -0
- tigrbl/bindings/handlers/ctx.py +74 -0
- tigrbl/bindings/handlers/identifiers.py +228 -0
- tigrbl/bindings/handlers/namespaces.py +51 -0
- tigrbl/bindings/handlers/steps.py +276 -0
- tigrbl/bindings/hooks.py +311 -0
- tigrbl/bindings/model.py +194 -0
- tigrbl/bindings/model_helpers.py +139 -0
- tigrbl/bindings/model_registry.py +77 -0
- tigrbl/bindings/rest/__init__.py +7 -0
- tigrbl/bindings/rest/attach.py +34 -0
- tigrbl/bindings/rest/collection.py +265 -0
- tigrbl/bindings/rest/common.py +116 -0
- tigrbl/bindings/rest/fastapi.py +76 -0
- tigrbl/bindings/rest/helpers.py +119 -0
- tigrbl/bindings/rest/io.py +317 -0
- tigrbl/bindings/rest/member.py +367 -0
- tigrbl/bindings/rest/router.py +292 -0
- tigrbl/bindings/rest/routing.py +133 -0
- tigrbl/bindings/rpc.py +364 -0
- tigrbl/bindings/schemas/__init__.py +11 -0
- tigrbl/bindings/schemas/builder.py +348 -0
- tigrbl/bindings/schemas/defaults.py +260 -0
- tigrbl/bindings/schemas/utils.py +193 -0
- tigrbl/column/README.md +62 -0
- tigrbl/column/__init__.py +72 -0
- tigrbl/column/_column.py +96 -0
- tigrbl/column/column_spec.py +40 -0
- tigrbl/column/field_spec.py +31 -0
- tigrbl/column/infer/__init__.py +25 -0
- tigrbl/column/infer/core.py +92 -0
- tigrbl/column/infer/jsonhints.py +44 -0
- tigrbl/column/infer/planning.py +133 -0
- tigrbl/column/infer/types.py +102 -0
- tigrbl/column/infer/utils.py +59 -0
- tigrbl/column/io_spec.py +133 -0
- tigrbl/column/mro_collect.py +59 -0
- tigrbl/column/shortcuts.py +89 -0
- tigrbl/column/storage_spec.py +65 -0
- tigrbl/config/__init__.py +19 -0
- tigrbl/config/constants.py +224 -0
- tigrbl/config/defaults.py +29 -0
- tigrbl/config/resolver.py +295 -0
- tigrbl/core/__init__.py +47 -0
- tigrbl/core/crud/__init__.py +36 -0
- tigrbl/core/crud/bulk.py +168 -0
- tigrbl/core/crud/helpers/__init__.py +76 -0
- tigrbl/core/crud/helpers/db.py +92 -0
- tigrbl/core/crud/helpers/enum.py +86 -0
- tigrbl/core/crud/helpers/filters.py +162 -0
- tigrbl/core/crud/helpers/model.py +123 -0
- tigrbl/core/crud/helpers/normalize.py +99 -0
- tigrbl/core/crud/ops.py +235 -0
- tigrbl/ddl/__init__.py +344 -0
- tigrbl/decorators.py +17 -0
- tigrbl/deps/__init__.py +20 -0
- tigrbl/deps/fastapi.py +45 -0
- tigrbl/deps/favicon.svg +4 -0
- tigrbl/deps/jinja.py +27 -0
- tigrbl/deps/pydantic.py +10 -0
- tigrbl/deps/sqlalchemy.py +94 -0
- tigrbl/deps/starlette.py +36 -0
- tigrbl/engine/__init__.py +26 -0
- tigrbl/engine/_engine.py +130 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +108 -0
- tigrbl/engine/engine_spec.py +261 -0
- tigrbl/engine/resolver.py +224 -0
- tigrbl/engine/shortcuts.py +216 -0
- tigrbl/hook/__init__.py +21 -0
- tigrbl/hook/_hook.py +22 -0
- tigrbl/hook/decorators.py +28 -0
- tigrbl/hook/hook_spec.py +24 -0
- tigrbl/hook/mro_collect.py +98 -0
- tigrbl/hook/shortcuts.py +44 -0
- tigrbl/hook/types.py +76 -0
- tigrbl/op/__init__.py +50 -0
- tigrbl/op/_op.py +31 -0
- tigrbl/op/canonical.py +31 -0
- tigrbl/op/collect.py +11 -0
- tigrbl/op/decorators.py +238 -0
- tigrbl/op/model_registry.py +301 -0
- tigrbl/op/mro_collect.py +99 -0
- tigrbl/op/resolver.py +216 -0
- tigrbl/op/types.py +136 -0
- tigrbl/orm/__init__.py +1 -0
- tigrbl/orm/mixins/_RowBound.py +83 -0
- tigrbl/orm/mixins/__init__.py +95 -0
- tigrbl/orm/mixins/bootstrappable.py +113 -0
- tigrbl/orm/mixins/bound.py +47 -0
- tigrbl/orm/mixins/edges.py +40 -0
- tigrbl/orm/mixins/fields.py +165 -0
- tigrbl/orm/mixins/hierarchy.py +54 -0
- tigrbl/orm/mixins/key_digest.py +44 -0
- tigrbl/orm/mixins/lifecycle.py +115 -0
- tigrbl/orm/mixins/locks.py +51 -0
- tigrbl/orm/mixins/markers.py +16 -0
- tigrbl/orm/mixins/operations.py +57 -0
- tigrbl/orm/mixins/ownable.py +337 -0
- tigrbl/orm/mixins/principals.py +98 -0
- tigrbl/orm/mixins/tenant_bound.py +301 -0
- tigrbl/orm/mixins/upsertable.py +111 -0
- tigrbl/orm/mixins/utils.py +49 -0
- tigrbl/orm/tables/__init__.py +72 -0
- tigrbl/orm/tables/_base.py +8 -0
- tigrbl/orm/tables/audit.py +56 -0
- tigrbl/orm/tables/client.py +25 -0
- tigrbl/orm/tables/group.py +29 -0
- tigrbl/orm/tables/org.py +30 -0
- tigrbl/orm/tables/rbac.py +76 -0
- tigrbl/orm/tables/status.py +106 -0
- tigrbl/orm/tables/tenant.py +22 -0
- tigrbl/orm/tables/user.py +39 -0
- tigrbl/response/README.md +34 -0
- tigrbl/response/__init__.py +33 -0
- tigrbl/response/bind.py +12 -0
- tigrbl/response/decorators.py +37 -0
- tigrbl/response/resolver.py +83 -0
- tigrbl/response/shortcuts.py +144 -0
- tigrbl/response/types.py +49 -0
- tigrbl/rest/__init__.py +27 -0
- tigrbl/runtime/README.md +129 -0
- tigrbl/runtime/__init__.py +20 -0
- tigrbl/runtime/atoms/__init__.py +102 -0
- tigrbl/runtime/atoms/emit/__init__.py +42 -0
- tigrbl/runtime/atoms/emit/paired_post.py +158 -0
- tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
- tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
- tigrbl/runtime/atoms/out/__init__.py +38 -0
- tigrbl/runtime/atoms/out/masking.py +135 -0
- tigrbl/runtime/atoms/refresh/__init__.py +38 -0
- tigrbl/runtime/atoms/refresh/demand.py +130 -0
- tigrbl/runtime/atoms/resolve/__init__.py +40 -0
- tigrbl/runtime/atoms/resolve/assemble.py +167 -0
- tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
- tigrbl/runtime/atoms/response/__init__.py +17 -0
- tigrbl/runtime/atoms/response/negotiate.py +30 -0
- tigrbl/runtime/atoms/response/negotiation.py +43 -0
- tigrbl/runtime/atoms/response/render.py +36 -0
- tigrbl/runtime/atoms/response/renderer.py +116 -0
- tigrbl/runtime/atoms/response/template.py +44 -0
- tigrbl/runtime/atoms/response/templates.py +88 -0
- tigrbl/runtime/atoms/schema/__init__.py +40 -0
- tigrbl/runtime/atoms/schema/collect_in.py +21 -0
- tigrbl/runtime/atoms/schema/collect_out.py +21 -0
- tigrbl/runtime/atoms/storage/__init__.py +38 -0
- tigrbl/runtime/atoms/storage/to_stored.py +167 -0
- tigrbl/runtime/atoms/wire/__init__.py +45 -0
- tigrbl/runtime/atoms/wire/build_in.py +166 -0
- tigrbl/runtime/atoms/wire/build_out.py +87 -0
- tigrbl/runtime/atoms/wire/dump.py +206 -0
- tigrbl/runtime/atoms/wire/validate_in.py +227 -0
- tigrbl/runtime/context.py +206 -0
- tigrbl/runtime/errors/__init__.py +61 -0
- tigrbl/runtime/errors/converters.py +214 -0
- tigrbl/runtime/errors/exceptions.py +124 -0
- tigrbl/runtime/errors/mappings.py +71 -0
- tigrbl/runtime/errors/utils.py +150 -0
- tigrbl/runtime/events.py +209 -0
- tigrbl/runtime/executor/__init__.py +6 -0
- tigrbl/runtime/executor/guards.py +132 -0
- tigrbl/runtime/executor/helpers.py +88 -0
- tigrbl/runtime/executor/invoke.py +150 -0
- tigrbl/runtime/executor/types.py +84 -0
- tigrbl/runtime/kernel.py +628 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +87 -0
- tigrbl/runtime/ordering.py +256 -0
- tigrbl/runtime/system.py +279 -0
- tigrbl/runtime/trace.py +330 -0
- tigrbl/schema/__init__.py +38 -0
- tigrbl/schema/_schema.py +27 -0
- tigrbl/schema/builder/__init__.py +17 -0
- tigrbl/schema/builder/build_schema.py +209 -0
- tigrbl/schema/builder/cache.py +24 -0
- tigrbl/schema/builder/compat.py +16 -0
- tigrbl/schema/builder/extras.py +85 -0
- tigrbl/schema/builder/helpers.py +51 -0
- tigrbl/schema/builder/list_params.py +117 -0
- tigrbl/schema/builder/strip_parent_fields.py +70 -0
- tigrbl/schema/collect.py +55 -0
- tigrbl/schema/decorators.py +68 -0
- tigrbl/schema/get_schema.py +86 -0
- tigrbl/schema/schema_spec.py +20 -0
- tigrbl/schema/shortcuts.py +42 -0
- tigrbl/schema/types.py +34 -0
- tigrbl/schema/utils.py +143 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +12 -0
- tigrbl/system/diagnostics/__init__.py +24 -0
- tigrbl/system/diagnostics/compat.py +31 -0
- tigrbl/system/diagnostics/healthz.py +41 -0
- tigrbl/system/diagnostics/hookz.py +51 -0
- tigrbl/system/diagnostics/kernelz.py +20 -0
- tigrbl/system/diagnostics/methodz.py +43 -0
- tigrbl/system/diagnostics/router.py +73 -0
- tigrbl/system/diagnostics/utils.py +43 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +237 -0
- tigrbl/table/_table.py +54 -0
- tigrbl/table/mro_collect.py +69 -0
- tigrbl/table/shortcuts.py +57 -0
- tigrbl/table/table_spec.py +28 -0
- tigrbl/transport/__init__.py +74 -0
- tigrbl/transport/jsonrpc/__init__.py +19 -0
- tigrbl/transport/jsonrpc/dispatcher.py +352 -0
- tigrbl/transport/jsonrpc/helpers.py +115 -0
- tigrbl/transport/jsonrpc/models.py +41 -0
- tigrbl/transport/rest/__init__.py +25 -0
- tigrbl/transport/rest/aggregator.py +132 -0
- tigrbl/types/__init__.py +174 -0
- tigrbl/types/allow_anon_provider.py +19 -0
- tigrbl/types/authn_abc.py +30 -0
- tigrbl/types/nested_path_provider.py +22 -0
- tigrbl/types/op.py +35 -0
- tigrbl/types/op_config_provider.py +17 -0
- tigrbl/types/op_verb_alias_provider.py +33 -0
- tigrbl/types/request_extras_provider.py +22 -0
- tigrbl/types/response_extras_provider.py +22 -0
- tigrbl/types/table_config_provider.py +13 -0
- tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
- tigrbl/ExampleAgent.py +0 -1
- tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
- tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from .types import DataKind, PyTypeInfo, SATypePlan, InferenceError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _plan_sa_type(
|
|
9
|
+
kind: DataKind,
|
|
10
|
+
py: PyTypeInfo,
|
|
11
|
+
*,
|
|
12
|
+
prefer_dialect: Optional[str],
|
|
13
|
+
max_length: Optional[int],
|
|
14
|
+
decimal_precision: Optional[int],
|
|
15
|
+
decimal_scale: Optional[int],
|
|
16
|
+
) -> SATypePlan:
|
|
17
|
+
d = prefer_dialect
|
|
18
|
+
|
|
19
|
+
if kind is DataKind.STRING:
|
|
20
|
+
if max_length and max_length > 0:
|
|
21
|
+
return SATypePlan(
|
|
22
|
+
name="String", args=(max_length,), kwargs={}, dialect=None
|
|
23
|
+
)
|
|
24
|
+
return SATypePlan(name="String", args=(), kwargs={}, dialect=None)
|
|
25
|
+
|
|
26
|
+
if kind is DataKind.TEXT:
|
|
27
|
+
return SATypePlan(name="Text", args=(), kwargs={}, dialect=None)
|
|
28
|
+
|
|
29
|
+
if kind is DataKind.BYTES:
|
|
30
|
+
return SATypePlan(name="LargeBinary", args=(), kwargs={}, dialect=None)
|
|
31
|
+
|
|
32
|
+
if kind is DataKind.BOOL:
|
|
33
|
+
return SATypePlan(name="Boolean", args=(), kwargs={}, dialect=None)
|
|
34
|
+
|
|
35
|
+
if kind is DataKind.INT:
|
|
36
|
+
return SATypePlan(name="Integer", args=(), kwargs={}, dialect=None)
|
|
37
|
+
|
|
38
|
+
if kind is DataKind.BIGINT:
|
|
39
|
+
return SATypePlan(name="BigInteger", args=(), kwargs={}, dialect=None)
|
|
40
|
+
|
|
41
|
+
if kind is DataKind.FLOAT:
|
|
42
|
+
return SATypePlan(name="Float", args=(), kwargs={}, dialect=None)
|
|
43
|
+
|
|
44
|
+
if kind is DataKind.DECIMAL:
|
|
45
|
+
kwargs: Dict[str, Any] = {}
|
|
46
|
+
if decimal_precision is not None:
|
|
47
|
+
kwargs["precision"] = decimal_precision
|
|
48
|
+
if decimal_scale is not None:
|
|
49
|
+
kwargs["scale"] = decimal_scale
|
|
50
|
+
return SATypePlan(name="Numeric", args=(), kwargs=kwargs, dialect=None)
|
|
51
|
+
|
|
52
|
+
if kind is DataKind.DATE:
|
|
53
|
+
return SATypePlan(name="Date", args=(), kwargs={}, dialect=None)
|
|
54
|
+
|
|
55
|
+
if kind is DataKind.TIME:
|
|
56
|
+
return SATypePlan(name="Time", args=(), kwargs={"timezone": True}, dialect=None)
|
|
57
|
+
|
|
58
|
+
if kind is DataKind.DATETIME:
|
|
59
|
+
return SATypePlan(
|
|
60
|
+
name="DateTime", args=(), kwargs={"timezone": True}, dialect=None
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if kind is DataKind.UUID:
|
|
64
|
+
if d == "postgresql":
|
|
65
|
+
return SATypePlan(
|
|
66
|
+
name="UUID", args=(), kwargs={"as_uuid": True}, dialect="postgresql"
|
|
67
|
+
)
|
|
68
|
+
return SATypePlan(name="String", args=(36,), kwargs={}, dialect=None)
|
|
69
|
+
|
|
70
|
+
if kind is DataKind.JSON:
|
|
71
|
+
if d == "postgresql":
|
|
72
|
+
return SATypePlan(name="JSONB", args=(), kwargs={}, dialect="postgresql")
|
|
73
|
+
return SATypePlan(name="JSON", args=(), kwargs={}, dialect=None)
|
|
74
|
+
|
|
75
|
+
if kind is DataKind.ENUM:
|
|
76
|
+
if not py.enum_cls:
|
|
77
|
+
raise InferenceError("ENUM kind requires enum_cls in PyTypeInfo")
|
|
78
|
+
return SATypePlan(
|
|
79
|
+
name="Enum", args=(py.enum_cls,), kwargs={"native_enum": True}, dialect=None
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if kind is DataKind.ARRAY:
|
|
83
|
+
if not py.array_item:
|
|
84
|
+
raise InferenceError("ARRAY kind requires array_item in PyTypeInfo")
|
|
85
|
+
elem_plan = _plan_sa_type(
|
|
86
|
+
_nested_kind_from_py(py.array_item),
|
|
87
|
+
py.array_item,
|
|
88
|
+
prefer_dialect=prefer_dialect,
|
|
89
|
+
max_length=None,
|
|
90
|
+
decimal_precision=None,
|
|
91
|
+
decimal_scale=None,
|
|
92
|
+
)
|
|
93
|
+
if prefer_dialect == "postgresql":
|
|
94
|
+
return SATypePlan(
|
|
95
|
+
name="ARRAY",
|
|
96
|
+
args=(elem_plan.name,),
|
|
97
|
+
kwargs={},
|
|
98
|
+
dialect="postgresql",
|
|
99
|
+
)
|
|
100
|
+
return SATypePlan(name="JSON", args=(), kwargs={}, dialect=None)
|
|
101
|
+
|
|
102
|
+
raise InferenceError(f"Cannot plan SA type for kind={kind!r}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _nested_kind_from_py(nested_py: PyTypeInfo) -> DataKind:
|
|
106
|
+
if nested_py.enum_cls is not None:
|
|
107
|
+
return DataKind.ENUM
|
|
108
|
+
b = nested_py.base
|
|
109
|
+
import datetime as _dt
|
|
110
|
+
import decimal as _dc
|
|
111
|
+
import uuid as _uuid
|
|
112
|
+
|
|
113
|
+
if b is str:
|
|
114
|
+
return DataKind.STRING
|
|
115
|
+
if b in (bytes, bytearray, memoryview):
|
|
116
|
+
return DataKind.BYTES
|
|
117
|
+
if b is bool:
|
|
118
|
+
return DataKind.BOOL
|
|
119
|
+
if b is int:
|
|
120
|
+
return DataKind.INT
|
|
121
|
+
if b is float:
|
|
122
|
+
return DataKind.FLOAT
|
|
123
|
+
if b is _dc.Decimal:
|
|
124
|
+
return DataKind.DECIMAL
|
|
125
|
+
if b is _dt.datetime:
|
|
126
|
+
return DataKind.DATETIME
|
|
127
|
+
if b is _dt.date:
|
|
128
|
+
return DataKind.DATE
|
|
129
|
+
if b is _dt.time:
|
|
130
|
+
return DataKind.TIME
|
|
131
|
+
if b is _uuid.UUID:
|
|
132
|
+
return DataKind.UUID
|
|
133
|
+
return DataKind.JSON
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Public markers (wire/adapters provide validation/normalization at runtime)
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Email:
|
|
14
|
+
"""Marker indicating email string semantics."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Phone:
|
|
18
|
+
"""Marker indicating E.164 phone string semantics."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Portable data-kind (DB- and adapter-agnostic)
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DataKind(str, Enum):
|
|
27
|
+
STRING = "string"
|
|
28
|
+
TEXT = "text"
|
|
29
|
+
BYTES = "bytes"
|
|
30
|
+
BOOL = "bool"
|
|
31
|
+
INT = "int"
|
|
32
|
+
BIGINT = "bigint"
|
|
33
|
+
FLOAT = "float"
|
|
34
|
+
DECIMAL = "decimal"
|
|
35
|
+
DATE = "date"
|
|
36
|
+
TIME = "time"
|
|
37
|
+
DATETIME = "datetime"
|
|
38
|
+
UUID = "uuid"
|
|
39
|
+
JSON = "json"
|
|
40
|
+
ENUM = "enum"
|
|
41
|
+
ARRAY = "array"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Structured outputs
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class PyTypeInfo:
|
|
51
|
+
"""Normalized info about the Python-side type annotation."""
|
|
52
|
+
|
|
53
|
+
base: Any
|
|
54
|
+
is_optional: bool = False
|
|
55
|
+
enum_cls: Optional[Type[Enum]] = None
|
|
56
|
+
array_item: Optional["PyTypeInfo"] = None
|
|
57
|
+
annotated: Tuple[Any, ...] = ()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class SATypePlan:
|
|
62
|
+
"""Declarative plan for constructing an SA column type downstream."""
|
|
63
|
+
|
|
64
|
+
name: str # e.g., "UUID", "String", "JSONB", "Enum", "ARRAY"
|
|
65
|
+
args: Tuple[Any, ...] # positional args (e.g., (enum_cls,))
|
|
66
|
+
kwargs: Dict[str, Any] # keyword args (e.g., {"as_uuid": True})
|
|
67
|
+
dialect: Optional[str] = None # e.g., "postgresql" for JSONB/UUID/ARRAY
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class JsonHint:
|
|
72
|
+
"""Minimal JSON Schema-ish hints for docs."""
|
|
73
|
+
|
|
74
|
+
type: str
|
|
75
|
+
format: Optional[str] = None
|
|
76
|
+
maxLength: Optional[int] = None
|
|
77
|
+
enum: Optional[List[str]] = None
|
|
78
|
+
items: Optional["JsonHint"] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class Inferred:
|
|
83
|
+
"""Primary product of inference."""
|
|
84
|
+
|
|
85
|
+
kind: DataKind
|
|
86
|
+
py: PyTypeInfo
|
|
87
|
+
sa: SATypePlan
|
|
88
|
+
json: JsonHint
|
|
89
|
+
nullable: bool
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Errors
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class InferenceError(ValueError):
|
|
98
|
+
"""Base class for inference-related errors."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class UnsupportedType(InferenceError):
|
|
102
|
+
"""Raised when a type cannot be inferred."""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
List,
|
|
7
|
+
Optional,
|
|
8
|
+
Tuple,
|
|
9
|
+
Type,
|
|
10
|
+
Union,
|
|
11
|
+
get_args,
|
|
12
|
+
get_origin,
|
|
13
|
+
Annotated,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _strip_optional(tp: Any) -> Tuple[Any, bool]:
|
|
18
|
+
"""Return (inner_type, is_optional) for Optional[T] / Union[T, None]."""
|
|
19
|
+
origin = get_origin(tp)
|
|
20
|
+
if origin is Union:
|
|
21
|
+
args = tuple(a for a in get_args(tp))
|
|
22
|
+
if len(args) == 2 and type(None) in args:
|
|
23
|
+
inner = args[0] if args[1] is type(None) else args[1]
|
|
24
|
+
return inner, True
|
|
25
|
+
return tp, False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _strip_annotated(tp: Any) -> Tuple[Any, Tuple[Any, ...]]:
|
|
29
|
+
"""Return (base, metadata) for Annotated[base, *meta]; otherwise (tp, ())."""
|
|
30
|
+
origin = get_origin(tp)
|
|
31
|
+
if origin is Annotated:
|
|
32
|
+
args = get_args(tp)
|
|
33
|
+
if len(args) >= 1:
|
|
34
|
+
base, meta = args[0], tuple(args[1:])
|
|
35
|
+
return base, meta
|
|
36
|
+
return tp, ()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _array_item(tp: Any) -> Optional[Any]:
|
|
40
|
+
origin = get_origin(tp)
|
|
41
|
+
if origin in (list, List, tuple, Tuple, set, frozenset):
|
|
42
|
+
args = get_args(tp)
|
|
43
|
+
if not args:
|
|
44
|
+
return Any
|
|
45
|
+
if origin in (tuple, Tuple) and len(args) == 2 and args[1] is Ellipsis:
|
|
46
|
+
return args[0]
|
|
47
|
+
if len(args) == 1:
|
|
48
|
+
return args[0]
|
|
49
|
+
return Any
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_enum(tp: Any) -> Optional[Type[Enum]]:
|
|
54
|
+
try:
|
|
55
|
+
if isinstance(tp, type) and issubclass(tp, Enum):
|
|
56
|
+
return tp
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return None
|
tigrbl/column/io_spec.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# --- io_spec.py --------------------------------------------------------------
|
|
2
|
+
from dataclasses import dataclass, replace
|
|
3
|
+
from typing import Callable, Tuple, Literal
|
|
4
|
+
from .field_spec import FieldSpec as F
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class _PairedCfg:
|
|
9
|
+
gen: Callable[[dict], object] # ctx -> raw
|
|
10
|
+
store: Callable[[object, dict], object] # (raw, ctx) -> stored
|
|
11
|
+
alias: str
|
|
12
|
+
verbs: Tuple[str, ...]
|
|
13
|
+
emit: str # "pre_flush" | "post_refresh" | "readtime"
|
|
14
|
+
alias_field: F
|
|
15
|
+
mask_last: int | None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class _AssembleCfg:
|
|
20
|
+
sources: Tuple[str, ...]
|
|
21
|
+
fn: Callable[[dict, dict], object] # (payload, ctx) -> stored
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class _ReadtimeAlias:
|
|
26
|
+
name: str
|
|
27
|
+
fn: Callable[[object, dict], object] # (obj, ctx) -> alias value
|
|
28
|
+
verbs: Tuple[str, ...]
|
|
29
|
+
alias_field: F
|
|
30
|
+
mask_last: int | None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
EmitPoint = Literal["pre_flush", "post_refresh", "pre_response", "readtime"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class Pair:
|
|
38
|
+
raw: object
|
|
39
|
+
stored: object
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class IOSpec:
|
|
44
|
+
"""Control how a column participates in API input and output.
|
|
45
|
+
|
|
46
|
+
``in_verbs`` and ``out_verbs`` enumerate which operations may accept or
|
|
47
|
+
emit the field. ``alias_in``/``alias_out`` allow different payload keys,
|
|
48
|
+
``sensitive`` and ``redact_last`` mark values for masking, and
|
|
49
|
+
``filter_ops``/``sortable`` flag whether the field can be used in query
|
|
50
|
+
filters or ordering. Advanced helpers like :meth:`assemble`,
|
|
51
|
+
:meth:`paired`, and :meth:`alias_readtime` derive values or expose extra
|
|
52
|
+
read-time aliases.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
in_verbs: Tuple[str, ...] = ()
|
|
56
|
+
out_verbs: Tuple[str, ...] = ()
|
|
57
|
+
mutable_verbs: Tuple[str, ...] = ()
|
|
58
|
+
alias_in: str | None = None
|
|
59
|
+
alias_out: str | None = None
|
|
60
|
+
sensitive: bool = False
|
|
61
|
+
redact_last: int | None = None
|
|
62
|
+
filter_ops: Tuple[str, ...] = ()
|
|
63
|
+
sortable: bool = False
|
|
64
|
+
allow_in: Callable[[str, dict], bool] | None = None
|
|
65
|
+
allow_out: Callable[[str, dict], bool] | None = None
|
|
66
|
+
_paired: _PairedCfg | None = None
|
|
67
|
+
_assemble: _AssembleCfg | None = None
|
|
68
|
+
_readtime_aliases: Tuple[_ReadtimeAlias, ...] = ()
|
|
69
|
+
|
|
70
|
+
def assemble(self, sources, fn):
|
|
71
|
+
"""Return a new spec that derives a value from ``sources`` using ``fn``."""
|
|
72
|
+
cfg = _AssembleCfg(sources=tuple(sources), fn=fn)
|
|
73
|
+
return replace(self, _assemble=cfg)
|
|
74
|
+
|
|
75
|
+
def paired(
|
|
76
|
+
self,
|
|
77
|
+
make,
|
|
78
|
+
*,
|
|
79
|
+
alias,
|
|
80
|
+
verbs=("create",),
|
|
81
|
+
emit: EmitPoint = "pre_flush",
|
|
82
|
+
alias_field: F = F(py_type=str),
|
|
83
|
+
mask_last: int | None = None,
|
|
84
|
+
):
|
|
85
|
+
"""Return a new spec with a paired field configuration."""
|
|
86
|
+
|
|
87
|
+
def gen(ctx):
|
|
88
|
+
pair = make(ctx)
|
|
89
|
+
temp = (
|
|
90
|
+
ctx.get("temp") if isinstance(ctx, dict) else getattr(ctx, "temp", None)
|
|
91
|
+
)
|
|
92
|
+
if isinstance(temp, dict):
|
|
93
|
+
temp.setdefault("_paired_cache", {})[alias] = pair.stored
|
|
94
|
+
return pair.raw
|
|
95
|
+
|
|
96
|
+
def store(raw, ctx):
|
|
97
|
+
temp = getattr(ctx, "temp", None)
|
|
98
|
+
if isinstance(temp, dict):
|
|
99
|
+
cached = temp.get("_paired_cache", {}).pop(alias, None)
|
|
100
|
+
if cached is not None:
|
|
101
|
+
return cached
|
|
102
|
+
return make(ctx).stored
|
|
103
|
+
|
|
104
|
+
cfg = _PairedCfg(
|
|
105
|
+
gen=gen,
|
|
106
|
+
store=store,
|
|
107
|
+
alias=alias,
|
|
108
|
+
verbs=tuple(verbs),
|
|
109
|
+
emit=emit,
|
|
110
|
+
alias_field=alias_field,
|
|
111
|
+
mask_last=mask_last,
|
|
112
|
+
)
|
|
113
|
+
return replace(self, _paired=cfg)
|
|
114
|
+
|
|
115
|
+
def alias_readtime(
|
|
116
|
+
self,
|
|
117
|
+
name,
|
|
118
|
+
fn,
|
|
119
|
+
*,
|
|
120
|
+
verbs=("read", "list"),
|
|
121
|
+
alias_field: F = F(py_type=str),
|
|
122
|
+
mask_last: int | None = None,
|
|
123
|
+
):
|
|
124
|
+
"""Return a new spec with an additional read-time alias."""
|
|
125
|
+
|
|
126
|
+
alias_cfg = _ReadtimeAlias(
|
|
127
|
+
name=name,
|
|
128
|
+
fn=fn,
|
|
129
|
+
verbs=tuple(verbs),
|
|
130
|
+
alias_field=alias_field,
|
|
131
|
+
mask_last=mask_last,
|
|
132
|
+
)
|
|
133
|
+
return replace(self, _readtime_aliases=self._readtime_aliases + (alias_cfg,))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
from .column_spec import ColumnSpec
|
|
8
|
+
from .io_spec import IOSpec as IO
|
|
9
|
+
from .storage_spec import StorageSpec as S
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("uvicorn")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Default inbound/outbound verbs for columns lacking an explicit ColumnSpec.
|
|
15
|
+
#
|
|
16
|
+
# Without this, plain SQLAlchemy ``Column`` definitions are omitted from the
|
|
17
|
+
# collected spec map, causing downstream components to treat their values as
|
|
18
|
+
# unknown. By seeding such columns with a permissive IO spec we ensure they
|
|
19
|
+
# participate in canonical CRUD operations just like columns defined via
|
|
20
|
+
# ``acol``.
|
|
21
|
+
_DEFAULT_IO = IO(
|
|
22
|
+
in_verbs=("create", "update", "replace"),
|
|
23
|
+
out_verbs=("read", "list"),
|
|
24
|
+
mutable_verbs=("create", "update", "replace"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@lru_cache(maxsize=None)
|
|
29
|
+
def mro_collect_columns(
|
|
30
|
+
model: type, *, _cache_bust: int | None = None
|
|
31
|
+
) -> Dict[str, ColumnSpec]:
|
|
32
|
+
"""Collect ColumnSpecs declared on *model* and all mixins.
|
|
33
|
+
|
|
34
|
+
Iterates across the model's MRO so that mixin-defined columns are included
|
|
35
|
+
in the resulting mapping. Later definitions take precedence over earlier
|
|
36
|
+
ones in the MRO. Any table-backed columns lacking a spec are populated with
|
|
37
|
+
a default ColumnSpec so they participate in opviews and schema generation.
|
|
38
|
+
"""
|
|
39
|
+
logger.info("Collecting columns for %s", model.__name__)
|
|
40
|
+
out: Dict[str, ColumnSpec] = {}
|
|
41
|
+
for base in reversed(model.__mro__):
|
|
42
|
+
mapping = getattr(base, "__tigrbl_colspecs__", None)
|
|
43
|
+
if isinstance(mapping, dict):
|
|
44
|
+
out.update(mapping)
|
|
45
|
+
mapping = getattr(base, "__tigrbl_cols__", None)
|
|
46
|
+
if isinstance(mapping, dict):
|
|
47
|
+
out.update(mapping)
|
|
48
|
+
|
|
49
|
+
table = getattr(model, "__table__", None)
|
|
50
|
+
if table is not None:
|
|
51
|
+
for col in table.columns:
|
|
52
|
+
name = getattr(col, "key", None) or col.name
|
|
53
|
+
out.setdefault(name, ColumnSpec(storage=S(), io=_DEFAULT_IO))
|
|
54
|
+
|
|
55
|
+
logger.info("Collected %d columns for %s", len(out), model.__name__)
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ["mro_collect_columns"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from ._column import Column
|
|
6
|
+
from .column_spec import ColumnSpec
|
|
7
|
+
from .field_spec import FieldSpec as F
|
|
8
|
+
from .io_spec import IOSpec as IO
|
|
9
|
+
from .storage_spec import StorageSpec as S
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"makeColumn",
|
|
13
|
+
"makeVirtualColumn",
|
|
14
|
+
"acol",
|
|
15
|
+
"vcol",
|
|
16
|
+
"F",
|
|
17
|
+
"IO",
|
|
18
|
+
"S",
|
|
19
|
+
"ColumnSpec",
|
|
20
|
+
"Column",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def makeColumn(
|
|
25
|
+
*,
|
|
26
|
+
storage: S | None = None,
|
|
27
|
+
field: F | None = None,
|
|
28
|
+
io: IO | None = None,
|
|
29
|
+
default_factory: Optional[Callable[[dict], Any]] = None,
|
|
30
|
+
read_producer: Optional[Callable[[object, dict], Any]] = None,
|
|
31
|
+
spec: ColumnSpec | None = None,
|
|
32
|
+
**kw: Any,
|
|
33
|
+
) -> Column:
|
|
34
|
+
"""Return a :class:`Column` descriptor for declarative models."""
|
|
35
|
+
if spec is not None:
|
|
36
|
+
if any(
|
|
37
|
+
x is not None for x in (storage, field, io, default_factory, read_producer)
|
|
38
|
+
):
|
|
39
|
+
raise ValueError("Provide either spec or individual components, not both.")
|
|
40
|
+
if spec is None:
|
|
41
|
+
if read_producer is not None and storage is not None:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"read_producer is only valid for virtual (storage=None) columns."
|
|
44
|
+
)
|
|
45
|
+
spec = ColumnSpec(
|
|
46
|
+
storage=storage,
|
|
47
|
+
field=field,
|
|
48
|
+
io=io,
|
|
49
|
+
default_factory=default_factory,
|
|
50
|
+
read_producer=read_producer,
|
|
51
|
+
)
|
|
52
|
+
return Column(spec=spec, **kw)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def makeVirtualColumn(
|
|
56
|
+
*,
|
|
57
|
+
field: F | None = None,
|
|
58
|
+
io: IO | None = None,
|
|
59
|
+
default_factory: Optional[Callable[[dict], Any]] = None,
|
|
60
|
+
producer: Optional[Callable[[object, dict], Any]] = None,
|
|
61
|
+
read_producer: Optional[Callable[[object, dict], Any]] = None,
|
|
62
|
+
spec: ColumnSpec | None = None,
|
|
63
|
+
**kw: Any,
|
|
64
|
+
) -> Column:
|
|
65
|
+
"""Convenience for wire-only virtual columns."""
|
|
66
|
+
if spec is not None:
|
|
67
|
+
if any(
|
|
68
|
+
x is not None for x in (field, io, default_factory, producer, read_producer)
|
|
69
|
+
):
|
|
70
|
+
raise ValueError("Provide either spec or individual components, not both.")
|
|
71
|
+
return Column(spec=spec, **kw)
|
|
72
|
+
if producer is not None and read_producer is not None:
|
|
73
|
+
raise ValueError("Provide only one of producer= or read_producer=, not both.")
|
|
74
|
+
rp = read_producer or producer
|
|
75
|
+
return Column(
|
|
76
|
+
spec=ColumnSpec(
|
|
77
|
+
storage=None,
|
|
78
|
+
field=field,
|
|
79
|
+
io=io,
|
|
80
|
+
default_factory=default_factory,
|
|
81
|
+
read_producer=rp,
|
|
82
|
+
),
|
|
83
|
+
**kw,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Convenience aliases retained for backward compatibility
|
|
88
|
+
acol = makeColumn
|
|
89
|
+
vcol = makeVirtualColumn
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ---------------------------------------
|
|
2
|
+
# storage_spec.py (S)
|
|
3
|
+
# ---------------------------------------
|
|
4
|
+
from dataclasses import KW_ONLY, dataclass
|
|
5
|
+
from typing import Any, Literal, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class StorageTransform:
|
|
10
|
+
"""Functions used to transform values on the way to and from the database."""
|
|
11
|
+
|
|
12
|
+
to_stored: Union[callable, None] = (
|
|
13
|
+
None # (python, ctx) -> python persisted (e.g., hash/encrypt/normalize)
|
|
14
|
+
)
|
|
15
|
+
from_stored: Union[callable, None] = (
|
|
16
|
+
None # (python, ctx) -> python exposed (rare; only if you ever expose stored)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ForeignKeySpec:
|
|
22
|
+
"""Lightweight description of a foreign key relationship."""
|
|
23
|
+
|
|
24
|
+
target: str # "tenant(id)" or fully-qualified
|
|
25
|
+
on_delete: Literal["CASCADE", "RESTRICT", "SET NULL", "SET DEFAULT"] = "RESTRICT"
|
|
26
|
+
on_update: Literal["CASCADE", "RESTRICT", "SET NULL", "SET DEFAULT"] = "RESTRICT"
|
|
27
|
+
deferrable: bool = False
|
|
28
|
+
initially_deferred: bool = False
|
|
29
|
+
match: Literal["FULL", "PARTIAL", "SIMPLE"] = "SIMPLE"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class StorageSpec:
|
|
34
|
+
"""Describe the database-level shape and behaviour of a column.
|
|
35
|
+
|
|
36
|
+
The spec maps closely to SQLAlchemy's :class:`~sqlalchemy.Column` keyword
|
|
37
|
+
arguments: ``type_`` and flags such as ``nullable`` or ``primary_key``
|
|
38
|
+
define the table schema while ``default`` and ``onupdate`` represent ORM
|
|
39
|
+
side defaults. ``server_default`` and ``refresh_on_return`` support
|
|
40
|
+
database-generated values. Optional helpers provide value transforms,
|
|
41
|
+
foreign keys, check constraints and comments.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# SQLAlchemy column shape (DDL/runtime)
|
|
45
|
+
type_: Any | None = None
|
|
46
|
+
_: KW_ONLY
|
|
47
|
+
nullable: bool | None = None
|
|
48
|
+
unique: bool = False
|
|
49
|
+
index: bool = False
|
|
50
|
+
primary_key: bool = False
|
|
51
|
+
autoincrement: bool | None = None
|
|
52
|
+
|
|
53
|
+
# ORM-side defaults (run in SQLAlchemy) – optional if you use API defaults/paired
|
|
54
|
+
default: Any | None = None # scalar or callable()
|
|
55
|
+
onupdate: Any | None = None
|
|
56
|
+
|
|
57
|
+
# DB-side defaults/generation (run in the database)
|
|
58
|
+
server_default: Any | None = None # e.g., func.now(), text("...")
|
|
59
|
+
refresh_on_return: bool = False # force refresh after flush when DB generated
|
|
60
|
+
|
|
61
|
+
# Optional storage helpers
|
|
62
|
+
transform: StorageTransform | None = None
|
|
63
|
+
fk: ForeignKeySpec | None = None
|
|
64
|
+
check: str | None = None
|
|
65
|
+
comment: str | None = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# tigrbl/v3/config/__init__.py
|
|
2
|
+
"""Tigrbl v3 – configuration surface.
|
|
3
|
+
|
|
4
|
+
Exports:
|
|
5
|
+
- DEFAULTS: canonical configuration defaults
|
|
6
|
+
- CfgView: read-only config view (attr + dict access)
|
|
7
|
+
- resolve_cfg(...): precedence-based merger across apps/api/tab/cols/op/overrides
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .defaults import DEFAULTS
|
|
13
|
+
from .resolver import CfgView, resolve_cfg
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"DEFAULTS",
|
|
17
|
+
"CfgView",
|
|
18
|
+
"resolve_cfg",
|
|
19
|
+
]
|