From f88b8410bf662590a8259b1b992b67dbcfb0cf10 Mon Sep 17 00:00:00 2001 From: Colin Goutte Date: Sat, 26 Aug 2023 23:20:18 +0200 Subject: [PATCH 1/4] Add short useful alias for refactoring --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5b3ddaf..9a2e9c5 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,13 @@ run_dev: git ls-files | entr -r pipenv run python dev.py tdd: - git ls-files | entr make test opt=$(opt) + git ls-files | entr make test opt='$(opt)' git ls-files | entr make functionnal_tests +refactor_tdd: + make tdd opt="--pdb --ff --lf --ff -x" + + watch_db: watch "sqlite3 sql_app.db 'select * from movies'" From 4543f2f0d7063444082cce11f8b9fb98f4d276f6 Mon Sep 17 00:00:00 2001 From: Colin Goutte Date: Sat, 26 Aug 2023 23:21:30 +0200 Subject: [PATCH 2/4] cleanup: make use of pydatic schemas for create --- dev.py | 24 +----------------------- schemas.py | 10 +++++----- utests/test_api.py | 34 ++++++++++++++++++++-------------- 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/dev.py b/dev.py index 1136b09..890beed 100644 --- a/dev.py +++ b/dev.py @@ -44,35 +44,13 @@ async def root(): return {"message": "Hello World"} -@app.post("/pydantic_movies/") +@app.post("/movies/") async def create_movie(payload: schemas.MoviePayload, db: Session = Depends(get_db)): movie = crud.create_movie(db, **payload.dict()) out = {"message": f"Created {movie.title} XX", "id": movie.id} return out -@app.post("/movies/") -async def create_movie( - name: str = "", db: Session = Depends(get_db), request: Request = None -): - out = {} - try: # Bypass for dev - data = await request.json() - except: - data = {} - crud_params = dict( - genres=data.get("genres", ["Unknown"]), - description=data.get("description", ""), - title=data.get("title", ""), - vote_average=data.get("vote_average"), - vote_count=data.get("vote_count"), - ) - - movie = crud.create_movie(db, **crud_params) - out = {"message": f"Created {movie.title} XX", "id": movie.id} - return out - - @app.put("/movies/{id_}") async def update_movie( id_: str, db: Session = Depends(get_db), request: Request = None diff --git a/schemas.py b/schemas.py index 8609fe6..c1baf48 100644 --- a/schemas.py +++ b/schemas.py @@ -4,9 +4,9 @@ from pydantic import BaseModel class MoviePayload(BaseModel): title: str - vote_count: int - vote_average: float + vote_count: int = 0 + vote_average: float | None = None - genres: list[str] - description: str - release_date: str + genres: list[str] = [] + description: str = "" + release_date: str | None = None # Use custom formatted string validation diff --git a/utests/test_api.py b/utests/test_api.py index 0af0fa1..7a855c0 100644 --- a/utests/test_api.py +++ b/utests/test_api.py @@ -51,6 +51,12 @@ def rand_name(): class BaseCrud(unittest.TestCase): + def setUp(self): + name = f"rand_{random.randint(1, 1000)}" + self.create_payload = { + "title": name, + } + def test_get_delete_movie_404_if_not_found(self): response = client.get("/movies/-1") assert response.status_code == 404 @@ -58,18 +64,16 @@ class BaseCrud(unittest.TestCase): assert response_delete.status_code == 404 def test_create_movie_api(self): - name = f"rand_{random.randint(1, 1000)}" - response = client.post("/movies/", json={"title": name}) + response = client.post("/movies/", json=self.create_payload) assert response.status_code == 200 movie_id = response.json()["id"] - assert f"Created {name}" in response.json()["message"] + assert f"Created {self.create_payload['title']}" in response.json()["message"] response = client.get(f"/movies/{movie_id}") - assert response.json()["title"] == name + assert response.json()["title"] == self.create_payload["title"] def test_delete_movie(self): - name = f"rand_{random.randint(1, 1000)}" - response = client.post("/movies/", json={"title": name}) + response = client.post("/movies/", json=self.create_payload) movie_id = response.json()["id"] created_movie_path = f"/movies/{movie_id}" @@ -81,12 +85,12 @@ class BaseCrud(unittest.TestCase): assert response_missing.status_code == 404 def test_update_movie_api(self): - name = f"rand_{random.randint(1, 1000)}" - response = client.post("/movies/", json={"title": name, "genres": ["anime"]}) + self.create_payload["genres"] = ["anime"] + response = client.post("/movies/", json=self.create_payload) movie_id = response.json()["id"] created_movie_path = f"/movies/{movie_id}" - new_name = name.replace("rand", "update") + new_name = self.create_payload["title"].replace("rand", "update") new_genres = ["Drama", "war"] response_get = client.get(f"/movies/{movie_id}") @@ -104,6 +108,7 @@ class BaseCrud(unittest.TestCase): def test_list_movies(self): response = client.get("/movies/") + assert response.status_code == 200 # assert response.json() == [] N = 10 @@ -112,7 +117,8 @@ class BaseCrud(unittest.TestCase): name = rand_name() names.append(name) - response = client.post("/movies/", json={"title": name}) + self.create_payload["title"] = name + response = client.post("/movies/", json=self.create_payload) assert response.status_code == 200 movies = client.get("/movies/") @@ -185,7 +191,7 @@ class ApiTestCase(unittest.TestCase): payload.pop(missing_key) - response = client.post("/pydantic_movies/", json=payload) + response = client.post("/movies/", json=payload) assert 400 <= response.status_code < 500 @@ -215,13 +221,13 @@ class ApiTestCase(unittest.TestCase): payload.pop(missing_key) - response = client.post("/pydantic_movies/", json=payload) + response = client.post("/movies/", json=payload) assert 400 <= response.status_code < 500 assert response.status_code == 400 - def test_payload_content_in_and_out_loopback_pydantic(self): + def test_payload_content_in_and_out_loopback(self): be_the_fun_in_de_funes = { "id": 1, "title": "La Grande Vadrouille", @@ -242,7 +248,7 @@ class ApiTestCase(unittest.TestCase): payload = {k: be_the_fun_in_de_funes[k] for k in domain_keys} # FIXME - response = client.post("/pydantic_movies/", json=payload) + response = client.post("/movies/", json=payload) assert response.status_code == 200 movie_id = response.json()["id"] From 5c00c51b437597b238fb425b9e2a7184de41d406 Mon Sep 17 00:00:00 2001 From: Colin Goutte Date: Sat, 26 Aug 2023 23:33:53 +0200 Subject: [PATCH 3/4] Refactor: Make use of pydantic schemas where possible --- database.py | 2 +- dev.py | 23 +++++++---------------- schemas.py | 6 +++++- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/database.py b/database.py index 388e28b..6f57615 100644 --- a/database.py +++ b/database.py @@ -25,7 +25,7 @@ def fill_db(): import random def _genres(): - random.choice([["Comedy"], ["Comedy", "Drama"], []]) + return random.choice(["Comedy"], ["Comedy", "Drama"], []) for _ in range(3): name = f"fill_db_{random.randint(1, 1000):03}" diff --git a/dev.py b/dev.py index 890beed..a0a8e2d 100644 --- a/dev.py +++ b/dev.py @@ -54,7 +54,7 @@ async def create_movie(payload: schemas.MoviePayload, db: Session = Depends(get_ @app.put("/movies/{id_}") async def update_movie( id_: str, db: Session = Depends(get_db), request: Request = None -): +) -> schemas.MovieObject: try: movie = crud.get_movie_by_id(db, id_) except LookupError: @@ -74,24 +74,21 @@ async def update_movie( movie = crud.update_movie(db, id_, **crud_params) - out = {k: v for (k, v) in movie.__dict__.items() if not k.startswith("_")} - return out + return movie @app.get("/movies/{id_}") -async def get_movie(id_: str, db: Session = Depends(get_db)): +async def get_movie(id_: str, db: Session = Depends(get_db)) -> schemas.MovieObject: try: movie = crud.get_movie_by_id(db, id_) - - out = {k: v for (k, v) in movie.__dict__.items() if not k.startswith("_")} except LookupError: raise HTTPException(status_code=404, detail=f"No movie found with id {id_}") else: - return out + return movie @app.delete("/movies/{id_}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_movie(id_: str, db: Session = Depends(get_db)): +async def delete_movie(id_: str, db: Session = Depends(get_db)) -> None: try: movie = crud.delete_movie_by_id(db, id_) except LookupError: @@ -99,14 +96,8 @@ async def delete_movie(id_: str, db: Session = Depends(get_db)): @app.get("/movies/") -async def list_movie(db: Session = Depends(get_db)): - movies = crud.get_all_movies(db) - - out = [ - {k: v for (k, v) in movie.__dict__.items() if not k.startswith("_")} - for movie in movies - ] - return out +async def list_movie(db: Session = Depends(get_db)) -> list[schemas.MovieObject]: + return crud.get_all_movies(db) if __name__ == "__main__": diff --git a/schemas.py b/schemas.py index c1baf48..7015bba 100644 --- a/schemas.py +++ b/schemas.py @@ -4,9 +4,13 @@ from pydantic import BaseModel class MoviePayload(BaseModel): title: str - vote_count: int = 0 + vote_count: int | None = 0 vote_average: float | None = None genres: list[str] = [] description: str = "" release_date: str | None = None # Use custom formatted string validation + + +class MovieObject(MoviePayload): + id: int | str From b410fa9bc8126fa463fb7deb6c1850130a86c441 Mon Sep 17 00:00:00 2001 From: Colin Goutte Date: Sat, 26 Aug 2023 23:42:16 +0200 Subject: [PATCH 4/4] Shorly update description --- dev.py | 18 ++++++++++++++++++ utests/test_api.py | 3 +++ 2 files changed, 21 insertions(+) diff --git a/dev.py b/dev.py index a0a8e2d..aeadee1 100644 --- a/dev.py +++ b/dev.py @@ -53,6 +53,24 @@ async def create_movie(payload: schemas.MoviePayload, db: Session = Depends(get_ @app.put("/movies/{id_}") async def update_movie( + id_: str, + payload: schemas.MoviePayload, + db: Session = Depends(get_db), + request: Request = None, +) -> schemas.MovieObject: + try: + movie = crud.get_movie_by_id(db, id_) + except LookupError: + raise HTTPException(status_code=404, detail=f"No movie found with id {id_}") + + crud_params = payload.dict() + movie = crud.update_movie(db, id_, **crud_params) + + return movie + + +@app.patch("/movies/{id_}") +async def patch_movie( id_: str, db: Session = Depends(get_db), request: Request = None ) -> schemas.MovieObject: try: diff --git a/utests/test_api.py b/utests/test_api.py index 7a855c0..768f693 100644 --- a/utests/test_api.py +++ b/utests/test_api.py @@ -86,6 +86,8 @@ class BaseCrud(unittest.TestCase): def test_update_movie_api(self): self.create_payload["genres"] = ["anime"] + + self.create_payload["description"] = legacy_description = "will be deleted" response = client.post("/movies/", json=self.create_payload) movie_id = response.json()["id"] created_movie_path = f"/movies/{movie_id}" @@ -105,6 +107,7 @@ class BaseCrud(unittest.TestCase): response_get = client.get(f"/movies/{movie_id}") assert response_get.json()["title"] == new_name assert response_get.json()["genres"] == new_genres + assert response_get.json()["description"] == "" def test_list_movies(self): response = client.get("/movies/")