mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-07 06:27:42 +00:00
Merge pull request #22 from o3LL/refactor/routes-email-subpackage
refactor(routes): move the email modules into routes/email/
This commit is contained in:
commit
eef390b9ae
16 changed files with 11127 additions and 10980 deletions
8
routes/email/__init__.py
Normal file
8
routes/email/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""Email route domain package.
|
||||
|
||||
Contains email_routes.py, email_helpers.py and email_pollers.py, migrated
|
||||
from the flat routes/ directory. Backward-compat shims at
|
||||
routes/email_routes.py, routes/email_helpers.py and routes/email_pollers.py
|
||||
replace themselves with these modules, so both import paths resolve to one
|
||||
object.
|
||||
"""
|
||||
2025
routes/email/email_helpers.py
Normal file
2025
routes/email/email_helpers.py
Normal file
File diff suppressed because it is too large
Load diff
1764
routes/email/email_pollers.py
Normal file
1764
routes/email/email_pollers.py
Normal file
File diff suppressed because it is too large
Load diff
7194
routes/email/email_routes.py
Normal file
7194
routes/email/email_routes.py
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
"""odysseus-mail — Unix-style command-line wrapper around the email
|
||||
backend that powers the web UI.
|
||||
|
||||
Calls the same helpers `routes/email_helpers.py` exports, so a request
|
||||
Calls the same helpers `routes/email/email_helpers.py` exports, so a request
|
||||
issued from the shell hits IMAP/SMTP through the same connection pool
|
||||
and the same parsing pipeline as the HTTP routes. State is shared via
|
||||
`data/app.db` and `data/.app_key` (passwords decrypt automatically).
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ This spec covers mail and contacts in:
|
|||
|
||||
- app wiring in `app.py`;
|
||||
- `core.database.EmailAccount`;
|
||||
- `routes/email_routes.py`, `routes/email_helpers.py`, and `routes/email_pollers.py`;
|
||||
- canonical `routes/email/email_routes.py`, `routes/email/email_helpers.py`, and
|
||||
`routes/email/email_pollers.py`, plus their shims at the old flat paths;
|
||||
- email threading in `src/email_thread_parser.py`;
|
||||
- email MCP tools in `mcp_servers/email_server.py`;
|
||||
- canonical contact/CardDAV routes in `routes/contacts/contacts_routes.py`,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ This spec covers durable state in:
|
|||
- `src/attachment_refs.py`, `src/upload_handler.py`, and
|
||||
`routes/upload_routes.py` for durable upload references and retention;
|
||||
- JSON stores managed by `core/auth.py`, `src/settings.py`, `src/api_key_manager.py`, `src/preset_manager.py`, `src/integrations.py`, `src/upload_handler.py`, `src/personal_docs.py`, `src/research_handler.py`, `src/bg_jobs.py`, `routes/prefs_routes.py`, canonical `routes/contacts/contacts_routes.py` and `routes/vault/vault_routes.py` plus their shims, `routes/cookbook_routes.py`, and memory/skills managers;
|
||||
- `routes/email_helpers.py` scheduled-email storage;
|
||||
- canonical `routes/email/email_helpers.py` scheduled-email storage;
|
||||
- `routes/backup_routes.py` and `scripts/odysseus-backup`;
|
||||
- runtime data under `data/`.
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ Email default-account state is serialized per owner. Startup normalizes legacy d
|
|||
|
||||
`core/models.py` owns pure dataclasses used by `SessionManager`. It does not own database persistence.
|
||||
|
||||
`routes/email_helpers.py` owns a second SQLite database at `data/scheduled_emails.db` for scheduled email, summary, reply, tag, sender-signature, urgency-alert, calendar-extraction, and cache state. Its migrations and owner backfills are local to that module, not `core/database.py`, and those auxiliary tables are owner-scoped.
|
||||
`routes/email/email_helpers.py` owns a second SQLite database at `data/scheduled_emails.db` for scheduled email, summary, reply, tag, sender-signature, urgency-alert, calendar-extraction, and cache state. Its migrations and owner backfills are local to that module, not `core/database.py`, and those auxiliary tables are owner-scoped.
|
||||
|
||||
## Migration Policy
|
||||
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ def test_unsubscribe_scan_status_sits_before_clean_action() -> None:
|
|||
|
||||
def test_unsubscribe_success_removes_messages_before_the_next_scan() -> None:
|
||||
frontend = (ROOT / "static/js/emailLibrary.js").read_text(encoding="utf-8")
|
||||
backend = (ROOT / "routes/email_routes.py").read_text(encoding="utf-8")
|
||||
backend = (ROOT / "routes/email/email_routes.py").read_text(encoding="utf-8")
|
||||
mcp = (ROOT / "mcp_servers/email_server.py").read_text(encoding="utf-8")
|
||||
assert "async function _deleteAfterUnsubscribe" in frontend
|
||||
assert "action: 'delete'" in frontend[frontend.index("async function _deleteAfterUnsubscribe"):]
|
||||
|
|
@ -311,7 +311,7 @@ def test_email_mutation_tool_events_include_exact_arguments() -> None:
|
|||
|
||||
|
||||
def test_unsubscribe_cleanup_can_remove_same_sender_unsubscribe_messages() -> None:
|
||||
source = (ROOT / "routes" / "email_routes.py").read_text()
|
||||
source = (ROOT / "routes" / "email" / "email_routes.py").read_text()
|
||||
cleanup = source[source.index('@router.post("/unsubscribe/cleanup")'):source.index('@router.get("/contacts")')]
|
||||
assert 'scope == "sender_unsubscribe"' in cleanup
|
||||
assert "_unsubscribe_sender_uids_sync" in cleanup
|
||||
|
|
@ -372,7 +372,7 @@ def test_email_settings_sections_use_static_headers() -> None:
|
|||
|
||||
|
||||
def test_unsubscribe_scan_defaults_to_bounded_page_in_api_and_tool_prompt() -> None:
|
||||
backend = (ROOT / "routes" / "email_routes.py").read_text()
|
||||
backend = (ROOT / "routes" / "email" / "email_routes.py").read_text()
|
||||
schema = (ROOT / "src" / "tool_schemas.py").read_text()
|
||||
agent = (ROOT / "src" / "agent_loop.py").read_text()
|
||||
scan_start = backend.index('@router.get("/unsubscribe/scan")')
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ def test_direct_upload_routes_use_bounded_reads():
|
|||
"routes/calendar_routes.py": [
|
||||
"read_upload_limited(file, ICS_MAX_BYTES",
|
||||
],
|
||||
"routes/email_routes.py": [
|
||||
"routes/email/email_routes.py": [
|
||||
"read_upload_limited(file, EMAIL_COMPOSE_UPLOAD_MAX_BYTES",
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from tests.helpers.document_source import document_source
|
|||
|
||||
_REPO = Path(__file__).resolve().parents[1]
|
||||
_EMAIL_LIBRARY = _REPO / "static" / "js" / "emailLibrary.js"
|
||||
_EMAIL_ROUTES = _REPO / "routes" / "email_routes.py"
|
||||
_EMAIL_ROUTES = _REPO / "routes" / "email" / "email_routes.py"
|
||||
_EMAIL_MCP_SERVER = _REPO / "mcp_servers" / "email_server.py"
|
||||
_EMAIL_FIXTURE_HELPER = _REPO / "scripts" / "ody_eval_email_fixture.py"
|
||||
|
||||
|
|
|
|||
90
tests/test_email_package_compatibility.py
Normal file
90
tests/test_email_package_compatibility.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CANONICAL = (
|
||||
ROOT / "routes" / "email" / "email_helpers.py",
|
||||
ROOT / "routes" / "email" / "email_pollers.py",
|
||||
ROOT / "routes" / "email" / "email_routes.py",
|
||||
)
|
||||
|
||||
LEGACY_NAMES = {
|
||||
"routes.email_helpers",
|
||||
"routes.email_pollers",
|
||||
"routes.email_routes",
|
||||
}
|
||||
|
||||
|
||||
def _imported_module_names(path: Path) -> set[str]:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
names: set[str] = set()
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
names.update(alias.name for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
names.add(node.module)
|
||||
|
||||
return names
|
||||
|
||||
|
||||
def test_canonical_email_package_does_not_import_legacy_shims() -> None:
|
||||
offenders: list[str] = []
|
||||
|
||||
for path in CANONICAL:
|
||||
imports = _imported_module_names(path)
|
||||
legacy = sorted(imports & LEGACY_NAMES)
|
||||
if legacy:
|
||||
offenders.append(f"{path.relative_to(ROOT)}: {legacy}")
|
||||
|
||||
assert not offenders, "\n".join(offenders)
|
||||
|
||||
|
||||
def test_legacy_email_modules_alias_canonical_module_objects() -> None:
|
||||
code = r'''
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
pairs = (
|
||||
("routes.email_helpers", "routes.email.email_helpers"),
|
||||
("routes.email_pollers", "routes.email.email_pollers"),
|
||||
("routes.email_routes", "routes.email.email_routes"),
|
||||
)
|
||||
|
||||
for legacy_name, canonical_name in pairs:
|
||||
legacy = importlib.import_module(legacy_name)
|
||||
canonical = importlib.import_module(canonical_name)
|
||||
|
||||
assert legacy is canonical, (legacy_name, canonical_name)
|
||||
assert sys.modules[legacy_name] is canonical
|
||||
assert sys.modules[canonical_name] is canonical
|
||||
|
||||
marker = object()
|
||||
legacy._compat_identity_probe = marker
|
||||
assert canonical._compat_identity_probe is marker
|
||||
del canonical._compat_identity_probe
|
||||
'''
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PYTHONDONTWRITEBYTECODE"] = "1"
|
||||
env["PYTHON_DOTENV_DISABLED"] = "1"
|
||||
env["ODYSSEUS_INPROCESS_POLLERS"] = "0"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", code],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, (
|
||||
f"stdout:\n{result.stdout}\n\nstderr:\n{result.stderr}"
|
||||
)
|
||||
|
|
@ -86,7 +86,7 @@ def test_known_imap_mailbox_call_sites_are_quoted():
|
|||
assert 'conn.uid("MOVE", _b(uid), dest_folder)' not in mcp
|
||||
assert 'conn.uid("COPY", _b(uid), dest_folder)' not in mcp
|
||||
|
||||
pollers = Path("routes/email_pollers.py").read_text()
|
||||
pollers = Path("routes/email/email_pollers.py").read_text()
|
||||
assert "conn.select(sent_name" not in pollers
|
||||
assert "imap.append(sent_folder" not in pollers
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ def test_routes_import_from_upload_limits_not_local_defs():
|
|||
],
|
||||
"routes/memory/memory_routes.py": ['int(os.getenv("ODYSSEUS_MEMORY_IMPORT_MAX_BYTES"'],
|
||||
"routes/personal_routes.py": ['os.getenv("ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES"'],
|
||||
"routes/email_routes.py": ["EMAIL_COMPOSE_UPLOAD_MAX_BYTES = 25 * 1024 * 1024"],
|
||||
"routes/email/email_routes.py": ["EMAIL_COMPOSE_UPLOAD_MAX_BYTES = 25 * 1024 * 1024"],
|
||||
"routes/stt_routes.py": ["STT_MAX_AUDIO_BYTES = 25 * 1024 * 1024"],
|
||||
"routes/calendar_routes.py": ["_ICS_MAX_BYTES = 10 * 1024 * 1024"],
|
||||
}
|
||||
|
|
@ -100,7 +100,7 @@ def test_routes_import_from_upload_limits_not_local_defs():
|
|||
"routes/gallery/gallery_routes.py": "GALLERY_UPLOAD_MAX_BYTES",
|
||||
"routes/memory/memory_routes.py": "MEMORY_IMPORT_MAX_BYTES",
|
||||
"routes/personal_routes.py": "PERSONAL_UPLOAD_MAX_BYTES",
|
||||
"routes/email_routes.py": "EMAIL_COMPOSE_UPLOAD_MAX_BYTES",
|
||||
"routes/email/email_routes.py": "EMAIL_COMPOSE_UPLOAD_MAX_BYTES",
|
||||
"routes/stt_routes.py": "STT_MAX_AUDIO_BYTES",
|
||||
"routes/calendar_routes.py": "ICS_MAX_BYTES",
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue