LogicGoInfotechSpaces commited on
Commit
1fd3202
·
verified ·
1 Parent(s): 73256f5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +160 -83
app.py CHANGED
@@ -5,7 +5,7 @@ import os
5
  from collections import defaultdict
6
  from datetime import datetime, timezone
7
  from typing import Dict, List, Optional, Tuple
8
-
9
  from bson import ObjectId
10
  from dotenv import load_dotenv
11
  from fastapi import FastAPI, HTTPException
@@ -32,7 +32,11 @@ class MonthlyExpense(BaseModel):
32
  year: int
33
  month: int
34
  total: float = Field(..., description="Total expenses recorded for the month")
35
-
 
 
 
 
36
 
37
  class CategoryPrediction(BaseModel):
38
  headCategoryId: str
@@ -56,6 +60,7 @@ class MongoConnection:
56
  self._database = self._client.get_default_database()
57
  self.transactions: Collection = self._database["transactions"]
58
  self.headcategories: Collection = self._database["headcategories"]
 
59
 
60
 
61
  mongo = MongoConnection()
@@ -82,6 +87,31 @@ def index_to_month(idx: int) -> Tuple[int, int]:
82
  year = idx // 12
83
  month = (idx % 12) + 1
84
  return year, month
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # ------------------------------------------------
86
 
87
  # ----------------- Time series utilities -----------------
@@ -466,98 +496,145 @@ def _predict_next_month(history: List[MonthlyExpense]) -> float:
466
 
467
 
468
  # ----------------- API endpoint -----------------
469
- @app.get("/users/{user_id}/expense-prediction", response_model=PredictionResponse)
470
- def predict_expense(user_id: str) -> PredictionResponse:
 
 
471
  try:
472
  user_object_id = ObjectId(user_id)
