vention-communication 0.3.0__py3-none-any.whl → 0.3.6__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/codegen.py +68 -36
- communication/connect_router.py +4 -8
- communication/decorators.py +2 -6
- communication/errors.py +1 -3
- communication/rpc_registry.py +1 -3
- communication/typing_utils.py +2 -6
- {vention_communication-0.3.0.dist-info → vention_communication-0.3.6.dist-info}/METADATA +79 -13
- vention_communication-0.3.6.dist-info/RECORD +13 -0
- vention_communication-0.3.0.dist-info/RECORD +0 -13
- {vention_communication-0.3.0.dist-info → vention_communication-0.3.6.dist-info}/WHEEL +0 -0
communication/codegen.py
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any, Dict, List, Optional, Type, Union, get_args, get_origin, Annotated
|
|
3
4
|
|
|
4
5
|
from .decorators import collect_bundle
|
|
5
6
|
from .typing_utils import is_pydantic_model
|
|
6
7
|
from .entries import RpcBundle, StreamEntry
|
|
7
8
|
|
|
9
|
+
|
|
10
|
+
# Handle Python 3.10+ types.UnionType
|
|
11
|
+
if sys.version_info >= (3, 10):
|
|
12
|
+
from types import UnionType
|
|
13
|
+
|
|
14
|
+
_UNION_TYPES = (Union, UnionType)
|
|
15
|
+
else:
|
|
16
|
+
_UNION_TYPES = (Union,)
|
|
17
|
+
|
|
8
18
|
_SCALAR_MAP = {
|
|
9
19
|
int: "int32",
|
|
10
20
|
float: "double",
|
|
11
21
|
str: "string",
|
|
12
22
|
bool: "bool",
|
|
23
|
+
bytes: "bytes",
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
MAX_NESTING_DEPTH = 10
|
|
27
|
+
|
|
15
28
|
HEADER = """syntax = "proto3";
|
|
16
29
|
package vention.app.v1;
|
|
17
30
|
|
|
@@ -20,22 +33,45 @@ import "google/protobuf/empty.proto";
|
|
|
20
33
|
"""
|
|
21
34
|
|
|
22
35
|
|
|
36
|
+
def _unwrap_annotated(type_annotation: Any) -> Any:
|
|
37
|
+
"""Unwrap Annotated[T, ...] to get the base type T."""
|
|
38
|
+
origin = get_origin(type_annotation)
|
|
39
|
+
if origin is Annotated:
|
|
40
|
+
args = get_args(type_annotation)
|
|
41
|
+
if args:
|
|
42
|
+
return args[0]
|
|
43
|
+
return type_annotation
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _filter_non_none_types(args: tuple[Any, ...]) -> list[Any]:
|
|
47
|
+
"""Filter out NoneType from union type arguments."""
|
|
48
|
+
return [arg for arg in args if arg is not type(None)]
|
|
49
|
+
|
|
50
|
+
|
|
23
51
|
def _unwrap_optional(type_annotation: Any) -> tuple[Any, bool]:
|
|
52
|
+
type_annotation = _unwrap_annotated(type_annotation)
|
|
53
|
+
|
|
24
54
|
origin = get_origin(type_annotation)
|
|
25
|
-
|
|
55
|
+
# Handle both typing.Union and types.UnionType (Python 3.10+)
|
|
56
|
+
if origin in _UNION_TYPES:
|
|
26
57
|
args = get_args(type_annotation)
|
|
27
|
-
non_none_args =
|
|
58
|
+
non_none_args = _filter_non_none_types(args)
|
|
28
59
|
if len(non_none_args) == 1:
|
|
29
|
-
|
|
60
|
+
# Recursively unwrap in case the inner type is also Annotated
|
|
61
|
+
return (_unwrap_annotated(non_none_args[0]), True)
|
|
30
62
|
return (type_annotation, False)
|
|
31
63
|
|
|
32
64
|
|
|
33
65
|
def _unwrap_list(type_annotation: Any) -> tuple[Any, bool]:
|
|
66
|
+
# First unwrap Annotated if present
|
|
67
|
+
type_annotation = _unwrap_annotated(type_annotation)
|
|
68
|
+
|
|
34
69
|
origin = get_origin(type_annotation)
|
|
35
70
|
if origin in (list, List):
|
|
36
71
|
args = get_args(type_annotation)
|
|
37
72
|
if args:
|
|
38
|
-
|
|
73
|
+
# Unwrap Annotated from the inner type as well
|
|
74
|
+
return (_unwrap_annotated(args[0]), True)
|
|
39
75
|
return (type_annotation, False)
|
|
40
76
|
|
|
41
77
|
|
|
@@ -47,16 +83,20 @@ def _determine_proto_type_for_field(
|
|
|
47
83
|
inner_type: Type[Any],
|
|
48
84
|
seen_models: set[str],
|
|
49
85
|
lines: list[str],
|
|
86
|
+
depth: int = 0,
|
|
50
87
|
) -> str:
|
|
88
|
+
if depth > MAX_NESTING_DEPTH:
|
|
89
|
+
raise ValueError(f"Maximum nesting depth ({MAX_NESTING_DEPTH}) exceeded in proto generation")
|
|
90
|
+
|
|
51
91
|
if inner_type in _SCALAR_MAP:
|
|
52
92
|
return _SCALAR_MAP[inner_type]
|
|
53
93
|
|
|
54
94
|
if is_pydantic_model(inner_type):
|
|
55
95
|
model_name = inner_type.__name__
|
|
56
|
-
|
|
96
|
+
|
|
57
97
|
if model_name not in seen_models:
|
|
58
98
|
seen_models.add(model_name)
|
|
59
|
-
lines.extend(_generate_pydantic_message(inner_type, seen_models, lines))
|
|
99
|
+
lines.extend(_generate_pydantic_message(inner_type, seen_models, lines, depth + 1))
|
|
60
100
|
return model_name
|
|
61
101
|
|
|
62
102
|
# Fallback to string for unknown types
|
|
@@ -69,14 +109,18 @@ def _process_pydantic_field(
|
|
|
69
109
|
field_index: int,
|
|
70
110
|
seen_models: set[str],
|
|
71
111
|
lines: list[str],
|
|
112
|
+
depth: int = 0,
|
|
72
113
|
) -> str:
|
|
73
|
-
|
|
74
|
-
|
|
114
|
+
unwrapped_type = _unwrap_annotated(field_type)
|
|
115
|
+
unwrapped_type, _ = _unwrap_optional(unwrapped_type)
|
|
116
|
+
list_inner_type, is_list = _unwrap_list(unwrapped_type)
|
|
117
|
+
# One more unwrap in case list contains Optional[Annotated[...]]
|
|
118
|
+
final_inner_type = _unwrap_annotated(list_inner_type)
|
|
75
119
|
|
|
76
|
-
proto_type = _determine_proto_type_for_field(
|
|
120
|
+
proto_type = _determine_proto_type_for_field(final_inner_type, seen_models, lines, depth)
|
|
77
121
|
|
|
78
122
|
if is_list:
|
|
79
|
-
|
|
123
|
+
return f" repeated {proto_type} {field_name} = {field_index};"
|
|
80
124
|
|
|
81
125
|
return f" {proto_type} {field_name} = {field_index};"
|
|
82
126
|
|
|
@@ -85,6 +129,7 @@ def _generate_pydantic_message(
|
|
|
85
129
|
type_annotation: Type[Any],
|
|
86
130
|
seen_models: set[str],
|
|
87
131
|
lines: list[str],
|
|
132
|
+
depth: int = 0,
|
|
88
133
|
) -> list[str]:
|
|
89
134
|
model_name = type_annotation.__name__
|
|
90
135
|
fields = []
|
|
@@ -97,6 +142,7 @@ def _generate_pydantic_message(
|
|
|
97
142
|
field_index,
|
|
98
143
|
seen_models,
|
|
99
144
|
lines,
|
|
145
|
+
depth,
|
|
100
146
|
)
|
|
101
147
|
fields.append(field_line)
|
|
102
148
|
field_index += 1
|
|
@@ -107,9 +153,7 @@ def _generate_pydantic_message(
|
|
|
107
153
|
return lines_result
|
|
108
154
|
|
|
109
155
|
|
|
110
|
-
def _generate_scalar_wrapper_message(
|
|
111
|
-
stream_name: str, payload_type: Type[Any]
|
|
112
|
-
) -> list[str]:
|
|
156
|
+
def _generate_scalar_wrapper_message(stream_name: str, payload_type: Type[Any]) -> list[str]:
|
|
113
157
|
wrapper_name = _msg_name_for_scalar_stream(stream_name)
|
|
114
158
|
lines = [
|
|
115
159
|
f"message {wrapper_name} {{",
|
|
@@ -150,17 +194,15 @@ def _register_pydantic_model(
|
|
|
150
194
|
if type_annotation is None:
|
|
151
195
|
return
|
|
152
196
|
|
|
153
|
-
|
|
197
|
+
unwrapped_type, _ = _unwrap_optional(type_annotation)
|
|
154
198
|
|
|
155
|
-
list_inner_type, _ = _unwrap_list(
|
|
199
|
+
list_inner_type, _ = _unwrap_list(unwrapped_type)
|
|
156
200
|
|
|
157
201
|
if is_pydantic_model(list_inner_type):
|
|
158
202
|
model_name = list_inner_type.__name__
|
|
159
203
|
if model_name not in seen_models:
|
|
160
204
|
seen_models.add(model_name)
|
|
161
|
-
lines.extend(
|
|
162
|
-
_generate_pydantic_message(list_inner_type, seen_models, lines)
|
|
163
|
-
)
|
|
205
|
+
lines.extend(_generate_pydantic_message(list_inner_type, seen_models, lines))
|
|
164
206
|
|
|
165
207
|
|
|
166
208
|
def _process_stream_payload(
|
|
@@ -169,17 +211,15 @@ def _process_stream_payload(
|
|
|
169
211
|
lines: list[str],
|
|
170
212
|
scalar_wrappers: Dict[str, str],
|
|
171
213
|
) -> None:
|
|
172
|
-
|
|
173
|
-
list_inner_type, _ = _unwrap_list(
|
|
214
|
+
unwrapped_type, _ = _unwrap_optional(stream_entry.payload_type)
|
|
215
|
+
list_inner_type, _ = _unwrap_list(unwrapped_type)
|
|
174
216
|
|
|
175
217
|
if is_pydantic_model(list_inner_type):
|
|
176
218
|
_register_pydantic_model(list_inner_type, seen_models, lines)
|
|
177
219
|
elif list_inner_type in _SCALAR_MAP:
|
|
178
220
|
wrapper_name = _msg_name_for_scalar_stream(stream_entry.name)
|
|
179
221
|
scalar_wrappers[stream_entry.name] = wrapper_name
|
|
180
|
-
lines.extend(
|
|
181
|
-
_generate_scalar_wrapper_message(stream_entry.name, list_inner_type)
|
|
182
|
-
)
|
|
222
|
+
lines.extend(_generate_scalar_wrapper_message(stream_entry.name, list_inner_type))
|
|
183
223
|
|
|
184
224
|
|
|
185
225
|
def _collect_message_types(
|
|
@@ -196,25 +236,17 @@ def _collect_message_types(
|
|
|
196
236
|
_process_stream_payload(stream_entry, seen_models, lines, scalar_wrappers)
|
|
197
237
|
|
|
198
238
|
|
|
199
|
-
def _generate_service_rpcs(
|
|
200
|
-
bundle: RpcBundle, lines: list[str], scalar_wrappers: Dict[str, str]
|
|
201
|
-
) -> None:
|
|
239
|
+
def _generate_service_rpcs(bundle: RpcBundle, lines: list[str], scalar_wrappers: Dict[str, str]) -> None:
|
|
202
240
|
rpc_prefix = " rpc"
|
|
203
241
|
|
|
204
242
|
for action_entry in bundle.actions:
|
|
205
243
|
input_type = _proto_type_name(action_entry.input_type, scalar_wrappers)
|
|
206
244
|
output_type = _proto_type_name(action_entry.output_type, scalar_wrappers)
|
|
207
|
-
lines.append(
|
|
208
|
-
f"{rpc_prefix} {action_entry.name} ({input_type}) returns ({output_type});"
|
|
209
|
-
)
|
|
245
|
+
lines.append(f"{rpc_prefix} {action_entry.name} ({input_type}) returns ({output_type});")
|
|
210
246
|
|
|
211
247
|
for stream_entry in bundle.streams:
|
|
212
|
-
output_type = _proto_type_name(
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
lines.append(
|
|
216
|
-
f"{rpc_prefix} {stream_entry.name} (google.protobuf.Empty) returns (stream {output_type});"
|
|
217
|
-
)
|
|
248
|
+
output_type = _proto_type_name(stream_entry.payload_type, scalar_wrappers, stream_entry.name)
|
|
249
|
+
lines.append(f"{rpc_prefix} {stream_entry.name} (google.protobuf.Empty) returns (stream {output_type});")
|
|
218
250
|
|
|
219
251
|
|
|
220
252
|
def generate_proto(app_name: str, *, bundle: Optional[RpcBundle] = None) -> str:
|
communication/connect_router.py
CHANGED
|
@@ -18,9 +18,7 @@ CONTENT_TYPE = "application/connect+json"
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _frame(payload: Dict[str, Any], *, trailer: bool = False) -> bytes:
|
|
21
|
-
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode(
|
|
22
|
-
"utf-8"
|
|
23
|
-
)
|
|
21
|
+
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
24
22
|
flag = 0x80 if trailer else 0x00
|
|
25
23
|
header = bytes([flag]) + len(body).to_bytes(4, byteorder="big", signed=False)
|
|
26
24
|
return header + body
|
|
@@ -121,9 +119,7 @@ class StreamManager:
|
|
|
121
119
|
"""
|
|
122
120
|
topic = self._topics[stream_name]
|
|
123
121
|
entry: StreamEntry = topic["entry"]
|
|
124
|
-
subscriber_queue: asyncio.Queue[Any] = asyncio.Queue(
|
|
125
|
-
maxsize=entry.queue_maxsize
|
|
126
|
-
)
|
|
122
|
+
subscriber_queue: asyncio.Queue[Any] = asyncio.Queue(maxsize=entry.queue_maxsize)
|
|
127
123
|
subscriber = _Subscriber(queue=subscriber_queue)
|
|
128
124
|
topic["subscribers"].add(subscriber)
|
|
129
125
|
self.start_distributor_if_needed(stream_name)
|
|
@@ -217,7 +213,7 @@ class ConnectRouter:
|
|
|
217
213
|
result = await _maybe_await(entry.func(validated_arg))
|
|
218
214
|
|
|
219
215
|
if hasattr(result, "model_dump"):
|
|
220
|
-
result = result.model_dump(by_alias=True)
|
|
216
|
+
result = result.model_dump(mode="json", by_alias=True)
|
|
221
217
|
return JSONResponse(result or {})
|
|
222
218
|
except Exception as exc:
|
|
223
219
|
return JSONResponse(error_envelope(exc))
|
|
@@ -283,7 +279,7 @@ class ConnectRouter:
|
|
|
283
279
|
|
|
284
280
|
def _serialize_stream_item(item: Any) -> Dict[str, Any]:
|
|
285
281
|
if hasattr(item, "model_dump"):
|
|
286
|
-
dumped = item.model_dump(by_alias=True)
|
|
282
|
+
dumped = item.model_dump(mode="json", by_alias=True)
|
|
287
283
|
if isinstance(dumped, dict):
|
|
288
284
|
return dumped
|
|
289
285
|
return {"value": dumped}
|
communication/decorators.py
CHANGED
|
@@ -38,9 +38,7 @@ def action(
|
|
|
38
38
|
def decorator(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
39
39
|
input_type = infer_input_type(function)
|
|
40
40
|
output_type = infer_output_type(function)
|
|
41
|
-
entry = ActionEntry(
|
|
42
|
-
name or function.__name__, function, input_type, output_type
|
|
43
|
-
)
|
|
41
|
+
entry = ActionEntry(name or function.__name__, function, input_type, output_type)
|
|
44
42
|
_actions.append(entry)
|
|
45
43
|
return function
|
|
46
44
|
|
|
@@ -71,9 +69,7 @@ def stream(
|
|
|
71
69
|
Decorator function that registers the stream
|
|
72
70
|
"""
|
|
73
71
|
if not (is_pydantic_model(payload) or payload in (int, float, str, bool, dict)):
|
|
74
|
-
raise ValueError(
|
|
75
|
-
"payload must be a pydantic BaseModel or a JSON-serializable scalar/dict"
|
|
76
|
-
)
|
|
72
|
+
raise ValueError("payload must be a pydantic BaseModel or a JSON-serializable scalar/dict")
|
|
77
73
|
|
|
78
74
|
def decorator(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
79
75
|
entry = StreamEntry(
|
communication/errors.py
CHANGED
|
@@ -5,9 +5,7 @@ from typing import Any, Dict, List, Optional
|
|
|
5
5
|
class ConnectError(Exception):
|
|
6
6
|
"""Application-level error to send over Connect transport."""
|
|
7
7
|
|
|
8
|
-
def __init__(
|
|
9
|
-
self, code: str, message: str, *, details: Optional[List[Any]] = None
|
|
10
|
-
) -> None:
|
|
8
|
+
def __init__(self, code: str, message: str, *, details: Optional[List[Any]] = None) -> None:
|
|
11
9
|
super().__init__(message)
|
|
12
10
|
self.code = code
|
|
13
11
|
self.message = message
|
communication/rpc_registry.py
CHANGED
|
@@ -39,9 +39,7 @@ class RpcRegistry:
|
|
|
39
39
|
return merged
|
|
40
40
|
|
|
41
41
|
# ------------- Model normalization / aliasing -------------
|
|
42
|
-
def _normalize_model(
|
|
43
|
-
self, model: Optional[Type[BaseModel]], seen: Set[Type[BaseModel]]
|
|
44
|
-
) -> None:
|
|
42
|
+
def _normalize_model(self, model: Optional[Type[BaseModel]], seen: Set[Type[BaseModel]]) -> None:
|
|
45
43
|
"""Recursively normalize a model and all its nested models."""
|
|
46
44
|
if model is None:
|
|
47
45
|
return
|
communication/typing_utils.py
CHANGED
|
@@ -94,9 +94,7 @@ def is_pydantic_model(type_annotation: Any) -> bool:
|
|
|
94
94
|
True if the type is a Pydantic BaseModel subclass, False otherwise
|
|
95
95
|
"""
|
|
96
96
|
try:
|
|
97
|
-
return isinstance(type_annotation, type) and issubclass(
|
|
98
|
-
type_annotation, BaseModel
|
|
99
|
-
)
|
|
97
|
+
return isinstance(type_annotation, type) and issubclass(type_annotation, BaseModel)
|
|
100
98
|
except Exception:
|
|
101
99
|
return False
|
|
102
100
|
|
|
@@ -120,9 +118,7 @@ def apply_aliases(model_cls: Type[BaseModel]) -> None:
|
|
|
120
118
|
if existing_config is None:
|
|
121
119
|
existing_dict: dict[str, Any] = {}
|
|
122
120
|
else:
|
|
123
|
-
existing_dict = (
|
|
124
|
-
dict(existing_config) if isinstance(existing_config, dict) else {}
|
|
125
|
-
)
|
|
121
|
+
existing_dict = dict(existing_config) if isinstance(existing_config, dict) else {}
|
|
126
122
|
|
|
127
123
|
merged_config: dict[str, Any] = {
|
|
128
124
|
"populate_by_name": True,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vention-communication
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: A framework for communication between machine apps and other services.
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Author: VentionCo
|
|
@@ -8,8 +8,22 @@ Requires-Python: >=3.10,<3.11
|
|
|
8
8
|
Classifier: License :: Other/Proprietary License
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
-
Requires-Dist:
|
|
12
|
-
Requires-Dist:
|
|
11
|
+
Requires-Dist: annotated-doc (==0.0.4) ; python_version == "3.10"
|
|
12
|
+
Requires-Dist: annotated-types (==0.7.0) ; python_version == "3.10"
|
|
13
|
+
Requires-Dist: anyio (==4.11.0) ; python_version == "3.10"
|
|
14
|
+
Requires-Dist: click (==8.1.8) ; python_version == "3.10"
|
|
15
|
+
Requires-Dist: colorama (==0.4.6) ; python_version == "3.10" and platform_system == "Windows"
|
|
16
|
+
Requires-Dist: exceptiongroup (==1.3.0) ; python_version == "3.10"
|
|
17
|
+
Requires-Dist: fastapi (==0.121.1) ; python_version == "3.10"
|
|
18
|
+
Requires-Dist: h11 (==0.16.0) ; python_version == "3.10"
|
|
19
|
+
Requires-Dist: idna (==3.11) ; python_version == "3.10"
|
|
20
|
+
Requires-Dist: pydantic (==2.12.3) ; python_version == "3.10"
|
|
21
|
+
Requires-Dist: pydantic-core (==2.41.4) ; python_version == "3.10"
|
|
22
|
+
Requires-Dist: sniffio (==1.3.1) ; python_version == "3.10"
|
|
23
|
+
Requires-Dist: starlette (==0.48.0) ; python_version == "3.10"
|
|
24
|
+
Requires-Dist: typing-extensions (==4.15.0) ; python_version == "3.10"
|
|
25
|
+
Requires-Dist: typing-inspection (==0.4.2) ; python_version == "3.10"
|
|
26
|
+
Requires-Dist: uvicorn (==0.35.0) ; python_version == "3.10"
|
|
13
27
|
Description-Content-Type: text/markdown
|
|
14
28
|
|
|
15
29
|
# Vention Communication
|
|
@@ -88,8 +102,10 @@ A complete "hello world" in three steps.
|
|
|
88
102
|
|
|
89
103
|
```python
|
|
90
104
|
from pydantic import BaseModel
|
|
91
|
-
from
|
|
92
|
-
import
|
|
105
|
+
from communication.app import VentionApp
|
|
106
|
+
from communication.decorators import action, stream
|
|
107
|
+
import time
|
|
108
|
+
import random
|
|
93
109
|
|
|
94
110
|
class PingRequest(BaseModel):
|
|
95
111
|
message: str
|
|
@@ -114,6 +130,15 @@ async def heartbeat():
|
|
|
114
130
|
|
|
115
131
|
app.finalize()
|
|
116
132
|
|
|
133
|
+
# Emit heartbeat every second
|
|
134
|
+
@app.on_event("startup")
|
|
135
|
+
async def startup():
|
|
136
|
+
asyncio.create_task(loop())
|
|
137
|
+
|
|
138
|
+
async def loop():
|
|
139
|
+
while True:
|
|
140
|
+
asyncio.create_task(heartbeat())
|
|
141
|
+
await asyncio.sleep(1)
|
|
117
142
|
```
|
|
118
143
|
|
|
119
144
|
**Run:**
|
|
@@ -128,21 +153,59 @@ Endpoints are automatically registered under `/rpc/vention.app.v1.DemoAppService
|
|
|
128
153
|
|
|
129
154
|
After startup, `proto/app.proto` is emitted automatically.
|
|
130
155
|
|
|
131
|
-
You can now use Buf or protoc to generate client SDKs
|
|
156
|
+
You can now use Buf or protoc to generate client SDKs, based on each client stack you desire.
|
|
157
|
+
|
|
158
|
+
The next section will provide an example for Typescript applications, but for any other environment, please refer to the official documentation on how to install and quickstart code generation:
|
|
159
|
+
|
|
160
|
+
https://buf.build/docs/cli/installation/
|
|
161
|
+
|
|
162
|
+
### 3. Example TypeScript Client
|
|
163
|
+
|
|
164
|
+
Make sure you have Node 24+ installed. Use [NVM](https://github.com/nvm-sh/nvm) to easily install and manage different Node versions.
|
|
165
|
+
|
|
166
|
+
1. Create a folder called `client` and `cd` into it.
|
|
167
|
+
|
|
168
|
+
#### Protobuf Javascript/Typescript libraries
|
|
169
|
+
|
|
170
|
+
2. Install the runtime library, code generator, and the Buf CLI:
|
|
132
171
|
|
|
133
172
|
```bash
|
|
134
|
-
|
|
135
|
-
|
|
173
|
+
npm install @bufbuild/protobuf
|
|
174
|
+
npm install --save-dev @bufbuild/protoc-gen-es @bufbuild/buf
|
|
136
175
|
```
|
|
137
176
|
|
|
138
|
-
|
|
177
|
+
3. Create a buf.gen.yaml file that looks like this:
|
|
139
178
|
|
|
140
|
-
|
|
179
|
+
```yaml
|
|
180
|
+
version: v2
|
|
181
|
+
inputs:
|
|
182
|
+
- directory: proto
|
|
183
|
+
plugins:
|
|
184
|
+
- local: protoc-gen-es
|
|
185
|
+
opt: target=ts
|
|
186
|
+
out: src/gen
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
4. Generate the client code, pointing the path to the newly generated *proto* folder:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npx buf generate ../proto
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Client Application
|
|
196
|
+
|
|
197
|
+
1. Install the client RPC libraries:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npm i @connectrpc/connect @connectrpc/connect-web
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
2. Then create an `index.ts` file in the `client/src` folder, and paste the following code in it:
|
|
141
204
|
|
|
142
205
|
```typescript
|
|
143
206
|
import { createClient } from "@connectrpc/connect";
|
|
144
207
|
import { createConnectTransport } from "@connectrpc/connect-web";
|
|
145
|
-
import { DemoAppService } from "./gen/connect/
|
|
208
|
+
import { DemoAppService } from "./gen/connect/app_pb.ts";
|
|
146
209
|
|
|
147
210
|
const transport = createConnectTransport({
|
|
148
211
|
baseUrl: "http://localhost:8000/rpc",
|
|
@@ -164,8 +227,11 @@ for await (const hb of client.heartbeat({})) {
|
|
|
164
227
|
### Add a new request-response endpoint
|
|
165
228
|
|
|
166
229
|
```python
|
|
230
|
+
class StatusResponse(BaseModel):
|
|
231
|
+
ok: bool
|
|
232
|
+
|
|
167
233
|
@action()
|
|
168
|
-
async def get_status() ->
|
|
234
|
+
async def get_status() -> StatusResponse:
|
|
169
235
|
return {"ok": True}
|
|
170
236
|
```
|
|
171
237
|
|
|
@@ -173,7 +239,7 @@ async def get_status() -> dict:
|
|
|
173
239
|
|
|
174
240
|
```python
|
|
175
241
|
@stream(name="Status", payload=dict)
|
|
176
|
-
async def publish_status() ->
|
|
242
|
+
async def publish_status() -> StatusResponse:
|
|
177
243
|
return {"ok": True}
|
|
178
244
|
```
|
|
179
245
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
communication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
communication/app.py,sha256=dxbm7oG8-uQSu_dY4S8exHsBXND1N1nC2GdsMthqZE0,3423
|
|
3
|
+
communication/codegen.py,sha256=uY-IJ1QggjNPxiGCGfBPhTAL8p_nzYhxSDshkDqm_EQ,8464
|
|
4
|
+
communication/connect_router.py,sha256=fWn2KA8FAKYx1J_6RgtQSqS5hzAdPGEBn4Uk3UlqfqM,10517
|
|
5
|
+
communication/decorators.py,sha256=a16UUi-J2zi9fB3CxMtOygL74VOlCZTrL3hYkyBHvGo,3375
|
|
6
|
+
communication/entries.py,sha256=vdZc8GAQztRWEiav6R2wM4l35GE-EiEdRH0ZJR4GShM,1065
|
|
7
|
+
communication/errors.py,sha256=pwFcb-uce-aPkSXIrM7Ov7XQQXJrNQIgKgRU9QEWrqY,1759
|
|
8
|
+
communication/registry.py,sha256=acbhwU0z1iRHqzOahXG47GfJS5VQHjf69UiruoXrr7g,4517
|
|
9
|
+
communication/rpc_registry.py,sha256=WLu5kRZdZsMr-TCGEXzsAIMQJFk4rPctB4mL53NU70U,2972
|
|
10
|
+
communication/typing_utils.py,sha256=I3FdTTlvcPfiyf7t4nw4--eXf82opwjdGXfVYtprqoU,4513
|
|
11
|
+
vention_communication-0.3.6.dist-info/METADATA,sha256=HLtkhAUyLUE_yj-R-kX91TwgmptBmS0-bTprF8q9WzE,13546
|
|
12
|
+
vention_communication-0.3.6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
13
|
+
vention_communication-0.3.6.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
communication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
communication/app.py,sha256=dxbm7oG8-uQSu_dY4S8exHsBXND1N1nC2GdsMthqZE0,3423
|
|
3
|
-
communication/codegen.py,sha256=_1LYma-MXedwRHn_P3CqPs1xIMEZrZgiUAsGSj9yff4,7089
|
|
4
|
-
communication/connect_router.py,sha256=tjNl9dRukjLecZH1y-GYIQXH-0xzz9JESbikSH5XhFo,10527
|
|
5
|
-
communication/decorators.py,sha256=3pVlXUSX4KXSKlweskF0RfD8pST2zisTuFGJQHHIvcI,3419
|
|
6
|
-
communication/entries.py,sha256=vdZc8GAQztRWEiav6R2wM4l35GE-EiEdRH0ZJR4GShM,1065
|
|
7
|
-
communication/errors.py,sha256=hdJBB9jPJNWx8hbxIxwLBNKt2JVpmhZ1YF8q9VKk-dI,1773
|
|
8
|
-
communication/registry.py,sha256=acbhwU0z1iRHqzOahXG47GfJS5VQHjf69UiruoXrr7g,4517
|
|
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
|