Skip to main content

corekit

Shared foundations for Python projects: structured logging, benchmarking, registries, a FastAPI application with routers and handlers, SQL statements and migrations, background tasks, an in-memory record store, and ETL scaffolding.

Requires Python 3.11+.

Install

pip install python-corekit

The distribution is python-corekit; the import is corekit.

Every dependency corekit needs is installed with it. There are no optional extras to remember, and no import that fails because something was left out.

Pin a compatible release rather than tracking whatever is newest:

python-corekit~=0.2.0

Before 1.0, the minor version carries breaking changes.

Logging

Inherit from Loggable and every instance gets a logger named after its class.

from corekit.observability import Loggable

class Importer(Loggable):
    def run(self) -> None:
        self.info("starting")
        try:
            ...
        except Exception:
            self.exception("import failed", exc_info=True)

Benchmarkable adds split timing on top:

from corekit.observability import Benchmarkable

class Report(Benchmarkable):
    def build(self) -> None:
        self.timing()            # start the clock
        ...
        self.timing("queried")   # logs the time since the previous split

Assembling an application

Application is a plain FastAPI subclass — every constructor argument, including lifespan, passes straight through. What it adds is a short set of assembly steps, each of which logs what it did.

from corekit.api import Application, Lifespan
from app.backend import routers

lifespan = Lifespan()
lifespan.add("cache", startup=cache.connect, shutdown=cache.disconnect)

app = Application(lifespan=lifespan)
app.discover_routers(routers)

discover_routers imports every module under the package so each router can register itself. It raises if it finds none: passing a package is a statement that routers live there, and an app that silently serves nothing is worse than one that refuses to start.

Lifespan steps start in the order added and shut down in reverse, the way nested with blocks unwind. If a step fails on the way up, the steps that already started are still torn down. A shutdown hook runs only if its own startup completed, so it may assume the state that startup builds — and a hook that raises cannot stop the unwind.

Middleware

Middleware order is a security property: a host check that reads a client address before the proxy-header layer has rewritten it is checking the proxy, not the client. So middleware is never auto-discovered, and corekit installs none by default. A stack is declared in one place, outermost first — the order a request actually meets the layers.

from corekit.api import MiddlewareStack

stack = (
    MiddlewareStack()
    .add(ProxyHeadersMiddleware, trusted_hosts="*")
    .add(TrustedHostMiddleware, allowed_hosts=HOSTS)
    .add(CORSMiddleware, allow_origins=ORIGINS, allow_credentials=True)
)
app.add_middleware_stack(stack)

Reading top to bottom gives the order a request travels, which is the property you need when reviewing it.

Routers and handlers

A router and the handler holding its business logic travel together. Declare the handler type in square brackets and the router builds it for you.

from corekit.api import BaseHandler, SmartRouter

class AdminHandler(BaseHandler):
    """
    Admin operations.

    Handlers inherit logging and benchmarking, and register themselves by name.
    """

    async def list_users(self) -> list[str]:
        return ["ada", "bob"]

router = SmartRouter[AdminHandler](route_prefix="/admin", tags=["Admin"])

@router.get("/users")
async def list_users() -> list[str]:
    return await router.handler.list_users()

Mount it with router.include(app) — the router adds itself, rather than the application having to know about it.

The handler is built on first use, and router.handler can be assigned, so tests can substitute a double without constructing the real thing:

router.handler = FakeAdminHandler()

Handlers register themselves under a normalized name, so any spelling finds them:

BaseHandler.get_handler_by_name("admin_handler")   # also "AdminHandler", "Admin Handler"

Datasets

An in-memory, schema-fixed collection with composable filters. Standard library only — no pandas.

from corekit.data import Dataset, Field

people = Dataset(id_key="name", schema=["name", "age"])
people.add({"name": "Ada", "age": 36})
people.add({"name": "Bob", "age": 17})

adults = people.filter(Field("age") >= 18)
people.get_record("Ada").age            # O(1) lookup by id

Stores pickle cleanly, including their dynamically generated record class.

SQL statements

Select, insert, update and delete are objects you build and then execute, so a statement can be assembled in pieces and passed around before it runs.

from corekit.connections.sql import SQLConnection, Select, Insert, Update, Delete
from corekit.data import Field

conn = SQLConnection("sqlite:///app.db")

conn.execute(Insert(table=User, rows=[{"name": "Ada", "age": 36}]).add(name="Bob", age=17))

adults = conn.fetch(Select(table=User).where(User.age >= 18).order_by(User.name).limit(10))

conn.execute(Update(table=User).where(User.name == "Bob").set(age=18))
conn.execute(Delete(table=User).where(User.age < 13))

where accepts a SQLAlchemy expression or a corekit.data one, so the same predicate language that filters a Dataset also filters a table:

Select(table=User).where(Field("age") >= 18)

Update and delete compile to a single statement rather than fetching rows and looping, which matters most over a network, where fetch-and-loop pays a round trip per row.

A Delete or Update with no condition raises rather than running:

Delete on User needs a condition; use truncate to empty a table

Background tasks

Tasks describe work and register themselves by name. Nothing here imports a queue library, so the same task runs under RQ, Celery, a cron entry, or a test with no queue at all.

from corekit.jobs import Task, run_task
from corekit.utils import encode_payload

class SendDigest(Task):
    def task_function(self, user: str) -> None:
        ...

# a worker, holding only a name and a JSON string
run_task("SendDigest", encode_payload(["ada"]))

Arguments cross the queue as JSON, never as a serialized object. pickle and dill execute code while loading, so a queue holding objects turns write access to the queue into code execution in a worker. encode_payload and decode_payload live in corekit.utils, since crossing a process boundary as data is not specific to queues.

