chore(video): drop internal spec refs + minio/test suppressions (folded hygiene)

This commit is contained in:
Renn F
2026-07-06 02:29:51 +02:00
parent 3a6fb8b23d
commit eac73f33ee
4 changed files with 21 additions and 28 deletions
+9 -13
View File
@@ -2,7 +2,7 @@
A thin wrapper around a singleton `minio.Minio` built from settings. The client A thin wrapper around a singleton `minio.Minio` built from settings. The client
is sync (minio-py is sync); every call site wraps the call in is sync (minio-py is sync); every call site wraps the call in
`asyncio.to_thread` — same pattern as `remotion_client._save`. `asyncio.to_thread` — same pattern as `video_renderer_client._save`.
Unconfigured guard: when `settings.minio_endpoint` is empty, `get_client()` Unconfigured guard: when `settings.minio_endpoint` is empty, `get_client()`
returns `None`. The write/serve paths (chunks 3/4) check `get_client()` and returns `None`. The write/serve paths (chunks 3/4) check `get_client()` and
@@ -26,15 +26,12 @@ from roboco.config import settings
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterator from collections.abc import Iterator
_client: Minio | None = None _cache: dict[str, Minio | None] = {}
_initialised = False
def _reset_client() -> None: def _reset_client() -> None:
"""Test-only: drop the cached singleton so the next `get_client()` rebuilds.""" """Test-only: drop the cached singleton so the next `get_client()` rebuilds."""
global _client, _initialised # noqa: PLW0603 - singleton cache, by design _cache.clear()
_client = None
_initialised = False
def get_client() -> Minio | None: def get_client() -> Minio | None:
@@ -44,25 +41,24 @@ def get_client() -> Minio | None:
write/serve paths fall back to local disk. Settings are load-time, so a write/serve paths fall back to local disk. Settings are load-time, so a
plain module-level singleton is fine (no runtime-config drift to guard). plain module-level singleton is fine (no runtime-config drift to guard).
""" """
global _client, _initialised # noqa: PLW0603 - singleton cache, by design if "c" in _cache:
if _initialised: return _cache["c"]
return _client
_initialised = True
endpoint = settings.minio_endpoint.strip() endpoint = settings.minio_endpoint.strip()
if not endpoint: if not endpoint:
_client = None _cache["c"] = None
return None return None
parsed = urlparse(endpoint if "://" in endpoint else f"//{endpoint}") parsed = urlparse(endpoint if "://" in endpoint else f"//{endpoint}")
secure = parsed.scheme == "https" secure = parsed.scheme == "https"
host = parsed.netloc or endpoint # no scheme → use as-is host = parsed.netloc or endpoint # no scheme → use as-is
_client = Minio( client = Minio(
endpoint=host, endpoint=host,
access_key=settings.minio_access_key or None, access_key=settings.minio_access_key or None,
secret_key=settings.minio_secret_key or None, secret_key=settings.minio_secret_key or None,
secure=secure, secure=secure,
region=settings.minio_region or None, region=settings.minio_region or None,
) )
return _client _cache["c"] = client
return client
def put_object(data: bytes, key: str) -> None: def put_object(data: bytes, key: str) -> None:
+2 -5
View File
@@ -6,9 +6,6 @@ refresh-token rotation. Mirrors ``x_client.py``'s Null/Live/build shape;
``NullTikTokPoster`` lives in ``video_post_service`` (the Protocol's home) ``NullTikTokPoster`` lives in ``video_post_service`` (the Protocol's home)
and is re-exported here as the "no credentials" branch of and is re-exported here as the "no credentials" branch of
``build_tiktok_poster``. ``build_tiktok_poster``.
See docs/internal/specs/2026-07-04-video-generation-remotion-design.md §11.3
for the verified sequence and constraints.
""" """
from __future__ import annotations from __future__ import annotations
@@ -48,7 +45,7 @@ _INIT_URL = f"{_API_BASE}/post/publish/inbox/video/init/"
_STATUS_URL = f"{_API_BASE}/post/publish/status/fetch/" _STATUS_URL = f"{_API_BASE}/post/publish/status/fetch/"
_TOKEN_URL = f"{_API_BASE}/oauth/token/" _TOKEN_URL = f"{_API_BASE}/oauth/token/"
# Asymmetric chunking (§11.3): every chunk 5-64 MB except the final one, which # Asymmetric chunking: every chunk 5-64 MB except the final one, which
# absorbs the remainder up to 128 MB — a small dangling tail is folded into # absorbs the remainder up to 128 MB — a small dangling tail is folded into
# one larger final PUT instead of being sent as its own tiny chunk. # one larger final PUT instead of being sent as its own tiny chunk.
_CHUNK_SIZE_BYTES = 64 * 1024 * 1024 _CHUNK_SIZE_BYTES = 64 * 1024 * 1024
@@ -130,7 +127,7 @@ class LiveTikTokPoster(TikTokPoster):
async def upload_to_inbox( async def upload_to_inbox(
self, *, mp4_path: str, caption: str self, *, mp4_path: str, caption: str
) -> TikTokUploadResult: ) -> TikTokUploadResult:
# Inbox-upload's init request carries only source_info (§11.3) — no # Inbox-upload's init request carries only source_info — no
# title/caption field exists on this endpoint; the creator composes # title/caption field exists on this endpoint; the creator composes
# the post manually in-app. Kept in the signature only because the # the post manually in-app. Kept in the signature only because the
# TikTokPoster Protocol is shared with future direct-post modes. # TikTokPoster Protocol is shared with future direct-post modes.
+3 -5
View File
@@ -7,10 +7,8 @@ user-context signed, reusing ``x_client``'s signer — the same
tweet. Mirrors ``x_client.py``'s Null/Live/build shape; ``NullXVideoPoster`` tweet. Mirrors ``x_client.py``'s Null/Live/build shape; ``NullXVideoPoster``
itself lives in ``video_post_service`` (the Protocol's home) and is itself lives in ``video_post_service`` (the Protocol's home) and is
re-exported here as the "no credentials" branch of ``build_x_video_poster``. re-exported here as the "no credentials" branch of ``build_x_video_poster``.
The v2 media-upload API documents no hard chunk ceiling, so this client
See docs/internal/specs/2026-07-04-video-generation-remotion-design.md §11.2 chunks conservatively.
for the verified sequence and constraints (no documented v2 chunk ceiling;
this client chunks conservatively).
""" """
from __future__ import annotations from __future__ import annotations
@@ -45,7 +43,7 @@ _API_BASE = "https://api.x.com/2"
_MEDIA_UPLOAD_URL = f"{_API_BASE}/media/upload" _MEDIA_UPLOAD_URL = f"{_API_BASE}/media/upload"
_TWEETS_URL = f"{_API_BASE}/tweets" _TWEETS_URL = f"{_API_BASE}/tweets"
_MEDIA_CATEGORY = "tweet_video" _MEDIA_CATEGORY = "tweet_video"
# Conservative chunk size — v2 documents no hard ceiling (§11.2); 4 MB keeps # Conservative chunk size — v2 documents no hard ceiling; 4 MB keeps
# each append well within any reasonable request-body limit. # each append well within any reasonable request-body limit.
_CHUNK_SIZE_BYTES = 4 * 1024 * 1024 _CHUNK_SIZE_BYTES = 4 * 1024 * 1024
_STATUS_POLL_MAX_ATTEMPTS = 60 _STATUS_POLL_MAX_ATTEMPTS = 60
+7 -5
View File
@@ -166,7 +166,9 @@ async def _seed_draft(
return task return task
def _build_app(db_session: AsyncSession, role: AgentRole, agent_id: UUID) -> FastAPI: def _build_app(
db_session: AsyncSession | None, role: AgentRole, agent_id: UUID
) -> FastAPI:
app = FastAPI() app = FastAPI()
app.include_router(video_router, prefix="/api/video") app.include_router(video_router, prefix="/api/video")
app.include_router(tiktok_router, prefix="/api/tiktok") app.include_router(tiktok_router, prefix="/api/tiktok")
@@ -614,7 +616,7 @@ async def test_media_serves_from_minio_when_configured(
monkeypatch.setattr(minio_client, "get_object_stream", _minio_stream) monkeypatch.setattr(minio_client, "get_object_stream", _minio_stream)
# CEO 200 — streamed from MinIO. # CEO 200 — streamed from MinIO.
app = _build_app(None, AgentRole.CEO, uuid4()) # type: ignore[arg-type] app = _build_app(None, AgentRole.CEO, uuid4())
transport = ASGITransport(app=app) transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client: async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical") resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical")
@@ -624,7 +626,7 @@ async def test_media_serves_from_minio_when_configured(
app.dependency_overrides.clear() app.dependency_overrides.clear()
# Non-CEO 403 — _require_ceo still gates end-to-end (no presigned URL). # Non-CEO 403 — _require_ceo still gates end-to-end (no presigned URL).
app = _build_app(None, AgentRole.DEVELOPER, uuid4()) # type: ignore[arg-type] app = _build_app(None, AgentRole.DEVELOPER, uuid4())
transport = ASGITransport(app=app) transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client: async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical") resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical")
@@ -647,7 +649,7 @@ async def test_media_falls_back_to_local_file_when_minio_unconfigured(
_patch_task_service(monkeypatch, _make_task(str(vertical), task_id)) _patch_task_service(monkeypatch, _make_task(str(vertical), task_id))
monkeypatch.setattr(minio_client, "get_client", lambda: None) monkeypatch.setattr(minio_client, "get_client", lambda: None)
app = _build_app(None, AgentRole.CEO, uuid4()) # type: ignore[arg-type] app = _build_app(None, AgentRole.CEO, uuid4())
transport = ASGITransport(app=app) transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client: async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical") resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical")
@@ -685,7 +687,7 @@ async def test_media_falls_back_to_local_file_when_minio_missing(
monkeypatch.setattr(minio_client, "get_object_stream", _stream_must_not_be_called) monkeypatch.setattr(minio_client, "get_object_stream", _stream_must_not_be_called)
app = _build_app(None, AgentRole.CEO, uuid4()) # type: ignore[arg-type] app = _build_app(None, AgentRole.CEO, uuid4())
transport = ASGITransport(app=app) transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client: async with AsyncClient(transport=transport, base_url="http://test") as client:
resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical") resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical")