initial commit
This commit is contained in:
commit
6c731b04d3
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
|
Loading…
Reference in New Issue
Block a user