cmdbox-cli 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.
Files changed (112) hide show
  1. cmdbox/__init__.py +0 -0
  2. cmdbox/cli/__init__.py +0 -0
  3. cmdbox/cli/app.py +125 -0
  4. cmdbox/cli/commands/__init__.py +0 -0
  5. cmdbox/cli/commands/alias_fallback.py +102 -0
  6. cmdbox/cli/commands/command_crud.py +429 -0
  7. cmdbox/cli/commands/command_run.py +255 -0
  8. cmdbox/cli/commands/history.py +109 -0
  9. cmdbox/cli/commands/init.py +54 -0
  10. cmdbox/cli/commands/settings.py +62 -0
  11. cmdbox/cli/commands/tag_crud.py +277 -0
  12. cmdbox/cli/commands/variable_crud.py +349 -0
  13. cmdbox/cli/common/__init__.py +0 -0
  14. cmdbox/cli/common/errors.py +58 -0
  15. cmdbox/cli/common/update_fields.py +88 -0
  16. cmdbox/cli/completions/__init__.py +0 -0
  17. cmdbox/cli/completions/commands.py +26 -0
  18. cmdbox/cli/completions/fields.py +31 -0
  19. cmdbox/cli/completions/tags.py +24 -0
  20. cmdbox/cli/completions/variables.py +26 -0
  21. cmdbox/cli/handlers/__init__.py +0 -0
  22. cmdbox/cli/handlers/command_handlers.py +357 -0
  23. cmdbox/cli/handlers/common_handlers.py +15 -0
  24. cmdbox/cli/handlers/history_handlers.py +94 -0
  25. cmdbox/cli/handlers/init_handler.py +127 -0
  26. cmdbox/cli/handlers/run_handler.py +178 -0
  27. cmdbox/cli/handlers/settings_handler.py +59 -0
  28. cmdbox/cli/handlers/tag_handlers.py +220 -0
  29. cmdbox/cli/handlers/variable_handlers.py +272 -0
  30. cmdbox/cli/prompts/__init__.py +0 -0
  31. cmdbox/cli/prompts/completers.py +161 -0
  32. cmdbox/cli/prompts/prompts.py +108 -0
  33. cmdbox/cli/prompts/validators.py +46 -0
  34. cmdbox/cli/ui/__init__.py +0 -0
  35. cmdbox/cli/ui/console.py +31 -0
  36. cmdbox/cli/ui/editor.py +141 -0
  37. cmdbox/cli/ui/presenters/__init__.py +0 -0
  38. cmdbox/cli/ui/presenters/app_presenter.py +8 -0
  39. cmdbox/cli/ui/presenters/command_presenter.py +168 -0
  40. cmdbox/cli/ui/presenters/history_presenter.py +83 -0
  41. cmdbox/cli/ui/presenters/init_instructions.py +52 -0
  42. cmdbox/cli/ui/presenters/init_presenter.py +57 -0
  43. cmdbox/cli/ui/presenters/result_presenter.py +144 -0
  44. cmdbox/cli/ui/presenters/settings_presenter.py +130 -0
  45. cmdbox/cli/ui/presenters/tag_presenter.py +97 -0
  46. cmdbox/cli/ui/presenters/variable_presenter.py +103 -0
  47. cmdbox/cli/ui/primitives.py +410 -0
  48. cmdbox/cli/ui/theme.py +43 -0
  49. cmdbox/cli/ui/theme_builder.py +49 -0
  50. cmdbox/common/__init__.py +0 -0
  51. cmdbox/common/io.py +34 -0
  52. cmdbox/container.py +156 -0
  53. cmdbox/core/__init__.py +0 -0
  54. cmdbox/core/fields.py +48 -0
  55. cmdbox/core/paths.py +52 -0
  56. cmdbox/database.py +65 -0
  57. cmdbox/exceptions.py +10 -0
  58. cmdbox/init/__init__.py +0 -0
  59. cmdbox/init/detect.py +82 -0
  60. cmdbox/init/integrations/bash.sh +10 -0
  61. cmdbox/init/integrations/cmd.bat +14 -0
  62. cmdbox/init/integrations/fish.fish +11 -0
  63. cmdbox/init/integrations/powershell.ps1 +14 -0
  64. cmdbox/init/integrations/zsh.sh +10 -0
  65. cmdbox/init/io.py +68 -0
  66. cmdbox/init/specs.py +54 -0
  67. cmdbox/logging_setup/__init__.py +0 -0
  68. cmdbox/logging_setup/log_config.py +123 -0
  69. cmdbox/logging_setup/log_decorators.py +40 -0
  70. cmdbox/logging_setup/log_handlers.py +94 -0
  71. cmdbox/migrations/__init__.py +1 -0
  72. cmdbox/migrations/errors.py +10 -0
  73. cmdbox/migrations/runner.py +127 -0
  74. cmdbox/migrations/versions/__init__.py +0 -0
  75. cmdbox/models.py +165 -0
  76. cmdbox/repositories/__init__.py +0 -0
  77. cmdbox/repositories/base_repository.py +181 -0
  78. cmdbox/repositories/command_repository.py +391 -0
  79. cmdbox/repositories/errors.py +120 -0
  80. cmdbox/repositories/history_repository.py +155 -0
  81. cmdbox/repositories/results.py +37 -0
  82. cmdbox/repositories/tag_repository.py +91 -0
  83. cmdbox/repositories/validators.py +256 -0
  84. cmdbox/repositories/variable_repository.py +324 -0
  85. cmdbox/resolve/__init__.py +0 -0
  86. cmdbox/resolve/errors.py +65 -0
  87. cmdbox/resolve/lookup.py +137 -0
  88. cmdbox/resolve/resolver.py +402 -0
  89. cmdbox/resolve/type_defs.py +96 -0
  90. cmdbox/runtime/__init__.py +0 -0
  91. cmdbox/runtime/executor.py +454 -0
  92. cmdbox/runtime/results.py +25 -0
  93. cmdbox/runtime/shell.py +90 -0
  94. cmdbox/services/__init__.py +0 -0
  95. cmdbox/services/command_services.py +261 -0
  96. cmdbox/services/errors.py +37 -0
  97. cmdbox/services/field_selection.py +162 -0
  98. cmdbox/services/history_service.py +68 -0
  99. cmdbox/services/run_service.py +204 -0
  100. cmdbox/services/tag_services.py +134 -0
  101. cmdbox/services/variable_services.py +224 -0
  102. cmdbox/settings/__init__.py +0 -0
  103. cmdbox/settings/models.py +129 -0
  104. cmdbox/settings/settings_repository.py +36 -0
  105. cmdbox/settings/settings_service.py +144 -0
  106. cmdbox/version.py +1 -0
  107. cmdbox_cli-1.0.0.dist-info/METADATA +125 -0
  108. cmdbox_cli-1.0.0.dist-info/RECORD +112 -0
  109. cmdbox_cli-1.0.0.dist-info/WHEEL +5 -0
  110. cmdbox_cli-1.0.0.dist-info/entry_points.txt +2 -0
  111. cmdbox_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
  112. cmdbox_cli-1.0.0.dist-info/top_level.txt +1 -0
