[build-system] requires = ["setuptools>=68", "wheel"] build-backend = "setuptools.build_meta" [project] name = "infinito-nexus" version = "16.2.3" description = "Infinito.Nexus" readme = "README.md" requires-python = ">=3.11,<3.15" license = { file = "LICENSE.md" } dependencies = [ # Pinned: utils/recovery reads the generation layout from baudolo's own # BackupPaths, so the version here and in svc-bkp-volume-2-local must match. "backup-docker-to-local==7.1.1", "bcrypt", "cryptography", "humanfriendly", "PyMySQL", "psycopg2-binary", "ldap3", "PyYAML", "ruamel.yaml", ] [project.optional-dependencies] deploy = [ "ansible", "colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip", "tld", "passlib", "requests", ] dev = [ "ruff==0.17.3", "import-linter", "pre-commit", "git-maintainer-tools", "pytest>=9.1", "pytest-xdist", ] fuzz = [ "atheris==3.1.0", ] [project.scripts] infinito = "cli.__main__:main" [tool.setuptools] packages = { find = { where = ["."], include = [ "cli*", "plugins*", "utils*", "library*", ], exclude = [ "roles*", "assets*", "docs*", "templates*", "logs*", "tasks*", "tests*", "__pycache__*", ] } } include-package-data = true [tool.ruff] respect-gitignore = true target-version = "py311" exclude = [ "build", "dist", "*.egg-info", ".venv", "venv", ] [tool.ruff.lint] # Default ruff is `E4 + E7 + E9 + F`. We add: # - `I` (isort) — import order, auto-fixable # - `B` (flake8-bugbear) — likely-bug patterns (mutable defaults, etc.) # - `UP` (pyupgrade) — modernise syntax to the target_version above # - `RUF` (ruff-specific) — unused-noqa, ambiguous unicode, mutable class attrs, … # - `SIM` (flake8-simplify) — `if x: True else: False` → `bool(x)`, etc. # - `C4` (flake8-comprehensions) — `list(x for x in y)` → `[x for x in y]`, … # - `PERF` (perflint) — avoid try/except in hot loops, etc. # - `RET` (flake8-return) — unneeded `else` after `return`, redundant assigns # - `PIE` (flake8-pie) — small wins (no-op `pass`, redundant `dict()` calls) # - `T10` (flake8-debugger) — committed `breakpoint()` / `pdb` calls # - `PGH` (pygrep-hooks) — blanket `# noqa` / `# type: ignore` without code # - `EXE` (flake8-executable) — shebang/executable-bit consistency # - `RSE` (flake8-raise) — `raise X()` style # - `ICN` (flake8-import-conventions) — standard import aliases (`numpy as np`) # - `DTZ` (flake8-datetimez) — tz-naive `datetime.now()` / `utcnow()` bugs # - `TID` (flake8-tidy-imports) — banned modules / banned relative imports # - `LOG` (flake8-logging) — `logging` module misuse # - `G` (flake8-logging-format) — f-strings inside `logging.x()` calls (defeats lazy-format) # - `S` (flake8-bandit) — security (hardcoded creds, weak hashes, subprocess(shell=True), …) # - `PTH` (flake8-use-pathlib) — modernise `os.path.*` calls to `pathlib.Path` # - `FURB` (refurb) — modern Python idioms beyond UP/SIM/PIE/PERF # - `W` (pycodestyle warnings) — natural pair with `E`; trailing whitespace etc. # - `FA` (flake8-future-annotations) — consistent `from __future__ import annotations` # - `YTT` (flake8-2020) — correct `sys.version` / `sys.version_info` checks # - `A` (flake8-builtins) — shadowing built-in names # - `ISC` (flake8-implicit-str-concat) — accidental `"foo" "bar"` typos # - `SLOT` (flake8-slots) — require `__slots__` on `str`/`tuple`/`namedtuple` subclasses # - `FLY` (flynt) — `"{}".format(x)` → f-string # - `PYI` (flake8-pyi) — stub-hygiene rules (also fire on `.py`) # - `TC` (flake8-type-checking) — runtime-only imports out of `if TYPE_CHECKING:` blocks # - `N` (pep8-naming) — class/function/variable naming consistency # - `PLE0605` — `__all__` must be a `list`/`tuple` (not a single str) # - `PLW1510` — `subprocess.run(...)` without `check=` swallows non-zero exits # - `PLW2901` — loop variable rebound inside the body (typo/bug class) # - `PLW0108` — `lambda x: f(x)` → just `f` # - `PLW0603` — `global` statement (code smell) # - `PLR5501` — `else: if …` → `elif` (auto-fixable) # - `PLC0207` — `.split()`/`.rsplit()` should use `maxsplit=` when only the first/last segment is used # - `PLR1722` — `sys.exit(0)` over `exit(0)` (interpreter-only alias) # - `PLR1714` — collapse repeated `x == "a" or x == "b"` into `x in {"a", "b"}` # - `TRY002` — don't `raise Exception(...)` (use a specific class) # - `TRY004` — type-check failures must `raise TypeError`, not `ValueError` # - `TRY300` — put the success-path under `else:` so `except:` only sees the dangerous bit # - `TRY301` — don't `raise` inside `try:` for an exception caught by the same `except:` # - `BLE001` (flake8-blind-except) — catch-all `except Exception:`/bare `except:` (ratchet; see per-file-ignores) select = [ "E", "F", "I", "B", "UP", "RUF", "SIM", "C4", "PERF", "RET", "PIE", "T10", "PGH", "EXE", "RSE", "ICN", "DTZ", "TID", "LOG", "G", "S", "PTH", "FURB", "W", "FA", "YTT", "A", "ISC", "SLOT", "FLY", "PYI", "TC", "N", "PLE0605", "PLW1510", "PLW2901", "PLW0108", "PLW0603", "PLR5501", "PLC0207", "PLR1722", "PLR1714", "TRY002", "TRY004", "TRY300", "TRY301", "BLE001", ] # `ruff format` (run via `make autoformat`) reflows whatever it can. # E501 leftovers are content the formatter can't break (long string # literals, URLs / shell commands inside f-strings, comments). Trust # the formatter; don't fail CI on the unsplittable rest. # # RUF001/002/003 (ambiguous unicode in strings / docstrings / comments) # exist to catch homoglyph attacks, not English typography. The project # uses em/en-dashes deliberately and `ℹ` as a UI-hint glyph in CLI help # output; flagging those is noise. ignore = [ "E501", "RUF001", "RUF002", "RUF003", # S603 (subprocess call without `shell=False`) and S607 (start # process with partial path) are over-eager for a project whose # CLI invokes `make` / `git` / `docker` / `ansible-playbook` from # PATH using list-form args. The list-form already prevents shell # injection (Python's `subprocess` does not parse argv[0] as a # shell command), and resolving every binary's absolute path in # the source would couple the code to the host filesystem layout. # The genuinely dangerous shell-injection patterns (`shell=True`) # are still caught by S602/S605. "S603", "S607", # PTH207 (`glob.glob` → `Path.glob`/`rglob`) and PTH208 # (`os.listdir` → `Path.iterdir`) change the return shape: # `list[str]` → iterator of `Path`. Every call site needs per-site # review of how the result is consumed (string-path arithmetic, # `.startswith` / `.endswith` checks, plain string comparisons, # downstream APIs that require `str`). That work belongs to a # separate "iter-modernisation" follow-up commit, not this # use-pathlib pass. "PTH207", "PTH208", # SIM112 (capitalise env var names) conflicts with the deliberate # lowercase routing-knob surface: `make compose-deploy disable=x` # sets shell env `disable` via GNU Make's auto-export, and the # consumers read the same name. Forcing uppercase would split the # surface (`make … disable=x` writes lowercase, code reads # `DISABLE`) and break the make/env name parity. "SIM112", ] [tool.ruff.lint.per-file-ignores] # Test code legitimately uses patterns flake8-bandit flags as security # concerns in production code: # - S101 (`assert`) is the unittest idiom. # - S102 (`exec`) is used to load script-under-test by file path. # - S105/S106 (hardcoded passwords) appear in fixtures / dummy creds. # - S108 (hardcoded /tmp paths) appears in tempdir fixtures. # - S110/S112 (try/except pass/continue) appear in cleanup helpers. # - BLE001 (broad `except Exception`) is idiomatic in test fixtures / # cleanup helpers that must never abort the suite on teardown. "tests/**" = ["S101", "S102", "S105", "S106", "S108", "S110", "S112", "BLE001"] # Standalone scripts under ``roles/*/files/`` and ``utils/cert_utils.py`` # are exercised by test fixtures that monkey-patch the legacy ``os.*`` # surface (``os.makedirs``, ``os.stat`` with non-pathlib kwargs, # ``patch.object(sut, "os")`` replacements). Converting these call # sites to ``pathlib.Path`` would require rewriting every coupled # test in lockstep — out of scope for the use-pathlib pass. "roles/sys-ctl-rpr-container-hard/files/python/script.py" = ["PTH"] "roles/sys-ctl-rpr-container-soft/files/python/script.py" = ["PTH"] # BLE001 (flake8-blind-except) ratchet. # Broad `except Exception:` is now selected project-wide so NEW code is # gated. Standalone role scripts under ``roles/*/files/`` legitimately # swallow-and-continue for resilience, so the whole tree is exempt. The # production modules below predate the rule and are grandfathered pending # a focused burn-down; they should shrink, not grow. New production files # are gated. CodeQL (`py/empty-except`) still covers the empty-body case # everywhere, including the exempt paths. "roles/**" = ["BLE001"] "cli/administration/deploy/container/command.py" = ["BLE001"] "cli/administration/deploy/dedicated/apps.py" = ["BLE001"] "cli/administration/deploy/dedicated/command.py" = ["BLE001"] "cli/administration/deploy/development/inventory/legacy_resolver.py" = ["BLE001"] "cli/administration/deploy/development/proc.py" = ["BLE001"] "cli/administration/inventory/credentials/vault.py" = ["BLE001"] "cli/administration/inventory/devices/command.py" = ["BLE001"] "cli/administration/inventory/provision/cli.py" = ["BLE001"] "cli/administration/inventory/provision/credentials_generator.py" = ["BLE001"] "cli/administration/inventory/provision/host_vars.py" = ["BLE001"] "cli/administration/inventory/provision/services_disabler.py" = ["BLE001"] "cli/administration/inventory/validate/loaders.py" = ["BLE001"] "cli/contributing/mirror/sync/__main__.py" = ["BLE001"] "cli/core/help.py" = ["BLE001"] "cli/meta/ci/roles/runtime/__main__.py" = ["BLE001"] "cli/meta/j2/compiler/__main__.py" = ["BLE001"] "cli/meta/roles/applications/all/__main__.py" = ["BLE001"] "cli/meta/roles/applications/in_group_deps/__main__.py" = ["BLE001"] "cli/meta/roles/applications/invokable/__main__.py" = ["BLE001"] "cli/meta/roles/applications/type/__main__.py" = ["BLE001"] "cli/meta/roles/lifecycle/__main__.py" = ["BLE001"] "cli/meta/roles/services/called/__init__.py" = ["BLE001"] "library/docker_bridge_gateway.py" = ["BLE001"] "plugins/filter/active_docker.py" = ["BLE001"] "plugins/filter/docker/service_enabled.py" = ["BLE001"] "plugins/filter/dotenv.py" = ["BLE001"] "plugins/filter/get/all/application_ids.py" = ["BLE001"] "plugins/filter/role_path_by_app_id.py" = ["BLE001"] "plugins/filter/split_postgres_connections.py" = ["BLE001"] "plugins/lookup/addon_env_flags.py" = ["BLE001"] "plugins/lookup/applications_current_play.py" = ["BLE001"] "plugins/lookup/image.py" = ["BLE001"] "plugins/lookup/email.py" = ["BLE001"] "plugins/lookup/prometheus_integration_active.py" = ["BLE001"] "plugins/lookup/version.py" = ["BLE001"] "utils/cache/base.py" = ["BLE001"] "utils/nexus/list_apt_proxy_repos.py" = ["BLE001"] "utils/roles/applications/in_group_deps.py" = ["BLE001"] "utils/roles/applications/services/resources.py" = ["BLE001"] "utils/roles/dependency_resolver.py" = ["BLE001"] "utils/roles/validation/invokable.py" = ["BLE001"] "utils/roles/validation/resources.py" = ["BLE001"] "utils/templating/ansible.py" = ["BLE001"] "utils/cert_utils.py" = ["PTH", "BLE001"] # N999 (invalid module name) on Ansible-invoked role-level scripts # whose filenames contain hyphens. Renaming to snake_case would break # the Ansible meta references that source them via the literal path, # so the hyphenated filename is locked in by the surrounding stack. # `include_r` would be the canonical lowercase form per N806, but the # matrix-deploy code consistently writes the round index as ``R`` in # its formulas (``R if R < count else 0``) and the variable name # ``include_R`` reads as "round R's include set" right next to those # formulas. The uppercase R here is documentation, not a constant. "cli/administration/deploy/development/deploy.py" = ["N806"] "cli/administration/deploy/development/init.py" = ["N806"] "cli/administration/deploy/development/inventory.py" = ["N806"] "tests/unit/python/cli/administration/deploy/development/test_inventory.py" = ["N806"] # Lazy-loaded class binding: ``ValueGenerator = _value_generator_cls()`` # rebinds the imported class inside the function. PEP 8 keeps the # UpperCamelCase form for class identifiers, including local re-binds. "plugins/filter/value_generator.py" = ["N806"] # --------------------------------------------------------------------------- # import-linter — architectural import contracts (run via `lint-imports`, # enforced by tests/lint/filesystem/python/test_import_contracts.py). # --------------------------------------------------------------------------- [tool.importlinter] root_package = "cli" [[tool.importlinter.contracts]] # The dev/CI deploy package was repeatedly hit by CodeQL # `py/unsafe-cyclic-import`. The fix split the env/arg helpers into the # leaf module `env`, giving the load-time DAG compose -> common -> env. # This layers contract locks that order in: a lower layer importing a # higher one (i.e. a new cycle) breaks the build. name = "cli.administration.deploy.development stays acyclic (compose -> common -> env)" type = "layers" layers = [ "cli.administration.deploy.development.compose", "cli.administration.deploy.development.common", "cli.administration.deploy.development.env", ] # Type-checking-only / lazily-deferred back-references to `compose` do # not run at import time, so they cannot form a load-time cycle. ignore_imports = [ "cli.administration.deploy.development.common -> cli.administration.deploy.development.compose", ]