feat: implement ASR configuration and HuggingFace token management services
- Introduced `AsrConfigService` for managing ASR engine reconfiguration, including job tracking and capabilities validation. - Added `HfTokenService` for secure management of HuggingFace API tokens, including storage, validation, and retrieval functionalities. - Updated gRPC service to include endpoints for ASR configuration and HuggingFace token management. - Refactored existing services to improve integration with new ASR and token management features. - Added unit and integration tests for both services to ensure functionality and reliability.
This commit is contained in:
@@ -7,7 +7,8 @@ from noteflow.application.services.auth_service import (
|
||||
LogoutResult,
|
||||
UserInfo,
|
||||
)
|
||||
from noteflow.application.services.export_service import ExportFormat, ExportService
|
||||
from noteflow.application.services.export_service import ExportService
|
||||
from noteflow.domain.value_objects import ExportFormat
|
||||
from noteflow.application.services.identity import IdentityService
|
||||
from noteflow.application.services.meeting import MeetingService
|
||||
from noteflow.application.services.project_service import ProjectService
|
||||
|
||||
307
src/noteflow/application/services/asr_config_service.py
Normal file
307
src/noteflow/application/services/asr_config_service.py
Normal file
@@ -0,0 +1,307 @@
|
||||
"""ASR configuration service for runtime reconfiguration.
|
||||
|
||||
Orchestrates ASR engine reconfiguration including model reload with progress tracking.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from noteflow.application.services.asr_config_types import (
|
||||
AsrCapabilities,
|
||||
AsrComputeType,
|
||||
AsrConfigJob,
|
||||
AsrConfigPhase,
|
||||
AsrDevice,
|
||||
DEVICE_COMPUTE_TYPES,
|
||||
)
|
||||
from noteflow.domain.constants.fields import (
|
||||
JOB_STATUS_COMPLETED,
|
||||
JOB_STATUS_FAILED,
|
||||
JOB_STATUS_QUEUED,
|
||||
JOB_STATUS_RUNNING,
|
||||
)
|
||||
from noteflow.infrastructure.asr import VALID_MODEL_SIZES
|
||||
from noteflow.infrastructure.logging import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from noteflow.infrastructure.asr import FasterWhisperEngine
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class AsrConfigService:
|
||||
"""Service for managing ASR engine configuration at runtime.
|
||||
|
||||
Provides capabilities discovery, validation, and orchestrated model reload
|
||||
with progress tracking via background jobs.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
asr_engine: FasterWhisperEngine | None,
|
||||
*,
|
||||
on_engine_update: Callable[[FasterWhisperEngine], None] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the service.
|
||||
|
||||
Args:
|
||||
asr_engine: The ASR engine instance to manage. May be None if ASR is disabled.
|
||||
on_engine_update: Optional callback when the active engine is replaced.
|
||||
"""
|
||||
self._asr_engine = asr_engine
|
||||
self._on_engine_update = on_engine_update
|
||||
self._jobs: dict[UUID, AsrConfigJob] = {}
|
||||
self._job_lock = asyncio.Lock()
|
||||
self._reload_lock = asyncio.Lock()
|
||||
|
||||
def detect_cuda_available(self) -> bool:
|
||||
"""Detect if CUDA is available for ASR.
|
||||
|
||||
Returns:
|
||||
True if CUDA is available, False otherwise.
|
||||
"""
|
||||
try:
|
||||
import torch
|
||||
|
||||
return torch.cuda.is_available()
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def get_capabilities(self) -> AsrCapabilities:
|
||||
"""Get current ASR configuration and capabilities.
|
||||
|
||||
Returns:
|
||||
Current ASR configuration including available options.
|
||||
"""
|
||||
cuda_available = self.detect_cuda_available()
|
||||
current_device = AsrDevice.CPU
|
||||
current_compute_type = AsrComputeType.INT8
|
||||
|
||||
if self._asr_engine is not None:
|
||||
current_device = AsrDevice(self._asr_engine.device)
|
||||
current_compute_type = AsrComputeType(self._asr_engine.compute_type)
|
||||
|
||||
return AsrCapabilities(
|
||||
model_size=self._asr_engine.model_size if self._asr_engine else None,
|
||||
device=current_device,
|
||||
compute_type=current_compute_type,
|
||||
is_ready=self._asr_engine.is_loaded if self._asr_engine else False,
|
||||
cuda_available=cuda_available,
|
||||
available_model_sizes=VALID_MODEL_SIZES,
|
||||
available_compute_types=DEVICE_COMPUTE_TYPES[current_device],
|
||||
)
|
||||
|
||||
def validate_configuration(
|
||||
self,
|
||||
model_size: str | None,
|
||||
device: AsrDevice | None,
|
||||
compute_type: AsrComputeType | None,
|
||||
) -> str | None:
|
||||
"""Validate configuration before applying.
|
||||
|
||||
Args:
|
||||
model_size: Requested model size (or None to keep current).
|
||||
device: Requested device (or None to keep current).
|
||||
compute_type: Requested compute type (or None to keep current).
|
||||
|
||||
Returns:
|
||||
Error message if validation fails, None if valid.
|
||||
"""
|
||||
if model_size is not None and model_size not in VALID_MODEL_SIZES:
|
||||
valid_sizes = ", ".join(VALID_MODEL_SIZES)
|
||||
return f"Invalid model size: {model_size}. Valid: {valid_sizes}"
|
||||
|
||||
if device == AsrDevice.CUDA and not self.detect_cuda_available():
|
||||
return "CUDA requested but not available on this server"
|
||||
|
||||
if device is not None and compute_type is not None:
|
||||
valid_types = DEVICE_COMPUTE_TYPES[device]
|
||||
if compute_type not in valid_types:
|
||||
return (
|
||||
f"Compute type {compute_type.value} not available for {device.value}"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _check_reconfiguration_preconditions(
|
||||
self,
|
||||
has_active_recordings: bool,
|
||||
) -> str | None:
|
||||
"""Check preconditions for ASR reconfiguration.
|
||||
|
||||
Args:
|
||||
has_active_recordings: Whether there are active recording streams.
|
||||
|
||||
Returns:
|
||||
Error message if preconditions fail, None if OK.
|
||||
"""
|
||||
if has_active_recordings:
|
||||
return "Cannot reconfigure ASR while recordings are active"
|
||||
return "ASR engine is not available" if self._asr_engine is None else None
|
||||
|
||||
async def start_reconfiguration(
|
||||
self,
|
||||
model_size: str | None,
|
||||
device: AsrDevice | None,
|
||||
compute_type: AsrComputeType | None,
|
||||
has_active_recordings: bool,
|
||||
) -> tuple[UUID | None, str | None]:
|
||||
"""Start ASR reconfiguration.
|
||||
|
||||
Returns:
|
||||
Tuple of (job_id, error_message). job_id is None on validation failure.
|
||||
"""
|
||||
if precondition_error := self._check_reconfiguration_preconditions(
|
||||
has_active_recordings
|
||||
):
|
||||
return None, precondition_error
|
||||
|
||||
# Use current values as defaults
|
||||
caps = self.get_capabilities()
|
||||
target_model = model_size or caps.model_size or "base"
|
||||
target_device = device or caps.device
|
||||
target_compute = compute_type or caps.compute_type
|
||||
|
||||
if error := self.validate_configuration(
|
||||
target_model, target_device, target_compute
|
||||
):
|
||||
return None, error
|
||||
|
||||
# Create and queue job
|
||||
job_id = uuid4()
|
||||
job = AsrConfigJob(
|
||||
job_id=job_id,
|
||||
status=JOB_STATUS_QUEUED,
|
||||
phase=AsrConfigPhase.VALIDATING,
|
||||
progress_percent=0.0,
|
||||
error_message="",
|
||||
target_model_size=target_model,
|
||||
target_device=target_device,
|
||||
target_compute_type=target_compute,
|
||||
)
|
||||
|
||||
async with self._job_lock:
|
||||
self._jobs[job_id] = job
|
||||
|
||||
job.task = asyncio.create_task(self._run_reconfiguration(job))
|
||||
return job_id, None
|
||||
|
||||
|
||||
def _build_engine_for_job(
|
||||
self,
|
||||
job: AsrConfigJob,
|
||||
current_engine: FasterWhisperEngine | None,
|
||||
) -> tuple[FasterWhisperEngine, bool]:
|
||||
"""Return an engine to load the model into.
|
||||
|
||||
If device/compute settings change, create a new engine but keep the
|
||||
current one active until the new model loads successfully.
|
||||
|
||||
Args:
|
||||
job: The reconfiguration job with target settings.
|
||||
current_engine: The currently active engine (if any).
|
||||
|
||||
Returns:
|
||||
Tuple of (engine_to_use, is_new_engine).
|
||||
"""
|
||||
if (
|
||||
current_engine is not None
|
||||
and job.target_device.value == current_engine.device
|
||||
and job.target_compute_type.value == current_engine.compute_type
|
||||
):
|
||||
return current_engine, False
|
||||
|
||||
# Import here to avoid circular imports
|
||||
from noteflow.infrastructure.asr import FasterWhisperEngine
|
||||
|
||||
return (
|
||||
FasterWhisperEngine(
|
||||
compute_type=job.target_compute_type.value,
|
||||
device=job.target_device.value,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
def _set_active_engine(self, engine: FasterWhisperEngine) -> None:
|
||||
"""Swap the active engine and notify listeners."""
|
||||
self._asr_engine = engine
|
||||
if self._on_engine_update is not None:
|
||||
self._on_engine_update(engine)
|
||||
|
||||
async def _load_model(
|
||||
self,
|
||||
engine: FasterWhisperEngine,
|
||||
model_size: str,
|
||||
) -> None:
|
||||
"""Load a model into the provided engine."""
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, engine.load_model, model_size)
|
||||
|
||||
def _mark_job_completed(self, job: AsrConfigJob) -> None:
|
||||
"""Update job state for successful completion."""
|
||||
job.phase = AsrConfigPhase.COMPLETED
|
||||
job.status = JOB_STATUS_COMPLETED
|
||||
job.progress_percent = 100.0
|
||||
|
||||
def _mark_job_failed(self, job: AsrConfigJob, error: Exception) -> None:
|
||||
"""Update job state for failure and log."""
|
||||
job.phase = AsrConfigPhase.FAILED
|
||||
job.status = JOB_STATUS_FAILED
|
||||
job.error_message = str(error)
|
||||
logger.error("asr_reconfiguration_failed", error=str(error))
|
||||
|
||||
async def _execute_reconfiguration(self, job: AsrConfigJob) -> None:
|
||||
"""Run reconfiguration steps and swap engine after success."""
|
||||
current_engine = self._asr_engine
|
||||
job.status = JOB_STATUS_RUNNING
|
||||
job.phase = AsrConfigPhase.LOADING
|
||||
job.progress_percent = 10.0
|
||||
|
||||
engine_to_use, is_new_engine = self._build_engine_for_job(job, current_engine)
|
||||
job.progress_percent = 50.0
|
||||
|
||||
await self._load_model(engine_to_use, job.target_model_size)
|
||||
job.progress_percent = 90.0
|
||||
|
||||
if is_new_engine:
|
||||
self._set_active_engine(engine_to_use)
|
||||
if current_engine is not None:
|
||||
current_engine.unload()
|
||||
|
||||
self._mark_job_completed(job)
|
||||
logger.info(
|
||||
"asr_reconfigured",
|
||||
model_size=job.target_model_size,
|
||||
device=job.target_device.value,
|
||||
compute_type=job.target_compute_type.value,
|
||||
)
|
||||
|
||||
async def _run_reconfiguration(self, job: AsrConfigJob) -> None:
|
||||
"""Execute the reconfiguration in background.
|
||||
|
||||
Args:
|
||||
job: The job to execute.
|
||||
"""
|
||||
async with self._reload_lock:
|
||||
try:
|
||||
await self._execute_reconfiguration(job)
|
||||
except Exception as e:
|
||||
self._mark_job_failed(job, e)
|
||||
|
||||
def get_job_status(self, job_id: UUID) -> AsrConfigJob | None:
|
||||
"""Get status of a reconfiguration job.
|
||||
|
||||
Args:
|
||||
job_id: The job identifier.
|
||||
|
||||
Returns:
|
||||
The job status, or None if not found.
|
||||
"""
|
||||
job = self._jobs.get(job_id)
|
||||
if job is None:
|
||||
logger.debug("job_not_found", job_id=str(job_id))
|
||||
return job
|
||||
72
src/noteflow/application/services/asr_config_types.py
Normal file
72
src/noteflow/application/services/asr_config_types.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Types and constants for ASR configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Final
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class AsrConfigPhase(str, Enum):
|
||||
"""Phases of ASR reconfiguration."""
|
||||
|
||||
VALIDATING = "validating"
|
||||
DOWNLOADING = "downloading"
|
||||
LOADING = "loading"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class AsrDevice(str, Enum):
|
||||
"""Supported ASR devices."""
|
||||
|
||||
CPU = "cpu"
|
||||
CUDA = "cuda"
|
||||
|
||||
|
||||
class AsrComputeType(str, Enum):
|
||||
"""Supported compute types."""
|
||||
|
||||
INT8 = "int8"
|
||||
FLOAT16 = "float16"
|
||||
FLOAT32 = "float32"
|
||||
|
||||
|
||||
DEVICE_COMPUTE_TYPES: Final[dict[AsrDevice, tuple[AsrComputeType, ...]]] = {
|
||||
AsrDevice.CPU: (AsrComputeType.INT8, AsrComputeType.FLOAT32),
|
||||
AsrDevice.CUDA: (
|
||||
AsrComputeType.INT8,
|
||||
AsrComputeType.FLOAT16,
|
||||
AsrComputeType.FLOAT32,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AsrConfigJob:
|
||||
"""Tracks ASR reconfiguration job state."""
|
||||
|
||||
job_id: UUID
|
||||
status: str # One of JOB_STATUS_* constants
|
||||
phase: AsrConfigPhase
|
||||
progress_percent: float
|
||||
error_message: str
|
||||
target_model_size: str
|
||||
target_device: AsrDevice
|
||||
target_compute_type: AsrComputeType
|
||||
task: asyncio.Task[None] | None = field(default=None, repr=False)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AsrCapabilities:
|
||||
"""Current ASR capabilities and configuration."""
|
||||
|
||||
model_size: str | None
|
||||
device: AsrDevice
|
||||
compute_type: AsrComputeType
|
||||
is_ready: bool
|
||||
cuda_available: bool
|
||||
available_model_sizes: tuple[str, ...]
|
||||
available_compute_types: tuple[AsrComputeType, ...]
|
||||
@@ -5,7 +5,6 @@ Orchestrates transcript export to various formats.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -14,6 +13,7 @@ from noteflow.config.constants import (
|
||||
EXPORT_EXT_HTML,
|
||||
EXPORT_EXT_PDF,
|
||||
)
|
||||
from noteflow.domain.value_objects import ExportFormat
|
||||
from noteflow.infrastructure.export import (
|
||||
HtmlExporter,
|
||||
MarkdownExporter,
|
||||
@@ -31,14 +31,6 @@ if TYPE_CHECKING:
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ExportFormat(Enum):
|
||||
"""Supported export formats."""
|
||||
|
||||
MARKDOWN = "markdown"
|
||||
HTML = "html"
|
||||
PDF = "pdf"
|
||||
|
||||
|
||||
# Module-level constant mapping file extensions to format values.
|
||||
# Keys are lowercase extensions, values are ExportFormat enum value strings.
|
||||
_EXTENSION_TO_FORMAT: dict[str, str] = {
|
||||
|
||||
248
src/noteflow/application/services/hf_token_service.py
Normal file
248
src/noteflow/application/services/hf_token_service.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""HuggingFace token service for secure token management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Final, cast
|
||||
|
||||
import httpx
|
||||
|
||||
from noteflow.domain.utils.time import utc_now
|
||||
from noteflow.infrastructure.logging import get_logger
|
||||
from noteflow.infrastructure.security.protocols import EncryptedChunk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from noteflow.domain.ports.unit_of_work import UnitOfWork
|
||||
from noteflow.infrastructure.security.crypto import AesGcmCryptoBox
|
||||
|
||||
_MetadataDict = dict[str, str | float | bool | None]
|
||||
_EncryptedDataDict = dict[str, str]
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Preference keys for HF token storage
|
||||
_HF_TOKEN_KEY: Final[str] = "_hf_token_encrypted"
|
||||
_HF_TOKEN_META_KEY: Final[str] = "_hf_token_meta"
|
||||
_HF_WHOAMI_URL: Final[str] = "https://huggingface.co/api/whoami-v2"
|
||||
|
||||
# Metadata and encrypted data field keys
|
||||
_META_USERNAME: Final[str] = "username"
|
||||
_META_VALIDATED_AT: Final[str] = "validated_at"
|
||||
_META_IS_VALIDATED: Final[str] = "is_validated"
|
||||
_ENC_WRAPPED_DEK: Final[str] = "wrapped_dek"
|
||||
_ENC_NONCE: Final[str] = "nonce"
|
||||
_ENC_CIPHERTEXT: Final[str] = "ciphertext"
|
||||
_ENC_TAG: Final[str] = "tag"
|
||||
_ASCII_ENCODING: Final[str] = "ascii"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HfTokenStatus:
|
||||
"""Status of the HuggingFace token."""
|
||||
|
||||
is_configured: bool
|
||||
is_validated: bool
|
||||
username: str
|
||||
validated_at: float | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HfValidationResult:
|
||||
"""Result of HuggingFace token validation."""
|
||||
|
||||
valid: bool
|
||||
username: str
|
||||
error_message: str
|
||||
|
||||
|
||||
class HfTokenService:
|
||||
"""Service for managing HuggingFace API tokens."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uow_factory: Callable[[], UnitOfWork],
|
||||
crypto: AesGcmCryptoBox,
|
||||
) -> None:
|
||||
"""Initialize with UoW factory and crypto box."""
|
||||
self._uow_factory = uow_factory
|
||||
self._crypto = crypto
|
||||
|
||||
async def set_token(
|
||||
self,
|
||||
token: str,
|
||||
*,
|
||||
validate: bool = True,
|
||||
) -> tuple[bool, HfValidationResult | None]:
|
||||
"""Set and optionally validate a HuggingFace token."""
|
||||
validation_result: HfValidationResult | None = None
|
||||
username = ""
|
||||
validated_at: float | None = None
|
||||
|
||||
if validate:
|
||||
validation_result = await self.validate_token_internal(token)
|
||||
if not validation_result.valid:
|
||||
return False, validation_result
|
||||
username = validation_result.username
|
||||
validated_at = utc_now().timestamp()
|
||||
|
||||
encrypted_data = self.encrypt_token(token)
|
||||
is_valid = validate and validation_result is not None and validation_result.valid
|
||||
metadata: dict[str, str | float | bool | None] = {
|
||||
_META_USERNAME: username,
|
||||
_META_VALIDATED_AT: validated_at,
|
||||
_META_IS_VALIDATED: is_valid,
|
||||
}
|
||||
|
||||
async with self._uow_factory() as uow:
|
||||
if not uow.supports_preferences:
|
||||
error_result = HfValidationResult(
|
||||
False,
|
||||
"",
|
||||
"Preferences storage not available",
|
||||
)
|
||||
logger.warning("hf_token_storage_unavailable")
|
||||
return False, error_result
|
||||
await uow.preferences.set(_HF_TOKEN_KEY, encrypted_data)
|
||||
await uow.preferences.set(_HF_TOKEN_META_KEY, metadata)
|
||||
await uow.commit()
|
||||
|
||||
logger.info("hf_token_stored", username=username, validated=validate)
|
||||
return True, validation_result
|
||||
|
||||
async def get_status(self) -> HfTokenStatus:
|
||||
"""Get the current status of the HuggingFace token."""
|
||||
async with self._uow_factory() as uow:
|
||||
if not uow.supports_preferences:
|
||||
return HfTokenStatus(False, False, "", None)
|
||||
encrypted_data = await uow.preferences.get(_HF_TOKEN_KEY)
|
||||
metadata = await uow.preferences.get(_HF_TOKEN_META_KEY)
|
||||
|
||||
if encrypted_data is None:
|
||||
return HfTokenStatus(False, False, "", None)
|
||||
|
||||
if metadata is not None and isinstance(metadata, dict):
|
||||
meta = cast(_MetadataDict, metadata)
|
||||
username = str(meta.get(_META_USERNAME, ""))
|
||||
is_validated = bool(meta.get(_META_IS_VALIDATED, False))
|
||||
raw_validated_at = meta.get(_META_VALIDATED_AT)
|
||||
validated_at = float(raw_validated_at) if raw_validated_at is not None else None
|
||||
else:
|
||||
username, is_validated, validated_at = "", False, None
|
||||
|
||||
return HfTokenStatus(True, is_validated, username, validated_at)
|
||||
|
||||
async def delete_token(self) -> bool:
|
||||
"""Delete the stored HuggingFace token."""
|
||||
async with self._uow_factory() as uow:
|
||||
if not uow.supports_preferences:
|
||||
return False
|
||||
deleted_token = await uow.preferences.delete(_HF_TOKEN_KEY)
|
||||
await uow.preferences.delete(_HF_TOKEN_META_KEY)
|
||||
await uow.commit()
|
||||
|
||||
if deleted_token:
|
||||
logger.info("hf_token_deleted")
|
||||
return deleted_token
|
||||
|
||||
async def _update_validation_metadata(self, username: str) -> None:
|
||||
"""Update token validation metadata after successful validation."""
|
||||
metadata: dict[str, str | float | bool | None] = {
|
||||
_META_USERNAME: username,
|
||||
_META_VALIDATED_AT: utc_now().timestamp(),
|
||||
_META_IS_VALIDATED: True,
|
||||
}
|
||||
async with self._uow_factory() as uow:
|
||||
if uow.supports_preferences:
|
||||
await uow.preferences.set(_HF_TOKEN_META_KEY, metadata)
|
||||
await uow.commit()
|
||||
|
||||
async def validate_stored_token(self) -> HfValidationResult:
|
||||
"""Validate the currently stored HuggingFace token."""
|
||||
async with self._uow_factory() as uow:
|
||||
if not uow.supports_preferences:
|
||||
return HfValidationResult(False, "", "Preferences storage not available")
|
||||
encrypted_data = await uow.preferences.get(_HF_TOKEN_KEY)
|
||||
|
||||
if encrypted_data is None:
|
||||
return HfValidationResult(False, "", "No token configured")
|
||||
|
||||
try:
|
||||
token = self.decrypt_token(encrypted_data)
|
||||
except ValueError as e:
|
||||
logger.error("hf_token_decrypt_failed", error=str(e))
|
||||
return HfValidationResult(False, "", "Token decryption failed")
|
||||
|
||||
result = await self.validate_token_internal(token)
|
||||
if result.valid:
|
||||
await self._update_validation_metadata(result.username)
|
||||
return result
|
||||
|
||||
async def get_token(self) -> str | None:
|
||||
"""Get the decrypted HuggingFace token, or None if not configured."""
|
||||
async with self._uow_factory() as uow:
|
||||
if not uow.supports_preferences:
|
||||
return None
|
||||
encrypted_data = await uow.preferences.get(_HF_TOKEN_KEY)
|
||||
|
||||
if encrypted_data is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return self.decrypt_token(encrypted_data)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
async def validate_token_internal(self, token: str) -> HfValidationResult:
|
||||
"""Validate a token against HuggingFace API."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
_HF_WHOAMI_URL,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return HfValidationResult(True, str(data.get("name", "")), "")
|
||||
if response.status_code == 401:
|
||||
return HfValidationResult(False, "", "Invalid or expired token")
|
||||
return HfValidationResult(False, "", f"HuggingFace API error: {response.status_code}")
|
||||
except httpx.TimeoutException:
|
||||
return HfValidationResult(False, "", "Connection timeout")
|
||||
except httpx.RequestError as e:
|
||||
return HfValidationResult(False, "", f"Connection error: {e}")
|
||||
|
||||
def encrypt_token(self, token: str) -> dict[str, str]:
|
||||
"""Encrypt a token for storage."""
|
||||
dek = self._crypto.generate_dek()
|
||||
wrapped_dek = self._crypto.wrap_dek(dek)
|
||||
token_bytes = token.encode("utf-8")
|
||||
encrypted = self._crypto.encrypt_chunk(token_bytes, dek)
|
||||
return {
|
||||
_ENC_WRAPPED_DEK: base64.b64encode(wrapped_dek).decode(_ASCII_ENCODING),
|
||||
_ENC_NONCE: base64.b64encode(encrypted.nonce).decode(_ASCII_ENCODING),
|
||||
_ENC_CIPHERTEXT: base64.b64encode(encrypted.ciphertext).decode(_ASCII_ENCODING),
|
||||
_ENC_TAG: base64.b64encode(encrypted.tag).decode(_ASCII_ENCODING),
|
||||
}
|
||||
|
||||
def decrypt_token(self, encrypted_data: object) -> str:
|
||||
"""Decrypt a stored token. Raises ValueError if decryption fails."""
|
||||
if not isinstance(encrypted_data, dict):
|
||||
raise ValueError("Invalid encrypted data format")
|
||||
|
||||
data = cast(_EncryptedDataDict, encrypted_data)
|
||||
try:
|
||||
wrapped_dek = base64.b64decode(data[_ENC_WRAPPED_DEK])
|
||||
nonce = base64.b64decode(data[_ENC_NONCE])
|
||||
ciphertext = base64.b64decode(data[_ENC_CIPHERTEXT])
|
||||
tag = base64.b64decode(data[_ENC_TAG])
|
||||
except (KeyError, ValueError) as e:
|
||||
raise ValueError(f"Invalid encrypted data: {e}") from e
|
||||
|
||||
dek = self._crypto.unwrap_dek(wrapped_dek)
|
||||
chunk = EncryptedChunk(nonce=nonce, ciphertext=ciphertext, tag=tag)
|
||||
plaintext = self._crypto.decrypt_chunk(chunk, dek)
|
||||
return plaintext.decode("utf-8")
|
||||
@@ -69,3 +69,10 @@ WRAPPED_DEK: Final[str] = "wrapped_dek"
|
||||
# Entity type names (for logging/messages)
|
||||
ENTITY_MEETING: Final[str] = "Meeting"
|
||||
ENTITY_WORKSPACE: Final[str] = "Workspace"
|
||||
|
||||
# Job status constants
|
||||
JOB_STATUS_QUEUED: Final[str] = "queued"
|
||||
JOB_STATUS_RUNNING: Final[str] = "running"
|
||||
JOB_STATUS_COMPLETED: Final[str] = "completed"
|
||||
JOB_STATUS_FAILED: Final[str] = "failed"
|
||||
JOB_STATUS_CANCELLED: Final[str] = "cancelled"
|
||||
|
||||
@@ -5,7 +5,18 @@ from __future__ import annotations
|
||||
from collections.abc import Sequence
|
||||
from typing import Protocol
|
||||
|
||||
from noteflow.domain.constants.fields import ACTION_ITEM, DECISION, NOTE, RISK, UNKNOWN
|
||||
from noteflow.domain.constants.fields import (
|
||||
ACTION_ITEM,
|
||||
DECISION,
|
||||
JOB_STATUS_CANCELLED,
|
||||
JOB_STATUS_COMPLETED,
|
||||
JOB_STATUS_FAILED,
|
||||
JOB_STATUS_QUEUED,
|
||||
JOB_STATUS_RUNNING,
|
||||
NOTE,
|
||||
RISK,
|
||||
UNKNOWN,
|
||||
)
|
||||
from noteflow.grpc._types import AnnotationInfo, MeetingInfo
|
||||
from noteflow.grpc.proto import noteflow_pb2
|
||||
|
||||
@@ -84,10 +95,11 @@ EXPORT_FORMAT_TO_PROTO: dict[str, int] = {
|
||||
# Job status mapping
|
||||
JOB_STATUS_MAP: dict[int, str] = {
|
||||
noteflow_pb2.JOB_STATUS_UNSPECIFIED: UNKNOWN,
|
||||
noteflow_pb2.JOB_STATUS_QUEUED: "queued",
|
||||
noteflow_pb2.JOB_STATUS_RUNNING: "running",
|
||||
noteflow_pb2.JOB_STATUS_COMPLETED: "completed",
|
||||
noteflow_pb2.JOB_STATUS_FAILED: "failed",
|
||||
noteflow_pb2.JOB_STATUS_QUEUED: JOB_STATUS_QUEUED,
|
||||
noteflow_pb2.JOB_STATUS_RUNNING: JOB_STATUS_RUNNING,
|
||||
noteflow_pb2.JOB_STATUS_COMPLETED: JOB_STATUS_COMPLETED,
|
||||
noteflow_pb2.JOB_STATUS_FAILED: JOB_STATUS_FAILED,
|
||||
noteflow_pb2.JOB_STATUS_CANCELLED: JOB_STATUS_CANCELLED,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
from ._types import GrpcContext, GrpcStatusContext
|
||||
from .annotation import AnnotationMixin
|
||||
from .asr_config import AsrConfigMixin
|
||||
from .calendar import CalendarMixin
|
||||
from .diarization import DiarizationMixin
|
||||
from .diarization_job import DiarizationJobMixin
|
||||
from .entities import EntitiesMixin
|
||||
from .export import ExportMixin
|
||||
from .hf_token import HfTokenMixin
|
||||
from .identity import IdentityMixin
|
||||
from .meeting import MeetingMixin
|
||||
from .observability import ObservabilityMixin
|
||||
@@ -24,6 +26,7 @@ from .webhooks import WebhooksMixin
|
||||
|
||||
__all__ = [
|
||||
"AnnotationMixin",
|
||||
"AsrConfigMixin",
|
||||
"CalendarMixin",
|
||||
"DiarizationJobMixin",
|
||||
"DiarizationMixin",
|
||||
@@ -31,6 +34,7 @@ __all__ = [
|
||||
"ExportMixin",
|
||||
"GrpcContext",
|
||||
"GrpcStatusContext",
|
||||
"HfTokenMixin",
|
||||
"IdentityMixin",
|
||||
"MeetingMixin",
|
||||
"ObservabilityMixin",
|
||||
|
||||
@@ -13,7 +13,9 @@ if TYPE_CHECKING:
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from noteflow.application.services.asr_config_service import AsrConfigService
|
||||
from noteflow.application.services.calendar import CalendarService
|
||||
from noteflow.application.services.hf_token_service import HfTokenService
|
||||
from noteflow.application.services.identity import IdentityService
|
||||
from noteflow.application.services.ner_service import NerService
|
||||
from noteflow.application.services.project_service import ProjectService
|
||||
@@ -42,6 +44,7 @@ class ServicerState(Protocol):
|
||||
|
||||
# Engines and services
|
||||
asr_engine: FasterWhisperEngine | None
|
||||
asr_config_service: AsrConfigService | None
|
||||
diarization_engine: DiarizationEngine | None
|
||||
summarization_service: SummarizationService | None
|
||||
ner_service: NerService | None
|
||||
@@ -49,6 +52,7 @@ class ServicerState(Protocol):
|
||||
webhook_service: WebhookService | None
|
||||
project_service: ProjectService | None
|
||||
identity_service: IdentityService
|
||||
hf_token_service: HfTokenService | None
|
||||
diarization_refinement_enabled: bool
|
||||
|
||||
# Audio writers
|
||||
|
||||
288
src/noteflow/grpc/_mixins/asr_config.py
Normal file
288
src/noteflow/grpc/_mixins/asr_config.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""ASR configuration mixin for gRPC service.
|
||||
|
||||
Provides runtime ASR engine reconfiguration including model size,
|
||||
device, and compute type changes with job-based progress tracking.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, cast
|
||||
|
||||
from noteflow.application.services.asr_config_service import (
|
||||
AsrComputeType,
|
||||
AsrConfigPhase,
|
||||
AsrConfigService,
|
||||
AsrDevice,
|
||||
)
|
||||
from noteflow.domain.constants.fields import (
|
||||
JOB_STATUS_COMPLETED,
|
||||
JOB_STATUS_FAILED,
|
||||
JOB_STATUS_QUEUED,
|
||||
JOB_STATUS_RUNNING,
|
||||
)
|
||||
from noteflow.infrastructure.logging import get_logger
|
||||
|
||||
from ..proto import noteflow_pb2
|
||||
from ._types import GrpcContext
|
||||
|
||||
|
||||
class _Copyable(Protocol):
|
||||
"""Protocol for protobuf CopyFrom method."""
|
||||
|
||||
def CopyFrom(self, other: _Copyable) -> None: ...
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# Proto enum value maps
|
||||
_DEVICE_TO_PROTO: dict[AsrDevice, int] = {
|
||||
AsrDevice.CPU: noteflow_pb2.ASR_DEVICE_CPU,
|
||||
AsrDevice.CUDA: noteflow_pb2.ASR_DEVICE_CUDA,
|
||||
}
|
||||
|
||||
_PROTO_TO_DEVICE: dict[int, AsrDevice] = {
|
||||
noteflow_pb2.ASR_DEVICE_CPU: AsrDevice.CPU,
|
||||
noteflow_pb2.ASR_DEVICE_CUDA: AsrDevice.CUDA,
|
||||
}
|
||||
|
||||
_COMPUTE_TYPE_TO_PROTO: dict[AsrComputeType, int] = {
|
||||
AsrComputeType.INT8: noteflow_pb2.ASR_COMPUTE_TYPE_INT8,
|
||||
AsrComputeType.FLOAT16: noteflow_pb2.ASR_COMPUTE_TYPE_FLOAT16,
|
||||
AsrComputeType.FLOAT32: noteflow_pb2.ASR_COMPUTE_TYPE_FLOAT32,
|
||||
}
|
||||
|
||||
_PROTO_TO_COMPUTE_TYPE: dict[int, AsrComputeType] = {
|
||||
noteflow_pb2.ASR_COMPUTE_TYPE_INT8: AsrComputeType.INT8,
|
||||
noteflow_pb2.ASR_COMPUTE_TYPE_FLOAT16: AsrComputeType.FLOAT16,
|
||||
noteflow_pb2.ASR_COMPUTE_TYPE_FLOAT32: AsrComputeType.FLOAT32,
|
||||
}
|
||||
|
||||
_STATUS_TO_PROTO: dict[str, int] = {
|
||||
JOB_STATUS_QUEUED: noteflow_pb2.JOB_STATUS_QUEUED,
|
||||
JOB_STATUS_RUNNING: noteflow_pb2.JOB_STATUS_RUNNING,
|
||||
JOB_STATUS_COMPLETED: noteflow_pb2.JOB_STATUS_COMPLETED,
|
||||
JOB_STATUS_FAILED: noteflow_pb2.JOB_STATUS_FAILED,
|
||||
}
|
||||
|
||||
_PHASE_TO_STRING: dict[AsrConfigPhase, str] = {
|
||||
AsrConfigPhase.VALIDATING: "validating",
|
||||
AsrConfigPhase.DOWNLOADING: "downloading",
|
||||
AsrConfigPhase.LOADING: "loading",
|
||||
AsrConfigPhase.COMPLETED: "completed",
|
||||
AsrConfigPhase.FAILED: "failed",
|
||||
}
|
||||
|
||||
|
||||
def _build_configuration_proto(
|
||||
service: AsrConfigService,
|
||||
) -> noteflow_pb2.AsrConfiguration:
|
||||
"""Build ASR configuration proto message from service state.
|
||||
|
||||
Args:
|
||||
service: The ASR config service.
|
||||
|
||||
Returns:
|
||||
Populated AsrConfiguration proto message.
|
||||
"""
|
||||
caps = service.get_capabilities()
|
||||
|
||||
# Convert compute types to proto enum values
|
||||
available_compute_types = [
|
||||
_COMPUTE_TYPE_TO_PROTO[ct] for ct in caps.available_compute_types
|
||||
]
|
||||
|
||||
return noteflow_pb2.AsrConfiguration(
|
||||
model_size=caps.model_size or "",
|
||||
device=_DEVICE_TO_PROTO.get(caps.device, noteflow_pb2.ASR_DEVICE_UNSPECIFIED),
|
||||
compute_type=_COMPUTE_TYPE_TO_PROTO.get(
|
||||
caps.compute_type, noteflow_pb2.ASR_COMPUTE_TYPE_UNSPECIFIED
|
||||
),
|
||||
is_ready=caps.is_ready,
|
||||
cuda_available=caps.cuda_available,
|
||||
available_model_sizes=list(caps.available_model_sizes),
|
||||
available_compute_types=available_compute_types,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def _parse_update_request(
|
||||
request: noteflow_pb2.UpdateAsrConfigurationRequest,
|
||||
) -> tuple[str | None, AsrDevice | None, AsrComputeType | None]:
|
||||
"""Parse UpdateAsrConfiguration request parameters.
|
||||
|
||||
Args:
|
||||
request: The gRPC request.
|
||||
|
||||
Returns:
|
||||
Tuple of (model_size, device, compute_type) with None for unspecified fields.
|
||||
"""
|
||||
model_size = request.model_size if request.HasField("model_size") else None
|
||||
device = (
|
||||
_PROTO_TO_DEVICE.get(request.device)
|
||||
if request.HasField("device")
|
||||
and request.device != noteflow_pb2.ASR_DEVICE_UNSPECIFIED
|
||||
else None
|
||||
)
|
||||
compute_type = (
|
||||
_PROTO_TO_COMPUTE_TYPE.get(request.compute_type)
|
||||
if request.HasField("compute_type")
|
||||
and request.compute_type != noteflow_pb2.ASR_COMPUTE_TYPE_UNSPECIFIED
|
||||
else None
|
||||
)
|
||||
return model_size, device, compute_type
|
||||
|
||||
|
||||
def _create_job_error_response(
|
||||
job_id: str,
|
||||
error_message: str,
|
||||
) -> noteflow_pb2.AsrConfigurationJobStatus:
|
||||
"""Create error response for job status request.
|
||||
|
||||
Args:
|
||||
job_id: The requested job ID.
|
||||
error_message: The error message to include.
|
||||
|
||||
Returns:
|
||||
Error response proto message.
|
||||
"""
|
||||
return noteflow_pb2.AsrConfigurationJobStatus(
|
||||
job_id=job_id,
|
||||
status=noteflow_pb2.JOB_STATUS_FAILED,
|
||||
progress_percent=0.0,
|
||||
phase="failed",
|
||||
error_message=error_message,
|
||||
)
|
||||
|
||||
class AsrConfigMixin:
|
||||
"""Mixin providing ASR configuration management functionality.
|
||||
|
||||
Requires host to have:
|
||||
- asr_config_service: AsrConfigService | None
|
||||
- active_streams: set[str]
|
||||
"""
|
||||
|
||||
# Protocol requirements (declared for type checking)
|
||||
asr_config_service: AsrConfigService | None
|
||||
active_streams: set[str]
|
||||
|
||||
async def GetAsrConfiguration(
|
||||
self,
|
||||
request: noteflow_pb2.GetAsrConfigurationRequest,
|
||||
context: GrpcContext,
|
||||
) -> noteflow_pb2.GetAsrConfigurationResponse:
|
||||
"""Get current ASR configuration and available options.
|
||||
|
||||
Returns capabilities including available model sizes, compute types,
|
||||
and CUDA availability for the UI to display configuration options.
|
||||
"""
|
||||
if self.asr_config_service is None:
|
||||
logger.warning("asr_config_requested_but_no_service")
|
||||
# Return empty configuration if ASR is not available
|
||||
return noteflow_pb2.GetAsrConfigurationResponse(
|
||||
configuration=noteflow_pb2.AsrConfiguration(
|
||||
model_size="",
|
||||
device=noteflow_pb2.ASR_DEVICE_UNSPECIFIED,
|
||||
compute_type=noteflow_pb2.ASR_COMPUTE_TYPE_UNSPECIFIED,
|
||||
is_ready=False,
|
||||
cuda_available=False,
|
||||
available_model_sizes=[],
|
||||
available_compute_types=[],
|
||||
)
|
||||
)
|
||||
|
||||
config_proto = _build_configuration_proto(self.asr_config_service)
|
||||
return noteflow_pb2.GetAsrConfigurationResponse(configuration=config_proto)
|
||||
|
||||
async def UpdateAsrConfiguration(
|
||||
self,
|
||||
request: noteflow_pb2.UpdateAsrConfigurationRequest,
|
||||
context: GrpcContext,
|
||||
) -> noteflow_pb2.UpdateAsrConfigurationResponse:
|
||||
"""Update ASR configuration and start model reload.
|
||||
|
||||
Initiates a background job to reconfigure the ASR engine. The job
|
||||
handles model download (if needed) and reload. Returns immediately
|
||||
with a job ID for progress tracking.
|
||||
"""
|
||||
if self.asr_config_service is None:
|
||||
return noteflow_pb2.UpdateAsrConfigurationResponse(
|
||||
job_id="",
|
||||
status=noteflow_pb2.JOB_STATUS_FAILED,
|
||||
accepted=False,
|
||||
error_message="ASR service is not available",
|
||||
)
|
||||
|
||||
model_size, device, compute_type = _parse_update_request(request)
|
||||
has_active_recordings = len(self.active_streams) > 0
|
||||
|
||||
job_id, error = await self.asr_config_service.start_reconfiguration(
|
||||
model_size=model_size,
|
||||
device=device,
|
||||
compute_type=compute_type,
|
||||
has_active_recordings=has_active_recordings,
|
||||
)
|
||||
|
||||
if error:
|
||||
return noteflow_pb2.UpdateAsrConfigurationResponse(
|
||||
job_id="",
|
||||
status=noteflow_pb2.JOB_STATUS_FAILED,
|
||||
accepted=False,
|
||||
error_message=error,
|
||||
)
|
||||
|
||||
return noteflow_pb2.UpdateAsrConfigurationResponse(
|
||||
job_id=str(job_id),
|
||||
status=noteflow_pb2.JOB_STATUS_QUEUED,
|
||||
accepted=True,
|
||||
error_message="",
|
||||
)
|
||||
|
||||
async def GetAsrConfigurationJobStatus(
|
||||
self,
|
||||
request: noteflow_pb2.GetAsrConfigurationJobStatusRequest,
|
||||
context: GrpcContext,
|
||||
) -> noteflow_pb2.AsrConfigurationJobStatus:
|
||||
"""Get status of an ASR reconfiguration job.
|
||||
|
||||
Returns progress information for a running or completed job,
|
||||
including the new configuration once reload is complete.
|
||||
"""
|
||||
from uuid import UUID
|
||||
|
||||
if self.asr_config_service is None:
|
||||
return _create_job_error_response(request.job_id, "ASR service is not available")
|
||||
|
||||
try:
|
||||
job_id = UUID(request.job_id)
|
||||
except ValueError:
|
||||
return _create_job_error_response(request.job_id, "Invalid job ID format")
|
||||
|
||||
job = self.asr_config_service.get_job_status(job_id)
|
||||
if job is None:
|
||||
return noteflow_pb2.AsrConfigurationJobStatus(
|
||||
job_id=request.job_id,
|
||||
status=noteflow_pb2.JOB_STATUS_UNSPECIFIED,
|
||||
progress_percent=0.0,
|
||||
phase="",
|
||||
error_message="Job not found",
|
||||
)
|
||||
|
||||
# Build response
|
||||
status_proto = _STATUS_TO_PROTO.get(job.status, noteflow_pb2.JOB_STATUS_UNSPECIFIED)
|
||||
phase_str = _PHASE_TO_STRING.get(job.phase, "")
|
||||
|
||||
response = noteflow_pb2.AsrConfigurationJobStatus(
|
||||
job_id=request.job_id,
|
||||
status=status_proto,
|
||||
progress_percent=job.progress_percent,
|
||||
phase=phase_str,
|
||||
error_message=job.error_message,
|
||||
)
|
||||
|
||||
# Include new configuration if job completed successfully
|
||||
if job.status == JOB_STATUS_COMPLETED:
|
||||
config_field = cast(_Copyable, response.new_configuration)
|
||||
config_proto = _build_configuration_proto(self.asr_config_service)
|
||||
config_field.CopyFrom(cast(_Copyable, config_proto))
|
||||
|
||||
return response
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Protocol, cast
|
||||
|
||||
from noteflow.application.services.export_service import ExportFormat as ApplicationExportFormat
|
||||
from noteflow.domain.entities import (
|
||||
Annotation,
|
||||
Meeting,
|
||||
@@ -18,8 +17,7 @@ from noteflow.domain.entities import (
|
||||
Summary,
|
||||
WordTiming,
|
||||
)
|
||||
from noteflow.domain.value_objects import AnnotationType, MeetingId
|
||||
from noteflow.domain.value_objects import ExportFormat as DomainExportFormat
|
||||
from noteflow.domain.value_objects import AnnotationType, ExportFormat, MeetingId
|
||||
from noteflow.infrastructure.converters import AsrConverter
|
||||
|
||||
from ...proto import noteflow_pb2
|
||||
@@ -324,25 +322,23 @@ def create_segment_from_asr(
|
||||
)
|
||||
|
||||
|
||||
def proto_to_export_format(proto_format: int) -> ApplicationExportFormat:
|
||||
"""Convert protobuf ExportFormat to application ExportFormat."""
|
||||
def proto_to_export_format(proto_format: int) -> ExportFormat:
|
||||
"""Convert protobuf ExportFormat to domain ExportFormat."""
|
||||
if proto_format == noteflow_pb2.EXPORT_FORMAT_HTML:
|
||||
return ApplicationExportFormat.HTML
|
||||
return ExportFormat.HTML
|
||||
if proto_format == noteflow_pb2.EXPORT_FORMAT_PDF:
|
||||
return ApplicationExportFormat.PDF
|
||||
return ApplicationExportFormat.MARKDOWN # Default to Markdown
|
||||
return ExportFormat.PDF
|
||||
return ExportFormat.MARKDOWN # Default to Markdown
|
||||
|
||||
|
||||
def export_format_to_proto(fmt: DomainExportFormat | ApplicationExportFormat) -> int:
|
||||
"""Convert domain or application ExportFormat to proto enum."""
|
||||
# Both enums have the same values, so we can use either
|
||||
format_value = fmt.value if hasattr(fmt, "value") else str(fmt)
|
||||
def export_format_to_proto(fmt: ExportFormat) -> int:
|
||||
"""Convert domain ExportFormat to proto enum."""
|
||||
mapping: dict[str, int] = {
|
||||
"markdown": noteflow_pb2.EXPORT_FORMAT_MARKDOWN,
|
||||
"html": noteflow_pb2.EXPORT_FORMAT_HTML,
|
||||
"pdf": noteflow_pb2.EXPORT_FORMAT_PDF,
|
||||
}
|
||||
return mapping.get(format_value, noteflow_pb2.EXPORT_FORMAT_UNSPECIFIED)
|
||||
return mapping.get(fmt.value, noteflow_pb2.EXPORT_FORMAT_UNSPECIFIED)
|
||||
|
||||
|
||||
class _Copyable(Protocol):
|
||||
|
||||
134
src/noteflow/grpc/_mixins/hf_token.py
Normal file
134
src/noteflow/grpc/_mixins/hf_token.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""HuggingFace token management mixin for gRPC service.
|
||||
|
||||
Provides secure storage, validation, and retrieval of HuggingFace
|
||||
API tokens used for accessing gated models like pyannote.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from noteflow.infrastructure.logging import get_logger
|
||||
|
||||
from ..proto import noteflow_pb2
|
||||
from ._types import GrpcContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from noteflow.application.services.hf_token_service import HfTokenService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HfTokenMixin:
|
||||
"""Mixin providing HuggingFace token management functionality.
|
||||
|
||||
Requires host to have:
|
||||
- hf_token_service: HfTokenService | None
|
||||
"""
|
||||
|
||||
# Protocol requirements (declared for type checking)
|
||||
hf_token_service: HfTokenService | None
|
||||
|
||||
async def SetHuggingFaceToken(
|
||||
self,
|
||||
request: noteflow_pb2.SetHuggingFaceTokenRequest,
|
||||
context: GrpcContext,
|
||||
) -> noteflow_pb2.SetHuggingFaceTokenResponse:
|
||||
"""Set and optionally validate a HuggingFace API token.
|
||||
|
||||
The token is encrypted before storage. If validation is requested,
|
||||
the token is verified against the HuggingFace API before saving.
|
||||
"""
|
||||
if self.hf_token_service is None:
|
||||
return noteflow_pb2.SetHuggingFaceTokenResponse(
|
||||
success=False,
|
||||
validation_error="HuggingFace token service is not available",
|
||||
)
|
||||
|
||||
if not request.token:
|
||||
return noteflow_pb2.SetHuggingFaceTokenResponse(
|
||||
success=False,
|
||||
validation_error="Token cannot be empty",
|
||||
)
|
||||
|
||||
success, validation_result = await self.hf_token_service.set_token(
|
||||
token=request.token,
|
||||
validate=request.validate,
|
||||
)
|
||||
|
||||
response = noteflow_pb2.SetHuggingFaceTokenResponse(success=success)
|
||||
|
||||
if validation_result is not None:
|
||||
response.valid = validation_result.valid
|
||||
response.username = validation_result.username
|
||||
if not validation_result.valid:
|
||||
response.validation_error = validation_result.error_message
|
||||
|
||||
return response
|
||||
|
||||
async def GetHuggingFaceTokenStatus(
|
||||
self,
|
||||
request: noteflow_pb2.GetHuggingFaceTokenStatusRequest,
|
||||
context: GrpcContext,
|
||||
) -> noteflow_pb2.GetHuggingFaceTokenStatusResponse:
|
||||
"""Get the current status of the HuggingFace token.
|
||||
|
||||
Returns whether a token is configured and validated, along with
|
||||
the associated username and validation timestamp.
|
||||
"""
|
||||
if self.hf_token_service is None:
|
||||
return noteflow_pb2.GetHuggingFaceTokenStatusResponse(
|
||||
is_configured=False,
|
||||
is_validated=False,
|
||||
username="",
|
||||
validated_at=0.0,
|
||||
)
|
||||
|
||||
status = await self.hf_token_service.get_status()
|
||||
|
||||
return noteflow_pb2.GetHuggingFaceTokenStatusResponse(
|
||||
is_configured=status.is_configured,
|
||||
is_validated=status.is_validated,
|
||||
username=status.username,
|
||||
validated_at=status.validated_at or 0.0,
|
||||
)
|
||||
|
||||
async def DeleteHuggingFaceToken(
|
||||
self,
|
||||
request: noteflow_pb2.DeleteHuggingFaceTokenRequest,
|
||||
context: GrpcContext,
|
||||
) -> noteflow_pb2.DeleteHuggingFaceTokenResponse:
|
||||
"""Delete the stored HuggingFace token.
|
||||
|
||||
Removes both the encrypted token and its metadata from storage.
|
||||
"""
|
||||
if self.hf_token_service is None:
|
||||
return noteflow_pb2.DeleteHuggingFaceTokenResponse(success=False)
|
||||
|
||||
success = await self.hf_token_service.delete_token()
|
||||
return noteflow_pb2.DeleteHuggingFaceTokenResponse(success=success)
|
||||
|
||||
async def ValidateHuggingFaceToken(
|
||||
self,
|
||||
request: noteflow_pb2.ValidateHuggingFaceTokenRequest,
|
||||
context: GrpcContext,
|
||||
) -> noteflow_pb2.ValidateHuggingFaceTokenResponse:
|
||||
"""Validate the currently stored HuggingFace token.
|
||||
|
||||
Tests the stored token against the HuggingFace API and updates
|
||||
the validation status in storage.
|
||||
"""
|
||||
if self.hf_token_service is None:
|
||||
return noteflow_pb2.ValidateHuggingFaceTokenResponse(
|
||||
valid=False,
|
||||
username="",
|
||||
error_message="HuggingFace token service is not available",
|
||||
)
|
||||
|
||||
result = await self.hf_token_service.validate_stored_token()
|
||||
|
||||
return noteflow_pb2.ValidateHuggingFaceTokenResponse(
|
||||
valid=result.valid,
|
||||
username=result.username,
|
||||
error_message=result.error_message,
|
||||
)
|
||||
@@ -52,6 +52,11 @@ service NoteFlowService {
|
||||
// Server health and capabilities
|
||||
rpc GetServerInfo(ServerInfoRequest) returns (ServerInfo);
|
||||
|
||||
// ASR Configuration Management (Sprint 19)
|
||||
rpc GetAsrConfiguration(GetAsrConfigurationRequest) returns (GetAsrConfigurationResponse);
|
||||
rpc UpdateAsrConfiguration(UpdateAsrConfigurationRequest) returns (UpdateAsrConfigurationResponse);
|
||||
rpc GetAsrConfigurationJobStatus(GetAsrConfigurationJobStatusRequest) returns (AsrConfigurationJobStatus);
|
||||
|
||||
// Named entity extraction (Sprint 4) + mutations (Sprint 8)
|
||||
rpc ExtractEntities(ExtractEntitiesRequest) returns (ExtractEntitiesResponse);
|
||||
rpc UpdateEntity(UpdateEntityRequest) returns (UpdateEntityResponse);
|
||||
@@ -79,6 +84,12 @@ service NoteFlowService {
|
||||
rpc RevokeCloudConsent(RevokeCloudConsentRequest) returns (RevokeCloudConsentResponse);
|
||||
rpc GetCloudConsentStatus(GetCloudConsentStatusRequest) returns (GetCloudConsentStatusResponse);
|
||||
|
||||
// HuggingFace Token Management (Sprint 19)
|
||||
rpc SetHuggingFaceToken(SetHuggingFaceTokenRequest) returns (SetHuggingFaceTokenResponse);
|
||||
rpc GetHuggingFaceTokenStatus(GetHuggingFaceTokenStatusRequest) returns (GetHuggingFaceTokenStatusResponse);
|
||||
rpc DeleteHuggingFaceToken(DeleteHuggingFaceTokenRequest) returns (DeleteHuggingFaceTokenResponse);
|
||||
rpc ValidateHuggingFaceToken(ValidateHuggingFaceTokenRequest) returns (ValidateHuggingFaceTokenResponse);
|
||||
|
||||
// User preferences sync (Sprint 14)
|
||||
rpc GetPreferences(GetPreferencesRequest) returns (GetPreferencesResponse);
|
||||
rpc SetPreferences(SetPreferencesRequest) returns (SetPreferencesResponse);
|
||||
@@ -613,6 +624,103 @@ message ServerInfo {
|
||||
int64 state_version = 10;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ASR Configuration Messages (Sprint 19)
|
||||
// =============================================================================
|
||||
|
||||
// Valid ASR devices
|
||||
enum AsrDevice {
|
||||
ASR_DEVICE_UNSPECIFIED = 0;
|
||||
ASR_DEVICE_CPU = 1;
|
||||
ASR_DEVICE_CUDA = 2;
|
||||
}
|
||||
|
||||
// Valid ASR compute types
|
||||
enum AsrComputeType {
|
||||
ASR_COMPUTE_TYPE_UNSPECIFIED = 0;
|
||||
ASR_COMPUTE_TYPE_INT8 = 1;
|
||||
ASR_COMPUTE_TYPE_FLOAT16 = 2;
|
||||
ASR_COMPUTE_TYPE_FLOAT32 = 3;
|
||||
}
|
||||
|
||||
// Current ASR configuration and capabilities
|
||||
message AsrConfiguration {
|
||||
// Currently loaded model size (e.g., "base", "small", "medium")
|
||||
string model_size = 1;
|
||||
|
||||
// Current device in use
|
||||
AsrDevice device = 2;
|
||||
|
||||
// Current compute type
|
||||
AsrComputeType compute_type = 3;
|
||||
|
||||
// Whether ASR engine is ready for transcription
|
||||
bool is_ready = 4;
|
||||
|
||||
// Whether CUDA is available on this server
|
||||
bool cuda_available = 5;
|
||||
|
||||
// Available model sizes that can be loaded
|
||||
repeated string available_model_sizes = 6;
|
||||
|
||||
// Available compute types for current device
|
||||
repeated AsrComputeType available_compute_types = 7;
|
||||
}
|
||||
|
||||
message GetAsrConfigurationRequest {}
|
||||
|
||||
message GetAsrConfigurationResponse {
|
||||
AsrConfiguration configuration = 1;
|
||||
}
|
||||
|
||||
message UpdateAsrConfigurationRequest {
|
||||
// New model size to load (optional, keeps current if empty)
|
||||
optional string model_size = 1;
|
||||
|
||||
// New device (optional, keeps current if unspecified)
|
||||
optional AsrDevice device = 2;
|
||||
|
||||
// New compute type (optional, keeps current if unspecified)
|
||||
optional AsrComputeType compute_type = 3;
|
||||
}
|
||||
|
||||
message UpdateAsrConfigurationResponse {
|
||||
// Background job identifier for tracking reload progress
|
||||
string job_id = 1;
|
||||
|
||||
// Initial status (always QUEUED or RUNNING)
|
||||
JobStatus status = 2;
|
||||
|
||||
// Error message if validation failed before job creation
|
||||
string error_message = 3;
|
||||
|
||||
// Whether the request was accepted (false if active recording)
|
||||
bool accepted = 4;
|
||||
}
|
||||
|
||||
message GetAsrConfigurationJobStatusRequest {
|
||||
string job_id = 1;
|
||||
}
|
||||
|
||||
message AsrConfigurationJobStatus {
|
||||
string job_id = 1;
|
||||
|
||||
// Current status
|
||||
JobStatus status = 2;
|
||||
|
||||
// Progress percentage (0.0-100.0), primarily for model download
|
||||
float progress_percent = 3;
|
||||
|
||||
// Current phase: "validating", "downloading", "loading", "completed"
|
||||
string phase = 4;
|
||||
|
||||
// Error message if failed
|
||||
string error_message = 5;
|
||||
|
||||
// New configuration after successful reload
|
||||
optional AsrConfiguration new_configuration = 6;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Annotation Messages
|
||||
// =============================================================================
|
||||
@@ -1309,6 +1417,62 @@ message GetCloudConsentStatusResponse {
|
||||
bool consent_granted = 1;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HuggingFace Token Management Messages (Sprint 19)
|
||||
// =============================================================================
|
||||
|
||||
message SetHuggingFaceTokenRequest {
|
||||
// HuggingFace access token (will be encrypted at rest)
|
||||
string token = 1;
|
||||
|
||||
// Whether to validate token against HuggingFace API
|
||||
bool validate = 2;
|
||||
}
|
||||
|
||||
message SetHuggingFaceTokenResponse {
|
||||
// Whether the token was saved successfully
|
||||
bool success = 1;
|
||||
|
||||
// Whether the token passed validation (if validate=true)
|
||||
optional bool valid = 2;
|
||||
|
||||
// Validation error message if valid=false
|
||||
string validation_error = 3;
|
||||
|
||||
// HuggingFace username associated with token (if validate=true and valid)
|
||||
string username = 4;
|
||||
}
|
||||
|
||||
message GetHuggingFaceTokenStatusRequest {}
|
||||
|
||||
message GetHuggingFaceTokenStatusResponse {
|
||||
// Whether a token is configured
|
||||
bool is_configured = 1;
|
||||
|
||||
// Whether the token has been validated
|
||||
bool is_validated = 2;
|
||||
|
||||
// HuggingFace username (if validated)
|
||||
string username = 3;
|
||||
|
||||
// Last validation timestamp (Unix epoch seconds)
|
||||
double validated_at = 4;
|
||||
}
|
||||
|
||||
message DeleteHuggingFaceTokenRequest {}
|
||||
|
||||
message DeleteHuggingFaceTokenResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message ValidateHuggingFaceTokenRequest {}
|
||||
|
||||
message ValidateHuggingFaceTokenResponse {
|
||||
bool valid = 1;
|
||||
string username = 2;
|
||||
string error_message = 3;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// User Preferences Sync Messages (Sprint 14)
|
||||
// =============================================================================
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -168,6 +168,21 @@ class NoteFlowServiceStub(object):
|
||||
request_serializer=noteflow__pb2.ServerInfoRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.ServerInfo.FromString,
|
||||
_registered_method=True)
|
||||
self.GetAsrConfiguration = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/GetAsrConfiguration',
|
||||
request_serializer=noteflow__pb2.GetAsrConfigurationRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.GetAsrConfigurationResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.UpdateAsrConfiguration = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/UpdateAsrConfiguration',
|
||||
request_serializer=noteflow__pb2.UpdateAsrConfigurationRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.UpdateAsrConfigurationResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.GetAsrConfigurationJobStatus = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/GetAsrConfigurationJobStatus',
|
||||
request_serializer=noteflow__pb2.GetAsrConfigurationJobStatusRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.AsrConfigurationJobStatus.FromString,
|
||||
_registered_method=True)
|
||||
self.ExtractEntities = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/ExtractEntities',
|
||||
request_serializer=noteflow__pb2.ExtractEntitiesRequest.SerializeToString,
|
||||
@@ -253,6 +268,26 @@ class NoteFlowServiceStub(object):
|
||||
request_serializer=noteflow__pb2.GetCloudConsentStatusRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.GetCloudConsentStatusResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.SetHuggingFaceToken = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/SetHuggingFaceToken',
|
||||
request_serializer=noteflow__pb2.SetHuggingFaceTokenRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.SetHuggingFaceTokenResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.GetHuggingFaceTokenStatus = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/GetHuggingFaceTokenStatus',
|
||||
request_serializer=noteflow__pb2.GetHuggingFaceTokenStatusRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.GetHuggingFaceTokenStatusResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.DeleteHuggingFaceToken = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/DeleteHuggingFaceToken',
|
||||
request_serializer=noteflow__pb2.DeleteHuggingFaceTokenRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.DeleteHuggingFaceTokenResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.ValidateHuggingFaceToken = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/ValidateHuggingFaceToken',
|
||||
request_serializer=noteflow__pb2.ValidateHuggingFaceTokenRequest.SerializeToString,
|
||||
response_deserializer=noteflow__pb2.ValidateHuggingFaceTokenResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.GetPreferences = channel.unary_unary(
|
||||
'/noteflow.NoteFlowService/GetPreferences',
|
||||
request_serializer=noteflow__pb2.GetPreferencesRequest.SerializeToString,
|
||||
@@ -596,6 +631,25 @@ class NoteFlowServiceServicer(object):
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def GetAsrConfiguration(self, request, context):
|
||||
"""ASR Configuration Management (Sprint 19)
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def UpdateAsrConfiguration(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def GetAsrConfigurationJobStatus(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def ExtractEntities(self, request, context):
|
||||
"""Named entity extraction (Sprint 4) + mutations (Sprint 8)
|
||||
"""
|
||||
@@ -703,6 +757,31 @@ class NoteFlowServiceServicer(object):
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def SetHuggingFaceToken(self, request, context):
|
||||
"""HuggingFace Token Management (Sprint 19)
|
||||
"""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def GetHuggingFaceTokenStatus(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def DeleteHuggingFaceToken(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def ValidateHuggingFaceToken(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def GetPreferences(self, request, context):
|
||||
"""User preferences sync (Sprint 14)
|
||||
"""
|
||||
@@ -1049,6 +1128,21 @@ def add_NoteFlowServiceServicer_to_server(servicer, server):
|
||||
request_deserializer=noteflow__pb2.ServerInfoRequest.FromString,
|
||||
response_serializer=noteflow__pb2.ServerInfo.SerializeToString,
|
||||
),
|
||||
'GetAsrConfiguration': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetAsrConfiguration,
|
||||
request_deserializer=noteflow__pb2.GetAsrConfigurationRequest.FromString,
|
||||
response_serializer=noteflow__pb2.GetAsrConfigurationResponse.SerializeToString,
|
||||
),
|
||||
'UpdateAsrConfiguration': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.UpdateAsrConfiguration,
|
||||
request_deserializer=noteflow__pb2.UpdateAsrConfigurationRequest.FromString,
|
||||
response_serializer=noteflow__pb2.UpdateAsrConfigurationResponse.SerializeToString,
|
||||
),
|
||||
'GetAsrConfigurationJobStatus': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetAsrConfigurationJobStatus,
|
||||
request_deserializer=noteflow__pb2.GetAsrConfigurationJobStatusRequest.FromString,
|
||||
response_serializer=noteflow__pb2.AsrConfigurationJobStatus.SerializeToString,
|
||||
),
|
||||
'ExtractEntities': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.ExtractEntities,
|
||||
request_deserializer=noteflow__pb2.ExtractEntitiesRequest.FromString,
|
||||
@@ -1134,6 +1228,26 @@ def add_NoteFlowServiceServicer_to_server(servicer, server):
|
||||
request_deserializer=noteflow__pb2.GetCloudConsentStatusRequest.FromString,
|
||||
response_serializer=noteflow__pb2.GetCloudConsentStatusResponse.SerializeToString,
|
||||
),
|
||||
'SetHuggingFaceToken': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.SetHuggingFaceToken,
|
||||
request_deserializer=noteflow__pb2.SetHuggingFaceTokenRequest.FromString,
|
||||
response_serializer=noteflow__pb2.SetHuggingFaceTokenResponse.SerializeToString,
|
||||
),
|
||||
'GetHuggingFaceTokenStatus': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetHuggingFaceTokenStatus,
|
||||
request_deserializer=noteflow__pb2.GetHuggingFaceTokenStatusRequest.FromString,
|
||||
response_serializer=noteflow__pb2.GetHuggingFaceTokenStatusResponse.SerializeToString,
|
||||
),
|
||||
'DeleteHuggingFaceToken': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.DeleteHuggingFaceToken,
|
||||
request_deserializer=noteflow__pb2.DeleteHuggingFaceTokenRequest.FromString,
|
||||
response_serializer=noteflow__pb2.DeleteHuggingFaceTokenResponse.SerializeToString,
|
||||
),
|
||||
'ValidateHuggingFaceToken': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.ValidateHuggingFaceToken,
|
||||
request_deserializer=noteflow__pb2.ValidateHuggingFaceTokenRequest.FromString,
|
||||
response_serializer=noteflow__pb2.ValidateHuggingFaceTokenResponse.SerializeToString,
|
||||
),
|
||||
'GetPreferences': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetPreferences,
|
||||
request_deserializer=noteflow__pb2.GetPreferencesRequest.FromString,
|
||||
@@ -2021,6 +2135,87 @@ class NoteFlowService(object):
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def GetAsrConfiguration(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/noteflow.NoteFlowService/GetAsrConfiguration',
|
||||
noteflow__pb2.GetAsrConfigurationRequest.SerializeToString,
|
||||
noteflow__pb2.GetAsrConfigurationResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def UpdateAsrConfiguration(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/noteflow.NoteFlowService/UpdateAsrConfiguration',
|
||||
noteflow__pb2.UpdateAsrConfigurationRequest.SerializeToString,
|
||||
noteflow__pb2.UpdateAsrConfigurationResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def GetAsrConfigurationJobStatus(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/noteflow.NoteFlowService/GetAsrConfigurationJobStatus',
|
||||
noteflow__pb2.GetAsrConfigurationJobStatusRequest.SerializeToString,
|
||||
noteflow__pb2.AsrConfigurationJobStatus.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def ExtractEntities(request,
|
||||
target,
|
||||
@@ -2480,6 +2675,114 @@ class NoteFlowService(object):
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def SetHuggingFaceToken(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/noteflow.NoteFlowService/SetHuggingFaceToken',
|
||||
noteflow__pb2.SetHuggingFaceTokenRequest.SerializeToString,
|
||||
noteflow__pb2.SetHuggingFaceTokenResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def GetHuggingFaceTokenStatus(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/noteflow.NoteFlowService/GetHuggingFaceTokenStatus',
|
||||
noteflow__pb2.GetHuggingFaceTokenStatusRequest.SerializeToString,
|
||||
noteflow__pb2.GetHuggingFaceTokenStatusResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def DeleteHuggingFaceToken(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/noteflow.NoteFlowService/DeleteHuggingFaceToken',
|
||||
noteflow__pb2.DeleteHuggingFaceTokenRequest.SerializeToString,
|
||||
noteflow__pb2.DeleteHuggingFaceTokenResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def ValidateHuggingFaceToken(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/noteflow.NoteFlowService/ValidateHuggingFaceToken',
|
||||
noteflow__pb2.ValidateHuggingFaceTokenRequest.SerializeToString,
|
||||
noteflow__pb2.ValidateHuggingFaceTokenResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def GetPreferences(request,
|
||||
target,
|
||||
|
||||
@@ -7,12 +7,10 @@ Provides real-time ASR streaming and meeting management
|
||||
|
||||
import abc
|
||||
import collections.abc
|
||||
import typing
|
||||
|
||||
import grpc
|
||||
import grpc.aio
|
||||
|
||||
import noteflow_pb2
|
||||
import typing
|
||||
|
||||
_T = typing.TypeVar("_T")
|
||||
|
||||
@@ -44,6 +42,14 @@ class NoteFlowServiceStub:
|
||||
DeleteMeeting: grpc.UnaryUnaryMultiCallable[noteflow_pb2.DeleteMeetingRequest, noteflow_pb2.DeleteMeetingResponse]
|
||||
GenerateSummary: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GenerateSummaryRequest, noteflow_pb2.Summary]
|
||||
"""Summary generation"""
|
||||
ListSummarizationTemplates: grpc.UnaryUnaryMultiCallable[noteflow_pb2.ListSummarizationTemplatesRequest, noteflow_pb2.ListSummarizationTemplatesResponse]
|
||||
"""Summarization templates"""
|
||||
GetSummarizationTemplate: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetSummarizationTemplateRequest, noteflow_pb2.GetSummarizationTemplateResponse]
|
||||
CreateSummarizationTemplate: grpc.UnaryUnaryMultiCallable[noteflow_pb2.CreateSummarizationTemplateRequest, noteflow_pb2.SummarizationTemplateMutationResponse]
|
||||
UpdateSummarizationTemplate: grpc.UnaryUnaryMultiCallable[noteflow_pb2.UpdateSummarizationTemplateRequest, noteflow_pb2.SummarizationTemplateMutationResponse]
|
||||
ArchiveSummarizationTemplate: grpc.UnaryUnaryMultiCallable[noteflow_pb2.ArchiveSummarizationTemplateRequest, noteflow_pb2.SummarizationTemplateProto]
|
||||
ListSummarizationTemplateVersions: grpc.UnaryUnaryMultiCallable[noteflow_pb2.ListSummarizationTemplateVersionsRequest, noteflow_pb2.ListSummarizationTemplateVersionsResponse]
|
||||
RestoreSummarizationTemplateVersion: grpc.UnaryUnaryMultiCallable[noteflow_pb2.RestoreSummarizationTemplateVersionRequest, noteflow_pb2.SummarizationTemplateProto]
|
||||
AddAnnotation: grpc.UnaryUnaryMultiCallable[noteflow_pb2.AddAnnotationRequest, noteflow_pb2.Annotation]
|
||||
"""Annotation management"""
|
||||
GetAnnotation: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetAnnotationRequest, noteflow_pb2.Annotation]
|
||||
@@ -60,6 +66,10 @@ class NoteFlowServiceStub:
|
||||
GetActiveDiarizationJobs: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetActiveDiarizationJobsRequest, noteflow_pb2.GetActiveDiarizationJobsResponse]
|
||||
GetServerInfo: grpc.UnaryUnaryMultiCallable[noteflow_pb2.ServerInfoRequest, noteflow_pb2.ServerInfo]
|
||||
"""Server health and capabilities"""
|
||||
GetAsrConfiguration: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetAsrConfigurationRequest, noteflow_pb2.GetAsrConfigurationResponse]
|
||||
"""ASR Configuration Management (Sprint 19)"""
|
||||
UpdateAsrConfiguration: grpc.UnaryUnaryMultiCallable[noteflow_pb2.UpdateAsrConfigurationRequest, noteflow_pb2.UpdateAsrConfigurationResponse]
|
||||
GetAsrConfigurationJobStatus: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetAsrConfigurationJobStatusRequest, noteflow_pb2.AsrConfigurationJobStatus]
|
||||
ExtractEntities: grpc.UnaryUnaryMultiCallable[noteflow_pb2.ExtractEntitiesRequest, noteflow_pb2.ExtractEntitiesResponse]
|
||||
"""Named entity extraction (Sprint 4) + mutations (Sprint 8)"""
|
||||
UpdateEntity: grpc.UnaryUnaryMultiCallable[noteflow_pb2.UpdateEntityRequest, noteflow_pb2.UpdateEntityResponse]
|
||||
@@ -82,6 +92,11 @@ class NoteFlowServiceStub:
|
||||
"""Cloud consent management (Sprint 7)"""
|
||||
RevokeCloudConsent: grpc.UnaryUnaryMultiCallable[noteflow_pb2.RevokeCloudConsentRequest, noteflow_pb2.RevokeCloudConsentResponse]
|
||||
GetCloudConsentStatus: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetCloudConsentStatusRequest, noteflow_pb2.GetCloudConsentStatusResponse]
|
||||
SetHuggingFaceToken: grpc.UnaryUnaryMultiCallable[noteflow_pb2.SetHuggingFaceTokenRequest, noteflow_pb2.SetHuggingFaceTokenResponse]
|
||||
"""HuggingFace Token Management (Sprint 19)"""
|
||||
GetHuggingFaceTokenStatus: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetHuggingFaceTokenStatusRequest, noteflow_pb2.GetHuggingFaceTokenStatusResponse]
|
||||
DeleteHuggingFaceToken: grpc.UnaryUnaryMultiCallable[noteflow_pb2.DeleteHuggingFaceTokenRequest, noteflow_pb2.DeleteHuggingFaceTokenResponse]
|
||||
ValidateHuggingFaceToken: grpc.UnaryUnaryMultiCallable[noteflow_pb2.ValidateHuggingFaceTokenRequest, noteflow_pb2.ValidateHuggingFaceTokenResponse]
|
||||
GetPreferences: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetPreferencesRequest, noteflow_pb2.GetPreferencesResponse]
|
||||
"""User preferences sync (Sprint 14)"""
|
||||
SetPreferences: grpc.UnaryUnaryMultiCallable[noteflow_pb2.SetPreferencesRequest, noteflow_pb2.SetPreferencesResponse]
|
||||
@@ -119,6 +134,12 @@ class NoteFlowServiceStub:
|
||||
UpdateProjectMemberRole: grpc.UnaryUnaryMultiCallable[noteflow_pb2.UpdateProjectMemberRoleRequest, noteflow_pb2.ProjectMembershipProto]
|
||||
RemoveProjectMember: grpc.UnaryUnaryMultiCallable[noteflow_pb2.RemoveProjectMemberRequest, noteflow_pb2.RemoveProjectMemberResponse]
|
||||
ListProjectMembers: grpc.UnaryUnaryMultiCallable[noteflow_pb2.ListProjectMembersRequest, noteflow_pb2.ListProjectMembersResponse]
|
||||
GetCurrentUser: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetCurrentUserRequest, noteflow_pb2.GetCurrentUserResponse]
|
||||
"""Identity management (Sprint 16+)"""
|
||||
ListWorkspaces: grpc.UnaryUnaryMultiCallable[noteflow_pb2.ListWorkspacesRequest, noteflow_pb2.ListWorkspacesResponse]
|
||||
SwitchWorkspace: grpc.UnaryUnaryMultiCallable[noteflow_pb2.SwitchWorkspaceRequest, noteflow_pb2.SwitchWorkspaceResponse]
|
||||
GetWorkspaceSettings: grpc.UnaryUnaryMultiCallable[noteflow_pb2.GetWorkspaceSettingsRequest, noteflow_pb2.WorkspaceSettingsProto]
|
||||
UpdateWorkspaceSettings: grpc.UnaryUnaryMultiCallable[noteflow_pb2.UpdateWorkspaceSettingsRequest, noteflow_pb2.WorkspaceSettingsProto]
|
||||
|
||||
@typing.type_check_only
|
||||
class NoteFlowServiceAsyncStub(NoteFlowServiceStub):
|
||||
@@ -138,6 +159,14 @@ class NoteFlowServiceAsyncStub(NoteFlowServiceStub):
|
||||
DeleteMeeting: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.DeleteMeetingRequest, noteflow_pb2.DeleteMeetingResponse] # type: ignore[assignment]
|
||||
GenerateSummary: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GenerateSummaryRequest, noteflow_pb2.Summary] # type: ignore[assignment]
|
||||
"""Summary generation"""
|
||||
ListSummarizationTemplates: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.ListSummarizationTemplatesRequest, noteflow_pb2.ListSummarizationTemplatesResponse] # type: ignore[assignment]
|
||||
"""Summarization templates"""
|
||||
GetSummarizationTemplate: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetSummarizationTemplateRequest, noteflow_pb2.GetSummarizationTemplateResponse] # type: ignore[assignment]
|
||||
CreateSummarizationTemplate: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.CreateSummarizationTemplateRequest, noteflow_pb2.SummarizationTemplateMutationResponse] # type: ignore[assignment]
|
||||
UpdateSummarizationTemplate: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.UpdateSummarizationTemplateRequest, noteflow_pb2.SummarizationTemplateMutationResponse] # type: ignore[assignment]
|
||||
ArchiveSummarizationTemplate: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.ArchiveSummarizationTemplateRequest, noteflow_pb2.SummarizationTemplateProto] # type: ignore[assignment]
|
||||
ListSummarizationTemplateVersions: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.ListSummarizationTemplateVersionsRequest, noteflow_pb2.ListSummarizationTemplateVersionsResponse] # type: ignore[assignment]
|
||||
RestoreSummarizationTemplateVersion: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.RestoreSummarizationTemplateVersionRequest, noteflow_pb2.SummarizationTemplateProto] # type: ignore[assignment]
|
||||
AddAnnotation: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.AddAnnotationRequest, noteflow_pb2.Annotation] # type: ignore[assignment]
|
||||
"""Annotation management"""
|
||||
GetAnnotation: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetAnnotationRequest, noteflow_pb2.Annotation] # type: ignore[assignment]
|
||||
@@ -154,6 +183,10 @@ class NoteFlowServiceAsyncStub(NoteFlowServiceStub):
|
||||
GetActiveDiarizationJobs: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetActiveDiarizationJobsRequest, noteflow_pb2.GetActiveDiarizationJobsResponse] # type: ignore[assignment]
|
||||
GetServerInfo: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.ServerInfoRequest, noteflow_pb2.ServerInfo] # type: ignore[assignment]
|
||||
"""Server health and capabilities"""
|
||||
GetAsrConfiguration: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetAsrConfigurationRequest, noteflow_pb2.GetAsrConfigurationResponse] # type: ignore[assignment]
|
||||
"""ASR Configuration Management (Sprint 19)"""
|
||||
UpdateAsrConfiguration: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.UpdateAsrConfigurationRequest, noteflow_pb2.UpdateAsrConfigurationResponse] # type: ignore[assignment]
|
||||
GetAsrConfigurationJobStatus: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetAsrConfigurationJobStatusRequest, noteflow_pb2.AsrConfigurationJobStatus] # type: ignore[assignment]
|
||||
ExtractEntities: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.ExtractEntitiesRequest, noteflow_pb2.ExtractEntitiesResponse] # type: ignore[assignment]
|
||||
"""Named entity extraction (Sprint 4) + mutations (Sprint 8)"""
|
||||
UpdateEntity: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.UpdateEntityRequest, noteflow_pb2.UpdateEntityResponse] # type: ignore[assignment]
|
||||
@@ -176,6 +209,11 @@ class NoteFlowServiceAsyncStub(NoteFlowServiceStub):
|
||||
"""Cloud consent management (Sprint 7)"""
|
||||
RevokeCloudConsent: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.RevokeCloudConsentRequest, noteflow_pb2.RevokeCloudConsentResponse] # type: ignore[assignment]
|
||||
GetCloudConsentStatus: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetCloudConsentStatusRequest, noteflow_pb2.GetCloudConsentStatusResponse] # type: ignore[assignment]
|
||||
SetHuggingFaceToken: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.SetHuggingFaceTokenRequest, noteflow_pb2.SetHuggingFaceTokenResponse] # type: ignore[assignment]
|
||||
"""HuggingFace Token Management (Sprint 19)"""
|
||||
GetHuggingFaceTokenStatus: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetHuggingFaceTokenStatusRequest, noteflow_pb2.GetHuggingFaceTokenStatusResponse] # type: ignore[assignment]
|
||||
DeleteHuggingFaceToken: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.DeleteHuggingFaceTokenRequest, noteflow_pb2.DeleteHuggingFaceTokenResponse] # type: ignore[assignment]
|
||||
ValidateHuggingFaceToken: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.ValidateHuggingFaceTokenRequest, noteflow_pb2.ValidateHuggingFaceTokenResponse] # type: ignore[assignment]
|
||||
GetPreferences: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetPreferencesRequest, noteflow_pb2.GetPreferencesResponse] # type: ignore[assignment]
|
||||
"""User preferences sync (Sprint 14)"""
|
||||
SetPreferences: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.SetPreferencesRequest, noteflow_pb2.SetPreferencesResponse] # type: ignore[assignment]
|
||||
@@ -213,6 +251,12 @@ class NoteFlowServiceAsyncStub(NoteFlowServiceStub):
|
||||
UpdateProjectMemberRole: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.UpdateProjectMemberRoleRequest, noteflow_pb2.ProjectMembershipProto] # type: ignore[assignment]
|
||||
RemoveProjectMember: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.RemoveProjectMemberRequest, noteflow_pb2.RemoveProjectMemberResponse] # type: ignore[assignment]
|
||||
ListProjectMembers: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.ListProjectMembersRequest, noteflow_pb2.ListProjectMembersResponse] # type: ignore[assignment]
|
||||
GetCurrentUser: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetCurrentUserRequest, noteflow_pb2.GetCurrentUserResponse] # type: ignore[assignment]
|
||||
"""Identity management (Sprint 16+)"""
|
||||
ListWorkspaces: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.ListWorkspacesRequest, noteflow_pb2.ListWorkspacesResponse] # type: ignore[assignment]
|
||||
SwitchWorkspace: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.SwitchWorkspaceRequest, noteflow_pb2.SwitchWorkspaceResponse] # type: ignore[assignment]
|
||||
GetWorkspaceSettings: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.GetWorkspaceSettingsRequest, noteflow_pb2.WorkspaceSettingsProto] # type: ignore[assignment]
|
||||
UpdateWorkspaceSettings: grpc.aio.UnaryUnaryMultiCallable[noteflow_pb2.UpdateWorkspaceSettingsRequest, noteflow_pb2.WorkspaceSettingsProto] # type: ignore[assignment]
|
||||
|
||||
class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
"""=============================================================================
|
||||
@@ -225,7 +269,7 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request_iterator: _MaybeAsyncIterator[noteflow_pb2.AudioChunk],
|
||||
context: _ServicerContext,
|
||||
) -> collections.abc.Iterator[noteflow_pb2.TranscriptUpdate] | collections.abc.AsyncIterator[noteflow_pb2.TranscriptUpdate]:
|
||||
) -> typing.Union[collections.abc.Iterator[noteflow_pb2.TranscriptUpdate], collections.abc.AsyncIterator[noteflow_pb2.TranscriptUpdate]]:
|
||||
"""Bidirectional streaming: client sends audio chunks, server returns transcripts"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -233,7 +277,7 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.CreateMeetingRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.Meeting | collections.abc.Awaitable[noteflow_pb2.Meeting]:
|
||||
) -> typing.Union[noteflow_pb2.Meeting, collections.abc.Awaitable[noteflow_pb2.Meeting]]:
|
||||
"""Meeting lifecycle management"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -241,43 +285,93 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.StopMeetingRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.Meeting | collections.abc.Awaitable[noteflow_pb2.Meeting]: ...
|
||||
) -> typing.Union[noteflow_pb2.Meeting, collections.abc.Awaitable[noteflow_pb2.Meeting]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListMeetings(
|
||||
self,
|
||||
request: noteflow_pb2.ListMeetingsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListMeetingsResponse | collections.abc.Awaitable[noteflow_pb2.ListMeetingsResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.ListMeetingsResponse, collections.abc.Awaitable[noteflow_pb2.ListMeetingsResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetMeeting(
|
||||
self,
|
||||
request: noteflow_pb2.GetMeetingRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.Meeting | collections.abc.Awaitable[noteflow_pb2.Meeting]: ...
|
||||
) -> typing.Union[noteflow_pb2.Meeting, collections.abc.Awaitable[noteflow_pb2.Meeting]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def DeleteMeeting(
|
||||
self,
|
||||
request: noteflow_pb2.DeleteMeetingRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.DeleteMeetingResponse | collections.abc.Awaitable[noteflow_pb2.DeleteMeetingResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.DeleteMeetingResponse, collections.abc.Awaitable[noteflow_pb2.DeleteMeetingResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GenerateSummary(
|
||||
self,
|
||||
request: noteflow_pb2.GenerateSummaryRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.Summary | collections.abc.Awaitable[noteflow_pb2.Summary]:
|
||||
) -> typing.Union[noteflow_pb2.Summary, collections.abc.Awaitable[noteflow_pb2.Summary]]:
|
||||
"""Summary generation"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListSummarizationTemplates(
|
||||
self,
|
||||
request: noteflow_pb2.ListSummarizationTemplatesRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.ListSummarizationTemplatesResponse, collections.abc.Awaitable[noteflow_pb2.ListSummarizationTemplatesResponse]]:
|
||||
"""Summarization templates"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetSummarizationTemplate(
|
||||
self,
|
||||
request: noteflow_pb2.GetSummarizationTemplateRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.GetSummarizationTemplateResponse, collections.abc.Awaitable[noteflow_pb2.GetSummarizationTemplateResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def CreateSummarizationTemplate(
|
||||
self,
|
||||
request: noteflow_pb2.CreateSummarizationTemplateRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.SummarizationTemplateMutationResponse, collections.abc.Awaitable[noteflow_pb2.SummarizationTemplateMutationResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def UpdateSummarizationTemplate(
|
||||
self,
|
||||
request: noteflow_pb2.UpdateSummarizationTemplateRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.SummarizationTemplateMutationResponse, collections.abc.Awaitable[noteflow_pb2.SummarizationTemplateMutationResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ArchiveSummarizationTemplate(
|
||||
self,
|
||||
request: noteflow_pb2.ArchiveSummarizationTemplateRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.SummarizationTemplateProto, collections.abc.Awaitable[noteflow_pb2.SummarizationTemplateProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListSummarizationTemplateVersions(
|
||||
self,
|
||||
request: noteflow_pb2.ListSummarizationTemplateVersionsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.ListSummarizationTemplateVersionsResponse, collections.abc.Awaitable[noteflow_pb2.ListSummarizationTemplateVersionsResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def RestoreSummarizationTemplateVersion(
|
||||
self,
|
||||
request: noteflow_pb2.RestoreSummarizationTemplateVersionRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.SummarizationTemplateProto, collections.abc.Awaitable[noteflow_pb2.SummarizationTemplateProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def AddAnnotation(
|
||||
self,
|
||||
request: noteflow_pb2.AddAnnotationRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.Annotation | collections.abc.Awaitable[noteflow_pb2.Annotation]:
|
||||
) -> typing.Union[noteflow_pb2.Annotation, collections.abc.Awaitable[noteflow_pb2.Annotation]]:
|
||||
"""Annotation management"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -285,35 +379,35 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.GetAnnotationRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.Annotation | collections.abc.Awaitable[noteflow_pb2.Annotation]: ...
|
||||
) -> typing.Union[noteflow_pb2.Annotation, collections.abc.Awaitable[noteflow_pb2.Annotation]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListAnnotations(
|
||||
self,
|
||||
request: noteflow_pb2.ListAnnotationsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListAnnotationsResponse | collections.abc.Awaitable[noteflow_pb2.ListAnnotationsResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.ListAnnotationsResponse, collections.abc.Awaitable[noteflow_pb2.ListAnnotationsResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def UpdateAnnotation(
|
||||
self,
|
||||
request: noteflow_pb2.UpdateAnnotationRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.Annotation | collections.abc.Awaitable[noteflow_pb2.Annotation]: ...
|
||||
) -> typing.Union[noteflow_pb2.Annotation, collections.abc.Awaitable[noteflow_pb2.Annotation]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def DeleteAnnotation(
|
||||
self,
|
||||
request: noteflow_pb2.DeleteAnnotationRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.DeleteAnnotationResponse | collections.abc.Awaitable[noteflow_pb2.DeleteAnnotationResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.DeleteAnnotationResponse, collections.abc.Awaitable[noteflow_pb2.DeleteAnnotationResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ExportTranscript(
|
||||
self,
|
||||
request: noteflow_pb2.ExportTranscriptRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ExportTranscriptResponse | collections.abc.Awaitable[noteflow_pb2.ExportTranscriptResponse]:
|
||||
) -> typing.Union[noteflow_pb2.ExportTranscriptResponse, collections.abc.Awaitable[noteflow_pb2.ExportTranscriptResponse]]:
|
||||
"""Export functionality"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -321,7 +415,7 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.RefineSpeakerDiarizationRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.RefineSpeakerDiarizationResponse | collections.abc.Awaitable[noteflow_pb2.RefineSpeakerDiarizationResponse]:
|
||||
) -> typing.Union[noteflow_pb2.RefineSpeakerDiarizationResponse, collections.abc.Awaitable[noteflow_pb2.RefineSpeakerDiarizationResponse]]:
|
||||
"""Speaker diarization"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -329,43 +423,65 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.RenameSpeakerRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.RenameSpeakerResponse | collections.abc.Awaitable[noteflow_pb2.RenameSpeakerResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.RenameSpeakerResponse, collections.abc.Awaitable[noteflow_pb2.RenameSpeakerResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetDiarizationJobStatus(
|
||||
self,
|
||||
request: noteflow_pb2.GetDiarizationJobStatusRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.DiarizationJobStatus | collections.abc.Awaitable[noteflow_pb2.DiarizationJobStatus]: ...
|
||||
) -> typing.Union[noteflow_pb2.DiarizationJobStatus, collections.abc.Awaitable[noteflow_pb2.DiarizationJobStatus]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def CancelDiarizationJob(
|
||||
self,
|
||||
request: noteflow_pb2.CancelDiarizationJobRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.CancelDiarizationJobResponse | collections.abc.Awaitable[noteflow_pb2.CancelDiarizationJobResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.CancelDiarizationJobResponse, collections.abc.Awaitable[noteflow_pb2.CancelDiarizationJobResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetActiveDiarizationJobs(
|
||||
self,
|
||||
request: noteflow_pb2.GetActiveDiarizationJobsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetActiveDiarizationJobsResponse | collections.abc.Awaitable[noteflow_pb2.GetActiveDiarizationJobsResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.GetActiveDiarizationJobsResponse, collections.abc.Awaitable[noteflow_pb2.GetActiveDiarizationJobsResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetServerInfo(
|
||||
self,
|
||||
request: noteflow_pb2.ServerInfoRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ServerInfo | collections.abc.Awaitable[noteflow_pb2.ServerInfo]:
|
||||
) -> typing.Union[noteflow_pb2.ServerInfo, collections.abc.Awaitable[noteflow_pb2.ServerInfo]]:
|
||||
"""Server health and capabilities"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetAsrConfiguration(
|
||||
self,
|
||||
request: noteflow_pb2.GetAsrConfigurationRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.GetAsrConfigurationResponse, collections.abc.Awaitable[noteflow_pb2.GetAsrConfigurationResponse]]:
|
||||
"""ASR Configuration Management (Sprint 19)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def UpdateAsrConfiguration(
|
||||
self,
|
||||
request: noteflow_pb2.UpdateAsrConfigurationRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.UpdateAsrConfigurationResponse, collections.abc.Awaitable[noteflow_pb2.UpdateAsrConfigurationResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetAsrConfigurationJobStatus(
|
||||
self,
|
||||
request: noteflow_pb2.GetAsrConfigurationJobStatusRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.AsrConfigurationJobStatus, collections.abc.Awaitable[noteflow_pb2.AsrConfigurationJobStatus]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ExtractEntities(
|
||||
self,
|
||||
request: noteflow_pb2.ExtractEntitiesRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ExtractEntitiesResponse | collections.abc.Awaitable[noteflow_pb2.ExtractEntitiesResponse]:
|
||||
) -> typing.Union[noteflow_pb2.ExtractEntitiesResponse, collections.abc.Awaitable[noteflow_pb2.ExtractEntitiesResponse]]:
|
||||
"""Named entity extraction (Sprint 4) + mutations (Sprint 8)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -373,21 +489,21 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.UpdateEntityRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.UpdateEntityResponse | collections.abc.Awaitable[noteflow_pb2.UpdateEntityResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.UpdateEntityResponse, collections.abc.Awaitable[noteflow_pb2.UpdateEntityResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def DeleteEntity(
|
||||
self,
|
||||
request: noteflow_pb2.DeleteEntityRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.DeleteEntityResponse | collections.abc.Awaitable[noteflow_pb2.DeleteEntityResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.DeleteEntityResponse, collections.abc.Awaitable[noteflow_pb2.DeleteEntityResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListCalendarEvents(
|
||||
self,
|
||||
request: noteflow_pb2.ListCalendarEventsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListCalendarEventsResponse | collections.abc.Awaitable[noteflow_pb2.ListCalendarEventsResponse]:
|
||||
) -> typing.Union[noteflow_pb2.ListCalendarEventsResponse, collections.abc.Awaitable[noteflow_pb2.ListCalendarEventsResponse]]:
|
||||
"""Calendar integration (Sprint 5)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -395,14 +511,14 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.GetCalendarProvidersRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetCalendarProvidersResponse | collections.abc.Awaitable[noteflow_pb2.GetCalendarProvidersResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.GetCalendarProvidersResponse, collections.abc.Awaitable[noteflow_pb2.GetCalendarProvidersResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def InitiateOAuth(
|
||||
self,
|
||||
request: noteflow_pb2.InitiateOAuthRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.InitiateOAuthResponse | collections.abc.Awaitable[noteflow_pb2.InitiateOAuthResponse]:
|
||||
) -> typing.Union[noteflow_pb2.InitiateOAuthResponse, collections.abc.Awaitable[noteflow_pb2.InitiateOAuthResponse]]:
|
||||
"""OAuth integration (generic for calendar, email, PKM, etc.)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -410,28 +526,28 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.CompleteOAuthRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.CompleteOAuthResponse | collections.abc.Awaitable[noteflow_pb2.CompleteOAuthResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.CompleteOAuthResponse, collections.abc.Awaitable[noteflow_pb2.CompleteOAuthResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetOAuthConnectionStatus(
|
||||
self,
|
||||
request: noteflow_pb2.GetOAuthConnectionStatusRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetOAuthConnectionStatusResponse | collections.abc.Awaitable[noteflow_pb2.GetOAuthConnectionStatusResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.GetOAuthConnectionStatusResponse, collections.abc.Awaitable[noteflow_pb2.GetOAuthConnectionStatusResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def DisconnectOAuth(
|
||||
self,
|
||||
request: noteflow_pb2.DisconnectOAuthRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.DisconnectOAuthResponse | collections.abc.Awaitable[noteflow_pb2.DisconnectOAuthResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.DisconnectOAuthResponse, collections.abc.Awaitable[noteflow_pb2.DisconnectOAuthResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def RegisterWebhook(
|
||||
self,
|
||||
request: noteflow_pb2.RegisterWebhookRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.WebhookConfigProto | collections.abc.Awaitable[noteflow_pb2.WebhookConfigProto]:
|
||||
) -> typing.Union[noteflow_pb2.WebhookConfigProto, collections.abc.Awaitable[noteflow_pb2.WebhookConfigProto]]:
|
||||
"""Webhook management (Sprint 6)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -439,35 +555,35 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.ListWebhooksRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListWebhooksResponse | collections.abc.Awaitable[noteflow_pb2.ListWebhooksResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.ListWebhooksResponse, collections.abc.Awaitable[noteflow_pb2.ListWebhooksResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def UpdateWebhook(
|
||||
self,
|
||||
request: noteflow_pb2.UpdateWebhookRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.WebhookConfigProto | collections.abc.Awaitable[noteflow_pb2.WebhookConfigProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.WebhookConfigProto, collections.abc.Awaitable[noteflow_pb2.WebhookConfigProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def DeleteWebhook(
|
||||
self,
|
||||
request: noteflow_pb2.DeleteWebhookRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.DeleteWebhookResponse | collections.abc.Awaitable[noteflow_pb2.DeleteWebhookResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.DeleteWebhookResponse, collections.abc.Awaitable[noteflow_pb2.DeleteWebhookResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetWebhookDeliveries(
|
||||
self,
|
||||
request: noteflow_pb2.GetWebhookDeliveriesRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetWebhookDeliveriesResponse | collections.abc.Awaitable[noteflow_pb2.GetWebhookDeliveriesResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.GetWebhookDeliveriesResponse, collections.abc.Awaitable[noteflow_pb2.GetWebhookDeliveriesResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GrantCloudConsent(
|
||||
self,
|
||||
request: noteflow_pb2.GrantCloudConsentRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GrantCloudConsentResponse | collections.abc.Awaitable[noteflow_pb2.GrantCloudConsentResponse]:
|
||||
) -> typing.Union[noteflow_pb2.GrantCloudConsentResponse, collections.abc.Awaitable[noteflow_pb2.GrantCloudConsentResponse]]:
|
||||
"""Cloud consent management (Sprint 7)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -475,21 +591,50 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.RevokeCloudConsentRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.RevokeCloudConsentResponse | collections.abc.Awaitable[noteflow_pb2.RevokeCloudConsentResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.RevokeCloudConsentResponse, collections.abc.Awaitable[noteflow_pb2.RevokeCloudConsentResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetCloudConsentStatus(
|
||||
self,
|
||||
request: noteflow_pb2.GetCloudConsentStatusRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetCloudConsentStatusResponse | collections.abc.Awaitable[noteflow_pb2.GetCloudConsentStatusResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.GetCloudConsentStatusResponse, collections.abc.Awaitable[noteflow_pb2.GetCloudConsentStatusResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def SetHuggingFaceToken(
|
||||
self,
|
||||
request: noteflow_pb2.SetHuggingFaceTokenRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.SetHuggingFaceTokenResponse, collections.abc.Awaitable[noteflow_pb2.SetHuggingFaceTokenResponse]]:
|
||||
"""HuggingFace Token Management (Sprint 19)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetHuggingFaceTokenStatus(
|
||||
self,
|
||||
request: noteflow_pb2.GetHuggingFaceTokenStatusRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.GetHuggingFaceTokenStatusResponse, collections.abc.Awaitable[noteflow_pb2.GetHuggingFaceTokenStatusResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def DeleteHuggingFaceToken(
|
||||
self,
|
||||
request: noteflow_pb2.DeleteHuggingFaceTokenRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.DeleteHuggingFaceTokenResponse, collections.abc.Awaitable[noteflow_pb2.DeleteHuggingFaceTokenResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ValidateHuggingFaceToken(
|
||||
self,
|
||||
request: noteflow_pb2.ValidateHuggingFaceTokenRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.ValidateHuggingFaceTokenResponse, collections.abc.Awaitable[noteflow_pb2.ValidateHuggingFaceTokenResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetPreferences(
|
||||
self,
|
||||
request: noteflow_pb2.GetPreferencesRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetPreferencesResponse | collections.abc.Awaitable[noteflow_pb2.GetPreferencesResponse]:
|
||||
) -> typing.Union[noteflow_pb2.GetPreferencesResponse, collections.abc.Awaitable[noteflow_pb2.GetPreferencesResponse]]:
|
||||
"""User preferences sync (Sprint 14)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -497,14 +642,14 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.SetPreferencesRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.SetPreferencesResponse | collections.abc.Awaitable[noteflow_pb2.SetPreferencesResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.SetPreferencesResponse, collections.abc.Awaitable[noteflow_pb2.SetPreferencesResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def StartIntegrationSync(
|
||||
self,
|
||||
request: noteflow_pb2.StartIntegrationSyncRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.StartIntegrationSyncResponse | collections.abc.Awaitable[noteflow_pb2.StartIntegrationSyncResponse]:
|
||||
) -> typing.Union[noteflow_pb2.StartIntegrationSyncResponse, collections.abc.Awaitable[noteflow_pb2.StartIntegrationSyncResponse]]:
|
||||
"""Integration sync orchestration (Sprint 9)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -512,21 +657,21 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.GetSyncStatusRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetSyncStatusResponse | collections.abc.Awaitable[noteflow_pb2.GetSyncStatusResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.GetSyncStatusResponse, collections.abc.Awaitable[noteflow_pb2.GetSyncStatusResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListSyncHistory(
|
||||
self,
|
||||
request: noteflow_pb2.ListSyncHistoryRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListSyncHistoryResponse | collections.abc.Awaitable[noteflow_pb2.ListSyncHistoryResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.ListSyncHistoryResponse, collections.abc.Awaitable[noteflow_pb2.ListSyncHistoryResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetUserIntegrations(
|
||||
self,
|
||||
request: noteflow_pb2.GetUserIntegrationsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetUserIntegrationsResponse | collections.abc.Awaitable[noteflow_pb2.GetUserIntegrationsResponse]:
|
||||
) -> typing.Union[noteflow_pb2.GetUserIntegrationsResponse, collections.abc.Awaitable[noteflow_pb2.GetUserIntegrationsResponse]]:
|
||||
"""Integration cache validation (Sprint 18.1)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -534,7 +679,7 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.GetRecentLogsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetRecentLogsResponse | collections.abc.Awaitable[noteflow_pb2.GetRecentLogsResponse]:
|
||||
) -> typing.Union[noteflow_pb2.GetRecentLogsResponse, collections.abc.Awaitable[noteflow_pb2.GetRecentLogsResponse]]:
|
||||
"""Observability (Sprint 9)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -542,14 +687,14 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.GetPerformanceMetricsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetPerformanceMetricsResponse | collections.abc.Awaitable[noteflow_pb2.GetPerformanceMetricsResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.GetPerformanceMetricsResponse, collections.abc.Awaitable[noteflow_pb2.GetPerformanceMetricsResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def RegisterOidcProvider(
|
||||
self,
|
||||
request: noteflow_pb2.RegisterOidcProviderRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.OidcProviderProto | collections.abc.Awaitable[noteflow_pb2.OidcProviderProto]:
|
||||
) -> typing.Union[noteflow_pb2.OidcProviderProto, collections.abc.Awaitable[noteflow_pb2.OidcProviderProto]]:
|
||||
"""OIDC Provider Management (Sprint 17)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -557,49 +702,49 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.ListOidcProvidersRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListOidcProvidersResponse | collections.abc.Awaitable[noteflow_pb2.ListOidcProvidersResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.ListOidcProvidersResponse, collections.abc.Awaitable[noteflow_pb2.ListOidcProvidersResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetOidcProvider(
|
||||
self,
|
||||
request: noteflow_pb2.GetOidcProviderRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.OidcProviderProto | collections.abc.Awaitable[noteflow_pb2.OidcProviderProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.OidcProviderProto, collections.abc.Awaitable[noteflow_pb2.OidcProviderProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def UpdateOidcProvider(
|
||||
self,
|
||||
request: noteflow_pb2.UpdateOidcProviderRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.OidcProviderProto | collections.abc.Awaitable[noteflow_pb2.OidcProviderProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.OidcProviderProto, collections.abc.Awaitable[noteflow_pb2.OidcProviderProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def DeleteOidcProvider(
|
||||
self,
|
||||
request: noteflow_pb2.DeleteOidcProviderRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.DeleteOidcProviderResponse | collections.abc.Awaitable[noteflow_pb2.DeleteOidcProviderResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.DeleteOidcProviderResponse, collections.abc.Awaitable[noteflow_pb2.DeleteOidcProviderResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def RefreshOidcDiscovery(
|
||||
self,
|
||||
request: noteflow_pb2.RefreshOidcDiscoveryRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.RefreshOidcDiscoveryResponse | collections.abc.Awaitable[noteflow_pb2.RefreshOidcDiscoveryResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.RefreshOidcDiscoveryResponse, collections.abc.Awaitable[noteflow_pb2.RefreshOidcDiscoveryResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListOidcPresets(
|
||||
self,
|
||||
request: noteflow_pb2.ListOidcPresetsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListOidcPresetsResponse | collections.abc.Awaitable[noteflow_pb2.ListOidcPresetsResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.ListOidcPresetsResponse, collections.abc.Awaitable[noteflow_pb2.ListOidcPresetsResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def CreateProject(
|
||||
self,
|
||||
request: noteflow_pb2.CreateProjectRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ProjectProto | collections.abc.Awaitable[noteflow_pb2.ProjectProto]:
|
||||
) -> typing.Union[noteflow_pb2.ProjectProto, collections.abc.Awaitable[noteflow_pb2.ProjectProto]]:
|
||||
"""Project management (Sprint 18)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -607,56 +752,56 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.GetProjectRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ProjectProto | collections.abc.Awaitable[noteflow_pb2.ProjectProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.ProjectProto, collections.abc.Awaitable[noteflow_pb2.ProjectProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetProjectBySlug(
|
||||
self,
|
||||
request: noteflow_pb2.GetProjectBySlugRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ProjectProto | collections.abc.Awaitable[noteflow_pb2.ProjectProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.ProjectProto, collections.abc.Awaitable[noteflow_pb2.ProjectProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListProjects(
|
||||
self,
|
||||
request: noteflow_pb2.ListProjectsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListProjectsResponse | collections.abc.Awaitable[noteflow_pb2.ListProjectsResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.ListProjectsResponse, collections.abc.Awaitable[noteflow_pb2.ListProjectsResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def UpdateProject(
|
||||
self,
|
||||
request: noteflow_pb2.UpdateProjectRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ProjectProto | collections.abc.Awaitable[noteflow_pb2.ProjectProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.ProjectProto, collections.abc.Awaitable[noteflow_pb2.ProjectProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ArchiveProject(
|
||||
self,
|
||||
request: noteflow_pb2.ArchiveProjectRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ProjectProto | collections.abc.Awaitable[noteflow_pb2.ProjectProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.ProjectProto, collections.abc.Awaitable[noteflow_pb2.ProjectProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def RestoreProject(
|
||||
self,
|
||||
request: noteflow_pb2.RestoreProjectRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ProjectProto | collections.abc.Awaitable[noteflow_pb2.ProjectProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.ProjectProto, collections.abc.Awaitable[noteflow_pb2.ProjectProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def DeleteProject(
|
||||
self,
|
||||
request: noteflow_pb2.DeleteProjectRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.DeleteProjectResponse | collections.abc.Awaitable[noteflow_pb2.DeleteProjectResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.DeleteProjectResponse, collections.abc.Awaitable[noteflow_pb2.DeleteProjectResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def SetActiveProject(
|
||||
self,
|
||||
request: noteflow_pb2.SetActiveProjectRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.SetActiveProjectResponse | collections.abc.Awaitable[noteflow_pb2.SetActiveProjectResponse]:
|
||||
) -> typing.Union[noteflow_pb2.SetActiveProjectResponse, collections.abc.Awaitable[noteflow_pb2.SetActiveProjectResponse]]:
|
||||
"""Active project (Sprint 18)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -664,14 +809,14 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.GetActiveProjectRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.GetActiveProjectResponse | collections.abc.Awaitable[noteflow_pb2.GetActiveProjectResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.GetActiveProjectResponse, collections.abc.Awaitable[noteflow_pb2.GetActiveProjectResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def AddProjectMember(
|
||||
self,
|
||||
request: noteflow_pb2.AddProjectMemberRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ProjectMembershipProto | collections.abc.Awaitable[noteflow_pb2.ProjectMembershipProto]:
|
||||
) -> typing.Union[noteflow_pb2.ProjectMembershipProto, collections.abc.Awaitable[noteflow_pb2.ProjectMembershipProto]]:
|
||||
"""Project membership management (Sprint 18)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -679,20 +824,56 @@ class NoteFlowServiceServicer(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
request: noteflow_pb2.UpdateProjectMemberRoleRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ProjectMembershipProto | collections.abc.Awaitable[noteflow_pb2.ProjectMembershipProto]: ...
|
||||
) -> typing.Union[noteflow_pb2.ProjectMembershipProto, collections.abc.Awaitable[noteflow_pb2.ProjectMembershipProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def RemoveProjectMember(
|
||||
self,
|
||||
request: noteflow_pb2.RemoveProjectMemberRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.RemoveProjectMemberResponse | collections.abc.Awaitable[noteflow_pb2.RemoveProjectMemberResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.RemoveProjectMemberResponse, collections.abc.Awaitable[noteflow_pb2.RemoveProjectMemberResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListProjectMembers(
|
||||
self,
|
||||
request: noteflow_pb2.ListProjectMembersRequest,
|
||||
context: _ServicerContext,
|
||||
) -> noteflow_pb2.ListProjectMembersResponse | collections.abc.Awaitable[noteflow_pb2.ListProjectMembersResponse]: ...
|
||||
) -> typing.Union[noteflow_pb2.ListProjectMembersResponse, collections.abc.Awaitable[noteflow_pb2.ListProjectMembersResponse]]: ...
|
||||
|
||||
def add_NoteFlowServiceServicer_to_server(servicer: NoteFlowServiceServicer, server: grpc.Server | grpc.aio.Server) -> None: ...
|
||||
@abc.abstractmethod
|
||||
def GetCurrentUser(
|
||||
self,
|
||||
request: noteflow_pb2.GetCurrentUserRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.GetCurrentUserResponse, collections.abc.Awaitable[noteflow_pb2.GetCurrentUserResponse]]:
|
||||
"""Identity management (Sprint 16+)"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def ListWorkspaces(
|
||||
self,
|
||||
request: noteflow_pb2.ListWorkspacesRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.ListWorkspacesResponse, collections.abc.Awaitable[noteflow_pb2.ListWorkspacesResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def SwitchWorkspace(
|
||||
self,
|
||||
request: noteflow_pb2.SwitchWorkspaceRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.SwitchWorkspaceResponse, collections.abc.Awaitable[noteflow_pb2.SwitchWorkspaceResponse]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def GetWorkspaceSettings(
|
||||
self,
|
||||
request: noteflow_pb2.GetWorkspaceSettingsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.WorkspaceSettingsProto, collections.abc.Awaitable[noteflow_pb2.WorkspaceSettingsProto]]: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def UpdateWorkspaceSettings(
|
||||
self,
|
||||
request: noteflow_pb2.UpdateWorkspaceSettingsRequest,
|
||||
context: _ServicerContext,
|
||||
) -> typing.Union[noteflow_pb2.WorkspaceSettingsProto, collections.abc.Awaitable[noteflow_pb2.WorkspaceSettingsProto]]: ...
|
||||
|
||||
def add_NoteFlowServiceServicer_to_server(servicer: NoteFlowServiceServicer, server: typing.Union[grpc.Server, grpc.aio.Server]) -> None: ...
|
||||
|
||||
@@ -23,11 +23,13 @@ from ._config import ServicesConfig
|
||||
from ._identity_singleton import default_identity_service
|
||||
from ._mixins import (
|
||||
AnnotationMixin,
|
||||
AsrConfigMixin,
|
||||
CalendarMixin,
|
||||
DiarizationJobMixin,
|
||||
DiarizationMixin,
|
||||
EntitiesMixin,
|
||||
ExportMixin,
|
||||
HfTokenMixin,
|
||||
IdentityMixin,
|
||||
MeetingMixin,
|
||||
ObservabilityMixin,
|
||||
@@ -56,6 +58,8 @@ from .stream_state import MeetingStreamState
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from noteflow.application.services.asr_config_service import AsrConfigService
|
||||
from noteflow.application.services.hf_token_service import HfTokenService
|
||||
from noteflow.infrastructure.asr import FasterWhisperEngine
|
||||
from noteflow.infrastructure.auth.oidc_registry import OidcAuthService
|
||||
|
||||
@@ -88,6 +92,8 @@ class NoteFlowServicer(
|
||||
IdentityMixin,
|
||||
ProjectMixin,
|
||||
ProjectMembershipMixin,
|
||||
AsrConfigMixin,
|
||||
HfTokenMixin,
|
||||
NoteFlowServicerStubs,
|
||||
GrpcBaseServicer,
|
||||
):
|
||||
@@ -159,6 +165,13 @@ class NoteFlowServicer(
|
||||
session_factory: async_sessionmaker[AsyncSession] | None,
|
||||
) -> None:
|
||||
"""Initialize audio recording infrastructure."""
|
||||
from noteflow.application.services.asr_config_service import (
|
||||
AsrConfigService as AsrConfigServiceImpl,
|
||||
)
|
||||
from noteflow.application.services.hf_token_service import (
|
||||
HfTokenService as HfTokenServiceImpl,
|
||||
)
|
||||
|
||||
self.memory_store: MeetingStore | None = (
|
||||
MeetingStore() if session_factory is None else None
|
||||
)
|
||||
@@ -167,6 +180,26 @@ class NoteFlowServicer(
|
||||
self.crypto = AesGcmCryptoBox(self._keystore)
|
||||
self.audio_writers: dict[str, MeetingAudioWriter] = {}
|
||||
|
||||
# Initialize ASR configuration service
|
||||
self.asr_config_service: AsrConfigService | None = (
|
||||
AsrConfigServiceImpl(
|
||||
self.asr_engine,
|
||||
on_engine_update=self._set_asr_engine,
|
||||
)
|
||||
if self.asr_engine
|
||||
else None
|
||||
)
|
||||
|
||||
# Initialize HuggingFace token service (requires UoW factory and crypto)
|
||||
self.hf_token_service: HfTokenService | None = HfTokenServiceImpl(
|
||||
uow_factory=self.create_uow,
|
||||
crypto=self.crypto,
|
||||
)
|
||||
|
||||
def _set_asr_engine(self, engine: FasterWhisperEngine) -> None:
|
||||
"""Update the active ASR engine reference for streaming."""
|
||||
self.asr_engine = engine
|
||||
|
||||
def _init_streaming_state(self) -> None:
|
||||
"""Initialize per-meeting streaming state containers."""
|
||||
self.vad_instances: dict[str, StreamingVad] = {}
|
||||
|
||||
@@ -10,7 +10,7 @@ from noteflow.infrastructure.asr.dto import (
|
||||
VadEventType,
|
||||
WordTiming,
|
||||
)
|
||||
from noteflow.infrastructure.asr.engine import FasterWhisperEngine
|
||||
from noteflow.infrastructure.asr.engine import FasterWhisperEngine, VALID_MODEL_SIZES
|
||||
from noteflow.infrastructure.asr.protocols import AsrEngine
|
||||
from noteflow.infrastructure.asr.segmenter import (
|
||||
AudioSegment,
|
||||
@@ -37,6 +37,7 @@ __all__ = [
|
||||
"SegmenterConfig",
|
||||
"SegmenterState",
|
||||
"StreamingVad",
|
||||
"VALID_MODEL_SIZES",
|
||||
"VadEngine",
|
||||
"VadEvent",
|
||||
"VadEventType",
|
||||
|
||||
414
tests/application/test_asr_config_service.py
Normal file
414
tests/application/test_asr_config_service.py
Normal file
@@ -0,0 +1,414 @@
|
||||
"""Unit tests for AsrConfigService.
|
||||
|
||||
Tests cover:
|
||||
- get_capabilities: Returns current ASR configuration and available options
|
||||
- validate_configuration: Validates model size, device, and compute type
|
||||
- start_reconfiguration: Starts background reconfiguration job
|
||||
- get_job_status: Returns status of reconfiguration jobs
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from unittest.mock import MagicMock, patch
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
from noteflow.application.services.asr_config_service import (
|
||||
AsrComputeType,
|
||||
AsrConfigJob,
|
||||
AsrConfigService,
|
||||
AsrDevice,
|
||||
)
|
||||
from noteflow.domain.constants.fields import (
|
||||
JOB_STATUS_COMPLETED,
|
||||
JOB_STATUS_FAILED,
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Fixtures
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_asr_engine() -> MagicMock:
|
||||
"""Create mock ASR engine for testing."""
|
||||
engine = MagicMock()
|
||||
engine.model_size = "base"
|
||||
engine.device = "cpu"
|
||||
engine.compute_type = "int8"
|
||||
engine.is_loaded = True
|
||||
engine.unload = MagicMock()
|
||||
engine.load_model = MagicMock()
|
||||
return engine
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def asr_config_service(mock_asr_engine: MagicMock) -> AsrConfigService:
|
||||
"""Create AsrConfigService with mock engine."""
|
||||
return AsrConfigService(asr_engine=mock_asr_engine)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def asr_config_service_no_engine() -> AsrConfigService:
|
||||
"""Create AsrConfigService without engine."""
|
||||
return AsrConfigService(asr_engine=None)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# get_capabilities tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_get_capabilities_returns_current_config(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""get_capabilities returns current ASR configuration."""
|
||||
with patch.object(asr_config_service, "detect_cuda_available", return_value=False):
|
||||
caps = asr_config_service.get_capabilities()
|
||||
|
||||
assert caps.model_size == "base", "model_size should be 'base' from engine"
|
||||
assert caps.device == AsrDevice.CPU, "device should be CPU"
|
||||
assert caps.compute_type == AsrComputeType.INT8, "compute_type should be INT8"
|
||||
assert caps.is_ready is True, "is_ready should be True when engine is loaded"
|
||||
assert caps.cuda_available is False, "cuda_available should be False"
|
||||
assert "base" in caps.available_model_sizes, "available_model_sizes should include 'base'"
|
||||
assert AsrComputeType.INT8 in caps.available_compute_types, "INT8 should be available"
|
||||
|
||||
|
||||
def test_get_capabilities_no_engine_returns_defaults(
|
||||
asr_config_service_no_engine: AsrConfigService,
|
||||
) -> None:
|
||||
"""get_capabilities returns defaults when no engine."""
|
||||
with patch.object(asr_config_service_no_engine, "detect_cuda_available", return_value=False):
|
||||
caps = asr_config_service_no_engine.get_capabilities()
|
||||
|
||||
assert caps.model_size is None, "model_size should be None without engine"
|
||||
assert caps.device == AsrDevice.CPU, "device should default to CPU"
|
||||
assert caps.is_ready is False, "is_ready should be False without engine"
|
||||
|
||||
|
||||
def test_get_capabilities_with_cuda_available(
|
||||
asr_config_service: AsrConfigService,
|
||||
mock_asr_engine: MagicMock,
|
||||
) -> None:
|
||||
"""get_capabilities includes CUDA compute types when available."""
|
||||
mock_asr_engine.device = "cuda"
|
||||
with patch.object(asr_config_service, "detect_cuda_available", return_value=True):
|
||||
caps = asr_config_service.get_capabilities()
|
||||
|
||||
assert caps.cuda_available is True, "cuda_available should be True when CUDA detected"
|
||||
assert caps.device == AsrDevice.CUDA, "device should be CUDA"
|
||||
assert AsrComputeType.FLOAT16 in caps.available_compute_types, (
|
||||
"FLOAT16 should be available for CUDA"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# validate_configuration tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_validate_configuration_valid_cpu_config(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""validate_configuration accepts valid CPU configuration."""
|
||||
with patch.object(asr_config_service, "detect_cuda_available", return_value=False):
|
||||
error = asr_config_service.validate_configuration(
|
||||
model_size="small",
|
||||
device=AsrDevice.CPU,
|
||||
compute_type=AsrComputeType.INT8,
|
||||
)
|
||||
|
||||
assert error is None, "valid CPU configuration should not return an error"
|
||||
|
||||
|
||||
def test_validate_configuration_invalid_model_size(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""validate_configuration rejects invalid model size."""
|
||||
error = asr_config_service.validate_configuration(
|
||||
model_size="invalid-model",
|
||||
device=None,
|
||||
compute_type=None,
|
||||
)
|
||||
|
||||
assert error is not None, "error should be set for invalid model"
|
||||
assert "Invalid model size" in error, "error should mention invalid model size"
|
||||
|
||||
|
||||
def test_validate_configuration_cuda_unavailable(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""validate_configuration rejects CUDA when unavailable."""
|
||||
with patch.object(asr_config_service, "detect_cuda_available", return_value=False):
|
||||
error = asr_config_service.validate_configuration(
|
||||
model_size=None,
|
||||
device=AsrDevice.CUDA,
|
||||
compute_type=None,
|
||||
)
|
||||
|
||||
assert error is not None, "error should be set when CUDA unavailable"
|
||||
assert "CUDA" in error, "error should mention CUDA"
|
||||
|
||||
|
||||
def test_validate_configuration_invalid_compute_for_device(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""validate_configuration rejects invalid compute type for device."""
|
||||
error = asr_config_service.validate_configuration(
|
||||
model_size=None,
|
||||
device=AsrDevice.CPU,
|
||||
compute_type=AsrComputeType.FLOAT16, # FLOAT16 not available for CPU
|
||||
)
|
||||
|
||||
assert error is not None, "error should be set for invalid compute type"
|
||||
assert "not available" in error, "error should mention unavailability"
|
||||
|
||||
|
||||
def test_validate_configuration_none_values_accepted(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""validate_configuration accepts None values (keep current)."""
|
||||
error = asr_config_service.validate_configuration(
|
||||
model_size=None,
|
||||
device=None,
|
||||
compute_type=None,
|
||||
)
|
||||
|
||||
assert error is None, "None values should be accepted for validation"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# start_reconfiguration tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_reconfiguration_returns_job_id(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""start_reconfiguration returns job ID on success."""
|
||||
with patch.object(asr_config_service, "detect_cuda_available", return_value=False):
|
||||
job_id, error = await asr_config_service.start_reconfiguration(
|
||||
model_size="small",
|
||||
device=None,
|
||||
compute_type=None,
|
||||
has_active_recordings=False,
|
||||
)
|
||||
|
||||
assert job_id is not None, "job_id should be returned on success"
|
||||
assert isinstance(job_id, UUID), "job_id should be a UUID"
|
||||
assert error is None, "error should be None on success"
|
||||
|
||||
# Clean up background task
|
||||
job = asr_config_service.get_job_status(job_id)
|
||||
assert job is not None, "job should exist after start_reconfiguration"
|
||||
assert job.task is not None, "job task should be created"
|
||||
job.task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await job.task
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_reconfiguration_blocked_during_recording(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""start_reconfiguration is blocked while recordings are active."""
|
||||
job_id, error = await asr_config_service.start_reconfiguration(
|
||||
model_size="small",
|
||||
device=None,
|
||||
compute_type=None,
|
||||
has_active_recordings=True,
|
||||
)
|
||||
|
||||
assert job_id is None, "job_id should be None when blocked"
|
||||
assert error is not None, "error should be set when recordings active"
|
||||
assert "recordings are active" in error, "error should explain why blocked"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_reconfiguration_no_engine(
|
||||
asr_config_service_no_engine: AsrConfigService,
|
||||
) -> None:
|
||||
"""start_reconfiguration fails when no engine available."""
|
||||
job_id, error = await asr_config_service_no_engine.start_reconfiguration(
|
||||
model_size="small",
|
||||
device=None,
|
||||
compute_type=None,
|
||||
has_active_recordings=False,
|
||||
)
|
||||
|
||||
assert job_id is None, "job_id should be None without engine"
|
||||
assert error is not None, "error should be set without engine"
|
||||
assert "not available" in error, "error should explain unavailability"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_reconfiguration_validation_failure(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""start_reconfiguration fails on invalid configuration."""
|
||||
with patch.object(asr_config_service, "detect_cuda_available", return_value=False):
|
||||
job_id, error = await asr_config_service.start_reconfiguration(
|
||||
model_size="invalid-model",
|
||||
device=None,
|
||||
compute_type=None,
|
||||
has_active_recordings=False,
|
||||
)
|
||||
|
||||
assert job_id is None, "job_id should be None on validation failure"
|
||||
assert error is not None, "error should be set on validation failure"
|
||||
assert "Invalid model size" in error, "error should mention invalid model"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# get_job_status tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_job_status_returns_job(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""get_job_status returns job info for valid ID."""
|
||||
with patch.object(asr_config_service, "detect_cuda_available", return_value=False):
|
||||
job_id, _ = await asr_config_service.start_reconfiguration(
|
||||
model_size="small",
|
||||
device=None,
|
||||
compute_type=None,
|
||||
has_active_recordings=False,
|
||||
)
|
||||
|
||||
assert job_id is not None, "job_id should be returned"
|
||||
job = asr_config_service.get_job_status(job_id)
|
||||
|
||||
assert job is not None, "job should be found for valid ID"
|
||||
assert isinstance(job, AsrConfigJob), "job should be AsrConfigJob type"
|
||||
assert job.target_model_size == "small", "target_model_size should match request"
|
||||
|
||||
# Clean up
|
||||
assert job.task is not None, "job task should be created"
|
||||
job.task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await job.task
|
||||
|
||||
|
||||
def test_get_job_status_returns_none_for_unknown_id(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""get_job_status returns None for unknown job ID."""
|
||||
from uuid import uuid4
|
||||
|
||||
job = asr_config_service.get_job_status(uuid4())
|
||||
assert job is None, "job should be None for unknown ID"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# detect_cuda_available tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_detect_cuda_available_with_cuda(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""detect_cuda_available returns True when CUDA is available."""
|
||||
mock_torch = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = True
|
||||
|
||||
with patch.dict("sys.modules", {"torch": mock_torch}):
|
||||
result = asr_config_service.detect_cuda_available()
|
||||
|
||||
assert result is True, "detect_cuda_available should return True when CUDA available"
|
||||
|
||||
|
||||
def test_detect_cuda_available_no_cuda(
|
||||
asr_config_service: AsrConfigService,
|
||||
) -> None:
|
||||
"""detect_cuda_available returns False when CUDA is not available."""
|
||||
mock_torch = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = False
|
||||
|
||||
with patch.dict("sys.modules", {"torch": mock_torch}):
|
||||
result = asr_config_service.detect_cuda_available()
|
||||
|
||||
assert result is False, "detect_cuda_available should return False when CUDA unavailable"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# reconfiguration behavior tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconfiguration_failure_keeps_active_engine(
|
||||
mock_asr_engine: MagicMock,
|
||||
) -> None:
|
||||
"""Reconfiguration failure should not replace or unload the active engine."""
|
||||
updates: list[MagicMock] = []
|
||||
asr_config_service = AsrConfigService(
|
||||
asr_engine=mock_asr_engine,
|
||||
on_engine_update=updates.append,
|
||||
)
|
||||
new_engine = MagicMock()
|
||||
|
||||
with (
|
||||
patch.object(asr_config_service, "_build_engine_for_job", return_value=(new_engine, True)),
|
||||
patch.object(asr_config_service, "_load_model", side_effect=RuntimeError("boom")),
|
||||
):
|
||||
job_id, error = await asr_config_service.start_reconfiguration(
|
||||
model_size="small",
|
||||
device=None,
|
||||
compute_type=None,
|
||||
has_active_recordings=False,
|
||||
)
|
||||
|
||||
assert job_id is not None, "job should be created even if load fails"
|
||||
assert error is None, "start_reconfiguration should not return an error on async failure"
|
||||
|
||||
job = asr_config_service.get_job_status(job_id)
|
||||
assert job is not None, "job should be retrievable"
|
||||
assert job.task is not None, "job task should be created"
|
||||
await job.task
|
||||
|
||||
assert job.status == JOB_STATUS_FAILED, "job should be marked failed on load error"
|
||||
mock_asr_engine.unload.assert_not_called()
|
||||
assert updates == [], "engine update callback should not fire on failure"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconfiguration_success_swaps_engine(
|
||||
mock_asr_engine: MagicMock,
|
||||
) -> None:
|
||||
"""Successful reconfiguration should swap engine and unload the old one."""
|
||||
updates: list[MagicMock] = []
|
||||
asr_config_service = AsrConfigService(
|
||||
asr_engine=mock_asr_engine,
|
||||
on_engine_update=updates.append,
|
||||
)
|
||||
new_engine = MagicMock()
|
||||
|
||||
with (
|
||||
patch.object(asr_config_service, "_build_engine_for_job", return_value=(new_engine, True)),
|
||||
patch.object(asr_config_service, "_load_model", return_value=None),
|
||||
):
|
||||
job_id, error = await asr_config_service.start_reconfiguration(
|
||||
model_size="small",
|
||||
device=None,
|
||||
compute_type=None,
|
||||
has_active_recordings=False,
|
||||
)
|
||||
|
||||
assert job_id is not None, "job should be created"
|
||||
assert error is None, "no error should be returned for successful start"
|
||||
|
||||
job = asr_config_service.get_job_status(job_id)
|
||||
assert job is not None, "job should be retrievable"
|
||||
assert job.task is not None, "job task should be created"
|
||||
await job.task
|
||||
|
||||
assert job.status == JOB_STATUS_COMPLETED, "job should complete successfully"
|
||||
mock_asr_engine.unload.assert_called_once()
|
||||
assert updates == [new_engine], "engine update callback should fire on success"
|
||||
546
tests/application/test_hf_token_service.py
Normal file
546
tests/application/test_hf_token_service.py
Normal file
@@ -0,0 +1,546 @@
|
||||
"""Unit tests for HfTokenService.
|
||||
|
||||
Tests cover:
|
||||
- set_token: Store and optionally validate a HuggingFace token
|
||||
- get_status: Get current token configuration status
|
||||
- delete_token: Remove stored token
|
||||
- validate_stored_token: Validate the stored token against HuggingFace API
|
||||
- get_token: Retrieve decrypted token
|
||||
- Encryption/decryption roundtrip
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from noteflow.application.services.hf_token_service import (
|
||||
HfTokenService,
|
||||
HfValidationResult,
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
# Sample timestamp for validation tests (2023-11-15 00:00:00 UTC)
|
||||
SAMPLE_VALIDATION_TIMESTAMP = 1700000000.0
|
||||
|
||||
# =============================================================================
|
||||
# Fixtures
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_crypto() -> MagicMock:
|
||||
"""Create mock crypto box for testing."""
|
||||
crypto = MagicMock()
|
||||
# Mock DEK generation and wrapping
|
||||
crypto.generate_dek.return_value = b"mock_dek_16bytes"
|
||||
crypto.wrap_dek.return_value = b"wrapped_dek_bytes"
|
||||
crypto.unwrap_dek.return_value = b"mock_dek_16bytes"
|
||||
|
||||
# Mock encryption
|
||||
mock_encrypted = MagicMock()
|
||||
mock_encrypted.nonce = b"nonce_12_bytes"
|
||||
mock_encrypted.ciphertext = b"encrypted_token_data"
|
||||
mock_encrypted.tag = b"tag_16_bytes_xx"
|
||||
crypto.encrypt_chunk.return_value = mock_encrypted
|
||||
|
||||
# Mock decryption
|
||||
crypto.decrypt_chunk.return_value = b"hf_test_token_value"
|
||||
|
||||
return crypto
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_preferences() -> MagicMock:
|
||||
"""Create mock preferences repository."""
|
||||
prefs = MagicMock()
|
||||
prefs.get = AsyncMock(return_value=None)
|
||||
prefs.set = AsyncMock()
|
||||
prefs.delete = AsyncMock(return_value=True)
|
||||
return prefs
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prefs_mock_uow(mock_preferences: MagicMock) -> MagicMock:
|
||||
"""Create mock unit of work with preferences support."""
|
||||
uow = MagicMock()
|
||||
uow.supports_preferences = True
|
||||
uow.preferences = mock_preferences
|
||||
uow.commit = AsyncMock()
|
||||
uow.__aenter__ = AsyncMock(return_value=uow)
|
||||
uow.__aexit__ = AsyncMock(return_value=None)
|
||||
return uow
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hf_token_service(prefs_mock_uow: MagicMock, mock_crypto: MagicMock) -> HfTokenService:
|
||||
"""Create HfTokenService with mocks."""
|
||||
|
||||
def uow_factory() -> MagicMock:
|
||||
return prefs_mock_uow
|
||||
|
||||
return HfTokenService(uow_factory=uow_factory, crypto=mock_crypto)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hf_token_service_no_prefs(mock_crypto: MagicMock) -> HfTokenService:
|
||||
"""Create HfTokenService with UoW that doesn't support preferences."""
|
||||
uow = MagicMock()
|
||||
uow.supports_preferences = False
|
||||
uow.__aenter__ = AsyncMock(return_value=uow)
|
||||
uow.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
def uow_factory() -> MagicMock:
|
||||
return uow
|
||||
|
||||
return HfTokenService(uow_factory=uow_factory, crypto=mock_crypto)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# set_token tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_token_without_validation(
|
||||
hf_token_service: HfTokenService,
|
||||
prefs_mock_uow: MagicMock,
|
||||
) -> None:
|
||||
"""set_token stores token without validation when validate=False."""
|
||||
success, result = await hf_token_service.set_token(
|
||||
"hf_test_token",
|
||||
validate=False,
|
||||
)
|
||||
|
||||
assert success is True, "set_token should succeed"
|
||||
assert result is None, "result should be None when validation skipped"
|
||||
prefs_mock_uow.preferences.set.assert_called()
|
||||
prefs_mock_uow.commit.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_token_with_successful_validation(
|
||||
hf_token_service: HfTokenService,
|
||||
prefs_mock_uow: MagicMock,
|
||||
) -> None:
|
||||
"""set_token validates and stores token when validate=True."""
|
||||
with patch.object(
|
||||
hf_token_service,
|
||||
"validate_token_internal",
|
||||
return_value=HfValidationResult(
|
||||
valid=True,
|
||||
username="testuser",
|
||||
error_message="",
|
||||
),
|
||||
):
|
||||
success, result = await hf_token_service.set_token(
|
||||
"hf_valid_token",
|
||||
validate=True,
|
||||
)
|
||||
|
||||
assert success is True, "set_token should succeed with valid token"
|
||||
assert result is not None, "result should contain validation info"
|
||||
assert result.valid is True, "validation should pass"
|
||||
assert result.username == "testuser", "username should match"
|
||||
prefs_mock_uow.commit.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_token_with_failed_validation(
|
||||
hf_token_service: HfTokenService,
|
||||
prefs_mock_uow: MagicMock,
|
||||
) -> None:
|
||||
"""set_token returns failure when validation fails."""
|
||||
with patch.object(
|
||||
hf_token_service,
|
||||
"validate_token_internal",
|
||||
return_value=HfValidationResult(
|
||||
valid=False,
|
||||
username="",
|
||||
error_message="Invalid or expired token",
|
||||
),
|
||||
):
|
||||
success, result = await hf_token_service.set_token(
|
||||
"hf_invalid_token",
|
||||
validate=True,
|
||||
)
|
||||
|
||||
assert success is False, "set_token should fail with invalid token"
|
||||
assert result is not None, "result should contain validation info"
|
||||
assert result.valid is False, "validation should fail"
|
||||
assert "Invalid" in result.error_message, "error should explain failure"
|
||||
prefs_mock_uow.commit.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_token_no_preferences_support(
|
||||
hf_token_service_no_prefs: HfTokenService,
|
||||
) -> None:
|
||||
"""set_token returns failure when preferences storage is unavailable."""
|
||||
success, result = await hf_token_service_no_prefs.set_token(
|
||||
"hf_test_token",
|
||||
validate=False,
|
||||
)
|
||||
|
||||
assert success is False, "set_token should fail without preferences support"
|
||||
assert result is not None, "result should describe storage failure"
|
||||
assert "not available" in result.error_message, "error should mention storage unavailability"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# get_status tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_status_no_token_configured(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
) -> None:
|
||||
"""get_status returns not configured when no token stored."""
|
||||
mock_preferences.get.return_value = None
|
||||
|
||||
status = await hf_token_service.get_status()
|
||||
|
||||
assert status.is_configured is False, "is_configured should be False"
|
||||
assert status.is_validated is False, "is_validated should be False"
|
||||
assert status.username == "", "username should be empty"
|
||||
assert status.validated_at is None, "validated_at should be None"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_status_token_configured_with_metadata(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
) -> None:
|
||||
"""get_status returns correct status when token is configured."""
|
||||
mock_preferences.get.side_effect = [
|
||||
{"wrapped_dek": "abc", "nonce": "def", "ciphertext": "ghi", "tag": "jkl"},
|
||||
{"username": "testuser", "is_validated": True, "validated_at": SAMPLE_VALIDATION_TIMESTAMP},
|
||||
]
|
||||
|
||||
status = await hf_token_service.get_status()
|
||||
|
||||
assert status.is_configured is True, "is_configured should be True"
|
||||
assert status.is_validated is True, "is_validated should be True"
|
||||
assert status.username == "testuser", "username should match stored value"
|
||||
assert status.validated_at == SAMPLE_VALIDATION_TIMESTAMP, (
|
||||
"validated_at should match stored value"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_status_no_preferences_support(
|
||||
hf_token_service_no_prefs: HfTokenService,
|
||||
) -> None:
|
||||
"""get_status returns not configured when preferences not supported."""
|
||||
status = await hf_token_service_no_prefs.get_status()
|
||||
|
||||
assert status.is_configured is False, "is_configured should be False without prefs"
|
||||
assert status.is_validated is False, "is_validated should be False without prefs"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# delete_token tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_token_success(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
prefs_mock_uow: MagicMock,
|
||||
) -> None:
|
||||
"""delete_token removes token and returns True."""
|
||||
mock_preferences.delete.return_value = True
|
||||
|
||||
result = await hf_token_service.delete_token()
|
||||
|
||||
assert result is True, "delete_token should return True when token deleted"
|
||||
prefs_mock_uow.commit.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_token_not_found(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
prefs_mock_uow: MagicMock,
|
||||
) -> None:
|
||||
"""delete_token returns False when no token exists."""
|
||||
mock_preferences.delete.return_value = False
|
||||
|
||||
result = await hf_token_service.delete_token()
|
||||
|
||||
assert result is False, "delete_token should return False when token missing"
|
||||
prefs_mock_uow.commit.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_token_no_preferences_support(
|
||||
hf_token_service_no_prefs: HfTokenService,
|
||||
) -> None:
|
||||
"""delete_token returns False when preferences not supported."""
|
||||
result = await hf_token_service_no_prefs.delete_token()
|
||||
|
||||
assert result is False, "delete_token should return False without prefs support"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# validate_stored_token tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_stored_token_success(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
prefs_mock_uow: MagicMock,
|
||||
) -> None:
|
||||
"""validate_stored_token validates and updates metadata."""
|
||||
mock_preferences.get.return_value = {
|
||||
"wrapped_dek": "abc",
|
||||
"nonce": "def",
|
||||
"ciphertext": "ghi",
|
||||
"tag": "jkl",
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
hf_token_service,
|
||||
"decrypt_token",
|
||||
return_value="hf_test_token",
|
||||
),
|
||||
patch.object(
|
||||
hf_token_service,
|
||||
"validate_token_internal",
|
||||
return_value=HfValidationResult(
|
||||
valid=True,
|
||||
username="validuser",
|
||||
error_message="",
|
||||
),
|
||||
),
|
||||
):
|
||||
result = await hf_token_service.validate_stored_token()
|
||||
|
||||
assert result.valid is True, "validation should succeed"
|
||||
assert result.username == "validuser", "username should match validation result"
|
||||
mock_preferences.set.assert_called_once()
|
||||
prefs_mock_uow.commit.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_stored_token_no_token(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
) -> None:
|
||||
"""validate_stored_token returns error when no token configured."""
|
||||
mock_preferences.get.return_value = None
|
||||
|
||||
result = await hf_token_service.validate_stored_token()
|
||||
|
||||
assert result.valid is False, "validation should fail when no token"
|
||||
assert "No token configured" in result.error_message, "error should explain missing token"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_stored_token_no_preferences_support(
|
||||
hf_token_service_no_prefs: HfTokenService,
|
||||
) -> None:
|
||||
"""validate_stored_token returns error when preferences not supported."""
|
||||
result = await hf_token_service_no_prefs.validate_stored_token()
|
||||
|
||||
assert result.valid is False, "validation should fail without prefs"
|
||||
assert "not available" in result.error_message, "error should explain unavailability"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_stored_token_decrypt_failure(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
prefs_mock_uow: MagicMock,
|
||||
) -> None:
|
||||
"""validate_stored_token returns error when decryption fails."""
|
||||
mock_preferences.get.return_value = {
|
||||
"wrapped_dek": "abc",
|
||||
"nonce": "def",
|
||||
"ciphertext": "ghi",
|
||||
"tag": "jkl",
|
||||
}
|
||||
|
||||
with patch.object(
|
||||
hf_token_service,
|
||||
"decrypt_token",
|
||||
side_effect=ValueError("bad token"),
|
||||
):
|
||||
result = await hf_token_service.validate_stored_token()
|
||||
|
||||
assert result.valid is False, "validation should fail when decryption fails"
|
||||
assert "decryption" in result.error_message.lower(), (
|
||||
"error should mention decryption failure"
|
||||
)
|
||||
mock_preferences.set.assert_not_called()
|
||||
prefs_mock_uow.commit.assert_not_called()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# get_token tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_token_returns_decrypted(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
) -> None:
|
||||
"""get_token returns decrypted token when configured."""
|
||||
mock_preferences.get.return_value = {
|
||||
"wrapped_dek": "abc",
|
||||
"nonce": "def",
|
||||
"ciphertext": "ghi",
|
||||
"tag": "jkl",
|
||||
}
|
||||
|
||||
with patch.object(
|
||||
hf_token_service,
|
||||
"decrypt_token",
|
||||
return_value="hf_test_token_value",
|
||||
):
|
||||
token = await hf_token_service.get_token()
|
||||
|
||||
assert token == "hf_test_token_value", "get_token should return decrypted token"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_token_returns_none_when_not_configured(
|
||||
hf_token_service: HfTokenService,
|
||||
mock_preferences: MagicMock,
|
||||
) -> None:
|
||||
"""get_token returns None when no token configured."""
|
||||
mock_preferences.get.return_value = None
|
||||
|
||||
token = await hf_token_service.get_token()
|
||||
|
||||
assert token is None, "get_token should return None when not configured"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_token_no_preferences_support(
|
||||
hf_token_service_no_prefs: HfTokenService,
|
||||
) -> None:
|
||||
"""get_token returns None when preferences not supported."""
|
||||
token = await hf_token_service_no_prefs.get_token()
|
||||
|
||||
assert token is None, "get_token should return None without prefs support"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Encryption roundtrip tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def test_encrypt_token_returns_expected_keys(
|
||||
hf_token_service: HfTokenService,
|
||||
) -> None:
|
||||
"""encrypt_token returns dict with required keys."""
|
||||
result = hf_token_service.encrypt_token("test_token")
|
||||
|
||||
assert "wrapped_dek" in result, "wrapped_dek should be present in encrypted output"
|
||||
assert "nonce" in result, "nonce should be present in encrypted output"
|
||||
assert "ciphertext" in result, "ciphertext should be present in encrypted output"
|
||||
assert "tag" in result, "tag should be present in encrypted output"
|
||||
|
||||
|
||||
def test_decrypt_token_invalid_format(
|
||||
hf_token_service: HfTokenService,
|
||||
) -> None:
|
||||
"""decrypt_token raises ValueError for invalid format."""
|
||||
with pytest.raises(ValueError, match="Invalid encrypted data format"):
|
||||
hf_token_service.decrypt_token("not_a_dict")
|
||||
|
||||
|
||||
def test_decrypt_token_missing_keys(
|
||||
hf_token_service: HfTokenService,
|
||||
) -> None:
|
||||
"""decrypt_token raises ValueError for missing keys."""
|
||||
with pytest.raises(ValueError, match="Invalid encrypted data"):
|
||||
hf_token_service.decrypt_token({"wrapped_dek": "abc"})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# validate_token_internal tests (mocked HTTP)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_token_internal_success(
|
||||
hf_token_service: HfTokenService,
|
||||
) -> None:
|
||||
"""validate_token_internal returns success for valid token."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"name": "apiuser"}
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
||||
|
||||
result = await hf_token_service.validate_token_internal("hf_valid")
|
||||
|
||||
assert result.valid is True, "token should be valid"
|
||||
assert result.username == "apiuser", "username should match API response"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_token_internal_unauthorized(
|
||||
hf_token_service: HfTokenService,
|
||||
) -> None:
|
||||
"""validate_token_internal returns failure for 401 response."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 401
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
||||
|
||||
result = await hf_token_service.validate_token_internal("hf_invalid")
|
||||
|
||||
assert result.valid is False, "token should be invalid for 401"
|
||||
assert "Invalid or expired" in result.error_message, "error should mention invalid token"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_token_internal_timeout(
|
||||
hf_token_service: HfTokenService,
|
||||
) -> None:
|
||||
"""validate_token_internal handles timeout gracefully."""
|
||||
import httpx
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
||||
side_effect=httpx.TimeoutException("timeout")
|
||||
)
|
||||
|
||||
result = await hf_token_service.validate_token_internal("hf_token")
|
||||
|
||||
assert result.valid is False, "timeout should return invalid result"
|
||||
assert "timeout" in result.error_message.lower(), "error should mention timeout"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_token_internal_connection_error(
|
||||
hf_token_service: HfTokenService,
|
||||
) -> None:
|
||||
"""validate_token_internal handles connection error gracefully."""
|
||||
import httpx
|
||||
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
||||
side_effect=httpx.RequestError("connection failed")
|
||||
)
|
||||
|
||||
result = await hf_token_service.validate_token_internal("hf_token")
|
||||
|
||||
assert result.valid is False, "connection error should return invalid result"
|
||||
assert "Connection error" in result.error_message, "error should mention connection error"
|
||||
225
tests/integration/test_asr_config_grpc.py
Normal file
225
tests/integration/test_asr_config_grpc.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""Integration tests for ASR configuration gRPC endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Final
|
||||
from unittest.mock import MagicMock, patch
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
from noteflow.grpc.proto import noteflow_pb2
|
||||
from noteflow.grpc.service import NoteFlowServicer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
# ============================================================================
|
||||
# Test Constants
|
||||
# ============================================================================
|
||||
|
||||
MODEL_BASE: Final[str] = "base"
|
||||
MODEL_SMALL: Final[str] = "small"
|
||||
DEVICE_CPU: Final[str] = "cpu"
|
||||
COMPUTE_INT8: Final[str] = "int8"
|
||||
INVALID_JOB_ID: Final[str] = "not-a-uuid"
|
||||
ACTIVE_STREAM_ID: Final[str] = "meeting-active"
|
||||
ERROR_ACTIVE_RECORDINGS: Final[str] = "Cannot reconfigure ASR while recordings are active"
|
||||
ERROR_INVALID_JOB_ID: Final[str] = "Invalid job ID format"
|
||||
PHASE_FAILED: Final[str] = "failed"
|
||||
EMPTY_JOB_ID: Final[str] = ""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def asr_engine_mock() -> MagicMock:
|
||||
"""Create mock ASR engine with CPU defaults."""
|
||||
engine = MagicMock()
|
||||
engine.model_size = MODEL_BASE
|
||||
engine.device = DEVICE_CPU
|
||||
engine.compute_type = COMPUTE_INT8
|
||||
engine.is_loaded = True
|
||||
engine.load_model = MagicMock()
|
||||
engine.unload = MagicMock()
|
||||
return engine
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def asr_servicer(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
meetings_dir: Path,
|
||||
asr_engine_mock: MagicMock,
|
||||
) -> NoteFlowServicer:
|
||||
"""Create servicer with ASR engine and database backing."""
|
||||
return NoteFlowServicer(
|
||||
asr_engine=asr_engine_mock,
|
||||
session_factory=session_factory,
|
||||
meetings_dir=meetings_dir,
|
||||
)
|
||||
|
||||
|
||||
async def _run_asr_update_and_wait(
|
||||
servicer: NoteFlowServicer,
|
||||
model_size: str,
|
||||
context: MagicMock,
|
||||
) -> str:
|
||||
"""Start an ASR update job and wait for completion."""
|
||||
service = servicer.asr_config_service
|
||||
assert service is not None, "asr_config_service should be initialized"
|
||||
|
||||
async def _load_model_stub(engine: MagicMock, size: str) -> None:
|
||||
engine.model_size = size
|
||||
|
||||
with patch.object(service, "_load_model", new=_load_model_stub):
|
||||
response = await servicer.UpdateAsrConfiguration(
|
||||
noteflow_pb2.UpdateAsrConfigurationRequest(model_size=model_size),
|
||||
context,
|
||||
)
|
||||
|
||||
assert response.accepted is True, "update should be accepted"
|
||||
assert (
|
||||
response.status == noteflow_pb2.JOB_STATUS_QUEUED
|
||||
), "initial job status should be QUEUED"
|
||||
assert response.job_id, "job_id should be returned for accepted request"
|
||||
|
||||
job = service.get_job_status(UUID(response.job_id))
|
||||
assert job is not None, "job should be retrievable by id"
|
||||
assert job.task is not None, "job task should be created"
|
||||
await job.task
|
||||
|
||||
return response.job_id
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAsrConfigGrpc:
|
||||
"""Integration tests for ASR config gRPC handlers."""
|
||||
|
||||
async def test_get_asr_configuration_returns_engine_state(
|
||||
self,
|
||||
asr_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""GetAsrConfiguration reflects current engine configuration."""
|
||||
service = asr_servicer.asr_config_service
|
||||
assert service is not None, "asr_config_service should be initialized"
|
||||
|
||||
with patch.object(service, "detect_cuda_available", return_value=False):
|
||||
response = await asr_servicer.GetAsrConfiguration(
|
||||
noteflow_pb2.GetAsrConfigurationRequest(),
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
config = response.configuration
|
||||
assert config.model_size == MODEL_BASE, "model_size should match engine"
|
||||
assert (
|
||||
config.device == noteflow_pb2.ASR_DEVICE_CPU
|
||||
), "device should map to ASR_DEVICE_CPU"
|
||||
assert (
|
||||
config.compute_type == noteflow_pb2.ASR_COMPUTE_TYPE_INT8
|
||||
), "compute_type should map to ASR_COMPUTE_TYPE_INT8"
|
||||
assert config.is_ready is True, "is_ready should reflect engine readiness"
|
||||
assert config.cuda_available is False, "cuda_available should reflect detection"
|
||||
assert (
|
||||
MODEL_BASE in config.available_model_sizes
|
||||
), "available_model_sizes should include current model"
|
||||
assert (
|
||||
noteflow_pb2.ASR_COMPUTE_TYPE_INT8 in config.available_compute_types
|
||||
), "available_compute_types should include INT8"
|
||||
|
||||
async def test_update_asr_configuration_completes_job_and_returns_new_config(
|
||||
self,
|
||||
asr_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""UpdateAsrConfiguration starts job and returns new configuration on completion."""
|
||||
job_id = await _run_asr_update_and_wait(
|
||||
asr_servicer,
|
||||
MODEL_SMALL,
|
||||
mock_grpc_context,
|
||||
)
|
||||
status_response = await asr_servicer.GetAsrConfigurationJobStatus(
|
||||
noteflow_pb2.GetAsrConfigurationJobStatusRequest(job_id=job_id),
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert (
|
||||
status_response.status == noteflow_pb2.JOB_STATUS_COMPLETED
|
||||
), "job status should be COMPLETED after reload"
|
||||
assert status_response.HasField("new_configuration"), (
|
||||
"new_configuration should be returned on completion"
|
||||
)
|
||||
assert (
|
||||
status_response.new_configuration.model_size == MODEL_SMALL
|
||||
), "new_configuration should reflect updated model size"
|
||||
|
||||
async def test_update_asr_configuration_rejects_when_active_recordings(
|
||||
self,
|
||||
asr_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""UpdateAsrConfiguration rejects while recordings are active."""
|
||||
asr_servicer.active_streams = {ACTIVE_STREAM_ID}
|
||||
|
||||
response = await asr_servicer.UpdateAsrConfiguration(
|
||||
noteflow_pb2.UpdateAsrConfigurationRequest(model_size=MODEL_SMALL),
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert response.accepted is False, "update should be rejected when active"
|
||||
assert (
|
||||
response.status == noteflow_pb2.JOB_STATUS_FAILED
|
||||
), "status should be FAILED when rejected"
|
||||
assert response.error_message == ERROR_ACTIVE_RECORDINGS, (
|
||||
"error_message should explain active recording rejection"
|
||||
)
|
||||
assert response.job_id == EMPTY_JOB_ID, "job_id should be empty on rejection"
|
||||
|
||||
async def test_get_asr_configuration_job_status_invalid_id(
|
||||
self,
|
||||
asr_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""GetAsrConfigurationJobStatus returns failure for invalid job ID."""
|
||||
response = await asr_servicer.GetAsrConfigurationJobStatus(
|
||||
noteflow_pb2.GetAsrConfigurationJobStatusRequest(job_id=INVALID_JOB_ID),
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert (
|
||||
response.status == noteflow_pb2.JOB_STATUS_FAILED
|
||||
), "invalid job id should produce FAILED status"
|
||||
assert response.phase == PHASE_FAILED, "phase should be failed for invalid job id"
|
||||
assert response.error_message == ERROR_INVALID_JOB_ID, (
|
||||
"error_message should explain invalid job id format"
|
||||
)
|
||||
|
||||
async def test_get_asr_configuration_when_service_unavailable(
|
||||
self,
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
meetings_dir: Path,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""GetAsrConfiguration returns empty configuration when ASR is unavailable."""
|
||||
servicer = NoteFlowServicer(session_factory=session_factory, meetings_dir=meetings_dir)
|
||||
|
||||
response = await servicer.GetAsrConfiguration(
|
||||
noteflow_pb2.GetAsrConfigurationRequest(),
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
config = response.configuration
|
||||
assert config.model_size == "", "model_size should be empty when ASR unavailable"
|
||||
assert (
|
||||
config.device == noteflow_pb2.ASR_DEVICE_UNSPECIFIED
|
||||
), "device should be unspecified when ASR unavailable"
|
||||
assert (
|
||||
config.compute_type == noteflow_pb2.ASR_COMPUTE_TYPE_UNSPECIFIED
|
||||
), "compute_type should be unspecified when ASR unavailable"
|
||||
assert config.is_ready is False, "is_ready should be False when ASR unavailable"
|
||||
assert config.cuda_available is False, "cuda_available should be False by default"
|
||||
assert (
|
||||
config.available_model_sizes == []
|
||||
), "available_model_sizes should be empty when ASR unavailable"
|
||||
assert (
|
||||
config.available_compute_types == []
|
||||
), "available_compute_types should be empty when ASR unavailable"
|
||||
259
tests/integration/test_hf_token_grpc.py
Normal file
259
tests/integration/test_hf_token_grpc.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""Integration tests for HuggingFace token gRPC endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Final
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from noteflow.application.services.hf_token_service import HfTokenService, HfValidationResult
|
||||
from noteflow.grpc.proto import noteflow_pb2
|
||||
from noteflow.grpc.service import NoteFlowServicer
|
||||
from noteflow.infrastructure.security.crypto import AesGcmCryptoBox
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
# ============================================================================
|
||||
# Test Constants
|
||||
# ============================================================================
|
||||
|
||||
TOKEN_VALUE: Final[str] = "hf_test_token"
|
||||
TOKEN_VALUE_SECOND: Final[str] = "hf_second_token"
|
||||
USERNAME_PRIMARY: Final[str] = "primary-user"
|
||||
USERNAME_VALIDATED: Final[str] = "validated-user"
|
||||
EMPTY_TOKEN: Final[str] = ""
|
||||
EMPTY_USERNAME: Final[str] = ""
|
||||
ERROR_EMPTY_TOKEN: Final[str] = "Token cannot be empty"
|
||||
ZERO_FLOAT: Final[float] = 0.0
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def hf_token_servicer(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
meetings_dir: Path,
|
||||
crypto: AesGcmCryptoBox,
|
||||
) -> NoteFlowServicer:
|
||||
"""Create servicer with HuggingFace token service using in-memory crypto."""
|
||||
servicer = NoteFlowServicer(session_factory=session_factory, meetings_dir=meetings_dir)
|
||||
servicer.crypto = crypto
|
||||
servicer.hf_token_service = HfTokenService(uow_factory=servicer.create_uow, crypto=crypto)
|
||||
return servicer
|
||||
|
||||
|
||||
async def _set_hf_token(
|
||||
servicer: NoteFlowServicer,
|
||||
token: str,
|
||||
validate: bool,
|
||||
context: MagicMock,
|
||||
) -> noteflow_pb2.SetHuggingFaceTokenResponse:
|
||||
"""Invoke SetHuggingFaceToken for test helpers."""
|
||||
return await servicer.SetHuggingFaceToken(
|
||||
noteflow_pb2.SetHuggingFaceTokenRequest(token=token, validate=validate),
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
async def _get_hf_status(
|
||||
servicer: NoteFlowServicer,
|
||||
context: MagicMock,
|
||||
) -> noteflow_pb2.GetHuggingFaceTokenStatusResponse:
|
||||
"""Invoke GetHuggingFaceTokenStatus for test helpers."""
|
||||
return await servicer.GetHuggingFaceTokenStatus(
|
||||
noteflow_pb2.GetHuggingFaceTokenStatusRequest(),
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
async def _validate_hf_token(
|
||||
servicer: NoteFlowServicer,
|
||||
context: MagicMock,
|
||||
) -> noteflow_pb2.ValidateHuggingFaceTokenResponse:
|
||||
"""Invoke ValidateHuggingFaceToken for test helpers."""
|
||||
return await servicer.ValidateHuggingFaceToken(
|
||||
noteflow_pb2.ValidateHuggingFaceTokenRequest(),
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
async def _delete_hf_token(
|
||||
servicer: NoteFlowServicer,
|
||||
context: MagicMock,
|
||||
) -> noteflow_pb2.DeleteHuggingFaceTokenResponse:
|
||||
"""Invoke DeleteHuggingFaceToken for test helpers."""
|
||||
return await servicer.DeleteHuggingFaceToken(
|
||||
noteflow_pb2.DeleteHuggingFaceTokenRequest(),
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
async def _set_unvalidated_token_and_assert(
|
||||
servicer: NoteFlowServicer,
|
||||
context: MagicMock,
|
||||
) -> None:
|
||||
"""Set a token without validation and assert initial status."""
|
||||
set_response = await _set_hf_token(
|
||||
servicer,
|
||||
TOKEN_VALUE_SECOND,
|
||||
False,
|
||||
context,
|
||||
)
|
||||
|
||||
assert set_response.success is True, "set token without validation should succeed"
|
||||
|
||||
status_before = await _get_hf_status(servicer, context)
|
||||
|
||||
assert status_before.is_configured is True, "token should be configured after set"
|
||||
assert status_before.is_validated is False, "token should be unvalidated before validate"
|
||||
assert status_before.username == EMPTY_USERNAME, "username should be empty before validate"
|
||||
assert status_before.validated_at == ZERO_FLOAT, "validated_at should be unset before validate"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestHuggingFaceTokenGrpc:
|
||||
"""Integration tests for HuggingFace token gRPC handlers."""
|
||||
|
||||
async def test_set_token_returns_validated_response(
|
||||
self,
|
||||
hf_token_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""SetHuggingFaceToken returns validated response when validation succeeds."""
|
||||
service = hf_token_servicer.hf_token_service
|
||||
assert service is not None, "hf_token_service should be initialized"
|
||||
|
||||
with patch.object(
|
||||
service,
|
||||
"validate_token_internal",
|
||||
return_value=HfValidationResult(
|
||||
valid=True,
|
||||
username=USERNAME_PRIMARY,
|
||||
error_message="",
|
||||
),
|
||||
):
|
||||
response = await _set_hf_token(
|
||||
hf_token_servicer,
|
||||
TOKEN_VALUE,
|
||||
True,
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert response.success is True, "set token should succeed"
|
||||
assert response.valid is True, "validated token should return valid=True"
|
||||
assert response.username == USERNAME_PRIMARY, "username should match validation"
|
||||
|
||||
async def test_get_status_reflects_validated_token(
|
||||
self,
|
||||
hf_token_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""GetHuggingFaceTokenStatus reflects validated token metadata."""
|
||||
service = hf_token_servicer.hf_token_service
|
||||
assert service is not None, "hf_token_service should be initialized"
|
||||
|
||||
with patch.object(
|
||||
service,
|
||||
"validate_token_internal",
|
||||
return_value=HfValidationResult(
|
||||
valid=True,
|
||||
username=USERNAME_PRIMARY,
|
||||
error_message="",
|
||||
),
|
||||
):
|
||||
set_response = await _set_hf_token(
|
||||
hf_token_servicer,
|
||||
TOKEN_VALUE,
|
||||
True,
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert set_response.success is True, "set token should succeed"
|
||||
|
||||
status = await _get_hf_status(hf_token_servicer, mock_grpc_context)
|
||||
|
||||
assert status.is_configured is True, "status should indicate token configured"
|
||||
assert status.is_validated is True, "status should indicate token validated"
|
||||
assert status.username == USERNAME_PRIMARY, "status username should match"
|
||||
assert status.validated_at > ZERO_FLOAT, "validated_at should be set"
|
||||
|
||||
async def test_validate_stored_token_updates_status(
|
||||
self,
|
||||
hf_token_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""ValidateHuggingFaceToken updates status for stored token."""
|
||||
service = hf_token_servicer.hf_token_service
|
||||
assert service is not None, "hf_token_service should be initialized"
|
||||
await _set_unvalidated_token_and_assert(hf_token_servicer, mock_grpc_context)
|
||||
|
||||
with patch.object(
|
||||
service,
|
||||
"validate_token_internal",
|
||||
return_value=HfValidationResult(
|
||||
valid=True,
|
||||
username=USERNAME_VALIDATED,
|
||||
error_message="",
|
||||
),
|
||||
):
|
||||
validate_response = await _validate_hf_token(
|
||||
hf_token_servicer,
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert validate_response.valid is True, "validate token should succeed"
|
||||
assert validate_response.username == USERNAME_VALIDATED, "validated username should match"
|
||||
|
||||
status_after = await _get_hf_status(hf_token_servicer, mock_grpc_context)
|
||||
|
||||
assert status_after.is_validated is True, "status should be validated after validation"
|
||||
assert status_after.username == USERNAME_VALIDATED, "status username should update"
|
||||
assert status_after.validated_at > ZERO_FLOAT, "validated_at should be set after validate"
|
||||
|
||||
async def test_delete_token_clears_status(
|
||||
self,
|
||||
hf_token_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""DeleteHuggingFaceToken removes token and resets status."""
|
||||
set_response = await _set_hf_token(
|
||||
hf_token_servicer,
|
||||
TOKEN_VALUE,
|
||||
False,
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert set_response.success is True, "set token should succeed before delete"
|
||||
|
||||
delete_response = await _delete_hf_token(
|
||||
hf_token_servicer,
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert delete_response.success is True, "delete token should succeed"
|
||||
|
||||
status = await _get_hf_status(hf_token_servicer, mock_grpc_context)
|
||||
|
||||
assert status.is_configured is False, "token should be cleared after delete"
|
||||
assert status.is_validated is False, "validation should reset after delete"
|
||||
assert status.username == EMPTY_USERNAME, "username should be empty after delete"
|
||||
assert status.validated_at == ZERO_FLOAT, "validated_at should reset after delete"
|
||||
|
||||
async def test_set_token_rejects_empty_value(
|
||||
self,
|
||||
hf_token_servicer: NoteFlowServicer,
|
||||
mock_grpc_context: MagicMock,
|
||||
) -> None:
|
||||
"""SetHuggingFaceToken rejects empty token values."""
|
||||
response = await _set_hf_token(
|
||||
hf_token_servicer,
|
||||
EMPTY_TOKEN,
|
||||
True,
|
||||
mock_grpc_context,
|
||||
)
|
||||
|
||||
assert response.success is False, "empty token should be rejected"
|
||||
assert response.validation_error == ERROR_EMPTY_TOKEN, (
|
||||
"validation_error should explain empty token rejection"
|
||||
)
|
||||
Reference in New Issue
Block a user