Skip to content

Commit ada53dc

Browse files
committed
add blocking service mixin
1 parent 8605c02 commit ada53dc

9 files changed

+536
-110
lines changed

.pre-commit-config.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,8 @@ repos:
4949
name: Check with ruff
5050
entry: ruff
5151
args: ["check", "--fix", "."]
52+
53+
- <<: *python-linters
54+
id: mypy
55+
name: Validate types with MyPy
56+
entry: mypy

history.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# x.x.x (xx-xx-xxxx)
22

3+
# 0.10.0 (xx-xx-xxxx)
4+
- add `BlockingServiceMixin` with subset of functionality from `ServiceMixin` that can be used in non-async code
5+
- add `AsyncioServiceMixin` as an alias to `ServiceMixin`
6+
- `ServiceMixin` is now deprecated and will be removed in 1.0.0
7+
- add mypy type hints
8+
39
# 0.9.1 (11-01-2022)
410
- prevent multiple calls to `set_exception`
511

pyproject.toml

+15
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dev = [
3030
"pre-commit",
3131
"black",
3232
"ruff",
33+
"mypy",
3334
]
3435

3536
[build-system]
@@ -67,3 +68,17 @@ log_format = "%(asctime)s.%(msecs)03d %(name)-20s %(levelname)-8s %(filename)-15
6768
log_date_format = "%H:%M:%S"
6869
log_level = "DEBUG"
6970
asyncio_mode = "strict"
71+
72+
[tool.mypy]
73+
files = "src/facet"
74+
strict = true
75+
ignore_missing_imports = true
76+
allow_subclassing_any = true
77+
allow_untyped_calls = true
78+
pretty = true
79+
show_error_codes = true
80+
implicit_reexport = true
81+
allow_untyped_decorators = true
82+
warn_unused_ignores = false
83+
warn_return_any = false
84+
namespace_packages = true

readme.md

