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,531 @@
|
|
|
1
|
+
"""Domain models -- immutable data classes for the transcript pipeline.
|
|
2
|
+
|
|
3
|
+
All models are plain data holders with no I/O, no network access, and no
|
|
4
|
+
dependency on infrastructure or adapters. They form the shared vocabulary
|
|
5
|
+
across the hexagonal architecture.
|
|
6
|
+
|
|
7
|
+
Schema version: 2.2.0
|
|
8
|
+
Created: 2026-04-25
|
|
9
|
+
Last updated: 2026-05-01 -- added DownloadInput Pydantic validation model
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import typing
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
19
|
+
|
|
20
|
+
if typing.TYPE_CHECKING:
|
|
21
|
+
import datetime as dt
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Transcript domain objects
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class TranscriptCue:
|
|
30
|
+
"""A structured transcript cue for JSON output.
|
|
31
|
+
|
|
32
|
+
Represents a single cue parsed from VTT content, with both formatted
|
|
33
|
+
timestamp strings and numeric seconds for programmatic consumption.
|
|
34
|
+
|
|
35
|
+
Fields:
|
|
36
|
+
id: Sequential cue identifier (1-based).
|
|
37
|
+
start: Start timestamp as VTT string (``HH:MM:SS.mmm``).
|
|
38
|
+
end: End timestamp as VTT string (``HH:MM:SS.mmm``).
|
|
39
|
+
start_sec: Start time in seconds (float).
|
|
40
|
+
end_sec: End time in seconds (float).
|
|
41
|
+
speaker: Speaker display name, or ``None`` when no ``<v>`` tag
|
|
42
|
+
is present on the cue.
|
|
43
|
+
text: Plain text content (voice tags stripped).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
id: int
|
|
47
|
+
start: str
|
|
48
|
+
end: str
|
|
49
|
+
start_sec: float
|
|
50
|
+
end_sec: float
|
|
51
|
+
speaker: str | None
|
|
52
|
+
text: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Segment:
|
|
57
|
+
"""A single transcript segment (cue) extracted from the Teams DOM.
|
|
58
|
+
|
|
59
|
+
Represents one speaker turn with timing information. Used as the
|
|
60
|
+
intermediate representation between DOM extraction and VTT generation.
|
|
61
|
+
|
|
62
|
+
Fields:
|
|
63
|
+
speaker: Display name of the speaker (may contain HTML entities
|
|
64
|
+
in raw form; cleaned before VTT output).
|
|
65
|
+
text: Spoken text content (may contain HTML in raw form).
|
|
66
|
+
start_sec: Start time in seconds from meeting start. May be
|
|
67
|
+
``None`` when the DOM element lacks timing data (forward-fill
|
|
68
|
+
is applied later).
|
|
69
|
+
end_sec: End time in seconds from meeting start. May be ``None``
|
|
70
|
+
(back-fill or default offset is applied later).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
speaker: str
|
|
74
|
+
text: str
|
|
75
|
+
start_sec: float | None = None
|
|
76
|
+
end_sec: float | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class RosterMember:
|
|
81
|
+
"""A member from the meeting roster popover.
|
|
82
|
+
|
|
83
|
+
Fields:
|
|
84
|
+
name: Display name (cleaned of suffixes like "(Guest)").
|
|
85
|
+
email: Email address. Empty string when unavailable.
|
|
86
|
+
object_id: Azure AD object ID (GUID). Empty string when unavailable.
|
|
87
|
+
role: Either ``"organiser"`` or ``"member"``.
|
|
88
|
+
rsvp_status: Calendar response status. One of ``"accepted"``,
|
|
89
|
+
``"tentative"``, ``"declined"``, ``"none"`` (no response),
|
|
90
|
+
or ``""`` (unknown / not available).
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
name: str
|
|
94
|
+
email: str = ""
|
|
95
|
+
object_id: str = ""
|
|
96
|
+
role: str = "member"
|
|
97
|
+
rsvp_status: str = ""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class MeetingParticipants:
|
|
102
|
+
"""Categorised meeting participant lists.
|
|
103
|
+
|
|
104
|
+
Combines data from multiple extraction sources (participant popover,
|
|
105
|
+
Recap Speakers tab, Event/Call ``<partlist>`` XML) into four
|
|
106
|
+
disjoint-ish categories:
|
|
107
|
+
|
|
108
|
+
- **organiser**: the meeting creator (usually 1, rarely 0).
|
|
109
|
+
- **invited**: everyone on the meeting chat thread (full membership).
|
|
110
|
+
- **speakers**: those who spoke during the meeting (from Recap).
|
|
111
|
+
- **attendees**: those who joined the call (from the Event/Call
|
|
112
|
+
partlist -- may be incomplete due to Teams truncation).
|
|
113
|
+
|
|
114
|
+
Note: ``speakers`` and ``attendees`` are subsets of ``invited``.
|
|
115
|
+
A person may appear in both ``speakers`` and ``attendees``.
|
|
116
|
+
|
|
117
|
+
Fields:
|
|
118
|
+
organiser: The meeting organiser(s). Usually a single-element
|
|
119
|
+
list; empty when the organiser cannot be identified.
|
|
120
|
+
invited: All members of the meeting chat thread, sorted by name.
|
|
121
|
+
speakers: Members who spoke, sorted by name.
|
|
122
|
+
attendees: Members who joined the call, sorted by name.
|
|
123
|
+
|
|
124
|
+
Schema version: 1.1.0
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
organiser: list[RosterMember] = field(default_factory=list)
|
|
128
|
+
invited: list[RosterMember] = field(default_factory=list)
|
|
129
|
+
speakers: list[RosterMember] = field(default_factory=list)
|
|
130
|
+
attendees: list[RosterMember] = field(default_factory=list)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class Reaction:
|
|
135
|
+
"""A reaction on a chat message.
|
|
136
|
+
|
|
137
|
+
Fields:
|
|
138
|
+
type: Emotion key (e.g. ``"like"``, ``"heart"``).
|
|
139
|
+
users: List of resolved display names. May be empty when
|
|
140
|
+
only a summary count is available.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
type: str
|
|
144
|
+
users: list[str] = field(default_factory=list)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class ChatMessage:
|
|
149
|
+
"""A chat message extracted from the meeting thread.
|
|
150
|
+
|
|
151
|
+
Fields:
|
|
152
|
+
timestamp: Local ISO 8601 datetime with timezone offset
|
|
153
|
+
(e.g. ``"2026-04-22T11:15:48+01:00"``).
|
|
154
|
+
author: Resolved display name of the message author.
|
|
155
|
+
text: Plain-text content (HTML stripped).
|
|
156
|
+
reactions: List of reactions, or ``None`` when no reactions exist.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
timestamp: str
|
|
160
|
+
author: str
|
|
161
|
+
text: str
|
|
162
|
+
reactions: list[Reaction] | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Verification result
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True)
|
|
171
|
+
class VerificationResult:
|
|
172
|
+
"""Result of verifying a VTT file's content.
|
|
173
|
+
|
|
174
|
+
Fields:
|
|
175
|
+
valid: Whether the content passed all checks.
|
|
176
|
+
cue_count: Number of cues found (0 when invalid).
|
|
177
|
+
speakers: Mapping of speaker name to cue count. Empty when invalid.
|
|
178
|
+
error: Human-readable error description. Empty string when valid.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
valid: bool
|
|
182
|
+
cue_count: int = 0
|
|
183
|
+
speakers: dict[str, int] = field(default_factory=dict)
|
|
184
|
+
error: str = ""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# Result summary
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class ResultSummary:
|
|
194
|
+
"""Structured result summary for JSON output and dry-run display.
|
|
195
|
+
|
|
196
|
+
Mutable because fields are populated incrementally during the
|
|
197
|
+
pipeline (metadata first, then verification data, then chat count).
|
|
198
|
+
|
|
199
|
+
Fields:
|
|
200
|
+
status: Always ``"ok"`` for successful runs.
|
|
201
|
+
meeting: Meeting name as provided by the user.
|
|
202
|
+
tenant: Tenant domain name (e.g. ``"abccorp.com"``), or ``None``
|
|
203
|
+
when ``--tenant`` was not specified. Included in JSON output
|
|
204
|
+
only when not ``None``.
|
|
205
|
+
start: ISO datetime or date string for the meeting start.
|
|
206
|
+
end: ISO datetime string for the meeting end, or ``None``.
|
|
207
|
+
members: Sorted list of member display names, or ``None``.
|
|
208
|
+
organiser: Single organiser name, list of names, or ``None``.
|
|
209
|
+
parts: Number of transcript parts detected.
|
|
210
|
+
cues: Total cue count from verification, or ``None`` before download.
|
|
211
|
+
speakers: Mapping of speaker name to cue count, or ``None``.
|
|
212
|
+
file_size: Output file size in bytes, or ``None`` before download.
|
|
213
|
+
chat_messages: Count of extracted chat messages, or ``None``.
|
|
214
|
+
dry_run: ``True`` only for dry-run output.
|
|
215
|
+
file: Output file path (when writing to file), or ``None``.
|
|
216
|
+
transcript: Structured cue list for JSON output, or ``None``.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
status: str = "ok"
|
|
220
|
+
meeting: str = ""
|
|
221
|
+
tenant: str | None = None
|
|
222
|
+
start: str = ""
|
|
223
|
+
end: str | None = None
|
|
224
|
+
members: list[str] | None = None
|
|
225
|
+
organiser: str | list[str] | None = None
|
|
226
|
+
parts: int = 1
|
|
227
|
+
cues: int | None = None
|
|
228
|
+
speakers: dict[str, int] | None = None
|
|
229
|
+
file_size: int | None = None
|
|
230
|
+
chat_messages: int | None = None
|
|
231
|
+
dry_run: bool = False
|
|
232
|
+
file: str | None = None
|
|
233
|
+
transcript: list[dict] | None = None
|
|
234
|
+
|
|
235
|
+
def to_dict(self) -> dict:
|
|
236
|
+
"""Convert to a plain dict, omitting ``None`` values.
|
|
237
|
+
|
|
238
|
+
Matches the JSON output schema: keys with ``None`` values are
|
|
239
|
+
excluded, and ``dry_run`` is only included when ``True``.
|
|
240
|
+
"""
|
|
241
|
+
d: dict[str, object] = {"status": self.status}
|
|
242
|
+
d["meeting"] = self.meeting
|
|
243
|
+
if self.tenant is not None:
|
|
244
|
+
d["tenant"] = self.tenant
|
|
245
|
+
d["start"] = self.start
|
|
246
|
+
if self.end is not None:
|
|
247
|
+
d["end"] = self.end
|
|
248
|
+
if self.members is not None:
|
|
249
|
+
d["members"] = self.members
|
|
250
|
+
if self.organiser is not None:
|
|
251
|
+
d["organiser"] = self.organiser
|
|
252
|
+
d["parts"] = self.parts
|
|
253
|
+
if self.cues is not None:
|
|
254
|
+
d["cues"] = self.cues
|
|
255
|
+
if self.speakers is not None:
|
|
256
|
+
d["speakers"] = self.speakers
|
|
257
|
+
if self.file_size is not None:
|
|
258
|
+
d["file_size"] = self.file_size
|
|
259
|
+
if self.chat_messages is not None:
|
|
260
|
+
d["chat_messages"] = self.chat_messages
|
|
261
|
+
if self.dry_run:
|
|
262
|
+
d["dry_run"] = True
|
|
263
|
+
if self.file is not None:
|
|
264
|
+
d["file"] = self.file
|
|
265
|
+
if self.transcript is not None:
|
|
266
|
+
d["transcript"] = self.transcript
|
|
267
|
+
return d
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
# Meeting configuration
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class MeetingConfig(BaseModel):
|
|
276
|
+
"""Resolved meeting configuration from CLI args and environment.
|
|
277
|
+
|
|
278
|
+
Immutable once constructed by ``infrastructure.config.resolve_config()``.
|
|
279
|
+
Passed through the pipeline as the single source of configuration truth.
|
|
280
|
+
|
|
281
|
+
This is a frozen Pydantic model. To create a modified copy (e.g. after
|
|
282
|
+
resolving ``--meeting-id`` via MCPS), use ``model_copy(update={...})``.
|
|
283
|
+
|
|
284
|
+
Fields:
|
|
285
|
+
meeting: Meeting name search string (``--meeting``). Empty string
|
|
286
|
+
when ``--meeting-id`` is used instead.
|
|
287
|
+
meeting_id: Meeting UUID from ``--meeting-id``, or ``None`` when
|
|
288
|
+
``--meeting`` is used instead. When set, the pipeline resolves the
|
|
289
|
+
meeting subject via MCPS before searching, and verifies the
|
|
290
|
+
thread_id after navigation.
|
|
291
|
+
call_id: Call identifier from ``--call-id``, or ``None`` when not
|
|
292
|
+
specified. When set, the pipeline searches MCPS for the exact
|
|
293
|
+
call_id match and resolves subject, date, and thread_id. This
|
|
294
|
+
is the preferred identifier for recurring meetings as it uniquely
|
|
295
|
+
identifies a specific occurrence.
|
|
296
|
+
date: Target date as ``YYYY-MM-DD`` string.
|
|
297
|
+
time: Target time as ``HH:MM`` string, or ``None`` for date-only.
|
|
298
|
+
output: Output file path. ``None`` during ``--dry-run``.
|
|
299
|
+
output_to_stdout: ``True`` when no ``--output`` was given (content
|
|
300
|
+
goes to stdout at the end).
|
|
301
|
+
dry_run: ``True`` for ``--dry-run`` mode.
|
|
302
|
+
port: CDP debugging port number.
|
|
303
|
+
gcp_project: Google Cloud project ID for Vertex AI.
|
|
304
|
+
gcp_region: Google Cloud region for Vertex AI.
|
|
305
|
+
model: LLM model identifier.
|
|
306
|
+
adc: Path to application default credentials JSON.
|
|
307
|
+
output_format: ``"vtt"`` or ``"json"``.
|
|
308
|
+
tenant: Tenant domain name for verification (e.g. ``"abccorp.com"``),
|
|
309
|
+
or ``None`` to skip tenant checking.
|
|
310
|
+
quiet: Suppress diagnostic output.
|
|
311
|
+
verbose: Enable verbose diagnostic output.
|
|
312
|
+
include_transcript: Include transcript cues in output.
|
|
313
|
+
include_chat: Include chat messages in output.
|
|
314
|
+
include_metadata: Include participant metadata in output.
|
|
315
|
+
force_restart: Allow Teams restart even when call state is active or unknown.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
model_config = ConfigDict(frozen=True)
|
|
319
|
+
|
|
320
|
+
meeting: str
|
|
321
|
+
date: str
|
|
322
|
+
time: str | None
|
|
323
|
+
output: str | None
|
|
324
|
+
output_to_stdout: bool
|
|
325
|
+
dry_run: bool
|
|
326
|
+
port: int
|
|
327
|
+
gcp_project: str
|
|
328
|
+
gcp_region: str
|
|
329
|
+
model: str
|
|
330
|
+
adc: str
|
|
331
|
+
output_format: str
|
|
332
|
+
meeting_id: str | None = None
|
|
333
|
+
call_id: str | None = None
|
|
334
|
+
tenant: str | None = None
|
|
335
|
+
quiet: bool = False
|
|
336
|
+
verbose: bool = False
|
|
337
|
+
include_transcript: bool = True
|
|
338
|
+
include_chat: bool = True
|
|
339
|
+
include_metadata: bool = True
|
|
340
|
+
force_restart: bool = False
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def format_json(self) -> bool:
|
|
344
|
+
"""Whether output format is JSON."""
|
|
345
|
+
return self.output_format == "json"
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def all_components(self) -> bool:
|
|
349
|
+
"""Whether all three output components are included."""
|
|
350
|
+
return self.include_transcript and self.include_chat and self.include_metadata
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
# Download input validation (Pydantic)
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class DownloadInput(BaseModel):
|
|
359
|
+
"""Validated download arguments shared by MCP tools and CLI config.
|
|
360
|
+
|
|
361
|
+
Captures the core argument-combination constraints that apply regardless
|
|
362
|
+
of entry point (MCP ``download()`` tool, CLI ``_resolve_config_inner()``).
|
|
363
|
+
All fields are optional with sensible defaults; the ``@model_validator``
|
|
364
|
+
enforces mutual-exclusivity and at-least-one rules.
|
|
365
|
+
|
|
366
|
+
Construct this early in both code paths. ``ValidationError`` is then
|
|
367
|
+
either converted to ``exit_error()`` (CLI) or propagated to the MCP
|
|
368
|
+
framework (which returns a structured error to the agent).
|
|
369
|
+
|
|
370
|
+
Fields:
|
|
371
|
+
meeting: Meeting name search string. Mutually exclusive with
|
|
372
|
+
``meeting_id`` and ``call_id``.
|
|
373
|
+
meeting_id: Meeting UUID. Mutually exclusive with ``meeting``,
|
|
374
|
+
``call_id``, ``date``, and ``time``.
|
|
375
|
+
call_id: Call identifier (from list_transcripts output). Mutually
|
|
376
|
+
exclusive with ``meeting``, ``meeting_id``, ``date``, and ``time``.
|
|
377
|
+
Uniquely identifies a specific occurrence of a recurring meeting.
|
|
378
|
+
date: Target date as ``YYYY-MM-DD``. Incompatible with
|
|
379
|
+
``meeting_id`` and ``call_id`` (date is resolved automatically).
|
|
380
|
+
time: Target time as ``HH:MM``. Incompatible with ``meeting_id``
|
|
381
|
+
and ``call_id``.
|
|
382
|
+
include_transcript: Whether to download the transcript.
|
|
383
|
+
include_chat: Whether to download chat messages.
|
|
384
|
+
include_metadata: Whether to download participant metadata.
|
|
385
|
+
|
|
386
|
+
Schema version: 1.1.0
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
model_config = ConfigDict(frozen=True)
|
|
390
|
+
|
|
391
|
+
meeting: str | None = None
|
|
392
|
+
meeting_id: str | None = None
|
|
393
|
+
call_id: str | None = None
|
|
394
|
+
date: str | None = None
|
|
395
|
+
time: str | None = None
|
|
396
|
+
include_transcript: bool = True
|
|
397
|
+
include_chat: bool = True
|
|
398
|
+
include_metadata: bool = True
|
|
399
|
+
dry_run: bool = False
|
|
400
|
+
|
|
401
|
+
@model_validator(mode="after")
|
|
402
|
+
def _check_constraints(self) -> DownloadInput:
|
|
403
|
+
"""Enforce argument-combination constraints.
|
|
404
|
+
|
|
405
|
+
Rules:
|
|
406
|
+
1. ``meeting_id``, ``call_id``, and ``meeting`` are mutually exclusive.
|
|
407
|
+
2. At least one of ``meeting_id``, ``call_id``, or ``meeting`` is required.
|
|
408
|
+
3. ``date`` is incompatible with ``meeting_id`` and ``call_id``.
|
|
409
|
+
4. ``time`` is incompatible with ``meeting_id`` and ``call_id``.
|
|
410
|
+
5. At least one content flag must be ``True``.
|
|
411
|
+
6. ``meeting_id`` must be a valid UUID.
|
|
412
|
+
7. ``call_id`` must be a non-empty string (no format constraint).
|
|
413
|
+
"""
|
|
414
|
+
id_sources = sum(1 for x in (self.meeting_id, self.call_id, self.meeting) if x)
|
|
415
|
+
if id_sources > 1:
|
|
416
|
+
raise ValueError(
|
|
417
|
+
"meeting_id, call_id, and meeting are mutually exclusive. "
|
|
418
|
+
"Pass exactly one identifier."
|
|
419
|
+
)
|
|
420
|
+
if id_sources == 0:
|
|
421
|
+
raise ValueError("One of meeting_id, call_id, or meeting is required.")
|
|
422
|
+
if self.meeting_id and self.date:
|
|
423
|
+
raise ValueError(
|
|
424
|
+
"date is incompatible with meeting_id. "
|
|
425
|
+
"The meeting date is resolved automatically from the UUID."
|
|
426
|
+
)
|
|
427
|
+
if self.meeting_id and self.time:
|
|
428
|
+
raise ValueError(
|
|
429
|
+
"time is incompatible with meeting_id. "
|
|
430
|
+
"The meeting time is resolved automatically from the UUID."
|
|
431
|
+
)
|
|
432
|
+
if self.call_id and self.date:
|
|
433
|
+
raise ValueError(
|
|
434
|
+
"date is incompatible with call_id. "
|
|
435
|
+
"The meeting date is resolved automatically from the call ID."
|
|
436
|
+
)
|
|
437
|
+
if self.call_id and self.time:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
"time is incompatible with call_id. "
|
|
440
|
+
"The meeting time is resolved automatically from the call ID."
|
|
441
|
+
)
|
|
442
|
+
if self.meeting_id:
|
|
443
|
+
try:
|
|
444
|
+
UUID(self.meeting_id)
|
|
445
|
+
except ValueError as exc:
|
|
446
|
+
raise ValueError("meeting_id must be a UUID.") from exc
|
|
447
|
+
if self.call_id is not None and not self.call_id.strip():
|
|
448
|
+
raise ValueError("call_id must be a non-empty string.")
|
|
449
|
+
if (
|
|
450
|
+
not self.include_transcript
|
|
451
|
+
and not self.include_chat
|
|
452
|
+
and not self.include_metadata
|
|
453
|
+
and not self.dry_run
|
|
454
|
+
):
|
|
455
|
+
raise ValueError(
|
|
456
|
+
"At least one of include_transcript, include_chat, "
|
|
457
|
+
"or include_metadata must be True."
|
|
458
|
+
)
|
|
459
|
+
return self
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# ---------------------------------------------------------------------------
|
|
463
|
+
# CDP target (lightweight typed wrapper)
|
|
464
|
+
# ---------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@dataclass(frozen=True)
|
|
468
|
+
class CdpTarget:
|
|
469
|
+
"""A Chrome DevTools Protocol page target.
|
|
470
|
+
|
|
471
|
+
Fields:
|
|
472
|
+
id: Target identifier string.
|
|
473
|
+
url: Page URL.
|
|
474
|
+
title: Page title.
|
|
475
|
+
ws_url: WebSocket debugger URL for connecting.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
id: str
|
|
479
|
+
url: str = ""
|
|
480
|
+
title: str = ""
|
|
481
|
+
ws_url: str = ""
|
|
482
|
+
|
|
483
|
+
@classmethod
|
|
484
|
+
def from_dict(cls, d: dict) -> CdpTarget:
|
|
485
|
+
"""Construct from a raw CDP ``/json`` response entry."""
|
|
486
|
+
return cls(
|
|
487
|
+
id=d.get("id", ""),
|
|
488
|
+
url=d.get("url", ""),
|
|
489
|
+
title=d.get("title", ""),
|
|
490
|
+
ws_url=d.get("webSocketDebuggerUrl", ""),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ---------------------------------------------------------------------------
|
|
495
|
+
# Meeting artifact (for --list-transcripts)
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@dataclass(frozen=True)
|
|
500
|
+
class MeetingArtifact:
|
|
501
|
+
"""A meeting with associated artifacts discovered via the MCPS API.
|
|
502
|
+
|
|
503
|
+
Represents a single meeting returned by
|
|
504
|
+
``startListArtifactsByUserWorkflow``. Each meeting may have a
|
|
505
|
+
transcript, a recording, or both.
|
|
506
|
+
|
|
507
|
+
Fields:
|
|
508
|
+
subject: Meeting subject line (may be empty for unnamed meetings).
|
|
509
|
+
thread_id: Teams thread identifier (``19:meeting_...``).
|
|
510
|
+
call_id: Call identifier from MCPS.
|
|
511
|
+
ical_uid: iCalendar UID for calendar correlation.
|
|
512
|
+
date: Date the meeting occurred.
|
|
513
|
+
has_transcript: Whether a TranscriptV2 resource is present.
|
|
514
|
+
has_recording: Whether a Recording resource is present.
|
|
515
|
+
transcript_url: SharePoint URL for transcript content download,
|
|
516
|
+
or ``None`` when no transcript is available.
|
|
517
|
+
recording_url: SharePoint URL for recording download,
|
|
518
|
+
or ``None`` when no recording is available.
|
|
519
|
+
|
|
520
|
+
Schema version: 1.0.0
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
subject: str
|
|
524
|
+
thread_id: str
|
|
525
|
+
call_id: str
|
|
526
|
+
ical_uid: str
|
|
527
|
+
date: dt.date
|
|
528
|
+
has_transcript: bool
|
|
529
|
+
has_recording: bool
|
|
530
|
+
transcript_url: str | None = None
|
|
531
|
+
recording_url: str | None = None
|