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,274 @@
|
|
|
1
|
+
"""Cache manager for token storage and retrieval with JSON persistence."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
8
|
+
|
|
9
|
+
from teams_phone.constants import CSV_STALE_DAYS, DEFAULT_CACHE_PATH, DEFAULT_TOKEN_PATH
|
|
10
|
+
from teams_phone.exceptions import ConfigurationError
|
|
11
|
+
from teams_phone.models import CachedToken, CacheMetadata
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CacheManager:
|
|
15
|
+
"""Manages token caching with JSON persistence.
|
|
16
|
+
|
|
17
|
+
Provides methods to save, retrieve, and clear authentication tokens
|
|
18
|
+
for tenants. Tokens are stored as JSON files in the token directory.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
cache_dir: Path to the cache directory for CSV files.
|
|
22
|
+
token_dir: Path to the token storage directory.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
cache_dir: str | None = None,
|
|
28
|
+
token_dir: str | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initialize the CacheManager.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
cache_dir: Path to the cache directory. Defaults to
|
|
34
|
+
~/.teams-phone/cache if not specified.
|
|
35
|
+
token_dir: Path to the token directory. Defaults to
|
|
36
|
+
~/.teams-phone/tokens if not specified.
|
|
37
|
+
"""
|
|
38
|
+
cache_str = cache_dir if cache_dir is not None else DEFAULT_CACHE_PATH
|
|
39
|
+
self._cache_dir: Path = Path(cache_str).expanduser()
|
|
40
|
+
|
|
41
|
+
token_str = token_dir if token_dir is not None else DEFAULT_TOKEN_PATH
|
|
42
|
+
self._token_dir: Path = Path(token_str).expanduser()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def cache_dir(self) -> Path:
|
|
46
|
+
"""Path to the cache directory."""
|
|
47
|
+
return self._cache_dir
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def token_dir(self) -> Path:
|
|
51
|
+
"""Path to the token directory."""
|
|
52
|
+
return self._token_dir
|
|
53
|
+
|
|
54
|
+
def ensure_token_dir(self) -> None:
|
|
55
|
+
"""Create the token directory if it doesn't exist.
|
|
56
|
+
|
|
57
|
+
Creates all parent directories as needed.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ConfigurationError: If directory creation fails due to permission error.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
self._token_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
except PermissionError as e:
|
|
65
|
+
raise ConfigurationError(
|
|
66
|
+
f"Cannot create token directory {self._token_dir}: {e}",
|
|
67
|
+
remediation=(
|
|
68
|
+
f"Check that you have write permissions to {self._token_dir}.\n"
|
|
69
|
+
"You may need to create the directory manually or run with elevated privileges."
|
|
70
|
+
),
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
def _get_token_path(self, tenant_name: str) -> Path:
|
|
74
|
+
"""Get the file path for a tenant's token.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
tenant_name: The tenant profile name.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Path to the token JSON file.
|
|
81
|
+
"""
|
|
82
|
+
return self._token_dir / f"{tenant_name}.json"
|
|
83
|
+
|
|
84
|
+
def get_token(self, tenant_name: str) -> CachedToken | None:
|
|
85
|
+
"""Retrieve a cached token for a tenant.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
tenant_name: The tenant profile name.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
CachedToken if found and valid, None if file doesn't exist or
|
|
92
|
+
contains invalid JSON.
|
|
93
|
+
|
|
94
|
+
Note:
|
|
95
|
+
Returns None silently for missing files or invalid JSON.
|
|
96
|
+
This enables graceful fallback to re-authentication.
|
|
97
|
+
"""
|
|
98
|
+
token_path = self._get_token_path(tenant_name)
|
|
99
|
+
|
|
100
|
+
if not token_path.exists():
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
content = token_path.read_text(encoding="utf-8")
|
|
105
|
+
return CachedToken.model_validate_json(content)
|
|
106
|
+
except (PydanticValidationError, ValueError):
|
|
107
|
+
# Invalid JSON or failed validation - treat as missing
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def save_token(self, tenant_name: str, token: CachedToken) -> None:
|
|
111
|
+
"""Save a token to the cache.
|
|
112
|
+
|
|
113
|
+
Creates the token directory if it doesn't exist.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
tenant_name: The tenant profile name.
|
|
117
|
+
token: The token to cache.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ConfigurationError: If the token directory cannot be created
|
|
121
|
+
due to permission issues.
|
|
122
|
+
"""
|
|
123
|
+
self.ensure_token_dir()
|
|
124
|
+
|
|
125
|
+
token_path = self._get_token_path(tenant_name)
|
|
126
|
+
content = token.model_dump_json()
|
|
127
|
+
token_path.write_text(content, encoding="utf-8")
|
|
128
|
+
|
|
129
|
+
def clear_tokens(self, tenant_name: str | None = None) -> None:
|
|
130
|
+
"""Clear cached tokens.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
tenant_name: The tenant name to clear tokens for.
|
|
134
|
+
If None, clears all token files.
|
|
135
|
+
|
|
136
|
+
Note:
|
|
137
|
+
Does not raise errors for missing files or directories.
|
|
138
|
+
The token directory is preserved when clearing all tokens.
|
|
139
|
+
"""
|
|
140
|
+
if tenant_name is not None:
|
|
141
|
+
# Clear specific tenant's token
|
|
142
|
+
token_path = self._get_token_path(tenant_name)
|
|
143
|
+
token_path.unlink(missing_ok=True)
|
|
144
|
+
else:
|
|
145
|
+
# Clear all token files
|
|
146
|
+
if self._token_dir.exists():
|
|
147
|
+
for token_file in self._token_dir.glob("*.json"):
|
|
148
|
+
token_file.unlink(missing_ok=True)
|
|
149
|
+
|
|
150
|
+
def _read_csv(self, filename: str) -> list[dict[str, str]]:
|
|
151
|
+
"""Read a CSV file from the cache directory.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
filename: Name of the CSV file (e.g., "locations.csv").
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of dictionaries, one per row. Empty list if file
|
|
158
|
+
doesn't exist or is malformed.
|
|
159
|
+
|
|
160
|
+
Note:
|
|
161
|
+
Uses UTF-8 with BOM support (utf-8-sig encoding) to handle
|
|
162
|
+
PowerShell exports which may include a byte order mark.
|
|
163
|
+
"""
|
|
164
|
+
csv_path = self._cache_dir / filename
|
|
165
|
+
|
|
166
|
+
if not csv_path.exists():
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
with csv_path.open(encoding="utf-8-sig") as f:
|
|
171
|
+
reader = csv.DictReader(f)
|
|
172
|
+
return list(reader)
|
|
173
|
+
except csv.Error:
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
def read_locations_csv(self) -> list[dict[str, str]]:
|
|
177
|
+
"""Read emergency locations from the cache.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of location dictionaries with keys like locationId,
|
|
181
|
+
description, address, city, etc. Empty list if file doesn't
|
|
182
|
+
exist or is malformed.
|
|
183
|
+
"""
|
|
184
|
+
return self._read_csv("locations.csv")
|
|
185
|
+
|
|
186
|
+
def read_policies_csv(self) -> list[dict[str, str]]:
|
|
187
|
+
"""Read policies from the cache.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of policy dictionaries with keys like policyType,
|
|
191
|
+
policyName, policyId, description, isGlobal. Empty list if
|
|
192
|
+
file doesn't exist or is malformed.
|
|
193
|
+
"""
|
|
194
|
+
return self._read_csv("policies.csv")
|
|
195
|
+
|
|
196
|
+
def get_csv_metadata(self) -> CacheMetadata | None:
|
|
197
|
+
"""Get metadata about the CSV cache files.
|
|
198
|
+
|
|
199
|
+
Checks for locations.csv and policies.csv, returning metadata
|
|
200
|
+
about when they were last updated and how many records they contain.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
CacheMetadata with last_updated (oldest mtime of the two files),
|
|
204
|
+
locations_count, and policies_count. Returns None if neither
|
|
205
|
+
CSV file exists.
|
|
206
|
+
"""
|
|
207
|
+
locations_path = self._cache_dir / "locations.csv"
|
|
208
|
+
policies_path = self._cache_dir / "policies.csv"
|
|
209
|
+
|
|
210
|
+
locations_exists = locations_path.exists()
|
|
211
|
+
policies_exists = policies_path.exists()
|
|
212
|
+
|
|
213
|
+
if not locations_exists and not policies_exists:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
# Get modification times, handling potential race conditions
|
|
217
|
+
mtimes: list[datetime] = []
|
|
218
|
+
|
|
219
|
+
if locations_exists:
|
|
220
|
+
try:
|
|
221
|
+
mtime = locations_path.stat().st_mtime
|
|
222
|
+
mtimes.append(datetime.fromtimestamp(mtime, tz=timezone.utc))
|
|
223
|
+
except FileNotFoundError:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
if policies_exists:
|
|
227
|
+
try:
|
|
228
|
+
mtime = policies_path.stat().st_mtime
|
|
229
|
+
mtimes.append(datetime.fromtimestamp(mtime, tz=timezone.utc))
|
|
230
|
+
except FileNotFoundError:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
# Use the OLDER mtime (indicates when cache was last completely updated)
|
|
234
|
+
last_updated = min(mtimes) if mtimes else None
|
|
235
|
+
|
|
236
|
+
# Get row counts using existing methods
|
|
237
|
+
locations_count = len(self.read_locations_csv())
|
|
238
|
+
policies_count = len(self.read_policies_csv())
|
|
239
|
+
|
|
240
|
+
return CacheMetadata(
|
|
241
|
+
last_updated=last_updated,
|
|
242
|
+
locations_count=locations_count,
|
|
243
|
+
policies_count=policies_count,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def get_cache_age_days(self) -> int | None:
|
|
247
|
+
"""Get the age of the cache in days.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Number of days since the oldest CSV file was modified,
|
|
251
|
+
or None if no cache metadata exists or last_updated is None.
|
|
252
|
+
"""
|
|
253
|
+
metadata = self.get_csv_metadata()
|
|
254
|
+
|
|
255
|
+
if metadata is None or metadata.last_updated is None:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
now = datetime.now(timezone.utc)
|
|
259
|
+
delta = now - metadata.last_updated
|
|
260
|
+
return delta.days
|
|
261
|
+
|
|
262
|
+
def is_cache_stale(self) -> bool:
|
|
263
|
+
"""Check if the CSV cache is stale and needs refreshing.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
True if no cache exists or the cache is older than CSV_STALE_DAYS
|
|
267
|
+
(30 days). False if the cache is fresh.
|
|
268
|
+
"""
|
|
269
|
+
age_days = self.get_cache_age_days()
|
|
270
|
+
|
|
271
|
+
if age_days is None:
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
return age_days > CSV_STALE_DAYS
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Configuration manager for reading and writing TOML config files."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if sys.version_info >= (3, 11):
|
|
8
|
+
import tomllib
|
|
9
|
+
else:
|
|
10
|
+
import tomli as tomllib # type: ignore[import-not-found,unused-ignore]
|
|
11
|
+
|
|
12
|
+
import tomlkit
|
|
13
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
14
|
+
|
|
15
|
+
from teams_phone.constants import DEFAULT_CONFIG_PATH
|
|
16
|
+
from teams_phone.exceptions import ConfigurationError, NotFoundError, ValidationError
|
|
17
|
+
from teams_phone.models import Config, TenantProfile
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConfigManager:
|
|
21
|
+
"""Manages Teams Phone CLI configuration file operations.
|
|
22
|
+
|
|
23
|
+
Provides methods to load and save configuration from/to TOML files,
|
|
24
|
+
with validation through Pydantic models and format preservation
|
|
25
|
+
when writing.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
config_path: Path to the configuration file.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config_path: str | None = None) -> None:
|
|
32
|
+
"""Initialize the ConfigManager.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config_path: Path to the configuration file. Defaults to
|
|
36
|
+
~/.teams-phone/config.toml if not specified.
|
|
37
|
+
"""
|
|
38
|
+
path_str = config_path if config_path is not None else DEFAULT_CONFIG_PATH
|
|
39
|
+
self.config_path: Path = Path(path_str).expanduser()
|
|
40
|
+
|
|
41
|
+
def ensure_config_dir(self) -> None:
|
|
42
|
+
"""Create the configuration directory if it doesn't exist.
|
|
43
|
+
|
|
44
|
+
Creates all parent directories as needed.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ConfigurationError: If directory creation fails due to permission error.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
except PermissionError as e:
|
|
52
|
+
raise ConfigurationError(
|
|
53
|
+
f"Cannot create configuration directory {self.config_path.parent}: {e}",
|
|
54
|
+
remediation=(
|
|
55
|
+
f"Check that you have write permissions to {self.config_path.parent}.\n"
|
|
56
|
+
"You may need to create the directory manually or run with elevated privileges."
|
|
57
|
+
),
|
|
58
|
+
) from e
|
|
59
|
+
|
|
60
|
+
def load_config(self) -> Config:
|
|
61
|
+
"""Load and validate configuration from the TOML file.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Config: Validated configuration object.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ConfigurationError: If the file exists but contains invalid
|
|
68
|
+
TOML syntax or fails Pydantic validation.
|
|
69
|
+
|
|
70
|
+
Note:
|
|
71
|
+
Returns a default Config instance if the file doesn't exist.
|
|
72
|
+
"""
|
|
73
|
+
if not self.config_path.exists():
|
|
74
|
+
return Config()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
content = self.config_path.read_text(encoding="utf-8")
|
|
78
|
+
data = tomllib.loads(content)
|
|
79
|
+
return Config.model_validate(data)
|
|
80
|
+
except tomllib.TOMLDecodeError as e:
|
|
81
|
+
raise ConfigurationError(
|
|
82
|
+
f"Invalid TOML syntax in {self.config_path}: {e}",
|
|
83
|
+
remediation=(
|
|
84
|
+
f"Check {self.config_path} for syntax errors.\n"
|
|
85
|
+
"Ensure all strings are properly quoted and "
|
|
86
|
+
"section headers use [brackets]."
|
|
87
|
+
),
|
|
88
|
+
) from e
|
|
89
|
+
except PydanticValidationError as e:
|
|
90
|
+
raise ConfigurationError(
|
|
91
|
+
f"Invalid configuration in {self.config_path}: {e}",
|
|
92
|
+
remediation=(
|
|
93
|
+
f"Review the configuration values in {self.config_path}.\n"
|
|
94
|
+
"Ensure all required fields are present and have valid types."
|
|
95
|
+
),
|
|
96
|
+
) from e
|
|
97
|
+
|
|
98
|
+
def save_config(self, config: Config) -> None:
|
|
99
|
+
"""Save configuration to the TOML file.
|
|
100
|
+
|
|
101
|
+
Uses tomlkit to serialize the configuration, which preserves
|
|
102
|
+
formatting when possible. Creates the configuration directory
|
|
103
|
+
if it doesn't exist.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: Configuration object to save.
|
|
107
|
+
|
|
108
|
+
Note:
|
|
109
|
+
This method overwrites the existing file. To preserve
|
|
110
|
+
comments in an existing file, load it with tomlkit first,
|
|
111
|
+
update values, and write back.
|
|
112
|
+
"""
|
|
113
|
+
self.ensure_config_dir()
|
|
114
|
+
|
|
115
|
+
# Serialize with mode="json" to convert UUID/Enum to strings
|
|
116
|
+
# exclude_none=True because tomlkit doesn't handle None values
|
|
117
|
+
data = config.model_dump(mode="json", exclude_none=True)
|
|
118
|
+
content = tomlkit.dumps(data)
|
|
119
|
+
self.config_path.write_text(content, encoding="utf-8")
|
|
120
|
+
|
|
121
|
+
def get_tenant(self, name: str) -> TenantProfile:
|
|
122
|
+
"""Retrieve a tenant profile by name.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
name: The tenant profile name to look up.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
TenantProfile: The tenant profile configuration.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
NotFoundError: If no tenant with the given name exists.
|
|
132
|
+
"""
|
|
133
|
+
config = self.load_config()
|
|
134
|
+
if name not in config.tenants:
|
|
135
|
+
raise NotFoundError(
|
|
136
|
+
f"Tenant '{name}' not found",
|
|
137
|
+
remediation="Run 'teams-phone tenants list' to see configured tenants.",
|
|
138
|
+
)
|
|
139
|
+
return config.tenants[name]
|
|
140
|
+
|
|
141
|
+
def set_tenant(self, name: str, profile: TenantProfile) -> None:
|
|
142
|
+
"""Add or update a tenant profile.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
name: The key name for the tenant profile.
|
|
146
|
+
profile: The tenant profile configuration to save.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValidationError: If profile.name does not match the name parameter.
|
|
150
|
+
"""
|
|
151
|
+
if profile.name != name:
|
|
152
|
+
raise ValidationError(
|
|
153
|
+
f"Tenant profile name '{profile.name}' does not match key '{name}'",
|
|
154
|
+
remediation="Ensure the profile name matches the key being set.",
|
|
155
|
+
)
|
|
156
|
+
config = self.load_config()
|
|
157
|
+
config.tenants[name] = profile
|
|
158
|
+
self.save_config(config)
|
|
159
|
+
|
|
160
|
+
def remove_tenant(self, name: str) -> None:
|
|
161
|
+
"""Remove a tenant profile by name.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name: The tenant profile name to remove.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
NotFoundError: If no tenant with the given name exists.
|
|
168
|
+
|
|
169
|
+
Note:
|
|
170
|
+
If the removed tenant is the default tenant, the default_tenant
|
|
171
|
+
setting is cleared.
|
|
172
|
+
"""
|
|
173
|
+
config = self.load_config()
|
|
174
|
+
if name not in config.tenants:
|
|
175
|
+
raise NotFoundError(
|
|
176
|
+
f"Tenant '{name}' not found",
|
|
177
|
+
remediation="Run 'teams-phone tenants list' to see configured tenants.",
|
|
178
|
+
)
|
|
179
|
+
del config.tenants[name]
|
|
180
|
+
if config.default_tenant == name:
|
|
181
|
+
config.default_tenant = None
|
|
182
|
+
self.save_config(config)
|
|
183
|
+
|
|
184
|
+
def get_default_tenant(self) -> str | None:
|
|
185
|
+
"""Retrieve the default tenant name.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The name of the default tenant, or None if not set.
|
|
189
|
+
"""
|
|
190
|
+
config = self.load_config()
|
|
191
|
+
return config.default_tenant
|
|
192
|
+
|
|
193
|
+
def set_default_tenant(self, name: str) -> None:
|
|
194
|
+
"""Set the default tenant.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
name: The tenant name to set as default.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
NotFoundError: If the specified tenant doesn't exist.
|
|
201
|
+
"""
|
|
202
|
+
config = self.load_config()
|
|
203
|
+
if name not in config.tenants:
|
|
204
|
+
raise NotFoundError(
|
|
205
|
+
f"Tenant '{name}' not found",
|
|
206
|
+
remediation="Run 'teams-phone tenants list' to see configured tenants.",
|
|
207
|
+
)
|
|
208
|
+
config.default_tenant = name
|
|
209
|
+
self.save_config(config)
|