diff --git a/pkgs/standards/autoapi/autoapi/v3/mixins/_RowBound.py b/pkgs/standards/autoapi/autoapi/v3/mixins/_RowBound.py index 582ed6a3b9..36bb9ca9d8 100644 --- a/pkgs/standards/autoapi/autoapi/v3/mixins/_RowBound.py +++ b/pkgs/standards/autoapi/autoapi/v3/mixins/_RowBound.py @@ -1,11 +1,13 @@ -# autoapi/v2/mixins/bound.py +# autoapi/v3/mixins/_RowBound.py from __future__ import annotations from typing import Any, Mapping, Sequence -from autoapi.v2.hooks import Phase -from autoapi.v2.types import HookProvider -from autoapi.v2.jsonrpc_models import HTTP_ERROR_MESSAGES, create_standardized_error +from ..types import HookProvider +from ..impl.runtime.errors import ( + HTTP_ERROR_MESSAGES, + create_standardized_error_from_status, +) class _RowBound(HookProvider): @@ -32,7 +34,7 @@ def __autoapi_register_hooks__(cls, api) -> None: return for op in ("read", "list"): - api.register_hook(model=cls, phase=Phase.POST_HANDLER, op=op)( + api.register_hook(model=cls, phase="POST_HANDLER", op=op)( cls._make_row_visibility_hook() ) @@ -54,7 +56,7 @@ def _row_visibility_hook(ctx: Mapping[str, Any]) -> None: # READ → invisible row → pretend 404 if not cls.is_visible(res, ctx): - http_exc, _, _ = create_standardized_error( + http_exc, _, _ = create_standardized_error_from_status( 404, message=HTTP_ERROR_MESSAGES[404] ) raise http_exc diff --git a/pkgs/standards/autoapi/autoapi/v3/mixins/__init__.py b/pkgs/standards/autoapi/autoapi/v3/mixins/__init__.py index 13480ae070..6d6aed4c82 100644 --- a/pkgs/standards/autoapi/autoapi/v3/mixins/__init__.py +++ b/pkgs/standards/autoapi/autoapi/v3/mixins/__init__.py @@ -24,7 +24,7 @@ UUID, uuid4, ) -from ..cfgs import AUTH_CONTEXT_KEY, USER_ID_KEY +from ..config.constants import CTX_USER_ID_KEY def tzutcnow() -> dt.datetime: # default/on‑update factory @@ -155,8 +155,7 @@ class OwnerBound: @classmethod def filter_for_ctx(cls, q, ctx): - auto_fields = ctx.get(AUTH_CONTEXT_KEY, {}) - return q.filter(cls.owner_id == auto_fields.get(USER_ID_KEY)) + return q.filter(cls.owner_id == ctx.get(CTX_USER_ID_KEY)) class UserBound: # membership rows @@ -168,8 +167,7 @@ class UserBound: # membership rows @classmethod def filter_for_ctx(cls, q, ctx): - auto_fields = ctx.get(AUTH_CONTEXT_KEY, {}) - return q.filter(cls.user_id == auto_fields.get(USER_ID_KEY)) + return q.filter(cls.user_id == ctx.get(CTX_USER_ID_KEY)) # ────────── lifecycle -------------------------------------------------- diff --git a/pkgs/standards/autoapi/autoapi/v3/mixins/ownable.py b/pkgs/standards/autoapi/autoapi/v3/mixins/ownable.py index e1c2c6d9b0..afcdcba0e7 100644 --- a/pkgs/standards/autoapi/autoapi/v3/mixins/ownable.py +++ b/pkgs/standards/autoapi/autoapi/v3/mixins/ownable.py @@ -2,11 +2,10 @@ import logging from uuid import UUID -from ..hooks import Phase -from ..jsonrpc_models import create_standardized_error from ..info_schema import check as _info_check from ..types import Column, ForeignKey, PgUUID, declared_attr -from ..cfgs import AUTH_CONTEXT_KEY, INJECTED_FIELDS_KEY, USER_ID_KEY +from ..config.constants import CTX_USER_ID_KEY +from ..impl.runtime.errors import create_standardized_error_from_status log = logging.getLogger(__name__) @@ -73,7 +72,7 @@ def __autoapi_register_hooks__(cls, api): pol = cls.__autoapi_owner_policy__ def _err(status: int, msg: str): - http_exc, _, _ = create_standardized_error(status, message=msg) + http_exc, _, _ = create_standardized_error_from_status(status, message=msg) raise http_exc def _ownable_before_create(ctx): @@ -82,16 +81,14 @@ def _ownable_before_create(ctx): # keep None so we can treat it as "missing" explicitly params = params.model_dump() - auto_fields = ctx.get(AUTH_CONTEXT_KEY, {}) - user_id = auto_fields.get(USER_ID_KEY) + user_id = ctx.get(CTX_USER_ID_KEY) provided = params.get("owner_id") missing = _is_missing(provided) log.info( - "Ownable before_create policy=%s params=%s auto_fields=%s", + "Ownable before_create policy=%s params=%s", pol, params, - auto_fields, ) if pol == OwnerPolicy.STRICT_SERVER: @@ -126,8 +123,7 @@ def _ownable_before_update(ctx, obj): _err(400, "owner_id is immutable.") new_val = _normalize_uuid(params["owner_id"]) - auto_fields = ctx.get(INJECTED_FIELDS_KEY, {}) - user_id = _normalize_uuid(auto_fields.get(USER_ID_KEY)) + user_id = _normalize_uuid(ctx.get(CTX_USER_ID_KEY)) log.info( "Ownable before_update new_val=%s obj_owner=%s injected=%s", @@ -142,9 +138,9 @@ def _ownable_before_update(ctx, obj): ): _err(403, "Cannot transfer ownership.") - api.register_hook(model=cls, phase=Phase.PRE_TX_BEGIN, op="create")( + api.register_hook(model=cls, phase="PRE_TX_BEGIN", op="create")( _ownable_before_create ) - api.register_hook(model=cls, phase=Phase.PRE_TX_BEGIN, op="update")( + api.register_hook(model=cls, phase="PRE_TX_BEGIN", op="update")( _ownable_before_update ) diff --git a/pkgs/standards/autoapi/autoapi/v3/mixins/tenant_bound.py b/pkgs/standards/autoapi/autoapi/v3/mixins/tenant_bound.py index 2ff04e6d23..e61eea2618 100644 --- a/pkgs/standards/autoapi/autoapi/v3/mixins/tenant_bound.py +++ b/pkgs/standards/autoapi/autoapi/v3/mixins/tenant_bound.py @@ -4,10 +4,9 @@ from ._RowBound import _RowBound from ..types import Column, ForeignKey, PgUUID, declared_attr -from ..hooks import Phase -from ..jsonrpc_models import create_standardized_error from ..info_schema import check as _info_check -from ..cfgs import AUTH_CONTEXT_KEY, INJECTED_FIELDS_KEY, TENANT_ID_KEY +from ..config.constants import CTX_TENANT_ID_KEY +from ..impl.runtime.errors import create_standardized_error_from_status log = logging.getLogger(__name__) @@ -89,8 +88,7 @@ def __tablename__(cls): # ------------------------------------------------------------------- @staticmethod def is_visible(obj, ctx) -> bool: - auto_fields = ctx.get(AUTH_CONTEXT_KEY, {}) - ctx_tenant_id = auto_fields.get(TENANT_ID_KEY) + ctx_tenant_id = ctx.get(CTX_TENANT_ID_KEY) return getattr(obj, "tenant_id", None) == ctx_tenant_id # ------------------------------------------------------------------- @@ -101,7 +99,7 @@ def __autoapi_register_hooks__(cls, api): pol = cls.__autoapi_tenant_policy__ def _err(code: int, msg: str): - http_exc, _, _ = create_standardized_error(code, message=msg) + http_exc, _, _ = create_standardized_error_from_status(code, message=msg) raise http_exc # INSERT @@ -112,9 +110,7 @@ def _tenantbound_before_create(ctx): # and rely on _is_missing() to decide. params = params.model_dump() - auto_fields = ctx.get(INJECTED_FIELDS_KEY, {}) - print(f"\n🚧{auto_fields}") - injected_tid = auto_fields.get(TENANT_ID_KEY) + injected_tid = ctx.get(CTX_TENANT_ID_KEY) print(f"\n🚧🚧{injected_tid}") provided = params.get("tenant_id") missing = _is_missing(provided) @@ -155,8 +151,7 @@ def _tenantbound_before_update(ctx, obj): _err(400, "tenant_id is immutable.") new_val = _normalize_uuid(provided) - auto_fields = ctx.get(INJECTED_FIELDS_KEY, {}) - injected_tid = _normalize_uuid(auto_fields.get(TENANT_ID_KEY)) + injected_tid = _normalize_uuid(ctx.get(CTX_TENANT_ID_KEY)) log.info( "TenantBound before_update new_val=%s obj_tid=%s injected=%s", @@ -173,9 +168,9 @@ def _tenantbound_before_update(ctx, obj): _err(403, "Cannot switch tenant context.") # Register hooks - api.register_hook(model=cls, phase=Phase.PRE_TX_BEGIN, op="create")( + api.register_hook(model=cls, phase="PRE_TX_BEGIN", op="create")( _tenantbound_before_create ) - api.register_hook(model=cls, phase=Phase.PRE_TX_BEGIN, op="update")( + api.register_hook(model=cls, phase="PRE_TX_BEGIN", op="update")( _tenantbound_before_update ) diff --git a/pkgs/standards/autoapi/autoapi/v3/mixins/upsertable.py b/pkgs/standards/autoapi/autoapi/v3/mixins/upsertable.py index 2ed7d35e09..e8f1cdb751 100644 --- a/pkgs/standards/autoapi/autoapi/v3/mixins/upsertable.py +++ b/pkgs/standards/autoapi/autoapi/v3/mixins/upsertable.py @@ -2,8 +2,8 @@ from __future__ import annotations from typing import Any, Mapping, Sequence, Optional, Tuple from sqlalchemy import and_, inspect as sa_inspect -from autoapi.v2.hooks import Phase -from autoapi.v2.types import Session, HookProvider +from ..types import Session, HookProvider + class Upsertable(HookProvider): """ @@ -12,13 +12,14 @@ class Upsertable(HookProvider): • Else if all PK parts are present -> decide by PK • Else -> no rewrite """ + __upsert_keys__: Sequence[str] | None = None # optional natural key list @classmethod def __autoapi_register_hooks__(cls, api) -> None: model = cls.__tablename__ for op in ("create", "update", "replace"): - api.register_hook(Phase.PRE_TX_BEGIN, model=model, op=op)( + api.register_hook("PRE_TX_BEGIN", model=model, op=op)( cls._make_upsert_rewrite_hook(op) ) @@ -57,7 +58,10 @@ async def _rewrite(ctx: Mapping[str, Any]) -> None: return _rewrite -def _extract_values(p: Mapping[str, Any], names: Sequence[str]) -> Optional[Tuple[Any, ...]]: + +def _extract_values( + p: Mapping[str, Any], names: Sequence[str] +) -> Optional[Tuple[Any, ...]]: vals = [] for n in names: v = p.get(n) @@ -66,12 +70,16 @@ def _extract_values(p: Mapping[str, Any], names: Sequence[str]) -> Optional[Tupl vals.append(v) return tuple(vals) -def _exists_by_names(model, db: Session, names: Sequence[str], vals: Tuple[Any, ...]) -> bool: + +def _exists_by_names( + model, db: Session, names: Sequence[str], vals: Tuple[Any, ...] +) -> bool: q = db.query(model) for n, v in zip(names, vals): q = q.filter(getattr(model, n) == v) return db.query(q.exists()).scalar() is True + def _exists_by_pk(model, db: Session, pk_cols, pk_vals: Tuple[Any, ...]) -> bool: if len(pk_cols) == 1: # fast path @@ -79,6 +87,7 @@ def _exists_by_pk(model, db: Session, pk_cols, pk_vals: Tuple[Any, ...]) -> bool conds = [getattr(model, c.key) == v for c, v in zip(pk_cols, pk_vals)] return db.query(db.query(model).filter(and_(*conds)).exists()).scalar() is True + def _rewrite_by_existence(ctx, tab: str, verb: str, exists: bool) -> None: if verb == "create" and exists: ctx["env"].method = f"{tab}.update"