tigrbl-orm 0.1.0.dev1__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.
@@ -0,0 +1,301 @@
1
+ """Tenant scoping mixin for Tigrbl v3."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from enum import Enum
7
+ from typing import Any, Mapping
8
+ from uuid import UUID
9
+
10
+ from ._RowBound import _RowBound
11
+ from ...specs import acol
12
+ from ...config.constants import (
13
+ TIGRBL_TENANT_POLICY_ATTR,
14
+ CTX_AUTH_KEY,
15
+ CTX_TENANT_ID_KEY,
16
+ )
17
+ from ...runtime.status import create_standardized_error
18
+ from ...specs import ColumnSpec, F, IO, S
19
+ from ..._spec.storage_spec import ForeignKeySpec
20
+ from ...types import Mapped, PgUUID, declared_attr
21
+
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+
26
+ class TenantPolicy(str, Enum):
27
+ CLIENT_SET = "client" # client may supply tenant_id on create/update
28
+ DEFAULT_TO_CTX = "default" # server fills tenant_id on create; immutable
29
+ STRICT_SERVER = "strict" # server forces tenant_id and forbids changes
30
+
31
+
32
+ def _infer_schema(cls, default: str = "public") -> str:
33
+ args = getattr(cls, "__table_args__", None)
34
+ if not args:
35
+ return default
36
+ if isinstance(args, dict):
37
+ return args.get("schema", default)
38
+ if isinstance(args, (tuple, list)):
39
+ for elem in args:
40
+ if isinstance(elem, dict) and "schema" in elem:
41
+ return elem["schema"]
42
+ return default
43
+
44
+
45
+ def _is_missing(value) -> bool:
46
+ """Treat None or empty strings as 'not provided'."""
47
+ return value is None or (isinstance(value, str) and not value.strip())
48
+
49
+
50
+ def _normalize_uuid(val):
51
+ if isinstance(val, UUID):
52
+ return val
53
+ if isinstance(val, str):
54
+ try:
55
+ return UUID(val)
56
+ except ValueError:
57
+ return val # let model validation surface the error
58
+ return val
59
+
60
+
61
+ class TenantBound(_RowBound):
62
+ """
63
+ Plug-and-play tenant isolation.
64
+
65
+ • tenant_id column is defined per subclass (declared_attr) so the schema
66
+ builder sees the right flags before caching.
67
+ • _RowBound’s read/list filters work because we implement `is_visible`.
68
+ """
69
+
70
+ __tigrbl_tenant_policy__: TenantPolicy = TenantPolicy.CLIENT_SET
71
+
72
+ # ────────────────────────────────────────────────────────────────────
73
+ # tenant_id column (Schema-Aware; PgUUID(as_uuid=True))
74
+ # -------------------------------------------------------------------
75
+ @declared_attr
76
+ def tenant_id(cls) -> Mapped[UUID]:
77
+ pol = getattr(cls, TIGRBL_TENANT_POLICY_ATTR, TenantPolicy.CLIENT_SET)
78
+ schema = _infer_schema(cls, default="public")
79
+
80
+ in_verbs = (
81
+ ("create", "update", "replace")
82
+ if pol == TenantPolicy.CLIENT_SET
83
+ else ("create",)
84
+ )
85
+ io = IO(
86
+ in_verbs=in_verbs,
87
+ out_verbs=("read", "list"),
88
+ mutable_verbs=in_verbs,
89
+ )
90
+
91
+ spec = ColumnSpec(
92
+ storage=S(
93
+ type_=PgUUID(as_uuid=True),
94
+ fk=ForeignKeySpec(target=f"{schema}.tenants.id"),
95
+ nullable=False,
96
+ index=True,
97
+ ),
98
+ field=F(py_type=UUID),
99
+ io=io,
100
+ )
101
+ return acol(spec=spec)
102
+
103
+ @declared_attr
104
+ def __tablename__(cls):
105
+ return cls.__name__.lower()
106
+
107
+ # -------------------------------------------------------------------
108
+ # Row-level visibility for _RowBound
109
+ # -------------------------------------------------------------------
110
+ @staticmethod
111
+ def is_visible(obj, ctx) -> bool:
112
+ return getattr(obj, "tenant_id", None) == _ctx_tenant_id(ctx)
113
+
114
+ # -------------------------------------------------------------------
115
+ # Runtime hooks
116
+ # -------------------------------------------------------------------
117
+ def __init_subclass__(cls, **kw):
118
+ super().__init_subclass__(**kw)
119
+ cls._install_tenant_bound_hooks()
120
+
121
+ @classmethod
122
+ def _install_tenant_bound_hooks(cls) -> None:
123
+ pol = getattr(cls, "__tigrbl_tenant_policy__", TenantPolicy.CLIENT_SET)
124
+
125
+ def _err(code: int, msg: str) -> None:
126
+ http_exc, _, _ = create_standardized_error(code, message=msg)
127
+ raise http_exc
128
+
129
+ def _before_create(ctx: dict[str, Any]) -> None:
130
+ env = (
131
+ ctx.get("env") if isinstance(ctx, dict) else getattr(ctx, "env", None)
132
+ ) or {}
133
+ params = (
134
+ (
135
+ env.get("params")
136
+ if isinstance(env, dict)
137
+ else getattr(env, "params", None)
138
+ )
139
+ or (
140
+ ctx.get("payload")
141
+ if isinstance(ctx, dict)
142
+ else getattr(ctx, "payload", None)
143
+ )
144
+ or {}
145
+ )
146
+ if hasattr(params, "model_dump"):
147
+ params = params.model_dump()
148
+
149
+ tenant_id = _ctx_tenant_id(ctx)
150
+ provided = params.get("tenant_id")
151
+ missing = _is_missing(provided)
152
+
153
+ if pol == TenantPolicy.STRICT_SERVER:
154
+ if tenant_id is None:
155
+ _err(400, "tenant_id is required.")
156
+ if not missing:
157
+ if _normalize_uuid(provided) != _normalize_uuid(tenant_id):
158
+ _err(400, "tenant_id mismatch.")
159
+ _err(400, "tenant_id is server-assigned.")
160
+ params["tenant_id"] = tenant_id
161
+ elif pol == TenantPolicy.DEFAULT_TO_CTX:
162
+ if missing and tenant_id is not None:
163
+ params["tenant_id"] = tenant_id
164
+ elif not missing:
165
+ params["tenant_id"] = _normalize_uuid(provided)
166
+ else: # CLIENT_SET
167
+ if not missing:
168
+ params["tenant_id"] = _normalize_uuid(provided)
169
+
170
+ env_attr = (
171
+ ctx.get("env") if isinstance(ctx, dict) else getattr(ctx, "env", None)
172
+ )
173
+ if env_attr is not None:
174
+ if isinstance(env_attr, dict):
175
+ env_attr["params"] = params
176
+ else:
177
+ env_attr.params = params
178
+ if isinstance(ctx, dict):
179
+ ctx["payload"] = params
180
+ else:
181
+ setattr(ctx, "payload", params)
182
+
183
+ def _before_update(ctx: dict[str, Any]) -> None:
184
+ env = (
185
+ ctx.get("env") if isinstance(ctx, dict) else getattr(ctx, "env", None)
186
+ ) or {}
187
+ params = (
188
+ (
189
+ env.get("params")
190
+ if isinstance(env, dict)
191
+ else getattr(env, "params", None)
192
+ )
193
+ or (
194
+ ctx.get("payload")
195
+ if isinstance(ctx, dict)
196
+ else getattr(ctx, "payload", None)
197
+ )
198
+ or {}
199
+ )
200
+ if hasattr(params, "model_dump"):
201
+ params = params.model_dump()
202
+
203
+ if "tenant_id" not in params:
204
+ return
205
+
206
+ if _is_missing(params.get("tenant_id")):
207
+ params.pop("tenant_id", None)
208
+ env_attr = (
209
+ ctx.get("env")
210
+ if isinstance(ctx, dict)
211
+ else getattr(ctx, "env", None)
212
+ )
213
+ if env_attr is not None:
214
+ if isinstance(env_attr, dict):
215
+ env_attr["params"] = params
216
+ else:
217
+ env_attr.params = params
218
+ if isinstance(ctx, dict):
219
+ ctx["payload"] = params
220
+ else:
221
+ setattr(ctx, "payload", params)
222
+ return
223
+
224
+ if pol != TenantPolicy.CLIENT_SET:
225
+ _err(400, "tenant_id is immutable.")
226
+
227
+ new_val = _normalize_uuid(params["tenant_id"])
228
+ tenant_id = _ctx_tenant_id(ctx)
229
+ is_admin = bool(ctx.get("is_admin"))
230
+
231
+ if not is_admin and tenant_id is not None and new_val != tenant_id:
232
+ _err(403, "Cannot switch tenant context.")
233
+
234
+ params["tenant_id"] = new_val
235
+ env_attr = (
236
+ ctx.get("env") if isinstance(ctx, dict) else getattr(ctx, "env", None)
237
+ )
238
+ if env_attr is not None:
239
+ if isinstance(env_attr, dict):
240
+ env_attr["params"] = params
241
+ else:
242
+ env_attr.params = params
243
+ if isinstance(ctx, dict):
244
+ ctx["payload"] = params
245
+ else:
246
+ setattr(ctx, "payload", params)
247
+
248
+ hooks = {**getattr(cls, "__tigrbl_hooks__", {})}
249
+
250
+ def _append(alias: str, phase: str, fn) -> None:
251
+ phase_map = hooks.get(alias) or {}
252
+ lst = list(phase_map.get(phase) or [])
253
+ if fn not in lst:
254
+ lst.append(fn)
255
+ phase_map[phase] = tuple(lst)
256
+ hooks[alias] = phase_map
257
+
258
+ _append("create", "PRE_TX_BEGIN", _before_create)
259
+ _append("update", "PRE_TX_BEGIN", _before_update)
260
+
261
+ setattr(cls, "__tigrbl_hooks__", hooks)
262
+
263
+
264
+ def _ctx_tenant_id(ctx: Mapping[str, Any]) -> Any | None:
265
+ """Best-effort extraction of tenant_id from ctx."""
266
+ t = (
267
+ ctx.get(CTX_TENANT_ID_KEY)
268
+ if isinstance(ctx, dict)
269
+ else getattr(ctx, CTX_TENANT_ID_KEY, None)
270
+ )
271
+ if t:
272
+ return _normalize_uuid(t)
273
+
274
+ auth = (
275
+ ctx.get(CTX_AUTH_KEY)
276
+ if isinstance(ctx, dict)
277
+ else getattr(ctx, CTX_AUTH_KEY, None)
278
+ ) or {}
279
+ t = auth.get(CTX_TENANT_ID_KEY)
280
+ if t:
281
+ return _normalize_uuid(t)
282
+
283
+ inj = (
284
+ ctx.get("injected_fields")
285
+ if isinstance(ctx, dict)
286
+ else getattr(ctx, "injected_fields", None)
287
+ ) or {}
288
+ t = inj.get(CTX_TENANT_ID_KEY)
289
+ if t:
290
+ return _normalize_uuid(t)
291
+
292
+ ac = (
293
+ ctx.get("auth_context")
294
+ if isinstance(ctx, dict)
295
+ else getattr(ctx, "auth_context", None)
296
+ ) or {}
297
+ t = ac.get(CTX_TENANT_ID_KEY)
298
+ if t:
299
+ return _normalize_uuid(t)
300
+
301
+ return None
@@ -0,0 +1,118 @@
1
+ # tigrbl/v3/mixins/upsertable.py
2
+ from __future__ import annotations
3
+
4
+ import warnings
5
+
6
+ from typing import Any, Mapping, Sequence, Optional, Tuple
7
+ from sqlalchemy import and_, inspect as sa_inspect
8
+ from tigrbl_typing.types import Session
9
+
10
+
11
+ class Upsertable:
12
+ """
13
+ Hybrid upsert:
14
+ • If __upsert_keys__ is set and fully present -> decide by those keys
15
+ • Else if all PK parts are present -> decide by PK
16
+ • Else -> no rewrite
17
+ """
18
+
19
+ __upsert_keys__: Sequence[str] | None = None # optional natural key list
20
+
21
+ def __init_subclass__(cls, **kw):
22
+ super().__init_subclass__(**kw)
23
+ warnings.warn(
24
+ "Upsertable is deprecated and will be removed in a future release.",
25
+ DeprecationWarning,
26
+ stacklevel=2,
27
+ )
28
+ cls._install_upsertable_hooks()
29
+
30
+ @classmethod
31
+ def _install_upsertable_hooks(cls) -> None:
32
+ hooks = {**getattr(cls, "__tigrbl_hooks__", {})}
33
+
34
+ def _append(alias: str, phase: str, fn) -> None:
35
+ phase_map = hooks.get(alias) or {}
36
+ lst = list(phase_map.get(phase) or [])
37
+ if fn not in lst:
38
+ lst.append(fn)
39
+ phase_map[phase] = tuple(lst)
40
+ hooks[alias] = phase_map
41
+
42
+ for op in ("create", "update", "replace"):
43
+ _append(op, "PRE_TX_BEGIN", cls._make_upsert_rewrite_hook(op))
44
+
45
+ setattr(cls, "__tigrbl_hooks__", hooks)
46
+
47
+ @classmethod
48
+ def _make_upsert_rewrite_hook(cls, verb: str):
49
+ tab = "".join(w.title() for w in cls.__tablename__.split("_"))
50
+
51
+ async def _rewrite(ctx: Mapping[str, Any]) -> None:
52
+ params = ctx["env"].params if ctx.get("env") else {}
53
+ if hasattr(params, "model_dump"):
54
+ params = params.model_dump()
55
+ if not isinstance(params, Mapping):
56
+ return
57
+
58
+ db: Session = ctx["db"]
59
+ mapper = sa_inspect(cls)
60
+
61
+ # 1) Try natural keys if declared
62
+ key_names: Sequence[str] | None = cls.__upsert_keys__
63
+ if key_names:
64
+ kv = _extract_values(params, key_names)
65
+ if kv is not None:
66
+ exists = _exists_by_names(cls, db, key_names, kv)
67
+ _rewrite_by_existence(ctx, tab, verb, exists)
68
+ return # done
69
+
70
+ # 2) Fall back to PKs if fully present
71
+ pk_cols = tuple(mapper.primary_key)
72
+ pk_names = tuple(c.key for c in pk_cols)
73
+ kv = _extract_values(params, pk_names)
74
+ if kv is None:
75
+ return # not enough info → no rewrite
76
+
77
+ exists = _exists_by_pk(cls, db, pk_cols, kv)
78
+ _rewrite_by_existence(ctx, tab, verb, exists)
79
+
80
+ return _rewrite
81
+
82
+
83
+ def _extract_values(
84
+ p: Mapping[str, Any], names: Sequence[str]
85
+ ) -> Optional[Tuple[Any, ...]]:
86
+ vals = []
87
+ for n in names:
88
+ v = p.get(n)
89
+ if v is None:
90
+ return None
91
+ vals.append(v)
92
+ return tuple(vals)
93
+
94
+
95
+ def _exists_by_names(
96
+ model, db: Session, names: Sequence[str], vals: Tuple[Any, ...]
97
+ ) -> bool:
98
+ q = db.query(model)
99
+ for n, v in zip(names, vals):
100
+ q = q.filter(getattr(model, n) == v)
101
+ return db.query(q.exists()).scalar() is True
102
+
103
+
104
+ def _exists_by_pk(model, db: Session, pk_cols, pk_vals: Tuple[Any, ...]) -> bool:
105
+ if len(pk_cols) == 1:
106
+ # fast path
107
+ return db.get(model, pk_vals[0]) is not None
108
+ conds = [getattr(model, c.key) == v for c, v in zip(pk_cols, pk_vals)]
109
+ return db.query(db.query(model).filter(and_(*conds)).exists()).scalar() is True
110
+
111
+
112
+ def _rewrite_by_existence(ctx, tab: str, verb: str, exists: bool) -> None:
113
+ if verb == "create" and exists:
114
+ ctx["env"].method = f"{tab}.update"
115
+ elif verb == "update" and not exists:
116
+ ctx["env"].method = f"{tab}.create"
117
+ elif verb == "replace":
118
+ ctx["env"].method = f"{tab}.update" if exists else f"{tab}.create"
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+
5
+ from ...specs import IO
6
+ from ...types import UUID
7
+
8
+
9
+ def tzutcnow() -> dt.datetime:
10
+ """Return an aware UTC ``datetime``."""
11
+ return dt.datetime.now(dt.timezone.utc)
12
+
13
+
14
+ def tzutcnow_plus_day() -> dt.datetime:
15
+ """Return an aware UTC ``datetime`` one day in the future."""
16
+ return tzutcnow() + dt.timedelta(days=1)
17
+
18
+
19
+ def _infer_schema(cls, default: str = "public") -> str:
20
+ """Extract schema from ``__table_args__`` in dict or tuple/list form."""
21
+ args = getattr(cls, "__table_args__", None)
22
+ if not args:
23
+ return default
24
+ if isinstance(args, dict):
25
+ return args.get("schema", default)
26
+ if isinstance(args, (tuple, list)):
27
+ for elem in args:
28
+ if isinstance(elem, dict) and "schema" in elem:
29
+ return elem["schema"]
30
+ return default
31
+
32
+
33
+ uuid_example = UUID("00000000-dead-beef-cafe-000000000000")
34
+
35
+ CRUD_IN = ("create", "update", "replace")
36
+ CRUD_OUT = ("read", "list")
37
+ CRUD_IO = IO(in_verbs=CRUD_IN, out_verbs=CRUD_OUT, mutable_verbs=CRUD_IN)
38
+ RO_IO = IO(out_verbs=CRUD_OUT)
39
+
40
+ __all__ = [
41
+ "tzutcnow",
42
+ "tzutcnow_plus_day",
43
+ "_infer_schema",
44
+ "uuid_example",
45
+ "CRUD_IN",
46
+ "CRUD_OUT",
47
+ "CRUD_IO",
48
+ "RO_IO",
49
+ ]
@@ -0,0 +1,73 @@
1
+ """Public façade for all table classes.
2
+
3
+ Usage
4
+ -----
5
+ from tigrbl_orm.orm.tables import (
6
+ Tenant,
7
+ User,
8
+ Group,
9
+ Role,
10
+ )
11
+ """
12
+
13
+ import importlib
14
+ from typing import TYPE_CHECKING, Any
15
+ from ._base import TableBase, Base
16
+
17
+ __all__ = [
18
+ "Tenant",
19
+ "Client",
20
+ "User",
21
+ "Group",
22
+ "Org",
23
+ "Role",
24
+ "RolePerm",
25
+ "RoleGrant",
26
+ "Status",
27
+ "StatusEnum",
28
+ "Change",
29
+ "TableBase",
30
+ "Base",
31
+ ]
32
+
33
+ # ------------------------------------------------------------------ #
34
+ # Lazy attribute loader (PEP 562). Keeps import graphs light-weight.
35
+ # ------------------------------------------------------------------ #
36
+ _module_map = {
37
+ "Tenant": f"{__name__}.tenant",
38
+ "Client": f"{__name__}.client",
39
+ "User": f"{__name__}.user",
40
+ "Group": f"{__name__}.group",
41
+ "Org": f"{__name__}.org",
42
+ "Role": f"{__name__}.rbac",
43
+ "RolePerm": f"{__name__}.rbac",
44
+ "RoleGrant": f"{__name__}.rbac",
45
+ "Status": f"{__name__}.status",
46
+ "StatusEnum": f"{__name__}.status",
47
+ "Change": f"{__name__}.audit",
48
+ }
49
+
50
+
51
+ def __getattr__(name: str) -> Any: # noqa: D401
52
+ """Dynamically import `tenant`, `user`, or `group` on first use."""
53
+ if name not in _module_map:
54
+ raise AttributeError(name)
55
+ module = importlib.import_module(_module_map[name])
56
+ obj = getattr(module, name)
57
+ globals()[name] = obj # cache for future look-ups
58
+ return obj
59
+
60
+
61
+ # ------------------------------------------------------------------ #
62
+ # Static typing support – imported eagerly only during type checking.
63
+ # ------------------------------------------------------------------ #
64
+ if TYPE_CHECKING: # pragma: no cover
65
+ from ._base import TableBase, Base
66
+ from .tenant import Tenant
67
+ from .client import Client
68
+ from .user import User
69
+ from .group import Group
70
+ from .org import Org
71
+ from .rbac import Role, RoleGrant, RolePerm
72
+ from .status import Status, StatusEnum
73
+ from .audit import Change
@@ -0,0 +1,7 @@
1
+ """Primary TableBase export for ORM tables."""
2
+
3
+ from ..._concrete._table import Table as TableBase
4
+
5
+ Base = TableBase
6
+
7
+ __all__ = ["TableBase", "Base"]
@@ -0,0 +1,56 @@
1
+ # tigrbl/tables/audit.py
2
+ import datetime as dt
3
+ from uuid import UUID
4
+
5
+ from . import TableBase
6
+ from ..mixins import GUIDPk, Timestamped
7
+ from ...specs import IO, F, acol, S
8
+ from ...types import DateTime, Integer, String, PgUUID, Mapped
9
+
10
+
11
+ class Change(TableBase, GUIDPk, Timestamped):
12
+ __tablename__ = "changes"
13
+
14
+ seq: Mapped[int] = acol(
15
+ storage=S(Integer, primary_key=True),
16
+ field=F(),
17
+ io=IO(out_verbs=("read", "list")),
18
+ )
19
+ at: Mapped[dt.datetime] = acol(
20
+ storage=S(DateTime, default=dt.datetime.utcnow),
21
+ field=F(),
22
+ io=IO(out_verbs=("read", "list")),
23
+ )
24
+ actor_id: Mapped[UUID | None] = acol(
25
+ storage=S(PgUUID, nullable=True),
26
+ field=F(),
27
+ io=IO(out_verbs=("read", "list")),
28
+ )
29
+ table_name: Mapped[str] = acol(
30
+ storage=S(String),
31
+ field=F(),
32
+ io=IO(out_verbs=("read", "list")),
33
+ )
34
+ row_id: Mapped[UUID | None] = acol(
35
+ storage=S(PgUUID, nullable=True),
36
+ field=F(),
37
+ io=IO(out_verbs=("read", "list")),
38
+ )
39
+ action: Mapped[str] = acol(
40
+ storage=S(String),
41
+ field=F(),
42
+ io=IO(out_verbs=("read", "list")),
43
+ )
44
+
45
+
46
+ __all__ = ["Change"]
47
+
48
+ for _name in list(globals()):
49
+ if _name not in __all__ and not _name.startswith("__"):
50
+ del globals()[_name]
51
+
52
+
53
+ def __dir__():
54
+ """Tighten ``dir()`` output for interactive sessions."""
55
+
56
+ return sorted(__all__)
@@ -0,0 +1,25 @@
1
+ # peagen/orm/api_key.py
2
+ from __future__ import annotations
3
+
4
+
5
+ from ...specs import acol, IO, S, F
6
+ from ...types import LargeBinary, Mapped, String
7
+
8
+ from ._base import TableBase
9
+ from ..mixins import ActiveToggle, GUIDPk, Timestamped, TenantBound
10
+
11
+
12
+ class Client(TableBase, GUIDPk, Timestamped, TenantBound, ActiveToggle):
13
+ __tablename__ = "clients"
14
+ __abstract__ = True
15
+ # ---------------------------------------------------------------- columns --
16
+ client_secret_hash: Mapped[bytes] = acol(
17
+ storage=S(LargeBinary(60), nullable=False),
18
+ field=F(),
19
+ io=IO(in_verbs=("create",)),
20
+ )
21
+ redirect_uris: Mapped[str] = acol(
22
+ storage=S(String, nullable=False),
23
+ field=F(constraints={"max_length": 1000}),
24
+ io=IO(),
25
+ )
@@ -0,0 +1,29 @@
1
+ """Group model."""
2
+
3
+ from ._base import TableBase
4
+ from ..mixins import GUIDPk, Timestamped, TenantBound, Principal
5
+ from ...specs import IO, F, acol, S
6
+ from ...types import Mapped, String
7
+
8
+
9
+ class Group(TableBase, GUIDPk, Timestamped, TenantBound, Principal):
10
+ __tablename__ = "groups"
11
+ name: Mapped[str] = acol(
12
+ storage=S(String),
13
+ field=F(),
14
+ io=IO(),
15
+ )
16
+
17
+
18
+ __all__ = ["Group"]
19
+
20
+
21
+ for _name in list(globals()):
22
+ if _name not in __all__ and not _name.startswith("__"):
23
+ del globals()[_name]
24
+
25
+
26
+ def __dir__():
27
+ """Tighten ``dir()`` output for interactive sessions."""
28
+
29
+ return sorted(__all__)
@@ -0,0 +1,30 @@
1
+ """Org model."""
2
+
3
+ from ._base import TableBase
4
+ from ..mixins import GUIDPk, Timestamped, TenantBound, Principal
5
+ from ...specs import IO, F, acol, S
6
+ from ...types import Mapped, String
7
+
8
+
9
+ class Org(TableBase, GUIDPk, Timestamped, TenantBound, Principal):
10
+ __tablename__ = "orgs"
11
+ __abstract__ = True
12
+ name: Mapped[str] = acol(
13
+ storage=S(String),
14
+ field=F(),
15
+ io=IO(),
16
+ )
17
+
18
+
19
+ __all__ = ["Org"]
20
+
21
+
22
+ for _name in list(globals()):
23
+ if _name not in __all__ and not _name.startswith("__"):
24
+ del globals()[_name]
25
+
26
+
27
+ def __dir__():
28
+ """Tighten ``dir()`` output for interactive sessions."""
29
+
30
+ return sorted(__all__)