simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit c5f91a6f7a26c74da9aab8559147adf3964bbb46
parent 0cef0722ad3486bbb8f3dd8fa88194c3ffe1ab64
Author: Silas Brack <57684859+silasbrack@users.noreply.github.com>
Date:   Mon, 29 Sep 2025 21:57:12 +0100

feat!: major refactoring, improve flake support (#1)

* feat: improvements

* feat!: major changes
Diffstat:
A.env.example | 9+++++++++
M.gitignore | 5++++-
MREADME.md | 10++++++++++
Mflake.nix | 155++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Alogging.json | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpyproject.toml | 17++++++++---------
Msrc/simple_web_app/__init__.py | 3+--
Asrc/simple_web_app/app.py | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/simple_web_app/log.py | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/simple_web_app/main.py | 102-------------------------------------------------------------------------------
Msrc/simple_web_app/migration.py | 29+++++++++++++++++++++++++++++
Msrc/simple_web_app/runner.py | 7++++---
Msrc/simple_web_app/static/style.css | 21+++++++++++++++++++++
Msrc/simple_web_app/templates/application.html | 12+++---------
CREADME.md -> src/simple_web_app/templates/chat.html | 0
Msrc/simple_web_app/templates/index.html | 38+++++++++++++++++++++++++++++++++++++-
Mtests/test_simple_web_app/conftest.py | 21++++++++++++++++-----
Mtests/test_simple_web_app/test_app.py | 2++
Muv.lock | 140++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
19 files changed, 603 insertions(+), 205 deletions(-)

diff --git a/.env.example b/.env.example @@ -0,0 +1,9 @@ +# Uvicorn settings +UVICORN_RELOAD=true +UVICORN_RELOAD_DIRS=src +UVICORN_RELOAD_INCLUDES=*.html *.css *.json *.sql + +# Application settings +DEBUG=true +DATABASE_PATH=db.sqlite3 + diff --git a/.gitignore b/.gitignore @@ -14,7 +14,7 @@ wheels/ .pytest_cache/ # Sqlite -db.sqlite3* +*.sqlite3* # Nix result @@ -22,3 +22,6 @@ result # Mac OS .DS_Store +# Environment +.env + diff --git a/README.md b/README.md @@ -0,0 +1,10 @@ +# Simple Web App + +We use `uv2nix` to build a python package and a virtual environment with all its dependencies. +Everything in the `src/` folder should get included in the built Python package. +The entire web app logic should be contained within the Python package and consequently in the `src/` folder. + +Configuration, on the other hand, should, whenever possible, be kept outside of the Python package. +Keep the application itself configuration-agnostic whenever possible and choose to instead maintain the `.env.example` file containing a suggested default configuration for developers. +Configuration should thus always be performed via environment variables. + diff --git a/flake.nix b/flake.nix @@ -41,43 +41,103 @@ # Create package overlay from workspace. overlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; - # Optionally customise PEP 508 environment - # environ = { - # platform_release = "5.10.65"; - # }; }; - # # Extend generated overlay with build fixups - # # - # # Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds. - # # This is an additional overlay implementing build fixups. - # # See: - # # - https://pyproject-nix.github.io/uv2nix/FAQ.html - # pyprojectOverrides = _final: _prev: { - # # Implement build fixups here. - # # Note that uv2nix is _not_ using Nixpkgs buildPythonPackage. - # # It's using https://pyproject-nix.github.io/pyproject.nix/build.html - # }; + # Construct package set pythonSets = forAllSystems ( system: let pkgs = nixpkgs.legacyPackages.${system}; - python = pkgs.python3; + inherit (pkgs) stdenv; + baseSet = pkgs.callPackage pyproject-nix.build.packages { + python = pkgs.python313; + }; + + includeLoggingConfigOverride = _final: prev: { + simple-web-app = prev.simple-web-app.overrideAttrs (old: { + postInstall = '' + mkdir -p $out/share + cp ${./logging.json} $out/share/logging.json + ''; + }); + }; + + # Extend generated overlay with build fixups + # + # Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds. + # This is an additional overlay implementing build fixups. + # See: + # - https://pyproject-nix.github.io/uv2nix/FAQ.html + pyprojectOverrides = final: prev: { + # Implement build fixups here. + # Note that uv2nix is _not_ using Nixpkgs buildPythonPackage. + # It's using https://pyproject-nix.github.io/pyproject.nix/build.html + simple-web-app = prev.simple-web-app.overrideAttrs (old: { + passthru = old.passthru // { + # Put all tests in the passthru.tests attribute set. + # Nixpkgs also uses the passthru.tests mechanism for ofborg test discovery. + # + # For usage with Flakes we will refer to the passthru.tests attributes to construct the flake checks attribute set. + tests = + let + # Construct a virtual environment with only the test dependency-group enabled. + virtualenv = final.mkVirtualEnv "simple-web-app-pytest-env" { + simple-web-app = [ "test" ]; + }; + + in + (old.tests or { }) + // { + pytest = stdenv.mkDerivation { + name = "${final.simple-web-app.name}-pytest"; + inherit (final.simple-web-app) src; + nativeBuildInputs = [ + virtualenv + ]; + dontConfigure = true; + + # Because this package is running tests, and not actually building the main package + # the build phase is running the tests. + # + # In this particular example we also output a HTML coverage report, which is used as the build output. + buildPhase = '' + runHook preBuild + pytest --cov tests --cov-report html + runHook postBuild + ''; + + # Install the HTML coverage report into the build output. + # + # If you wanted to install multiple test output formats such as TAP outputs + # you could make this derivation a multiple-output derivation. + # + # See https://nixos.org/manual/nixpkgs/stable/#chap-multiple-output for more information on multiple outputs. + installPhase = '' + runHook preInstall + mv htmlcov $out + runHook postInstall + ''; + }; + + }; + }; + }); + }; in # Use base package set from pyproject.nix builders - (pkgs.callPackage pyproject-nix.build.packages { - inherit python; - }).overrideScope + baseSet.overrideScope ( lib.composeManyExtensions [ pyproject-build-systems.overlays.default overlay - # pyprojectOverrides + pyprojectOverrides + includeLoggingConfigOverride ] ) ); + # inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication; in { @@ -91,7 +151,7 @@ # package = pythonSet.hello-world; # }; - # Make hello runnable with `nix run` + # Make simple-web-app runnable with `nix run` apps = forAllSystems ( system: let @@ -105,6 +165,17 @@ } ); + # Run the checks by running `nix flake check` + checks = forAllSystems ( + system: + let + pythonSet = pythonSets.${system}; + in + { + inherit (pythonSet.simple-web-app.passthru.tests) pytest; + } + ); + # This example provides two different modes of development: # - Impurely using uv to manage virtual environments # - Pure development using uv2nix to manage virtual environments @@ -112,7 +183,7 @@ system: let pkgs = nixpkgs.legacyPackages.${system}; - python = pkgs.python3; + python = pkgs.python313; in { # It is of course perfectly OK to keep using an impure virtualenv workflow and only use uv2nix to build packages. # This devShell simply adds Python and undoes the dependency leakage done by Nixpkgs Python infrastructure. @@ -121,21 +192,18 @@ python pkgs.uv pkgs.ruff + pkgs.ty pkgs.basedpyright ]; env = { # Prevent uv from managing Python downloads UV_PYTHON_DOWNLOADS = "never"; + # Force uv to use nixpkgs Python interpreter UV_PYTHON = python.interpreter; + LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib/:/run/opengl-driver/lib/"; - # LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1; }; - # // lib.optionalAttrs pkgs.stdenv.isLinux { - # # Python libraries often load native shared objects using dlopen(3). - # # Setting LD_LIBRARY_PATH makes the dynamic library loader aware of libraries without using RPATH for lookup. - # LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1; - #}; shellHook = '' unset PYTHONPATH ''; @@ -166,7 +234,7 @@ # Apply fixups for building an editable package of your workspace packages (final: prev: { - hello-world = prev.hello-world.overrideAttrs (old: { + simple-web-app = prev.simple-web-app.overrideAttrs (old: { # It's a good idea to filter the sources going into an editable build # so the editable package doesn't have to be rebuilt on every change. src = lib.fileset.toSource { @@ -174,21 +242,21 @@ fileset = lib.fileset.unions [ (old.src + "/pyproject.toml") (old.src + "/README.md") - (old.src + "/src/hello_world/__init__.py") + (old.src + "/src/simple_web_app/__init__.py") ]; }; - # Hatchling (our build system) has a dependency on the `editables` package when building editables. - # - # In normal Python flows this dependency is dynamically handled, and doesn't need to be explicitly declared. - # This behaviour is documented in PEP-660. - # - # With Nix the dependency needs to be explicitly declared. - nativeBuildInputs = - old.nativeBuildInputs - ++ final.resolveBuildSystem { - editables = [ ]; - }; + # # Hatchling (our build system) has a dependency on the `editables` package when building editables. + # # + # # In normal Python flows this dependency is dynamically handled, and doesn't need to be explicitly declared. + # # This behaviour is documented in PEP-660. + # # + # # With Nix the dependency needs to be explicitly declared. + # nativeBuildInputs = + # old.nativeBuildInputs + # ++ final.resolveBuildSystem { + # editables = [ ]; + # }; }); }) @@ -198,7 +266,7 @@ # Build virtual environment, with local packages being editable. # # Enable all optional dependencies for development. - virtualenv = editablePythonSet.mkVirtualEnv "hello-world-dev-env" workspace.deps.all; + virtualenv = editablePythonSet.mkVirtualEnv "simple-web-app-dev-env" workspace.deps.all; in pkgs.mkShell { @@ -206,6 +274,7 @@ virtualenv pkgs.uv pkgs.ruff + pkgs.ty pkgs.basedpyright ]; @@ -218,8 +287,6 @@ # Prevent uv from downloading managed Python's UV_PYTHON_DOWNLOADS = "never"; - - # LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1; }; shellHook = '' diff --git a/logging.json b/logging.json @@ -0,0 +1,59 @@ +{ + "version": 1, + "formatters": { + "default": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + }, + "extra": { + "format": "%(asctime)-16s %(name)-8s %(filename)-12s %(lineno)-6s %(funcName)-30s %(levelname)-8s %(message)s", + "datefmt": "%m-%d %H:%M:%S" + }, + "json": { + "()": "simple_web_app.log.JsonFormatter", + "fmt_dict": { + "level": "levelname", + "message": "message", + "loggerName": "name", + "processName": "processName", + "processID": "process", + "threadName": "threadName", + "threadID": "thread", + "timestamp": "asctime" + } + } + }, + "handlers": { + "root": { + "class": "logging.StreamHandler", + "level": "WARN", + "formatter": "json", + "stream": "ext://sys.stdout" + }, + "simple_web_app": { + "class": "logging.StreamHandler", + "level": "INFO", + "formatter": "json", + "stream": "ext://sys.stdout" + }, + "uvicorn": { + "class": "logging.StreamHandler", + "level": "INFO", + "formatter": "json", + "stream": "ext://sys.stdout" + } + }, + "loggers": { + "simple_web_app": { + "level": "INFO", + "handlers": ["simple_web_app"] + }, + "uvicorn": { + "level": "INFO", + "handlers": ["uvicorn"] + } + }, + "root": { + "level": "WARN", + "handlers": ["root"] + } +} diff --git a/pyproject.toml b/pyproject.toml @@ -19,22 +19,21 @@ dependencies = [ ] [dependency-groups] -prod = [ - "gunicorn", - "uvicorn-worker", -] dev = [ { include-group = "test" }, - "uvicorn", + "ruff", + "watchfiles", ] test = [ "pytest", - "httpx", # Starlette TestClient support - "locust", # Stress testing + "pytest-cov", + "httpx", # Starlette TestClient support + "locust", # Stress testing ] [project.scripts] -simple-web-app = "simple_web_app.runner:run" +simple-web-app = "simple_web_app.runner:run_uvicorn" +create-migration = "simple_web_app.migration:run_create_migration" [build-system] requires = ["setuptools", "wheel"] @@ -44,5 +43,5 @@ build-backend = "setuptools.build_meta" where = ["src"] [tool.setuptools.package-data] -simple_web_app = ["queries/**", "migrations/**", "templates/**", "static/**"] +simple_web_app = ["queries/**", "migrations/**", "templates/**", "static/**", "logging.json"] diff --git a/src/simple_web_app/__init__.py b/src/simple_web_app/__init__.py @@ -1,2 +1 @@ -from simple_web_app.runner import run -from simple_web_app.main import app + diff --git a/src/simple_web_app/app.py b/src/simple_web_app/app.py @@ -0,0 +1,119 @@ +import contextlib +import importlib.resources +import logging +import os +import sqlite3 +from pathlib import Path + +import aiosql +import aiosqlite +from starlette.applications import Starlette +from starlette.config import Config +from starlette.middleware import Middleware +# from starlette.middleware.sessions import SessionMiddleware +from starlette.requests import Request +from starlette.routing import Mount, Route +from starlette.staticfiles import StaticFiles +from starlette.templating import Jinja2Templates + +from simple_web_app.migration import apply_migrations, create_migrations_table_if_not_exists + +logger = logging.getLogger(__name__) + +SQLITE_PRAGMAS = [ + "PRAGMA journal_mode = WAL;", + "PRAGMA foreign_keys = ON;", + "PRAGMA synchronous = NORMAL;", + "PRAGMA cache_size = 1000000000;", + "PRAGMA temp_store = MEMORY;", + "PRAGMA busy_timeout = 5000;", + "PRAGMA foreign_keys = ON;", + "PRAGMA synchronous = NORMAL;", +] + +config = Config() +DEBUG = config("DEBUG", cast=bool, default=True) +DATABASE_PATH = config("DATABASE_PATH", default="./db.sqlite3") +# SECRET_KEY = config("SECRET_KEY", default="asdf") + +DATABASE_PATH = Path(DATABASE_PATH) +MIGRATION_DIR = importlib.resources.files("simple_web_app").joinpath("migrations") +TEMPLATE_DIR = importlib.resources.files("simple_web_app").joinpath("templates") +QUERY_DIR = importlib.resources.files("simple_web_app").joinpath("queries") +STATIC_DIR = importlib.resources.files("simple_web_app").joinpath("static") + +templates = Jinja2Templates(directory=TEMPLATE_DIR) +queries_basic = aiosql.from_path(QUERY_DIR / "basic.sql", "aiosqlite") + + +def is_htmx_request(request: Request) -> bool: + is_hx_request = request.headers.get("HX-Request") is not None + is_hx_history_restore = ( + request.headers.get("HX-History-Restore-Request") is not None + ) + return is_hx_request or is_hx_history_restore + + +def render( + request: Request, partial_template: str, include_oob: str | None = None, **kwargs +): + ctx = kwargs.get("context", {}) + is_authenticated = False # request.user.is_authenticated if request.user else False + ctx = ctx | {"is_authenticated": is_authenticated} + headers = kwargs.get("headers", {}) + if is_htmx_request(request): + if include_oob: + ctx = ctx | { + "context_partial": partial_template, + "oob_template": include_oob, + } + return templates.TemplateResponse( + request, partial_template, ctx, headers=headers + ) + return templates.TemplateResponse( + request, + "application.html", + ctx | {"content": partial_template}, + headers=headers, + ) + + +async def show_home_page(request: Request): + return render(request, "index.html") + + +async def join_chat(request: Request): + async with request.form() as form: + username = form["username"] + return render(request, "chat.html") + + +@contextlib.asynccontextmanager +async def lifespan(app: Starlette): + conn = sqlite3.connect(DATABASE_PATH) + create_migrations_table_if_not_exists(conn) + migration_files = sorted(MIGRATION_DIR.glob("*.sql")) + migration_queries = [p.read_text() for p in migration_files] + apply_migrations(conn, migration_queries) + + async with aiosqlite.connect(DATABASE_PATH, autocommit=True) as conn: + conn.row_factory = aiosqlite.Row + cursors = [conn.execute(query) for query in SQLITE_PRAGMAS] + [await c for c in cursors] + yield {"conn": conn} + + +routes = [ + Route("/", methods=["GET"], endpoint=show_home_page), + Route("/chat/join", methods=["POST"], endpoint=join_chat), + Mount("/static", StaticFiles(directory=STATIC_DIR), name="static"), +] +middleware = [ + # Middleware(SessionMiddleware, secret_key=SECRET_KEY), +] +app = Starlette( + debug=DEBUG, + routes=routes, + middleware=middleware, + lifespan=lifespan, +) diff --git a/src/simple_web_app/log.py b/src/simple_web_app/log.py @@ -0,0 +1,59 @@ +import logging +import json + +import orjson + + +class JsonFormatter(logging.Formatter): + """ + Formatter that outputs JSON strings after parsing the LogRecord. + + @param dict fmt_dict: Key: logging format attribute pairs. Defaults to {"message": "message"}. + @param str time_format: time.strftime() format string. Default: "%Y-%m-%dT%H:%M:%S" + @param str msec_format: Microsecond formatting. Appended at the end. Default: "%s.%03dZ" + """ + def __init__(self, fmt_dict: dict = None, time_format: str = "%Y-%m-%dT%H:%M:%S", msec_format: str = "%s.%03dZ"): + self.fmt_dict = fmt_dict if fmt_dict is not None else {"message": "message"} + self.default_time_format = time_format + self.default_msec_format = msec_format + self.datefmt = None + + def usesTime(self) -> bool: + """ + Overwritten to look for the attribute in the format dict values instead of the fmt string. + """ + return "asctime" in self.fmt_dict.values() + + def formatMessage(self, record) -> dict: + """ + Overwritten to return a dictionary of the relevant LogRecord attributes instead of a string. + KeyError is raised if an unknown attribute is provided in the fmt_dict. + """ + return {fmt_key: record.__dict__[fmt_val] for fmt_key, fmt_val in self.fmt_dict.items()} + + def format(self, record) -> str: + """ + Mostly the same as the parent's class method, the difference being that a dict is manipulated and dumped as JSON + instead of a string. + """ + record.message = record.getMessage() + + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + + message_dict = self.formatMessage(record) + + if record.exc_info: + # Cache the traceback text to avoid converting it multiple times + # (it's constant anyway) + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + + if record.exc_text: + message_dict["exc_info"] = record.exc_text + + if record.stack_info: + message_dict["stack_info"] = self.formatStack(record.stack_info) + + return json.dumps(message_dict, default=str) + diff --git a/src/simple_web_app/main.py b/src/simple_web_app/main.py @@ -1,102 +0,0 @@ -import contextlib -import importlib.resources -import logging -import os -import sqlite3 -from pathlib import Path - -import aiosql -import aiosqlite -from starlette.applications import Starlette -from starlette.requests import Request -from starlette.routing import Mount, Route -from starlette.staticfiles import StaticFiles -from starlette.templating import Jinja2Templates - -from simple_web_app.migration import ( - apply_migrations, - create_migrations_table_if_not_exists, -) - -logging.basicConfig() -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - -SQLITE_PRAGMAS = [ - "PRAGMA journal_mode = WAL;", - "PRAGMA foreign_keys = ON;", - "PRAGMA synchronous = NORMAL;", - "PRAGMA cache_size = 1000000000;", - "PRAGMA temp_store = MEMORY;", - "PRAGMA busy_timeout = 5000;", - "PRAGMA foreign_keys = ON;", - "PRAGMA synchronous = NORMAL;", -] - - -DATABASE_PATH = Path(os.getenv("DATABASE_PATH", default="./db.sqlite3")) -MIGRATION_DIR = importlib.resources.files("simple_web_app").joinpath("migrations") -TEMPLATE_DIR = importlib.resources.files("simple_web_app").joinpath("templates") -QUERY_DIR = importlib.resources.files("simple_web_app").joinpath("queries") -STATIC_DIR = importlib.resources.files("simple_web_app").joinpath("static") - -templates = Jinja2Templates(directory=TEMPLATE_DIR) -queries_basic = aiosql.from_path(QUERY_DIR / "basic.sql", "aiosqlite") - - -def is_htmx_request(request: Request) -> bool: - is_hx_request = request.headers.get("HX-Request") is not None - is_hx_history_restore = ( - request.headers.get("HX-History-Restore-Request") is not None - ) - return is_hx_request or is_hx_history_restore - - -def render( - request: Request, partial_template: str, include_oob: str | None = None, **kwargs -): - ctx = kwargs.get("context", {}) - is_authenticated = False # request.user.is_authenticated if request.user else False - ctx = ctx | {"is_authenticated": is_authenticated} - headers = kwargs.get("headers", {}) - if is_htmx_request(request): - if include_oob: - ctx = ctx | { - "context_partial": partial_template, - "oob_template": include_oob, - } - return templates.TemplateResponse( - request, partial_template, ctx, headers=headers - ) - return templates.TemplateResponse( - request, - "application.html", - ctx | {"content": partial_template}, - headers=headers, - ) - - -async def show_home_page(request: Request): - return render(request, "index.html") - - -@contextlib.asynccontextmanager -async def lifespan(app: Starlette): - conn = sqlite3.connect(DATABASE_PATH) - create_migrations_table_if_not_exists(conn) - migration_files = sorted(MIGRATION_DIR.glob("*.sql")) - migration_queries = [p.read_text() for p in migration_files] - apply_migrations(conn, migration_queries) - - async with aiosqlite.connect(DATABASE_PATH, autocommit=True) as conn: - conn.row_factory = aiosqlite.Row - cursors = [conn.execute(query) for query in SQLITE_PRAGMAS] - [await c for c in cursors] - yield {"conn": conn} - - -routes = [ - Route("/", methods=["GET"], endpoint=show_home_page), - Mount("/static", StaticFiles(directory=STATIC_DIR), name="static"), -] -app = Starlette(debug=False, routes=routes, lifespan=lifespan) diff --git a/src/simple_web_app/migration.py b/src/simple_web_app/migration.py @@ -1,5 +1,7 @@ +import datetime import logging import sqlite3 +from pathlib import Path logger = logging.getLogger(__name__) @@ -32,3 +34,30 @@ def apply_migrations(conn: sqlite3.Connection, migration_queries: list[str]) -> raise _ = conn.execute("UPDATE migration_version SET version = ?", [i + 1]) conn.commit() + + +def create_migration(name: str, migrations_dir: Path): + current_timestamp = datetime.datetime.now(tz=datetime.UTC) + file_name = f"{current_timestamp.strftime('%Y%m%d%H%M%S')}_{name}.sql" + file_path = migrations_dir / file_name + file_path.touch() + return file_path + + +def run_create_migration(): + import argparse + import importlib.resources + + default_migrations_dir = importlib.resources.files("simple_web_app").joinpath("migrations") + parser = argparse.ArgumentParser() + parser.add_argument("name") + parser.add_argument("-d", "--dir", default=default_migrations_dir) + args = parser.parse_args() + + path = create_migration(args.name, Path(args.dir)) + print(str(path)) + + +if __name__ == "__main__": + run_create_migration() + diff --git a/src/simple_web_app/runner.py b/src/simple_web_app/runner.py @@ -1,5 +1,6 @@ -import uvicorn +import subprocess -def run(): - uvicorn.run("simple_web_app.main:app") +def run_uvicorn(): + subprocess.run(["uvicorn", "simple_web_app.app:app"]) + diff --git a/src/simple_web_app/static/style.css b/src/simple_web_app/static/style.css @@ -0,0 +1,21 @@ +.hidden { + display: none; +} + +.msg { + text-decoration: none; + font-size: 5em; + list-style: none; + text-align:left; + margin-top: 0px; + margin-bottom: 0px; +} + +.event { + text-decoration: none; + list-style: none; + font-size: 2em; + text-align:center; + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/src/simple_web_app/templates/application.html b/src/simple_web_app/templates/application.html @@ -2,19 +2,13 @@ <html lang="en"> <head> <meta charset="utf-8" /> - <title>SaxoChat</title> + <title>My Chat</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script> <!--<script src="https://cdn.jsdelivr.net/npm/htmx-ext-preload@2.1.0"></script>--> <!--<script src="https://unpkg.com/hyperscript.org@0.9.14"></script>--> - <!--<link - rel="stylesheet" - href="{{ url_for('static', path='style.css') }}" - />--> - <link - rel="stylesheet" - href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.blue.min.css" - /> + <link rel="stylesheet" href="{{ url_for('static', path='style.css') }}"/> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.blue.min.css"/> </head> <body class="container" hx-ext="preload" style="--pico-font-family: Inter"> <main><div id="content">{% include content %}</div></main> diff --git a/README.md b/src/simple_web_app/templates/chat.html diff --git a/src/simple_web_app/templates/index.html b/src/simple_web_app/templates/index.html @@ -1 +1,37 @@ -<h1>Hello world!</h1> +<div id="login"> + <article> + <hgroup> + <h1>Welcome to RealTimeChat!</h1> + <p>Type your username to enter the chat</p> + </hgroup> + <form hx-post="/chat/join" id="username-form"> + <input id="username" name="username" type="text" placeholder="Username" autocomplete="off" required> + <button type="submit">Enter Chat</button> + </form> + </article> +</div> + +<div id="chat" class="hidden"> + <article> + <hgroup> + <h1>RealTimeChat</h1> + <p id="username"> + <div class="msg">Asdfdsfd</div> + </p> + </hgroup> + + <article aria-busy="true" id="loading"></article> + + <ul id="area"> + </ul> + + <form id="message-form"> + <fieldset role="group"> + <input id="message" type="text" placeholder="Type a message..." autocomplete="off" /> + <button type="submit"> + <svg width="24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M568.4 37.7C578.2 34.2 589 36.7 596.4 44C603.8 51.3 606.2 62.2 602.7 72L424.7 568.9C419.7 582.8 406.6 592 391.9 592C377.7 592 364.9 583.4 359.6 570.3L295.4 412.3C290.9 401.3 292.9 388.7 300.6 379.7L395.1 267.3C400.2 261.2 399.8 252.3 394.2 246.7C388.6 241.1 379.6 240.7 373.6 245.8L261.2 340.1C252.1 347.7 239.6 349.7 228.6 345.3L70.1 280.8C57 275.5 48.4 262.7 48.4 248.5C48.4 233.8 57.6 220.7 71.5 215.7L568.4 37.7z"/></svg> + </button> + </fieldset> + </form> + </article> +</div> diff --git a/tests/test_simple_web_app/conftest.py b/tests/test_simple_web_app/conftest.py @@ -1,13 +1,24 @@ +import os +import unittest.mock from collections.abc import AsyncGenerator import pytest from starlette.testclient import TestClient -from simple_web_app.main import app + +@pytest.fixture() +def test_env() -> dict[str, str]: + return { + "DEBUG": "true", + "DATABASE_PATH": ":memory:", + } @pytest.fixture() -def client() -> AsyncGenerator[TestClient, None]: - with TestClient(app) as client: - # Application's lifespan is called on entering the block - yield client +def client(test_env: dict[str, str]) -> AsyncGenerator[TestClient, None]: + with unittest.mock.patch.dict(os.environ, test_env, clear=True): + from simple_web_app.app import app + with TestClient(app) as client: + # Application's lifespan is called on entering the block + yield client + diff --git a/tests/test_simple_web_app/test_app.py b/tests/test_simple_web_app/test_app.py @@ -1,3 +1,4 @@ +import os from starlette.testclient import TestClient @@ -13,3 +14,4 @@ def test_home_htmx(client: TestClient): response = client.get("/", headers={"HX-Request": "asdf"}) assert response.status_code == 200 assert response.template.name == "index.html" + diff --git a/uv.lock b/uv.lock @@ -190,6 +190,41 @@ wheels = [ ] [[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[[package]] name = "flask" version = "3.1.2" source = { registry = "https://pypi.org/simple" } @@ -302,18 +337,6 @@ wheels = [ ] [[package]] -name = "gunicorn" -version = "23.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, -] - -[[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } @@ -580,6 +603,20 @@ wheels = [ ] [[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] name = "python-engineio" version = "4.12.2" source = { registry = "https://pypi.org/simple" } @@ -678,6 +715,32 @@ wheels = [ ] [[package]] +name = "ruff" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/df/8d7d8c515d33adfc540e2edf6c6021ea1c5a58a678d8cfce9fae59aabcab/ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff", size = 5416417, upload-time = "2025-09-25T14:54:09.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/84/5716a7fa4758e41bf70e603e13637c42cfb9dbf7ceb07180211b9bbf75ef/ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3", size = 12343254, upload-time = "2025-09-25T14:53:27.784Z" }, + { url = "https://files.pythonhosted.org/packages/9b/77/c7042582401bb9ac8eff25360e9335e901d7a1c0749a2b28ba4ecb239991/ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2", size = 13040891, upload-time = "2025-09-25T14:53:31.38Z" }, + { url = "https://files.pythonhosted.org/packages/c6/15/125a7f76eb295cb34d19c6778e3a82ace33730ad4e6f28d3427e134a02e0/ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46", size = 12243588, upload-time = "2025-09-25T14:53:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/9e/eb/0093ae04a70f81f8be7fd7ed6456e926b65d238fc122311293d033fdf91e/ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6", size = 12491359, upload-time = "2025-09-25T14:53:35.892Z" }, + { url = "https://files.pythonhosted.org/packages/43/fe/72b525948a6956f07dad4a6f122336b6a05f2e3fd27471cea612349fedb9/ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07", size = 12162486, upload-time = "2025-09-25T14:53:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e3/0fac422bbbfb2ea838023e0d9fcf1f30183d83ab2482800e2cb892d02dfe/ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8", size = 13871203, upload-time = "2025-09-25T14:53:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/6b/82/b721c8e3ec5df6d83ba0e45dcf00892c4f98b325256c42c38ef136496cbf/ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89", size = 14929635, upload-time = "2025-09-25T14:53:43.953Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/ad56faf6daa507b83079a1ad7a11694b87d61e6bf01c66bd82b466f21821/ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0", size = 14338783, upload-time = "2025-09-25T14:53:46.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/77/ad1d9156db8f99cd01ee7e29d74b34050e8075a8438e589121fcd25c4b08/ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa", size = 13355322, upload-time = "2025-09-25T14:53:48.164Z" }, + { url = "https://files.pythonhosted.org/packages/64/8b/e87cfca2be6f8b9f41f0bb12dc48c6455e2d66df46fe61bb441a226f1089/ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3", size = 13354427, upload-time = "2025-09-25T14:53:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/7f/df/bf382f3fbead082a575edb860897287f42b1b3c694bafa16bc9904c11ed3/ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d", size = 13537637, upload-time = "2025-09-25T14:53:52.887Z" }, + { url = "https://files.pythonhosted.org/packages/51/70/1fb7a7c8a6fc8bd15636288a46e209e81913b87988f26e1913d0851e54f4/ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b", size = 12340025, upload-time = "2025-09-25T14:53:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/27/1e5b3f1c23ca5dd4106d9d580e5c13d9acb70288bff614b3d7b638378cc9/ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22", size = 12133449, upload-time = "2025-09-25T14:53:57.089Z" }, + { url = "https://files.pythonhosted.org/packages/2d/09/b92a5ccee289f11ab128df57d5911224197d8d55ef3bd2043534ff72ca54/ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736", size = 13051369, upload-time = "2025-09-25T14:53:59.124Z" }, + { url = "https://files.pythonhosted.org/packages/89/99/26c9d1c7d8150f45e346dc045cc49f23e961efceb4a70c47dea0960dea9a/ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2", size = 13523644, upload-time = "2025-09-25T14:54:01.622Z" }, + { url = "https://files.pythonhosted.org/packages/f7/00/e7f1501e81e8ec290e79527827af1d88f541d8d26151751b46108978dade/ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac", size = 12245990, upload-time = "2025-09-25T14:54:03.647Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bd/d9f33a73de84fafd0146c6fba4f497c4565fe8fa8b46874b8e438869abc2/ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585", size = 13324004, upload-time = "2025-09-25T14:54:06.05Z" }, + { url = "https://files.pythonhosted.org/packages/c3/12/28fa2f597a605884deb0f65c1b1ae05111051b2a7030f5d8a4ff7f4599ba/ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7", size = 12484437, upload-time = "2025-09-25T14:54:08.022Z" }, +] + +[[package]] name = "setuptools" version = "80.9.0" source = { registry = "https://pypi.org/simple" } @@ -706,16 +769,15 @@ dev = [ { name = "httpx" }, { name = "locust" }, { name = "pytest" }, - { name = "uvicorn" }, -] -prod = [ - { name = "gunicorn" }, - { name = "uvicorn-worker" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "watchfiles" }, ] test = [ { name = "httpx" }, { name = "locust" }, { name = "pytest" }, + { name = "pytest-cov" }, ] [package.metadata] @@ -735,16 +797,15 @@ dev = [ { name = "httpx" }, { name = "locust" }, { name = "pytest" }, - { name = "uvicorn" }, -] -prod = [ - { name = "gunicorn" }, - { name = "uvicorn-worker" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "watchfiles" }, ] test = [ { name = "httpx" }, { name = "locust" }, { name = "pytest" }, + { name = "pytest-cov" }, ] [[package]] @@ -812,16 +873,37 @@ wheels = [ ] [[package]] -name = "uvicorn-worker" -version = "0.4.0" +name = "watchfiles" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "gunicorn" }, - { name = "uvicorn" }, + { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/59/9101b9c0680fd80e9d26c07deb822a5d18a324339fcf9cd017885ee808ad/uvicorn_worker-0.4.0.tar.gz", hash = "sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493", size = 9361, upload-time = "2025-09-20T10:47:01.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/25/09cd7a90c8bb7fb693be0d6704fccd5f9778d5513214b7a01cc4a94ff314/uvicorn_worker-0.4.0-py3-none-any.whl", hash = "sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde", size = 5364, upload-time = "2025-09-20T10:46:59.776Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" }, + { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" }, + { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" }, + { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" }, + { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" }, + { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" }, + { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" }, + { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" }, + { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" }, + { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" }, + { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" }, + { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" }, ] [[package]]