updsts 0.8.9__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.
updsts/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # encoding: utf-8-sig
updsts/__main__.py ADDED
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8-sig
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from argparse import ArgumentParser
7
+
8
+ from .cmdparam import *
9
+ from .cmd_handler import *
10
+ from .logutil import get_logger
11
+
12
+ # ---------------------------------------------------------------------------------------
13
+ def main():
14
+ argp = ArgumentParser(prog="updsts",
15
+ description="Get/Update profile of the aws sts security tokens.")
16
+ # Register common arguments
17
+ common = ArgumentParser(add_help=False)
18
+ common.add_argument(
19
+ '-v',
20
+ '--verbose',
21
+ type=int,
22
+ default=0,
23
+ choices=[0, 1, 2],
24
+ metavar='LEVEL',
25
+ help='Set the verbosity level (0: normal, 1: verbose, 2: debug).'
26
+ )
27
+ user_home = os.path.expanduser("~")
28
+ common.add_argument(
29
+ '-c',
30
+ '--credential_file',
31
+ type=str,
32
+ default=None,
33
+ help='Path to the credetial file where profiles stored. (default: ~/.aws/credentials)'
34
+ )
35
+
36
+ subparsers = argp.add_subparsers(dest='command',
37
+ help='Available commands')
38
+ # Register subcommands
39
+ register_sub_get(subparsers, handle_get, parent_parser=common)
40
+ register_sub_list(subparsers, handle_list, parent_parser=common)
41
+ register_sub_mcp(subparsers, handle_mcp, parent_parser=common)
42
+
43
+ try:
44
+ # Parse the command line arguments
45
+ args = argp.parse_args()
46
+ # If no command is specified, show help
47
+ if args.command is None:
48
+ argp.print_help()
49
+ else:
50
+ # Initialize the logger with the specified verbosity level
51
+ _ = get_logger(verbose_level=args.verbose)
52
+ # Execute the handler for the specified command
53
+ if hasattr(args, 'handler'):
54
+ args.handler(args)
55
+ else:
56
+ argp.print_help()
57
+
58
+ except FileNotFoundError:
59
+ print("Error: Secrets file not found. Please ensure the file exists or specify a valid path.")
60
+ except PermissionError:
61
+ print("Error: Permission denied when accessing the credential file. Check your permissions.")
62
+ except KeyError as e:
63
+ print(f"Error: Profile name '{e}' is not found in the credential file. Please check the profile name.")
64
+ except ValueError as e:
65
+ print(f"Error: {e}")
66
+ except Exception as e:
67
+ print(f"Exception: {e}")
68
+
69
+ # ---------------------------------------------------------------------------------------
70
+ if __name__ == "__main__":
71
+ main()
updsts/awsutil.py ADDED
@@ -0,0 +1,308 @@
1
+ # encoding: utf-8-sig
2
+
3
+ import os
4
+ import sys
5
+ import boto3
6
+ from datetime import datetime, timezone
7
+
8
+ from botocore.exceptions import BotoCoreError, ClientError
9
+ from configparser import ConfigParser, NoSectionError, NoOptionError
10
+ from pathlib import Path
11
+ from typing import Optional, Dict, Any, Callable
12
+
13
+ from .logutil import get_logger
14
+ from .upcred import CredentialUpdater
15
+
16
+ # ----------------------------------------------------------------------------
17
+ def mask_string(s: str,
18
+ unmask_chars_head: int = 4,
19
+ unmask_chars_tail: int = 4,
20
+ max_strlen = 16) -> str:
21
+ """
22
+ Mask the middle characters of the input string `s`, leaving head and tail visible.
23
+
24
+ Args:
25
+ s (str): The input string to mask
26
+ unmask_chars_head (int, optional): Number of characters to show at the beginning. Defaults to 4.
27
+ unmask_chars_tail (int, optional): Number of characters to show at the end. Defaults to 4.
28
+ max_strlen (int, optional): Maximum display length before truncation. Defaults to 16.
29
+
30
+ Returns:
31
+ str: Masked string with the following behavior:
32
+ - If string length <= unmask_chars_head + unmask_chars_tail: returns the string as is
33
+ - If string length > max_strlen: shows head + masked chars + tail + ' (total_length chars)'
34
+ - Otherwise: shows head + masked middle + tail
35
+
36
+ Examples:
37
+ mask_string("abc", 4, 4) -> "abc"
38
+ mask_string("abcdefgh", 4, 0) -> "abcd****"
39
+ mask_string("abcdefgh", 4, 2) -> "abcd**gh"
40
+ mask_string("very_long_string_example", 4, 4, 16) -> "very********mple (24 chars)"
41
+ """
42
+ if not s:
43
+ return ''
44
+
45
+ # Strip whitespace and newlines from input
46
+ s = s.strip()
47
+ str_len = len(s)
48
+ if str_len <= unmask_chars_head + unmask_chars_tail:
49
+ return s
50
+
51
+ tail_part = s[-unmask_chars_tail:] if unmask_chars_tail > 0 else ''
52
+
53
+ # Handle long strings
54
+ if str_len > max_strlen:
55
+ truncate_len = max_strlen
56
+ middle_mask_len = truncate_len - unmask_chars_head - unmask_chars_tail
57
+ if middle_mask_len <= 0:
58
+ return s[:unmask_chars_head] + tail_part + f' ({str_len} chars)'
59
+ masked_part = '*' * middle_mask_len
60
+ return s[:unmask_chars_head] + masked_part + tail_part + f' ({str_len} chars)'
61
+
62
+ # Normal masking for strings within max_strlen
63
+ middle_mask_len = str_len - unmask_chars_head - unmask_chars_tail
64
+ masked_part = '*' * middle_mask_len
65
+ return s[:unmask_chars_head] + masked_part + tail_part
66
+
67
+
68
+ # ----------------------------------------------------------------------------
69
+ def get_credential_file_path(credential_file: str | None = None) -> Path:
70
+ """
71
+ Get the path to the AWS credentials file.
72
+ Args:
73
+ credential_file (str | None, optional): Path to the AWS credentials file.
74
+ If None, the default location (~/.aws/credentials) is used. Defaults to None.
75
+ Returns:
76
+ credential_file_path (Path): Path object of the credential file.
77
+ """
78
+ cred_path = credential_file.strip() if credential_file is not None else credential_file
79
+ if not cred_path:
80
+ user_home = os.path.expanduser("~")
81
+ credential_file_path = Path(user_home) / ".aws" / "credentials"
82
+ else:
83
+ credential_file_path = Path(cred_path)
84
+ logger = get_logger()
85
+ logger.debug(f"Using credential file: {credential_file_path}")
86
+ return credential_file_path
87
+
88
+ # ----------------------------------------------------------------------------
89
+ def get_sts_token(profile_name: str,
90
+ totp_token: str,
91
+ duration_seconds: int = 3600,
92
+ credential_file: str | None = None) -> Optional[Dict[str, Any]]:
93
+ """
94
+ Get temporary STS token using MFA.
95
+ Args:
96
+ profile_name (str): The profile name in the AWS credentials file.
97
+ totp_token (str): The TOTP token of the registerd MFA device.
98
+ credential_file (str | None, optional): Path to the AWS credentials file.
99
+ If None, the default location (~/.aws/credentials) is used. Defaults to None.
100
+ Returns:
101
+ Optional[Dict[str, Any]]: A dictionary containing the temporary STS credentials
102
+ if successful, None otherwise.
103
+ Raises:
104
+ Exception: If there is an error reading the credentials file or obtaining the STS token.
105
+ """
106
+ logger = get_logger()
107
+
108
+ if totp_token is None or len(totp_token) == 0:
109
+ raise ValueError("TOTP token is required to obtain STS token.")
110
+
111
+ credential_file = get_credential_file_path(credential_file)
112
+ if not credential_file.exists():
113
+ raise FileNotFoundError(f"Credential file '{credential_file}' does not exist.")
114
+ config = ConfigParser()
115
+ config.read(credential_file)
116
+ try:
117
+ access_key = config.get(profile_name, 'aws_access_key_id')
118
+ secret_key = config.get(profile_name, 'aws_secret_access_key')
119
+ mfa_arn = config.get(profile_name, 'mfa_device_arn')
120
+ except (NoOptionError) as e:
121
+ raise Exception(f"Profile '{profile_name}' is missing required options: {e}")
122
+ except (NoSectionError) as e:
123
+ raise Exception(f"Error reading profile '{profile_name}' from credentials file: {e}")
124
+
125
+ logger.debug(f"Using profile '{profile_name}' with access key '{access_key}' and MFA device ARN '{mfa_arn}'")
126
+
127
+ try:
128
+ # Read proxy settings from environment variables
129
+ proxies = {}
130
+ http_proxy = os.environ.get('http_proxy') or os.environ.get('HTTP_PROXY')
131
+ https_proxy = os.environ.get('https_proxy') or os.environ.get('HTTPS_PROXY')
132
+ if http_proxy:
133
+ proxies['http'] = http_proxy
134
+ logger.debug(f"Using HTTP proxy: {http_proxy}")
135
+ if https_proxy:
136
+ proxies['https'] = https_proxy
137
+ logger.debug(f"Using HTTPS proxy: {https_proxy}")
138
+
139
+ # Create a session with explicit credentials
140
+ session = boto3.Session(
141
+ aws_access_key_id=access_key,
142
+ aws_secret_access_key=secret_key
143
+ )
144
+
145
+ # Create STS client with proxy configuration if available
146
+ client_config = {}
147
+ if proxies:
148
+ client_config['proxies'] = proxies
149
+
150
+ sts_client = session.client('sts', **client_config)
151
+ response = sts_client.get_session_token(DurationSeconds=duration_seconds,
152
+ SerialNumber=mfa_arn,
153
+ TokenCode=totp_token)
154
+ credentials = response['Credentials']
155
+ logger.info(f"Successfully obtained temporary STS credentials for profile '{profile_name}'")
156
+
157
+ # Convert expiration time from UTC to local timezone
158
+ expiration_utc = credentials['Expiration']
159
+ # AWS returns naive datetime in UTC, so we need to make it timezone-aware
160
+ if expiration_utc.tzinfo is None:
161
+ expiration_utc = expiration_utc.replace(tzinfo=timezone.utc)
162
+ # Convert to local timezone
163
+ expiration_local = expiration_utc.astimezone()
164
+
165
+ return {
166
+ 'AccessKeyId': credentials['AccessKeyId'],
167
+ 'SecretAccessKey': credentials['SecretAccessKey'],
168
+ 'SessionToken': credentials['SessionToken'],
169
+ 'Expiration': expiration_local.isoformat()
170
+ }
171
+ except (BotoCoreError, ClientError) as e:
172
+ logger.error(f"Error obtaining STS token: {e}")
173
+ return None
174
+
175
+ # ----------------------------------------------------------------------------
176
+ def get_profile_info(profile_name: str,
177
+ credential_file: str | None = None,
178
+ secret_mask: bool = False,
179
+ ctx: ConfigParser = None) -> Optional[Dict[str, str]]:
180
+ """
181
+ Get the credencials from the AWS credentials file for the specified profile.
182
+ Args:
183
+ profile_name (str): The profile name in the AWS credentials file.
184
+ credential_file (str | None, optional): Path to the AWS credentials file.
185
+ If None, the default location (~/.aws/credentials) is used. Defaults to None.
186
+ Returns:
187
+ Optional[Dict[str, str]]: A dictionary containing secret details
188
+ if successful, None otherwise.
189
+ Raises:
190
+ Exception: If there is an error reading the profile from the credentials file.
191
+ """
192
+ logger = get_logger()
193
+ config = ctx
194
+ if config is None:
195
+ credential_file = get_credential_file_path(credential_file)
196
+ if not credential_file.exists():
197
+ raise FileNotFoundError(f"Credential file '{credential_file}' does not exist.")
198
+ config = ConfigParser()
199
+ config.read(credential_file)
200
+ item_dic = None
201
+ try:
202
+ item_dic = {
203
+ 'aws_access_key_id': '(not defined)',
204
+ 'aws_secret_access_key': '(not defined)',
205
+ 'aws_session_token': '(not defined)',
206
+ 'mfa_device_arn': '(not defined)',
207
+ 'totp_secret_name': '(not defined)',
208
+ 'expiration_datetime': '(not defined)'
209
+ }
210
+ for key, value in config.items(profile_name):
211
+ # Remove whitespace and newlines from log output
212
+ raw_sensitive_value = value.strip() if value else value
213
+ logger.debug(f"Profile '{profile_name}': {key} = {raw_sensitive_value}")
214
+ # Mask secret key and session token if required
215
+ if key == 'aws_access_key_id':
216
+ secret_key_id = mask_string(raw_sensitive_value) if (raw_sensitive_value and secret_mask) else raw_sensitive_value
217
+ item_dic[key] = secret_key_id
218
+ elif key == 'aws_secret_access_key':
219
+ secret_key = mask_string(raw_sensitive_value) if (raw_sensitive_value and secret_mask) else raw_sensitive_value
220
+ item_dic[key] = secret_key
221
+ elif key == 'aws_session_token':
222
+ session_token = mask_string(raw_sensitive_value) if (raw_sensitive_value and secret_mask) else raw_sensitive_value
223
+ item_dic[key] = session_token
224
+ else:
225
+ item_dic[key] = raw_sensitive_value
226
+ item_dic['profile_name'] = profile_name
227
+ return item_dic
228
+ except (NoSectionError) as e:
229
+ logger.error(f"Error reading profile '{profile_name}' from credentials file: {e}")
230
+ except (NoOptionError) as e:
231
+ logger.error(f"Profile '{profile_name}' is missing required options: {e}")
232
+ return item_dic
233
+
234
+ # ----------------------------------------------------------------------------
235
+ def get_profile_list(credential_file: str | None = None,
236
+ secret_mask: bool = False) -> list[dict[str, str]]:
237
+ """
238
+ Get the list of credencials from the AWS credentials file.
239
+ Args:
240
+ credential_file (str | None, optional): Path to the AWS credentials file.
241
+ If None, the default location (~/.aws/credentials) is used. Defaults to None.
242
+ Returns:
243
+ Optional[list[Dict[str, str]]]: A list of dictionaries containing secret details
244
+ if successful, None otherwise.
245
+ Raises:
246
+ Exception: If there is an error reading the profiles file.
247
+ """
248
+ logger = get_logger()
249
+ credential_file = get_credential_file_path(credential_file)
250
+ if not credential_file.exists():
251
+ raise FileNotFoundError(f"Credential file '{credential_file}' does not exist.")
252
+ config = ConfigParser()
253
+ config.read(credential_file)
254
+ profiles = []
255
+ for section in config.sections():
256
+ item_dic = get_profile_info(profile_name=section,
257
+ credential_file=credential_file,
258
+ secret_mask=secret_mask,
259
+ ctx=config)
260
+ if item_dic:
261
+ profiles.append(item_dic)
262
+
263
+ if not profiles:
264
+ logger.info("No valid profiles found in the credentials file.")
265
+ else:
266
+ logger.info(f"Found {len(profiles)} profiles in the credentials file.")
267
+ return profiles
268
+
269
+ # ----------------------------------------------------------------------------
270
+ def update_credentials(profile_name: str,
271
+ totp_token: str,
272
+ duration: int = 3600,
273
+ sts_profile_name: str | None = None,
274
+ target_key: str | None = None,
275
+ cred_file: str | os.PathLike | None = None,
276
+ message_handler: Callable[[str], None] | None = None) -> dict[str, str] | None:
277
+ """
278
+ Update the AWS credentials file with new STS tokens.
279
+ message_handler receives user-facing progress messages (e.g. print for CLI).
280
+ Leave it None in MCP mode so that nothing is written to stdout.
281
+ """
282
+ logger = get_logger()
283
+ ret = None
284
+ try:
285
+ sts_credentials = get_sts_token(profile_name=profile_name,
286
+ totp_token=totp_token,
287
+ credential_file=cred_file,
288
+ duration_seconds=duration)
289
+ if sts_credentials:
290
+ target_key = profile_name if target_key is None else target_key
291
+ credential_file_path = Path(cred_file) if cred_file else get_credential_file_path()
292
+ updater = CredentialUpdater(credential_path=credential_file_path)
293
+ updater.set_target_tag_name(target_key)
294
+ updater.set_credentials(sts_credentials)
295
+ updater.set_sts_profile_name(sts_profile_name)
296
+ updater.set_message_handler(message_handler)
297
+ ret = updater.update_credential_file()
298
+ # The result may be returned to the LLM via MCP, so never expose the raw access key id.
299
+ if ret and ret.get('aws_access_key_id'):
300
+ ret['aws_access_key_id'] = mask_string(ret['aws_access_key_id'])
301
+ logger.info(f"STS Credentials for profile '{profile_name}' updated successfully.")
302
+ else:
303
+ logger.error("Failed to retrieve STS credentials.")
304
+ raise Exception("Failed to retrieve STS credentials.")
305
+ except Exception as e:
306
+ logger.error(f"Error: {e}")
307
+ raise
308
+ return ret
updsts/cmd_handler.py ADDED
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8-sig
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from argparse import ArgumentParser
7
+
8
+ from .logutil import get_logger
9
+ from .cmdparam import *
10
+ from .awsutil import *
11
+ from .mcp_server import *
12
+
13
+ # ----------------------------------------------------------------------------
14
+ def handle_get(args):
15
+ """
16
+ Handle the 'get' command to generate a TOTP token for a given secret name.
17
+
18
+ Args:
19
+ args (argparse.Namespace): Parsed command line arguments.
20
+ """
21
+ profile_name = args.profile_name
22
+ totp_token = args.totp_token
23
+ cred_file = args.credential_file if args.credential_file else None
24
+ sts_profile_name = args.sts_profile_name if hasattr(args, 'sts_profile_name') and args.sts_profile_name else None
25
+ duration = args.duration if args.duration else 3600
26
+ sts_profile_name = args.sts_profile_name if hasattr(args, 'sts_profile_name') and args.sts_profile_name else None
27
+ target_key = args.target_key if hasattr(args, 'target_key') and args.target_key else None
28
+
29
+ ret = update_credentials(profile_name=profile_name,
30
+ totp_token=totp_token,
31
+ duration=duration,
32
+ sts_profile_name=sts_profile_name,
33
+ target_key=target_key,
34
+ cred_file=cred_file,
35
+ message_handler=print)
36
+ if ret:
37
+ print(f"STS Credentials of profile '{profile_name}' updated successfully.")
38
+ print(f"The temporary credential({ret.get('updated_profile_name', '')}) will expire at: {ret.get('aws_token_expiration', '')}")
39
+
40
+ # ----------------------------------------------------------------------------
41
+ def handle_list(args):
42
+ """
43
+ Handle the 'list' command to display all registered secrets.
44
+
45
+ Args:
46
+ args (argparse.Namespace): Parsed command line arguments.
47
+ """
48
+ credential_file = args.credential_file if args.credential_file else None
49
+ profiles = get_profile_list(credential_file=credential_file,
50
+ secret_mask=True)
51
+ if not profiles:
52
+ print("No profiles found.")
53
+ else:
54
+ for prof in profiles:
55
+ print(f"Profile Name: {prof['profile_name']}")
56
+ print(f" Access Key ID : {prof['aws_access_key_id']}")
57
+ print(f" Secret Key : {prof['aws_secret_access_key']}")
58
+ print(f" Session Token : {prof['aws_session_token']}")
59
+ print(f" Expiration DateTime : {prof['expiration_datetime']}")
60
+ print(f" MFA Device ARN : {prof['mfa_device_arn']}")
61
+ print(f" TOTP Secret Name : {prof['totp_secret_name']}")
62
+ print()
63
+
64
+ # ----------------------------------------------------------------------------
65
+ def handle_mcp(args):
66
+ """
67
+ Handle the 'mcp' command to run the MCP server.
68
+
69
+ Args:
70
+ args (argparse.Namespace): Parsed command line arguments.
71
+ """
72
+
73
+ run_mcp = args.mcp_server if args.mcp_server else False
74
+ if run_mcp:
75
+ # If the MCP server flag is set, run the MCP server
76
+ run_as_mcp_server()
77
+ pass
78
+ else:
79
+ # Otherwise, run the MCP server test
80
+ disp_tools()
81
+ pass
updsts/cmdparam.py ADDED
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8-sig
2
+
3
+ import argparse
4
+
5
+ # ----------------------------------------------------------------------------
6
+ def register_sub_get(subparsers,
7
+ handle_get: callable,
8
+ parent_parser: argparse.ArgumentParser):
9
+ """
10
+ Register the 'update' subcommand to the argument parser.
11
+ """
12
+ get_parser = subparsers.add_parser(
13
+ 'get',
14
+ help='Get/Update new sts token',
15
+ description='Get/Update new sts token.',
16
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
17
+ parents=[parent_parser]
18
+ )
19
+ get_parser.add_argument(
20
+ '-n',
21
+ '--profile_name',
22
+ type=str,
23
+ required=True,
24
+ help='Profile name to get sts secret key.'
25
+ )
26
+ get_parser.add_argument(
27
+ '-sn',
28
+ '--sts_profile_name',
29
+ type=str,
30
+ required=False,
31
+ default=None,
32
+ help='STS Profile name in the AWS credentials file.'
33
+ )
34
+ get_parser.add_argument(
35
+ '-t',
36
+ '--totp_token',
37
+ type=str,
38
+ required=True,
39
+ help='MFA TOTP token of the user'
40
+ )
41
+ get_parser.add_argument(
42
+ '-d',
43
+ '--duration',
44
+ type=int,
45
+ required=False,
46
+ default=3600,
47
+ help='Duration seconds of the sts token (default: 3600)'
48
+ )
49
+ get_parser.set_defaults(handler=handle_get)
50
+ return subparsers
51
+
52
+ # ----------------------------------------------------------------------------
53
+ def register_sub_list(subparsers,
54
+ handle_list: callable,
55
+ parent_parser: argparse.ArgumentParser):
56
+ """
57
+ Register the 'list' subcommand to the argument parser.
58
+ """
59
+ list_parser = subparsers.add_parser(
60
+ 'list',
61
+ help='List all profile names',
62
+ description='List all profile names.',
63
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
64
+ parents=[parent_parser]
65
+ )
66
+ # Set the function to handle the 'list' command
67
+ list_parser.set_defaults(handler=handle_list)
68
+ return subparsers
69
+
70
+ # ----------------------------------------------------------------------------
71
+ def register_sub_mcp(subparsers,
72
+ handle_mcp: callable,
73
+ parent_parser: argparse.ArgumentParser):
74
+ """
75
+ Register the 'mcp' subcommand to the argument parser.
76
+ """
77
+ mcp_parser = subparsers.add_parser(
78
+ 'mcp',
79
+ help='Subcommand for mcp functions',
80
+ description='Manage TOTP secrets using MCP tools.',
81
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
82
+ parents=[parent_parser]
83
+ )
84
+ mcp_parser.add_argument(
85
+ "--mcp-server",
86
+ action="store_true",
87
+ help="Run as MCP server"
88
+ )
89
+ mcp_parser.set_defaults(handler=handle_mcp)
90
+ return subparsers
updsts/logutil.py ADDED
@@ -0,0 +1,109 @@
1
+ # encoding: utf-8-sig
2
+
3
+ import os
4
+ import sys
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ # globals
10
+ logger_obj: Optional[logging.Logger] = None
11
+ is_initialized: bool = False
12
+
13
+ DEFAULT_LOGGER_NAME = "awssts"
14
+ DEFALT_LOG_LEVEL = logging.WARNING
15
+ DEFAULT_FILE_LEVEL = logging.WARNING
16
+ DEFAULT_CONSOLE_LEVEL = logging.WARNING
17
+
18
+ # ----------------------------------------------------------------------------
19
+ def get_with_init(
20
+ log_file: str | None = None,
21
+ *,
22
+ level: int = DEFALT_LOG_LEVEL,
23
+ file_level: int = DEFAULT_FILE_LEVEL,
24
+ console_level: int = DEFAULT_CONSOLE_LEVEL,
25
+ force: bool = False
26
+ ) -> logging.Logger:
27
+ """
28
+ Initialize the logger with the specified configuration.
29
+ If the logger is already initialized, it will return the existing logger
30
+
31
+ Args:
32
+ log_file (str | None, optional): Defaults to None.
33
+ level (int, optional): Defaults to DEFAULT_LEVEL.
34
+ file_level (int, optional): Defaults to DEFAULT_FILE_LEVEL.
35
+ console_level (int, optional): Defaults to DEFAULT_CONSOLE_LEVEL.
36
+ force (bool, optional): Defaults to False.
37
+
38
+ Returns:
39
+ logging.Logger: Logger instance for the mktotp module.
40
+ """
41
+
42
+ if log_file is None:
43
+ user_home = os.path.expanduser("~")
44
+ log_dir = Path(user_home) / ".awscm" / "log"
45
+ os.makedirs(log_dir, exist_ok=True)
46
+ log_file = log_dir / "awscm.log"
47
+ else:
48
+ # make the directory for the specified path if it does not exist
49
+ try:
50
+ os.makedirs(os.path.dirname(log_file), exist_ok=True)
51
+ except OSError:
52
+ # If the directory cannot be created, we just ignore it
53
+ pass
54
+
55
+ global logger_obj, is_initialized
56
+ if not is_initialized:
57
+ logger_obj = logging.getLogger(DEFAULT_LOGGER_NAME)
58
+ logger_obj.setLevel(level=level)
59
+ formatter = logging.Formatter(
60
+ '%(asctime)s [%(name)s] [%(levelname)s] %(message)s'
61
+ )
62
+
63
+ try:
64
+ file_handler = logging.FileHandler(log_file, encoding="utf-8")
65
+ file_handler.setFormatter(formatter)
66
+ file_handler.setLevel(file_level)
67
+ logger_obj.addHandler(file_handler)
68
+ except (OSError, PermissionError):
69
+ # If file handler creation fails, we just skip it and only use console handler
70
+ pass
71
+
72
+ console_handler = logging.StreamHandler(sys.stderr)
73
+ console_handler.setFormatter(formatter)
74
+ console_handler.setLevel(console_level)
75
+ logger_obj.addHandler(console_handler)
76
+
77
+ is_initialized = True
78
+ return logger_obj
79
+
80
+ # ----------------------------------------------------------------------------
81
+ def get_logger(verbose_level: int | None = None) -> logging.Logger:
82
+ """
83
+ Module-level shortcut to get the logger.
84
+
85
+ Returns:
86
+ logging.Logger: Logger instance for the mktotp module.
87
+ """
88
+ global logger_obj, is_initialized
89
+
90
+ # If logger is already initialized and no verbose_level is specified,
91
+ # return the existing logger instance
92
+ if is_initialized and verbose_level is None:
93
+ return logger_obj
94
+
95
+ logObj = None
96
+ if verbose_level is not None:
97
+ if verbose_level == 0:
98
+ logObj = get_with_init(level=logging.WARNING)
99
+ elif verbose_level == 1:
100
+ logObj = get_with_init(level=logging.INFO)
101
+ elif verbose_level == 2:
102
+ logObj = get_with_init(level=logging.DEBUG)
103
+ else:
104
+ # If no verbose level is specified, use the default level
105
+ logObj = get_with_init()
106
+ return logObj
107
+
108
+
109
+ __all__ = ["get_logger"]