# Root of the epicurus uv workspace. # # This root is a *virtual* project (package = false): it is not built or # published. It exists to pin shared dev tooling and tool config for every # workspace member, and to declare which members make up the monorepo. # # Add new packages under libs/ (shared) or services/ (deployable modules); the # member globs below auto-discover them. [project] name = "epicurus" version = "0.0.0" description = "Self-hosted, modular, local-first personal-assistant platform." requires-python = ">=3.11" dependencies = [] [tool.uv] package = false [tool.uv.workspace] members = ["libs/*", "services/*"] # services/web is the Node/TypeScript web shell — not a Python package. exclude = ["services/web"] [dependency-groups] dev = [ # Lint/type gates are pinned exact: `uv run mypy` / `uv run ruff` must match CI # byte-for-byte. With a floor (`>=`) any `uv lock` re-resolve floats the tool # upward and the bump rides invisibly inside an unrelated PR's lockfile — a # green-local/red-CI split (#514: mypy 1.13 -> 2.1.0 flagged new no-any-return). # Bump these deliberately, in their own chore PR. "ruff==0.16.8", "mypy==2.3.1", "pytest>=9.1.1", "pytest-asyncio>=0.24", "pytest-timeout>=2.3", "aiosqlite>=0.20", "httpx>=0.27", "testcontainers[minio]>=4.15.0", "cookiecutter>=2.5", "pre-commit>=4.6.1", # tests/test_docker_proxy_allowlist.py (#726) parses `docker compose config` output — # already resolved transitively (uvicorn[standard]), made an explicit direct # dependency since the test takes a real, load-bearing dependency on it. "pyyaml>=6.0", "types-python-dateutil>=2.9.0.20260716", "types-pyyaml>=6.0.12.20260815", ] [tool.ruff] line-length = 100 target-version = "py311" src = ["libs", "services"] # Cookiecutter templates contain Jinja, not valid Python — don't lint them. extend-exclude = ["templates"] [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM", "C4", "RUF"] [tool.ruff.lint.isort] known-first-party = ["epicurus_core", "epicurus_core_app", "epicurus_echo", "epicurus_storage", "epicurus_knowledge", "epicurus_websearch", "epicurus_calendar", "epicurus_mail", "epicurus_tasks", "epicurus_notes", "epicurus_messaging"] [tool.mypy] strict = true python_version = "3.11" pretty = true # The single source of truth for what gets type-checked: `uv run mypy` (no # args) reads this. Register every new workspace package here — its `src` in # both `mypy_path` and `files`, its `tests` in `files` only — and in ruff's # known-first-party above. `task new-module` wires all of it for you. # # `files`, not `packages`: mypy's own CLI rejects combining `packages` with # `files`/`modules` in one run ("May only specify one of: module/package, # files, or command"), and `packages` mode requires each entry to be a real # importable package (`__init__.py`) — most `tests/` dirs deliberately lack # one, since pytest's importlib import-mode is what lets several services # each ship their own `test_service.py` without colliding (see AGENTS.md). # `explicit_package_bases` + `namespace_packages` make mypy treat every listed # `tests/` dir as its own namespace package rooted at that dir, so same-named # test files across services get distinct qualified module names # (`services.mail.tests.test_service` vs `services.tasks.tests.test_service`) # instead of colliding on a bare `test_service`. `mypy_path` must list the # same `src` dirs so a file reached directly (via `files`) and the same file # reached via import (e.g. a test doing `from epicurus_tasks... import`, # resolved through the editable install on sys.path) resolve to the *same* # module name — otherwise mypy reports the source file "found twice under # different module names". explicit_package_bases = true namespace_packages = true mypy_path = [ "libs/epicurus-core/src", "services/calendar/src", "services/core-app/src", "services/echo/src", "services/knowledge/src", "services/mail/src", "services/messaging/src", "services/notes/src", "services/storage/src", "services/tasks/src", "services/websearch/src", ] # services/web is the Node/TypeScript web shell — excluded (see [tool.uv.workspace]). files = [ "libs/epicurus-core/src", "libs/epicurus-core/tests", "services/calendar/src", "services/calendar/tests", "services/core-app/src", "services/core-app/tests", "services/echo/src", "services/echo/tests", "services/knowledge/src", "services/knowledge/tests", "services/mail/src", "services/mail/tests", "services/messaging/src", "services/messaging/tests", "services/notes/src", "services/notes/tests", "services/storage/src", "services/storage/tests", "services/tasks/src", "services/tasks/tests", "services/websearch/src", "services/websearch/tests", "scripts", "tests", ] # Third-party packages without a py.typed marker; don't fail strict on them. [[tool.mypy.overrides]] module = ["prometheus_client.*", "nats.*", "hvac.*", "litellm.*", "qdrant_client.*", "docker.*", "py_vapid.*", "pywebpush.*"] ignore_missing_imports = true [[tool.mypy.overrides]] module = ["aioboto3.*", "aiobotocore.*", "botocore.*"] ignore_missing_imports = true # link_ingest's extraction stack (#739): neither ships a py.typed marker. [[tool.mypy.overrides]] module = ["trafilatura.*", "yt_dlp.*"] ignore_missing_imports = true # scripts/new_module.py and its test drive the cookiecutter template renderer (#845 — pulled # into the strict gate along with root tests/); cookiecutter ships no py.typed marker either. [[tool.mypy.overrides]] module = ["cookiecutter.*"] ignore_missing_imports = true [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["libs", "services", "tests"] # importlib mode: modules across services can share a test filename (e.g. several # test_service.py) without colliding — no tests/__init__.py needed or wanted. addopts = "-q --import-mode=importlib" # Per-test deadline: a deadlocked async test (e.g. an await inside `except # CancelledError`, or a task cancelled before its first step — see AGENTS.md) # fails with a dumped stack instead of hanging the gate. Healthy tests run in # well under a second, so 60s only ever trips a genuine hang. `thread` is the # only portable method (signal/SIGALRM is POSIX-only; dev runs on Windows). # Integration tests boot testcontainers — a cold image pull can exceed this — so # conftest.py lifts their budget (they carry no per-test `@timeout` of their own). timeout = 60 timeout_method = "thread" markers = [ "integration: requires Docker (spins ephemeral containers via testcontainers)", ]