Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions services/earnings/jobs/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
from __future__ import annotations

import contextlib
import importlib
import json
import logging
import os
Expand All @@ -96,6 +97,22 @@

_LOG = logging.getLogger("services.earnings.jobs.capture")


def _load_gcs_storage() -> object:
"""Resolve ``google.cloud.storage``, honoring a ``sys.modules`` stub.

Uses :func:`importlib.import_module` instead of ``from google.cloud import
storage``. ``google.cloud`` is a PEP-420 namespace package: once the real
``google.cloud.storage`` submodule is imported anywhere in the process (e.g.
transitively via ``gcsfs`` under the ``[satellite]`` extra) Python binds it as
an attribute on the ``google.cloud`` package object, and a ``from google.cloud
import storage`` statement then reads that attribute and skips ``sys.modules`` —
silently bypassing a test's injected fake. ``importlib.import_module`` consults
``sys.modules`` first, so the fake wins regardless of import order; runtime
behavior is byte-identical when no stub is present."""
return importlib.import_module("google.cloud.storage")


#: The Pub/Sub ack-deadline (seconds) the lease loop resets the message to on each
#: extension. Pub/Sub caps modify_ack_deadline at 600s; a 60-90 min capture needs
#: many extensions, so we re-lease at 600 and refresh well before it lapses.
Expand Down Expand Up @@ -270,9 +287,11 @@ def _upload_handoff(audio_path: str, bucket: str, *, ticker: str, call_id: str)
The cross-service handoff to STT (capture and STT do NOT share a disk). The
object key is namespaced by ``(ticker, call_id)``; it lands in the private,
in-firewall ``AUDIO_HANDOFF_BUCKET`` — NOT R2, NEVER served. STT downloads it,
transcribes, and deletes it. ``google-cloud-storage`` is lazy-imported here.
transcribes, and deletes it. ``google-cloud-storage`` is lazy-imported here
(via importlib so a test's sys.modules stub is honored even after the real
submodule was imported elsewhere — see _load_gcs_storage).
"""
from google.cloud import storage
storage = _load_gcs_storage()

ext = os.path.splitext(audio_path)[1] or ".audio"
blob_name = f"handoff/{ticker}/{call_id}{ext}"
Expand Down
28 changes: 26 additions & 2 deletions services/earnings/jobs/stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,34 @@
from __future__ import annotations

import contextlib
import importlib
import logging
import os
import tempfile
from collections.abc import Iterator

from services.earnings.jobs._env import optional_env, require_env


def _load_gcs_storage() -> object:
"""Resolve the ``google.cloud.storage`` module, honoring a ``sys.modules`` stub.

Uses :func:`importlib.import_module` rather than ``from google.cloud import
storage``. ``google.cloud`` is a PEP-420 namespace package: the first time the
real ``google.cloud.storage`` submodule is imported ANYWHERE in the process
(e.g. transitively via ``gcsfs`` under the ``[satellite]`` extra), Python binds
it as an attribute on the already-loaded ``google.cloud`` package object. After
that, a ``from google.cloud import storage`` statement reads that bound
attribute and NEVER consults ``sys.modules`` — so a test's
``monkeypatch.setitem(sys.modules, "google.cloud.storage", fake)`` is silently
bypassed and the real client is used (a test-isolation footgun that only
surfaces in a venv where something already imported the real submodule).
``importlib.import_module`` consults ``sys.modules`` FIRST, so the injected fake
wins regardless of import order. Runtime behavior is byte-identical to the
import statement when no stub is present (same real module object)."""
return importlib.import_module("google.cloud.storage")


_LOG = logging.getLogger("services.earnings.jobs.stt")


Expand Down Expand Up @@ -118,7 +139,10 @@ def _resolve_audio_reference(

bucket, key = gs
# Lazy import: google-cloud-storage only when a GCS handoff object is fetched.
from google.cloud import storage
# Resolved via importlib (not `from google.cloud import storage`) so a test's
# sys.modules stub is honored even after the real submodule was imported
# elsewhere (see _load_gcs_storage).
storage = _load_gcs_storage()

ext = os.path.splitext(key)[1] or ".audio"
fd, local_path = tempfile.mkstemp(prefix="earnings-stt-handoff-", suffix=ext)
Expand All @@ -143,7 +167,7 @@ def _delete_handoff_source(bucket: str, key: str) -> None:
data (the ledger has the transcript) and is logged loudly (the bucket lifecycle
policy is the backstop) rather than failing the already-successful call.
"""
from google.cloud import storage
storage = _load_gcs_storage()

try:
storage.Client().bucket(bucket).blob(key).delete()
Expand Down
Loading