vention-communication 0.2.2__py3-none-any.whl → 0.3.0__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.
- communication/app.py +1 -1
- communication/rpc_registry.py +90 -0
- communication/typing_utils.py +49 -1
- {vention_communication-0.2.2.dist-info → vention_communication-0.3.0.dist-info}/METADATA +1 -1
- {vention_communication-0.2.2.dist-info → vention_communication-0.3.0.dist-info}/RECORD +6 -5
- {vention_communication-0.2.2.dist-info → vention_communication-0.3.0.dist-info}/WHEEL +0 -0
communication/app.py
CHANGED
|
@@ -7,7 +7,7 @@ from .connect_router import ConnectRouter
|
|
|
7
7
|
from .decorators import collect_bundle, set_global_app
|
|
8
8
|
from .codegen import generate_proto, sanitize_service_name
|
|
9
9
|
from .entries import RpcBundle
|
|
10
|
-
from .
|
|
10
|
+
from .rpc_registry import RpcRegistry
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class VentionApp(FastAPI):
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import List, Optional, Set, Type
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from .entries import RpcBundle
|
|
9
|
+
from .typing_utils import extract_nested_models, is_pydantic_model, apply_aliases
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RpcRegistry:
|
|
14
|
+
"""
|
|
15
|
+
Central registry that collects RPC bundles, applies model normalization
|
|
16
|
+
(Pydantic field aliasing), and exposes a unified bundle.
|
|
17
|
+
|
|
18
|
+
- Plugins and decorators add RpcBundle instances.
|
|
19
|
+
- Registry merges them.
|
|
20
|
+
- Registry applies camelCase aliases to all Pydantic models exactly once.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
service_name: str = "VentionApp"
|
|
24
|
+
_bundles: List[RpcBundle] = field(default_factory=list)
|
|
25
|
+
_models_normalized: bool = False
|
|
26
|
+
|
|
27
|
+
# ------------- Bundle registration -------------
|
|
28
|
+
|
|
29
|
+
def add_bundle(self, bundle: RpcBundle) -> None:
|
|
30
|
+
"""Register a bundle for inclusion in the unified RPC view."""
|
|
31
|
+
self._bundles.append(bundle)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def bundle(self) -> RpcBundle:
|
|
35
|
+
"""Return a merged RpcBundle (does not mutate stored bundles)."""
|
|
36
|
+
merged = RpcBundle()
|
|
37
|
+
for bundle in self._bundles:
|
|
38
|
+
merged.extend(bundle)
|
|
39
|
+
return merged
|
|
40
|
+
|
|
41
|
+
# ------------- Model normalization / aliasing -------------
|
|
42
|
+
def _normalize_model(
|
|
43
|
+
self, model: Optional[Type[BaseModel]], seen: Set[Type[BaseModel]]
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Recursively normalize a model and all its nested models."""
|
|
46
|
+
if model is None:
|
|
47
|
+
return
|
|
48
|
+
if not is_pydantic_model(model):
|
|
49
|
+
return
|
|
50
|
+
if model in seen:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
seen.add(model)
|
|
54
|
+
apply_aliases(model)
|
|
55
|
+
|
|
56
|
+
if hasattr(model, "model_fields"):
|
|
57
|
+
for _, field_info in model.model_fields.items():
|
|
58
|
+
nested_models = extract_nested_models(field_info.annotation)
|
|
59
|
+
for nested_model in nested_models:
|
|
60
|
+
self._normalize_model(nested_model, seen)
|
|
61
|
+
|
|
62
|
+
def normalize_models_and_apply_aliases(self) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Walk all RPCs in all bundles and apply camelCase JSON aliases
|
|
65
|
+
to every Pydantic model exactly once, including nested models.
|
|
66
|
+
"""
|
|
67
|
+
if self._models_normalized:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
seen: Set[Type[BaseModel]] = set()
|
|
71
|
+
|
|
72
|
+
for bundle in self._bundles:
|
|
73
|
+
for action in bundle.actions:
|
|
74
|
+
self._normalize_model(action.input_type, seen)
|
|
75
|
+
self._normalize_model(action.output_type, seen)
|
|
76
|
+
for stream in bundle.streams:
|
|
77
|
+
self._normalize_model(stream.payload_type, seen)
|
|
78
|
+
|
|
79
|
+
self._models_normalized = True
|
|
80
|
+
|
|
81
|
+
# ------------- Unified, normalized view -------------
|
|
82
|
+
|
|
83
|
+
def get_unified_bundle(self) -> RpcBundle:
|
|
84
|
+
"""
|
|
85
|
+
Get the fully merged, normalized RPC bundle.
|
|
86
|
+
|
|
87
|
+
This will apply aliasing exactly once and then return a merged RpcBundle.
|
|
88
|
+
"""
|
|
89
|
+
self.normalize_models_and_apply_aliases()
|
|
90
|
+
return self.bundle
|
communication/typing_utils.py
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import inspect
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
List,
|
|
7
|
+
Optional,
|
|
8
|
+
Type,
|
|
9
|
+
Union,
|
|
10
|
+
get_args,
|
|
11
|
+
get_origin,
|
|
12
|
+
get_type_hints,
|
|
13
|
+
cast,
|
|
14
|
+
)
|
|
4
15
|
|
|
5
16
|
from pydantic import BaseModel, ConfigDict
|
|
6
17
|
|
|
@@ -122,3 +133,40 @@ def apply_aliases(model_cls: Type[BaseModel]) -> None:
|
|
|
122
133
|
|
|
123
134
|
# Force rebuild
|
|
124
135
|
model_cls.model_rebuild(force=True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def unwrap_optional(field_type: Any) -> Any:
|
|
139
|
+
"""Unwrap Optional[Type] or Union[Type, None] to get the non-None type."""
|
|
140
|
+
origin = get_origin(field_type)
|
|
141
|
+
if origin is not Union:
|
|
142
|
+
return field_type
|
|
143
|
+
|
|
144
|
+
args = get_args(field_type)
|
|
145
|
+
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
146
|
+
if len(non_none_args) == 1:
|
|
147
|
+
return non_none_args[0]
|
|
148
|
+
return field_type
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def unwrap_list(field_type: Any) -> Any:
|
|
152
|
+
"""Unwrap List[Type] to get the inner type, handling Optional inside List."""
|
|
153
|
+
origin = get_origin(field_type)
|
|
154
|
+
if origin not in (list, List):
|
|
155
|
+
return field_type
|
|
156
|
+
|
|
157
|
+
args = get_args(field_type)
|
|
158
|
+
if not args:
|
|
159
|
+
return field_type
|
|
160
|
+
|
|
161
|
+
inner_type = args[0]
|
|
162
|
+
return unwrap_optional(inner_type)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def extract_nested_models(field_type: Any) -> List[Type[BaseModel]]:
|
|
166
|
+
"""Extract Pydantic models from a field type, handling Optional, List, etc."""
|
|
167
|
+
field_type = unwrap_optional(field_type)
|
|
168
|
+
field_type = unwrap_list(field_type)
|
|
169
|
+
|
|
170
|
+
if is_pydantic_model(field_type):
|
|
171
|
+
return [field_type]
|
|
172
|
+
return []
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
communication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
communication/app.py,sha256=
|
|
2
|
+
communication/app.py,sha256=dxbm7oG8-uQSu_dY4S8exHsBXND1N1nC2GdsMthqZE0,3423
|
|
3
3
|
communication/codegen.py,sha256=_1LYma-MXedwRHn_P3CqPs1xIMEZrZgiUAsGSj9yff4,7089
|
|
4
4
|
communication/connect_router.py,sha256=tjNl9dRukjLecZH1y-GYIQXH-0xzz9JESbikSH5XhFo,10527
|
|
5
5
|
communication/decorators.py,sha256=3pVlXUSX4KXSKlweskF0RfD8pST2zisTuFGJQHHIvcI,3419
|
|
6
6
|
communication/entries.py,sha256=vdZc8GAQztRWEiav6R2wM4l35GE-EiEdRH0ZJR4GShM,1065
|
|
7
7
|
communication/errors.py,sha256=hdJBB9jPJNWx8hbxIxwLBNKt2JVpmhZ1YF8q9VKk-dI,1773
|
|
8
8
|
communication/registry.py,sha256=acbhwU0z1iRHqzOahXG47GfJS5VQHjf69UiruoXrr7g,4517
|
|
9
|
-
communication/
|
|
10
|
-
|
|
11
|
-
vention_communication-0.
|
|
12
|
-
vention_communication-0.
|
|
9
|
+
communication/rpc_registry.py,sha256=90r4YYHKveVGBO-pOD91qAs9GSB3651WPclC6oRnrZo,2986
|
|
10
|
+
communication/typing_utils.py,sha256=6S6LvtjFBZKo-gWPc8fbh4F1rlPWFgrH_mX8esZbzWM,4559
|
|
11
|
+
vention_communication-0.3.0.dist-info/METADATA,sha256=e6h67l5DGs9LSAL_eBan_spH60FDQfBVcmeyoNWrlvg,11255
|
|
12
|
+
vention_communication-0.3.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
13
|
+
vention_communication-0.3.0.dist-info/RECORD,,
|
|
File without changes
|