vention-communication 0.3.0__tar.gz → 0.3.6__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,3 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: vention-communication
3
+ Version: 0.3.6
4
+ Summary: A framework for communication between machine apps and other services.
5
+ License: Proprietary
6
+ Author: VentionCo
7
+ Requires-Python: >=3.10,<3.11
8
+ Classifier: License :: Other/Proprietary License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
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"
27
+ Description-Content-Type: text/markdown
28
+
1
29
  # Vention Communication
2
30
 
3
31
  A thin, FastAPI-powered RPC layer for machine-apps that exposes Connect-compatible request-response and server-streaming endpoints — plus .proto generation from Python decorators, allowing typed SDKs to be generated separately via Buf.
@@ -74,8 +102,10 @@ A complete "hello world" in three steps.
74
102
 
75
103
  ```python
76
104
  from pydantic import BaseModel
77
- from vention_communication import VentionApp, action, stream
78
- import time, random
105
+ from communication.app import VentionApp
106
+ from communication.decorators import action, stream
107
+ import time
108
+ import random
79
109
 
80
110
  class PingRequest(BaseModel):
81
111
  message: str
@@ -100,6 +130,15 @@ async def heartbeat():
100
130
 
101
131
  app.finalize()
102
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)
103
142
  ```
104
143
 
105
144
  **Run:**
@@ -114,21 +153,59 @@ Endpoints are automatically registered under `/rpc/vention.app.v1.DemoAppService
114
153
 
115
154
  After startup, `proto/app.proto` is emitted automatically.
116
155
 
117
- 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:
118
171
 
119
172
  ```bash
120
- buf generate --template buf.gen.ts.yaml
121
- buf generate --template buf.gen.python.yaml
173
+ npm install @bufbuild/protobuf
174
+ npm install --save-dev @bufbuild/protoc-gen-es @bufbuild/buf
122
175
  ```
123
176
 
124
- SDK generation is external to vention-communication allowing you to control versions and plugins.
177
+ 3. Create a buf.gen.yaml file that looks like this:
125
178
 
126
- ### 3. Example TypeScript Client
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:
127
204
 
128
205
  ```typescript
129
206
  import { createClient } from "@connectrpc/connect";
130
207
  import { createConnectTransport } from "@connectrpc/connect-web";
131
- import { DemoAppService } from "./gen/connect/proto/app_connect";
208
+ import { DemoAppService } from "./gen/connect/app_pb.ts";
132
209
 
