Skip to content

chore: migrate dependency management to uv #149

Description

@williaby

Migrate dependency management to uv

This issue tracks consolidating williaby/ledgerbase from its current
triple-state dependency management (Poetry + generated requirements.txt +
uv-already-invoked-in-CI) down to a single uv source of truth: a committed
uv.lock plus a PEP 621 [project] table and PEP 735 [dependency-groups].

This is a planning issue. Execution must follow the local clone, branch,
signed commit, PR workflow (the repo enforces signed commits and branch
protection; the GitHub Contents API cannot be used for commits).

1. Current state (verified)

The repo is in the messiest possible dual/triple state:

  • pyproject.toml uses the legacy [tool.poetry] format only. There is NO
    PEP 621 [project] table and NO [dependency-groups].
  • poetry.lock is committed (lock metadata python-versions = ">=3.10").
  • poetry.toml configures an in-project .venv and no-root = true.
  • requirements.txt and dev-requirements.txt are committed and are
    GENERATED artifacts: generate_requirements.sh runs
    poetry export --without-hashes to produce them, and
    prepare-poetry.yml regenerates dev-requirements.txt the same way.
  • uv is ALREADY invoked in three CI workflows against a repo that has no
    uv project layout:
    • .github/workflows/codeql.yml: uv sync --no-dev
    • .github/workflows/fips-compatibility.yml: uv sync --frozen (also
      uv python install 3.12, uv run ...)
    • .github/workflows/slsa-provenance.yml: uv build
  • .github/workflows/ci.yml delegates to the org reusable
    ByronWilliamsCPA/.github/.github/workflows/python-ci.yml, which expects a
    uv project layout (a committed uv.lock).
  • Dockerfile installs via Poetry (COPY pyproject.toml poetry.lock,
    poetry install --no-root, CMD ["poetry", "run", "flask", ...]).
  • Pre-commit (.pre-commit-config.yaml) drives every local gate through
    poetry run ... (ruff, nox sessions for mypy/bandit/semgrep/vulture,
    pip-audit).
  • noxfile.py is large (about 49 KB) and is the local task runner; nox is
    also a runtime dependency in pyproject.
  • No renovate.json / .github/renovate.json and no dependabot.yml
    present, so there is no bot manager config to reconcile right now.

2. Which mechanism is authoritative today

Poetry is authoritative. requirements.txt and dev-requirements.txt
are derived from poetry.lock via poetry export. The uv references in CI
are aspirational and currently cannot succeed: uv sync requires a PEP 621
[project] table or a committed uv.lock, and the repo has neither. Those
three uv jobs (codeql, fips-compatibility, slsa-provenance) are therefore
either failing or not resolving the intended dependency set. This is the
single most important correctness reason to finish the migration rather than
leave it half-done.

3. Version drift between the three mechanisms (DO NOT assume they agree)

The committed requirements.txt does NOT match the committed poetry.lock.
Confirmed conflicts (poetry.lock value vs requirements.txt value):

Package poetry.lock requirements.txt Conflict
cryptography 44.0.2 46.0.7 YES, major drift
requests 2.32.3 2.33.0 YES
marshmallow 3.26.1 3.26.2 YES
python-dotenv 1.1.0 1.2.2 YES
packaging (pin per lock) 24.2 check during reconcile

requirements.txt also carries packages not in the Poetry main group
(for example peewee, full opentelemetry-* stack), indicating it was
generated at a different time or with a different resolution than the current
poetry.lock. Treat requirements.txt as STALE. The reconciliation step
below resolves which versions win; do not blindly trust either file.

Additional inconsistency: pyproject.toml declares
python = ">=3.11,<4.0", but poetry.lock metadata says
python-versions = ">=3.10". The house standard is requires-python = ">=3.10".
This three-way Python-version disagreement must be resolved during migration.

4. App vs package; target Python

  • Type: deployable application (Flask web app), but it DOES ship an
    importable package.
    src/ledgerbase/ is a real package
    (__init__.py, wsgi.py, config.py, etc.) and [tool.poetry] declares
    packages = [{ include = "ledgerbase", from = "src" }]. There are also
    sibling source trees: src/cli, src/etl, src/flask, src/schema,
    src/scripts, src/services.
  • Because it is run as an app (gunicorn / flask run in the Dockerfile) and
    is not published as a consumed library, a build backend is only needed if a
    wheel is actually built. NOTE: slsa-provenance.yml currently runs
    uv build, which DOES require a build backend. Decision required: either
    keep building a wheel (add hatchling as the build backend per house
    standard, replacing poetry-core) or drop uv build from the SLSA
    workflow if no wheel is consumed. Recommendation: keep hatchling so the
    existing SLSA provenance flow and release.yml continue to function.
  • Target Python: set requires-python = ">=3.10" per house standard.
    This widens the current >=3.11 floor; confirm nothing in the codebase
    uses 3.11-only syntax before committing the lower floor. If 3.11 features
    are in use, document the deviation and keep >=3.11.

