From 8e94bc04fae97d9c60555e74effad5943075d948 Mon Sep 17 00:00:00 2001 From: Michael Herman Date: Tue, 23 Jan 2024 09:01:44 -0700 Subject: [PATCH] updates --- .github/workflows/main.yml | 96 ++------ project/Dockerfile | 7 +- project/Dockerfile.prod | 49 +--- project/app/api/crud.py | 30 +-- project/app/api/summaries.py | 50 +--- project/app/config.py | 3 +- project/app/models/pydantic.py | 8 +- project/app/summarizer.py | 21 -- project/db/Dockerfile | 2 +- ...65417_init.py => 0_20240122171233_init.py} | 0 project/requirements-dev.txt | 8 - project/requirements.txt | 21 +- project/tests/test_summaries.py | 180 +------------- project/tests/test_summaries_unit.py | 222 ------------------ release.sh | 12 - 15 files changed, 68 insertions(+), 641 deletions(-) delete mode 100644 project/app/summarizer.py rename project/migrations/models/{0_20230318165417_init.py => 0_20240122171233_init.py} (100%) delete mode 100644 project/requirements-dev.txt delete mode 100644 project/tests/test_summaries_unit.py delete mode 100644 release.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b0dfd5..9013472 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,32 +14,24 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: - ref: main + ref: updates - name: Log in to GitHub Packages run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_ACTOR} --password-stdin ghcr.io env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Pull images + - name: Pull image run: | - docker pull ${{ env.IMAGE }}-builder:latest || true - docker pull ${{ env.IMAGE }}-final:latest || true - - name: Build images + docker pull ${{ env.IMAGE }}:latest || true + - name: Build image run: | docker build \ - --target builder \ - --cache-from ${{ env.IMAGE }}-builder:latest \ - --tag ${{ env.IMAGE }}-builder:latest \ - --file ./project/Dockerfile.prod \ - "./project" - docker build \ - --cache-from ${{ env.IMAGE }}-final:latest \ - --tag ${{ env.IMAGE }}-final:latest \ + --cache-from ${{ env.IMAGE }}:latest \ + --tag ${{ env.IMAGE }}:latest \ --file ./project/Dockerfile.prod \ "./project" - - name: Push images + - name: Push image run: | - docker push ${{ env.IMAGE }}-builder:latest - docker push ${{ env.IMAGE }}-final:latest + docker push ${{ env.IMAGE }}:latest test: name: Test Docker Image @@ -49,26 +41,19 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: - ref: main + ref: updates - name: Log in to GitHub Packages run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_ACTOR} --password-stdin ghcr.io env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Pull images + - name: Pull image run: | - docker pull ${{ env.IMAGE }}-builder:latest || true - docker pull ${{ env.IMAGE }}-final:latest || true - - name: Build images + docker pull ${{ env.IMAGE }}:latest || true + - name: Build image run: | docker build \ - --target builder \ - --cache-from ${{ env.IMAGE }}-builder:latest \ - --tag ${{ env.IMAGE }}-builder:latest \ - --file ./project/Dockerfile.prod \ - "./project" - docker build \ - --cache-from ${{ env.IMAGE }}-final:latest \ - --tag ${{ env.IMAGE }}-final:latest \ + --cache-from ${{ env.IMAGE }}:latest \ + --tag ${{ env.IMAGE }}:latest \ --file ./project/Dockerfile.prod \ "./project" - name: Run container @@ -81,9 +66,7 @@ jobs: -e DATABASE_URL=sqlite://sqlite.db \ -e DATABASE_TEST_URL=sqlite://sqlite.db \ -p 5003:8765 \ - ${{ env.IMAGE }}-final:latest - - name: Install requirements - run: docker exec fastapi-tdd pip install black==23.1.0 flake8==6.0.0 isort==5.12.0 pytest==7.2.2 + ${{ env.IMAGE }}:latest - name: Pytest run: docker exec fastapi-tdd python -m pytest . - name: Flake8 @@ -92,52 +75,3 @@ jobs: run: docker exec fastapi-tdd python -m black . --check - name: isort run: docker exec fastapi-tdd python -m isort . --check-only - - deploy: - name: Deploy to Heroku - runs-on: ubuntu-latest - needs: [build, test] - env: - HEROKU_APP_NAME: radiant-everglades-49858 - HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/summarizer - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: main - - name: Log in to GitHub Packages - run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_ACTOR} --password-stdin ghcr.io - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Pull images - run: | - docker pull ${{ env.IMAGE }}-builder:latest || true - docker pull ${{ env.IMAGE }}-final:latest || true - - name: Build images - run: | - docker build \ - --target builder \ - --cache-from ${{ env.IMAGE }}-builder:latest \ - --tag ${{ env.IMAGE }}-builder:latest \ - --file ./project/Dockerfile.prod \ - "./project" - docker build \ - --cache-from ${{ env.IMAGE }}-final:latest \ - --tag ${{ env.IMAGE }}:latest \ - --tag ${{ env.HEROKU_REGISTRY_IMAGE }}:latest \ - --file ./project/Dockerfile.prod \ - "./project" - - name: Log in to the Heroku Container Registry - run: docker login -u _ -p ${HEROKU_AUTH_TOKEN} registry.heroku.com - env: - HEROKU_AUTH_TOKEN: ${{ secrets.HEROKU_AUTH_TOKEN }} - - name: Push to the registry - run: docker push ${{ env.HEROKU_REGISTRY_IMAGE }}:latest - - name: Set environment variables - run: | - echo "HEROKU_REGISTRY_IMAGE=${{ env.HEROKU_REGISTRY_IMAGE }}" >> $GITHUB_ENV - echo "HEROKU_AUTH_TOKEN=${{ secrets.HEROKU_AUTH_TOKEN }}" >> $GITHUB_ENV - - name: Release - run: | - chmod +x ./release.sh - ./release.sh diff --git a/project/Dockerfile b/project/Dockerfile index 79f4fd9..b752128 100644 --- a/project/Dockerfile +++ b/project/Dockerfile @@ -1,5 +1,5 @@ # pull official base image -FROM python:3.11.2-slim-buster +FROM python:3.12.1-slim-bookworm # set working directory WORKDIR /usr/src/app @@ -10,14 +10,13 @@ ENV PYTHONUNBUFFERED 1 # install system dependencies RUN apt-get update \ - && apt-get -y install netcat gcc postgresql \ + && apt-get -y install netcat-traditional gcc postgresql \ && apt-get clean # install python dependencies RUN pip install --upgrade pip COPY ./requirements.txt . -COPY ./requirements-dev.txt . -RUN pip install -r requirements-dev.txt +RUN pip install -r requirements.txt # add app COPY . . diff --git a/project/Dockerfile.prod b/project/Dockerfile.prod index 27dff14..4015b19 100644 --- a/project/Dockerfile.prod +++ b/project/Dockerfile.prod @@ -1,41 +1,5 @@ -########### -# BUILDER # -########### - # pull official base image -FROM python:3.11.2-slim-buster as builder - -# install system dependencies -RUN apt-get update \ - && apt-get -y install gcc postgresql \ - && apt-get clean - -# set work directory -WORKDIR /usr/src/app - -# set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 - -# install dependencies -RUN pip install --upgrade pip -COPY ./requirements.txt . -RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt - -# lint -COPY . /usr/src/app/ -RUN pip install black==23.1.0 flake8==6.0.0 isort==5.12.0 -RUN flake8 . -RUN black --exclude=migrations . -RUN isort . - - -######### -# FINAL # -######### - -# pull official base image -FROM python:3.11.2-slim-buster +FROM python:3.12.1-slim-bookworm # create directory for the app user RUN mkdir -p /home/app @@ -57,21 +21,20 @@ ENV TESTING 0 # install system dependencies RUN apt-get update \ - && apt-get -y install netcat gcc postgresql \ + && apt-get -y install netcat-traditional gcc postgresql \ && apt-get clean # install python dependencies -COPY --from=builder /usr/src/app/wheels /wheels -COPY --from=builder /usr/src/app/requirements.txt . RUN pip install --upgrade pip -RUN pip install --no-cache /wheels/* -RUN pip install "uvicorn[standard]==0.21.1" +COPY ./requirements.txt . +RUN pip install -r requirements.txt +RUN pip install "uvicorn[standard]==0.26.0" # add app COPY . . # chown all the files to the app user -RUN chown -R app:app $HOME +RUN chown -R app:app $APP_HOME # change to the app user USER app diff --git a/project/app/api/crud.py b/project/app/api/crud.py index 2f3e314..b025098 100644 --- a/project/app/api/crud.py +++ b/project/app/api/crud.py @@ -4,11 +4,6 @@ from app.models.tortoise import TextSummary -async def get_all() -> List: - summaries = await TextSummary.all().values() - return summaries - - async def get(id: int) -> Union[dict, None]: summary = await TextSummary.filter(id=id).first().values() if summary: @@ -16,22 +11,15 @@ async def get(id: int) -> Union[dict, None]: return None -async def post(payload: SummaryPayloadSchema) -> int: - summary = TextSummary(url=payload.url, summary="") - await summary.save() - return summary.id +async def get_all() -> List: + summaries = await TextSummary.all().values() + return summaries -async def put(id: int, payload: SummaryPayloadSchema) -> Union[dict, None]: - summary = await TextSummary.filter(id=id).update( - url=payload.url, summary=payload.summary +async def post(payload: SummaryPayloadSchema) -> int: + summary = TextSummary( + url=payload.url, + summary="dummy summary", ) - if summary: - updated_summary = await TextSummary.filter(id=id).first().values() - return updated_summary - return None - - -async def delete(id: int) -> int: - summary = await TextSummary.filter(id=id).first().delete() - return summary + await summary.save() + return summary.id diff --git a/project/app/api/summaries.py b/project/app/api/summaries.py index 787fe83..0257951 100644 --- a/project/app/api/summaries.py +++ b/project/app/api/summaries.py @@ -1,27 +1,16 @@ from typing import List -from fastapi import APIRouter, BackgroundTasks, HTTPException, Path +from fastapi import APIRouter, HTTPException from app.api import crud +from app.models.pydantic import SummaryPayloadSchema, SummaryResponseSchema from app.models.tortoise import SummarySchema -from app.summarizer import generate_summary - -from app.models.pydantic import ( # isort:skip - SummaryPayloadSchema, - SummaryResponseSchema, - SummaryUpdatePayloadSchema, -) router = APIRouter() -@router.get("/", response_model=List[SummarySchema]) -async def read_all_summaries() -> List[SummarySchema]: - return await crud.get_all() - - @router.get("/{id}/", response_model=SummarySchema) -async def read_summary(id: int = Path(..., gt=0)) -> SummarySchema: +async def read_summary(id: int) -> SummarySchema: summary = await crud.get(id) if not summary: raise HTTPException(status_code=404, detail="Summary not found") @@ -29,35 +18,14 @@ async def read_summary(id: int = Path(..., gt=0)) -> SummarySchema: return summary +@router.get("/", response_model=List[SummarySchema]) +async def read_all_summaries() -> List[SummarySchema]: + return await crud.get_all() + + @router.post("/", response_model=SummaryResponseSchema, status_code=201) -async def create_summary( - payload: SummaryPayloadSchema, background_tasks: BackgroundTasks -) -> SummaryResponseSchema: +async def create_summary(payload: SummaryPayloadSchema) -> SummaryResponseSchema: summary_id = await crud.post(payload) - background_tasks.add_task(generate_summary, summary_id, payload.url) - response_object = {"id": summary_id, "url": payload.url} return response_object - - -@router.put("/{id}/", response_model=SummarySchema) -async def update_summary( - payload: SummaryUpdatePayloadSchema, id: int = Path(..., gt=0) -) -> SummarySchema: - summary = await crud.put(id, payload) - if not summary: - raise HTTPException(status_code=404, detail="Summary not found") - - return summary - - -@router.delete("/{id}/", response_model=SummaryResponseSchema) -async def delete_summary(id: int = Path(..., gt=0)) -> SummaryResponseSchema: - summary = await crud.get(id) - if not summary: - raise HTTPException(status_code=404, detail="Summary not found") - - await crud.delete(id) - - return summary diff --git a/project/app/config.py b/project/app/config.py index 05895b0..cc80aa4 100644 --- a/project/app/config.py +++ b/project/app/config.py @@ -1,7 +1,8 @@ import logging from functools import lru_cache -from pydantic import AnyUrl, BaseSettings +from pydantic import AnyUrl +from pydantic_settings import BaseSettings log = logging.getLogger("uvicorn") diff --git a/project/app/models/pydantic.py b/project/app/models/pydantic.py index 0dc2734..d60d3fa 100644 --- a/project/app/models/pydantic.py +++ b/project/app/models/pydantic.py @@ -1,13 +1,9 @@ -from pydantic import AnyHttpUrl, BaseModel +from pydantic import BaseModel class SummaryPayloadSchema(BaseModel): - url: AnyHttpUrl + url: str class SummaryResponseSchema(SummaryPayloadSchema): id: int - - -class SummaryUpdatePayloadSchema(SummaryPayloadSchema): - summary: str diff --git a/project/app/summarizer.py b/project/app/summarizer.py deleted file mode 100644 index 85c794a..0000000 --- a/project/app/summarizer.py +++ /dev/null @@ -1,21 +0,0 @@ -import nltk -from newspaper import Article - -from app.models.tortoise import TextSummary - - -async def generate_summary(summary_id: int, url: str) -> None: - article = Article(url) - article.download() - article.parse() - - try: - nltk.data.find("tokenizers/punkt") - except LookupError: - nltk.download("punkt") - finally: - article.nlp() - - summary = article.summary - - await TextSummary.filter(id=summary_id).update(summary=summary) diff --git a/project/db/Dockerfile b/project/db/Dockerfile index 954ad8f..69c75af 100644 --- a/project/db/Dockerfile +++ b/project/db/Dockerfile @@ -1,5 +1,5 @@ # pull official base image -FROM postgres:15 +FROM postgres:16 # run create.sql on init ADD create.sql /docker-entrypoint-initdb.d diff --git a/project/migrations/models/0_20230318165417_init.py b/project/migrations/models/0_20240122171233_init.py similarity index 100% rename from project/migrations/models/0_20230318165417_init.py rename to project/migrations/models/0_20240122171233_init.py diff --git a/project/requirements-dev.txt b/project/requirements-dev.txt deleted file mode 100644 index c86a47f..0000000 --- a/project/requirements-dev.txt +++ /dev/null @@ -1,8 +0,0 @@ -black==23.1.0 -flake8==6.0.0 -isort==5.12.0 -pytest==7.2.2 -pytest-cov==4.0.0 -pytest-xdist==3.2.1 - --r requirements.txt diff --git a/project/requirements.txt b/project/requirements.txt index 743f2ca..2aeffa2 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -1,8 +1,13 @@ -aerich==0.7.1 -asyncpg==0.27.0 -fastapi==0.94.1 -gunicorn==20.1.0 -httpx==0.23.3 -newspaper3k==0.2.8 -tortoise-orm==0.19.3 -uvicorn==0.21.1 +aerich==0.7.2 +asyncpg==0.29.0 +black==23.12.1 +fastapi==0.109.0 +flake8==7.0.0 +gunicorn==21.0.1 +httpx==0.26.0 +isort==5.13.2 +pydantic-settings==2.1.0 +pytest==7.4.4 +pytest-cov==4.1.0 +tortoise-orm==0.20.0 +uvicorn==0.26.0 diff --git a/project/tests/test_summaries.py b/project/tests/test_summaries.py index 266f334..2f2684c 100644 --- a/project/tests/test_summaries.py +++ b/project/tests/test_summaries.py @@ -1,16 +1,7 @@ import json -import pytest - -from app.api import summaries - - -def test_create_summary(test_app_with_db, monkeypatch): - def mock_generate_summary(summary_id, url): - return None - - monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary) +def test_create_summary(test_app_with_db): response = test_app_with_db.post( "/summaries/", data=json.dumps({"url": "https://foo.bar"}) ) @@ -25,24 +16,17 @@ def test_create_summaries_invalid_json(test_app): assert response.json() == { "detail": [ { + "input": {}, "loc": ["body", "url"], - "msg": "field required", - "type": "value_error.missing", + "msg": "Field required", + "type": "missing", + "url": "https://errors.pydantic.dev/2.5/v/missing", } ] } - response = test_app.post("/summaries/", data=json.dumps({"url": "invalid://url"})) - assert response.status_code == 422 - assert response.json()["detail"][0]["msg"] == "URL scheme not permitted" - - -def test_read_summary(test_app_with_db, monkeypatch): - def mock_generate_summary(summary_id, url): - return None - - monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary) +def test_read_summary(test_app_with_db): response = test_app_with_db.post( "/summaries/", data=json.dumps({"url": "https://foo.bar"}) ) @@ -54,6 +38,7 @@ def mock_generate_summary(summary_id, url): response_dict = response.json() assert response_dict["id"] == summary_id assert response_dict["url"] == "https://foo.bar" + assert response_dict["summary"] assert response_dict["created_at"] @@ -62,26 +47,8 @@ def test_read_summary_incorrect_id(test_app_with_db): assert response.status_code == 404 assert response.json()["detail"] == "Summary not found" - response = test_app_with_db.get("/summaries/0/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "loc": ["path", "id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ] - } - - -def test_read_all_summaries(test_app_with_db, monkeypatch): - def mock_generate_summary(summary_id, url): - return None - - monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary) +def test_read_all_summaries(test_app_with_db): response = test_app_with_db.post( "/summaries/", data=json.dumps({"url": "https://foo.bar"}) ) @@ -92,134 +59,3 @@ def mock_generate_summary(summary_id, url): response_list = response.json() assert len(list(filter(lambda d: d["id"] == summary_id, response_list))) == 1 - - -def test_remove_summary(test_app_with_db, monkeypatch): - def mock_generate_summary(summary_id, url): - return None - - monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary) - - response = test_app_with_db.post( - "/summaries/", data=json.dumps({"url": "https://foo.bar"}) - ) - summary_id = response.json()["id"] - - response = test_app_with_db.delete(f"/summaries/{summary_id}/") - assert response.status_code == 200 - assert response.json() == {"id": summary_id, "url": "https://foo.bar"} - - -def test_remove_summary_incorrect_id(test_app_with_db): - response = test_app_with_db.delete("/summaries/999/") - assert response.status_code == 404 - assert response.json()["detail"] == "Summary not found" - - response = test_app_with_db.delete("/summaries/0/") - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "loc": ["path", "id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ] - } - - -def test_update_summary(test_app_with_db, monkeypatch): - def mock_generate_summary(summary_id, url): - return None - - monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary) - - response = test_app_with_db.post( - "/summaries/", data=json.dumps({"url": "https://foo.bar"}) - ) - summary_id = response.json()["id"] - - response = test_app_with_db.put( - f"/summaries/{summary_id}/", - data=json.dumps({"url": "https://foo.bar", "summary": "updated!"}), - ) - assert response.status_code == 200 - - response_dict = response.json() - assert response_dict["id"] == summary_id - assert response_dict["url"] == "https://foo.bar" - assert response_dict["summary"] == "updated!" - assert response_dict["created_at"] - - -@pytest.mark.parametrize( - "summary_id, payload, status_code, detail", - [ - [ - 999, - {"url": "https://foo.bar", "summary": "updated!"}, - 404, - "Summary not found", - ], - [ - 0, - {"url": "https://foo.bar", "summary": "updated!"}, - 422, - [ - { - "loc": ["path", "id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ], - ], - [ - 1, - {}, - 422, - [ - { - "loc": ["body", "url"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "summary"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - ], - [ - 1, - {"url": "https://foo.bar"}, - 422, - [ - { - "loc": ["body", "summary"], - "msg": "field required", - "type": "value_error.missing", - } - ], - ], - ], -) -def test_update_summary_invalid( - test_app_with_db, summary_id, payload, status_code, detail -): - response = test_app_with_db.put( - f"/summaries/{summary_id}/", data=json.dumps(payload) - ) - assert response.status_code == status_code - assert response.json()["detail"] == detail - - -def test_update_summary_invalid_url(test_app): - response = test_app.put( - "/summaries/1/", - data=json.dumps({"url": "invalid://url", "summary": "updated!"}), - ) - assert response.status_code == 422 - assert response.json()["detail"][0]["msg"] == "URL scheme not permitted" diff --git a/project/tests/test_summaries_unit.py b/project/tests/test_summaries_unit.py deleted file mode 100644 index bc810ba..0000000 --- a/project/tests/test_summaries_unit.py +++ /dev/null @@ -1,222 +0,0 @@ -import json -from datetime import datetime - -import pytest - -from app.api import crud, summaries - - -def test_create_summary(test_app, monkeypatch): - def mock_generate_summary(summary_id, url): - return None - - monkeypatch.setattr(summaries, "generate_summary", mock_generate_summary) - - test_request_payload = {"url": "https://foo.bar"} - test_response_payload = {"id": 1, "url": "https://foo.bar"} - - async def mock_post(payload): - return 1 - - monkeypatch.setattr(crud, "post", mock_post) - - response = test_app.post( - "/summaries/", - data=json.dumps(test_request_payload), - ) - - assert response.status_code == 201 - assert response.json() == test_response_payload - - -def test_create_summaries_invalid_json(test_app): - response = test_app.post("/summaries/", data=json.dumps({})) - assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "loc": ["body", "url"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - - response = test_app.post("/summaries/", data=json.dumps({"url": "invalid://url"})) - assert response.status_code == 422 - assert response.json()["detail"][0]["msg"] == "URL scheme not permitted" - - -def test_read_summary(test_app, monkeypatch): - test_data = { - "id": 1, - "url": "https://foo.bar", - "summary": "summary", - "created_at": datetime.utcnow().isoformat(), - } - - async def mock_get(id): - return test_data - - monkeypatch.setattr(crud, "get", mock_get) - - response = test_app.get("/summaries/1/") - assert response.status_code == 200 - assert response.json() == test_data - - -def test_read_summary_incorrect_id(test_app, monkeypatch): - async def mock_get(id): - return None - - monkeypatch.setattr(crud, "get", mock_get) - - response = test_app.get("/summaries/999/") - assert response.status_code == 404 - assert response.json()["detail"] == "Summary not found" - - -def test_read_all_summaries(test_app, monkeypatch): - test_data = [ - { - "id": 1, - "url": "https://foo.bar", - "summary": "summary", - "created_at": datetime.utcnow().isoformat(), - }, - { - "id": 2, - "url": "https://testdrivenn.io", - "summary": "summary", - "created_at": datetime.utcnow().isoformat(), - }, - ] - - async def mock_get_all(): - return test_data - - monkeypatch.setattr(crud, "get_all", mock_get_all) - - response = test_app.get("/summaries/") - assert response.status_code == 200 - assert response.json() == test_data - - -def test_remove_summary(test_app, monkeypatch): - async def mock_get(id): - return { - "id": 1, - "url": "https://foo.bar", - "summary": "summary", - "created_at": datetime.utcnow().isoformat(), - } - - monkeypatch.setattr(crud, "get", mock_get) - - async def mock_delete(id): - return id - - monkeypatch.setattr(crud, "delete", mock_delete) - - response = test_app.delete("/summaries/1/") - assert response.status_code == 200 - assert response.json() == {"id": 1, "url": "https://foo.bar"} - - -def test_remove_summary_incorrect_id(test_app, monkeypatch): - async def mock_get(id): - return None - - monkeypatch.setattr(crud, "get", mock_get) - - response = test_app.delete("/summaries/999/") - assert response.status_code == 404 - assert response.json()["detail"] == "Summary not found" - - -def test_update_summary(test_app, monkeypatch): - test_request_payload = {"url": "https://foo.bar", "summary": "updated"} - test_response_payload = { - "id": 1, - "url": "https://foo.bar", - "summary": "summary", - "created_at": datetime.utcnow().isoformat(), - } - - async def mock_put(id, payload): - return test_response_payload - - monkeypatch.setattr(crud, "put", mock_put) - - response = test_app.put( - "/summaries/1/", - data=json.dumps(test_request_payload), - ) - assert response.status_code == 200 - assert response.json() == test_response_payload - - -@pytest.mark.parametrize( - "summary_id, payload, status_code, detail", - [ - [ - 999, - {"url": "https://foo.bar", "summary": "updated!"}, - 404, - "Summary not found", - ], - [ - 0, - {"url": "https://foo.bar", "summary": "updated!"}, - 422, - [ - { - "loc": ["path", "id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, - } - ], - ], - [ - 1, - {}, - 422, - [ - { - "loc": ["body", "url"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "summary"], - "msg": "field required", - "type": "value_error.missing", - }, - ], - ], - [ - 1, - {"url": "https://foo.bar"}, - 422, - [ - { - "loc": ["body", "summary"], - "msg": "field required", - "type": "value_error.missing", - } - ], - ], - ], -) -def test_update_summary_invalid( - test_app, monkeypatch, summary_id, payload, status_code, detail -): - async def mock_put(id, payload): - return None - - monkeypatch.setattr(crud, "put", mock_put) - - response = test_app.put(f"/summaries/{summary_id}/", data=json.dumps(payload)) - assert response.status_code == status_code - assert response.json()["detail"] == detail diff --git a/release.sh b/release.sh deleted file mode 100644 index bb7e334..0000000 --- a/release.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -set -e - -IMAGE_ID=$(docker inspect ${HEROKU_REGISTRY_IMAGE} --format={{.Id}}) -PAYLOAD='{"updates": [{"type": "web", "docker_image": "'"$IMAGE_ID"'"}]}' - -curl -n -X PATCH https://api.heroku.com/apps/$HEROKU_APP_NAME/formation \ - -d "${PAYLOAD}" \ - -H "Content-Type: application/json" \ - -H "Accept: application/vnd.heroku+json; version=3.docker-releases" \ - -H "Authorization: Bearer ${HEROKU_AUTH_TOKEN}"