[project] name = "svcforge" version = "0.1.0" description = "X-as-a-Service control plane — reference implementation" requires-python = ">=3.12" dependencies = [ "svcforge-core", "fastapi>=0.115", "uvicorn[standard]>=0.32", "psycopg[binary,pool]>=3.2", "pyjwt[crypto]>=2.9", "httpx>=0.27", "croniter>=3.0", "tzdata>=2024.2", "structlog>=24.4", "prometheus-client>=0.21", "redis>=5.2", "typer>=0.15", "opentelemetry-api>=1.28", "opentelemetry-sdk>=1.28", "opentelemetry-instrumentation-fastapi>=0.49b0", "opentelemetry-instrumentation-psycopg>=0.49b0", ] [dependency-groups] dev = [ "pytest>=8.3", "pytest-asyncio>=0.24", "pytest-cov>=6.0", "mypy>=1.13", "ruff>=0.8", "hypothesis>=6.122", "pre-commit>=4.0", "testcontainers[postgres]>=4.9", "types-pyyaml>=6.0.12.20260518", ] [project.scripts] svcforge = "services.cli.main:app" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["services"] [tool.uv.sources] # editable here is a DEVELOPMENT convenience: source edits are visible without a # reinstall. The Dockerfiles deliberately override it with `uv sync --no-editable`, # because an editable install in an image resolves imports to /app/libs and ships a # path, not a package. svcforge-core = { path = "libs/svcforge_core", editable = true } [tool.ruff] line-length = 110 target-version = "py312" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "ANN", "S", "C4", "RUF"] [tool.ruff.lint.isort] # svcforge_core lives under libs/, so isort cannot infer it is ours. known-first-party = ["svcforge_core", "services"] [tool.ruff.lint.per-file-ignores] # Tests may assert, and fixtures shadow names by design. "tests/**" = ["S101"] # The e2e tests drive the real `kubectl`/`helm`/`pgrep` off PATH — that is the whole point # of them, and pinning absolute paths would make them pass on one machine only. Scoped to # this directory so S603/S607 keep guarding the application code, where `team` is tenant # input that reaches a helm release name. "tests/e2e/**" = ["S101", "S603", "S607"] [tool.bandit] # bandit is the belt to ruff's suspenders: ruff's `S` ruleset IS flake8-bandit and runs on # every file in the lint stage. These two skips are the rules ruff already enforces here, # where each real site carries an individually justified `# noqa` that bandit cannot see — # so bandit re-reports them as findings that ruff has already adjudicated. # # B608 (SQL built by string) == ruff S608. Every occurrence interpolates `_COLUMNS`, a # module constant. Tenant input goes through psycopg parameters, never the f-string, # and ruff fails the build if that ever changes. # B104 (bind 0.0.0.0) == ruff S104. A container must bind all interfaces; the pod's # network namespace is the boundary, not the listen address. # # Nothing else is skipped. If you add a third, justify it here or you are just turning the # gate off one rule at a time. skips = ["B608", "B104"] exclude_dirs = [".venv", "tests"] [tool.mypy] strict = true python_version = "3.12" warn_unreachable = true [[tool.mypy.overrides]] module = ["testcontainers.*", "croniter.*"] ignore_missing_imports = true # The OTLP exporter is an optional runtime dependency: in the cluster the API runs under # `opentelemetry-instrument`, which brings its own. obs.py imports it inside a try/except # and degrades to in-process traces without it, so mypy must not require it to be installed. [[tool.mypy.overrides]] module = ["opentelemetry.exporter.*"] ignore_missing_imports = true [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-q --strict-markers" asyncio_mode = "auto" markers = [ "slow: >1s", "e2e: needs a cluster and real helm", ]