Ctrl K
ComunidadGuías…

pyproject.toml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#
# pyproject.toml
# Copyright (C) 2026 Gill-Bates http://github.com/Gill-Bates
#
# Single source of truth for the project version, its dependencies and the
# lint/test tooling. Replaces the former VERSION, requirements.txt and
# docs/requirements-docs.txt files.
#

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "wirebuddy"
# The one place the release version lives. app/utils/version.py reads it back
# at runtime, the release workflow validates the git tag against it, and
# docker/Dockerfile copies this file into the image so the About page reports
# the same number the tag was cut from.
version = "1.7.0"
description = "Lightweight WireGuard Management WebUI"
readme = "README.md"
requires-python = ">=3.13"
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "Gill-Bates", email = "contact@wirebuddy.example.com" }]
keywords = ["wireguard", "vpn", "fastapi", "dns", "self-hosted"]
classifiers = [
    "Environment :: Web Environment",
    "Framework :: FastAPI",
    "Intended Audience :: End Users/Desktop",
    "Operating System :: POSIX :: Linux",
    "Programming Language :: Python :: 3.13",
    "Topic :: System :: Networking",
]

# Everything the app imports directly. Every entry carries an exact pin, so
# the published image and a local install always resolve to the same
# versions.
dependencies = [
    "aiosqlite==0.22.1",
    "cbor2==6.1.5",
    "cryptography==50.0.2",
    "email-validator==2.3.0",
    "fastapi==0.162.0",
    "geoip2==5.3.0",
    "httpx[http2]==0.28.1",
    "jinja2==3.1.6",
    "markdown==3.11.2",
    "nh3==0.3.7",
    "Pillow==13.0.0",
    "pydantic==2.14.1",
    "pydantic-settings==2.16.1",
    "pyotp==2.10.0",
    "python-multipart==0.0.32",
    "qrcode[pil]==8.2",
    "uvicorn[standard]==0.54.0",
    "webauthn==3.0.1",
]


[project.optional-dependencies]
dev = [
    "pytest",
    "pytest-asyncio",
    "pytest-cov",
    "ruff==0.17.3",
]
docs = [
    "markdown",
    "mkdocs",
    "mkdocs-git-revision-date-localized-plugin",
    "mkdocs-material",
    "mkdocs-minify-plugin",
    "mkdocs-redirects",
    "pymdown-extensions",
]

[project.urls]
Homepage = "https://github.com/Gill-Bates/wirebuddy"
Documentation = "https://gill-bates.github.io/wirebuddy/"
Source = "https://github.com/Gill-Bates/wirebuddy"
Changelog = "https://github.com/Gill-Bates/wirebuddy/blob/main/CHANGELOG.md"

# The repo is a flat-layout application, not a library, and it declares no
# console script: `pip install -e .` is a convenience for local dev that pulls
# in the dependencies and puts the source tree on sys.path. The Docker image
# installs the dependencies alone and runs the copied source tree.
#
# Discovery rather than an explicit list, because none of these directories
# carries an __init__.py at every level - they are namespace packages, and an
# explicit `packages = ["app"]` risks silently dropping a subpackage the
# discovery would otherwise find. The excludes keep the two asset trees out:
# they hold no Python, and discovery would otherwise declare them as empty
# namespace packages.
[tool.setuptools]
include-package-data = false

[tool.setuptools.packages.find]
include = ["app*"]
exclude = ["app.static*", "app.templates*"]

# --------------------------------------------------------------------------- #
# Ruff - lint + format for every Python file in the repo
# --------------------------------------------------------------------------- #
[tool.ruff]
line-length = 160
target-version = "py313"
# A different local ruff must not silently produce other results than the CI gate.
required-version = "==0.17.3"
extend-exclude = ["data", "dev-data", "site", ".mkdocs-cache"]

