aspyx-service 0.10.3__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.3 → aspyx_service-0.10.4}/PKG-INFO +31 -4
- {aspyx_service-0.10.3 → aspyx_service-0.10.4}/README.md +28 -2
- 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.3/performance_tests → aspyx_service-0.10.4/performance-test}/performance-test.py +86 -12
- 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.3 → aspyx_service-0.10.4}/pyproject.toml +13 -2
- {aspyx_service-0.10.3 → 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.3 → aspyx_service-0.10.4}/src/aspyx_service/healthcheck.py +1 -1
- {aspyx_service-0.10.3 → aspyx_service-0.10.4}/src/aspyx_service/registries.py +5 -5
- {aspyx_service-0.10.3 → aspyx_service-0.10.4}/src/aspyx_service/restchannel.py +15 -18
- {aspyx_service-0.10.3 → aspyx_service-0.10.4}/src/aspyx_service/serialization.py +3 -3
- {aspyx_service-0.10.3 → aspyx_service-0.10.4}/src/aspyx_service/server.py +139 -69
- {aspyx_service-0.10.3 → aspyx_service-0.10.4}/src/aspyx_service/service.py +47 -12
- 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.3 → aspyx_service-0.10.4}/tests/common.py +139 -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.3 → aspyx_service-0.10.4}/tests/test_serialization.py +8 -6
- aspyx_service-0.10.4/tests/test_service.py +80 -0
- aspyx_service-0.10.3/src/aspyx_service/channels.py +0 -282
- aspyx_service-0.10.3/tests/test_async_service.py +0 -56
- aspyx_service-0.10.3/tests/test_service.py +0 -82
- {aspyx_service-0.10.3 → aspyx_service-0.10.4}/.gitignore +0 -0
- {aspyx_service-0.10.3 → aspyx_service-0.10.4}/LICENSE +0 -0
- {aspyx_service-0.10.3/tests → aspyx_service-0.10.4/performance-test}/__init__.py +0 -0
- {aspyx_service-0.10.3/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
|
|
|
@@ -166,10 +169,24 @@ environment = server.boot(Module)
|
|
|
166
169
|
Of course, service can also be called locally. In case of multiple possible channels, a keyword argument is used to
|
|
167
170
|
determine a specific channel. As a local channel has the name "local", the appropriate call is:
|
|
168
171
|
|
|
172
|
+
**Example**:
|
|
173
|
+
|
|
169
174
|
```python
|
|
170
175
|
service = service_manager.get_service(TestService, preferred_channel="local")
|
|
171
176
|
```
|
|
172
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
|
+
|
|
173
190
|
## Features
|
|
174
191
|
|
|
175
192
|
The library offers:
|
|
@@ -352,12 +369,14 @@ Channels implement the possible transport layer protocols. In the sense of a dyn
|
|
|
352
369
|
Several channels are implemented:
|
|
353
370
|
|
|
354
371
|
- `dispatch-json`
|
|
355
|
-
channel that
|
|
372
|
+
channel that posts generic `Request` objects via a `invoke` POST-call
|
|
356
373
|
- `dispatch-msgpack`
|
|
357
|
-
channel that
|
|
374
|
+
channel that posts generic `Request` objects via a `invoke` POST-call after packing the json with msgpack
|
|
358
375
|
- `rest`
|
|
359
376
|
channel that executes regular rest-calls as defined by a couple of decorators.
|
|
360
377
|
|
|
378
|
+
The `dispatch`channels have the big advantage, that you don`t have to deal with additional http decorators!
|
|
379
|
+
|
|
361
380
|
All channels react on changed URLs as provided by the component registry.
|
|
362
381
|
|
|
363
382
|
A so called `URLSelector` is used internally to provide URLs for every single call. Two subclasses exist that offer a different logic
|
|
@@ -482,6 +501,14 @@ class Module():
|
|
|
482
501
|
This setup will also expose all service interfaces decorated with the corresponding http decorators!
|
|
483
502
|
No need to add any FastAPI decorators, since the mapping is already done internally!
|
|
484
503
|
|
|
504
|
+
## Session
|
|
505
|
+
|
|
506
|
+
TODO
|
|
507
|
+
|
|
508
|
+
## Authorization
|
|
509
|
+
|
|
510
|
+
TODO
|
|
511
|
+
|
|
485
512
|
## Implementing Channels
|
|
486
513
|
|
|
487
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
|
|
|
@@ -130,10 +132,24 @@ environment = server.boot(Module)
|
|
|
130
132
|
Of course, service can also be called locally. In case of multiple possible channels, a keyword argument is used to
|
|
131
133
|
determine a specific channel. As a local channel has the name "local", the appropriate call is:
|
|
132
134
|
|
|
135
|
+
**Example**:
|
|
136
|
+
|
|
133
137
|
```python
|
|
134
138
|
service = service_manager.get_service(TestService, preferred_channel="local")
|
|
135
139
|
```
|
|
136
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
|
+
|
|
137
153
|
## Features
|
|
138
154
|
|
|
139
155
|
The library offers:
|
|
@@ -316,12 +332,14 @@ Channels implement the possible transport layer protocols. In the sense of a dyn
|
|
|
316
332
|
Several channels are implemented:
|
|
317
333
|
|
|
318
334
|
- `dispatch-json`
|
|
319
|
-
channel that
|
|
335
|
+
channel that posts generic `Request` objects via a `invoke` POST-call
|
|
320
336
|
- `dispatch-msgpack`
|
|
321
|
-
channel that
|
|
337
|
+
channel that posts generic `Request` objects via a `invoke` POST-call after packing the json with msgpack
|
|
322
338
|
- `rest`
|
|
323
339
|
channel that executes regular rest-calls as defined by a couple of decorators.
|
|
324
340
|
|
|
341
|
+
The `dispatch`channels have the big advantage, that you don`t have to deal with additional http decorators!
|
|
342
|
+
|
|
325
343
|
All channels react on changed URLs as provided by the component registry.
|
|
326
344
|
|
|
327
345
|
A so called `URLSelector` is used internally to provide URLs for every single call. Two subclasses exist that offer a different logic
|
|
@@ -446,6 +464,14 @@ class Module():
|
|
|
446
464
|
This setup will also expose all service interfaces decorated with the corresponding http decorators!
|
|
447
465
|
No need to add any FastAPI decorators, since the mapping is already done internally!
|
|
448
466
|
|
|
467
|
+
## Session
|
|
468
|
+
|
|
469
|
+
TODO
|
|
470
|
+
|
|
471
|
+
## Authorization
|
|
472
|
+
|
|
473
|
+
TODO
|
|
474
|
+
|
|
449
475
|
## Implementing Channels
|
|
450
476
|
|
|
451
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)
|
|
@@ -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)
|
{aspyx_service-0.10.3/performance_tests → aspyx_service-0.10.4/performance-test}/performance-test.py
RENAMED
|
@@ -2,22 +2,91 @@
|
|
|
2
2
|
Tests
|
|
3
3
|
"""
|
|
4
4
|
import asyncio
|
|
5
|
-
import logging
|
|
6
5
|
import threading
|
|
7
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()
|
|
8
41
|
|
|
9
|
-
|
|
42
|
+
return invocation.proceed()
|
|
10
43
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
14
59
|
|
|
15
60
|
T = TypeVar("T")
|
|
16
61
|
|
|
17
62
|
# main
|
|
18
63
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
)
|
|
21
90
|
|
|
22
91
|
def run_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], None]):
|
|
23
92
|
start = time.perf_counter()
|
|
@@ -43,6 +112,8 @@ def run_threaded_async_loops(name: str, loops: int, n_threads: int, type: Type[
|
|
|
43
112
|
threads = []
|
|
44
113
|
|
|
45
114
|
def worker(thread_id: int):
|
|
115
|
+
#print(f"worker {thread_id} running on thread {threading.current_thread().name}")
|
|
116
|
+
|
|
46
117
|
loop = asyncio.new_event_loop()
|
|
47
118
|
asyncio.set_event_loop(loop)
|
|
48
119
|
|
|
@@ -73,6 +144,8 @@ def run_threaded_sync_loops(name: str, loops: int, n_threads: int, type: Type[T
|
|
|
73
144
|
threads = []
|
|
74
145
|
|
|
75
146
|
def worker(thread_id: int):
|
|
147
|
+
#print(f"worker {thread_id} running on thread {threading.current_thread().name}")
|
|
148
|
+
|
|
76
149
|
loop = asyncio.new_event_loop()
|
|
77
150
|
asyncio.set_event_loop(loop)
|
|
78
151
|
|
|
@@ -99,10 +172,13 @@ def run_threaded_sync_loops(name: str, loops: int, n_threads: int, type: Type[T
|
|
|
99
172
|
|
|
100
173
|
print(f"{name} {loops} in {n_threads} threads: {took} ms, avg: {avg_ms}ms")
|
|
101
174
|
|
|
175
|
+
manager = boot()
|
|
176
|
+
|
|
102
177
|
async def main():
|
|
178
|
+
print("start tests...")
|
|
179
|
+
|
|
103
180
|
# get service manager
|
|
104
181
|
|
|
105
|
-
manager = service_manager()
|
|
106
182
|
loops = 1000
|
|
107
183
|
|
|
108
184
|
# tests
|
|
@@ -166,7 +242,7 @@ async def main():
|
|
|
166
242
|
# sync
|
|
167
243
|
|
|
168
244
|
run_threaded_sync_loops("threaded sync json, 1 thread", loops, 1, TestService,
|
|
169
|
-
manager.get_service(
|
|
245
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
170
246
|
lambda service: service.hello("world"))
|
|
171
247
|
run_threaded_sync_loops("threaded sync json, 2 thread", loops, 2, TestService,
|
|
172
248
|
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
@@ -199,7 +275,5 @@ async def main():
|
|
|
199
275
|
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
200
276
|
lambda service: service.hello("world"))
|
|
201
277
|
|
|
202
|
-
|
|
203
278
|
if __name__ == "__main__":
|
|
204
|
-
asyncio.run(main())
|
|
205
|
-
|
|
279
|
+
asyncio.run(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 --reload --log-level warning
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from consul import Consul
|
|
8
|
+
|
|
9
|
+
from aspyx.di import on_running, module, create
|
|
10
|
+
from aspyx.di.aop import Invocation, advice, error
|
|
11
|
+
from aspyx.exception import handle, ExceptionManager
|
|
12
|
+
from aspyx_service import HealthCheckManager, ServiceModule, ConsulComponentRegistry
|
|
13
|
+
from aspyx_service.service import ChannelAddress, Server, \
|
|
14
|
+
component_services, AbstractComponent, implementation, health, ComponentRegistry
|
|
15
|
+
|
|
16
|
+
from client import ClientModule, TestService, TestAsyncService, Data, Pydantic, TestRestService, TestAsyncRestService, TestComponent
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# implementation classes
|
|
20
|
+
|
|
21
|
+
@implementation()
|
|
22
|
+
class TestServiceImpl(TestService):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def hello(self, message: str) -> str:
|
|
27
|
+
return message
|
|
28
|
+
|
|
29
|
+
def throw(self, message: str) -> str:
|
|
30
|
+
raise Exception(message)
|
|
31
|
+
|
|
32
|
+
def data(self, data: Data) -> Data:
|
|
33
|
+
return data
|
|
34
|
+
|
|
35
|
+
def pydantic(self, data: Pydantic) -> Pydantic:
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
@implementation()
|
|
39
|
+
class TestAsyncServiceImpl(TestAsyncService):
|
|
40
|
+
def __init__(self):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
async def hello(self, message: str) -> str:
|
|
44
|
+
return message
|
|
45
|
+
|
|
46
|
+
async def data(self, data: Data) -> Data:
|
|
47
|
+
return data
|
|
48
|
+
|
|
49
|
+
async def pydantic(self, data: Pydantic) -> Pydantic:
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
@implementation()
|
|
53
|
+
class TestRestServiceImpl(TestRestService):
|
|
54
|
+
def __init__(self):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def get(self, message: str) -> str:
|
|
58
|
+
return message
|
|
59
|
+
|
|
60
|
+
def put(self, message: str) -> str:
|
|
61
|
+
return message
|
|
62
|
+
|
|
63
|
+
def post_pydantic(self, message: str, data: Pydantic) -> Pydantic:
|
|
64
|
+
return data
|
|
65
|
+
|
|
66
|
+
def post_data(self, message: str, data: Data) -> Data:
|
|
67
|
+
return data
|
|
68
|
+
|
|
69
|
+
def delete(self, message: str) -> str:
|
|
70
|
+
return message
|
|
71
|
+
|
|
72
|
+
@implementation()
|
|
73
|
+
class TestAsyncRestServiceImpl(TestAsyncRestService):
|
|
74
|
+
def __init__(self):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
async def get(self, message: str) -> str:
|
|
78
|
+
return message
|
|
79
|
+
|
|
80
|
+
async def put(self, message: str) -> str:
|
|
81
|
+
return message
|
|
82
|
+
|
|
83
|
+
async def post_pydantic(self, message: str, data: Pydantic) -> Pydantic:
|
|
84
|
+
return data
|
|
85
|
+
|
|
86
|
+
async def post_data(self, message: str, data: Data) -> Data:
|
|
87
|
+
return data
|
|
88
|
+
|
|
89
|
+
async def delete(self, message: str) -> str:
|
|
90
|
+
return message
|
|
91
|
+
|
|
92
|
+
@implementation()
|
|
93
|
+
@health("/health")
|
|
94
|
+
@advice
|
|
95
|
+
class TestComponentImpl(AbstractComponent, TestComponent):
|
|
96
|
+
# constructor
|
|
97
|
+
|
|
98
|
+
def __init__(self):
|
|
99
|
+
super().__init__()
|
|
100
|
+
|
|
101
|
+
self.health_check_manager : Optional[HealthCheckManager] = None
|
|
102
|
+
self.exception_manager = ExceptionManager()
|
|
103
|
+
|
|
104
|
+
# exception handler
|
|
105
|
+
|
|
106
|
+
@handle()
|
|
107
|
+
def handle_exception(self, exception: Exception):
|
|
108
|
+
print("caught exception!")
|
|
109
|
+
return exception
|
|
110
|
+
|
|
111
|
+
# aspects
|
|
112
|
+
|
|
113
|
+
@error(component_services(TestComponent))
|
|
114
|
+
def catch(self, invocation: Invocation):
|
|
115
|
+
return self.exception_manager.handle(invocation.exception)
|
|
116
|
+
|
|
117
|
+
# lifecycle
|
|
118
|
+
|
|
119
|
+
@on_running()
|
|
120
|
+
def setup_exception_handlers(self):
|
|
121
|
+
self.exception_manager.collect_handlers(self)
|
|
122
|
+
|
|
123
|
+
# implement
|
|
124
|
+
|
|
125
|
+
async def get_health(self) -> HealthCheckManager.Health:
|
|
126
|
+
return HealthCheckManager.Health()
|
|
127
|
+
|
|
128
|
+
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
129
|
+
return [
|
|
130
|
+
ChannelAddress("rest", f"http://{Server.get_local_ip()}:{port}"),
|
|
131
|
+
ChannelAddress("dispatch-json", f"http://{Server.get_local_ip()}:{port}"),
|
|
132
|
+
ChannelAddress("dispatch-msgpack", f"http://{Server.get_local_ip()}:{port}")
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
def startup(self) -> None:
|
|
136
|
+
print("### startup")
|
|
137
|
+
|
|
138
|
+
def shutdown(self) -> None:
|
|
139
|
+
print("### shutdown")
|
|
140
|
+
|
|
141
|
+
# module
|
|
142
|
+
|
|
143
|
+
@module(imports=[ClientModule, ServiceModule])
|
|
144
|
+
class ServerModule:
|
|
145
|
+
def __init__(self):
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
#@create()
|
|
149
|
+
#def create_yaml_source(self) -> YamlConfigurationSource:
|
|
150
|
+
# return YamlConfigurationSource(f"{Path(__file__).parent}/config.yaml")
|
|
151
|
+
|
|
152
|
+
@create()
|
|
153
|
+
def create_registry(self) -> ComponentRegistry:
|
|
154
|
+
return ConsulComponentRegistry(port=int(os.getenv("FAST_API_PORT", 8000)), consul=Consul(host="localhost", port=8500))
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[project]
|
|
4
4
|
name = "aspyx_service"
|
|
5
|
-
version = "0.10.
|
|
5
|
+
version = "0.10.4"
|
|
6
6
|
description = "Aspyx Service framework"
|
|
7
7
|
authors = [{ name = "Andreas Ernst", email = "andreas.ernst7@gmail.com" }]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
license = { file = "LICENSE" }
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"aspyx>=1.5.
|
|
12
|
+
"aspyx>=1.5.3",
|
|
13
13
|
"python-consul2~=0.1.5",
|
|
14
14
|
"fastapi~=0.115.13",
|
|
15
15
|
"httpx~=0.28.1",
|
|
16
16
|
"msgpack~=1.1.1",
|
|
17
|
+
"cachetools~= 5.5.2",
|
|
17
18
|
"uvicorn[standard]"
|
|
18
19
|
]
|
|
19
20
|
|
|
@@ -27,5 +28,15 @@ source = "src"
|
|
|
27
28
|
[tool.hatch.build.targets.wheel]
|
|
28
29
|
packages = ["src/aspyx_service"]
|
|
29
30
|
|
|
31
|
+
[tool.hatch.envs.test]
|
|
32
|
+
dependencies = [
|
|
33
|
+
".",
|
|
34
|
+
"pytest",
|
|
35
|
+
"pytest-cov",
|
|
36
|
+
"pytest-asyncio",
|
|
37
|
+
"anyio",
|
|
38
|
+
"PyJWT"
|
|
39
|
+
]
|
|
40
|
+
|
|
30
41
|
[tool.hatch.metadata]
|
|
31
42
|
allow-direct-references = true
|