pyproject.toml
Rama main · réplica de lectura del repositorio original.
# 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)",
]