aspyx-service 0.10.2__tar.gz → 0.10.3__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.
Potentially problematic release.
This version of aspyx-service might be problematic. Click here for more details.
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/PKG-INFO +11 -7
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/README.md +10 -6
- aspyx_service-0.10.3/performance_tests/performance-test.py +205 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/pyproject.toml +1 -1
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/src/aspyx_service/channels.py +18 -13
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/src/aspyx_service/restchannel.py +10 -11
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/src/aspyx_service/serialization.py +3 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/src/aspyx_service/service.py +8 -7
- aspyx_service-0.10.3/tests/__init__.py +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/tests/common.py +5 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/tests/test_async_service.py +6 -8
- aspyx_service-0.10.3/tests/test_serialization.py +69 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/tests/test_service.py +5 -5
- aspyx_service-0.10.2/tests/__init__.py +0 -1
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/.gitignore +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/LICENSE +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/src/aspyx_service/__init__.py +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/src/aspyx_service/healthcheck.py +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/src/aspyx_service/registries.py +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/src/aspyx_service/server.py +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.3}/tests/config.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aspyx_service
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.3
|
|
4
4
|
Summary: Aspyx Service framework
|
|
5
5
|
Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -70,19 +70,23 @@ that lets you deploy, discover and call services with different remoting protoco
|
|
|
70
70
|
|
|
71
71
|
The basic design consists of four different concepts:
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
**Service**
|
|
74
|
+
|
|
74
75
|
defines a group of methods that can be called either locally or remotely.
|
|
75
76
|
These methods represent the functional interface exposed to clients — similar to an interface in traditional programming
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
**Component**
|
|
79
|
+
|
|
78
80
|
a component bundles one or more services and declares the channels (protocols) used to expose them.
|
|
79
81
|
Think of a component as a deployment unit or module.
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
**Component Registry**
|
|
84
|
+
|
|
82
85
|
acts as the central directory for managing available components.
|
|
83
86
|
It allows the framework to register, discover, and resolve components and their services.
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
**Channel**
|
|
89
|
+
|
|
86
90
|
is a pluggable transport layer that defines how service method invocations are transmitted and handled.
|
|
87
91
|
|
|
88
92
|
Let's look at the "interface" layer first.
|
|
@@ -111,7 +115,7 @@ class Module:
|
|
|
111
115
|
|
|
112
116
|
@create()
|
|
113
117
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
114
|
-
return ConsulComponentRegistry(Server.port, "
|
|
118
|
+
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
115
119
|
|
|
116
120
|
environment = Environment(Module)
|
|
117
121
|
service_manager = environment.get(ServiceManager)
|
|
@@ -361,7 +365,7 @@ A so called `URLSelector` is used internally to provide URLs for every single ca
|
|
|
361
365
|
- `FirstURLSelector` always returns the first URL of the list of possible URLs
|
|
362
366
|
- `RoundRobinURLSelector` switches sequentially between all URLs.
|
|
363
367
|
|
|
364
|
-
To customize the behavior, an around advice can be implemented easily:
|
|
368
|
+
To customize the behavior, an `around` advice can be implemented easily:
|
|
365
369
|
|
|
366
370
|
**Example**:
|
|
367
371
|
|
|
@@ -34,19 +34,23 @@ that lets you deploy, discover and call services with different remoting protoco
|
|
|
34
34
|
|
|
35
35
|
The basic design consists of four different concepts:
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**Service**
|
|
38
|
+
|
|
38
39
|
defines a group of methods that can be called either locally or remotely.
|
|
39
40
|
These methods represent the functional interface exposed to clients — similar to an interface in traditional programming
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
**Component**
|
|
43
|
+
|
|
42
44
|
a component bundles one or more services and declares the channels (protocols) used to expose them.
|
|
43
45
|
Think of a component as a deployment unit or module.
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
**Component Registry**
|
|
48
|
+
|
|
46
49
|
acts as the central directory for managing available components.
|
|
47
50
|
It allows the framework to register, discover, and resolve components and their services.
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
**Channel**
|
|
53
|
+
|
|
50
54
|
is a pluggable transport layer that defines how service method invocations are transmitted and handled.
|
|
51
55
|
|
|
52
56
|
Let's look at the "interface" layer first.
|
|
@@ -75,7 +79,7 @@ class Module:
|
|
|
75
79
|
|
|
76
80
|
@create()
|
|
77
81
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
78
|
-
return ConsulComponentRegistry(Server.port, "
|
|
82
|
+
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
79
83
|
|
|
80
84
|
environment = Environment(Module)
|
|
81
85
|
service_manager = environment.get(ServiceManager)
|
|
@@ -325,7 +329,7 @@ A so called `URLSelector` is used internally to provide URLs for every single ca
|
|
|
325
329
|
- `FirstURLSelector` always returns the first URL of the list of possible URLs
|
|
326
330
|
- `RoundRobinURLSelector` switches sequentially between all URLs.
|
|
327
331
|
|
|
328
|
-
To customize the behavior, an around advice can be implemented easily:
|
|
332
|
+
To customize the behavior, an `around` advice can be implemented easily:
|
|
329
333
|
|
|
330
334
|
**Example**:
|
|
331
335
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from typing import Callable, TypeVar, Type, Awaitable, Any
|
|
10
|
+
|
|
11
|
+
from packages.aspyx_service.tests.common import service_manager, TestService, TestRestService, Pydantic, Data, \
|
|
12
|
+
TestAsyncRestService, TestAsyncService
|
|
13
|
+
from packages.aspyx_service.tests.test_async_service import data
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
# main
|
|
18
|
+
|
|
19
|
+
pydantic = Pydantic(i=1, f=1.0, b=True, s="s")
|
|
20
|
+
data = Data(i=1, f=1.0, b=True, s="s")
|
|
21
|
+
|
|
22
|
+
def run_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], None]):
|
|
23
|
+
start = time.perf_counter()
|
|
24
|
+
for _ in range(loops):
|
|
25
|
+
callable(instance)
|
|
26
|
+
|
|
27
|
+
end = time.perf_counter()
|
|
28
|
+
avg_ms = ((end - start) / loops) * 1000
|
|
29
|
+
|
|
30
|
+
print(f"run {name}, loops={loops}: avg={avg_ms:.3f} ms")
|
|
31
|
+
|
|
32
|
+
async def run_async_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], Awaitable[Any]]):
|
|
33
|
+
start = time.perf_counter()
|
|
34
|
+
for _ in range(loops):
|
|
35
|
+
await callable(instance)
|
|
36
|
+
|
|
37
|
+
end = time.perf_counter()
|
|
38
|
+
avg_ms = ((end - start) / loops) * 1000
|
|
39
|
+
|
|
40
|
+
print(f"run {name}, loops={loops}: avg={avg_ms:.3f} ms")
|
|
41
|
+
|
|
42
|
+
def run_threaded_async_loops(name: str, loops: int, n_threads: int, type: Type[T], instance: T, callable: Callable[[T], Awaitable[Any]]):
|
|
43
|
+
threads = []
|
|
44
|
+
|
|
45
|
+
def worker(thread_id: int):
|
|
46
|
+
loop = asyncio.new_event_loop()
|
|
47
|
+
asyncio.set_event_loop(loop)
|
|
48
|
+
|
|
49
|
+
async def run():
|
|
50
|
+
for i in range(loops):
|
|
51
|
+
await callable(instance)
|
|
52
|
+
|
|
53
|
+
loop.run_until_complete(run())
|
|
54
|
+
loop.close()
|
|
55
|
+
|
|
56
|
+
start = time.perf_counter()
|
|
57
|
+
|
|
58
|
+
for t_id in range(0, n_threads):
|
|
59
|
+
thread = threading.Thread(target=worker, args=(t_id,))
|
|
60
|
+
threads.append(thread)
|
|
61
|
+
thread.start()
|
|
62
|
+
|
|
63
|
+
for thread in threads:
|
|
64
|
+
thread.join()
|
|
65
|
+
|
|
66
|
+
end = time.perf_counter()
|
|
67
|
+
took = (end - start) * 1000
|
|
68
|
+
avg_ms = ((end - start) / (n_threads * loops)) * 1000
|
|
69
|
+
|
|
70
|
+
print(f"{name} {loops} in {n_threads} threads: {took} ms, avg: {avg_ms}ms")
|
|
71
|
+
|
|
72
|
+
def run_threaded_sync_loops(name: str, loops: int, n_threads: int, type: Type[T], instance: T, callable: Callable[[T], Any]):
|
|
73
|
+
threads = []
|
|
74
|
+
|
|
75
|
+
def worker(thread_id: int):
|
|
76
|
+
loop = asyncio.new_event_loop()
|
|
77
|
+
asyncio.set_event_loop(loop)
|
|
78
|
+
|
|
79
|
+
async def run():
|
|
80
|
+
for i in range(loops):
|
|
81
|
+
callable(instance)
|
|
82
|
+
|
|
83
|
+
loop.run_until_complete(run())
|
|
84
|
+
loop.close()
|
|
85
|
+
|
|
86
|
+
start = time.perf_counter()
|
|
87
|
+
|
|
88
|
+
for t_id in range(0, n_threads):
|
|
89
|
+
thread = threading.Thread(target=worker, args=(t_id,))
|
|
90
|
+
threads.append(thread)
|
|
91
|
+
thread.start()
|
|
92
|
+
|
|
93
|
+
for thread in threads:
|
|
94
|
+
thread.join()
|
|
95
|
+
|
|
96
|
+
end = time.perf_counter()
|
|
97
|
+
took = (end - start) * 1000
|
|
98
|
+
avg_ms = ((end - start) / (n_threads * loops)) * 1000
|
|
99
|
+
|
|
100
|
+
print(f"{name} {loops} in {n_threads} threads: {took} ms, avg: {avg_ms}ms")
|
|
101
|
+
|
|
102
|
+
async def main():
|
|
103
|
+
# get service manager
|
|
104
|
+
|
|
105
|
+
manager = service_manager()
|
|
106
|
+
loops = 1000
|
|
107
|
+
|
|
108
|
+
# tests
|
|
109
|
+
|
|
110
|
+
run_loops("rest", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"), lambda service: service.get("world"))
|
|
111
|
+
run_loops("json", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-json"), lambda service: service.hello("world"))
|
|
112
|
+
run_loops("msgpack", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-msgpack"), lambda service: service.hello("world"))
|
|
113
|
+
|
|
114
|
+
# pydantic
|
|
115
|
+
|
|
116
|
+
run_loops("rest & pydantic", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"), lambda service: service.post_pydantic("hello", pydantic))
|
|
117
|
+
run_loops("json & pydantic", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-json"), lambda service: service.pydantic(pydantic))
|
|
118
|
+
run_loops("msgpack & pydantic", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-msgpack"), lambda service: service.pydantic(pydantic))
|
|
119
|
+
|
|
120
|
+
# data class
|
|
121
|
+
|
|
122
|
+
run_loops("rest & data", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"),
|
|
123
|
+
lambda service: service.post_data("hello", data))
|
|
124
|
+
run_loops("json & data", loops, TestService,
|
|
125
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
126
|
+
lambda service: service.data(data))
|
|
127
|
+
run_loops("msgpack & data", loops, TestService,
|
|
128
|
+
manager.get_service(TestService, preferred_channel="dispatch-msgpack"),
|
|
129
|
+
lambda service: service.data(data))
|
|
130
|
+
|
|
131
|
+
# async
|
|
132
|
+
|
|
133
|
+
await run_async_loops("async rest", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
134
|
+
lambda service: service.get("world"))
|
|
135
|
+
await run_async_loops("async json", loops, TestAsyncService, manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
136
|
+
lambda service: service.hello("world"))
|
|
137
|
+
await run_async_loops("async msgpack", loops, TestAsyncService, manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
138
|
+
lambda service: service.hello("world"))
|
|
139
|
+
|
|
140
|
+
# pydantic
|
|
141
|
+
|
|
142
|
+
await run_async_loops("async rest & pydantic", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
143
|
+
lambda service: service.post_pydantic("hello", pydantic))
|
|
144
|
+
await run_async_loops("async json & pydantic", loops, TestAsyncService,
|
|
145
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
146
|
+
lambda service: service.pydantic(pydantic))
|
|
147
|
+
await run_async_loops("async msgpack & pydantic", loops, TestAsyncService,
|
|
148
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
149
|
+
lambda service: service.pydantic(pydantic))
|
|
150
|
+
|
|
151
|
+
# data class
|
|
152
|
+
|
|
153
|
+
# pydantic
|
|
154
|
+
|
|
155
|
+
await run_async_loops("async rest & data", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
156
|
+
lambda service: service.post_data("hello", data))
|
|
157
|
+
await run_async_loops("async json & data", loops, TestAsyncService,
|
|
158
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
159
|
+
lambda service: service.data(data))
|
|
160
|
+
await run_async_loops("async msgpack & data", loops, TestAsyncService,
|
|
161
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
162
|
+
lambda service: service.data(data))
|
|
163
|
+
|
|
164
|
+
# a real thread test
|
|
165
|
+
|
|
166
|
+
# sync
|
|
167
|
+
|
|
168
|
+
run_threaded_sync_loops("threaded sync json, 1 thread", loops, 1, TestService,
|
|
169
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
170
|
+
lambda service: service.hello("world"))
|
|
171
|
+
run_threaded_sync_loops("threaded sync json, 2 thread", loops, 2, TestService,
|
|
172
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
173
|
+
lambda service: service.hello("world"))
|
|
174
|
+
run_threaded_sync_loops("threaded sync json, 4 thread", loops, 4, TestService,
|
|
175
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
176
|
+
lambda service: service.hello("world"))
|
|
177
|
+
run_threaded_sync_loops("threaded sync json, 8 thread", loops, 8, TestService,
|
|
178
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
179
|
+
lambda service: service.hello("world"))
|
|
180
|
+
run_threaded_sync_loops("threaded sync json, 16 thread", loops, 16, TestService,
|
|
181
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
182
|
+
lambda service: service.hello("world"))
|
|
183
|
+
|
|
184
|
+
# async
|
|
185
|
+
|
|
186
|
+
run_threaded_async_loops("threaded async json, 1 thread", loops, 1, TestAsyncService,
|
|
187
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
188
|
+
lambda service: service.hello("world") )
|
|
189
|
+
run_threaded_async_loops("threaded async json, 2 thread", loops, 2, TestAsyncService,
|
|
190
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
191
|
+
lambda service: service.hello("world"))
|
|
192
|
+
run_threaded_async_loops("threaded async json, 4 thread", loops, 4, TestAsyncService,
|
|
193
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
194
|
+
lambda service: service.hello("world"))
|
|
195
|
+
run_threaded_async_loops("threaded async json, 8 thread", loops, 8, TestAsyncService,
|
|
196
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
197
|
+
lambda service: service.hello("world"))
|
|
198
|
+
run_threaded_async_loops("threaded async json, 16 thread", loops, 16, TestAsyncService,
|
|
199
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
200
|
+
lambda service: service.hello("world"))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
asyncio.run(main())
|
|
205
|
+
|
|
@@ -13,6 +13,7 @@ from pydantic import BaseModel
|
|
|
13
13
|
|
|
14
14
|
from aspyx.di.configuration import inject_value
|
|
15
15
|
from aspyx.reflection import DynamicProxy, TypeDescriptor
|
|
16
|
+
from aspyx.threading import ThreadLocal
|
|
16
17
|
from .service import ServiceManager, ServiceCommunicationException
|
|
17
18
|
|
|
18
19
|
from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
|
|
@@ -28,6 +29,11 @@ class HTTPXChannel(Channel):
|
|
|
28
29
|
"timeout"
|
|
29
30
|
]
|
|
30
31
|
|
|
32
|
+
# class properties
|
|
33
|
+
|
|
34
|
+
client_local = ThreadLocal[Client]()
|
|
35
|
+
async_client_local = ThreadLocal[AsyncClient]()
|
|
36
|
+
|
|
31
37
|
# class methods
|
|
32
38
|
|
|
33
39
|
@classmethod
|
|
@@ -61,8 +67,6 @@ class HTTPXChannel(Channel):
|
|
|
61
67
|
super().__init__()
|
|
62
68
|
|
|
63
69
|
self.timeout = 1000.0
|
|
64
|
-
self.client: Optional[Client] = None
|
|
65
|
-
self.async_client: Optional[AsyncClient] = None
|
|
66
70
|
self.service_names: dict[Type, str] = {}
|
|
67
71
|
self.deserializers: dict[Callable, Callable] = {}
|
|
68
72
|
|
|
@@ -95,24 +99,25 @@ class HTTPXChannel(Channel):
|
|
|
95
99
|
for service in component_descriptor.services:
|
|
96
100
|
self.service_names[service.type] = service.name
|
|
97
101
|
|
|
98
|
-
# make client
|
|
99
|
-
|
|
100
|
-
self.client = self.make_client()
|
|
101
|
-
self.async_client = self.make_async_client()
|
|
102
|
-
|
|
103
102
|
# public
|
|
104
103
|
|
|
105
104
|
def get_client(self) -> Client:
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
client = self.client_local.get()
|
|
106
|
+
|
|
107
|
+
if client is None:
|
|
108
|
+
client = self.make_client()
|
|
109
|
+
self.client_local.set(client)
|
|
108
110
|
|
|
109
|
-
return
|
|
111
|
+
return client
|
|
110
112
|
|
|
111
113
|
def get_async_client(self) -> AsyncClient:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
async_client = self.async_client_local.get()
|
|
115
|
+
|
|
116
|
+
if async_client is None:
|
|
117
|
+
async_client = self.make_async_client()
|
|
118
|
+
self.async_client_local.set(async_client)
|
|
114
119
|
|
|
115
|
-
return
|
|
120
|
+
return async_client
|
|
116
121
|
|
|
117
122
|
def make_client(self) -> Client:
|
|
118
123
|
return Client() # base_url=url
|
|
@@ -249,16 +249,15 @@ class RestChannel(HTTPXChannel):
|
|
|
249
249
|
try:
|
|
250
250
|
result = None
|
|
251
251
|
if call.type == "get":
|
|
252
|
-
result = await self.
|
|
252
|
+
result = await self.get_async_client().get(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
253
253
|
elif call.type == "put":
|
|
254
|
-
result = await self.
|
|
254
|
+
result = await self.get_async_client().put(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
255
255
|
elif call.type == "delete":
|
|
256
|
-
result = await self.
|
|
256
|
+
result = await self.get_async_client().delete(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
257
257
|
elif call.type == "post":
|
|
258
|
-
result = await self.
|
|
259
|
-
timeout=self.timeout).json()
|
|
258
|
+
result = await self.get_async_client().post(self.get_url() + url, params=query_params, json=body, timeout=self.timeout)
|
|
260
259
|
|
|
261
|
-
return self.get_deserializer(invocation.type, invocation.method)(result)
|
|
260
|
+
return self.get_deserializer(invocation.type, invocation.method)(result.json())
|
|
262
261
|
except ServiceCommunicationException:
|
|
263
262
|
raise
|
|
264
263
|
|
|
@@ -285,15 +284,15 @@ class RestChannel(HTTPXChannel):
|
|
|
285
284
|
try:
|
|
286
285
|
result = None
|
|
287
286
|
if call.type == "get":
|
|
288
|
-
result = self.get_client().get(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
287
|
+
result = self.get_client().get(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
289
288
|
elif call.type == "put":
|
|
290
|
-
result = self.get_client().put(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
289
|
+
result = self.get_client().put(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
291
290
|
elif call.type == "delete":
|
|
292
|
-
result = self.get_client().delete(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
291
|
+
result = self.get_client().delete(self.get_url() + url, params=query_params, timeout=self.timeout)
|
|
293
292
|
elif call.type == "post":
|
|
294
|
-
result = self.get_client().post(self.get_url() + url, params=query_params, json=body, timeout=self.timeout)
|
|
293
|
+
result = self.get_client().post(self.get_url() + url, params=query_params, json=body, timeout=self.timeout)
|
|
295
294
|
|
|
296
|
-
return self.get_deserializer(invocation.type, invocation.method)(result)
|
|
295
|
+
return self.get_deserializer(invocation.type, invocation.method)(result.json())
|
|
297
296
|
except ServiceCommunicationException:
|
|
298
297
|
raise
|
|
299
298
|
|
|
@@ -42,6 +42,9 @@ class TypeDeserializer:
|
|
|
42
42
|
if is_dataclass(typ):
|
|
43
43
|
field_deserializers = {f.name: TypeDeserializer(f.type) for f in fields(typ)}
|
|
44
44
|
def deser_dataclass(value):
|
|
45
|
+
if is_dataclass(value):
|
|
46
|
+
return value
|
|
47
|
+
|
|
45
48
|
return typ(**{
|
|
46
49
|
k: field_deserializers[k](v) for k, v in value.items()
|
|
47
50
|
})
|
|
@@ -769,11 +769,12 @@ class ServiceManager:
|
|
|
769
769
|
|
|
770
770
|
# check proxy
|
|
771
771
|
|
|
772
|
-
|
|
772
|
+
channel_key = TypeAndChannel(type=component_descriptor.type, channel=preferred_channel)
|
|
773
|
+
proxy_key = TypeAndChannel(type=service_type, channel=preferred_channel)
|
|
773
774
|
|
|
774
|
-
proxy = self.proxy_cache.get(
|
|
775
|
+
proxy = self.proxy_cache.get(proxy_key, None)
|
|
775
776
|
if proxy is None:
|
|
776
|
-
channel_instance = self.channel_cache.get(
|
|
777
|
+
channel_instance = self.channel_cache.get(channel_key, None)
|
|
777
778
|
|
|
778
779
|
if channel_instance is None:
|
|
779
780
|
address = self.find_service_address(component_descriptor, preferred_channel)
|
|
@@ -786,9 +787,9 @@ class ServiceManager:
|
|
|
786
787
|
# channel may have changed
|
|
787
788
|
|
|
788
789
|
if address.channel != preferred_channel:
|
|
789
|
-
|
|
790
|
+
channel_key = TypeAndChannel(type=component_descriptor.type, channel=address.channel)
|
|
790
791
|
|
|
791
|
-
channel_instance = self.channel_cache.get(
|
|
792
|
+
channel_instance = self.channel_cache.get(channel_key, None)
|
|
792
793
|
if channel_instance is None:
|
|
793
794
|
# create channel
|
|
794
795
|
|
|
@@ -796,7 +797,7 @@ class ServiceManager:
|
|
|
796
797
|
|
|
797
798
|
# cache
|
|
798
799
|
|
|
799
|
-
self.channel_cache[
|
|
800
|
+
self.channel_cache[channel_key] = channel_instance
|
|
800
801
|
|
|
801
802
|
# and watch for changes in the addresses
|
|
802
803
|
|
|
@@ -805,7 +806,7 @@ class ServiceManager:
|
|
|
805
806
|
# create proxy
|
|
806
807
|
|
|
807
808
|
proxy = DynamicProxy.create(service_type, channel_instance)
|
|
808
|
-
self.proxy_cache[
|
|
809
|
+
self.proxy_cache[proxy_key] = proxy
|
|
809
810
|
|
|
810
811
|
return proxy
|
|
811
812
|
|
|
File without changes
|
|
@@ -6,7 +6,7 @@ import unittest
|
|
|
6
6
|
from .common import TestAsyncService, TestAsyncRestService, Pydantic, Data, service_manager
|
|
7
7
|
|
|
8
8
|
pydantic = Pydantic(i=1, f=1.0, b=True, s="s")
|
|
9
|
-
data = Data(i=1, f=1.0, b=True, s="s"
|
|
9
|
+
data = Data(i=1, f=1.0, b=True, s="s")
|
|
10
10
|
|
|
11
11
|
class TestAsyncRemoteService(unittest.IsolatedAsyncioTestCase):
|
|
12
12
|
@classmethod
|
|
@@ -25,7 +25,7 @@ class TestAsyncRemoteService(unittest.IsolatedAsyncioTestCase):
|
|
|
25
25
|
result_pydantic = await test_service.pydantic(pydantic)
|
|
26
26
|
self.assertEqual(result_pydantic, pydantic)
|
|
27
27
|
|
|
28
|
-
async def
|
|
28
|
+
async def xtest_dispatch_msgpack(self):
|
|
29
29
|
test_service = self.service_manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack")
|
|
30
30
|
|
|
31
31
|
result = await test_service.hello("hello")
|
|
@@ -37,7 +37,7 @@ class TestAsyncRemoteService(unittest.IsolatedAsyncioTestCase):
|
|
|
37
37
|
result_pydantic = await test_service.pydantic(pydantic)
|
|
38
38
|
self.assertEqual(result_pydantic, pydantic)
|
|
39
39
|
|
|
40
|
-
async def
|
|
40
|
+
async def xtest_rest(self):
|
|
41
41
|
test_service = self.service_manager.get_service(TestAsyncRestService, preferred_channel="rest")
|
|
42
42
|
|
|
43
43
|
result = await test_service.get("hello")
|
|
@@ -49,10 +49,8 @@ class TestAsyncRemoteService(unittest.IsolatedAsyncioTestCase):
|
|
|
49
49
|
result = await test_service.delete("hello")
|
|
50
50
|
self.assertEqual(result, "hello")
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
result_pydantic = test_service.post_pydantic("message", pydantic)
|
|
52
|
+
result_pydantic = await test_service.post_pydantic("message", pydantic)
|
|
55
53
|
self.assertEqual(result_pydantic, pydantic)
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
result_data = await test_service.post_data("message", data)
|
|
56
|
+
self.assertEqual(result_data, data)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from aspyx_service.serialization import get_deserializer, get_serializer
|
|
7
|
+
|
|
8
|
+
class Pydantic(BaseModel):
|
|
9
|
+
i : int
|
|
10
|
+
f : float
|
|
11
|
+
b: bool
|
|
12
|
+
s: str
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Data:
|
|
16
|
+
i: int
|
|
17
|
+
f: float
|
|
18
|
+
b: bool
|
|
19
|
+
s: str
|
|
20
|
+
|
|
21
|
+
class PydanticAndData(BaseModel):
|
|
22
|
+
data: Data
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DataAndPydantic:
|
|
26
|
+
pydantic: Pydantic
|
|
27
|
+
|
|
28
|
+
pydantic = Pydantic(i=1, f=1.0, b=True, s="s")
|
|
29
|
+
data = Data(i=1, f=1.0, b=True, s="s")
|
|
30
|
+
|
|
31
|
+
p_plus_d = PydanticAndData(data=data)
|
|
32
|
+
d_plus_p = DataAndPydantic(pydantic=pydantic)
|
|
33
|
+
|
|
34
|
+
class TestSerialization(unittest.TestCase):
|
|
35
|
+
def test_pydantic(self):
|
|
36
|
+
serializer = get_serializer(Pydantic)
|
|
37
|
+
deserializer = get_deserializer(Pydantic)
|
|
38
|
+
|
|
39
|
+
output = serializer(pydantic)
|
|
40
|
+
reverse = deserializer(output)
|
|
41
|
+
|
|
42
|
+
self.assertEqual(reverse, pydantic)
|
|
43
|
+
|
|
44
|
+
def test_data(self):
|
|
45
|
+
serializer = get_serializer(Data)
|
|
46
|
+
deserializer = get_deserializer(Data)
|
|
47
|
+
|
|
48
|
+
output = serializer(data)
|
|
49
|
+
reverse = deserializer(output)
|
|
50
|
+
|
|
51
|
+
self.assertEqual(reverse, data)
|
|
52
|
+
|
|
53
|
+
def test_pydantic_plus_data(self):
|
|
54
|
+
serializer = get_serializer(PydanticAndData)
|
|
55
|
+
deserializer = get_deserializer(PydanticAndData)
|
|
56
|
+
|
|
57
|
+
output = serializer(p_plus_d)
|
|
58
|
+
reverse = deserializer(output)
|
|
59
|
+
|
|
60
|
+
self.assertEqual(p_plus_d, p_plus_d)
|
|
61
|
+
|
|
62
|
+
def test_data_plus_pydantic(self):
|
|
63
|
+
serializer = get_serializer(DataAndPydantic)
|
|
64
|
+
deserializer = get_deserializer(DataAndPydantic)
|
|
65
|
+
|
|
66
|
+
output = serializer(d_plus_p)
|
|
67
|
+
reverse = deserializer(output)
|
|
68
|
+
|
|
69
|
+
self.assertEqual(reverse, d_plus_p)
|
|
@@ -7,7 +7,7 @@ import unittest
|
|
|
7
7
|
from .common import TestService, TestRestService, Test, Pydantic, Data, service_manager
|
|
8
8
|
|
|
9
9
|
pydantic = Pydantic(i=1, f=1.0, b=True, s="s")
|
|
10
|
-
data = Data(i=1, f=1.0, b=True, s="s"
|
|
10
|
+
data = Data(i=1, f=1.0, b=True, s="s")
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class TestLocalService(unittest.TestCase):
|
|
@@ -37,7 +37,7 @@ class TestSyncRemoteService(unittest.TestCase):
|
|
|
37
37
|
def setUpClass(cls):
|
|
38
38
|
cls.service_manager = service_manager()
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def test_dispatch_json(self):
|
|
41
41
|
test_service = self.service_manager.get_service(TestService, preferred_channel="dispatch-json")
|
|
42
42
|
|
|
43
43
|
result = test_service.hello("hello")
|
|
@@ -49,7 +49,7 @@ class TestSyncRemoteService(unittest.TestCase):
|
|
|
49
49
|
result_pydantic = test_service.pydantic(pydantic)
|
|
50
50
|
self.assertEqual(result_pydantic, pydantic)
|
|
51
51
|
|
|
52
|
-
def
|
|
52
|
+
def test_dispatch_msgpack(self):
|
|
53
53
|
test_service = self.service_manager.get_service(TestService, preferred_channel="dispatch-msgpack")
|
|
54
54
|
|
|
55
55
|
result = test_service.hello("hello")
|
|
@@ -78,5 +78,5 @@ class TestSyncRemoteService(unittest.TestCase):
|
|
|
78
78
|
result_pydantic = test_service.post_pydantic("message", pydantic)
|
|
79
79
|
self.assertEqual(result_pydantic, pydantic)
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
result_data= test_service.post_data("message", data)
|
|
82
|
+
self.assertEqual(result_data, data)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|