aeth-ext 2.0.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.
- aeth_ext-2.0.0/PKG-INFO +491 -0
- aeth_ext-2.0.0/README.md +472 -0
- aeth_ext-2.0.0/pyproject.toml +89 -0
- aeth_ext-2.0.0/src/aeth_ext/__init__.py +71 -0
- aeth_ext-2.0.0/src/aeth_ext/_search_for_subclasses.py +940 -0
- aeth_ext-2.0.0/src/aeth_ext/const_parsing.py +70 -0
- aeth_ext-2.0.0/src/aeth_ext/errors/__init__.py +4 -0
- aeth_ext-2.0.0/src/aeth_ext/errors/err_handling.py +184 -0
- aeth_ext-2.0.0/src/aeth_ext/errors/send_alert_email.py +41 -0
- aeth_ext-2.0.0/src/aeth_ext/ftp/__init__.py +16 -0
- aeth_ext-2.0.0/src/aeth_ext/ftp/adapter.py +544 -0
- aeth_ext-2.0.0/src/aeth_ext/ftp/errors.py +5 -0
- aeth_ext-2.0.0/src/aeth_ext/ftp/types.py +116 -0
- aeth_ext-2.0.0/src/aeth_ext/logging/__init__.py +0 -0
- aeth_ext-2.0.0/src/aeth_ext/logging/bases.py +240 -0
- aeth_ext-2.0.0/src/aeth_ext/logging/config.py +252 -0
- aeth_ext-2.0.0/src/aeth_ext/logging/init.py +132 -0
- aeth_ext-2.0.0/src/aeth_ext/monkey_patcher.py +89 -0
- aeth_ext-2.0.0/src/aeth_ext/py.typed +0 -0
- aeth_ext-2.0.0/src/aeth_ext/rich/__init__.py +4 -0
- aeth_ext-2.0.0/src/aeth_ext/rich/progress.py +185 -0
- aeth_ext-2.0.0/src/aeth_ext/settings.py +71 -0
- aeth_ext-2.0.0/src/aeth_ext/types/__init__.py +45 -0
- aeth_ext-2.0.0/src/aeth_ext/types/abc.py +167 -0
- aeth_ext-2.0.0/src/aeth_ext/utils.py +240 -0
aeth_ext-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: aeth-ext
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Jacob Ogden
|
|
6
|
+
Author-email: Jacob Ogden <jacob.ogden@yahoo.com>
|
|
7
|
+
Requires-Dist: aiologic>=0.16.0
|
|
8
|
+
Requires-Dist: pydantic-settings>=2.14.1
|
|
9
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
10
|
+
Requires-Dist: rich>=15.0.0
|
|
11
|
+
Requires-Dist: tzdata>=2026.2
|
|
12
|
+
Requires-Dist: uvloop>=0.22.1 ; sys_platform == 'linux' and extra == 'async'
|
|
13
|
+
Requires-Dist: winloop>=0.6.0 ; sys_platform == 'win32' and extra == 'async'
|
|
14
|
+
Requires-Dist: paramiko>=5.0.0 ; extra == 'sftp'
|
|
15
|
+
Requires-Python: >=3.14
|
|
16
|
+
Provides-Extra: async
|
|
17
|
+
Provides-Extra: sftp
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# aeth-ext
|
|
21
|
+
|
|
22
|
+
> This markdown file and _search_for_subclasses.py was AI generated by Claude. All other code was written by me.
|
|
23
|
+
> Sweet Fire Tobacco shared library — a batteries-included foundation for building
|
|
24
|
+
> Python services, batch jobs, and CLI tools.
|
|
25
|
+
|
|
26
|
+
`aeth-ext` bundles the cross-cutting infrastructure that the Sweet Fire Tobacco
|
|
27
|
+
projects rely on: a one-call application bootstrap, an opinionated logging stack
|
|
28
|
+
built on [Rich](https://github.com/Textualize/rich), pydantic-based settings,
|
|
29
|
+
FTP/SFTP transfer adapters, a static (import-free) subclass discovery engine, a
|
|
30
|
+
monkey-patching framework, alert emails, and assorted utilities.
|
|
31
|
+
|
|
32
|
+
- **Python:** `>=3.14`
|
|
33
|
+
- **Package name (PyPI):** `aeth-ext`
|
|
34
|
+
- **Import name:** `aeth_ext`
|
|
35
|
+
- **Author:** Jacob Ogden
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Table of contents
|
|
40
|
+
|
|
41
|
+
- [aeth-ext](#aeth-ext)
|
|
42
|
+
- [Table of contents](#table-of-contents)
|
|
43
|
+
- [Installation](#installation)
|
|
44
|
+
- [Quick start](#quick-start)
|
|
45
|
+
- [Architecture overview](#architecture-overview)
|
|
46
|
+
- [Modules](#modules)
|
|
47
|
+
- [`aeth_ext` — application bootstrap](#aeth_ext--application-bootstrap)
|
|
48
|
+
- [`settings` — configuration](#settings--configuration)
|
|
49
|
+
- [Helpers](#helpers)
|
|
50
|
+
- [`logging` — logging stack](#logging--logging-stack)
|
|
51
|
+
- [Public API](#public-api)
|
|
52
|
+
- [`errors` — fatal-exception handling \& alerts](#errors--fatal-exception-handling--alerts)
|
|
53
|
+
- [`ftp` — FTP / SFTP adapters](#ftp--ftp--sftp-adapters)
|
|
54
|
+
- [FTP API](#ftp-api)
|
|
55
|
+
- [`monkey_patcher` — patch framework](#monkey_patcher--patch-framework)
|
|
56
|
+
- [`_search_for_subclasses` — static class discovery](#_search_for_subclasses--static-class-discovery)
|
|
57
|
+
- [`const_parsing` — constant extraction](#const_parsing--constant-extraction)
|
|
58
|
+
- [`utils` — email \& datetime helpers](#utils--email--datetime-helpers)
|
|
59
|
+
- [`types` — shared types \& mixins](#types--shared-types--mixins)
|
|
60
|
+
- [`rich` — enhanced progress bars](#rich--enhanced-progress-bars)
|
|
61
|
+
- [Configuration reference](#configuration-reference)
|
|
62
|
+
- [Development](#development)
|
|
63
|
+
- [Releasing](#releasing)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
The library is published to the internal SFTPyPI index.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# base install
|
|
73
|
+
uv add aeth-ext
|
|
74
|
+
|
|
75
|
+
# with the high-performance async event loop (uvloop on Linux, winloop on Windows)
|
|
76
|
+
uv add "aeth-ext[async]"
|
|
77
|
+
|
|
78
|
+
# with SFTP support (paramiko)
|
|
79
|
+
uv add "aeth-ext[sftp]"
|
|
80
|
+
|
|
81
|
+
# everything
|
|
82
|
+
uv add "aeth-ext[async,sftp]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Extra | Pulls in | Use when |
|
|
86
|
+
| ------- | -------------------------------------- | ------------------------------------ |
|
|
87
|
+
| `async` | `uvloop` (Linux) / `winloop` (Windows) | You call `initialize(asyncio=True)` |
|
|
88
|
+
| `sftp` | `paramiko` | You use `AdaptedSFTP` |
|
|
89
|
+
|
|
90
|
+
Core runtime dependencies: `aiologic`, `pydantic-settings`, `python-dateutil`,
|
|
91
|
+
`rich`, `tzdata`.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from aeth_ext import initialize
|
|
99
|
+
|
|
100
|
+
# Bootstraps logging, applies registered monkey patches, and (optionally)
|
|
101
|
+
# installs a high-performance asyncio event loop.
|
|
102
|
+
initialize(asyncio=True)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A more complete service entry point:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# main.py
|
|
109
|
+
from aeth_ext import initialize
|
|
110
|
+
from aeth_ext.settings import BaseSettings
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# 1. Define your settings by subclassing BaseSettings.
|
|
114
|
+
class Settings(BaseSettings):
|
|
115
|
+
api_token: str # read from env var API_TOKEN (or .env in debug)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main() -> None:
|
|
119
|
+
initialize(asyncio=False) # logging + monkey patches
|
|
120
|
+
settings = Settings.get_settings() # resolved singleton
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Architecture overview
|
|
131
|
+
|
|
132
|
+
A central theme of the library is **static, import-free discovery**: rather than
|
|
133
|
+
forcing you to register components in a central place, `aeth_ext` scans your
|
|
134
|
+
source tree, finds the most-derived subclass of a given base, and wires it up
|
|
135
|
+
automatically. This powers settings (`BaseSettings`), logging
|
|
136
|
+
(`BaseLoggingConfig`), and patches (`MonkeyPatcher`).
|
|
137
|
+
|
|
138
|
+
```mermaid
|
|
139
|
+
flowchart TD
|
|
140
|
+
A["initialize()"] --> B["init_logging() / init_logging_worker()"]
|
|
141
|
+
A --> C["MonkeyPatcher.apply_monkey_patches()"]
|
|
142
|
+
A --> D["install uvloop / winloop (asyncio=True)"]
|
|
143
|
+
B --> E["discover deepest BaseLoggingConfig subclass"]
|
|
144
|
+
C --> F["discover MonkeyPatcher subclasses"]
|
|
145
|
+
E --> G["_search_for_subclasses"]
|
|
146
|
+
F --> G
|
|
147
|
+
H["BaseSettings.get_settings()"] --> I["CapturesSubclasses mixin"]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Modules
|
|
153
|
+
|
|
154
|
+
### `aeth_ext` — application bootstrap
|
|
155
|
+
|
|
156
|
+
The package root exposes a single orchestration function.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
def initialize(
|
|
160
|
+
*queues: QueueCatchall,
|
|
161
|
+
asyncio: bool = False,
|
|
162
|
+
worker: bool = False,
|
|
163
|
+
run_monkey_patches: bool = True,
|
|
164
|
+
return_wrapped: bool = False,
|
|
165
|
+
) -> None | Callable[[], None]: ...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Parameter | Default | Description |
|
|
169
|
+
| -------------------- | ------- | --------------------------------------------------------------------------------------------- |
|
|
170
|
+
| `*queues` | none | Logging queues (`QueueCatchall`) to attach for multi-process / multi-thread log fan-in. |
|
|
171
|
+
| `asyncio` | `False` | Install `uvloop` (POSIX) or `winloop` (Windows) as the active event loop. Requires `[async]`. |
|
|
172
|
+
| `worker` | `False` | Use worker-process logging config (`init_logging_worker`) instead of main-process config. |
|
|
173
|
+
| `run_monkey_patches` | `True` | Discover and apply every `MonkeyPatcher` subclass before the app starts. |
|
|
174
|
+
| `return_wrapped` | `False` | Return the initializer as a callable instead of running it immediately (useful for deferral). |
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# Run immediately
|
|
178
|
+
initialize()
|
|
179
|
+
|
|
180
|
+
# Defer execution (e.g. to pass into a process pool initializer)
|
|
181
|
+
init = initialize(asyncio=True, return_wrapped=True)
|
|
182
|
+
init()
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### `settings` — configuration
|
|
188
|
+
|
|
189
|
+
`BaseSettings` extends `pydantic_settings.BaseSettings` and the
|
|
190
|
+
`CapturesSubclasses` mixin, so the *most-derived* subclass is resolved
|
|
191
|
+
automatically. In debug builds it reads a `.env` file; in release builds it
|
|
192
|
+
relies purely on environment variables.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from aeth_ext.settings import BaseSettings
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class Settings(BaseSettings):
|
|
199
|
+
api_token: str # required, from API_TOKEN
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
settings = Settings.get_settings() # singleton; same instance every call
|
|
203
|
+
creds = settings.creds_file_reusable("Missing creds", "ftp", "creds.json")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Key built-in fields (all overridable via env vars / `.env`):
|
|
207
|
+
|
|
208
|
+
| Field | Env var | Default |
|
|
209
|
+
| -------------------- | -------------------- | -------------------------------------------------- |
|
|
210
|
+
| `persisted_dir_loc` | `PERSISTED_DIR_LOC` | `./persisted_data` (debug) / `/app/persisted_data` |
|
|
211
|
+
| `alerts_smtp_server` | `ALERTS_SMTP_SERVER` | `smtppro.zoho.com` |
|
|
212
|
+
| `alerts_smtp_port` | `ALERTS_SMTP_PORT` | `587` |
|
|
213
|
+
| `alerts_email` | `ALERTS_EMAIL` | `info@sweetfiretobacco.com` |
|
|
214
|
+
| `alerts_email_pwd` | `ALERTS_EMAIL_PWD` | *(required)* |
|
|
215
|
+
| `alerts_recipients` | `ALERTS_RECIPIENTS` | `{jacob.ogden@sweetfiretobacco.com}` |
|
|
216
|
+
| `log_loc_folder` | `LOG_LOC_FOLDER` | `<persisted_dir_loc>/logs` |
|
|
217
|
+
| `tz` | `TZ` | `US/Eastern` |
|
|
218
|
+
|
|
219
|
+
#### Helpers
|
|
220
|
+
|
|
221
|
+
- `get_settings()` / `get_final_model()` — resolve the singleton.
|
|
222
|
+
- `creds_file_reusable(err_msg, *path_parts)` — validate and return a file path
|
|
223
|
+
under `persisted_dir_loc`, raising `FileNotFoundError` with `err_msg` if absent.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
### `logging` — logging stack
|
|
228
|
+
|
|
229
|
+
A Rich-powered logging system with daily/per-run file rotation, abbreviated
|
|
230
|
+
library paths, and queue-based fan-in for multi-process apps. Usually you don't
|
|
231
|
+
call these directly — `initialize()` does — but you can customize behavior by
|
|
232
|
+
subclassing `BaseLoggingConfig`.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from aeth_ext.logging.config import BaseLoggingConfig
|
|
236
|
+
from rich.console import Console
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class LoggingConfig(BaseLoggingConfig):
|
|
240
|
+
def configure_logging_main(rich_console: Console, project_name: str, **kw) -> None:
|
|
241
|
+
# override to customize handlers, formats, rotation, etc.
|
|
242
|
+
...
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### Public API
|
|
246
|
+
|
|
247
|
+
- `init_logging(*queues)` — main-process setup; discovers the deepest
|
|
248
|
+
`BaseLoggingConfig` subclass and the `configure_logging_main` constants from your
|
|
249
|
+
`__main__`.
|
|
250
|
+
- `init_logging_worker(queue)` — worker-process setup; routes logs to the parent
|
|
251
|
+
via a `QueueHandler`.
|
|
252
|
+
- `BaseLoggingConfig` — override point for `configure_logging_main`,
|
|
253
|
+
`configure_logging_worker`, `configure_base_per_runner`, and `configure_base_once`.
|
|
254
|
+
- `QueueCatchall` — union type of the supported queue backends
|
|
255
|
+
(`InterpreterQueue | ProcessQueue | ThreadQueue`).
|
|
256
|
+
- `FixedRichHandler`, `FixedLogRecord`, `CustomTimedRotatingFileHandler` —
|
|
257
|
+
the handler/record building blocks that abbreviate `site-packages`/`src`/`Lib`
|
|
258
|
+
paths in output.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### `errors` — fatal-exception handling & alerts
|
|
263
|
+
|
|
264
|
+
Decorators that wrap a callable, log + email on any unhandled exception, set a
|
|
265
|
+
shared `FATAL_EVENT`, and swallow the error (returning `None`).
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from aeth_ext.errors.err_handling import (
|
|
269
|
+
handle_fatal_exc_sync,
|
|
270
|
+
handle_fatal_exc_async,
|
|
271
|
+
FATAL_EVENT,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@handle_fatal_exc_sync
|
|
276
|
+
def risky() -> int:
|
|
277
|
+
return 1 / 0 # logs, emails an alert, sets FATAL_EVENT, returns None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@handle_fatal_exc_async
|
|
281
|
+
async def risky_async() -> None:
|
|
282
|
+
...
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**`send_alert_email(subject, content)`** composes and batch-sends an alert email
|
|
286
|
+
to `settings.alerts_recipients`, attaching `content` as a UTF-8 file. It logs (and
|
|
287
|
+
no-ops) if no recipients are configured.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### `ftp` — FTP / SFTP adapters
|
|
292
|
+
|
|
293
|
+
A unified, context-managed interface over plain FTP and Paramiko SFTP, with
|
|
294
|
+
optional Rich progress bars and server-to-server transfers.
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from aeth_ext.ftp.adapter import AdaptedSFTP # requires the [sftp] extra
|
|
298
|
+
from aeth_ext.rich.progress import Progress
|
|
299
|
+
|
|
300
|
+
with Progress() as pbar, AdaptedSFTP(sftp_protocol, "my-container", pbar=pbar) as ftp:
|
|
301
|
+
ftp.download_file(remote_path, callback, task_msg="Downloading")
|
|
302
|
+
ftp.upload_file(remote_path, callback, file_size, task_msg="Uploading")
|
|
303
|
+
ok = ftp.transfer_file(source, dest, other, task_msg="Relaying", ...)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### FTP API
|
|
307
|
+
|
|
308
|
+
- `AdaptedFTP` / `AdaptedSFTP` — context managers exposing `upload_file`,
|
|
309
|
+
`download_file`, and `transfer_file` (SFTP additionally provides `rename`,
|
|
310
|
+
`makedir`, `get_size`, `test_connection`).
|
|
311
|
+
- `AdapterProtocol`, `FTPProtocol`, `SFTPProtocol`, `ProtocolEnum`,
|
|
312
|
+
`ListDirResult` — the protocol/types layer (`ftp/types.py`).
|
|
313
|
+
- `ServerNotAvailableError(ConnectionError)` — raised when a server is unreachable.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### `monkey_patcher` — patch framework
|
|
318
|
+
|
|
319
|
+
Organize monkey patches as subclasses. Each plain method you define is forced
|
|
320
|
+
into a `staticmethod` by the metaclass and is invoked once when patches are
|
|
321
|
+
applied. The class is **not instantiable** — call its classmethods directly.
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from aeth_ext.monkey_patcher import MonkeyPatcher
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class MyPatches(MonkeyPatcher):
|
|
328
|
+
def patch_some_library():
|
|
329
|
+
import some_library
|
|
330
|
+
some_library.thing = replacement
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
MonkeyPatcher.apply_monkey_patches() # discovers + runs every subclass's patches
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`initialize(run_monkey_patches=True)` calls `apply_monkey_patches()` for you.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
### `_search_for_subclasses` — static class discovery
|
|
341
|
+
|
|
342
|
+
The engine behind the auto-wiring. It scans `.py` files with the `ast` module —
|
|
343
|
+
**without importing them** — to find subclasses, then loads only the ones you ask
|
|
344
|
+
for.
|
|
345
|
+
|
|
346
|
+
- `find_subclasses(base, roots, *, ignored_dirs=..., include_name_fallback=False, recursive=True)`
|
|
347
|
+
→ `tuple[SubclassInfo, ...]`
|
|
348
|
+
- `get_entrypoint_root()` → topmost package dir of the running entrypoint.
|
|
349
|
+
- `SubclassInfo` — `NamedTuple` with `qualname`, `name`, `module`, `file`,
|
|
350
|
+
`lineno`, `depth`; call `.load()` to import the live class.
|
|
351
|
+
- `iter_python_files`, `build_subclass_index`, `load_subclasses`,
|
|
352
|
+
`reset_subclass_caches` — supporting helpers.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
### `const_parsing` — constant extraction
|
|
357
|
+
|
|
358
|
+
Read uppercase constant assignments out of a source file via AST and safely
|
|
359
|
+
evaluate them against a restricted namespace.
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from pathlib import Path
|
|
363
|
+
from aeth_ext.const_parsing import parse_and_grab_constants
|
|
364
|
+
|
|
365
|
+
values = parse_and_grab_constants(
|
|
366
|
+
Path("config.py"),
|
|
367
|
+
expected_constants={"PROJECT_NAME": "project_name"},
|
|
368
|
+
eval_locals={},
|
|
369
|
+
)
|
|
370
|
+
# -> {"project_name": "<value of PROJECT_NAME>"}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
It scans both module-level statements and the `if __name__ == "__main__":` block.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
### `utils` — email & datetime helpers
|
|
378
|
+
|
|
379
|
+
Email composition / batch sending plus offset-aware datetime helpers.
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
from aeth_ext.utils import prepare_email_message, batch_send_emails, get_now, today
|
|
383
|
+
|
|
384
|
+
msg = prepare_email_message({
|
|
385
|
+
"subject": "Report",
|
|
386
|
+
"body": "See attached.",
|
|
387
|
+
"from_addr": "info@sweetfiretobacco.com",
|
|
388
|
+
"to_addrs": ["jacob.ogden@sweetfiretobacco.com"],
|
|
389
|
+
"attachments": Path("report.csv"),
|
|
390
|
+
})
|
|
391
|
+
batch_send_emails(msg) # SMTP config defaults to the alerts.* settings
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
| Function | Purpose |
|
|
395
|
+
| ------------------------------------- | ------------------------------------------------------------- |
|
|
396
|
+
| `prepare_email_message(parts)` | Build an `EmailMessage` from an `EmailMessageParts` dict. |
|
|
397
|
+
| `batch_send_emails(msgs, ...)` | Send one or many messages over SMTP (defaults to alerts cfg). |
|
|
398
|
+
| `handle_addrlike` / `..._sequence` | Normalize flexible `AddressLike` values. |
|
|
399
|
+
| `handle_attachment(path)` | Read a file and return `(bytes, mime-info)`. |
|
|
400
|
+
| `get_now(tz=None)` / `today(tz=None)` | Current datetime / midnight with configurable offset. |
|
|
401
|
+
| `get_last_sat(...)` / `get_next_sat` | Previous / next Saturday. |
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
### `types` — shared types & mixins
|
|
406
|
+
|
|
407
|
+
- `AddressLike` — `str | Address | tuple[str, str | None, str | None, str | None]`.
|
|
408
|
+
- `EmailMessageParts` — `TypedDict` for `prepare_email_message` (`subject`,
|
|
409
|
+
`body`, `from_addr`, `to_addrs` required; `cc_addrs`, `bcc_addrs`,
|
|
410
|
+
`attachments` optional).
|
|
411
|
+
- `StrEnum` — string enum whose value mirrors the member name.
|
|
412
|
+
- `CapturesSubclasses` (`types/abc.py`) — mixin that registers instances and can
|
|
413
|
+
resolve the deepest subclass / final model; the backbone of the auto-wiring used
|
|
414
|
+
by `BaseSettings` and `BaseLoggingConfig`.
|
|
415
|
+
- `SingletonType`, `SingletonTypeABC`, `SingletonTypeBaseModel` — singleton
|
|
416
|
+
metaclasses.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
### `rich` — enhanced progress bars
|
|
421
|
+
|
|
422
|
+
`Progress` is a `rich.progress.Progress` subclass preconfigured with a sensible
|
|
423
|
+
column layout (bar, M-of-N, percentage, time remaining). Its `TaskID` supports
|
|
424
|
+
use as a context manager so a task is auto-removed on exit.
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
from aeth_ext.rich.progress import Progress
|
|
428
|
+
|
|
429
|
+
with Progress() as progress:
|
|
430
|
+
with progress.add_task("Working", total=100) as task_id:
|
|
431
|
+
progress.update(task_id, advance=50)
|
|
432
|
+
# task is removed automatically here
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Configuration reference
|
|
438
|
+
|
|
439
|
+
Settings are read (in priority order) from explicit constructor args →
|
|
440
|
+
environment variables → a `.env` file (debug builds only) → field defaults.
|
|
441
|
+
Empty env values are ignored, and unknown keys are dropped (`extra="ignore"`).
|
|
442
|
+
|
|
443
|
+
Example `.env`:
|
|
444
|
+
|
|
445
|
+
```dotenv
|
|
446
|
+
PERSISTED_DIR_LOC=./persisted_data
|
|
447
|
+
ALERTS_EMAIL_PWD=super-secret
|
|
448
|
+
ALERTS_RECIPIENTS=["ops@sweetfiretobacco.com","jacob.ogden@sweetfiretobacco.com"]
|
|
449
|
+
TZ=US/Eastern
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
> Never commit real secrets. Provide `ALERTS_EMAIL_PWD` and any credentials via
|
|
453
|
+
> the environment or your secret manager.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Development
|
|
458
|
+
|
|
459
|
+
This project uses [uv](https://github.com/astral-sh/uv).
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
# install dependencies (including the dev group)
|
|
463
|
+
uv sync
|
|
464
|
+
|
|
465
|
+
# run the type checker
|
|
466
|
+
uv run pyright
|
|
467
|
+
|
|
468
|
+
# lint
|
|
469
|
+
uv run ruff check
|
|
470
|
+
|
|
471
|
+
# import smoke test
|
|
472
|
+
uv run python -c "import aeth_ext"
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
The dev group includes `paramiko`, `pyright`, `types-python-dateutil`, and the
|
|
476
|
+
async event loop backends.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Releasing
|
|
481
|
+
|
|
482
|
+
A [Poe the Poet](https://poethepoet.natn.io/) task automates version bump, tag,
|
|
483
|
+
build, and publish to GitHub + SFTPyPI:
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
uv run poe release patch # or: minor | major
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
It bumps the version in `pyproject.toml`, commits, tags `vX.Y.Z`, pushes with
|
|
490
|
+
tags, builds, publishes to the `SFTPyPI` index, and creates a GitHub release with
|
|
491
|
+
generated notes.
|