aspyx-service 0.10.2__tar.gz → 0.10.4__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.4}/PKG-INFO +41 -10
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/README.md +38 -8
- aspyx_service-0.10.4/performance-test/__init__.py +0 -0
- aspyx_service-0.10.4/performance-test/client.py +154 -0
- aspyx_service-0.10.4/performance-test/main.py +23 -0
- aspyx_service-0.10.4/performance-test/performance-test.py +279 -0
- aspyx_service-0.10.4/performance-test/readme.txt +1 -0
- aspyx_service-0.10.4/performance-test/server.py +154 -0
- aspyx_service-0.10.4/performance-test/start_server_8000.sh +3 -0
- aspyx_service-0.10.4/performance-test/start_server_8001.sh +3 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/pyproject.toml +13 -2
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/src/aspyx_service/__init__.py +28 -4
- aspyx_service-0.10.4/src/aspyx_service/authorization.py +135 -0
- aspyx_service-0.10.4/src/aspyx_service/channels.py +471 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/src/aspyx_service/healthcheck.py +1 -1
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/src/aspyx_service/registries.py +5 -5
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/src/aspyx_service/restchannel.py +17 -21
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/src/aspyx_service/serialization.py +6 -3
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/src/aspyx_service/server.py +139 -69
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/src/aspyx_service/service.py +55 -19
- aspyx_service-0.10.4/src/aspyx_service/session.py +97 -0
- aspyx_service-0.10.4/tests/__init__.py +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/tests/common.py +144 -31
- aspyx_service-0.10.4/tests/config.yaml +16 -0
- aspyx_service-0.10.4/tests/test_async_service.py +53 -0
- aspyx_service-0.10.4/tests/test_healthcheck.py +51 -0
- aspyx_service-0.10.4/tests/test_jwt.py +403 -0
- aspyx_service-0.10.4/tests/test_serialization.py +71 -0
- aspyx_service-0.10.4/tests/test_service.py +80 -0
- aspyx_service-0.10.2/src/aspyx_service/channels.py +0 -277
- aspyx_service-0.10.2/tests/__init__.py +0 -1
- aspyx_service-0.10.2/tests/test_async_service.py +0 -58
- aspyx_service-0.10.2/tests/test_service.py +0 -82
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/.gitignore +0 -0
- {aspyx_service-0.10.2 → aspyx_service-0.10.4}/LICENSE +0 -0
- {aspyx_service-0.10.2/tests → aspyx_service-0.10.4/performance-test}/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.4
|
|
4
4
|
Summary: Aspyx Service framework
|
|
5
5
|
Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -26,7 +26,8 @@ License: MIT License
|
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Python: >=3.9
|
|
29
|
-
Requires-Dist: aspyx>=1.5.
|
|
29
|
+
Requires-Dist: aspyx>=1.5.3
|
|
30
|
+
Requires-Dist: cachetools~=5.5.2
|
|
30
31
|
Requires-Dist: fastapi~=0.115.13
|
|
31
32
|
Requires-Dist: httpx~=0.28.1
|
|
32
33
|
Requires-Dist: msgpack~=1.1.1
|
|
@@ -60,6 +61,8 @@ Description-Content-Type: text/markdown
|
|
|
60
61
|
- [Rest Calls](#rest-calls)
|
|
61
62
|
- [Intercepting calls](#intercepting-calls)
|
|
62
63
|
- [FastAPI server](#fastapi-server)
|
|
64
|
+
- [Session](#session)
|
|
65
|
+
- [Authorization](#authorization)
|
|
63
66
|
- [Implementing Channels](#implementing-channels)
|
|
64
67
|
- [Version History](#version-history)
|
|
65
68
|
|
|
@@ -70,19 +73,23 @@ that lets you deploy, discover and call services with different remoting protoco
|
|
|
70
73
|
|
|
71
74
|
The basic design consists of four different concepts:
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
**Service**
|
|
77
|
+
|
|
74
78
|
defines a group of methods that can be called either locally or remotely.
|
|
75
79
|
These methods represent the functional interface exposed to clients — similar to an interface in traditional programming
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
**Component**
|
|
82
|
+
|
|
78
83
|
a component bundles one or more services and declares the channels (protocols) used to expose them.
|
|
79
84
|
Think of a component as a deployment unit or module.
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
**Component Registry**
|
|
87
|
+
|
|
82
88
|
acts as the central directory for managing available components.
|
|
83
89
|
It allows the framework to register, discover, and resolve components and their services.
|
|
84
90
|
|
|
85
|
-
|
|
91
|
+
**Channel**
|
|
92
|
+
|
|
86
93
|
is a pluggable transport layer that defines how service method invocations are transmitted and handled.
|
|
87
94
|
|
|
88
95
|
Let's look at the "interface" layer first.
|
|
@@ -111,7 +118,7 @@ class Module:
|
|
|
111
118
|
|
|
112
119
|
@create()
|
|
113
120
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
114
|
-
return ConsulComponentRegistry(Server.port, "
|
|
121
|
+
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
115
122
|
|
|
116
123
|
environment = Environment(Module)
|
|
117
124
|
service_manager = environment.get(ServiceManager)
|
|
@@ -162,10 +169,24 @@ environment = server.boot(Module)
|
|
|
162
169
|
Of course, service can also be called locally. In case of multiple possible channels, a keyword argument is used to
|
|
163
170
|
determine a specific channel. As a local channel has the name "local", the appropriate call is:
|
|
164
171
|
|
|
172
|
+
**Example**:
|
|
173
|
+
|
|
165
174
|
```python
|
|
166
175
|
service = service_manager.get_service(TestService, preferred_channel="local")
|
|
167
176
|
```
|
|
168
177
|
|
|
178
|
+
The default can be set globally with the method `set_preferred_channel(channel: str)`
|
|
179
|
+
|
|
180
|
+
Injecting services is also possible via the decorator `@inject_service(preferred_channel=""")`
|
|
181
|
+
|
|
182
|
+
**Example**:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
@inject_service()
|
|
186
|
+
def set_service(self, service: TestService)
|
|
187
|
+
self.service = service
|
|
188
|
+
```
|
|
189
|
+
|
|
169
190
|
## Features
|
|
170
191
|
|
|
171
192
|
The library offers:
|
|
@@ -348,12 +369,14 @@ Channels implement the possible transport layer protocols. In the sense of a dyn
|
|
|
348
369
|
Several channels are implemented:
|
|
349
370
|
|
|
350
371
|
- `dispatch-json`
|
|
351
|
-
channel that
|
|
372
|
+
channel that posts generic `Request` objects via a `invoke` POST-call
|
|
352
373
|
- `dispatch-msgpack`
|
|
353
|
-
channel that
|
|
374
|
+
channel that posts generic `Request` objects via a `invoke` POST-call after packing the json with msgpack
|
|
354
375
|
- `rest`
|
|
355
376
|
channel that executes regular rest-calls as defined by a couple of decorators.
|
|
356
377
|
|
|
378
|
+
The `dispatch`channels have the big advantage, that you don`t have to deal with additional http decorators!
|
|
379
|
+
|
|
357
380
|
All channels react on changed URLs as provided by the component registry.
|
|
358
381
|
|
|
359
382
|
A so called `URLSelector` is used internally to provide URLs for every single call. Two subclasses exist that offer a different logic
|
|
@@ -361,7 +384,7 @@ A so called `URLSelector` is used internally to provide URLs for every single ca
|
|
|
361
384
|
- `FirstURLSelector` always returns the first URL of the list of possible URLs
|
|
362
385
|
- `RoundRobinURLSelector` switches sequentially between all URLs.
|
|
363
386
|
|
|
364
|
-
To customize the behavior, an around advice can be implemented easily:
|
|
387
|
+
To customize the behavior, an `around` advice can be implemented easily:
|
|
365
388
|
|
|
366
389
|
**Example**:
|
|
367
390
|
|
|
@@ -478,6 +501,14 @@ class Module():
|
|
|
478
501
|
This setup will also expose all service interfaces decorated with the corresponding http decorators!
|
|
479
502
|
No need to add any FastAPI decorators, since the mapping is already done internally!
|
|
480
503
|
|
|
504
|
+
## Session
|
|
505
|
+
|
|
506
|
+
TODO
|
|
507
|
+
|
|
508
|
+
## Authorization
|
|
509
|
+
|
|
510
|
+
TODO
|
|
511
|
+
|
|
481
512
|
## Implementing Channels
|
|
482
513
|
|
|
483
514
|
To implement a new channel, you only need to derive from one of the possible base classes ( `Channel` or `HTTPXChannel` that already has a `httpx` client)
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
- [Rest Calls](#rest-calls)
|
|
25
25
|
- [Intercepting calls](#intercepting-calls)
|
|
26
26
|
- [FastAPI server](#fastapi-server)
|
|
27
|
+
- [Session](#session)
|
|
28
|
+
- [Authorization](#authorization)
|
|
27
29
|
- [Implementing Channels](#implementing-channels)
|
|
28
30
|
- [Version History](#version-history)
|
|
29
31
|
|
|
@@ -34,19 +36,23 @@ that lets you deploy, discover and call services with different remoting protoco
|
|
|
34
36
|
|
|
35
37
|
The basic design consists of four different concepts:
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
**Service**
|
|
40
|
+
|
|
38
41
|
defines a group of methods that can be called either locally or remotely.
|
|
39
42
|
These methods represent the functional interface exposed to clients — similar to an interface in traditional programming
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
**Component**
|
|
45
|
+
|
|
42
46
|
a component bundles one or more services and declares the channels (protocols) used to expose them.
|
|
43
47
|
Think of a component as a deployment unit or module.
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
**Component Registry**
|
|
50
|
+
|
|
46
51
|
acts as the central directory for managing available components.
|
|
47
52
|
It allows the framework to register, discover, and resolve components and their services.
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
**Channel**
|
|
55
|
+
|
|
50
56
|
is a pluggable transport layer that defines how service method invocations are transmitted and handled.
|
|
51
57
|
|
|
52
58
|
Let's look at the "interface" layer first.
|
|
@@ -75,7 +81,7 @@ class Module:
|
|
|
75
81
|
|
|
76
82
|
@create()
|
|
77
83
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
78
|
-
return ConsulComponentRegistry(Server.port, "
|
|
84
|
+
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
79
85
|
|
|
80
86
|
environment = Environment(Module)
|
|
81
87
|
service_manager = environment.get(ServiceManager)
|
|
@@ -126,10 +132,24 @@ environment = server.boot(Module)
|
|
|
126
132
|
Of course, service can also be called locally. In case of multiple possible channels, a keyword argument is used to
|
|
127
133
|
determine a specific channel. As a local channel has the name "local", the appropriate call is:
|
|
128
134
|
|
|
135
|
+
**Example**:
|
|
136
|
+
|
|
129
137
|
```python
|
|
130
138
|
service = service_manager.get_service(TestService, preferred_channel="local")
|
|
131
139
|
```
|
|
132
140
|
|
|
141
|
+
The default can be set globally with the method `set_preferred_channel(channel: str)`
|
|
142
|
+
|
|
143
|
+
Injecting services is also possible via the decorator `@inject_service(preferred_channel=""")`
|
|
144
|
+
|
|
145
|
+
**Example**:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
@inject_service()
|
|
149
|
+
def set_service(self, service: TestService)
|
|
150
|
+
self.service = service
|
|
151
|
+
```
|
|
152
|
+
|
|
133
153
|
## Features
|
|
134
154
|
|
|
135
155
|
The library offers:
|
|
@@ -312,12 +332,14 @@ Channels implement the possible transport layer protocols. In the sense of a dyn
|
|
|
312
332
|
Several channels are implemented:
|
|
313
333
|
|
|
314
334
|
- `dispatch-json`
|
|
315
|
-
channel that
|
|
335
|
+
channel that posts generic `Request` objects via a `invoke` POST-call
|
|
316
336
|
- `dispatch-msgpack`
|
|
317
|
-
channel that
|
|
337
|
+
channel that posts generic `Request` objects via a `invoke` POST-call after packing the json with msgpack
|
|
318
338
|
- `rest`
|
|
319
339
|
channel that executes regular rest-calls as defined by a couple of decorators.
|
|
320
340
|
|
|
341
|
+
The `dispatch`channels have the big advantage, that you don`t have to deal with additional http decorators!
|
|
342
|
+
|
|
321
343
|
All channels react on changed URLs as provided by the component registry.
|
|
322
344
|
|
|
323
345
|
A so called `URLSelector` is used internally to provide URLs for every single call. Two subclasses exist that offer a different logic
|
|
@@ -325,7 +347,7 @@ A so called `URLSelector` is used internally to provide URLs for every single ca
|
|
|
325
347
|
- `FirstURLSelector` always returns the first URL of the list of possible URLs
|
|
326
348
|
- `RoundRobinURLSelector` switches sequentially between all URLs.
|
|
327
349
|
|
|
328
|
-
To customize the behavior, an around advice can be implemented easily:
|
|
350
|
+
To customize the behavior, an `around` advice can be implemented easily:
|
|
329
351
|
|
|
330
352
|
**Example**:
|
|
331
353
|
|
|
@@ -442,6 +464,14 @@ class Module():
|
|
|
442
464
|
This setup will also expose all service interfaces decorated with the corresponding http decorators!
|
|
443
465
|
No need to add any FastAPI decorators, since the mapping is already done internally!
|
|
444
466
|
|
|
467
|
+
## Session
|
|
468
|
+
|
|
469
|
+
TODO
|
|
470
|
+
|
|
471
|
+
## Authorization
|
|
472
|
+
|
|
473
|
+
TODO
|
|
474
|
+
|
|
445
475
|
## Implementing Channels
|
|
446
476
|
|
|
447
477
|
To implement a new channel, you only need to derive from one of the possible base classes ( `Channel` or `HTTPXChannel` that already has a `httpx` client)
|
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from aspyx.di import module
|
|
11
|
+
|
|
12
|
+
from aspyx_service import ServiceModule, delete, post, put, get, rest, Body
|
|
13
|
+
from aspyx_service.service import component, Component, Service, service
|
|
14
|
+
|
|
15
|
+
class Pydantic(BaseModel):
|
|
16
|
+
i: int
|
|
17
|
+
f: float
|
|
18
|
+
b: bool
|
|
19
|
+
s: str
|
|
20
|
+
|
|
21
|
+
str0 : str
|
|
22
|
+
str1: str
|
|
23
|
+
str2: str
|
|
24
|
+
str3: str
|
|
25
|
+
str4: str
|
|
26
|
+
str5: str
|
|
27
|
+
str6: str
|
|
28
|
+
str7: str
|
|
29
|
+
str8: str
|
|
30
|
+
str9: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Data:
|
|
35
|
+
i: int
|
|
36
|
+
f: float
|
|
37
|
+
b: bool
|
|
38
|
+
s: str
|
|
39
|
+
|
|
40
|
+
str0: str
|
|
41
|
+
str1: str
|
|
42
|
+
str2: str
|
|
43
|
+
str3: str
|
|
44
|
+
str4: str
|
|
45
|
+
str5: str
|
|
46
|
+
str6: str
|
|
47
|
+
str7: str
|
|
48
|
+
str8: str
|
|
49
|
+
str9: str
|
|
50
|
+
|
|
51
|
+
class PydanticAndData(BaseModel):
|
|
52
|
+
p: Pydantic
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class DataAndPydantic:
|
|
56
|
+
d: Data
|
|
57
|
+
|
|
58
|
+
# service
|
|
59
|
+
|
|
60
|
+
@service(name="test-service", description="cool")
|
|
61
|
+
class TestService(Service):
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def hello(self, message: str) -> str:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def throw(self, message: str) -> str:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def data(self, data: Data) -> Data:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def pydantic(self, data: Pydantic) -> Pydantic:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@service(name="test-async-service", description="cool")
|
|
79
|
+
class TestAsyncService(Service):
|
|
80
|
+
@abstractmethod
|
|
81
|
+
async def hello(self, message: str) -> str:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
async def data(self, data: Data) -> Data:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
async def pydantic(self, data: Pydantic) -> Pydantic:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@service(name="test-rest-service", description="cool")
|
|
93
|
+
@rest("/api")
|
|
94
|
+
class TestRestService(Service):
|
|
95
|
+
@abstractmethod
|
|
96
|
+
@get("/hello/{message}")
|
|
97
|
+
def get(self, message: str) -> str:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@put("/hello/{message}")
|
|
101
|
+
def put(self, message: str) -> str:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@post("/hello/{message}")
|
|
105
|
+
def post_pydantic(self, message: str, data: Body(Pydantic)) -> Pydantic:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@post("/hello/{message}")
|
|
109
|
+
def post_data(self, message: str, data: Body(Data)) -> Data:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@delete("/hello/{message}")
|
|
113
|
+
def delete(self, message: str) -> str:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@service(name="test-async-rest-service", description="cool")
|
|
117
|
+
@rest("/async-api")
|
|
118
|
+
class TestAsyncRestService(Service):
|
|
119
|
+
@abstractmethod
|
|
120
|
+
@get("/hello/{message}")
|
|
121
|
+
async def get(self, message: str) -> str:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
@put("/hello/{message}")
|
|
125
|
+
async def put(self, message: str) -> str:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@post("/hello/{message}")
|
|
129
|
+
async def post_pydantic(self, message: str, data: Body(Pydantic)) -> Pydantic:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@post("/hello/{message}")
|
|
133
|
+
async def post_data(self, message: str, data: Body(Data)) -> Data:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
@delete("/hello/{message}")
|
|
137
|
+
async def delete(self, message: str) -> str:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
@component(services =[
|
|
141
|
+
TestService,
|
|
142
|
+
TestAsyncService,
|
|
143
|
+
TestRestService,
|
|
144
|
+
TestAsyncRestService
|
|
145
|
+
])
|
|
146
|
+
class TestComponent(Component): # pylint: disable=abstract-method
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
# module
|
|
150
|
+
|
|
151
|
+
@module(imports=[ServiceModule])
|
|
152
|
+
class ClientModule:
|
|
153
|
+
def __init__(self):
|
|
154
|
+
pass
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from aspyx.util import Logger
|
|
5
|
+
from aspyx_service import FastAPIServer
|
|
6
|
+
from server import ServerModule
|
|
7
|
+
|
|
8
|
+
Logger.configure(default_level=logging.DEBUG, levels={
|
|
9
|
+
"httpx": logging.ERROR,
|
|
10
|
+
"aspyx.di": logging.ERROR,
|
|
11
|
+
"aspyx.di.aop": logging.ERROR,
|
|
12
|
+
"aspyx.service": logging.ERROR
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
PORT = int(os.getenv("FAST_API_PORT", 8000))
|
|
16
|
+
|
|
17
|
+
FastAPIServer.boot(module=ServerModule, host="0.0.0.0", port=PORT, start = False)
|
|
18
|
+
|
|
19
|
+
app = FastAPIServer.fast_api
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
import uvicorn
|
|
23
|
+
uvicorn.run("main:app", host="0.0.0.0", port=PORT, reload=True, log_level="warning", access_log=False)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from typing import Callable, TypeVar, Type, Awaitable, Any, Dict, cast
|
|
10
|
+
|
|
11
|
+
from consul import Consul
|
|
12
|
+
|
|
13
|
+
from aspyx.di import module, Environment, create
|
|
14
|
+
from aspyx.di.aop import advice, around, methods, Invocation
|
|
15
|
+
from aspyx.util import Logger
|
|
16
|
+
|
|
17
|
+
from aspyx_service import ConsulComponentRegistry
|
|
18
|
+
from aspyx_service.service import ServiceManager, ComponentRegistry, Channel
|
|
19
|
+
|
|
20
|
+
Logger.configure(default_level=logging.DEBUG, levels={
|
|
21
|
+
"httpx": logging.ERROR,
|
|
22
|
+
"aspyx.di": logging.ERROR,
|
|
23
|
+
"aspyx.di.aop": logging.ERROR,
|
|
24
|
+
"aspyx.service": logging.ERROR
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
from client import TestService, TestRestService, Pydantic, Data, TestAsyncRestService, TestAsyncService, ClientModule
|
|
28
|
+
|
|
29
|
+
# main
|
|
30
|
+
|
|
31
|
+
@advice
|
|
32
|
+
class ChannelAdvice:
|
|
33
|
+
def __init__(self):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@around(methods().named("customize").of_type(Channel))
|
|
37
|
+
def customize_channel(self, invocation: Invocation):
|
|
38
|
+
channel = cast(Channel, invocation.args[0])
|
|
39
|
+
|
|
40
|
+
channel.select_round_robin() # or select_first_url()
|
|
41
|
+
|
|
42
|
+
return invocation.proceed()
|
|
43
|
+
|
|
44
|
+
@module(imports=[ClientModule])
|
|
45
|
+
class TestModule:
|
|
46
|
+
def __init__(self):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@create()
|
|
50
|
+
def create_registry(self) -> ComponentRegistry:
|
|
51
|
+
return ConsulComponentRegistry(port=8000, consul=Consul(host="localhost", port=8500))
|
|
52
|
+
|
|
53
|
+
def boot() -> ServiceManager:
|
|
54
|
+
environment = Environment(TestModule)
|
|
55
|
+
|
|
56
|
+
service_manager = environment.get(ServiceManager)
|
|
57
|
+
|
|
58
|
+
return service_manager
|
|
59
|
+
|
|
60
|
+
T = TypeVar("T")
|
|
61
|
+
|
|
62
|
+
# main
|
|
63
|
+
|
|
64
|
+
lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
|
65
|
+
|
|
66
|
+
pydantic = Pydantic(i=1, f=1.0, b=True, s="s",
|
|
67
|
+
str0=lorem_ipsum,
|
|
68
|
+
str1=lorem_ipsum,
|
|
69
|
+
str2=lorem_ipsum,
|
|
70
|
+
str3=lorem_ipsum,
|
|
71
|
+
str4=lorem_ipsum,
|
|
72
|
+
str5=lorem_ipsum,
|
|
73
|
+
str6=lorem_ipsum,
|
|
74
|
+
str7=lorem_ipsum,
|
|
75
|
+
str8=lorem_ipsum,
|
|
76
|
+
str9=lorem_ipsum
|
|
77
|
+
)
|
|
78
|
+
data = Data(i=1, f=1.0, b=True, s="s",
|
|
79
|
+
str0=lorem_ipsum,
|
|
80
|
+
str1=lorem_ipsum,
|
|
81
|
+
str2=lorem_ipsum,
|
|
82
|
+
str3=lorem_ipsum,
|
|
83
|
+
str4=lorem_ipsum,
|
|
84
|
+
str5=lorem_ipsum,
|
|
85
|
+
str6=lorem_ipsum,
|
|
86
|
+
str7=lorem_ipsum,
|
|
87
|
+
str8=lorem_ipsum,
|
|
88
|
+
str9=lorem_ipsum
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def run_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], None]):
|
|
92
|
+
start = time.perf_counter()
|
|
93
|
+
for _ in range(loops):
|
|
94
|
+
callable(instance)
|
|
95
|
+
|
|
96
|
+
end = time.perf_counter()
|
|
97
|
+
avg_ms = ((end - start) / loops) * 1000
|
|
98
|
+
|
|
99
|
+
print(f"run {name}, loops={loops}: avg={avg_ms:.3f} ms")
|
|
100
|
+
|
|
101
|
+
async def run_async_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], Awaitable[Any]]):
|
|
102
|
+
start = time.perf_counter()
|
|
103
|
+
for _ in range(loops):
|
|
104
|
+
await callable(instance)
|
|
105
|
+
|
|
106
|
+
end = time.perf_counter()
|
|
107
|
+
avg_ms = ((end - start) / loops) * 1000
|
|
108
|
+
|
|
109
|
+
print(f"run {name}, loops={loops}: avg={avg_ms:.3f} ms")
|
|
110
|
+
|
|
111
|
+
def run_threaded_async_loops(name: str, loops: int, n_threads: int, type: Type[T], instance: T, callable: Callable[[T], Awaitable[Any]]):
|
|
112
|
+
threads = []
|
|
113
|
+
|
|
114
|
+
def worker(thread_id: int):
|
|
115
|
+
#print(f"worker {thread_id} running on thread {threading.current_thread().name}")
|
|
116
|
+
|
|
117
|
+
loop = asyncio.new_event_loop()
|
|
118
|
+
asyncio.set_event_loop(loop)
|
|
119
|
+
|
|
120
|
+
async def run():
|
|
121
|
+
for i in range(loops):
|
|
122
|
+
await callable(instance)
|
|
123
|
+
|
|
124
|
+
loop.run_until_complete(run())
|
|
125
|
+
loop.close()
|
|
126
|
+
|
|
127
|
+
start = time.perf_counter()
|
|
128
|
+
|
|
129
|
+
for t_id in range(0, n_threads):
|
|
130
|
+
thread = threading.Thread(target=worker, args=(t_id,))
|
|
131
|
+
threads.append(thread)
|
|
132
|
+
thread.start()
|
|
133
|
+
|
|
134
|
+
for thread in threads:
|
|
135
|
+
thread.join()
|
|
136
|
+
|
|
137
|
+
end = time.perf_counter()
|
|
138
|
+
took = (end - start) * 1000
|
|
139
|
+
avg_ms = ((end - start) / (n_threads * loops)) * 1000
|
|
140
|
+
|
|
141
|
+
print(f"{name} {loops} in {n_threads} threads: {took} ms, avg: {avg_ms}ms")
|
|
142
|
+
|
|
143
|
+
def run_threaded_sync_loops(name: str, loops: int, n_threads: int, type: Type[T], instance: T, callable: Callable[[T], Any]):
|
|
144
|
+
threads = []
|
|
145
|
+
|
|
146
|
+
def worker(thread_id: int):
|
|
147
|
+
#print(f"worker {thread_id} running on thread {threading.current_thread().name}")
|
|
148
|
+
|
|
149
|
+
loop = asyncio.new_event_loop()
|
|
150
|
+
asyncio.set_event_loop(loop)
|
|
151
|
+
|
|
152
|
+
async def run():
|
|
153
|
+
for i in range(loops):
|
|
154
|
+
callable(instance)
|
|
155
|
+
|
|
156
|
+
loop.run_until_complete(run())
|
|
157
|
+
loop.close()
|
|
158
|
+
|
|
159
|
+
start = time.perf_counter()
|
|
160
|
+
|
|
161
|
+
for t_id in range(0, n_threads):
|
|
162
|
+
thread = threading.Thread(target=worker, args=(t_id,))
|
|
163
|
+
threads.append(thread)
|
|
164
|
+
thread.start()
|
|
165
|
+
|
|
166
|
+
for thread in threads:
|
|
167
|
+
thread.join()
|
|
168
|
+
|
|
169
|
+
end = time.perf_counter()
|
|
170
|
+
took = (end - start) * 1000
|
|
171
|
+
avg_ms = ((end - start) / (n_threads * loops)) * 1000
|
|
172
|
+
|
|
173
|
+
print(f"{name} {loops} in {n_threads} threads: {took} ms, avg: {avg_ms}ms")
|
|
174
|
+
|
|
175
|
+
manager = boot()
|
|
176
|
+
|
|
177
|
+
async def main():
|
|
178
|
+
print("start tests...")
|
|
179
|
+
|
|
180
|
+
# get service manager
|
|
181
|
+
|
|
182
|
+
loops = 1000
|
|
183
|
+
|
|
184
|
+
# tests
|
|
185
|
+
|
|
186
|
+
run_loops("rest", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"), lambda service: service.get("world"))
|
|
187
|
+
run_loops("json", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-json"), lambda service: service.hello("world"))
|
|
188
|
+
run_loops("msgpack", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-msgpack"), lambda service: service.hello("world"))
|
|
189
|
+
|
|
190
|
+
# pydantic
|
|
191
|
+
|
|
192
|
+
run_loops("rest & pydantic", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"), lambda service: service.post_pydantic("hello", pydantic))
|
|
193
|
+
run_loops("json & pydantic", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-json"), lambda service: service.pydantic(pydantic))
|
|
194
|
+
run_loops("msgpack & pydantic", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-msgpack"), lambda service: service.pydantic(pydantic))
|
|
195
|
+
|
|
196
|
+
# data class
|
|
197
|
+
|
|
198
|
+
run_loops("rest & data", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"),
|
|
199
|
+
lambda service: service.post_data("hello", data))
|
|
200
|
+
run_loops("json & data", loops, TestService,
|
|
201
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
202
|
+
lambda service: service.data(data))
|
|
203
|
+
run_loops("msgpack & data", loops, TestService,
|
|
204
|
+
manager.get_service(TestService, preferred_channel="dispatch-msgpack"),
|
|
205
|
+
lambda service: service.data(data))
|
|
206
|
+
|
|
207
|
+
# async
|
|
208
|
+
|
|
209
|
+
await run_async_loops("async rest", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
210
|
+
lambda service: service.get("world"))
|
|
211
|
+
await run_async_loops("async json", loops, TestAsyncService, manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
212
|
+
lambda service: service.hello("world"))
|
|
213
|
+
await run_async_loops("async msgpack", loops, TestAsyncService, manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
214
|
+
lambda service: service.hello("world"))
|
|
215
|
+
|
|
216
|
+
# pydantic
|
|
217
|
+
|
|
218
|
+
await run_async_loops("async rest & pydantic", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
219
|
+
lambda service: service.post_pydantic("hello", pydantic))
|
|
220
|
+
await run_async_loops("async json & pydantic", loops, TestAsyncService,
|
|
221
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
222
|
+
lambda service: service.pydantic(pydantic))
|
|
223
|
+
await run_async_loops("async msgpack & pydantic", loops, TestAsyncService,
|
|
224
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
225
|
+
lambda service: service.pydantic(pydantic))
|
|
226
|
+
|
|
227
|
+
# data class
|
|
228
|
+
|
|
229
|
+
# pydantic
|
|
230
|
+
|
|
231
|
+
await run_async_loops("async rest & data", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
232
|
+
lambda service: service.post_data("hello", data))
|
|
233
|
+
await run_async_loops("async json & data", loops, TestAsyncService,
|
|
234
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
235
|
+
lambda service: service.data(data))
|
|
236
|
+
await run_async_loops("async msgpack & data", loops, TestAsyncService,
|
|
237
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
238
|
+
lambda service: service.data(data))
|
|
239
|
+
|
|
240
|
+
# a real thread test
|
|
241
|
+
|
|
242
|
+
# sync
|
|
243
|
+
|
|
244
|
+
run_threaded_sync_loops("threaded sync json, 1 thread", loops, 1, TestService,
|
|
245
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
246
|
+
lambda service: service.hello("world"))
|
|
247
|
+
run_threaded_sync_loops("threaded sync json, 2 thread", loops, 2, TestService,
|
|
248
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
249
|
+
lambda service: service.hello("world"))
|
|
250
|
+
run_threaded_sync_loops("threaded sync json, 4 thread", loops, 4, TestService,
|
|
251
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
252
|
+
lambda service: service.hello("world"))
|
|
253
|
+
run_threaded_sync_loops("threaded sync json, 8 thread", loops, 8, TestService,
|
|
254
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
255
|
+
lambda service: service.hello("world"))
|
|
256
|
+
run_threaded_sync_loops("threaded sync json, 16 thread", loops, 16, TestService,
|
|
257
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
258
|
+
lambda service: service.hello("world"))
|
|
259
|
+
|
|
260
|
+
# async
|
|
261
|
+
|
|
262
|
+
run_threaded_async_loops("threaded async json, 1 thread", loops, 1, TestAsyncService,
|
|
263
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
264
|
+
lambda service: service.hello("world") )
|
|
265
|
+
run_threaded_async_loops("threaded async json, 2 thread", loops, 2, TestAsyncService,
|
|
266
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
267
|
+
lambda service: service.hello("world"))
|
|
268
|
+
run_threaded_async_loops("threaded async json, 4 thread", loops, 4, TestAsyncService,
|
|
269
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
270
|
+
lambda service: service.hello("world"))
|
|
271
|
+
run_threaded_async_loops("threaded async json, 8 thread", loops, 8, TestAsyncService,
|
|
272
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
273
|
+
lambda service: service.hello("world"))
|
|
274
|
+
run_threaded_async_loops("threaded async json, 16 thread", loops, 16, TestAsyncService,
|
|
275
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
276
|
+
lambda service: service.hello("world"))
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
asyncio.run(main())
|