├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── database.py ├── docker-compose.yml ├── main.py ├── models.py ├── requirements.txt ├── start └── templates ├── base.html ├── fragments └── header.html ├── home.html └── todo ├── form.html └── item.html /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .* 3 | *.db 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .idea 3 | 4 | __pycache__ 5 | 6 | *.db 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from fastapi import Depends 4 | from fastapi import FastAPI, Form 5 | from fastapi import Request, Response 6 | from fastapi.responses import HTMLResponse 7 | from fastapi.templating import Jinja2Templates 8 | from sqlalchemy.orm import Session 9 | 10 | from database import Base 11 | from database import SessionLocal 12 | from database import engine 13 | from models import create_todo 14 | from models import delete_todo 15 | from models import get_todo 16 | from models import get_todos 17 | from models import update_todo 18 | 19 | Base.metadata.create_all(bind=engine) 20 | 21 | app = FastAPI() 22 | templates = Jinja2Templates(directory="templates") 23 | 24 | 25 | def get_db(): 26 | db = SessionLocal() 27 | try: 28 | yield db 29 | finally: 30 | db.close() 31 | 32 | 33 | @app.get("/", response_class=HTMLResponse) 34 | def home(request: Request, db: Session = Depends(get_db)): 35 | session_key = request.cookies.get("session_key", uuid.uuid4().hex) 36 | todos = get_todos(db, session_key) 37 | context = { 38 | "request": request, 39 | "todos": todos, 40 | "title": "Home" 41 | } 42 | response = templates.TemplateResponse("home.html", context) 43 | response.set_cookie(key="session_key", value=session_key, expires=259200) # 3 days 44 | return response 45 | 46 | 47 | @app.post("/add", response_class=HTMLResponse) 48 | def post_add(request: Request, content: str = Form(...), db: Session = Depends(get_db)): 49 | session_key = request.cookies.get("session_key") 50 | todo = create_todo(db, content=content, session_key=session_key) 51 | context = {"request": request, "todo": todo} 52 | return templates.TemplateResponse("todo/item.html", context) 53 | 54 | 55 | @app.get("/edit/{item_id}", response_class=HTMLResponse) 56 | def get_edit(request: Request, item_id: int, db: Session = Depends(get_db)): 57 | todo = get_todo(db, item_id) 58 | context = {"request": request, "todo": todo} 59 | return templates.TemplateResponse("todo/form.html", context) 60 | 61 | 62 | @app.put("/edit/{item_id}", response_class=HTMLResponse) 63 | def put_edit(request: Request, item_id: int, content: str = Form(...), db: Session = Depends(get_db)): 64 | todo = update_todo(db, item_id, content) 65 | context = {"request": request, "todo": todo} 66 | return templates.TemplateResponse("todo/item.html", context) 67 | 68 | 69 | @app.delete("/delete/{item_id}", response_class=Response) 70 | def delete(item_id: int, db: Session = Depends(get_db)): 71 | delete_todo(db, item_id) 72 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer 2 | from sqlalchemy import String 3 | from sqlalchemy.orm import Session, Mapped, mapped_column 4 | 5 | from database import Base 6 | 7 | 8 | class ToDo(Base): 9 | __tablename__ = "todos" 10 | 11 | id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) 12 | content: Mapped[str] = mapped_column(String) 13 | session_key: Mapped[str] = mapped_column(String) 14 | 15 | 16 | def create_todo(db: Session, content: str, session_key: str): 17 | todo = ToDo(content=content, session_key=session_key) 18 | db.add(todo) 19 | db.commit() 20 | db.refresh(todo) 21 | return todo 22 | 23 | 24 | def get_todo(db: Session, item_id: int): 25 | return db.query(ToDo).filter(ToDo.id == item_id).first() 26 | 27 | 28 | def update_todo(db: Session, item_id: int, content: str): 29 | todo = get_todo(db, item_id) 30 | todo.content = content 31 | db.commit() 32 | db.refresh(todo) 33 | return todo 34 | 35 | 36 | def get_todos(db: Session, session_key: str, skip: int = 0, limit: int = 100): 37 | return db.query(ToDo).filter(ToDo.session_key == session_key).offset(skip).limit(limit).all() 38 | 39 | 40 | def delete_todo(db: Session, item_id: int): 41 | todo = get_todo(db, item_id) 42 | db.delete(todo) 43 | db.commit() 44 | 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.4.0 3 | certifi==2024.6.2 4 | click==8.1.7 5 | dnspython==2.6.1 6 | email_validator==2.2.0 7 | fastapi==0.111.0 8 | fastapi-cli==0.0.4 9 | greenlet==3.0.3 10 | h11==0.14.0 11 | httpcore==1.0.5 12 | httptools==0.6.1 13 | httpx==0.27.0 14 | idna==3.7 15 | Jinja2==3.1.4 16 | markdown-it-py==3.0.0 17 | MarkupSafe==2.1.5 18 | mdurl==0.1.2 19 | orjson==3.10.5 20 | pydantic==2.7.4 21 | pydantic_core==2.18.4 22 | Pygments==2.18.0 23 | python-dotenv==1.0.1 24 | python-multipart==0.0.9 25 | PyYAML==6.0.1 26 | rich==13.7.1 27 | shellingham==1.5.4 28 | sniffio==1.3.1 29 | SQLAlchemy==2.0.31 30 | starlette==0.37.2 31 | typer==0.12.3 32 | typing_extensions==4.12.2 33 | ujson==5.10.0 34 | uvicorn==0.30.1 35 | uvloop==0.19.0 36 | watchfiles==0.22.0 37 | websockets==12.0 38 | -------------------------------------------------------------------------------- /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/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |{{ todo.content }}
3 | 4 |