# Vein Patterns This document describes the patterns that emerged from building Jira, Slack, and Google veins side-by-side. ## Philosophy **Core = Isolated API client** - Can run without FastAPI, framework-agnostic **Vein = Corset/wrapper** - Makes the core speak to pawprint ecosystem The vein wrapper is not a literal folder - it's following structural conventions and patterns. ## Directory Structure (Standard) ``` vein/{service}/ ├── core/ # ISOLATED - can run standalone │ ├── __init__.py │ ├── config.py # Pydantic settings from .env │ ├── auth.py # Auth logic (optional, for complex auth) │ ├── client.py # Main API client │ └── {domain}.py # Additional clients (sheets, drive, etc.) │ ├── api/ # WRAPPER - FastAPI integration │ ├── __init__.py │ └── routes.py # APIRouter with endpoints │ ├── models/ # Data models │ ├── __init__.py │ ├── {domain}.py # Pydantic models with from_{service}() │ └── formatter.py # Text formatters for LLM output │ ├── storage/ # Persistent data (optional, for OAuth tokens) │ └── .gitignore │ ├── main.py # FastAPI app setup ├── run.py # Standalone runner ├── requirements.txt # Dependencies ├── .env.example # Configuration template └── README.md # Service-specific docs ``` ## Base Classes ### `BaseVein` (vein/base.py) Minimal interface for all veins: - `name: str` - Service name - `get_client(creds) -> Client` - Create API client - `health_check(creds) -> dict` - Test connection Used for simple token-based auth (Jira, Slack, WhatsApp). ### `BaseOAuthVein` (vein/oauth.py) Extends BaseVein for OAuth2 services: - `get_auth_url(state) -> str` - Generate OAuth URL - `exchange_code(code) -> dict` - Code for tokens - `refresh_token(refresh_token) -> dict` - Refresh expired tokens - `storage: TokenStorage` - Token persistence Used for OAuth2 services (Google, GitHub, GitLab). ### `TokenStorage` (vein/oauth.py) File-based token storage (can be overridden for Redis/DB): - `save_tokens(user_id, tokens)` - Persist tokens - `load_tokens(user_id) -> dict` - Retrieve tokens - `is_expired(tokens) -> bool` - Check expiration - `delete_tokens(user_id)` - Logout ## Core Module Patterns ### config.py ```python from pathlib import Path from pydantic_settings import BaseSettings ENV_FILE = Path(__file__).parent.parent / ".env" class {Service}Config(BaseSettings): # Service-specific settings api_port: int = 800X # Unique port per vein model_config = { "env_file": ENV_FILE, "env_file_encoding": "utf-8", } settings = {Service}Config() ``` **Pattern**: Pydantic BaseSettings with .env file at vein root. ### client.py (Simple Auth - Jira, Slack) ```python from {sdk} import Client def get_client(credentials) -> Client: """Create authenticated client.""" return Client(credentials) ``` **Pattern**: Simple factory function returning SDK client. ### oauth.py (OAuth2 - Google) ```python class {Service}OAuth: """OAuth2 client.""" def get_authorization_url(self, state=None) -> str: """Generate auth URL for user redirect.""" def exchange_code_for_tokens(self, code: str) -> dict: """Exchange code for tokens.""" def refresh_access_token(self, refresh_token: str) -> dict: """Refresh expired token.""" def get_credentials(self, access_token, refresh_token=None): """Create SDK credentials from tokens.""" ``` **Pattern**: OAuth client handles full flow, separate from API client. ## API Module Patterns ### routes.py ```python from fastapi import APIRouter, HTTPException, Query from fastapi.responses import PlainTextResponse router = APIRouter() @router.get("/health") async def health(creds = Depends(get_credentials)): """Test connection.""" return {"status": "ok", "user": "..."} @router.get("/resource") async def get_resource(text: bool = False): """Get resource data.""" # ... fetch data return _maybe_text(data, text, formatter) ``` **Standard endpoints**: - `/health` - Connection test (required) - Service-specific resources - `?text=true` query param for text output ### Helper Functions ```python def _maybe_text(data, text: bool, formatter): """Return text or JSON based on query param.""" if not text: return data return PlainTextResponse(formatter(data)) ``` **Pattern**: Consistent text/JSON toggle across all veins. ## Model Patterns ### Domain Models ```python from pydantic import BaseModel class Resource(BaseModel): id: str name: str # ... fields @classmethod def from_{service}(cls, raw: dict) -> "Resource": """Parse from service API response.""" return cls( id=raw["id"], name=raw["name"], # ... mapping ) ``` **Pattern**: Pydantic models with `from_{service}()` factory methods. ### Formatters ```python def format_{resource}(resource: Resource) -> str: """Format resource as text (LLM-friendly).""" return f"{resource.name} (ID: {resource.id})" ``` **Pattern**: Simple functions returning plain text, no fancy tables. ## Authentication Patterns ### Simple Token Auth (Jira, Slack, WhatsApp) **Headers or .env fallback**: ```python async def get_{service}_credentials( x_{service}_token: str | None = Header(None), ) -> Credentials: # Use header if provided if x_{service}_token and x_{service}_token.strip(): return Credentials(token=x_{service}_token.strip()) # Fall back to .env if settings.{service}_token: return Credentials(token=settings.{service}_token) raise HTTPException(401, "Missing credentials") ``` **Pattern**: Per-request headers for web UI, .env for standalone/API use. ### OAuth2 (Google, GitHub, GitLab) **Three-step flow**: 1. **Start**: `GET /oauth/start` → Redirect to service 2. **Callback**: `GET /oauth/callback?code=...` → Exchange code, save tokens 3. **Use**: Load tokens from storage, auto-refresh if expired **Pattern**: Stateful (requires token storage), user must complete browser flow. ## Error Handling ```python try: client = get_client(creds) data = client.fetch_something() return data except {Service}ClientError as e: raise HTTPException(500, str(e)) except Exception as e: raise HTTPException(500, f"Unexpected error: {e}") ``` **Pattern**: Catch service-specific errors first, then generic fallback. ## Configuration Files ### .env.example Include all required settings with placeholder values: ```dotenv # Service credentials SERVICE_API_KEY=your_key_here SERVICE_URL=https://api.service.com # Vein config API_PORT=8001 ``` ### requirements.txt Minimal dependencies: ```txt fastapi>=0.104.0 uvicorn>=0.24.0 pydantic>=2.0.0 pydantic-settings>=2.0.0 {service-specific-sdk}>=X.Y.Z ``` ## Main App Pattern ```python from fastapi import FastAPI from api.routes import router from core.config import settings app = FastAPI(title="{Service} Vein", version="0.1.0") app.include_router(router) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=settings.api_port) ``` **Pattern**: Simple FastAPI app, routes included at root or with prefix. ## Testing Isolation Because `core/` is isolated from FastAPI: ```python # Can test core directly without HTTP from vein.google.core.sheets import GoogleSheetsClient def test_sheets_client(): client = GoogleSheetsClient(mock_credentials) data = client.get_sheet_values("sheet_id", "A1:D10") assert len(data) > 0 ``` **Pattern**: Core modules are testable without spinning up FastAPI. ## Port Allocation - **8001**: Jira - **8002**: Slack - **8003**: Google - **8004**: WhatsApp (planned) - **8005+**: Future veins **Pattern**: Sequential ports starting from 8001. ## Vein Types ### Type 1: Simple Token Auth **Examples**: Jira, Slack, WhatsApp **Auth**: Token in headers or .env **Stateless**: No storage needed **Inherits**: BaseVein ### Type 2: OAuth2 **Examples**: Google, GitHub, GitLab **Auth**: OAuth2 flow with callback **Stateful**: Requires token storage **Inherits**: BaseOAuthVein ### Type 3: Hybrid (Future) **Examples**: Services with webhooks + API **May need**: Database, Redis, webhook endpoints **Consider**: Pulse instead of vein (composed service) ## When to Use Pulse vs Vein **Vein**: Pure connector - Stateless or minimal state (OAuth tokens) - Pull-based (you call the API) - No database required **Pulse**: Composed service - Stateful (database, message queue) - Push-based (webhooks, real-time) - Combines vein + storage + processing **Example**: WhatsApp webhook receiver = pulse, WhatsApp API client = vein. ## Standardization Checklist When creating a new vein: - [ ] Follow directory structure (core/, api/, models/) - [ ] Create .env.example with all settings - [ ] Implement /health endpoint - [ ] Support ?text=true for all data endpoints - [ ] Use from_{service}() factory methods in models - [ ] Create text formatters in models/formatter.py - [ ] Include README.md with setup instructions - [ ] Choose correct base class (BaseVein or BaseOAuthVein) - [ ] Allocate unique port (8001+) - [ ] Keep core/ isolated from FastAPI ## Evolution This document captures patterns as of having 3 veins (Jira, Slack, Google). **Do not** enforce these patterns rigidly - they should evolve as we build more veins. **Do** use this as a starting point for consistency. **Do** update this document when patterns change. The abstract classes exist to enforce interfaces, not implementations. The patterns exist to reduce cognitive load, not to restrict flexibility.