├── .dockerignore ├── .gitignore ├── start ├── templates ├── fragments │ └── header.html ├── todo │ ├── item.html │ └── form.html ├── base.html └── home.html ├── requirements.txt ├── database.py ├── Dockerfile ├── docker-compose.yml ├── LICENSE ├── models.py └── main.py /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .* 3 | *.db 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .idea 3 | 4 | __pycache__ 5 | 6 | *.db 7 | -------------------------------------------------------------------------------- /start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | uvicorn main:app --port 8001 --host 0.0.0.0 8 | -------------------------------------------------------------------------------- /templates/fragments/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /templates/todo/item.html: -------------------------------------------------------------------------------- 1 |
  • 2 |

    {{ todo.content }}

    3 | 4 |
  • 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.65.2 2 | SQLAlchemy~=1.4.15 3 | aiofiles==0.6.0 4 | click==7.1.2 5 | fastapi==0.65.2 6 | greenlet==1.1.0 7 | h11==0.12.0 8 | Jinja2==3.0.0 9 | MarkupSafe==2.0.0 10 | pydantic==1.8.2 11 | python-multipart==0.0.5 12 | six==1.16.0 13 | SQLAlchemy==1.4.15 14 | starlette==0.14.2 15 | typing-extensions==3.10.0.0 16 | uvicorn==0.13.4 17 | -------------------------------------------------------------------------------- /templates/todo/form.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 |
    5 |
  • 6 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" 6 | 7 | engine = create_engine( 8 | SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} 9 | ) 10 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 11 | 12 | Base = declarative_base() 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN addgroup --system fastapi \ 6 | && adduser --system --ingroup fastapi fastapi 7 | 8 | COPY --chown=fastapi:fastapi ./requirements.txt /requirements.txt 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | COPY --chown=fastapi:fastapi ./start /start 12 | RUN sed -i 's/\r$//g' /start 13 | RUN chmod +x /start 14 | 15 | COPY --chown=fastapi:fastapi . /app 16 | 17 | WORKDIR /app 18 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block style %} 17 | {% endblock %} 18 | 19 | 20 | 21 | {% block header %} 22 | {% include 'fragments/header.html' %} 23 | {% endblock %} 24 |
    25 | {% block content %} 26 | {% endblock %} 27 |
    28 | 29 | {% block javascript %} 30 | {% endblock %} 31 | 32 | 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | networks: 4 | traefik-public: 5 | external: true 6 | 7 | services: 8 | todo_app: 9 | build: 10 | context: . 11 | dockerfile: Dockerfile 12 | command: /start 13 | image: renceinbox/fastapi-todo:main 14 | networks: 15 | - traefik-public 16 | deploy: 17 | labels: 18 | - traefik.enable=true 19 | - traefik.docker.network=traefik-public 20 | - traefik.constraint-label=traefik-public 21 | - traefik.http.routers.fastapi-todo-http.rule=Host(`todo-fastapi.renceinbox.com`) 22 | - traefik.http.routers.fastapi-todo-http.entrypoints=http 23 | - traefik.http.routers.fastapi-todo-http.middlewares=https-redirect 24 | - traefik.http.routers.fastapi-todo-https.rule=Host(`todo-fastapi.renceinbox.com`) 25 | - traefik.http.routers.fastapi-todo-https.entrypoints=https 26 | - traefik.http.routers.fastapi-todo-https.tls=true 27 | - traefik.http.routers.fastapi-todo-https.tls.certresolver=le 28 | - traefik.http.services.fastapi-todo.loadbalancer.server.port=8001 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rommel Terrence Juanillo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block style %} 4 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
    14 |
    15 |
    16 | 17 | 18 |
    19 |
    20 | 25 |
    26 | {% endblock %} 27 | 28 | {% block javascript %} 29 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column 2 | from sqlalchemy import Integer 3 | from sqlalchemy import String 4 | from sqlalchemy.orm import Session 5 | 6 | from database import Base 7 | 8 | 9 | class ToDo(Base): 10 | __tablename__ = "todos" 11 | 12 | id = Column(Integer, primary_key=True, index=True) 13 | content = Column(String) 14 | session_key = Column(String) 15 | 16 | 17 | def create_todo(db: Session, content: str, session_key: str): 18 | todo = ToDo(content=content, session_key=session_key) 19 | db.add(todo) 20 | db.commit() 21 | db.refresh(todo) 22 | return todo 23 | 24 | 25 | def get_todo(db: Session, item_id: int): 26 | return db.query(ToDo).filter(ToDo.id == item_id).first() 27 | 28 | 29 | def update_todo(db: Session, item_id: int, content: str): 30 | todo = get_todo(db, item_id) 31 | todo.content = content 32 | db.commit() 33 | db.refresh(todo) 34 | return todo 35 | 36 | 37 | def get_todos(db: Session, session_key: str, skip: int = 0, limit: int = 100): 38 | return db.query(ToDo).filter(ToDo.session_key == session_key).offset(skip).limit(limit).all() 39 | 40 | 41 | def delete_todo(db: Session, item_id: int): 42 | todo = get_todo(db, item_id) 43 | db.delete(todo) 44 | db.commit() 45 | 46 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import random 2 | import uuid 3 | from datetime import timedelta 4 | 5 | from fastapi import Depends 6 | from fastapi import FastAPI, Form 7 | from fastapi import Request, Response 8 | from fastapi.responses import HTMLResponse 9 | 10 | from fastapi.templating import Jinja2Templates 11 | from sqlalchemy.orm import Session 12 | 13 | from database import Base 14 | from database import SessionLocal 15 | from database import engine 16 | from models import create_todo 17 | from models import delete_todo 18 | from models import get_todo 19 | from models import get_todos 20 | from models import update_todo 21 | 22 | Base.metadata.create_all(bind=engine) 23 | 24 | app = FastAPI() 25 | templates = Jinja2Templates(directory="templates") 26 | 27 | 28 | def get_db(): 29 | db = SessionLocal() 30 | try: 31 | yield db 32 | finally: 33 | db.close() 34 | 35 | 36 | @app.get("/", response_class=HTMLResponse) 37 | def home(request: Request, db: Session = Depends(get_db)): 38 | session_key = request.cookies.get("session_key", uuid.uuid4().hex) 39 | todos = get_todos(db, session_key) 40 | context = { 41 | "request": request, 42 | "todos": todos, 43 | "title": "Home" 44 | } 45 | response = templates.TemplateResponse("home.html", context) 46 | response.set_cookie(key="session_key", value=session_key, expires=259200) # 3 days 47 | return response 48 | 49 | 50 | @app.post("/add", response_class=HTMLResponse) 51 | def post_add(request: Request, content: str = Form(...), db: Session = Depends(get_db)): 52 | session_key = request.cookies.get("session_key") 53 | todo = create_todo(db, content=content, session_key=session_key) 54 | context = {"request": request, "todo": todo} 55 | return templates.TemplateResponse("todo/item.html", context) 56 | 57 | 58 | @app.get("/edit/{item_id}", response_class=HTMLResponse) 59 | def get_edit(request: Request, item_id: int, db: Session = Depends(get_db)): 60 | todo = get_todo(db, item_id) 61 | context = {"request": request, "todo": todo} 62 | return templates.TemplateResponse("todo/form.html", context) 63 | 64 | 65 | @app.put("/edit/{item_id}", response_class=HTMLResponse) 66 | def put_edit(request: Request, item_id: int, content: str = Form(...), db: Session = Depends(get_db)): 67 | todo = update_todo(db, item_id, content) 68 | context = {"request": request, "todo": todo} 69 | return templates.TemplateResponse("todo/item.html", context) 70 | 71 | 72 | @app.delete("/delete/{item_id}", response_class=Response) 73 | def delete(item_id: int, db: Session = Depends(get_db)): 74 | delete_todo(db, item_id) 75 | --------------------------------------------------------------------------------