Imported from appKODE/probirka (
AGENTS.md). Install upstream withnpx skills add appKODE/probirka. Copyright stays with the author.
AGENTS.md
Working rules for probirka: what the package is, where its boundaries are and how a change is expected to look. They apply to everyone who edits the repository, people and tools alike.
What probirka is
A framework-agnostic library for running health probes in Python applications. The core has no runtime dependencies. Ready-made probes for client libraries (Redis, PostgreSQL, HTTP, Kafka, ...) and adapters for HTTP frameworks (FastAPI, aiohttp, Django, plain ASGI) ship in the same package but are imported only when used.
The core is the flat modules of the package root: _probe, _probirka, _results, _redact,
_lazy. It is complete on its own: a custom probe needs nothing but ProbeBase, ProbeFailure and
Probirka. The subpackages _probes (ready-made probes) and _ext (framework adapters) are
additions built on the core; the core never imports them.
Map
| Path | Role |
|---|---|
probirka/__init__.py |
The public surface: re-exports and __all__, nothing else |
probirka/_lazy.py |
Registry of the names that need a third-party package, the PEP 562 hooks, MissingDependencyError |
probirka/_redact.py |
Secret masking, stdlib only |
probirka/_results.py |
ProbeResult, ProbirkaResult |
probirka/_probe.py |
Probe protocol, ProbeBase, CallableProbe, ProbeFailure |
probirka/_probirka.py |
Probirka: registration, groups, concurrency, timeouts |
probirka/_probes/__init__.py |
Barrel: dependency-free probe names in __all__, the others under TYPE_CHECKING |
probirka/_probes/_common.py |
ClientOrFactory and the constructor helpers of the client probes |
probirka/_probes/_client_base.py, _http_base.py, _mongo_base.py |
Base classes shared by the driver probes |
probirka/_probes/_http_policy.py |
HttpProbePolicy, the SSRF protection of the HTTP probes, stdlib only |
probirka/_probes/_tcp.py |
TcpProbe, the one ready-made probe without a client library |
probirka/_probes/_<library>.py |
One driver probe per client library; imports the library at module level |
probirka/_ext/__init__.py |
Barrel: make_asgi_app in __all__, the framework adapters under TYPE_CHECKING |
probirka/_ext/_common.py |
"Run the probes, render the body", shared by the adapters |
probirka/_ext/asgi.py |
make_asgi_app, needs no framework |
probirka/_ext/<framework>.py |
One adapter per framework; imports the framework at module level |
tests/probes/ |
Unit tests of the probes with the client mocked |
tests/integration/ |
Tests against live services, run with PROBIRKA_INTEGRATION=1 |
docs/source/ |
Sphinx documentation, api.rst has one automodule per private module |
Architecture boundaries
Public API
- Public is what
probirka/__init__.pyexports. Everything under an_-prefixed path is private and may change without notice. - README, the Sphinx docs (
add_module_names = False) and the tests refer to public names asprobirka.X, never by the private module they live in.
The core imports with nothing but the standard library
import probirkaandfrom probirka import *work with no third-party package installed. CI runs exactly that in an isolated interpreter.- A third-party library is imported at module level only in the driver module that needs it:
probirka/_probes/_<library>.pyorprobirka/_ext/<framework>.py. Nothing imports those modules eagerly; the package root reaches them through the registry inprobirka/_lazy.py, and__all__lists them only when their package is installed. make_asgi_appandTcpProbeneed no third-party package and are the only eager exceptions.
Layers import downwards only
The core:
_redact,_lazy(import nothing from the package)_results_probe_probirka
The additions:
_probes/_common,_probes/_http_policy,_probes/_client_base,_probes/_tcp_probes/_http_base,_probes/_mongo_base,_ext/_common- driver probes
_probes/_<library>.py, adapters_ext/asgi.pyand_ext/<framework>.py - the barrels
_probes/__init__.py,_ext/__init__.py, and the package root
A module imports from its own layer or a lower one, never from a higher one. In particular: the
core never imports _probes or _ext; nothing inside the package imports the root probirka;
_probes/* never import _probirka or _ext; _ext/* never import _probes.
__init__.py files only re-export
- Allowed in an
__init__.py: a module docstring,importandfrom ... import, aTYPE_CHECKINGblock of imports,__all__, package metadata dunders such as__version__. - Not allowed: classes, functions, constants, control flow. Code lives in a named module
(
_probe.py, not__init__.py). - Inside a subpackage, siblings import each other by concrete module path
(
from probirka._probes._common import resolve), never through the subpackage__init__. A barrel imported by its own members is a circular import waiting to happen. Only the package root and the tests consume barrels. - A subpackage
__init__re-exports its dependency-free names in__all__. The names that need a third-party package appear there only inside aTYPE_CHECKINGblock, so type checkers see the whole surface while the barrel imports nothing at runtime. Staticallyprobirka._probes.RedisProbelooks present; at runtime it is not, and nothing reaches for it there. - The package root imports only its direct children: the flat modules (
_lazy,_redact,_results,_probe,_probirka) by name and the subpackages through their barrels, neverprobirka._probes._<module>. The lazy names come from the barrels underTYPE_CHECKINGas well; their runtime path is theLAZYregistry. - The package root additionally binds the PEP 562 hooks by assignment
(
__getattr__ = _module_getattr,__dir__ = _module_dir) and appends the installed lazy names to__all__. That is the whole exception. tests/test_architecture.pyenforces all of the above.
Probe contract
run_check()never raises. Exceptions and timeouts becomeProbeResult.error.- Raise
ProbeFailurewhen the service answered but is not healthy; its message is the error. - A probe registers every secret it hands to a client library with
_register_secrets()so that the error text is masked. Ready-made probes register the connection-string password and the request header values. allow_failure=Truekeeps a probe's failure out of the overallok; the probe's own result still shows it.
Adding a probe or an adapter
- Create
probirka/_probes/_<library>.pyextendingClientProbeBase(orHttpProbeBase,MongoProbeBase), orprobirka/_ext/<framework>.pybuilt onrun_and_renderfrom_ext/_common.py. Import the library at module level in that file only. - Add the name to
LAZYinprobirka/_lazy.py(module path, import name, pip package), aTYPE_CHECKINGre-export to the subpackage barrel (probirka/_probes/__init__.pyorprobirka/_ext/__init__.py) and one more, from that barrel, toprobirka/__init__.py. - Add the library to the
devdependency group inpyproject.toml. - Unit tests with the client mocked in
tests/probes/; an integration test intests/integration/plus the service intests/integration/compose.yamland in theservices:block of.github/workflows/tests.yml. - An
automoduleblock indocs/source/api.rst, a README section and aCHANGELOG.mdentry underUnreleased.
Tooling
uv sync --devonce; thenjust fmt,just lint,just ty,just tests,just doc.just services-up && just tests-integrationruns the live-service tests. CI runs the same recipes.- Ruff (lint and format) covers
probirka,testsanddocs;tycoversprobirka. - The ruff configuration is in
pyproject.toml. Every global ignore and every per-file ignore carries a comment saying why. A# noqacarries a reason (# noqa: S101 -- narrowed by require_exactly_one in __init__). Do not widenper-file-ignoresto silence one line. - Pre-commit runs ruff and ty on the changed files; keep its ruff version equal to the one in
uv.lock.
Code style
- Single quotes, 120 columns,
from __future__ import annotationsin every module, absolute imports only, typing-only imports underTYPE_CHECKING. - Docstrings in pep257 form with Sphinx fields (
:param:,:return:,:raises:) and an imperative first line. Every public module, class and function has one. - Exception messages go through a variable:
msg = ...; raise ValueError(msg). - Booleans are passed by keyword at call sites.
- Identifiers, comments, docstrings and commit messages are English.
Tests
- Import from
probirka. Use a private module only for a symbol that has no public path or tomonkeypatcha module object. tests/test_lazy_imports.pyandtests/test_architecture.pyare the executable specification of the boundaries above; extend them when a boundary changes.just testsneeds no Docker:tests/integration/is skipped unlessPROBIRKA_INTEGRATION=1.
Commits, merge requests, changelog
- Commit subject:
<type>: <imperative summary>with one offeat,fix,chore,test,docs; English, no trailing period. - Merge request description: short. Plain sentences and bulleted or numbered lists are the only formatting. No headings, bold text, tables, code blocks or emoji.
- Do not mention AI assistants, language models or code generators anywhere in the repository or
its history: not in code, comments or docstrings, not in commit messages or trailers (no
Co-authored-byfor a tool, no "generated with"), not in merge request titles or descriptions, not in the changelog. - User-visible changes go to
CHANGELOG.mdunderUnreleased, in the existing style.