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,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping, Tuple
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ....column.mro_collect import mro_collect_columns
|
|
8
|
+
|
|
9
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
10
|
+
logger = logging.getLogger("uvicorn")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _pk_columns(model: type) -> Tuple[Any, ...]:
|
|
14
|
+
logger.debug("_pk_columns called with model=%s", model)
|
|
15
|
+
table = getattr(model, "__table__", None)
|
|
16
|
+
if table is None:
|
|
17
|
+
raise ValueError(f"{model.__name__} has no __table__")
|
|
18
|
+
pks = tuple(table.primary_key.columns) # type: ignore[attr-defined]
|
|
19
|
+
if not pks:
|
|
20
|
+
raise ValueError(f"{model.__name__} has no primary key")
|
|
21
|
+
logger.debug("_pk_columns returning %s", pks)
|
|
22
|
+
return pks
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _single_pk_name(model: type) -> str:
|
|
26
|
+
logger.debug("_single_pk_name called with model=%s", model)
|
|
27
|
+
pks = _pk_columns(model)
|
|
28
|
+
if len(pks) != 1:
|
|
29
|
+
raise NotImplementedError(
|
|
30
|
+
f"{model.__name__} has composite PK; not supported by default core"
|
|
31
|
+
)
|
|
32
|
+
name = pks[0].name
|
|
33
|
+
logger.debug("_single_pk_name returning %s", name)
|
|
34
|
+
return name
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _coerce_pk_value(model: type, value: Any) -> Any:
|
|
38
|
+
logger.debug("_coerce_pk_value called with model=%s value=%s", model, value)
|
|
39
|
+
if value is None:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
col = _pk_columns(model)[0]
|
|
43
|
+
py_type = col.type.python_type # type: ignore[attr-defined]
|
|
44
|
+
except Exception: # pragma: no cover - best effort
|
|
45
|
+
logger.debug("_coerce_pk_value returning original value %s", value)
|
|
46
|
+
return value
|
|
47
|
+
if isinstance(value, py_type):
|
|
48
|
+
return value
|
|
49
|
+
try:
|
|
50
|
+
coerced = py_type(value)
|
|
51
|
+
logger.debug("_coerce_pk_value coerced %s to %s", value, coerced)
|
|
52
|
+
return coerced
|
|
53
|
+
except Exception: # pragma: no cover - fallback to original
|
|
54
|
+
logger.debug("_coerce_pk_value failed to coerce %s", value)
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _model_columns(model: type) -> Tuple[str, ...]:
|
|
59
|
+
logger.debug("_model_columns called with model=%s", model)
|
|
60
|
+
table = getattr(model, "__table__", None)
|
|
61
|
+
if table is None:
|
|
62
|
+
return ()
|
|
63
|
+
cols = tuple(c.name for c in table.columns)
|
|
64
|
+
logger.debug("_model_columns returning %s", cols)
|
|
65
|
+
return cols
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _colspecs(model: type) -> Mapping[str, Any]:
|
|
69
|
+
logger.info("_colspecs called with model=%s", model)
|
|
70
|
+
cache_bust = hash(
|
|
71
|
+
(
|
|
72
|
+
id(getattr(model, "__tigrbl_colspecs__", None)),
|
|
73
|
+
id(getattr(model, "__tigrbl_cols__", None)),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
specs = mro_collect_columns(model, _cache_bust=cache_bust)
|
|
77
|
+
logger.info("_colspecs returning %s", specs)
|
|
78
|
+
return specs
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _filter_in_values(
|
|
82
|
+
model: type, data: Mapping[str, Any], verb: str
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
logger.info("_filter_in_values called with data=%s verb=%s", data, verb)
|
|
85
|
+
specs = _colspecs(model)
|
|
86
|
+
if not specs:
|
|
87
|
+
result = dict(data)
|
|
88
|
+
logger.debug("_filter_in_values returning %s", result)
|
|
89
|
+
return result
|
|
90
|
+
out: Dict[str, Any] = {}
|
|
91
|
+
for k, v in data.items():
|
|
92
|
+
sp = specs.get(k)
|
|
93
|
+
if sp is None:
|
|
94
|
+
out[k] = v
|
|
95
|
+
continue
|
|
96
|
+
io = getattr(sp, "io", None)
|
|
97
|
+
allowed = True
|
|
98
|
+
if io is not None:
|
|
99
|
+
in_verbs = getattr(io, "in_verbs", ())
|
|
100
|
+
mutable = getattr(io, "mutable_verbs", ())
|
|
101
|
+
if in_verbs and verb not in in_verbs:
|
|
102
|
+
allowed = False
|
|
103
|
+
if mutable and verb not in mutable:
|
|
104
|
+
allowed = False
|
|
105
|
+
if allowed:
|
|
106
|
+
out[k] = v
|
|
107
|
+
logger.info("_filter_in_values returning %s", out)
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _immutable_columns(model: type, verb: str) -> set[str]:
|
|
112
|
+
logger.info("_immutable_columns called with model=%s verb=%s", model, verb)
|
|
113
|
+
specs = _colspecs(model)
|
|
114
|
+
if not specs:
|
|
115
|
+
return set()
|
|
116
|
+
imm: set[str] = set()
|
|
117
|
+
for name, sp in specs.items():
|
|
118
|
+
io = getattr(sp, "io", None)
|
|
119
|
+
mutable = getattr(io, "mutable_verbs", ()) if io else ()
|
|
120
|
+
if mutable and verb not in mutable:
|
|
121
|
+
imm.add(name)
|
|
122
|
+
logger.info("_immutable_columns returning %s", imm)
|
|
123
|
+
return imm
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping, Optional, Union
|
|
4
|
+
import builtins as _builtins
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from . import AsyncSession, Session
|
|
8
|
+
|
|
9
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
10
|
+
logger = logging.getLogger("uvicorn")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _pop_bound_self(args: list[Any]) -> None:
|
|
14
|
+
logger.debug("_pop_bound_self called with args=%s", args)
|
|
15
|
+
if args and not isinstance(args[0], type):
|
|
16
|
+
args.pop(0)
|
|
17
|
+
logger.debug("_pop_bound_self result args=%s", args)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _extract_db(
|
|
21
|
+
args: list[Any], kwargs: dict[str, Any]
|
|
22
|
+
) -> Union[Session, AsyncSession]:
|
|
23
|
+
logger.debug("_extract_db called with args=%s kwargs=%s", args, kwargs)
|
|
24
|
+
db = kwargs.pop("db", None)
|
|
25
|
+
if db is not None:
|
|
26
|
+
logger.debug("_extract_db found db in kwargs=%s", db)
|
|
27
|
+
return db
|
|
28
|
+
for i, a in enumerate(args):
|
|
29
|
+
if isinstance(a, (Session, AsyncSession)) or hasattr(a, "execute"):
|
|
30
|
+
args.pop(i)
|
|
31
|
+
logger.debug("_extract_db using positional db=%s", a)
|
|
32
|
+
return a # type: ignore[return-value]
|
|
33
|
+
logger.debug("_extract_db failed to find db")
|
|
34
|
+
raise TypeError("db session is required")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _as_pos_int(x: Any) -> Optional[int]:
|
|
38
|
+
logger.debug("_as_pos_int called with x=%s", x)
|
|
39
|
+
if x is None:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
v = int(x)
|
|
43
|
+
result = v if v >= 0 else 0
|
|
44
|
+
logger.debug("_as_pos_int returning %s", result)
|
|
45
|
+
return result
|
|
46
|
+
except Exception:
|
|
47
|
+
logger.debug("_as_pos_int returning None for x=%s", x)
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _normalize_list_call(
|
|
52
|
+
_args: tuple[Any, ...], _kwargs: dict[str, Any]
|
|
53
|
+
) -> tuple[type, Dict[str, Any]]:
|
|
54
|
+
logger.debug("_normalize_list_call called with _args=%s _kwargs=%s", _args, _kwargs)
|
|
55
|
+
args = _builtins.list(_args)
|
|
56
|
+
kwargs = dict(_kwargs)
|
|
57
|
+
|
|
58
|
+
_pop_bound_self(args)
|
|
59
|
+
|
|
60
|
+
if args and isinstance(args[0], type):
|
|
61
|
+
model = args.pop(0)
|
|
62
|
+
else:
|
|
63
|
+
model = kwargs.pop("model", None)
|
|
64
|
+
if not isinstance(model, type):
|
|
65
|
+
raise TypeError("list(model, ...) requires a model class")
|
|
66
|
+
|
|
67
|
+
filters = kwargs.pop("filters", None)
|
|
68
|
+
if filters is None and args:
|
|
69
|
+
maybe = args[0]
|
|
70
|
+
if isinstance(maybe, Mapping):
|
|
71
|
+
filters = args.pop(0)
|
|
72
|
+
|
|
73
|
+
skip = _as_pos_int(kwargs.pop("skip", None))
|
|
74
|
+
limit = _as_pos_int(kwargs.pop("limit", None))
|
|
75
|
+
sort = kwargs.pop("sort", None)
|
|
76
|
+
|
|
77
|
+
if skip is None and args:
|
|
78
|
+
skip = _as_pos_int(args[0])
|
|
79
|
+
if skip is not None:
|
|
80
|
+
args.pop(0)
|
|
81
|
+
if limit is None and args:
|
|
82
|
+
limit = _as_pos_int(args[0])
|
|
83
|
+
if limit is not None:
|
|
84
|
+
args.pop(0)
|
|
85
|
+
|
|
86
|
+
db = _extract_db(args, kwargs)
|
|
87
|
+
|
|
88
|
+
if filters is None:
|
|
89
|
+
filters = {}
|
|
90
|
+
|
|
91
|
+
result = {
|
|
92
|
+
"filters": filters,
|
|
93
|
+
"skip": skip,
|
|
94
|
+
"limit": limit,
|
|
95
|
+
"db": db,
|
|
96
|
+
"sort": sort,
|
|
97
|
+
}
|
|
98
|
+
logger.debug("_normalize_list_call returning model=%s params=%s", model, result)
|
|
99
|
+
return model, result
|
tigrbl/core/crud/ops.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Mapping, Optional, Union
|
|
4
|
+
|
|
5
|
+
import builtins as _builtins
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from .helpers import (
|
|
9
|
+
AsyncSession,
|
|
10
|
+
Session,
|
|
11
|
+
NoResultFound,
|
|
12
|
+
select,
|
|
13
|
+
sa_delete,
|
|
14
|
+
_apply_filters,
|
|
15
|
+
_apply_sort,
|
|
16
|
+
_coerce_filters,
|
|
17
|
+
_coerce_pk_value,
|
|
18
|
+
_filter_in_values,
|
|
19
|
+
_immutable_columns,
|
|
20
|
+
_maybe_delete,
|
|
21
|
+
_maybe_execute,
|
|
22
|
+
_maybe_flush,
|
|
23
|
+
_maybe_get,
|
|
24
|
+
_normalize_list_call,
|
|
25
|
+
_set_attrs,
|
|
26
|
+
_single_pk_name,
|
|
27
|
+
_validate_enum_values,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
31
|
+
logger = logging.getLogger("uvicorn")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def create(
|
|
35
|
+
model: type, data: Mapping[str, Any], db: Union[Session, AsyncSession]
|
|
36
|
+
) -> Any:
|
|
37
|
+
"""
|
|
38
|
+
Insert a single row. Returns the persisted model instance.
|
|
39
|
+
Flush-only (commit happens later in END_TX).
|
|
40
|
+
"""
|
|
41
|
+
logger.debug("create called with model=%s data=%s", model, data)
|
|
42
|
+
data = _filter_in_values(model, data or {}, "create")
|
|
43
|
+
_validate_enum_values(model, data)
|
|
44
|
+
obj = model(**data)
|
|
45
|
+
if hasattr(db, "add"):
|
|
46
|
+
db.add(obj)
|
|
47
|
+
await _maybe_flush(db)
|
|
48
|
+
logger.debug("create persisted obj=%s", obj)
|
|
49
|
+
return obj
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def read(model: type, ident: Any, db: Union[Session, AsyncSession]) -> Any:
|
|
53
|
+
"""
|
|
54
|
+
Load a single row by primary key. Raises NoResultFound if not found.
|
|
55
|
+
"""
|
|
56
|
+
logger.debug("read called with model=%s ident=%s", model, ident)
|
|
57
|
+
obj = await _maybe_get(db, model, ident)
|
|
58
|
+
if obj is None:
|
|
59
|
+
logger.debug("read did not find model=%s ident=%s", model, ident)
|
|
60
|
+
raise NoResultFound(f"{model.__name__}({ident!r}) not found")
|
|
61
|
+
logger.debug("read returning obj=%s", obj)
|
|
62
|
+
return obj
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def update(
|
|
66
|
+
model: type, ident: Any, data: Mapping[str, Any], db: Union[Session, AsyncSession]
|
|
67
|
+
) -> Any:
|
|
68
|
+
"""
|
|
69
|
+
Partial update by primary key. Missing keys are left unchanged.
|
|
70
|
+
Returns the updated model instance. Flush-only.
|
|
71
|
+
"""
|
|
72
|
+
logger.debug("update called with model=%s ident=%s data=%s", model, ident, data)
|
|
73
|
+
data = _filter_in_values(model, data or {}, "update")
|
|
74
|
+
_validate_enum_values(model, data)
|
|
75
|
+
obj = await read(model, ident, db)
|
|
76
|
+
skip = _immutable_columns(model, "update")
|
|
77
|
+
_set_attrs(obj, data, allow_missing=True, skip=skip)
|
|
78
|
+
await _maybe_flush(db)
|
|
79
|
+
logger.debug("update returning obj=%s", obj)
|
|
80
|
+
return obj
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def replace(
|
|
84
|
+
model: type, ident: Any, data: Mapping[str, Any], db: Union[Session, AsyncSession]
|
|
85
|
+
) -> Any:
|
|
86
|
+
"""
|
|
87
|
+
PUT semantics with upsert behaviour.
|
|
88
|
+
|
|
89
|
+
If the row exists it is replaced entirely (missing attributes are nulled).
|
|
90
|
+
If the row does not exist it is created with the provided identifier.
|
|
91
|
+
Flush-only.
|
|
92
|
+
"""
|
|
93
|
+
logger.debug("replace called with model=%s ident=%s data=%s", model, ident, data)
|
|
94
|
+
data = _filter_in_values(model, data or {}, "replace")
|
|
95
|
+
_validate_enum_values(model, data)
|
|
96
|
+
pk = _single_pk_name(model)
|
|
97
|
+
obj = await _maybe_get(db, model, ident)
|
|
98
|
+
if obj is None:
|
|
99
|
+
payload = {pk: ident, **data}
|
|
100
|
+
result = await create(model, payload, db=db)
|
|
101
|
+
logger.debug("replace created obj=%s", result)
|
|
102
|
+
return result
|
|
103
|
+
skip = _immutable_columns(model, "replace")
|
|
104
|
+
_set_attrs(obj, data, allow_missing=False, skip=skip)
|
|
105
|
+
await _maybe_flush(db)
|
|
106
|
+
logger.debug("replace updated obj=%s", obj)
|
|
107
|
+
return obj
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def merge(
|
|
111
|
+
model: type, ident: Any, data: Mapping[str, Any], db: Union[Session, AsyncSession]
|
|
112
|
+
) -> Any:
|
|
113
|
+
"""PATCH semantics with upsert behaviour."""
|
|
114
|
+
logger.debug("merge called with model=%s ident=%s data=%s", model, ident, data)
|
|
115
|
+
pk = _single_pk_name(model)
|
|
116
|
+
ident = _coerce_pk_value(model, ident)
|
|
117
|
+
obj = await _maybe_get(db, model, ident)
|
|
118
|
+
|
|
119
|
+
verb = "update" if obj is not None else "create"
|
|
120
|
+
data = _filter_in_values(model, data or {}, verb)
|
|
121
|
+
_validate_enum_values(model, data)
|
|
122
|
+
data_no_pk = {k: v for k, v in data.items() if k != pk}
|
|
123
|
+
if obj is None:
|
|
124
|
+
payload = {pk: ident, **data_no_pk}
|
|
125
|
+
result = await create(model, payload, db=db)
|
|
126
|
+
logger.debug("merge created obj=%s", result)
|
|
127
|
+
return result
|
|
128
|
+
skip = _immutable_columns(model, "update")
|
|
129
|
+
_set_attrs(obj, data_no_pk, allow_missing=True, skip=skip)
|
|
130
|
+
await _maybe_flush(db)
|
|
131
|
+
logger.debug("merge updated obj=%s", obj)
|
|
132
|
+
return obj
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def delete(
|
|
136
|
+
model: type, ident: Any, db: Union[Session, AsyncSession]
|
|
137
|
+
) -> Dict[str, int]:
|
|
138
|
+
"""
|
|
139
|
+
Delete by primary key. Returns {"deleted": 1} if removed, else raises NoResultFound.
|
|
140
|
+
Flush-only.
|
|
141
|
+
"""
|
|
142
|
+
logger.debug("delete called with model=%s ident=%s", model, ident)
|
|
143
|
+
obj = await read(model, ident, db)
|
|
144
|
+
await _maybe_delete(db, obj)
|
|
145
|
+
await _maybe_flush(db)
|
|
146
|
+
logger.debug("delete removed obj=%s", obj)
|
|
147
|
+
return {"deleted": 1}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# NOTE: tolerant signature: accepts positional/keyword and ignores stray args
|
|
151
|
+
async def list(*_args: Any, **_kwargs: Any) -> List[Any]: # noqa: A001 (shadow built-in)
|
|
152
|
+
"""
|
|
153
|
+
Simple list with equality filters + skip/limit (+ optional sort).
|
|
154
|
+
Tolerant to:
|
|
155
|
+
- missing filters (defaults to {})
|
|
156
|
+
- accidental bound-method 'self' (first positional arg)
|
|
157
|
+
- positional or keyword args
|
|
158
|
+
- stray extras (e.g., request) which are ignored
|
|
159
|
+
"""
|
|
160
|
+
logger.debug("list called with args=%s kwargs=%s", _args, _kwargs)
|
|
161
|
+
model, params = _normalize_list_call(_args, _kwargs)
|
|
162
|
+
|
|
163
|
+
filters: Mapping[str, Any] = _coerce_filters(model, params["filters"])
|
|
164
|
+
skip: Optional[int] = params["skip"]
|
|
165
|
+
limit: Optional[int] = params["limit"]
|
|
166
|
+
db: Union[Session, AsyncSession] = params["db"]
|
|
167
|
+
sort = params["sort"]
|
|
168
|
+
|
|
169
|
+
if select is None: # pragma: no cover
|
|
170
|
+
# Fallback: legacy query API
|
|
171
|
+
q = db.query(model) # type: ignore[attr-defined]
|
|
172
|
+
if filters:
|
|
173
|
+
q = q.filter_by(**filters) # type: ignore[attr-defined]
|
|
174
|
+
if isinstance(skip, int):
|
|
175
|
+
q = q.offset(max(skip, 0)) # type: ignore[attr-defined]
|
|
176
|
+
if isinstance(limit, int) and limit is not None:
|
|
177
|
+
q = q.limit(max(limit, 0)) # type: ignore[attr-defined]
|
|
178
|
+
return _builtins.list(q.all()) # type: ignore[attr-defined]
|
|
179
|
+
|
|
180
|
+
where = _apply_filters(model, filters)
|
|
181
|
+
stmt = select(model)
|
|
182
|
+
if where is not None:
|
|
183
|
+
stmt = stmt.where(where)
|
|
184
|
+
|
|
185
|
+
order_exprs = _apply_sort(model, sort)
|
|
186
|
+
if order_exprs:
|
|
187
|
+
for ob in order_exprs:
|
|
188
|
+
stmt = stmt.order_by(ob)
|
|
189
|
+
|
|
190
|
+
if isinstance(skip, int):
|
|
191
|
+
stmt = stmt.offset(max(skip, 0))
|
|
192
|
+
if isinstance(limit, int) and limit is not None:
|
|
193
|
+
stmt = stmt.limit(max(limit, 0))
|
|
194
|
+
|
|
195
|
+
result = await _maybe_execute(db, stmt)
|
|
196
|
+
items = _builtins.list(result.scalars().all()) # type: ignore[attr-defined]
|
|
197
|
+
logger.debug("list returning %d items", len(items))
|
|
198
|
+
return items
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def clear(
|
|
202
|
+
*args: Any,
|
|
203
|
+
**kwargs: Any,
|
|
204
|
+
) -> Dict[str, int]:
|
|
205
|
+
"""
|
|
206
|
+
Delete many rows matching equality filters. Returns {"deleted": N}.
|
|
207
|
+
Flush-only. Tolerant to the same calling variations as `list`.
|
|
208
|
+
"""
|
|
209
|
+
# Reuse normalizer to accept the same shapes
|
|
210
|
+
logger.debug("clear called with args=%s kwargs=%s", args, kwargs)
|
|
211
|
+
model, params = _normalize_list_call(args, kwargs)
|
|
212
|
+
raw_filters: Mapping[str, Any] = params["filters"]
|
|
213
|
+
db: Union[Session, AsyncSession] = params["db"]
|
|
214
|
+
|
|
215
|
+
if sa_delete is None: # pragma: no cover
|
|
216
|
+
# Fallback path: manual iteration
|
|
217
|
+
items = await list(model, raw_filters, db=db)
|
|
218
|
+
n = 0
|
|
219
|
+
for obj in items:
|
|
220
|
+
await _maybe_delete(db, obj)
|
|
221
|
+
n += 1
|
|
222
|
+
await _maybe_flush(db)
|
|
223
|
+
return {"deleted": n}
|
|
224
|
+
|
|
225
|
+
filt = _coerce_filters(model, raw_filters)
|
|
226
|
+
where = _apply_filters(model, filt)
|
|
227
|
+
stmt = sa_delete(model)
|
|
228
|
+
if where is not None:
|
|
229
|
+
stmt = stmt.where(where)
|
|
230
|
+
|
|
231
|
+
res = await _maybe_execute(db, stmt)
|
|
232
|
+
await _maybe_flush(db)
|
|
233
|
+
n = int(getattr(res, "rowcount", 0) or 0)
|
|
234
|
+
logger.debug("clear removed %d rows", n)
|
|
235
|
+
return {"deleted": n}
|