feat: initial shared library extraction

Contains shared infrastructure for all coop services:
- shared/ (factory, urls, user_loader, context, internal_api, jinja_setup)
- models/ (User, Order, Calendar, Ticket, Product, Ghost CMS)
- db/ (SQLAlchemy async session, base)
- suma_browser/app/ (csrf, middleware, errors, authz, redis_cacher, payments, filters, utils)
- suma_browser/templates/ (shared base layouts, macros, error pages)
- static/ (CSS, JS, fonts, images)
- alembic/ (database migrations)
- config/ (app-config.yaml)
- editor/ (Lexical editor Node.js build)
- requirements.txt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-09 23:11:36 +00:00
commit 668d9c7df8
446 changed files with 22741 additions and 0 deletions

0
db/__init__.py Normal file
View File

4
db/base.py Normal file
View File

@@ -0,0 +1,4 @@
from __future__ import annotations
from sqlalchemy.orm import declarative_base
Base = declarative_base()

99
db/session.py Normal file
View File

@@ -0,0 +1,99 @@
from __future__ import annotations
import os
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from quart import Quart, g
DATABASE_URL = (
os.getenv("DATABASE_URL_ASYNC")
or os.getenv("DATABASE_URL")
or "postgresql+asyncpg://localhost/coop"
)
_engine = create_async_engine(
DATABASE_URL,
future=True,
echo=False,
pool_pre_ping=True,
pool_size=-1 # ned to look at this!!!
)
_Session = async_sessionmaker(
bind=_engine,
class_=AsyncSession,
expire_on_commit=False,
)
@asynccontextmanager
async def get_session():
"""Always create a fresh AsyncSession for this block."""
sess = _Session()
try:
yield sess
finally:
await sess.close()
def register_db(app: Quart):
#@app.before_request
#async def _open_session():
# g.s = _Session()
# g.tx = await g.s.begin() # begin txn now (or begin_nested if you like)
#@app.after_request
#async def _commit_session(response):
# print('after request')
# return response
#@app.teardown_request
#async def _rollback_on_error(exc):
# print('teardown')
# # Quart calls this when an exception happened
# if exc is not None and hasattr(g, "tx"):
# await g.tx.rollback()
# if exc and hasattr(g, 'tx'):
# await g.tx.commit()
# if hasattr(g, "sess"):
# await g.s.close()
@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.teardown_request
async def finish(exc):
try:
# If an exception occurred OR we didnt 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