Initial commit
This commit is contained in:
795
init_project.py
Normal file
795
init_project.py
Normal file
@@ -0,0 +1,795 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Project initialization script for Firefly III Analytics
|
||||
Creates the complete directory structure and scaffolding
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def create_dir(path):
|
||||
"""Create directory if it doesn't exist"""
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
print(f"✓ Created directory: {path}")
|
||||
|
||||
def create_file(path, content):
|
||||
"""Create file with content"""
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f"✓ Created file: {path}")
|
||||
|
||||
# Backend files
|
||||
backend_pyproject = '''[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",
|
||||
]
|
||||
'''
|
||||
|
||||
backend_dockerfile = '''FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
||||
gcc \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pyproject.toml pyproject.toml
|
||||
COPY app/ app/
|
||||
|
||||
RUN pip install --no-cache-dir -e .
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
'''
|
||||
|
||||
backend_config = '''"""
|
||||
Firefly III Analytics Backend - Configuration
|
||||
"""
|
||||
import os
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings from environment variables"""
|
||||
|
||||
# Firefly III
|
||||
FIREFLY_URL: str = os.getenv("FIREFLY_URL", "https://firefly.scsimedia.duckdns.org")
|
||||
FIREFLY_API_TOKEN: str = os.getenv("FIREFLY_API_TOKEN", "")
|
||||
|
||||
# Database
|
||||
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./data/app.db")
|
||||
|
||||
# Sync
|
||||
SYNC_INTERVAL_MINUTES: int = int(os.getenv("SYNC_INTERVAL_MINUTES", "30"))
|
||||
|
||||
# API
|
||||
API_TIMEOUT: int = 30
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
settings = Settings()
|
||||
'''
|
||||
|
||||
backend_models = '''"""
|
||||
Database models for Firefly III Analytics
|
||||
"""
|
||||
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):
|
||||
"""Transaction model"""
|
||||
__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) # withdrawal, deposit, transfer
|
||||
category_id = Column(Integer, ForeignKey("categories.id"))
|
||||
from_account_id = Column(Integer, ForeignKey("accounts.id"))
|
||||
to_account_id = Column(Integer, ForeignKey("accounts.id"))
|
||||
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])
|
||||
from_account = relationship("Account", foreign_keys=[from_account_id])
|
||||
to_account = relationship("Account", foreign_keys=[to_account_id])
|
||||
|
||||
class Category(Base):
|
||||
"""Category model"""
|
||||
__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) # expense, revenue, etc
|
||||
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])
|
||||
|
||||
class Account(Base):
|
||||
"""Account model"""
|
||||
__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) # asset, expense, revenue, liability, etc
|
||||
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):
|
||||
"""Track sync operations"""
|
||||
__tablename__ = "sync_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
entity_type = Column(String) # transactions, categories, accounts
|
||||
last_sync = Column(DateTime)
|
||||
status = Column(String) # success, failure
|
||||
message = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
'''
|
||||
|
||||
backend_database = '''"""
|
||||
Database initialization and connection
|
||||
"""
|
||||
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():
|
||||
"""Initialize database tables"""
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
def get_db() -> Session:
|
||||
"""Get database session"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
'''
|
||||
|
||||
backend_firefly_client = '''"""
|
||||
Firefly III API Client
|
||||
"""
|
||||
import httpx
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from app.config import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FireflyClient:
|
||||
"""Client for Firefly III API"""
|
||||
|
||||
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]]:
|
||||
"""Fetch transactions from Firefly III"""
|
||||
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()
|
||||
data = response.json()
|
||||
return data.get("data", [])
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching transactions: {e}")
|
||||
return []
|
||||
|
||||
async def get_categories(self) -> List[Dict[str, Any]]:
|
||||
"""Fetch categories from Firefly III"""
|
||||
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()
|
||||
data = response.json()
|
||||
return data.get("data", [])
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching categories: {e}")
|
||||
return []
|
||||
|
||||
async def get_accounts(self) -> List[Dict[str, Any]]:
|
||||
"""Fetch accounts from Firefly III"""
|
||||
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()
|
||||
data = response.json()
|
||||
return data.get("data", [])
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching accounts: {e}")
|
||||
return []
|
||||
|
||||
firefly_client = FireflyClient()
|
||||
'''
|
||||
|
||||
backend_main = '''"""
|
||||
Firefly III Analytics Backend - Main Application
|
||||
"""
|
||||
import os
|
||||
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):
|
||||
"""Startup and shutdown events"""
|
||||
# Startup
|
||||
init_db()
|
||||
start_sync_scheduler()
|
||||
yield
|
||||
# Shutdown
|
||||
stop_sync_scheduler()
|
||||
|
||||
app = FastAPI(
|
||||
title="Firefly III Analytics API",
|
||||
description="Analytics and reporting for Firefly III",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000", "http://localhost"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
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():
|
||||
"""Health check"""
|
||||
return {"status": "ok", "message": "Firefly III Analytics API"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
'''
|
||||
|
||||
backend_routers_init = '''"""
|
||||
API routers
|
||||
"""
|
||||
'''
|
||||
|
||||
backend_routers_transactions = '''"""
|
||||
Transactions API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.services.transaction_service import TransactionService
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
router = APIRouter()
|
||||
transaction_service = TransactionService()
|
||||
|
||||
@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)
|
||||
):
|
||||
"""Get transactions with optional filtering"""
|
||||
filters = {}
|
||||
if start_date:
|
||||
filters["start_date"] = datetime.fromisoformat(start_date)
|
||||
if end_date:
|
||||
filters["end_date"] = datetime.fromisoformat(end_date)
|
||||
if category_id:
|
||||
filters["category_id"] = category_id
|
||||
|
||||
transactions = await transaction_service.get_transactions(db, filters)
|
||||
return {"data": transactions, "count": len(transactions)}
|
||||
'''
|
||||
|
||||
backend_routers_categories = '''"""
|
||||
Categories API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.services.category_service import CategoryService
|
||||
|
||||
router = APIRouter()
|
||||
category_service = CategoryService()
|
||||
|
||||
@router.get("/")
|
||||
async def get_categories(db: Session = Depends(get_db)):
|
||||
"""Get all categories"""
|
||||
categories = category_service.get_all_categories(db)
|
||||
return {"data": categories, "count": len(categories)}
|
||||
'''
|
||||
|
||||
backend_routers_accounts = '''"""
|
||||
Accounts API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.services.account_service import AccountService
|
||||
|
||||
router = APIRouter()
|
||||
account_service = AccountService()
|
||||
|
||||
@router.get("/")
|
||||
async def get_accounts(db: Session = Depends(get_db)):
|
||||
"""Get all accounts"""
|
||||
accounts = account_service.get_all_accounts(db)
|
||||
return {"data": accounts, "count": len(accounts)}
|
||||
'''
|
||||
|
||||
backend_routers_reports = '''"""
|
||||
Reports API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.services.report_service import ReportService
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
report_service = ReportService()
|
||||
|
||||
@router.get("/spending-by-category")
|
||||
async def spending_by_category(
|
||||
start_date: str = Query(...),
|
||||
end_date: str = Query(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get spending by category for date range"""
|
||||
start = datetime.fromisoformat(start_date)
|
||||
end = datetime.fromisoformat(end_date)
|
||||
data = await report_service.get_spending_by_category(db, start, end)
|
||||
return {"data": data}
|
||||
|
||||
@router.get("/income-vs-expenses")
|
||||
async def income_vs_expenses(
|
||||
start_date: str = Query(...),
|
||||
end_date: str = Query(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get income vs expenses for date range"""
|
||||
start = datetime.fromisoformat(start_date)
|
||||
end = datetime.fromisoformat(end_date)
|
||||
data = await report_service.get_income_vs_expenses(db, start, end)
|
||||
return {"data": data}
|
||||
|
||||
@router.get("/trends")
|
||||
async def trends(
|
||||
start_date: str = Query(...),
|
||||
end_date: str = Query(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get financial trends over time"""
|
||||
start = datetime.fromisoformat(start_date)
|
||||
end = datetime.fromisoformat(end_date)
|
||||
data = await report_service.get_trends(db, start, end)
|
||||
return {"data": data}
|
||||
|
||||
@router.get("/budget-vs-actual")
|
||||
async def budget_vs_actual(
|
||||
start_date: str = Query(...),
|
||||
end_date: str = Query(...),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get budget vs actual spending"""
|
||||
start = datetime.fromisoformat(start_date)
|
||||
end = datetime.fromisoformat(end_date)
|
||||
data = await report_service.get_budget_vs_actual(db, start, end)
|
||||
return {"data": data}
|
||||
'''
|
||||
|
||||
backend_routers_summary = '''"""
|
||||
Summary API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.services.summary_service import SummaryService
|
||||
|
||||
router = APIRouter()
|
||||
summary_service = SummaryService()
|
||||
|
||||
@router.get("/")
|
||||
async def get_summary(db: Session = Depends(get_db)):
|
||||
"""Get dashboard summary metrics"""
|
||||
summary = await summary_service.get_summary(db)
|
||||
return summary
|
||||
'''
|
||||
|
||||
backend_sync_service = '''"""
|
||||
Background sync service for Firefly III data
|
||||
"""
|
||||
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():
|
||||
"""Sync data from Firefly III"""
|
||||
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.type = attrs.get("type")
|
||||
existing.balance = float(attrs.get("current_balance", 0))
|
||||
existing.currency_code = attrs.get("currency_code")
|
||||
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():
|
||||
"""Wrapper for async sync job"""
|
||||
asyncio.run(sync_data())
|
||||
|
||||
def start_sync_scheduler():
|
||||
"""Start background 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():
|
||||
"""Stop background sync scheduler"""
|
||||
if scheduler.running:
|
||||
scheduler.shutdown()
|
||||
logger.info("Sync scheduler stopped")
|
||||
'''
|
||||
|
||||
backend_transaction_service = '''"""
|
||||
Transaction service business logic
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from app.models import Transaction
|
||||
from datetime import datetime
|
||||
|
||||
class TransactionService:
|
||||
def get_transactions(self, db: Session, filters: dict):
|
||||
"""Get transactions with filters"""
|
||||
query = db.query(Transaction)
|
||||
|
||||
if "start_date" in filters:
|
||||
query = query.filter(Transaction.date >= filters["start_date"])
|
||||
if "end_date" in filters:
|
||||
query = query.filter(Transaction.date <= filters["end_date"])
|
||||
if "category_id" in filters:
|
||||
query = query.filter(Transaction.category_id == filters["category_id"])
|
||||
|
||||
return query.order_by(Transaction.date.desc()).all()
|
||||
'''
|
||||
|
||||
backend_category_service = '''"""
|
||||
Category service business logic
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models import Category
|
||||
|
||||
class CategoryService:
|
||||
def get_all_categories(self, db: Session):
|
||||
"""Get all categories"""
|
||||
return db.query(Category).all()
|
||||
'''
|
||||
|
||||
backend_account_service = '''"""
|
||||
Account service business logic
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models import Account
|
||||
|
||||
class AccountService:
|
||||
def get_all_accounts(self, db: Session):
|
||||
"""Get all accounts"""
|
||||
return db.query(Account).all()
|
||||
'''
|
||||
|
||||
backend_report_service = '''"""
|
||||
Report service for generating analytics
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from app.models import Transaction, Category, Account
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
|
||||
class ReportService:
|
||||
async def get_spending_by_category(self, db: Session, start: datetime, end: datetime) -> Dict[str, Any]:
|
||||
"""Get spending breakdown by category"""
|
||||
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]
|
||||
}
|
||||
|
||||
async def get_income_vs_expenses(self, db: Session, start: datetime, end: datetime) -> Dict[str, Any]:
|
||||
"""Get income vs expenses"""
|
||||
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)
|
||||
}
|
||||
|
||||
async def get_trends(self, db: Session, start: datetime, end: datetime) -> Dict[str, Any]:
|
||||
"""Get financial trends over time"""
|
||||
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]
|
||||
}
|
||||
|
||||
async def get_budget_vs_actual(self, db: Session, start: datetime, end: datetime) -> Dict[str, Any]:
|
||||
"""Get budget vs actual spending (requires budget data)"""
|
||||
return {
|
||||
"period": {"start": start.isoformat(), "end": end.isoformat()},
|
||||
"data": [],
|
||||
"note": "Budget comparison requires budget data from Firefly III"
|
||||
}
|
||||
'''
|
||||
|
||||
backend_summary_service = '''"""
|
||||
Summary service for dashboard metrics
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from app.models import Transaction, Account
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class SummaryService:
|
||||
async def get_summary(self, db: Session):
|
||||
"""Get dashboard summary"""
|
||||
# Last 30 days
|
||||
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
|
||||
|
||||
last_30_income = db.query(func.sum(Transaction.amount)).filter(
|
||||
Transaction.date >= thirty_days_ago,
|
||||
Transaction.type == "deposit"
|
||||
).scalar() or 0
|
||||
|
||||
last_30_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(last_30_income),
|
||||
"expenses": float(last_30_expenses),
|
||||
"net": float(last_30_income - last_30_expenses)
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
def main():
|
||||
base_path = Path(os.getcwd())
|
||||
|
||||
# Create backend structure
|
||||
print("Setting up Firefly III Analytics Backend...")
|
||||
create_file("backend/pyproject.toml", backend_pyproject)
|
||||
create_file("backend/Dockerfile", backend_dockerfile)
|
||||
create_file("backend/app/__init__.py", "")
|
||||
create_file("backend/app/config.py", backend_config)
|
||||
create_file("backend/app/models.py", backend_models)
|
||||
create_file("backend/app/database.py", backend_database)
|
||||
create_file("backend/app/main.py", backend_main)
|
||||
|
||||
# Routers
|
||||
create_file("backend/app/routers/__init__.py", backend_routers_init)
|
||||
create_file("backend/app/routers/transactions.py", backend_routers_transactions)
|
||||
create_file("backend/app/routers/categories.py", backend_routers_categories)
|
||||
create_file("backend/app/routers/accounts.py", backend_routers_accounts)
|
||||
create_file("backend/app/routers/reports.py", backend_routers_reports)
|
||||
create_file("backend/app/routers/summary.py", backend_routers_summary)
|
||||
|
||||
# Clients
|
||||
create_file("backend/app/clients/__init__.py", "")
|
||||
create_file("backend/app/clients/firefly_client.py", backend_firefly_client)
|
||||
|
||||
# Services
|
||||
create_file("backend/app/services/__init__.py", "")
|
||||
create_file("backend/app/services/sync_service.py", backend_sync_service)
|
||||
create_file("backend/app/services/transaction_service.py", backend_transaction_service)
|
||||
create_file("backend/app/services/category_service.py", backend_category_service)
|
||||
create_file("backend/app/services/account_service.py", backend_account_service)
|
||||
create_file("backend/app/services/report_service.py", backend_report_service)
|
||||
create_file("backend/app/services/summary_service.py", backend_summary_service)
|
||||
|
||||
print("\n✓ Backend scaffolding complete!")
|
||||
print("\nNext steps:")
|
||||
print("1. Create .env file with your Firefly III API token")
|
||||
print("2. cd backend && python -m venv venv")
|
||||
print("3. source venv/bin/activate")
|
||||
print("4. pip install -e .")
|
||||
print("5. uvicorn app.main:app --reload")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user