From afa582458a964914bb4ecf814870d06658b69138 Mon Sep 17 00:00:00 2001 From: Taylor McNeil Date: Mon, 20 Oct 2025 13:20:51 -0400 Subject: [PATCH 1/6] feat(backend): init FastAPI Movies API with utils, error handling and onboarding docs - Set up FastAPI backend for Movies API - Implement create_success_response and create_error_response utils - Add MongoDB error mapper and global exception handler - Add .gitignore for Python, FastAPI, MongoDB data - Write detailed onboarding Readme with setup, FastAPI leaning and project mapping - Verified parity with TS response formatting and error handling --- server/python/.gitignore | 17 ++++ server/python/README.md | 129 +++++++++++++++++++++++++ server/python/database/mongo_client.py | 8 ++ server/python/main.py | 27 ++++++ server/python/models/models.py | 100 +++++++++++++++++++ server/python/requirements.txt | 40 ++++++++ server/python/routers/movies.py | 93 ++++++++++++++++++ server/python/utils/errorHandler.py | 128 ++++++++++++++++++++++++ 8 files changed, 542 insertions(+) create mode 100644 server/python/.gitignore create mode 100644 server/python/README.md create mode 100644 server/python/database/mongo_client.py create mode 100644 server/python/main.py create mode 100644 server/python/models/models.py create mode 100644 server/python/requirements.txt create mode 100644 server/python/routers/movies.py create mode 100644 server/python/utils/errorHandler.py diff --git a/server/python/.gitignore b/server/python/.gitignore new file mode 100644 index 0000000..9d98c50 --- /dev/null +++ b/server/python/.gitignore @@ -0,0 +1,17 @@ +#------------------------- +# Python / FastAPI backend +#------------------------- + +# Byte-compiled / optimized files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environment +.venv/ + +# Environment variables +.env + +#MacOS files +.DS_Store diff --git a/server/python/README.md b/server/python/README.md new file mode 100644 index 0000000..f6f337b --- /dev/null +++ b/server/python/README.md @@ -0,0 +1,129 @@ +# Welcome to the Python Backend + +TOC +1. [Overview](#overview) +2. [Getting Setup](#getting-setup) +3. [Learning FastAPI](#learning-fastapi) +4. [Exploring the Codebase](#exploring-the-codebase) +5. [Feature Parity Status](#feature-parity-status) +6. [Utilities and Nice to Know](#nice-to-know) + + +## Overview +We're porting our Express backend to Python/FastAPI to achieve **functional parity** - same endpoints, same responses, same MongoDB operations. This allows one frontend to work with multiple backends. + + +**Important**: We're not rewriting - we're replicating behavior exactly. So this means we are figuring out the pythonic way of doing things. You will find that we can accomplish things with Fast in fewer lines of code than in Express. + +## Getting Setup +0. Install Python if you don't already have it. (*Depending on your version of python, you might need to use python3 and pip3 vs python and pip*) +1. Clone the main repo. + ```sh + https://github.com/mongodb/docs-sample-apps.git + ``` + +2. Fetch all branches + + ```sh + git fetch --all + ``` + +3. Checkout my branch + ```sh + git checkout python-backend-setup + ``` + +4. Verify you are on the right branch + ```sh + git branch + ``` +5. In the root directory. /python, make a virtual environment. + ```sh + python -m venv .venv + ``` +6. Activate the virtual environment + ```sh + source .venv/bin/activate/ + ``` +7. Navigate back to /python, install the required packages. + ```sh + pip install -r requirements.txt + ``` +8. Create an .env file at the /python root. +It should have this format: + ```python + MONGO_URI="place your connection string here" + MONGO_DB="sample_mflix" + ``` +9. Navigate back to the root and start the server. + ```sh + fastapi dev main.py or uvicorn main:app --reload + ``` + (NOTE: THESE WONT WORK IF THE VIRTUAL ENVIRONMENT IS NOT ACTIVATED) +10. Click the link in the terminal or visit ```localhost:8000/docs``` +11. Try visiting ```localhost:8000/api/movies```. + + If the setup ran correctly, you should see data 🎉 + +*I recommend having your atlas instance up to explore your data and query the db directly.* + +**Troubleshooting**: If commands fail, ensure your virtual environment is activated (you should see `(.venv)` in your terminal prompt). + + +## Learning FastAPI +Before diving into the repo, I suggest spending some time with the official [FastAPI tutorial](https://fastapi.tiangolo.com/tutorial/). + +You only need to read up to the 'Request Body' section to get comfortable. *The query parameters and path parameters section can be read later.* + +Key Takeaways: +- Decorators (@app.get, @app.post,etc.) +- Query, path, and body parameters +- Pydantic validation/ serialization +- Automatic JSON responses +- Built-in docs and testing at '/docs' + + +## Exploring the Codebase +Once you have completed the tutorial, I would suggest exploring the code and just noticing the differences between Express and Python. The apps are setup similarly but there are some small differences. + +### Architecture +|Layer|Purpose|Express Equivalent| Differences| +|:---|:-------|:----------|:------| +|Routes
`routers/movies.py`| Defines all /movies endpoints (GET, POST, PUT, etc.)| /controllers/movieController.ts |The movies.ts file inside the routes file in the Express backend it actually wiring up the endpoints. Fast handles this for us in the main.py in one line. ```app.include_router(movies.router, prefix="/api/movies", tags=["movies"])```| +|Models
`models/models.py`| Pydantic schema for validating and serializing request/response data.| /types/index.ts|There are some differences in how the models are constructed. Take note on how nested classes are handled.| +|Utils
`utils/errorHandler.py`| Centralized utilities for responses, error handling and MongoDB exception mapping.|utils/errorHandler.ts |Express requires devs to write exception handling and validation on their own. Pydanic handles the validation and exception handling is a bit cleaner in Fast. You will notice the most differences here.| +|Database
`database/mongo_client.py`| Handles the connection to the db|/config/database.ts| *The current database file does not have feature parity with the Express version*| + + +## Feature Parity Status +|Feature|Status|Owner|Notes| +|:------|:-----|:----|:----| +|Global Exception Handling| DONE| - |Found in Utils | +|JSON Response Matching| DONE | - |Found in Utils| +|CRUD- ```insertOne()```| Not Started| Angela| - | +|CRUD- ```insertMany()```| Not Started| Taylor| - | +|CRUD- ```findOne()```| Not Started| Angela| - | +|CRUD- ```find()```| DONE| - | Found in movies.py as ```get_all_movies()``` This is a good function to look at to understand query parameters, requests and responses. Your functions will be simpler than this. But that is a good base to start with.| +|CRUD- ```updateOne()```| Not Started| Angela| - | +|CRUD- ```updateMany()```| Not Started| Taylor| - | +|CRUD- ```deleteOne()```| Not Started| Angela| - | +|CRUD- ```deleteMany()```| Not Started| Taylor| -| +|CRUD- ```findOne()```| Not Started| Angela| - | +|CRUD- ```findOneAndDelete()```| Not Started| Angela| This one is a bit harder but I figure this could be a fun challenge.| + + +## Nice to Know +### Utilities (errorHandler.py) +This module provides a parity replacement for the Express errorHandler.ts file. It provides the same response shapes and error handling and removes the middleware. + +- create_success_response(data,message) - creates a success response with the same shape as the Express version. +- create_error_response(message,code,details) - creates a error response with the same shape as the Express version. +- parse_mongo_exception(exc) - ensures PyMongo exceptions return JSON identical to the Express Version +- register_error_handlers - hooks our error system into the Fast app + +Fast automatically handles async and validation errors, so there is no need for asyncHandler or validateRequiredFields from the TS version. + +### Useful Links +- [Main Repo]( https://github.com/mongodb/docs-sample-apps/tree/main ) +- [Sample App Scoping Doc](https://docs.google.com/document/d/12dROckw_Cp0ku2IIGku-ch7MvEuBPo0V4Gs-5wQgeHQ/edit?tab=t.0) +- [Sample App Project Description & Breakdown Doc](https://docs.google.com/document/d/1xv2dmcNrT-HYk5TBE-KtVDPmW0274rpBZ0_0QmC66ac/edit?tab=t.0#heading=h.ki9tatw08ilc) \ No newline at end of file diff --git a/server/python/database/mongo_client.py b/server/python/database/mongo_client.py new file mode 100644 index 0000000..afe00f5 --- /dev/null +++ b/server/python/database/mongo_client.py @@ -0,0 +1,8 @@ +from pymongo import AsyncMongoClient +from dotenv import load_dotenv +import os + +load_dotenv() + +client = AsyncMongoClient(os.getenv("MONGO_URI")) +db =client[os.getenv("MONGO_DB")] diff --git a/server/python/main.py b/server/python/main.py new file mode 100644 index 0000000..6daaeef --- /dev/null +++ b/server/python/main.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI +from routers import movies +from utils.errorHandler import register_error_handlers + +app = FastAPI() +register_error_handlers(app) +app.include_router(movies.router, prefix="/api/movies", tags=["movies"]) + + + + + + +# Health Check Endpoint +@app.get("/") +async def root(): + return {"message": "Backend is running!"} + +@app.get("/test-duplicate") +async def test_duplicate(): + from pymongo.errors import DuplicateKeyError + raise DuplicateKeyError("This is a test duplicate key error.") + +@app.get("/test-generic") +async def test_generic(): + from pymongo.errors import PyMongoError + raise PyMongoError("This is a test generic pymongo error.") \ No newline at end of file diff --git a/server/python/models/models.py b/server/python/models/models.py new file mode 100644 index 0000000..5b97d10 --- /dev/null +++ b/server/python/models/models.py @@ -0,0 +1,100 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional, TypeVar, Generic, Any + + +T = TypeVar("T") + +class Awards(BaseModel): + wins: Optional[int] = None + nominations: Optional[int] = None + text: Optional[str] = None + +class Imbd(BaseModel): + rating: Optional[float] = None + votes: Optional[int] = None + id: Optional[int] = None + +class Movie(BaseModel): + id: Optional[str] = Field(alias="_id") + title: str + year: Optional[int] = None + plot: Optional[str] = None + fullplot: Optional[str] = None + released: datetime = None + runtime: Optional[int] = None + poster: Optional[str] = None + genres: Optional[list[str]] = None + directors: Optional[list[str]] = None + writers: Optional[list[str]] = None + cast: Optional[list[str]] = None + countries: Optional[list[str]] = None + languages: Optional[list[str]] = None + rated: Optional[str] = None + awards: Optional[Awards] = None + imdb: Optional[Imbd] = None + + model_config = { + "populate_by_name" : True + } + + +''' +So this an interesting conversion. Pydanic doesn't cleanly support constructing +models that have MongoDB query operators as field names. This becomes an issue when +we want to convert the validated model back to a dictionary to use as a MongoDB query filter. +For example, if a user leaves the 'q' parameter blank, we don't want to include the '$text' operator +in the filter at all but validation will send an empty value for it and that causes errors. So +I am handling the validation in the query router itself, but leaving this here as an example of how +it could be done, if I am wrong about Pydantic's capabilities. +''' + +class TextFilter(BaseModel): + search: str = Field(..., alias="$search") + +class RegexFilter(BaseModel): + regex: str = Field(..., alias="$regex") + options: Optional[str] = Field(None, alias="$options") + +class RatingFilter(BaseModel): + gte: Optional[float] = Field(None, alias="$gte") + lte: Optional[float] = Field(None, alias="$lte") + +class MovieFilter(BaseModel): + text: Optional[TextFilter] = Field(None, alias="$text") + genres: Optional[RegexFilter] = None + year: Optional[int] = None + imdb_rating: Optional[RatingFilter] = Field(None, alias="imdb.rating") + + model_config = { + "populate_by_name" : True + } + + +class Pagination(BaseModel): + page: int + limit: int + total: int + pages: int + + + +class SuccessResponse(BaseModel, Generic[T]): + success: bool = True + message: Optional[str] + data: T + timestamp: str + pagination: Optional[Pagination] = None + + +class ErrorDetails(BaseModel): + message: str + code: Optional[str] + details: Optional[Any] = None + +class ErrorResponse(BaseModel): + success: bool = False + message: str + error: ErrorDetails + timestamp: str + \ No newline at end of file diff --git a/server/python/requirements.txt b/server/python/requirements.txt new file mode 100644 index 0000000..be1d228 --- /dev/null +++ b/server/python/requirements.txt @@ -0,0 +1,40 @@ +annotated-types==0.7.0 +anyio==4.11.0 +certifi==2025.10.5 +click==8.3.0 +dnspython==2.8.0 +email-validator==2.3.0 +fastapi==0.119.0 +fastapi-cli==0.0.13 +fastapi-cloud-cli==0.3.1 +h11==0.16.0 +httpcore==1.0.9 +httptools==0.7.1 +httpx==0.28.1 +idna==3.11 +Jinja2==3.1.6 +markdown-it-py==4.0.0 +MarkupSafe==3.0.3 +mdurl==0.1.2 +pydantic==2.12.2 +pydantic_core==2.41.4 +Pygments==2.19.2 +pymongo==4.15.3 +python-dotenv==1.1.1 +python-multipart==0.0.20 +PyYAML==6.0.3 +rich==14.2.0 +rich-toolkit==0.15.1 +rignore==0.7.1 +sentry-sdk==2.42.0 +shellingham==1.5.4 +sniffio==1.3.1 +starlette==0.48.0 +typer==0.19.2 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.5.0 +uvicorn==0.37.0 +uvloop==0.21.0 +watchfiles==1.1.1 +websockets==15.0.1 diff --git a/server/python/routers/movies.py b/server/python/routers/movies.py new file mode 100644 index 0000000..53b5777 --- /dev/null +++ b/server/python/routers/movies.py @@ -0,0 +1,93 @@ +from fastapi import APIRouter, HTTPException, Query +from database.mongo_client import db +from models.models import Movie, MovieFilter, SuccessResponse +from typing import List +from datetime import datetime +from utils.errorHandler import create_success_response, create_error_response + +router = APIRouter() + + +""" + GET /api/movies/ + + Retrieve a list of movies with optional filtering, sorting, and pagination. + + Query Parameters: + q (str, optional): Text search query (searches title, plot, fullplot). + genre (str, optional): Filter by genre. + year (int, optional): Filter by year. + min_rating (float, optional): Minimum IMDB rating. + max_rating (float, optional): Maximum IMDB rating. + limitNum (int, optional): Number of results to return (default: 20, max: 100). + skipNum (int, optional): Number of documents to skip for pagination (default: 0). + sortBy (str, optional): Field to sort by (default: "title"). + sort_order (str, optional): Sort direction, "asc" or "desc" (default: "asc"). + + Returns: + SuccessResponse[List[Movie]]: A response object containing the list of movies and metadata. +""" + +@router.get("/", response_model=SuccessResponse[List[Movie]]) +async def get_all_movies( + q:str = Query(default=None), + genre:str = Query(default=None), + year:int = Query(default=None), + min_rating:float = Query(default=None), + max_rating:float = Query(default=None), + limit_num:int = Query(default=20, ge=1, le=100), + skip_num:int = Query(default=0, ge=0), + sort_by:str = Query(default="title"), + sort_order:str = Query(default="asc") +): + + # This variable naming might not be ideal, but it helps illustrate the point. + filter = {} + if q: + filter["$text"] = {"$search": q} + if genre: + filter["genres"] = {"$regex": genre, "$options": "i"} + if year: + # I personally got some dirty data in the year field, a 1995è + filter["year"] = year + if min_rating is not None or max_rating is not None: + rating_filter = {} + if min_rating is not None: + rating_filter["$gte"] = min_rating + if max_rating is not None: + rating_filter["$lte"] = max_rating + filter["imdb.rating"] = rating_filter + + + # Comment why this is important + filter = MovieFilter(**filter) + + filter = filter.model_dump(by_alias=True, exclude_none=True) + + sort_order = -1 if sort_order == "desc" else 1 + sort = [(sort_by, sort_order)] + + + cursor = db.movies.find(filter).sort(sort).skip(skip_num).limit(limit_num) + movies = [] + async for movie in cursor: + movie["_id"] = str(movie["_id"]) # Convert ObjectId to string + movies.append(movie) + return create_success_response(movies, f"Found {len(movies)} movies.") + + + + + + +# Testing the ErrorReponse Model +@router.get("/error") +async def test_error(): + try: + raise ValueError("This is a test error.") + except ValueError as e: + return create_error_response( + message="A test error occurred.", + code="TEST_ERROR", + details=str(e) + ) \ No newline at end of file diff --git a/server/python/utils/errorHandler.py b/server/python/utils/errorHandler.py new file mode 100644 index 0000000..f25d192 --- /dev/null +++ b/server/python/utils/errorHandler.py @@ -0,0 +1,128 @@ +from fastapi import Request +from fastapi.responses import JSONResponse +from pymongo.errors import PyMongoError, DuplicateKeyError, WriteError +from datetime import datetime +from typing import Any, Dict, Optional +from models.models import ErrorDetails, ErrorResponse, SuccessResponse, T + + +''' +Open to having a conversation about parity in the code. From my understanding exception handeling, validation errors and enforcement(Pydantic), +and error response formatting are all handled natively by FastAPI. So I don't believe I need to create the ValidationError +class, middleware, or exception handlers present in the TS code. + +''' + + +''' +Creates a standardized success response. + + +Args: + data (T): The data to include in the response. + message (Optional[str]): An optional message to include. + +Returns: + SuccessResponse[T]: A standardized success response object. + ''' + + +# TODO: Verify the timestamp format is acceptable. +def create_success_response(data:T, message: Optional[str] = None) -> SuccessResponse[T]: + return SuccessResponse( + message=message or "Operation completed successfully.", + data=data, + timestamp=datetime.utcnow().isoformat() + "Z", + ) + + +''' +Creates a standardized error response. + +Args: + message (str): The error message. + code (Optional[str]): An optional error code. + details (Optional[Any]): Additional error details. + +Returns: + ErrorResponse: A standardized error response object. + +''' + +# TODO: Verify the timestamp format is acceptable. +def create_error_response(message: str, code: Optional[str]=None, details: Optional[Any]=None) -> ErrorResponse: + return ErrorResponse( + message=message, + error=ErrorDetails( + message=message, + code=code, + details=details + ), + timestamp=datetime.utcnow().isoformat() + "Z", + ) + + + +''' +This is interesting, I am not sure if this is worth explaining that compared to Node, you are +not going to get exceptions thrown from MongoDB operations in the same way. You are not getting +error codes back from operations, you are getting exceptions. +''' + +def parse_mongo_exception(exc: Exception) -> dict: + if isinstance(exc, DuplicateKeyError): + return{ + "message": "Duplicate key error occurred.", + "code": "DUPLICATE_KEY_ERROR", + "details": "A document with the same key already exists.", + "statusCode":409 + } + + # This is stating that the data that you are trying to implement is the wrong shape + # for the schema implemented in MongoDB. + elif isinstance(exc, WriteError): + return{ + "message": "Document validation failed.", + "code": "WRITE_ERROR", + "details": str(exc), + "statusCode":400 + } + + elif isinstance(exc, PyMongoError): + return { + "message" : "A database error occurred.", + "code": "DATABASE_ERROR", + "details": str(exc), + "statusCode":500 + } + return { + "message": "An unknown error occurred.", + "code": "UNKNOWN_ERROR", + "details": str(exc), + "statusCode": 500 + } + +def register_error_handlers(app): + + @app.exception_handler(PyMongoError) + async def mongo_exception_handler(request: Request, exc: PyMongoError): + error_details = parse_mongo_exception(exc) + return JSONResponse( + status_code = error_details["statusCode"], + content=create_error_response( + message=error_details["message"], + code=error_details["code"], + details=error_details["details"] + ).model_dump() + ) + + @app.exception_handler(Exception) + async def generic_exception_handler(request: Request, exc: Exception): + return JSONResponse( + status_code=500, + content=create_error_response( + message=str(exc), + code="INTERNAL_SERVER_ERROR", + details=getattr(exc, 'detail', None) or getattr(exc, 'args', None) + ).model_dump() + ) \ No newline at end of file From 4cacbcaec909da05454519e173ab3a5c996b858d Mon Sep 17 00:00:00 2001 From: Taylor McNeil Date: Mon, 20 Oct 2025 19:13:51 -0400 Subject: [PATCH 2/6] chore: Restructured project and added inital unit test setup --- server/python/README.md | 22 ++++------ server/python/main.py | 15 +++++-- server/python/requirements.txt | 5 +++ .../python/{ => src}/database/mongo_client.py | 3 ++ server/python/{ => src}/models/models.py | 0 server/python/{ => src}/routers/movies.py | 44 ++++++++++++++++--- server/python/{ => src}/utils/errorHandler.py | 2 +- server/python/tests/conftest.py | 34 ++++++++++++++ 8 files changed, 101 insertions(+), 24 deletions(-) rename server/python/{ => src}/database/mongo_client.py (78%) rename server/python/{ => src}/models/models.py (100%) rename server/python/{ => src}/routers/movies.py (66%) rename server/python/{ => src}/utils/errorHandler.py (98%) create mode 100644 server/python/tests/conftest.py diff --git a/server/python/README.md b/server/python/README.md index f6f337b..504019a 100644 --- a/server/python/README.md +++ b/server/python/README.md @@ -17,18 +17,12 @@ We're porting our Express backend to Python/FastAPI to achieve **functional pari ## Getting Setup 0. Install Python if you don't already have it. (*Depending on your version of python, you might need to use python3 and pip3 vs python and pip*) -1. Clone the main repo. +1. Clone from my fork of the main repo. ```sh - https://github.com/mongodb/docs-sample-apps.git + https://github.com/tmcneil-mdb/docs-sample-apps.git ``` -2. Fetch all branches - - ```sh - git fetch --all - ``` - -3. Checkout my branch +2. Checkout my branch ```sh git checkout python-backend-setup ``` @@ -43,7 +37,7 @@ We're porting our Express backend to Python/FastAPI to achieve **functional pari ``` 6. Activate the virtual environment ```sh - source .venv/bin/activate/ + source .venv/bin/activate ``` 7. Navigate back to /python, install the required packages. ```sh @@ -89,10 +83,10 @@ Once you have completed the tutorial, I would suggest exploring the code and jus ### Architecture |Layer|Purpose|Express Equivalent| Differences| |:---|:-------|:----------|:------| -|Routes
`routers/movies.py`| Defines all /movies endpoints (GET, POST, PUT, etc.)| /controllers/movieController.ts |The movies.ts file inside the routes file in the Express backend it actually wiring up the endpoints. Fast handles this for us in the main.py in one line. ```app.include_router(movies.router, prefix="/api/movies", tags=["movies"])```| -|Models
`models/models.py`| Pydantic schema for validating and serializing request/response data.| /types/index.ts|There are some differences in how the models are constructed. Take note on how nested classes are handled.| -|Utils
`utils/errorHandler.py`| Centralized utilities for responses, error handling and MongoDB exception mapping.|utils/errorHandler.ts |Express requires devs to write exception handling and validation on their own. Pydanic handles the validation and exception handling is a bit cleaner in Fast. You will notice the most differences here.| -|Database
`database/mongo_client.py`| Handles the connection to the db|/config/database.ts| *The current database file does not have feature parity with the Express version*| +|Routes
`src/routers/movies.py`| Defines all /movies endpoints (GET, POST, PUT, etc.)| /controllers/movieController.ts |The movies.ts file inside the routes file in the Express backend it actually wiring up the endpoints. Fast handles this for us in the main.py in one line. ```app.include_router(movies.router, prefix="/api/movies", tags=["movies"])```| +|Models
`src/models/models.py`| Pydantic schema for validating and serializing request/response data.| /types/index.ts|There are some differences in how the models are constructed. Take note on how nested classes are handled.| +|Utils
`src/utils/errorHandler.py`| Centralized utilities for responses, error handling and MongoDB exception mapping.|utils/errorHandler.ts |Express requires devs to write exception handling and validation on their own. Pydanic handles the validation and exception handling is a bit cleaner in Fast. You will notice the most differences here.| +|Database
`src/database/mongo_client.py`| Handles the connection to the db|/config/database.ts| *The current database file does not have feature parity with the Express version*| ## Feature Parity Status diff --git a/server/python/main.py b/server/python/main.py index 6daaeef..a1a67e9 100644 --- a/server/python/main.py +++ b/server/python/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI -from routers import movies -from utils.errorHandler import register_error_handlers +from src.routers import movies +from src.utils.errorHandler import register_error_handlers app = FastAPI() register_error_handlers(app) @@ -11,7 +11,13 @@ -# Health Check Endpoint + + +#------------------------------------ +# Testing error endpoints. Will be removed later +#------------------------------------ + +''' @app.get("/") async def root(): return {"message": "Backend is running!"} @@ -24,4 +30,5 @@ async def test_duplicate(): @app.get("/test-generic") async def test_generic(): from pymongo.errors import PyMongoError - raise PyMongoError("This is a test generic pymongo error.") \ No newline at end of file + raise PyMongoError("This is a test generic pymongo error.") +''' \ No newline at end of file diff --git a/server/python/requirements.txt b/server/python/requirements.txt index be1d228..7fa4744 100644 --- a/server/python/requirements.txt +++ b/server/python/requirements.txt @@ -12,14 +12,19 @@ httpcore==1.0.9 httptools==0.7.1 httpx==0.28.1 idna==3.11 +iniconfig==2.3.0 Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 mdurl==0.1.2 +packaging==25.0 +pluggy==1.6.0 pydantic==2.12.2 pydantic_core==2.41.4 Pygments==2.19.2 pymongo==4.15.3 +pytest==8.4.2 +pytest-asyncio==1.2.0 python-dotenv==1.1.1 python-multipart==0.0.20 PyYAML==6.0.3 diff --git a/server/python/database/mongo_client.py b/server/python/src/database/mongo_client.py similarity index 78% rename from server/python/database/mongo_client.py rename to server/python/src/database/mongo_client.py index afe00f5..e300702 100644 --- a/server/python/database/mongo_client.py +++ b/server/python/src/database/mongo_client.py @@ -6,3 +6,6 @@ client = AsyncMongoClient(os.getenv("MONGO_URI")) db =client[os.getenv("MONGO_DB")] + +def get_collection(name:str): + return db[name] \ No newline at end of file diff --git a/server/python/models/models.py b/server/python/src/models/models.py similarity index 100% rename from server/python/models/models.py rename to server/python/src/models/models.py diff --git a/server/python/routers/movies.py b/server/python/src/routers/movies.py similarity index 66% rename from server/python/routers/movies.py rename to server/python/src/routers/movies.py index 53b5777..6ad5716 100644 --- a/server/python/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -1,13 +1,18 @@ from fastapi import APIRouter, HTTPException, Query -from database.mongo_client import db -from models.models import Movie, MovieFilter, SuccessResponse +from src.database.mongo_client import db, get_collection +from src.models.models import Movie, MovieFilter, SuccessResponse from typing import List from datetime import datetime -from utils.errorHandler import create_success_response, create_error_response +from src.utils.errorHandler import create_success_response, create_error_response + router = APIRouter() +#------------------------------------ +# Place get_movie_by_id endpoint here +#------------------------------------ + """ GET /api/movies/ @@ -28,6 +33,7 @@ SuccessResponse[List[Movie]]: A response object containing the list of movies and metadata. """ +# Not valid query filter, how can we clean the data before sending to Pydantic? Should we have this default values. @router.get("/", response_model=SuccessResponse[List[Movie]]) async def get_all_movies( q:str = Query(default=None), @@ -43,6 +49,7 @@ async def get_all_movies( # This variable naming might not be ideal, but it helps illustrate the point. filter = {} + movies_collection = get_collection("movies") if q: filter["$text"] = {"$search": q} if genre: @@ -68,18 +75,44 @@ async def get_all_movies( sort = [(sort_by, sort_order)] - cursor = db.movies.find(filter).sort(sort).skip(skip_num).limit(limit_num) + cursor = movies_collection.find(filter).sort(sort).skip(skip_num).limit(limit_num) movies = [] async for movie in cursor: movie["_id"] = str(movie["_id"]) # Convert ObjectId to string movies.append(movie) return create_success_response(movies, f"Found {len(movies)} movies.") +#------------------------------------ +# Place create_movie endpoint here +#------------------------------------ + +#------------------------------------ +# Place create_movies_batch endpoint here +#------------------------------------ + +#------------------------------------ +# Place update_movie endpoint here +#------------------------------------ + +#------------------------------------ +# Place update_movies_by_batch endpoint here +#------------------------------------ +#------------------------------------ +# Place delete_movie endpoint here +#------------------------------------ +#------------------------------------ +# Place delete_movies_by_batch endpoint here +#------------------------------------ +#------------------------------------ +# Place find_and_delete_movie endpoint here +#------------------------------------ +# ---- Old testing endpoint +''' # Testing the ErrorReponse Model @router.get("/error") async def test_error(): @@ -90,4 +123,5 @@ async def test_error(): message="A test error occurred.", code="TEST_ERROR", details=str(e) - ) \ No newline at end of file + ) +''' \ No newline at end of file diff --git a/server/python/utils/errorHandler.py b/server/python/src/utils/errorHandler.py similarity index 98% rename from server/python/utils/errorHandler.py rename to server/python/src/utils/errorHandler.py index f25d192..c7a7e94 100644 --- a/server/python/utils/errorHandler.py +++ b/server/python/src/utils/errorHandler.py @@ -3,7 +3,7 @@ from pymongo.errors import PyMongoError, DuplicateKeyError, WriteError from datetime import datetime from typing import Any, Dict, Optional -from models.models import ErrorDetails, ErrorResponse, SuccessResponse, T +from src.models.models import ErrorDetails, ErrorResponse, SuccessResponse, T ''' diff --git a/server/python/tests/conftest.py b/server/python/tests/conftest.py new file mode 100644 index 0000000..22be62f --- /dev/null +++ b/server/python/tests/conftest.py @@ -0,0 +1,34 @@ +import os +import pytest +from httpx import AsyncClient +from src.main import app +from unittest.mock import AsyncMock, MagicMock + +os.environ["ENV"] = "test" +os.environ["MONGO_DB_NAME"] = "sample_mflix" +os.environ["MONGO_URI"] = "mock://no-connection" + +#--------------------------------- +# # 1. Setup Environment for Tests +#--------------------------------- + +@pytest.fixture(scope="session", autouse=True) +def set_test_env(): + print("\n [Setup] Running test environment setup...") + yield + print("\n [Teardown] Tests completed. Cleaning up...") + +#--------------------------- +# 2. Mock MongoDB Collection +#--------------------------- + +@pytest.fixture() +def mock_movies_collection(): + mock_collection = MagicMock() + mock_collection.find = AsyncMock() + + +@pytest.fixture() +def client(): + with AsyncClient(app=app, base_url="http://test") as client: + yield client \ No newline at end of file From a8ac9f30e16475a67ab2d14fe6e96791ffd3ebb4 Mon Sep 17 00:00:00 2001 From: Taylor McNeil Date: Tue, 21 Oct 2025 15:56:11 -0400 Subject: [PATCH 3/6] feat(api): add create_models_batch endpoint and added createMovieRequest model --- server/python/src/models/models.py | 16 ++++++ server/python/src/routers/movies.py | 77 ++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/server/python/src/models/models.py b/server/python/src/models/models.py index 5b97d10..7a4e201 100644 --- a/server/python/src/models/models.py +++ b/server/python/src/models/models.py @@ -78,6 +78,22 @@ class Pagination(BaseModel): pages: int +class CreateMovieRequest(BaseModel): + title: str + year: Optional[int] = None + plot: Optional[str] = None + fullplot: Optional[str] = None + genres: Optional[list[str]] = None + directors: Optional[list[str]] = None + writers: Optional[list[str]] = None + cast: Optional[list[str]] = None + countries: Optional[list[str]] = None + languages: Optional[list[str]] = None + rated: Optional[str] = None + runtime: Optional[int] = None + poster: Optional[str] = None + + class SuccessResponse(BaseModel, Generic[T]): success: bool = True diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 6ad5716..2657c16 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -1,14 +1,22 @@ from fastapi import APIRouter, HTTPException, Query from src.database.mongo_client import db, get_collection -from src.models.models import Movie, MovieFilter, SuccessResponse +from src.models.models import CreateMovieRequest, Movie, MovieFilter, SuccessResponse from typing import List from datetime import datetime from src.utils.errorHandler import create_success_response, create_error_response +''' +This file contains all the business logic for movie operations. +Each method demonstrates different MongoDB operations using the PyMongo driver. -router = APIRouter() +Implemented Endpoints: +- GET /api/movies/ : Retrieve a list of movies with optional filtering, sorting, + and pagination. +- POST /api/movies/batch : Create multiple movies in a single request. +''' +router = APIRouter() #------------------------------------ # Place get_movie_by_id endpoint here #------------------------------------ @@ -33,8 +41,8 @@ SuccessResponse[List[Movie]]: A response object containing the list of movies and metadata. """ -# Not valid query filter, how can we clean the data before sending to Pydantic? Should we have this default values. @router.get("/", response_model=SuccessResponse[List[Movie]]) +# Validate the query parameters using FastAPI's Query functionality. async def get_all_movies( q:str = Query(default=None), genre:str = Query(default=None), @@ -47,7 +55,7 @@ async def get_all_movies( sort_order:str = Query(default="asc") ): - # This variable naming might not be ideal, but it helps illustrate the point. + # This variable naming might not be ideal as Python has a function named filter, but it helps illustrate the point. filter = {} movies_collection = get_collection("movies") if q: @@ -55,7 +63,7 @@ async def get_all_movies( if genre: filter["genres"] = {"$regex": genre, "$options": "i"} if year: - # I personally got some dirty data in the year field, a 1995è + # I personally got some dirty data in the year field, a 1995è. Should we guard against this? filter["year"] = year if min_rating is not None or max_rating is not None: rating_filter = {} @@ -66,20 +74,23 @@ async def get_all_movies( filter["imdb.rating"] = rating_filter - # Comment why this is important + filter = MovieFilter(**filter) filter = filter.model_dump(by_alias=True, exclude_none=True) + # Building the sort object based on user input sort_order = -1 if sort_order == "desc" else 1 sort = [(sort_by, sort_order)] - + # Query the database with the constructed filter, sort, skip, and limit. cursor = movies_collection.find(filter).sort(sort).skip(skip_num).limit(limit_num) movies = [] async for movie in cursor: movie["_id"] = str(movie["_id"]) # Convert ObjectId to string movies.append(movie) + + # Return the results wrapped in a SuccessResponse return create_success_response(movies, f"Found {len(movies)} movies.") #------------------------------------ @@ -90,6 +101,56 @@ async def get_all_movies( # Place create_movies_batch endpoint here #------------------------------------ +''' +POST /api/movies/batch + +Create multiple movies in a single request. + +Request Body: + movies (List[CreateMovieRequest]): A list of movie objects to insert. Each object should include: + - title (str): The movie title. + - year (int, optional): The release year. + - plot (str, optional): Short plot summary. + - fullplot (str, optional): Full plot summary. + - genres (List[str], optional): List of genres. + - directors (List[str], optional): List of directors. + - writers (List[str], optional): List of writers. + - cast (List[str], optional): List of cast members. + - countries (List[str], optional): List of countries. + - languages (List[str], optional): List of languages. + - rated (str, optional): Movie rating. + - runtime (int, optional): Runtime in minutes. + - poster (str, optional): Poster URL. + + Returns: + SuccessResponse: A response object containing the number of inserted movies and their IDs. + +''' + +@router.post("/batch") + +# We do not have to write explicit validation code here, Pydantic will handle it for us. +# Invalid movies will result in a 422 response with details about the validation errors. + +async def create_movies_batch(movies: List[CreateMovieRequest]): + movies_collection = get_collection("movies") + movies_dicts = [] + for movie in movies: + movies_dicts.append(movie.model_dump(exclude_unset=True, exclude_none=True)) + + #The above line can also be written using list comprehension as: + # movies_dicts = [movie.model_dump(exclude_unset=True) for movie in + + result = await movies_collection.insert_many(movies_dicts) + return create_success_response({ + "insertedCount": len(result.inserted_ids), + "insertedIds": [str(_id) for _id in result.inserted_ids] + }, + f"Successfully created {len(result.inserted_ids)} movies." + ) + + + #------------------------------------ # Place update_movie endpoint here #------------------------------------ @@ -111,7 +172,7 @@ async def get_all_movies( #------------------------------------ -# ---- Old testing endpoint +# ---- Old testing endpoint, will be removed later ---- ''' # Testing the ErrorReponse Model @router.get("/error") From 68ff1f1aecf130bb8ed5f556485c3423e0711154 Mon Sep 17 00:00:00 2001 From: Taylor McNeil Date: Tue, 21 Oct 2025 15:59:02 -0400 Subject: [PATCH 4/6] chore: removing incomplete unit tests --- server/python/tests/conftest.py | 34 --------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 server/python/tests/conftest.py diff --git a/server/python/tests/conftest.py b/server/python/tests/conftest.py deleted file mode 100644 index 22be62f..0000000 --- a/server/python/tests/conftest.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -import pytest -from httpx import AsyncClient -from src.main import app -from unittest.mock import AsyncMock, MagicMock - -os.environ["ENV"] = "test" -os.environ["MONGO_DB_NAME"] = "sample_mflix" -os.environ["MONGO_URI"] = "mock://no-connection" - -#--------------------------------- -# # 1. Setup Environment for Tests -#--------------------------------- - -@pytest.fixture(scope="session", autouse=True) -def set_test_env(): - print("\n [Setup] Running test environment setup...") - yield - print("\n [Teardown] Tests completed. Cleaning up...") - -#--------------------------- -# 2. Mock MongoDB Collection -#--------------------------- - -@pytest.fixture() -def mock_movies_collection(): - mock_collection = MagicMock() - mock_collection.find = AsyncMock() - - -@pytest.fixture() -def client(): - with AsyncClient(app=app, base_url="http://test") as client: - yield client \ No newline at end of file From 89d3ea3d445528c6b726e055ece96ddbd24747b1 Mon Sep 17 00:00:00 2001 From: Taylor McNeil Date: Thu, 23 Oct 2025 14:27:26 -0400 Subject: [PATCH 5/6] fix(pr feedback & dirty response data): The current mflix db has years with special chars within them. This fix strips all non-digit chars from the year field or sets the year field to none of the string is empty. Typos, spacing, and renaming addressed as well. Removed error msg handling explanation. --- server/python/src/database/mongo_client.py | 2 +- server/python/src/models/models.py | 6 +-- server/python/src/routers/movies.py | 43 ++++++++++++---------- server/python/src/utils/errorHandler.py | 7 ++-- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/server/python/src/database/mongo_client.py b/server/python/src/database/mongo_client.py index e300702..a608530 100644 --- a/server/python/src/database/mongo_client.py +++ b/server/python/src/database/mongo_client.py @@ -5,7 +5,7 @@ load_dotenv() client = AsyncMongoClient(os.getenv("MONGO_URI")) -db =client[os.getenv("MONGO_DB")] +db = client[os.getenv("MONGO_DB")] def get_collection(name:str): return db[name] \ No newline at end of file diff --git a/server/python/src/models/models.py b/server/python/src/models/models.py index 7a4e201..9e66a82 100644 --- a/server/python/src/models/models.py +++ b/server/python/src/models/models.py @@ -10,7 +10,7 @@ class Awards(BaseModel): nominations: Optional[int] = None text: Optional[str] = None -class Imbd(BaseModel): +class Imdb(BaseModel): rating: Optional[float] = None votes: Optional[int] = None id: Optional[int] = None @@ -21,7 +21,7 @@ class Movie(BaseModel): year: Optional[int] = None plot: Optional[str] = None fullplot: Optional[str] = None - released: datetime = None + released: Optional[datetime] = None runtime: Optional[int] = None poster: Optional[str] = None genres: Optional[list[str]] = None @@ -32,7 +32,7 @@ class Movie(BaseModel): languages: Optional[list[str]] = None rated: Optional[str] = None awards: Optional[Awards] = None - imdb: Optional[Imbd] = None + imdb: Optional[Imdb] = None model_config = { "populate_by_name" : True diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 2657c16..73c3da1 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -4,13 +4,14 @@ from typing import List from datetime import datetime from src.utils.errorHandler import create_success_response, create_error_response +import re ''' This file contains all the business logic for movie operations. Each method demonstrates different MongoDB operations using the PyMongo driver. Implemented Endpoints: -- GET /api/movies/ : Retrieve a list of movies with optional filtering, sorting, +- GET /api/movies/ : Retrieve a list of movies with optional filter_dicting, sorting, and pagination. - POST /api/movies/batch : Create multiple movies in a single request. @@ -21,6 +22,8 @@ # Place get_movie_by_id endpoint here #------------------------------------ + + """ GET /api/movies/ @@ -45,50 +48,54 @@ # Validate the query parameters using FastAPI's Query functionality. async def get_all_movies( q:str = Query(default=None), + title: str = Query(default=None), genre:str = Query(default=None), year:int = Query(default=None), min_rating:float = Query(default=None), max_rating:float = Query(default=None), - limit_num:int = Query(default=20, ge=1, le=100), - skip_num:int = Query(default=0, ge=0), + limit:int = Query(default=20, ge=1, le=100), + skip:int = Query(default=0, ge=0), sort_by:str = Query(default="title"), sort_order:str = Query(default="asc") ): - - # This variable naming might not be ideal as Python has a function named filter, but it helps illustrate the point. - filter = {} movies_collection = get_collection("movies") + filter_dict = {} if q: - filter["$text"] = {"$search": q} + filter_dict["$text"] = {"$search": q} + if title: + filter_dict["title"] = {"$regex": title, "$options": "i"} if genre: - filter["genres"] = {"$regex": genre, "$options": "i"} + filter_dict["genres"] = {"$regex": genre, "$options": "i"} if year: # I personally got some dirty data in the year field, a 1995è. Should we guard against this? - filter["year"] = year + filter_dict["year"] = year if min_rating is not None or max_rating is not None: rating_filter = {} if min_rating is not None: rating_filter["$gte"] = min_rating if max_rating is not None: rating_filter["$lte"] = max_rating - filter["imdb.rating"] = rating_filter + filter_dict["imdb.rating"] = rating_filter - - filter = MovieFilter(**filter) - - filter = filter.model_dump(by_alias=True, exclude_none=True) # Building the sort object based on user input sort_order = -1 if sort_order == "desc" else 1 sort = [(sort_by, sort_order)] # Query the database with the constructed filter, sort, skip, and limit. - cursor = movies_collection.find(filter).sort(sort).skip(skip_num).limit(limit_num) + cursor = movies_collection.find(filter_dict).sort(sort).skip(skip).limit(limit) movies = [] async for movie in cursor: movie["_id"] = str(movie["_id"]) # Convert ObjectId to string - movies.append(movie) + # Guarding against the dirty data in the year field. + if "year" in movie and not isinstance(movie["year"], int): + cleaned_year = re.sub(r"\D", "", str(movie["year"])) + try: + movie["year"] = int(cleaned_year) if cleaned_year else None + except ValueError: + movie["year"] = None + movies.append(movie) # Return the results wrapped in a SuccessResponse return create_success_response(movies, f"Found {len(movies)} movies.") @@ -128,10 +135,6 @@ async def get_all_movies( ''' @router.post("/batch") - -# We do not have to write explicit validation code here, Pydantic will handle it for us. -# Invalid movies will result in a 422 response with details about the validation errors. - async def create_movies_batch(movies: List[CreateMovieRequest]): movies_collection = get_collection("movies") movies_dicts = [] diff --git a/server/python/src/utils/errorHandler.py b/server/python/src/utils/errorHandler.py index c7a7e94..4258e3a 100644 --- a/server/python/src/utils/errorHandler.py +++ b/server/python/src/utils/errorHandler.py @@ -1,7 +1,7 @@ from fastapi import Request from fastapi.responses import JSONResponse from pymongo.errors import PyMongoError, DuplicateKeyError, WriteError -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict, Optional from src.models.models import ErrorDetails, ErrorResponse, SuccessResponse, T @@ -32,7 +32,8 @@ def create_success_response(data:T, message: Optional[str] = None) -> SuccessRes return SuccessResponse( message=message or "Operation completed successfully.", data=data, - timestamp=datetime.utcnow().isoformat() + "Z", + timestamp=datetime.now(timezone.utc).isoformat() + "Z", + ) @@ -58,7 +59,7 @@ def create_error_response(message: str, code: Optional[str]=None, details: Optio code=code, details=details ), - timestamp=datetime.utcnow().isoformat() + "Z", + timestamp=datetime.now(timezone.utc).isoformat() + "Z", ) From 38e49731925c4c7e9f3fd378c3caaa9425bfb143 Mon Sep 17 00:00:00 2001 From: Taylor McNeil Date: Fri, 24 Oct 2025 11:16:20 -0400 Subject: [PATCH 6/6] chore: Adopting pip-tools pinning strategy --- server/python/requirements.in | 56 ++++++++++++ server/python/requirements.txt | 135 +++++++++++++++++++++++++--- server/python/src/routers/movies.py | 17 ++-- 3 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 server/python/requirements.in diff --git a/server/python/requirements.in b/server/python/requirements.in new file mode 100644 index 0000000..e8ee7b7 --- /dev/null +++ b/server/python/requirements.in @@ -0,0 +1,56 @@ +# ============================================================================== +# 1. CORE WEB FRAMEWORK & ASGI SERVER +# FastAPI and its main components. +# ------------------------------------------------------------------------------ +fastapi~=0.120.0 # The main web framework +starlette~=0.48.0 # FastAPI's underlying ASGI toolkit +uvicorn~=0.38.0 # Production-ready ASGI server +uvloop~=0.22.0 # Optional: High-performance event loop for uvicorn +websockets~=15.0.0 # For WebSocket support +watchfiles~=1.1.0 # For hot-reloading in development + +# ============================================================================== +# 2. DATA VALIDATION & CORE UTILITIES +# Primary libraries for data models and environment config. +# ------------------------------------------------------------------------------ +pydantic~=2.12.0 # Data validation and settings management +python-dotenv~=1.1.0 # For loading configuration from .env files +python-multipart~=0.0.0 # For parsing form data and file uploads +PyYAML~=6.0.0 # For handling YAML configuration or data + +# ============================================================================== +# 3. DATABASE & CONNECTIVITY +# Database driver and necessary utilities. +# ------------------------------------------------------------------------------ +pymongo~=4.15.0 # MongoDB driver +dnspython~=2.8.0 # Required for SRV record lookups by pymongo (e.g., MongoDB Atlas) + +# ============================================================================== +# 4. HTTP CLIENT & UTILITIES +# Primary libraries for making external HTTP requests. +# ------------------------------------------------------------------------------ +httpx~=0.28.0 # Asynchronous HTTP client for requests to external APIs +email-validator~=2.3.0 # Utility for validating email addresses + +# ============================================================================== +# 5. CLI & DEVELOPMENT TOOLS +# Tools for building command-line interfaces for management tasks. +# ------------------------------------------------------------------------------ +typer~=0.20.0 # Library for creating command-line applications +fastapi-cli~=0.0.0 # Tools to run and manage FastAPI projects +fastapi-cloud-cli~=0.3.0 # Tools for cloud deployment (specific to your pipeline) + +# ============================================================================== +# 6. TESTING & MONITORING +# Frameworks for ensuring code quality and production health. +# ------------------------------------------------------------------------------ +pytest~=8.4.0 # Primary testing framework +pytest-asyncio~=1.2.0 # Plugin to make asynchronous tests easy with pytest +sentry-sdk~=2.42.0 # For error tracking and performance monitoring + +# ============================================================================== +# 7. LOGGING AND TERMINAL OUTPUT +# Libraries for rich console output and debugging. +# ------------------------------------------------------------------------------ +rich~=14.2.0 # For rich, formatted terminal output +rich-toolkit~=0.15.0 # Extensions for the 'rich' library \ No newline at end of file diff --git a/server/python/requirements.txt b/server/python/requirements.txt index 7fa4744..3beb0d8 100644 --- a/server/python/requirements.txt +++ b/server/python/requirements.txt @@ -1,45 +1,154 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile requirements.in +# +annotated-doc==0.0.3 + # via fastapi annotated-types==0.7.0 + # via pydantic anyio==4.11.0 + # via + # httpx + # starlette + # watchfiles certifi==2025.10.5 + # via + # httpcore + # httpx + # sentry-sdk click==8.3.0 + # via + # rich-toolkit + # typer + # uvicorn dnspython==2.8.0 + # via + # -r requirements.in + # email-validator + # pymongo email-validator==2.3.0 -fastapi==0.119.0 -fastapi-cli==0.0.13 + # via + # -r requirements.in + # pydantic +fastapi==0.120.0 + # via -r requirements.in +fastapi-cli==0.0.14 + # via -r requirements.in fastapi-cloud-cli==0.3.1 + # via -r requirements.in h11==0.16.0 + # via + # httpcore + # uvicorn httpcore==1.0.9 + # via httpx httptools==0.7.1 + # via uvicorn httpx==0.28.1 + # via + # -r requirements.in + # fastapi-cloud-cli idna==3.11 + # via + # anyio + # email-validator + # httpx iniconfig==2.3.0 -Jinja2==3.1.6 + # via pytest markdown-it-py==4.0.0 -MarkupSafe==3.0.3 + # via rich mdurl==0.1.2 + # via markdown-it-py packaging==25.0 + # via pytest pluggy==1.6.0 -pydantic==2.12.2 -pydantic_core==2.41.4 -Pygments==2.19.2 + # via pytest +pydantic[email]==2.12.3 + # via + # -r requirements.in + # fastapi + # fastapi-cloud-cli +pydantic-core==2.41.4 + # via pydantic +pygments==2.19.2 + # via + # pytest + # rich pymongo==4.15.3 + # via -r requirements.in pytest==8.4.2 + # via + # -r requirements.in + # pytest-asyncio pytest-asyncio==1.2.0 + # via -r requirements.in python-dotenv==1.1.1 + # via + # -r requirements.in + # uvicorn python-multipart==0.0.20 -PyYAML==6.0.3 + # via -r requirements.in +pyyaml==6.0.3 + # via + # -r requirements.in + # uvicorn rich==14.2.0 + # via + # -r requirements.in + # rich-toolkit + # typer rich-toolkit==0.15.1 + # via + # -r requirements.in + # fastapi-cli + # fastapi-cloud-cli rignore==0.7.1 -sentry-sdk==2.42.0 + # via fastapi-cloud-cli +sentry-sdk==2.42.1 + # via + # -r requirements.in + # fastapi-cloud-cli shellingham==1.5.4 + # via typer sniffio==1.3.1 + # via anyio starlette==0.48.0 -typer==0.19.2 + # via + # -r requirements.in + # fastapi +typer==0.20.0 + # via + # -r requirements.in + # fastapi-cli + # fastapi-cloud-cli +typing-extensions==4.15.0 + # via + # fastapi + # pydantic + # pydantic-core + # rich-toolkit + # typer + # typing-inspection typing-inspection==0.4.2 -typing_extensions==4.15.0 + # via pydantic urllib3==2.5.0 -uvicorn==0.37.0 -uvloop==0.21.0 + # via sentry-sdk +uvicorn[standard]==0.38.0 + # via + # -r requirements.in + # fastapi-cli + # fastapi-cloud-cli +uvloop==0.22.1 + # via + # -r requirements.in + # uvicorn watchfiles==1.1.1 + # via + # -r requirements.in + # uvicorn websockets==15.0.1 + # via + # -r requirements.in + # uvicorn diff --git a/server/python/src/routers/movies.py b/server/python/src/routers/movies.py index 73c3da1..efdf6dd 100644 --- a/server/python/src/routers/movies.py +++ b/server/python/src/routers/movies.py @@ -1,6 +1,6 @@ -from fastapi import APIRouter, HTTPException, Query +from fastapi import APIRouter, Query from src.database.mongo_client import db, get_collection -from src.models.models import CreateMovieRequest, Movie, MovieFilter, SuccessResponse +from src.models.models import CreateMovieRequest, Movie, SuccessResponse from typing import List from datetime import datetime from src.utils.errorHandler import create_success_response, create_error_response @@ -11,7 +11,7 @@ Each method demonstrates different MongoDB operations using the PyMongo driver. Implemented Endpoints: -- GET /api/movies/ : Retrieve a list of movies with optional filter_dicting, sorting, +- GET /api/movies/ : Retrieve a list of movies with optional filter, sorting, and pagination. - POST /api/movies/batch : Create multiple movies in a single request. @@ -67,7 +67,6 @@ async def get_all_movies( if genre: filter_dict["genres"] = {"$regex": genre, "$options": "i"} if year: - # I personally got some dirty data in the year field, a 1995è. Should we guard against this? filter_dict["year"] = year if min_rating is not None or max_rating is not None: rating_filter = {} @@ -76,9 +75,7 @@ async def get_all_movies( if max_rating is not None: rating_filter["$lte"] = max_rating filter_dict["imdb.rating"] = rating_filter - - - + # Building the sort object based on user input sort_order = -1 if sort_order == "desc" else 1 sort = [(sort_by, sort_order)] @@ -88,7 +85,7 @@ async def get_all_movies( movies = [] async for movie in cursor: movie["_id"] = str(movie["_id"]) # Convert ObjectId to string - # Guarding against the dirty data in the year field. + # Ensure that the year field contains int value. if "year" in movie and not isinstance(movie["year"], int): cleaned_year = re.sub(r"\D", "", str(movie["year"])) try: @@ -140,10 +137,6 @@ async def create_movies_batch(movies: List[CreateMovieRequest]): movies_dicts = [] for movie in movies: movies_dicts.append(movie.model_dump(exclude_unset=True, exclude_none=True)) - - #The above line can also be written using list comprehension as: - # movies_dicts = [movie.model_dump(exclude_unset=True) for movie in - result = await movies_collection.insert_many(movies_dicts) return create_success_response({ "insertedCount": len(result.inserted_ids),