Compare commits

..
Author SHA1 Message Date
Typosaurus 38c2bab503 ticket #111 attempt 1
DevPlace CI / test (pull_request) Failing after 11s
2026-07-23 03:03:02 +00:00
9 changed files with 91 additions and 6 deletions
File diff suppressed because one or more lines are too long
+2
View File
@@ -38,6 +38,7 @@ def field(
example="", example="",
description="", description="",
options=None, options=None,
nullable=False,
): ):
spec = { spec = {
"name": name, "name": name,
@@ -46,6 +47,7 @@ def field(
"required": required, "required": required,
"example": example, "example": example,
"description": description, "description": description,
"nullable": nullable,
} }
if options: if options:
spec["options"] = list(options) spec["options"] = list(options)
+11 -2
View File
@@ -39,7 +39,15 @@ def _unwrap_optional(annotation):
return annotation return annotation
def _value(name, annotation, stack): def _is_optional(annotation):
if get_origin(annotation) is Union:
return type(None) in get_args(annotation)
return False
def _value(name, annotation, stack, is_nullable=False):
if is_nullable:
return None
annotation = _unwrap_optional(annotation) annotation = _unwrap_optional(annotation)
origin = get_origin(annotation) origin = get_origin(annotation)
if origin in (list, set, tuple): if origin in (list, set, tuple):
@@ -71,7 +79,8 @@ def _from_model(model, stack):
stack = stack | {model} stack = stack | {model}
example = {} example = {}
for name, info in model.model_fields.items(): for name, info in model.model_fields.items():
example[name] = _value(name, info.annotation, stack) is_nullable = _is_optional(info.annotation)
example[name] = _value(name, info.annotation, stack, is_nullable)
return example return example
+5
View File
@@ -12,6 +12,7 @@ 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,
@@ -609,6 +610,10 @@ 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)
+10
View File
@@ -218,6 +218,16 @@
font-weight: 700; font-weight: 700;
} }
.param-nullable {
font-size: 0.625rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
border: 1px dashed var(--border);
border-radius: 999px;
padding: 0.05rem 0.4rem;
}
.param-loc { .param-loc {
font-size: 0.625rem; font-size: 0.625rem;
text-transform: uppercase; text-transform: uppercase;
+1
View File
@@ -139,6 +139,7 @@ export class ApiTester {
const label = this.el("div", { class: "param-label" }, [ const label = this.el("div", { class: "param-label" }, [
this.el("span", { class: "param-name", text: param.name }), this.el("span", { class: "param-name", text: param.name }),
param.required ? this.el("span", { class: "param-required", text: "*" }) : null, param.required ? this.el("span", { class: "param-required", text: "*" }) : null,
param.nullable ? this.el("span", { class: "param-nullable", text: "nullable" }) : null,
this.el("span", { class: "param-loc param-loc-" + param.location, text: param.location }), this.el("span", { class: "param-loc param-loc-" + param.location, text: param.location }),
]); ]);
const allowed = param.type === "enum" && param.options && param.options.length const allowed = param.type === "enum" && param.options && param.options.length
@@ -11,7 +11,7 @@ How a request flows through middleware to a router, how the database layer is bu
## Middleware ## Middleware
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. 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:
| Middleware (outermost first) | Responsibility | | Middleware (outermost first) | Responsibility |
|------------|----------------| |------------|----------------|
+1 -2
View File
@@ -4,8 +4,7 @@ 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>=0.110.0", "fastapi",
"starlette>=0.37.0",
"uvicorn[standard]", "uvicorn[standard]",
"jinja2", "jinja2",
"python-multipart", "python-multipart",
+59
View File
@@ -0,0 +1,59 @@
# retoor <retoor@molodetz.nl>
from typing import Optional
from pydantic import BaseModel
from devplacepy.docs_api._shared import field
from devplacepy.docs_examples import _is_optional, schema_example
def test_field_nullable_parameter():
f = field("bio", "query", "string", False, "", "User biography", nullable=True)
assert f["nullable"] is True
f2 = field("username", "path", "string", True, "alice", "Target username")
assert f2["nullable"] is False
def test_is_optional_optional_type():
assert _is_optional(Optional[str]) is True
assert _is_optional(Optional[int]) is True
assert _is_optional(Optional[list[str]]) is True
def test_is_optional_non_optional():
assert _is_optional(str) is False
assert _is_optional(int) is False
assert _is_optional(list[str]) is False
assert _is_optional(dict) is False
def test_schema_example_nullable_fields():
class NullableModel(BaseModel):
name: str
bio: Optional[str] = None
age: int
score: Optional[int] = None
tags: Optional[list[str]] = None
result = schema_example(NullableModel)
assert result["name"] == "string"
assert result["age"] == 0
assert result["bio"] is None
assert result["score"] is None
assert result["tags"] is None
def test_schema_example_non_nullable_fields_unaffected():
class StrictModel(BaseModel):
x: str
y: int
z: bool
result = schema_example(StrictModel)
assert result["x"] == "string"
assert result["y"] == 0
assert result["z"] is False