ic-code 1.0.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.
- ic/__init__.py +29 -0
- ic/cli.py +769 -0
- ic/commands/__init__.py +10 -0
- ic/commands/config.py +699 -0
- ic/compat/__init__.py +241 -0
- ic/compat/cli.py +289 -0
- ic/compat/common.py +243 -0
- ic/config/__init__.py +10 -0
- ic/config/cleanup.py +382 -0
- ic/config/docs_organizer.py +587 -0
- ic/config/external.py +456 -0
- ic/config/manager.py +898 -0
- ic/config/migration.py +628 -0
- ic/config/schema.py +595 -0
- ic/config/secrets.py +437 -0
- ic/config/security.py +462 -0
- ic/core/__init__.py +16 -0
- ic/core/logging.py +311 -0
- ic/core/mcp_manager.py +856 -0
- ic/core/session.py +392 -0
- ic/core/silence_logging.py +67 -0
- ic_code-1.0.0.dist-info/METADATA +354 -0
- ic_code-1.0.0.dist-info/RECORD +27 -0
- ic_code-1.0.0.dist-info/WHEEL +5 -0
- ic_code-1.0.0.dist-info/entry_points.txt +2 -0
- ic_code-1.0.0.dist-info/licenses/LICENSE +21 -0
- ic_code-1.0.0.dist-info/top_level.txt +1 -0
ic/core/session.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enhanced AWS Session Manager for IC CLI
|
|
4
|
+
|
|
5
|
+
This module provides intelligent AWS session management with:
|
|
6
|
+
- Profile type detection (assume_role vs direct credentials)
|
|
7
|
+
- Session caching for improved performance
|
|
8
|
+
- Account alias resolution with fallback to account ID
|
|
9
|
+
- Backward compatibility with existing configurations
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
import configparser
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
from typing import Dict, Optional, Tuple, Any
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
20
|
+
|
|
21
|
+
import boto3
|
|
22
|
+
from botocore.exceptions import BotoCoreError, ClientError, NoCredentialsError
|
|
23
|
+
|
|
24
|
+
from ic.core.logging import get_logger
|
|
25
|
+
|
|
26
|
+
logger = get_logger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ProfileInfo:
|
|
31
|
+
"""Information about an AWS profile"""
|
|
32
|
+
name: str
|
|
33
|
+
type: str # 'assume_role' or 'direct'
|
|
34
|
+
account_id: str
|
|
35
|
+
role_arn: Optional[str] = None
|
|
36
|
+
source_profile: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class SessionInfo:
|
|
41
|
+
"""Information about an AWS session"""
|
|
42
|
+
session: boto3.Session
|
|
43
|
+
account_id: str
|
|
44
|
+
account_alias: str
|
|
45
|
+
region: str
|
|
46
|
+
created_at: datetime
|
|
47
|
+
expires_at: Optional[datetime] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AWSSessionManager:
|
|
51
|
+
"""Enhanced AWS session manager with intelligent profile detection and caching"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, config=None):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the AWS session manager
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
config: Configuration object with AWS settings
|
|
59
|
+
"""
|
|
60
|
+
self.config = config
|
|
61
|
+
self.profile_cache: Dict[str, ProfileInfo] = {}
|
|
62
|
+
self.session_cache: Dict[str, SessionInfo] = {}
|
|
63
|
+
self.account_alias_cache: Dict[str, str] = {}
|
|
64
|
+
self._profiles_loaded = False
|
|
65
|
+
|
|
66
|
+
# Configuration defaults
|
|
67
|
+
self.session_duration = getattr(config, 'session_duration', 3600) if config else 3600
|
|
68
|
+
self.max_workers = getattr(config, 'max_workers', 10) if config else 10
|
|
69
|
+
|
|
70
|
+
def get_profiles(self) -> Dict[str, ProfileInfo]:
|
|
71
|
+
"""
|
|
72
|
+
Get AWS profiles with account ID mapping and type detection
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dict mapping account_id to ProfileInfo
|
|
76
|
+
"""
|
|
77
|
+
if self._profiles_loaded and self.profile_cache:
|
|
78
|
+
return self.profile_cache
|
|
79
|
+
|
|
80
|
+
logger.log_info_file_only("Loading AWS profiles from ~/.aws/config")
|
|
81
|
+
|
|
82
|
+
config = configparser.ConfigParser()
|
|
83
|
+
aws_config_path = os.path.expanduser('~/.aws/config')
|
|
84
|
+
|
|
85
|
+
if not os.path.exists(aws_config_path):
|
|
86
|
+
logger.log_error(f"AWS config file not found: {aws_config_path}")
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
config.read(aws_config_path)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.log_error(f"Failed to read AWS config: {e}")
|
|
93
|
+
return {}
|
|
94
|
+
|
|
95
|
+
profiles = {}
|
|
96
|
+
|
|
97
|
+
# Process profile sections
|
|
98
|
+
for section in config.sections():
|
|
99
|
+
if section.startswith('profile '):
|
|
100
|
+
profile_name = section.split('profile ')[1]
|
|
101
|
+
self._process_profile_section(config[section], profile_name, profiles)
|
|
102
|
+
|
|
103
|
+
# Process default profile if exists
|
|
104
|
+
if 'default' in config.sections():
|
|
105
|
+
self._process_profile_section(config['default'], 'default', profiles)
|
|
106
|
+
|
|
107
|
+
self.profile_cache = profiles
|
|
108
|
+
self._profiles_loaded = True
|
|
109
|
+
|
|
110
|
+
logger.log_info_file_only(f"Loaded {len(profiles)} AWS profiles")
|
|
111
|
+
return profiles
|
|
112
|
+
|
|
113
|
+
def _process_profile_section(self, section, profile_name: str, profiles: Dict[str, ProfileInfo]):
|
|
114
|
+
"""Process a single profile section from AWS config"""
|
|
115
|
+
try:
|
|
116
|
+
role_arn = section.get('role_arn')
|
|
117
|
+
|
|
118
|
+
if role_arn:
|
|
119
|
+
# Assume role profile
|
|
120
|
+
account_id = self._extract_account_id_from_arn(role_arn)
|
|
121
|
+
if account_id:
|
|
122
|
+
source_profile = section.get('source_profile')
|
|
123
|
+
profiles[account_id] = ProfileInfo(
|
|
124
|
+
name=profile_name,
|
|
125
|
+
type='assume_role',
|
|
126
|
+
account_id=account_id,
|
|
127
|
+
role_arn=role_arn,
|
|
128
|
+
source_profile=source_profile
|
|
129
|
+
)
|
|
130
|
+
logger.log_info_file_only(f"Found assume_role profile: {profile_name} -> {account_id}")
|
|
131
|
+
else:
|
|
132
|
+
# Direct credentials profile
|
|
133
|
+
account_id = self._get_account_id_from_session(profile_name)
|
|
134
|
+
if account_id:
|
|
135
|
+
profiles[account_id] = ProfileInfo(
|
|
136
|
+
name=profile_name,
|
|
137
|
+
type='direct',
|
|
138
|
+
account_id=account_id
|
|
139
|
+
)
|
|
140
|
+
logger.log_info_file_only(f"Found direct profile: {profile_name} -> {account_id}")
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.log_error(f"Error processing profile {profile_name}: {e}")
|
|
144
|
+
|
|
145
|
+
def _extract_account_id_from_arn(self, role_arn: str) -> Optional[str]:
|
|
146
|
+
"""Extract account ID from role ARN"""
|
|
147
|
+
match = re.search(r'arn:aws:iam::(\d+):role', role_arn)
|
|
148
|
+
return match.group(1) if match else None
|
|
149
|
+
|
|
150
|
+
def _get_account_id_from_session(self, profile_name: str) -> Optional[str]:
|
|
151
|
+
"""Get account ID from a session using STS get_caller_identity"""
|
|
152
|
+
try:
|
|
153
|
+
session = boto3.Session(profile_name=profile_name)
|
|
154
|
+
sts = session.client('sts')
|
|
155
|
+
identity = sts.get_caller_identity()
|
|
156
|
+
return identity['Account']
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.log_info_file_only(f"Could not get account ID for profile {profile_name}: {e}")
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def create_session(self, account_id: str, region: str) -> Optional[boto3.Session]:
|
|
162
|
+
"""
|
|
163
|
+
Create an AWS session for the specified account and region
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
account_id: AWS account ID
|
|
167
|
+
region: AWS region name
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
boto3.Session or None if creation fails
|
|
171
|
+
"""
|
|
172
|
+
cache_key = f"{account_id}:{region}"
|
|
173
|
+
|
|
174
|
+
# Check cache first
|
|
175
|
+
if cache_key in self.session_cache:
|
|
176
|
+
session_info = self.session_cache[cache_key]
|
|
177
|
+
if self._is_session_valid(session_info):
|
|
178
|
+
logger.log_info_file_only(f"Using cached session for {account_id} in {region}")
|
|
179
|
+
return session_info.session
|
|
180
|
+
else:
|
|
181
|
+
# Remove expired session
|
|
182
|
+
del self.session_cache[cache_key]
|
|
183
|
+
|
|
184
|
+
# Get profile information
|
|
185
|
+
profiles = self.get_profiles()
|
|
186
|
+
profile_info = profiles.get(account_id)
|
|
187
|
+
|
|
188
|
+
if not profile_info:
|
|
189
|
+
logger.log_error(f"No profile found for account {account_id}")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
logger.log_info_file_only(f"Creating {profile_info.type} session for account {account_id} in {region}")
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
if profile_info.type == 'assume_role':
|
|
196
|
+
session = self._create_assume_role_session(profile_info, region)
|
|
197
|
+
else:
|
|
198
|
+
session = self._create_direct_session(profile_info, region)
|
|
199
|
+
|
|
200
|
+
if session:
|
|
201
|
+
# Get account alias for caching
|
|
202
|
+
account_alias = self.get_account_alias(session)
|
|
203
|
+
|
|
204
|
+
# Cache the session
|
|
205
|
+
session_info = SessionInfo(
|
|
206
|
+
session=session,
|
|
207
|
+
account_id=account_id,
|
|
208
|
+
account_alias=account_alias,
|
|
209
|
+
region=region,
|
|
210
|
+
created_at=datetime.now(),
|
|
211
|
+
expires_at=datetime.now() + timedelta(seconds=self.session_duration) if profile_info.type == 'assume_role' else None
|
|
212
|
+
)
|
|
213
|
+
self.session_cache[cache_key] = session_info
|
|
214
|
+
|
|
215
|
+
logger.log_info_file_only(f"Successfully created session for {account_id} ({account_alias}) in {region}")
|
|
216
|
+
return session
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.log_error(f"Failed to create session for account {account_id}: {e}")
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
def _create_assume_role_session(self, profile_info: ProfileInfo, region: str) -> Optional[boto3.Session]:
|
|
224
|
+
"""Create session using assume role"""
|
|
225
|
+
if not profile_info.source_profile or not profile_info.role_arn:
|
|
226
|
+
logger.log_error(f"Missing source_profile or role_arn for assume_role profile {profile_info.name}")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
# Create source session
|
|
231
|
+
source_session = boto3.Session(
|
|
232
|
+
profile_name=profile_info.source_profile,
|
|
233
|
+
region_name=region
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Assume role
|
|
237
|
+
sts_client = source_session.client('sts')
|
|
238
|
+
response = sts_client.assume_role(
|
|
239
|
+
RoleArn=profile_info.role_arn,
|
|
240
|
+
RoleSessionName=f"ic-session-{int(time.time())}",
|
|
241
|
+
DurationSeconds=self.session_duration
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
credentials = response['Credentials']
|
|
245
|
+
return boto3.Session(
|
|
246
|
+
aws_access_key_id=credentials['AccessKeyId'],
|
|
247
|
+
aws_secret_access_key=credentials['SecretAccessKey'],
|
|
248
|
+
aws_session_token=credentials['SessionToken'],
|
|
249
|
+
region_name=region
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.log_error(f"Failed to assume role {profile_info.role_arn}: {e}")
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
def _create_direct_session(self, profile_info: ProfileInfo, region: str) -> Optional[boto3.Session]:
|
|
257
|
+
"""Create session using direct credentials"""
|
|
258
|
+
try:
|
|
259
|
+
return boto3.Session(
|
|
260
|
+
profile_name=profile_info.name,
|
|
261
|
+
region_name=region
|
|
262
|
+
)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.log_error(f"Failed to create direct session for profile {profile_info.name}: {e}")
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
def _is_session_valid(self, session_info: SessionInfo) -> bool:
|
|
268
|
+
"""Check if a cached session is still valid"""
|
|
269
|
+
if session_info.expires_at:
|
|
270
|
+
# Check if assume role session has expired
|
|
271
|
+
return datetime.now() < session_info.expires_at
|
|
272
|
+
else:
|
|
273
|
+
# Direct sessions don't expire, but check if they're too old
|
|
274
|
+
age = datetime.now() - session_info.created_at
|
|
275
|
+
return age < timedelta(hours=1) # Refresh after 1 hour
|
|
276
|
+
|
|
277
|
+
def get_account_alias(self, session: boto3.Session) -> str:
|
|
278
|
+
"""
|
|
279
|
+
Get account alias with fallback to account ID
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
session: boto3.Session
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Account alias or account ID if alias not available
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
# Try to get from cache first
|
|
289
|
+
sts = session.client('sts')
|
|
290
|
+
identity = sts.get_caller_identity()
|
|
291
|
+
account_id = identity['Account']
|
|
292
|
+
|
|
293
|
+
if account_id in self.account_alias_cache:
|
|
294
|
+
return self.account_alias_cache[account_id]
|
|
295
|
+
|
|
296
|
+
# Try to get account alias
|
|
297
|
+
iam = session.client('iam')
|
|
298
|
+
aliases = iam.list_account_aliases()
|
|
299
|
+
|
|
300
|
+
if aliases['AccountAliases']:
|
|
301
|
+
alias = aliases['AccountAliases'][0]
|
|
302
|
+
self.account_alias_cache[account_id] = alias
|
|
303
|
+
return alias
|
|
304
|
+
else:
|
|
305
|
+
# No alias, use account ID
|
|
306
|
+
self.account_alias_cache[account_id] = account_id
|
|
307
|
+
return account_id
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.log_info_file_only(f"Failed to get account alias: {e}")
|
|
311
|
+
return "unknown"
|
|
312
|
+
|
|
313
|
+
def create_sessions_parallel(self, account_regions: list) -> Dict[str, boto3.Session]:
|
|
314
|
+
"""
|
|
315
|
+
Create multiple sessions in parallel
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
account_regions: List of (account_id, region) tuples
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Dict mapping "account_id:region" to session
|
|
322
|
+
"""
|
|
323
|
+
sessions = {}
|
|
324
|
+
|
|
325
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
326
|
+
future_to_key = {}
|
|
327
|
+
|
|
328
|
+
for account_id, region in account_regions:
|
|
329
|
+
cache_key = f"{account_id}:{region}"
|
|
330
|
+
future = executor.submit(self.create_session, account_id, region)
|
|
331
|
+
future_to_key[future] = cache_key
|
|
332
|
+
|
|
333
|
+
for future in as_completed(future_to_key):
|
|
334
|
+
cache_key = future_to_key[future]
|
|
335
|
+
try:
|
|
336
|
+
session = future.result()
|
|
337
|
+
if session:
|
|
338
|
+
sessions[cache_key] = session
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.log_error(f"Failed to create session for {cache_key}: {e}")
|
|
341
|
+
|
|
342
|
+
return sessions
|
|
343
|
+
|
|
344
|
+
def clear_cache(self):
|
|
345
|
+
"""Clear all cached sessions and profiles"""
|
|
346
|
+
self.session_cache.clear()
|
|
347
|
+
self.profile_cache.clear()
|
|
348
|
+
self.account_alias_cache.clear()
|
|
349
|
+
self._profiles_loaded = False
|
|
350
|
+
logger.log_info_file_only("Cleared AWS session cache")
|
|
351
|
+
|
|
352
|
+
def get_session_info(self, account_id: str, region: str) -> Optional[SessionInfo]:
|
|
353
|
+
"""Get cached session information"""
|
|
354
|
+
cache_key = f"{account_id}:{region}"
|
|
355
|
+
return self.session_cache.get(cache_key)
|
|
356
|
+
|
|
357
|
+
def list_cached_sessions(self) -> Dict[str, SessionInfo]:
|
|
358
|
+
"""List all cached sessions"""
|
|
359
|
+
return self.session_cache.copy()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# Backward compatibility functions
|
|
363
|
+
def get_profiles() -> Dict[str, str]:
|
|
364
|
+
"""
|
|
365
|
+
Backward compatibility function for existing code
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Dict mapping account_id to profile_name
|
|
369
|
+
"""
|
|
370
|
+
manager = AWSSessionManager()
|
|
371
|
+
profiles = manager.get_profiles()
|
|
372
|
+
return {account_id: profile.name for account_id, profile in profiles.items()}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def create_session(profile_name: str, region_name: str) -> Optional[boto3.Session]:
|
|
376
|
+
"""
|
|
377
|
+
Backward compatibility function for existing code
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
profile_name: AWS profile name
|
|
381
|
+
region_name: AWS region name
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
boto3.Session or None
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
session = boto3.Session(profile_name=profile_name, region_name=region_name)
|
|
388
|
+
logger.log_info_file_only(f"Created session for profile '{profile_name}' in region '{region_name}'")
|
|
389
|
+
return session
|
|
390
|
+
except Exception as e:
|
|
391
|
+
logger.log_error(f"Failed to create session for profile '{profile_name}' in region '{region_name}': {e}")
|
|
392
|
+
return None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging silencer module to suppress console output except for ERROR messages.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SilentFilter(logging.Filter):
|
|
11
|
+
"""Filter to suppress all log messages except ERROR and CRITICAL."""
|
|
12
|
+
|
|
13
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
14
|
+
return record.levelno >= logging.ERROR
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SilentHandler(logging.Handler):
|
|
18
|
+
"""Handler that does nothing - completely silent."""
|
|
19
|
+
|
|
20
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def silence_all_logging():
|
|
25
|
+
"""Silence all logging output to console except ERROR and CRITICAL."""
|
|
26
|
+
# Get root logger
|
|
27
|
+
root_logger = logging.getLogger()
|
|
28
|
+
|
|
29
|
+
# Remove all existing handlers
|
|
30
|
+
for handler in root_logger.handlers[:]:
|
|
31
|
+
root_logger.removeHandler(handler)
|
|
32
|
+
|
|
33
|
+
# Add silent handler for everything below ERROR
|
|
34
|
+
silent_handler = SilentHandler()
|
|
35
|
+
silent_handler.setLevel(logging.DEBUG)
|
|
36
|
+
root_logger.addHandler(silent_handler)
|
|
37
|
+
|
|
38
|
+
# Add console handler only for ERROR and above
|
|
39
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
40
|
+
console_handler.setLevel(logging.ERROR)
|
|
41
|
+
console_handler.addFilter(SilentFilter())
|
|
42
|
+
root_logger.addHandler(console_handler)
|
|
43
|
+
|
|
44
|
+
# Set root logger level to DEBUG to catch everything
|
|
45
|
+
root_logger.setLevel(logging.DEBUG)
|
|
46
|
+
|
|
47
|
+
# Also silence specific loggers that might be problematic
|
|
48
|
+
problematic_loggers = [
|
|
49
|
+
'ic.config.manager',
|
|
50
|
+
'ic.config.secrets',
|
|
51
|
+
'ic.config.external',
|
|
52
|
+
'ic.config.security',
|
|
53
|
+
'ic.core.logging',
|
|
54
|
+
'rich'
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
for logger_name in problematic_loggers:
|
|
58
|
+
logger = logging.getLogger(logger_name)
|
|
59
|
+
logger.handlers.clear()
|
|
60
|
+
logger.addHandler(SilentHandler())
|
|
61
|
+
logger.setLevel(logging.CRITICAL)
|
|
62
|
+
logger.propagate = False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def restore_error_only_logging():
|
|
66
|
+
"""Restore logging to show only ERROR and CRITICAL messages."""
|
|
67
|
+
silence_all_logging()
|