chore(release): 0.0.3
Squash-merge lib-standardisation: adopt repo standards across packaging, tooling, docs, and CI. - standardize pyproject.toml (canonical packaging, lint config, extras, GSDFC pydoclint) - add py.typed marker, modernize type hints and Google-style docstrings - nest generated docs under docs/lib and docs/mcp; add hand-written wiki - adopt standard Type A PyPI CI pipeline (tag==version guard + quality gate) - promote generated CHANGELOG.md; bump version to 0.0.3
This commit is contained in:
@@ -137,7 +137,6 @@ No individual symbols are re-exported at the package root.
|
||||
---
|
||||
"""
|
||||
|
||||
|
||||
from . import ingestion
|
||||
from . import adapters
|
||||
from . import auth
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
from . import ingestion
|
||||
from . import adapters
|
||||
from . import auth
|
||||
from . import credentials
|
||||
from . import models
|
||||
from . import config
|
||||
from . import exceptions
|
||||
from . import adapters, auth, config, credentials, exceptions, ingestion, models
|
||||
|
||||
__all__ = [
|
||||
"ingestion",
|
||||
|
||||
@@ -12,7 +12,8 @@ types or semantics should leak beyond implementations of this interface.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Iterator, Dict, Any
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
|
||||
class MailIntakeAdapter(ABC):
|
||||
@@ -32,7 +33,7 @@ class MailIntakeAdapter(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def iter_message_refs(self, query: str) -> Iterator[Dict[str, str]]:
|
||||
def iter_message_refs(self, query: str) -> Iterator[dict[str, str]]:
|
||||
"""
|
||||
Iterate over lightweight message references matching a query.
|
||||
|
||||
@@ -40,8 +41,12 @@ class MailIntakeAdapter(ABC):
|
||||
query (str):
|
||||
Provider-specific query string used to filter messages.
|
||||
|
||||
Returns:
|
||||
Iterator[dict[str, str]]:
|
||||
An iterator over lightweight message reference dictionaries.
|
||||
|
||||
Yields:
|
||||
Dict[str, str]:
|
||||
dict[str, str]:
|
||||
Dictionaries containing message and thread identifiers.
|
||||
|
||||
Notes:
|
||||
@@ -63,7 +68,7 @@ class MailIntakeAdapter(ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def fetch_message(self, message_id: str) -> Dict[str, Any]:
|
||||
def fetch_message(self, message_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch a full raw message by message identifier.
|
||||
|
||||
@@ -72,13 +77,13 @@ class MailIntakeAdapter(ABC):
|
||||
Provider-specific message identifier.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]:
|
||||
dict[str, Any]:
|
||||
Provider-native message payload (e.g., Gmail message JSON structure).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def fetch_thread(self, thread_id: str) -> Dict[str, Any]:
|
||||
def fetch_thread(self, thread_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch a full raw thread by thread identifier.
|
||||
|
||||
@@ -87,7 +92,7 @@ class MailIntakeAdapter(ABC):
|
||||
Provider-specific thread identifier.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]:
|
||||
dict[str, Any]:
|
||||
Provider-native thread payload.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Iterator, Dict, Any
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
class MailIntakeAdapter(ABC):
|
||||
@abstractmethod
|
||||
def iter_message_refs(self, query: str) -> Iterator[Dict[str, str]]: ...
|
||||
def iter_message_refs(self, query: str) -> Iterator[dict[str, str]]: ...
|
||||
@abstractmethod
|
||||
def fetch_message(self, message_id: str) -> Dict[str, Any]: ...
|
||||
def fetch_message(self, message_id: str) -> dict[str, Any]: ...
|
||||
@abstractmethod
|
||||
def fetch_thread(self, thread_id: str) -> Dict[str, Any]: ...
|
||||
def fetch_thread(self, thread_id: str) -> dict[str, Any]: ...
|
||||
|
||||
@@ -15,14 +15,15 @@ It is the only place in the codebase where:
|
||||
All Gmail-specific behavior must be strictly contained within this module.
|
||||
"""
|
||||
|
||||
from typing import Iterator, Dict, Any
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from mail_intake.adapters.base import MailIntakeAdapter
|
||||
from mail_intake.exceptions import MailIntakeAdapterError
|
||||
from mail_intake.auth.base import MailIntakeAuthProvider
|
||||
from mail_intake.exceptions import MailIntakeAdapterError
|
||||
|
||||
|
||||
class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
@@ -71,10 +72,6 @@ class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
"""
|
||||
Lazily initialize and return the Gmail API service client.
|
||||
|
||||
Returns:
|
||||
Any:
|
||||
Initialized Gmail API service instance.
|
||||
|
||||
Raises:
|
||||
MailIntakeAdapterError:
|
||||
If the Gmail service cannot be initialized.
|
||||
@@ -89,7 +86,7 @@ class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
) from exc
|
||||
return self._service
|
||||
|
||||
def iter_message_refs(self, query: str) -> Iterator[Dict[str, str]]:
|
||||
def iter_message_refs(self, query: str) -> Iterator[dict[str, str]]:
|
||||
"""
|
||||
Iterate over message references matching the query.
|
||||
|
||||
@@ -98,7 +95,7 @@ class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
Gmail search query string.
|
||||
|
||||
Yields:
|
||||
Dict[str, str]:
|
||||
dict[str, str]:
|
||||
Dictionaries containing ``message_id`` and ``thread_id``.
|
||||
|
||||
Raises:
|
||||
@@ -107,9 +104,7 @@ class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
"""
|
||||
try:
|
||||
request = (
|
||||
self.service.users()
|
||||
.messages()
|
||||
.list(userId=self._user_id, q=query)
|
||||
self.service.users().messages().list(userId=self._user_id, q=query)
|
||||
)
|
||||
|
||||
while request is not None:
|
||||
@@ -121,18 +116,14 @@ class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
"thread_id": msg["threadId"],
|
||||
}
|
||||
|
||||
request = (
|
||||
self.service.users()
|
||||
.messages()
|
||||
.list_next(request, response)
|
||||
)
|
||||
request = self.service.users().messages().list_next(request, response)
|
||||
|
||||
except HttpError as exc:
|
||||
raise MailIntakeAdapterError(
|
||||
"Gmail API error while listing messages"
|
||||
) from exc
|
||||
|
||||
def fetch_message(self, message_id: str) -> Dict[str, Any]:
|
||||
def fetch_message(self, message_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch a full Gmail message by message ID.
|
||||
|
||||
@@ -141,7 +132,7 @@ class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
Gmail message identifier.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]:
|
||||
dict[str, Any]:
|
||||
Provider-native Gmail message payload.
|
||||
|
||||
Raises:
|
||||
@@ -160,7 +151,7 @@ class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
f"Gmail API error while fetching message {message_id}"
|
||||
) from exc
|
||||
|
||||
def fetch_thread(self, thread_id: str) -> Dict[str, Any]:
|
||||
def fetch_thread(self, thread_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch a full Gmail thread by thread ID.
|
||||
|
||||
@@ -169,7 +160,7 @@ class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
Gmail thread identifier.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]:
|
||||
dict[str, Any]:
|
||||
Provider-native Gmail thread payload.
|
||||
|
||||
Raises:
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from typing import Iterator, Dict, Any
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
from mail_intake.adapters.base import MailIntakeAdapter
|
||||
from mail_intake.auth.base import MailIntakeAuthProvider
|
||||
|
||||
class MailIntakeGmailAdapter(MailIntakeAdapter):
|
||||
def __init__(self, auth_provider: MailIntakeAuthProvider, user_id: str = ...) -> None: ...
|
||||
def __init__(
|
||||
self, auth_provider: MailIntakeAuthProvider[Any], user_id: str = ...
|
||||
) -> None: ...
|
||||
@property
|
||||
def service(self) -> Any: ...
|
||||
def iter_message_refs(self, query: str) -> Iterator[Dict[str, str]]: ...
|
||||
def fetch_message(self, message_id: str) -> Dict[str, Any]: ...
|
||||
def fetch_thread(self, thread_id: str) -> Dict[str, Any]: ...
|
||||
def iter_message_refs(self, query: str) -> Iterator[dict[str, str]]: ...
|
||||
def fetch_message(self, message_id: str) -> dict[str, Any]: ...
|
||||
def fetch_thread(self, thread_id: str) -> dict[str, Any]: ...
|
||||
|
||||
@@ -17,12 +17,12 @@ No Google authentication details should leak outside this module.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
import google.auth.exceptions
|
||||
from google.auth.transport.requests import Request
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from google.oauth2.credentials import Credentials
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
|
||||
from mail_intake.auth.base import MailIntakeAuthProvider
|
||||
from mail_intake.credentials.store import CredentialStore
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from typing import Sequence, Any
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from mail_intake.auth.base import MailIntakeAuthProvider
|
||||
from mail_intake.credentials.store import CredentialStore
|
||||
|
||||
class MailIntakeGoogleAuth(MailIntakeAuthProvider[Any]):
|
||||
def __init__(self, credentials_path: str, store: CredentialStore[Any], scopes: Sequence[str]) -> None: ...
|
||||
def __init__(
|
||||
self, credentials_path: str, store: CredentialStore[Any], scopes: Sequence[str]
|
||||
) -> None: ...
|
||||
def get_credentials(self) -> Any: ...
|
||||
|
||||
@@ -12,7 +12,6 @@ environment reads to ensure predictability and testability.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -46,12 +45,12 @@ class MailIntakeConfig:
|
||||
Whether ingestion should operate in read-only mode.
|
||||
"""
|
||||
|
||||
credentials_path: Optional[str] = None
|
||||
credentials_path: str | None = None
|
||||
"""
|
||||
Optional path to provider credentials configuration.
|
||||
"""
|
||||
|
||||
token_path: Optional[str] = None
|
||||
token_path: str | None = None
|
||||
"""
|
||||
Optional path to persisted authentication tokens.
|
||||
"""
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
from typing import Optional
|
||||
|
||||
class MailIntakeConfig:
|
||||
provider: str
|
||||
user_id: str
|
||||
readonly: bool
|
||||
credentials_path: Optional[str]
|
||||
token_path: Optional[str]
|
||||
def __init__(self, provider: str = ..., user_id: str = ..., readonly: bool = ..., credentials_path: Optional[str] = ..., token_path: Optional[str] = ...) -> None: ...
|
||||
credentials_path: str | None
|
||||
token_path: str | None
|
||||
def __init__(
|
||||
self,
|
||||
provider: str = ...,
|
||||
user_id: str = ...,
|
||||
readonly: bool = ...,
|
||||
credentials_path: str | None = ...,
|
||||
token_path: str | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .store import CredentialStore
|
||||
from .pickle import PickleCredentialStore
|
||||
from .redis import RedisCredentialStore
|
||||
from .store import CredentialStore
|
||||
|
||||
__all__ = ["CredentialStore", "PickleCredentialStore", "RedisCredentialStore"]
|
||||
|
||||
@@ -16,7 +16,7 @@ untrusted environments.
|
||||
"""
|
||||
|
||||
import pickle
|
||||
from typing import Optional, TypeVar
|
||||
from typing import TypeVar
|
||||
|
||||
from mail_intake.credentials.store import CredentialStore
|
||||
|
||||
@@ -55,12 +55,12 @@ class PickleCredentialStore(CredentialStore[T]):
|
||||
"""
|
||||
self.path = path
|
||||
|
||||
def load(self) -> Optional[T]:
|
||||
def load(self) -> T | None:
|
||||
"""
|
||||
Load credentials from the local filesystem.
|
||||
|
||||
Returns:
|
||||
Optional[T]:
|
||||
T | None:
|
||||
An instance of type `T` if credentials are present and
|
||||
successfully deserialized; otherwise `None`.
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Optional, TypeVar
|
||||
from typing import TypeVar
|
||||
|
||||
from .store import CredentialStore
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -6,6 +7,6 @@ T = TypeVar("T")
|
||||
class PickleCredentialStore(CredentialStore[T]):
|
||||
path: str
|
||||
def __init__(self, path: str) -> None: ...
|
||||
def load(self) -> Optional[T]: ...
|
||||
def load(self) -> T | None: ...
|
||||
def save(self, credentials: T) -> None: ...
|
||||
def clear(self) -> None: ...
|
||||
|
||||
@@ -24,8 +24,8 @@ Credential validation, refresh, rotation, and acquisition remain the
|
||||
responsibility of authentication provider implementations.
|
||||
"""
|
||||
|
||||
|
||||
from typing import Optional, TypeVar, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from mail_intake.credentials.store import CredentialStore
|
||||
|
||||
@@ -57,30 +57,27 @@ class RedisCredentialStore(CredentialStore[T]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redis_client,
|
||||
redis_client: Any,
|
||||
key: str,
|
||||
serialize: Callable[[T], bytes],
|
||||
deserialize: Callable[[bytes], T],
|
||||
ttl_seconds: Optional[int] = None,
|
||||
ttl_seconds: int | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize a Redis-backed credential store.
|
||||
|
||||
Args:
|
||||
redis_client (Any):
|
||||
An initialized Redis client instance (for example, ``redis.Redis`` or a compatible interface) used to communicate with the Redis server.
|
||||
|
||||
Initialized Redis client instance used for persistence.
|
||||
key (str):
|
||||
The Redis key under which credentials are stored. Callers are responsible for applying appropriate namespacing to avoid collisions.
|
||||
|
||||
Storage key under which credentials are persisted.
|
||||
serialize (Callable[[T], bytes]):
|
||||
A callable that converts a credential object of type ``T`` into a ``bytes`` representation suitable for storage in Redis.
|
||||
|
||||
Callable that encodes credentials to bytes for storage.
|
||||
deserialize (Callable[[bytes], T]):
|
||||
A callable that converts a ``bytes`` payload retrieved from Redis back into a credential object of type ``T``.
|
||||
|
||||
ttl_seconds (Optional[int]):
|
||||
Optional time-to-live (TTL) for the stored credentials, expressed in seconds. When provided, Redis will automatically expire the stored credentials after the specified duration. If ``None``, credentials are stored without an expiration.
|
||||
Callable that decodes stored bytes back into credentials.
|
||||
ttl_seconds (int | None):
|
||||
Optional time-to-live in seconds after which stored
|
||||
credentials expire automatically. ``None`` disables expiry.
|
||||
"""
|
||||
self.redis = redis_client
|
||||
self.key = key
|
||||
@@ -88,12 +85,12 @@ class RedisCredentialStore(CredentialStore[T]):
|
||||
self.deserialize = deserialize
|
||||
self.ttl_seconds = ttl_seconds
|
||||
|
||||
def load(self) -> Optional[T]:
|
||||
def load(self) -> T | None:
|
||||
"""
|
||||
Load credentials from Redis.
|
||||
|
||||
Returns:
|
||||
Optional[T]:
|
||||
T | None:
|
||||
An instance of type `T` if credentials are present and
|
||||
successfully deserialized; otherwise `None`.
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from typing import Optional, TypeVar, Callable, Any
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from .store import CredentialStore
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -8,8 +10,15 @@ class RedisCredentialStore(CredentialStore[T]):
|
||||
key: str
|
||||
serialize: Callable[[T], bytes]
|
||||
deserialize: Callable[[bytes], T]
|
||||
ttl_seconds: Optional[int]
|
||||
def __init__(self, redis_client: Any, key: str, serialize: Callable[[T], bytes], deserialize: Callable[[bytes], T], ttl_seconds: Optional[int] = ...) -> None: ...
|
||||
def load(self) -> Optional[T]: ...
|
||||
ttl_seconds: int | None
|
||||
def __init__(
|
||||
self,
|
||||
redis_client: Any,
|
||||
key: str,
|
||||
serialize: Callable[[T], bytes],
|
||||
deserialize: Callable[[bytes], T],
|
||||
ttl_seconds: int | None = ...,
|
||||
) -> None: ...
|
||||
def load(self) -> T | None: ...
|
||||
def save(self, credentials: T) -> None: ...
|
||||
def clear(self) -> None: ...
|
||||
|
||||
@@ -19,9 +19,8 @@ or secrets managers—without coupling authentication logic to any
|
||||
specific storage mechanism.
|
||||
"""
|
||||
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, Optional, TypeVar
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -48,12 +47,12 @@ class CredentialStore(ABC, Generic[T]):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def load(self) -> Optional[T]:
|
||||
def load(self) -> T | None:
|
||||
"""
|
||||
Load previously persisted credentials.
|
||||
|
||||
Returns:
|
||||
Optional[T]:
|
||||
T | None:
|
||||
An instance of type `T` if credentials are available and
|
||||
loadable; otherwise `None`.
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, Optional, TypeVar
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class CredentialStore(ABC, Generic[T]):
|
||||
@abstractmethod
|
||||
def load(self) -> Optional[T]: ...
|
||||
def load(self) -> T | None: ...
|
||||
@abstractmethod
|
||||
def save(self, credentials: T) -> None: ...
|
||||
@abstractmethod
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"""
|
||||
# Summary
|
||||
|
||||
Exception hierarchy for Mail Intake.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This module defines the **canonical exception types** used throughout the
|
||||
Mail Intake library.
|
||||
|
||||
|
||||
@@ -16,16 +16,17 @@ It coordinates:
|
||||
No provider-specific logic or API semantics are permitted in this layer.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime
|
||||
from typing import Iterator, Dict, Any
|
||||
from typing import Any
|
||||
|
||||
from mail_intake.adapters.base import MailIntakeAdapter
|
||||
from mail_intake.exceptions import MailIntakeParsingError
|
||||
from mail_intake.models.message import MailIntakeMessage
|
||||
from mail_intake.models.thread import MailIntakeThread
|
||||
from mail_intake.parsers.headers import parse_headers, extract_sender
|
||||
from mail_intake.parsers.body import extract_body
|
||||
from mail_intake.parsers.headers import extract_sender, parse_headers
|
||||
from mail_intake.parsers.subject import normalize_subject
|
||||
from mail_intake.exceptions import MailIntakeParsingError
|
||||
|
||||
|
||||
class MailIntakeReader:
|
||||
@@ -87,9 +88,9 @@ class MailIntakeReader:
|
||||
query (str):
|
||||
Provider-specific query string used to filter messages.
|
||||
|
||||
Yields:
|
||||
MailIntakeThread:
|
||||
An iterator of `MailIntakeThread` instances.
|
||||
Returns:
|
||||
Iterator[MailIntakeThread]:
|
||||
An iterator over `MailIntakeThread` instances.
|
||||
|
||||
Raises:
|
||||
`MailIntakeParsingError`:
|
||||
@@ -101,7 +102,7 @@ class MailIntakeReader:
|
||||
- Messages are grouped by `thread_id` and yielded as complete
|
||||
thread objects containing all associated messages.
|
||||
"""
|
||||
threads: Dict[str, MailIntakeThread] = {}
|
||||
threads: dict[str, MailIntakeThread] = {}
|
||||
|
||||
for ref in self._adapter.iter_message_refs(query):
|
||||
raw = self._adapter.fetch_message(ref["message_id"])
|
||||
@@ -119,12 +120,12 @@ class MailIntakeReader:
|
||||
|
||||
return iter(threads.values())
|
||||
|
||||
def _parse_message(self, raw_message: Dict[str, Any]) -> MailIntakeMessage:
|
||||
def _parse_message(self, raw_message: dict[str, Any]) -> MailIntakeMessage:
|
||||
"""
|
||||
Parse a raw provider message into a `MailIntakeMessage`.
|
||||
|
||||
Args:
|
||||
raw_message (Dict[str, Any]):
|
||||
raw_message (dict[str, Any]):
|
||||
Provider-native message payload.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from typing import Iterator, Dict, Any
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
from mail_intake.adapters.base import MailIntakeAdapter
|
||||
from mail_intake.models.message import MailIntakeMessage
|
||||
from mail_intake.models.thread import MailIntakeThread
|
||||
@@ -7,4 +9,4 @@ class MailIntakeReader:
|
||||
def __init__(self, adapter: MailIntakeAdapter) -> None: ...
|
||||
def iter_messages(self, query: str) -> Iterator[MailIntakeMessage]: ...
|
||||
def iter_threads(self, query: str) -> Iterator[MailIntakeThread]: ...
|
||||
def _parse_message(self, raw_message: Dict[str, Any]) -> MailIntakeMessage: ...
|
||||
def _parse_message(self, raw_message: dict[str, Any]) -> MailIntakeMessage: ...
|
||||
|
||||
@@ -13,7 +13,6 @@ provider-specific fields or semantics.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -54,7 +53,7 @@ class MailIntakeMessage:
|
||||
Sender email address.
|
||||
"""
|
||||
|
||||
from_name: Optional[str]
|
||||
from_name: str | None
|
||||
"""
|
||||
Optional human-readable sender name.
|
||||
"""
|
||||
@@ -74,7 +73,7 @@ class MailIntakeMessage:
|
||||
Short provider-supplied preview snippet of the message.
|
||||
"""
|
||||
|
||||
raw_headers: Dict[str, str]
|
||||
raw_headers: dict[str, str]
|
||||
"""
|
||||
Normalized mapping of message headers (header name → value).
|
||||
"""
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict
|
||||
|
||||
class MailIntakeMessage:
|
||||
message_id: str
|
||||
thread_id: str
|
||||
timestamp: datetime
|
||||
from_email: str
|
||||
from_name: Optional[str]
|
||||
from_name: str | None
|
||||
subject: str
|
||||
body_text: str
|
||||
snippet: str
|
||||
raw_headers: Dict[str, str]
|
||||
def __init__(self, message_id: str, thread_id: str, timestamp: datetime, from_email: str, from_name: Optional[str], subject: str, body_text: str, snippet: str, raw_headers: Dict[str, str]) -> None: ...
|
||||
raw_headers: dict[str, str]
|
||||
def __init__(
|
||||
self,
|
||||
message_id: str,
|
||||
thread_id: str,
|
||||
timestamp: datetime,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
subject: str,
|
||||
body_text: str,
|
||||
snippet: str,
|
||||
raw_headers: dict[str, str],
|
||||
) -> None: ...
|
||||
|
||||
@@ -12,7 +12,6 @@ for higher-level correspondence workflows.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import List, Set
|
||||
|
||||
from mail_intake.models.message import MailIntakeMessage
|
||||
|
||||
@@ -42,12 +41,12 @@ class MailIntakeThread:
|
||||
Normalized subject line used to group related messages.
|
||||
"""
|
||||
|
||||
participants: Set[str] = field(default_factory=set)
|
||||
participants: set[str] = field(default_factory=set)
|
||||
"""
|
||||
Set of unique participant email addresses observed in the thread.
|
||||
"""
|
||||
|
||||
messages: List[MailIntakeMessage] = field(default_factory=list)
|
||||
messages: list[MailIntakeMessage] = field(default_factory=list)
|
||||
"""
|
||||
Ordered list of messages belonging to this thread.
|
||||
"""
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Set, Optional
|
||||
|
||||
from .message import MailIntakeMessage
|
||||
|
||||
class MailIntakeThread:
|
||||
thread_id: str
|
||||
normalized_subject: str
|
||||
participants: Set[str]
|
||||
messages: List[MailIntakeMessage]
|
||||
last_activity_at: Optional[datetime]
|
||||
def __init__(self, thread_id: str, normalized_subject: str, participants: Set[str] = ..., messages: List[MailIntakeMessage] = ..., last_activity_at: Optional[datetime] = ...) -> None: ...
|
||||
participants: set[str]
|
||||
messages: list[MailIntakeMessage]
|
||||
last_activity_at: datetime | None
|
||||
def __init__(
|
||||
self,
|
||||
thread_id: str,
|
||||
normalized_subject: str,
|
||||
participants: set[str] = ...,
|
||||
messages: list[MailIntakeMessage] = ...,
|
||||
last_activity_at: datetime | None = ...,
|
||||
) -> None: ...
|
||||
def add_message(self, message: MailIntakeMessage) -> None: ...
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .body import extract_body
|
||||
from .headers import parse_headers, extract_sender
|
||||
from .headers import extract_sender, parse_headers
|
||||
from .subject import normalize_subject
|
||||
|
||||
__all__ = ["extract_body", "parse_headers", "extract_sender", "normalize_subject"]
|
||||
|
||||
@@ -11,7 +11,7 @@ prefers human-readable text over fidelity to original formatting.
|
||||
"""
|
||||
|
||||
import base64
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
@@ -45,7 +45,7 @@ def _decode_base64(data: str) -> str:
|
||||
raise MailIntakeParsingError("Failed to decode message body") from exc
|
||||
|
||||
|
||||
def _extract_from_part(part: Dict[str, Any]) -> Optional[str]:
|
||||
def _extract_from_part(part: dict[str, Any]) -> str | None:
|
||||
"""
|
||||
Extract text content from a single MIME part.
|
||||
|
||||
@@ -55,11 +55,11 @@ def _extract_from_part(part: Dict[str, Any]) -> Optional[str]:
|
||||
- `text/html` (converted to plain text)
|
||||
|
||||
Args:
|
||||
part (Dict[str, Any]):
|
||||
part (dict[str, Any]):
|
||||
MIME part dictionary from a provider payload.
|
||||
|
||||
Returns:
|
||||
Optional[str]:
|
||||
str | None:
|
||||
Extracted plain-text content, or `None` if unsupported or empty.
|
||||
"""
|
||||
mime_type = part.get("mimeType")
|
||||
@@ -82,7 +82,7 @@ def _extract_from_part(part: Dict[str, Any]) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def extract_body(payload: Dict[str, Any]) -> str:
|
||||
def extract_body(payload: dict[str, Any]) -> str:
|
||||
"""
|
||||
Extract the best-effort message body from a Gmail payload.
|
||||
|
||||
@@ -94,7 +94,7 @@ def extract_body(payload: Dict[str, Any]) -> str:
|
||||
4. Empty string (if nothing usable found)
|
||||
|
||||
Args:
|
||||
payload (Dict[str, Any]):
|
||||
payload (dict[str, Any]):
|
||||
Provider-native message payload dictionary.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from typing import Dict, Any
|
||||
from typing import Any
|
||||
|
||||
def extract_body(payload: Dict[str, Any]) -> str: ...
|
||||
def extract_body(payload: dict[str, Any]) -> str: ...
|
||||
|
||||
@@ -10,19 +10,17 @@ The functions here are intentionally simple and tolerant of malformed
|
||||
or incomplete header data.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
|
||||
def parse_headers(raw_headers: List[Dict[str, str]]) -> Dict[str, str]:
|
||||
def parse_headers(raw_headers: list[dict[str, str]]) -> dict[str, str]:
|
||||
"""
|
||||
Convert a list of Gmail-style headers into a normalized dict.
|
||||
|
||||
Args:
|
||||
raw_headers (List[Dict[str, str]]):
|
||||
raw_headers (list[dict[str, str]]):
|
||||
List of header dictionaries, each containing `name` and `value` keys.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]:
|
||||
dict[str, str]:
|
||||
Dictionary mapping lowercase header names to stripped values.
|
||||
|
||||
Notes:
|
||||
@@ -50,7 +48,7 @@ def parse_headers(raw_headers: List[Dict[str, str]]) -> Dict[str, str]:
|
||||
}
|
||||
```
|
||||
"""
|
||||
headers: Dict[str, str] = {}
|
||||
headers: dict[str, str] = {}
|
||||
|
||||
for header in raw_headers or []:
|
||||
name = header.get("name")
|
||||
@@ -64,16 +62,16 @@ def parse_headers(raw_headers: List[Dict[str, str]]) -> Dict[str, str]:
|
||||
return headers
|
||||
|
||||
|
||||
def extract_sender(headers: Dict[str, str]) -> Tuple[str, Optional[str]]:
|
||||
def extract_sender(headers: dict[str, str]) -> tuple[str, str | None]:
|
||||
"""
|
||||
Extract sender email and optional display name from headers.
|
||||
|
||||
Args:
|
||||
headers (Dict[str, str]):
|
||||
headers (dict[str, str]):
|
||||
Normalized header dictionary as returned by `parse_headers()`.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Optional[str]]:
|
||||
tuple[str, str | None]:
|
||||
A tuple `(email, name)` where `email` is the sender email address
|
||||
and `name` is the display name, or `None` if unavailable.
|
||||
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
def parse_headers(raw_headers: List[Dict[str, str]]) -> Dict[str, str]: ...
|
||||
def extract_sender(headers: Dict[str, str]) -> Tuple[str, Optional[str]]: ...
|
||||
def parse_headers(raw_headers: list[dict[str, str]]) -> dict[str, str]: ...
|
||||
def extract_sender(headers: dict[str, str]) -> tuple[str, str | None]: ...
|
||||
|
||||
@@ -12,7 +12,6 @@ meaning while removing common reply and forward prefixes.
|
||||
|
||||
import re
|
||||
|
||||
|
||||
_PREFIX_RE = re.compile(r"^(re|fw|fwd)\s*:\s*", re.IGNORECASE)
|
||||
"""
|
||||
Regular expression matching common reply/forward subject prefixes.
|
||||
|
||||
0
mail_intake/py.typed
Normal file
0
mail_intake/py.typed
Normal file
Reference in New Issue
Block a user