EL GHAFRAOUI AYOUB commited on
Commit
4099797
·
1 Parent(s): 1c468a7
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
- # Use SQLite with aiosqlite
 
 
 
 
6
  DATABASE_URL = "sqlite+aiosqlite:///./sql_app.db"
7
 
8
- engine = create_async_engine(DATABASE_URL, echo=True)
9
- AsyncSessionLocal = sessionmaker(
10
- engine, class_=AsyncSession, expire_on_commit=False
 
 
 
 
 
 
 
11
  )
12
 
 
 
 
 
 
 
13
 
14
- async def database_exists(conn) -> bool:
15
- """Check if any tables exist in the database."""
16
- result = await conn.run_sync(lambda sync_conn: sync_conn.dialect.has_table(sync_conn, "your_table_name"))
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 AsyncSessionLocal() as session:
 
33
  try:
34
  yield session
35
- await session.commit()
36
- except Exception:
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
- # Configure CORS
 
 
 
24
  app.add_middleware(
25
  CORSMiddleware,
26
- allow_origins=["*"], # In production, replace with your frontend domain
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
- # Get current max ID
27
- query = select(func.max(Invoice.id))
28
- result = await db.execute(query)
29
- current_max_id = result.scalar() or 0
30
- next_id = current_max_id + 1
31
-
32
- # Extract client type from the invoice number
33
- client_type = invoice.invoice_number.split('/')[1]
34
-
35
- # Create new invoice number
36
- new_invoice_number = f"DCP/{client_type}/{next_id:04d}"
37
-
38
- # Create invoice with the new number
39
- db_invoice = Invoice(
40
- id=next_id,
41
- invoice_number=new_invoice_number,
42
- date=invoice.date,
43
- project=invoice.project,
44
- client_name=invoice.client_name,
45
- client_phone=invoice.client_phone,
46
- address=invoice.address,
47
- total_ht=invoice.total_ht,
48
- tax=invoice.tax,
49
- total_ttc=invoice.total_ttc,
50
- frame_number=invoice.frame_number,
51
- commercial=invoice.commercial
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
- db.add(db_item)
70
-
71
- await db.commit()
72
- await db.refresh(db_invoice)
73
-
74
- # Explicitly load the items relationship
75
- result = await db.execute(
76
- select(Invoice)
77
- .options(selectinload(Invoice.items))
78
- .filter(Invoice.id == db_invoice.id)
79
- )
80
- return result.scalar_one()
 
 
 
 
 
 
 
81
 
 
 
 
 
 
 
 
82
  except Exception as e:
83
- await db.rollback()
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)