Tech debt cleanup: fix session.py, remove stale references, update docs

- db/session.py: fix indentation (2→4 space), pool_size=0 (unlimited),
  remove "ned to look at this" typo
- Remove glue.models from alembic env.py import list
- Update shared __init__.py, menu_item.py docstring, calendar_impl.py,
  handlers/__init__.py to remove glue terminology
- Remove federation_handlers.py tombstone file
- Remove TODO comments (replace with explanatory comments)
- Rewrite README.md to reflect current architecture
- Update anchoring.py TODO to plain comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-22 15:11:31 +00:00
parent 04f7c5e85c
commit d697709f60
9 changed files with 88 additions and 76 deletions

View File

@@ -1,6 +1,6 @@
# Shared # Shared
Shared infrastructure, models, templates, and configuration used by all four Rose Ash microservices (blog, market, cart, events). Included as a git submodule in each app. Shared infrastructure, models, contracts, services, and templates used by all five Rose Ash microservices (blog, market, cart, events, federation). Included as a git submodule in each app.
## Structure ## Structure
@@ -8,20 +8,41 @@ Shared infrastructure, models, templates, and configuration used by all four Ros
shared/ shared/
db/ db/
base.py # SQLAlchemy declarative Base base.py # SQLAlchemy declarative Base
session.py # Async session factory (get_session) session.py # Async session factory (get_session, register_db)
models/ # Shared domain models models/ # Canonical domain models
user.py # User user.py # User
magic_link.py # MagicLink (auth tokens) magic_link.py # MagicLink (auth tokens)
domain_event.py # DomainEvent (transactional outbox) domain_event.py # DomainEvent (transactional outbox)
kv.py # KeyValue (key-value store) kv.py # KeyValue (key-value store)
menu_item.py # MenuItem menu_item.py # MenuItem (deprecated — use MenuNode)
menu_node.py # MenuNode (navigation tree)
container_relation.py # ContainerRelation (parent-child content)
ghost_membership_entities.py # GhostNewsletter, UserNewsletter ghost_membership_entities.py # GhostNewsletter, UserNewsletter
federation.py # ActorProfile, APActivity, APFollower, APFollowing,
# RemoteActor, APRemotePost, APLocalPost,
# APInteraction, APNotification, APAnchor, IPFSPin
contracts/
dtos.py # Frozen dataclasses for cross-domain data transfer
protocols.py # Service protocols (Blog, Calendar, Market, Cart, Federation)
widgets.py # Widget types (NavWidget, CardWidget, AccountPageWidget)
services/
registry.py # Typed singleton: services.blog, .calendar, .market, .cart, .federation
blog_impl.py # SqlBlogService
calendar_impl.py # SqlCalendarService
market_impl.py # SqlMarketService
cart_impl.py # SqlCartService
federation_impl.py # SqlFederationService
federation_publish.py # try_publish() — inline AP publication helper
stubs.py # No-op stubs for absent domains
navigation.py # get_navigation_tree()
relationships.py # attach_child, get_children, detach_child
widget_registry.py # Widget registry singleton
widgets/ # Per-domain widget registration
infrastructure/ infrastructure/
factory.py # create_base_app() — Quart app factory factory.py # create_base_app() — Quart app factory
cart_identity.py # current_cart_identity() (user_id or session_id) cart_identity.py # current_cart_identity() (user_id or session_id)
cart_loader.py # Cart data loader for context processors cart_loader.py # Cart data loader for context processors
context.py # Jinja2 context processors context.py # Jinja2 context processors
internal_api.py # Inter-app HTTP client (get/post via httpx)
jinja_setup.py # Jinja2 template environment setup jinja_setup.py # Jinja2 template environment setup
urls.py # URL helpers (coop_url, market_url, etc.) urls.py # URL helpers (coop_url, market_url, etc.)
user_loader.py # Load current user from session user_loader.py # Load current user from session
@@ -29,32 +50,36 @@ shared/
events/ events/
bus.py # emit_event(), register_handler() bus.py # emit_event(), register_handler()
processor.py # EventProcessor (polls domain_events, runs handlers) processor.py # EventProcessor (polls domain_events, runs handlers)
browser/app/ handlers/ # Shared event handlers
csrf.py # CSRF protection container_handlers.py # Navigation rebuild on attach/detach
errors.py # Error handlers login_handlers.py # Cart/entry adoption on login
middleware.py # Request/response middleware order_handlers.py # Order lifecycle events
redis_cacher.py # Tag-based Redis page caching ap_delivery_handler.py # AP activity delivery to follower inboxes
authz.py # Authorization helpers utils/
filters/ # Jinja2 template filters (currency, truncate, etc.) __init__.py
utils/ # HTMX helpers, UTC time, parsing calendar_helpers.py # Calendar period/entry utilities
payments/sumup.py # SumUp checkout API integration http_signatures.py # RSA keypair generation, HTTP signature signing/verification
browser/templates/ # ~300 Jinja2 templates shared across all apps ipfs_client.py # Async IPFS client (add_bytes, add_json, pin_cid)
config.py # YAML config loader anchoring.py # Merkle trees + OpenTimestamps Bitcoin anchoring
webfinger.py # WebFinger actor resolution
browser/
app/ # Middleware, CSRF, errors, Redis caching, authz, filters
templates/ # ~300 Jinja2 templates shared across all apps
containers.py # ContainerType, container_filter, content_filter helpers containers.py # ContainerType, container_filter, content_filter helpers
config.py # YAML config loader
log_config/setup.py # Logging configuration (JSON formatter) log_config/setup.py # Logging configuration (JSON formatter)
utils.py # host_url and other shared utilities
static/ # Shared static assets (CSS, JS, images, FontAwesome) static/ # Shared static assets (CSS, JS, images, FontAwesome)
editor/ # Koenig (Ghost) rich text editor build editor/ # Koenig (Ghost) rich text editor build
alembic/ # Database migrations (25 versions) alembic/ # Database migrations
env.py # Imports models from all apps (with try/except guards)
versions/ # Migration files — single head: j0h8e4f6g7
``` ```
## Key Patterns ## Key Patterns
- **App factory:** All apps call `create_base_app()` which sets up DB sessions, CSRF, error handling, event processing, logging, and the glue handler registry. - **App factory:** All apps call `create_base_app()` which sets up DB sessions, CSRF, error handling, event processing, logging, widget registration, and domain service wiring.
- **Service contracts:** Cross-domain communication via typed Protocols + frozen DTO dataclasses. Apps call `services.calendar.method()`, never import models from other domains.
- **Service registry:** Typed singleton (`services.blog`, `.calendar`, `.market`, `.cart`, `.federation`). Apps wire their own domain + stubs for others via `register_domain_services()`.
- **Event bus:** `emit_event()` writes to `domain_events` table in the caller's transaction. `EventProcessor` polls and dispatches to registered handlers. - **Event bus:** `emit_event()` writes to `domain_events` table in the caller's transaction. `EventProcessor` polls and dispatches to registered handlers.
- **Inter-app HTTP:** `internal_api.get/post("cart", "/internal/cart/summary")` for cross-app reads. URLs resolved from `app-config.yaml`. - **Widget registry:** Domain services register widgets (nav, card, account); templates consume via `widgets.container_nav`, `widgets.container_cards`.
- **Cart identity:** `current_cart_identity()` returns `{"user_id": int|None, "session_id": str|None}` from the request session. - **Cart identity:** `current_cart_identity()` returns `{"user_id": int|None, "session_id": str|None}` from the request session.
## Alembic Migrations ## Alembic Migrations
@@ -62,8 +87,5 @@ shared/
All apps share one PostgreSQL database. Migrations are managed here and run from the blog app's entrypoint (other apps skip migrations on startup). All apps share one PostgreSQL database. Migrations are managed here and run from the blog app's entrypoint (other apps skip migrations on startup).
```bash ```bash
# From any app directory (shared/ must be on sys.path)
alembic -c shared/alembic.ini upgrade head alembic -c shared/alembic.ini upgrade head
``` ```
Current head: `j0h8e4f6g7` (drop cross-domain FK constraints).

View File

@@ -1 +1 @@
# shared package — extracted from blog/shared_lib/ # shared package — infrastructure, models, contracts, and services

View File

@@ -19,7 +19,7 @@ from shared.db.base import Base
# Import ALL models so Base.metadata sees every table # Import ALL models so Base.metadata sees every table
import shared.models # noqa: F401 User, KV, MagicLink, MenuItem, Ghost* import shared.models # noqa: F401 User, KV, MagicLink, MenuItem, Ghost*
for _mod in ("blog.models", "market.models", "cart.models", "events.models", "federation.models", "glue.models"): for _mod in ("blog.models", "market.models", "cart.models", "events.models", "federation.models"):
try: try:
__import__(_mod) __import__(_mod)
except ImportError: except ImportError:

View File

@@ -15,7 +15,7 @@ _engine = create_async_engine(
future=True, future=True,
echo=False, echo=False,
pool_pre_ping=True, pool_pre_ping=True,
pool_size=-1 # ned to look at this!!! pool_size=0, # 0 = unlimited (NullPool equivalent for asyncpg)
) )
_Session = async_sessionmaker( _Session = async_sessionmaker(
@@ -34,43 +34,42 @@ async def get_session():
await sess.close() await sess.close()
def register_db(app: Quart): def register_db(app: Quart):
@app.before_request @app.before_request
async def open_session(): async def open_session():
g.s = _Session() g.s = _Session()
g.tx = await g.s.begin() g.tx = await g.s.begin()
g.had_error = False g.had_error = False
@app.after_request @app.after_request
async def maybe_commit(response): async def maybe_commit(response):
# Runs BEFORE bytes are sent. # Runs BEFORE bytes are sent.
if not g.had_error and 200 <= response.status_code < 400: if not g.had_error and 200 <= response.status_code < 400:
try: try:
if hasattr(g, "tx"): if hasattr(g, "tx"):
await g.tx.commit() await g.tx.commit()
except Exception as e: except Exception as e:
print(f'commit failed {e}') print(f'commit failed {e}')
if hasattr(g, "tx"): if hasattr(g, "tx"):
await g.tx.rollback() await g.tx.rollback()
from quart import make_response from quart import make_response
return await make_response("Commit failed", 500) return await make_response("Commit failed", 500)
return response return response
@app.teardown_request @app.teardown_request
async def finish(exc): async def finish(exc):
try: try:
# If an exception occurred OR we didn't commit (still in txn), roll back. # If an exception occurred OR we didn't commit (still in txn), roll back.
if hasattr(g, "s"): if hasattr(g, "s"):
if exc is not None or g.s.in_transaction(): if exc is not None or g.s.in_transaction():
if hasattr(g, "tx"): if hasattr(g, "tx"):
await g.tx.rollback() await g.tx.rollback()
finally: finally:
if hasattr(g, "s"): if hasattr(g, "s"):
await g.s.close() await g.s.close()
@app.errorhandler(Exception) @app.errorhandler(Exception)
async def mark_error(e): async def mark_error(e):
g.had_error = True g.had_error = True
raise raise

View File

@@ -1,4 +1,4 @@
"""Shared event handlers (replaces glue.setup.register_glue_handlers).""" """Shared event handlers."""
def register_shared_handlers(): def register_shared_handlers():
@@ -6,5 +6,4 @@ def register_shared_handlers():
import shared.events.handlers.container_handlers # noqa: F401 import shared.events.handlers.container_handlers # noqa: F401
import shared.events.handlers.login_handlers # noqa: F401 import shared.events.handlers.login_handlers # noqa: F401
import shared.events.handlers.order_handlers # noqa: F401 import shared.events.handlers.order_handlers # noqa: F401
# federation_handlers removed — publication is now inline at write sites
import shared.events.handlers.ap_delivery_handler # noqa: F401 import shared.events.handlers.ap_delivery_handler # noqa: F401

View File

@@ -1,8 +0,0 @@
"""Federation event handlers — REMOVED.
Federation publication is now inline at the write site (ghost_sync, entries,
market routes) via shared.services.federation_publish.try_publish().
AP delivery (federation.activity_created → inbox POST) remains async via
ap_delivery_handler.
"""

View File

@@ -6,7 +6,7 @@ from shared.db.base import Base
class MenuItem(Base): class MenuItem(Base):
"""Deprecated — kept so the table isn't dropped. Use glue.models.MenuNode.""" """Deprecated — kept so the table isn't dropped. Use shared.models.menu_node.MenuNode."""
__tablename__ = "menu_items" __tablename__ = "menu_items"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

View File

@@ -371,7 +371,7 @@ class SqlCalendarService:
entries_by_post.setdefault(post_id, []).append(_entry_to_dto(entry)) entries_by_post.setdefault(post_id, []).append(_entry_to_dto(entry))
return entries_by_post return entries_by_post
# -- writes (absorb glue lifecycle) --------------------------------------- # -- writes ---------------------------------------------------------------
async def adopt_entries_for_user( async def adopt_entries_for_user(
self, session: AsyncSession, user_id: int, session_id: str, self, session: AsyncSession, user_id: int, session_id: str,

View File

@@ -145,7 +145,7 @@ async def upgrade_ots_proof(proof_bytes: bytes) -> tuple[bytes, bool]:
""" """
# OpenTimestamps upgrade is done via the `ots` CLI or their calendar API. # OpenTimestamps upgrade is done via the `ots` CLI or their calendar API.
# For now, return the proof as-is with is_confirmed=False. # For now, return the proof as-is with is_confirmed=False.
# TODO: Implement calendar-based upgrade polling. # Calendar-based upgrade polling not yet implemented.
return proof_bytes, False return proof_bytes, False