From eac73f33eebbb3cdda7e1f34ed911cbd82ddc13f Mon Sep 17 00:00:00 2001 From: Renn F Date: Mon, 6 Jul 2026 02:29:51 +0200 Subject: [PATCH] chore(video): drop internal spec refs + minio/test suppressions (folded hygiene) --- roboco/services/minio_client.py | 22 +++++++++------------- roboco/services/tiktok_client.py | 7 ++----- roboco/services/x_video_client.py | 8 +++----- tests/integration/test_video_routes.py | 12 +++++++----- 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/roboco/services/minio_client.py b/roboco/services/minio_client.py index e67b7a8c..db232138 100644 --- a/roboco/services/minio_client.py +++ b/roboco/services/minio_client.py @@ -2,7 +2,7 @@ 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 -`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()` 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: from collections.abc import Iterator -_client: Minio | None = None -_initialised = False +_cache: dict[str, Minio | None] = {} def _reset_client() -> None: """Test-only: drop the cached singleton so the next `get_client()` rebuilds.""" - global _client, _initialised # noqa: PLW0603 - singleton cache, by design - _client = None - _initialised = False + _cache.clear() 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 plain module-level singleton is fine (no runtime-config drift to guard). """ - global _client, _initialised # noqa: PLW0603 - singleton cache, by design - if _initialised: - return _client - _initialised = True + if "c" in _cache: + return _cache["c"] endpoint = settings.minio_endpoint.strip() if not endpoint: - _client = None + _cache["c"] = None return None parsed = urlparse(endpoint if "://" in endpoint else f"//{endpoint}") secure = parsed.scheme == "https" host = parsed.netloc or endpoint # no scheme → use as-is - _client = Minio( + client = Minio( endpoint=host, access_key=settings.minio_access_key or None, secret_key=settings.minio_secret_key or None, secure=secure, region=settings.minio_region or None, ) - return _client + _cache["c"] = client + return client def put_object(data: bytes, key: str) -> None: diff --git a/roboco/services/tiktok_client.py b/roboco/services/tiktok_client.py index 955fabcf..c9dcd298 100644 --- a/roboco/services/tiktok_client.py +++ b/roboco/services/tiktok_client.py @@ -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) and is re-exported here as the "no credentials" branch of ``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 @@ -48,7 +45,7 @@ _INIT_URL = f"{_API_BASE}/post/publish/inbox/video/init/" _STATUS_URL = f"{_API_BASE}/post/publish/status/fetch/" _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 # one larger final PUT instead of being sent as its own tiny chunk. _CHUNK_SIZE_BYTES = 64 * 1024 * 1024 @@ -130,7 +127,7 @@ class LiveTikTokPoster(TikTokPoster): async def upload_to_inbox( self, *, mp4_path: str, caption: str ) -> 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 # the post manually in-app. Kept in the signature only because the # TikTokPoster Protocol is shared with future direct-post modes. diff --git a/roboco/services/x_video_client.py b/roboco/services/x_video_client.py index e7fb01a9..6cf272fa 100644 --- a/roboco/services/x_video_client.py +++ b/roboco/services/x_video_client.py @@ -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`` 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``. - -See docs/internal/specs/2026-07-04-video-generation-remotion-design.md §11.2 -for the verified sequence and constraints (no documented v2 chunk ceiling; -this client chunks conservatively). +The v2 media-upload API documents no hard chunk ceiling, so this client +chunks conservatively. """ from __future__ import annotations @@ -45,7 +43,7 @@ _API_BASE = "https://api.x.com/2" _MEDIA_UPLOAD_URL = f"{_API_BASE}/media/upload" _TWEETS_URL = f"{_API_BASE}/tweets" _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. _CHUNK_SIZE_BYTES = 4 * 1024 * 1024 _STATUS_POLL_MAX_ATTEMPTS = 60 diff --git a/tests/integration/test_video_routes.py b/tests/integration/test_video_routes.py index 3b133242..dd892397 100644 --- a/tests/integration/test_video_routes.py +++ b/tests/integration/test_video_routes.py @@ -166,7 +166,9 @@ async def _seed_draft( 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.include_router(video_router, prefix="/api/video") 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) # 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) async with AsyncClient(transport=transport, base_url="http://test") as client: 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() # 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) async with AsyncClient(transport=transport, base_url="http://test") as client: 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)) 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) async with AsyncClient(transport=transport, base_url="http://test") as client: 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) - app = _build_app(None, AgentRole.CEO, uuid4()) # type: ignore[arg-type] + app = _build_app(None, AgentRole.CEO, uuid4()) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: resp = await client.get(f"/api/video/posts/{task_id}/media?cut=vertical")