python-corekit 0.1.0__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.
- python_corekit-0.1.0/LICENSE +21 -0
- python_corekit-0.1.0/PKG-INFO +417 -0
- python_corekit-0.1.0/README.md +382 -0
- python_corekit-0.1.0/corekit/__init__.py +0 -0
- python_corekit-0.1.0/corekit/api/__init__.py +9 -0
- python_corekit-0.1.0/corekit/api/handler.py +76 -0
- python_corekit-0.1.0/corekit/api/responses.py +40 -0
- python_corekit-0.1.0/corekit/api/routers.py +115 -0
- python_corekit-0.1.0/corekit/concurrency/__init__.py +9 -0
- python_corekit-0.1.0/corekit/concurrency/decorators.py +72 -0
- python_corekit-0.1.0/corekit/concurrency/thread_local.py +99 -0
- python_corekit-0.1.0/corekit/concurrency/worker.py +65 -0
- python_corekit-0.1.0/corekit/config/__init__.py +47 -0
- python_corekit-0.1.0/corekit/config/loader.py +153 -0
- python_corekit-0.1.0/corekit/config/settings.py +161 -0
- python_corekit-0.1.0/corekit/config/sources.py +125 -0
- python_corekit-0.1.0/corekit/connections/__init__.py +31 -0
- python_corekit-0.1.0/corekit/connections/connectable.py +212 -0
- python_corekit-0.1.0/corekit/connections/decorators.py +92 -0
- python_corekit-0.1.0/corekit/connections/redis/__init__.py +7 -0
- python_corekit-0.1.0/corekit/connections/redis/connection.py +239 -0
- python_corekit-0.1.0/corekit/connections/registry.py +80 -0
- python_corekit-0.1.0/corekit/connections/sql/__init__.py +10 -0
- python_corekit-0.1.0/corekit/connections/sql/connection.py +342 -0
- python_corekit-0.1.0/corekit/connections/sql/fields/__init__.py +7 -0
- python_corekit-0.1.0/corekit/connections/sql/fields/jsonb.py +67 -0
- python_corekit-0.1.0/corekit/connections/sql/migration/__init__.py +57 -0
- python_corekit-0.1.0/corekit/connections/sql/migration/base.py +40 -0
- python_corekit-0.1.0/corekit/connections/sql/migration/operations.py +416 -0
- python_corekit-0.1.0/corekit/connections/sql/migration/registry.py +166 -0
- python_corekit-0.1.0/corekit/connections/sql/migration/table.py +27 -0
- python_corekit-0.1.0/corekit/connections/sql/query.py +68 -0
- python_corekit-0.1.0/corekit/connections/sql/table.py +96 -0
- python_corekit-0.1.0/corekit/constants.py +45 -0
- python_corekit-0.1.0/corekit/crypto/__init__.py +1 -0
- python_corekit-0.1.0/corekit/crypto/constants.py +7 -0
- python_corekit-0.1.0/corekit/crypto/enum.py +11 -0
- python_corekit-0.1.0/corekit/crypto/hasher.py +89 -0
- python_corekit-0.1.0/corekit/data/__init__.py +81 -0
- python_corekit-0.1.0/corekit/data/dataset.py +340 -0
- python_corekit-0.1.0/corekit/data/expressions/__init__.py +46 -0
- python_corekit-0.1.0/corekit/data/expressions/comparison.py +252 -0
- python_corekit-0.1.0/corekit/data/expressions/expression.py +98 -0
- python_corekit-0.1.0/corekit/data/record.py +147 -0
- python_corekit-0.1.0/corekit/data/stats.py +157 -0
- python_corekit-0.1.0/corekit/decorators/__init__.py +2 -0
- python_corekit-0.1.0/corekit/decorators/exception_handling.py +43 -0
- python_corekit-0.1.0/corekit/decorators/warnings.py +35 -0
- python_corekit-0.1.0/corekit/docker/__init__.py +7 -0
- python_corekit-0.1.0/corekit/docker/watchdog.py +222 -0
- python_corekit-0.1.0/corekit/etl/__init__.py +44 -0
- python_corekit-0.1.0/corekit/etl/connection.py +44 -0
- python_corekit-0.1.0/corekit/etl/extract/__init__.py +0 -0
- python_corekit-0.1.0/corekit/etl/extract/extractor.py +48 -0
- python_corekit-0.1.0/corekit/etl/extract/schemas.py +18 -0
- python_corekit-0.1.0/corekit/etl/load/__init__.py +0 -0
- python_corekit-0.1.0/corekit/etl/load/loader.py +53 -0
- python_corekit-0.1.0/corekit/etl/load/schemas.py +33 -0
- python_corekit-0.1.0/corekit/etl/orchestrator.py +201 -0
- python_corekit-0.1.0/corekit/etl/schemas.py +22 -0
- python_corekit-0.1.0/corekit/etl/transform/__init__.py +0 -0
- python_corekit-0.1.0/corekit/etl/transform/schemas.py +15 -0
- python_corekit-0.1.0/corekit/etl/transform/transformer.py +28 -0
- python_corekit-0.1.0/corekit/events/__init__.py +38 -0
- python_corekit-0.1.0/corekit/events/enum.py +58 -0
- python_corekit-0.1.0/corekit/events/frames.py +51 -0
- python_corekit-0.1.0/corekit/events/models.py +23 -0
- python_corekit-0.1.0/corekit/events/publisher.py +75 -0
- python_corekit-0.1.0/corekit/events/reader.py +132 -0
- python_corekit-0.1.0/corekit/events/sse.py +109 -0
- python_corekit-0.1.0/corekit/events/websocket.py +97 -0
- python_corekit-0.1.0/corekit/exceptions/__init__.py +0 -0
- python_corekit-0.1.0/corekit/exceptions/base.py +45 -0
- python_corekit-0.1.0/corekit/exceptions/custom/__init__.py +0 -0
- python_corekit-0.1.0/corekit/exceptions/http/__init__.py +0 -0
- python_corekit-0.1.0/corekit/exceptions/http/exceptions.py +37 -0
- python_corekit-0.1.0/corekit/exceptions/types.py +17 -0
- python_corekit-0.1.0/corekit/files/__init__.py +25 -0
- python_corekit-0.1.0/corekit/files/base.py +117 -0
- python_corekit-0.1.0/corekit/files/enum.py +30 -0
- python_corekit-0.1.0/corekit/files/json.py +12 -0
- python_corekit-0.1.0/corekit/files/pickle.py +12 -0
- python_corekit-0.1.0/corekit/files/toml.py +43 -0
- python_corekit-0.1.0/corekit/http/__init__.py +0 -0
- python_corekit-0.1.0/corekit/http/client.py +176 -0
- python_corekit-0.1.0/corekit/http/exponential_backoff.py +100 -0
- python_corekit-0.1.0/corekit/http/response.py +12 -0
- python_corekit-0.1.0/corekit/log_monitor/__init__.py +23 -0
- python_corekit-0.1.0/corekit/log_monitor/constants.py +8 -0
- python_corekit-0.1.0/corekit/log_monitor/models.py +150 -0
- python_corekit-0.1.0/corekit/log_monitor/service.py +418 -0
- python_corekit-0.1.0/corekit/notifications/__init__.py +8 -0
- python_corekit-0.1.0/corekit/notifications/base.py +51 -0
- python_corekit-0.1.0/corekit/notifications/models.py +34 -0
- python_corekit-0.1.0/corekit/observability/__init__.py +21 -0
- python_corekit-0.1.0/corekit/observability/benchmarkable.py +12 -0
- python_corekit-0.1.0/corekit/observability/loggable.py +29 -0
- python_corekit-0.1.0/corekit/observability/timing/__init__.py +0 -0
- python_corekit-0.1.0/corekit/observability/timing/constants.py +1 -0
- python_corekit-0.1.0/corekit/observability/timing/split.py +20 -0
- python_corekit-0.1.0/corekit/observability/timing/timer.py +30 -0
- python_corekit-0.1.0/corekit/py.typed +0 -0
- python_corekit-0.1.0/corekit/registry/__init__.py +12 -0
- python_corekit-0.1.0/corekit/registry/registry.py +134 -0
- python_corekit-0.1.0/corekit/schemas/__init__.py +0 -0
- python_corekit-0.1.0/corekit/schemas/dataclasses/__init__.py +0 -0
- python_corekit-0.1.0/corekit/schemas/enum.py +49 -0
- python_corekit-0.1.0/corekit/schemas/models/__init__.py +0 -0
- python_corekit-0.1.0/corekit/schemas/models/arbitrary.py +11 -0
- python_corekit-0.1.0/corekit/schemas/models/date_models.py +18 -0
- python_corekit-0.1.0/corekit/schemas/pydantic/__init__.py +0 -0
- python_corekit-0.1.0/corekit/schemas/pydantic/fields.py +35 -0
- python_corekit-0.1.0/corekit/schemas/types.py +40 -0
- python_corekit-0.1.0/corekit/serialization/__init__.py +0 -0
- python_corekit-0.1.0/corekit/serialization/enum.py +21 -0
- python_corekit-0.1.0/corekit/serialization/serializable.py +42 -0
- python_corekit-0.1.0/corekit/serialization/serializer.py +179 -0
- python_corekit-0.1.0/corekit/utils/__init__.py +5 -0
- python_corekit-0.1.0/corekit/utils/ids.py +5 -0
- python_corekit-0.1.0/corekit/utils/raise_exc.py +8 -0
- python_corekit-0.1.0/corekit/utils/time.py +21 -0
- python_corekit-0.1.0/corekit/utils/validators.py +15 -0
- python_corekit-0.1.0/corekit/utils/void.py +8 -0
- python_corekit-0.1.0/pyproject.toml +59 -0
- python_corekit-0.1.0/python_corekit.egg-info/PKG-INFO +417 -0
- python_corekit-0.1.0/python_corekit.egg-info/SOURCES.txt +149 -0
- python_corekit-0.1.0/python_corekit.egg-info/dependency_links.txt +1 -0
- python_corekit-0.1.0/python_corekit.egg-info/requires.txt +17 -0
- python_corekit-0.1.0/python_corekit.egg-info/top_level.txt +1 -0
- python_corekit-0.1.0/setup.cfg +4 -0
- python_corekit-0.1.0/setup.py +10 -0
- python_corekit-0.1.0/tests/test_api.py +162 -0
- python_corekit-0.1.0/tests/test_architecture.py +186 -0
- python_corekit-0.1.0/tests/test_concurrency.py +280 -0
- python_corekit-0.1.0/tests/test_config.py +180 -0
- python_corekit-0.1.0/tests/test_connections.py +299 -0
- python_corekit-0.1.0/tests/test_data.py +126 -0
- python_corekit-0.1.0/tests/test_docker.py +175 -0
- python_corekit-0.1.0/tests/test_etl.py +257 -0
- python_corekit-0.1.0/tests/test_events.py +344 -0
- python_corekit-0.1.0/tests/test_files.py +75 -0
- python_corekit-0.1.0/tests/test_imports.py +32 -0
- python_corekit-0.1.0/tests/test_imports_are_top_level.py +162 -0
- python_corekit-0.1.0/tests/test_log_monitor.py +182 -0
- python_corekit-0.1.0/tests/test_migration.py +220 -0
- python_corekit-0.1.0/tests/test_redis.py +196 -0
- python_corekit-0.1.0/tests/test_registry.py +70 -0
- python_corekit-0.1.0/tests/test_requests.py +204 -0
- python_corekit-0.1.0/tests/test_serialization.py +127 -0
- python_corekit-0.1.0/tests/test_sql.py +167 -0
- python_corekit-0.1.0/tests/test_utils.py +236 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steven Jacobsen
|
|
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.
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-corekit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI routers/handlers, data stores, and ETL
|
|
5
|
+
Author: Steven Jacobsen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/stevejaker/corekit
|
|
8
|
+
Project-URL: Issues, https://github.com/stevejaker/corekit/issues
|
|
9
|
+
Keywords: fastapi,etl,homelab,logging,benchmarking
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
19
|
+
Requires-Dist: pydantic-settings<3,>=2.0
|
|
20
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
21
|
+
Requires-Dist: sqlmodel<0.1,>=0.0.16
|
|
22
|
+
Requires-Dist: SQLAlchemy<3,>=2.0
|
|
23
|
+
Requires-Dist: redis<7,>=5.0
|
|
24
|
+
Requires-Dist: httpx<1,>=0.27
|
|
25
|
+
Requires-Dist: docker<8,>=7.0
|
|
26
|
+
Requires-Dist: PyYAML<7,>=6.0
|
|
27
|
+
Requires-Dist: dill<0.5,>=0.3.8
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff<0.16,>=0.15; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# corekit
|
|
37
|
+
|
|
38
|
+
Shared foundations for Python projects: structured logging, benchmarking,
|
|
39
|
+
registries, FastAPI routers with built-in handlers, an in-memory record store,
|
|
40
|
+
and ETL scaffolding.
|
|
41
|
+
|
|
42
|
+
Requires Python 3.11+.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install python-corekit
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
No credentials, no SSH key, no token — which means a project that depends on
|
|
51
|
+
corekit can be cloned and built by anyone, including inside a Docker build.
|
|
52
|
+
|
|
53
|
+
Every dependency corekit needs is installed with it. There are no optional
|
|
54
|
+
extras to remember, and no import that fails because something was left out.
|
|
55
|
+
|
|
56
|
+
The distribution is `python-corekit`; the import is `corekit`. Pin a compatible
|
|
57
|
+
release rather than tracking whatever is newest:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
python-corekit~=0.1.0
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Before 1.0, the minor version carries breaking changes.
|
|
64
|
+
|
|
65
|
+
## Logging
|
|
66
|
+
|
|
67
|
+
Inherit from `Loggable` and every instance gets a logger named after its class.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from corekit.observability import Loggable
|
|
71
|
+
|
|
72
|
+
class Importer(Loggable):
|
|
73
|
+
def run(self) -> None:
|
|
74
|
+
self.info("starting")
|
|
75
|
+
try:
|
|
76
|
+
...
|
|
77
|
+
except Exception:
|
|
78
|
+
self.exception("import failed", exc_info=True)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`Benchmarkable` adds split timing on top:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from corekit.observability import Benchmarkable
|
|
85
|
+
|
|
86
|
+
class Report(Benchmarkable):
|
|
87
|
+
def build(self) -> None:
|
|
88
|
+
self.timing() # start the clock
|
|
89
|
+
...
|
|
90
|
+
self.timing("queried") # logs the time since the previous split
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Routers and handlers
|
|
94
|
+
|
|
95
|
+
A router and the handler holding its business logic travel together. Declare the
|
|
96
|
+
handler type in square brackets and the router builds it for you.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from corekit.api import BaseHandler, SmartRouter
|
|
100
|
+
|
|
101
|
+
class AdminHandler(BaseHandler):
|
|
102
|
+
"""
|
|
103
|
+
Admin operations.
|
|
104
|
+
|
|
105
|
+
Handlers inherit logging and benchmarking, and register themselves by name.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
async def list_users(self) -> list[str]:
|
|
109
|
+
return ["ada", "bob"]
|
|
110
|
+
|
|
111
|
+
router = SmartRouter[AdminHandler](route_prefix="/admin", tags=["Admin"])
|
|
112
|
+
|
|
113
|
+
@router.get("/users")
|
|
114
|
+
async def list_users() -> list[str]:
|
|
115
|
+
return await router.handler.list_users()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Mount it with `router.include(app)` — the router adds itself, rather than the
|
|
119
|
+
application having to know about it.
|
|
120
|
+
|
|
121
|
+
The handler is built on first use, and `router.handler` can be assigned, so
|
|
122
|
+
tests can substitute a double without constructing the real thing:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
router.handler = FakeAdminHandler()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Handlers register themselves under a normalized name, so any spelling finds them:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
BaseHandler.get_handler_by_name("admin_handler") # also "AdminHandler", "Admin Handler"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Datasets
|
|
135
|
+
|
|
136
|
+
An in-memory, schema-fixed collection with composable filters. Standard library
|
|
137
|
+
only — no pandas.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from corekit.data import Dataset, Field
|
|
141
|
+
|
|
142
|
+
people = Dataset(id_key="name", schema=["name", "age"])
|
|
143
|
+
people.add({"name": "Ada", "age": 36})
|
|
144
|
+
people.add({"name": "Bob", "age": 17})
|
|
145
|
+
|
|
146
|
+
adults = people.filter(Field("age") >= 18)
|
|
147
|
+
people.get_record("Ada").age # O(1) lookup by id
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Stores pickle cleanly, including their dynamically generated record class.
|
|
151
|
+
|
|
152
|
+
## Homelab pieces
|
|
153
|
+
|
|
154
|
+
### Container control
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from corekit.docker import Watchdog
|
|
158
|
+
|
|
159
|
+
watchdog = Watchdog(enforce_label=True)
|
|
160
|
+
watchdog.restart_container_by_name("minecraft")
|
|
161
|
+
watchdog.find_and_stop(label="app", value="staging")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`enforce_label` limits the blast radius: with it on, only containers carrying
|
|
165
|
+
the `watchdog=true` label can be started, stopped or paused, so a mistyped name
|
|
166
|
+
cannot take down something unrelated. Leave it on unless the watchdog is meant
|
|
167
|
+
to control everything on the host.
|
|
168
|
+
|
|
169
|
+
### Reacting to logs
|
|
170
|
+
|
|
171
|
+
Describe what to watch for and what to do about it:
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
# config.yaml
|
|
175
|
+
containers:
|
|
176
|
+
- name: "minecraft-.*"
|
|
177
|
+
rules:
|
|
178
|
+
- name: "out of memory"
|
|
179
|
+
pattern: "java.lang.OutOfMemoryError"
|
|
180
|
+
severity: critical
|
|
181
|
+
send_notification: true
|
|
182
|
+
actions:
|
|
183
|
+
- type: restart_container
|
|
184
|
+
max_restarts: 3
|
|
185
|
+
restart_window: 3600
|
|
186
|
+
advanced:
|
|
187
|
+
ignore_patterns:
|
|
188
|
+
- "healthcheck"
|
|
189
|
+
rate_limits:
|
|
190
|
+
restart_container:
|
|
191
|
+
count: 5
|
|
192
|
+
period: hour
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from corekit.log_monitor import LogMonitor
|
|
197
|
+
|
|
198
|
+
LogMonitor.run("config.yaml")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Restarts are capped per container, so a crash loop cannot become a restart loop.
|
|
202
|
+
|
|
203
|
+
### Notifications
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from corekit.notifications import BaseNotificationService, Notification, NotificationType
|
|
207
|
+
|
|
208
|
+
class DiscordNotifier(BaseNotificationService):
|
|
209
|
+
"""
|
|
210
|
+
Sends notifications to a Discord channel.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def _send(self, message: str) -> None:
|
|
214
|
+
discord.post(message)
|
|
215
|
+
|
|
216
|
+
notifier.notify(Notification(message="disk full", type=NotificationType.ERROR))
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Override `_send`, not `send`. `notify()` formats the message and calls `_send`,
|
|
220
|
+
so an override with any other name is silently ignored.
|
|
221
|
+
|
|
222
|
+
### Real-time updates
|
|
223
|
+
|
|
224
|
+
Publish from wherever the work happens:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from corekit.events import EventPublisher
|
|
228
|
+
|
|
229
|
+
publisher = EventPublisher.for_resource("minecraft", "server", "survival")
|
|
230
|
+
publisher.publish("backup_finished", {"size": "4.2GB"})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Stream it to the browser:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from corekit.api import SSEResponse
|
|
237
|
+
from corekit.events import SSEStream
|
|
238
|
+
|
|
239
|
+
@router.get("/events")
|
|
240
|
+
async def events(channel: str) -> SSEResponse:
|
|
241
|
+
return SSEResponse(SSEStream(channel, keepalive_interval=15))
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The browser side is three lines, and reconnects on its own:
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
const source = new EventSource("/events?channel=minecraft:server:survival");
|
|
248
|
+
source.addEventListener("backup_finished", e => console.log(JSON.parse(e.data)));
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`SSEStream` sends a `connected` frame on subscribe, an optional `initial_state`
|
|
252
|
+
so a client arriving late renders immediately, and a comment frame every
|
|
253
|
+
`keepalive_interval` seconds so proxies do not close an idle connection. For
|
|
254
|
+
WebSockets, `WebSocketBridge` relays the same channel and stops on a terminal
|
|
255
|
+
status.
|
|
256
|
+
|
|
257
|
+
Publishing never raises: an event that cannot be delivered should not take down
|
|
258
|
+
the operation that produced it. `publish` returns whether it worked.
|
|
259
|
+
|
|
260
|
+
## Parallel work
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from corekit.concurrency import parallelize
|
|
264
|
+
|
|
265
|
+
@parallelize()
|
|
266
|
+
def fetch(url: str) -> Response:
|
|
267
|
+
return client.get(url)
|
|
268
|
+
|
|
269
|
+
for response in fetch(urls):
|
|
270
|
+
...
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Results arrive as they finish; pass `ordered=True` for input order. The thread
|
|
274
|
+
count comes from `concurrency.default_threads` unless you name one, and is
|
|
275
|
+
capped at `max_threads` either way — asking for 9,999 threads gets you the
|
|
276
|
+
ceiling, not 9,999 threads.
|
|
277
|
+
|
|
278
|
+
Failures propagate by default. Pass `raise_on_error=False` to log and skip them
|
|
279
|
+
instead, which loses results silently and so is opt-in.
|
|
280
|
+
|
|
281
|
+
## HTTP clients
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from corekit.http.client import BaseApiClient
|
|
285
|
+
|
|
286
|
+
class GithubClient(BaseApiClient):
|
|
287
|
+
"""
|
|
288
|
+
Talks to the GitHub API.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def base_url(self) -> str:
|
|
293
|
+
return "https://api.github.com"
|
|
294
|
+
|
|
295
|
+
response = GithubClient().get("/users/octocat")
|
|
296
|
+
response.data["login"]
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Retries 429 and 5xx with exponential backoff. Every response is a
|
|
300
|
+
`BaseApiResponse`, so a non-JSON error page leaves `data` empty rather than
|
|
301
|
+
raising. `async_get`, `async_post` and friends do the same without blocking.
|
|
302
|
+
|
|
303
|
+
## Serialization
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from corekit.serialization.serializer import Serializer
|
|
307
|
+
from corekit.serialization.enum import SerializerEngine
|
|
308
|
+
|
|
309
|
+
serializer = Serializer(SerializerEngine.JSON)
|
|
310
|
+
serializer.deserialize(serializer.serialize({"a": 1}))
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
JSON is the default because it cannot execute code. `pickle` and `dill` can,
|
|
314
|
+
so selecting either requires a key, and payloads are authenticated with an
|
|
315
|
+
HMAC that is verified before anything is decoded:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
Serializer(SerializerEngine.PICKLE, key=os.environ["APP_KEY"])
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Never deserialize untrusted bytes with an engine that executes code, even
|
|
322
|
+
signed. The key proves the payload came from you, not that its contents are safe.
|
|
323
|
+
|
|
324
|
+
## Configuration
|
|
325
|
+
|
|
326
|
+
Configuration is optional. corekit never reads the environment at import time, so
|
|
327
|
+
importing it can never fail for want of a variable.
|
|
328
|
+
|
|
329
|
+
Precedence, highest first: explicit argument, environment, config file, default.
|
|
330
|
+
|
|
331
|
+
```toml
|
|
332
|
+
# corekit.toml, or a [tool.corekit] table in pyproject.toml
|
|
333
|
+
[standards]
|
|
334
|
+
require_handler_docstrings = true
|
|
335
|
+
|
|
336
|
+
[concurrency]
|
|
337
|
+
default_threads = 4 # used when a caller does not say
|
|
338
|
+
max_threads = 32 # never exceeded, however it is asked
|
|
339
|
+
|
|
340
|
+
[database]
|
|
341
|
+
url = "postgresql://localhost/app"
|
|
342
|
+
|
|
343
|
+
[crypto]
|
|
344
|
+
salt = "..."
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Settings are grouped by concern, so `get_settings().concurrency.max_threads`
|
|
348
|
+
says where a value belongs. Environment variables use a double underscore for
|
|
349
|
+
the section: `COREKIT_CONCURRENCY__MAX_THREADS=16`.
|
|
350
|
+
|
|
351
|
+
Environment variables use a `COREKIT_` prefix (`COREKIT_CRYPTO_SALT`). Empty
|
|
352
|
+
values are treated as unset, because container runtimes routinely pass `FOO=`
|
|
353
|
+
for a variable that was never set.
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
from corekit.config import CorekitSettings, StandardsSettings, set_settings
|
|
357
|
+
|
|
358
|
+
set_settings(CorekitSettings(standards=StandardsSettings(require_handler_docstrings=True)))
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Requiring docstrings
|
|
362
|
+
|
|
363
|
+
Off by default. Turn it on and every `BaseHandler` subclass must carry a
|
|
364
|
+
multiline docstring or fail at import. Individual classes can opt out with
|
|
365
|
+
`__require_doc__ = False`.
|
|
366
|
+
|
|
367
|
+
## Layout
|
|
368
|
+
|
|
369
|
+
Packages are named for what they are, and sit in the layer they belong to.
|
|
370
|
+
Imports go downward only.
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
corekit/
|
|
374
|
+
config.py constants.py
|
|
375
|
+
|
|
376
|
+
exceptions/ error types
|
|
377
|
+
|
|
378
|
+
observability/ Loggable, Benchmarkable, Timer
|
|
379
|
+
registry/ schemas/ utils/ SmartRegistry, enums and fields, helpers
|
|
380
|
+
data/ Dataset and its filter expressions
|
|
381
|
+
crypto/ files/ serialization/
|
|
382
|
+
concurrency/ ThreadLocalRegistry, ThreadWorker
|
|
383
|
+
decorators/
|
|
384
|
+
|
|
385
|
+
connections/ the Connectable lifecycle and @connect
|
|
386
|
+
sql/ SQLConnection, queries, migrations
|
|
387
|
+
redis/ RedisConnection
|
|
388
|
+
http/ BaseApiClient, retries, responses
|
|
389
|
+
|
|
390
|
+
api/ handlers, routers, responses
|
|
391
|
+
docker/ notifications/ etl/
|
|
392
|
+
|
|
393
|
+
events/ log_monitor/ built on the capabilities above
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`sql` and `redis` sit under `connections` because both implement `Connectable`.
|
|
397
|
+
`docker` does not -- `Watchdog` manages containers and has no connection
|
|
398
|
+
lifecycle -- so it stays a top-level integration.
|
|
399
|
+
|
|
400
|
+
`tests/test_architecture.py` enforces the direction: it fails on a cycle, on an
|
|
401
|
+
import pointing upward, or on a new package that has not been placed in the
|
|
402
|
+
layering deliberately.
|
|
403
|
+
|
|
404
|
+
## Development
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
pip install -e ".[dev,all]"
|
|
408
|
+
pytest
|
|
409
|
+
ruff format . && ruff check --fix .
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`tests/test_imports.py` imports every module in the package. Keep it passing:
|
|
413
|
+
several modules were broken for months because nothing ever imported them.
|
|
414
|
+
|
|
415
|
+
## Licence
|
|
416
|
+
|
|
417
|
+
MIT.
|