vention-communication 0.2.2__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vention-communication
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: A framework for communication between machine apps and other services.
5
5
  License: Proprietary
6
6
  Author: VentionCo
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "vention-communication"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "A framework for communication between machine apps and other services."
5
5
  authors = [ "VentionCo" ]
6
6
  readme = "README.md"
@@ -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 .registry import RpcRegistry
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
@@ -1,6 +1,17 @@
1
1
  from __future__ import annotations
2
2
  import inspect
3
- from typing import Any, Callable, Optional, Type, get_type_hints, cast
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 []