#!/usr/bin/env python3 """Direct file creation script with detailed output""" import sys from pathlib import Path def main(): backend_base = Path(r"C:\Users\Nabeel\Nextcloud\Projects\firefly_reports\backend") print("=" * 80) print("CREATING BACKEND PYTHON SOURCE FILES") print("=" * 80) print() # File contents dictionary files_content = { "pyproject.toml": """[project] name = "firefly-analytics-backend" version = "0.1.0" description = "Firefly III Analytics & Reporting Backend" requires-python = ">=3.11" dependencies = [ "fastapi==0.104.1", "uvicorn[standard]==0.24.0", "httpx==0.25.2", "python-dotenv==1.0.0", "pydantic==2.5.0", "pydantic-settings==2.1.0", "sqlalchemy==2.0.23", "python-dateutil==2.8.2", "apscheduler==3.10.4", ] [project.optional-dependencies] dev = [ "pytest==7.4.3", "pytest-asyncio==0.21.1", "black==23.12.0", "ruff==0.1.8", ] """, "app/__init__.py": "# Firefly Analytics App\n", "app/config.py": """import os from pydantic_settings import BaseSettings class Settings(BaseSettings): FIREFLY_URL: str = os.getenv("FIREFLY_URL", "https://firefly.scsimedia.duckdns.org") FIREFLY_API_TOKEN: str = os.getenv("FIREFLY_API_TOKEN", "") DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./data/app.db") SYNC_INTERVAL_MINUTES: int = int(os.getenv("SYNC_INTERVAL_MINUTES", "30")) API_TIMEOUT: int = 30 class Config: env_file = ".env" settings = Settings() """, "app/models.py": """from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from datetime import datetime Base = declarative_base() class Transaction(Base): __tablename__ = "transactions" id = Column(Integer, primary_key=True, index=True) firefly_id = Column(Integer, unique=True, index=True) date = Column(DateTime, index=True) amount = Column(Float) description = Column(String) type = Column(String) category_id = Column(Integer, ForeignKey("categories.id"), nullable=True) from_account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True) to_account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True) notes = Column(Text, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) category = relationship("Category", foreign_keys=[category_id], back_populates="transactions") from_account = relationship("Account", foreign_keys=[from_account_id]) to_account = relationship("Account", foreign_keys=[to_account_id]) class Category(Base): __tablename__ = "categories" id = Column(Integer, primary_key=True, index=True) firefly_id = Column(Integer, unique=True, index=True) name = Column(String, unique=True, index=True) type = Column(String) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) transactions = relationship("Transaction", foreign_keys=[Transaction.category_id], back_populates="category") class Account(Base): __tablename__ = "accounts" id = Column(Integer, primary_key=True, index=True) firefly_id = Column(Integer, unique=True, index=True) name = Column(String, unique=True, index=True) type = Column(String) balance = Column(Float) currency_code = Column(String) active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class SyncLog(Base): __tablename__ = "sync_logs" id = Column(Integer, primary_key=True, index=True) entity_type = Column(String) last_sync = Column(DateTime) status = Column(String) message = Column(String, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) """, "app/database.py": """from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session from app.config import settings from app.models import Base engine = create_engine( settings.DATABASE_URL, connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {} ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def init_db(): Base.metadata.create_all(bind=engine) def get_db() -> Session: db = SessionLocal() try: yield db finally: db.close() """, "app/main.py": """from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from dotenv import load_dotenv from app.database import init_db from app.routers import transactions, categories, accounts, reports, summary from app.services.sync_service import start_sync_scheduler, stop_sync_scheduler load_dotenv() @asynccontextmanager async def lifespan(app: FastAPI): init_db() start_sync_scheduler() yield stop_sync_scheduler() app = FastAPI( title="Firefly III Analytics API", description="Analytics and reporting for Firefly III", version="0.1.0", lifespan=lifespan ) app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000", "http://localhost"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(transactions.router, prefix="/api/transactions", tags=["transactions"]) app.include_router(categories.router, prefix="/api/categories", tags=["categories"]) app.include_router(accounts.router, prefix="/api/accounts", tags=["accounts"]) app.include_router(reports.router, prefix="/api/reports", tags=["reports"]) app.include_router(summary.router, prefix="/api/summary", tags=["summary"]) @app.get("/") async def root(): return {"status": "ok", "message": "Firefly III Analytics API"} @app.get("/health") async def health(): return {"status": "healthy"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) """, "app/routers/__init__.py": "# API routers\n", "app/routers/transactions.py": """from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from app.database import get_db from datetime import datetime router = APIRouter() @router.get("/") async def get_transactions( start_date: str = Query(None), end_date: str = Query(None), category_id: int = Query(None), db: Session = Depends(get_db) ): from app.models import Transaction query = db.query(Transaction) if start_date: query = query.filter(Transaction.date >= datetime.fromisoformat(start_date)) if end_date: query = query.filter(Transaction.date <= datetime.fromisoformat(end_date)) if category_id: query = query.filter(Transaction.category_id == category_id) transactions = query.order_by(Transaction.date.desc()).all() return {"data": transactions, "count": len(transactions)} """, "app/routers/categories.py": """from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from app.database import get_db from app.models import Category router = APIRouter() @router.get("/") async def get_categories(db: Session = Depends(get_db)): categories = db.query(Category).all() return {"data": categories, "count": len(categories)} """, "app/routers/accounts.py": """from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from app.database import get_db from app.models import Account router = APIRouter() @router.get("/") async def get_accounts(db: Session = Depends(get_db)): accounts = db.query(Account).all() return {"data": accounts, "count": len(accounts)} """, "app/routers/reports.py": """from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from sqlalchemy import func from app.database import get_db from app.models import Transaction, Category from datetime import datetime router = APIRouter() @router.get("/spending-by-category") async def spending_by_category( start_date: str = Query(...), end_date: str = Query(...), db: Session = Depends(get_db) ): start = datetime.fromisoformat(start_date) end = datetime.fromisoformat(end_date) results = db.query( Category.name, func.sum(Transaction.amount).label("total") ).join( Transaction ).filter( Transaction.date >= start, Transaction.date <= end, Transaction.type == "withdrawal" ).group_by(Category.name).all() return { "period": {"start": start.isoformat(), "end": end.isoformat()}, "data": [{"category": r[0], "amount": float(r[1]) if r[1] else 0} for r in results] } @router.get("/income-vs-expenses") async def income_vs_expenses( start_date: str = Query(...), end_date: str = Query(...), db: Session = Depends(get_db) ): start = datetime.fromisoformat(start_date) end = datetime.fromisoformat(end_date) income = db.query(func.sum(Transaction.amount)).filter( Transaction.date >= start, Transaction.date <= end, Transaction.type == "deposit" ).scalar() or 0 expenses = db.query(func.sum(Transaction.amount)).filter( Transaction.date >= start, Transaction.date <= end, Transaction.type == "withdrawal" ).scalar() or 0 return { "period": {"start": start.isoformat(), "end": end.isoformat()}, "income": float(income), "expenses": float(expenses), "net": float(income - expenses) } @router.get("/trends") async def trends( start_date: str = Query(...), end_date: str = Query(...), db: Session = Depends(get_db) ): start = datetime.fromisoformat(start_date) end = datetime.fromisoformat(end_date) results = db.query( func.date(Transaction.date).label("date"), func.sum(Transaction.amount).label("amount") ).filter( Transaction.date >= start, Transaction.date <= end ).group_by(func.date(Transaction.date)).order_by("date").all() return { "period": {"start": start.isoformat(), "end": end.isoformat()}, "data": [{"date": str(r[0]), "amount": float(r[1]) if r[1] else 0} for r in results] } """, "app/routers/summary.py": """from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from sqlalchemy import func from app.database import get_db from app.models import Transaction, Account from datetime import datetime, timedelta router = APIRouter() @router.get("/") async def get_summary(db: Session = Depends(get_db)): thirty_days_ago = datetime.utcnow() - timedelta(days=30) total_assets = db.query(func.sum(Account.balance)).scalar() or 0 recent_transactions = db.query(func.count(Transaction.id)).filter( Transaction.date >= thirty_days_ago ).scalar() or 0 income = db.query(func.sum(Transaction.amount)).filter( Transaction.date >= thirty_days_ago, Transaction.type == "deposit" ).scalar() or 0 expenses = db.query(func.sum(Transaction.amount)).filter( Transaction.date >= thirty_days_ago, Transaction.type == "withdrawal" ).scalar() or 0 return { "total_assets": float(total_assets), "recent_transactions": recent_transactions, "last_30_days": { "income": float(income), "expenses": float(expenses), "net": float(income - expenses) } } """, "app/clients/__init__.py": "# API clients\n", "app/clients/firefly_client.py": """import httpx from typing import Optional, List, Dict, Any from app.config import settings import logging logger = logging.getLogger(__name__) class FireflyClient: def __init__(self): self.base_url = settings.FIREFLY_URL.rstrip('/') self.headers = { "Authorization": f"Bearer {settings.FIREFLY_API_TOKEN}", "Accept": "application/vnd.api+json", "Content-Type": "application/json" } async def get_transactions( self, start_date: Optional[str] = None, end_date: Optional[str] = None, limit: int = 50 ) -> List[Dict[str, Any]]: async with httpx.AsyncClient() as client: params = {"limit": limit} if start_date: params["start"] = start_date if end_date: params["end"] = end_date try: response = await client.get( f"{self.base_url}/api/v1/transactions", headers=self.headers, params=params, timeout=settings.API_TIMEOUT ) response.raise_for_status() return response.json().get("data", []) except Exception as e: logger.error(f"Error fetching transactions: {e}") return [] async def get_categories(self) -> List[Dict[str, Any]]: async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.base_url}/api/v1/categories", headers=self.headers, timeout=settings.API_TIMEOUT ) response.raise_for_status() return response.json().get("data", []) except Exception as e: logger.error(f"Error fetching categories: {e}") return [] async def get_accounts(self) -> List[Dict[str, Any]]: async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.base_url}/api/v1/accounts", headers=self.headers, timeout=settings.API_TIMEOUT ) response.raise_for_status() return response.json().get("data", []) except Exception as e: logger.error(f"Error fetching accounts: {e}") return [] firefly_client = FireflyClient() """, "app/services/__init__.py": "# Services\n", "app/services/sync_service.py": """from apscheduler.schedulers.background import BackgroundScheduler from app.config import settings from app.clients.firefly_client import firefly_client from app.database import SessionLocal from app.models import Transaction, Category, Account, SyncLog from datetime import datetime import logging import asyncio logger = logging.getLogger(__name__) scheduler = BackgroundScheduler() async def sync_data(): db = SessionLocal() try: # Sync categories categories = await firefly_client.get_categories() for cat in categories: attrs = cat.get("attributes", {}) existing = db.query(Category).filter_by(firefly_id=cat["id"]).first() if existing: existing.name = attrs.get("name") existing.type = attrs.get("type") existing.updated_at = datetime.utcnow() else: db.add(Category( firefly_id=cat["id"], name=attrs.get("name"), type=attrs.get("type") )) # Sync accounts accounts = await firefly_client.get_accounts() for acc in accounts: attrs = acc.get("attributes", {}) existing = db.query(Account).filter_by(firefly_id=acc["id"]).first() if existing: existing.name = attrs.get("name") existing.balance = float(attrs.get("current_balance", 0)) existing.updated_at = datetime.utcnow() else: db.add(Account( firefly_id=acc["id"], name=attrs.get("name"), type=attrs.get("type"), balance=float(attrs.get("current_balance", 0)), currency_code=attrs.get("currency_code") )) # Sync transactions transactions = await firefly_client.get_transactions() for txn in transactions: attrs = txn.get("attributes", {}) existing = db.query(Transaction).filter_by(firefly_id=txn["id"]).first() if not existing: db.add(Transaction( firefly_id=txn["id"], date=datetime.fromisoformat(attrs.get("date")), amount=float(attrs.get("amount", 0)), description=attrs.get("description"), type=attrs.get("type"), notes=attrs.get("notes") )) db.commit() logger.info("Data sync completed successfully") except Exception as e: logger.error(f"Error syncing data: {e}") db.rollback() finally: db.close() def sync_job(): asyncio.run(sync_data()) def start_sync_scheduler(): if not scheduler.running: scheduler.add_job(sync_job, 'interval', minutes=settings.SYNC_INTERVAL_MINUTES) scheduler.start() logger.info(f"Sync scheduler started (interval: {settings.SYNC_INTERVAL_MINUTES} minutes)") def stop_sync_scheduler(): if scheduler.running: scheduler.shutdown() logger.info("Sync scheduler stopped") """, } created_files = [] for rel_path, content in files_content.items(): file_path = backend_base / rel_path try: file_path.parent.mkdir(parents=True, exist_ok=True) with open(file_path, 'w', encoding='utf-8') as f: f.write(content) created_files.append(str(file_path)) print(f"✓ Created: {file_path}") except Exception as e: print(f"✗ ERROR creating {file_path}: {e}") return 1 print() print("=" * 80) print(f"✓ All {len(created_files)} backend files created successfully!") print("=" * 80) print() print("Backend directory structure:") print(f" {backend_base}") print(f" ├── pyproject.toml") print(f" └── app/") print(f" ├── __init__.py") print(f" ├── config.py") print(f" ├── models.py") print(f" ├── database.py") print(f" ├── main.py") print(f" ├── routers/") print(f" │ ├── __init__.py") print(f" │ ├── transactions.py") print(f" │ ├── categories.py") print(f" │ ├── accounts.py") print(f" │ ├── reports.py") print(f" │ └── summary.py") print(f" ├── clients/") print(f" │ ├── __init__.py") print(f" │ └── firefly_client.py") print(f" └── services/") print(f" ├── __init__.py") print(f" └── sync_service.py") print() print("Files created:") for f in sorted(created_files): print(f" - {f}") return 0 if __name__ == "__main__": exit(main())