initial commit
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
.venv
 | 
			
		||||
**/*.pyc
 | 
			
		||||
 | 
			
		||||
data/db/*
 | 
			
		||||
data/static/avatars/*
 | 
			
		||||
!data/static/avatars/default.webp
 | 
			
		||||
							
								
								
									
										23
									
								
								app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
from flask import Flask
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
def create_app():
 | 
			
		||||
    app = Flask(__name__)
 | 
			
		||||
 | 
			
		||||
    app.config["DB_PATH"] = "data/db/db.sqlite"
 | 
			
		||||
 | 
			
		||||
    os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
 | 
			
		||||
    with app.app_context():
 | 
			
		||||
        from .schema import create as create_tables
 | 
			
		||||
        from .migrations import run_migrations
 | 
			
		||||
        create_tables()
 | 
			
		||||
        run_migrations()
 | 
			
		||||
 | 
			
		||||
    if os.getenv("PYROM_PROD") is None:
 | 
			
		||||
        app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
 | 
			
		||||
        app.debug = True
 | 
			
		||||
 | 
			
		||||
    from app.routes.app import bp as app_bp
 | 
			
		||||
    app.register_blueprint(app_bp)
 | 
			
		||||
 | 
			
		||||
    return app
 | 
			
		||||
							
								
								
									
										12
									
								
								app/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/auth.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
from argon2 import PasswordHasher
 | 
			
		||||
 | 
			
		||||
ph = PasswordHasher()
 | 
			
		||||
 | 
			
		||||
def hash_password(password):
 | 
			
		||||
    return ph.hash(password)
 | 
			
		||||
 | 
			
		||||
def verify(expected, given):
 | 
			
		||||
    try:
 | 
			
		||||
        return ph.verify(expected, given)
 | 
			
		||||
    except:
 | 
			
		||||
        return False
 | 
			
		||||
							
								
								
									
										218
									
								
								app/db.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								app/db.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,218 @@
 | 
			
		||||
import sqlite3
 | 
			
		||||
from contextlib import contextmanager
 | 
			
		||||
from flask import current_app
 | 
			
		||||
 | 
			
		||||
class DB:
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self._transaction_depth = 0
 | 
			
		||||
        self._connection = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @contextmanager
 | 
			
		||||
    def _get_connection(self):
 | 
			
		||||
        if self._connection and self._transaction_depth > 0:
 | 
			
		||||
            yield self._connection
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        conn = sqlite3.connect(current_app.config["DB_PATH"])
 | 
			
		||||
        conn.row_factory = sqlite3.Row
 | 
			
		||||
        conn.execute("PRAGMA FOREIGN_KEYS = 1")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            yield conn
 | 
			
		||||
        finally:
 | 
			
		||||
            if self._transaction_depth == 0:
 | 
			
		||||
                conn.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @contextmanager
 | 
			
		||||
    def transaction(self):
 | 
			
		||||
        """Transaction context."""
 | 
			
		||||
        self.begin()
 | 
			
		||||
        try:
 | 
			
		||||
            yield
 | 
			
		||||
            self.commit()
 | 
			
		||||
        except Exception:
 | 
			
		||||
            self.rollback()
 | 
			
		||||
            raise
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def begin(self):
 | 
			
		||||
        """Begins a new transaction."""
 | 
			
		||||
        if self._transaction_depth == 0:
 | 
			
		||||
            if not self._connection:
 | 
			
		||||
                self._connection = sqlite3.connect(current_app.config["DB_PATH"])
 | 
			
		||||
                self._connection.row_factory = sqlite3.Row
 | 
			
		||||
                self._connection.execute("PRAGMA FOREIGN_KEYS = 1")
 | 
			
		||||
            self._connection.execute("BEGIN")
 | 
			
		||||
        self._transaction_depth += 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def commit(self):
 | 
			
		||||
        """Commits the current transaction."""
 | 
			
		||||
        if self._transaction_depth > 0:
 | 
			
		||||
            self._transaction_depth -= 1
 | 
			
		||||
            if self._transaction_depth == 0:
 | 
			
		||||
                self._connection.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def rollback(self):
 | 
			
		||||
        """Rolls back the current transaction."""
 | 
			
		||||
        if self._transaction_depth > 0:
 | 
			
		||||
            self._transaction_depth = 0
 | 
			
		||||
            self._connection.rollback()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def query(self, sql, *args):
 | 
			
		||||
        """Executes a query and returns a list of dictionaries."""
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            rows = conn.execute(sql, args).fetchall()
 | 
			
		||||
            return [dict(row) for row in rows]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def insert(self, table, columns, *values):
 | 
			
		||||
        if isinstance(columns, (list, tuple)):
 | 
			
		||||
            columns = ", ".join(columns)
 | 
			
		||||
 | 
			
		||||
        placeholders = ", ".join(["?"] * len(values))
 | 
			
		||||
        sql = f"""
 | 
			
		||||
            INSERT INTO {table} ({columns})
 | 
			
		||||
            VALUES ({placeholders})
 | 
			
		||||
            RETURNING *
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            result = conn.execute(sql, values).fetchone()
 | 
			
		||||
            conn.commit()
 | 
			
		||||
            return dict(result) if result else None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def execute(self, sql, *args):
 | 
			
		||||
        """Executes a query without returning."""
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            conn.execute(sql, args)
 | 
			
		||||
            conn.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def fetch_one(self, sql, *args):
 | 
			
		||||
        """Grabs the first row of a query."""
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            row = conn.execute(sql, args).fetchone()
 | 
			
		||||
            return dict(row) if row else None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    class QueryBuilder:
 | 
			
		||||
        def __init__(self, table):
 | 
			
		||||
            self.table = table
 | 
			
		||||
            self._where = {}
 | 
			
		||||
            self._select = "*"
 | 
			
		||||
            self._params = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        def select(self, columns = "*"):
 | 
			
		||||
            self._select = columns
 | 
			
		||||
            return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        def where(self, condition):
 | 
			
		||||
            self._where.update(condition)
 | 
			
		||||
            return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        def build_select(self):
 | 
			
		||||
            sql = f"SELECT {self._select} FROM {self.table}"
 | 
			
		||||
            if self._where:
 | 
			
		||||
                conditions = " AND ".join(f"{k} = ?" for k in self._where.keys())
 | 
			
		||||
                sql += f" WHERE {conditions}"
 | 
			
		||||
            return sql, list(self._where.values())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        def build_update(self, data):
 | 
			
		||||
            columns = ", ".join(f"{k} = ?" for k in data.keys())
 | 
			
		||||
            sql = f"UPDATE {self.table} SET {columns}"
 | 
			
		||||
            if self._where:
 | 
			
		||||
                conditions = " AND ".join(f"{k} = ?" for k in self._where.keys())
 | 
			
		||||
                sql += f" WHERE {conditions}"
 | 
			
		||||
            params = list(data.values()) + list(self._where.values())
 | 
			
		||||
            return sql, params
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        def build_delete(self):
 | 
			
		||||
            sql = f"DELETE FROM {self.table}"
 | 
			
		||||
            if self._where:
 | 
			
		||||
                conditions = " AND ".join(f"{k} = ?" for k in self._where.keys())
 | 
			
		||||
                sql += f" WHERE {conditions}"
 | 
			
		||||
            return sql, list(self._where.values())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        def first(self):
 | 
			
		||||
            sql, params = self.build_select()
 | 
			
		||||
            print(sql, params)
 | 
			
		||||
            return db.fetch_one(f"{sql} LIMIT 1", *params)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        def all(self):
 | 
			
		||||
            sql, params = self.build_select()
 | 
			
		||||
            return db.query(sql, *params)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Model:
 | 
			
		||||
    def __init__(self, table):
 | 
			
		||||
        self.table = table
 | 
			
		||||
        self._data = {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def __getitem__(self, key):
 | 
			
		||||
        return self._data[key]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def __getattr__(self, key):
 | 
			
		||||
        try:
 | 
			
		||||
            return self._data[key]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            raise AttributeError(f"No column '{key}'")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def find(cls, condition):
 | 
			
		||||
        row = db.QueryBuilder(cls.table)\
 | 
			
		||||
            .where(condition)\
 | 
			
		||||
            .first()
 | 
			
		||||
        if not row:
 | 
			
		||||
            return None
 | 
			
		||||
        instance = cls(cls.table)
 | 
			
		||||
        instance._data = dict(row)
 | 
			
		||||
        return instance
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def create(cls, values):
 | 
			
		||||
        if not values:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        columns = list(values.keys())
 | 
			
		||||
        row = db.insert(cls.table, columns, *values.values())
 | 
			
		||||
 | 
			
		||||
        if row:
 | 
			
		||||
            instance = cls(cls.table)
 | 
			
		||||
            instance._data = row
 | 
			
		||||
            return instance
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def update(self, data):
 | 
			
		||||
        qb = db.QueryBuilder(self.table)\
 | 
			
		||||
            .where({"id": self._data["id"]})
 | 
			
		||||
        sql, params = qb.build_update(data)
 | 
			
		||||
        db.execute(sql, *params)
 | 
			
		||||
        self._data.update(data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def delete(self):
 | 
			
		||||
        qb = db.QueryBuilder(self.table)\
 | 
			
		||||
            .where({"id": self._data["id"]})
 | 
			
		||||
        sql, params = qb.build_delete()
 | 
			
		||||
        db.execute(sql, *params)
 | 
			
		||||
        self._data = {}
 | 
			
		||||
 | 
			
		||||
db = DB()
 | 
			
		||||
							
								
								
									
										30
									
								
								app/migrations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/migrations.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
from .db import db
 | 
			
		||||
 | 
			
		||||
# format: {integer: str|list<str>}
 | 
			
		||||
MIGRATIONS = {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def run_migrations():
 | 
			
		||||
    print("Running migrations...")
 | 
			
		||||
    ran = 0
 | 
			
		||||
    db.execute("""
 | 
			
		||||
        CREATE TABLE IF NOT EXISTS _migrations(
 | 
			
		||||
            id INTEGER PRIMARY KEY
 | 
			
		||||
        )
 | 
			
		||||
    """)
 | 
			
		||||
    completed = [row["id"] for row in db.query("SELECT id FROM _migrations")]
 | 
			
		||||
    for migration_id in sorted(MIGRATIONS.keys()):
 | 
			
		||||
        if migration_id not in completed:
 | 
			
		||||
            print(f"Running migration #{migration_id}")
 | 
			
		||||
            ran += 1
 | 
			
		||||
            statements = MIGRATIONS[migration_id]
 | 
			
		||||
            # support both strings and lists
 | 
			
		||||
            if isinstance(statements, str):
 | 
			
		||||
                statements = [statements]
 | 
			
		||||
 | 
			
		||||
            for sql in statements:
 | 
			
		||||
                db.execute(sql)
 | 
			
		||||
 | 
			
		||||
            db.execute("INSERT INTO _migrations (id) VALUES (?)", migration_id)
 | 
			
		||||
    print(f"Ran {ran} migrations.")
 | 
			
		||||
							
								
								
									
										1
									
								
								app/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/models.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
from .db import Model
 | 
			
		||||
							
								
								
									
										7
									
								
								app/routes/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/routes/app.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
from flask import Blueprint
 | 
			
		||||
 | 
			
		||||
bp = Blueprint("app", __name__, url_prefix = "/")
 | 
			
		||||
 | 
			
		||||
@bp.route("/")
 | 
			
		||||
def hello_world():
 | 
			
		||||
    return f"<img src='static/avatars/default.webp'></img>"
 | 
			
		||||
							
								
								
									
										10
									
								
								app/run.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/run.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
from app import create_app
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
app = create_app()
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    app.run(
 | 
			
		||||
        host = "127.0.0.1",
 | 
			
		||||
        port = 8080
 | 
			
		||||
    )
 | 
			
		||||
							
								
								
									
										13
									
								
								app/schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/schema.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
from .db import db
 | 
			
		||||
 | 
			
		||||
# list of statements
 | 
			
		||||
SCHEMA = [
 | 
			
		||||
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
def create():
 | 
			
		||||
    print("Creating schema...")
 | 
			
		||||
    with db.transaction():
 | 
			
		||||
        for stmt in SCHEMA:
 | 
			
		||||
            db.execute(stmt)
 | 
			
		||||
    print("Schema completed.")
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								data/static/avatars/default.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								data/static/avatars/default.webp
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.1 KiB  | 
							
								
								
									
										3
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
flask
 | 
			
		||||
argon2-cffi
 | 
			
		||||
wand
 | 
			
		||||
		Reference in New Issue
	
	Block a user