pycraftcore
The shared technical foundation of the homelab's Python services: configuration, logging, telemetry, resilient HTTP, database repositories, safe SQL and a sandbox for running untrusted Python. Small, swappable building blocks rather than a framework — every module is an interface (a port) plus the implementations behind it (adapters).
Using pycraftcore — install it and use each module in your service. Working on pycraftcore — how it is organised and how to release it.
your service (agent-orchestrator, agent-toolbox, …)
│
▼
┌─ pycraftcore ─────────────────────────────────────────────────────────────┐
│ │
│ set up application_configuration · logger · telemetry · context │
│ talk out http · resilient_http (retry + circuit_breaker) │
│ store repository: SQLite · PostgreSQL · MongoDB │
│ run safely query_language (read-only SQL) · runtime (Python sandbox) │
│ utilities file_handler · serializer · computation_engine · profiler │
│ … new modules, same port + adapter shape │
│ │
└───────────────────────────────────────────────────────────────────────────┘
Using pycraftcore
Install
uv add pycraftcore # or: pip install pycraftcore
Requires Python 3.14. Development builds are published to TestPyPI (pip install -i https://test.pypi.org/simple/ pycraftcore); use them for testing only.
What's inside
Each module stands on its own: use the ones you need. The modules today (new ones follow the same shape, see Working on pycraftcore):
| Package | What it gives you |
|---|---|
application_configuration |
YAML configuration (connectors and operations) driven by environment variables, loaded into typed objects |
authentication |
The auth models a connector can use: none, basic, token |
logger |
One log format for the whole process, with the request id on every line |
context |
request_id_context, the request id shared by logs, telemetry and outgoing calls |
telemetry |
OpenTelemetry traces, metrics and logs, exported to an OTLP collector (or nothing) |
http |
Async HTTP clients (httpx, aiohttp), request-id and request-context middlewares, retry policies |
resilient_http |
An HTTP transport with retries and a circuit breaker built in |
retry, circuit_breaker |
The two policies on their own, for any async call |
repository |
Async repositories built from a database connector — today SQLite (pooled), PostgreSQL and MongoDB |
query_language |
Checks that a SQL statement is read-only, and translates between dialects, before it reaches a database |
runtime |
Runs untrusted Python in a separate process with memory, time and module limits; optionally lets it call your functions |
file_handler |
Read and write files by extension (today csv, json, yml, md, txt, svg); a new format is a new reader/writer pair |
serializer |
JSON, dict and binary (msgpack) serialisation of dataclasses |
computation_engine |
Numerical helpers: financial arithmetic, integration, interpolation |
profiler |
@profiled, an async profiling decorator |
Configuration
A service's configuration is a folder of YAML files, with values taken from environment variables (${oc.env:NAME}):
| Path | What it holds |
|---|---|
root.yml |
The environment and the list of connectors and operations in use |
<env>/connector/*.yml |
Where things are: APIs, databases, files, MCP servers, telemetry |
<env>/operation/*.yml |
What you do with them; each operation points at a connector |
# <env>/connector/api.yml
connector:
weather:
name: weather
type: api
base_url: https://<api-host>
timeout: 5
retry: 3
auth:
type: token
key_name: apikey
key_value: ${oc.env:WEATHER_API_KEY}
# <env>/operation/weather.yml
operation:
forecast:
name: forecast
type: api
connector: ${connector.weather}
endpoint: /forecast
method: GET
parameters:
days: 3
# root.yml
application_configuration:
env: ${oc.env:APP_ENV,debug}
run: async
connector:
api:
weather: ${connector.weather}
operation:
forecast: ${operation.forecast}
from pathlib import Path
from pycraftcore.application_configuration.adapter import (
LoadApplicationConfiguration,
OmegaConfigurationReader,
)
from pycraftcore.application_configuration.enum import RunTypeEnvironment
from pycraftcore.logger.adapter import StandardLogger
reader = OmegaConfigurationReader(RunTypeEnvironment.debug, Path("config"))
config = LoadApplicationConfiguration(reader, StandardLogger()).load()
connector = config.connector.api("weather") # ApiConnector
operation = config.operation.api("forecast") # ApiOperation
config.connector returns typed connectors per kind (.api(), .database(), .file(), .mcp(), .telemetry()) and config.operation typed operations (.api(), .file()); the type field of each entry decides which fields it takes.
Logging and the request id
from pycraftcore.logger import configure_logging
from pycraftcore.logger.adapter import StandardLogger
configure_logging("INFO")
logger = StandardLogger()
logger.info("booted")
2026-09-27 10:06:17.679 | INFO | 1bddfefb-… | controller:execute:51 - stream request accepted
- The third column is
request_id_context. In a web service,RequestIDMiddleware(pycraftcore.http.middleware) sets it from theX-Request-IDheader; every line logged during that request shows it,-otherwise. Don't write it in your messages. - Uvicorn logs use the same format; chatty libraries (httpx, mcp, pymongo…) stay at
WARNING. - Calling
configure_loggingagain replaces the handler rather than adding a second one.
Telemetry
from pycraftcore.application_configuration.enum import RunTypeEnvironment
from pycraftcore.telemetry.adapter import OpenTelemetryProvider
telemetry = OpenTelemetryProvider(
service_name="my-service",
environment=RunTypeEnvironment.debug,
otlp_endpoint="<collector-host>:<port>", # None: nothing is exported
)
tracer = telemetry.tracer("my-service")
Spans and log records carry the request id as an attribute, so logs and traces line up in Grafana.
Resilient HTTP
A transport that retries what is worth retrying and stops calling a backend that keeps failing:
from pycraftcore.circuit_breaker.configuration import CircuitBreakerSettings
from pycraftcore.http.configuration import HttpClientSettings, LimitsSettings
from pycraftcore.http.policy.http_error_policy import is_business_error, is_retryable
from pycraftcore.resilient_http.adapter import ResilientTransportFactory
from pycraftcore.resilient_http.configuration import ResilientHttpSettings
from pycraftcore.retry.configuration import RetrySettings
settings = ResilientHttpSettings(
http=HttpClientSettings(limits=LimitsSettings(timeout=10)),
retry=RetrySettings(
retry_count=3, retry_delay=1, max_retry_delay=20, should_retry=is_retryable
),
circuit_breaker=CircuitBreakerSettings(
failure_threshold=3, recovery_timeout=30, is_excluded=is_business_error, name="weather"
),
)
client = ResilientTransportFactory(
settings=settings, trace_manager=tracer, logger=logger
).create_async_client()
client is an httpx.AsyncClient: pass it to any library that accepts one.
Databases
Repositories are built from a database connector and share one interface, AsyncRepository.execute(statement):
from pycraftcore.repository.adapter import SqliteRepositoryFactory, SqliteSettingsMapper
factory = SqliteRepositoryFactory(SqliteSettingsMapper(config.connector.database("users"))())
repository = await factory.connect()
rows = await repository.execute("SELECT id, email FROM users LIMIT 10")
await factory.disconnect()
PostgresRepositoryFactory / PostgresSettingsMapper and MongoRepositoryFactory / MongoSettingsMapper work the same way, and so will any database added later: a factory, a settings mapper and a repository behind the same AsyncRepository port.
Read-only SQL
from pycraftcore.query_language.adapter import SqlHandlerFactory
statement = SqlHandlerFactory()("SELECT * FROM users", dialect="sqlite").transpile()
transpile() raises when the statement writes, deletes or changes the schema, so only reads reach the database.
Running untrusted Python
from pycraftcore.runtime.adapter import PythonSafeCodeFactory
from pycraftcore.runtime.schema import CodeResult, SafeCodeSettings
factory = PythonSafeCodeFactory(settings=SafeCodeSettings(code_timeout=30, max_memory_mb=256))
output = await factory(code="import math\nresult = math.sqrt(2)").execute()
print(CodeResult.from_stdout(output.stdout).value) # 1.4142135623730951
- The code runs in its own process; only the modules in
PYTHON_ALLOWLISTcan be imported (pandas, numpy, matplotlib, datetime…). working_directoryinSafeCodeSettingsconfines file access to one folder.- With a
HostBridgeServer, the code can call functions of your service as if they were local — this is how agent-toolbox lets sandboxed code use its other tools.
Working on pycraftcore
How a module is organised
Every package follows the same shape, so a new backend is a new adapter, never a change for the services that use the port:
pycraftcore/<module>/
├── port/ Protocols the services depend on (AsyncRepository, Code, Logger…)
├── adapter/ Implementations (SqliteRepository, PythonSafeCode, StandardLogger…)
├── configuration/ Pydantic settings objects (RetrySettings, CircuitBreakerSettings…)
├── schema/ Data passed across the port (CodeResult, SafeCodeSettings…)
└── enum/ Fixed choices (ConnectorType, HttpMethod…)
Not every module needs every folder: add a folder when it has something to hold.
Design rules
- Services import the port and receive an adapter; they never construct a third-party client themselves.
- Adapters are thin: they translate between the port and the library, and keep library types out of the port.
- Settings are validated pydantic models; nothing reads environment variables except the configuration loader.
- Every public module is typed:
tyruns in strict mode.
Adding a module or an adapter
- Define the protocol in
<module>/port/. - Implement it in
<module>/adapter/, with its settings inconfiguration/and the data it exchanges inschema/. - Export the public names from the package's
__init__.py. - Add tests under
tests/<module>/, and the module to What's inside — with a short example under Using pycraftcore if services will use it directly.
Tests and checks
make install_dev # uv sync --group dev
make check # ruff + ty + pytest
make test
make lint / make fix / make format / make typecheck
uv run pytest --cov=pycraftcore --cov-report=term-missing # coverage per module
Pre-commit runs the test suite before every commit and push:
uv run pre-commit install --hook-type pre-commit --hook-type pre-push
Releasing
- Bump
versioninpyproject.toml. make build.make publish_devto TestPyPI (needsTEST_PYPI_TOKEN), try it in a service, thenmake publishto PyPI (needsRELEASE_PYPI_TOKEN).- Raise the minimum version in the services that need the change (
pycraftcore>=…in theirpyproject.toml), thenuv lock --upgrade-package pycraftcore.
To work on pycraftcore and a service at the same time, point the service at your checkout with an editable path dependency in its pyproject.toml: pycraftcore = { path = "../pycraftcore", editable = true } under [tool.uv.sources].
Metadata
Release files for pycraftcore 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pycraftcore-2.0.1.tar.gz | 51.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pycraftcore-2.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 148.9 kB
Release files / pycraftcore-2.0.1.tar.gz
| Download URL | pycraftcore-2.0.1.tar.gz |
|---|---|
| Size | 51.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0d3d23c1176c6bb690d7c20bee4ebe9852aa95065350543bd17da33aa630dc06
|
|
BLAKE2b-256 checksum How to use checksums |
0782d6271f0d1b4dcfd70b3adfe98ef120cf619844a6a2396bab1ff0ea2faed2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / pycraftcore-2.0.1-py3-none-any.whl
| Download URL | pycraftcore-2.0.1-py3-none-any.whl |
|---|---|
| Size | 97.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2d629a4ae9571d747941e029f7eca7fbc7f897bbcf6a5a0e3b880ed9cf14d3a6
|
|
BLAKE2b-256 checksum How to use checksums |
7f541e484c67ef4689b6f9bcb6e729b8f1991ad3f5735cece985878e1ad75375
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|