diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86163cf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +instance/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..1c1ffa9 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,6 @@ +# Run locally + +In increasing complexity: + +* `./basic-run.sh` - just raw-dog it +* `docker compose up --build` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3f9bf57 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +ARG PYTHON_VERSION=3.9.6 +FROM python:${PYTHON_VERSION}-slim as base + +# Prevents Python from writing pyc files. +ENV PYTHONDONTWRITEBYTECODE=1 + +# Keeps Python from buffering stdout and stderr to avoid situations where +# the application crashes without emitting any logs due to buffering. +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. +# Leverage a bind mount to requirements.txt to avoid having to copy them into +# into this layer. +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + python -m pip install -r requirements.txt + +# Switch to the non-privileged user to run the application. +USER appuser + +# Copy the source code into the container. +COPY . . + +# Expose the port that the application listens on. +EXPOSE 5000 + + +# Run the application. Note that this sets a hard-coded secret key, which is not secure - use proper secret generation +# and management in production! +CMD FLASK_APP=app SECRET_KEY=super-secret flask run --host=0.0.0.0 diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..42ec51a --- /dev/null +++ b/NOTES.md @@ -0,0 +1,40 @@ +# Development plan + +- [X] Basic Game Definition +- [X] Basic Player Definition +- [ ] Basic Deck Definition +- [ ] Basic testing +- [ ] Swagger API +- [ ] Local development tool to clear/seed database +... +- [ ] Authentication (will need to link `user` table to `player`) +... +- [ ] Helm chart including an initContainer to create the database if it doesn't exist already + + +# Tables + +Tables: +* Decks + * Name + * Description + * Owner + * DecklistId (optional) +* Players (not the same as Users! Can model a Player who is not a User) +* Users + * Standard auth stuff +* Games + * Date + * Location + * DeckIds (array) + * WinningDeckId + * FinalTurnNum + * Notes + +# Database Migrations + +https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/quickstart/#create-the-tables + +# Authentication + +https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..48b4f98 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,32 @@ +import os +import sys + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +def create_app(): + app = Flask(__name__) + + secret_key = os.environ.get('SECRET_KEY') + if not secret_key: + sys.stderr.write('YOU NEED TO SET AN ENV VARIABLE NAMED SECRET_KEY\n') + sys.exit(1) + app.config['SECRET_KEY'] = secret_key + + # TODO - support other database types 🙃 + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' + + db.init_app(app) + + from .main import main as main_blueprint + app.register_blueprint(main_blueprint) + + # TODO - understand how this works, since `db.create_all()` requires that the model classes have already been + # imported in order to know what to create. Perhaps `__init__.py` just magically has the context of everything in + # its module? Good opportunity to learn more about the Python import system! + with app.app_context(): + db.create_all() + + return app diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..7965301 --- /dev/null +++ b/app/main.py @@ -0,0 +1,22 @@ +from flask import Blueprint, request +from . import db +from .models import Deck, Game, Player + +main = Blueprint("main", __name__) + +@main.route("/") +def index(): + return 'Hello, World - but new!' + +@main.route("/player", methods=["POST"]) +def create_player(): + data = request.json + player = Player( + name=data['name'] + ) + db.session.add(player) + db.session.commit() + return {'id': player.id} + +# TODO - implement a GET method - can it be a separate method, or must it be the same annotation with an `if method==`? +# Time for testing, methinks! diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..63960d4 --- /dev/null +++ b/app/models.py @@ -0,0 +1,32 @@ +from . import db + +# Note that a `Player` is "someone who plays in the Pod", whereas `User` (which will be implemented later) is "a user of +# this system". While all Users will _probably_ be Players, they do not have to be - and, it is likely that several +# Players will not be Users (if they don't register to use the system). +class Player(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, nullable=False) + +class Deck(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(60), nullable=False) + description = db.Column(db.String) + owner = db.Column(db.String, db.ForeignKey(Player.__table__.c.id), nullable=False, ) + +class Game(db.Model): + id = db.Column(db.Integer, primary_key=True) + # TODO - columns like `location`, `writeups`, etc. + + # Not wild about this structure ("fill in non-null columns to indicate decks that were present"), but what can you + # do with a database that doesn't support arrays? (Postgres _does_, but I don't wanna ramp up on a whole other + # database system just for that...) + deck_1 = db.Column(db.Integer) + deck_2 = db.Column(db.Integer) + deck_3 = db.Column(db.Integer) + deck_4 = db.Column(db.Integer) + deck_5 = db.Column(db.Integer) + deck_6 = db.Column(db.Integer) + deck_7 = db.Column(db.Integer) + deck_8 = db.Column(db.Integer) + deck_9 = db.Column(db.Integer) + deck_10 = db.Column(db.Integer) diff --git a/basic-run.sh b/basic-run.sh new file mode 100755 index 0000000..2345ede --- /dev/null +++ b/basic-run.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +# Idempotent +source .venv/bin/activate +FLASK_APP=app SECRET_KEY=super-secret flask run diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..224ba53 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,49 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "server". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + server: + build: + context: . + ports: + - 5000:5000 + +# The commented out section below is an example of how to define a PostgreSQL +# database that your application can use. `depends_on` tells Docker Compose to +# start the database before your application. The `db-data` volume persists the +# database data between container restarts. The `db-password` secret is used +# to set the database password. You must create `db/password.txt` and add +# a password of your choosing to it before running `docker compose up`. +# depends_on: +# db: +# condition: service_healthy +# db: +# image: postgres +# restart: always +# user: postgres +# secrets: +# - db-password +# volumes: +# - db-data:/var/lib/postgresql/data +# environment: +# - POSTGRES_DB=example +# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password +# expose: +# - 5432 +# healthcheck: +# test: [ "CMD", "pg_isready" ] +# interval: 10s +# timeout: 5s +# retries: 5 +# volumes: +# db-data: +# secrets: +# db-password: +# file: db/password.txt + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e0858f3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Flask +Flask-SQLAlchemy +flasgger +flask-login