This file provides guidance to AI agents working with code in this repository. Every line of it is loaded into every session's context, so it has to earn that cost: add a rule only when an agent would otherwise get it wrong, keep it to one line, and prefer rewording an existing rule over appending a new one.
bfabricPy is a Python client library for B-Fabric, a data management platform at the Functional Genomics Center Zurich (FGCZ). It communicates with B-Fabric via SOAP/WSDL.
This is a uv workspace with 5 packages:
| Package | Purpose | Min Python |
|---|---|---|
bfabric |
Core client library | 3.11 |
bfabric_scripts |
CLI scripts and utilities | 3.11 |
bfabric_app_runner |
Application runner for workflows | 3.12 |
bfabric_rest_proxy |
FastAPI REST proxy | 3.12 |
bfabric_asgi_auth |
ASGI auth middleware | 3.13 |
Each package has its own pyproject.toml under its directory. Workspace references mean changes to bfabric are immediately available to dependent packages.
uv sync --all-packages --all-extrasnox # all test sessions
nox -s test_bfabric # core package only
nox -s test_bfabric_scripts # scripts package
nox -s test_bfabric_app_runner # app runner
nox -s test_bfabric-3.13 # specific Python version
nox -s test_bfabric-3.11(lowest-direct) # specific resolution strategypytest tests/bfabric # core package
pytest tests/bfabric_scripts # scripts (also tests/bfabric_cli)
pytest tests/bfabric_app_runner # app runner
pytest tests/bfabric/test_something.py # single file
pytest tests/bfabric -k test_name # single testRun each package's suite in a separate pytest invocation, as nox does — one invocation over two package trees fails at collection because identically-named test modules collide (see the __init__.py convention below).
nox -s basedpyright(bfabric)
nox -s basedpyright(bfabric_scripts)
nox -s basedpyright(bfabric_app_runner)nox -s code_style # ruff lint via nox — lint only, checks no formatting
ruff check bfabric # ruff directly
pre-commit run --all-files # the formatter gate (black, and blacken-docs for md code blocks)black is the formatter — never run ruff format. It disagrees with black on ~15 files and produces a large spurious diff. Since neither nox -s code_style nor CI checks formatting, the pre-commit hook rejecting the commit is the only thing that catches a wrong formatter.
nox -s docs # build all docs to site/
cd bfabric/docs && make html # local previewbfabric.py—Bfabricclass: the main client. Create viaBfabric.connect()(config file) orBfabric.connect_webapp()(token auth). Providesread(),save(),delete(),exists(),upload_resource().config/— Pydantic-based config:BfabricAuth(login + 32-char SecretStr password),BfabricClientConfig(base_url, engine choice), loaded from~/.bfabricpy.yml. Environment selection viaBFABRICPY_CONFIG_ENV. Override viaBFABRICPY_CONFIG_OVERRIDE(JSON).engine/—EngineSUDS, the SOAP transport, over the suds library. It is the only engine;BfabricClientConfig.engineis vestigial and a config naming any other value fails to load.entities/— Entity models withHasOne/HasManyrelationship descriptors and lazy loading.EntityReaderprovides ORM-like access with caching (cache_entities()context manager).results/—ResultContainerwraps API responses with pagination, error handling, andto_polars()conversion.utils/cli_integration.py—@use_clientdecorator for CLI commands: auto-createsBfabricclient, injects config_env/config_file parameters.
Modern CLI built with cyclopts: bfabric-cli api|dataset|executable|workunit|feeder|external-job. Legacy scripts (bfabric_read.py, etc.) are preserved as entry points.
Handles dispatch → process → collect workflow for B-Fabric applications. Uses pydantic for spec validation, mako for templating.
Docs live alongside each package's source; skim the index when working in one. bfabric/docs/index.md and bfabric_app_runner/docs/index.md are full sites (getting started, user guides, API reference, design) and the only two that nox -s docs builds. bfabric_scripts, bfabric_rest_proxy and bfabric_asgi_auth have just a docs/changelog.md, plus a README.md for the latter two.
- Tests must NOT contain
__init__.pyfiles (enforced by thecheck_test_initsnox session). - Test order is randomised via pytest-random-order (
addopts = "--random-order"), so tests must be isolation-safe. The seed is printed in the pytest header; reproduce an order withpytest --random-order-seed=<N>. Quarantine a genuinely order-dependent module withpytestmark = pytest.mark.random_order(disabled=True)only as a last resort. - Test conftest sets
BFABRICPY_CONFIG_ENV=__MOCKto avoid real credentials. - Use the pytest-mock
mockerfixture for all mocking — do notimport unittest.mock. - Group related tests with plain
class TestXyz:blocks (no base class needed), not# --- section ---comment banners. Move fixtures used by only one group inside its class. Keep method names specific enough to read on their own. - Integration tests live in a separate repository.
- Use TDD: write a failing test first, verify it fails, then fix the code, then verify the test passes.
- Line length is 120 (ruff and black). Ruff lint is currently only enforced on the
bfabricpackage (scripts, wrapper_creator, tests, noxfile are excluded via per-file-ignores). - basedpyright uses per-package baseline files at
.basedpyright/baseline.{package}.json— do not edit a baseline to silence a new error; fix the code or add a targeted# pyright: ignore[...]on the offending line. Baselines only exist to grandfather in pre-existing errors. - Docstrings use Sphinx
:param:/:raises:; Google-styleArgs:/Returns:blocks survive in a few older modules (e.g.entities/core/uri.py) — don't add more. - Keep docstrings at the lowest useful altitude: one summary line by default, plus a short paragraph only for a contract the signature cannot show (why this exists beside a similar function, a gotcha, an ordering constraint). Skip
Returns:blocks and:param:lines that restate the annotation, and don't restate a default the signature already shows. Do document what a value means — a sentinel's runtime resolution (Nonereads all results,Nonewrites to./output.yml) or a magic value (0= auto-assign) — phrased as "Nonedoes X", not "(default: X)". - Avoid
>>>examples: no session collects doctests, so they read as tested without being tested. Put a short example in the prose instead. - Exception: a cyclopts command function's docstring is its
--helptext — the summary line becomes the command description and each:param:line becomes that option's help (seebfabric-cli workunit not-available --help). That is user-facing copy: keep it complete and clear, and trim the internal helpers around it instead.
Each package has its own docs/changelog.md following Keep a Changelog: ### Added / ### Changed / ### Fixed / ### Removed subsections under ## \[Unreleased\]. Headings escape the brackets — nox -s changelog matches ## \[<version>\] literally.
- One or two lines per entry: the symbol, and the effect a user sees. No code blocks, no before/after examples, no "previously it did X because Y" — rationale belongs in the commit message.
- Label an entry Breaking only when the API has real external consumers; otherwise just describe it in its new shape.
- Collect dev-facing typing/tooling changes into a single trailing
Internal:bullet. - Release-candidate entries are the one exception to the subsections (flat bullets, cumulative) — see RELEASING.md.
The body is a short changelog-style bullet list — one line per change a user would notice, phrased "Add/Change/Fix X to do Y" — and then it stops. No ## section headings, no test plan or "Testing:" line, no background paragraph, no implementation notes. Omit internal refactors, docs, and tooling changes entirely; that detail belongs in the commit messages and the package changelog. If a caveat genuinely must be flagged, make it one more bullet, never a section.
rel-* release PRs get an empty body (--body ""): the pipeline publishes the changelog section as the release notes, so a body would only be a worse copy of it.
Each package is versioned and released independently — own pyproject.toml version, own docs/changelog.md, own <package>/<version> git tag. Release preparation is mechanics only (version bumps, changelog graduation, dependency-pin updates) and must not introduce code changes; see the rel-* rule under Branches.
The full procedure — the rel-* → release → main branch loop, the release-candidate and hotfix conventions, and how to verify a build offline before it ships — is in RELEASING.md. Read it before touching a release.
main— active developmentrelease— pushing to it runspublish_release.ymland publishes to PyPI. Release-prep PRs target this branch, notmain, and it is merged back intomainafterwards.rel-<date>-NN— release-preparation branches, cut fresh fromorigin/main, carrying only release mechanics so the release ships exactly what is already onorigin/main. Never merge feature or refactor work into one — new functionality lands onorigin/mainvia its own PR and is picked up by a later cut. A genuine hotfix, on the user's explicit say-so, is namedhotfix-*/patch-*instead so the intent is unambiguous.- When pushing, give the remote branch a reasonable, descriptive name even if the local branch has an auto-generated worktree name (e.g.
worktree-quiet-gathering-mitten) — push with an explicit remote ref:git push -u origin HEAD:feature/short-descriptive-name.