473
- except Exception as exc:
474
- raise HTTPException(status_code=400, detail="Invalid user id") from exc
475
-
476
- now = datetime.now(timezone.utc)
477
- # fetch up to MAX_HISTORY_MONTHS of history
478
- start_period = _shift_months(_first_day_of_month(now), -MAX_HISTORY_MONTHS + 1)
479
- prediction_month = _shift_months(_first_day_of_month(now), 1)
480
-
481
- pipeline = [
482
- {
483
- "$match": {
484
- "user": user_object_id,
485
- "type": "EXPENSE",
486
- "headCategory": {"$ne": None},
487
- "date": {"$gte": start_period},
488
- }
489
- },
490
- {
491
- "$project": {
492
- "amount": 1,
493
- "headCategory": 1,
494
- "year": {"$year": "$date"},
495
- "month": {"$month": "$date"},
496
- }
497
- },
498
- {
499
- "$group": {
500
- "_id": {
501
- "headCategory": "$headCategory",
502
- "year": "$year",
503
- "month": "$month",
504
- },
505
- "total": {"$sum": "$amount"},
506
- }
507
- },
508
- {
509
- "$lookup": {
510
- "from": "headcategories",
511
- "localField": "_id.headCategory",
512
- "foreignField": "_id",
513
- "as": "headCategoryDoc",
514
- }
515
- },
516
- {"$unwind": "$headCategoryDoc"},
517
- {"$sort": {"_id.headCategory": 1, "_id.year": 1, "_id.month": 1}},
518
- ]
519
-
520
- results = list(mongo.transactions.aggregate(pipeline))
521
-
522
- grouped: Dict[ObjectId, Dict[str, List[MonthlyExpense]]] = defaultdict(lambda: {"history": []})
523
-
524
- for item in results:
525
- head_category_id: ObjectId = item["_id"]["headCategory"]
526
- category_record = grouped[head_category_id]
527
- category_record["title"] = item["headCategoryDoc"].get("title", "Unknown")
528
- category_record["history"].append(
529
- MonthlyExpense(
530
- year=item["_id"]["year"],
531
- month=item["_id"]["month"],
532
- total=float(item["total"]),
533
- )
534
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
- categories: List[CategoryPrediction] = []
537
- for head_category_id, record in grouped.items():
538
- history = sorted(record["history"], key=lambda doc: (doc.year, doc.month))
539
- predicted_total = _predict_next_month(history)
540
-
541
- categories.append(
542
- CategoryPrediction(
543
- headCategoryId=str(head_category_id),
544
- title=record.get("title", "Unknown"),
545
- history=history,
546
- predictionMonth=MonthlyExpense(
547
- year=prediction_month.year,
548
- month=prediction_month.month,
549
- total=predicted_total,
550
- ),
 
551
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  )
553
 
554
- return PredictionResponse(userId=user_id, categories=categories)
 
 
 
 
 
 
 
 
555
 
556
 
557
- # Optional: health check
558
  @app.get("/health")
559
  def health():
560
- return {"status": "healthy"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
 
562
 
563
 
 
5
  from collections import defaultdict
6
  from datetime import datetime, timezone
7
  from typing import Dict, List, Optional, Tuple
8
+ from time import perf_counter
9
  from bson import ObjectId
10
  from dotenv import load_dotenv
11
  from fastapi import FastAPI, HTTPException
 
32
  year: int
33
  month: int
34
  total: float = Field(..., description="Total expenses recorded for the month")
35
+
36
+ class APIResponse(BaseModel):
37
+ status: str
38
+ message: str
39
+ data: Optional[PredictionResponse] = None
40
 
41
  class CategoryPrediction(BaseModel):
42
  headCategoryId: str
 
60
  self._database = self._client.get_default_database()
61
  self.transactions: Collection = self._database["transactions"]
62
  self.headcategories: Collection = self._database["headcategories"]
63
+ self.api_logs: Collection = self._database["api_logs"]
64
 
65
 
66
  mongo = MongoConnection()
 
87
  year = idx // 12
88
  month = (idx % 12) + 1
89
  return year, month
90
+
91
+ def log_api_event(
92
+ name: str,
93
+ status: str,
94
+ response_time: float,
95
+ user_id: Optional[str] = None,
96
+ error_message: Optional[str] = None,
97
+ ):
98
+ payload = {
99
+ "name": name,
100
+ "status": status,
101
+ "response_time": round(response_time, 3),
102
+ "user_id": user_id or "anonymous",
103
+ "timestamp": datetime.now(timezone.utc),
104
+ }
105
+
106
+ if error_message:
107
+ payload["error_message"] = error_message
108
+
109
+ try:
110
+ mongo.api_logs.insert_one(payload)
111
+ except Exception:
112
+ # never crash API because of logging
113
+ pass
114
+
115
  # ------------------------------------------------
116
 
117
  # ----------------- Time series utilities -----------------
 
496
 
497
 
498
  # ----------------- API endpoint -----------------
499
+ @app.get("/users/{user_id}/expense-prediction",response_model=APIResponse,)
500
+ def predict_expense(user_id: str):
501
+ start_time = perf_counter()
502
+
503
  try:
504
  user_object_id = ObjectId(user_id)
505
+ except Exception:
506
+ log_api_event(
507
+ name="Expense Prediction",
508
+ status="failed",
509
+ response_time=0,
510
+ user_id=user_id,
511
+ error_message="Invalid user id",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  )
513
+ raise HTTPException(status_code=400, detail="Invalid user id")
514
+
515
+ try:
516
+ now = datetime.now(timezone.utc)
517
+ start_period = _shift_months(_first_day_of_month(now), -MAX_HISTORY_MONTHS + 1)
518
+ prediction_month = _shift_months(_first_day_of_month(now), 1)
519
+
520
+ pipeline = [
521
+ {
522
+ "$match": {
523
+ "user": user_object_id,
524
+ "type": "EXPENSE",
525
+ "headCategory": {"$ne": None},
526
+ "date": {"$gte": start_period},
527
+ }
528
+ },
529
+ {
530
+ "$project": {
531
+ "amount": 1,
532
+ "headCategory": 1,
533
+ "year": {"$year": "$date"},
534
+ "month": {"$month": "$date"},
535
+ }
536
+ },
537
+ {
538
+ "$group": {
539
+ "_id": {
540
+ "headCategory": "$headCategory",
541
+ "year": "$year",
542
+ "month": "$month",
543
+ },
544
+ "total": {"$sum": "$amount"},
545
+ }
546
+ },
547
+ {
548
+ "$lookup": {
549
+ "from": "headcategories",
550
+ "localField": "_id.headCategory",
551
+ "foreignField": "_id",
552
+ "as": "headCategoryDoc",
553
+ }
554
+ },
555
+ {"$unwind": "$headCategoryDoc"},
556
+ {"$sort": {"_id.headCategory": 1, "_id.year": 1, "_id.month": 1}},
557
+ ]
558
+
559
+ results = list(mongo.transactions.aggregate(pipeline))
560
+
561
+ grouped: Dict[ObjectId, Dict[str, List[MonthlyExpense]]] = defaultdict(lambda: {"history": []})
562
+
563
+ for item in results:
564
+ head_category_id: ObjectId = item["_id"]["headCategory"]
565
+ category_record = grouped[head_category_id]
566
+ category_record["title"] = item["headCategoryDoc"].get("title", "Unknown")
567
+ category_record["history"].append(
568
+ MonthlyExpense(
569
+ year=item["_id"]["year"],
570
+ month=item["_id"]["month"],
571
+ total=float(item["total"]),
572
+ )
573
+ )
574
 
575
+ categories: List[CategoryPrediction] = []
576
+ for head_category_id, record in grouped.items():
577
+ history = sorted(record["history"], key=lambda doc: (doc.year, doc.month))
578
+ predicted_total = _predict_next_month(history)
579
+
580
+ categories.append(
581
+ CategoryPrediction(
582
+ headCategoryId=str(head_category_id),
583
+ title=record.get("title", "Unknown"),
584
+ history=history,
585
+ predictionMonth=MonthlyExpense(
586
+ year=prediction_month.year,
587
+ month=prediction_month.month,
588
+ total=predicted_total,
589
+ ),
590
+ )
591
  )
592
+
593
+ response_data = PredictionResponse(userId=user_id, categories=categories)
594
+
595
+ log_api_event(
596
+ name="Expense Prediction",
597
+ status="success",
598
+ response_time=perf_counter() - start_time,
599
+ user_id=user_id,
600
+ )
601
+
602
+ return APIResponse(
603
+ status="success",
604
+ message="Expense prediction generated successfully",
605
+ data=response_data,
606
  )
607
 
608
+ except Exception as exc:
609
+ log_api_event(
610
+ name="Expense Prediction",
611
+ status="failed",
612
+ response_time=perf_counter() - start_time,
613
+ user_id=user_id,
614
+ error_message=str(exc),
615
+ )
616
+ raise HTTPException(status_code=500, detail="Internal server error")
617
 
618
 
 
619
  @app.get("/health")
620
  def health():
621
+ try:
622
+ mongo._client.admin.command("ping")
623
+ return {
624
+ "status": "ok",
625
+ "message": "Service is healthy",
626
+ "timestamp": datetime.now(timezone.utc),
627
+ }
628
+ except Exception as exc:
629
+ raise HTTPException(
630
+ status_code=503,
631
+ detail={
632
+ "status": "down",
633
+ "message": "Database connectivity failed",
634
+ "error": str(exc),
635
+ },
636
+ )
637
+
638
 
639
 
640