teams-transcripts 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- legacy/__init__.py +0 -0
- legacy/transcript_download.py +3969 -0
- teams_transcripts/__init__.py +1 -0
- teams_transcripts/__main__.py +288 -0
- teams_transcripts/adapters/__init__.py +1 -0
- teams_transcripts/adapters/cdp/__init__.py +1 -0
- teams_transcripts/adapters/cdp/browser.py +196 -0
- teams_transcripts/adapters/cdp/connection.py +168 -0
- teams_transcripts/adapters/cdp/helpers.py +449 -0
- teams_transcripts/adapters/cdp/teams.py +463 -0
- teams_transcripts/adapters/vertex.py +274 -0
- teams_transcripts/domain/__init__.py +1 -0
- teams_transcripts/domain/chat.py +115 -0
- teams_transcripts/domain/html.py +126 -0
- teams_transcripts/domain/mcps.py +343 -0
- teams_transcripts/domain/models.py +531 -0
- teams_transcripts/domain/participants.py +292 -0
- teams_transcripts/domain/scoring.py +106 -0
- teams_transcripts/domain/speakers.py +130 -0
- teams_transcripts/domain/summary.py +266 -0
- teams_transcripts/domain/timestamps.py +350 -0
- teams_transcripts/domain/vtt.py +444 -0
- teams_transcripts/infrastructure/__init__.py +1 -0
- teams_transcripts/infrastructure/config.py +987 -0
- teams_transcripts/infrastructure/errors.py +120 -0
- teams_transcripts/infrastructure/logging.py +69 -0
- teams_transcripts/infrastructure/signals.py +143 -0
- teams_transcripts/mcp_server.py +818 -0
- teams_transcripts/ports/__init__.py +1 -0
- teams_transcripts/ports/agent.py +58 -0
- teams_transcripts/ports/browser.py +126 -0
- teams_transcripts/services/__init__.py +1 -0
- teams_transcripts/services/downloader.py +1283 -0
- teams_transcripts/services/extraction.py +1603 -0
- teams_transcripts/services/listing.py +826 -0
- teams_transcripts/services/navigation.py +1721 -0
- teams_transcripts/services/output.py +84 -0
- teams_transcripts/services/tenant.py +684 -0
- teams_transcripts-0.5.0.dist-info/METADATA +1438 -0
- teams_transcripts-0.5.0.dist-info/RECORD +42 -0
- teams_transcripts-0.5.0.dist-info/WHEEL +4 -0
- teams_transcripts-0.5.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Exit codes, error codes, and error-handling utilities.
|
|
2
|
+
|
|
3
|
+
Replaces the module-level exit code constants and ``_exit_error()`` function
|
|
4
|
+
from the original ``transcript_download.py``.
|
|
5
|
+
|
|
6
|
+
Key design change: ``exit_error()`` takes ``format_json`` as an explicit
|
|
7
|
+
parameter instead of reading the global ``_format_json`` flag. This makes
|
|
8
|
+
the function pure with respect to configuration -- the caller (services
|
|
9
|
+
layer or ``__main__``) decides whether JSON output is active.
|
|
10
|
+
|
|
11
|
+
Schema version: 1.0.0
|
|
12
|
+
Created: 2026-04-25
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
import typing
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Exit codes (matching POSIX conventions)
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
EXIT_OK: int = 0
|
|
26
|
+
"""Successful completion."""
|
|
27
|
+
|
|
28
|
+
EXIT_BAD_ARGS: int = 1
|
|
29
|
+
"""Invalid CLI arguments or missing configuration."""
|
|
30
|
+
|
|
31
|
+
EXIT_CDP_UNAVAILABLE: int = 2
|
|
32
|
+
"""Teams/CDP not running or not responding."""
|
|
33
|
+
|
|
34
|
+
EXIT_MEETING_NOT_FOUND: int = 3
|
|
35
|
+
"""Meeting search failed after all retries."""
|
|
36
|
+
|
|
37
|
+
EXIT_DOWNLOAD_FAILED: int = 4
|
|
38
|
+
"""Transcript download or permission error."""
|
|
39
|
+
|
|
40
|
+
EXIT_VERIFICATION_FAILED: int = 5
|
|
41
|
+
"""Output file invalid."""
|
|
42
|
+
|
|
43
|
+
EXIT_TENANT_NOT_FOUND: int = 6
|
|
44
|
+
"""Requested tenant does not match the active Teams tenant."""
|
|
45
|
+
|
|
46
|
+
EXIT_INTERRUPTED: int = 130
|
|
47
|
+
"""Interrupted by SIGINT (128 + 2)."""
|
|
48
|
+
|
|
49
|
+
EXIT_TERMINATED: int = 143
|
|
50
|
+
"""Terminated by SIGTERM (128 + 15)."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Error codes (machine-stable identifiers)
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# These are string constants used in JSON output ``code`` fields and stderr
|
|
57
|
+
# ``[PREFIX]`` tags. They are plain strings rather than an enum because
|
|
58
|
+
# they must serialise directly into JSON without conversion.
|
|
59
|
+
|
|
60
|
+
ERROR_BAD_ARGS: str = "BAD_ARGS"
|
|
61
|
+
ERROR_CDP_UNAVAILABLE: str = "CDP_UNAVAILABLE"
|
|
62
|
+
ERROR_NO_TARGETS: str = "NO_TARGETS"
|
|
63
|
+
ERROR_SEARCH_FAILED: str = "SEARCH_FAILED"
|
|
64
|
+
ERROR_MEETING_NOT_FOUND: str = "MEETING_NOT_FOUND"
|
|
65
|
+
ERROR_RECAP_NOT_FOUND: str = "RECAP_NOT_FOUND"
|
|
66
|
+
ERROR_TRANSCRIPT_NOT_FOUND: str = "TRANSCRIPT_NOT_FOUND"
|
|
67
|
+
ERROR_IFRAME_NOT_FOUND: str = "IFRAME_NOT_FOUND"
|
|
68
|
+
ERROR_DOWNLOAD_FAILED: str = "DOWNLOAD_FAILED"
|
|
69
|
+
ERROR_VERIFICATION_FAILED: str = "VERIFICATION_FAILED"
|
|
70
|
+
ERROR_TENANT_NOT_FOUND: str = "TENANT_NOT_FOUND"
|
|
71
|
+
ERROR_RESTART_BLOCKED: str = "RESTART_BLOCKED"
|
|
72
|
+
ERROR_INTERRUPTED: str = "INTERRUPTED"
|
|
73
|
+
ERROR_TERMINATED: str = "TERMINATED"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Error output
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def exit_error(
|
|
82
|
+
code: int,
|
|
83
|
+
error_code: str,
|
|
84
|
+
message: str,
|
|
85
|
+
*,
|
|
86
|
+
step: str | None = None,
|
|
87
|
+
format_json: bool = False,
|
|
88
|
+
) -> typing.NoReturn:
|
|
89
|
+
"""Emit an actionable error and terminate the process.
|
|
90
|
+
|
|
91
|
+
Always prints a human-readable ``[ERROR_CODE] message`` to stderr.
|
|
92
|
+
When *format_json* is ``True``, also emits a JSON error object to
|
|
93
|
+
stdout so that calling agents can parse the failure without scraping
|
|
94
|
+
stderr.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
code: Numeric exit code (one of the ``EXIT_*`` constants).
|
|
98
|
+
error_code: Short machine-stable identifier (one of the
|
|
99
|
+
``ERROR_*`` constants).
|
|
100
|
+
message: Human-readable error description.
|
|
101
|
+
step: Optional pipeline step identifier (e.g. ``"step7"``).
|
|
102
|
+
Included in the JSON payload when truthy.
|
|
103
|
+
format_json: Whether to emit a JSON error object to stdout.
|
|
104
|
+
"""
|
|
105
|
+
# Human-readable on stderr (always)
|
|
106
|
+
prefix = f"[{error_code}]"
|
|
107
|
+
print(f"{prefix} {message}", file=sys.stderr, flush=True)
|
|
108
|
+
|
|
109
|
+
# Machine-readable on stdout (when JSON output is requested)
|
|
110
|
+
if format_json:
|
|
111
|
+
payload: dict[str, object] = {
|
|
112
|
+
"status": "error",
|
|
113
|
+
"code": error_code,
|
|
114
|
+
"message": message,
|
|
115
|
+
}
|
|
116
|
+
if step:
|
|
117
|
+
payload["step"] = step
|
|
118
|
+
print(json.dumps(payload, ensure_ascii=False), flush=True)
|
|
119
|
+
|
|
120
|
+
sys.exit(code)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Structured logging for the transcript pipeline.
|
|
2
|
+
|
|
3
|
+
Replaces the module-level ``log()`` function and the ``_quiet`` / ``_verbose``
|
|
4
|
+
global flags from the original ``transcript_download.py``.
|
|
5
|
+
|
|
6
|
+
The :class:`Logger` class encapsulates output control as instance state,
|
|
7
|
+
making it injectable and testable without patching module globals.
|
|
8
|
+
|
|
9
|
+
Schema version: 1.0.0
|
|
10
|
+
Created: 2026-04-25
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Logger:
|
|
19
|
+
"""Structured logger that writes ``[step] message`` lines to stderr.
|
|
20
|
+
|
|
21
|
+
All diagnostic output goes to stderr so that stdout is reserved for
|
|
22
|
+
data (VTT content or JSON).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
quiet: Suppress all diagnostic output. Errors and structured
|
|
26
|
+
output are still emitted via ``exit_error()``, not through
|
|
27
|
+
this logger.
|
|
28
|
+
verbose: Enable additional diagnostic detail (CDP payloads,
|
|
29
|
+
agent prompts, data sizes). Has no effect when *quiet*
|
|
30
|
+
is ``True``.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, quiet: bool = False, verbose: bool = False) -> None:
|
|
34
|
+
self._quiet = quiet
|
|
35
|
+
self._verbose = verbose
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def quiet(self) -> bool:
|
|
39
|
+
"""Whether diagnostic output is suppressed."""
|
|
40
|
+
return self._quiet
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def verbose(self) -> bool:
|
|
44
|
+
"""Whether verbose diagnostic output is enabled."""
|
|
45
|
+
return self._verbose
|
|
46
|
+
|
|
47
|
+
def log(self, step: str, msg: str) -> None:
|
|
48
|
+
"""Print a structured log line to stderr.
|
|
49
|
+
|
|
50
|
+
Suppressed entirely when ``quiet`` is active.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
step: Pipeline step identifier (e.g. ``"step1"``, ``"step7b"``).
|
|
54
|
+
msg: Human-readable diagnostic message.
|
|
55
|
+
"""
|
|
56
|
+
if not self._quiet:
|
|
57
|
+
print(f"[{step}] {msg}", file=sys.stderr, flush=True)
|
|
58
|
+
|
|
59
|
+
def verbose_log(self, step: str, msg: str) -> None:
|
|
60
|
+
"""Print a verbose diagnostic line to stderr.
|
|
61
|
+
|
|
62
|
+
Only emits when ``verbose`` is ``True`` and ``quiet`` is ``False``.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
step: Pipeline step identifier.
|
|
66
|
+
msg: Detailed diagnostic message.
|
|
67
|
+
"""
|
|
68
|
+
if self._verbose and not self._quiet:
|
|
69
|
+
print(f"[{step}] {msg}", file=sys.stderr, flush=True)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Signal handling and graceful shutdown coordination.
|
|
2
|
+
|
|
3
|
+
Replaces the module-level ``_shutdown_requested``, ``_shutdown_signal`` globals
|
|
4
|
+
and the ``_handle_signal``, ``_check_shutdown``, ``_interruptible_sleep``,
|
|
5
|
+
``_interruptible_sleep_sync``, ``_install_signal_handlers`` functions from the
|
|
6
|
+
original ``transcript_download.py``.
|
|
7
|
+
|
|
8
|
+
The :class:`ShutdownCoordinator` encapsulates all shutdown state and behaviour
|
|
9
|
+
as instance state, making it injectable and testable without patching globals.
|
|
10
|
+
It uses ``exit_error()`` from :mod:`infrastructure.errors` for structured error
|
|
11
|
+
output, with ``format_json`` passed at construction time.
|
|
12
|
+
|
|
13
|
+
Schema version: 1.0.0
|
|
14
|
+
Created: 2026-04-25
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import signal
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
from teams_transcripts.infrastructure.errors import (
|
|
25
|
+
EXIT_INTERRUPTED,
|
|
26
|
+
EXIT_TERMINATED,
|
|
27
|
+
exit_error,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ShutdownCoordinator:
|
|
32
|
+
"""Coordinates graceful shutdown on SIGINT/SIGTERM.
|
|
33
|
+
|
|
34
|
+
The first signal sets a flag so that the next loop iteration or
|
|
35
|
+
pipeline checkpoint exits cleanly. A second signal forces immediate
|
|
36
|
+
termination.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
format_json: Whether to emit JSON error output to stdout on
|
|
40
|
+
shutdown. Passed through to ``exit_error()``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, format_json: bool = False) -> None:
|
|
44
|
+
self._format_json = format_json
|
|
45
|
+
self._shutdown_requested = False
|
|
46
|
+
self._shutdown_signal: int | None = None
|
|
47
|
+
|
|
48
|
+
# -- Properties ----------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def shutdown_requested(self) -> bool:
|
|
52
|
+
"""Whether a shutdown signal has been received."""
|
|
53
|
+
return self._shutdown_requested
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def shutdown_signal(self) -> int | None:
|
|
57
|
+
"""The signal number that triggered shutdown, or ``None``."""
|
|
58
|
+
return self._shutdown_signal
|
|
59
|
+
|
|
60
|
+
# -- Signal handler ------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def handle_signal(self, signum: int, _frame: object) -> None:
|
|
63
|
+
"""Handle SIGINT/SIGTERM by setting the shutdown flag.
|
|
64
|
+
|
|
65
|
+
The first signal sets the flag so that the next checkpoint exits
|
|
66
|
+
cleanly. A second signal terminates immediately.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
signum: Signal number (``signal.SIGINT`` or ``signal.SIGTERM``).
|
|
70
|
+
_frame: Stack frame (unused, required by signal API).
|
|
71
|
+
"""
|
|
72
|
+
if self._shutdown_requested:
|
|
73
|
+
# Second signal -- force exit.
|
|
74
|
+
sys.exit(128 + signum)
|
|
75
|
+
|
|
76
|
+
self._shutdown_requested = True
|
|
77
|
+
self._shutdown_signal = signum
|
|
78
|
+
sig_name = "SIGINT" if signum == signal.SIGINT else "SIGTERM"
|
|
79
|
+
print(
|
|
80
|
+
f"[{sig_name}] Shutdown requested, finishing current step...",
|
|
81
|
+
file=sys.stderr,
|
|
82
|
+
flush=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# -- Shutdown check ------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def check_shutdown(self) -> None:
|
|
88
|
+
"""If a shutdown signal has been received, exit with the appropriate code.
|
|
89
|
+
|
|
90
|
+
Call this at the top of long-running loop iterations and between
|
|
91
|
+
pipeline stages.
|
|
92
|
+
"""
|
|
93
|
+
if not self._shutdown_requested:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
signum = self._shutdown_signal or signal.SIGINT
|
|
97
|
+
code = EXIT_INTERRUPTED if signum == signal.SIGINT else EXIT_TERMINATED
|
|
98
|
+
error_code = "INTERRUPTED" if signum == signal.SIGINT else "TERMINATED"
|
|
99
|
+
sig_name = "SIGINT" if signum == signal.SIGINT else "SIGTERM"
|
|
100
|
+
|
|
101
|
+
exit_error(
|
|
102
|
+
code,
|
|
103
|
+
error_code,
|
|
104
|
+
f"Process terminated by {sig_name}.",
|
|
105
|
+
format_json=self._format_json,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# -- Interruptible sleep -------------------------------------------------
|
|
109
|
+
|
|
110
|
+
async def interruptible_sleep(self, seconds: float) -> None:
|
|
111
|
+
"""Sleep in 0.5s increments, checking for shutdown between each.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
seconds: Total duration to sleep.
|
|
115
|
+
"""
|
|
116
|
+
remaining = seconds
|
|
117
|
+
while remaining > 0:
|
|
118
|
+
chunk = min(remaining, 0.5)
|
|
119
|
+
await asyncio.sleep(chunk)
|
|
120
|
+
remaining -= chunk
|
|
121
|
+
if self._shutdown_requested:
|
|
122
|
+
self.check_shutdown()
|
|
123
|
+
|
|
124
|
+
def interruptible_sleep_sync(self, seconds: float) -> None:
|
|
125
|
+
"""Synchronous equivalent of :meth:`interruptible_sleep`.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
seconds: Total duration to sleep.
|
|
129
|
+
"""
|
|
130
|
+
remaining = seconds
|
|
131
|
+
while remaining > 0:
|
|
132
|
+
chunk = min(remaining, 0.5)
|
|
133
|
+
time.sleep(chunk)
|
|
134
|
+
remaining -= chunk
|
|
135
|
+
if self._shutdown_requested:
|
|
136
|
+
self.check_shutdown()
|
|
137
|
+
|
|
138
|
+
# -- Signal registration -------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def install_signal_handlers(self) -> None:
|
|
141
|
+
"""Register this coordinator's handler for SIGINT and SIGTERM."""
|
|
142
|
+
signal.signal(signal.SIGINT, self.handle_signal)
|
|
143
|
+
signal.signal(signal.SIGTERM, self.handle_signal)
|