Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
QUART_DEBUG=true
QUART_TESTING=true
QUART_AUTH_COOKIE_SECURE=
QUART_SECRET_KEY=''
QUART_SUBDOMAIN_MATCHING=false
GITHUB_ID=''
GITHUB_SECRET=''
SERVER_NAME=''
DATABASE_PATH=''
DATABASE_PATH=''
PORT=5000
18 changes: 18 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Deploy to dev server (https://dev.yip.cat)
on:
push:
branches: [ dev ]

jobs:
deploy-dev:
name: deploy-dev
runs-on: ubuntu-latest
steps:
- name: Execute remote SSH commands using SSH key
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
port: ${{ secrets.PORT }}
script: ./deploy-dev.sh
18 changes: 18 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Deploy to main server (https://yip.cat)
on:
push:
branches: [ main ]

jobs:
deploy:
name: deploy
runs-on: ubuntu-latest
steps:
- name: Execute remote SSH commands using SSH key
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
port: ${{ secrets.PORT }}
script: ./deploy.sh
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
__pycache__
.env
*.db
*.log
*.log
blueprint-config.yaml
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.9.26
hooks:
- id: uv-lock
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.13
hooks:
# Run the linter.
- id: ruff-check
args: [ --fix ]
# Run the formatter.
- id: ruff-format
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"python-envs.defaultEnvManager": "ms-python.python:venv",
"python-envs.pythonProjects": []
}
83 changes: 7 additions & 76 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,87 +1,18 @@
import re
from quart import Quart, request, render_template
from quart_cors import cors
from quart_auth import QuartAuth

from logging import getLogger
from os import environ

from src.about import is_dev
from src.auth import User


class ASGIMiddleware:
# this is my baby. she is deformed.
# be nice to my baby.
def __init__(self, app) -> None:
self.app = app

async def inner(self, scope):
if scope["type"] != "http":
return scope
headers = scope.get("headers", [])
host = (list(filter(lambda e: e[0] == b"host", headers)) or [None])[0]
if host is None or len(host) != 2:
return scope
if host[1].decode().startswith("dev."):
index = headers.index(host)
host = (host[0], host[1].decode().replace("dev.", "").encode())
headers[index] = host
scope["headers"] = headers
return scope

async def __call__(self, scope, recv, send):
scope = await self.inner(scope)
await self.app(scope, recv, send)


app = Quart(__name__)
app.config.from_prefixed_env("QUART")
app.asgi_app = ASGIMiddleware(app.asgi_app)
config_mode = "Production"

if app.config["DEBUG"]:
config_mode = "Development"
app.logger.info("Loading Development configuration...")
else:
app = cors(app, allow_origin=re.compile("https://*.yip.cat*"))
app.config.from_object(f"src.config.{config_mode}")

auth_manager = QuartAuth(app)
auth_manager.user_class = User

from src.blueprints import ( # noqa: E402
index_blueprint,
term_blueprint,
github_blueprint,
)

for blueprint in [index_blueprint, term_blueprint, github_blueprint]:
app.register_blueprint(blueprint)


async def static(location=None, filename=None):
if filename is None:
return
from quart import url_for

if location is not None:
return url_for(f"{location}.static", filename=filename)
return url_for("static", filename=filename)


app.jinja_env.globals.update(static=static)
auth_manager.init_app(app)
from dotenv import load_dotenv

from src.app import create_app

@app.errorhandler(404)
async def http_404(e):
return await render_template("404.jinja", page=request.host), 404
load_dotenv()

app = create_app(__name__)

if __name__ == "__main__":
if app.config["DEBUG"]:
app.run(port=5000)
app.run(port=environ.get("PORT", 5000))
else:
getLogger("hypercorn.access").disabled = True
getLogger("hypercorn.error").disabled = True
app.run(host="0.0.0.0", port=80 if not is_dev else 1080)
app.run(host="127.0.0.1", port=environ.get("PORT", 80))
12 changes: 11 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
[project]
name = "foxes-new"
version = "0.6.1"
version = "0.7.0"
description = "taggie waggie's website"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"aiohttp>=3.13.2",
"aiosqlite>=0.21.0",
"anyio>=4.12.1",
"dotenv>=0.9.9",
"pyyaml>=6.0.3",
"quart>=0.20.0",
"quart-auth>=0.11.0",
"quart-cors>=0.8.0",
"quart-schema[pydantic]>=0.23.0",
"sqlalchemy[asyncio]>=2.0.45",
]

