pavao 0.10.1__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.
- pavao-0.10.1/LICENSE +21 -0
- pavao-0.10.1/PKG-INFO +159 -0
- pavao-0.10.1/README.md +125 -0
- pavao-0.10.1/pavao/__init__.py +25 -0
- pavao-0.10.1/pavao/__main__.py +5 -0
- pavao-0.10.1/pavao/broadcasting.py +59 -0
- pavao-0.10.1/pavao/builtin/foundation/sqlite/persisted_settings.py +51 -0
- pavao-0.10.1/pavao/builtin/foundation/sqlite/supplier.py +103 -0
- pavao-0.10.1/pavao/builtin/foundation/sqlite/task_store.py +201 -0
- pavao-0.10.1/pavao/config.py +171 -0
- pavao-0.10.1/pavao/daemon.py +128 -0
- pavao-0.10.1/pavao/endpoint.py +6 -0
- pavao-0.10.1/pavao/exception.py +4 -0
- pavao-0.10.1/pavao/filter.py +75 -0
- pavao-0.10.1/pavao/http/__init__.py +6 -0
- pavao-0.10.1/pavao/http/cookie.py +172 -0
- pavao-0.10.1/pavao/http/handler.py +200 -0
- pavao-0.10.1/pavao/http/request.py +174 -0
- pavao-0.10.1/pavao/http/response.py +90 -0
- pavao-0.10.1/pavao/i18n.py +154 -0
- pavao-0.10.1/pavao/json.py +322 -0
- pavao-0.10.1/pavao/lifespan/__init__.py +4 -0
- pavao-0.10.1/pavao/lifespan/handler.py +56 -0
- pavao-0.10.1/pavao/loader.py +243 -0
- pavao-0.10.1/pavao/logger.py +102 -0
- pavao-0.10.1/pavao/parameters.py +135 -0
- pavao-0.10.1/pavao/persistence.py +77 -0
- pavao-0.10.1/pavao/pkg.py +381 -0
- pavao-0.10.1/pavao/predicates.py +10 -0
- pavao-0.10.1/pavao/refresher.py +225 -0
- pavao-0.10.1/pavao/registry.py +263 -0
- pavao-0.10.1/pavao/run.py +70 -0
- pavao-0.10.1/pavao/scheduler.py +190 -0
- pavao-0.10.1/pavao/server.py +485 -0
- pavao-0.10.1/pavao/sse.py +183 -0
- pavao-0.10.1/pavao/supplier.py +274 -0
- pavao-0.10.1/pavao/task.py +245 -0
- pavao-0.10.1/pavao/utils.py +134 -0
- pavao-0.10.1/pavao.egg-info/PKG-INFO +159 -0
- pavao-0.10.1/pavao.egg-info/SOURCES.txt +66 -0
- pavao-0.10.1/pavao.egg-info/dependency_links.txt +1 -0
- pavao-0.10.1/pavao.egg-info/entry_points.txt +3 -0
- pavao-0.10.1/pavao.egg-info/requires.txt +13 -0
- pavao-0.10.1/pavao.egg-info/top_level.txt +1 -0
- pavao-0.10.1/pyproject.toml +61 -0
- pavao-0.10.1/setup.cfg +4 -0
- pavao-0.10.1/tests/test_broadcasting.py +113 -0
- pavao-0.10.1/tests/test_config.py +260 -0
- pavao-0.10.1/tests/test_cookie.py +134 -0
- pavao-0.10.1/tests/test_daemon.py +353 -0
- pavao-0.10.1/tests/test_filter.py +179 -0
- pavao-0.10.1/tests/test_handler.py +493 -0
- pavao-0.10.1/tests/test_i18n.py +284 -0
- pavao-0.10.1/tests/test_json.py +486 -0
- pavao-0.10.1/tests/test_lifespan.py +200 -0
- pavao-0.10.1/tests/test_loader.py +380 -0
- pavao-0.10.1/tests/test_logger.py +138 -0
- pavao-0.10.1/tests/test_parameters.py +210 -0
- pavao-0.10.1/tests/test_refresher.py +531 -0
- pavao-0.10.1/tests/test_registry.py +323 -0
- pavao-0.10.1/tests/test_request.py +288 -0
- pavao-0.10.1/tests/test_response.py +183 -0
- pavao-0.10.1/tests/test_scheduler.py +537 -0
- pavao-0.10.1/tests/test_server.py +1067 -0
- pavao-0.10.1/tests/test_sse.py +184 -0
- pavao-0.10.1/tests/test_supplier.py +411 -0
- pavao-0.10.1/tests/test_task.py +450 -0
- pavao-0.10.1/tests/test_utils.py +166 -0
pavao-0.10.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Gustavo Venturini
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pavao-0.10.1/PKG-INFO
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pavao
|
|
3
|
+
Version: 0.10.1
|
|
4
|
+
Summary: Modular, injection based, Python ASGI web framework
|
|
5
|
+
Author-email: Gustavo Venturini <gustavo.c.venturini@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vottini/pavao
|
|
8
|
+
Keywords: framework,asgi,api,web,rest,http,dependency-injection,server-sent-events
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
16
|
+
Classifier: Framework :: AsyncIO
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: aiosqlite==0.20.0
|
|
21
|
+
Requires-Dist: click==8.1.7
|
|
22
|
+
Requires-Dist: croniter==3.0.3
|
|
23
|
+
Requires-Dist: h11==0.14.0
|
|
24
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
25
|
+
Requires-Dist: pytz==2024.2
|
|
26
|
+
Requires-Dist: PyYAML==6.0.2
|
|
27
|
+
Requires-Dist: six==1.16.0
|
|
28
|
+
Requires-Dist: typing_extensions==4.12.2
|
|
29
|
+
Requires-Dist: uvicorn==0.30.6
|
|
30
|
+
Requires-Dist: watchdog==4.0.2
|
|
31
|
+
Requires-Dist: jsonbp==1.3.1
|
|
32
|
+
Requires-Dist: umapper==0.1.6
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# pavao
|
|
37
|
+
|
|
38
|
+
[](http://pavao.readthedocs.io/?badge=latest)
|
|
39
|
+
[](https://codecov.io/github/vottini/pavao)
|
|
40
|
+
|
|
41
|
+
**pavao** (from "Pavão", Portuguese for *peacock*) is a Python ASGI framework built
|
|
42
|
+
on two ideas that work together.
|
|
43
|
+
|
|
44
|
+
One is **modularity**: point pavao at a directory of Python files and it discovers and
|
|
45
|
+
serves every endpoint module it finds there. Adding a new endpoint means dropping a file
|
|
46
|
+
into the directory; removing one means deleting it. Nothing else in the application needs
|
|
47
|
+
to change.
|
|
48
|
+
|
|
49
|
+
The other is **name-based dependency injection**: instead of importing or constructing the
|
|
50
|
+
objects your endpoint needs, you just declare them as arguments to your handler function.
|
|
51
|
+
pavao uses the names of the parameters to deliver the correct objects where they are requested.
|
|
52
|
+
Parameters are instantiated on demand — if a module does not declare `db_connection`,
|
|
53
|
+
no database connection is created for it.
|
|
54
|
+
|
|
55
|
+
A minimal endpoint:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
def process(response):
|
|
59
|
+
response.set_plain_text("World")
|
|
60
|
+
|
|
61
|
+
def pavao_endpoint():
|
|
62
|
+
return {
|
|
63
|
+
'method': 'GET',
|
|
64
|
+
'path': '/hello',
|
|
65
|
+
'processor': process
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
$ python -m venv .venv
|
|
73
|
+
$ source .venv/bin/activate
|
|
74
|
+
$ pip install pavao
|
|
75
|
+
$ mkdir modules
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Save the example above as `modules/hello.py`, then:
|
|
79
|
+
|
|
80
|
+
```console
|
|
81
|
+
$ pavao-run modules/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Open `http://localhost:8000/hello` and you should see `World`.
|
|
85
|
+
|
|
86
|
+
## Going further
|
|
87
|
+
|
|
88
|
+
Both pillars extend naturally as an application grows.
|
|
89
|
+
|
|
90
|
+
On the modularity side, modules can be organised into subdirectories, share helper code through plain Python imports, and be hot-reloaded on change without restarting the server. Because each endpoint lives in its own file, the reload boundary is exactly the unit of work: edit one handler, save, and only that module is reloaded — the rest of the application keeps running. Alongside endpoint modules, the same directory can contain scheduled task modules (`pavao_task()`) and always-on background service modules (`pavao_service()`).
|
|
91
|
+
|
|
92
|
+
On the injection side, the built-in injectables — `request`, `response`, `path_variables`, `broadcasting`, and others — cover the common cases. The set is open: custom suppliers placed in a foundation directory can introduce new named parameters, making application-specific resources (database connections, caches, auth tokens) available to any module that declares them.
|
|
93
|
+
|
|
94
|
+
Crucially, injection applies to descriptor functions too, not only to processors. A module can declare a dependency in its `pavao_endpoint()` function and use it to decide whether to register at all, or to configure the endpoint based on runtime state. Each function asks for exactly what it needs — nothing is shared implicitly.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import pavao
|
|
98
|
+
|
|
99
|
+
delete_refresh = pavao.include('queries/delete_refresh.sql')
|
|
100
|
+
|
|
101
|
+
async def process(response, db_connection, auth_data):
|
|
102
|
+
row = await db_connection.fetchrow(
|
|
103
|
+
"SELECT id FROM pavao.refresh_token WHERE account = $1",
|
|
104
|
+
auth_data.account_id)
|
|
105
|
+
|
|
106
|
+
if row is None:
|
|
107
|
+
response.set_status(404)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
await db_connection.execute(
|
|
111
|
+
delete_refresh,
|
|
112
|
+
auth_data.account_id,
|
|
113
|
+
auth_data.token)
|
|
114
|
+
|
|
115
|
+
response.add_cookie(
|
|
116
|
+
pavao.http.Cookie('token')
|
|
117
|
+
.set_path('/refresh-token')
|
|
118
|
+
.set_sameSite(pavao.http.SameSite.STRICT)
|
|
119
|
+
.set_to_be_removed()
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
response.set_status(204)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def pavao_endpoint(loaded_settings):
|
|
126
|
+
if not loaded_settings.get_setting('api.v1.enabled', True):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
'method': 'DELETE',
|
|
131
|
+
'path': '/v1/logout',
|
|
132
|
+
'processor': process
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The descriptor receives `loaded_settings` and checks a configuration flag — if the v1 API is disabled the module is quietly skipped. Otherwise the endpoint is registered, and its processor independently declares `response`, `db_connection`, and `auth_data`. Each function asks for exactly what it needs at the time it needs it.
|
|
137
|
+
|
|
138
|
+
## Features
|
|
139
|
+
|
|
140
|
+
- Point-and-serve module directory: endpoints, tasks, and services auto-discovered at startup
|
|
141
|
+
- Dependency injection by parameter name across all modules
|
|
142
|
+
- Typed URL path variables and URL query parameters
|
|
143
|
+
- Schema-based YAML configuration with environment variable substitution
|
|
144
|
+
- Endpoint access filtering with full DI support
|
|
145
|
+
- Asynchronous inter-module communication bus
|
|
146
|
+
- Scheduled tasks (cron-style) and always-on background services
|
|
147
|
+
- Typed JSON parsing and serialization via [jsonbp](https://github.com/vottini/jsonbp)
|
|
148
|
+
- Server-Sent Events (SSE) with heartbeat and disconnection support
|
|
149
|
+
- Hot-reloading of modules on file change
|
|
150
|
+
- Response internationalization
|
|
151
|
+
- Extension management via `pavao-pkg`
|
|
152
|
+
|
|
153
|
+
## Documentation
|
|
154
|
+
|
|
155
|
+
Full documentation is available at [vottini.github.io/pavao](https://vottini.github.io/pavao).
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
pavao-0.10.1/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
|
|
2
|
+
# pavao
|
|
3
|
+
|
|
4
|
+
[](http://pavao.readthedocs.io/?badge=latest)
|
|
5
|
+
[](https://codecov.io/github/vottini/pavao)
|
|
6
|
+
|
|
7
|
+
**pavao** (from "Pavão", Portuguese for *peacock*) is a Python ASGI framework built
|
|
8
|
+
on two ideas that work together.
|
|
9
|
+
|
|
10
|
+
One is **modularity**: point pavao at a directory of Python files and it discovers and
|
|
11
|
+
serves every endpoint module it finds there. Adding a new endpoint means dropping a file
|
|
12
|
+
into the directory; removing one means deleting it. Nothing else in the application needs
|
|
13
|
+
to change.
|
|
14
|
+
|
|
15
|
+
The other is **name-based dependency injection**: instead of importing or constructing the
|
|
16
|
+
objects your endpoint needs, you just declare them as arguments to your handler function.
|
|
17
|
+
pavao uses the names of the parameters to deliver the correct objects where they are requested.
|
|
18
|
+
Parameters are instantiated on demand — if a module does not declare `db_connection`,
|
|
19
|
+
no database connection is created for it.
|
|
20
|
+
|
|
21
|
+
A minimal endpoint:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
def process(response):
|
|
25
|
+
response.set_plain_text("World")
|
|
26
|
+
|
|
27
|
+
def pavao_endpoint():
|
|
28
|
+
return {
|
|
29
|
+
'method': 'GET',
|
|
30
|
+
'path': '/hello',
|
|
31
|
+
'processor': process
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
$ python -m venv .venv
|
|
39
|
+
$ source .venv/bin/activate
|
|
40
|
+
$ pip install pavao
|
|
41
|
+
$ mkdir modules
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Save the example above as `modules/hello.py`, then:
|
|
45
|
+
|
|
46
|
+
```console
|
|
47
|
+
$ pavao-run modules/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Open `http://localhost:8000/hello` and you should see `World`.
|
|
51
|
+
|
|
52
|
+
## Going further
|
|
53
|
+
|
|
54
|
+
Both pillars extend naturally as an application grows.
|
|
55
|
+
|
|
56
|
+
On the modularity side, modules can be organised into subdirectories, share helper code through plain Python imports, and be hot-reloaded on change without restarting the server. Because each endpoint lives in its own file, the reload boundary is exactly the unit of work: edit one handler, save, and only that module is reloaded — the rest of the application keeps running. Alongside endpoint modules, the same directory can contain scheduled task modules (`pavao_task()`) and always-on background service modules (`pavao_service()`).
|
|
57
|
+
|
|
58
|
+
On the injection side, the built-in injectables — `request`, `response`, `path_variables`, `broadcasting`, and others — cover the common cases. The set is open: custom suppliers placed in a foundation directory can introduce new named parameters, making application-specific resources (database connections, caches, auth tokens) available to any module that declares them.
|
|
59
|
+
|
|
60
|
+
Crucially, injection applies to descriptor functions too, not only to processors. A module can declare a dependency in its `pavao_endpoint()` function and use it to decide whether to register at all, or to configure the endpoint based on runtime state. Each function asks for exactly what it needs — nothing is shared implicitly.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import pavao
|
|
64
|
+
|
|
65
|
+
delete_refresh = pavao.include('queries/delete_refresh.sql')
|
|
66
|
+
|
|
67
|
+
async def process(response, db_connection, auth_data):
|
|
68
|
+
row = await db_connection.fetchrow(
|
|
69
|
+
"SELECT id FROM pavao.refresh_token WHERE account = $1",
|
|
70
|
+
auth_data.account_id)
|
|
71
|
+
|
|
72
|
+
if row is None:
|
|
73
|
+
response.set_status(404)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
await db_connection.execute(
|
|
77
|
+
delete_refresh,
|
|
78
|
+
auth_data.account_id,
|
|
79
|
+
auth_data.token)
|
|
80
|
+
|
|
81
|
+
response.add_cookie(
|
|
82
|
+
pavao.http.Cookie('token')
|
|
83
|
+
.set_path('/refresh-token')
|
|
84
|
+
.set_sameSite(pavao.http.SameSite.STRICT)
|
|
85
|
+
.set_to_be_removed()
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
response.set_status(204)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def pavao_endpoint(loaded_settings):
|
|
92
|
+
if not loaded_settings.get_setting('api.v1.enabled', True):
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
'method': 'DELETE',
|
|
97
|
+
'path': '/v1/logout',
|
|
98
|
+
'processor': process
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The descriptor receives `loaded_settings` and checks a configuration flag — if the v1 API is disabled the module is quietly skipped. Otherwise the endpoint is registered, and its processor independently declares `response`, `db_connection`, and `auth_data`. Each function asks for exactly what it needs at the time it needs it.
|
|
103
|
+
|
|
104
|
+
## Features
|
|
105
|
+
|
|
106
|
+
- Point-and-serve module directory: endpoints, tasks, and services auto-discovered at startup
|
|
107
|
+
- Dependency injection by parameter name across all modules
|
|
108
|
+
- Typed URL path variables and URL query parameters
|
|
109
|
+
- Schema-based YAML configuration with environment variable substitution
|
|
110
|
+
- Endpoint access filtering with full DI support
|
|
111
|
+
- Asynchronous inter-module communication bus
|
|
112
|
+
- Scheduled tasks (cron-style) and always-on background services
|
|
113
|
+
- Typed JSON parsing and serialization via [jsonbp](https://github.com/vottini/jsonbp)
|
|
114
|
+
- Server-Sent Events (SSE) with heartbeat and disconnection support
|
|
115
|
+
- Hot-reloading of modules on file change
|
|
116
|
+
- Response internationalization
|
|
117
|
+
- Extension management via `pavao-pkg`
|
|
118
|
+
|
|
119
|
+
## Documentation
|
|
120
|
+
|
|
121
|
+
Full documentation is available at [vottini.github.io/pavao](https://vottini.github.io/pavao).
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
from .exception import PavaoException
|
|
3
|
+
from .logger import Logger
|
|
4
|
+
from .utils import adapter, async_adapter, silent_generator
|
|
5
|
+
from .config import ConfigException, load_settings
|
|
6
|
+
from .filter import RequestFilter
|
|
7
|
+
from .supplier import ParameterSupplier
|
|
8
|
+
from .persistence import TaskStore, StoredSettings
|
|
9
|
+
from .i18n import Internationalization, ModuleLocalizer, LazyLocalizer
|
|
10
|
+
from .json import accepts_json, generates_json, error_blueprint, field_present
|
|
11
|
+
from .sse import generates_sse
|
|
12
|
+
from .parameters import accepts_parameters
|
|
13
|
+
from .loader import path_to, include, import_module, from_module
|
|
14
|
+
from .broadcasting import SHUTDOWN_TOPIC
|
|
15
|
+
from .scheduler import TaskProblem
|
|
16
|
+
from .server import Server
|
|
17
|
+
|
|
18
|
+
from umapper import register_mapping_class
|
|
19
|
+
from umapper import assemble_dicts
|
|
20
|
+
from umapper import convert_to_object
|
|
21
|
+
|
|
22
|
+
from . import predicates
|
|
23
|
+
from . import lifespan
|
|
24
|
+
from . import http
|
|
25
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
SHUTDOWN_TOPIC = "PAVAO.SHUTDOWN"
|
|
5
|
+
|
|
6
|
+
class Sink:
|
|
7
|
+
def __init__(self, topic):
|
|
8
|
+
# maxsize=0 means unbounded; set a positive value to apply backpressure
|
|
9
|
+
self.queue = asyncio.Queue()
|
|
10
|
+
self.topic = topic
|
|
11
|
+
|
|
12
|
+
async def get_message(self):
|
|
13
|
+
msg = await self.queue.get()
|
|
14
|
+
self.queue.task_done()
|
|
15
|
+
return msg
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Broadcaster:
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.topics = dict()
|
|
21
|
+
|
|
22
|
+
def open_sink(self, topic):
|
|
23
|
+
sink = Sink(topic)
|
|
24
|
+
topic_sinks = self.topics.get(topic, list())
|
|
25
|
+
topic_sinks.append(sink)
|
|
26
|
+
self.topics[topic] = topic_sinks
|
|
27
|
+
return sink
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def write_message(self, topic, message):
|
|
31
|
+
if not topic in self.topics: return
|
|
32
|
+
for sink in self.topics[topic]:
|
|
33
|
+
await sink.queue.put(message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def close_sink(self, sink):
|
|
37
|
+
topic_sinks = self.topics.get(sink.topic, list())
|
|
38
|
+
|
|
39
|
+
filtered_sinks = [ snk
|
|
40
|
+
for snk in topic_sinks
|
|
41
|
+
if snk is not sink
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
if len(filtered_sinks) == 0:
|
|
45
|
+
self.topics.pop(sink.topic, None)
|
|
46
|
+
|
|
47
|
+
else:
|
|
48
|
+
self.topics[sink.topic] = filtered_sinks
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def announce_shutdown(self):
|
|
52
|
+
await self.write_message(SHUTDOWN_TOPIC, None)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"SHUTDOWN_TOPIC",
|
|
57
|
+
"Broadcaster",
|
|
58
|
+
]
|
|
59
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
import re
|
|
3
|
+
import asyncio
|
|
4
|
+
import builtins
|
|
5
|
+
import collections
|
|
6
|
+
import pavao
|
|
7
|
+
|
|
8
|
+
from pavao.persistence import StoredSettings
|
|
9
|
+
|
|
10
|
+
get_setting = pavao.include("queries/get_setting.sql")
|
|
11
|
+
upsert_setting = pavao.include("queries/upsert_setting.sql")
|
|
12
|
+
list_pattern = re.compile(r"\[([^\]]+)\]")
|
|
13
|
+
|
|
14
|
+
class PersistedSettings(StoredSettings):
|
|
15
|
+
def __init__(self, internal_db):
|
|
16
|
+
self.internal_db = internal_db
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def get(self, setting):
|
|
20
|
+
async with self.internal_db.execute(get_setting, [setting]) as cursor:
|
|
21
|
+
row = await cursor.fetchone()
|
|
22
|
+
if row is None:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
typ, value = row
|
|
26
|
+
list_search = list_pattern.search(value)
|
|
27
|
+
ctor = getattr(builtins, typ)
|
|
28
|
+
|
|
29
|
+
if list_search is not None:
|
|
30
|
+
list_content = list_search.group(1)
|
|
31
|
+
return [ ctor(vle) for vle in list_content.split("::") ]
|
|
32
|
+
|
|
33
|
+
return ctor(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def set(self, setting, value):
|
|
37
|
+
value_type = type(value).__name__
|
|
38
|
+
original_value = value
|
|
39
|
+
|
|
40
|
+
if isinstance(value, collections.abc.Sequence) and not isinstance(value, str):
|
|
41
|
+
value_type = type(value[0]).__name__
|
|
42
|
+
str_values = map(str, value)
|
|
43
|
+
value = f"[{'::'.join(str_values)}]"
|
|
44
|
+
|
|
45
|
+
async with self.internal_db.cursor() as cursor:
|
|
46
|
+
await cursor.execute(upsert_setting, {
|
|
47
|
+
'name': setting,
|
|
48
|
+
'type': value_type,
|
|
49
|
+
'value': str(value)
|
|
50
|
+
})
|
|
51
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import asyncio
|
|
4
|
+
import datetime
|
|
5
|
+
import aiosqlite
|
|
6
|
+
import sqlite3
|
|
7
|
+
import umapper
|
|
8
|
+
import pavao
|
|
9
|
+
|
|
10
|
+
from pavao.builtin.foundation.sqlite.task_store import SqliteTaskStore
|
|
11
|
+
|
|
12
|
+
config_schema = {
|
|
13
|
+
"enabled": True,
|
|
14
|
+
"persist": True,
|
|
15
|
+
"filename": "pavao.db",
|
|
16
|
+
"trace": False
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
queries_dir = pavao.path_to('database')
|
|
20
|
+
db_creation = [arq for arq in os.listdir(queries_dir) if arq.endswith('.sql')]
|
|
21
|
+
db_creation.sort()
|
|
22
|
+
|
|
23
|
+
#-------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def convert_datetime(val):
|
|
26
|
+
return datetime.datetime.fromisoformat(
|
|
27
|
+
val.decode())
|
|
28
|
+
|
|
29
|
+
aiosqlite.register_adapter(datetime.datetime, lambda val: val.isoformat())
|
|
30
|
+
aiosqlite.register_adapter(bool, lambda val: '1' if val else '0')
|
|
31
|
+
aiosqlite.register_converter('timestamp', convert_datetime)
|
|
32
|
+
aiosqlite.register_converter('bool', lambda val: val.decode() == '1')
|
|
33
|
+
|
|
34
|
+
#-------------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
pavao.from_module('persisted_settings').import_item('PersistedSettings')
|
|
37
|
+
|
|
38
|
+
class SqliteSupplier(pavao.ParameterSupplier):
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self.connection = None
|
|
41
|
+
self.specs = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parameters_supplied(self):
|
|
45
|
+
if not self.specs.enabled:
|
|
46
|
+
return dict()
|
|
47
|
+
|
|
48
|
+
async def provide_task_store():
|
|
49
|
+
return SqliteTaskStore(self.connection)
|
|
50
|
+
|
|
51
|
+
async def provide_stored_settings():
|
|
52
|
+
return PersistedSettings(self.connection)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
'task_store': [provide_task_store, None],
|
|
56
|
+
'stored_settings': [provide_stored_settings, None]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def configure(self, loaded_settings):
|
|
61
|
+
db_settings = loaded_settings.get_setting(
|
|
62
|
+
"pavao.database.sqlite",
|
|
63
|
+
config_schema)
|
|
64
|
+
|
|
65
|
+
self.specs = umapper.convert_to_object(db_settings)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def initialize(self):
|
|
69
|
+
if not self.specs.enabled:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
if not self.specs.persist:
|
|
74
|
+
pavao.Logger.info("Pavao internal database set as not persistent")
|
|
75
|
+
db_name = ':memory:'
|
|
76
|
+
|
|
77
|
+
else: db_name = self.specs.filename
|
|
78
|
+
self.connection = await aiosqlite.connect(db_name,
|
|
79
|
+
detect_types=sqlite3.PARSE_DECLTYPES)
|
|
80
|
+
|
|
81
|
+
if self.specs.trace:
|
|
82
|
+
sqlite3.enable_callback_tracebacks(True)
|
|
83
|
+
await self.connection.set_trace_callback(print)
|
|
84
|
+
|
|
85
|
+
pavao.Logger.info("Pavao internal database connected")
|
|
86
|
+
|
|
87
|
+
for query in db_creation:
|
|
88
|
+
async with self.connection.cursor() as cursor:
|
|
89
|
+
with open(os.path.join(queries_dir, query)) as fd:
|
|
90
|
+
await cursor.executescript(fd.read())
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
pavao.Logger.error("Error while setting up internal database: " + str(e))
|
|
94
|
+
raise pavao.PavaoException("Unable to setup internal Pavao database")
|
|
95
|
+
|
|
96
|
+
#------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
async def shutdown(self):
|
|
99
|
+
if self.connection is not None:
|
|
100
|
+
await self.connection.close()
|
|
101
|
+
pavao.Logger.info("Pavao internal database connection closed")
|
|
102
|
+
self.connection = None
|
|
103
|
+
|