Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__pycache__
env
.idea
.idea
.coverage
htmlcov
3 changes: 3 additions & 0 deletions project/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[run]
omit = tests/*
branch = True
2 changes: 2 additions & 0 deletions project/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.coverage
htmlcov
43 changes: 43 additions & 0 deletions project/Dockerfile.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# pull official base image
FROM python:3.8.8-slim-buster

# create directory for the app user
RUN mkdir -p /home/app

# create the app user
RUN addgroup --system app && adduser --system --group app

# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV ENVIRONMENT prod
ENV TESTING 0

# install system dependencies
RUN apt-get update \
&& apt-get -y install netcat gcc postgresql \
&& apt-get clean

# install python dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
RUN pip install "uvicorn[standard]==0.13.4"

# add app
COPY . .

# chown all the files to the app user
RUN chown -R app:app $APP_HOME

# change to the app user
USER app

# run gunicorn
CMD gunicorn --bind 0.0.0.0:$PORT app.main:app -k uvicorn.workers.UvicornWorker
31 changes: 31 additions & 0 deletions project/app/api/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# project/app/api/crud.py


from typing import List, Union

from app.models.pydanticSchemas import SummaryPayloadSchema
from app.models.tortoiseORM import TextSummary


async def post(payload: SummaryPayloadSchema) -> int:
summary = TextSummary(
url=payload.url,
summary="dummy summary",
)
await summary.save()
return summary.id


async def get(id: int) -> Union[dict, None]:
summary = await TextSummary.filter(id=id).first().values()
if summary:
return summary[0]
return None


async def get_all() -> Union[List, None]:
summaries = await TextSummary.all().values()

if not summaries:
return None
return summaries
7 changes: 3 additions & 4 deletions project/app/api/ping.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

from fastapi import APIRouter, Depends

from app.config import get_settings, Settings

from app.config import Settings, get_settings

router = APIRouter()

Expand All @@ -14,5 +13,5 @@ async def pong(settings: Settings = Depends(get_settings)):
return {
"ping": "pong!",
"environment": settings.environment,
"testing": settings.testing
}
"testing": settings.testing,
}
38 changes: 38 additions & 0 deletions project/app/api/summaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# project/app/api/summaries.py

import logging

log = logging.getLogger("uvicorn")

from typing import List, Union

from fastapi import APIRouter, HTTPException

from app.api import crud
from app.models.pydanticSchemas import (SummaryPayloadSchema,
SummaryResponseSchema)
from app.models.tortoiseORM import SummarySchema

router = APIRouter()


@router.post("/", response_model=SummaryResponseSchema, status_code=201)
async def create_summary(payload: SummaryPayloadSchema) -> SummaryResponseSchema:
summary_id = await crud.post(payload)
log.info("inside post method")
response_object = {"id": summary_id, "url": payload.url}
return response_object


@router.get("/{id}/", response_model=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")

return summary


@router.get("/", response_model=List[SummarySchema])
async def read_all_summaries() -> List[SummarySchema]:
return await crud.get_all()
5 changes: 2 additions & 3 deletions project/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import os
from functools import lru_cache

from pydantic import BaseSettings, AnyUrl

from pydantic import AnyUrl, BaseSettings

log = logging.getLogger("uvicorn")

Expand All @@ -20,4 +19,4 @@ class Settings(BaseSettings):
@lru_cache()
def get_settings() -> BaseSettings:
log.info("Loading config settings from the environment...")
return Settings()
return Settings()
8 changes: 4 additions & 4 deletions project/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@ def init_db(app: FastAPI) -> None:
generate_schemas=False,
add_exception_handlers=False,
)
Tortoise.init_models(['app.models.tortoiseORM'], 'models')
Tortoise.init_models(["app.models.tortoiseORM"], "models")


async def generate_schema() -> None:
log.info("Initializing Tortoise...")

await Tortoise.init(
db_url=os.environ.get("DATABASE_URL"),
modules={"models": ["app.models.tortoiseORM"]},
modules={"models": ["models.tortoiseORM"]},
)
log.info("Generating database schema via Tortoise...")
await Tortoise.generate_schemas()
await Tortoise.close_connections()


# run_async(generate_schema())
if __name__ == "__main__":
run_async(generate_schema())
run_async(generate_schema())
8 changes: 4 additions & 4 deletions project/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@


import logging
import asyncio

from fastapi import FastAPI

from app.api import ping, summaries # updated
from app.db import init_db


log = logging.getLogger("uvicorn")


def create_application() -> FastAPI:
application = FastAPI()
application.include_router(ping.router)
application.include_router(summaries.router, prefix="/summaries", tags=["summaries"]) # new
application.include_router(
summaries.router, prefix="/summaries", tags=["summaries"]
) # new

return application

Expand All @@ -32,4 +32,4 @@ async def startup_event():

@app.on_event("shutdown")
async def shutdown_event():
log.info("Shutting down...")
log.info("Shutting down...")
12 changes: 12 additions & 0 deletions project/app/models/pydanticSchemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# project/app/models/pydanticSchemas.py


from pydantic import BaseModel


class SummaryPayloadSchema(BaseModel):
url: str


class SummaryResponseSchema(SummaryPayloadSchema):
id: int
17 changes: 17 additions & 0 deletions project/app/models/tortoiseORM.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# project/app/models/tortoise.py


from tortoise import fields, models
from tortoise.contrib.pydantic import pydantic_model_creator # new


class TextSummary(models.Model):
url = fields.TextField()
summary = fields.TextField()
created_at = fields.DatetimeField(auto_now_add=True)

def __str__(self):
return self.url


SummarySchema = pydantic_model_creator(TextSummary) # new
7 changes: 6 additions & 1 deletion project/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ uvicorn==0.13.4
asyncpg==0.22.0
tortoise-orm==0.16.21
pytest==6.2.2
requests==2.25.1
requests==2.25.1
gunicorn==20.0.4
pytest-cov==2.11.1
flake8===3.8.4
black==20.8b1
isort==5.7.0
2 changes: 2 additions & 0 deletions project/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 119
6 changes: 3 additions & 3 deletions project/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import pytest
from starlette.testclient import TestClient
from tortoise.contrib.fastapi import register_tortoise

from app.config import Settings, get_settings
from app.main import create_application
from app.config import get_settings, Settings
from tortoise.contrib.fastapi import register_tortoise


def get_settings_override():
Expand Down Expand Up @@ -43,4 +43,4 @@ def test_app_with_db():
# testing
yield test_client

# tear down
# tear down
3 changes: 2 additions & 1 deletion project/tests/test_ping.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

from app import main


def test_ping(test_app):
response = test_app.get("/ping")
assert response.status_code == 200
assert response.json() == {"environment": "dev", "ping": "pong!", "testing": True}
assert response.json() == {"environment": "dev", "ping": "pong!", "testing": True}
19 changes: 19 additions & 0 deletions project/tests/test_read_all_summaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import json
import logging

log = logging.getLogger("uvicorn")


def test_read_all_summaries(test_app_with_db):
response = test_app_with_db.post(
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
)
summary_id = response.json()["id"]

response = test_app_with_db.get("/summaries/")
assert response.status_code == 200

log.info(response)

response_list = response.json()
assert len(list(filter(lambda d: d["id"] == summary_id, response_list))) == 1
17 changes: 17 additions & 0 deletions project/tests/test_read_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import json


def test_read_summary(test_app_with_db):
response = test_app_with_db.post(
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
)
summary_id = response.json()["id"]

response = test_app_with_db.get(f"/summaries/{summary_id}/")
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"]
assert response_dict["created_at"]
4 changes: 4 additions & 0 deletions project/tests/test_read_summary_incorrect_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def test_read_summary_incorrect_id(test_app_with_db):
response = test_app_with_db.get("/summaries/999/")
assert response.status_code == 404
assert response.json()["detail"] == "Summary not found"
29 changes: 29 additions & 0 deletions project/tests/test_summaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# project/tests/test_summaries.py


import json

import pytest


def test_create_summary(test_app_with_db):
response = test_app_with_db.post(
"/summaries/", data=json.dumps({"url": "https://foo.bar"})
)

assert response.status_code == 201
assert response.json()["url"] == "https://foo.bar"


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",
}
]
}