Skip to content

Commit 7b4207a

Browse files
author
meghost77
committed
Refactor codebase, update README, add KeyboardInterrupt handling, and fix import issues
1 parent 7dae014 commit 7b4207a

File tree

7 files changed

+200
-441
lines changed

7 files changed

+200
-441
lines changed

Diff for: db/database.py

+44-83
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,49 @@
1-
from typing import Optional
2-
31
from motor.motor_asyncio import AsyncIOMotorClient
42

53
from config import settings
64

7-
# Global variables for MongoDB client and database
8-
client = None
9-
db = None
10-
11-
# Initialize the database connection
12-
13-
14-
async def init_db():
15-
global client, db
16-
# Create a new MongoDB client
17-
client = AsyncIOMotorClient(settings.MONGODB_URI)
18-
# Select the database
19-
db = client[settings.DB_NAME]
20-
21-
# Close the database connection
22-
23-
24-
async def close_db():
25-
global client
26-
if client:
27-
# Close the MongoDB client connection
28-
client.close()
29-
30-
# Get a specific collection from the database
31-
32-
33-
async def get_collection(name):
34-
return db[name]
35-
36-
# Update a single document in the collection
37-
38-
39-
async def update_one(collection_name, filter, update, upsert=False):
40-
collection = await get_collection(collection_name)
41-
# Update a document in the collection with the specified filter and update
42-
return await collection.update_one(filter, update, upsert=upsert)
43-
44-
# Find documents in the collection with optional sorting, skipping, and limiting
45-
46-
47-
async def find(collection_name, filter={}, sort=None, skip=0, limit=0):
48-
collection = await get_collection(collection_name)
49-
cursor = collection.find(filter)
50-
if sort:
51-
cursor = cursor.sort(sort)
52-
if skip:
53-
cursor = cursor.skip(skip)
54-
if limit:
55-
cursor = cursor.limit(limit)
56-
# Convert the cursor to a list with the specified limit
57-
return await cursor.to_list(length=limit)
58-
59-
# Find a single document in the collection
60-
61-
62-
async def find_one(collection_name, filter):
63-
collection = await get_collection(collection_name)
64-
# Find one document that matches the filter
65-
return await collection.find_one(filter)
66-
67-
# List all collection names in the database
68-
69-
70-
async def list_collection_names():
71-
return await db.list_collection_names()
72-
73-
# Count the number of posts matching the query and optional city
74-
75-
76-
async def count_posts(collection_name: str):
77-
collection = await get_collection(collection_name)
78-
return await collection.count_documents({'_id': {'$ne': 'last_update'}})
79-
80-
# Get the latest posts sorted by post date
81-
825

83-
async def get_latest_posts(limit: int):
84-
collection = await get_collection('posts')
85-
# Find the latest posts sorted by post date in descending order
86-
cursor = collection.find().sort([('postDate', -1)]).limit(limit)
87-
# Convert the cursor to a list with the specified limit
88-
return await cursor.to_list(length=limit)
6+
class Database:
7+
client = None
8+
db = None
9+
10+
@classmethod
11+
async def connect(cls):
12+
cls.client = AsyncIOMotorClient(settings.MONGODB_URI)
13+
cls.db = cls.client[settings.DB_NAME]
14+
15+
@classmethod
16+
async def close(cls):
17+
if cls.client:
18+
cls.client.close()
19+
20+
@classmethod
21+
async def get_collection(cls, name):
22+
return cls.db[name]
23+
24+
@classmethod
25+
async def update_one(cls, collection_name, filter, update, upsert=False):
26+
collection = await cls.get_collection(collection_name)
27+
return await collection.update_one(filter, update, upsert=upsert)
28+
29+
@classmethod
30+
async def find(cls, collection_name, filter={}, sort=None, skip=0, limit=0):
31+
collection = await cls.get_collection(collection_name)
32+
cursor = collection.find(filter)
33+
if sort:
34+
cursor = cursor.sort(sort)
35+
return await cursor.skip(skip).limit(limit).to_list(length=limit)
36+
37+
@classmethod
38+
async def find_one(cls, collection_name, filter):
39+
collection = await cls.get_collection(collection_name)
40+
return await collection.find_one(filter)
41+
42+
@classmethod
43+
async def list_collection_names(cls):
44+
return await cls.db.list_collection_names()
45+
46+
@classmethod
47+
async def count_posts(cls, collection_name: str):
48+
collection = await cls.get_collection(collection_name)
49+
return await collection.count_documents({'_id': {'$ne': 'last_update'}})

Diff for: db/models.py

+10-13
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,29 @@
33

44
from pydantic import BaseModel, Field
55

6-
# Base model for a post
7-
86

97
class Price(BaseModel):
108
formattedPrice: Optional[str] = Field(
119
None, description="Formatted price of the item")
1210

1311

