From 73f51155a661b39f0f48d1805aca2ce74bee2bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Goran=20Meki=C4=87?= Date: Tue, 16 Jun 2026 18:46:04 +0200 Subject: [PATCH] Implement mailing lists --- bin/devel.sh | 3 +- freenit/api/__init__.py | 1 + freenit/api/mailinglist.py | 574 ++ freenit/app.py | 11 + freenit/mailinglist/__init__.py | 0 freenit/mailinglist/mail.py | 61 + freenit/mailinglist/worker.py | 200 + freenit/models/sql/base.py | 64 + freenit/models/sql/base.pyi | 5016 ++++++++++++++++++ freenit/permissions.py | 1 + freenit/stalwart.py | 266 + migrations/0003_create_mailing_list_table.py | 463 ++ tests/test_mailinglist.py | 75 + 13 files changed, 6733 insertions(+), 2 deletions(-) create mode 100644 freenit/api/mailinglist.py create mode 100644 freenit/mailinglist/__init__.py create mode 100644 freenit/mailinglist/mail.py create mode 100644 freenit/mailinglist/worker.py create mode 100644 freenit/models/sql/base.pyi create mode 100644 freenit/stalwart.py create mode 100644 migrations/0003_create_mailing_list_table.py create mode 100644 tests/test_mailinglist.py diff --git a/bin/devel.sh b/bin/devel.sh index 21b0c46..8a1dd73 100755 --- a/bin/devel.sh +++ b/bin/devel.sh @@ -6,10 +6,9 @@ export OFFLINE=${OFFLINE:="no"} . ${BIN_DIR}/common.sh -setup +setup yes no export FREENIT_ENV="dev" -python migrate.py echo "Backend" echo "===============" diff --git a/freenit/api/__init__.py b/freenit/api/__init__.py index e8509db..bf90b88 100644 --- a/freenit/api/__init__.py +++ b/freenit/api/__init__.py @@ -3,6 +3,7 @@ import freenit.api.domain import freenit.api.jabber import freenit.api.mail +import freenit.api.mailinglist import freenit.api.omemo import freenit.api.role import freenit.api.sieve diff --git a/freenit/api/mailinglist.py b/freenit/api/mailinglist.py new file mode 100644 index 0000000..86cfcd5 --- /dev/null +++ b/freenit/api/mailinglist.py @@ -0,0 +1,574 @@ +import logging +from datetime import datetime +from typing import Any, List +from uuid import uuid4 + +import oxyde +import pydantic +from fastapi import Depends, Header, HTTPException, Request + +from freenit.api.router import route +from freenit.config import getConfig +from freenit.decorators import description +from freenit.mailinglist.mail import ( + subscribe_confirmation, + unsubscribe_confirmation, +) +from freenit.mailinglist.worker import ( + approve_message, + process_mailing_list, + reject_message, +) +from freenit.mail import sendmail +from freenit.models.pagination import Page, paginate +from freenit.models.sql.base import MailingList, ModerationMessage, PendingSubscriber +from freenit.models.user import User +from freenit.permissions import mailinglist_perms +from freenit.stalwart import ( + add_external_member, + create_archive_account, + create_inbox_account, + create_list_principal, + delete_principal, + fetch_email_bodies, + fetch_email_summaries, + fetch_mailbox_messages, + fetch_principal, + remove_external_member, +) + +config = getConfig() +log = logging.getLogger("mailinglist") + +tags = ["mailinglist"] + + +def _current_user(user=Depends(mailinglist_perms)): + return user + + +def _require_admin(cur_user: User) -> None: + if not cur_user.admin: + raise HTTPException(status_code=403, detail="Only admin users can perform this action") + + +def _parse_address(address: pydantic.EmailStr) -> tuple[str, str]: + local, _, domain = address.partition("@") + return local, domain + + +class MailingListCreate(pydantic.BaseModel): + name: str + address: pydantic.EmailStr + description: str | None = None + public: bool = True + archive_enabled: bool = True + moderation_enabled: bool = False + + +class MailingListUpdate(pydantic.BaseModel): + name: str | None = None + description: str | None = None + public: bool | None = None + archive_enabled: bool | None = None + moderation_enabled: bool | None = None + + +class MailingListResponse(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True) + + id: int + name: str + address: pydantic.EmailStr + distribution_address: pydantic.EmailStr + archive_address: pydantic.EmailStr + description: str | None = None + public: bool + archive_enabled: bool + moderation_enabled: bool + created_at: datetime | None = None + + +class PublicMailingListResponse(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True) + + id: int + name: str + address: pydantic.EmailStr + description: str | None = None + + +class SubscriberAction(pydantic.BaseModel): + email: pydantic.EmailStr + + +class ArchiveEmailResponse(pydantic.BaseModel): + id: str + message_id: str | None = None + subject: str | None = None + sender: pydantic.EmailStr | None = None + sent_at: datetime | None = None + text_body: str | None = None + html_body: str | None = None + + +class ModerationMessageResponse(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True) + + id: int + message_id: str | None = None + subject: str | None = None + sender: pydantic.EmailStr | None = None + sent_at: datetime | None = None + text_body: str | None = None + html_body: str | None = None + status: str + created_at: datetime | None = None + decided_at: datetime | None = None + + +class SubscriberResponse(pydantic.BaseModel): + email: pydantic.EmailStr + + +async def _get_list(id: int) -> MailingList: + try: + return await MailingList.objects.get(id=id) + except oxyde.NotFoundError: + raise HTTPException(status_code=404, detail="No such mailing list") + + +async def _get_pending(id: int, token: str, action: str) -> PendingSubscriber: + try: + return await PendingSubscriber.objects.filter( + mailing_list_id=id, token=token, action=action + ).get() + except oxyde.NotFoundError: + raise HTTPException(status_code=404, detail="Invalid or expired token") + + +async def _get_moderation(id: int, msg_id: int) -> ModerationMessage: + try: + return await ModerationMessage.objects.filter( + id=msg_id, mailing_list_id=id + ).get() + except oxyde.NotFoundError: + raise HTTPException(status_code=404, detail="No such moderation message") + + +@route("/mailinglists", tags=tags) +class MailingListListAPI: + @staticmethod + @description("Get mailing lists") + async def get( + page: int = Header(default=1), + perpage: int = Header(default=10), + cur_user: User = Depends(mailinglist_perms), + ) -> Page[MailingListResponse]: + _require_admin(cur_user) + return await paginate(MailingList.objects, page, perpage) + + @staticmethod + @description("Create mailing list") + async def post( + data: MailingListCreate, + cur_user: User = Depends(mailinglist_perms), + ) -> MailingListResponse: + _require_admin(cur_user) + local, domain = _parse_address(data.address) + distribution_address = f"{local}-members@{domain}" + archive_address = f"{local}-archive@{domain}" + + existing_count = await MailingList.objects.filter( + address__in=[data.address, distribution_address, archive_address] + ).count() + if existing_count > 0: + raise HTTPException(status_code=409, detail="Mailing list address already in use") + + try: + inbox_id = await create_inbox_account(data.name, data.address) + list_id = await create_list_principal(data.name, distribution_address) + archive_id = await create_archive_account(data.name, archive_address) + await add_external_member(list_id, archive_address) + except Exception as e: + log.error("Failed to create Stalwart principals: %s", e) + raise HTTPException(status_code=502, detail=f"Stalwart error: {e}") + + try: + now = datetime.utcnow() + mailing_list = await MailingList.objects.create( + name=data.name, + address=data.address, + distribution_address=distribution_address, + archive_address=archive_address, + description=data.description, + public=data.public, + archive_enabled=data.archive_enabled, + moderation_enabled=data.moderation_enabled, + principal_id=list_id, + inbox_principal_id=inbox_id, + archive_principal_id=archive_id, + created_at=now, + updated_at=now, + ) + except oxyde.IntegrityError as e: + raise HTTPException(status_code=409, detail=f"Mailing list already exists: {e}") + + return MailingListResponse.model_validate(mailing_list) + + +@route("/mailinglists/public", tags=tags) +class MailingListPublicAPI: + @staticmethod + @description("Get public mailing lists") + async def get( + page: int = Header(default=1), + perpage: int = Header(default=10), + ) -> Page[PublicMailingListResponse]: + return await paginate( + MailingList.objects.filter(public=True), + page, + perpage, + ) + + +@route("/mailinglists/{id}", tags=tags) +class MailingListDetailAPI: + @staticmethod + async def get(id: int, cur_user: User = Depends(mailinglist_perms)) -> MailingListResponse: + _require_admin(cur_user) + mailing_list = await _get_list(id) + return MailingListResponse.model_validate(mailing_list) + + @staticmethod + async def patch( + id: int, + data: MailingListUpdate, + cur_user: User = Depends(mailinglist_perms), + ) -> MailingListResponse: + _require_admin(cur_user) + mailing_list = await _get_list(id) + await mailing_list.patch(data) + return MailingListResponse.model_validate(mailing_list) + + @staticmethod + async def delete(id: int, cur_user: User = Depends(mailinglist_perms)) -> MailingListResponse: + _require_admin(cur_user) + mailing_list = await _get_list(id) + try: + for principal_id in [ + mailing_list.inbox_principal_id, + mailing_list.principal_id, + mailing_list.archive_principal_id, + ]: + if principal_id: + await delete_principal(principal_id) + except Exception as e: + log.error("Failed to delete Stalwart principals for list %s: %s", id, e) + await mailing_list.delete() + return MailingListResponse.model_validate(mailing_list) + + +@route("/mailinglists/{id}/subscribers", tags=tags) +class MailingListSubscribersAPI: + @staticmethod + @description("Get confirmed subscribers from Stalwart") + async def get( + id: int, + cur_user: User = Depends(mailinglist_perms), + ) -> List[SubscriberResponse]: + _require_admin(cur_user) + mailing_list = await _get_list(id) + if not mailing_list.principal_id: + return [] + try: + principal = await fetch_principal(mailing_list.principal_id) + except Exception as e: + log.error("Failed to fetch Stalwart principal %s: %s", mailing_list.principal_id, e) + raise HTTPException(status_code=502, detail=f"Stalwart error: {e}") + members = principal.get("externalMembers", []) + return [SubscriberResponse(email=email) for email in members] + + +def _email_address(header_value: dict[str, Any] | list[dict[str, Any]] | None) -> str | None: + if not header_value: + return None + if isinstance(header_value, list): + header_value = header_value[0] + return header_value.get("email") or header_value.get("address") + + +@route("/mailinglists/{id}/archive", tags=tags) +class MailingListArchiveAPI: + @staticmethod + @description("Get public archive") + async def get( + id: int, + page: int = Header(default=1), + perpage: int = Header(default=10), + ) -> Page[ArchiveEmailResponse]: + mailing_list = await _get_list(id) + if not mailing_list.public or not mailing_list.archive_enabled: + raise HTTPException(status_code=403, detail="Archive not available") + try: + email_ids = await fetch_mailbox_messages(mailing_list.archive_address, "inbox") + except Exception as e: + log.error("Failed to fetch archive mailbox for %s: %s", mailing_list.archive_address, e) + raise HTTPException(status_code=502, detail=f"Stalwart error: {e}") + + total = len(email_ids) + pages = (total + perpage - 1) // perpage + offset = (page - 1) * perpage + page_ids = email_ids[offset:offset + perpage] + + try: + summaries = await fetch_email_summaries(mailing_list.archive_address, page_ids) + except Exception as e: + log.error("Failed to fetch archive summaries for %s: %s", mailing_list.archive_address, e) + raise HTTPException(status_code=502, detail=f"Stalwart error: {e}") + + data = [] + for item in summaries: + received_at = item.get("receivedAt") + sent_at = datetime.fromisoformat(received_at.replace("Z", "+00:00")) if received_at else None + message_ids = item.get("messageId", []) + data.append(ArchiveEmailResponse( + id=item.get("id"), + message_id=message_ids[0] if isinstance(message_ids, list) and message_ids else None, + subject=item.get("subject"), + sender=_email_address(item.get("from")), + sent_at=sent_at, + )) + + return Page(data=data, page=page, perpage=perpage, pages=pages, total=total) + + +@route("/mailinglists/{id}/archive/{msg_id}", tags=tags) +class MailingListArchiveMessageAPI: + @staticmethod + @description("Get single archive message") + async def get(id: int, msg_id: str) -> ArchiveEmailResponse: + mailing_list = await _get_list(id) + if not mailing_list.public or not mailing_list.archive_enabled: + raise HTTPException(status_code=403, detail="Archive not available") + try: + email_data = await fetch_email_bodies(mailing_list.archive_address, msg_id) + except Exception as e: + log.error("Failed to fetch archive message %s for %s: %s", msg_id, mailing_list.archive_address, e) + raise HTTPException(status_code=502, detail=f"Stalwart error: {e}") + if not email_data: + raise HTTPException(status_code=404, detail="No such message") + + received_at = email_data.get("receivedAt") + sent_at = datetime.fromisoformat(received_at.replace("Z", "+00:00")) if received_at else None + message_ids = email_data.get("messageId", []) + + body_values = email_data.get("bodyValues", {}) + text_body = None + html_body = None + for part in email_data.get("textBody", []): + part_id = part.get("partId") + if part_id and part_id in body_values: + text_body = body_values[part_id].get("value") + break + for part in email_data.get("htmlBody", []): + part_id = part.get("partId") + if part_id and part_id in body_values: + html_body = body_values[part_id].get("value") + break + + return ArchiveEmailResponse( + id=email_data.get("id"), + message_id=message_ids[0] if isinstance(message_ids, list) and message_ids else None, + subject=email_data.get("subject"), + sender=_email_address(email_data.get("from")), + sent_at=sent_at, + text_body=text_body, + html_body=html_body, + ) + + +@route("/mailinglists/{id}/subscribe", tags=tags) +class MailingListSubscribeAPI: + @staticmethod + @description("Request subscription") + async def post( + id: int, + data: SubscriberAction, + request: Request, + ) -> dict[str, str]: + mailing_list = await _get_list(id) + if not mailing_list.public: + raise HTTPException(status_code=403, detail="Subscribing is not allowed") + + try: + pending = await PendingSubscriber.objects.filter( + mailing_list_id=id, email=data.email, action="subscribe" + ).get() + except oxyde.NotFoundError: + pending = await PendingSubscriber.objects.create( + mailing_list=mailing_list, + mailing_list_id=mailing_list.id, + email=data.email, + action="subscribe", + token=str(uuid4()), + created_at=datetime.utcnow(), + ) + confirm_url = f"{request.base_url}api/v1/mailinglists/{id}/confirm/{pending.token}" + msg = subscribe_confirmation(mailing_list.name, mailing_list.address, confirm_url) + msg["To"] = data.email + try: + sendmail([data.email], msg) + except Exception as e: + log.error("Failed to send subscribe confirmation to %s: %s", data.email, e) + raise HTTPException(status_code=502, detail="Failed to send confirmation email") + return {"detail": "Please check your email to confirm subscription"} + + +@route("/mailinglists/{id}/confirm/{token}", tags=tags) +class MailingListConfirmAPI: + @staticmethod + @description("Confirm subscription") + async def get(id: int, token: str) -> dict[str, str]: + mailing_list = await _get_list(id) + if not mailing_list.public: + raise HTTPException(status_code=403, detail="Subscribing is not allowed") + pending = await _get_pending(id, token, "subscribe") + try: + await add_external_member(mailing_list.principal_id, pending.email) + except Exception as e: + log.error("Failed to add member %s to list %s: %s", pending.email, id, e) + raise HTTPException(status_code=502, detail=f"Stalwart error: {e}") + await pending.delete() + return {"detail": "Subscription confirmed"} + + +@route("/mailinglists/{id}/unsubscribe", tags=tags) +class MailingListUnsubscribeAPI: + @staticmethod + @description("Request unsubscription") + async def post( + id: int, + data: SubscriberAction, + request: Request, + ) -> dict[str, str]: + mailing_list = await _get_list(id) + if not mailing_list.public: + raise HTTPException(status_code=403, detail="Unsubscribing is not allowed") + + try: + pending = await PendingSubscriber.objects.filter( + mailing_list_id=id, email=data.email, action="unsubscribe" + ).get() + except oxyde.NotFoundError: + pending = await PendingSubscriber.objects.create( + mailing_list=mailing_list, + mailing_list_id=mailing_list.id, + email=data.email, + action="unsubscribe", + token=str(uuid4()), + created_at=datetime.utcnow(), + ) + confirm_url = f"{request.base_url}api/v1/mailinglists/{id}/unsubscribe/{pending.token}" + msg = unsubscribe_confirmation(mailing_list.name, mailing_list.address, confirm_url) + msg["To"] = data.email + try: + sendmail([data.email], msg) + except Exception as e: + log.error("Failed to send unsubscribe confirmation to %s: %s", data.email, e) + raise HTTPException(status_code=502, detail="Failed to send confirmation email") + return {"detail": "Please check your email to confirm unsubscription"} + + +@route("/mailinglists/{id}/unsubscribe/{token}", tags=tags) +class MailingListUnsubscribeConfirmAPI: + @staticmethod + @description("Confirm unsubscription") + async def get(id: int, token: str) -> dict[str, str]: + mailing_list = await _get_list(id) + if not mailing_list.public: + raise HTTPException(status_code=403, detail="Unsubscribing is not allowed") + pending = await _get_pending(id, token, "unsubscribe") + try: + await remove_external_member(mailing_list.principal_id, pending.email) + except Exception as e: + log.error("Failed to remove member %s from list %s: %s", pending.email, id, e) + raise HTTPException(status_code=502, detail=f"Stalwart error: {e}") + await pending.delete() + return {"detail": "Unsubscription confirmed"} + + +@route("/mailinglists/{id}/moderation", tags=tags) +class MailingListModerationAPI: + @staticmethod + @description("Get pending moderation queue") + async def get( + id: int, + page: int = Header(default=1), + perpage: int = Header(default=10), + cur_user: User = Depends(mailinglist_perms), + ) -> Page[ModerationMessageResponse]: + _require_admin(cur_user) + mailing_list = await _get_list(id) + return await paginate( + ModerationMessage.objects.filter( + mailing_list_id=mailing_list.id, status="pending" + ).order_by("-created_at"), + page, + perpage, + ) + + +@route("/mailinglists/{id}/moderation/{msg_id}/approve", tags=tags) +class MailingListApproveAPI: + @staticmethod + @description("Approve moderated message") + async def post( + id: int, + msg_id: int, + cur_user: User = Depends(mailinglist_perms), + ) -> ModerationMessageResponse: + _require_admin(cur_user) + mailing_list = await _get_list(id) + moderation_message = await _get_moderation(id, msg_id) + try: + await approve_message(mailing_list, moderation_message) + except Exception as e: + log.error("Failed to approve message %s: %s", msg_id, e) + raise HTTPException(status_code=502, detail=f"Failed to approve: {e}") + return ModerationMessageResponse.model_validate(moderation_message) + + +@route("/mailinglists/{id}/moderation/{msg_id}/reject", tags=tags) +class MailingListRejectAPI: + @staticmethod + @description("Reject moderated message") + async def post( + id: int, + msg_id: int, + cur_user: User = Depends(mailinglist_perms), + ) -> ModerationMessageResponse: + _require_admin(cur_user) + moderation_message = await _get_moderation(id, msg_id) + await reject_message(moderation_message) + return ModerationMessageResponse.model_validate(moderation_message) + + +@route("/mailinglists/{id}/process", tags=tags) +class MailingListProcessAPI: + @staticmethod + @description("Poll list inbox and process messages") + async def post( + id: int, + cur_user: User = Depends(mailinglist_perms), + ) -> dict[str, int]: + _require_admin(cur_user) + mailing_list = await _get_list(id) + try: + stats = await process_mailing_list(mailing_list) + except Exception as e: + log.error("Failed to process mailing list %s: %s", id, e) + raise HTTPException(status_code=502, detail=f"Processing failed: {e}") + return stats + + diff --git a/freenit/app.py b/freenit/app.py index 3bfb915..ce5bd0e 100644 --- a/freenit/app.py +++ b/freenit/app.py @@ -1,3 +1,6 @@ +import asyncio +import os +import subprocess from contextlib import asynccontextmanager from fastapi import FastAPI @@ -8,8 +11,16 @@ config = getConfig() +def _run_migrations() -> None: + env = os.environ.copy() + env.setdefault("FREENIT_ENV", "prod") + subprocess.run(["oxyde", "migrate"], check=True, env=env) + + @asynccontextmanager async def lifespan(_: FastAPI): + if os.environ.get("FREENIT_ENV") != "test": + await asyncio.to_thread(_run_migrations) if not config.database.connected: await config.database.connect() yield diff --git a/freenit/mailinglist/__init__.py b/freenit/mailinglist/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freenit/mailinglist/mail.py b/freenit/mailinglist/mail.py new file mode 100644 index 0000000..e930390 --- /dev/null +++ b/freenit/mailinglist/mail.py @@ -0,0 +1,61 @@ +from email.message import EmailMessage + +from freenit.config import getConfig + +config = getConfig() + + +def subscribe_confirmation(list_name: str, list_address: str, confirm_url: str) -> EmailMessage: + msg = EmailMessage() + msg["From"] = list_address + msg["Subject"] = f"[{list_name}] Confirm subscription" + msg.set_content( + f"""Hello, + +Please confirm your subscription to {list_name} <{list_address}> by visiting: + +{confirm_url} + +If you did not request this subscription, you can ignore this message. + +Regards, +{config.name} +""" + ) + return msg + + +def unsubscribe_confirmation(list_name: str, list_address: str, confirm_url: str) -> EmailMessage: + msg = EmailMessage() + msg["From"] = list_address + msg["Subject"] = f"[{list_name}] Confirm unsubscription" + msg.set_content( + f"""Hello, + +Please confirm that you want to unsubscribe from {list_name} <{list_address}> by visiting: + +{confirm_url} + +If you did not request this, you can ignore this message. + +Regards, +{config.name} +""" + ) + return msg + + +def moderation_notice(list_name: str, list_address: str, subject: str) -> EmailMessage: + msg = EmailMessage() + msg["From"] = list_address + msg["Subject"] = f"[{list_name}] Your post is awaiting moderation" + msg.set_content( + f"""Hello, + +Your message "{subject}" sent to {list_address} is awaiting moderator approval. + +Regards, +{config.name} +""" + ) + return msg diff --git a/freenit/mailinglist/worker.py b/freenit/mailinglist/worker.py new file mode 100644 index 0000000..058bc78 --- /dev/null +++ b/freenit/mailinglist/worker.py @@ -0,0 +1,200 @@ +import logging +from datetime import datetime +from email.message import EmailMessage +from typing import Any + +from freenit.config import getConfig +from freenit.mail import sendmail +from freenit.models.sql.base import MailingList, ModerationMessage +from freenit.stalwart import ( + destroy_emails, + fetch_email_bodies, + jmap_request, +) + +from .mail import moderation_notice + +config = getConfig() +log = logging.getLogger("mailinglist.worker") + + +def _email_address(header_value: dict[str, Any] | list[dict[str, Any]] | None) -> str | None: + if not header_value: + return None + if isinstance(header_value, list): + header_value = header_value[0] + return header_value.get("email") or header_value.get("address") + + +def _build_forward(original: dict[str, Any], list_address: str, distribution_address: str) -> EmailMessage: + msg = EmailMessage() + sender = _email_address(original.get("from")) or list_address + msg["From"] = sender + msg["To"] = distribution_address + msg["Subject"] = original.get("subject", "") + msg["List-Id"] = f"<{list_address}>" + msg["Precedence"] = "list" + msg["Reply-To"] = list_address + + text_body = original.get("textBody") + html_body = original.get("htmlBody") + + if text_body and html_body: + msg.set_content(text_body) + msg.add_alternative(html_body, subtype="html") + elif html_body: + msg.add_alternative(html_body, subtype="html") + elif text_body: + msg.set_content(text_body) + else: + msg.set_content("") + + return msg + + +async def _fetch_inbox_email_ids(account_email: str) -> list[tuple[str, str]]: + """Return list of (email_id, thread_id) for emails in the inbox.""" + result = await jmap_request(account_email, [ + ["Mailbox/query", {"filter": {"role": "inbox"}}, "0"], + ["Email/query", { + "filter": { + "inMailbox": "#{0}/ids[0]", + }, + "sort": [{"property": "receivedAt", "isAscending": False}], + "limit": 100, + }, "1"], + ]) + responses = {r[2]: r for r in result.get("methodResponses", [])} + email_response = responses.get("1") + if not email_response: + return [] + _, data, _ = email_response + return [(item.get("id"), item.get("threadId")) for item in data.get("ids", [])] + + +async def _fetch_email_details(account_email: str, email_id: str) -> dict[str, Any]: + result = await jmap_request(account_email, [ + ["Email/get", { + "ids": [email_id], + "properties": ["id", "messageId", "subject", "from", "receivedAt", "bodyValues"], + "fetchAllBodyValues": True, + "maxBodyValueBytes": 1048576, + }, "0"], + ]) + responses = result.get("methodResponses", []) + if not responses: + return {} + _, data, _ = responses[0] + items = data.get("list", []) + return items[0] if items else {} + + +def _extract_bodies(email_data: dict[str, Any]) -> tuple[str | None, str | None]: + body_values = email_data.get("bodyValues", {}) + text_body = None + html_body = None + for part in email_data.get("textBody", []): + part_id = part.get("partId") + if part_id and part_id in body_values: + text_body = body_values[part_id].get("value") + break + for part in email_data.get("htmlBody", []): + part_id = part.get("partId") + if part_id and part_id in body_values: + html_body = body_values[part_id].get("value") + break + return text_body, html_body + + +async def _store_moderation(mailing_list: MailingList, email_data: dict[str, Any], text_body: str | None, html_body: str | None) -> ModerationMessage: + message_ids = email_data.get("messageId", []) + message_id = message_ids[0] if isinstance(message_ids, list) and message_ids else None + received_at = email_data.get("receivedAt") + sent_at = datetime.fromisoformat(received_at.replace("Z", "+00:00")) if received_at else datetime.utcnow() + return await ModerationMessage.objects.create( + mailing_list=mailing_list, + mailing_list_id=mailing_list.id, + message_id=message_id, + subject=email_data.get("subject"), + sender=_email_address(email_data.get("from")), + sent_at=sent_at, + text_body=text_body, + html_body=html_body, + status="pending", + created_at=datetime.utcnow(), + ) + + +async def _distribute(mailing_list: MailingList, email_data: dict[str, Any], text_body: str | None, html_body: str | None) -> None: + original = { + "from": email_data.get("from"), + "subject": email_data.get("subject"), + "textBody": text_body, + "htmlBody": html_body, + } + msg = _build_forward(original, mailing_list.address, mailing_list.distribution_address) + sendmail([mailing_list.distribution_address], msg) + + +async def process_mailing_list(mailing_list: MailingList) -> dict[str, int]: + stats = {"distributed": 0, "moderated": 0, "errors": 0} + if not mailing_list.inbox_principal_id: + return stats + + try: + email_ids = await _fetch_inbox_email_ids(mailing_list.address) + except Exception as e: + log.error("Failed to fetch inbox for %s: %s", mailing_list.address, e) + return stats + + for email_id in email_ids: + try: + email_data = await _fetch_email_details(mailing_list.address, email_id) + if not email_data: + continue + text_body, html_body = _extract_bodies(email_data) + + if mailing_list.moderation_enabled: + await _store_moderation(mailing_list, email_data, text_body, html_body) + stats["moderated"] += 1 + sender = _email_address(email_data.get("from")) + if sender: + notice = moderation_notice( + mailing_list.name, + mailing_list.address, + email_data.get("subject", ""), + ) + notice["To"] = sender + try: + sendmail([sender], notice) + except Exception as e: + log.warning("Failed to send moderation notice to %s: %s", sender, e) + else: + await _distribute(mailing_list, email_data, text_body, html_body) + stats["distributed"] += 1 + + await destroy_emails(mailing_list.address, [email_id]) + except Exception as e: + log.error("Failed to process email %s for %s: %s", email_id, mailing_list.address, e) + stats["errors"] += 1 + + return stats + + +async def approve_message(mailing_list: MailingList, moderation_message: ModerationMessage) -> None: + email_data = { + "from": [{"email": moderation_message.sender}], + "subject": moderation_message.subject, + "messageId": [moderation_message.message_id] if moderation_message.message_id else [], + "receivedAt": moderation_message.sent_at.isoformat() if moderation_message.sent_at else datetime.utcnow().isoformat(), + } + await _distribute(mailing_list, email_data, moderation_message.text_body, moderation_message.html_body) + moderation_message.status = "approved" + moderation_message.decided_at = datetime.utcnow() + await moderation_message.save(update_fields=["status", "decided_at"]) + + +async def reject_message(moderation_message: ModerationMessage) -> None: + moderation_message.status = "rejected" + moderation_message.decided_at = datetime.utcnow() + await moderation_message.save(update_fields=["status", "decided_at"]) diff --git a/freenit/models/sql/base.py b/freenit/models/sql/base.py index 71c304b..f2f352a 100644 --- a/freenit/models/sql/base.py +++ b/freenit/models/sql/base.py @@ -1,6 +1,8 @@ from __future__ import annotations +from datetime import datetime from typing import ClassVar +from uuid import uuid4 import oxyde import pydantic @@ -166,6 +168,68 @@ class Meta: table_name = "theme" +class MailingList(OxydeBaseModel): + id: int | None = oxyde.Field(default=None, db_pk=True) + name: str = oxyde.Field(db_unique=True) + address: pydantic.EmailStr = oxyde.Field(db_unique=True) + distribution_address: pydantic.EmailStr = oxyde.Field(db_unique=True) + archive_address: pydantic.EmailStr = oxyde.Field(db_unique=True) + description: str | None = oxyde.Field(default=None) + public: bool = oxyde.Field(default=True) + archive_enabled: bool = oxyde.Field(default=True) + moderation_enabled: bool = oxyde.Field(default=False) + principal_id: int | None = oxyde.Field(default=None) + inbox_principal_id: int | None = oxyde.Field(default=None) + archive_principal_id: int | None = oxyde.Field(default=None) + created_at: datetime | None = oxyde.Field(default=None) + updated_at: datetime | None = oxyde.Field(default=None) + + class Meta: + is_table = True + table_name = "mailing_list" + + +class PendingSubscriber(OxydeBaseModel): + """Subscriptions/unsubscriptions awaiting email confirmation.""" + + id: int | None = oxyde.Field(default=None, db_pk=True) + mailing_list: MailingList | None = oxyde.Field( + default=None, db_fk="id", db_on_delete="CASCADE" + ) + email: pydantic.EmailStr = oxyde.Field() + token: str = oxyde.Field(default_factory=lambda: str(uuid4())) + action: str = oxyde.Field(default="subscribe") + created_at: datetime | None = oxyde.Field(default=None) + + class Meta: + is_table = True + table_name = "pending_subscriber" + unique_together = [("mailing_list_id", "email", "action")] + + +class ModerationMessage(OxydeBaseModel): + id: int | None = oxyde.Field(default=None, db_pk=True) + mailing_list: MailingList | None = oxyde.Field( + default=None, db_fk="id", db_on_delete="CASCADE" + ) + message_id: str | None = oxyde.Field(default=None) + subject: str | None = oxyde.Field(default=None) + sender: pydantic.EmailStr | None = oxyde.Field(default=None) + sent_at: datetime | None = oxyde.Field(default=None) + text_body: str | None = oxyde.Field(default=None) + html_body: str | None = oxyde.Field(default=None) + status: str = oxyde.Field(default="pending") + created_at: datetime | None = oxyde.Field(default=None) + decided_at: datetime | None = oxyde.Field(default=None) + + class Meta: + is_table = True + table_name = "moderation_message" + + User.model_rebuild() BaseRole.model_rebuild() UserRole.model_rebuild() +MailingList.model_rebuild() +PendingSubscriber.model_rebuild() +ModerationMessage.model_rebuild() diff --git a/freenit/models/sql/base.pyi b/freenit/models/sql/base.pyi new file mode 100644 index 0000000..56dc07c --- /dev/null +++ b/freenit/models/sql/base.pyi @@ -0,0 +1,5016 @@ +# Auto-generated by oxyde generate-stubs +# DO NOT EDIT - This file will be overwritten + +from typing import Any, ClassVar, Literal +from datetime import datetime, date, time +from decimal import Decimal +from uuid import UUID + +from oxyde import Model +from oxyde.queries import Query, QueryManager + +from __future__ import annotations +from datetime import datetime +from typing import ClassVar +from uuid import uuid4 +import oxyde +import pydantic +from fastapi import HTTPException +from freenit.auth import verify +from freenit.config import getConfig + +class BaseRole(Model): + class Meta: + is_table: bool + table_name: str + id: int | None + name: str + def model_post_init(self, __context): + ... + async def fetch_users(self) -> list['User']: + ... + objects: ClassVar["BaseRoleManager"] + + +class BaseRoleQuery(Query[BaseRole]): + """Type-safe Query for BaseRole model.""" + + # Query building methods (sync, return Query) + + def filter( + self, + *args: Any, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + ) -> "BaseRoleQuery": + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + ) -> "BaseRoleQuery": + """Exclude objects matching field lookups.""" + ... + + def order_by(self, *fields: Literal["id", "-id", "name", "-name"]) -> "BaseRoleQuery": # type: ignore[override] + """Order results by fields.""" + ... + + def limit(self, n: int) -> "BaseRoleQuery": + """Limit number of results.""" + ... + + def offset(self, n: int) -> "BaseRoleQuery": + """Skip first n results.""" + ... + + def distinct(self, value: bool = True) -> "BaseRoleQuery": + """Return distinct results.""" + ... + + def select(self, *fields: Literal["id", "name"]) -> "BaseRoleQuery": # type: ignore[override] + """Select specific fields.""" + ... + + def join(self, *paths: str) -> "BaseRoleQuery": + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> "BaseRoleQuery": + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> "BaseRoleQuery": + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> "BaseRoleQuery": + """Add FOR SHARE lock to query.""" + ... + + def annotate(self, **annotations: Any) -> "BaseRoleQuery": + """Add computed fields using aggregate functions.""" + ... + + def group_by(self, *fields: Literal["id", "name"]) -> "BaseRoleQuery": # type: ignore[override] + """Add GROUP BY clause.""" + ... + + def having(self, *q_exprs: Any, **kwargs: Any) -> "BaseRoleQuery": + """Add HAVING clause for filtering grouped results.""" + ... + + def values(self, *fields: Literal["id", "name"]) -> "BaseRoleQuery": # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["id", "name"], flat: bool = False) -> "BaseRoleQuery": # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + # Terminal methods (async, execute query) + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> list[BaseRole]: + """Execute query and return all results.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> BaseRole | None: + """Execute query and return first result.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> BaseRole | None: + """Execute query and return last result.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count matching objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def update( # type: ignore[override] + self, + *, + client: Any | None = None, + using: str | None = None, + **values: Any, + ) -> int: + """Update matching objects.""" + ... + + async def increment( + self, + field: str, + by: int | float = 1, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Atomically increment a field value.""" + ... + + async def delete( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Delete matching objects.""" + ... + +class BaseRoleManager(QueryManager[BaseRole]): + """Type-safe Manager for BaseRole model.""" + + # Query building methods (sync, return Query) + + def query(self) -> BaseRoleQuery: + """Return a Query builder for this model.""" + ... + + def filter( + self, + *args: Any, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + ) -> BaseRoleQuery: + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + ) -> BaseRoleQuery: + """Exclude objects matching field lookups.""" + ... + + def values(self, *fields: Literal["id", "name"]) -> BaseRoleQuery: # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["id", "name"], flat: bool = False) -> BaseRoleQuery: # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + def distinct(self, distinct: bool = True) -> BaseRoleQuery: + """Return distinct results.""" + ... + + def join(self, *paths: str) -> BaseRoleQuery: + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> BaseRoleQuery: + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> BaseRoleQuery: + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> BaseRoleQuery: + """Add FOR SHARE lock to query.""" + ... + + # Terminal methods (async, execute query) + + async def get( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> BaseRole: + """Get single object matching lookups.""" + ... + + async def get_or_none( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> BaseRole | None: + """Get object or None if not found.""" + ... + + async def get_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[BaseRole, bool]: + """Get object or create if not found. Returns (object, created).""" + ... + + async def update_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[BaseRole, bool]: + """Get object, create if missing, or update it when defaults are provided.""" + ... + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + mode: str = "models", + ) -> list[BaseRole]: + """Get all objects.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> BaseRole | None: + """Get first object.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> BaseRole | None: + """Get last object.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count all objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def create( # type: ignore[override] + self, + *, + instance: BaseRole | None = None, + client: Any | None = None, + using: str | None = None, + id: int | None = None, + name: str | None = None, + ) -> BaseRole: + """Create new object.""" + ... + + async def bulk_create( # type: ignore[override] + self, + objects: list[BaseRole], + *, + batch_size: int | None = None, + client: Any | None = None, + using: str | None = None, + ) -> list[BaseRole]: + """Bulk create objects.""" + ... + + async def bulk_update( # type: ignore[override] + self, + objects: list[BaseRole], + fields: list[str], + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Bulk update objects.""" + ... + + +class User(Model): + class Meta: + is_table: bool + table_name: str + id: int | None + email: EmailStr + password: str + fullname: str | None + active: bool + admin: bool + omemo_bundle: str | None + def model_post_init(self, __context): + ... + def check(self, password: str) -> bool: + ... + @classmethod + async def login(cls, credentials) -> 'User': + ... + async def fetch_roles(self) -> list[BaseRole]: + ... + objects: ClassVar["UserManager"] + + +class UserQuery(Query[User]): + """Type-safe Query for User model.""" + + # Query building methods (sync, return Query) + + def filter( + self, + *args: Any, + active: bool | None = None, + active__in: list[bool] | None = None, + active__isnull: bool | None = None, + admin: bool | None = None, + admin__in: list[bool] | None = None, + admin__isnull: bool | None = None, + email: EmailStr | None = None, + email__in: list[EmailStr] | None = None, + email__isnull: bool | None = None, + fullname: str | None = None, + fullname__contains: str | None = None, + fullname__icontains: str | None = None, + fullname__startswith: str | None = None, + fullname__istartswith: str | None = None, + fullname__endswith: str | None = None, + fullname__iendswith: str | None = None, + fullname__iexact: str | None = None, + fullname__in: list[str] | None = None, + fullname__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + omemo_bundle: str | None = None, + omemo_bundle__contains: str | None = None, + omemo_bundle__icontains: str | None = None, + omemo_bundle__startswith: str | None = None, + omemo_bundle__istartswith: str | None = None, + omemo_bundle__endswith: str | None = None, + omemo_bundle__iendswith: str | None = None, + omemo_bundle__iexact: str | None = None, + omemo_bundle__in: list[str] | None = None, + omemo_bundle__isnull: bool | None = None, + password: str | None = None, + password__contains: str | None = None, + password__icontains: str | None = None, + password__startswith: str | None = None, + password__istartswith: str | None = None, + password__endswith: str | None = None, + password__iendswith: str | None = None, + password__iexact: str | None = None, + password__in: list[str] | None = None, + password__isnull: bool | None = None, + ) -> "UserQuery": + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + active: bool | None = None, + active__in: list[bool] | None = None, + active__isnull: bool | None = None, + admin: bool | None = None, + admin__in: list[bool] | None = None, + admin__isnull: bool | None = None, + email: EmailStr | None = None, + email__in: list[EmailStr] | None = None, + email__isnull: bool | None = None, + fullname: str | None = None, + fullname__contains: str | None = None, + fullname__icontains: str | None = None, + fullname__startswith: str | None = None, + fullname__istartswith: str | None = None, + fullname__endswith: str | None = None, + fullname__iendswith: str | None = None, + fullname__iexact: str | None = None, + fullname__in: list[str] | None = None, + fullname__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + omemo_bundle: str | None = None, + omemo_bundle__contains: str | None = None, + omemo_bundle__icontains: str | None = None, + omemo_bundle__startswith: str | None = None, + omemo_bundle__istartswith: str | None = None, + omemo_bundle__endswith: str | None = None, + omemo_bundle__iendswith: str | None = None, + omemo_bundle__iexact: str | None = None, + omemo_bundle__in: list[str] | None = None, + omemo_bundle__isnull: bool | None = None, + password: str | None = None, + password__contains: str | None = None, + password__icontains: str | None = None, + password__startswith: str | None = None, + password__istartswith: str | None = None, + password__endswith: str | None = None, + password__iendswith: str | None = None, + password__iexact: str | None = None, + password__in: list[str] | None = None, + password__isnull: bool | None = None, + ) -> "UserQuery": + """Exclude objects matching field lookups.""" + ... + + def order_by(self, *fields: Literal["active", "-active", "admin", "-admin", "email", "-email", "fullname", "-fullname", "id", "-id", "omemo_bundle", "-omemo_bundle", "password", "-password"]) -> "UserQuery": # type: ignore[override] + """Order results by fields.""" + ... + + def limit(self, n: int) -> "UserQuery": + """Limit number of results.""" + ... + + def offset(self, n: int) -> "UserQuery": + """Skip first n results.""" + ... + + def distinct(self, value: bool = True) -> "UserQuery": + """Return distinct results.""" + ... + + def select(self, *fields: Literal["active", "admin", "email", "fullname", "id", "omemo_bundle", "password"]) -> "UserQuery": # type: ignore[override] + """Select specific fields.""" + ... + + def join(self, *paths: str) -> "UserQuery": + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> "UserQuery": + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> "UserQuery": + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> "UserQuery": + """Add FOR SHARE lock to query.""" + ... + + def annotate(self, **annotations: Any) -> "UserQuery": + """Add computed fields using aggregate functions.""" + ... + + def group_by(self, *fields: Literal["active", "admin", "email", "fullname", "id", "omemo_bundle", "password"]) -> "UserQuery": # type: ignore[override] + """Add GROUP BY clause.""" + ... + + def having(self, *q_exprs: Any, **kwargs: Any) -> "UserQuery": + """Add HAVING clause for filtering grouped results.""" + ... + + def values(self, *fields: Literal["active", "admin", "email", "fullname", "id", "omemo_bundle", "password"]) -> "UserQuery": # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["active", "admin", "email", "fullname", "id", "omemo_bundle", "password"], flat: bool = False) -> "UserQuery": # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + # Terminal methods (async, execute query) + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> list[User]: + """Execute query and return all results.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> User | None: + """Execute query and return first result.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> User | None: + """Execute query and return last result.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count matching objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def update( # type: ignore[override] + self, + *, + client: Any | None = None, + using: str | None = None, + **values: Any, + ) -> int: + """Update matching objects.""" + ... + + async def increment( + self, + field: str, + by: int | float = 1, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Atomically increment a field value.""" + ... + + async def delete( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Delete matching objects.""" + ... + +class UserManager(QueryManager[User]): + """Type-safe Manager for User model.""" + + # Query building methods (sync, return Query) + + def query(self) -> UserQuery: + """Return a Query builder for this model.""" + ... + + def filter( + self, + *args: Any, + active: bool | None = None, + active__in: list[bool] | None = None, + active__isnull: bool | None = None, + admin: bool | None = None, + admin__in: list[bool] | None = None, + admin__isnull: bool | None = None, + email: EmailStr | None = None, + email__in: list[EmailStr] | None = None, + email__isnull: bool | None = None, + fullname: str | None = None, + fullname__contains: str | None = None, + fullname__icontains: str | None = None, + fullname__startswith: str | None = None, + fullname__istartswith: str | None = None, + fullname__endswith: str | None = None, + fullname__iendswith: str | None = None, + fullname__iexact: str | None = None, + fullname__in: list[str] | None = None, + fullname__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + omemo_bundle: str | None = None, + omemo_bundle__contains: str | None = None, + omemo_bundle__icontains: str | None = None, + omemo_bundle__startswith: str | None = None, + omemo_bundle__istartswith: str | None = None, + omemo_bundle__endswith: str | None = None, + omemo_bundle__iendswith: str | None = None, + omemo_bundle__iexact: str | None = None, + omemo_bundle__in: list[str] | None = None, + omemo_bundle__isnull: bool | None = None, + password: str | None = None, + password__contains: str | None = None, + password__icontains: str | None = None, + password__startswith: str | None = None, + password__istartswith: str | None = None, + password__endswith: str | None = None, + password__iendswith: str | None = None, + password__iexact: str | None = None, + password__in: list[str] | None = None, + password__isnull: bool | None = None, + ) -> UserQuery: + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + active: bool | None = None, + active__in: list[bool] | None = None, + active__isnull: bool | None = None, + admin: bool | None = None, + admin__in: list[bool] | None = None, + admin__isnull: bool | None = None, + email: EmailStr | None = None, + email__in: list[EmailStr] | None = None, + email__isnull: bool | None = None, + fullname: str | None = None, + fullname__contains: str | None = None, + fullname__icontains: str | None = None, + fullname__startswith: str | None = None, + fullname__istartswith: str | None = None, + fullname__endswith: str | None = None, + fullname__iendswith: str | None = None, + fullname__iexact: str | None = None, + fullname__in: list[str] | None = None, + fullname__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + omemo_bundle: str | None = None, + omemo_bundle__contains: str | None = None, + omemo_bundle__icontains: str | None = None, + omemo_bundle__startswith: str | None = None, + omemo_bundle__istartswith: str | None = None, + omemo_bundle__endswith: str | None = None, + omemo_bundle__iendswith: str | None = None, + omemo_bundle__iexact: str | None = None, + omemo_bundle__in: list[str] | None = None, + omemo_bundle__isnull: bool | None = None, + password: str | None = None, + password__contains: str | None = None, + password__icontains: str | None = None, + password__startswith: str | None = None, + password__istartswith: str | None = None, + password__endswith: str | None = None, + password__iendswith: str | None = None, + password__iexact: str | None = None, + password__in: list[str] | None = None, + password__isnull: bool | None = None, + ) -> UserQuery: + """Exclude objects matching field lookups.""" + ... + + def values(self, *fields: Literal["active", "admin", "email", "fullname", "id", "omemo_bundle", "password"]) -> UserQuery: # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["active", "admin", "email", "fullname", "id", "omemo_bundle", "password"], flat: bool = False) -> UserQuery: # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + def distinct(self, distinct: bool = True) -> UserQuery: + """Return distinct results.""" + ... + + def join(self, *paths: str) -> UserQuery: + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> UserQuery: + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> UserQuery: + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> UserQuery: + """Add FOR SHARE lock to query.""" + ... + + # Terminal methods (async, execute query) + + async def get( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> User: + """Get single object matching lookups.""" + ... + + async def get_or_none( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> User | None: + """Get object or None if not found.""" + ... + + async def get_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[User, bool]: + """Get object or create if not found. Returns (object, created).""" + ... + + async def update_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[User, bool]: + """Get object, create if missing, or update it when defaults are provided.""" + ... + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + mode: str = "models", + ) -> list[User]: + """Get all objects.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> User | None: + """Get first object.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> User | None: + """Get last object.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count all objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def create( # type: ignore[override] + self, + *, + instance: User | None = None, + client: Any | None = None, + using: str | None = None, + active: bool | None = None, + admin: bool | None = None, + email: EmailStr | None = None, + fullname: str | None = None, + id: int | None = None, + omemo_bundle: str | None = None, + password: str | None = None, + ) -> User: + """Create new object.""" + ... + + async def bulk_create( # type: ignore[override] + self, + objects: list[User], + *, + batch_size: int | None = None, + client: Any | None = None, + using: str | None = None, + ) -> list[User]: + """Bulk create objects.""" + ... + + async def bulk_update( # type: ignore[override] + self, + objects: list[User], + fields: list[str], + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Bulk update objects.""" + ... + + +class UserRole(Model): + class Meta: + is_table: bool + table_name: str + id: int | None + user: User | None + role: BaseRole | None + user_id: int | None + role_id: int | None + objects: ClassVar["UserRoleManager"] + + +class UserRoleQuery(Query[UserRole]): + """Type-safe Query for UserRole model.""" + + # Query building methods (sync, return Query) + + def filter( + self, + *args: Any, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + role: BaseRole | None = None, + role__in: list[BaseRole] | None = None, + role__isnull: bool | None = None, + role_id: int | None = None, + role_id__gt: int | None = None, + role_id__gte: int | None = None, + role_id__lt: int | None = None, + role_id__lte: int | None = None, + role_id__between: tuple[int, int] | None = None, + role_id__range: int | None = None, + role_id__in: list[int] | None = None, + role_id__isnull: bool | None = None, + user: User | None = None, + user__in: list[User] | None = None, + user__isnull: bool | None = None, + user_id: int | None = None, + user_id__gt: int | None = None, + user_id__gte: int | None = None, + user_id__lt: int | None = None, + user_id__lte: int | None = None, + user_id__between: tuple[int, int] | None = None, + user_id__range: int | None = None, + user_id__in: list[int] | None = None, + user_id__isnull: bool | None = None, + ) -> "UserRoleQuery": + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + role: BaseRole | None = None, + role__in: list[BaseRole] | None = None, + role__isnull: bool | None = None, + role_id: int | None = None, + role_id__gt: int | None = None, + role_id__gte: int | None = None, + role_id__lt: int | None = None, + role_id__lte: int | None = None, + role_id__between: tuple[int, int] | None = None, + role_id__range: int | None = None, + role_id__in: list[int] | None = None, + role_id__isnull: bool | None = None, + user: User | None = None, + user__in: list[User] | None = None, + user__isnull: bool | None = None, + user_id: int | None = None, + user_id__gt: int | None = None, + user_id__gte: int | None = None, + user_id__lt: int | None = None, + user_id__lte: int | None = None, + user_id__between: tuple[int, int] | None = None, + user_id__range: int | None = None, + user_id__in: list[int] | None = None, + user_id__isnull: bool | None = None, + ) -> "UserRoleQuery": + """Exclude objects matching field lookups.""" + ... + + def order_by(self, *fields: Literal["id", "-id", "role", "-role", "role_id", "-role_id", "user", "-user", "user_id", "-user_id"]) -> "UserRoleQuery": # type: ignore[override] + """Order results by fields.""" + ... + + def limit(self, n: int) -> "UserRoleQuery": + """Limit number of results.""" + ... + + def offset(self, n: int) -> "UserRoleQuery": + """Skip first n results.""" + ... + + def distinct(self, value: bool = True) -> "UserRoleQuery": + """Return distinct results.""" + ... + + def select(self, *fields: Literal["id", "role", "role_id", "user", "user_id"]) -> "UserRoleQuery": # type: ignore[override] + """Select specific fields.""" + ... + + def join(self, *paths: str) -> "UserRoleQuery": + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> "UserRoleQuery": + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> "UserRoleQuery": + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> "UserRoleQuery": + """Add FOR SHARE lock to query.""" + ... + + def annotate(self, **annotations: Any) -> "UserRoleQuery": + """Add computed fields using aggregate functions.""" + ... + + def group_by(self, *fields: Literal["id", "role", "role_id", "user", "user_id"]) -> "UserRoleQuery": # type: ignore[override] + """Add GROUP BY clause.""" + ... + + def having(self, *q_exprs: Any, **kwargs: Any) -> "UserRoleQuery": + """Add HAVING clause for filtering grouped results.""" + ... + + def values(self, *fields: Literal["id", "role", "role_id", "user", "user_id"]) -> "UserRoleQuery": # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["id", "role", "role_id", "user", "user_id"], flat: bool = False) -> "UserRoleQuery": # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + # Terminal methods (async, execute query) + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> list[UserRole]: + """Execute query and return all results.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> UserRole | None: + """Execute query and return first result.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> UserRole | None: + """Execute query and return last result.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count matching objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def update( # type: ignore[override] + self, + *, + client: Any | None = None, + using: str | None = None, + **values: Any, + ) -> int: + """Update matching objects.""" + ... + + async def increment( + self, + field: str, + by: int | float = 1, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Atomically increment a field value.""" + ... + + async def delete( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Delete matching objects.""" + ... + +class UserRoleManager(QueryManager[UserRole]): + """Type-safe Manager for UserRole model.""" + + # Query building methods (sync, return Query) + + def query(self) -> UserRoleQuery: + """Return a Query builder for this model.""" + ... + + def filter( + self, + *args: Any, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + role: BaseRole | None = None, + role__in: list[BaseRole] | None = None, + role__isnull: bool | None = None, + role_id: int | None = None, + role_id__gt: int | None = None, + role_id__gte: int | None = None, + role_id__lt: int | None = None, + role_id__lte: int | None = None, + role_id__between: tuple[int, int] | None = None, + role_id__range: int | None = None, + role_id__in: list[int] | None = None, + role_id__isnull: bool | None = None, + user: User | None = None, + user__in: list[User] | None = None, + user__isnull: bool | None = None, + user_id: int | None = None, + user_id__gt: int | None = None, + user_id__gte: int | None = None, + user_id__lt: int | None = None, + user_id__lte: int | None = None, + user_id__between: tuple[int, int] | None = None, + user_id__range: int | None = None, + user_id__in: list[int] | None = None, + user_id__isnull: bool | None = None, + ) -> UserRoleQuery: + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + role: BaseRole | None = None, + role__in: list[BaseRole] | None = None, + role__isnull: bool | None = None, + role_id: int | None = None, + role_id__gt: int | None = None, + role_id__gte: int | None = None, + role_id__lt: int | None = None, + role_id__lte: int | None = None, + role_id__between: tuple[int, int] | None = None, + role_id__range: int | None = None, + role_id__in: list[int] | None = None, + role_id__isnull: bool | None = None, + user: User | None = None, + user__in: list[User] | None = None, + user__isnull: bool | None = None, + user_id: int | None = None, + user_id__gt: int | None = None, + user_id__gte: int | None = None, + user_id__lt: int | None = None, + user_id__lte: int | None = None, + user_id__between: tuple[int, int] | None = None, + user_id__range: int | None = None, + user_id__in: list[int] | None = None, + user_id__isnull: bool | None = None, + ) -> UserRoleQuery: + """Exclude objects matching field lookups.""" + ... + + def values(self, *fields: Literal["id", "role", "role_id", "user", "user_id"]) -> UserRoleQuery: # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["id", "role", "role_id", "user", "user_id"], flat: bool = False) -> UserRoleQuery: # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + def distinct(self, distinct: bool = True) -> UserRoleQuery: + """Return distinct results.""" + ... + + def join(self, *paths: str) -> UserRoleQuery: + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> UserRoleQuery: + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> UserRoleQuery: + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> UserRoleQuery: + """Add FOR SHARE lock to query.""" + ... + + # Terminal methods (async, execute query) + + async def get( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> UserRole: + """Get single object matching lookups.""" + ... + + async def get_or_none( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> UserRole | None: + """Get object or None if not found.""" + ... + + async def get_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[UserRole, bool]: + """Get object or create if not found. Returns (object, created).""" + ... + + async def update_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[UserRole, bool]: + """Get object, create if missing, or update it when defaults are provided.""" + ... + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + mode: str = "models", + ) -> list[UserRole]: + """Get all objects.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> UserRole | None: + """Get first object.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> UserRole | None: + """Get last object.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count all objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def create( # type: ignore[override] + self, + *, + instance: UserRole | None = None, + client: Any | None = None, + using: str | None = None, + id: int | None = None, + role: BaseRole | None = None, + role_id: int | None = None, + user: User | None = None, + user_id: int | None = None, + ) -> UserRole: + """Create new object.""" + ... + + async def bulk_create( # type: ignore[override] + self, + objects: list[UserRole], + *, + batch_size: int | None = None, + client: Any | None = None, + using: str | None = None, + ) -> list[UserRole]: + """Bulk create objects.""" + ... + + async def bulk_update( # type: ignore[override] + self, + objects: list[UserRole], + fields: list[str], + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Bulk update objects.""" + ... + + +class Theme(Model): + class Meta: + is_table: bool + table_name: str + id: int | None + name: str + bg_color: str + bg_secondary_color: str + color_primary: str + color_lightGrey: str + color_grey: str + color_darkGrey: str + color_error: str + color_success: str + grid_maxWidth: str + grid_gutter: str + font_size: str + font_color: str + font_family_sans: str + font_family_mono: str + objects: ClassVar["ThemeManager"] + + +class ThemeQuery(Query[Theme]): + """Type-safe Query for Theme model.""" + + # Query building methods (sync, return Query) + + def filter( + self, + *args: Any, + bg_color: str | None = None, + bg_color__contains: str | None = None, + bg_color__icontains: str | None = None, + bg_color__startswith: str | None = None, + bg_color__istartswith: str | None = None, + bg_color__endswith: str | None = None, + bg_color__iendswith: str | None = None, + bg_color__iexact: str | None = None, + bg_color__in: list[str] | None = None, + bg_color__isnull: bool | None = None, + bg_secondary_color: str | None = None, + bg_secondary_color__contains: str | None = None, + bg_secondary_color__icontains: str | None = None, + bg_secondary_color__startswith: str | None = None, + bg_secondary_color__istartswith: str | None = None, + bg_secondary_color__endswith: str | None = None, + bg_secondary_color__iendswith: str | None = None, + bg_secondary_color__iexact: str | None = None, + bg_secondary_color__in: list[str] | None = None, + bg_secondary_color__isnull: bool | None = None, + color_darkGrey: str | None = None, + color_darkGrey__contains: str | None = None, + color_darkGrey__icontains: str | None = None, + color_darkGrey__startswith: str | None = None, + color_darkGrey__istartswith: str | None = None, + color_darkGrey__endswith: str | None = None, + color_darkGrey__iendswith: str | None = None, + color_darkGrey__iexact: str | None = None, + color_darkGrey__in: list[str] | None = None, + color_darkGrey__isnull: bool | None = None, + color_error: str | None = None, + color_error__contains: str | None = None, + color_error__icontains: str | None = None, + color_error__startswith: str | None = None, + color_error__istartswith: str | None = None, + color_error__endswith: str | None = None, + color_error__iendswith: str | None = None, + color_error__iexact: str | None = None, + color_error__in: list[str] | None = None, + color_error__isnull: bool | None = None, + color_grey: str | None = None, + color_grey__contains: str | None = None, + color_grey__icontains: str | None = None, + color_grey__startswith: str | None = None, + color_grey__istartswith: str | None = None, + color_grey__endswith: str | None = None, + color_grey__iendswith: str | None = None, + color_grey__iexact: str | None = None, + color_grey__in: list[str] | None = None, + color_grey__isnull: bool | None = None, + color_lightGrey: str | None = None, + color_lightGrey__contains: str | None = None, + color_lightGrey__icontains: str | None = None, + color_lightGrey__startswith: str | None = None, + color_lightGrey__istartswith: str | None = None, + color_lightGrey__endswith: str | None = None, + color_lightGrey__iendswith: str | None = None, + color_lightGrey__iexact: str | None = None, + color_lightGrey__in: list[str] | None = None, + color_lightGrey__isnull: bool | None = None, + color_primary: str | None = None, + color_primary__contains: str | None = None, + color_primary__icontains: str | None = None, + color_primary__startswith: str | None = None, + color_primary__istartswith: str | None = None, + color_primary__endswith: str | None = None, + color_primary__iendswith: str | None = None, + color_primary__iexact: str | None = None, + color_primary__in: list[str] | None = None, + color_primary__isnull: bool | None = None, + color_success: str | None = None, + color_success__contains: str | None = None, + color_success__icontains: str | None = None, + color_success__startswith: str | None = None, + color_success__istartswith: str | None = None, + color_success__endswith: str | None = None, + color_success__iendswith: str | None = None, + color_success__iexact: str | None = None, + color_success__in: list[str] | None = None, + color_success__isnull: bool | None = None, + font_color: str | None = None, + font_color__contains: str | None = None, + font_color__icontains: str | None = None, + font_color__startswith: str | None = None, + font_color__istartswith: str | None = None, + font_color__endswith: str | None = None, + font_color__iendswith: str | None = None, + font_color__iexact: str | None = None, + font_color__in: list[str] | None = None, + font_color__isnull: bool | None = None, + font_family_mono: str | None = None, + font_family_mono__contains: str | None = None, + font_family_mono__icontains: str | None = None, + font_family_mono__startswith: str | None = None, + font_family_mono__istartswith: str | None = None, + font_family_mono__endswith: str | None = None, + font_family_mono__iendswith: str | None = None, + font_family_mono__iexact: str | None = None, + font_family_mono__in: list[str] | None = None, + font_family_mono__isnull: bool | None = None, + font_family_sans: str | None = None, + font_family_sans__contains: str | None = None, + font_family_sans__icontains: str | None = None, + font_family_sans__startswith: str | None = None, + font_family_sans__istartswith: str | None = None, + font_family_sans__endswith: str | None = None, + font_family_sans__iendswith: str | None = None, + font_family_sans__iexact: str | None = None, + font_family_sans__in: list[str] | None = None, + font_family_sans__isnull: bool | None = None, + font_size: str | None = None, + font_size__contains: str | None = None, + font_size__icontains: str | None = None, + font_size__startswith: str | None = None, + font_size__istartswith: str | None = None, + font_size__endswith: str | None = None, + font_size__iendswith: str | None = None, + font_size__iexact: str | None = None, + font_size__in: list[str] | None = None, + font_size__isnull: bool | None = None, + grid_gutter: str | None = None, + grid_gutter__contains: str | None = None, + grid_gutter__icontains: str | None = None, + grid_gutter__startswith: str | None = None, + grid_gutter__istartswith: str | None = None, + grid_gutter__endswith: str | None = None, + grid_gutter__iendswith: str | None = None, + grid_gutter__iexact: str | None = None, + grid_gutter__in: list[str] | None = None, + grid_gutter__isnull: bool | None = None, + grid_maxWidth: str | None = None, + grid_maxWidth__contains: str | None = None, + grid_maxWidth__icontains: str | None = None, + grid_maxWidth__startswith: str | None = None, + grid_maxWidth__istartswith: str | None = None, + grid_maxWidth__endswith: str | None = None, + grid_maxWidth__iendswith: str | None = None, + grid_maxWidth__iexact: str | None = None, + grid_maxWidth__in: list[str] | None = None, + grid_maxWidth__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + ) -> "ThemeQuery": + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + bg_color: str | None = None, + bg_color__contains: str | None = None, + bg_color__icontains: str | None = None, + bg_color__startswith: str | None = None, + bg_color__istartswith: str | None = None, + bg_color__endswith: str | None = None, + bg_color__iendswith: str | None = None, + bg_color__iexact: str | None = None, + bg_color__in: list[str] | None = None, + bg_color__isnull: bool | None = None, + bg_secondary_color: str | None = None, + bg_secondary_color__contains: str | None = None, + bg_secondary_color__icontains: str | None = None, + bg_secondary_color__startswith: str | None = None, + bg_secondary_color__istartswith: str | None = None, + bg_secondary_color__endswith: str | None = None, + bg_secondary_color__iendswith: str | None = None, + bg_secondary_color__iexact: str | None = None, + bg_secondary_color__in: list[str] | None = None, + bg_secondary_color__isnull: bool | None = None, + color_darkGrey: str | None = None, + color_darkGrey__contains: str | None = None, + color_darkGrey__icontains: str | None = None, + color_darkGrey__startswith: str | None = None, + color_darkGrey__istartswith: str | None = None, + color_darkGrey__endswith: str | None = None, + color_darkGrey__iendswith: str | None = None, + color_darkGrey__iexact: str | None = None, + color_darkGrey__in: list[str] | None = None, + color_darkGrey__isnull: bool | None = None, + color_error: str | None = None, + color_error__contains: str | None = None, + color_error__icontains: str | None = None, + color_error__startswith: str | None = None, + color_error__istartswith: str | None = None, + color_error__endswith: str | None = None, + color_error__iendswith: str | None = None, + color_error__iexact: str | None = None, + color_error__in: list[str] | None = None, + color_error__isnull: bool | None = None, + color_grey: str | None = None, + color_grey__contains: str | None = None, + color_grey__icontains: str | None = None, + color_grey__startswith: str | None = None, + color_grey__istartswith: str | None = None, + color_grey__endswith: str | None = None, + color_grey__iendswith: str | None = None, + color_grey__iexact: str | None = None, + color_grey__in: list[str] | None = None, + color_grey__isnull: bool | None = None, + color_lightGrey: str | None = None, + color_lightGrey__contains: str | None = None, + color_lightGrey__icontains: str | None = None, + color_lightGrey__startswith: str | None = None, + color_lightGrey__istartswith: str | None = None, + color_lightGrey__endswith: str | None = None, + color_lightGrey__iendswith: str | None = None, + color_lightGrey__iexact: str | None = None, + color_lightGrey__in: list[str] | None = None, + color_lightGrey__isnull: bool | None = None, + color_primary: str | None = None, + color_primary__contains: str | None = None, + color_primary__icontains: str | None = None, + color_primary__startswith: str | None = None, + color_primary__istartswith: str | None = None, + color_primary__endswith: str | None = None, + color_primary__iendswith: str | None = None, + color_primary__iexact: str | None = None, + color_primary__in: list[str] | None = None, + color_primary__isnull: bool | None = None, + color_success: str | None = None, + color_success__contains: str | None = None, + color_success__icontains: str | None = None, + color_success__startswith: str | None = None, + color_success__istartswith: str | None = None, + color_success__endswith: str | None = None, + color_success__iendswith: str | None = None, + color_success__iexact: str | None = None, + color_success__in: list[str] | None = None, + color_success__isnull: bool | None = None, + font_color: str | None = None, + font_color__contains: str | None = None, + font_color__icontains: str | None = None, + font_color__startswith: str | None = None, + font_color__istartswith: str | None = None, + font_color__endswith: str | None = None, + font_color__iendswith: str | None = None, + font_color__iexact: str | None = None, + font_color__in: list[str] | None = None, + font_color__isnull: bool | None = None, + font_family_mono: str | None = None, + font_family_mono__contains: str | None = None, + font_family_mono__icontains: str | None = None, + font_family_mono__startswith: str | None = None, + font_family_mono__istartswith: str | None = None, + font_family_mono__endswith: str | None = None, + font_family_mono__iendswith: str | None = None, + font_family_mono__iexact: str | None = None, + font_family_mono__in: list[str] | None = None, + font_family_mono__isnull: bool | None = None, + font_family_sans: str | None = None, + font_family_sans__contains: str | None = None, + font_family_sans__icontains: str | None = None, + font_family_sans__startswith: str | None = None, + font_family_sans__istartswith: str | None = None, + font_family_sans__endswith: str | None = None, + font_family_sans__iendswith: str | None = None, + font_family_sans__iexact: str | None = None, + font_family_sans__in: list[str] | None = None, + font_family_sans__isnull: bool | None = None, + font_size: str | None = None, + font_size__contains: str | None = None, + font_size__icontains: str | None = None, + font_size__startswith: str | None = None, + font_size__istartswith: str | None = None, + font_size__endswith: str | None = None, + font_size__iendswith: str | None = None, + font_size__iexact: str | None = None, + font_size__in: list[str] | None = None, + font_size__isnull: bool | None = None, + grid_gutter: str | None = None, + grid_gutter__contains: str | None = None, + grid_gutter__icontains: str | None = None, + grid_gutter__startswith: str | None = None, + grid_gutter__istartswith: str | None = None, + grid_gutter__endswith: str | None = None, + grid_gutter__iendswith: str | None = None, + grid_gutter__iexact: str | None = None, + grid_gutter__in: list[str] | None = None, + grid_gutter__isnull: bool | None = None, + grid_maxWidth: str | None = None, + grid_maxWidth__contains: str | None = None, + grid_maxWidth__icontains: str | None = None, + grid_maxWidth__startswith: str | None = None, + grid_maxWidth__istartswith: str | None = None, + grid_maxWidth__endswith: str | None = None, + grid_maxWidth__iendswith: str | None = None, + grid_maxWidth__iexact: str | None = None, + grid_maxWidth__in: list[str] | None = None, + grid_maxWidth__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + ) -> "ThemeQuery": + """Exclude objects matching field lookups.""" + ... + + def order_by(self, *fields: Literal["bg_color", "-bg_color", "bg_secondary_color", "-bg_secondary_color", "color_darkGrey", "-color_darkGrey", "color_error", "-color_error", "color_grey", "-color_grey", "color_lightGrey", "-color_lightGrey", "color_primary", "-color_primary", "color_success", "-color_success", "font_color", "-font_color", "font_family_mono", "-font_family_mono", "font_family_sans", "-font_family_sans", "font_size", "-font_size", "grid_gutter", "-grid_gutter", "grid_maxWidth", "-grid_maxWidth", "id", "-id", "name", "-name"]) -> "ThemeQuery": # type: ignore[override] + """Order results by fields.""" + ... + + def limit(self, n: int) -> "ThemeQuery": + """Limit number of results.""" + ... + + def offset(self, n: int) -> "ThemeQuery": + """Skip first n results.""" + ... + + def distinct(self, value: bool = True) -> "ThemeQuery": + """Return distinct results.""" + ... + + def select(self, *fields: Literal["bg_color", "bg_secondary_color", "color_darkGrey", "color_error", "color_grey", "color_lightGrey", "color_primary", "color_success", "font_color", "font_family_mono", "font_family_sans", "font_size", "grid_gutter", "grid_maxWidth", "id", "name"]) -> "ThemeQuery": # type: ignore[override] + """Select specific fields.""" + ... + + def join(self, *paths: str) -> "ThemeQuery": + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> "ThemeQuery": + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> "ThemeQuery": + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> "ThemeQuery": + """Add FOR SHARE lock to query.""" + ... + + def annotate(self, **annotations: Any) -> "ThemeQuery": + """Add computed fields using aggregate functions.""" + ... + + def group_by(self, *fields: Literal["bg_color", "bg_secondary_color", "color_darkGrey", "color_error", "color_grey", "color_lightGrey", "color_primary", "color_success", "font_color", "font_family_mono", "font_family_sans", "font_size", "grid_gutter", "grid_maxWidth", "id", "name"]) -> "ThemeQuery": # type: ignore[override] + """Add GROUP BY clause.""" + ... + + def having(self, *q_exprs: Any, **kwargs: Any) -> "ThemeQuery": + """Add HAVING clause for filtering grouped results.""" + ... + + def values(self, *fields: Literal["bg_color", "bg_secondary_color", "color_darkGrey", "color_error", "color_grey", "color_lightGrey", "color_primary", "color_success", "font_color", "font_family_mono", "font_family_sans", "font_size", "grid_gutter", "grid_maxWidth", "id", "name"]) -> "ThemeQuery": # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["bg_color", "bg_secondary_color", "color_darkGrey", "color_error", "color_grey", "color_lightGrey", "color_primary", "color_success", "font_color", "font_family_mono", "font_family_sans", "font_size", "grid_gutter", "grid_maxWidth", "id", "name"], flat: bool = False) -> "ThemeQuery": # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + # Terminal methods (async, execute query) + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> list[Theme]: + """Execute query and return all results.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> Theme | None: + """Execute query and return first result.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> Theme | None: + """Execute query and return last result.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count matching objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def update( # type: ignore[override] + self, + *, + client: Any | None = None, + using: str | None = None, + **values: Any, + ) -> int: + """Update matching objects.""" + ... + + async def increment( + self, + field: str, + by: int | float = 1, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Atomically increment a field value.""" + ... + + async def delete( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Delete matching objects.""" + ... + +class ThemeManager(QueryManager[Theme]): + """Type-safe Manager for Theme model.""" + + # Query building methods (sync, return Query) + + def query(self) -> ThemeQuery: + """Return a Query builder for this model.""" + ... + + def filter( + self, + *args: Any, + bg_color: str | None = None, + bg_color__contains: str | None = None, + bg_color__icontains: str | None = None, + bg_color__startswith: str | None = None, + bg_color__istartswith: str | None = None, + bg_color__endswith: str | None = None, + bg_color__iendswith: str | None = None, + bg_color__iexact: str | None = None, + bg_color__in: list[str] | None = None, + bg_color__isnull: bool | None = None, + bg_secondary_color: str | None = None, + bg_secondary_color__contains: str | None = None, + bg_secondary_color__icontains: str | None = None, + bg_secondary_color__startswith: str | None = None, + bg_secondary_color__istartswith: str | None = None, + bg_secondary_color__endswith: str | None = None, + bg_secondary_color__iendswith: str | None = None, + bg_secondary_color__iexact: str | None = None, + bg_secondary_color__in: list[str] | None = None, + bg_secondary_color__isnull: bool | None = None, + color_darkGrey: str | None = None, + color_darkGrey__contains: str | None = None, + color_darkGrey__icontains: str | None = None, + color_darkGrey__startswith: str | None = None, + color_darkGrey__istartswith: str | None = None, + color_darkGrey__endswith: str | None = None, + color_darkGrey__iendswith: str | None = None, + color_darkGrey__iexact: str | None = None, + color_darkGrey__in: list[str] | None = None, + color_darkGrey__isnull: bool | None = None, + color_error: str | None = None, + color_error__contains: str | None = None, + color_error__icontains: str | None = None, + color_error__startswith: str | None = None, + color_error__istartswith: str | None = None, + color_error__endswith: str | None = None, + color_error__iendswith: str | None = None, + color_error__iexact: str | None = None, + color_error__in: list[str] | None = None, + color_error__isnull: bool | None = None, + color_grey: str | None = None, + color_grey__contains: str | None = None, + color_grey__icontains: str | None = None, + color_grey__startswith: str | None = None, + color_grey__istartswith: str | None = None, + color_grey__endswith: str | None = None, + color_grey__iendswith: str | None = None, + color_grey__iexact: str | None = None, + color_grey__in: list[str] | None = None, + color_grey__isnull: bool | None = None, + color_lightGrey: str | None = None, + color_lightGrey__contains: str | None = None, + color_lightGrey__icontains: str | None = None, + color_lightGrey__startswith: str | None = None, + color_lightGrey__istartswith: str | None = None, + color_lightGrey__endswith: str | None = None, + color_lightGrey__iendswith: str | None = None, + color_lightGrey__iexact: str | None = None, + color_lightGrey__in: list[str] | None = None, + color_lightGrey__isnull: bool | None = None, + color_primary: str | None = None, + color_primary__contains: str | None = None, + color_primary__icontains: str | None = None, + color_primary__startswith: str | None = None, + color_primary__istartswith: str | None = None, + color_primary__endswith: str | None = None, + color_primary__iendswith: str | None = None, + color_primary__iexact: str | None = None, + color_primary__in: list[str] | None = None, + color_primary__isnull: bool | None = None, + color_success: str | None = None, + color_success__contains: str | None = None, + color_success__icontains: str | None = None, + color_success__startswith: str | None = None, + color_success__istartswith: str | None = None, + color_success__endswith: str | None = None, + color_success__iendswith: str | None = None, + color_success__iexact: str | None = None, + color_success__in: list[str] | None = None, + color_success__isnull: bool | None = None, + font_color: str | None = None, + font_color__contains: str | None = None, + font_color__icontains: str | None = None, + font_color__startswith: str | None = None, + font_color__istartswith: str | None = None, + font_color__endswith: str | None = None, + font_color__iendswith: str | None = None, + font_color__iexact: str | None = None, + font_color__in: list[str] | None = None, + font_color__isnull: bool | None = None, + font_family_mono: str | None = None, + font_family_mono__contains: str | None = None, + font_family_mono__icontains: str | None = None, + font_family_mono__startswith: str | None = None, + font_family_mono__istartswith: str | None = None, + font_family_mono__endswith: str | None = None, + font_family_mono__iendswith: str | None = None, + font_family_mono__iexact: str | None = None, + font_family_mono__in: list[str] | None = None, + font_family_mono__isnull: bool | None = None, + font_family_sans: str | None = None, + font_family_sans__contains: str | None = None, + font_family_sans__icontains: str | None = None, + font_family_sans__startswith: str | None = None, + font_family_sans__istartswith: str | None = None, + font_family_sans__endswith: str | None = None, + font_family_sans__iendswith: str | None = None, + font_family_sans__iexact: str | None = None, + font_family_sans__in: list[str] | None = None, + font_family_sans__isnull: bool | None = None, + font_size: str | None = None, + font_size__contains: str | None = None, + font_size__icontains: str | None = None, + font_size__startswith: str | None = None, + font_size__istartswith: str | None = None, + font_size__endswith: str | None = None, + font_size__iendswith: str | None = None, + font_size__iexact: str | None = None, + font_size__in: list[str] | None = None, + font_size__isnull: bool | None = None, + grid_gutter: str | None = None, + grid_gutter__contains: str | None = None, + grid_gutter__icontains: str | None = None, + grid_gutter__startswith: str | None = None, + grid_gutter__istartswith: str | None = None, + grid_gutter__endswith: str | None = None, + grid_gutter__iendswith: str | None = None, + grid_gutter__iexact: str | None = None, + grid_gutter__in: list[str] | None = None, + grid_gutter__isnull: bool | None = None, + grid_maxWidth: str | None = None, + grid_maxWidth__contains: str | None = None, + grid_maxWidth__icontains: str | None = None, + grid_maxWidth__startswith: str | None = None, + grid_maxWidth__istartswith: str | None = None, + grid_maxWidth__endswith: str | None = None, + grid_maxWidth__iendswith: str | None = None, + grid_maxWidth__iexact: str | None = None, + grid_maxWidth__in: list[str] | None = None, + grid_maxWidth__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + ) -> ThemeQuery: + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + bg_color: str | None = None, + bg_color__contains: str | None = None, + bg_color__icontains: str | None = None, + bg_color__startswith: str | None = None, + bg_color__istartswith: str | None = None, + bg_color__endswith: str | None = None, + bg_color__iendswith: str | None = None, + bg_color__iexact: str | None = None, + bg_color__in: list[str] | None = None, + bg_color__isnull: bool | None = None, + bg_secondary_color: str | None = None, + bg_secondary_color__contains: str | None = None, + bg_secondary_color__icontains: str | None = None, + bg_secondary_color__startswith: str | None = None, + bg_secondary_color__istartswith: str | None = None, + bg_secondary_color__endswith: str | None = None, + bg_secondary_color__iendswith: str | None = None, + bg_secondary_color__iexact: str | None = None, + bg_secondary_color__in: list[str] | None = None, + bg_secondary_color__isnull: bool | None = None, + color_darkGrey: str | None = None, + color_darkGrey__contains: str | None = None, + color_darkGrey__icontains: str | None = None, + color_darkGrey__startswith: str | None = None, + color_darkGrey__istartswith: str | None = None, + color_darkGrey__endswith: str | None = None, + color_darkGrey__iendswith: str | None = None, + color_darkGrey__iexact: str | None = None, + color_darkGrey__in: list[str] | None = None, + color_darkGrey__isnull: bool | None = None, + color_error: str | None = None, + color_error__contains: str | None = None, + color_error__icontains: str | None = None, + color_error__startswith: str | None = None, + color_error__istartswith: str | None = None, + color_error__endswith: str | None = None, + color_error__iendswith: str | None = None, + color_error__iexact: str | None = None, + color_error__in: list[str] | None = None, + color_error__isnull: bool | None = None, + color_grey: str | None = None, + color_grey__contains: str | None = None, + color_grey__icontains: str | None = None, + color_grey__startswith: str | None = None, + color_grey__istartswith: str | None = None, + color_grey__endswith: str | None = None, + color_grey__iendswith: str | None = None, + color_grey__iexact: str | None = None, + color_grey__in: list[str] | None = None, + color_grey__isnull: bool | None = None, + color_lightGrey: str | None = None, + color_lightGrey__contains: str | None = None, + color_lightGrey__icontains: str | None = None, + color_lightGrey__startswith: str | None = None, + color_lightGrey__istartswith: str | None = None, + color_lightGrey__endswith: str | None = None, + color_lightGrey__iendswith: str | None = None, + color_lightGrey__iexact: str | None = None, + color_lightGrey__in: list[str] | None = None, + color_lightGrey__isnull: bool | None = None, + color_primary: str | None = None, + color_primary__contains: str | None = None, + color_primary__icontains: str | None = None, + color_primary__startswith: str | None = None, + color_primary__istartswith: str | None = None, + color_primary__endswith: str | None = None, + color_primary__iendswith: str | None = None, + color_primary__iexact: str | None = None, + color_primary__in: list[str] | None = None, + color_primary__isnull: bool | None = None, + color_success: str | None = None, + color_success__contains: str | None = None, + color_success__icontains: str | None = None, + color_success__startswith: str | None = None, + color_success__istartswith: str | None = None, + color_success__endswith: str | None = None, + color_success__iendswith: str | None = None, + color_success__iexact: str | None = None, + color_success__in: list[str] | None = None, + color_success__isnull: bool | None = None, + font_color: str | None = None, + font_color__contains: str | None = None, + font_color__icontains: str | None = None, + font_color__startswith: str | None = None, + font_color__istartswith: str | None = None, + font_color__endswith: str | None = None, + font_color__iendswith: str | None = None, + font_color__iexact: str | None = None, + font_color__in: list[str] | None = None, + font_color__isnull: bool | None = None, + font_family_mono: str | None = None, + font_family_mono__contains: str | None = None, + font_family_mono__icontains: str | None = None, + font_family_mono__startswith: str | None = None, + font_family_mono__istartswith: str | None = None, + font_family_mono__endswith: str | None = None, + font_family_mono__iendswith: str | None = None, + font_family_mono__iexact: str | None = None, + font_family_mono__in: list[str] | None = None, + font_family_mono__isnull: bool | None = None, + font_family_sans: str | None = None, + font_family_sans__contains: str | None = None, + font_family_sans__icontains: str | None = None, + font_family_sans__startswith: str | None = None, + font_family_sans__istartswith: str | None = None, + font_family_sans__endswith: str | None = None, + font_family_sans__iendswith: str | None = None, + font_family_sans__iexact: str | None = None, + font_family_sans__in: list[str] | None = None, + font_family_sans__isnull: bool | None = None, + font_size: str | None = None, + font_size__contains: str | None = None, + font_size__icontains: str | None = None, + font_size__startswith: str | None = None, + font_size__istartswith: str | None = None, + font_size__endswith: str | None = None, + font_size__iendswith: str | None = None, + font_size__iexact: str | None = None, + font_size__in: list[str] | None = None, + font_size__isnull: bool | None = None, + grid_gutter: str | None = None, + grid_gutter__contains: str | None = None, + grid_gutter__icontains: str | None = None, + grid_gutter__startswith: str | None = None, + grid_gutter__istartswith: str | None = None, + grid_gutter__endswith: str | None = None, + grid_gutter__iendswith: str | None = None, + grid_gutter__iexact: str | None = None, + grid_gutter__in: list[str] | None = None, + grid_gutter__isnull: bool | None = None, + grid_maxWidth: str | None = None, + grid_maxWidth__contains: str | None = None, + grid_maxWidth__icontains: str | None = None, + grid_maxWidth__startswith: str | None = None, + grid_maxWidth__istartswith: str | None = None, + grid_maxWidth__endswith: str | None = None, + grid_maxWidth__iendswith: str | None = None, + grid_maxWidth__iexact: str | None = None, + grid_maxWidth__in: list[str] | None = None, + grid_maxWidth__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + ) -> ThemeQuery: + """Exclude objects matching field lookups.""" + ... + + def values(self, *fields: Literal["bg_color", "bg_secondary_color", "color_darkGrey", "color_error", "color_grey", "color_lightGrey", "color_primary", "color_success", "font_color", "font_family_mono", "font_family_sans", "font_size", "grid_gutter", "grid_maxWidth", "id", "name"]) -> ThemeQuery: # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["bg_color", "bg_secondary_color", "color_darkGrey", "color_error", "color_grey", "color_lightGrey", "color_primary", "color_success", "font_color", "font_family_mono", "font_family_sans", "font_size", "grid_gutter", "grid_maxWidth", "id", "name"], flat: bool = False) -> ThemeQuery: # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + def distinct(self, distinct: bool = True) -> ThemeQuery: + """Return distinct results.""" + ... + + def join(self, *paths: str) -> ThemeQuery: + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> ThemeQuery: + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> ThemeQuery: + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> ThemeQuery: + """Add FOR SHARE lock to query.""" + ... + + # Terminal methods (async, execute query) + + async def get( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> Theme: + """Get single object matching lookups.""" + ... + + async def get_or_none( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> Theme | None: + """Get object or None if not found.""" + ... + + async def get_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[Theme, bool]: + """Get object or create if not found. Returns (object, created).""" + ... + + async def update_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[Theme, bool]: + """Get object, create if missing, or update it when defaults are provided.""" + ... + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + mode: str = "models", + ) -> list[Theme]: + """Get all objects.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> Theme | None: + """Get first object.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> Theme | None: + """Get last object.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count all objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def create( # type: ignore[override] + self, + *, + instance: Theme | None = None, + client: Any | None = None, + using: str | None = None, + bg_color: str | None = None, + bg_secondary_color: str | None = None, + color_darkGrey: str | None = None, + color_error: str | None = None, + color_grey: str | None = None, + color_lightGrey: str | None = None, + color_primary: str | None = None, + color_success: str | None = None, + font_color: str | None = None, + font_family_mono: str | None = None, + font_family_sans: str | None = None, + font_size: str | None = None, + grid_gutter: str | None = None, + grid_maxWidth: str | None = None, + id: int | None = None, + name: str | None = None, + ) -> Theme: + """Create new object.""" + ... + + async def bulk_create( # type: ignore[override] + self, + objects: list[Theme], + *, + batch_size: int | None = None, + client: Any | None = None, + using: str | None = None, + ) -> list[Theme]: + """Bulk create objects.""" + ... + + async def bulk_update( # type: ignore[override] + self, + objects: list[Theme], + fields: list[str], + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Bulk update objects.""" + ... + + +class MailingList(Model): + class Meta: + is_table: bool + table_name: str + id: int | None + name: str + address: EmailStr + distribution_address: EmailStr + archive_address: EmailStr + description: str | None + public: bool + archive_enabled: bool + moderation_enabled: bool + principal_id: int | None + inbox_principal_id: int | None + archive_principal_id: int | None + created_at: datetime | None + updated_at: datetime | None + objects: ClassVar["MailingListManager"] + + +class MailingListQuery(Query[MailingList]): + """Type-safe Query for MailingList model.""" + + # Query building methods (sync, return Query) + + def filter( + self, + *args: Any, + address: EmailStr | None = None, + address__in: list[EmailStr] | None = None, + address__isnull: bool | None = None, + archive_address: EmailStr | None = None, + archive_address__in: list[EmailStr] | None = None, + archive_address__isnull: bool | None = None, + archive_enabled: bool | None = None, + archive_enabled__in: list[bool] | None = None, + archive_enabled__isnull: bool | None = None, + archive_principal_id: int | None = None, + archive_principal_id__gt: int | None = None, + archive_principal_id__gte: int | None = None, + archive_principal_id__lt: int | None = None, + archive_principal_id__lte: int | None = None, + archive_principal_id__between: tuple[int, int] | None = None, + archive_principal_id__range: int | None = None, + archive_principal_id__in: list[int] | None = None, + archive_principal_id__isnull: bool | None = None, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + description: str | None = None, + description__contains: str | None = None, + description__icontains: str | None = None, + description__startswith: str | None = None, + description__istartswith: str | None = None, + description__endswith: str | None = None, + description__iendswith: str | None = None, + description__iexact: str | None = None, + description__in: list[str] | None = None, + description__isnull: bool | None = None, + distribution_address: EmailStr | None = None, + distribution_address__in: list[EmailStr] | None = None, + distribution_address__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + inbox_principal_id: int | None = None, + inbox_principal_id__gt: int | None = None, + inbox_principal_id__gte: int | None = None, + inbox_principal_id__lt: int | None = None, + inbox_principal_id__lte: int | None = None, + inbox_principal_id__between: tuple[int, int] | None = None, + inbox_principal_id__range: int | None = None, + inbox_principal_id__in: list[int] | None = None, + inbox_principal_id__isnull: bool | None = None, + moderation_enabled: bool | None = None, + moderation_enabled__in: list[bool] | None = None, + moderation_enabled__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + principal_id: int | None = None, + principal_id__gt: int | None = None, + principal_id__gte: int | None = None, + principal_id__lt: int | None = None, + principal_id__lte: int | None = None, + principal_id__between: tuple[int, int] | None = None, + principal_id__range: int | None = None, + principal_id__in: list[int] | None = None, + principal_id__isnull: bool | None = None, + public: bool | None = None, + public__in: list[bool] | None = None, + public__isnull: bool | None = None, + updated_at: datetime | None = None, + updated_at__gt: datetime | None = None, + updated_at__gte: datetime | None = None, + updated_at__lt: datetime | None = None, + updated_at__lte: datetime | None = None, + updated_at__between: tuple[datetime, datetime] | None = None, + updated_at__range: datetime | None = None, + updated_at__year: int | None = None, + updated_at__month: int | None = None, + updated_at__day: int | None = None, + updated_at__in: list[datetime] | None = None, + updated_at__isnull: bool | None = None, + ) -> "MailingListQuery": + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + address: EmailStr | None = None, + address__in: list[EmailStr] | None = None, + address__isnull: bool | None = None, + archive_address: EmailStr | None = None, + archive_address__in: list[EmailStr] | None = None, + archive_address__isnull: bool | None = None, + archive_enabled: bool | None = None, + archive_enabled__in: list[bool] | None = None, + archive_enabled__isnull: bool | None = None, + archive_principal_id: int | None = None, + archive_principal_id__gt: int | None = None, + archive_principal_id__gte: int | None = None, + archive_principal_id__lt: int | None = None, + archive_principal_id__lte: int | None = None, + archive_principal_id__between: tuple[int, int] | None = None, + archive_principal_id__range: int | None = None, + archive_principal_id__in: list[int] | None = None, + archive_principal_id__isnull: bool | None = None, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + description: str | None = None, + description__contains: str | None = None, + description__icontains: str | None = None, + description__startswith: str | None = None, + description__istartswith: str | None = None, + description__endswith: str | None = None, + description__iendswith: str | None = None, + description__iexact: str | None = None, + description__in: list[str] | None = None, + description__isnull: bool | None = None, + distribution_address: EmailStr | None = None, + distribution_address__in: list[EmailStr] | None = None, + distribution_address__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + inbox_principal_id: int | None = None, + inbox_principal_id__gt: int | None = None, + inbox_principal_id__gte: int | None = None, + inbox_principal_id__lt: int | None = None, + inbox_principal_id__lte: int | None = None, + inbox_principal_id__between: tuple[int, int] | None = None, + inbox_principal_id__range: int | None = None, + inbox_principal_id__in: list[int] | None = None, + inbox_principal_id__isnull: bool | None = None, + moderation_enabled: bool | None = None, + moderation_enabled__in: list[bool] | None = None, + moderation_enabled__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + principal_id: int | None = None, + principal_id__gt: int | None = None, + principal_id__gte: int | None = None, + principal_id__lt: int | None = None, + principal_id__lte: int | None = None, + principal_id__between: tuple[int, int] | None = None, + principal_id__range: int | None = None, + principal_id__in: list[int] | None = None, + principal_id__isnull: bool | None = None, + public: bool | None = None, + public__in: list[bool] | None = None, + public__isnull: bool | None = None, + updated_at: datetime | None = None, + updated_at__gt: datetime | None = None, + updated_at__gte: datetime | None = None, + updated_at__lt: datetime | None = None, + updated_at__lte: datetime | None = None, + updated_at__between: tuple[datetime, datetime] | None = None, + updated_at__range: datetime | None = None, + updated_at__year: int | None = None, + updated_at__month: int | None = None, + updated_at__day: int | None = None, + updated_at__in: list[datetime] | None = None, + updated_at__isnull: bool | None = None, + ) -> "MailingListQuery": + """Exclude objects matching field lookups.""" + ... + + def order_by(self, *fields: Literal["address", "-address", "archive_address", "-archive_address", "archive_enabled", "-archive_enabled", "archive_principal_id", "-archive_principal_id", "created_at", "-created_at", "description", "-description", "distribution_address", "-distribution_address", "id", "-id", "inbox_principal_id", "-inbox_principal_id", "moderation_enabled", "-moderation_enabled", "name", "-name", "principal_id", "-principal_id", "public", "-public", "updated_at", "-updated_at"]) -> "MailingListQuery": # type: ignore[override] + """Order results by fields.""" + ... + + def limit(self, n: int) -> "MailingListQuery": + """Limit number of results.""" + ... + + def offset(self, n: int) -> "MailingListQuery": + """Skip first n results.""" + ... + + def distinct(self, value: bool = True) -> "MailingListQuery": + """Return distinct results.""" + ... + + def select(self, *fields: Literal["address", "archive_address", "archive_enabled", "archive_principal_id", "created_at", "description", "distribution_address", "id", "inbox_principal_id", "moderation_enabled", "name", "principal_id", "public", "updated_at"]) -> "MailingListQuery": # type: ignore[override] + """Select specific fields.""" + ... + + def join(self, *paths: str) -> "MailingListQuery": + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> "MailingListQuery": + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> "MailingListQuery": + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> "MailingListQuery": + """Add FOR SHARE lock to query.""" + ... + + def annotate(self, **annotations: Any) -> "MailingListQuery": + """Add computed fields using aggregate functions.""" + ... + + def group_by(self, *fields: Literal["address", "archive_address", "archive_enabled", "archive_principal_id", "created_at", "description", "distribution_address", "id", "inbox_principal_id", "moderation_enabled", "name", "principal_id", "public", "updated_at"]) -> "MailingListQuery": # type: ignore[override] + """Add GROUP BY clause.""" + ... + + def having(self, *q_exprs: Any, **kwargs: Any) -> "MailingListQuery": + """Add HAVING clause for filtering grouped results.""" + ... + + def values(self, *fields: Literal["address", "archive_address", "archive_enabled", "archive_principal_id", "created_at", "description", "distribution_address", "id", "inbox_principal_id", "moderation_enabled", "name", "principal_id", "public", "updated_at"]) -> "MailingListQuery": # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["address", "archive_address", "archive_enabled", "archive_principal_id", "created_at", "description", "distribution_address", "id", "inbox_principal_id", "moderation_enabled", "name", "principal_id", "public", "updated_at"], flat: bool = False) -> "MailingListQuery": # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + # Terminal methods (async, execute query) + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> list[MailingList]: + """Execute query and return all results.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> MailingList | None: + """Execute query and return first result.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> MailingList | None: + """Execute query and return last result.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count matching objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def update( # type: ignore[override] + self, + *, + client: Any | None = None, + using: str | None = None, + **values: Any, + ) -> int: + """Update matching objects.""" + ... + + async def increment( + self, + field: str, + by: int | float = 1, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Atomically increment a field value.""" + ... + + async def delete( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Delete matching objects.""" + ... + +class MailingListManager(QueryManager[MailingList]): + """Type-safe Manager for MailingList model.""" + + # Query building methods (sync, return Query) + + def query(self) -> MailingListQuery: + """Return a Query builder for this model.""" + ... + + def filter( + self, + *args: Any, + address: EmailStr | None = None, + address__in: list[EmailStr] | None = None, + address__isnull: bool | None = None, + archive_address: EmailStr | None = None, + archive_address__in: list[EmailStr] | None = None, + archive_address__isnull: bool | None = None, + archive_enabled: bool | None = None, + archive_enabled__in: list[bool] | None = None, + archive_enabled__isnull: bool | None = None, + archive_principal_id: int | None = None, + archive_principal_id__gt: int | None = None, + archive_principal_id__gte: int | None = None, + archive_principal_id__lt: int | None = None, + archive_principal_id__lte: int | None = None, + archive_principal_id__between: tuple[int, int] | None = None, + archive_principal_id__range: int | None = None, + archive_principal_id__in: list[int] | None = None, + archive_principal_id__isnull: bool | None = None, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + description: str | None = None, + description__contains: str | None = None, + description__icontains: str | None = None, + description__startswith: str | None = None, + description__istartswith: str | None = None, + description__endswith: str | None = None, + description__iendswith: str | None = None, + description__iexact: str | None = None, + description__in: list[str] | None = None, + description__isnull: bool | None = None, + distribution_address: EmailStr | None = None, + distribution_address__in: list[EmailStr] | None = None, + distribution_address__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + inbox_principal_id: int | None = None, + inbox_principal_id__gt: int | None = None, + inbox_principal_id__gte: int | None = None, + inbox_principal_id__lt: int | None = None, + inbox_principal_id__lte: int | None = None, + inbox_principal_id__between: tuple[int, int] | None = None, + inbox_principal_id__range: int | None = None, + inbox_principal_id__in: list[int] | None = None, + inbox_principal_id__isnull: bool | None = None, + moderation_enabled: bool | None = None, + moderation_enabled__in: list[bool] | None = None, + moderation_enabled__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + principal_id: int | None = None, + principal_id__gt: int | None = None, + principal_id__gte: int | None = None, + principal_id__lt: int | None = None, + principal_id__lte: int | None = None, + principal_id__between: tuple[int, int] | None = None, + principal_id__range: int | None = None, + principal_id__in: list[int] | None = None, + principal_id__isnull: bool | None = None, + public: bool | None = None, + public__in: list[bool] | None = None, + public__isnull: bool | None = None, + updated_at: datetime | None = None, + updated_at__gt: datetime | None = None, + updated_at__gte: datetime | None = None, + updated_at__lt: datetime | None = None, + updated_at__lte: datetime | None = None, + updated_at__between: tuple[datetime, datetime] | None = None, + updated_at__range: datetime | None = None, + updated_at__year: int | None = None, + updated_at__month: int | None = None, + updated_at__day: int | None = None, + updated_at__in: list[datetime] | None = None, + updated_at__isnull: bool | None = None, + ) -> MailingListQuery: + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + address: EmailStr | None = None, + address__in: list[EmailStr] | None = None, + address__isnull: bool | None = None, + archive_address: EmailStr | None = None, + archive_address__in: list[EmailStr] | None = None, + archive_address__isnull: bool | None = None, + archive_enabled: bool | None = None, + archive_enabled__in: list[bool] | None = None, + archive_enabled__isnull: bool | None = None, + archive_principal_id: int | None = None, + archive_principal_id__gt: int | None = None, + archive_principal_id__gte: int | None = None, + archive_principal_id__lt: int | None = None, + archive_principal_id__lte: int | None = None, + archive_principal_id__between: tuple[int, int] | None = None, + archive_principal_id__range: int | None = None, + archive_principal_id__in: list[int] | None = None, + archive_principal_id__isnull: bool | None = None, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + description: str | None = None, + description__contains: str | None = None, + description__icontains: str | None = None, + description__startswith: str | None = None, + description__istartswith: str | None = None, + description__endswith: str | None = None, + description__iendswith: str | None = None, + description__iexact: str | None = None, + description__in: list[str] | None = None, + description__isnull: bool | None = None, + distribution_address: EmailStr | None = None, + distribution_address__in: list[EmailStr] | None = None, + distribution_address__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + inbox_principal_id: int | None = None, + inbox_principal_id__gt: int | None = None, + inbox_principal_id__gte: int | None = None, + inbox_principal_id__lt: int | None = None, + inbox_principal_id__lte: int | None = None, + inbox_principal_id__between: tuple[int, int] | None = None, + inbox_principal_id__range: int | None = None, + inbox_principal_id__in: list[int] | None = None, + inbox_principal_id__isnull: bool | None = None, + moderation_enabled: bool | None = None, + moderation_enabled__in: list[bool] | None = None, + moderation_enabled__isnull: bool | None = None, + name: str | None = None, + name__contains: str | None = None, + name__icontains: str | None = None, + name__startswith: str | None = None, + name__istartswith: str | None = None, + name__endswith: str | None = None, + name__iendswith: str | None = None, + name__iexact: str | None = None, + name__in: list[str] | None = None, + name__isnull: bool | None = None, + principal_id: int | None = None, + principal_id__gt: int | None = None, + principal_id__gte: int | None = None, + principal_id__lt: int | None = None, + principal_id__lte: int | None = None, + principal_id__between: tuple[int, int] | None = None, + principal_id__range: int | None = None, + principal_id__in: list[int] | None = None, + principal_id__isnull: bool | None = None, + public: bool | None = None, + public__in: list[bool] | None = None, + public__isnull: bool | None = None, + updated_at: datetime | None = None, + updated_at__gt: datetime | None = None, + updated_at__gte: datetime | None = None, + updated_at__lt: datetime | None = None, + updated_at__lte: datetime | None = None, + updated_at__between: tuple[datetime, datetime] | None = None, + updated_at__range: datetime | None = None, + updated_at__year: int | None = None, + updated_at__month: int | None = None, + updated_at__day: int | None = None, + updated_at__in: list[datetime] | None = None, + updated_at__isnull: bool | None = None, + ) -> MailingListQuery: + """Exclude objects matching field lookups.""" + ... + + def values(self, *fields: Literal["address", "archive_address", "archive_enabled", "archive_principal_id", "created_at", "description", "distribution_address", "id", "inbox_principal_id", "moderation_enabled", "name", "principal_id", "public", "updated_at"]) -> MailingListQuery: # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["address", "archive_address", "archive_enabled", "archive_principal_id", "created_at", "description", "distribution_address", "id", "inbox_principal_id", "moderation_enabled", "name", "principal_id", "public", "updated_at"], flat: bool = False) -> MailingListQuery: # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + def distinct(self, distinct: bool = True) -> MailingListQuery: + """Return distinct results.""" + ... + + def join(self, *paths: str) -> MailingListQuery: + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> MailingListQuery: + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> MailingListQuery: + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> MailingListQuery: + """Add FOR SHARE lock to query.""" + ... + + # Terminal methods (async, execute query) + + async def get( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> MailingList: + """Get single object matching lookups.""" + ... + + async def get_or_none( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> MailingList | None: + """Get object or None if not found.""" + ... + + async def get_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[MailingList, bool]: + """Get object or create if not found. Returns (object, created).""" + ... + + async def update_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[MailingList, bool]: + """Get object, create if missing, or update it when defaults are provided.""" + ... + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + mode: str = "models", + ) -> list[MailingList]: + """Get all objects.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> MailingList | None: + """Get first object.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> MailingList | None: + """Get last object.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count all objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def create( # type: ignore[override] + self, + *, + instance: MailingList | None = None, + client: Any | None = None, + using: str | None = None, + address: EmailStr | None = None, + archive_address: EmailStr | None = None, + archive_enabled: bool | None = None, + archive_principal_id: int | None = None, + created_at: datetime | None = None, + description: str | None = None, + distribution_address: EmailStr | None = None, + id: int | None = None, + inbox_principal_id: int | None = None, + moderation_enabled: bool | None = None, + name: str | None = None, + principal_id: int | None = None, + public: bool | None = None, + updated_at: datetime | None = None, + ) -> MailingList: + """Create new object.""" + ... + + async def bulk_create( # type: ignore[override] + self, + objects: list[MailingList], + *, + batch_size: int | None = None, + client: Any | None = None, + using: str | None = None, + ) -> list[MailingList]: + """Bulk create objects.""" + ... + + async def bulk_update( # type: ignore[override] + self, + objects: list[MailingList], + fields: list[str], + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Bulk update objects.""" + ... + + +class PendingSubscriber(Model): + class Meta: + is_table: bool + table_name: str + id: int | None + mailing_list: MailingList | None + email: EmailStr + token: str + action: str + created_at: datetime | None + mailing_list_id: int | None + objects: ClassVar["PendingSubscriberManager"] + + +class PendingSubscriberQuery(Query[PendingSubscriber]): + """Type-safe Query for PendingSubscriber model.""" + + # Query building methods (sync, return Query) + + def filter( + self, + *args: Any, + action: str | None = None, + action__contains: str | None = None, + action__icontains: str | None = None, + action__startswith: str | None = None, + action__istartswith: str | None = None, + action__endswith: str | None = None, + action__iendswith: str | None = None, + action__iexact: str | None = None, + action__in: list[str] | None = None, + action__isnull: bool | None = None, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + email: EmailStr | None = None, + email__in: list[EmailStr] | None = None, + email__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + mailing_list: MailingList | None = None, + mailing_list__in: list[MailingList] | None = None, + mailing_list__isnull: bool | None = None, + mailing_list_id: int | None = None, + mailing_list_id__gt: int | None = None, + mailing_list_id__gte: int | None = None, + mailing_list_id__lt: int | None = None, + mailing_list_id__lte: int | None = None, + mailing_list_id__between: tuple[int, int] | None = None, + mailing_list_id__range: int | None = None, + mailing_list_id__in: list[int] | None = None, + mailing_list_id__isnull: bool | None = None, + token: str | None = None, + token__contains: str | None = None, + token__icontains: str | None = None, + token__startswith: str | None = None, + token__istartswith: str | None = None, + token__endswith: str | None = None, + token__iendswith: str | None = None, + token__iexact: str | None = None, + token__in: list[str] | None = None, + token__isnull: bool | None = None, + ) -> "PendingSubscriberQuery": + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + action: str | None = None, + action__contains: str | None = None, + action__icontains: str | None = None, + action__startswith: str | None = None, + action__istartswith: str | None = None, + action__endswith: str | None = None, + action__iendswith: str | None = None, + action__iexact: str | None = None, + action__in: list[str] | None = None, + action__isnull: bool | None = None, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + email: EmailStr | None = None, + email__in: list[EmailStr] | None = None, + email__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + mailing_list: MailingList | None = None, + mailing_list__in: list[MailingList] | None = None, + mailing_list__isnull: bool | None = None, + mailing_list_id: int | None = None, + mailing_list_id__gt: int | None = None, + mailing_list_id__gte: int | None = None, + mailing_list_id__lt: int | None = None, + mailing_list_id__lte: int | None = None, + mailing_list_id__between: tuple[int, int] | None = None, + mailing_list_id__range: int | None = None, + mailing_list_id__in: list[int] | None = None, + mailing_list_id__isnull: bool | None = None, + token: str | None = None, + token__contains: str | None = None, + token__icontains: str | None = None, + token__startswith: str | None = None, + token__istartswith: str | None = None, + token__endswith: str | None = None, + token__iendswith: str | None = None, + token__iexact: str | None = None, + token__in: list[str] | None = None, + token__isnull: bool | None = None, + ) -> "PendingSubscriberQuery": + """Exclude objects matching field lookups.""" + ... + + def order_by(self, *fields: Literal["action", "-action", "created_at", "-created_at", "email", "-email", "id", "-id", "mailing_list", "-mailing_list", "mailing_list_id", "-mailing_list_id", "token", "-token"]) -> "PendingSubscriberQuery": # type: ignore[override] + """Order results by fields.""" + ... + + def limit(self, n: int) -> "PendingSubscriberQuery": + """Limit number of results.""" + ... + + def offset(self, n: int) -> "PendingSubscriberQuery": + """Skip first n results.""" + ... + + def distinct(self, value: bool = True) -> "PendingSubscriberQuery": + """Return distinct results.""" + ... + + def select(self, *fields: Literal["action", "created_at", "email", "id", "mailing_list", "mailing_list_id", "token"]) -> "PendingSubscriberQuery": # type: ignore[override] + """Select specific fields.""" + ... + + def join(self, *paths: str) -> "PendingSubscriberQuery": + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> "PendingSubscriberQuery": + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> "PendingSubscriberQuery": + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> "PendingSubscriberQuery": + """Add FOR SHARE lock to query.""" + ... + + def annotate(self, **annotations: Any) -> "PendingSubscriberQuery": + """Add computed fields using aggregate functions.""" + ... + + def group_by(self, *fields: Literal["action", "created_at", "email", "id", "mailing_list", "mailing_list_id", "token"]) -> "PendingSubscriberQuery": # type: ignore[override] + """Add GROUP BY clause.""" + ... + + def having(self, *q_exprs: Any, **kwargs: Any) -> "PendingSubscriberQuery": + """Add HAVING clause for filtering grouped results.""" + ... + + def values(self, *fields: Literal["action", "created_at", "email", "id", "mailing_list", "mailing_list_id", "token"]) -> "PendingSubscriberQuery": # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["action", "created_at", "email", "id", "mailing_list", "mailing_list_id", "token"], flat: bool = False) -> "PendingSubscriberQuery": # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + # Terminal methods (async, execute query) + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> list[PendingSubscriber]: + """Execute query and return all results.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> PendingSubscriber | None: + """Execute query and return first result.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> PendingSubscriber | None: + """Execute query and return last result.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count matching objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def update( # type: ignore[override] + self, + *, + client: Any | None = None, + using: str | None = None, + **values: Any, + ) -> int: + """Update matching objects.""" + ... + + async def increment( + self, + field: str, + by: int | float = 1, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Atomically increment a field value.""" + ... + + async def delete( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Delete matching objects.""" + ... + +class PendingSubscriberManager(QueryManager[PendingSubscriber]): + """Type-safe Manager for PendingSubscriber model.""" + + # Query building methods (sync, return Query) + + def query(self) -> PendingSubscriberQuery: + """Return a Query builder for this model.""" + ... + + def filter( + self, + *args: Any, + action: str | None = None, + action__contains: str | None = None, + action__icontains: str | None = None, + action__startswith: str | None = None, + action__istartswith: str | None = None, + action__endswith: str | None = None, + action__iendswith: str | None = None, + action__iexact: str | None = None, + action__in: list[str] | None = None, + action__isnull: bool | None = None, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + email: EmailStr | None = None, + email__in: list[EmailStr] | None = None, + email__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + mailing_list: MailingList | None = None, + mailing_list__in: list[MailingList] | None = None, + mailing_list__isnull: bool | None = None, + mailing_list_id: int | None = None, + mailing_list_id__gt: int | None = None, + mailing_list_id__gte: int | None = None, + mailing_list_id__lt: int | None = None, + mailing_list_id__lte: int | None = None, + mailing_list_id__between: tuple[int, int] | None = None, + mailing_list_id__range: int | None = None, + mailing_list_id__in: list[int] | None = None, + mailing_list_id__isnull: bool | None = None, + token: str | None = None, + token__contains: str | None = None, + token__icontains: str | None = None, + token__startswith: str | None = None, + token__istartswith: str | None = None, + token__endswith: str | None = None, + token__iendswith: str | None = None, + token__iexact: str | None = None, + token__in: list[str] | None = None, + token__isnull: bool | None = None, + ) -> PendingSubscriberQuery: + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + action: str | None = None, + action__contains: str | None = None, + action__icontains: str | None = None, + action__startswith: str | None = None, + action__istartswith: str | None = None, + action__endswith: str | None = None, + action__iendswith: str | None = None, + action__iexact: str | None = None, + action__in: list[str] | None = None, + action__isnull: bool | None = None, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + email: EmailStr | None = None, + email__in: list[EmailStr] | None = None, + email__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + mailing_list: MailingList | None = None, + mailing_list__in: list[MailingList] | None = None, + mailing_list__isnull: bool | None = None, + mailing_list_id: int | None = None, + mailing_list_id__gt: int | None = None, + mailing_list_id__gte: int | None = None, + mailing_list_id__lt: int | None = None, + mailing_list_id__lte: int | None = None, + mailing_list_id__between: tuple[int, int] | None = None, + mailing_list_id__range: int | None = None, + mailing_list_id__in: list[int] | None = None, + mailing_list_id__isnull: bool | None = None, + token: str | None = None, + token__contains: str | None = None, + token__icontains: str | None = None, + token__startswith: str | None = None, + token__istartswith: str | None = None, + token__endswith: str | None = None, + token__iendswith: str | None = None, + token__iexact: str | None = None, + token__in: list[str] | None = None, + token__isnull: bool | None = None, + ) -> PendingSubscriberQuery: + """Exclude objects matching field lookups.""" + ... + + def values(self, *fields: Literal["action", "created_at", "email", "id", "mailing_list", "mailing_list_id", "token"]) -> PendingSubscriberQuery: # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["action", "created_at", "email", "id", "mailing_list", "mailing_list_id", "token"], flat: bool = False) -> PendingSubscriberQuery: # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + def distinct(self, distinct: bool = True) -> PendingSubscriberQuery: + """Return distinct results.""" + ... + + def join(self, *paths: str) -> PendingSubscriberQuery: + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> PendingSubscriberQuery: + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> PendingSubscriberQuery: + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> PendingSubscriberQuery: + """Add FOR SHARE lock to query.""" + ... + + # Terminal methods (async, execute query) + + async def get( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> PendingSubscriber: + """Get single object matching lookups.""" + ... + + async def get_or_none( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> PendingSubscriber | None: + """Get object or None if not found.""" + ... + + async def get_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[PendingSubscriber, bool]: + """Get object or create if not found. Returns (object, created).""" + ... + + async def update_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[PendingSubscriber, bool]: + """Get object, create if missing, or update it when defaults are provided.""" + ... + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + mode: str = "models", + ) -> list[PendingSubscriber]: + """Get all objects.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> PendingSubscriber | None: + """Get first object.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> PendingSubscriber | None: + """Get last object.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count all objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def create( # type: ignore[override] + self, + *, + instance: PendingSubscriber | None = None, + client: Any | None = None, + using: str | None = None, + action: str | None = None, + created_at: datetime | None = None, + email: EmailStr | None = None, + id: int | None = None, + mailing_list: MailingList | None = None, + mailing_list_id: int | None = None, + token: str | None = None, + ) -> PendingSubscriber: + """Create new object.""" + ... + + async def bulk_create( # type: ignore[override] + self, + objects: list[PendingSubscriber], + *, + batch_size: int | None = None, + client: Any | None = None, + using: str | None = None, + ) -> list[PendingSubscriber]: + """Bulk create objects.""" + ... + + async def bulk_update( # type: ignore[override] + self, + objects: list[PendingSubscriber], + fields: list[str], + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Bulk update objects.""" + ... + + +class ModerationMessage(Model): + class Meta: + is_table: bool + table_name: str + id: int | None + mailing_list: MailingList | None + message_id: str | None + subject: str | None + sender: EmailStr | None + sent_at: datetime | None + text_body: str | None + html_body: str | None + status: str + created_at: datetime | None + decided_at: datetime | None + mailing_list_id: int | None + objects: ClassVar["ModerationMessageManager"] + + +class ModerationMessageQuery(Query[ModerationMessage]): + """Type-safe Query for ModerationMessage model.""" + + # Query building methods (sync, return Query) + + def filter( + self, + *args: Any, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + decided_at: datetime | None = None, + decided_at__gt: datetime | None = None, + decided_at__gte: datetime | None = None, + decided_at__lt: datetime | None = None, + decided_at__lte: datetime | None = None, + decided_at__between: tuple[datetime, datetime] | None = None, + decided_at__range: datetime | None = None, + decided_at__year: int | None = None, + decided_at__month: int | None = None, + decided_at__day: int | None = None, + decided_at__in: list[datetime] | None = None, + decided_at__isnull: bool | None = None, + html_body: str | None = None, + html_body__contains: str | None = None, + html_body__icontains: str | None = None, + html_body__startswith: str | None = None, + html_body__istartswith: str | None = None, + html_body__endswith: str | None = None, + html_body__iendswith: str | None = None, + html_body__iexact: str | None = None, + html_body__in: list[str] | None = None, + html_body__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + mailing_list: MailingList | None = None, + mailing_list__in: list[MailingList] | None = None, + mailing_list__isnull: bool | None = None, + mailing_list_id: int | None = None, + mailing_list_id__gt: int | None = None, + mailing_list_id__gte: int | None = None, + mailing_list_id__lt: int | None = None, + mailing_list_id__lte: int | None = None, + mailing_list_id__between: tuple[int, int] | None = None, + mailing_list_id__range: int | None = None, + mailing_list_id__in: list[int] | None = None, + mailing_list_id__isnull: bool | None = None, + message_id: str | None = None, + message_id__contains: str | None = None, + message_id__icontains: str | None = None, + message_id__startswith: str | None = None, + message_id__istartswith: str | None = None, + message_id__endswith: str | None = None, + message_id__iendswith: str | None = None, + message_id__iexact: str | None = None, + message_id__in: list[str] | None = None, + message_id__isnull: bool | None = None, + sender: EmailStr | None = None, + sender__in: list[EmailStr] | None = None, + sender__isnull: bool | None = None, + sent_at: datetime | None = None, + sent_at__gt: datetime | None = None, + sent_at__gte: datetime | None = None, + sent_at__lt: datetime | None = None, + sent_at__lte: datetime | None = None, + sent_at__between: tuple[datetime, datetime] | None = None, + sent_at__range: datetime | None = None, + sent_at__year: int | None = None, + sent_at__month: int | None = None, + sent_at__day: int | None = None, + sent_at__in: list[datetime] | None = None, + sent_at__isnull: bool | None = None, + status: str | None = None, + status__contains: str | None = None, + status__icontains: str | None = None, + status__startswith: str | None = None, + status__istartswith: str | None = None, + status__endswith: str | None = None, + status__iendswith: str | None = None, + status__iexact: str | None = None, + status__in: list[str] | None = None, + status__isnull: bool | None = None, + subject: str | None = None, + subject__contains: str | None = None, + subject__icontains: str | None = None, + subject__startswith: str | None = None, + subject__istartswith: str | None = None, + subject__endswith: str | None = None, + subject__iendswith: str | None = None, + subject__iexact: str | None = None, + subject__in: list[str] | None = None, + subject__isnull: bool | None = None, + text_body: str | None = None, + text_body__contains: str | None = None, + text_body__icontains: str | None = None, + text_body__startswith: str | None = None, + text_body__istartswith: str | None = None, + text_body__endswith: str | None = None, + text_body__iendswith: str | None = None, + text_body__iexact: str | None = None, + text_body__in: list[str] | None = None, + text_body__isnull: bool | None = None, + ) -> "ModerationMessageQuery": + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + decided_at: datetime | None = None, + decided_at__gt: datetime | None = None, + decided_at__gte: datetime | None = None, + decided_at__lt: datetime | None = None, + decided_at__lte: datetime | None = None, + decided_at__between: tuple[datetime, datetime] | None = None, + decided_at__range: datetime | None = None, + decided_at__year: int | None = None, + decided_at__month: int | None = None, + decided_at__day: int | None = None, + decided_at__in: list[datetime] | None = None, + decided_at__isnull: bool | None = None, + html_body: str | None = None, + html_body__contains: str | None = None, + html_body__icontains: str | None = None, + html_body__startswith: str | None = None, + html_body__istartswith: str | None = None, + html_body__endswith: str | None = None, + html_body__iendswith: str | None = None, + html_body__iexact: str | None = None, + html_body__in: list[str] | None = None, + html_body__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + mailing_list: MailingList | None = None, + mailing_list__in: list[MailingList] | None = None, + mailing_list__isnull: bool | None = None, + mailing_list_id: int | None = None, + mailing_list_id__gt: int | None = None, + mailing_list_id__gte: int | None = None, + mailing_list_id__lt: int | None = None, + mailing_list_id__lte: int | None = None, + mailing_list_id__between: tuple[int, int] | None = None, + mailing_list_id__range: int | None = None, + mailing_list_id__in: list[int] | None = None, + mailing_list_id__isnull: bool | None = None, + message_id: str | None = None, + message_id__contains: str | None = None, + message_id__icontains: str | None = None, + message_id__startswith: str | None = None, + message_id__istartswith: str | None = None, + message_id__endswith: str | None = None, + message_id__iendswith: str | None = None, + message_id__iexact: str | None = None, + message_id__in: list[str] | None = None, + message_id__isnull: bool | None = None, + sender: EmailStr | None = None, + sender__in: list[EmailStr] | None = None, + sender__isnull: bool | None = None, + sent_at: datetime | None = None, + sent_at__gt: datetime | None = None, + sent_at__gte: datetime | None = None, + sent_at__lt: datetime | None = None, + sent_at__lte: datetime | None = None, + sent_at__between: tuple[datetime, datetime] | None = None, + sent_at__range: datetime | None = None, + sent_at__year: int | None = None, + sent_at__month: int | None = None, + sent_at__day: int | None = None, + sent_at__in: list[datetime] | None = None, + sent_at__isnull: bool | None = None, + status: str | None = None, + status__contains: str | None = None, + status__icontains: str | None = None, + status__startswith: str | None = None, + status__istartswith: str | None = None, + status__endswith: str | None = None, + status__iendswith: str | None = None, + status__iexact: str | None = None, + status__in: list[str] | None = None, + status__isnull: bool | None = None, + subject: str | None = None, + subject__contains: str | None = None, + subject__icontains: str | None = None, + subject__startswith: str | None = None, + subject__istartswith: str | None = None, + subject__endswith: str | None = None, + subject__iendswith: str | None = None, + subject__iexact: str | None = None, + subject__in: list[str] | None = None, + subject__isnull: bool | None = None, + text_body: str | None = None, + text_body__contains: str | None = None, + text_body__icontains: str | None = None, + text_body__startswith: str | None = None, + text_body__istartswith: str | None = None, + text_body__endswith: str | None = None, + text_body__iendswith: str | None = None, + text_body__iexact: str | None = None, + text_body__in: list[str] | None = None, + text_body__isnull: bool | None = None, + ) -> "ModerationMessageQuery": + """Exclude objects matching field lookups.""" + ... + + def order_by(self, *fields: Literal["created_at", "-created_at", "decided_at", "-decided_at", "html_body", "-html_body", "id", "-id", "mailing_list", "-mailing_list", "mailing_list_id", "-mailing_list_id", "message_id", "-message_id", "sender", "-sender", "sent_at", "-sent_at", "status", "-status", "subject", "-subject", "text_body", "-text_body"]) -> "ModerationMessageQuery": # type: ignore[override] + """Order results by fields.""" + ... + + def limit(self, n: int) -> "ModerationMessageQuery": + """Limit number of results.""" + ... + + def offset(self, n: int) -> "ModerationMessageQuery": + """Skip first n results.""" + ... + + def distinct(self, value: bool = True) -> "ModerationMessageQuery": + """Return distinct results.""" + ... + + def select(self, *fields: Literal["created_at", "decided_at", "html_body", "id", "mailing_list", "mailing_list_id", "message_id", "sender", "sent_at", "status", "subject", "text_body"]) -> "ModerationMessageQuery": # type: ignore[override] + """Select specific fields.""" + ... + + def join(self, *paths: str) -> "ModerationMessageQuery": + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> "ModerationMessageQuery": + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> "ModerationMessageQuery": + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> "ModerationMessageQuery": + """Add FOR SHARE lock to query.""" + ... + + def annotate(self, **annotations: Any) -> "ModerationMessageQuery": + """Add computed fields using aggregate functions.""" + ... + + def group_by(self, *fields: Literal["created_at", "decided_at", "html_body", "id", "mailing_list", "mailing_list_id", "message_id", "sender", "sent_at", "status", "subject", "text_body"]) -> "ModerationMessageQuery": # type: ignore[override] + """Add GROUP BY clause.""" + ... + + def having(self, *q_exprs: Any, **kwargs: Any) -> "ModerationMessageQuery": + """Add HAVING clause for filtering grouped results.""" + ... + + def values(self, *fields: Literal["created_at", "decided_at", "html_body", "id", "mailing_list", "mailing_list_id", "message_id", "sender", "sent_at", "status", "subject", "text_body"]) -> "ModerationMessageQuery": # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["created_at", "decided_at", "html_body", "id", "mailing_list", "mailing_list_id", "message_id", "sender", "sent_at", "status", "subject", "text_body"], flat: bool = False) -> "ModerationMessageQuery": # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + # Terminal methods (async, execute query) + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> list[ModerationMessage]: + """Execute query and return all results.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> ModerationMessage | None: + """Execute query and return first result.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> ModerationMessage | None: + """Execute query and return last result.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count matching objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def update( # type: ignore[override] + self, + *, + client: Any | None = None, + using: str | None = None, + **values: Any, + ) -> int: + """Update matching objects.""" + ... + + async def increment( + self, + field: str, + by: int | float = 1, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Atomically increment a field value.""" + ... + + async def delete( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Delete matching objects.""" + ... + +class ModerationMessageManager(QueryManager[ModerationMessage]): + """Type-safe Manager for ModerationMessage model.""" + + # Query building methods (sync, return Query) + + def query(self) -> ModerationMessageQuery: + """Return a Query builder for this model.""" + ... + + def filter( + self, + *args: Any, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + decided_at: datetime | None = None, + decided_at__gt: datetime | None = None, + decided_at__gte: datetime | None = None, + decided_at__lt: datetime | None = None, + decided_at__lte: datetime | None = None, + decided_at__between: tuple[datetime, datetime] | None = None, + decided_at__range: datetime | None = None, + decided_at__year: int | None = None, + decided_at__month: int | None = None, + decided_at__day: int | None = None, + decided_at__in: list[datetime] | None = None, + decided_at__isnull: bool | None = None, + html_body: str | None = None, + html_body__contains: str | None = None, + html_body__icontains: str | None = None, + html_body__startswith: str | None = None, + html_body__istartswith: str | None = None, + html_body__endswith: str | None = None, + html_body__iendswith: str | None = None, + html_body__iexact: str | None = None, + html_body__in: list[str] | None = None, + html_body__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + mailing_list: MailingList | None = None, + mailing_list__in: list[MailingList] | None = None, + mailing_list__isnull: bool | None = None, + mailing_list_id: int | None = None, + mailing_list_id__gt: int | None = None, + mailing_list_id__gte: int | None = None, + mailing_list_id__lt: int | None = None, + mailing_list_id__lte: int | None = None, + mailing_list_id__between: tuple[int, int] | None = None, + mailing_list_id__range: int | None = None, + mailing_list_id__in: list[int] | None = None, + mailing_list_id__isnull: bool | None = None, + message_id: str | None = None, + message_id__contains: str | None = None, + message_id__icontains: str | None = None, + message_id__startswith: str | None = None, + message_id__istartswith: str | None = None, + message_id__endswith: str | None = None, + message_id__iendswith: str | None = None, + message_id__iexact: str | None = None, + message_id__in: list[str] | None = None, + message_id__isnull: bool | None = None, + sender: EmailStr | None = None, + sender__in: list[EmailStr] | None = None, + sender__isnull: bool | None = None, + sent_at: datetime | None = None, + sent_at__gt: datetime | None = None, + sent_at__gte: datetime | None = None, + sent_at__lt: datetime | None = None, + sent_at__lte: datetime | None = None, + sent_at__between: tuple[datetime, datetime] | None = None, + sent_at__range: datetime | None = None, + sent_at__year: int | None = None, + sent_at__month: int | None = None, + sent_at__day: int | None = None, + sent_at__in: list[datetime] | None = None, + sent_at__isnull: bool | None = None, + status: str | None = None, + status__contains: str | None = None, + status__icontains: str | None = None, + status__startswith: str | None = None, + status__istartswith: str | None = None, + status__endswith: str | None = None, + status__iendswith: str | None = None, + status__iexact: str | None = None, + status__in: list[str] | None = None, + status__isnull: bool | None = None, + subject: str | None = None, + subject__contains: str | None = None, + subject__icontains: str | None = None, + subject__startswith: str | None = None, + subject__istartswith: str | None = None, + subject__endswith: str | None = None, + subject__iendswith: str | None = None, + subject__iexact: str | None = None, + subject__in: list[str] | None = None, + subject__isnull: bool | None = None, + text_body: str | None = None, + text_body__contains: str | None = None, + text_body__icontains: str | None = None, + text_body__startswith: str | None = None, + text_body__istartswith: str | None = None, + text_body__endswith: str | None = None, + text_body__iendswith: str | None = None, + text_body__iexact: str | None = None, + text_body__in: list[str] | None = None, + text_body__isnull: bool | None = None, + ) -> ModerationMessageQuery: + """Filter by Q-expressions or field lookups.""" + ... + + def exclude( + self, + *args: Any, + created_at: datetime | None = None, + created_at__gt: datetime | None = None, + created_at__gte: datetime | None = None, + created_at__lt: datetime | None = None, + created_at__lte: datetime | None = None, + created_at__between: tuple[datetime, datetime] | None = None, + created_at__range: datetime | None = None, + created_at__year: int | None = None, + created_at__month: int | None = None, + created_at__day: int | None = None, + created_at__in: list[datetime] | None = None, + created_at__isnull: bool | None = None, + decided_at: datetime | None = None, + decided_at__gt: datetime | None = None, + decided_at__gte: datetime | None = None, + decided_at__lt: datetime | None = None, + decided_at__lte: datetime | None = None, + decided_at__between: tuple[datetime, datetime] | None = None, + decided_at__range: datetime | None = None, + decided_at__year: int | None = None, + decided_at__month: int | None = None, + decided_at__day: int | None = None, + decided_at__in: list[datetime] | None = None, + decided_at__isnull: bool | None = None, + html_body: str | None = None, + html_body__contains: str | None = None, + html_body__icontains: str | None = None, + html_body__startswith: str | None = None, + html_body__istartswith: str | None = None, + html_body__endswith: str | None = None, + html_body__iendswith: str | None = None, + html_body__iexact: str | None = None, + html_body__in: list[str] | None = None, + html_body__isnull: bool | None = None, + id: int | None = None, + id__gt: int | None = None, + id__gte: int | None = None, + id__lt: int | None = None, + id__lte: int | None = None, + id__between: tuple[int, int] | None = None, + id__range: int | None = None, + id__in: list[int] | None = None, + id__isnull: bool | None = None, + mailing_list: MailingList | None = None, + mailing_list__in: list[MailingList] | None = None, + mailing_list__isnull: bool | None = None, + mailing_list_id: int | None = None, + mailing_list_id__gt: int | None = None, + mailing_list_id__gte: int | None = None, + mailing_list_id__lt: int | None = None, + mailing_list_id__lte: int | None = None, + mailing_list_id__between: tuple[int, int] | None = None, + mailing_list_id__range: int | None = None, + mailing_list_id__in: list[int] | None = None, + mailing_list_id__isnull: bool | None = None, + message_id: str | None = None, + message_id__contains: str | None = None, + message_id__icontains: str | None = None, + message_id__startswith: str | None = None, + message_id__istartswith: str | None = None, + message_id__endswith: str | None = None, + message_id__iendswith: str | None = None, + message_id__iexact: str | None = None, + message_id__in: list[str] | None = None, + message_id__isnull: bool | None = None, + sender: EmailStr | None = None, + sender__in: list[EmailStr] | None = None, + sender__isnull: bool | None = None, + sent_at: datetime | None = None, + sent_at__gt: datetime | None = None, + sent_at__gte: datetime | None = None, + sent_at__lt: datetime | None = None, + sent_at__lte: datetime | None = None, + sent_at__between: tuple[datetime, datetime] | None = None, + sent_at__range: datetime | None = None, + sent_at__year: int | None = None, + sent_at__month: int | None = None, + sent_at__day: int | None = None, + sent_at__in: list[datetime] | None = None, + sent_at__isnull: bool | None = None, + status: str | None = None, + status__contains: str | None = None, + status__icontains: str | None = None, + status__startswith: str | None = None, + status__istartswith: str | None = None, + status__endswith: str | None = None, + status__iendswith: str | None = None, + status__iexact: str | None = None, + status__in: list[str] | None = None, + status__isnull: bool | None = None, + subject: str | None = None, + subject__contains: str | None = None, + subject__icontains: str | None = None, + subject__startswith: str | None = None, + subject__istartswith: str | None = None, + subject__endswith: str | None = None, + subject__iendswith: str | None = None, + subject__iexact: str | None = None, + subject__in: list[str] | None = None, + subject__isnull: bool | None = None, + text_body: str | None = None, + text_body__contains: str | None = None, + text_body__icontains: str | None = None, + text_body__startswith: str | None = None, + text_body__istartswith: str | None = None, + text_body__endswith: str | None = None, + text_body__iendswith: str | None = None, + text_body__iexact: str | None = None, + text_body__in: list[str] | None = None, + text_body__isnull: bool | None = None, + ) -> ModerationMessageQuery: + """Exclude objects matching field lookups.""" + ... + + def values(self, *fields: Literal["created_at", "decided_at", "html_body", "id", "mailing_list", "mailing_list_id", "message_id", "sender", "sent_at", "status", "subject", "text_body"]) -> ModerationMessageQuery: # type: ignore[override] + """Return dicts instead of models.""" + ... + + def values_list(self, *fields: Literal["created_at", "decided_at", "html_body", "id", "mailing_list", "mailing_list_id", "message_id", "sender", "sent_at", "status", "subject", "text_body"], flat: bool = False) -> ModerationMessageQuery: # type: ignore[override] + """Return tuples/values instead of models.""" + ... + + def distinct(self, distinct: bool = True) -> ModerationMessageQuery: + """Return distinct results.""" + ... + + def join(self, *paths: str) -> ModerationMessageQuery: + """Perform LEFT JOIN for relations.""" + ... + + def prefetch(self, *paths: str) -> ModerationMessageQuery: + """Prefetch related objects (separate queries).""" + ... + + def for_update(self) -> ModerationMessageQuery: + """Add FOR UPDATE lock to query.""" + ... + + def for_share(self) -> ModerationMessageQuery: + """Add FOR SHARE lock to query.""" + ... + + # Terminal methods (async, execute query) + + async def get( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> ModerationMessage: + """Get single object matching lookups.""" + ... + + async def get_or_none( + self, + *, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> ModerationMessage | None: + """Get object or None if not found.""" + ... + + async def get_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[ModerationMessage, bool]: + """Get object or create if not found. Returns (object, created).""" + ... + + async def update_or_create( + self, + *, + defaults: dict[str, Any] | None = None, + client: Any | None = None, + using: str | None = None, + **filters: Any, + ) -> tuple[ModerationMessage, bool]: + """Get object, create if missing, or update it when defaults are provided.""" + ... + + async def all( + self, + *, + client: Any | None = None, + using: str | None = None, + mode: str = "models", + ) -> list[ModerationMessage]: + """Get all objects.""" + ... + + async def first( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> ModerationMessage | None: + """Get first object.""" + ... + + async def last( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> ModerationMessage | None: + """Get last object.""" + ... + + async def count( + self, + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Count all objects.""" + ... + + async def sum( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate sum of field values.""" + ... + + async def avg( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Calculate average of field values.""" + ... + + async def max( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get maximum field value.""" + ... + + async def min( + self, + field: str, + *, + client: Any | None = None, + using: str | None = None, + ) -> Any: + """Get minimum field value.""" + ... + + async def create( # type: ignore[override] + self, + *, + instance: ModerationMessage | None = None, + client: Any | None = None, + using: str | None = None, + created_at: datetime | None = None, + decided_at: datetime | None = None, + html_body: str | None = None, + id: int | None = None, + mailing_list: MailingList | None = None, + mailing_list_id: int | None = None, + message_id: str | None = None, + sender: EmailStr | None = None, + sent_at: datetime | None = None, + status: str | None = None, + subject: str | None = None, + text_body: str | None = None, + ) -> ModerationMessage: + """Create new object.""" + ... + + async def bulk_create( # type: ignore[override] + self, + objects: list[ModerationMessage], + *, + batch_size: int | None = None, + client: Any | None = None, + using: str | None = None, + ) -> list[ModerationMessage]: + """Bulk create objects.""" + ... + + async def bulk_update( # type: ignore[override] + self, + objects: list[ModerationMessage], + fields: list[str], + *, + client: Any | None = None, + using: str | None = None, + ) -> int: + """Bulk update objects.""" + ... + diff --git a/freenit/permissions.py b/freenit/permissions.py index 9c089fc..7aca7e3 100644 --- a/freenit/permissions.py +++ b/freenit/permissions.py @@ -2,6 +2,7 @@ domain_perms = permissions() group_perms = permissions() +mailinglist_perms = permissions() profile_perms = permissions() role_perms = permissions() theme_perms = permissions() diff --git a/freenit/stalwart.py b/freenit/stalwart.py new file mode 100644 index 0000000..376b4ed --- /dev/null +++ b/freenit/stalwart.py @@ -0,0 +1,266 @@ +import base64 +import logging +from typing import Any + +import httpx +from freenit.config import getConfig + +config = getConfig() +log = logging.getLogger("stalwart") + + +def _admin_auth() -> str: + credentials = f"{config.stalwart_admin}:{config.stalwart_admin_pass}" + return "Basic " + base64.b64encode(credentials.encode()).decode() + + +def _jmap_auth(user_email: str) -> str: + credentials = f"{user_email}%{config.stalwart_admin}:{config.stalwart_admin_pass}" + return "Basic " + base64.b64encode(credentials.encode()).decode() + + +def _headers() -> dict[str, str]: + return { + "Authorization": _admin_auth(), + "Content-Type": "application/json", + "Accept": "application/json", + } + + +def _jmap_headers(user_email: str) -> dict[str, str]: + return { + "Authorization": _jmap_auth(user_email), + "Content-Type": "application/json", + "Accept": "application/json", + } + + +async def create_inbox_account(name: str, address: str) -> int: + payload = { + "type": "individual", + "name": name, + "description": f"Mailing list inbox for {address}", + "emails": [address], + "secrets": [], + "memberOf": [], + "roles": [], + "lists": [], + "members": [], + "enabledPermissions": [], + "disabledPermissions": [], + "externalMembers": [], + } + async with httpx.AsyncClient() as client: + resp = await client.post( + f"{config.stalwart_url}/api/principal", + json=payload, + headers=_headers(), + ) + if resp.status_code >= 400: + log.error("Failed to create inbox account %s: %s %s", address, resp.status_code, resp.text[:500]) + raise RuntimeError(f"Stalwart create inbox failed: {resp.text}") + data = resp.json().get("data", {}) + principal_id = data if isinstance(data, int) else data.get("id") + log.info("Created inbox account %s with principal id %s", address, principal_id) + return principal_id + + +async def create_list_principal(name: str, address: str) -> int: + payload = { + "type": "list", + "name": name, + "description": f"Mailing list distribution for {address}", + "emails": [address], + "secrets": [], + "memberOf": [], + "roles": [], + "lists": [], + "members": [], + "enabledPermissions": [], + "disabledPermissions": [], + "externalMembers": [], + } + async with httpx.AsyncClient() as client: + resp = await client.post( + f"{config.stalwart_url}/api/principal", + json=payload, + headers=_headers(), + ) + if resp.status_code >= 400: + log.error("Failed to create list principal %s: %s %s", address, resp.status_code, resp.text[:500]) + raise RuntimeError(f"Stalwart create list failed: {resp.text}") + data = resp.json().get("data", {}) + principal_id = data if isinstance(data, int) else data.get("id") + log.info("Created list principal %s with id %s", address, principal_id) + return principal_id + + +async def create_archive_account(name: str, address: str) -> int: + payload = { + "type": "individual", + "name": name, + "description": f"Mailing list archive for {address}", + "emails": [address], + "secrets": [], + "memberOf": [], + "roles": [], + "lists": [], + "members": [], + "enabledPermissions": [], + "disabledPermissions": [], + "externalMembers": [], + } + async with httpx.AsyncClient() as client: + resp = await client.post( + f"{config.stalwart_url}/api/principal", + json=payload, + headers=_headers(), + ) + if resp.status_code >= 400: + log.error("Failed to create archive account %s: %s %s", address, resp.status_code, resp.text[:500]) + raise RuntimeError(f"Stalwart create archive failed: {resp.text}") + data = resp.json().get("data", {}) + principal_id = data if isinstance(data, int) else data.get("id") + log.info("Created archive account %s with id %s", address, principal_id) + return principal_id + + +async def delete_principal(principal_id: int) -> None: + async with httpx.AsyncClient() as client: + resp = await client.delete( + f"{config.stalwart_url}/api/principal/{principal_id}", + headers=_headers(), + ) + if resp.status_code >= 400: + log.error("Failed to delete principal %s: %s %s", principal_id, resp.status_code, resp.text[:500]) + raise RuntimeError(f"Stalwart delete principal failed: {resp.text}") + + +async def patch_principal(principal_id: int, patches: list[dict[str, Any]]) -> None: + async with httpx.AsyncClient() as client: + resp = await client.patch( + f"{config.stalwart_url}/api/principal/{principal_id}", + json=patches, + headers=_headers(), + ) + if resp.status_code >= 400: + log.error("Failed to patch principal %s: %s %s", principal_id, resp.status_code, resp.text[:500]) + raise RuntimeError(f"Stalwart patch principal failed: {resp.text}") + + +async def add_external_member(list_principal_id: int, email: str) -> None: + await patch_principal(list_principal_id, [ + {"action": "addItem", "field": "externalMembers", "value": email} + ]) + + +async def remove_external_member(list_principal_id: int, email: str) -> None: + await patch_principal(list_principal_id, [ + {"action": "removeItem", "field": "externalMembers", "value": email} + ]) + + +async def jmap_request(account_email: str, method_calls: list[list[Any]]) -> dict[str, Any]: + payload = { + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail", + ], + "methodCalls": method_calls, + } + async with httpx.AsyncClient() as client: + resp = await client.post( + f"{config.stalwart_url}/jmap", + json=payload, + headers=_jmap_headers(account_email), + ) + if resp.status_code >= 400: + log.error("JMAP request failed for %s: %s %s", account_email, resp.status_code, resp.text[:500]) + raise RuntimeError(f"JMAP request failed: {resp.text}") + return resp.json() + + +async def fetch_mailbox_messages(account_email: str, role: str = "inbox") -> list[dict[str, Any]]: + result = await jmap_request(account_email, [ + ["Mailbox/query", {"filter": {"role": role}}, "0"], + ["Email/query", { + "filter": { + "inMailbox": "#{0}/ids[0]", + }, + "sort": [{"property": "receivedAt", "isAscending": False}], + "limit": 50, + }, "1"], + ]) + responses = {r[2]: r for r in result.get("methodResponses", [])} + email_response = responses.get("1") + if not email_response: + return [] + _, data, _ = email_response + return data.get("ids", []) + + +async def fetch_inbox_messages(account_email: str) -> list[dict[str, Any]]: + return await fetch_mailbox_messages(account_email, "inbox") + + +async def fetch_email_bodies(account_email: str, email_id: str) -> dict[str, Any]: + result = await jmap_request(account_email, [ + ["Email/get", { + "ids": [email_id], + "properties": ["id", "messageId", "subject", "from", "receivedAt", "bodyValues"], + "fetchAllBodyValues": True, + "maxBodyValueBytes": 1048576, + }, "0"], + ]) + responses = result.get("methodResponses", []) + if not responses: + return {} + _, data, _ = responses[0] + return data.get("list", [{}])[0] + + +async def fetch_email_summaries(account_email: str, email_ids: list[str]) -> list[dict[str, Any]]: + if not email_ids: + return [] + result = await jmap_request(account_email, [ + ["Email/get", { + "ids": email_ids, + "properties": ["id", "messageId", "subject", "from", "receivedAt"], + }, "0"], + ]) + responses = result.get("methodResponses", []) + if not responses: + return [] + _, data, _ = responses[0] + return data.get("list", []) + + +async def destroy_emails(account_email: str, email_ids: list[str]) -> None: + await jmap_request(account_email, [ + ["Email/set", {"destroy": email_ids}, "0"], + ]) + + +async def fetch_principal(principal_id: int) -> dict[str, Any]: + async with httpx.AsyncClient() as client: + resp = await client.get( + f"{config.stalwart_url}/api/principal/{principal_id}", + headers=_headers(), + ) + if resp.status_code >= 400: + log.error("Failed to fetch principal %s: %s %s", principal_id, resp.status_code, resp.text[:500]) + raise RuntimeError(f"Stalwart fetch principal failed: {resp.text}") + return resp.json().get("data", {}) + + +async def list_principals(types: str = "list", page: int = 1, limit: int = 100) -> list[dict[str, Any]]: + async with httpx.AsyncClient() as client: + resp = await client.get( + f"{config.stalwart_url}/api/principal", + params={"types": types, "page": page - 1, "limit": limit}, + headers=_headers(), + ) + if resp.status_code >= 400: + log.error("Failed to list principals: %s %s", resp.status_code, resp.text[:500]) + raise RuntimeError(f"Stalwart list principals failed: {resp.text}") + return resp.json().get("data", {}).get("items", []) diff --git a/migrations/0003_create_mailing_list_table.py b/migrations/0003_create_mailing_list_table.py new file mode 100644 index 0000000..86cd8b9 --- /dev/null +++ b/migrations/0003_create_mailing_list_table.py @@ -0,0 +1,463 @@ +"""Auto-generated migration. + +Created: 2026-06-16 16:31:33 +""" + +depends_on = "0002_add_omemo_bundle" + + +def upgrade(ctx): + """Apply migration.""" + ctx.create_table( + "mailing_list", + fields=[ + { + 'name': 'id', + 'python_type': 'int', + 'db_type': None, + 'nullable': True, + 'primary_key': True, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'name', + 'python_type': 'str', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': True, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'address', + 'python_type': 'emailstr', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': True, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'distribution_address', + 'python_type': 'emailstr', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': True, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'archive_address', + 'python_type': 'emailstr', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': True, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'description', + 'python_type': 'str', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'public', + 'python_type': 'bool', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': False, + 'default': '1', + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'archive_enabled', + 'python_type': 'bool', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': False, + 'default': '1', + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'moderation_enabled', + 'python_type': 'bool', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': False, + 'default': '0', + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'principal_id', + 'python_type': 'int', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'inbox_principal_id', + 'python_type': 'int', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'archive_principal_id', + 'python_type': 'int', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'created_at', + 'python_type': 'datetime', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'updated_at', + 'python_type': 'datetime', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + } + ], + ) + ctx.create_table( + "moderation_message", + fields=[ + { + 'name': 'id', + 'python_type': 'int', + 'db_type': None, + 'nullable': True, + 'primary_key': True, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'message_id', + 'python_type': 'str', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'subject', + 'python_type': 'str', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'sender', + 'python_type': 'emailstr', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'sent_at', + 'python_type': 'datetime', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'text_body', + 'python_type': 'str', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'html_body', + 'python_type': 'str', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'status', + 'python_type': 'str', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': False, + 'default': "'pending'", + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'created_at', + 'python_type': 'datetime', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'decided_at', + 'python_type': 'datetime', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'mailing_list_id', + 'python_type': 'int', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + } + ], + foreign_keys=[ + { + 'name': 'fk_moderation_message_mailing_list_id', + 'columns': [ + 'mailing_list_id' + ], + 'ref_table': 'mailing_list', + 'ref_columns': [ + 'id' + ], + 'on_delete': 'CASCADE', + 'on_update': 'CASCADE' + } + ], + ) + ctx.create_table( + "pending_subscriber", + fields=[ + { + 'name': 'id', + 'python_type': 'int', + 'db_type': None, + 'nullable': True, + 'primary_key': True, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'email', + 'python_type': 'emailstr', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'token', + 'python_type': 'str', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'action', + 'python_type': 'str', + 'db_type': None, + 'nullable': False, + 'primary_key': False, + 'unique': False, + 'default': "'subscribe'", + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'created_at', + 'python_type': 'datetime', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + }, + { + 'name': 'mailing_list_id', + 'python_type': 'int', + 'db_type': None, + 'nullable': True, + 'primary_key': False, + 'unique': False, + 'default': None, + 'auto_increment': False, + 'max_length': None, + 'max_digits': None, + 'decimal_places': None + } + ], + foreign_keys=[ + { + 'name': 'fk_pending_subscriber_mailing_list_id', + 'columns': [ + 'mailing_list_id' + ], + 'ref_table': 'mailing_list', + 'ref_columns': [ + 'id' + ], + 'on_delete': 'CASCADE', + 'on_update': 'CASCADE' + } + ], + ) + + +def downgrade(ctx): + """Revert migration.""" + ctx.drop_table("pending_subscriber") + ctx.drop_table("moderation_message") + ctx.drop_table("mailing_list") diff --git a/tests/test_mailinglist.py b/tests/test_mailinglist.py new file mode 100644 index 0000000..0ca218a --- /dev/null +++ b/tests/test_mailinglist.py @@ -0,0 +1,75 @@ +from unittest.mock import patch + +import pytest + +from . import factories + + +@pytest.mark.asyncio +class TestMailingList: + async def _admin(self): + user = factories.User(admin=True) + await user.save() + return user + + async def test_get_mailinglist_list_admin(self, client): + user = await self._admin() + client.login(user=user) + response = client.get("/mailinglists") + assert response.status_code == 200 + + async def test_get_mailinglist_list_public(self, client): + response = client.get("/mailinglists/public") + assert response.status_code == 200 + + async def test_create_mailinglist_requires_admin(self, client): + user = factories.User() + await user.save() + client.login(user=user) + data = {"name": "testlist", "address": "testlist@example.com"} + response = client.post("/mailinglists", data=data) + assert response.status_code == 403 + + @patch("freenit.api.mailinglist.create_inbox_account") + @patch("freenit.api.mailinglist.create_list_principal") + @patch("freenit.api.mailinglist.create_archive_account") + @patch("freenit.api.mailinglist.add_external_member") + async def test_create_mailinglist(self, add_member, create_archive, create_list, create_inbox, client): + create_inbox.return_value = 1 + create_list.return_value = 2 + create_archive.return_value = 3 + user = await self._admin() + client.login(user=user) + data = {"name": "testlist", "address": "testlist@example.com"} + response = client.post("/mailinglists", data=data) + assert response.status_code == 200 + result = response.json() + assert result["name"] == "testlist" + assert result["address"] == "testlist@example.com" + assert result["distribution_address"] == "testlist-members@example.com" + assert result["archive_address"] == "testlist-archive@example.com" + + @patch("freenit.api.mailinglist.create_inbox_account") + @patch("freenit.api.mailinglist.create_list_principal") + @patch("freenit.api.mailinglist.create_archive_account") + @patch("freenit.api.mailinglist.add_external_member") + async def test_subscribe_flow(self, add_member, create_archive, create_list, create_inbox, client): + create_inbox.return_value = 1 + create_list.return_value = 2 + create_archive.return_value = 3 + user = await self._admin() + client.login(user=user) + data = {"name": "testlist", "address": "testlist@example.com"} + response = client.post("/mailinglists", data=data) + assert response.status_code == 200 + list_id = response.json()["id"] + + with patch("freenit.api.mailinglist.sendmail") as mock_send: + response = client.post(f"/mailinglists/{list_id}/subscribe", data={"email": "subscriber@example.com"}) + assert response.status_code == 200 + mock_send.assert_called_once() + + async def test_public_archive_forbidden_when_private(self, client): + # Without mocking stalwart this just validates the endpoint wiring + response = client.get("/mailinglists/1/archive") + assert response.status_code in (404, 403)