Developer Guide¶
This guide is for contributors to okapipy (and for anyone who wants to understand what's going on under the hood). If you're trying to use okapipy to build a client, the User Guide is the right place.
Getting set up¶
okapipy is managed with uv. Python is pinned to 3.13 via
.python-version because spaCy doesn't yet ship 3.14 wheels.
That installs the runtime dependencies plus the dev group (mypy, ruff,
pytest, pytest-cov, pytest-mock, pytest-httpserver, etc.).
The first NLP-dependent test run will download the spaCy
en_core_web_sm model (~12 MB) into ./.spacy/. Subsequent runs are
offline. To pre-warm:
Common commands¶
uv sync # install deps (incl. dev group)
# Tests
uv run pytest # full suite + coverage
uv run pytest tests/parser/test_X.py # single file
uv run pytest -k "name_of_test" # single test by substring
uv run pytest --no-cov # faster: skip coverage
# Type-check (strict on parser + generator only)
uv run mypy src tests
# Lint + format check
uv run ruff check src tests
uv run ruff format --check src tests
# CLI smoke
uv run okapipy --help
uv run okapipy spec parse tests/fixtures/<some>.yaml
pyproject.toml configures --cov=okapipy so plain pytest always emits
a coverage report; htmlcov/ and coverage.xml are written. The
parser package has a 90 % coverage minimum (currently sits ~94 %).
Project layout¶
src/okapipy/
├── app.py # entry point (typer)
├── parser/ # spec → APIModel tree (the "lift")
│ ├── api.py # public parse(...) entry
│ ├── loader.py # JSON/YAML loader, $ref inlining, base path detect
│ ├── nlp.py # spaCy pipeline, fetch_model, plural/verb detection
│ ├── extension.py # x-okapipy-* readers
│ ├── rules.py # Pydantic models for the rules file
│ ├── classifier.py # segment → kind decision logic
│ ├── builder.py # walks paths, mutates APIModel in place
│ ├── model.py # the APIModel / Namespace / Collection / ... Pydantic types
│ ├── dump.py # write APIModel to JSON / YAML
│ └── errors.py # ParserError hierarchy
├── generator/ # APIModel → Python project (the "emit")
│ ├── api.py # public generate(...) entry
│ ├── emit/ # one module per output surface (client, project, runtime, …)
│ ├── runtime/ # runtime helpers vendored into every generated client
│ ├── templates/ # Jinja templates for the generated code
│ ├── models.py # datamodel-code-generator integration
│ ├── manifest.py # _manifest.json (drift detection)
│ ├── edges.py # parent-child wiring metadata
│ ├── inline_schemas.py # schema name recovery from $refs
│ ├── templating.py # ChoiceLoader: user templates → packaged templates
│ ├── vfs.py # virtual filesystem + write_to_disk
│ └── errors.py # GenerationError hierarchy
└── cli/ # typer subcommands
├── __init__.py # `okapipy` root, dispatches to nlp / spec
├── nlp_cmd.py # `okapipy nlp ...`
├── spec_cmd.py # `okapipy spec ...`
└── console.py # rich-based stdout/stderr helpers
The split is intentional: the parser is a pure function from
(spec, rules) → tree. The generator is a pure function from
tree → virtual filesystem. The CLI glues them together, but anything you
can do from the CLI you can do programmatically by calling parse(...)
followed by generate(...).
Where to dig next¶
- Parser internals — the eight-phase pipeline, the classifier's precedence rules, and the NLP tricks (the "the X" wrapper, the verb registry).
- Generator internals — how the parser tree becomes a multi-file Python project, the base-vs-user lifecycle, the manifest + drift detection, and the templating layer.
House style¶
Conventions that are easier to follow if you know they're deliberate:
- No underscore-prefixed "private" functions. Module-internal helpers that aren't used outside the source file are fine; otherwise functions are public.
- No import aliases unless strictly necessary.
- All imports stay at the top of the file. Local imports inside functions are reserved for genuine cyclic-import or lazy-initialization cases.
from __future__ import annotationseverywhere. Type hints are mandatory in the parser; the generator follows suit.mypy --strictis enforced forokapipy.parser.*andokapipy.generator.*. Tests are looser.- Pydantic v2. No v1-style configs.
- Google-style docstrings on every public function. They drive the auto-generated Reference section of these docs.
Tests¶
- pytest functions only, no
unittest.TestCaseclasses. - Every test has a docstring explaining what it's verifying — not just restating the function name.
- Fixtures live in
tests/conftest.py, not inline in test files. Theenglish_nlpsession-scoped fixture loads the spaCy pipeline; an autouse fixture clears the in-process cache between tests so loader paths stay observable. - Mocking uses
pytest_mock(mockerfixture). Nofrom unittest.mock import .... - OpenAPI fixtures live under
tests/fixtures/.pytest-httpserverserves them over HTTP for URL-source tests.
Releasing¶
A new release is cut by publishing a GitHub release with a v<x.y.z>
tag. The release.yml workflow runs lint + type-check + tests, then
builds and publishes the sdist + wheel to PyPI via trusted publishing
(no token in the repo).
The same release event triggers the docs deployment (see
.github/workflows/docs.yml), which builds these docs with mkdocs and
pushes them to the gh-pages branch.