5. Dependencies that complicate uv lock

  • Assured OSS private index. pyproject.toml declares
    [[tool.poetry.source]] for assured-oss
    (https://us-python.pkg.dev/cloud-aoss/cloud-aoss-python/simple) as the
    PRIMARY source with PyPI supplemental, and CI authenticates via
    keyrings.google-artifactregistry-auth and GCP_SA_JSON. uv has no
    [tool.poetry.source]; this must be re-expressed as
    [[tool.uv.index]] (with default/priority semantics) plus credential
    handling through UV_INDEX_* env vars or keyring. This is the single
    biggest migration risk: a misconfigured index makes uv lock resolve from
    the wrong registry or fail to authenticate.
  • Native / C-extension deps: cryptography, psycopg[binary] (extras),
    ruamel-yaml-clib, greenlet. uv resolves wheels fine, but the platform
    markers in the current requirements.txt (aarch64/x86_64, CPython-only,
    win32 tzdata) must be allowed to regenerate naturally under uv lock; do
    not hand-port them.
  • Heavy / unusual deps: semgrep (large, pulls many transitive deps),
    plaid-python (pinned 30.0.0), keyring + the Google artifact registry
    keyring backend.
  • Tooling-as-runtime smell: nox is listed under
    [tool.poetry.dependencies] (runtime) AND [tool.poetry.group.dev].
    During migration, nox belongs in the dev dependency group only (or
    stays a system tool), not in [project.dependencies].

6. CI, pre-commit, and container surfaces that must switch to uv

Surface Current Target
ci.yml -> org python-ci.yml expects uv.lock provide committed uv.lock
codeql.yml uv sync --no-dev (broken, no lock) works once uv.lock exists
fips-compatibility.yml uv sync --frozen (broken, no lock) works once uv.lock exists
slsa-provenance.yml uv build keep; requires hatchling build backend
prepare-poetry.yml installs Poetry, poetry export remove or replace with a uv setup
security-pip-audit.yml audits requirements.txt + dev-requirements.txt audit via uv export or uv pip compile output, or pip-audit against the uv env
other poetry-referencing workflows (gh-pages, license, release, sbom, security-semgrep/snyk/trivy, weekly-check, template workflows) poetry ... migrate each to uv sync / uv run
.pre-commit-config.yaml every hook uses poetry run ... switch to uv run ...
Dockerfile Poetry install + poetry run flask uv sync --frozen + uv run (or gunicorn entrypoint)
Makefile poetry hint in setup target update to uv
generate_requirements.sh poetry export delete (uv.lock replaces it)

7. Monorepo / workspace

Single pyproject.toml at root. Multiple source trees live under src/
(ledgerbase, cli, etl, flask, schema, schema, services,
scripts), but they are one project, not separate distributions. No uv
workspace needed unless these are later split. Flag for confirmation during
execution.

8. What is missing for full uv adoption

  1. A PEP 621 [project] table (name, version, requires-python,
    dependencies).
  2. A [dependency-groups] table with a dev group (PEP 735).
  3. A committed uv.lock.
  4. uv-native index config ([[tool.uv.index]]) replacing
    [[tool.poetry.source]], with Assured OSS auth wired through uv.
  5. A chosen build backend (hatchling) if uv build / wheel publishing is
    retained.
  6. Removal of [tool.poetry], poetry.toml, poetry.lock,
    requirements.txt, dev-requirements.txt, generate_requirements.sh,
    and prepare-poetry.yml once uv.lock is the source of truth.

9. Gotchas to honor during execution

  • Renovate: none configured today. If a renovate.json is added later,
    the Python manager MUST be "pep621", never "uv" (Renovate silently
    rejects the whole config otherwise). Run renovate-config-validator after
    any change.
  • Signed commits + branch protection: executor must use local clone,
    branch, signed commit, PR. No Contents API commits.
  • AOSS auth in CI: the migration must keep Assured OSS reachable for
    uv lock / uv sync in CI, or resolution will fall back to PyPI or fail.

Ordered execution checklist

  1. Read ByronWilliamsCPA/cookiecutter-python-template/pyproject.toml as the
    target layout reference.
  2. Add a PEP 621 [project] table: name, version,
    requires-python = ">=3.10" (confirm no 3.11-only syntax first), and move
    the current [tool.poetry.dependencies] runtime deps into
    [project.dependencies] (drop nox from runtime).
  3. Add [dependency-groups] with a dev group containing the current
    [tool.poetry.group.dev.dependencies].
  4. Replace [[tool.poetry.source]] with [[tool.uv.index]] for Assured OSS
    (primary) and PyPI, and wire credentials via UV_INDEX_* / keyring.
  5. Set the build backend: [build-system] requires = ["hatchling"],
    build-backend = "hatchling.build", plus [tool.hatch.build.targets.wheel]
    packages = ["src/ledgerbase"] (retain uv build / SLSA flow).
  6. Remove all [tool.poetry*] tables and poetry.toml.
  7. Reconcile version drift: run uv lock and treat its resolution as the new
    truth; explicitly review the cryptography 44 vs 46, requests, marshmallow,
    python-dotenv, packaging conflicts and pin intentionally where needed.
  8. Commit the generated uv.lock.
  9. Migrate .pre-commit-config.yaml hooks from poetry run to uv run.
  10. Migrate Dockerfile to uv sync --frozen + uv run (or gunicorn).
  11. Migrate every poetry-referencing workflow to uv; delete
    prepare-poetry.yml; update security-pip-audit.yml to audit the uv
    environment.
  12. Verify codeql.yml, fips-compatibility.yml, slsa-provenance.yml, and
    the org python-ci.yml path all pass with the committed uv.lock.
  13. Delete requirements.txt, dev-requirements.txt, generate_requirements.sh,
    and poetry.lock.
  14. Update Makefile setup hint and README.md install docs to uv.
  15. Run pre-commit run --all-files; open a signed-commit PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions