Compare commits

...

1 Commits

Author SHA1 Message Date
giles
d697709f60 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>
2026-02-22 15:11:31 +00:00
9 changed files with 88 additions and 76 deletions

View File

@@ -1,6 +1,6 @@
# 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
@@ -8,20 +8,41 @@ Shared infrastructure, models, templates, and configuration used by all four Ros
shared/
db/
base.py # SQLAlchemy declarative Base
session.py # Async session factory (get_session)
models/ # Shared domain models
session.py # Async session factory (get_session, register_db)
models/ # Canonical domain models
user.py # User
magic_link.py # MagicLink (auth tokens)
domain_event.py # DomainEvent (transactional outbox)
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
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/
factory.py # create_base_app() — Quart app factory
cart_identity.py # current_cart_identity() (user_id or session_id)
cart_loader.py # Cart data loader for 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
urls.py # URL helpers (coop_url, market_url, etc.)
user_loader.py # Load current user from session
@@ -29,32 +50,36 @@ shared/
events/
bus.py # emit_event(), register_handler()
processor.py # EventProcessor (polls domain_events, runs handlers)
browser/app/
csrf.py # CSRF protection
errors.py # Error handlers
middleware.py # Request/response middleware
redis_cacher.py # Tag-based Redis page caching
authz.py # Authorization helpers
filters/ # Jinja2 template filters (currency, truncate, etc.)
utils/ # HTMX helpers, UTC time, parsing
payments/sumup.py # SumUp checkout API integration
browser/templates/ # ~300 Jinja2 templates shared across all apps
config.py # YAML config loader
handlers/ # Shared event handlers
container_handlers.py # Navigation rebuild on attach/detach
login_handlers.py # Cart/entry adoption on login
order_handlers.py # Order lifecycle events
ap_delivery_handler.py # AP activity delivery to follower inboxes
utils/
__init__.py
calendar_helpers.py # Calendar period/entry utilities
http_signatures.py # RSA keypair generation, HTTP signature signing/verification
ipfs_client.py # Async IPFS client (add_bytes, add_json, pin_cid)
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
config.py # YAML config loader
log_config/setup.py # Logging configuration (JSON formatter)
utils.py # host_url and other shared utilities
static/ # Shared static assets (CSS, JS, images, FontAwesome)
editor/ # Koenig (Ghost) rich text editor build
alembic/ # Database migrations (25 versions)
env.py # Imports models from all apps (with try/except guards)
versions/ # Migration files — single head: j0h8e4f6g7
alembic/ # Database migrations
```
## 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.
- **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.
## 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).
```bash
# From any app directory (shared/ must be on sys.path)
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 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:
__import__(_mod)
except ImportError:

View File

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

View File

@@ -1,4 +1,4 @@
"""Shared event handlers (replaces glue.setup.register_glue_handlers)."""
"""Shared event 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.login_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

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):
"""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"
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))
return entries_by_post
# -- writes (absorb glue lifecycle) ---------------------------------------
# -- writes ---------------------------------------------------------------
async def adopt_entries_for_user(
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.
# 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