mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
chore(video): drop internal spec refs + minio/test suppressions (folded hygiene)
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user