1412
class Post(BaseModel):
15-
id: Union[str, int] # ID can be either string or integer
16-
bodyHTML: str # HTML content of the post
17-
title: str # Title of the post
18-
URL: str # URL of the post
19-
city: str # City related to the post
20-
postDate: datetime # Date when the post was created
21-
updateDate: datetime # Date when the post was last updated
22-
firstImage: Optional[str] = None # Optional field for the first image URL
23-
commentCount: int # Number of comments on the post
13+
id: Union[str, int]
14+
bodyHTML: str
15+
title: str
16+
URL: str
17+
city: str
18+
postDate: datetime
19+
updateDate: datetime
20+
firstImage: Optional[str] = None
21+
commentCount: int
2422
tags: Optional[List[str]] = None
2523
authorUsername: Optional[str] = None
2624
price: Optional[Price] = None
27-
# Detailed model for a post extending the base Post model
2825

2926

3027
class PostDetail(Post):
31-
bodyHTML: str # HTML content of the post
28+
pass
3229

3330

3431
class SearchResponse(BaseModel):

Diff for: main.py

+22-42
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,35 @@
11
import logging
2+
import signal
3+
import sys
24
from contextlib import asynccontextmanager
35

6+
import uvicorn
47
from apscheduler.schedulers.asyncio import AsyncIOScheduler
58
from apscheduler.triggers.interval import IntervalTrigger
69
from fastapi import FastAPI
710

8-
from config import settings # Import settings from config
9-
from db.database import close_db, init_db
11+
from config import settings
12+
from db.database import Database
1013
from utils.routes import router
11-
from utils.services import update_all_collections
14+
from utils.services import PostService
1215

13-
# Logging Setup
1416
logging.basicConfig(level=logging.INFO,
1517
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
1618
logger = logging.getLogger(__name__)
1719

18-
# Initialize the scheduler for periodic tasks
1920
scheduler = AsyncIOScheduler()
2021

21-
# Context manager for application lifespan
22-
2322

2423
@asynccontextmanager
2524
async def lifespan(app: FastAPI):
26-
# Startup
2725
logger.info("Initializing database connection...")
28-
await init_db() # Establish database connection
26+
await Database.connect()
2927
logger.info("Database connection initialized.")
3028

3129
logger.info("Starting scheduler...")
32-
scheduler.start() # Start the scheduler
33-
# Add a job to update all collections every hour
30+
scheduler.start()
3431
scheduler.add_job(
35-
update_all_collections,
32+
PostService.update_all_collections,
3633
IntervalTrigger(hours=1),
3734
id='periodic_update',
3835
replace_existing=True
@@ -42,51 +39,34 @@ async def lifespan(app: FastAPI):
4239
try:
4340
yield
4441
finally:
45-
# Shutdown
4642
logger.info("Shutting down scheduler...")
47-
scheduler.shutdown() # Shutdown the scheduler
43+
scheduler.shutdown()
4844
logger.info("Scheduler shut down.")
4945

5046
logger.info("Closing database connection...")
51-
await close_db() # Close database connection
47+
await Database.close()
5248
logger.info("Database connection closed.")
5349

54-
# Create FastAPI app with the custom lifespan context manager
5550
app = FastAPI(lifespan=lifespan)
56-
# Include the application routes from the router
5751
app.include_router(router)
5852

59-
# Data update function with logging
6053

54+
def signal_handler(sig, frame):
55+
logger.info("Received shutdown signal. Initiating graceful shutdown...")
56+
scheduler.shutdown(wait=False)
57+
sys.exit(0)
6158

62-
async def update_all_collections():
63-
logger.info("Starting the data collection process...")
64-
try:
65-
# Logic for data update
66-
logger.info("Data collection process completed successfully.")
67-
except Exception as e:
68-
logger.error(f"An error occurred during data collection: {e}")
6959

7060
if __name__ == "__main__":
71-
import signal
72-
import sys
73-
74-
import uvicorn
75-
76-
# Graceful shutdown handler
77-
def handle_exit(*args):
78-
logger.info("Shutting down gracefully...")
79-
sys.exit(0)
80-
81-
# Register signal handlers for graceful shutdown
82-
for sig in (signal.SIGINT, signal.SIGTERM):
83-
signal.signal(sig, handle_exit)
61+
signal.signal(signal.SIGINT, signal_handler)
62+
signal.signal(signal.SIGTERM, signal_handler)
8463

8564
try:
86-
logger.info("Starting the script...")
87-
# Run the application using the configured port
65+
logger.info("Starting the Haraj Scraper Backend...")
8866
uvicorn.run(app, host="0.0.0.0", port=settings.PORT)
8967
except KeyboardInterrupt:
90-
handle_exit()
68+
logger.info("Keyboard interrupt received. Shutting down...")
69+
except Exception as e:
70+
logger.error(f"An error occurred: {str(e)}")
9171
finally:
92-
logger.info("Script has been stopped.")
72+
logger.info("Application shutdown complete.")

Diff for: utils/decorators.py

-17
This file was deleted.

0 commit comments

Comments
 (0)