Add your description here
Project description
aeth-ext
This markdown file and _search_for_subclasses.py was AI generated by Claude. All other code was written by me. Sweet Fire Tobacco shared library — a batteries-included foundation for building Python services, batch jobs, and CLI tools.
aeth-ext bundles the cross-cutting infrastructure that the Sweet Fire Tobacco
projects rely on: a one-call application bootstrap, an opinionated logging stack
built on Rich, pydantic-based settings,
FTP/SFTP transfer adapters, a static (import-free) subclass discovery engine, a
monkey-patching framework, alert emails, and assorted utilities.
- Python:
>=3.14 - Package name (PyPI):
aeth-ext - Import name:
aeth_ext - Author: Jacob Ogden
Table of contents
- aeth-ext
- Table of contents
- Installation
- Quick start
- Architecture overview
- Modules
aeth_ext— application bootstrapsettings— configurationlogging— logging stackerrors— fatal-exception handling & alertsftp— FTP / SFTP adaptersmonkey_patcher— patch framework_search_for_subclasses— static class discoveryconst_parsing— constant extractionutils— email & datetime helperstypes— shared types & mixinsrich— enhanced progress bars
- Configuration reference
- Development
- Releasing
Installation
The library is published to the internal SFTPyPI index.
# base install
uv add aeth-ext
# with the high-performance async event loop (uvloop on Linux, winloop on Windows)
uv add "aeth-ext[async]"
# with SFTP support (paramiko)
uv add "aeth-ext[sftp]"
# everything
uv add "aeth-ext[async,sftp]"
| Extra | Pulls in | Use when |
|---|---|---|
async |
uvloop (Linux) / winloop (Windows) |
You call initialize(asyncio=True) |
sftp |
paramiko |
You use AdaptedSFTP |
Core runtime dependencies: aiologic, pydantic-settings, python-dateutil,
rich, tzdata.
Quick start
from aeth_ext import initialize
# Bootstraps logging, applies registered monkey patches, and (optionally)
# installs a high-performance asyncio event loop.
initialize(asyncio=True)
A more complete service entry point:
# main.py
from aeth_ext import initialize
from aeth_ext.settings import BaseSettings
# 1. Define your settings by subclassing BaseSettings.
class Settings(BaseSettings):
api_token: str # read from env var API_TOKEN (or .env in debug)
def main() -> None:
initialize(asyncio=False) # logging + monkey patches
settings = Settings.get_settings() # resolved singleton
...
if __name__ == "__main__":
main()
Architecture overview
A central theme of the library is static, import-free discovery: rather than
forcing you to register components in a central place, aeth_ext scans your
source tree, finds the most-derived subclass of a given base, and wires it up
automatically. This powers settings (BaseSettings), logging
(BaseLoggingConfig), and patches (MonkeyPatcher).
flowchart TD
A["initialize()"] --> B["init_logging() / init_logging_worker()"]
A --> C["MonkeyPatcher.apply_monkey_patches()"]
A --> D["install uvloop / winloop (asyncio=True)"]
B --> E["discover deepest BaseLoggingConfig subclass"]
C --> F["discover MonkeyPatcher subclasses"]
E --> G["_search_for_subclasses"]
F --> G
H["BaseSettings.get_settings()"] --> I["CapturesSubclasses mixin"]
Modules
aeth_ext — application bootstrap
The package root exposes a single orchestration function.
def initialize(
*queues: QueueCatchall,
asyncio: bool = False,
worker: bool = False,
run_monkey_patches: bool = True,
return_wrapped: bool = False,
) -> None | Callable[[], None]: ...
| Parameter | Default | Description |
|---|---|---|
*queues |
none | Logging queues (QueueCatchall) to attach for multi-process / multi-thread log fan-in. |
asyncio |
False |
Install uvloop (POSIX) or winloop (Windows) as the active event loop. Requires [async]. |
worker |
False |
Use worker-process logging config (init_logging_worker) instead of main-process config. |
run_monkey_patches |
True |
Discover and apply every MonkeyPatcher subclass before the app starts. |
return_wrapped |
False |
Return the initializer as a callable instead of running it immediately (useful for deferral). |
# Run immediately
initialize()
# Defer execution (e.g. to pass into a process pool initializer)
init = initialize(asyncio=True, return_wrapped=True)
init()
settings — configuration
BaseSettings extends pydantic_settings.BaseSettings and the
CapturesSubclasses mixin, so the most-derived subclass is resolved
automatically. In debug builds it reads a .env file; in release builds it
relies purely on environment variables.
from aeth_ext.settings import BaseSettings
class Settings(BaseSettings):
api_token: str # required, from API_TOKEN
settings = Settings.get_settings() # singleton; same instance every call
creds = settings.creds_file_reusable("Missing creds", "ftp", "creds.json")
Key built-in fields (all overridable via env vars / .env):
| Field | Env var | Default |
|---|---|---|
persisted_dir_loc |
PERSISTED_DIR_LOC |
./persisted_data (debug) / /app/persisted_data |
alerts_smtp_server |
ALERTS_SMTP_SERVER |
smtppro.zoho.com |
alerts_smtp_port |
ALERTS_SMTP_PORT |
587 |
alerts_email |
ALERTS_EMAIL |
info@sweetfiretobacco.com |
alerts_email_pwd |
ALERTS_EMAIL_PWD |
(required) |
alerts_recipients |
ALERTS_RECIPIENTS |
{jacob.ogden@sweetfiretobacco.com} |
log_loc_folder |
LOG_LOC_FOLDER |
<persisted_dir_loc>/logs |
tz |
TZ |
US/Eastern |
Helpers
get_settings()/get_final_model()— resolve the singleton.creds_file_reusable(err_msg, *path_parts)— validate and return a file path underpersisted_dir_loc, raisingFileNotFoundErrorwitherr_msgif absent.
logging — logging stack
A Rich-powered logging system with daily/per-run file rotation, abbreviated
library paths, and queue-based fan-in for multi-process apps. Usually you don't
call these directly — initialize() does — but you can customize behavior by
subclassing BaseLoggingConfig.
from aeth_ext.logging.config import BaseLoggingConfig
from rich.console import Console
class LoggingConfig(BaseLoggingConfig):
def configure_logging_main(rich_console: Console, project_name: str, **kw) -> None:
# override to customize handlers, formats, rotation, etc.
...
Public API
init_logging(*queues)— main-process setup; discovers the deepestBaseLoggingConfigsubclass and theconfigure_logging_mainconstants from your__main__.init_logging_worker(queue)— worker-process setup; routes logs to the parent via aQueueHandler.BaseLoggingConfig— override point forconfigure_logging_main,configure_logging_worker,configure_base_per_runner, andconfigure_base_once.QueueCatchall— union type of the supported queue backends (InterpreterQueue | ProcessQueue | ThreadQueue).FixedRichHandler,FixedLogRecord,CustomTimedRotatingFileHandler— the handler/record building blocks that abbreviatesite-packages/src/Libpaths in output.
errors — fatal-exception handling & alerts
Decorators that wrap a callable, log + email on any unhandled exception, set a
shared FATAL_EVENT, and swallow the error (returning None).
from aeth_ext.errors.err_handling import (
handle_fatal_exc_sync,
handle_fatal_exc_async,
FATAL_EVENT,
)
@handle_fatal_exc_sync
def risky() -> int:
return 1 / 0 # logs, emails an alert, sets FATAL_EVENT, returns None
@handle_fatal_exc_async
async def risky_async() -> None:
...
send_alert_email(subject, content) composes and batch-sends an alert email
to settings.alerts_recipients, attaching content as a UTF-8 file. It logs (and
no-ops) if no recipients are configured.
ftp — FTP / SFTP adapters
A unified, context-managed interface over plain FTP and Paramiko SFTP, with optional Rich progress bars and server-to-server transfers.
from aeth_ext.ftp.adapter import AdaptedSFTP # requires the [sftp] extra
from aeth_ext.rich.progress import Progress
with Progress() as pbar, AdaptedSFTP(sftp_protocol, "my-container", pbar=pbar) as ftp:
ftp.download_file(remote_path, callback, task_msg="Downloading")
ftp.upload_file(remote_path, callback, file_size, task_msg="Uploading")
ok = ftp.transfer_file(source, dest, other, task_msg="Relaying", ...)
FTP API
AdaptedFTP/AdaptedSFTP— context managers exposingupload_file,download_file, andtransfer_file(SFTP additionally providesrename,makedir,get_size,test_connection).AdapterProtocol,FTPProtocol,SFTPProtocol,ProtocolEnum,ListDirResult— the protocol/types layer (ftp/types.py).ServerNotAvailableError(ConnectionError)— raised when a server is unreachable.
monkey_patcher — patch framework
Organize monkey patches as subclasses. Each plain method you define is forced
into a staticmethod by the metaclass and is invoked once when patches are
applied. The class is not instantiable — call its classmethods directly.
from aeth_ext.monkey_patcher import MonkeyPatcher
class MyPatches(MonkeyPatcher):
def patch_some_library():
import some_library
some_library.thing = replacement
MonkeyPatcher.apply_monkey_patches() # discovers + runs every subclass's patches
initialize(run_monkey_patches=True) calls apply_monkey_patches() for you.
_search_for_subclasses — static class discovery
The engine behind the auto-wiring. It scans .py files with the ast module —
without importing them — to find subclasses, then loads only the ones you ask
for.
find_subclasses(base, roots, *, ignored_dirs=..., include_name_fallback=False, recursive=True)→tuple[SubclassInfo, ...]get_entrypoint_root()→ topmost package dir of the running entrypoint.SubclassInfo—NamedTuplewithqualname,name,module,file,lineno,depth; call.load()to import the live class.iter_python_files,build_subclass_index,load_subclasses,reset_subclass_caches— supporting helpers.
const_parsing — constant extraction
Read uppercase constant assignments out of a source file via AST and safely evaluate them against a restricted namespace.
from pathlib import Path
from aeth_ext.const_parsing import parse_and_grab_constants
values = parse_and_grab_constants(
Path("config.py"),
expected_constants={"PROJECT_NAME": "project_name"},
eval_locals={},
)
# -> {"project_name": "<value of PROJECT_NAME>"}
It scans both module-level statements and the if __name__ == "__main__": block.
utils — email & datetime helpers
Email composition / batch sending plus offset-aware datetime helpers.
from aeth_ext.utils import prepare_email_message, batch_send_emails, get_now, today
msg = prepare_email_message({
"subject": "Report",
"body": "See attached.",
"from_addr": "info@sweetfiretobacco.com",
"to_addrs": ["jacob.ogden@sweetfiretobacco.com"],
"attachments": Path("report.csv"),
})
batch_send_emails(msg) # SMTP config defaults to the alerts.* settings
| Function | Purpose |
|---|---|
prepare_email_message(parts) |
Build an EmailMessage from an EmailMessageParts dict. |
batch_send_emails(msgs, ...) |
Send one or many messages over SMTP (defaults to alerts cfg). |
handle_addrlike / ..._sequence |
Normalize flexible AddressLike values. |
handle_attachment(path) |
Read a file and return (bytes, mime-info). |
get_now(tz=None) / today(tz=None) |
Current datetime / midnight with configurable offset. |
get_last_sat(...) / get_next_sat |
Previous / next Saturday. |
types — shared types & mixins
AddressLike—str | Address | tuple[str, str | None, str | None, str | None].EmailMessageParts—TypedDictforprepare_email_message(subject,body,from_addr,to_addrsrequired;cc_addrs,bcc_addrs,attachmentsoptional).StrEnum— string enum whose value mirrors the member name.CapturesSubclasses(types/abc.py) — mixin that registers instances and can resolve the deepest subclass / final model; the backbone of the auto-wiring used byBaseSettingsandBaseLoggingConfig.SingletonType,SingletonTypeABC,SingletonTypeBaseModel— singleton metaclasses.
rich — enhanced progress bars
Progress is a rich.progress.Progress subclass preconfigured with a sensible
column layout (bar, M-of-N, percentage, time remaining). Its TaskID supports
use as a context manager so a task is auto-removed on exit.
from aeth_ext.rich.progress import Progress
with Progress() as progress:
with progress.add_task("Working", total=100) as task_id:
progress.update(task_id, advance=50)
# task is removed automatically here
Configuration reference
Settings are read (in priority order) from explicit constructor args →
environment variables → a .env file (debug builds only) → field defaults.
Empty env values are ignored, and unknown keys are dropped (extra="ignore").
Example .env:
PERSISTED_DIR_LOC=./persisted_data
ALERTS_EMAIL_PWD=super-secret
ALERTS_RECIPIENTS=["ops@sweetfiretobacco.com","jacob.ogden@sweetfiretobacco.com"]
TZ=US/Eastern
Never commit real secrets. Provide
ALERTS_EMAIL_PWDand any credentials via the environment or your secret manager.
Development
This project uses uv.
# install dependencies (including the dev group)
uv sync
# run the type checker
uv run pyright
# lint
uv run ruff check
# import smoke test
uv run python -c "import aeth_ext"
The dev group includes paramiko, pyright, types-python-dateutil, and the
async event loop backends.
Releasing
A Poe the Poet task automates version bump, tag, build, and publish to GitHub + SFTPyPI:
uv run poe release patch # or: minor | major
It bumps the version in pyproject.toml, commits, tags vX.Y.Z, pushes with
tags, builds, publishes to the SFTPyPI index, and creates a GitHub release with
generated notes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aeth_ext-2.0.0.tar.gz.
File metadata
- Download URL: aeth_ext-2.0.0.tar.gz
- Upload date:
- Size: 38.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30a628160bbd2a34ce168ba81c717d6b0e12bc640c41eb45b2bf644ed46d4447
|
|
| MD5 |
e6818f88fdd17a59fc7a013df3ba3375
|
|
| BLAKE2b-256 |
758deb9bc0a3744e6aae9e787ce3843891c8de8f2d2013441a54f1267d5f330a
|
File details
Details for the file aeth_ext-2.0.0-py3-none-any.whl.
File metadata
- Download URL: aeth_ext-2.0.0-py3-none-any.whl
- Upload date:
- Size: 46.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b847cfa65113e6792655f52aeffb8ffebe81403992dc541ced27e7b62de7bd84
|
|
| MD5 |
488fcff15ed7d8895d7e5ecdd7c2594b
|
|
| BLAKE2b-256 |
8e49caea4a83b9e6d39805b150829b56a45822356011f366eeec30dc6fa4d4d0
|