f1-circuits: become a nox mesh module
The mesh builds the CDN from this repository and a commit (novox/hq ADR 0069); registry-api.novox.be, where HAL's image came from, is retired, so the running container can no longer be re-pulled. The HAL module.yml and service/docker-compose.yml retire with HAL. The Kometa metadata links every poster to this service's own address, which was the literal https://f1-circuits.zurag.be. It is now F1_CIRCUITS_BASE_URL, written by the mesh from the route's name (${bound:route:name}, mesh-controller #149) - falling back to the old address, so HAL's deployment behaves as before. The tile user agent follows it. The poster cache is a placed directory mounted at /app/cache, as HAL's /services/f1-circuits/cache was. The image was built from unpinned pip installs; service/requirements.txt now pins the exact set the running container has (pip freeze), and the base is python 3.12.13-alpine3.24 by digest (a later alpine 3.24 patch than the running build's 3.24.0 - the older digest is no longer tagged). The slug "f1" keeps the route login within 20 characters on any machine. Verified: the manifest parses on mesh-controller main and #149 and renders for a zurag.be node (F1_CIRCUITS_BASE_URL=https://f1-circuits. zurag.be). Built from this commit and run on an empty cache: /circuits, /circuits/<country> and a rendered Spa track are byte-identical to what ace serves today; the base URL comes from the variable, and without it falls back to the old address. Depends on mesh-controller #149.
This commit is contained in:
+82
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"module": "f1-circuits",
|
||||||
|
"slug": "f1",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"label": "f1-circuits",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 8000,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the track-layout CDN, the race posters and the Kometa metadata over http; its public name is a route grant and the proxy reaches it here. Kometa fetches the metadata and every poster by that public name"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cache",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "site-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/site.env",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "F1_CIRCUITS_BASE_URL=https://${bound:route:name}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "f1-circuits",
|
||||||
|
"artifact": "cdn",
|
||||||
|
"ports": [
|
||||||
|
"8000"
|
||||||
|
],
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/site.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:cache}:/app/cache"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"site-env"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"on": [
|
||||||
|
{
|
||||||
|
"arg": "PYTHON_BASE",
|
||||||
|
"image": "python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "cdn",
|
||||||
|
"kind": "image",
|
||||||
|
"from": "service/Dockerfile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-2
@@ -16,6 +16,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -61,8 +62,13 @@ TILE_PROVIDERS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The address this service is reached at, which the Kometa metadata links every poster to. The
|
||||||
|
# mesh writes it from the name the route is served under (F1_CIRCUITS_BASE_URL); the fallback is
|
||||||
|
# the address HAL served it at, so a deployment that sets nothing behaves as before.
|
||||||
|
PUBLIC_BASE_URL = os.environ.get("F1_CIRCUITS_BASE_URL", "https://f1-circuits.zurag.be").rstrip("/")
|
||||||
|
|
||||||
# Polite UA — both providers expect one and Esri throttles aggressively without it.
|
# Polite UA — both providers expect one and Esri throttles aggressively without it.
|
||||||
TILE_USER_AGENT = "f1-circuits-posters/1.0 (+https://f1-circuits.zurag.be)"
|
TILE_USER_AGENT = f"f1-circuits-posters/1.0 (+{PUBLIC_BASE_URL})"
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -641,7 +647,7 @@ class PostersService:
|
|||||||
|
|
||||||
# ------------------------------------------------------------------ kometa metadata
|
# ------------------------------------------------------------------ kometa metadata
|
||||||
|
|
||||||
BASE_URL = "https://f1-circuits.zurag.be"
|
BASE_URL = PUBLIC_BASE_URL
|
||||||
|
|
||||||
def render_kometa_metadata(self, first_season: int = 1950, last_offset: int = 1) -> str:
|
def render_kometa_metadata(self, first_season: int = 1950, last_offset: int = 1) -> str:
|
||||||
"""Emit the Kometa metadata YAML for the Formula 1 library, covering
|
"""Emit the Kometa metadata YAML for the Formula 1 library, covering
|
||||||
|
|||||||
+8
-8
@@ -1,4 +1,8 @@
|
|||||||
FROM python:3.12-alpine
|
# The base arrives pinned from module.json's build.on (python 3.12.13 on alpine 3.24); the mesh
|
||||||
|
# builds this image from this repository and a commit (novox/hq ADR 0069). The build context is
|
||||||
|
# the repository root, so every COPY names its path from there.
|
||||||
|
ARG PYTHON_BASE
|
||||||
|
FROM ${PYTHON_BASE}
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -14,13 +18,9 @@ RUN apk add --no-cache \
|
|||||||
|
|
||||||
# All pip deps ship musllinux wheels — no build-deps needed.
|
# All pip deps ship musllinux wheels — no build-deps needed.
|
||||||
# numpy is pulled in by models/geo_json/geometry.py for vector ops at import time.
|
# numpy is pulled in by models/geo_json/geometry.py for vector ops at import time.
|
||||||
RUN pip install --no-cache-dir \
|
# The versions are the ones the running deployment had (service/requirements.txt).
|
||||||
fastapi \
|
COPY service/requirements.txt ./requirements.txt
|
||||||
uvicorn \
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
python-slugify \
|
|
||||||
pillow \
|
|
||||||
cairosvg \
|
|
||||||
numpy
|
|
||||||
|
|
||||||
COPY cdn-api.py circuits_service.py ergast_service.py posters_service.py ./
|
COPY cdn-api.py circuits_service.py ergast_service.py posters_service.py ./
|
||||||
COPY models/ ./models/
|
COPY models/ ./models/
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# The exact set the deployment ace ran on 2026-09-30 was built with (pip freeze inside it).
|
||||||
|
# Pinned so a rebuild from this commit installs what was running, not whatever is newest.
|
||||||
|
annotated-doc==0.0.4
|
||||||
|
annotated-types==0.7.0
|
||||||
|
anyio==4.13.0
|
||||||
|
cairocffi==1.7.1
|
||||||
|
CairoSVG==2.9.0
|
||||||
|
cffi==2.0.0
|
||||||
|
click==8.4.1
|
||||||
|
cssselect2==0.9.0
|
||||||
|
defusedxml==0.7.1
|
||||||
|
fastapi==0.137.0
|
||||||
|
h11==0.16.0
|
||||||
|
idna==3.18
|
||||||
|
numpy==2.4.6
|
||||||
|
pillow==12.2.0
|
||||||
|
pycparser==3.0
|
||||||
|
pydantic==2.13.4
|
||||||
|
pydantic_core==2.46.4
|
||||||
|
python-slugify==8.0.4
|
||||||
|
starlette==1.3.1
|
||||||
|
text-unidecode==1.3
|
||||||
|
tinycss2==1.5.1
|
||||||
|
typing-inspection==0.4.2
|
||||||
|
typing_extensions==4.15.0
|
||||||
|
uvicorn==0.49.0
|
||||||
|
webencodings==0.5.1
|
||||||
Reference in New Issue
Block a user