tigrbl-base 0.4.3.dev4__py3-none-any.whl → 0.4.4.dev7__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.
@@ -13,6 +13,8 @@ _EXPORTS = {
13
13
  "AliasBase": "_alias_base",
14
14
  "EngineBase": "_engine_base",
15
15
  "EngineProviderBase": "_engine_provider_base",
16
+ "HeaderCookiesBase": "_headers_base",
17
+ "HeadersBase": "_headers_base",
16
18
  "HookBase": "_hook_base",
17
19
  "RouterBase": "_router_base",
18
20
  "ForeignKeyBase": "_storage",
@@ -1,9 +1,82 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Iterable, Mapping
4
+ from typing import Any
5
+
6
+ from tigrbl_core._spec.headers_spec import HeadersSpec
7
+
8
+
9
+ HeaderInput = Iterable[tuple[str, str]] | Mapping[str, str] | HeadersSpec | None
10
+
11
+
12
+ def _normalize_header_name(name: Any) -> str:
13
+ if isinstance(name, (bytes, bytearray)):
14
+ return name.decode("latin-1").lower()
15
+ return str(name).lower()
16
+
17
+
18
+ def _normalize_header_value(value: Any) -> str:
19
+ if isinstance(value, (bytes, bytearray)):
20
+ return value.decode("latin-1")
21
+ return str(value)
22
+
23
+
24
+ def _iter_header_items(values: HeaderInput):
25
+ if values is None:
26
+ return ()
27
+ if isinstance(values, HeadersSpec):
28
+ return values.values.items()
29
+ if hasattr(values, "items"):
30
+ return values.items() # type: ignore[union-attr]
31
+ return values
32
+
3
33
 
4
34
  class HeadersBase(dict[str, str]):
5
35
  """Base header mapping type for response implementations."""
6
36
 
37
+ required: tuple[str, ...]
38
+ expose: tuple[str, ...]
39
+
40
+ def __init__(
41
+ self,
42
+ values: HeaderInput = None,
43
+ *,
44
+ required: tuple[str, ...] = (),
45
+ expose: tuple[str, ...] = (),
46
+ ) -> None:
47
+ if isinstance(values, HeadersSpec):
48
+ required = values.required
49
+ expose = values.expose
50
+ super().__init__(
51
+ {
52
+ _normalize_header_name(key): _normalize_header_value(value)
53
+ for key, value in _iter_header_items(values)
54
+ }
55
+ )
56
+ self.required = tuple(_normalize_header_name(name) for name in required)
57
+ self.expose = tuple(_normalize_header_name(name) for name in expose)
58
+
59
+ def __getitem__(self, key: str) -> str:
60
+ return super().__getitem__(_normalize_header_name(key))
61
+
62
+ def __contains__(self, key: object) -> bool:
63
+ if isinstance(key, (str, bytes, bytearray)):
64
+ return super().__contains__(_normalize_header_name(key))
65
+ return super().__contains__(key)
66
+
67
+ def get(self, key: str, default: Any = None) -> str | Any:
68
+ return super().get(_normalize_header_name(key), default)
69
+
70
+ @property
71
+ def raw_headers(self) -> list[tuple[bytes, bytes]]:
72
+ return [
73
+ (key.encode("latin-1"), value.encode("latin-1"))
74
+ for key, value in self.items()
75
+ ]
76
+
77
+ def as_spec(self) -> HeadersSpec:
78
+ return HeadersSpec(values=dict(self), required=self.required, expose=self.expose)
79
+
7
80
 
8
81
  class HeaderCookiesBase(dict[str, str]):
9
82
  """Base cookie mapping type for response implementations."""
@@ -38,18 +38,13 @@ class ResponseBase(ResponseSpec):
38
38
  payload = (
39
39
  body if body is not None else (content if content is not None else b"")
40
40
  )
41
- normalized_headers = {
42
- str(key).lower(): str(value)
43
- for key, value in (
44
- headers.items() if hasattr(headers, "items") else (headers or [])
45
- )
46
- }
41
+ headers_base = HeadersBase(headers)
47
42
 
48
43
  super().__init__(
49
44
  kind=kind,
50
45
  media_type=media_type,
51
46
  status_code=status_code,
52
- headers=dict(normalized_headers),
47
+ headers=dict(headers_base),
53
48
  envelope=envelope,
54
49
  template=template,
55
50
  filename=filename,
@@ -59,15 +54,13 @@ class ResponseBase(ResponseSpec):
59
54
  redirect_to=redirect_to,
60
55
  )
61
56
  self.status_code = status_code
62
- self.headers: HeadersBase = HeadersBase(normalized_headers)
57
+ self.headers: HeadersBase = headers_base
63
58
  self.body = payload
64
59
  self.media_type = media_type
65
60
 
66
61
  @property
67
62
  def raw_headers(self) -> list[tuple[bytes, bytes]]:
68
- return [
69
- (k.encode("latin-1"), v.encode("latin-1")) for k, v in self.headers.items()
70
- ]
63
+ return self.headers.raw_headers
71
64
 
72
65
  @property
73
66
  def headers_map(self) -> HeadersBase:
@@ -9,6 +9,39 @@ from tigrbl_core.config.constants import TIGRBL_SCHEMA_DECLS_ATTR
9
9
  class SchemaBase:
10
10
  """Shared schema helpers used by concrete schema wrappers."""
11
11
 
12
+ @staticmethod
13
+ def bind_declared_schema(
14
+ model: type, alias: str, kind: str, schema_cls: type
15
+ ) -> None:
16
+ """Expose a declared schema on ``model.schemas.<alias>`` immediately."""
17
+
18
+ if kind not in ("in", "out"):
19
+ raise ValueError("schema_ctx(kind=...) must be 'in' or 'out'")
20
+
21
+ schemas = getattr(model, "schemas", None)
22
+ if not isinstance(schemas, SimpleNamespace):
23
+ schemas = SimpleNamespace()
24
+ setattr(model, "schemas", schemas)
25
+
26
+ alias_ns = getattr(schemas, alias, None)
27
+ if not isinstance(alias_ns, SimpleNamespace):
28
+ alias_ns = SimpleNamespace()
29
+ setattr(schemas, alias, alias_ns)
30
+
31
+ setattr(alias_ns, "in_" if kind == "in" else "out", schema_cls)
32
+
33
+ @classmethod
34
+ def bind_nested_declarations(cls, model: type) -> None:
35
+ """Bind nested ``schema_ctx`` declarations onto a table/model class."""
36
+
37
+ for obj in tuple(model.__dict__.values()):
38
+ if not inspect.isclass(obj):
39
+ continue
40
+ decl = getattr(obj, "__tigrbl_schema_decl__", None)
41
+ if decl is None:
42
+ continue
43
+ cls.bind_declared_schema(model, decl.alias, decl.kind, obj)
44
+
12
45
  @classmethod
13
46
  def collect(cls, model: type) -> dict[str, dict[str, type]]:
14
47
  """Collect schema declarations from explicit mappings and decorator metadata."""
@@ -18,6 +18,7 @@ from tigrbl_core._spec.table_profile_spec import (
18
18
  TableProfileError,
19
19
  coerce_table_profile,
20
20
  )
21
+ from ._schema_base import SchemaBase
21
22
 
22
23
  # ──────────────────────────────────────────────────────────────────────────────
23
24
  # Helpers – type inference & SA type instantiation
@@ -521,6 +522,10 @@ class TableBase(DeclarativeBase):
521
522
  # 2) Let SQLAlchemy map the class (PK now exists)
522
523
  super().__init_subclass__(**kw)
523
524
 
525
+ # 2.3) Surface table-local schema_ctx declarations immediately without
526
+ # invoking full schema synthesis or runtime binding.
527
+ SchemaBase.bind_nested_declarations(cls)
528
+
524
529
  # 2.5) Surface ctx-only op declarations for lightweight introspection.
525
530
  has_decorated_ops = False
526
531
  if not hasattr(cls, "__tigrbl_ops__"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrbl-base
3
- Version: 0.4.3.dev4
3
+ Version: 0.4.4.dev7
4
4
  Summary: Base contract package for Tigrbl apps, routers, tables, sessions, middleware, requests, responses, bindings, and engine interfaces.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -1,4 +1,4 @@
1
- tigrbl_base/_base/__init__.py,sha256=8-d-nNB1jEfqGR8m7KLU7H5jwin7vMhiEnpz_1hQxB4,1284
1
+ tigrbl_base/_base/__init__.py,sha256=WCgpQb4SDuYas2jXWtU5BvyhFz71iVru-RGjHXwkDB0,1362
2
2
  tigrbl_base/_base/_alias_base.py,sha256=DeBwVYrkOYbQliZriBfnopsLIEZwn6SQMbkS8hGOrO8,1265
3
3
  tigrbl_base/_base/_app_base.py,sha256=osoATbXZhRjEzR8k0S6ACfIFqXPVM1HyOg6-xcPbIrU,5366
4
4
  tigrbl_base/_base/_assembly.py,sha256=k3KRKijcB6XTzm-pSnqOvkAb_htVHlrm5n-tV_dnC1U,1809
@@ -7,22 +7,22 @@ tigrbl_base/_base/_column_base.py,sha256=tA08qOVe7OTaO-xSQvybE00VJPj2kFZ8Z_UkeJB
7
7
  tigrbl_base/_base/_datatype_lowering.py,sha256=XVVSaOqdAwPViBVuhXpNmrDSawwbBzxoqu0_x1f0IZE,1653
8
8
  tigrbl_base/_base/_engine_base.py,sha256=G5ODOOF413ViX7Pkm6FRl7dcqPdzK-PLJiL2Qv9DQ28,764
9
9
  tigrbl_base/_base/_engine_provider_base.py,sha256=WEj4pISQ7z34SOZMNN2ZNci3f5kcPWx85k131db06uM,321
10
- tigrbl_base/_base/_headers_base.py,sha256=7DMJIn1LsVlZIjuuBHYrtwW1nLAGum_RIIIPnD8Nvl0,294
10
+ tigrbl_base/_base/_headers_base.py,sha256=btikCRtQw7K0btJpHQtHcv6R4gI2F5wi2sO1eLXQA2E,2593
11
11
  tigrbl_base/_base/_hook_base.py,sha256=ycRlUXZkzpmE4-sF9UaTHKJ7KqIECd-69dAiSQc8xio,579
12
12
  tigrbl_base/_base/_mapping_access.py,sha256=uPhHw7pZJecg8rldwtwXdO355ofm0B29MhXWklWoGn0,683
13
13
  tigrbl_base/_base/_middleware_base.py,sha256=O2QKBimBgaBuzlC3cZKif-Aq-0HebNV0-uqispwOSXg,4579
14
14
  tigrbl_base/_base/_op_base.py,sha256=4kejZkpj6tTcIfF2JAFNxh49z3lgIhvxn9NBw-pUyhc,230
15
15
  tigrbl_base/_base/_request_base.py,sha256=9SKnJfQ9XQgUbUHxx4PXdPUh_53gQkGTCvpANzD30rc,1554
16
- tigrbl_base/_base/_response_base.py,sha256=-q__euq7-5SRKhn3VUWQ4BYzVZVwdkRuF6_YU1SGKsY,3094
16
+ tigrbl_base/_base/_response_base.py,sha256=HkYtkN6UMiamJk5t-xghbbdWM_H4MfjTVkK8yDi8PAc,2828
17
17
  tigrbl_base/_base/_rest_map.py,sha256=CG9UGz2F4o6cmgu58gdZiAqdCwxrCnJwdaPE3dy_1D8,3345
18
18
  tigrbl_base/_base/_router_base.py,sha256=PXaVlNc0TWE3gQbGO9CvrOSJIbb3z_VvBsxtp11IOJ8,1556
19
19
  tigrbl_base/_base/_rpc_map.py,sha256=67_TvblvR0IQzONThBj8AeByGPE3M492OnNh-XHW_x8,18468
20
- tigrbl_base/_base/_schema_base.py,sha256=v17Ev7J46exof-8i_1aAUjprwXFb0lnW1X58U-2ADhw,1819
20
+ tigrbl_base/_base/_schema_base.py,sha256=XWTlcm8FuxPVMmDCiy_fX56HI9O_NWBXEYeTpd0lWM0,3060
21
21
  tigrbl_base/_base/_security_base.py,sha256=LTv3TYeW6WX60KixLsEtIPta5ap4lmqDiSfBB1MP2YE,2284
22
22
  tigrbl_base/_base/_session_abc.py,sha256=oHEvhJyhHrcp4TJrU9znUMA9AlYZCm8i8b6nm6xZL2I,1207
23
23
  tigrbl_base/_base/_session_base.py,sha256=KDdz-tK-0aDhYSv-CQDlnRT17gNT9PaephJmkRtMzU8,4343
24
24
  tigrbl_base/_base/_storage.py,sha256=yCpmR_B9_JV7w-DGJIpKb-VDGtA4yf3F_GJyG4V4JxI,508
25
- tigrbl_base/_base/_table_base.py,sha256=9EMvqqs6MvQh_ykjmIdCyauXfpeNQftW1l56G3PjHFE,23284
25
+ tigrbl_base/_base/_table_base.py,sha256=ETFE7AYTcnkBTmlHvHauQlgvGqWnxofIpJmTt2yeXtU,23511
26
26
  tigrbl_base/_base/_table_registry_base.py,sha256=0bkl0g6JFonzaaDiP5dVPxJ8g2LVqq25vyu9JniY1zM,1801
27
27
  tigrbl_base/column/__init__.py,sha256=DfJkmiDkpThisBttbzSACAaKc8XtdHH2HMiDBwsuDlc,3306
28
28
  tigrbl_base/column/infer/__init__.py,sha256=JMfMGGiGtc9jMql6po7mu4r7GVZqnyi_YzNNjLrL53I,367
@@ -31,8 +31,8 @@ tigrbl_base/column/infer/jsonhints.py,sha256=pCTXhFUvPvzd9KLxZob_m2SYNaZI0nVIKc7
31
31
  tigrbl_base/column/infer/planning.py,sha256=PebtJaYEIkBVepazmIajbFkvfAEEwFEt9M1p97vjSs0,4264
32
32
  tigrbl_base/column/infer/types.py,sha256=NGdnddAugowHDl8Lhw0b83wxCCLAROjtPtWiyMagoEs,1720
33
33
  tigrbl_base/column/infer/utils.py,sha256=J8VcLGA-KXjy3IJ2ROQ1alyfYP9qj8ukb5Z3IMvasi4,1532
34
- tigrbl_base-0.4.3.dev4.dist-info/METADATA,sha256=h8WC_GJW3rjXfwarEcx4iGpWIfoEtZ8fKgeUTGTy4d4,27445
35
- tigrbl_base-0.4.3.dev4.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
36
- tigrbl_base-0.4.3.dev4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
37
- tigrbl_base-0.4.3.dev4.dist-info/licenses/NOTICE,sha256=EvJMTshzsWz43LiK-DeN2ZuLtrP49cxvlrFlJ8F_buc,221
38
- tigrbl_base-0.4.3.dev4.dist-info/RECORD,,
34
+ tigrbl_base-0.4.4.dev7.dist-info/METADATA,sha256=npJfC8fHH2702cpxmlU59M4L7Mfw1RU3OJxnXd8kaLQ,27445
35
+ tigrbl_base-0.4.4.dev7.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
36
+ tigrbl_base-0.4.4.dev7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
37
+ tigrbl_base-0.4.4.dev7.dist-info/licenses/NOTICE,sha256=EvJMTshzsWz43LiK-DeN2ZuLtrP49cxvlrFlJ8F_buc,221
38
+ tigrbl_base-0.4.4.dev7.dist-info/RECORD,,