mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02: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:
@@ -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.
|
||||
"""
|
||||
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
+11
-2020
File diff suppressed because it is too large
Load Diff
+11
-1759
File diff suppressed because it is too large
Load Diff
+11
-7189
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"
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user