+164-65
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,80 @@
11
# Facet
2+
Service manager for asyncio (and classic blocking code since version 0.10.0).
3+
24
[![Github actions status for master branch](https://github.com/pohmelie/facet/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/pohmelie/facet/actions/workflows/ci.yaml)
35
[![Codecov coverage for master branch](https://codecov.io/gh/pohmelie/facet/branch/master/graph/badge.svg)](https://codecov.io/gh/pohmelie/facet)
46
[![Pypi version](https://img.shields.io/pypi/v/facet.svg)](https://pypi.org/project/facet/)
57
[![Pypi downloads count](https://img.shields.io/pypi/dm/facet)](https://pypi.org/project/facet/)
68

7-
Service manager for asyncio.
8-
9-
# Reason
9+
- [Facet](#facet)
10+
- [Reasons](#reasons)
11+
- [Asyncio](#asyncio)
12+
- [Blocking code](#blocking-code)
13+
- [Features](#features)
14+
- [License](#license)
15+
- [Requirements](#requirements)
16+
- [Usage](#usage)
17+
- [Asyncio](#asyncio-1)
18+
- [Blocking code](#blocking-code-1)
19+
- [API](#api)
20+
- [Asyncio](#asyncio-2)
21+
- [`start`](#start)
22+
- [`stop`](#stop)
23+
- [`dependencies`](#dependencies)
24+
- [`add_task`](#add_task)
25+
- [`run`](#run)
26+
- [`wait`](#wait)
27+
- [`graceful_shutdown_timeout`](#graceful_shutdown_timeout)
28+
- [`running`](#running)
29+
- [Blocking code](#blocking-code-2)
30+
- [`start`](#start-1)
31+
- [`stop`](#stop-1)
32+
- [`dependencies`](#dependencies-1)
33+
- [`running`](#running-1)
34+
35+
## Reasons
36+
### Asyncio
1037
[`mode`](https://github.com/ask/mode) tries to do too much job:
1138
- Messy callbacks (`on_start`, `on_started`, `on_crashed`, etc.).
1239
- Inheritance restrict naming and forces `super()` calls.
1340
- Forced logging module and logging configuration.
41+
### Blocking code
42+
- [`ExitStack`](https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack) is too low-level to manage services.
43+
- Common api for async and blocking worlds.
1444

15-
# Features
45+
## Features
1646
- Simple (`start`, `stop`, `dependencies` and `add_task`).
1747
- Configurable via inheritance (graceful shutdown timeout).
1848
- Mixin (no `super()` required).
19-
- Requires no runner engine (`Worker`, `Runner`, etc.) just plain `await` or `async with`.
49+
- Requires no runner engine (`Worker`, `Runner`, etc.) just plain `await` or `async with`/`with`.
2050

21-
# License
51+
## License
2252
`facet` is offered under MIT license.
2353

24-
# Requirements
25-
* python 3.6+
26-
27-
# Usage
28-
``` python
29-
import asyncio
30-
import logging
54+
## Requirements
55+
- python 3.11+
3156

32-
from facet import ServiceMixin
57+
## Usage
3358

59+
### Asyncio
3460

35-
class B(ServiceMixin):
61+
``` python
62+
import asyncio
63+
from facet import AsyncioServiceMixin
3664

65+
class B(AsyncioServiceMixin):
3766
def __init__(self):
3867
self.value = 0
3968

4069
async def start(self):
4170
self.value += 1
42-
logging.info("b started")
71+
print("b started")
4372

4473
async def stop(self):
4574
self.value -= 1
46-
logging.info("b stopped")
47-
48-
49-
class A(ServiceMixin):
75+
print("b stopped")
5076

77+
class A(AsyncioServiceMixin):
5178
def __init__(self):
5279
self.b = B()
5380

@@ -56,26 +83,24 @@ class A(ServiceMixin):
5683
return [self.b]
5784

5885
async def start(self):
59-
logging.info("a started")
86+
print("a started")
6087

6188
async def stop(self):
62-
logging.info("a stopped")
89+
print("a stopped")
6390

64-
65-
logging.basicConfig(level=logging.DEBUG)
6691
asyncio.run(A().run())
6792
```
6893
This will produce:
6994
```
70-
INFO:root:b started
71-
INFO:root:a started
95+
b started
96+
a started
7297
```
7398
Start and stop order determined by strict rule: **dependencies must be started first and stopped last**. That is why `B` starts before `A`. Since `A` may use `B` in `start` routine.
7499

75100
Hit `ctrl-c` and you will see:
76101
```
77-
INFO:root:a stopped
78-
INFO:root:b stopped
102+
a stopped
103+
b stopped
79104
Traceback (most recent call last):
80105
...
81106
KeyboardInterrupt
@@ -98,33 +123,29 @@ asyncio.run(main())
98123

99124
Another service feature is `add_task` method:
100125
``` python
101-
class A(ServiceMixin):
102-
126+
class A(AsyncioServiceMixin):
103127
async def task(self):
104128
await asyncio.sleep(1)
105-
logging.info("task done")
129+
print("task done")
106130

107131
async def start(self):
108132
self.add_task(self.task())
109-
logging.info("start done")
133+
print("start done")
110134

111-
112-
logging.basicConfig(level=logging.DEBUG)
113135
asyncio.run(A().run())
114136
```
115137
This will lead to background task creation and handling:
116138
```
117-
INFO:root:start done
118-
INFO:root:task done
139+
start done
140+
task done
119141
```
120142
Any non-handled exception on background task will lead the whole service stack crashed. This is also a key feature to fall down fast and loud.
121143

122144
All background tasks will be cancelled and awaited on service stop.
123145

124146
You can manage dependencies start/stop to start sequently, parallel or mixed. Like this:
125147
``` python
126-
class A(ServiceMixin):
127-
148+
class A(AsyncioServiceMixin):
128149
def __init__(self):
129150
self.b = B()
130151
self.c = C()
@@ -141,65 +162,143 @@ This leads to first `b` and `c` starts parallel, after they successfully started
141162

142163
The rule here is **first nesting level is sequential, second nesting level is parallel**
143164

144-
# API
165+
### Blocking code
166+
Since version 0.10.0 `facet` can be used in blocking code with pretty same rules. **But with limited API**. For example:
167+
``` python
168+
from facet import BlockingServiceMixin
169+
170+
class B(BlockingServiceMixin):
171+
def __init__(self):
172+
self.value = 0
173+
174+
def start(self):
175+
self.value += 1
176+
print("b started")
177+
178+
def stop(self):
179+
self.value -= 1
180+
print("b stopped")
181+
182+
class A(BlockingServiceMixin):
183+
def __init__(self):
184+
self.b = B()
185+
186+
@property
187+
def dependencies(self):
188+
return [self.b]
189+
190+
def start(self):
191+
print("a started")
192+
193+
def stop(self):
194+
print("a stopped")
195+
196+
with A() as a:
197+
assert a.b.value == 1
198+
```
199+
This will produce:
200+
```
201+
b started
202+
a started
203+
a stopped
204+
b stopped
205+
```
206+
As you can see, there is no `wait` method. Waiting and background tasks are on user shoulders and technically can be implemented with `concurrent.futures` module. But `facet` do not provide such functionality, since there are a lot of ways to do it: `threading`/`multiprocessing` and their primitives.
207+
208+
Also, there are no «sequential, parallel and mixed starts/stops for dependencies» feature. So, just put dependencies in `dependencies` property as a plain `list` and they will be started/stopped sequentially.
209+
210+
## API
211+
### Asyncio
145212
Here is public methods you get on inheritance/mixin:
146-
## `wait`
213+
214+
#### `start`
147215
``` python
148-
async def wait(self):
216+
async def start(self):
217+
pass
149218
```
150-
Wait for service stop. Service must be started. This is useful when you use service as a context manager.
219+
Start routine.
151220

152-
## `run`
221+
#### `stop`
153222
``` python
154-
async def run(self):
223+
async def stop(self):
224+
pass
225+
```
226+
Stop routine.
227+
228+
#### `dependencies`
229+
``` python
230+
@property
231+
def dependencies(self) -> list[AsyncioServiceMixin | list[AsyncioServiceMixin]]:
232+
return []
233+
```
234+
Should return iterable of current service dependencies instances.
235+
236+
#### `add_task`
237+
``` python
238+
def add_task(self, coroutine: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
239+
```
240+
Add background task.
241+
242+
#### `run`
243+
``` python
244+
async def run(self) -> None:
155245
```
156246
Run service and wait until it stop.
157247

158-
## `graceful_shutdown_timeout`
248+
#### `wait`
249+
``` python
250+
async def wait(self) -> None:
251+
```
252+
Wait for service stop. Service must be started. This is useful when you use service as a context manager.
253+
254+
#### `graceful_shutdown_timeout`
159255
``` python
160256
@property
161-
def graceful_shutdown_timeout(self):
257+
def graceful_shutdown_timeout(self) -> int:
162258
return 10
163259
```
164260
How much total time in seconds wait for stop routines. This property can be overriden with subclass:
165261
``` python
166-
class CustomServiceMixin(ServiceMixin):
262+
class CustomServiceMixin(AsyncioServiceMixin):
167263
@property
168264
def graceful_shutdown_timeout(self):
169265
return 60
170266
```
171267

172-
## `dependencies`
173-
``` python
174-
@property
175-
def dependencies(self):
176-
return []
177-
```
178-
Should return iterable of current service dependencies instances.
179-
180-
## `running`
268+
#### `running`
181269
``` python
182270
@property
183271
def running(self) -> bool:
184272
```
185273
Check if service is running
186274

187-
## `add_task`
188-
``` python
189-
def add_task(self, coro) -> asyncio.Task:
190-
```
191-
Add background task.
275+
### Blocking code
192276

193-
## `start`
277+
#### `start`
194278
``` python
195-
async def start(self):
279+
def start(self):
196280
pass
197281
```
198282
Start routine.
199283

200-
## `stop`
284+
#### `stop`
201285
``` python
202-
async def stop(self):
286+
def stop(self):
203287
pass
204288
```
205289
Stop routine.
290+
291+
#### `dependencies`
292+
``` python
293+
@property
294+
def dependencies(self) -> list[BlockingServiceMixin | list[BlockingServiceMixin]]:
295+
return []
296+
```
297+
Should return iterable of current service dependencies instances.
298+
299+
#### `running`
300+
``` python
301+
@property
302+
def running(self) -> bool:
303+
```
304+
Check if service is running

0 commit comments

Comments
 (0)