133
210
  const transport = createConnectTransport({
134
211
  baseUrl: "http://localhost:8000/rpc",
@@ -150,8 +227,11 @@ for await (const hb of client.heartbeat({})) {
150
227
  ### Add a new request-response endpoint
151
228
 
152
229
  ```python
230
+ class StatusResponse(BaseModel):
231
+ ok: bool
232
+
153
233
  @action()
154
- async def get_status() -> dict:
234
+ async def get_status() -> StatusResponse:
155
235
  return {"ok": True}
156
236
  ```
157
237
 
@@ -159,7 +239,7 @@ async def get_status() -> dict:
159
239
 
160
240
  ```python
161
241
  @stream(name="Status", payload=dict)
162
- async def publish_status() -> dict:
242
+ async def publish_status() -> StatusResponse:
163
243
  return {"ok": True}
164
244
  ```
165
245
 
@@ -316,3 +396,4 @@ Ensure `app.finalize()` has been called before publishing or subscribing.
316
396
  **Q: How do I integrate this with other libraries (state machine, storage, etc.)?**
317
397
 
318
398
  Use `app.register_rpc_plugin()` to merge additional RPC definitions before calling `.finalize()`.
399
+
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: vention-communication
3
- Version: 0.3.0
4
- Summary: A framework for communication between machine apps and other services.
5
- License: Proprietary
6
- Author: VentionCo
7
- Requires-Python: >=3.10,<3.11
8
- Classifier: License :: Other/Proprietary License
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.10
11
- Requires-Dist: fastapi (==0.121.1)
12
- Requires-Dist: uvicorn (>=0.35.0,<0.36.0)
13
- Description-Content-Type: text/markdown
14
-
15
1
  # Vention Communication
16
2
 
17
3
  A thin, FastAPI-powered RPC layer for machine-apps that exposes Connect-compatible request-response and server-streaming endpoints — plus .proto generation from Python decorators, allowing typed SDKs to be generated separately via Buf.
@@ -88,8 +74,10 @@ A complete "hello world" in three steps.
88
74
 
89
75
  ```python
90
76
  from pydantic import BaseModel
91
- from vention_communication import VentionApp, action, stream
92
- import time, random
77
+ from communication.app import VentionApp
78
+ from communication.decorators import action, stream
79
+ import time
80
+ import random
93
81
 
94
82
  class PingRequest(BaseModel):
95
83
  message: str
@@ -114,6 +102,15 @@ async def heartbeat():
114
102
 
115
103
  app.finalize()
116
104
 
105
+ # Emit heartbeat every second
106
+ @app.on_event("startup")
107
+ async def startup():
108
+ asyncio.create_task(loop())
109
+
110
+ async def loop():
111
+ while True:
112
+ asyncio.create_task(heartbeat())
113
+ await asyncio.sleep(1)
117
114
  ```
118
115
 
119
116
  **Run:**
@@ -128,21 +125,59 @@ Endpoints are automatically registered under `/rpc/vention.app.v1.DemoAppService
128
125
 
129
126
  After startup, `proto/app.proto` is emitted automatically.
130
127
 
131
- You can now use Buf or protoc to generate client SDKs:
128
+ You can now use Buf or protoc to generate client SDKs, based on each client stack you desire.
129
+
130
+ 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:
131
+
132
+ https://buf.build/docs/cli/installation/
133
+
134
+ ### 3. Example TypeScript Client
135
+
136
+ Make sure you have Node 24+ installed. Use [NVM](https://github.com/nvm-sh/nvm) to easily install and manage different Node versions.
137
+
138
+ 1. Create a folder called `client` and `cd` into it.
139
+
140
+ #### Protobuf Javascript/Typescript libraries
141
+
142
+ 2. Install the runtime library, code generator, and the Buf CLI:
132
143
 
133
144
  ```bash
134
- buf generate --template buf.gen.ts.yaml
135
- buf generate --template buf.gen.python.yaml
145
+ npm install @bufbuild/protobuf
146
+ npm install --save-dev @bufbuild/protoc-gen-es @bufbuild/buf
136
147
  ```
137
148
 
138
- SDK generation is external to vention-communication allowing you to control versions and plugins.
149
+ 3. Create a buf.gen.yaml file that looks like this:
139
150
 
140
- ### 3. Example TypeScript Client
151
+ ```yaml
152
+ version: v2
153
+ inputs:
154
+ - directory: proto
155
+ plugins:
156
+ - local: protoc-gen-es
157
+ opt: target=ts
158
+ out: src/gen
159
+ ```
160
+
161
+ 4. Generate the client code, pointing the path to the newly generated *proto* folder:
162
+
163
+ ```bash
164
+ npx buf generate ../proto
165
+ ```
166
+
167
+ #### Client Application
168
+
169
+ 1. Install the client RPC libraries:
170
+
171
+ ```bash
172
+ npm i @connectrpc/connect @connectrpc/connect-web
173
+ ```
174
+
175
+ 2. Then create an `index.ts` file in the `client/src` folder, and paste the following code in it:
141
176
 
142
177
  ```typescript
143
178
  import { createClient } from "@connectrpc/connect";
144
179
  import { createConnectTransport } from "@connectrpc/connect-web";
145
- import { DemoAppService } from "./gen/connect/proto/app_connect";
180
+ import { DemoAppService } from "./gen/connect/app_pb.ts";
146
181
 
147
182
  const transport = createConnectTransport({
148
183
  baseUrl: "http://localhost:8000/rpc",
@@ -164,8 +199,11 @@ for await (const hb of client.heartbeat({})) {
164
199
  ### Add a new request-response endpoint
165
200
 
166
201
  ```python
202
+ class StatusResponse(BaseModel):
203
+ ok: bool
204
+
167
205
  @action()
168
- async def get_status() -> dict:
206
+ async def get_status() -> StatusResponse:
169
207
  return {"ok": True}
170
208
  ```
171
209
 
@@ -173,7 +211,7 @@ async def get_status() -> dict:
173
211
 
174
212
  ```python
175
213
  @stream(name="Status", payload=dict)
176
- async def publish_status() -> dict:
214
+ async def publish_status() -> StatusResponse:
177
215
  return {"ok": True}
178
216
  ```
179
217
 
@@ -330,4 +368,3 @@ Ensure `app.finalize()` has been called before publishing or subscribing.
330
368
  **Q: How do I integrate this with other libraries (state machine, storage, etc.)?**
331
369
 
332
370
  Use `app.register_rpc_plugin()` to merge additional RPC definitions before calling `.finalize()`.
333
-
@@ -0,0 +1,100 @@
1
+ [tool.poetry]
2
+ name = "vention-communication"
3
+ version = "0.3.6"
4
+ description = "A framework for communication between machine apps and other services."
5
+ authors = [ "VentionCo" ]
6
+ readme = "README.md"
7
+ license = "Proprietary"
8
+
9
+ [[tool.poetry.packages]]
10
+ include = "communication"
11
+ from = "src"
12
+
13
+ [tool.poetry.build]
14
+ generate-setup-file = false
15
+
16
+ [tool.poetry.dependencies]
17
+ python = ">=3.10,<3.11"
18
+
19
+ [tool.poetry.dependencies.annotated-doc]
20
+ version = "0.0.4"
21
+ markers = 'python_version == "3.10"'
22
+ optional = false
23
+
24
+ [tool.poetry.dependencies.annotated-types]
25
+ version = "0.7.0"
26
+ markers = 'python_version == "3.10"'
27
+ optional = false
28
+
29
+ [tool.poetry.dependencies.anyio]
30
+ version = "4.11.0"
31
+ markers = 'python_version == "3.10"'
32
+ optional = false
33
+
34
+ [tool.poetry.dependencies.click]
35
+ version = "8.1.8"
36
+ markers = 'python_version == "3.10"'
37
+ optional = false
38
+
39
+ [tool.poetry.dependencies.colorama]
40
+ version = "0.4.6"
41
+ markers = 'python_version == "3.10" and platform_system == "Windows"'
42
+ optional = false
43
+
44
+ [tool.poetry.dependencies.exceptiongroup]
45
+ version = "1.3.0"
46
+ markers = 'python_version == "3.10"'
47
+ optional = false
48
+
49
+ [tool.poetry.dependencies.fastapi]
50
+ version = "0.121.1"
51
+ markers = 'python_version == "3.10"'
52
+ optional = false
53
+
54
+ [tool.poetry.dependencies.h11]
55
+ version = "0.16.0"
56
+ markers = 'python_version == "3.10"'
57
+ optional = false
58
+
59
+ [tool.poetry.dependencies.idna]
60
+ version = "3.11"
61
+ markers = 'python_version == "3.10"'
62
+ optional = false
63
+
64
+ [tool.poetry.dependencies.pydantic-core]
65
+ version = "2.41.4"
66
+ markers = 'python_version == "3.10"'
67
+ optional = false
68
+
69
+ [tool.poetry.dependencies.pydantic]
70
+ version = "2.12.3"
71
+ markers = 'python_version == "3.10"'
72
+ optional = false
73
+
74
+ [tool.poetry.dependencies.sniffio]
75
+ version = "1.3.1"
76
+ markers = 'python_version == "3.10"'
77
+ optional = false
78
+
79
+ [tool.poetry.dependencies.starlette]
80
+ version = "0.48.0"
81
+ markers = 'python_version == "3.10"'
82
+ optional = false
83
+
84
+ [tool.poetry.dependencies.typing-extensions]
85
+ version = "4.15.0"
86
+ markers = 'python_version == "3.10"'
87
+ optional = false
88
+
89
+ [tool.poetry.dependencies.typing-inspection]
90
+ version = "0.4.2"
91
+ markers = 'python_version == "3.10"'
92
+ optional = false
93
+
94
+ [tool.poetry.dependencies.uvicorn]
95
+ version = "0.35.0"
96
+ markers = 'python_version == "3.10"'
97
+ optional = false
98
+
99
+ [tool.poetry.group.dev]
100
+ dependencies = { }
@@ -1,17 +1,30 @@
1
1
  from __future__ import annotations
2
- from typing import Any, Dict, List, Optional, Type, Union, get_args, get_origin
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
- if origin is Union:
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 = [arg for arg in args if arg is not type(None)]
58
+ non_none_args = _filter_non_none_types(args)
28
59
  if len(non_none_args) == 1:
29
- return (non_none_args[0], True)
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
- return (args[0], True)
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
- # Recursively register nested model if not seen before
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
- inner_type, _ = _unwrap_optional(field_type)
74
- list_inner_type, is_list = _unwrap_list(inner_type)
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(list_inner_type, seen_models, lines)
120
+ proto_type = _determine_proto_type_for_field(final_inner_type, seen_models, lines, depth)
77
121
 
78
122
  if is_list:
79
- proto_type = f"repeated {proto_type}"
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
- inner_type, _ = _unwrap_optional(type_annotation)
197
+ unwrapped_type, _ = _unwrap_optional(type_annotation)
154
198
 
155
- list_inner_type, _ = _unwrap_list(inner_type)
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
- inner_type, _ = _unwrap_optional(stream_entry.payload_type)
173
- list_inner_type, _ = _unwrap_list(inner_type)
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
- stream_entry.payload_type, scalar_wrappers, stream_entry.name
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:
@@ -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}
@@ -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(
@@ -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
@@ -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
@@ -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,17 +0,0 @@
1
- [tool.poetry]
2
- name = "vention-communication"
3
- version = "0.3.0"
4
- description = "A framework for communication between machine apps and other services."
5
- authors = [ "VentionCo" ]
6
- readme = "README.md"
7
- license = "Proprietary"
8
- packages = [{ include = "communication", from = "src" }]
9
-
10
- [tool.poetry.dependencies]
11
- python = ">=3.10,<3.11"
12
- fastapi = "0.121.1"
13
- uvicorn = "^0.35.0"
14
-
15
- [tool.poetry.group.dev.dependencies]
16
- pytest = "^8.3.4"
17
- ruff = "^0.8.0"