Spaces:
Sleeping
Sleeping
EL GHAFRAOUI AYOUB
commited on
Commit
·
4099797
1
Parent(s):
1c468a7
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/db/__pycache__/database.cpython-312.pyc +0 -0
- app/db/database.py +99 -24
- app/main.py +49 -5
- app/routes/__pycache__/invoices.cpython-312.pyc +0 -0
- app/routes/invoices.py +55 -55
app/__pycache__/main.cpython-312.pyc
CHANGED
Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ
|
|
app/db/__pycache__/database.cpython-312.pyc
CHANGED
Binary files a/app/db/__pycache__/database.cpython-312.pyc and b/app/db/__pycache__/database.cpython-312.pyc differ
|
|
app/db/database.py
CHANGED
@@ -1,43 +1,118 @@
|
|
1 |
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
2 |
from sqlalchemy.orm import sessionmaker
|
|
|
|
|
|
|
|
|
|
|
3 |
from app.db.models import Base
|
4 |
|
5 |
-
#
|
|
|
|
|
|
|
|
|
6 |
DATABASE_URL = "sqlite+aiosqlite:///./sql_app.db"
|
7 |
|
8 |
-
engine
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
)
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
return result
|
18 |
-
|
19 |
-
|
20 |
-
async def init_db():
|
21 |
-
async with engine.begin() as conn:
|
22 |
-
# Check if the database already has tables
|
23 |
-
if not await database_exists(conn):
|
24 |
-
print("Initializing database: Creating tables...")
|
25 |
-
await conn.run_sync(Base.metadata.create_all)
|
26 |
-
else:
|
27 |
-
print("Database already exists. Skipping initialization.")
|
28 |
-
|
29 |
-
|
30 |
|
31 |
async def get_db() -> AsyncSession:
|
32 |
-
async with
|
|
|
33 |
try:
|
34 |
yield session
|
35 |
-
|
36 |
-
|
37 |
await session.rollback()
|
38 |
-
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
finally:
|
40 |
await session.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
|
|
|
43 |
###sdf
|
|
|
1 |
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
2 |
from sqlalchemy.orm import sessionmaker
|
3 |
+
from sqlalchemy.exc import SQLAlchemyError
|
4 |
+
from sqlalchemy.pool import QueuePool
|
5 |
+
from fastapi import HTTPException
|
6 |
+
import asyncio
|
7 |
+
import logging
|
8 |
from app.db.models import Base
|
9 |
|
10 |
+
# Set up logging
|
11 |
+
logging.basicConfig(level=logging.INFO)
|
12 |
+
logger = logging.getLogger(__name__)
|
13 |
+
|
14 |
+
# Use SQLite with aiosqlite and connection pooling
|
15 |
DATABASE_URL = "sqlite+aiosqlite:///./sql_app.db"
|
16 |
|
17 |
+
# Configure the engine with connection pooling and timeouts
|
18 |
+
engine = create_async_engine(
|
19 |
+
DATABASE_URL,
|
20 |
+
echo=True,
|
21 |
+
pool_size=20, # Maximum number of connections in the pool
|
22 |
+
max_overflow=10, # Maximum number of connections that can be created beyond pool_size
|
23 |
+
pool_timeout=30, # Timeout for getting a connection from the pool
|
24 |
+
pool_recycle=1800, # Recycle connections after 30 minutes
|
25 |
+
pool_pre_ping=True, # Enable connection health checks
|
26 |
+
poolclass=QueuePool
|
27 |
)
|
28 |
|
29 |
+
# Configure session with retry logic
|
30 |
+
AsyncSessionLocal = sessionmaker(
|
31 |
+
engine,
|
32 |
+
class_=AsyncSession,
|
33 |
+
expire_on_commit=False
|
34 |
+
)
|
35 |
|
36 |
+
# Semaphore to limit concurrent database operations
|
37 |
+
MAX_CONCURRENT_DB_OPS = 10
|
38 |
+
db_semaphore = asyncio.Semaphore(MAX_CONCURRENT_DB_OPS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
|
40 |
async def get_db() -> AsyncSession:
|
41 |
+
async with db_semaphore: # Limit concurrent database operations
|
42 |
+
session = AsyncSessionLocal()
|
43 |
try:
|
44 |
yield session
|
45 |
+
except SQLAlchemyError as e:
|
46 |
+
logger.error(f"Database error: {str(e)}")
|
47 |
await session.rollback()
|
48 |
+
raise HTTPException(
|
49 |
+
status_code=503,
|
50 |
+
detail="Database service temporarily unavailable. Please try again."
|
51 |
+
)
|
52 |
+
except Exception as e:
|
53 |
+
logger.error(f"Unexpected error: {str(e)}")
|
54 |
+
await session.rollback()
|
55 |
+
raise HTTPException(
|
56 |
+
status_code=500,
|
57 |
+
detail="An unexpected error occurred. Please try again."
|
58 |
+
)
|
59 |
finally:
|
60 |
await session.close()
|
61 |
+
|
62 |
+
# Rate limiting configuration
|
63 |
+
from fastapi import Request
|
64 |
+
import time
|
65 |
+
from collections import defaultdict
|
66 |
+
|
67 |
+
class RateLimiter:
|
68 |
+
def __init__(self, requests_per_minute=60):
|
69 |
+
self.requests_per_minute = requests_per_minute
|
70 |
+
self.requests = defaultdict(list)
|
71 |
+
|
72 |
+
def is_allowed(self, client_ip: str) -> bool:
|
73 |
+
now = time.time()
|
74 |
+
minute_ago = now - 60
|
75 |
+
|
76 |
+
# Clean old requests
|
77 |
+
self.requests[client_ip] = [req_time for req_time in self.requests[client_ip]
|
78 |
+
if req_time > minute_ago]
|
79 |
+
|
80 |
+
# Check if allowed
|
81 |
+
if len(self.requests[client_ip]) >= self.requests_per_minute:
|
82 |
+
return False
|
83 |
+
|
84 |
+
# Add new request
|
85 |
+
self.requests[client_ip].append(now)
|
86 |
+
return True
|
87 |
+
|
88 |
+
rate_limiter = RateLimiter(requests_per_minute=60)
|
89 |
+
|
90 |
+
# Add this to your database initialization
|
91 |
+
async def init_db():
|
92 |
+
try:
|
93 |
+
async with engine.begin() as conn:
|
94 |
+
# Check if the database already has tables
|
95 |
+
if not await database_exists(conn):
|
96 |
+
logger.info("Initializing database: Creating tables...")
|
97 |
+
await conn.run_sync(Base.metadata.create_all)
|
98 |
+
logger.info("Database initialization completed successfully")
|
99 |
+
else:
|
100 |
+
logger.info("Database already exists. Skipping initialization.")
|
101 |
+
except Exception as e:
|
102 |
+
logger.error(f"Error initializing database: {str(e)}")
|
103 |
+
raise
|
104 |
+
|
105 |
+
async def database_exists(conn) -> bool:
|
106 |
+
"""Check if any tables exist in the database."""
|
107 |
+
try:
|
108 |
+
result = await conn.run_sync(
|
109 |
+
lambda sync_conn: sync_conn.dialect.has_table(sync_conn, "invoices")
|
110 |
+
)
|
111 |
+
return result
|
112 |
+
except Exception as e:
|
113 |
+
logger.error(f"Error checking database existence: {str(e)}")
|
114 |
+
return False
|
115 |
|
116 |
|
117 |
+
|
118 |
###sdf
|
app/main.py
CHANGED
@@ -1,11 +1,19 @@
|
|
1 |
-
from fastapi import FastAPI, Request
|
2 |
from fastapi.staticfiles import StaticFiles
|
3 |
from fastapi.templating import Jinja2Templates
|
4 |
from .routes import invoices
|
5 |
-
from app.db.database import init_db
|
6 |
import os
|
7 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
8 |
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
# Load environment variables
|
11 |
load_dotenv()
|
@@ -13,6 +21,32 @@ load_dotenv()
|
|
13 |
# Get the absolute path to the app directory
|
14 |
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
app = FastAPI(
|
18 |
title="Invoice Generator",
|
@@ -20,13 +54,17 @@ app = FastAPI(
|
|
20 |
version="1.0.0"
|
21 |
)
|
22 |
|
23 |
-
#
|
|
|
|
|
|
|
24 |
app.add_middleware(
|
25 |
CORSMiddleware,
|
26 |
-
allow_origins=["*"], # In production, replace with
|
27 |
allow_credentials=True,
|
28 |
allow_methods=["*"],
|
29 |
allow_headers=["*"],
|
|
|
30 |
)
|
31 |
|
32 |
# Mount static files with absolute path
|
@@ -40,7 +78,13 @@ app.include_router(invoices.router)
|
|
40 |
|
41 |
@app.on_event("startup")
|
42 |
async def startup_event():
|
|
|
43 |
await init_db()
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
# Root endpoint to serve the HTML page
|
46 |
@app.get("/")
|
@@ -50,7 +94,7 @@ async def root(request: Request):
|
|
50 |
# Health check endpoint
|
51 |
@app.get("/health")
|
52 |
async def health_check():
|
53 |
-
return {"status": "healthy"}
|
54 |
|
55 |
@app.get("/history")
|
56 |
async def history_page(request: Request):
|
|
|
1 |
+
from fastapi import FastAPI, Request, HTTPException
|
2 |
from fastapi.staticfiles import StaticFiles
|
3 |
from fastapi.templating import Jinja2Templates
|
4 |
from .routes import invoices
|
5 |
+
from app.db.database import init_db, rate_limiter
|
6 |
import os
|
7 |
from fastapi.middleware.cors import CORSMiddleware
|
8 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
9 |
from dotenv import load_dotenv
|
10 |
+
import logging
|
11 |
+
from typing import Callable
|
12 |
+
import time
|
13 |
+
|
14 |
+
# Set up logging
|
15 |
+
logging.basicConfig(level=logging.INFO)
|
16 |
+
logger = logging.getLogger(__name__)
|
17 |
|
18 |
# Load environment variables
|
19 |
load_dotenv()
|
|
|
21 |
# Get the absolute path to the app directory
|
22 |
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
23 |
|
24 |
+
class RateLimitMiddleware(BaseHTTPMiddleware):
|
25 |
+
async def dispatch(self, request: Request, call_next: Callable):
|
26 |
+
# Get client IP
|
27 |
+
client_ip = request.client.host
|
28 |
+
|
29 |
+
# Check rate limit
|
30 |
+
if not rate_limiter.is_allowed(client_ip):
|
31 |
+
logger.warning(f"Rate limit exceeded for IP: {client_ip}")
|
32 |
+
raise HTTPException(
|
33 |
+
status_code=429,
|
34 |
+
detail="Too many requests. Please try again later."
|
35 |
+
)
|
36 |
+
|
37 |
+
# Process request
|
38 |
+
start_time = time.time()
|
39 |
+
response = await call_next(request)
|
40 |
+
process_time = time.time() - start_time
|
41 |
+
|
42 |
+
# Log request details
|
43 |
+
logger.info(
|
44 |
+
f"Request: {request.method} {request.url.path} "
|
45 |
+
f"Client: {client_ip} "
|
46 |
+
f"Process time: {process_time:.2f}s"
|
47 |
+
)
|
48 |
+
|
49 |
+
return response
|
50 |
|
51 |
app = FastAPI(
|
52 |
title="Invoice Generator",
|
|
|
54 |
version="1.0.0"
|
55 |
)
|
56 |
|
57 |
+
# Add rate limiting middleware
|
58 |
+
app.add_middleware(RateLimitMiddleware)
|
59 |
+
|
60 |
+
# Configure CORS with more specific settings
|
61 |
app.add_middleware(
|
62 |
CORSMiddleware,
|
63 |
+
allow_origins=["*"], # In production, replace with specific domains
|
64 |
allow_credentials=True,
|
65 |
allow_methods=["*"],
|
66 |
allow_headers=["*"],
|
67 |
+
max_age=3600, # Cache preflight requests for 1 hour
|
68 |
)
|
69 |
|
70 |
# Mount static files with absolute path
|
|
|
78 |
|
79 |
@app.on_event("startup")
|
80 |
async def startup_event():
|
81 |
+
logger.info("Starting application...")
|
82 |
await init_db()
|
83 |
+
logger.info("Application started successfully")
|
84 |
+
|
85 |
+
@app.on_event("shutdown")
|
86 |
+
async def shutdown_event():
|
87 |
+
logger.info("Shutting down application...")
|
88 |
|
89 |
# Root endpoint to serve the HTML page
|
90 |
@app.get("/")
|
|
|
94 |
# Health check endpoint
|
95 |
@app.get("/health")
|
96 |
async def health_check():
|
97 |
+
return {"status": "healthy", "timestamp": time.time()}
|
98 |
|
99 |
@app.get("/history")
|
100 |
async def history_page(request: Request):
|
app/routes/__pycache__/invoices.cpython-312.pyc
CHANGED
Binary files a/app/routes/__pycache__/invoices.cpython-312.pyc and b/app/routes/__pycache__/invoices.cpython-312.pyc differ
|
|
app/routes/invoices.py
CHANGED
@@ -23,64 +23,64 @@ async def create_new_invoice(
|
|
23 |
db: AsyncSession = Depends(get_db)
|
24 |
):
|
25 |
try:
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
db.add(db_invoice)
|
55 |
-
await db.commit()
|
56 |
-
await db.refresh(db_invoice)
|
57 |
-
|
58 |
-
# Create invoice items
|
59 |
-
for item_data in invoice.items:
|
60 |
-
db_item = InvoiceItem(
|
61 |
-
invoice_id=db_invoice.id,
|
62 |
-
description=item_data.description,
|
63 |
-
unit=item_data.unit,
|
64 |
-
quantity=item_data.quantity,
|
65 |
-
length=item_data.length,
|
66 |
-
unit_price=item_data.unit_price,
|
67 |
-
total_price=item_data.total_price
|
68 |
)
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
except Exception as e:
|
83 |
-
|
84 |
raise HTTPException(status_code=500, detail=str(e))
|
85 |
|
86 |
@router.get("/{invoice_id}", response_model=InvoiceResponse)
|
|
|
23 |
db: AsyncSession = Depends(get_db)
|
24 |
):
|
25 |
try:
|
26 |
+
async with db.begin():
|
27 |
+
# Get current max ID
|
28 |
+
query = select(func.max(Invoice.id))
|
29 |
+
result = await db.execute(query)
|
30 |
+
current_max_id = result.scalar() or 0
|
31 |
+
next_id = current_max_id + 1
|
32 |
+
|
33 |
+
# Extract client type from the invoice number
|
34 |
+
client_type = invoice.invoice_number.split('/')[1]
|
35 |
+
|
36 |
+
# Create new invoice number
|
37 |
+
new_invoice_number = f"DCP/{client_type}/{next_id:04d}"
|
38 |
+
|
39 |
+
# Create invoice with the new number
|
40 |
+
db_invoice = Invoice(
|
41 |
+
id=next_id,
|
42 |
+
invoice_number=new_invoice_number,
|
43 |
+
date=invoice.date,
|
44 |
+
project=invoice.project,
|
45 |
+
client_name=invoice.client_name,
|
46 |
+
client_phone=invoice.client_phone,
|
47 |
+
address=invoice.address,
|
48 |
+
ville=invoice.ville,
|
49 |
+
total_ht=invoice.total_ht,
|
50 |
+
tax=invoice.tax,
|
51 |
+
total_ttc=invoice.total_ttc,
|
52 |
+
frame_number=invoice.frame_number,
|
53 |
+
commercial=invoice.commercial
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
)
|
55 |
+
|
56 |
+
# Create all invoice items
|
57 |
+
db_items = [
|
58 |
+
InvoiceItem(
|
59 |
+
invoice_id=next_id,
|
60 |
+
description=item_data.description,
|
61 |
+
unit=item_data.unit,
|
62 |
+
quantity=item_data.quantity,
|
63 |
+
length=item_data.length,
|
64 |
+
unit_price=item_data.unit_price,
|
65 |
+
total_price=item_data.total_price
|
66 |
+
)
|
67 |
+
for item_data in invoice.items
|
68 |
+
]
|
69 |
+
|
70 |
+
# Add all objects to the session
|
71 |
+
db.add(db_invoice)
|
72 |
+
db.add_all(db_items)
|
73 |
+
await db.flush()
|
74 |
|
75 |
+
# Query the complete invoice with items
|
76 |
+
query = select(Invoice).options(selectinload(Invoice.items)).filter(Invoice.id == next_id)
|
77 |
+
result = await db.execute(query)
|
78 |
+
complete_invoice = result.scalar_one()
|
79 |
+
|
80 |
+
return complete_invoice
|
81 |
+
|
82 |
except Exception as e:
|
83 |
+
logger.error(f"Error creating invoice: {str(e)}", exc_info=True)
|
84 |
raise HTTPException(status_code=500, detail=str(e))
|
85 |
|
86 |
@router.get("/{invoice_id}", response_model=InvoiceResponse)
|