[tool.ruff.lint]
select = [
    "A",     # flake8-builtins - shadowing open/id/type hides real bugs
    "ARG",   # unused arguments
    "ASYNC", # blocking calls inside async def - this app is ASGI end to end
    "B",     # bugbear
    "C4",    # comprehensions
    "D",     # pydocstyle - Google-style docstrings (see [tool.ruff.lint.pydocstyle])
    "DTZ",   # naive datetimes; timestamps are UTC-aware on purpose
    "E",     # pycodestyle errors
    "EM",    # exception messages assigned to a name first
    "F",     # pyflakes
    "FAST001", # redundant response_model in the route decorator
    "FAST003", # path parameter missing from the handler signature
    "G",     # logging-format: %s args, never f-strings (lazy formatting)
    "I",     # import sorting
    "ICN",   # import conventions
    "ISC",   # implicit string concat
    "LOG",   # logging misuse
    "N",     # pep8-naming
    "PERF",  # obvious performance traps
    "PIE",   # misc lints
    "PL",    # pylint subset
    "PTH",   # prefer pathlib
    "RET",   # return simplifications
    "RSE",   # raise cleanups
    "RUF",   # ruff-specific
    "S",     # bandit - this app parses untrusted input and shells out to wg/unbound
    "SIM",   # simplifications
    "SLF",   # private member access across modules
    "T20",   # stray print()
    "TRY",   # exception antipatterns
    "UP",    # pyupgrade
    "W",     # pycodestyle warnings
]
ignore = [
    # Formatter-owned; keeping this on fights `ruff format`.
    "ISC001",
    # Further rules Ruff documents as conflicting with `ruff format`.
    "E111",
    "E114",
    "E117",
    "D300",
    # The codebase is indented with tabs on purpose (see AGENTS.md); this is a
    # style choice, not a defect.
    "W191",
    # Tab indentation makes E101 (mixed spaces/tabs at line start) fire on
    # otherwise-consistent code; the formatter is the source of truth here.
    "E101",
    # Same tab decision one layer down: D206 wants docstring continuation lines
    # indented with spaces, which `ruff format` with indent-style = "tab" undoes
    # on the next run. Ruff itself lists it as formatter-incompatible.
    "D206",
    # The *presence* of docstrings is not gated. What this codebase documents is
    # the non-obvious: the modules carry a header block, and the functions whose
    # behaviour needs explaining carry a Google-style docstring, while the
    # single-statement DB accessors and route handlers whose name already says
    # everything do not. Turning these on would demand ~270 new docstrings whose
    # only content would be a restatement of the signature, which is how a
    # docstring becomes something readers learn to skip. The D2xx/D4xx rules stay
    # enabled, so every docstring that *is* written has to be well-formed.
    "D100",
    "D101",
    "D102",
    "D103",
    "D104",
    "D105",
    "D106",
    "D107",
    # Exception messages are inlined rather than assigned to a name first;
    # matches the existing codebase style throughout app/.
    "EM101",
    "EM102",
    "TRY003",
    # try/except -> job/task status transitions read better inline than
    # split across helper functions for this branchy pipeline code.
    "TRY300",
    "TRY301",
    "B904",
    # Deliberate lazy imports (heavy or optional dependencies, import cycles)
    # across app/ (main, server, tasks, api, utils, node).
    "PLC0415",
    # Package-level singletons (db connections, schedulers, caches) are
    # rebound by lifespan hooks and module-level setup on purpose.
    "PLW0603",
    # Thresholds and HTTP status codes read fine inline; naming every 200/404
    # would add indirection without adding meaning.
    "PLR2004",
    # The DNS/WireGuard/multi-node orchestration code is genuinely branchy;
    # splitting it to satisfy a counter would hide the sequence it documents.
    "PLR0911",
    "PLR0912",
    "PLR0913",
    "PLR0915",
    "PLR0917",
    # Every occurrence is an isinstance() check on *deserialised external input* -
    # enrollment token payloads, WebAuthn clientDataJSON, a node's persisted state
    # file, a master's HTTP response. A wrong type there is a data-validation
    # failure, not a caller passing the wrong argument, so ValueError (or
    # RuntimeError for corrupted state) is the right class and the callers catch
    # exactly that. app/api/passkeys.py even converts TypeError *into* ValueError
    # two lines above one of these checks; app/utils/node_token.py raises
    # ValueError for all three ways one token field can be malformed. TRY004
    # would split those into two exception classes for no gain.
    "TRY004",
    # A `timeout` parameter on an async function is deliberate throughout: these
    # are wrappers that hand the value to the underlying primitive, and for
    # app/utils/subprocess.py it also selects the SIGTERM-then-kill escalation
    # that `asyncio.timeout()` at the call site cannot express (see
    # tests/test_subprocess_limits.py). Pushing it to callers would be a wide API
    # change that loses behaviour.
    "ASYNC109",
]

