Monorepo: consolidate 7 repos into one
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s
Combines shared, blog, market, cart, events, federation, and account into a single repository. Eliminates submodule sync, sibling model copying at build time, and per-app CI orchestration. Changes: - Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs - Remove stale sibling model copies from each app - Update all 6 Dockerfiles for monorepo build context (root = .) - Add build directives to docker-compose.yml - Add single .gitea/workflows/ci.yml with change detection - Add .dockerignore for monorepo build context - Create __init__.py for federation and account (cross-app imports)
This commit is contained in:
31
shared/contracts/__init__.py
Normal file
31
shared/contracts/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Typed contracts (DTOs + Protocols) for cross-domain service interfaces."""
|
||||
|
||||
from .dtos import (
|
||||
PostDTO,
|
||||
CalendarDTO,
|
||||
CalendarEntryDTO,
|
||||
MarketPlaceDTO,
|
||||
ProductDTO,
|
||||
CartItemDTO,
|
||||
CartSummaryDTO,
|
||||
)
|
||||
from .protocols import (
|
||||
BlogService,
|
||||
CalendarService,
|
||||
MarketService,
|
||||
CartService,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PostDTO",
|
||||
"CalendarDTO",
|
||||
"CalendarEntryDTO",
|
||||
"MarketPlaceDTO",
|
||||
"ProductDTO",
|
||||
"CartItemDTO",
|
||||
"CartSummaryDTO",
|
||||
"BlogService",
|
||||
"CalendarService",
|
||||
"MarketService",
|
||||
"CartService",
|
||||
]
|
||||
255
shared/contracts/dtos.py
Normal file
255
shared/contracts/dtos.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""Frozen dataclasses for cross-domain data transfer.
|
||||
|
||||
These are the *only* shapes that cross domain boundaries. Consumers never
|
||||
see ORM model instances from another domain — only these DTOs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blog domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PostDTO:
|
||||
id: int
|
||||
slug: str
|
||||
title: str
|
||||
status: str
|
||||
visibility: str
|
||||
is_page: bool = False
|
||||
feature_image: str | None = None
|
||||
html: str | None = None
|
||||
excerpt: str | None = None
|
||||
custom_excerpt: str | None = None
|
||||
published_at: datetime | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar / Events domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CalendarDTO:
|
||||
id: int
|
||||
container_type: str
|
||||
container_id: int
|
||||
name: str
|
||||
slug: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TicketDTO:
|
||||
id: int
|
||||
code: str
|
||||
state: str
|
||||
entry_name: str
|
||||
entry_start_at: datetime
|
||||
entry_end_at: datetime | None = None
|
||||
ticket_type_name: str | None = None
|
||||
calendar_name: str | None = None
|
||||
created_at: datetime | None = None
|
||||
checked_in_at: datetime | None = None
|
||||
entry_id: int | None = None
|
||||
ticket_type_id: int | None = None
|
||||
price: Decimal | None = None
|
||||
order_id: int | None = None
|
||||
calendar_container_id: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CalendarEntryDTO:
|
||||
id: int
|
||||
calendar_id: int
|
||||
name: str
|
||||
start_at: datetime
|
||||
state: str
|
||||
cost: Decimal
|
||||
end_at: datetime | None = None
|
||||
user_id: int | None = None
|
||||
session_id: str | None = None
|
||||
order_id: int | None = None
|
||||
slot_id: int | None = None
|
||||
ticket_price: Decimal | None = None
|
||||
ticket_count: int | None = None
|
||||
calendar_name: str | None = None
|
||||
calendar_slug: str | None = None
|
||||
calendar_container_id: int | None = None
|
||||
calendar_container_type: str | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Market domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MarketPlaceDTO:
|
||||
id: int
|
||||
container_type: str
|
||||
container_id: int
|
||||
name: str
|
||||
slug: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProductDTO:
|
||||
id: int
|
||||
slug: str
|
||||
title: str | None = None
|
||||
image: str | None = None
|
||||
description_short: str | None = None
|
||||
rrp: Decimal | None = None
|
||||
regular_price: Decimal | None = None
|
||||
special_price: Decimal | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cart domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CartItemDTO:
|
||||
id: int
|
||||
product_id: int
|
||||
quantity: int
|
||||
product_title: str | None = None
|
||||
product_slug: str | None = None
|
||||
product_image: str | None = None
|
||||
unit_price: Decimal | None = None
|
||||
market_place_id: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CartSummaryDTO:
|
||||
count: int = 0
|
||||
total: Decimal = Decimal("0")
|
||||
calendar_count: int = 0
|
||||
calendar_total: Decimal = Decimal("0")
|
||||
items: list[CartItemDTO] = field(default_factory=list)
|
||||
ticket_count: int = 0
|
||||
ticket_total: Decimal = Decimal("0")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Federation / ActivityPub domain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ActorProfileDTO:
|
||||
id: int
|
||||
user_id: int
|
||||
preferred_username: str
|
||||
public_key_pem: str
|
||||
display_name: str | None = None
|
||||
summary: str | None = None
|
||||
inbox_url: str | None = None
|
||||
outbox_url: str | None = None
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class APActivityDTO:
|
||||
id: int
|
||||
activity_id: str
|
||||
activity_type: str
|
||||
actor_profile_id: int
|
||||
object_type: str | None = None
|
||||
object_data: dict | None = None
|
||||
published: datetime | None = None
|
||||
is_local: bool = True
|
||||
source_type: str | None = None
|
||||
source_id: int | None = None
|
||||
ipfs_cid: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class APFollowerDTO:
|
||||
id: int
|
||||
actor_profile_id: int
|
||||
follower_acct: str
|
||||
follower_inbox: str
|
||||
follower_actor_url: str
|
||||
created_at: datetime | None = None
|
||||
app_domain: str = "federation"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class APAnchorDTO:
|
||||
id: int
|
||||
merkle_root: str
|
||||
activity_count: int = 0
|
||||
tree_ipfs_cid: str | None = None
|
||||
ots_proof_cid: str | None = None
|
||||
confirmed_at: datetime | None = None
|
||||
bitcoin_txid: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemoteActorDTO:
|
||||
id: int
|
||||
actor_url: str
|
||||
inbox_url: str
|
||||
preferred_username: str
|
||||
domain: str
|
||||
display_name: str | None = None
|
||||
summary: str | None = None
|
||||
icon_url: str | None = None
|
||||
shared_inbox_url: str | None = None
|
||||
public_key_pem: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemotePostDTO:
|
||||
id: int
|
||||
remote_actor_id: int
|
||||
object_id: str
|
||||
content: str
|
||||
summary: str | None = None
|
||||
url: str | None = None
|
||||
attachments: list[dict] = field(default_factory=list)
|
||||
tags: list[dict] = field(default_factory=list)
|
||||
published: datetime | None = None
|
||||
actor: RemoteActorDTO | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TimelineItemDTO:
|
||||
id: str # composite key for cursor pagination
|
||||
post_type: str # "local" | "remote" | "boost"
|
||||
content: str # HTML
|
||||
published: datetime
|
||||
actor_name: str
|
||||
actor_username: str
|
||||
object_id: str | None = None
|
||||
summary: str | None = None
|
||||
url: str | None = None
|
||||
attachments: list[dict] = field(default_factory=list)
|
||||
tags: list[dict] = field(default_factory=list)
|
||||
actor_domain: str | None = None # None = local
|
||||
actor_icon: str | None = None
|
||||
actor_url: str | None = None
|
||||
boosted_by: str | None = None
|
||||
like_count: int = 0
|
||||
boost_count: int = 0
|
||||
liked_by_me: bool = False
|
||||
boosted_by_me: bool = False
|
||||
author_inbox: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NotificationDTO:
|
||||
id: int
|
||||
notification_type: str # follow/like/boost/mention/reply
|
||||
from_actor_name: str
|
||||
from_actor_username: str
|
||||
created_at: datetime
|
||||
read: bool
|
||||
from_actor_domain: str | None = None
|
||||
from_actor_icon: str | None = None
|
||||
target_content_preview: str | None = None
|
||||
368
shared/contracts/protocols.py
Normal file
368
shared/contracts/protocols.py
Normal file
@@ -0,0 +1,368 @@
|
||||
"""Protocol classes defining each domain's service interface.
|
||||
|
||||
All cross-domain callers program against these Protocols. Concrete
|
||||
implementations (Sql*Service) and no-op stubs both satisfy them.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .dtos import (
|
||||
PostDTO,
|
||||
CalendarDTO,
|
||||
CalendarEntryDTO,
|
||||
TicketDTO,
|
||||
MarketPlaceDTO,
|
||||
ProductDTO,
|
||||
CartItemDTO,
|
||||
CartSummaryDTO,
|
||||
ActorProfileDTO,
|
||||
APActivityDTO,
|
||||
APFollowerDTO,
|
||||
RemoteActorDTO,
|
||||
RemotePostDTO,
|
||||
TimelineItemDTO,
|
||||
NotificationDTO,
|
||||
)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class BlogService(Protocol):
|
||||
async def get_post_by_slug(self, session: AsyncSession, slug: str) -> PostDTO | None: ...
|
||||
async def get_post_by_id(self, session: AsyncSession, id: int) -> PostDTO | None: ...
|
||||
async def get_posts_by_ids(self, session: AsyncSession, ids: list[int]) -> list[PostDTO]: ...
|
||||
|
||||
async def search_posts(
|
||||
self, session: AsyncSession, query: str, page: int = 1, per_page: int = 10,
|
||||
) -> tuple[list[PostDTO], int]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class CalendarService(Protocol):
|
||||
async def calendars_for_container(
|
||||
self, session: AsyncSession, container_type: str, container_id: int,
|
||||
) -> list[CalendarDTO]: ...
|
||||
|
||||
async def pending_entries(
|
||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
||||
) -> list[CalendarEntryDTO]: ...
|
||||
|
||||
async def entries_for_page(
|
||||
self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None,
|
||||
) -> list[CalendarEntryDTO]: ...
|
||||
|
||||
async def entry_by_id(self, session: AsyncSession, entry_id: int) -> CalendarEntryDTO | None: ...
|
||||
|
||||
async def associated_entries(
|
||||
self, session: AsyncSession, content_type: str, content_id: int, page: int,
|
||||
) -> tuple[list[CalendarEntryDTO], bool]: ...
|
||||
|
||||
async def toggle_entry_post(
|
||||
self, session: AsyncSession, entry_id: int, content_type: str, content_id: int,
|
||||
) -> bool: ...
|
||||
|
||||
async def adopt_entries_for_user(
|
||||
self, session: AsyncSession, user_id: int, session_id: str,
|
||||
) -> None: ...
|
||||
|
||||
async def claim_entries_for_order(
|
||||
self, session: AsyncSession, order_id: int, user_id: int | None,
|
||||
session_id: str | None, page_post_id: int | None,
|
||||
) -> None: ...
|
||||
|
||||
async def confirm_entries_for_order(
|
||||
self, session: AsyncSession, order_id: int, user_id: int | None,
|
||||
session_id: str | None,
|
||||
) -> None: ...
|
||||
|
||||
async def get_entries_for_order(
|
||||
self, session: AsyncSession, order_id: int,
|
||||
) -> list[CalendarEntryDTO]: ...
|
||||
|
||||
async def user_tickets(
|
||||
self, session: AsyncSession, *, user_id: int,
|
||||
) -> list[TicketDTO]: ...
|
||||
|
||||
async def user_bookings(
|
||||
self, session: AsyncSession, *, user_id: int,
|
||||
) -> list[CalendarEntryDTO]: ...
|
||||
|
||||
async def confirmed_entries_for_posts(
|
||||
self, session: AsyncSession, post_ids: list[int],
|
||||
) -> dict[int, list[CalendarEntryDTO]]: ...
|
||||
|
||||
async def pending_tickets(
|
||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
||||
) -> list[TicketDTO]: ...
|
||||
|
||||
async def tickets_for_page(
|
||||
self, session: AsyncSession, page_id: int, *, user_id: int | None, session_id: str | None,
|
||||
) -> list[TicketDTO]: ...
|
||||
|
||||
async def claim_tickets_for_order(
|
||||
self, session: AsyncSession, order_id: int, user_id: int | None,
|
||||
session_id: str | None, page_post_id: int | None,
|
||||
) -> None: ...
|
||||
|
||||
async def confirm_tickets_for_order(
|
||||
self, session: AsyncSession, order_id: int,
|
||||
) -> None: ...
|
||||
|
||||
async def get_tickets_for_order(
|
||||
self, session: AsyncSession, order_id: int,
|
||||
) -> list[TicketDTO]: ...
|
||||
|
||||
async def adopt_tickets_for_user(
|
||||
self, session: AsyncSession, user_id: int, session_id: str,
|
||||
) -> None: ...
|
||||
|
||||
async def adjust_ticket_quantity(
|
||||
self, session: AsyncSession, entry_id: int, count: int, *,
|
||||
user_id: int | None, session_id: str | None,
|
||||
ticket_type_id: int | None = None,
|
||||
) -> int: ...
|
||||
|
||||
async def entry_ids_for_content(
|
||||
self, session: AsyncSession, content_type: str, content_id: int,
|
||||
) -> set[int]: ...
|
||||
|
||||
async def upcoming_entries_for_container(
|
||||
self, session: AsyncSession,
|
||||
container_type: str | None = None, container_id: int | None = None,
|
||||
*, page: int = 1, per_page: int = 20,
|
||||
) -> tuple[list[CalendarEntryDTO], bool]: ...
|
||||
|
||||
async def visible_entries_for_period(
|
||||
self, session: AsyncSession, calendar_id: int,
|
||||
period_start: datetime, period_end: datetime,
|
||||
*, user_id: int | None, is_admin: bool, session_id: str | None,
|
||||
) -> list[CalendarEntryDTO]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class MarketService(Protocol):
|
||||
async def marketplaces_for_container(
|
||||
self, session: AsyncSession, container_type: str, container_id: int,
|
||||
) -> list[MarketPlaceDTO]: ...
|
||||
|
||||
async def product_by_id(self, session: AsyncSession, product_id: int) -> ProductDTO | None: ...
|
||||
|
||||
async def create_marketplace(
|
||||
self, session: AsyncSession, container_type: str, container_id: int,
|
||||
name: str, slug: str,
|
||||
) -> MarketPlaceDTO: ...
|
||||
|
||||
async def list_marketplaces(
|
||||
self, session: AsyncSession,
|
||||
container_type: str | None = None, container_id: int | None = None,
|
||||
*, page: int = 1, per_page: int = 20,
|
||||
) -> tuple[list[MarketPlaceDTO], bool]: ...
|
||||
|
||||
async def soft_delete_marketplace(
|
||||
self, session: AsyncSession, container_type: str, container_id: int,
|
||||
slug: str,
|
||||
) -> bool: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class CartService(Protocol):
|
||||
async def cart_summary(
|
||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
||||
page_slug: str | None = None,
|
||||
) -> CartSummaryDTO: ...
|
||||
|
||||
async def cart_items(
|
||||
self, session: AsyncSession, *, user_id: int | None, session_id: str | None,
|
||||
) -> list[CartItemDTO]: ...
|
||||
|
||||
async def adopt_cart_for_user(
|
||||
self, session: AsyncSession, user_id: int, session_id: str,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class FederationService(Protocol):
|
||||
# -- Actor management -----------------------------------------------------
|
||||
async def get_actor_by_username(
|
||||
self, session: AsyncSession, username: str,
|
||||
) -> ActorProfileDTO | None: ...
|
||||
|
||||
async def get_actor_by_user_id(
|
||||
self, session: AsyncSession, user_id: int,
|
||||
) -> ActorProfileDTO | None: ...
|
||||
|
||||
async def create_actor(
|
||||
self, session: AsyncSession, user_id: int, preferred_username: str,
|
||||
display_name: str | None = None, summary: str | None = None,
|
||||
) -> ActorProfileDTO: ...
|
||||
|
||||
async def username_available(
|
||||
self, session: AsyncSession, username: str,
|
||||
) -> bool: ...
|
||||
|
||||
# -- Publishing (core cross-domain API) -----------------------------------
|
||||
async def publish_activity(
|
||||
self, session: AsyncSession, *,
|
||||
actor_user_id: int,
|
||||
activity_type: str,
|
||||
object_type: str,
|
||||
object_data: dict,
|
||||
source_type: str | None = None,
|
||||
source_id: int | None = None,
|
||||
) -> APActivityDTO: ...
|
||||
|
||||
# -- Queries --------------------------------------------------------------
|
||||
async def get_activity(
|
||||
self, session: AsyncSession, activity_id: str,
|
||||
) -> APActivityDTO | None: ...
|
||||
|
||||
async def get_outbox(
|
||||
self, session: AsyncSession, username: str,
|
||||
page: int = 1, per_page: int = 20,
|
||||
origin_app: str | None = None,
|
||||
) -> tuple[list[APActivityDTO], int]: ...
|
||||
|
||||
async def get_activity_for_source(
|
||||
self, session: AsyncSession, source_type: str, source_id: int,
|
||||
) -> APActivityDTO | None: ...
|
||||
|
||||
async def count_activities_for_source(
|
||||
self, session: AsyncSession, source_type: str, source_id: int,
|
||||
*, activity_type: str,
|
||||
) -> int: ...
|
||||
|
||||
# -- Followers ------------------------------------------------------------
|
||||
async def get_followers(
|
||||
self, session: AsyncSession, username: str,
|
||||
app_domain: str | None = None,
|
||||
) -> list[APFollowerDTO]: ...
|
||||
|
||||
async def get_followers_paginated(
|
||||
self, session: AsyncSession, username: str,
|
||||
page: int = 1, per_page: int = 20,
|
||||
) -> tuple[list[RemoteActorDTO], int]: ...
|
||||
|
||||
async def add_follower(
|
||||
self, session: AsyncSession, username: str,
|
||||
follower_acct: str, follower_inbox: str, follower_actor_url: str,
|
||||
follower_public_key: str | None = None,
|
||||
app_domain: str = "federation",
|
||||
) -> APFollowerDTO: ...
|
||||
|
||||
async def remove_follower(
|
||||
self, session: AsyncSession, username: str, follower_acct: str,
|
||||
app_domain: str = "federation",
|
||||
) -> bool: ...
|
||||
|
||||
# -- Remote actors --------------------------------------------------------
|
||||
async def get_or_fetch_remote_actor(
|
||||
self, session: AsyncSession, actor_url: str,
|
||||
) -> RemoteActorDTO | None: ...
|
||||
|
||||
async def search_remote_actor(
|
||||
self, session: AsyncSession, acct: str,
|
||||
) -> RemoteActorDTO | None: ...
|
||||
|
||||
async def search_actors(
|
||||
self, session: AsyncSession, query: str, page: int = 1, limit: int = 20,
|
||||
) -> tuple[list[RemoteActorDTO], int]: ...
|
||||
|
||||
# -- Following (outbound) -------------------------------------------------
|
||||
async def send_follow(
|
||||
self, session: AsyncSession, local_username: str, remote_actor_url: str,
|
||||
) -> None: ...
|
||||
|
||||
async def get_following(
|
||||
self, session: AsyncSession, username: str,
|
||||
page: int = 1, per_page: int = 20,
|
||||
) -> tuple[list[RemoteActorDTO], int]: ...
|
||||
|
||||
async def accept_follow_response(
|
||||
self, session: AsyncSession, local_username: str, remote_actor_url: str,
|
||||
) -> None: ...
|
||||
|
||||
async def unfollow(
|
||||
self, session: AsyncSession, local_username: str, remote_actor_url: str,
|
||||
) -> None: ...
|
||||
|
||||
# -- Remote posts ---------------------------------------------------------
|
||||
async def ingest_remote_post(
|
||||
self, session: AsyncSession, remote_actor_id: int,
|
||||
activity_json: dict, object_json: dict,
|
||||
) -> None: ...
|
||||
|
||||
async def delete_remote_post(
|
||||
self, session: AsyncSession, object_id: str,
|
||||
) -> None: ...
|
||||
|
||||
async def get_remote_post(
|
||||
self, session: AsyncSession, object_id: str,
|
||||
) -> RemotePostDTO | None: ...
|
||||
|
||||
# -- Timelines ------------------------------------------------------------
|
||||
async def get_home_timeline(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
before: datetime | None = None, limit: int = 20,
|
||||
) -> list[TimelineItemDTO]: ...
|
||||
|
||||
async def get_public_timeline(
|
||||
self, session: AsyncSession,
|
||||
before: datetime | None = None, limit: int = 20,
|
||||
) -> list[TimelineItemDTO]: ...
|
||||
|
||||
async def get_actor_timeline(
|
||||
self, session: AsyncSession, remote_actor_id: int,
|
||||
before: datetime | None = None, limit: int = 20,
|
||||
) -> list[TimelineItemDTO]: ...
|
||||
|
||||
# -- Local posts ----------------------------------------------------------
|
||||
async def create_local_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
content: str, visibility: str = "public",
|
||||
in_reply_to: str | None = None,
|
||||
) -> int: ...
|
||||
|
||||
async def delete_local_post(
|
||||
self, session: AsyncSession, actor_profile_id: int, post_id: int,
|
||||
) -> None: ...
|
||||
|
||||
# -- Interactions ---------------------------------------------------------
|
||||
async def like_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
object_id: str, author_inbox: str,
|
||||
) -> None: ...
|
||||
|
||||
async def unlike_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
object_id: str, author_inbox: str,
|
||||
) -> None: ...
|
||||
|
||||
async def boost_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
object_id: str, author_inbox: str,
|
||||
) -> None: ...
|
||||
|
||||
async def unboost_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
object_id: str, author_inbox: str,
|
||||
) -> None: ...
|
||||
|
||||
# -- Notifications --------------------------------------------------------
|
||||
async def get_notifications(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
before: datetime | None = None, limit: int = 20,
|
||||
) -> list[NotificationDTO]: ...
|
||||
|
||||
async def unread_notification_count(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
) -> int: ...
|
||||
|
||||
async def mark_notifications_read(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
) -> None: ...
|
||||
|
||||
# -- Stats ----------------------------------------------------------------
|
||||
async def get_stats(self, session: AsyncSession) -> dict: ...
|
||||
49
shared/contracts/widgets.py
Normal file
49
shared/contracts/widgets.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Widget descriptors for cross-domain UI composition.
|
||||
|
||||
Each widget type describes a UI fragment that one domain contributes to
|
||||
another domain's page. Host apps iterate widgets generically — they never
|
||||
name the contributing domain.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NavWidget:
|
||||
"""Renders nav items on a container page (entries, calendars, markets)."""
|
||||
domain: str
|
||||
order: int
|
||||
context_fn: Callable # async (session, *, container_type, container_id, **kw) -> dict
|
||||
template: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CardWidget:
|
||||
"""Decorates content cards in listings with domain data."""
|
||||
domain: str
|
||||
order: int
|
||||
batch_fn: Callable # async (session, post_ids) -> dict[int, list]
|
||||
context_key: str # key injected into each post dict
|
||||
template: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AccountPageWidget:
|
||||
"""Sub-page under /auth/<slug>/."""
|
||||
domain: str
|
||||
slug: str
|
||||
label: str
|
||||
order: int
|
||||
context_fn: Callable # async (session, *, user_id, **kw) -> dict
|
||||
template: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AccountNavLink:
|
||||
"""Nav link on account page (internal or external)."""
|
||||
label: str
|
||||
order: int
|
||||
href_fn: Callable # () -> str
|
||||
external: bool = False
|
||||
Reference in New Issue
Block a user