From 9e5cbbad1f2cc9203a5240edfa4e1f0e55c9a57d Mon Sep 17 00:00:00 2001 From: buenosairesam Date: Fri, 2 Jan 2026 17:09:58 -0300 Subject: [PATCH] refactor: separate standalone and managed room configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - veins → shunts rename - add cfg/standalone/ and cfg// structure - remove old data/*.json (moved to cfg//data/) - update build.py and ctrl scripts --- .woodpecker.yml | 28 ++ README.md | 93 ++++-- artery/index.html | 9 +- artery/shunts/amar/.env.example | 13 + artery/shunts/amar/README.md | 173 ++++++++++ artery/shunts/amar/__init__.py | 1 + artery/shunts/amar/api/__init__.py | 1 + artery/shunts/amar/api/routes.py | 302 ++++++++++++++++++ artery/shunts/amar/core/__init__.py | 1 + artery/shunts/amar/core/config.py | 27 ++ artery/shunts/amar/main.py | 40 +++ .../amar}/requirements.txt | 0 artery/shunts/amar/run.py | 18 ++ artery/shunts/amar/templates/index.html | 298 +++++++++++++++++ .../mercadopago/.env.example | 0 .../{veins => shunts}/mercadopago/README.md | 0 .../{veins => shunts}/mercadopago/__init__.py | 0 .../mercadopago/api/__init__.py | 0 .../mercadopago/api/routes.py | 0 .../mercadopago/core/__init__.py | 0 .../mercadopago/core/config.py | 0 artery/{veins => shunts}/mercadopago/main.py | 0 artery/shunts/mercadopago/requirements.txt | 4 + artery/{veins => shunts}/mercadopago/run.py | 0 .../mercadopago/templates/index.html | 0 build.py | 172 +++++----- .../config.json} | 14 +- {data => cfg/amar/data}/__init__.py | 0 {data => cfg/amar/data}/books.json | 0 {data => cfg/amar/data}/depots.json | 0 {data => cfg/amar/data}/desks.json | 0 {data => cfg/amar/data}/monitors.json | 0 {data => cfg/amar/data}/pulses.json | 0 {data => cfg/amar/data}/rooms.json | 0 {data => cfg/amar/data}/tables.json | 0 .../data}/template/feature-form/template.md | 0 {data => cfg/amar/data}/templates.json | 0 {data => cfg/amar/data}/tools.json | 0 cfg/amar/data/veins.json | 60 ++++ cfg/standalone/config.json | 140 ++++++++ cfg/standalone/data/__init__.py | 156 +++++++++ cfg/standalone/data/books.json | 79 +++++ cfg/standalone/data/depots.json | 12 + cfg/standalone/data/desks.json | 3 + cfg/standalone/data/monitors.json | 22 ++ cfg/standalone/data/pulses.json | 3 + cfg/standalone/data/rooms.json | 5 + cfg/standalone/data/tables.json | 3 + .../data/template/feature-form/template.md | 38 +++ cfg/standalone/data/templates.json | 12 + cfg/standalone/data/tools.json | 48 +++ cfg/standalone/data/veins.json | 60 ++++ ctrl/build.sh | 20 +- ctrl/logs.sh | 26 +- ctrl/start.sh | 28 +- ctrl/stop.sh | 15 +- data/veins.json | 14 - 57 files changed, 1788 insertions(+), 150 deletions(-) create mode 100644 .woodpecker.yml create mode 100644 artery/shunts/amar/.env.example create mode 100644 artery/shunts/amar/README.md create mode 100644 artery/shunts/amar/__init__.py create mode 100644 artery/shunts/amar/api/__init__.py create mode 100644 artery/shunts/amar/api/routes.py create mode 100644 artery/shunts/amar/core/__init__.py create mode 100644 artery/shunts/amar/core/config.py create mode 100644 artery/shunts/amar/main.py rename artery/{veins/mercadopago => shunts/amar}/requirements.txt (100%) create mode 100644 artery/shunts/amar/run.py create mode 100644 artery/shunts/amar/templates/index.html rename artery/{veins => shunts}/mercadopago/.env.example (100%) rename artery/{veins => shunts}/mercadopago/README.md (100%) rename artery/{veins => shunts}/mercadopago/__init__.py (100%) rename artery/{veins => shunts}/mercadopago/api/__init__.py (100%) rename artery/{veins => shunts}/mercadopago/api/routes.py (100%) rename artery/{veins => shunts}/mercadopago/core/__init__.py (100%) rename artery/{veins => shunts}/mercadopago/core/config.py (100%) rename artery/{veins => shunts}/mercadopago/main.py (100%) create mode 100644 artery/shunts/mercadopago/requirements.txt rename artery/{veins => shunts}/mercadopago/run.py (100%) rename artery/{veins => shunts}/mercadopago/templates/index.html (100%) rename cfg/{soleprint.config.json => amar/config.json} (88%) rename {data => cfg/amar/data}/__init__.py (100%) rename {data => cfg/amar/data}/books.json (100%) rename {data => cfg/amar/data}/depots.json (100%) rename {data => cfg/amar/data}/desks.json (100%) rename {data => cfg/amar/data}/monitors.json (100%) rename {data => cfg/amar/data}/pulses.json (100%) rename {data => cfg/amar/data}/rooms.json (100%) rename {data => cfg/amar/data}/tables.json (100%) rename {data => cfg/amar/data}/template/feature-form/template.md (100%) rename {data => cfg/amar/data}/templates.json (100%) rename {data => cfg/amar/data}/tools.json (100%) create mode 100644 cfg/amar/data/veins.json create mode 100644 cfg/standalone/config.json create mode 100644 cfg/standalone/data/__init__.py create mode 100644 cfg/standalone/data/books.json create mode 100644 cfg/standalone/data/depots.json create mode 100644 cfg/standalone/data/desks.json create mode 100644 cfg/standalone/data/monitors.json create mode 100644 cfg/standalone/data/pulses.json create mode 100644 cfg/standalone/data/rooms.json create mode 100644 cfg/standalone/data/tables.json create mode 100644 cfg/standalone/data/template/feature-form/template.md create mode 100644 cfg/standalone/data/templates.json create mode 100644 cfg/standalone/data/tools.json create mode 100644 cfg/standalone/data/veins.json delete mode 100644 data/veins.json diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..76a3c7d --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,28 @@ +# Generic woodpecker trigger +# Copy to any repo as .woodpecker.yml +# Runs matching pipeline from ppl/pipelines// + +when: + event: push + branch: main + +steps: + - name: build + image: alpine + commands: + - apk add --no-cache git openssh-client + - mkdir -p ~/.ssh + - echo "$SSH_KEY" | base64 -d > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - ssh -o StrictHostKeyChecking=no mariano@mcrn.ar "cd ~/ppl && ./ctrl/run-pipeline.sh ${CI_REPO_NAME} build" + secrets: [ssh_key] + + - name: deploy + image: alpine + commands: + - apk add --no-cache openssh-client + - mkdir -p ~/.ssh + - echo "$SSH_KEY" | base64 -d > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - ssh -o StrictHostKeyChecking=no mariano@mcrn.ar "cd ~/ppl && ./ctrl/run-pipeline.sh ${CI_REPO_NAME} deploy" + secrets: [ssh_key] diff --git a/README.md b/README.md index 233b597..e138646 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,64 @@ Development workflow platform. Run, test, and document everything in one place. ## Quick Start ```bash -# Build +# Build standalone +python build.py dev +cd gen/standalone && .venv/bin/python run.py + +# Build with room config python build.py dev --cfg amar +cd gen/amar && .venv/bin/python run.py -# Run standalone (Docker) -./ctrl/start.sh - -# Or bare-metal -cd gen && .venv/bin/python run.py # Visit http://localhost:12000 ``` +## Commands + +```bash +# Build +python build.py dev # → gen/standalone/ +python build.py dev --cfg amar # → gen/amar/ +python build.py dev --all # → both + +# ctrl scripts +./ctrl/build.sh # Build standalone +./ctrl/build.sh amar # Build amar +./ctrl/build.sh --all # Build all + +./ctrl/start.sh # Start standalone (Docker) +./ctrl/start.sh amar # Start amar +./ctrl/start.sh -d # Detached +./ctrl/stop.sh # Stop +./ctrl/logs.sh # View logs +``` + +## Adding a New Managed Room + +1. Create room config directory: +```bash +mkdir -p cfg/clientx +``` + +2. Add required files: +``` +cfg/clientx/ +├── .env.example # Environment template +├── docker-compose.yml # Room services (optional) +├── databrowse/depot/ # Database schemas (optional) +├── tester/tests/ # Room-specific tests (optional) +├── monitors/ # Room-specific monitors (optional) +├── models/ # Room-specific models (optional) +└── data/ # Room-specific data files (optional) + ├── depots.json # Doc depots for Atlas + └── shunts.json # Mock connectors +``` + +3. Build and run: +```bash +python build.py dev --cfg clientx +./ctrl/start.sh clientx +``` + ## Systems | | System | What it does | @@ -34,28 +81,30 @@ spr/ ├── build.py # Build tool ├── cfg/ # Room configurations │ ├── soleprint.config.json -│ └── amar/ # AMAR room (docker-compose, tests, models) -├── ctrl/ # Standalone Docker scripts +│ └── amar/ # AMAR room config +├── ctrl/ # Docker scripts │ ├── artery/ # Connectors │ ├── veins/ # Jira, Slack, Google │ ├── shunts/ # Fake connectors for testing -│ └── plexuses/ # Full apps (backend + frontend) +│ └── plexus/ # Full apps (backend + frontend) │ ├── atlas/ # Documentation -│ └── book/ # Gherkin samples, feature docs +│ └── books/ # Soleprint docs only │ ├── station/ # Tools & monitors │ ├── tools/ # modelgen, tester, datagen │ └── monitors/ # databrowse │ ├── soleprint/ # Core (versioned) -├── gen/ # Built instance (gitignored) +│ +├── gen/ # Built instances (gitignored) +│ ├── standalone/ # Base soleprint +│ └── amar/ # With amar config │ └── mainroom/ # Orchestration with managed room ├── amar -> cfg/amar ├── soleprint/ # Soleprint Docker config - ├── sbwrapper/ # Sidebar wrapper UI └── ctrl/ # start, stop, deploy scripts ``` @@ -71,26 +120,6 @@ Vein ──► Pulse ──► Plexus Shunt ── Fake connector for testing ``` -## Usage - -### Standalone (soleprint only) -```bash -python build.py dev -./ctrl/start.sh # Docker -./ctrl/stop.sh -``` - -### With Managed Room (amar + soleprint) -```bash -python build.py dev --cfg amar -docker network create soleprint_network - -cd mainroom/ctrl -./start.sh -d # Start detached -./stop.sh # Stop all -./deploy.sh # Deploy to AWS -``` - ## Ports | Service | Port | diff --git a/artery/index.html b/artery/index.html index e57e61c..479e01d 100644 --- a/artery/index.html +++ b/artery/index.html @@ -428,14 +428,7 @@

