diff --git a/python/api/__init__.py b/python/api/__init__.py deleted file mode 100644 index 7b09757..0000000 --- a/python/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""FastAPI applications.""" diff --git a/python/api/main.py b/python/api/main.py deleted file mode 100644 index af918b5..0000000 --- a/python/api/main.py +++ /dev/null @@ -1,56 +0,0 @@ -"""FastAPI interface for Contact database.""" - -from __future__ import annotations - -import logging -from contextlib import asynccontextmanager -from typing import TYPE_CHECKING, Annotated - -import typer -import uvicorn -from fastapi import FastAPI - -from python.api.routers import contact_router, views_router -from python.common import configure_logger -from python.fastapi_tools import ZstdMiddleware -from python.orm.common import get_postgres_engine - -if TYPE_CHECKING: - from collections.abc import AsyncIterator - -logger = logging.getLogger(__name__) - - -def create_app() -> FastAPI: - """Create and configure the FastAPI application.""" - - @asynccontextmanager - async def lifespan(app: FastAPI) -> AsyncIterator[None]: - """Manage application lifespan.""" - app.state.engine = get_postgres_engine() - yield - app.state.engine.dispose() - - app = FastAPI(title="Contact Database API", lifespan=lifespan) - app.add_middleware(ZstdMiddleware) - - app.include_router(contact_router) - app.include_router(views_router) - - return app - - -def serve( - host: Annotated[str, typer.Option("--host", "-h", help="Host to bind to")], - port: Annotated[int, typer.Option("--port", "-p", help="Port to bind to")] = 8000, - log_level: Annotated[str, typer.Option("--log-level", "-l", help="Log level")] = "INFO", -) -> None: - """Start the Contact API server.""" - configure_logger(log_level) - - app = create_app() - uvicorn.run(app, host=host, port=port) - - -if __name__ == "__main__": - typer.run(serve) diff --git a/python/api/routers/__init__.py b/python/api/routers/__init__.py deleted file mode 100644 index 87d808b..0000000 --- a/python/api/routers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""API routers.""" - -from python.api.routers.contact import router as contact_router -from python.api.routers.views import router as views_router - -__all__ = ["contact_router", "views_router"] diff --git a/python/api/routers/contact.py b/python/api/routers/contact.py deleted file mode 100644 index 57da325..0000000 --- a/python/api/routers/contact.py +++ /dev/null @@ -1,481 +0,0 @@ -"""Contact API router.""" - -from pathlib import Path - -from fastapi import APIRouter, HTTPException, Request -from fastapi.responses import HTMLResponse -from fastapi.templating import Jinja2Templates -from pydantic import BaseModel -from sqlalchemy import select -from sqlalchemy.orm import selectinload - -from python.fastapi_tools.db import DbSession # noqa: TC001 this is a FastAPI needed at runtime -from python.orm.richie.contact import Contact, ContactRelationship, Need, RelationshipType - -TEMPLATES_DIR = Path(__file__).parent.parent / "templates" -templates = Jinja2Templates(directory=TEMPLATES_DIR) - - -def _is_htmx(request: Request) -> bool: - """Check if the request is from HTMX.""" - return request.headers.get("HX-Request") == "true" - - -class NeedBase(BaseModel): - """Base schema for Need.""" - - name: str - description: str | None = None - - -class NeedCreate(NeedBase): - """Schema for creating a Need.""" - - -class NeedResponse(NeedBase): - """Schema for Need response.""" - - id: int - - model_config = {"from_attributes": True} - - -class ContactRelationshipCreate(BaseModel): - """Schema for creating a contact relationship.""" - - related_contact_id: int - relationship_type: RelationshipType - closeness_weight: int | None = None - - -class ContactRelationshipUpdate(BaseModel): - """Schema for updating a contact relationship.""" - - relationship_type: RelationshipType | None = None - closeness_weight: int | None = None - - -class ContactRelationshipResponse(BaseModel): - """Schema for contact relationship response.""" - - contact_id: int - related_contact_id: int - relationship_type: str - closeness_weight: int - - model_config = {"from_attributes": True} - - -class RelationshipTypeInfo(BaseModel): - """Information about a relationship type.""" - - value: str - display_name: str - default_weight: int - - -class GraphNode(BaseModel): - """Node in the relationship graph.""" - - id: int - name: str - current_job: str | None = None - - -class GraphEdge(BaseModel): - """Edge in the relationship graph.""" - - source: int - target: int - relationship_type: str - closeness_weight: int - - -class GraphData(BaseModel): - """Complete graph data for visualization.""" - - nodes: list[GraphNode] - edges: list[GraphEdge] - - -class ContactBase(BaseModel): - """Base schema for Contact.""" - - name: str - age: int | None = None - bio: str | None = None - current_job: str | None = None - gender: str | None = None - goals: str | None = None - legal_name: str | None = None - profile_pic: str | None = None - safe_conversation_starters: str | None = None - self_sufficiency_score: int | None = None - social_structure_style: str | None = None - ssn: str | None = None - suffix: str | None = None - timezone: str | None = None - topics_to_avoid: str | None = None - - -class ContactCreate(ContactBase): - """Schema for creating a Contact.""" - - need_ids: list[int] = [] - - -class ContactUpdate(BaseModel): - """Schema for updating a Contact.""" - - name: str | None = None - age: int | None = None - bio: str | None = None - current_job: str | None = None - gender: str | None = None - goals: str | None = None - legal_name: str | None = None - profile_pic: str | None = None - safe_conversation_starters: str | None = None - self_sufficiency_score: int | None = None - social_structure_style: str | None = None - ssn: str | None = None - suffix: str | None = None - timezone: str | None = None - topics_to_avoid: str | None = None - need_ids: list[int] | None = None - - -class ContactResponse(ContactBase): - """Schema for Contact response with relationships.""" - - id: int - needs: list[NeedResponse] = [] - related_to: list[ContactRelationshipResponse] = [] - related_from: list[ContactRelationshipResponse] = [] - - model_config = {"from_attributes": True} - - -class ContactListResponse(ContactBase): - """Schema for Contact list response.""" - - id: int - - model_config = {"from_attributes": True} - - -router = APIRouter(prefix="/api", tags=["contacts"]) - - -@router.post("/needs", response_model=NeedResponse) -def create_need(need: NeedCreate, db: DbSession) -> Need: - """Create a new need.""" - db_need = Need(name=need.name, description=need.description) - db.add(db_need) - db.commit() - db.refresh(db_need) - return db_need - - -@router.get("/needs", response_model=list[NeedResponse]) -def list_needs(db: DbSession) -> list[Need]: - """List all needs.""" - return list(db.scalars(select(Need)).all()) - - -@router.get("/needs/{need_id}", response_model=NeedResponse) -def get_need(need_id: int, db: DbSession) -> Need: - """Get a need by ID.""" - need = db.get(Need, need_id) - if not need: - raise HTTPException(status_code=404, detail="Need not found") - return need - - -@router.delete("/needs/{need_id}", response_model=None) -def delete_need(need_id: int, request: Request, db: DbSession) -> dict[str, bool] | HTMLResponse: - """Delete a need by ID.""" - need = db.get(Need, need_id) - if not need: - raise HTTPException(status_code=404, detail="Need not found") - db.delete(need) - db.commit() - if _is_htmx(request): - return HTMLResponse("") - return {"deleted": True} - - -@router.post("/contacts", response_model=ContactResponse) -def create_contact(contact: ContactCreate, db: DbSession) -> Contact: - """Create a new contact.""" - need_ids = contact.need_ids - contact_data = contact.model_dump(exclude={"need_ids"}) - db_contact = Contact(**contact_data) - - if need_ids: - needs = list(db.scalars(select(Need).where(Need.id.in_(need_ids))).all()) - db_contact.needs = needs - - db.add(db_contact) - db.commit() - db.refresh(db_contact) - return db_contact - - -@router.get("/contacts", response_model=list[ContactListResponse]) -def list_contacts( - db: DbSession, - skip: int = 0, - limit: int = 100, -) -> list[Contact]: - """List all contacts with pagination.""" - return list(db.scalars(select(Contact).offset(skip).limit(limit)).all()) - - -@router.get("/contacts/{contact_id}", response_model=ContactResponse) -def get_contact(contact_id: int, db: DbSession) -> Contact: - """Get a contact by ID with all relationships.""" - contact = db.scalar( - select(Contact) - .where(Contact.id == contact_id) - .options( - selectinload(Contact.needs), - selectinload(Contact.related_to), - selectinload(Contact.related_from), - ) - ) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - return contact - - -@router.patch("/contacts/{contact_id}", response_model=ContactResponse) -def update_contact( - contact_id: int, - contact: ContactUpdate, - db: DbSession, -) -> Contact: - """Update a contact by ID.""" - db_contact = db.get(Contact, contact_id) - if not db_contact: - raise HTTPException(status_code=404, detail="Contact not found") - - update_data = contact.model_dump(exclude_unset=True) - need_ids = update_data.pop("need_ids", None) - - for key, value in update_data.items(): - setattr(db_contact, key, value) - - if need_ids is not None: - needs = list(db.scalars(select(Need).where(Need.id.in_(need_ids))).all()) - db_contact.needs = needs - - db.commit() - db.refresh(db_contact) - return db_contact - - -@router.delete("/contacts/{contact_id}", response_model=None) -def delete_contact(contact_id: int, request: Request, db: DbSession) -> dict[str, bool] | HTMLResponse: - """Delete a contact by ID.""" - contact = db.get(Contact, contact_id) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - db.delete(contact) - db.commit() - if _is_htmx(request): - return HTMLResponse("") - return {"deleted": True} - - -@router.post("/contacts/{contact_id}/needs/{need_id}") -def add_need_to_contact( - contact_id: int, - need_id: int, - db: DbSession, -) -> dict[str, bool]: - """Add a need to a contact.""" - contact = db.get(Contact, contact_id) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - need = db.get(Need, need_id) - if not need: - raise HTTPException(status_code=404, detail="Need not found") - - if need not in contact.needs: - contact.needs.append(need) - db.commit() - - return {"added": True} - - -@router.delete("/contacts/{contact_id}/needs/{need_id}", response_model=None) -def remove_need_from_contact( - contact_id: int, - need_id: int, - request: Request, - db: DbSession, -) -> dict[str, bool] | HTMLResponse: - """Remove a need from a contact.""" - contact = db.get(Contact, contact_id) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - need = db.get(Need, need_id) - if not need: - raise HTTPException(status_code=404, detail="Need not found") - - if need in contact.needs: - contact.needs.remove(need) - db.commit() - - if _is_htmx(request): - return HTMLResponse("") - return {"removed": True} - - -@router.post( - "/contacts/{contact_id}/relationships", - response_model=ContactRelationshipResponse, -) -def add_contact_relationship( - contact_id: int, - relationship: ContactRelationshipCreate, - db: DbSession, -) -> ContactRelationship: - """Add a relationship between two contacts.""" - contact = db.get(Contact, contact_id) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - related_contact = db.get(Contact, relationship.related_contact_id) - if not related_contact: - raise HTTPException(status_code=404, detail="Related contact not found") - - if contact_id == relationship.related_contact_id: - raise HTTPException(status_code=400, detail="Cannot relate contact to itself") - - # Use provided weight or default from relationship type - weight = relationship.closeness_weight - if weight is None: - weight = relationship.relationship_type.default_weight - - db_relationship = ContactRelationship( - contact_id=contact_id, - related_contact_id=relationship.related_contact_id, - relationship_type=relationship.relationship_type.value, - closeness_weight=weight, - ) - db.add(db_relationship) - db.commit() - db.refresh(db_relationship) - return db_relationship - - -@router.get( - "/contacts/{contact_id}/relationships", - response_model=list[ContactRelationshipResponse], -) -def get_contact_relationships( - contact_id: int, - db: DbSession, -) -> list[ContactRelationship]: - """Get all relationships for a contact.""" - contact = db.get(Contact, contact_id) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - outgoing = list(db.scalars(select(ContactRelationship).where(ContactRelationship.contact_id == contact_id)).all()) - incoming = list( - db.scalars(select(ContactRelationship).where(ContactRelationship.related_contact_id == contact_id)).all() - ) - return outgoing + incoming - - -@router.patch( - "/contacts/{contact_id}/relationships/{related_contact_id}", - response_model=ContactRelationshipResponse, -) -def update_contact_relationship( - contact_id: int, - related_contact_id: int, - update: ContactRelationshipUpdate, - db: DbSession, -) -> ContactRelationship: - """Update a relationship between two contacts.""" - relationship = db.scalar( - select(ContactRelationship).where( - ContactRelationship.contact_id == contact_id, - ContactRelationship.related_contact_id == related_contact_id, - ) - ) - if not relationship: - raise HTTPException(status_code=404, detail="Relationship not found") - - if update.relationship_type is not None: - relationship.relationship_type = update.relationship_type.value - if update.closeness_weight is not None: - relationship.closeness_weight = update.closeness_weight - - db.commit() - db.refresh(relationship) - return relationship - - -@router.delete("/contacts/{contact_id}/relationships/{related_contact_id}", response_model=None) -def remove_contact_relationship( - contact_id: int, - related_contact_id: int, - request: Request, - db: DbSession, -) -> dict[str, bool] | HTMLResponse: - """Remove a relationship between two contacts.""" - relationship = db.scalar( - select(ContactRelationship).where( - ContactRelationship.contact_id == contact_id, - ContactRelationship.related_contact_id == related_contact_id, - ) - ) - if not relationship: - raise HTTPException(status_code=404, detail="Relationship not found") - - db.delete(relationship) - db.commit() - if _is_htmx(request): - return HTMLResponse("") - return {"deleted": True} - - -@router.get("/relationship-types") -def list_relationship_types() -> list[RelationshipTypeInfo]: - """List all available relationship types with their default weights.""" - return [ - RelationshipTypeInfo( - value=rt.value, - display_name=rt.display_name, - default_weight=rt.default_weight, - ) - for rt in RelationshipType - ] - - -@router.get("/graph") -def get_relationship_graph(db: DbSession) -> GraphData: - """Get all contacts and relationships as graph data for visualization.""" - contacts = list(db.scalars(select(Contact)).all()) - relationships = list(db.scalars(select(ContactRelationship)).all()) - - nodes = [GraphNode(id=c.id, name=c.name, current_job=c.current_job) for c in contacts] - - edges = [ - GraphEdge( - source=rel.contact_id, - target=rel.related_contact_id, - relationship_type=rel.relationship_type, - closeness_weight=rel.closeness_weight, - ) - for rel in relationships - ] - - return GraphData(nodes=nodes, edges=edges) diff --git a/python/api/routers/views.py b/python/api/routers/views.py deleted file mode 100644 index 4faacfa..0000000 --- a/python/api/routers/views.py +++ /dev/null @@ -1,345 +0,0 @@ -"""HTMX server-rendered view router.""" - -from pathlib import Path -from typing import Annotated, Any - -from fastapi import APIRouter, Form, HTTPException, Request -from fastapi.responses import HTMLResponse, RedirectResponse -from fastapi.templating import Jinja2Templates -from sqlalchemy import select -from sqlalchemy.orm import Session, selectinload - -from python.fastapi_tools.db import DbSession # noqa: TC001 this is a FastAPI needed at runtime -from python.orm.richie.contact import Contact, ContactRelationship, Need, RelationshipType - -TEMPLATES_DIR = Path(__file__).parent.parent / "templates" -templates = Jinja2Templates(directory=TEMPLATES_DIR) - -router = APIRouter(tags=["views"]) - -FAMILIAL_TYPES = { - "parent", - "child", - "sibling", - "grandparent", - "grandchild", - "aunt_uncle", - "niece_nephew", - "cousin", - "in_law", -} -FRIEND_TYPES = {"best_friend", "close_friend", "friend", "acquaintance", "neighbor"} -PARTNER_TYPES = {"spouse", "partner"} -PROFESSIONAL_TYPES = {"mentor", "mentee", "business_partner", "colleague", "manager", "direct_report", "client"} - -CONTACT_STRING_FIELDS = ( - "name", - "legal_name", - "suffix", - "gender", - "current_job", - "timezone", - "profile_pic", - "bio", - "goals", - "social_structure_style", - "safe_conversation_starters", - "topics_to_avoid", - "ssn", -) - -CONTACT_INT_FIELDS = ("age", "self_sufficiency_score") - - -def _group_relationships(relationships: list[ContactRelationship]) -> dict[str, list[ContactRelationship]]: - """Group relationships by category.""" - groups: dict[str, list[ContactRelationship]] = { - "familial": [], - "partners": [], - "friends": [], - "professional": [], - "other": [], - } - for rel in relationships: - if rel.relationship_type in FAMILIAL_TYPES: - groups["familial"].append(rel) - elif rel.relationship_type in PARTNER_TYPES: - groups["partners"].append(rel) - elif rel.relationship_type in FRIEND_TYPES: - groups["friends"].append(rel) - elif rel.relationship_type in PROFESSIONAL_TYPES: - groups["professional"].append(rel) - else: - groups["other"].append(rel) - return groups - - -def _build_contact_name_map(database: Session, contact: Contact) -> dict[int, str]: - """Build a mapping of contact IDs to names for relationship display.""" - related_ids = {rel.related_contact_id for rel in contact.related_to} - related_ids |= {rel.contact_id for rel in contact.related_from} - related_ids.discard(contact.id) - - if not related_ids: - return {} - - related_contacts = list(database.scalars(select(Contact).where(Contact.id.in_(related_ids))).all()) - return {related.id: related.name for related in related_contacts} - - -def _get_relationship_type_display() -> dict[str, str]: - """Build a mapping of relationship type values to display names.""" - return {rel_type.value: rel_type.display_name for rel_type in RelationshipType} - - -async def _parse_contact_form(request: Request) -> dict[str, Any]: - """Parse contact form data from a multipart/form request.""" - form_data = await request.form() - result: dict[str, Any] = {} - - for field in CONTACT_STRING_FIELDS: - value = form_data.get(field, "") - result[field] = str(value) if value else None - - for field in CONTACT_INT_FIELDS: - value = form_data.get(field, "") - result[field] = int(value) if value else None - - result["need_ids"] = [int(value) for value in form_data.getlist("need_ids")] - return result - - -def _save_contact_from_form(database: Session, contact: Contact, form_result: dict[str, Any]) -> None: - """Apply parsed form data to a Contact and save associated needs.""" - need_ids = form_result.pop("need_ids") - - for key, value in form_result.items(): - setattr(contact, key, value) - - if need_ids: - contact.needs = list(database.scalars(select(Need).where(Need.id.in_(need_ids))).all()) - else: - contact.needs = [] - - -@router.get("/", response_class=HTMLResponse) -@router.get("/contacts", response_class=HTMLResponse) -def contact_list_page(request: Request, database: DbSession) -> HTMLResponse: - """Render the contacts list page.""" - contacts = list(database.scalars(select(Contact)).all()) - return templates.TemplateResponse(request, "contact_list.html", {"contacts": contacts}) - - -@router.get("/contacts/new", response_class=HTMLResponse) -def new_contact_page(request: Request, database: DbSession) -> HTMLResponse: - """Render the new contact form page.""" - all_needs = list(database.scalars(select(Need)).all()) - return templates.TemplateResponse(request, "contact_form.html", {"contact": None, "all_needs": all_needs}) - - -@router.post("/htmx/contacts/new") -async def create_contact_form(request: Request, database: DbSession) -> RedirectResponse: - """Handle the create contact form submission.""" - form_result = await _parse_contact_form(request) - contact = Contact() - _save_contact_from_form(database, contact, form_result) - - database.add(contact) - database.commit() - database.refresh(contact) - return RedirectResponse(url=f"/contacts/{contact.id}", status_code=303) - - -@router.get("/contacts/{contact_id}", response_class=HTMLResponse) -def contact_detail_page(contact_id: int, request: Request, database: DbSession) -> HTMLResponse: - """Render the contact detail page.""" - contact = database.scalar( - select(Contact) - .where(Contact.id == contact_id) - .options( - selectinload(Contact.needs), - selectinload(Contact.related_to), - selectinload(Contact.related_from), - ) - ) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - contact_names = _build_contact_name_map(database, contact) - grouped_relationships = _group_relationships(contact.related_to) - all_contacts = list(database.scalars(select(Contact)).all()) - all_needs = list(database.scalars(select(Need)).all()) - available_needs = [need for need in all_needs if need not in contact.needs] - - return templates.TemplateResponse( - request, - "contact_detail.html", - { - "contact": contact, - "contact_names": contact_names, - "grouped_relationships": grouped_relationships, - "all_contacts": all_contacts, - "available_needs": available_needs, - "relationship_types": list(RelationshipType), - }, - ) - - -@router.get("/contacts/{contact_id}/edit", response_class=HTMLResponse) -def edit_contact_page(contact_id: int, request: Request, database: DbSession) -> HTMLResponse: - """Render the edit contact form page.""" - contact = database.scalar(select(Contact).where(Contact.id == contact_id).options(selectinload(Contact.needs))) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - all_needs = list(database.scalars(select(Need)).all()) - return templates.TemplateResponse(request, "contact_form.html", {"contact": contact, "all_needs": all_needs}) - - -@router.post("/htmx/contacts/{contact_id}/edit") -async def update_contact_form(contact_id: int, request: Request, database: DbSession) -> RedirectResponse: - """Handle the edit contact form submission.""" - contact = database.get(Contact, contact_id) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - form_result = await _parse_contact_form(request) - _save_contact_from_form(database, contact, form_result) - - database.commit() - return RedirectResponse(url=f"/contacts/{contact_id}", status_code=303) - - -@router.post("/htmx/contacts/{contact_id}/add-need", response_class=HTMLResponse) -def add_need_to_contact_htmx( - contact_id: int, - request: Request, - database: DbSession, - need_id: Annotated[int, Form()], -) -> HTMLResponse: - """Add a need to a contact and return updated manage-needs partial.""" - contact = database.scalar(select(Contact).where(Contact.id == contact_id).options(selectinload(Contact.needs))) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - need = database.get(Need, need_id) - if not need: - raise HTTPException(status_code=404, detail="Need not found") - - if need not in contact.needs: - contact.needs.append(need) - database.commit() - database.refresh(contact) - - return templates.TemplateResponse(request, "partials/manage_needs.html", {"contact": contact}) - - -@router.post("/htmx/contacts/{contact_id}/add-relationship", response_class=HTMLResponse) -def add_relationship_htmx( - contact_id: int, - request: Request, - database: DbSession, - related_contact_id: Annotated[int, Form()], - relationship_type: Annotated[str, Form()], -) -> HTMLResponse: - """Add a relationship and return updated manage-relationships partial.""" - contact = database.scalar(select(Contact).where(Contact.id == contact_id).options(selectinload(Contact.related_to))) - if not contact: - raise HTTPException(status_code=404, detail="Contact not found") - - related_contact = database.get(Contact, related_contact_id) - if not related_contact: - raise HTTPException(status_code=404, detail="Related contact not found") - - rel_type = RelationshipType(relationship_type) - weight = rel_type.default_weight - - relationship = ContactRelationship( - contact_id=contact_id, - related_contact_id=related_contact_id, - relationship_type=relationship_type, - closeness_weight=weight, - ) - database.add(relationship) - database.commit() - database.refresh(contact) - - contact_names = _build_contact_name_map(database, contact) - return templates.TemplateResponse( - request, - "partials/manage_relationships.html", - {"contact": contact, "contact_names": contact_names}, - ) - - -@router.post("/htmx/contacts/{contact_id}/relationships/{related_contact_id}/weight") -def update_relationship_weight_htmx( - contact_id: int, - related_contact_id: int, - database: DbSession, - closeness_weight: Annotated[int, Form()], -) -> HTMLResponse: - """Update a relationship's closeness weight from HTMX range input.""" - relationship = database.scalar( - select(ContactRelationship).where( - ContactRelationship.contact_id == contact_id, - ContactRelationship.related_contact_id == related_contact_id, - ) - ) - if not relationship: - raise HTTPException(status_code=404, detail="Relationship not found") - - relationship.closeness_weight = closeness_weight - database.commit() - return HTMLResponse("") - - -@router.post("/htmx/needs", response_class=HTMLResponse) -def create_need_htmx( - request: Request, - database: DbSession, - name: Annotated[str, Form()], - description: Annotated[str, Form()] = "", -) -> HTMLResponse: - """Create a need via form data and return updated needs list.""" - need = Need(name=name, description=description or None) - database.add(need) - database.commit() - needs = list(database.scalars(select(Need)).all()) - return templates.TemplateResponse(request, "partials/need_items.html", {"needs": needs}) - - -@router.get("/needs", response_class=HTMLResponse) -def needs_page(request: Request, database: DbSession) -> HTMLResponse: - """Render the needs list page.""" - needs = list(database.scalars(select(Need)).all()) - return templates.TemplateResponse(request, "need_list.html", {"needs": needs}) - - -@router.get("/graph", response_class=HTMLResponse) -def graph_page(request: Request, database: DbSession) -> HTMLResponse: - """Render the relationship graph page.""" - contacts = list(database.scalars(select(Contact)).all()) - relationships = list(database.scalars(select(ContactRelationship)).all()) - - graph_data = { - "nodes": [{"id": contact.id, "name": contact.name, "current_job": contact.current_job} for contact in contacts], - "edges": [ - { - "source": rel.contact_id, - "target": rel.related_contact_id, - "relationship_type": rel.relationship_type, - "closeness_weight": rel.closeness_weight, - } - for rel in relationships - ], - } - - return templates.TemplateResponse( - request, - "graph.html", - { - "graph_data": graph_data, - "relationship_type_display": _get_relationship_type_display(), - }, - ) diff --git a/python/api/templates/base.html b/python/api/templates/base.html deleted file mode 100644 index 649e1e9..0000000 --- a/python/api/templates/base.html +++ /dev/null @@ -1,198 +0,0 @@ - - -
- - -Drag nodes to reposition. Closer relationships have shorter, darker edges.
- - -| Name | -Job | -Timezone | -Actions | -
|---|---|---|---|
| {{ contact.name }} | -{{ contact.current_job or "-" }} | -{{ contact.timezone or "-" }} | -- Edit - - | -
No contacts yet.
-{% endif %} diff --git a/python/api/templates/partials/manage_needs.html b/python/api/templates/partials/manage_needs.html deleted file mode 100644 index f1a4b69..0000000 --- a/python/api/templates/partials/manage_needs.html +++ /dev/null @@ -1,14 +0,0 @@ -{{ need.description }}
{% endif %} -No needs defined yet.
-{% endif %} diff --git a/systems/jeeves/services/contact_api.nix b/systems/jeeves/services/contact_api.nix deleted file mode 100644 index 0762591..0000000 --- a/systems/jeeves/services/contact_api.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ - pkgs, - inputs, - ... -}: -{ - networking.firewall.allowedTCPPorts = [ - 8069 - ]; - systemd.services.contact-api = { - description = "Contact Database API"; - after = [ - "postgresql.service" - "network.target" - ]; - requires = [ "postgresql.service" ]; - wantedBy = [ "multi-user.target" ]; - - environment = { - PYTHONPATH = "${inputs.self}"; - POSTGRES_DB = "richie"; - POSTGRES_HOST = "/run/postgresql"; - POSTGRES_USER = "richie"; - POSTGRES_PORT = "5432"; - }; - - serviceConfig = { - Type = "simple"; - ExecStart = "${pkgs.my_python}/bin/python -m python.api.main --host 192.168.90.40 --port 8069"; - Restart = "on-failure"; - RestartSec = "5s"; - StandardOutput = "journal"; - StandardError = "journal"; - NoNewPrivileges = true; - ProtectSystem = "strict"; - ProtectHome = "read-only"; - PrivateTmp = true; - ReadOnlyPaths = [ - "${inputs.self}" - ]; - }; - }; -}