# The prose uses real typography: EN DASH in docstrings, log messages and UI
# strings, MULTIPLICATION SIGN in comments. Every other homoglyph is still flagged.
allowed-confusables = ["–", "×"]

[tool.ruff.lint.flake8-bugbear]
# `Depends(...)` / `File(...)` in defaults *is* the FastAPI DI contract; B008 stays
# active for every other call in a default argument.
extend-immutable-calls = [
    "fastapi.Depends",
    "fastapi.File",
    "fastapi.Form",
    "fastapi.Query",
    "fastapi.Body",
    "fastapi.Header",
    "fastapi.Cookie",
    "fastapi.Path",
    "fastapi.Security",
]

[tool.ruff.lint.per-file-ignores]
# The one module that is deliberately *not* UTC: it schedules the speedtest for a
# wall-clock hour in the operator's own timezone ("run at 2am local"), and
# time.mktime() on a naive datetime is what applies the system's DST rules. Making
# these aware would move the window whenever DST shifts, which is the opposite of
# the intent. DTZ stays enabled everywhere else, where timestamps really are
# UTC-aware (see app/utils/time.py and tests/test_time.py).
"app/utils/speedtest_window.py" = ["DTZ001", "DTZ005", "DTZ006"]
"tests/*" = [
    "S603",
    "S608",
    # Stub signatures have to match what the real callable is invoked with,
    # so monkeypatch lambdas routinely take arguments they ignore.
    "ARG001",
    "ARG002",
    "ARG005",
    "PLR2004",
    "PTH101",
    "RUF012",
    "S101",
    "S105",
    "S106",
    "S107",
    "S108",
    "SIM117",
    "SLF001",
    "T20",
]
"tools/*.py" = ["T20"]
# This application's job is driving `wg`, `ip`, `iptables` and `conntrack`. S603
# fires on every direct subprocess call and cannot be satisfied while that remains
# true, so it is exempt only in the modules that make such calls. The properties it
# stands in for are enforced instead: there is no `shell=True` anywhere in app/,
# every call passes an argument *list*, and the privileged binaries are resolved to
# absolute paths from fixed allowlists rather than PATH. S607 stays enabled
# everywhere, which is what catches a binary invoked by bare name. A new module
# calling subprocess directly is flagged and has to justify itself.
"app/utils/conntrack.py" = ["S603"]
"app/node/firewall.py" = ["S603"]
"app/node/wg_manager.py" = ["S603"]
"app/api/frontend_pages.py" = ["S603"]
# Dynamic SQL here only ever interpolates *identifiers* the code itself owns -
# column lists built from literals like `"username = ?"`, a generated `?,?,?`
# placeholder string, table names from module-level allowlists. Every value is
# bound, never formatted in, and SQL parameters cannot express a dynamic column
# list. Exempt only in the DB layer and the backup restore; S608 stays on
# everywhere else, where a formatted query would be a real injection risk.
"app/db/*.py" = ["S608"]
"app/api/backup.py" = ["S608"]
# Fullwidth digits are the input under test (NFKC normalisation of MFA codes).
"tests/test_mfa_code_normalization.py" = ["RUF001"]

[tool.ruff.lint.pydocstyle]
# Google style is what the tree already uses: a one-line summary, a blank line,
# then `Args:` / `Returns:` / `Raises:` sections. Setting the convention also
# switches off the D rules that contradict it (D203, D213, D215, D400, D401,
# D404, D406-D409, D413), so only one docstring shape is ever demanded.
convention = "google"

[tool.ruff.format]
quote-style = "double"
indent-style = "tab"
line-ending = "lf"

# --------------------------------------------------------------------------- #
# pytest
# --------------------------------------------------------------------------- #
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
pythonpath = ["."]
python_files = ["test_*.py"]
addopts = "-q --strict-markers --strict-config"

[tool.coverage.run]
source = ["app"]
branch = true
omit = ["*/__pycache__/*"]

[tool.coverage.report]
show_missing = true
skip_covered = true
exclude_lines = [
    "pragma: no cover",
    "if TYPE_CHECKING:",
    "raise NotImplementedError",
]