{{ vein.title }}

diff --git a/artery/shunts/amar/.env.example b/artery/shunts/amar/.env.example new file mode 100644 index 0000000..2e61175 --- /dev/null +++ b/artery/shunts/amar/.env.example @@ -0,0 +1,13 @@ +# Amar (MOCK) Vein Configuration +API_PORT=8005 + +# Mock data settings +MOCK_DATA_PATH=./mock_data + +# Mock behavior +ENABLE_RANDOM_DELAYS=true +MIN_DELAY_MS=100 +MAX_DELAY_MS=500 + +# Simulate errors +ERROR_RATE=0.0 # 0.0 to 1.0 (0% to 100%) diff --git a/artery/shunts/amar/README.md b/artery/shunts/amar/README.md new file mode 100644 index 0000000..e027a15 --- /dev/null +++ b/artery/shunts/amar/README.md @@ -0,0 +1,173 @@ +# Amar (MOCK) Vein + +Mock Amar API for testing - returns predictable test data for turnero flow testing. + +## Purpose + +Enables testing of the turnero flow (VET-535-540) without hitting the real Amar backend: +- Guest petowner creation (VET-536) +- Pet creation (VET-537) +- Cart creation and price calculation (VET-538) +- Service/category filtering (VET-539, VET-540) +- Service request creation + +## Quick Start + +```bash +# Install dependencies +pip install -r requirements.txt + +# Start the mock server +python run.py + +# API docs: http://localhost:8005/docs +# Health check: http://localhost:8005/health +``` + +## Configuration + +Copy `.env.example` to `.env` and adjust: + +```bash +API_PORT=8005 # Server port +ENABLE_RANDOM_DELAYS=true # Add realistic delays +MIN_DELAY_MS=100 # Minimum delay +MAX_DELAY_MS=500 # Maximum delay +ERROR_RATE=0.0 # Error rate (0.0 to 1.0) +``` + +## API Endpoints + +### Turnero Flow (VET-536-540) + +```bash +# Create guest petowner (VET-536) +POST /api/v1/pet-owners/ +{ + "address": "Av. Santa Fe 1234, Palermo" +} + +# Create pet (VET-537) +POST /api/v1/pets/ +{ + "owner_id": 1234, + "name": "Luna", + "species": "DOG", + "age": 3, + "age_unit": "years" +} + +# Create cart (VET-538) +POST /api/v1/cart/ +{ + "owner_id": 1234 +} + +# Add item to cart +POST /api/v1/cart/{cart_id}/items/ +{ + "service_id": 1, + "pet_id": 5678, + "quantity": 1 +} + +# List services (VET-540) +GET /api/v1/services/?species=DOG&neighborhood_id=1 + +# List categories (VET-539) +GET /api/v1/categories/?species=DOG&neighborhood_id=1 + +# Create service request +POST /solicitudes/service-requests/?cart_id=12345 +``` + +### Mock Control + +```bash +# Get mock database stats +GET /mock/stats + +# Reset mock database +GET /mock/reset +``` + +## Response Format + +All responses include `_mock: true` to identify mock data: + +```json +{ + "id": 1234, + "address": "Av. Santa Fe 1234, Palermo", + "is_guest": true, + "_mock": true +} +``` + +## Testing Scenarios + +### Complete Turnero Flow + +```python +import requests + +BASE_URL = "http://localhost:8005" + +# Step 1: Create guest petowner +owner_resp = requests.post(f"{BASE_URL}/api/v1/pet-owners/", json={ + "address": "Av. Santa Fe 1234, Palermo" +}) +owner = owner_resp.json() + +# Step 2: Create pet +pet_resp = requests.post(f"{BASE_URL}/api/v1/pets/", json={ + "owner_id": owner["id"], + "name": "Luna", + "species": "DOG", + "age": 3, + "age_unit": "years" +}) +pet = pet_resp.json() + +# Step 3: Create cart +cart_resp = requests.post(f"{BASE_URL}/api/v1/cart/", json={ + "owner_id": owner["id"] +}) +cart = cart_resp.json() + +# Step 4: Get available services +services_resp = requests.get( + f"{BASE_URL}/api/v1/services/", + params={"species": "DOG", "neighborhood_id": owner["neighborhood"]["id"]} +) +services = services_resp.json() + +# Step 5: Add service to cart +cart_resp = requests.post(f"{BASE_URL}/api/v1/cart/{cart['id']}/items/", json={ + "service_id": services[0]["id"], + "pet_id": pet["id"], + "quantity": 1 +}) +cart = cart_resp.json() +print(f"Cart total: ${cart['resume']['total']}") + +# Step 6: Create service request +request_resp = requests.post( + f"{BASE_URL}/solicitudes/service-requests/", + params={"cart_id": cart["id"]} +) +service_request = request_resp.json() +print(f"Service request created: {service_request['id']}") +``` + +## Data Generator + +This vein uses the independent `datagen` tool from `ward/tools/datagen/amar.py`. +See `ward/tools/datagen/README.md` for data generation details. + +## Notes + +- Mock database is in-memory (resets on server restart) +- Use `/mock/reset` to clear data during testing +- All IDs are randomly generated on creation +- Multi-pet discounts are automatically calculated diff --git a/artery/shunts/amar/__init__.py b/artery/shunts/amar/__init__.py new file mode 100644 index 0000000..54f82fb --- /dev/null +++ b/artery/shunts/amar/__init__.py @@ -0,0 +1 @@ +"""Amar (MOCK) vein - Mock Amar API for testing.""" diff --git a/artery/shunts/amar/api/__init__.py b/artery/shunts/amar/api/__init__.py new file mode 100644 index 0000000..9430ca4 --- /dev/null +++ b/artery/shunts/amar/api/__init__.py @@ -0,0 +1 @@ +"""API routes for Amar mock vein.""" diff --git a/artery/shunts/amar/api/routes.py b/artery/shunts/amar/api/routes.py new file mode 100644 index 0000000..9c74eac --- /dev/null +++ b/artery/shunts/amar/api/routes.py @@ -0,0 +1,302 @@ +"""API routes for Amar (MOCK) vein - Mock Amar API for testing.""" + +import asyncio +import random +from fastapi import APIRouter, HTTPException, Query +from fastapi.responses import JSONResponse +from typing import Optional, List, Dict, Any +from pydantic import BaseModel + +# Import datagen from ward/tools +import sys +from pathlib import Path +ward_tools_path = Path(__file__).parent.parent.parent.parent.parent / "ward" / "tools" +sys.path.insert(0, str(ward_tools_path)) + +from datagen.amar import AmarDataGenerator + +from ..core.config import settings + +router = APIRouter() + +# In-memory storage for mock data (reset on restart) +MOCK_DB = { + "petowners": {}, + "pets": {}, + "carts": {}, + "service_requests": {}, +} + + +# Request/Response Models +class CreatePetOwnerRequest(BaseModel): + address: str + email: Optional[str] = None + phone: Optional[str] = None + + +class CreatePetRequest(BaseModel): + owner_id: int + name: str + species: str # DOG, CAT + age: Optional[int] = None + age_unit: str = "years" # years, months + + +class CreateCartRequest(BaseModel): + owner_id: int + + +class AddCartItemRequest(BaseModel): + service_id: int + pet_id: int + quantity: int = 1 + + +async def _mock_delay(): + """Add realistic delay if enabled.""" + if settings.enable_random_delays: + delay_ms = random.randint(settings.min_delay_ms, settings.max_delay_ms) + await asyncio.sleep(delay_ms / 1000) + + +def _maybe_error(): + """Randomly raise an error based on error_rate.""" + if random.random() < settings.error_rate: + raise HTTPException(500, "Mock error: Simulated failure") + + +@router.get("/health") +async def health(): + """Health check endpoint.""" + return { + "status": "ok", + "vein": "Amar\n(MOCK)", + "message": "Mock Amar API for testing", + "_mock": True, + } + + +@router.post("/api/v1/pet-owners/") +async def create_petowner(request: CreatePetOwnerRequest): + """Create a guest petowner (VET-536).""" + await _mock_delay() + _maybe_error() + + owner = AmarDataGenerator.petowner( + address=request.address, + is_guest=True, + email=request.email, + phone=request.phone, + ) + + # Store in mock DB + MOCK_DB["petowners"][owner["id"]] = owner + + owner["_mock"] = True + return owner + + +@router.get("/api/v1/pet-owners/{owner_id}") +async def get_petowner(owner_id: int): + """Get petowner by ID.""" + await _mock_delay() + _maybe_error() + + owner = MOCK_DB["petowners"].get(owner_id) + if not owner: + raise HTTPException(404, f"PetOwner {owner_id} not found") + + return owner + + +@router.post("/api/v1/pets/") +async def create_pet(request: CreatePetRequest): + """Create a pet (VET-537).""" + await _mock_delay() + _maybe_error() + + # Verify owner exists + if request.owner_id not in MOCK_DB["petowners"]: + raise HTTPException(404, f"Owner {request.owner_id} not found") + + pet = AmarDataGenerator.pet( + owner_id=request.owner_id, + name=request.name, + species=request.species, + age_value=request.age, + age_unit=request.age_unit, + ) + + # Store in mock DB + MOCK_DB["pets"][pet["id"]] = pet + + pet["_mock"] = True + return pet + + +@router.get("/api/v1/pets/{pet_id}") +async def get_pet(pet_id: int): + """Get pet by ID.""" + await _mock_delay() + _maybe_error() + + pet = MOCK_DB["pets"].get(pet_id) + if not pet: + raise HTTPException(404, f"Pet {pet_id} not found") + + return pet + + +@router.post("/api/v1/cart/") +async def create_cart(request: CreateCartRequest): + """Create a cart (VET-538).""" + await _mock_delay() + _maybe_error() + + # Verify owner exists + if request.owner_id not in MOCK_DB["petowners"]: + raise HTTPException(404, f"Owner {request.owner_id} not found") + + cart = AmarDataGenerator.cart(owner_id=request.owner_id) + + # Store in mock DB + MOCK_DB["carts"][cart["id"]] = cart + + cart["_mock"] = True + return cart + + +@router.post("/api/v1/cart/{cart_id}/items/") +async def add_cart_item(cart_id: int, request: AddCartItemRequest): + """Add item to cart and recalculate summary.""" + await _mock_delay() + _maybe_error() + + cart = MOCK_DB["carts"].get(cart_id) + if not cart: + raise HTTPException(404, f"Cart {cart_id} not found") + + # Get service price + services = AmarDataGenerator.SERVICES + service = next((s for s in services if s["id"] == request.service_id), None) + if not service: + raise HTTPException(404, f"Service {request.service_id} not found") + + # Add item + item = { + "service_id": request.service_id, + "service_name": service["name"], + "pet_id": request.pet_id, + "quantity": request.quantity, + "price": service["price"], + } + + cart["items"].append(item) + + # Recalculate summary + cart = AmarDataGenerator.calculate_cart_summary(cart, cart["items"]) + MOCK_DB["carts"][cart_id] = cart + + cart["_mock"] = True + return cart + + +@router.get("/api/v1/cart/{cart_id}") +async def get_cart(cart_id: int): + """Get cart by ID.""" + await _mock_delay() + _maybe_error() + + cart = MOCK_DB["carts"].get(cart_id) + if not cart: + raise HTTPException(404, f"Cart {cart_id} not found") + + return cart + + +@router.get("/api/v1/services/") +async def list_services( + species: Optional[str] = Query(None), + neighborhood_id: Optional[int] = Query(None), +): + """List available services filtered by species and neighborhood (VET-540).""" + await _mock_delay() + _maybe_error() + + services = AmarDataGenerator.filter_services( + species=species, + neighborhood_id=neighborhood_id, + ) + + for service in services: + service["_mock"] = True + + return services + + +@router.get("/api/v1/categories/") +async def list_categories( + species: Optional[str] = Query(None), + neighborhood_id: Optional[int] = Query(None), +): + """List categories with available services (VET-539).""" + await _mock_delay() + _maybe_error() + + categories = AmarDataGenerator.filter_categories( + species=species, + neighborhood_id=neighborhood_id, + ) + + for category in categories: + category["_mock"] = True + + return categories + + +@router.post("/solicitudes/service-requests/") +async def create_service_request(cart_id: int, requested_date: Optional[str] = None): + """Create a service request.""" + await _mock_delay() + _maybe_error() + + cart = MOCK_DB["carts"].get(cart_id) + if not cart: + raise HTTPException(404, f"Cart {cart_id} not found") + + request = AmarDataGenerator.service_request( + cart_id=cart_id, + requested_date=requested_date, + ) + + MOCK_DB["service_requests"][request["id"]] = request + + request["_mock"] = True + return request + + +@router.get("/mock/reset") +async def reset_mock_db(): + """Reset the mock database (useful for testing).""" + MOCK_DB["petowners"].clear() + MOCK_DB["pets"].clear() + MOCK_DB["carts"].clear() + MOCK_DB["service_requests"].clear() + + return { + "message": "Mock database reset", + "_mock": True, + } + + +@router.get("/mock/stats") +async def mock_stats(): + """Get mock database statistics.""" + return { + "petowners": len(MOCK_DB["petowners"]), + "pets": len(MOCK_DB["pets"]), + "carts": len(MOCK_DB["carts"]), + "service_requests": len(MOCK_DB["service_requests"]), + "_mock": True, + } diff --git a/artery/shunts/amar/core/__init__.py b/artery/shunts/amar/core/__init__.py new file mode 100644 index 0000000..b0281c0 --- /dev/null +++ b/artery/shunts/amar/core/__init__.py @@ -0,0 +1 @@ +"""Core logic for Amar mock API.""" diff --git a/artery/shunts/amar/core/config.py b/artery/shunts/amar/core/config.py new file mode 100644 index 0000000..c93dc14 --- /dev/null +++ b/artery/shunts/amar/core/config.py @@ -0,0 +1,27 @@ +"""Configuration for Amar mock vein.""" + +from pathlib import Path +from pydantic_settings import BaseSettings + +ENV_FILE = Path(__file__).parent.parent / ".env" + + +class AmarMockConfig(BaseSettings): + """Configuration for Amar (MOCK) vein.""" + + api_port: int = 8005 + mock_data_path: str = "./mock_data" + + # Mock behavior + enable_random_delays: bool = True + min_delay_ms: int = 100 + max_delay_ms: int = 500 + error_rate: float = 0.0 # 0.0 to 1.0 + + model_config = { + "env_file": ENV_FILE if ENV_FILE.exists() else None, + "env_file_encoding": "utf-8", + } + + +settings = AmarMockConfig() diff --git a/artery/shunts/amar/main.py b/artery/shunts/amar/main.py new file mode 100644 index 0000000..ed85e50 --- /dev/null +++ b/artery/shunts/amar/main.py @@ -0,0 +1,40 @@ +"""Amar (MOCK) Vein - FastAPI app.""" + +from pathlib import Path +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from fastapi.middleware.cors import CORSMiddleware +from .api.routes import router +from .core.config import settings + +app = FastAPI( + title="Amar API (MOCK)", + description="Mock Amar API for testing - returns predictable test data", + version="0.1.0", +) + +# Enable CORS for testing from frontend +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, specify exact origins + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Templates for configuration UI +templates = Jinja2Templates(directory=Path(__file__).parent / "templates") + +@app.get("/", response_class=HTMLResponse) +def index(request: Request): + """Mock configuration UI.""" + return templates.TemplateResponse("index.html", {"request": request}) + +# Include router at root (matches real Amar API structure) +app.include_router(router) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=settings.api_port) diff --git a/artery/veins/mercadopago/requirements.txt b/artery/shunts/amar/requirements.txt similarity index 100% rename from artery/veins/mercadopago/requirements.txt rename to artery/shunts/amar/requirements.txt diff --git a/artery/shunts/amar/run.py b/artery/shunts/amar/run.py new file mode 100644 index 0000000..a2064ab --- /dev/null +++ b/artery/shunts/amar/run.py @@ -0,0 +1,18 @@ +"""Standalone runner for Amar mock vein.""" + +import logging +import uvicorn +from main import app +from core.config import settings + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +if __name__ == "__main__": + logger.info(f"Starting Amar (MOCK) vein on port {settings.api_port}") + logger.info(f"API docs: http://localhost:{settings.api_port}/docs") + logger.info(f"Health check: http://localhost:{settings.api_port}/health") + uvicorn.run(app, host="0.0.0.0", port=settings.api_port, reload=True) diff --git a/artery/shunts/amar/templates/index.html b/artery/shunts/amar/templates/index.html new file mode 100644 index 0000000..24220f6 --- /dev/null +++ b/artery/shunts/amar/templates/index.html @@ -0,0 +1,298 @@ + + + + + + Amar API (MOCK) - Configuration + + + +
+
+

Amar API MOCK

+
Configure mock responses for Amar backend endpoints
+
+ + +
+
Mock Database Stats
+
+
+
-
+
Pet Owners
+
+
+
-
+
Pets
+
+
+
-
+
Carts
+
+
+
-
+
Requests
+
+
+
+ +
+
+ + +
+
Configure Endpoint Responses
+
+
+
+ POST + /api/v1/pet-owners/ +
+
Create guest pet owner (VET-536)
+
+
+
+ POST + /api/v1/pets/ +
+
Create pet (VET-537)
+
+
+
+ POST + /api/v1/cart/ +
+
Create cart (VET-538)
+
+
+
+ GET + /api/v1/services/ +
+
List services (VET-540)
+
+
+
+ GET + /api/v1/categories/ +
+
List categories (VET-539)
+
+
+
+ + + + + +
+
Quick Test
+

Test endpoint URL to hit for configured responses:

+
+ http://localhost:8005/api/v1/pet-owners/ +
+
+
+ + + + diff --git a/artery/veins/mercadopago/.env.example b/artery/shunts/mercadopago/.env.example similarity index 100% rename from artery/veins/mercadopago/.env.example rename to artery/shunts/mercadopago/.env.example diff --git a/artery/veins/mercadopago/README.md b/artery/shunts/mercadopago/README.md similarity index 100% rename from artery/veins/mercadopago/README.md rename to artery/shunts/mercadopago/README.md diff --git a/artery/veins/mercadopago/__init__.py b/artery/shunts/mercadopago/__init__.py similarity index 100% rename from artery/veins/mercadopago/__init__.py rename to artery/shunts/mercadopago/__init__.py diff --git a/artery/veins/mercadopago/api/__init__.py b/artery/shunts/mercadopago/api/__init__.py similarity index 100% rename from artery/veins/mercadopago/api/__init__.py rename to artery/shunts/mercadopago/api/__init__.py diff --git a/artery/veins/mercadopago/api/routes.py b/artery/shunts/mercadopago/api/routes.py similarity index 100% rename from artery/veins/mercadopago/api/routes.py rename to artery/shunts/mercadopago/api/routes.py diff --git a/artery/veins/mercadopago/core/__init__.py b/artery/shunts/mercadopago/core/__init__.py similarity index 100% rename from artery/veins/mercadopago/core/__init__.py rename to artery/shunts/mercadopago/core/__init__.py diff --git a/artery/veins/mercadopago/core/config.py b/artery/shunts/mercadopago/core/config.py similarity index 100% rename from artery/veins/mercadopago/core/config.py rename to artery/shunts/mercadopago/core/config.py diff --git a/artery/veins/mercadopago/main.py b/artery/shunts/mercadopago/main.py similarity index 100% rename from artery/veins/mercadopago/main.py rename to artery/shunts/mercadopago/main.py diff --git a/artery/shunts/mercadopago/requirements.txt b/artery/shunts/mercadopago/requirements.txt new file mode 100644 index 0000000..aaa3fb5 --- /dev/null +++ b/artery/shunts/mercadopago/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.104.0 +uvicorn>=0.24.0 +pydantic>=2.0.0 +pydantic-settings>=2.0.0 diff --git a/artery/veins/mercadopago/run.py b/artery/shunts/mercadopago/run.py similarity index 100% rename from artery/veins/mercadopago/run.py rename to artery/shunts/mercadopago/run.py diff --git a/artery/veins/mercadopago/templates/index.html b/artery/shunts/mercadopago/templates/index.html similarity index 100% rename from artery/veins/mercadopago/templates/index.html rename to artery/shunts/mercadopago/templates/index.html diff --git a/build.py b/build.py index 9362505..6889aba 100644 --- a/build.py +++ b/build.py @@ -8,18 +8,23 @@ Both dev and deploy modes copy files (no symlinks) for Docker compatibility. After editing source files, re-run `python build.py dev` to update gen/. Usage: - python build.py dev # Build gen/ from source - python build.py dev --cfg amar # Include amar room config + python build.py dev # Build gen/standalone/ + python build.py dev --cfg amar # Build gen/amar/ + python build.py dev --all # Build all (standalone + rooms) python build.py deploy --output /path/ # Build for production python build.py models # Only regenerate models Examples: - # Set up dev environment + # Set up dev environment (standalone) python build.py dev - cd gen && .venv/bin/python run.py + cd gen/standalone && .venv/bin/python run.py # With room config python build.py dev --cfg amar + cd gen/amar && .venv/bin/python run.py + + # Build all targets + python build.py dev --all # Build for deployment python build.py deploy --output ../deploy/soleprint/ @@ -72,13 +77,15 @@ def count_files(path: Path) -> int: return sum(1 for _ in path.rglob("*") if _.is_file()) -def generate_models(output_dir: Path): +def generate_models(output_dir: Path, cfg_name: str | None = None): """Generate models using modelgen tool. Args: output_dir: Directory where models/pydantic/__init__.py will be created + cfg_name: Room config name (e.g., 'amar'), or None for standalone """ - config_path = SPR_ROOT / "cfg" / "soleprint.config.json" + room = cfg_name or "standalone" + config_path = SPR_ROOT / "cfg" / room / "config.json" if not config_path.exists(): log.warning(f"Config not found at {config_path}") @@ -111,62 +118,64 @@ def copy_cfg(output_dir: Path, cfg_name: str | None): Args: output_dir: Target directory - cfg_name: Name of room config (e.g., 'amar'), or None for base only + cfg_name: Name of room config (e.g., 'amar'), or None for standalone """ + room = cfg_name or "standalone" + room_cfg = SPR_ROOT / "cfg" / room + + if not room_cfg.exists(): + log.warning(f"Room config '{room}' not found at {room_cfg}") + return + + log.info(f"\nCopying {room} room config...") + + # Copy config.json to cfg/ cfg_dir = output_dir / "cfg" ensure_dir(cfg_dir) + room_config = room_cfg / "config.json" + if room_config.exists(): + copy_path(room_config, cfg_dir / "config.json") - # Always copy base config - base_config = SPR_ROOT / "cfg" / "soleprint.config.json" - if base_config.exists(): - copy_path(base_config, cfg_dir / "soleprint.config.json") + # Copy data/ to output data/ + room_data = room_cfg / "data" + if room_data.exists(): + log.info(f" Copying {room} data files...") + copy_path(room_data, output_dir / "data") - # Copy room-specific config if specified - if cfg_name: - room_cfg = SPR_ROOT / "cfg" / cfg_name - if room_cfg.exists() and room_cfg.is_dir(): - log.info(f"\nCopying {cfg_name} room config...") - for item in room_cfg.iterdir(): - if item.name == ".env.example": - # Copy .env.example to output root as template - copy_path(item, output_dir / ".env.example") - elif item.is_dir(): - copy_path(item, cfg_dir / cfg_name / item.name) - else: - ensure_dir(cfg_dir / cfg_name) - copy_path(item, cfg_dir / cfg_name / item.name) + # Copy .env.example to output root + env_example = room_cfg / ".env.example" + if env_example.exists(): + copy_path(env_example, output_dir / ".env.example") - # Copy room-specific databrowse depot if exists - room_databrowse = room_cfg / "databrowse" / "depot" - if room_databrowse.exists(): - log.info(f" Copying {cfg_name} databrowse depot...") - target = output_dir / "station" / "monitors" / "databrowse" / "depot" - copy_path(room_databrowse, target) + # Copy room-specific databrowse depot if exists + room_databrowse = room_cfg / "databrowse" / "depot" + if room_databrowse.exists(): + log.info(f" Copying {room} databrowse depot...") + target = output_dir / "station" / "monitors" / "databrowse" / "depot" + copy_path(room_databrowse, target) - # Copy room-specific tester tests if exists - room_tests = room_cfg / "tester" / "tests" - if room_tests.exists(): - log.info(f" Copying {cfg_name} tester tests...") - target = output_dir / "station" / "tools" / "tester" / "tests" - copy_path(room_tests, target) + # Copy room-specific tester tests if exists + room_tests = room_cfg / "tester" / "tests" + if room_tests.exists(): + log.info(f" Copying {room} tester tests...") + target = output_dir / "station" / "tools" / "tester" / "tests" + copy_path(room_tests, target) - # Copy room-specific monitors if exists - room_monitors = room_cfg / "monitors" - if room_monitors.exists(): - log.info(f" Copying {cfg_name} monitors...") - for monitor in room_monitors.iterdir(): - if monitor.is_dir(): - target = output_dir / "station" / "monitors" / monitor.name - copy_path(monitor, target) + # Copy room-specific monitors if exists + room_monitors = room_cfg / "monitors" + if room_monitors.exists(): + log.info(f" Copying {room} monitors...") + for monitor in room_monitors.iterdir(): + if monitor.is_dir(): + target = output_dir / "station" / "monitors" / monitor.name + copy_path(monitor, target) - # Copy room-specific models if exists - room_models = room_cfg / "models" - if room_models.exists(): - log.info(f" Copying {cfg_name} models...") - target = output_dir / "models" / cfg_name - copy_path(room_models, target) - else: - log.warning(f"Room config '{cfg_name}' not found at {room_cfg}") + # Copy room-specific models if exists + room_models = room_cfg / "models" + if room_models.exists(): + log.info(f" Copying {room} models...") + target = output_dir / "models" / room + copy_path(room_models, target) def build_dev(output_dir: Path, cfg_name: str | None = None): @@ -174,7 +183,7 @@ def build_dev(output_dir: Path, cfg_name: str | None = None): Build for development using copies (Docker-compatible). Structure: - gen/ + gen/standalone/ or gen// ├── main.py ├── run.py ├── index.html @@ -189,7 +198,7 @@ def build_dev(output_dir: Path, cfg_name: str | None = None): ├── .env.example # From cfg//.env.example └── models/ # Generated - After editing source files, re-run `python build.py dev` to update gen/. + After editing source files, re-run `python build.py dev` to update. """ log.info("\n=== Building DEV environment ===") log.info(f"SPR root: {SPR_ROOT}") @@ -217,17 +226,13 @@ def build_dev(output_dir: Path, cfg_name: str | None = None): if source.exists(): copy_path(source, output_dir / system) - # Data directory - log.info("\nCopying data...") - copy_path(SPR_ROOT / "data", output_dir / "data") - - # Config + # Config (includes data/ from room) log.info("\nCopying config...") copy_cfg(output_dir, cfg_name) # Models (generated) log.info("\nGenerating models...") - if not generate_models(output_dir): + if not generate_models(output_dir, cfg_name): log.warning("Model generation failed, you may need to run it manually") log.info("\n✓ Dev build complete!") @@ -236,7 +241,12 @@ def build_dev(output_dir: Path, cfg_name: str | None = None): log.info(f" python3 -m venv .venv") log.info(f" .venv/bin/pip install -r requirements.txt") log.info(f" .venv/bin/python run.py # Single-port bare-metal dev") - log.info(f"\nAfter editing source, rebuild with: python build.py dev") + if cfg_name: + log.info( + f"\nAfter editing source, rebuild with: python build.py dev --cfg {cfg_name}" + ) + else: + log.info(f"\nAfter editing source, rebuild with: python build.py dev") def build_deploy(output_dir: Path, cfg_name: str | None = None): @@ -276,19 +286,16 @@ def build_deploy(output_dir: Path, cfg_name: str | None = None): if source.exists(): copy_path(source, output_dir / system) - # Data directory (copy) - log.info("\nCopying data...") - copy_path(SPR_ROOT / "data", output_dir / "data") - - # Config (copy) + # Config (includes data/ from room) log.info("\nCopying config...") copy_cfg(output_dir, cfg_name) # Models (generate fresh) - pass output_dir, modelgen adds models/pydantic log.info("\nGenerating models...") - if not generate_models(output_dir): + if not generate_models(output_dir, cfg_name): # Fallback: copy from gen if exists - existing = SPR_ROOT / "gen" / "models" + room = cfg_name or "standalone" + existing = SPR_ROOT / "gen" / room / "models" if existing.exists(): log.info(" Using existing models from gen/") copy_path(existing, output_dir / "models") @@ -354,8 +361,8 @@ def main(): "--output", "-o", type=Path, - default=SPR_ROOT / "gen", - help="Output directory (default: gen/)", + default=None, + help="Output directory (default: gen/standalone/ or gen//)", ) dev_parser.add_argument( "--cfg", @@ -364,6 +371,11 @@ def main(): default=None, help="Room config to include (e.g., 'amar')", ) + dev_parser.add_argument( + "--all", + action="store_true", + help="Build all configs (standalone + all rooms in cfg/)", + ) # deploy command deploy_parser = subparsers.add_parser( @@ -390,7 +402,23 @@ def main(): args = parser.parse_args() if args.command == "dev": - build_dev(args.output.resolve(), args.cfg) + if getattr(args, "all", False): + # Build standalone + build_dev(SPR_ROOT / "gen" / "standalone", None) + # Build all room configs + cfg_dir = SPR_ROOT / "cfg" + for room in cfg_dir.iterdir(): + if room.is_dir() and room.name not in ("__pycache__",): + build_dev(SPR_ROOT / "gen" / room.name, room.name) + else: + # Determine output directory + if args.output: + output_dir = args.output.resolve() + elif args.cfg: + output_dir = SPR_ROOT / "gen" / args.cfg + else: + output_dir = SPR_ROOT / "gen" / "standalone" + build_dev(output_dir, args.cfg) elif args.command == "deploy": build_deploy(args.output.resolve(), args.cfg) elif args.command == "models": diff --git a/cfg/soleprint.config.json b/cfg/amar/config.json similarity index 88% rename from cfg/soleprint.config.json rename to cfg/amar/config.json index 7ac2710..ec62d31 100644 --- a/cfg/soleprint.config.json +++ b/cfg/amar/config.json @@ -56,15 +56,27 @@ "connector": { "name": "vein", "title": "Vein", - "description": "Single responsibility connector", + "description": "Stateless API connector", "plural": "veins" }, + "mock": { + "name": "shunt", + "title": "Shunt", + "description": "Fake connector for testing", + "plural": "shunts" + }, "composed": { "name": "pulse", "title": "Pulse", "description": "Composed data flow", "plural": "pulses", "formula": "Vein + Room + Depot" + }, + "app": { + "name": "plexus", + "title": "Plexus", + "description": "Full app with backend, frontend and DB", + "plural": "plexus" } }, "documentation": { diff --git a/data/__init__.py b/cfg/amar/data/__init__.py similarity index 100% rename from data/__init__.py rename to cfg/amar/data/__init__.py diff --git a/data/books.json b/cfg/amar/data/books.json similarity index 100% rename from data/books.json rename to cfg/amar/data/books.json diff --git a/data/depots.json b/cfg/amar/data/depots.json similarity index 100% rename from data/depots.json rename to cfg/amar/data/depots.json diff --git a/data/desks.json b/cfg/amar/data/desks.json similarity index 100% rename from data/desks.json rename to cfg/amar/data/desks.json diff --git a/data/monitors.json b/cfg/amar/data/monitors.json similarity index 100% rename from data/monitors.json rename to cfg/amar/data/monitors.json diff --git a/data/pulses.json b/cfg/amar/data/pulses.json similarity index 100% rename from data/pulses.json rename to cfg/amar/data/pulses.json diff --git a/data/rooms.json b/cfg/amar/data/rooms.json similarity index 100% rename from data/rooms.json rename to cfg/amar/data/rooms.json diff --git a/data/tables.json b/cfg/amar/data/tables.json similarity index 100% rename from data/tables.json rename to cfg/amar/data/tables.json diff --git a/data/template/feature-form/template.md b/cfg/amar/data/template/feature-form/template.md similarity index 100% rename from data/template/feature-form/template.md rename to cfg/amar/data/template/feature-form/template.md diff --git a/data/templates.json b/cfg/amar/data/templates.json similarity index 100% rename from data/templates.json rename to cfg/amar/data/templates.json diff --git a/data/tools.json b/cfg/amar/data/tools.json similarity index 100% rename from data/tools.json rename to cfg/amar/data/tools.json diff --git a/cfg/amar/data/veins.json b/cfg/amar/data/veins.json new file mode 100644 index 0000000..8b1ca3f --- /dev/null +++ b/cfg/amar/data/veins.json @@ -0,0 +1,60 @@ +{ + "items": [ + { + "name": "jira", + "slug": "jira", + "title": "Jira", + "status": "live", + "system": "artery" + }, + { + "name": "slack", + "slug": "slack", + "title": "Slack", + "status": "building", + "system": "artery" + }, + { + "name": "google", + "slug": "google", + "title": "Google", + "status": "planned", + "system": "artery" + }, + { + "name": "maps", + "slug": "maps", + "title": "Maps", + "status": "planned", + "system": "artery" + }, + { + "name": "whatsapp", + "slug": "whatsapp", + "title": "WhatsApp", + "status": "planned", + "system": "artery" + }, + { + "name": "gnucash", + "slug": "gnucash", + "title": "GNUCash", + "status": "planned", + "system": "artery" + }, + { + "name": "vnc", + "slug": "vnc", + "title": "VNC", + "status": "planned", + "system": "artery" + }, + { + "name": "ia", + "slug": "ia", + "title": "IA", + "status": "planned", + "system": "artery" + } + ] +} diff --git a/cfg/standalone/config.json b/cfg/standalone/config.json new file mode 100644 index 0000000..ec62d31 --- /dev/null +++ b/cfg/standalone/config.json @@ -0,0 +1,140 @@ +{ + "framework": { + "name": "soleprint", + "slug": "soleprint", + "version": "0.1.0", + "description": "Development workflow and documentation system", + "tagline": "Mapping development footprints", + "icon": "👣", + "hub_port": 12000 + }, + "systems": [ + { + "key": "data_flow", + "name": "artery", + "slug": "artery", + "title": "Artery", + "tagline": "Todo lo vital", + "port": 12001, + "icon": "💉" + }, + { + "key": "documentation", + "name": "atlas", + "slug": "atlas", + "title": "Atlas", + "tagline": "Documentación accionable", + "port": 12002, + "icon": "🗺️" + }, + { + "key": "execution", + "name": "station", + "slug": "station", + "title": "Station", + "tagline": "Monitores, Entornos y Herramientas", + "port": 12003, + "icon": "🎛️" + } + ], + "components": { + "shared": { + "config": { + "name": "room", + "title": "Room", + "description": "Runtime environment configuration", + "plural": "rooms" + }, + "data": { + "name": "depot", + "title": "Depot", + "description": "Data storage / provisions", + "plural": "depots" + } + }, + "data_flow": { + "connector": { + "name": "vein", + "title": "Vein", + "description": "Stateless API connector", + "plural": "veins" + }, + "mock": { + "name": "shunt", + "title": "Shunt", + "description": "Fake connector for testing", + "plural": "shunts" + }, + "composed": { + "name": "pulse", + "title": "Pulse", + "description": "Composed data flow", + "plural": "pulses", + "formula": "Vein + Room + Depot" + }, + "app": { + "name": "plexus", + "title": "Plexus", + "description": "Full app with backend, frontend and DB", + "plural": "plexus" + } + }, + "documentation": { + "pattern": { + "name": "template", + "title": "Template", + "description": "Documentation pattern", + "plural": "templates" + }, + "library": { + "name": "book", + "title": "Book", + "description": "Documentation library" + }, + "composed": { + "name": "book", + "title": "Book", + "description": "Composed documentation", + "plural": "books", + "formula": "Template + Depot" + } + }, + "execution": { + "utility": { + "name": "tool", + "title": "Tool", + "description": "Execution utility", + "plural": "tools" + }, + "watcher": { + "name": "monitor", + "title": "Monitor", + "description": "Service monitor", + "plural": "monitors" + }, + "container": { + "name": "cabinet", + "title": "Cabinet", + "description": "Tool container", + "plural": "cabinets" + }, + "workspace": { + "name": "desk", + "title": "Desk", + "description": "Execution workspace" + }, + "workbench": { + "name": "desk", + "title": "Desk", + "description": "Work surface" + }, + "composed": { + "name": "desk", + "title": "Desk", + "description": "Composed execution bundle", + "plural": "desks", + "formula": "Cabinet + Room + Depots" + } + } + } +} diff --git a/cfg/standalone/data/__init__.py b/cfg/standalone/data/__init__.py new file mode 100644 index 0000000..9836af5 --- /dev/null +++ b/cfg/standalone/data/__init__.py @@ -0,0 +1,156 @@ +""" +Pawprint Data Layer + +JSON file storage (future: MongoDB) +""" + +import json +from pathlib import Path +from typing import List, Optional + +# Add parent to path for models import +import sys +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from models.pydantic import ( + Vein, Nest, Larder, Template, Tool, + Pulse, Book, Table, + VeinCollection, NestCollection, LarderCollection, + TemplateCollection, ToolCollection, + PulseCollection, BookCollection, TableCollection, + Status +) + +DATA_DIR = Path(__file__).parent.resolve() + +# Debug: print data dir on import +print(f"[data] DATA_DIR: {DATA_DIR}") +print(f"[data] DATA_DIR exists: {DATA_DIR.exists()}") +print(f"[data] veins.json exists: {(DATA_DIR / 'veins.json').exists()}") + + +def _load_json(filename: str) -> dict: + filepath = DATA_DIR / filename + if filepath.exists(): + with open(filepath) as f: + data = json.load(f) + print(f"[data] Loaded {filename}: {len(data.get('items', []))} items") + return data + print(f"[data] File not found: {filepath}") + return {"items": []} + + +def _save_json(filename: str, data: dict): + filepath = DATA_DIR / filename + with open(filepath, 'w') as f: + json.dump(data, f, indent=2) + + +# === Loaders === + +def get_veins() -> List[Vein]: + data = _load_json("veins.json") + return VeinCollection(**data).items + + +def get_nests() -> List[Nest]: + data = _load_json("nests.json") + return NestCollection(**data).items + + +def get_larders() -> List[Larder]: + data = _load_json("larders.json") + return LarderCollection(**data).items + + +def get_templates() -> List[Template]: + data = _load_json("templates.json") + return TemplateCollection(**data).items + + +def get_tools() -> List[Tool]: + data = _load_json("tools.json") + return ToolCollection(**data).items + + +def get_cabinets() -> list: + """Load cabinets (simple dict, no pydantic yet).""" + data = _load_json("cabinets.json") + return data.get("items", []) + + +def get_monitors() -> list: + """Load monitors (simple dict, no pydantic yet).""" + data = _load_json("monitors.json") + return data.get("items", []) + + +def get_pulses() -> List[Pulse]: + data = _load_json("pulses.json") + return PulseCollection(**data).items + + +def get_books() -> List[Book]: + data = _load_json("books.json") + return BookCollection(**data).items + + +def get_tables() -> List[Table]: + data = _load_json("tables.json") + return TableCollection(**data).items + + +# === Helpers === + +def get_vein(name: str) -> Optional[Vein]: + for v in get_veins(): + if v.name == name: + return v + return None + + +def get_nest(name: str) -> Optional[Nest]: + for n in get_nests(): + if n.name == name: + return n + return None + + +def get_larder(name: str) -> Optional[Larder]: + for l in get_larders(): + if l.name == name: + return l + return None + + +# === For frontend rendering === + +def get_artery_data() -> dict: + """Data for artery frontend.""" + return { + "veins": [v.model_dump() for v in get_veins()], + "nests": [n.model_dump() for n in get_nests()], + "larders": [l.model_dump() for l in get_larders()], + "pulses": [p.model_dump() for p in get_pulses()], + } + + +def get_album_data() -> dict: + """Data for album frontend.""" + return { + "templates": [t.model_dump() for t in get_templates()], + "larders": [l.model_dump() for l in get_larders()], + "books": [b.model_dump() for b in get_books()], + } + + +def get_ward_data() -> dict: + """Data for ward frontend.""" + return { + "tools": [t.model_dump() for t in get_tools()], + "monitors": get_monitors(), + "cabinets": get_cabinets(), + "nests": [n.model_dump() for n in get_nests()], + "larders": [l.model_dump() for l in get_larders()], + "tables": [t.model_dump() for t in get_tables()], + } diff --git a/cfg/standalone/data/books.json b/cfg/standalone/data/books.json new file mode 100644 index 0000000..d201c7f --- /dev/null +++ b/cfg/standalone/data/books.json @@ -0,0 +1,79 @@ +{ + "items": [ + { + "name": "arch-model", + "slug": "arch-model", + "title": "Architecture Model", + "status": "ready", + "template": null, + "larder": { + "name": "arch-model", + "slug": "arch-model", + "title": "Architecture Model", + "status": "ready", + "source_template": null, + "data_path": "album/book/arch-model" + }, + "output_larder": null, + "system": "album" + }, + { + "name": "feature-flow", + "slug": "feature-flow", + "title": "Feature Flow Pipeline", + "status": "ready", + "template": null, + "larder": { + "name": "feature-flow", + "slug": "feature-flow", + "title": "Feature Flow Pipeline", + "status": "ready", + "source_template": null, + "data_path": "album/book/feature-flow" + }, + "output_larder": null, + "system": "album" + }, + { + "name": "gherkin-samples", + "slug": "gherkin-samples", + "title": "Gherkin Samples", + "status": "ready", + "template": null, + "larder": { + "name": "gherkin-samples", + "slug": "gherkin-samples", + "title": "Gherkin Samples", + "status": "ready", + "source_template": null, + "data_path": "album/book/gherkin-samples" + }, + "output_larder": null, + "system": "album" + }, + { + "name": "feature-form-samples", + "slug": "feature-form-samples", + "title": "Feature Form Samples", + "status": "ready", + "template": { + "name": "feature-form", + "slug": "feature-form", + "title": "Feature Form Template", + "status": "ready", + "template_path": "album/template/feature-form", + "system": "album" + }, + "larder": { + "name": "feature-form", + "slug": "feature-form", + "title": "Feature Forms", + "status": "ready", + "source_template": "feature-form", + "data_path": "album/book/feature-form-samples/feature-form" + }, + "output_larder": null, + "system": "album" + } + ] +} diff --git a/cfg/standalone/data/depots.json b/cfg/standalone/data/depots.json new file mode 100644 index 0000000..72be870 --- /dev/null +++ b/cfg/standalone/data/depots.json @@ -0,0 +1,12 @@ +{ + "items": [ + { + "name": "feature-form", + "slug": "feature-form", + "title": "Feature Forms", + "status": "ready", + "source_template": "feature-form", + "data_path": "album/book/feature-form-samples/feature-form" + } + ] +} diff --git a/cfg/standalone/data/desks.json b/cfg/standalone/data/desks.json new file mode 100644 index 0000000..2feb210 --- /dev/null +++ b/cfg/standalone/data/desks.json @@ -0,0 +1,3 @@ +{ + "items": [] +} diff --git a/cfg/standalone/data/monitors.json b/cfg/standalone/data/monitors.json new file mode 100644 index 0000000..503de9d --- /dev/null +++ b/cfg/standalone/data/monitors.json @@ -0,0 +1,22 @@ +{ + "items": [ + { + "name": "turnos", + "slug": "turnos", + "title": "Turnos Monitor", + "status": "dev", + "system": "ward", + "description": "Pipeline view of requests → turnos. Shows vet-petowner at a glance.", + "path": "ward/monitor/turnos" + }, + { + "name": "data_browse", + "slug": "data-browse", + "title": "Data Browse", + "status": "ready", + "system": "ward", + "description": "Quick navigation to test users and data states. Book/larder pattern with SQL mode for manual testing workflows.", + "path": "ward/monitor/data_browse" + } + ] +} diff --git a/cfg/standalone/data/pulses.json b/cfg/standalone/data/pulses.json new file mode 100644 index 0000000..2feb210 --- /dev/null +++ b/cfg/standalone/data/pulses.json @@ -0,0 +1,3 @@ +{ + "items": [] +} diff --git a/cfg/standalone/data/rooms.json b/cfg/standalone/data/rooms.json new file mode 100644 index 0000000..9310a6e --- /dev/null +++ b/cfg/standalone/data/rooms.json @@ -0,0 +1,5 @@ +{ + "items": [ + {"name": "pawprint-local", "slug": "pawprint-local", "title": "Pawprint Local", "status": "dev", "config_path": "deploy/pawprint-local"} + ] +} diff --git a/cfg/standalone/data/tables.json b/cfg/standalone/data/tables.json new file mode 100644 index 0000000..2feb210 --- /dev/null +++ b/cfg/standalone/data/tables.json @@ -0,0 +1,3 @@ +{ + "items": [] +} diff --git a/cfg/standalone/data/template/feature-form/template.md b/cfg/standalone/data/template/feature-form/template.md new file mode 100644 index 0000000..6b49e99 --- /dev/null +++ b/cfg/standalone/data/template/feature-form/template.md @@ -0,0 +1,38 @@ +# {{nombre_flujo}} + +## Tipo de usuario +{{tipo_usuario}} + +## Donde empieza +{{punto_entrada}} + +## Que quiere hacer el usuario +{{objetivo}} + +## Pasos + +1. {{paso_1}} +2. {{paso_2}} +3. {{paso_3}} + +## Que deberia pasar + +- {{resultado_1}} +- {{resultado_2}} + +## Problemas comunes + +- {{problema_1}} +- {{problema_2}} + +## Casos especiales + +- {{caso_especial_1}} + +## Flujos relacionados + +- {{flujo_relacionado_1}} + +## Notas tecnicas + +- {{nota_tecnica_1}} diff --git a/cfg/standalone/data/templates.json b/cfg/standalone/data/templates.json new file mode 100644 index 0000000..781b31d --- /dev/null +++ b/cfg/standalone/data/templates.json @@ -0,0 +1,12 @@ +{ + "items": [ + { + "name": "feature-form", + "slug": "feature-form", + "title": "Feature Form Template", + "status": "ready", + "template_path": "data/template/feature-form", + "system": "album" + } + ] +} diff --git a/cfg/standalone/data/tools.json b/cfg/standalone/data/tools.json new file mode 100644 index 0000000..165fcc0 --- /dev/null +++ b/cfg/standalone/data/tools.json @@ -0,0 +1,48 @@ +{ + "items": [ + { + "name": "tester", + "slug": "tester", + "title": "Contract Tests", + "status": "live", + "system": "ward", + "type": "app", + "description": "HTTP contract test runner with multi-environment support. Filter, run, and track tests against dev/stage/prod.", + "path": "ward/tools/tester", + "url": "/tools/tester/" + }, + { + "name": "datagen", + "slug": "datagen", + "title": "Test Data Generator", + "status": "live", + "system": "ward", + "type": "cli", + "description": "Generate realistic test data for Amar domain (users, pets, services) and MercadoPago API responses. Used by mock veins and test seeders.", + "path": "ward/tools/datagen", + "cli": "python -m datagen" + }, + { + "name": "generate_test_data", + "slug": "generate-test-data", + "title": "DB Test Data Extractor", + "status": "dev", + "system": "ward", + "type": "cli", + "description": "Extract representative subsets from PostgreSQL dumps for testing/development.", + "path": "ward/tools/generate_test_data", + "cli": "python -m generate_test_data" + }, + { + "name": "modelgen", + "slug": "modelgen", + "title": "Model Generator", + "status": "dev", + "system": "ward", + "type": "cli", + "description": "Generate platform-specific models (Pydantic, Django, Prisma) from JSON Schema.", + "path": "ward/tools/modelgen", + "cli": "python -m modelgen" + } + ] +} diff --git a/cfg/standalone/data/veins.json b/cfg/standalone/data/veins.json new file mode 100644 index 0000000..8b1ca3f --- /dev/null +++ b/cfg/standalone/data/veins.json @@ -0,0 +1,60 @@ +{ + "items": [ + { + "name": "jira", + "slug": "jira", + "title": "Jira", + "status": "live", + "system": "artery" + }, + { + "name": "slack", + "slug": "slack", + "title": "Slack", + "status": "building", + "system": "artery" + }, + { + "name": "google", + "slug": "google", + "title": "Google", + "status": "planned", + "system": "artery" + }, + { + "name": "maps", + "slug": "maps", + "title": "Maps", + "status": "planned", + "system": "artery" + }, + { + "name": "whatsapp", + "slug": "whatsapp", + "title": "WhatsApp", + "status": "planned", + "system": "artery" + }, + { + "name": "gnucash", + "slug": "gnucash", + "title": "GNUCash", + "status": "planned", + "system": "artery" + }, + { + "name": "vnc", + "slug": "vnc", + "title": "VNC", + "status": "planned", + "system": "artery" + }, + { + "name": "ia", + "slug": "ia", + "title": "IA", + "status": "planned", + "system": "artery" + } + ] +} diff --git a/ctrl/build.sh b/ctrl/build.sh index 65b5146..1c9f21b 100755 --- a/ctrl/build.sh +++ b/ctrl/build.sh @@ -2,21 +2,25 @@ # Build soleprint for local development # # Usage: -# ./build.sh # Build gen/ with default config -# ./build.sh amar # Build gen/ with amar room config +# ./build.sh # Build gen/standalone/ +# ./build.sh amar # Build gen/amar/ +# ./build.sh --all # Build all targets set -e cd "$(dirname "$0")/.." -CFG="${1:-}" +TARGET="${1:-}" -if [ -n "$CFG" ]; then - echo "Building with --cfg $CFG..." - python build.py dev --cfg "$CFG" +if [ "$TARGET" = "--all" ]; then + echo "Building all targets..." + python build.py dev --all +elif [ -n "$TARGET" ]; then + echo "Building gen/$TARGET/..." + python build.py dev --cfg "$TARGET" else - echo "Building soleprint..." + echo "Building gen/standalone/..." python build.py dev fi echo "" -echo "Done. Run with: ./ctrl/start.sh" +echo "Done. Run with: ./ctrl/start.sh [target]" diff --git a/ctrl/logs.sh b/ctrl/logs.sh index 3c3ba44..798c81e 100755 --- a/ctrl/logs.sh +++ b/ctrl/logs.sh @@ -1,7 +1,29 @@ #!/bin/bash # View soleprint logs +# +# Usage: +# ./logs.sh # Logs for standalone +# ./logs.sh amar # Logs for amar set -e -cd "$(dirname "$0")/../gen" +cd "$(dirname "$0")/.." -docker compose logs -f "$@" +TARGET="standalone" +ARGS="" + +for arg in "$@"; do + case $arg in + -*) ARGS="$ARGS $arg" ;; + *) TARGET="$arg" ;; + esac +done + +GEN_DIR="gen/$TARGET" + +if [ ! -d "$GEN_DIR" ]; then + echo "$GEN_DIR not found" + exit 1 +fi + +cd "$GEN_DIR" +docker compose logs -f $ARGS diff --git a/ctrl/start.sh b/ctrl/start.sh index e48ce1e..3dc5d25 100755 --- a/ctrl/start.sh +++ b/ctrl/start.sh @@ -1,28 +1,32 @@ #!/bin/bash -# Start soleprint with Docker (for network access to other services) +# Start soleprint with Docker # # Usage: -# ./start.sh # Start in foreground -# ./start.sh -d # Start detached -# ./start.sh --build # Rebuild image first +# ./start.sh # Start standalone +# ./start.sh amar # Start amar +# ./start.sh -d # Detached +# ./start.sh amar -d # Start amar detached set -e cd "$(dirname "$0")/.." -# Ensure gen/ exists -if [ ! -d "gen" ]; then - echo "gen/ not found. Run ./ctrl/build.sh first" - exit 1 -fi - -cd gen - +TARGET="standalone" ARGS="" + for arg in "$@"; do case $arg in -d|--detach) ARGS="$ARGS -d" ;; --build) ARGS="$ARGS --build" ;; + *) TARGET="$arg" ;; esac done +GEN_DIR="gen/$TARGET" + +if [ ! -d "$GEN_DIR" ]; then + echo "$GEN_DIR not found. Run ./ctrl/build.sh $TARGET first" + exit 1 +fi + +cd "$GEN_DIR" docker compose up $ARGS diff --git a/ctrl/stop.sh b/ctrl/stop.sh index 71cd1d6..a199ee2 100755 --- a/ctrl/stop.sh +++ b/ctrl/stop.sh @@ -1,7 +1,20 @@ #!/bin/bash # Stop soleprint Docker container +# +# Usage: +# ./stop.sh # Stop standalone +# ./stop.sh amar # Stop amar set -e -cd "$(dirname "$0")/../gen" +cd "$(dirname "$0")/.." +TARGET="${1:-standalone}" +GEN_DIR="gen/$TARGET" + +if [ ! -d "$GEN_DIR" ]; then + echo "$GEN_DIR not found" + exit 1 +fi + +cd "$GEN_DIR" docker compose down diff --git a/data/veins.json b/data/veins.json deleted file mode 100644 index b5e1747..0000000 --- a/data/veins.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "items": [ - {"name": "jira", "slug": "jira", "title": "Jira", "status": "live", "system": "artery"}, - {"name": "slack", "slug": "slack", "title": "Slack", "status": "building", "system": "artery"}, - {"name": "amar", "slug": "amar", "title": "Amar API", "status": "ready", "system": "artery", "mock": true, "description": "Mock Amar backend - configure endpoint responses"}, - {"name": "mercadopago", "slug": "mercadopago", "title": "MercadoPago", "status": "ready", "system": "artery", "mock": true, "description": "Mock MercadoPago API - configure payment responses"}, - {"name": "google", "slug": "google", "title": "Google", "status": "planned", "system": "artery"}, - {"name": "maps", "slug": "maps", "title": "Maps", "status": "planned", "system": "artery"}, - {"name": "whatsapp", "slug": "whatsapp", "title": "WhatsApp", "status": "planned", "system": "artery"}, - {"name": "gnucash", "slug": "gnucash", "title": "GNUCash", "status": "planned", "system": "artery"}, - {"name": "vnc", "slug": "vnc", "title": "VPN", "status": "planned", "system": "artery"}, - {"name": "ia", "slug": "ia", "title": "IA", "status": "planned", "system": "artery"} - ] -}