fix(api): synchronize hydraulic playback binary contract
This commit is contained in:
@@ -13,6 +13,8 @@ from app.domain.time import parse_utc_time
|
||||
from .dependencies import get_timescale_connection
|
||||
|
||||
router = APIRouter(prefix="/timeseries/realtime/playback")
|
||||
BINARY_RESPONSE = {200: {"description": "Versioned little-endian Float32 packet",
|
||||
"content": {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}}}}
|
||||
# Bounded per-process cache, private to the authorized project and database route.
|
||||
_cache: OrderedDict[tuple, tuple[float, PlaybackGeometry]] = OrderedDict()
|
||||
_load_lock = asyncio.Lock()
|
||||
@@ -36,7 +38,8 @@ async def geometry_for_project(ctx: ProjectContext, conn: AsyncConnection) -> Pl
|
||||
return geometry
|
||||
|
||||
|
||||
@router.get("/geometry", summary="读取带版本的水力专题层静态几何")
|
||||
@router.get("/geometry", summary="读取带版本的水力专题层静态几何",
|
||||
response_class=Response, responses=BINARY_RESPONSE)
|
||||
async def playback_geometry(ctx: ProjectContext = Depends(get_project_context),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection)):
|
||||
try:
|
||||
@@ -47,7 +50,8 @@ async def playback_geometry(ctx: ProjectContext = Depends(get_project_context),
|
||||
headers={"Cache-Control": "private, no-store", "ETag": f'"{geometry.version}"'})
|
||||
|
||||
|
||||
@router.get("/frames", summary="读取索引一致的 Float32 水力时间块")
|
||||
@router.get("/frames", summary="读取索引一致的 Float32 水力时间块",
|
||||
response_class=Response, responses=BINARY_RESPONSE)
|
||||
async def playback_frames(
|
||||
start_time: str,
|
||||
model_version: str = Query(min_length=24, max_length=24),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"contracts": {
|
||||
"server": {
|
||||
"file": "server-v1.openapi.json",
|
||||
"sha256": "9801087d40ac0e1f67871f9a1db850cbcce47614cceec0a7e8771cdf58385f53"
|
||||
"sha256": "04aeed6f1a7414b37267d84141d036d00d2f8579ee28af37590bd4b92137e712"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50912,6 +50912,246 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/timeseries/realtime/playback/frames": {
|
||||
"get": {
|
||||
"operationId": "get_timeseries_realtime_playback_frames",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "start_time",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"title": "Start Time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "model_version",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"maxLength": 24,
|
||||
"minLength": 24,
|
||||
"title": "Model Version",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "frame_count",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"default": 4,
|
||||
"maximum": 8,
|
||||
"minimum": 1,
|
||||
"title": "Frame Count",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "step_seconds",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"default": 900,
|
||||
"maximum": 3600,
|
||||
"minimum": 60,
|
||||
"title": "Step Seconds",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "X-Project-Id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"title": "X-Project-Id",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Versioned little-endian Float32 packet"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Authentication required"
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Insufficient permission"
|
||||
},
|
||||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Resource not found"
|
||||
},
|
||||
"409": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Resource conflict"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation error"
|
||||
},
|
||||
"503": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Dependency unavailable"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"OAuth2PasswordBearer": []
|
||||
}
|
||||
],
|
||||
"summary": "读取索引一致的 Float32 水力时间块",
|
||||
"tags": [
|
||||
"TimescaleDB - Hydraulic Playback"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/timeseries/realtime/playback/geometry": {
|
||||
"get": {
|
||||
"operationId": "get_timeseries_realtime_playback_geometry",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "X-Project-Id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"title": "X-Project-Id",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Versioned little-endian Float32 packet"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Authentication required"
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Insufficient permission"
|
||||
},
|
||||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Resource not found"
|
||||
},
|
||||
"409": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Resource conflict"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation error"
|
||||
},
|
||||
"503": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Dependency unavailable"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"OAuth2PasswordBearer": []
|
||||
}
|
||||
],
|
||||
"summary": "读取带版本的水力专题层静态几何",
|
||||
"tags": [
|
||||
"TimescaleDB - Hydraulic Playback"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/timeseries/realtime/records": {
|
||||
"get": {
|
||||
"description": "查询指定时间点的实时属性值。query_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||
|
||||
@@ -77,3 +77,10 @@ def test_cache_separates_projects_and_database_routes(monkeypatch):
|
||||
asyncio.run(run())
|
||||
assert calls == ["one", "two", "one"]
|
||||
playback._cache.clear()
|
||||
|
||||
|
||||
def test_contract_advertises_binary_packets_instead_of_json():
|
||||
schema = build_test_app(playback.router, "/api/v1").openapi()
|
||||
for suffix in ("geometry", "frames"):
|
||||
content = schema["paths"][f"/api/v1/timeseries/realtime/playback/{suffix}"]["get"]["responses"]["200"]["content"]
|
||||
assert content == {"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}}
|
||||
|
||||
Reference in New Issue
Block a user