Skip to content

Conversation

@shuangela
Copy link
Collaborator

@shuangela shuangela commented Oct 24, 2025

Basic crud functionality and aggregation pipelines for python backend

This PR introduces basic CRUD functionality and aggregation pipelines for the FastAPI application. It sets up find, insert, delete, and find and delete operations. It also adds aggregations for movies by year, most recent comments (joining the movies collection with the comments collection), and by director.

Key Changes

  • Added CRUD functionality
  • Added three aggregation pipelines

Testing

  • Verified all endpoints locally via FastAPI docs (/docs)
  • Confirmed database connection and data persistence
  • Checked error responses for validation and connection issues

@shuangela shuangela changed the title Crud and Aggregations for Python feat: crud functionality and aggregations for python backend Oct 24, 2025
Copy link
Collaborator

@cbullinger cbullinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a couple comments/questions. nice job!

@router.get("/{id}", response_model=SuccessResponse[Movie])
async def get_movie_by_id(id: str):
# Validate ObjectId format
object_id = ObjectId(id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we wrap this in a try-except? i.e. what happens if id isn't valid?

# Use findOne() to get a single document by _id
movie = await db.movies.find_one({"_id": object_id})

movie["_id"] = str(movie["_id"]) # Convert ObjectId to string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same - what happens if movie is None?

print(f"Database name: {db.name if hasattr(db, 'name') else 'unknown'}")
print(f"Collection name: movies")

# For motor (async MongoDB driver), we need to await the aggregate call
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we weren't using Motor (instead using PyMongo's native async)

Suggested change
# For motor (async MongoDB driver), we need to await the aggregate call
# For async PyMongo driver, we need to await the aggregate call

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, we aren't using motor. Just async within the PyMongo driver.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not, i think copilot added this comment for some reason despite not using motor 😓


# For motor (async MongoDB driver), we need to await the aggregate call
cursor = await db.movies.aggregate(pipeline)
results = await cursor.to_list(length=None) # Convert cursor to list
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to point out why we're using to_list() for aggregations vs. async for for find queries (what you did in lines 105-108)

cursor = await db.movies.aggregate(pipeline)
results = await cursor.to_list(length=None) # Convert cursor to list

print(f"Aggregation returned {len(results)} results") # Debug logging
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a ticket to add proper logging?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe there's an official logging ticket, this was just my logging for my own testing purposes locally. I can remove it if that makes the code cleaner.

Copy link
Collaborator

@tmcneil-mdb tmcneil-mdb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job!
These are some minor changes. Mostly to keep the code similar and adding in validation. I didn't get to the end of the file. I will get to find and delete on Monday.

I havent written an aggregation yet, so I might leave those for now. I will ping you, if I get to them.

object_id = ObjectId(id)

# Use findOne() to get a single document by _id
movie = await db.movies.find_one({"_id": object_id})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To grab the db, I added a function called get_collection from mongo_client.py file to make unit testing easier later. I am calling the db like this in the rest of the functions:

movies_collection = get_collection("movies")

genre (str): The genre of the movie.
year (int): The year the movie was released.
min_rating (float): The minimum IMDB rating.
max_rating (float): The maximum IMDB rating.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request body for this the CreateMovieRequest object.

result = await db.movies.insert_one(movie_data)

# Retrieve the created document to return complete data
created_movie = await db.movies.find_one({"_id": result.inserted_id})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to verify that the document was created before querying it. A check that result is acknowledged.

SuccessResponse[Movie]: A response object containing the created movie data.
"""

@router.post("/", response_model=SuccessResponse[CreateMovieRequest], status_code=201)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be:
response_model=SuccessResponse[Movie]

We are returning the movie


@router.delete("/{id}", response_model=SuccessResponse[dict])
async def delete_movie_by_id(id: str):
object_id = ObjectId(id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap in a try/catch. Id might not be valid.

result = await db.movies.delete_one({"_id": object_id})

if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="Movie not found")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use the create_error_response() to keep the errors consistent.

object_id = ObjectId(id)

# Use deleteOne() to remove a single document
result = await db.movies.delete_one({"_id": object_id})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above about accessing the db.


@router.delete("/{id}/find-and-delete", response_model=SuccessResponse[Movie])
async def find_and_delete_movie(id: str):
object_id = ObjectId(id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap in try /except

deleted_movie = await db.movies.find_one_and_delete({"_id": object_id})

if deleted_movie is None:
raise HTTPException(status_code=404, detail="Movie not found")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convert to our standard error response

SuccessResponse[List[dict]]: A response object containing aggregated genre statistics.
"""

@router.get("/aggregate/by-genre", response_model=SuccessResponse[List[dict]])
Copy link
Collaborator

@tmcneil-mdb tmcneil-mdb Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we are doing by genre?

Either way the endpoint should be /api/movies/reportingByGenre

I'm not against using /aggregate. I think that would look nicer, but its a change we all have to agree on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, i will remove

}
]

# Execute the aggregation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try/except

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this code

SuccessResponse[List[dict]]: A response object containing movies with their most recent comments.
"""

@router.get("/aggregate/recent-commented", response_model=SuccessResponse[List[dict]])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.
I think the endpoint is /api/movies/reportingByYear

object_id = ObjectId(movie_id)
pipeline[0]["$match"]["_id"] = object_id
except Exception:
raise HTTPException(status_code=400, detail="Invalid movie_id format")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standardize error response.

])

# Execute the aggregation
results = await execute_aggregation(pipeline)
Copy link
Collaborator

@tmcneil-mdb tmcneil-mdb Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try / except.

"$sort": {"mostRecentCommentDate": -1}
},
{
"$limit": 50 if movie_id else 20
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use limit? You defined it earlier.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, i think this is some copilot weirdness i should've caught! fixing

SuccessResponse[List[dict]]: A response object containing yearly movie statistics.
"""

@router.get("/aggregate/by-year", response_model=SuccessResponse[List[dict]])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/movies/reportingByYear

]

# Execute the aggregation
results = await execute_aggregation(pipeline)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try/ except

]

# Execute the aggregation
results = await execute_aggregation(pipeline)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try / except

SuccessResponse[List[dict]]: A response object containing director statistics.
"""

@router.get("/aggregate/directors", response_model=SuccessResponse[List[dict]])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/movies/reportingByDirector

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants