Skip to content

Commit b14464a

Browse files
authored
Merge pull request #13 from bandprotocol/BST-549-refactor-app
[refactor] Refactored app
2 parents cca1ab5 + efd54af commit b14464a

File tree

14 files changed

+353
-258
lines changed

14 files changed

+353
-258
lines changed

app/config.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
from typing import Literal
2+
13
from pydantic import BaseSettings, HttpUrl
24

5+
MODES = Literal["production", "development"]
6+
LOG_LEVELS = Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"]
7+
38

49
class Settings(BaseSettings):
5-
MODE: str = "development"
6-
LOG_LEVEL: str = "INFO"
10+
MODE: MODES = "development"
11+
LOG_LEVEL: LOG_LEVELS = "INFO"
712

813
# VERIFICATION
914
VERIFY_REQUEST_URL: HttpUrl
1015
ALLOWED_DATA_SOURCE_IDS: list[int]
1116
CACHE_SIZE: int = 1000
12-
TTL_TIME: str = "10m"
17+
TTL: str = "10m"
1318
MAX_DELAY_VERIFICATION: int = 0
1419

1520
# ADAPTER

app/main.py

Lines changed: 53 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,54 @@
11
import logging
2-
from functools import lru_cache
3-
from fastapi import Depends, FastAPI, Request
2+
from typing import Any
3+
4+
from fastapi import Depends, Request, FastAPI, HTTPException
45
from httpx import HTTPStatusError
56
from pytimeparse.timeparse import timeparse
6-
from fastapi import HTTPException
77

88
from adapter import init_adapter
9-
from app import config
9+
from app.config import Settings
1010
from app.report import init_db
11-
from app.report.middlewares import CollectVerifyData, CollectRequestData
12-
from app.report.models import Verify
11+
from app.report.middlewares import CollectRequestData
12+
from app.report.models import Verify, StatusReport, GatewayInfo
1313
from app.utils.cache import Cache
14-
from app.utils.types import VerifyErrorType
15-
from app.utils.helper import is_allow_data_source_id, get_band_signature_hash, verify_request, get_band_signature_hash
14+
from app.utils.helper import is_data_source_id_allowed, verify_request_from_bandchain, get_band_signature_hash
1615
from app.utils.log_config import init_loggers
16+
from app.utils.types import VerifyErrorType
1717

1818
app = FastAPI()
1919

20+
# Get setting
21+
settings = Settings()
2022

21-
@lru_cache()
22-
def get_settings():
23-
return config.Settings()
24-
25-
26-
settings = get_settings()
27-
28-
# create logger
23+
# Setup logger
2924
init_loggers(log_level=settings.LOG_LEVEL)
3025
log = logging.getLogger("pds_gateway_log")
31-
32-
3326
log.info(f"GATEWAY_MODE: {settings.MODE}")
3427

35-
36-
app.state.cache_data = Cache(settings.CACHE_SIZE, timeparse(settings.TTL_TIME))
28+
# Setup app state
29+
app.state.cache_data = Cache(settings.CACHE_SIZE, timeparse(settings.TTL))
3730
app.state.db = init_db(settings.MONGO_DB_URL, settings.COLLECTION_DB_NAME, log)
3831
app.state.adapter = init_adapter(settings.ADAPTER_TYPE, settings.ADAPTER_NAME)
3932

4033

