socx-cli 0.13.3__tar.gz → 0.13.4__tar.gz
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.
- {socx_cli-0.13.3 → socx_cli-0.13.4}/PKG-INFO +2 -1
- {socx_cli-0.13.3 → socx_cli-0.13.4}/pyproject.toml +2 -1
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/__init__.py +0 -8
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/callbacks.py +8 -6
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/_paths.py +8 -2
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/io/__init__.py +0 -8
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/io/log.py +164 -62
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/__init__.py +0 -2
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/progress.py +68 -66
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/regression.py +145 -148
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/test.py +38 -19
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/cli.yaml +1 -1
- socx_cli-0.13.4/socx/static/settings/logging.yaml +24 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/regression.yaml +4 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/settings.yaml +2 -1
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/callbacks.py +10 -2
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/run.py +5 -2
- socx_cli-0.13.4/socx_plugins/regression/tui.py +30 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/app.py +7 -2
- socx_cli-0.13.4/socx_tui/regression/details.py +158 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/widget.py +62 -92
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/static/tcss/regression/app.tcss +3 -1
- socx_cli-0.13.3/socx_plugins/regression/tui.py +0 -11
- socx_cli-0.13.3/socx_tui/regression/details.py +0 -165
- {socx_cli-0.13.3 → socx_cli-0.13.4}/.gitignore +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/LICENSE +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/README.md +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/__main__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/_cli.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/_jinja.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/cfg.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/cli.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/params.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/types.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/_config.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/_settings.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/converters.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/encoders.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/formatters.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/serializers.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/validators.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/encoder.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/enums.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/funcs.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/metadata.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/paths.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/git/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/git/git.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/git/manifest.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/plugin.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/types.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/serializer.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/git/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/git/_git.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/git/_manifest.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/git/_ssh.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/io/console.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/io/decorators.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/mixins/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/mixins/proxy.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/mixins/uid.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/singleton/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/singleton/singleton.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/visitor/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/visitor/protocol.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/visitor/traversal.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/status.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/validator.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/visitor.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/console.yaml +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/git.yaml +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/plugins.yaml +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/rich_click.yaml +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/sql/socx.sql +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/utils/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/utils/decorators.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/config/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/config/_config.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/config/edit.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/arguments.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/callbacks.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/cli.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/manifest.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/renderables.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/summary.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/utils.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/plugin/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/plugin/example.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/plugin/schema.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/_run.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/cli.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/version/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/version/__main__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/__main__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/bindings/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/bindings/vim/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/bindings/vim/mode.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/bindings/vim/vim.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/containers.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/dialog.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/mixins/__init__.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/mixins/composable.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/preview.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/table.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/tree.py +0 -0
- {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/static/tcss/regression/preview.tcss +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: socx-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.4
|
|
4
4
|
Summary: System on chip verification and tooling infrastructure.
|
|
5
5
|
Project-URL: Issues, https://github.com/sagikimhi/socx-cli/issues
|
|
6
6
|
Project-URL: Homepage, https://sagikimhi.dev/socx-cli
|
|
@@ -31,6 +31,7 @@ Requires-Python: >=3.12
|
|
|
31
31
|
Requires-Dist: anyio>=4.12.1
|
|
32
32
|
Requires-Dist: click
|
|
33
33
|
Requires-Dist: copier~=9.11
|
|
34
|
+
Requires-Dist: declare
|
|
34
35
|
Requires-Dist: dynaconf<3.2.12
|
|
35
36
|
Requires-Dist: gitpython
|
|
36
37
|
Requires-Dist: hoptex
|
|
@@ -29,7 +29,7 @@ socx = 'socx.__main__:main'
|
|
|
29
29
|
[project]
|
|
30
30
|
name = "socx-cli"
|
|
31
31
|
readme = "README.md"
|
|
32
|
-
version = "0.13.
|
|
32
|
+
version = "0.13.4"
|
|
33
33
|
license = "Apache-2.0"
|
|
34
34
|
authors = [{ name = "Sagi Kimhi", email = "sagi.kim5@gmail.com" }]
|
|
35
35
|
maintainers = [{ name = "Sagi Kimhi", email = "sagi.kim5@gmail.com" }]
|
|
@@ -59,6 +59,7 @@ dependencies = [
|
|
|
59
59
|
"pip",
|
|
60
60
|
"rich",
|
|
61
61
|
"click",
|
|
62
|
+
"declare",
|
|
62
63
|
"typer~=0.19",
|
|
63
64
|
"jinja2",
|
|
64
65
|
"psutil",
|
|
@@ -47,7 +47,6 @@ __all__ = (
|
|
|
47
47
|
"DEFAULT_HANDLERS",
|
|
48
48
|
"DEFAULT_TIME_FORMAT",
|
|
49
49
|
"Level",
|
|
50
|
-
"log",
|
|
51
50
|
"log_it",
|
|
52
51
|
"logger",
|
|
53
52
|
"console",
|
|
@@ -135,7 +134,6 @@ __all__ = (
|
|
|
135
134
|
"Regression",
|
|
136
135
|
"TestStatus",
|
|
137
136
|
"TestResult",
|
|
138
|
-
"RegressionTree",
|
|
139
137
|
"RegressionProgress",
|
|
140
138
|
)
|
|
141
139
|
|
|
@@ -180,11 +178,6 @@ from socx.core import deduplicate as deduplicate
|
|
|
180
178
|
|
|
181
179
|
from socx.utils import join_decorators as join_decorators
|
|
182
180
|
|
|
183
|
-
from socx.io import log as log
|
|
184
|
-
from socx.io import DEFAULT_LEVEL as DEFAULT_LEVEL
|
|
185
|
-
from socx.io import DEFAULT_FORMAT as DEFAULT_FORMAT
|
|
186
|
-
from socx.io import DEFAULT_HANDLERS as DEFAULT_HANDLERS
|
|
187
|
-
from socx.io import DEFAULT_TIME_FORMAT as DEFAULT_TIME_FORMAT
|
|
188
181
|
from socx.io import Level as Level
|
|
189
182
|
from socx.io import log_it as log_it
|
|
190
183
|
from socx.io import logger as logger
|
|
@@ -264,7 +257,6 @@ from socx.regression import TestBase as TestBase
|
|
|
264
257
|
from socx.regression import Regression as Regression
|
|
265
258
|
from socx.regression import TestStatus as TestStatus
|
|
266
259
|
from socx.regression import TestResult as TestResult
|
|
267
|
-
from socx.regression import RegressionTree as RegressionTree
|
|
268
260
|
from socx.regression import RegressionProgress as RegressionProgress
|
|
269
261
|
|
|
270
262
|
from socx.cli import FuncType as FuncType
|
|
@@ -12,7 +12,7 @@ from rich_click import Context
|
|
|
12
12
|
from rich_click import Parameter
|
|
13
13
|
from rich_click import RichContext
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
from socx.io.log import Level, set_level, get_level, _get_logger
|
|
16
16
|
from socx.io.console import console
|
|
17
17
|
from socx.io.decorators import log_it
|
|
18
18
|
from socx.config._config import settings
|
|
@@ -45,10 +45,11 @@ def cwd_cb(ctx: Context, param: Parameter, value: Path) -> Path:
|
|
|
45
45
|
@log_it(logger=logger)
|
|
46
46
|
def debug_cb(_: Context, param: Parameter, value: bool) -> bool:
|
|
47
47
|
"""Enable debug logging and persist the CLI switch to settings."""
|
|
48
|
+
socx_logger = _get_logger()
|
|
48
49
|
settings.cli.params[param.name] = value
|
|
49
50
|
if value:
|
|
50
|
-
|
|
51
|
-
settings.cli.params["verbosity"] =
|
|
51
|
+
set_level(Level.DEBUG, socx_logger)
|
|
52
|
+
settings.cli.params["verbosity"] = get_level().name
|
|
52
53
|
return value
|
|
53
54
|
|
|
54
55
|
|
|
@@ -88,10 +89,11 @@ def configure_cb(ctx: Context, param: Parameter, value: str) -> str:
|
|
|
88
89
|
@log_it(logger=logger)
|
|
89
90
|
def verbosity_cb(_: Context, param: Parameter, value: str) -> str:
|
|
90
91
|
"""Update the global log level while respecting existing overrides."""
|
|
91
|
-
|
|
92
|
+
socx_logger = _get_logger()
|
|
93
|
+
level = Level[value.upper()]
|
|
92
94
|
if not settings.cli.params.debug:
|
|
93
|
-
|
|
94
|
-
settings.cli.params[param.name] =
|
|
95
|
+
set_level(level, socx_logger)
|
|
96
|
+
settings.cli.params[param.name] = get_level().name
|
|
95
97
|
return settings.cli.params[param.name]
|
|
96
98
|
|
|
97
99
|
|
|
@@ -109,8 +109,11 @@ USER_LOG_DIR: Path = user_log_path(
|
|
|
109
109
|
).resolve()
|
|
110
110
|
"""Absolute path to platform's native application logs directory."""
|
|
111
111
|
|
|
112
|
-
USER_LOG_FILENAME: str = f"{__appname__}.log"
|
|
113
|
-
"""File name of application's
|
|
112
|
+
USER_LOG_FILENAME: str = f"{__appname__}_log.log"
|
|
113
|
+
"""File name of application's log file."""
|
|
114
|
+
|
|
115
|
+
USER_ROTATING_LOG_FILENAME: str = f"rotating_{__appname__}_log.log"
|
|
116
|
+
"""File name of application's rotating log file."""
|
|
114
117
|
|
|
115
118
|
USER_CONFIG_FILENAME: str = f"{__appname__}.yaml"
|
|
116
119
|
"""File name searched in user's config directory to load user configs."""
|
|
@@ -118,6 +121,9 @@ USER_CONFIG_FILENAME: str = f"{__appname__}.yaml"
|
|
|
118
121
|
USER_LOG_FILE: Path = USER_LOG_DIR / USER_LOG_FILENAME
|
|
119
122
|
"""Absolute path to application's main log for the current local user."""
|
|
120
123
|
|
|
124
|
+
USER_ROTATING_LOG_FILE: Path = USER_LOG_DIR / USER_ROTATING_LOG_FILENAME
|
|
125
|
+
"""Absolute path to application's rotating log for the current local user."""
|
|
126
|
+
|
|
121
127
|
USER_CONFIG_FILE: Path = USER_CONFIG_DIR / USER_CONFIG_FILENAME
|
|
122
128
|
"""Absolute path to application's user config file."""
|
|
123
129
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Expose logging, console, and decorator utilities for I/O facilities."""
|
|
2
2
|
|
|
3
3
|
__all__ = (
|
|
4
|
-
# Modules
|
|
5
|
-
"log",
|
|
6
4
|
# Constants
|
|
7
5
|
"DEFAULT_LEVEL",
|
|
8
6
|
"DEFAULT_FORMAT",
|
|
@@ -32,12 +30,6 @@ __all__ = (
|
|
|
32
30
|
"print_command_outputs",
|
|
33
31
|
)
|
|
34
32
|
|
|
35
|
-
from socx.io import log as log
|
|
36
|
-
|
|
37
|
-
from socx.io.log import DEFAULT_LEVEL as DEFAULT_LEVEL
|
|
38
|
-
from socx.io.log import DEFAULT_FORMAT as DEFAULT_FORMAT
|
|
39
|
-
from socx.io.log import DEFAULT_HANDLERS as DEFAULT_HANDLERS
|
|
40
|
-
from socx.io.log import DEFAULT_TIME_FORMAT as DEFAULT_TIME_FORMAT
|
|
41
33
|
from socx.io.log import Level as Level
|
|
42
34
|
from socx.io.log import logger as logger
|
|
43
35
|
from socx.io.log import get_level as get_level
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
"""Logging helpers that standardise Rich-powered output across SoCX."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
from types import ModuleType
|
|
4
5
|
|
|
5
|
-
import os
|
|
6
6
|
import enum
|
|
7
7
|
import logging
|
|
8
8
|
import logging.handlers
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, IO
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from collections import ChainMap
|
|
12
12
|
from collections.abc import Iterable
|
|
13
13
|
|
|
14
14
|
from rich.console import Console
|
|
15
15
|
from rich.logging import RichHandler
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
from socx.config._config import settings
|
|
18
|
+
from socx.core.metadata import __appname__
|
|
17
19
|
|
|
18
20
|
__all__ = (
|
|
19
21
|
# Logging
|
|
@@ -39,10 +41,7 @@ __all__ = (
|
|
|
39
41
|
# Types
|
|
40
42
|
"Level",
|
|
41
43
|
# Defaults
|
|
42
|
-
"DEFAULT_LEVEL",
|
|
43
|
-
"DEFAULT_FORMAT",
|
|
44
44
|
"DEFAULT_HANDLERS",
|
|
45
|
-
"DEFAULT_TIME_FORMAT",
|
|
46
45
|
)
|
|
47
46
|
|
|
48
47
|
|
|
@@ -59,95 +58,195 @@ class Level(enum.IntEnum):
|
|
|
59
58
|
CRITICAL = logging.CRITICAL
|
|
60
59
|
|
|
61
60
|
|
|
62
|
-
def
|
|
61
|
+
def _get_console(
|
|
62
|
+
file: IO | None = None,
|
|
63
|
+
stderr: bool = False,
|
|
64
|
+
markup: bool = True,
|
|
65
|
+
tab_size: int = 4,
|
|
66
|
+
force_terminal: bool = True,
|
|
67
|
+
**kwargs: Any,
|
|
68
|
+
) -> Console:
|
|
69
|
+
defaults = dict(
|
|
70
|
+
file=file,
|
|
71
|
+
markup=markup,
|
|
72
|
+
stderr=stderr,
|
|
73
|
+
tab_size=tab_size,
|
|
74
|
+
force_terminal=force_terminal,
|
|
75
|
+
)
|
|
76
|
+
kwargs = dict(ChainMap(kwargs, defaults))
|
|
77
|
+
return Console(**kwargs)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_level(level: str | int | Level) -> Level:
|
|
81
|
+
if isinstance(level, str):
|
|
82
|
+
level = Level[level]
|
|
83
|
+
elif isinstance(level, int):
|
|
84
|
+
level = Level(level)
|
|
85
|
+
return level
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_console_handler(
|
|
89
|
+
file: IO | None = None,
|
|
90
|
+
level: int | str | Level = Level.INFO,
|
|
91
|
+
stderr: bool = False,
|
|
92
|
+
tab_size: int = 4,
|
|
93
|
+
tracebacks: bool = True,
|
|
94
|
+
force_terminal: bool = True,
|
|
95
|
+
tracebacks_theme: str | None = None,
|
|
96
|
+
tracebacks_suppress: Iterable[ModuleType] | None = None,
|
|
97
|
+
tracebacks_show_locals: bool = True,
|
|
98
|
+
) -> logging.Handler:
|
|
63
99
|
"""Create a Rich console handler configured for interactive output."""
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
100
|
+
import click
|
|
101
|
+
import rich_click
|
|
102
|
+
from socx.io.console import console as _console
|
|
103
|
+
|
|
104
|
+
level = _get_level(level)
|
|
105
|
+
tracebacks_theme = tracebacks_theme or "ansi_dark"
|
|
106
|
+
tracebacks_suppress = tracebacks_suppress or [click, rich_click]
|
|
107
|
+
if file is None and stderr is None:
|
|
108
|
+
console = _console
|
|
109
|
+
else:
|
|
110
|
+
console = _get_console(
|
|
111
|
+
file=file,
|
|
112
|
+
stderr=stderr,
|
|
113
|
+
tab_size=tab_size,
|
|
114
|
+
force_terminal=force_terminal,
|
|
115
|
+
)
|
|
116
|
+
handler = RichHandler(
|
|
67
117
|
console=console,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
118
|
+
rich_tracebacks=tracebacks,
|
|
119
|
+
tracebacks_theme=tracebacks_theme,
|
|
120
|
+
tracebacks_suppress=tracebacks_suppress,
|
|
121
|
+
tracebacks_show_locals=tracebacks_show_locals,
|
|
71
122
|
)
|
|
123
|
+
formatter = logging.Formatter(**settings.logging.formatters.default)
|
|
124
|
+
handler.name = "console"
|
|
125
|
+
handler.setLevel(level)
|
|
126
|
+
handler.setFormatter(formatter)
|
|
127
|
+
return handler
|
|
72
128
|
|
|
73
129
|
|
|
74
130
|
def _get_file_handler(
|
|
75
|
-
path: str | Path,
|
|
131
|
+
path: str | Path,
|
|
132
|
+
mode: str | None = None,
|
|
133
|
+
level: Level = Level.INFO,
|
|
134
|
+
stderr: bool = False,
|
|
135
|
+
tab_size: int = 4,
|
|
136
|
+
tracebacks: bool = True,
|
|
137
|
+
force_terminal: bool = False,
|
|
138
|
+
tracebacks_theme: str | None = None,
|
|
139
|
+
tracebacks_suppress: Iterable[ModuleType] | None = None,
|
|
140
|
+
tracebacks_show_locals: bool = True,
|
|
141
|
+
) -> logging.Handler:
|
|
142
|
+
import atexit
|
|
143
|
+
|
|
144
|
+
def close_if_open(file: IO) -> None:
|
|
145
|
+
if not file.closed:
|
|
146
|
+
file.close()
|
|
147
|
+
|
|
148
|
+
mode = mode or "a"
|
|
149
|
+
file = open(path, mode=mode) # noqa: SIM115
|
|
150
|
+
atexit.register(close_if_open, file)
|
|
151
|
+
handler = _get_console_handler(
|
|
152
|
+
file=file,
|
|
153
|
+
level=level,
|
|
154
|
+
stderr=stderr,
|
|
155
|
+
tab_size=tab_size,
|
|
156
|
+
tracebacks=tracebacks,
|
|
157
|
+
force_terminal=force_terminal,
|
|
158
|
+
tracebacks_theme=tracebacks_theme,
|
|
159
|
+
tracebacks_suppress=tracebacks_suppress,
|
|
160
|
+
tracebacks_show_locals=tracebacks_show_locals,
|
|
161
|
+
)
|
|
162
|
+
handler.name = "file"
|
|
163
|
+
handler.setFormatter(DEFAULT_CHILD_FORMATTER)
|
|
164
|
+
return handler
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _get_rotating_file_handler(
|
|
168
|
+
path: str | Path,
|
|
169
|
+
level: Level = Level.DEBUG,
|
|
170
|
+
stderr: bool = False,
|
|
171
|
+
mode: str | None = None,
|
|
76
172
|
) -> logging.Handler:
|
|
77
173
|
"""Create a Rich handler that writes log output to ``path``."""
|
|
174
|
+
|
|
175
|
+
def MBs(n: int) -> int: # noqa: N802
|
|
176
|
+
return 1024 * 1024 * n
|
|
177
|
+
|
|
178
|
+
mode = mode or "w"
|
|
78
179
|
handler = logging.handlers.RotatingFileHandler(
|
|
79
|
-
|
|
180
|
+
# no particular reason for size or backup count - arbitrarily chosen
|
|
181
|
+
path,
|
|
182
|
+
mode=mode,
|
|
183
|
+
maxBytes=MBs(10),
|
|
184
|
+
backupCount=5,
|
|
80
185
|
)
|
|
186
|
+
handler.name = "rotating_file"
|
|
81
187
|
handler.setLevel(level)
|
|
82
|
-
handler.setFormatter(
|
|
83
|
-
logging.Formatter(DEFAULT_CHILD_FORMAT, DEFAULT_TIME_FORMAT)
|
|
84
|
-
)
|
|
188
|
+
handler.setFormatter(DEFAULT_CHILD_FORMATTER)
|
|
85
189
|
return handler
|
|
86
190
|
|
|
87
191
|
|
|
88
|
-
|
|
89
|
-
|
|
192
|
+
def _get_handler(handler: str) -> logging.Handler | None:
|
|
193
|
+
match handler:
|
|
194
|
+
case "file":
|
|
195
|
+
return _get_file_handler(**settings.logging.handlers.file)
|
|
196
|
+
case "console":
|
|
197
|
+
return _get_console_handler(**settings.logging.handlers.console)
|
|
198
|
+
case "rotating_file":
|
|
199
|
+
return _get_rotating_file_handler(
|
|
200
|
+
**settings.logging.handlers.rotating_file
|
|
201
|
+
)
|
|
202
|
+
case _:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _get_logger() -> logging.Logger:
|
|
207
|
+
"""Initialise and return the module-level root logger."""
|
|
208
|
+
socx_logger = logging.getLogger(__appname__)
|
|
209
|
+
|
|
210
|
+
if not socx_logger.hasHandlers():
|
|
211
|
+
unknown_handlers = []
|
|
90
212
|
|
|
91
|
-
|
|
92
|
-
|
|
213
|
+
for handler_name in settings.logging.handlers:
|
|
214
|
+
handler = _get_handler(handler_name)
|
|
215
|
+
if handler is not None:
|
|
216
|
+
socx_logger.addHandler(handler)
|
|
217
|
+
else:
|
|
218
|
+
unknown_handlers.append(handler_name)
|
|
93
219
|
|
|
94
|
-
|
|
95
|
-
"
|
|
220
|
+
for handler in unknown_handlers:
|
|
221
|
+
msg = f"Ignored unknown handler configuration: '{handler}'"
|
|
222
|
+
socx_logger.warning(msg)
|
|
96
223
|
|
|
97
|
-
|
|
98
|
-
"""Default log message format used by the root handler."""
|
|
224
|
+
return socx_logger
|
|
99
225
|
|
|
100
|
-
DEFAULT_TIME_FORMAT: str = os.environ.get("SOCX_TIME_FORMAT", "[%x %X]")
|
|
101
|
-
"""Default timestamp format injected into log records."""
|
|
102
226
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"%(asctime)s %(levelname)5s - %(filename)5s:%(lineno)-4d - %(message)s",
|
|
227
|
+
DEFAULT_FORMATTER: logging.Formatter = logging.Formatter(
|
|
228
|
+
**settings.logging.formatters.default
|
|
106
229
|
)
|
|
107
|
-
"""
|
|
230
|
+
"""Default application logging formatter."""
|
|
108
231
|
|
|
109
232
|
DEFAULT_CHILD_FORMATTER: logging.Formatter = logging.Formatter(
|
|
110
|
-
|
|
233
|
+
**settings.logging.formatters.child
|
|
111
234
|
)
|
|
112
235
|
"""Formatter applied to file handlers registered on child loggers."""
|
|
113
236
|
|
|
114
|
-
DEFAULT_LOG_DIRECTORY: Path = Path(
|
|
115
|
-
os.environ.get(
|
|
116
|
-
"SOCX_LOG_DIR",
|
|
117
|
-
user_log_path(appname=APP_LIB_NAME, ensure_exists=True),
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
"""Default application log directory."""
|
|
121
|
-
|
|
122
|
-
DEFAULT_LOG_FILE: str = os.environ.get("SOCX_LOG_FILE", f"{APP_LIB_NAME}.log")
|
|
123
|
-
"""Default application log file."""
|
|
124
|
-
|
|
125
237
|
DEFAULT_HANDLERS: list[logging.Handler] = [
|
|
126
|
-
|
|
127
|
-
|
|
238
|
+
_get_file_handler(**settings.logging.handlers.file),
|
|
239
|
+
_get_console_handler(**settings.logging.handlers.console),
|
|
240
|
+
_get_rotating_file_handler(**settings.logging.handlers.rotating_file),
|
|
128
241
|
]
|
|
129
242
|
"""Handlers attached to the module-level logger by default."""
|
|
130
243
|
|
|
131
|
-
DEFAULT_LOGGING_CONFIG: dict[str, Any] = dict(
|
|
132
|
-
level=DEFAULT_LEVEL,
|
|
133
|
-
format=DEFAULT_FORMAT,
|
|
134
|
-
handlers=DEFAULT_HANDLERS,
|
|
135
|
-
encoding=DEFAULT_ENCODING,
|
|
136
|
-
datefmt=DEFAULT_TIME_FORMAT,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _get_logger(**kwargs: Any) -> logging.Logger:
|
|
141
|
-
"""Initialise and return the module-level root logger."""
|
|
142
|
-
logging.basicConfig(**dict(ChainMap(kwargs, DEFAULT_LOGGING_CONFIG)))
|
|
143
|
-
return logging.getLogger(APP_LIB_NAME)
|
|
144
|
-
|
|
145
244
|
|
|
146
245
|
def get_logger(name: str, filename: str | None = None) -> logging.Logger:
|
|
147
246
|
"""Return a child logger configured with optional file output."""
|
|
148
247
|
rv = logger.getChild(name)
|
|
149
248
|
if filename is not None:
|
|
150
|
-
handler =
|
|
249
|
+
handler = _get_rotating_file_handler(filename)
|
|
151
250
|
handler.setFormatter(DEFAULT_CHILD_FORMATTER)
|
|
152
251
|
rv.addHandler(handler)
|
|
153
252
|
return rv
|
|
@@ -243,8 +342,11 @@ def get_level(logger_: logging.Logger | None = None) -> Level:
|
|
|
243
342
|
|
|
244
343
|
|
|
245
344
|
def set_level(level: Level, logger_: logging.Logger | None = None) -> None:
|
|
246
|
-
"""Set the log level on
|
|
345
|
+
"""Set the log level on ``logger_`` and all currently attached handlers."""
|
|
247
346
|
logger_ = logger_ or logger
|
|
347
|
+
level = level if isinstance(level, str | int) else level.value
|
|
348
|
+
for handler in logger_.handlers:
|
|
349
|
+
handler.setLevel(level)
|
|
248
350
|
logger_.setLevel(level)
|
|
249
351
|
|
|
250
352
|
|
|
@@ -6,7 +6,6 @@ __all__ = (
|
|
|
6
6
|
"TestStatus",
|
|
7
7
|
"TestResult",
|
|
8
8
|
"Regression",
|
|
9
|
-
"RegressionTree",
|
|
10
9
|
"RegressionProgress",
|
|
11
10
|
)
|
|
12
11
|
|
|
@@ -15,5 +14,4 @@ from socx.regression.test import TestBase as TestBase
|
|
|
15
14
|
from socx.regression.test import TestStatus as TestStatus
|
|
16
15
|
from socx.regression.test import TestResult as TestResult
|
|
17
16
|
from socx.regression.regression import Regression as Regression
|
|
18
|
-
from socx.regression.regression import RegressionTree as RegressionTree
|
|
19
17
|
from socx.regression.progress import RegressionProgress as RegressionProgress
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import anyio
|
|
4
|
-
import anyio.lowlevel
|
|
5
4
|
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
from collections import ChainMap
|
|
6
7
|
|
|
7
8
|
from rich.progress import (
|
|
8
9
|
Progress as BaseProgress,
|
|
@@ -17,30 +18,23 @@ from rich.progress import (
|
|
|
17
18
|
Task,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
|
-
from socx.io.console import console
|
|
21
21
|
from socx.regression.test import TestStatus
|
|
22
22
|
from socx.regression.regression import Regression
|
|
23
23
|
|
|
24
|
+
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class PipelineProgress(BaseProgress):
|
|
28
|
-
def __init__(self) -> None:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
speed_estimate_period=10,
|
|
32
|
-
redirect_stderr=True,
|
|
33
|
-
redirect_stdout=True,
|
|
34
|
-
console=console,
|
|
35
|
-
)
|
|
29
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
30
|
+
kwargs = dict(ChainMap(kwargs, dict(speed_estimate_period=10)))
|
|
31
|
+
super().__init__(*self.get_default_columns(), **kwargs)
|
|
36
32
|
|
|
37
33
|
@classmethod
|
|
38
34
|
def get_default_columns(cls) -> tuple[ProgressColumn, ...]:
|
|
39
35
|
return (
|
|
40
36
|
SpinnerColumn(),
|
|
41
|
-
TextColumn(
|
|
42
|
-
"[progress.description]{task.description}", justify="right"
|
|
43
|
-
),
|
|
37
|
+
TextColumn("[progress.description]{task.description}"),
|
|
44
38
|
MofNCompleteColumn(),
|
|
45
39
|
BarColumn(),
|
|
46
40
|
TaskProgressColumn(),
|
|
@@ -56,11 +50,7 @@ class RegressionProgress:
|
|
|
56
50
|
self.tasks = {}
|
|
57
51
|
self.total = len(regression)
|
|
58
52
|
self.regression = regression
|
|
59
|
-
self.
|
|
60
|
-
# for child in self.regression.tests:
|
|
61
|
-
# if isinstance(child, Regression):
|
|
62
|
-
# self.progress_map[child.id] = PipelineProgress()
|
|
63
|
-
self.progress_map[self.regression.id] = PipelineProgress()
|
|
53
|
+
self.progress = PipelineProgress()
|
|
64
54
|
|
|
65
55
|
def __len__(self) -> int:
|
|
66
56
|
"""Get the total number of test items in a regression's progress."""
|
|
@@ -72,11 +62,20 @@ class RegressionProgress:
|
|
|
72
62
|
exclude: set[str] | None = None,
|
|
73
63
|
) -> None:
|
|
74
64
|
"""Update progress tasks and flush log messages while running."""
|
|
75
|
-
with
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
with self.progress:
|
|
66
|
+
if include is None and exclude is None:
|
|
67
|
+
async with anyio.create_task_group() as tg:
|
|
68
|
+
for obj in self.regression.tests:
|
|
69
|
+
tg.start_soon(
|
|
70
|
+
self.track_regression,
|
|
71
|
+
obj,
|
|
72
|
+
name=f"track_{obj.name}_progress",
|
|
73
|
+
)
|
|
74
|
+
tg.start_soon(
|
|
75
|
+
self.regression.start,
|
|
76
|
+
name=f"{self.regression.name}",
|
|
77
|
+
)
|
|
78
|
+
return
|
|
80
79
|
|
|
81
80
|
async with anyio.create_task_group() as tg:
|
|
82
81
|
for obj in self.regression.tests:
|
|
@@ -86,41 +85,31 @@ class RegressionProgress:
|
|
|
86
85
|
if include is not None and obj.name not in include:
|
|
87
86
|
continue
|
|
88
87
|
|
|
89
|
-
if isinstance(obj, Regression):
|
|
90
|
-
|
|
91
|
-
track_task_name = f"track_{obj.name}_progress"
|
|
88
|
+
if isinstance(obj, Regression) and not obj.started:
|
|
89
|
+
tg.start_soon(obj.start, name=obj.name)
|
|
92
90
|
tg.start_soon(
|
|
93
|
-
|
|
91
|
+
self.track_regression,
|
|
92
|
+
obj,
|
|
93
|
+
name=f"track_{obj.name}_progress",
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
run_task_func = obj.start
|
|
97
|
-
run_task_name = f"run_{obj.name}"
|
|
98
|
-
tg.start_soon(run_task_func, name=run_task_name)
|
|
99
|
-
|
|
100
|
-
if include is None and exclude is None:
|
|
101
|
-
run_task_func = self.regression.start
|
|
102
|
-
run_task_name = f"run_{self.regression.name}"
|
|
103
|
-
tg.start_soon(run_task_func, name=run_task_name)
|
|
104
|
-
|
|
105
|
-
del self.progress_map[self.regression.id]
|
|
106
|
-
|
|
107
96
|
async def track_regression(self, regression: Regression) -> None:
|
|
108
|
-
|
|
97
|
+
finished = 0
|
|
98
|
+
status = TestStatus.Idle
|
|
99
|
+
progress = self.progress
|
|
100
|
+
|
|
109
101
|
if regression.id not in self.tasks:
|
|
110
102
|
self.tasks[regression.id] = progress.add_task(
|
|
111
103
|
total=len(regression),
|
|
112
104
|
description=(
|
|
113
|
-
f"[
|
|
105
|
+
f"[gray39]{self._get_task_tag(regression)}: "
|
|
106
|
+
f"{regression.status.name}"
|
|
114
107
|
),
|
|
115
108
|
)
|
|
116
|
-
await self._track_regression(regression)
|
|
117
109
|
|
|
118
|
-
|
|
119
|
-
finished = 0
|
|
120
|
-
status = regression.status
|
|
121
|
-
while True:
|
|
110
|
+
while not progress.finished:
|
|
122
111
|
if regression.finished:
|
|
123
|
-
|
|
112
|
+
self.update_regression(regression, len(regression))
|
|
124
113
|
break
|
|
125
114
|
|
|
126
115
|
prev_status = status
|
|
@@ -133,39 +122,49 @@ class RegressionProgress:
|
|
|
133
122
|
)
|
|
134
123
|
|
|
135
124
|
if prev_finished != finished or prev_status != status:
|
|
136
|
-
|
|
125
|
+
self.update_regression(regression, finished)
|
|
137
126
|
|
|
138
|
-
await anyio.sleep(0.
|
|
127
|
+
await anyio.sleep(0.5)
|
|
139
128
|
|
|
140
|
-
|
|
141
|
-
progress = self.
|
|
129
|
+
def advance_regression(self, regression: Regression, n: int) -> None:
|
|
130
|
+
progress = self.progress
|
|
142
131
|
tid = self.tasks.get(regression.id)
|
|
143
132
|
if progress is not None and tid is not None:
|
|
144
133
|
task = progress.tasks[tid]
|
|
145
|
-
|
|
134
|
+
self.update_regression(regression, task.completed + n)
|
|
146
135
|
|
|
147
|
-
|
|
148
|
-
progress = self.
|
|
136
|
+
def update_regression(self, regression: Regression, n: int) -> None:
|
|
137
|
+
progress = self.progress
|
|
149
138
|
tid = self.tasks.get(regression.id)
|
|
150
139
|
if progress is not None and tid is not None:
|
|
151
140
|
task: Task = progress.tasks[tid]
|
|
152
|
-
if task.completed >= n:
|
|
153
|
-
return
|
|
154
141
|
|
|
155
|
-
if
|
|
156
|
-
|
|
157
|
-
|
|
142
|
+
if (
|
|
143
|
+
regression.is_idle()
|
|
144
|
+
or regression.is_pending()
|
|
145
|
+
or regression.is_suspended()
|
|
146
|
+
):
|
|
147
|
+
task.description = (
|
|
148
|
+
f"[gray39]{self._get_task_tag(regression)}: "
|
|
149
|
+
f"{regression.status.name}"
|
|
158
150
|
)
|
|
159
|
-
|
|
160
|
-
description = (
|
|
161
|
-
f"[
|
|
151
|
+
elif regression.is_running():
|
|
152
|
+
task.description = (
|
|
153
|
+
f"[yellow]{self._get_task_tag(regression)}: "
|
|
154
|
+
f"{regression.status.name}"
|
|
155
|
+
)
|
|
156
|
+
elif regression.finished:
|
|
157
|
+
task.description = (
|
|
158
|
+
f"[green]{self._get_task_tag(regression)}: "
|
|
159
|
+
f"{regression.status.name}"
|
|
160
|
+
)
|
|
161
|
+
elif regression.terminated:
|
|
162
|
+
task.description = (
|
|
163
|
+
f"[red]{self._get_task_tag(regression)}: "
|
|
164
|
+
f"{regression.status.name}"
|
|
162
165
|
)
|
|
163
166
|
|
|
164
|
-
|
|
165
|
-
tid,
|
|
166
|
-
completed=min(n, task.total),
|
|
167
|
-
description=description,
|
|
168
|
-
)
|
|
167
|
+
task.completed = min(n, task.total)
|
|
169
168
|
|
|
170
169
|
def _count_statuses(
|
|
171
170
|
self, regression: Regression, *statuses: TestStatus
|
|
@@ -174,3 +173,6 @@ class RegressionProgress:
|
|
|
174
173
|
return sum(
|
|
175
174
|
1 for test in regression.tests if test.status in statuses
|
|
176
175
|
)
|
|
176
|
+
|
|
177
|
+
def _get_task_tag(self, regression: Regression) -> str:
|
|
178
|
+
return f"{regression.__class__.__name__}({regression.name})"
|