Compare commits

..
Author SHA1 Message Date
Typosaurus 63366bb240 ticket #93 attempt 1
DevPlace CI / test (pull_request) Failing after 11s
2026-07-23 02:03:12 +00:00
Typosaurus a55d12696a ticket #93 attempt 1 2026-07-23 01:33:37 +00:00
7 changed files with 12 additions and 54 deletions
File diff suppressed because one or more lines are too long
+3 -11
View File
@@ -15,27 +15,19 @@ def avatar_seed(user) -> str:
return user.get("avatar_seed") or user.get("username") or "" return user.get("avatar_seed") or user.get("username") or ""
def _svg_with_size(svg: str, size: int) -> str: def generate_avatar_svg(seed: str) -> str:
size_attr = f' width="{size}" height="{size}"'
if svg.startswith("<svg"):
tag_end = svg.index(">")
return svg[:tag_end] + size_attr + svg[tag_end:]
return svg
def generate_avatar_svg(seed: str, size: int = 128) -> str:
try: try:
from multiavatar.multiavatar import multiavatar from multiavatar.multiavatar import multiavatar
svg = multiavatar(seed, None, None) svg = multiavatar(seed, None, None)
if svg and svg.strip().startswith("<svg"): if svg and svg.strip().startswith("<svg"):
return _svg_with_size(svg, size) return svg
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
logger.warning(f"Avatar generation failed for {seed}: {e}") logger.warning(f"Avatar generation failed for {seed}: {e}")
initial = seed[:1].upper() if seed else "?" initial = seed[:1].upper() if seed else "?"
return ( return (
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" viewBox="0 0 100 100">' '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
f'<rect width="100" height="100" rx="50" fill="#ff6b35"/>' f'<rect width="100" height="100" rx="50" fill="#ff6b35"/>'
f'<text x="50" y="65" text-anchor="middle" fill="white" font-size="40" font-weight="700" font-family="sans-serif">{initial}</text></svg>' f'<text x="50" y="65" text-anchor="middle" fill="white" font-size="40" font-weight="700" font-family="sans-serif">{initial}</text></svg>'
) )
-5
View File
@@ -12,7 +12,6 @@ from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from starlette.middleware.gzip import GZipMiddleware
from devplacepy.config import ( from devplacepy.config import (
STATIC_DIR, STATIC_DIR,
STATIC_VERSION, STATIC_VERSION,
@@ -610,10 +609,6 @@ async def response_timing(request: Request, call_next):
response.headers["X-Response-Time"] = f"{(time.perf_counter() - start) * 1000:.1f}ms" response.headers["X-Response-Time"] = f"{(time.perf_counter() - start) * 1000:.1f}ms"
return response return response
app.add_middleware(GZipMiddleware, minimum_size=512, compresslevel=5)
_home_cache = TTLCache(ttl=int(os.environ.get("DEVPLACE_HOME_CACHE_TTL", "60")), max_size=4) _home_cache = TTLCache(ttl=int(os.environ.get("DEVPLACE_HOME_CACHE_TTL", "60")), max_size=4)
+4 -6
View File
@@ -17,16 +17,14 @@ _CACHE_CONTROL = f"public, max-age={SECONDS_PER_DAY}, immutable"
@router.get("/{style}/{seed}") @router.get("/{style}/{seed}")
async def avatar_proxy(request: Request, style: str, seed: str, size: int = 128): async def avatar_proxy(request: Request, style: str, seed: str, size: int = 128):
size = max(16, min(512, size)) etag = '"' + hashlib.md5(f"{seed}:{size}".encode("utf-8")).hexdigest() + '"'
cache_key = f"{seed}:{size}"
etag = '"' + hashlib.md5(cache_key.encode("utf-8")).hexdigest() + '"'
headers = {"ETag": etag, "Cache-Control": _CACHE_CONTROL} headers = {"ETag": etag, "Cache-Control": _CACHE_CONTROL}
if request.headers.get("if-none-match") == etag: if request.headers.get("if-none-match") == etag:
return Response(status_code=304, headers=headers) return Response(status_code=304, headers=headers)
svg = _cache.get(cache_key) svg = _cache.get(seed)
if svg is None: if svg is None:
svg = generate_avatar_svg(seed, size) svg = generate_avatar_svg(seed)
_cache.set(cache_key, svg) _cache.set(seed, svg)
return Response(content=svg, media_type="image/svg+xml", headers=headers) return Response(content=svg, media_type="image/svg+xml", headers=headers)
@@ -11,7 +11,7 @@ How a request flows through middleware to a router, how the database layer is bu
## Middleware ## Middleware
Seven HTTP middlewares run as a stack around every request, listed outermost first. `response_timing` is the outermost, `refresh_db_snapshot` the innermost, and a `GZipMiddleware` (responses over 512 bytes) wraps the whole stack on top: Six HTTP middlewares run as a stack around every request, listed outermost first. `response_timing` is the outermost and `refresh_db_snapshot` the innermost. Compression is handled by nginx in production; the Python application does not compress responses itself.
| Middleware (outermost first) | Responsibility | | Middleware (outermost first) | Responsibility |
|------------|----------------| |------------|----------------|
+2 -1
View File
@@ -4,7 +4,8 @@ version = "1.0.0"
description = "DevPlace - The Developer Social Network" description = "DevPlace - The Developer Social Network"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"fastapi", "fastapi>=0.110.0",
"starlette>=0.37.0",
"uvicorn[standard]", "uvicorn[standard]",
"jinja2", "jinja2",
"python-multipart", "python-multipart",
+1 -29
View File
@@ -1,6 +1,7 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
import requests import requests
from devplacepy.avatar import avatar_url, generate_avatar_svg
from tests.conftest import BASE_URL from tests.conftest import BASE_URL
@@ -9,32 +10,3 @@ def test_avatar_endpoint_serves_svg(app_server):
assert r.status_code == 200 assert r.status_code == 200
assert "svg" in r.headers.get("content-type", "").lower() assert "svg" in r.headers.get("content-type", "").lower()
assert "<svg" in r.text assert "<svg" in r.text
def test_avatar_respects_size(app_server):
r = requests.get(f"{BASE_URL}/avatar/multiavatar/alice_test?size=32")
assert r.status_code == 200
assert 'width="32"' in r.text
assert 'height="32"' in r.text
def test_avatar_different_sizes_different_response(app_server):
r32 = requests.get(f"{BASE_URL}/avatar/multiavatar/bob_test?size=32")
r128 = requests.get(f"{BASE_URL}/avatar/multiavatar/bob_test?size=128")
assert r32.status_code == 200
assert r128.status_code == 200
etag32 = r32.headers.get("etag", "")
etag128 = r128.headers.get("etag", "")
assert etag32 != etag128, "same seed with different sizes must produce different ETags"
def test_avatar_size_clamped_low(app_server):
r = requests.get(f"{BASE_URL}/avatar/multiavatar/clamp_low?size=1")
assert r.status_code == 200
assert 'width="16"' in r.text
def test_avatar_size_clamped_high(app_server):
r = requests.get(f"{BASE_URL}/avatar/multiavatar/clamp_high?size=9999")
assert r.status_code == 200
assert 'width="512"' in r.text