41-
@CollectVerifyData(db=app.state.db)
42-
async def verify(request: Request, settings: config.Settings = settings):
43-
if settings.MODE == "production":
44-
# pass verify if already cache
45-
if app.state.cache_data.get_data(get_band_signature_hash(request.headers)):
46-
return Verify(response_code=200, is_delay=False)
47-
48-
verified = await verify_request(request.headers, settings.VERIFY_REQUEST_URL, settings.MAX_DELAY_VERIFICATION)
49-
50-
if not is_allow_data_source_id(int(verified["data_source_id"]), settings.ALLOWED_DATA_SOURCE_IDS):
51-
raise HTTPException(
52-
status_code=401,
53-
detail={
54-
"verify_error_type": VerifyErrorType.UNSUPPORTED_DS_ID.value,
55-
"error_msg": f'wrong data_source_id. expected {settings.ALLOWED_DATA_SOURCE_IDS}, got {verified["data_source_id"]}.',
56-
},
34+
async def verify_request(req: Request) -> Verify:
35+
"""Verifies if the request originated from BandChain"""
36+
# Skip verification if request has already been cached
37+
if settings.MODE == "production" and app.state.cache_data.get_data(get_band_signature_hash(req.headers)):
38+
# Verify the request is from BandChain
39+
verified = await verify_request_from_bandchain(
40+
req.headers, settings.VERIFY_REQUEST_URL, settings.MAX_DELAY_VERIFICATION
41+
)
42+
43+
# Checks if the data source requesting is whitelisted
44+
if not is_data_source_id_allowed(int(verified["data_source_id"]), settings.ALLOWED_DATA_SOURCE_IDS):
45+
return Verify(
46+
response_code=401,
47+
is_delay=None,
48+
error_type=VerifyErrorType.UNSUPPORTED_DS_ID.value,
49+
error_msg="wrong data_source_id. expected {allowed}, got {actual}.".format(
50+
allowed=settings.ALLOWED_DATA_SOURCE_IDS, actual=verified["data_source_id"]
51+
),
5752
)
5853

5954
return Verify(response_code=200, is_delay=verified["is_delay"])
@@ -63,30 +58,26 @@ async def verify(request: Request, settings: config.Settings = settings):
6358

6459
@app.get("/")
6560
@CollectRequestData(db=app.state.db)
66-
async def request(
67-
request: Request, settings: config.Settings = Depends(get_settings), verify: Verify = Depends(verify)
68-
):
61+
async def request_data(request: Request, verify: Verify = Depends(verify_request)) -> Any:
62+
"""Requests data from the premium data source"""
63+
assert verify
64+
6965
if settings.MODE == "production":
70-
# get cache data
71-
latest_data = app.state.cache_data.get_data(get_band_signature_hash(request.headers))
72-
if latest_data:
73-
latest_data["cached_data"] = True
66+
# Get cached data and if it exists return it
67+
if latest_data := app.state.cache_data.get_data(get_band_signature_hash(request.headers)):
7468
return latest_data
7569

7670
try:
7771
output = await app.state.adapter.unified_call(dict(request.query_params))
7872
if settings.MODE == "production":
79-
# cache data
73+
# Cache data
8074
app.state.cache_data.set_data(get_band_signature_hash(request.headers), output)
81-
8275
return output
83-
8476
except HTTPStatusError as e:
8577
raise HTTPException(
8678
status_code=e.response.status_code,
8779
detail={"error_msg": f"{e}"},
8880
)
89-
9081
except Exception as e:
9182
raise HTTPException(
9283
status_code=500,
@@ -95,21 +86,23 @@ async def request(
9586

9687

9788
@app.get("/status")
98-
async def get_report_status(settings: config.Settings = Depends(get_settings)):
99-
res = {
100-
"gateway_info": {
101-
"allow_data_source_ids": settings.ALLOWED_DATA_SOURCE_IDS,
102-
"max_delay_verification": settings.MAX_DELAY_VERIFICATION,
103-
}
104-
}
89+
async def get_status_report() -> StatusReport:
90+
"""Gets a status report: gateway info, latest request and latest failed request"""
91+
gateway_info = GatewayInfo(
92+
allow_data_source_ids=settings.ALLOWED_DATA_SOURCE_IDS,
93+
max_delay_verification=settings.MAX_DELAY_VERIFICATION,
94+
)
10595

10696
if app.state.db:
10797
try:
108-
res["latest_request"] = await app.state.db.get_latest_request_info()
109-
res["latest_failed_request"] = await app.state.db.get_latest_verify_failed()
110-
111-
return res
98+
latest_request = await app.state.db.get_latest_request_info()
99+
latest_failed_request = await app.state.db.get_latest_failed_request_info()
112100
except Exception as e:
113-
raise HTTPException(f"{e}", status_code=500)
101+
raise HTTPException(status_code=500, detail=f"{e}")
102+
else:
103+
latest_request = None
104+
latest_failed_request = None
114105

115-
return res
106+
return StatusReport(
107+
gateway_info=gateway_info, latest_request=latest_request, latest_failed_request=latest_failed_request
108+
)

app/report/__init__.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,2 @@
1-
from logging import Logger
21
from .db import DB
3-
4-
5-
def init_db(url: str, collection_name: str, log: Logger) -> DB:
6-
if url and collection_name:
7-
db = DB(url, collection_name)
8-
log.info(f"DB : init report data on mongo db")
9-
return db
10-
else:
11-
log.info(
12-
f"DB : No DB config -> if you want to save data on db please add [MONGO_DB_URL, COLLECTION_DB_NAME] in env"
13-
)
14-
return None
2+
from .db_helper import init_db

app/report/db.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,66 @@
1+
from typing import Optional
2+
13
from motor import motor_asyncio
24

35
from app.report.models import Report
46

57

68
class DB:
7-
def __init__(self, mongo_db_url: str, db_name: str):
9+
"""A MongoDB wrapper class for storing Reports.
10+
11+
Attributes:
12+
report: AsyncIOMotorClient instance to connect with MongoDB and get the "report" collection.
13+
"""
14+
15+
def __init__(self, mongo_db_url: str, db_name: str) -> None:
16+
"""Initializes DB with the MongoDB URL and database name.
17+
18+
Args:
19+
mongo_db_url: MongoDB URL.
20+
db_name: Database name.
21+
"""
822
self.report = motor_asyncio.AsyncIOMotorClient(mongo_db_url)[db_name].get_collection("report")
923

10-
async def get_latest_request_info(self):
11-
cursor = self.report.find({}, {"_id": 0, "user_ip": 0}).sort("created_at", -1).limit(1)
24+
async def get_latest_request_info(self) -> Optional[Report]:
25+
"""Gets the latest request information from the database.
26+
27+
Returns:
28+
A report containing the latest report
29+
"""
30+
cursor = self.report.find().sort("created_at", -1).limit(1)
1231
latest_request_info = await cursor.to_list(length=1)
1332

1433
if len(latest_request_info) == 0:
15-
return {}
34+
return None
35+
36+
return Report(**latest_request_info[0])
1637

17-
return latest_request_info[0]
38+
async def get_latest_failed_request_info(self) -> Optional[Report]:
39+
"""Gets the detail of the latest failed request from the database
1840
19-
async def get_latest_verify_failed(self):
41+
Returns:
42+
A report containing the latest failed report
43+
"""
2044
cursor = (
2145
self.report.find(
2246
{"$or": [{"verify.response_code": {"$ne": 200}}, {"provider_response.response_code": {"$ne": 200}}]},
23-
{"_id": 0, "user_ip": 0},
47+
{"_id": 0},
2448
)
2549
.sort("created_at", -1)
2650
.limit(1)
2751
)
52+
2853
latest_request_info = await cursor.to_list(length=1)
2954

3055
if len(latest_request_info) == 0:
31-
return {}
56+
return None
57+
58+
return Report(**latest_request_info[0])
3259

33-
return latest_request_info[0]
60+
def save(self, report: Report) -> None:
61+
"""Saves the given report to the database.
3462
35-
def save_report(self, report: Report):
63+
Args:
64+
report: The Report object to be saved.
65+
"""
3666
self.report.insert_one(report.to_dict())

app/report/db_helper.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from logging import Logger
2+
from typing import Optional
3+
4+
from app.report import DB
5+
6+
7+
def init_db(url: Optional[str], collection_name: Optional[str], log: Logger) -> Optional[DB]:
8+
"""Initializes the database if the URL and collection name are provided.
9+
10+
Args:
11+
url: The URL of the MongoDB instance.
12+
collection_name: The name of the collection to store reports in.
13+
log: The logger instance.
14+
15+
Returns:
16+
The DB instance if the url and collection_name are provided, or None if not.
17+
"""
18+
if url and collection_name:
19+
db = DB(url, collection_name)
20+
log.info(f"DB: Report data is being stored on MongoDB collection {collection_name}.")
21+
return db
22+
else:
23+
log.info(
24+
f"DB: No DB config -> if you want to save data on db please add [MONGO_DB_URL, COLLECTION_DB_NAME] in env"
25+
)
26+
return None

0 commit comments

Comments
 (0)