cmdbox/core/fields.py ADDED
@@ -0,0 +1,48 @@
1
+ COMMAND_DISPLAY_FIELDS = [
2
+ "alias",
3
+ "template",
4
+ "description",
5
+ "cwd",
6
+ "shell",
7
+ "env",
8
+ "timeout",
9
+ "date_created",
10
+ "last_updated",
11
+ "used",
12
+ "last_used",
13
+ ]
14
+ COMMAND_SEARCH_FIELDS = [
15
+ "alias",
16
+ "template",
17
+ "description",
18
+ "date_created",
19
+ "last_updated",
20
+ "used",
21
+ "last_used",
22
+ ]
23
+
24
+ VARIABLE_DISPLAY_FIELDS = [
25
+ "name",
26
+ "value",
27
+ "date_created",
28
+ "last_updated",
29
+ ]
30
+ VARIABLE_SEARCH_FIELDS = [
31
+ "name",
32
+ "value",
33
+ "date_created",
34
+ "last_updated",
35
+ ]
36
+
37
+ TAG_DISPLAY_FIELDS = [
38
+ "name",
39
+ "description",
40
+ "date_created",
41
+ "last_updated",
42
+ ]
43
+ TAG_SEARCH_FIELDS = [
44
+ "name",
45
+ "description",
46
+ "date_created",
47
+ "last_updated",
48
+ ]
cmdbox/core/paths.py ADDED
@@ -0,0 +1,52 @@
1
+ from pathlib import Path
2
+ import os
3
+ import sys
4
+
5
+ VENDOR = "SomeGuySoftware"
6
+ APP_NAME = "CmdBox"
7
+
8
+
9
+ def get_app_data_dir() -> Path:
10
+ """
11
+ Gets the application data directory for the current platform.
12
+
13
+ This function determines the appropriate location where application data
14
+ should be stored for the current operating system. The directory varies
15
+ based on the operating system, ensuring compatibility and proper organization
16
+ of application data.
17
+
18
+ Returns:
19
+ Path: A `Path` object representing the full path to the application
20
+ data directory for the current platform.
21
+ """
22
+ if sys.platform == "win32":
23
+ base = Path(os.environ["APPDATA"])
24
+ elif sys.platform == "darwin":
25
+ base = Path.home() / "Library" / "Application Support"
26
+ else:
27
+ base = Path.home() / ".local" / "share"
28
+ return base / VENDOR / APP_NAME
29
+
30
+
31
+ APP_DATA_DIR = get_app_data_dir()
32
+ APP_DATA_DIR.mkdir(parents=True, exist_ok=True)
33
+
34
+
35
+ def get_log_dir() -> Path:
36
+ """
37
+ Retrieves the directory path designated for storing log files.
38
+
39
+ This function ensures the existence of the 'logs' directory within the application
40
+ data directory. If the directory does not exist, it gets created with necessary
41
+ parent directories.
42
+
43
+ Returns:
44
+ Path: A `Path` object representing the log directory.
45
+ """
46
+ log_dir = APP_DATA_DIR / "logs"
47
+ log_dir.mkdir(parents=True, exist_ok=True)
48
+ return log_dir
49
+
50
+
51
+ def get_log_file_path() -> Path:
52
+ return get_log_dir() / "cmdbox.log"
cmdbox/database.py ADDED
@@ -0,0 +1,65 @@
1
+ import logging
2
+ import os
3
+ from peewee import SqliteDatabase
4
+
5
+ from cmdbox.core.paths import get_app_data_dir
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+
10
+ DB_PATH = get_app_data_dir() / "cmdbox.db"
11
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
12
+
13
+
14
+ db = SqliteDatabase(None)
15
+ _db_initialized = False
16
+
17
+
18
+ def init_database(testing: bool = False) -> None:
19
+ """
20
+ Initializes the database connection based on the environment or testing flag.
21
+
22
+ This function determines whether to use an in-memory SQLite database for
23
+ testing purposes or the standard SQLite database file path for production
24
+ or other environments. The behavior is controlled by the `testing` argument
25
+ and the `CMDBOX_ENV` environment variable.
26
+
27
+ Args:
28
+ testing (bool | None): A flag to indicate whether to use the testing
29
+ (in-memory) database. If None, the value is derived from the
30
+ `CMDBOX_ENV` environment variable. Defaults to None.
31
+ """
32
+ global _db_initialized
33
+ if _db_initialized:
34
+ return
35
+ if testing is None:
36
+ env = os.environ.get("CMDBOX_ENV", "production")
37
+ testing = env == "testing"
38
+
39
+ if testing:
40
+ db_path = ":memory:"
41
+ else:
42
+ db_path = str(DB_PATH)
43
+ db.init(db_path)
44
+
45
+ if not testing:
46
+ from cmdbox.migrations.runner import ensure_migrated
47
+
48
+ ensure_migrated(db_path)
49
+
50
+ _db_initialized = True
51
+ log.debug("Database initialized %s", "in testing mode" if testing else "")
52
+
53
+
54
+ def get_db(testing: bool = False) -> SqliteDatabase:
55
+ init_database(testing)
56
+ if db.is_closed():
57
+ db.connect(reuse_if_open=True)
58
+ return db
59
+
60
+
61
+ def ensure_schema() -> None:
62
+ from cmdbox.models import ALL_MODELS
63
+
64
+ _db = get_db()
65
+ _db.create_tables(ALL_MODELS, safe=True)
cmdbox/exceptions.py ADDED
@@ -0,0 +1,10 @@
1
+ class CmdboxError(Exception):
2
+ """
3
+ Base class for exceptions in the cmdbox module.
4
+
5
+ This custom exception class serves as a base for all exceptions raised
6
+ within the cmdbox module. It provides a means to handle module-specific
7
+ errors in a structured manner.
8
+ """
9
+
10
+ pass
File without changes
cmdbox/init/detect.py ADDED
@@ -0,0 +1,82 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import psutil
6
+
7
+
8
+ def detect_shell() -> str:
9
+ """
10
+ Detect the shell being used in the current operating environment.
11
+
12
+ The function identifies the shell by examining the parent process of the current
13
+ script and its name. On Unix systems, it also checks the `SHELL` environment
14
+ variable as a fallback. For Windows, it supports shell detection for cmd,
15
+ PowerShell, and Git Bash.
16
+
17
+ Returns:
18
+ str: The name of the detected shell. If the shell cannot be determined using
19
+ process information, it falls back to environment-based detection.
20
+ """
21
+ parent = psutil.Process(os.getpid()).parent()
22
+ name = ((parent.name() if parent else "") or "").lower()
23
+
24
+ # Windows
25
+ if name in {"pwsh.exe", "powershell.exe"}:
26
+ return "powershell"
27
+ if name == "cmd.exe":
28
+ return "cmd"
29
+ if name in {"bash.exe", "git-bash.exe", "msys2.exe"}:
30
+ return "bash"
31
+
32
+ # Unix
33
+ if name in {"bash", "zsh", "fish"}:
34
+ return name
35
+
36
+ # Fallback
37
+ p = os.environ.get("SHELL")
38
+ if p:
39
+ sh = Path(p).name.lower()
40
+ if sh in {"bash", "zsh", "fish"}:
41
+ return sh
42
+
43
+ # Default to env detection if everything else fails
44
+ return detect_shell_env()
45
+
46
+
47
+ def detect_shell_env() -> str:
48
+ """
49
+ Detects the shell being used on the operating system.
50
+
51
+ This function determines the active shell based on environment variables,
52
+ platform information, and commonly used shell identifiers. It identifies
53
+ the shell for both Windows and Unix-based systems.
54
+
55
+ Returns:
56
+ str: A string representing the detected shell. Possible values include
57
+ 'bash', 'powershell', 'cmd', 'zsh', and 'fish'. Defaults to 'bash'
58
+ if unable to determine the specific shell.
59
+ """
60
+ if sys.platform.startswith("win"):
61
+ # Git bash / MSYS
62
+ if (
63
+ os.environ.get("MSYSTEM")
64
+ or os.environ.get("MINGW_PREFIX")
65
+ or os.environ.get("SHELL")
66
+ ):
67
+ return "bash"
68
+ # Powershell
69
+ if os.environ.get("PSModulePath") or os.environ.get(
70
+ "POWERSHELL_DISTRIBUTION_CHANNEL"
71
+ ):
72
+ return "powershell"
73
+ # Default to cmd
74
+ return "cmd"
75
+
76
+ sh = os.environ.get("SHELL")
77
+ if sh:
78
+ base = Path(sh).name.lower()
79
+ if base in {"bash", "zsh", "fish"}:
80
+ return base
81
+
82
+ return "bash"
@@ -0,0 +1,10 @@
1
+ cbe() {
2
+ local out status
3
+ out="$(command cb "$@" --emit)"
4
+ status=$?
5
+ if [ $status -ne 0 ]; then
6
+ return $status
7
+ fi
8
+ [ -z "$out" ] && return 0
9
+ eval "$out"
10
+ }
@@ -0,0 +1,14 @@
1
+ @echo off
2
+ setlocal EnableExtensions EnableDelayedExpansion
3
+
4
+ set "_tmp=%TEMP%\cmdbox_emit_%RANDOM%%RANDOM%.cmd"
5
+
6
+ cb %* --emit > "%_tmp%"
7
+ if errorlevel 1 (
8
+ del "%_tmp%" >nul 2>&1
9
+ exit /b %errorlevel%
10
+ )
11
+
12
+ call "%_tmp%"
13
+ del "%_tmp%" >nul 2>&1
14
+ exit /b 0
@@ -0,0 +1,11 @@
1
+ function cbe
2
+ set -l out (cb cmdbox $argv --emit)
3
+ set -l status $status
4
+ if test $status -ne 0
5
+ return $status
6
+ end
7
+ if test -z "$out"
8
+ return 0
9
+ end
10
+ eval $out
11
+ end
@@ -0,0 +1,14 @@
1
+ function cbe
2
+ {
3
+ $out = & cb @args --emit
4
+ $code = $LASTEXITCODE
5
+ if ($code -ne 0)
6
+ {
7
+ return
8
+ }
9
+ if ( [string]::IsNullOrWhiteSpace($out))
10
+ {
11
+ return
12
+ }
13
+ Invoke-Expression ($out -join "`n")
14
+ }
@@ -0,0 +1,10 @@
1
+ cbe() {
2
+ local out status
3
+ out="$(command cb "$@" --emit)"
4
+ status=$?
5
+ if [ $status -ne 0 ]; then
6
+ return $status
7
+ fi
8
+ [ -z "$out" ] && return 0
9
+ eval "$out"
10
+ }
cmdbox/init/io.py ADDED
@@ -0,0 +1,68 @@
1
+ import re
2
+ import shutil
3
+ from pathlib import Path
4
+ from importlib import resources
5
+
6
+ from cmdbox.logging_setup.log_decorators import log_action
7
+
8
+ START_MARK = "# >>> cmdbox shell integration >>>"
9
+ END_MARK = "# <<< cmdbox shell integration <<<"
10
+
11
+
12
+ def load_integration_text(filename: str) -> str:
13
+ """
14
+ Loads a text file from the 'cmdbox.init.integrations' resource directory, reads its contents, and
15
+ returns the text stripped of trailing whitespace with a newline character appended.
16
+
17
+ Args:
18
+ filename (str): The name of the file to be loaded and read.
19
+
20
+ Returns:
21
+ str: The contents of the specified file as a string, with trailing whitespace removed
22
+ and a newline character appended at the end.
23
+ """
24
+ return (
25
+ resources.files("cmdbox.init.integrations")
26
+ .joinpath(filename)
27
+ .read_text(encoding="utf-8")
28
+ .rstrip()
29
+ + "\n"
30
+ )
31
+
32
+
33
+ @log_action(__name__, "upsert_marked_block")
34
+ def upsert_marked_block(profile_path: Path, block_text: str) -> None:
35
+ """
36
+ Updates or inserts a marked text block into a given file at the specified path.
37
+ If the marked block already exists in the file, it is replaced with the provided
38
+ new content. Otherwise, the block is added to the end of the file. Additionally,
39
+ a backup of the original file is created if it already exists.
40
+
41
+ Args:
42
+ profile_path (Path): The path to the file where the marked block will be
43
+ updated or inserted.
44
+ block_text (str): The content to be added or updated as part of the marked
45
+ block.
46
+
47
+ """
48
+ profile_path.parent.mkdir(parents=True, exist_ok=True)
49
+ existing = profile_path.read_text(encoding="utf-8") if profile_path.exists() else ""
50
+
51
+ marked = f"{START_MARK}\n{block_text.rstrip()}\n{END_MARK}\n"
52
+
53
+ pattern = re.compile(
54
+ re.escape(START_MARK) + r".*?" + re.escape(END_MARK) + r"\n?",
55
+ flags=re.DOTALL,
56
+ )
57
+
58
+ if pattern.search(existing):
59
+ new_text = pattern.sub(marked, existing)
60
+ else:
61
+ sep = "\n" if existing and not existing.endswith("\n") else ""
62
+ new_text = existing + sep + marked
63
+
64
+ if profile_path.exists():
65
+ backup = profile_path.with_suffix(profile_path.suffix + ".bak")
66
+ shutil.copy2(profile_path, backup)
67
+
68
+ profile_path.write_text(new_text, encoding="utf-8")
cmdbox/init/specs.py ADDED
@@ -0,0 +1,54 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Callable
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ShellSpec:
9
+ name: str
10
+ filename: str
11
+ default_path_fn: Callable[[], Path] | None
12
+ install_mode: str # "profile_block" or "write_file" or "wrapper_hint"
13
+
14
+
15
+ def default_bashrc() -> Path:
16
+ return Path.home() / ".bashrc"
17
+
18
+
19
+ def default_zshrc() -> Path:
20
+ return Path.home() / ".zshrc"
21
+
22
+
23
+ def default_fish_function() -> Path:
24
+ return (
25
+ Path.home() / ".config" / "fish" / "functions" / "cb.fish"
26
+ ) # TODO: Does this need to be cbe.fish?
27
+
28
+
29
+ def default_powershell_profile() -> Path:
30
+ # Approximation that works for typical pwsh installations
31
+ # Users can override with --path if needed
32
+ docs = os.environ.get("USERPROFILE")
33
+ if not docs:
34
+ return (
35
+ Path.home()
36
+ / "Documents"
37
+ / "PowerShell"
38
+ / "Microsoft.PowerShell_profile.ps1"
39
+ )
40
+ return Path(docs) / "Documents" / "PowerShell" / "Microsoft.PowerShell_profile.ps1"
41
+
42
+
43
+ SHELLS: dict[str, ShellSpec] = {
44
+ "bash": ShellSpec("bash", "bash.sh", default_bashrc, "profile_block"),
45
+ "zsh": ShellSpec("zsh", "zsh.sh", default_zshrc, "profile_block"),
46
+ "fish": ShellSpec("fish", "fish.fish", default_fish_function, "write_file"),
47
+ "powershell": ShellSpec(
48
+ "powershell", "powershell.ps1", default_powershell_profile, "profile_block"
49
+ ),
50
+ "pwsh": ShellSpec(
51
+ "pwsh", "powershell.ps1", default_powershell_profile, "profile_block"
52
+ ),
53
+ "cmd": ShellSpec("cmd", "cmd.bat", None, "wrapper_hint"),
54
+ }
File without changes
@@ -0,0 +1,123 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+ from cmdbox.core.paths import get_log_file_path
6
+
7
+ LOGGER_NAME = "cmdbox"
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class LogConfig:
12
+ console_level: int
13
+ file_enabled: bool
14
+ file_level: int
15
+ file_path: Path
16
+ max_bytes: int
17
+ backups: int
18
+
19
+
20
+ def _level(level_str: str) -> int:
21
+ s = (level_str or "").upper().strip()
22
+ return getattr(logging, s, logging.INFO)
23
+
24
+
25
+ def build_log_config(
26
+ settings, *, verbose: bool, debug: bool, file_logs: bool | None
27
+ ) -> LogConfig:
28
+ """
29
+ Builds and returns a LogConfig instance based on the provided settings and flags.
30
+
31
+ This function determines the appropriate logging configuration for the application
32
+ by evaluating the verbosity, debug settings, and file-based logging preferences. It
33
+ retrieves logging levels and file configurations from the provided settings.
34
+
35
+ Args:
36
+ settings: Application-specific configuration object containing logging
37
+ settings such as file size limits and backup count.
38
+ verbose: Flag indicating whether verbose logging is enabled.
39
+ debug: Flag indicating whether debug-level logging is enabled.
40
+ file_logs: Flag indicating whether file-based logging is enabled. If None,
41
+ it defaults to the settings specified in the application configuration.
42
+
43
+ Returns:
44
+ LogConfig: A fully constructed LogConfig instance containing the logging
45
+ configuration details, such as console logging level, file logging level,
46
+ file path, maximum file size, and backup count.
47
+ """
48
+ console_level = get_console_level(settings, verbose=verbose, debug=debug)
49
+
50
+ file_enabled = get_file_enabled(settings, file_logs=file_logs)
51
+ file_level = get_file_level(settings, verbose=verbose, debug=debug)
52
+
53
+ file_path = get_log_file_path()
54
+
55
+ return LogConfig(
56
+ console_level=console_level,
57
+ file_enabled=file_enabled,
58
+ file_level=file_level,
59
+ file_path=file_path,
60
+ max_bytes=settings.logging.file.max_bytes,
61
+ backups=settings.logging.file.backups,
62
+ )
63
+
64
+
65
+ def get_console_level(settings, *, verbose: bool, debug: bool) -> int:
66
+ """
67
+ Determines and returns the appropriate logging level for console output.
68
+
69
+ Args:
70
+ settings: Application settings object containing logging configuration.
71
+ verbose: Flag indicating whether verbose mode is enabled.
72
+ debug: Flag indicating whether debug mode is enabled.
73
+
74
+ Returns:
75
+ int: Numerical value representing the logging level.
76
+ """
77
+ if debug:
78
+ return logging.DEBUG
79
+ elif verbose:
80
+ return logging.INFO
81
+ else:
82
+ return _level(settings.logging.console_level)
83
+
84
+
85
+ def get_file_enabled(settings, *, file_logs: bool | None) -> bool:
86
+ """
87
+ Returns whether file logging is enabled based on the provided settings and optional override.
88
+
89
+ Args:
90
+ settings: Application configuration containing logging settings.
91
+ file_logs (bool | None): Optional override for enabling or disabling file logging.
92
+
93
+ Returns:
94
+ bool: True if file logging is enabled; otherwise, False.
95
+ """
96
+ if file_logs is not None:
97
+ return file_logs
98
+ return bool(settings.logging.file.enabled)
99
+
100
+
101
+ def get_file_level(settings, *, verbose: bool, debug: bool) -> int:
102
+ """
103
+ Determines the appropriate logging level for file-based logging based on the provided settings
104
+ and debug/verbose flags.
105
+
106
+ Args:
107
+ settings: Configuration settings that include logging level information.
108
+ verbose: Enables verbose logging if set to True.
109
+ debug: Enables debug-level logging if set to True.
110
+
111
+ Returns:
112
+ int: The resolved logging level for file-based logging.
113
+ """
114
+ if debug:
115
+ return logging.DEBUG
116
+ elif verbose:
117
+ return logging.INFO
118
+ else:
119
+ return _level(settings.logging.file.level)
120
+
121
+
122
+ def get_logger() -> logging.Logger:
123
+ return logging.getLogger(LOGGER_NAME)
@@ -0,0 +1,40 @@
1
+ import logging
2
+ import time
3
+ from functools import wraps
4
+
5
+
6
+ def log_action(
7
+ module_name: str,
8
+ action_name: str,
9
+ ):
10
+ """
11
+ Decorator to log the execution of a function, including the start, end, and elapsed time in milliseconds.
12
+ Logs will be captured using the specified module name and include details about the specified action name.
13
+
14
+ The decorator also logs exceptions if the wrapped function raises one.
15
+
16
+ Args:
17
+ module_name (str): The name of the module to associate with the logger.
18
+ action_name (str): The name of the action to log.
19
+ """
20
+
21
+ def decorator(func):
22
+ @wraps(func)
23
+ def wrapper(*args, **kwargs):
24
+ log = logging.getLogger(module_name)
25
+ start_time = time.perf_counter()
26
+ log.info("%s start", action_name)
27
+
28
+ try:
29
+ result = func(*args, **kwargs)
30
+ return result
31
+ except Exception:
32
+ log.error("%s failed", action_name)
33
+ raise
34
+ finally:
35
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
36
+ log.info("%s finished in %.2f ms", action_name, elapsed_ms)
37
+
38
+ return wrapper
39
+
40
+ return decorator