teams-phone-cli 0.1.2__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.
- teams_phone/__init__.py +3 -0
- teams_phone/__main__.py +7 -0
- teams_phone/cli/__init__.py +8 -0
- teams_phone/cli/api_check.py +267 -0
- teams_phone/cli/auth.py +201 -0
- teams_phone/cli/context.py +108 -0
- teams_phone/cli/helpers.py +65 -0
- teams_phone/cli/locations.py +308 -0
- teams_phone/cli/main.py +99 -0
- teams_phone/cli/numbers.py +1644 -0
- teams_phone/cli/policies.py +893 -0
- teams_phone/cli/tenants.py +364 -0
- teams_phone/cli/users.py +394 -0
- teams_phone/constants.py +97 -0
- teams_phone/exceptions.py +137 -0
- teams_phone/infrastructure/__init__.py +22 -0
- teams_phone/infrastructure/cache_manager.py +274 -0
- teams_phone/infrastructure/config_manager.py +209 -0
- teams_phone/infrastructure/debug_logger.py +321 -0
- teams_phone/infrastructure/graph_client.py +666 -0
- teams_phone/infrastructure/output_formatter.py +234 -0
- teams_phone/models/__init__.py +76 -0
- teams_phone/models/api_responses.py +69 -0
- teams_phone/models/auth.py +100 -0
- teams_phone/models/cache.py +25 -0
- teams_phone/models/config.py +66 -0
- teams_phone/models/location.py +36 -0
- teams_phone/models/number.py +184 -0
- teams_phone/models/policy.py +26 -0
- teams_phone/models/tenant.py +45 -0
- teams_phone/models/user.py +117 -0
- teams_phone/services/__init__.py +21 -0
- teams_phone/services/auth_service.py +536 -0
- teams_phone/services/bulk_operations.py +562 -0
- teams_phone/services/location_service.py +195 -0
- teams_phone/services/number_service.py +489 -0
- teams_phone/services/policy_service.py +330 -0
- teams_phone/services/tenant_service.py +205 -0
- teams_phone/services/user_service.py +435 -0
- teams_phone/utils.py +172 -0
- teams_phone_cli-0.1.2.dist-info/METADATA +15 -0
- teams_phone_cli-0.1.2.dist-info/RECORD +45 -0
- teams_phone_cli-0.1.2.dist-info/WHEEL +4 -0
- teams_phone_cli-0.1.2.dist-info/entry_points.txt +2 -0
- teams_phone_cli-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Output formatter for Teams Phone CLI.
|
|
2
|
+
|
|
3
|
+
This module provides rich terminal output formatting including tables,
|
|
4
|
+
JSON output mode, progress bars, and styled messages with remediation guidance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.progress import (
|
|
17
|
+
BarColumn,
|
|
18
|
+
Progress,
|
|
19
|
+
SpinnerColumn,
|
|
20
|
+
TaskProgressColumn,
|
|
21
|
+
TextColumn,
|
|
22
|
+
)
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Iterator
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _can_encode_unicode() -> bool:
|
|
31
|
+
"""Check if stdout can encode Unicode characters.
|
|
32
|
+
|
|
33
|
+
Tests by attempting to encode a Unicode checkmark. On Windows with
|
|
34
|
+
legacy console (cp1252), this will fail.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if Unicode is supported, False if ASCII-only mode is needed.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
# Test with checkmark - if this fails, we need ASCII mode
|
|
41
|
+
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
|
|
42
|
+
"✓".encode(encoding)
|
|
43
|
+
return True
|
|
44
|
+
except (UnicodeEncodeError, LookupError):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ASCII-safe placeholder for "no value" display
|
|
49
|
+
# Em-dash (—) works in most encodings but we provide a fallback
|
|
50
|
+
PLACEHOLDER_UNICODE = "—"
|
|
51
|
+
PLACEHOLDER_ASCII = "-"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class OutputFormatter:
|
|
55
|
+
"""Format and display output for the CLI.
|
|
56
|
+
|
|
57
|
+
Provides methods for displaying data as tables, JSON, or styled messages.
|
|
58
|
+
Supports color control via constructor flag and NO_COLOR environment variable.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
console: Rich Console instance for output.
|
|
62
|
+
json_mode: Whether to output JSON instead of Rich formatting.
|
|
63
|
+
no_color: Whether color output is disabled.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
console: Console | None = None,
|
|
69
|
+
*,
|
|
70
|
+
json_mode: bool = False,
|
|
71
|
+
no_color: bool = False,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Initialize the OutputFormatter.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
console: Optional Rich Console instance. If not provided, creates one.
|
|
77
|
+
json_mode: If True, output JSON to stdout instead of Rich formatting.
|
|
78
|
+
no_color: If True, disable color output. Also respects NO_COLOR env var.
|
|
79
|
+
"""
|
|
80
|
+
self.json_mode = json_mode
|
|
81
|
+
# Respect both the flag and the NO_COLOR environment variable
|
|
82
|
+
self.no_color = no_color or os.environ.get("NO_COLOR") is not None
|
|
83
|
+
# Detect Unicode support for Windows compatibility
|
|
84
|
+
self._use_ascii = not _can_encode_unicode()
|
|
85
|
+
|
|
86
|
+
if console is not None:
|
|
87
|
+
self.console = console
|
|
88
|
+
else:
|
|
89
|
+
self.console = Console(
|
|
90
|
+
force_terminal=not json_mode,
|
|
91
|
+
no_color=self.no_color,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def table(
|
|
95
|
+
self,
|
|
96
|
+
data: list[dict[str, Any]],
|
|
97
|
+
columns: list[str],
|
|
98
|
+
*,
|
|
99
|
+
title: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Display data as a formatted table.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
data: List of dictionaries containing row data.
|
|
105
|
+
columns: List of column names to display.
|
|
106
|
+
title: Optional title for the table.
|
|
107
|
+
"""
|
|
108
|
+
if self.json_mode:
|
|
109
|
+
self.json(data)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
table = Table(title=title)
|
|
113
|
+
|
|
114
|
+
for column in columns:
|
|
115
|
+
table.add_column(column, style="cyan")
|
|
116
|
+
|
|
117
|
+
for row in data:
|
|
118
|
+
table.add_row(*[str(row.get(col, "")) for col in columns])
|
|
119
|
+
|
|
120
|
+
self.console.print(table)
|
|
121
|
+
|
|
122
|
+
def json(self, data: Any) -> None:
|
|
123
|
+
"""Output data as JSON to stdout.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
data: Data to serialize as JSON.
|
|
127
|
+
"""
|
|
128
|
+
# Use plain print to stdout for JSON mode (no Rich formatting)
|
|
129
|
+
print(json.dumps(data, indent=2, default=str), file=sys.stdout)
|
|
130
|
+
|
|
131
|
+
def success(self, message: str) -> None:
|
|
132
|
+
"""Display a success message in green.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
message: The success message to display.
|
|
136
|
+
"""
|
|
137
|
+
if self.json_mode:
|
|
138
|
+
return
|
|
139
|
+
# Use ASCII-safe marker on Windows legacy console
|
|
140
|
+
marker = "OK:" if self._use_ascii else "✓"
|
|
141
|
+
self.console.print(f"[green]{marker}[/green] {message}")
|
|
142
|
+
|
|
143
|
+
def error(self, message: str, *, remediation: str | None = None) -> None:
|
|
144
|
+
"""Display an error message in red with optional remediation guidance.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
message: The error message to display.
|
|
148
|
+
remediation: Optional guidance on how to fix the issue.
|
|
149
|
+
"""
|
|
150
|
+
if self.json_mode:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
self.console.print(f"[red]Error:[/red] {message}")
|
|
154
|
+
|
|
155
|
+
if remediation:
|
|
156
|
+
self.console.print()
|
|
157
|
+
self.console.print("[yellow]To fix:[/yellow]")
|
|
158
|
+
self.console.print(f" {remediation}")
|
|
159
|
+
|
|
160
|
+
def warning(self, message: str) -> None:
|
|
161
|
+
"""Display a warning message in yellow.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
message: The warning message to display.
|
|
165
|
+
"""
|
|
166
|
+
if self.json_mode:
|
|
167
|
+
return
|
|
168
|
+
self.console.print(f"[yellow]Warning:[/yellow] {message}")
|
|
169
|
+
|
|
170
|
+
def info(self, message: str) -> None:
|
|
171
|
+
"""Display an informational message.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
message: The info message to display.
|
|
175
|
+
"""
|
|
176
|
+
if self.json_mode:
|
|
177
|
+
return
|
|
178
|
+
self.console.print(message)
|
|
179
|
+
|
|
180
|
+
def get_spinner_name(self) -> str:
|
|
181
|
+
"""Get the appropriate spinner name based on Unicode support.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
'line' for ASCII-only mode (Windows legacy console),
|
|
185
|
+
'dots' for Unicode-capable terminals.
|
|
186
|
+
"""
|
|
187
|
+
return "line" if self._use_ascii else "dots"
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def placeholder(self) -> str:
|
|
191
|
+
"""Get the appropriate placeholder for 'no value' display.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
'-' for ASCII-only mode, '—' (em-dash) for Unicode terminals.
|
|
195
|
+
"""
|
|
196
|
+
return PLACEHOLDER_ASCII if self._use_ascii else PLACEHOLDER_UNICODE
|
|
197
|
+
|
|
198
|
+
@contextmanager
|
|
199
|
+
def progress(
|
|
200
|
+
self,
|
|
201
|
+
total: int,
|
|
202
|
+
description: str = "Processing",
|
|
203
|
+
) -> Iterator[Progress]:
|
|
204
|
+
"""Return a Progress context manager for displaying progress bars.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
total: Total number of items to process.
|
|
208
|
+
description: Description text for the progress bar.
|
|
209
|
+
|
|
210
|
+
Yields:
|
|
211
|
+
Progress instance for tracking progress.
|
|
212
|
+
"""
|
|
213
|
+
if self.json_mode:
|
|
214
|
+
# Return a minimal progress that does nothing in JSON mode
|
|
215
|
+
progress = Progress(
|
|
216
|
+
console=Console(force_terminal=False, quiet=True),
|
|
217
|
+
disable=True,
|
|
218
|
+
)
|
|
219
|
+
with progress:
|
|
220
|
+
progress.add_task(description, total=total)
|
|
221
|
+
yield progress
|
|
222
|
+
else:
|
|
223
|
+
# Use ASCII-safe spinner on Windows legacy console
|
|
224
|
+
spinner_name = self.get_spinner_name()
|
|
225
|
+
progress = Progress(
|
|
226
|
+
SpinnerColumn(spinner_name=spinner_name),
|
|
227
|
+
TextColumn("[progress.description]{task.description}"),
|
|
228
|
+
BarColumn(),
|
|
229
|
+
TaskProgressColumn(),
|
|
230
|
+
console=self.console,
|
|
231
|
+
)
|
|
232
|
+
with progress:
|
|
233
|
+
progress.add_task(description, total=total)
|
|
234
|
+
yield progress
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Pydantic models for API responses and domain objects."""
|
|
2
|
+
|
|
3
|
+
from teams_phone.models.api_responses import (
|
|
4
|
+
GraphErrorDetail,
|
|
5
|
+
GraphErrorResponse,
|
|
6
|
+
GraphListResponse,
|
|
7
|
+
)
|
|
8
|
+
from teams_phone.models.auth import (
|
|
9
|
+
AuthStatus,
|
|
10
|
+
AuthStatusType,
|
|
11
|
+
CachedToken,
|
|
12
|
+
ConnectionTestResult,
|
|
13
|
+
)
|
|
14
|
+
from teams_phone.models.cache import CacheMetadata
|
|
15
|
+
from teams_phone.models.config import CacheSettings, Config, NetworkSettings
|
|
16
|
+
from teams_phone.models.location import EmergencyLocation
|
|
17
|
+
from teams_phone.models.number import (
|
|
18
|
+
ActivationState,
|
|
19
|
+
AssignmentCategory,
|
|
20
|
+
AssignmentStatus,
|
|
21
|
+
AsyncOperation,
|
|
22
|
+
NumberAssignment,
|
|
23
|
+
NumberType,
|
|
24
|
+
OperationNumber,
|
|
25
|
+
OperationStatus,
|
|
26
|
+
)
|
|
27
|
+
from teams_phone.models.policy import Policy
|
|
28
|
+
from teams_phone.models.tenant import AuthMethod, TenantProfile
|
|
29
|
+
from teams_phone.models.user import (
|
|
30
|
+
AccountType,
|
|
31
|
+
EffectivePolicyAssignment,
|
|
32
|
+
PolicyAssignmentDetail,
|
|
33
|
+
TelephoneNumber,
|
|
34
|
+
UserConfiguration,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# API response models
|
|
40
|
+
"GraphErrorDetail",
|
|
41
|
+
"GraphErrorResponse",
|
|
42
|
+
"GraphListResponse",
|
|
43
|
+
# Auth models
|
|
44
|
+
"AuthStatus",
|
|
45
|
+
"AuthStatusType",
|
|
46
|
+
"CachedToken",
|
|
47
|
+
"ConnectionTestResult",
|
|
48
|
+
# Cache models
|
|
49
|
+
"CacheMetadata",
|
|
50
|
+
# Config models
|
|
51
|
+
"CacheSettings",
|
|
52
|
+
"Config",
|
|
53
|
+
"NetworkSettings",
|
|
54
|
+
# Number models
|
|
55
|
+
"ActivationState",
|
|
56
|
+
"AssignmentCategory",
|
|
57
|
+
"AssignmentStatus",
|
|
58
|
+
"AsyncOperation",
|
|
59
|
+
"NumberAssignment",
|
|
60
|
+
"NumberType",
|
|
61
|
+
"OperationNumber",
|
|
62
|
+
"OperationStatus",
|
|
63
|
+
# Location models
|
|
64
|
+
"EmergencyLocation",
|
|
65
|
+
# Policy models
|
|
66
|
+
"Policy",
|
|
67
|
+
# Tenant models
|
|
68
|
+
"AuthMethod",
|
|
69
|
+
"TenantProfile",
|
|
70
|
+
# User models
|
|
71
|
+
"AccountType",
|
|
72
|
+
"EffectivePolicyAssignment",
|
|
73
|
+
"PolicyAssignmentDetail",
|
|
74
|
+
"TelephoneNumber",
|
|
75
|
+
"UserConfiguration",
|
|
76
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Generic Graph API response models for pagination and error handling."""
|
|
2
|
+
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GraphErrorDetail(BaseModel):
|
|
12
|
+
"""Nested error details from Graph API error responses.
|
|
13
|
+
|
|
14
|
+
Graph API errors can contain nested details for complex error scenarios.
|
|
15
|
+
The details field is self-referential to support arbitrary nesting depth.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
code: Error code string (e.g., "BadRequest", "ResourceNotFound").
|
|
19
|
+
message: Human-readable error message.
|
|
20
|
+
target: The target of the error, typically the field or parameter that caused it.
|
|
21
|
+
details: Nested error details for additional context.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
25
|
+
|
|
26
|
+
code: str
|
|
27
|
+
message: str
|
|
28
|
+
target: str | None = None
|
|
29
|
+
details: list["GraphErrorDetail"] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GraphErrorResponse(BaseModel):
|
|
33
|
+
"""Graph API error response wrapper.
|
|
34
|
+
|
|
35
|
+
Standard error response format returned by Microsoft Graph API
|
|
36
|
+
when a request fails. Contains a single error object with details.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
error: The error detail object containing code, message, and optional nested details.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
43
|
+
|
|
44
|
+
error: GraphErrorDetail
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GraphListResponse(BaseModel, Generic[T]):
|
|
48
|
+
"""Generic wrapper for paginated Graph API list responses.
|
|
49
|
+
|
|
50
|
+
Microsoft Graph API returns list results in a consistent format with
|
|
51
|
+
OData annotations for context, count, and pagination. This generic
|
|
52
|
+
model can wrap any item type T for type-safe parsing.
|
|
53
|
+
|
|
54
|
+
Type Parameters:
|
|
55
|
+
T: The type of items in the value list (e.g., UserConfiguration).
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
odata_context: OData context URL describing the response metadata.
|
|
59
|
+
odata_count: Total count of items when requested with $count=true.
|
|
60
|
+
odata_next_link: URL to fetch the next page of results.
|
|
61
|
+
value: List of items of type T.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
65
|
+
|
|
66
|
+
odata_context: str | None = Field(alias="@odata.context", default=None)
|
|
67
|
+
odata_count: int | None = Field(alias="@odata.count", default=None)
|
|
68
|
+
odata_next_link: str | None = Field(alias="@odata.nextLink", default=None)
|
|
69
|
+
value: list[T]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Authentication models for token caching and status tracking."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthStatusType(str, Enum):
|
|
10
|
+
"""Authentication status states.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
AUTHENTICATED: Successfully authenticated with valid token.
|
|
14
|
+
UNAUTHENTICATED: No valid authentication (requires login).
|
|
15
|
+
EXPIRED: Token has expired (requires refresh or re-authentication).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
AUTHENTICATED = "authenticated"
|
|
19
|
+
UNAUTHENTICATED = "unauthenticated"
|
|
20
|
+
EXPIRED = "expired"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CachedToken(BaseModel):
|
|
24
|
+
"""Cached authentication token for a tenant.
|
|
25
|
+
|
|
26
|
+
Stores the access token and expiration timestamp for token caching.
|
|
27
|
+
MSAL handles refresh tokens internally, so only access token is stored.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
access_token: The OAuth 2.0 access token string.
|
|
31
|
+
expires_at: UTC datetime when the token expires.
|
|
32
|
+
tenant_id: The tenant ID this token is associated with.
|
|
33
|
+
scopes: List of scopes granted to this token.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra="forbid")
|
|
37
|
+
|
|
38
|
+
access_token: str
|
|
39
|
+
expires_at: datetime
|
|
40
|
+
tenant_id: str
|
|
41
|
+
scopes: list[str]
|
|
42
|
+
|
|
43
|
+
def is_expired(self) -> bool:
|
|
44
|
+
"""Check if the token has expired.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if current time is past expiration, False otherwise.
|
|
48
|
+
"""
|
|
49
|
+
now = datetime.now(timezone.utc)
|
|
50
|
+
expires = self.expires_at
|
|
51
|
+
# Handle both naive and aware datetimes for comparison
|
|
52
|
+
if expires.tzinfo is None:
|
|
53
|
+
expires = expires.replace(tzinfo=timezone.utc)
|
|
54
|
+
return now >= expires
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AuthStatus(BaseModel):
|
|
58
|
+
"""Current authentication status for a tenant.
|
|
59
|
+
|
|
60
|
+
Tracks the authentication state and provides context about
|
|
61
|
+
the current session.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
status: Current authentication state.
|
|
65
|
+
tenant_name: Profile name of the authenticated tenant.
|
|
66
|
+
tenant_id: Tenant ID if authenticated.
|
|
67
|
+
expires_at: Token expiration time if authenticated.
|
|
68
|
+
error_message: Error details if authentication failed.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(extra="forbid")
|
|
72
|
+
|
|
73
|
+
status: AuthStatusType
|
|
74
|
+
tenant_name: str | None = None
|
|
75
|
+
tenant_id: str | None = None
|
|
76
|
+
expires_at: datetime | None = None
|
|
77
|
+
error_message: str | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ConnectionTestResult(BaseModel):
|
|
81
|
+
"""Result of a tenant connection test.
|
|
82
|
+
|
|
83
|
+
Contains the outcome of testing connectivity to Microsoft Graph API
|
|
84
|
+
for a given tenant, including latency measurement and permission info.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
success: Whether the connection test succeeded.
|
|
88
|
+
tenant_name: Name of the tenant that was tested.
|
|
89
|
+
latency_ms: Round-trip latency in milliseconds (None if failed before API call).
|
|
90
|
+
permissions: List of application permissions (scopes) available.
|
|
91
|
+
error_message: Error details if the test failed.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(extra="forbid")
|
|
95
|
+
|
|
96
|
+
success: bool
|
|
97
|
+
tenant_name: str
|
|
98
|
+
latency_ms: int | None = None
|
|
99
|
+
permissions: list[str] = []
|
|
100
|
+
error_message: str | None = None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Cache-related models for metadata and staleness tracking."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheMetadata(BaseModel):
|
|
9
|
+
"""Metadata about CSV cache files.
|
|
10
|
+
|
|
11
|
+
Tracks when the cache was last updated and how much data it contains.
|
|
12
|
+
Used to determine if the cache is stale and needs refreshing.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
last_updated: UTC datetime when the oldest cache file was modified.
|
|
16
|
+
None if no cache files exist.
|
|
17
|
+
locations_count: Number of location records in the cache.
|
|
18
|
+
policies_count: Number of policy records in the cache.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(extra="forbid")
|
|
22
|
+
|
|
23
|
+
last_updated: datetime | None
|
|
24
|
+
locations_count: int
|
|
25
|
+
policies_count: int
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Configuration models for CLI settings and defaults."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from teams_phone.constants import (
|
|
6
|
+
CSV_STALE_DAYS,
|
|
7
|
+
DEFAULT_CACHE_PATH,
|
|
8
|
+
DEFAULT_CONFIG_PATH,
|
|
9
|
+
DEFAULT_TIMEOUT,
|
|
10
|
+
DEFAULT_TOKEN_PATH,
|
|
11
|
+
MAX_RETRIES,
|
|
12
|
+
)
|
|
13
|
+
from teams_phone.models.tenant import TenantProfile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CacheSettings(BaseModel):
|
|
17
|
+
"""Cache configuration settings.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
cache_path: Directory path for cached data files.
|
|
21
|
+
stale_days: Number of days before cache is considered stale.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(extra="forbid")
|
|
25
|
+
|
|
26
|
+
cache_path: str = DEFAULT_CACHE_PATH
|
|
27
|
+
stale_days: int = CSV_STALE_DAYS
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NetworkSettings(BaseModel):
|
|
31
|
+
"""Network and HTTP client settings.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
timeout: HTTP request timeout in seconds.
|
|
35
|
+
max_retries: Maximum retry attempts for transient failures.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(extra="forbid")
|
|
39
|
+
|
|
40
|
+
timeout: int = DEFAULT_TIMEOUT
|
|
41
|
+
max_retries: int = MAX_RETRIES
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Config(BaseModel):
|
|
45
|
+
"""Main configuration model for Teams Phone CLI.
|
|
46
|
+
|
|
47
|
+
Root configuration containing all settings, defaults, and tenant profiles.
|
|
48
|
+
Loaded from config.toml at startup.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
config_path: Path to the configuration file.
|
|
52
|
+
token_path: Directory path for authentication token storage.
|
|
53
|
+
default_tenant: Name of the default tenant profile to use.
|
|
54
|
+
cache: Cache-related settings.
|
|
55
|
+
network: Network and HTTP client settings.
|
|
56
|
+
tenants: Dictionary of tenant profiles keyed by name.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(extra="forbid")
|
|
60
|
+
|
|
61
|
+
config_path: str = DEFAULT_CONFIG_PATH
|
|
62
|
+
token_path: str = DEFAULT_TOKEN_PATH
|
|
63
|
+
default_tenant: str | None = None
|
|
64
|
+
cache: CacheSettings = Field(default_factory=CacheSettings)
|
|
65
|
+
network: NetworkSettings = Field(default_factory=NetworkSettings)
|
|
66
|
+
tenants: dict[str, TenantProfile] = Field(default_factory=dict)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Emergency location model for CSV cache data."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EmergencyLocation(BaseModel):
|
|
7
|
+
"""Emergency location from CSV cache.
|
|
8
|
+
|
|
9
|
+
Represents an emergency calling location exported via PowerShell.
|
|
10
|
+
Used for validation and display since Graph API doesn't expose location endpoints.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
location_id: Unique identifier for the location (GUID).
|
|
14
|
+
civic_address_id: Associated civic address ID (GUID).
|
|
15
|
+
description: Location description/name.
|
|
16
|
+
company_name: Company name at this location.
|
|
17
|
+
address: Street address.
|
|
18
|
+
city: City name.
|
|
19
|
+
state_or_province: State or province.
|
|
20
|
+
postal_code: Postal/ZIP code.
|
|
21
|
+
country_or_region: Country or region code.
|
|
22
|
+
is_default: Whether this is the default location for the civic address.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
26
|
+
|
|
27
|
+
location_id: str = Field(alias="locationId")
|
|
28
|
+
civic_address_id: str = Field(alias="civicAddressId")
|
|
29
|
+
description: str = Field(alias="description")
|
|
30
|
+
company_name: str | None = Field(alias="companyName", default=None)
|
|
31
|
+
address: str | None = Field(alias="address", default=None)
|
|
32
|
+
city: str | None = Field(alias="city", default=None)
|
|
33
|
+
state_or_province: str | None = Field(alias="stateOrProvince", default=None)
|
|
34
|
+
postal_code: str | None = Field(alias="postalCode", default=None)
|
|
35
|
+
country_or_region: str | None = Field(alias="countryOrRegion", default=None)
|
|
36
|
+
is_default: bool = Field(alias="isDefault")
|