[dependency-groups]
dev = [
"pre-commit>=4.5.1",
"ruff>=0.14.13",
]
37 changes: 37 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
line-length = 88
indent-width = 4

[lint]
select = [
"E", "W", "F",
"I",
"UP",
"C4",
"ICN",
"RET",
"SIM",
"TC",
"N",
"ERA",
"ANN",
"ASYNC",
"S",
"EM",
"INP",
"T20",
"ARG",
"PERF",
"FURB"
]
ignore = [
"ANN201", "E501" # fuck you i make my lines as long as i want
]

[lint.per-file-ignores]
"__init__.py" = ["E402"]

[format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
2 changes: 1 addition & 1 deletion src/about.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

version = "0.6.4"
version = "0.7.1"

if os.path.exists(".git/HEAD"):
with open(".git/HEAD") as fp:
Expand Down
2 changes: 2 additions & 0 deletions src/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__all__ = ["create_app"]
from .app import create_app
36 changes: 36 additions & 0 deletions src/app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

import re

from quart import Quart
from quart_auth import QuartAuth
from quart_cors import cors
from quart_schema import QuartSchema

from src.auth import User
from src.blueprints import blueprints

from .middleware import ASGIMiddleware
from .static import static


def create_app(import_name: str) -> Quart:
app = Quart(import_name)
QuartSchema(app)
app.config.from_prefixed_env("QUART")
app.asgi_app = ASGIMiddleware(app.asgi_app)
if app.config["DEBUG"]:
app.logger.info("Loading Development configuration...")
else:
app = cors(app, allow_origin=re.compile("https://*.yip.cat*"))

auth_manager = QuartAuth(app)
auth_manager.user_class = User

for blueprint in blueprints:
app.register_blueprint(blueprint)

app.jinja_env.globals.update(static=static)
auth_manager.init_app(app)

return app
47 changes: 47 additions & 0 deletions src/app/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from hypercorn.typing import (
ASGIFramework,
ASGIReceiveCallable,
ASGISendCallable,
HTTPScope,
)

__all__ = ["ASGIMiddleware"]


class ASGIMiddleware:
"""
Custom middleware to remove "dev." subdomain from HTTP requests.

Entirely optional, but used to host a redirect from dev.site.tld to site.tld
through something like CloudFlare
"""

# this is my baby. she is deformed.
# be nice to my baby.
def __init__(self, app: ASGIFramework) -> None:
self.app = app

async def inner(self, scope: HTTPScope):
if scope["type"] != "http":
return scope
headers = scope.get("headers", [])
host = (list(filter(lambda e: e[0] == b"host", headers)) or [None])[0]
if host is None or len(host) != 2:
return scope
if host[1].decode().startswith("dev."):
index = headers.index(host)
host = (host[0], host[1].decode().replace("dev.", "").encode())
headers[index] = host
scope["headers"] = headers
return scope

async def __call__(
self, scope: HTTPScope, recv: ASGIReceiveCallable, send: ASGISendCallable
) -> None:
scope = await self.inner(scope)
await self.app(scope, recv, send)
19 changes: 19 additions & 0 deletions src/app/static.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from quart import url_for


async def static(location: str = None, filename: str = None) -> str | None:
"""Custom static implementation that accepts routes for other static locations

Args:
location (str, optional): Endpoint/folder to search in. Default: None (current template's folder)
filename (str, optional): Default: None (causes this function to return None)

Returns:
str: URL to a given file, if it exists
"""
if filename is None:
return None

if location is not None:
return url_for(f"{location}.static", filename=filename)
return url_for("static", filename=filename)
2 changes: 2 additions & 0 deletions src/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
__all__ = ["Permissions", "User"]

from .permissions import Permissions
from .user import UserClass as User
2 changes: 1 addition & 1 deletion src/auth/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Permissions:
UPLOAD_IMAGES = 16

@staticmethod
def sort():
def sort() -> list:
_current = [
value
for name, value in vars(Permissions).items()
Expand Down
5 changes: 3 additions & 2 deletions src/auth/user.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from quart_auth import AuthUser
from sqlalchemy import update

from src.database import Session, User


class UserClass(AuthUser):
def __init__(self, auth_id):
def __init__(self, auth_id: str | None) -> None:
super().__init__(auth_id)
self._resolved = False
self._id = None
self._login = None
self._permissions = None

async def _resolve(self):
async def _resolve(self) -> None:
if self._resolved:
return
if not await self.is_authenticated:
Expand Down
Loading