ScheduledTask adds an interval for tasks a scheduler should repeat.

Parallel work

from corekit.concurrency import parallelize

@parallelize()
def fetch(url: str) -> Response:
    return client.get(url)

for response in fetch(urls):
    ...

Results arrive as they finish; pass ordered=True for input order. The thread count comes from concurrency.default_threads unless you name one, and is capped at max_threads either way — asking for 9,999 threads gets you the ceiling, not 9,999 threads.

Failures propagate by default. Pass raise_on_error=False to log and skip them instead, which loses results silently and so is opt-in.

HTTP clients

from corekit.http.client import BaseApiClient

class GithubClient(BaseApiClient):
    """
    Talks to the GitHub API.
    """

    @property
    def base_url(self) -> str:
        return "https://api.github.com"

response = GithubClient().get("/users/octocat")
response.data["login"]

Retries 429 and 5xx with exponential backoff. Every response is a BaseApiResponse, so a non-JSON error page leaves data empty rather than raising. async_get, async_post and friends do the same without blocking.

Serialization

from corekit.serialization.serializer import Serializer
from corekit.serialization.enum import SerializerEngine

serializer = Serializer(SerializerEngine.JSON)
serializer.deserialize(serializer.serialize({"a": 1}))

JSON is the default because it cannot execute code. pickle and dill can, so selecting either requires a key, and payloads are authenticated with an HMAC that is verified before anything is decoded:

Serializer(SerializerEngine.PICKLE, key=os.environ["APP_KEY"])

Never deserialize untrusted bytes with an engine that executes code, even signed. The key proves the payload came from you, not that its contents are safe.

Configuration

Configuration is optional. corekit never reads the environment at import time, so importing it can never fail for want of a variable.

Precedence, highest first: explicit argument, environment, config file, default.

# corekit.toml, or a [tool.corekit] table in pyproject.toml
[standards]
require_handler_docstrings = true

[concurrency]
default_threads = 4      # used when a caller does not say
max_threads = 32         # never exceeded, however it is asked

[database]
url = "postgresql://localhost/app"

[crypto]
salt = "..."

Settings are grouped by concern, so get_settings().concurrency.max_threads says where a value belongs. Environment variables use a double underscore for the section: COREKIT_CONCURRENCY__MAX_THREADS=16.

Environment variables use a COREKIT_ prefix (COREKIT_CRYPTO_SALT). Empty values are treated as unset, because container runtimes routinely pass FOO= for a variable that was never set.

from corekit.config import CorekitSettings, StandardsSettings, set_settings

set_settings(CorekitSettings(standards=StandardsSettings(require_handler_docstrings=True)))

Requiring docstrings

Off by default. Turn it on and every BaseHandler subclass must carry a multiline docstring or fail at import. Individual classes can opt out with __require_doc__ = False.

Layout

Packages are named for what they are, and sit in the layer they belong to. Imports go downward only.

corekit/
  config/                          settings, sources, loader
  constants.py

  exceptions/                      error types

  observability/                   Loggable, Benchmarkable, Timer
  registry/  schemas/  utils/      registries, enums and fields, helpers
  data/                            Dataset and its filter expressions
  jobs/                            queue-independent background tasks
  crypto/  files/  serialization/
  concurrency/                     ThreadLocalRegistry, ThreadWorker
  decorators/

  connections/                     the Connectable lifecycle and @connect
    sql/                           SQLConnection, statements, migrations
    redis/                         RedisConnection
  http/                            BaseApiClient, retries, responses

  api/                             Application, lifespan, middleware, routers
  docker/  notifications/  etl/

  events/  log_monitor/            built on the capabilities above

sql and redis sit under connections because both implement Connectable. docker does not -- Watchdog manages containers and has no connection lifecycle -- so it stays a top-level integration.

tests/test_architecture.py enforces the direction: it fails on a cycle, on an import pointing upward, or on a new package that has not been placed in the layering deliberately.

Development

pip install -e ".[dev]"
pytest
ruff format . && ruff check --fix .

tests/test_imports.py imports every module in the package, so a module that nothing else happens to import still has to be importable.

Licence

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python_corekit-0.2.0.tar.gz (157.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

python_corekit-0.2.0-py3-none-any.whl (144.2 kB view details)

Uploaded Python 3

File details

Details for the file python_corekit-0.2.0.tar.gz.

File metadata

  • Download URL: python_corekit-0.2.0.tar.gz
  • Upload date:
  • Size: 157.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for python_corekit-0.2.0.tar.gz
Algorithm Hash digest
SHA256 bd94222b4da0db78bb1fb00bfde8f0b87e2c7193ca68b03a4ad4f385c2d243c3
MD5 3e65d1bc8edbfde25eb44dd10ac2f745
BLAKE2b-256 f34de89cc4e3118be51013e96739f3a8df04ddf09460bf894eed5840bd556ff2

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_corekit-0.2.0.tar.gz:

Publisher: release.yml on stevejaker/corekit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file python_corekit-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: python_corekit-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 144.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for python_corekit-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9ac4b2058c4230292ca4150f658ed28abecb436a4aac62d008e954ad0ada2637
MD5 16351044bf13bfd9d382214ecbc1b7cb
BLAKE2b-256 00b72a043c3a01cfb322d182bdddc17c46544b16edfad002a907b692ae0b16e3

See more details on using hashes here.

Provenance

The following attestation bundles were made for python_corekit-0.2.0-py3-none-any.whl:

Publisher: release.yml on stevejaker/corekit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page