commit 6c731b04d375baf72249f81fbf09c6aa92dc26e6 Author: Lera ElvoƩ Date: Sun Jun 29 18:17:04 2025 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1cda462 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.venv +**/*.pyc + +data/db/* +data/static/avatars/* +!data/static/avatars/default.webp diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..bd0f573 --- /dev/null +++ b/app/__init__.py @@ -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 diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..47a5d28 --- /dev/null +++ b/app/auth.py @@ -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 diff --git a/app/db.py b/app/db.py new file mode 100644 index 0000000..db8d9ba --- /dev/null +++ b/app/db.py @@ -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() diff --git a/app/migrations.py b/app/migrations.py new file mode 100644 index 0000000..07a7eca --- /dev/null +++ b/app/migrations.py @@ -0,0 +1,30 @@ +from .db import db + +# format: {integer: str|list} +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.") diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..f0987c4 --- /dev/null +++ b/app/models.py @@ -0,0 +1 @@ +from .db import Model diff --git a/app/routes/app.py b/app/routes/app.py new file mode 100644 index 0000000..67b0e87 --- /dev/null +++ b/app/routes/app.py @@ -0,0 +1,7 @@ +from flask import Blueprint + +bp = Blueprint("app", __name__, url_prefix = "/") + +@bp.route("/") +def hello_world(): + return f"" diff --git a/app/run.py b/app/run.py new file mode 100644 index 0000000..eb66f73 --- /dev/null +++ b/app/run.py @@ -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 + ) diff --git a/app/schema.py b/app/schema.py new file mode 100644 index 0000000..a856aff --- /dev/null +++ b/app/schema.py @@ -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.") diff --git a/data/static/avatars/default.webp b/data/static/avatars/default.webp new file mode 100644 index 0000000..69329fe Binary files /dev/null and b/data/static/avatars/default.webp differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..06db0fc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +argon2-cffi +wand