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.
Files changed (42) hide show
  1. legacy/__init__.py +0 -0
  2. legacy/transcript_download.py +3969 -0
  3. teams_transcripts/__init__.py +1 -0
  4. teams_transcripts/__main__.py +288 -0
  5. teams_transcripts/adapters/__init__.py +1 -0
  6. teams_transcripts/adapters/cdp/__init__.py +1 -0
  7. teams_transcripts/adapters/cdp/browser.py +196 -0
  8. teams_transcripts/adapters/cdp/connection.py +168 -0
  9. teams_transcripts/adapters/cdp/helpers.py +449 -0
  10. teams_transcripts/adapters/cdp/teams.py +463 -0
  11. teams_transcripts/adapters/vertex.py +274 -0
  12. teams_transcripts/domain/__init__.py +1 -0
  13. teams_transcripts/domain/chat.py +115 -0
  14. teams_transcripts/domain/html.py +126 -0
  15. teams_transcripts/domain/mcps.py +343 -0
  16. teams_transcripts/domain/models.py +531 -0
  17. teams_transcripts/domain/participants.py +292 -0
  18. teams_transcripts/domain/scoring.py +106 -0
  19. teams_transcripts/domain/speakers.py +130 -0
  20. teams_transcripts/domain/summary.py +266 -0
  21. teams_transcripts/domain/timestamps.py +350 -0
  22. teams_transcripts/domain/vtt.py +444 -0
  23. teams_transcripts/infrastructure/__init__.py +1 -0
  24. teams_transcripts/infrastructure/config.py +987 -0
  25. teams_transcripts/infrastructure/errors.py +120 -0
  26. teams_transcripts/infrastructure/logging.py +69 -0
  27. teams_transcripts/infrastructure/signals.py +143 -0
  28. teams_transcripts/mcp_server.py +818 -0
  29. teams_transcripts/ports/__init__.py +1 -0
  30. teams_transcripts/ports/agent.py +58 -0
  31. teams_transcripts/ports/browser.py +126 -0
  32. teams_transcripts/services/__init__.py +1 -0
  33. teams_transcripts/services/downloader.py +1283 -0
  34. teams_transcripts/services/extraction.py +1603 -0
  35. teams_transcripts/services/listing.py +826 -0
  36. teams_transcripts/services/navigation.py +1721 -0
  37. teams_transcripts/services/output.py +84 -0
  38. teams_transcripts/services/tenant.py +684 -0
  39. teams_transcripts-0.5.0.dist-info/METADATA +1438 -0
  40. teams_transcripts-0.5.0.dist-info/RECORD +42 -0
  41. teams_transcripts-0.5.0.dist-info/WHEEL +4 -0
  42. 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)