docker-tidy 4.2.0__tar.gz → 4.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-tidy
3
- Version: 4.2.0
3
+ Version: 4.2.2
4
4
  Summary: Keep docker hosts tidy.
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -24,24 +24,24 @@ Classifier: Programming Language :: Python :: 3.14
24
24
  Classifier: Topic :: Software Development
25
25
  Classifier: Topic :: System :: Systems Administration
26
26
  Classifier: Topic :: Utilities
27
- Requires-Dist: anyconfig (==0.15.1)
27
+ Requires-Dist: anyconfig (==0.16.0)
28
28
  Requires-Dist: appdirs (==1.4.4)
29
29
  Requires-Dist: certifi (==2026.7.22)
30
30
  Requires-Dist: colorama (==0.4.6)
31
- Requires-Dist: dateparser (==1.4.2)
31
+ Requires-Dist: dateparser (==1.4.3)
32
32
  Requires-Dist: docker (==7.2.0)
33
33
  Requires-Dist: docker-pycreds (==0.4.0)
34
- Requires-Dist: environs (==15.1.0)
35
- Requires-Dist: idna (==3.18)
34
+ Requires-Dist: environs (==15.2.0)
35
+ Requires-Dist: idna (==3.19)
36
36
  Requires-Dist: ipaddress (==1.0.23)
37
37
  Requires-Dist: jsonschema (==4.26.0)
38
38
  Requires-Dist: nested-lookup (==0.2.25)
39
39
  Requires-Dist: pathspec (==1.1.1)
40
40
  Requires-Dist: python-dateutil (==2.9.0.post0)
41
- Requires-Dist: python-json-logger (==4.1.0)
41
+ Requires-Dist: python-json-logger (==4.2.0)
42
42
  Requires-Dist: requests (==2.34.2)
43
43
  Requires-Dist: ruamel.yaml (==0.19.1)
44
- Requires-Dist: websocket_client (==1.9.0)
44
+ Requires-Dist: websocket_client (==1.9.2)
45
45
  Requires-Dist: zipp (==4.1.0)
46
46
  Project-URL: Documentation, https://docker-tidy.geekdocs.de/
47
47
  Project-URL: Homepage, https://docker-tidy.geekdocs.de/
@@ -0,0 +1,8 @@
1
+ """Default package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("docker-tidy")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
@@ -9,7 +9,7 @@ import environs
9
9
  import jsonschema.exceptions
10
10
  import ruamel.yaml
11
11
  from appdirs import AppDirs
12
- from jsonschema._utils import format_as_index
12
+ from environs.exceptions import EnvError, EnvNotSetError
13
13
 
14
14
  import dockertidy.exception
15
15
  import dockertidy.parser
@@ -167,20 +167,23 @@ class Config:
167
167
 
168
168
  def _get_envs(self) -> dict[str, Any]:
169
169
  normalized: dict[str, Any] = {}
170
+ prefix = "TIDY_"
170
171
  for key, item in self.SETTINGS.items():
171
- if item.get("env"):
172
- prefix = "TIDY_"
173
- env_name = prefix + item["env"]
174
- try:
175
- value = item["type"](env_name)
176
- normalized = self._add_dict_branch(normalized, key.split("."), value)
177
- except environs.EnvError as e:
178
- if f'"{env_name}" not set' in str(e):
179
- pass
180
- else:
181
- raise dockertidy.exception.ConfigError(
182
- "Unable to read environment variable", str(e)
183
- ) from e
172
+ env = item.get("env")
173
+ if not env:
174
+ continue
175
+
176
+ env_name = prefix + env
177
+ try:
178
+ value = item["type"](env_name)
179
+ except EnvNotSetError:
180
+ continue
181
+ except EnvError as e:
182
+ raise dockertidy.exception.ConfigError(
183
+ "Unable to read environment variable", str(e)
184
+ ) from e
185
+
186
+ normalized = self._add_dict_branch(normalized, key.split("."), value)
184
187
 
185
188
  return normalized
186
189
 
@@ -249,8 +252,9 @@ class Config:
249
252
  try:
250
253
  anyconfig.validate(config, self.schema, ac_schema_safe=False)
251
254
  except jsonschema.exceptions.ValidationError as e:
252
- schema = format_as_index("config", list(e.relative_schema_path)[1:-1])
253
- schema_error = f"Failed validating '{e.validator}' in schema {schema}\n{e.message}"
255
+ schema_error = (
256
+ f"Failed validating '{e.validator}' in schema {e.json_path}\n{e.message}"
257
+ )
254
258
  raise dockertidy.exception.ConfigError("Configuration error", schema_error) from e
255
259
 
256
260
  return True
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ """Global utility methods and classes."""
3
+
4
+ import copy
5
+ import logging
6
+ import os
7
+ import sys
8
+ from typing import Any, NoReturn
9
+
10
+ import colorama
11
+ from pythonjsonlogger.json import JsonFormatter
12
+
13
+ from dockertidy.utils import Singleton
14
+
15
+ JSON_FORMAT = "%(asctime)s %(levelname)s %(message)s"
16
+
17
+ _LEVEL_COLORS: dict[int, tuple[str, str]] = {
18
+ logging.DEBUG: (colorama.Fore.BLUE, colorama.Style.BRIGHT),
19
+ logging.INFO: (colorama.Fore.CYAN, colorama.Style.BRIGHT),
20
+ logging.WARNING: (colorama.Fore.YELLOW, colorama.Style.BRIGHT),
21
+ logging.ERROR: (colorama.Fore.RED, colorama.Style.BRIGHT),
22
+ logging.CRITICAL: (colorama.Fore.RED, colorama.Style.BRIGHT),
23
+ }
24
+
25
+ _STDOUT_LEVELS = {logging.INFO, logging.WARNING}
26
+ _STDERR_LEVELS = {logging.DEBUG, logging.ERROR, logging.CRITICAL}
27
+
28
+
29
+ def _should_do_markup() -> bool:
30
+ py_colors = os.environ.get("PY_COLORS")
31
+ if py_colors is not None:
32
+ return py_colors.strip().lower() in {"1", "true", "yes", "on", "y", "t"}
33
+
34
+ return sys.stdout.isatty() and os.environ.get("TERM") != "dumb"
35
+
36
+
37
+ colorama.init(autoreset=True, strip=not _should_do_markup())
38
+
39
+
40
+ class LevelFilter(logging.Filter):
41
+ """Keep only records whose level belongs to the configured level set."""
42
+
43
+ def __init__(self, levels: set[int]) -> None:
44
+ super().__init__()
45
+ self._levels = levels
46
+
47
+ def filter(self, record: logging.LogRecord) -> bool:
48
+ return record.levelno in self._levels
49
+
50
+
51
+ class ConsoleFormatter(logging.Formatter):
52
+ """Colorize the level prefix and reset the color after line breaks."""
53
+
54
+ def format(self, record: logging.LogRecord) -> str:
55
+ color, style = _LEVEL_COLORS.get(record.levelno, (colorama.Fore.WHITE, ""))
56
+ reset = colorama.Style.RESET_ALL
57
+ message = record.getMessage().replace("\n", f"\n{reset}... ")
58
+ output = f"{color}{style}[{record.levelname}]{reset} {message}"
59
+
60
+ if record.exc_info and not record.exc_text:
61
+ record.exc_text = self.formatException(record.exc_info)
62
+ if record.exc_text:
63
+ if output[-1:] != "\n":
64
+ output += "\n"
65
+ output += record.exc_text
66
+
67
+ return output
68
+
69
+
70
+ class MultilineJsonFormatter(JsonFormatter):
71
+ """JSON formatter that replaces line breaks in messages with spaces."""
72
+
73
+ def format(self, record: logging.LogRecord) -> str:
74
+ record = copy.copy(record)
75
+ record.msg = record.msg.replace("\n", " ")
76
+ return JsonFormatter.format(self, record)
77
+
78
+
79
+ class Log:
80
+ """Base logging object."""
81
+
82
+ def __init__(
83
+ self, level: int = logging.WARNING, name: str = "dockertidy", json: bool = False
84
+ ) -> None:
85
+ self.logger = logging.getLogger(name)
86
+ self.logger.propagate = False
87
+ self.logger.setLevel(level)
88
+ self._configure(json)
89
+
90
+ def _configure(self, json: bool = False) -> None:
91
+ for handler in self.logger.handlers[:]:
92
+ self.logger.removeHandler(handler)
93
+
94
+ self.logger.addHandler(self._get_handler(sys.stdout, _STDOUT_LEVELS, json))
95
+ self.logger.addHandler(self._get_handler(sys.stderr, _STDERR_LEVELS, json))
96
+
97
+ def _get_handler(self, stream: Any, levels: set[int], json: bool = False) -> logging.Handler:
98
+ handler = logging.StreamHandler(stream)
99
+ handler.setLevel(logging.DEBUG)
100
+ handler.addFilter(LevelFilter(levels))
101
+ handler.setFormatter(self._get_formatter(json))
102
+ return handler
103
+
104
+ def _get_formatter(self, json: bool = False) -> logging.Formatter:
105
+ if json:
106
+ return MultilineJsonFormatter(JSON_FORMAT)
107
+ return ConsoleFormatter()
108
+
109
+ def set_level(self, s: str | int) -> None:
110
+ """Set log level."""
111
+ self.logger.setLevel(s)
112
+
113
+ def sysexit(self, code: int = 1) -> NoReturn:
114
+ """Exit running program with given exit code."""
115
+ sys.exit(code)
116
+
117
+ def sysexit_with_message(self, msg: Any, code: int = 1) -> NoReturn:
118
+ """Exit running program with given exit code and message."""
119
+ self.logger.critical(str(msg))
120
+ self.sysexit(code)
121
+
122
+
123
+ class SingleLog(Log, metaclass=Singleton):
124
+ """Singleton logger object."""
125
+
126
+ pass
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python3
2
+ """Global utility methods and classes."""
3
+
4
+ import threading
5
+ from typing import Any
6
+
7
+
8
+ def dict_intersect(d1: dict[str, Any], d2: dict[str, Any]) -> dict[str, Any]:
9
+ return {
10
+ k: dict_intersect(d1[k], d2[k]) if isinstance(d1[k], dict) else d1[k]
11
+ for k in d1.keys() & d2.keys()
12
+ }
13
+
14
+
15
+ class Singleton(type):
16
+ """Thread-safe singleton metaclass."""
17
+
18
+ _instances: dict[Any, Any] = {}
19
+ _lock = threading.Lock()
20
+
21
+ def __call__(cls, *args: Any, **kwargs: Any) -> Any:
22
+ if cls not in cls._instances:
23
+ with cls._lock:
24
+ if cls not in cls._instances:
25
+ cls._instances[cls] = super().__call__(*args, **kwargs)
26
+ return cls._instances[cls]
@@ -28,44 +28,44 @@ name = "docker-tidy"
28
28
  packages = [{ include = "dockertidy", format = ["sdist", "wheel"] }]
29
29
  readme = "README.md"
30
30
  repository = "https://github.com/thegeeklab/docker-tidy/"
31
- version = "4.2.0"
31
+ version = "4.2.2"
32
32
 
33
33
  [tool.poetry.dependencies]
34
- anyconfig = "0.15.1"
34
+ anyconfig = "0.16.0"
35
35
  appdirs = "1.4.4"
36
36
  certifi = "2026.7.22"
37
37
  colorama = "0.4.6"
38
- dateparser = "1.4.2"
38
+ dateparser = "1.4.3"
39
39
  docker = "7.2.0"
40
40
  docker-pycreds = "0.4.0"
41
- environs = "15.1.0"
42
- idna = "3.18"
41
+ environs = "15.2.0"
42
+ idna = "3.19"
43
43
  ipaddress = "1.0.23"
44
44
  jsonschema = "4.26.0"
45
45
  nested-lookup = "0.2.25"
46
46
  pathspec = "1.1.1"
47
47
  python = "^3.11.0"
48
48
  python-dateutil = "2.9.0.post0"
49
- python-json-logger = "4.1.0"
49
+ python-json-logger = "4.2.0"
50
50
  requests = "2.34.2"
51
51
  "ruamel.yaml" = "0.19.1"
52
- websocket_client = "1.9.0"
52
+ websocket_client = "1.9.2"
53
53
  zipp = "4.1.0"
54
54
 
55
55
  [tool.poetry.scripts]
56
56
  docker-tidy = "dockertidy.cli:main"
57
57
 
58
58
  [tool.poetry.group.dev.dependencies]
59
- ruff = "0.16.2"
59
+ ruff = "0.16.7"
60
60
  pytest = "9.1.1"
61
61
  pytest-mock = "3.15.1"
62
62
  pytest-cov = "7.1.0"
63
63
  toml = "0.10.2"
64
64
  types-appdirs = "1.4.3.5"
65
- types-docker = "7.2.0.20260806"
66
- mypy = "2.3.0"
65
+ types-docker = "7.2.0.20260827"
66
+ mypy = "2.3.1"
67
67
  types-python-dateutil = "2.9.0.20260807"
68
- types-dateparser = "1.4.1.20260806"
68
+ types-dateparser = "1.4.3.20260907"
69
69
  types-jsonschema = "4.26.0.20260518"
70
70
  types-colorama = "0.4.15.20260508"
71
71
 
@@ -1,3 +0,0 @@
1
- """Default package."""
2
-
3
- __version__ = "4.2.0"
@@ -1,201 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Global utility methods and classes."""
3
-
4
- import logging
5
- import os
6
- import sys
7
- from typing import Any, NoReturn
8
-
9
- import colorama
10
- from pythonjsonlogger.json import JsonFormatter
11
-
12
- from dockertidy.utils import Singleton, to_bool
13
-
14
- CONSOLE_FORMAT = "{}[%(levelname)s]{} %(message)s"
15
- JSON_FORMAT = "%(asctime)s %(levelname)s %(message)s"
16
-
17
-
18
- def _should_do_markup() -> bool:
19
- py_colors = os.environ.get("PY_COLORS", None)
20
- if py_colors is not None:
21
- return to_bool(py_colors)
22
-
23
- return sys.stdout.isatty() and os.environ.get("TERM") != "dumb"
24
-
25
-
26
- colorama.init(autoreset=True, strip=not _should_do_markup())
27
-
28
-
29
- class LogFilter:
30
- """A custom log filter which excludes log messages above the logged level."""
31
-
32
- def __init__(self, level: int) -> None:
33
- """
34
- Initialize a new custom log filter.
35
-
36
- :param level: Log level limit
37
- :returns: None
38
-
39
- """
40
- self.__level = level
41
-
42
- def filter(self, logRecord: logging.LogRecord) -> bool: # noqa
43
- # https://docs.python.org/3/library/logging.html#logrecord-attributes
44
- return logRecord.levelno <= self.__level
45
-
46
-
47
- class MultilineFormatter(logging.Formatter):
48
- """Logging Formatter to reset color after newline characters."""
49
-
50
- def format(self, record: logging.LogRecord) -> str:
51
- record.msg = record.msg.replace("\n", f"\n{colorama.Style.RESET_ALL}... ")
52
- return logging.Formatter.format(self, record)
53
-
54
-
55
- class MultilineJsonFormatter(JsonFormatter):
56
- """Logging Formatter to remove newline characters."""
57
-
58
- def format(self, record: logging.LogRecord) -> str:
59
- record.msg = record.msg.replace("\n", " ")
60
- return JsonFormatter.format(self, record)
61
-
62
-
63
- class Log:
64
- """Base logging object."""
65
-
66
- def __init__(
67
- self, level: int = logging.WARNING, name: str = "dockertidy", json: bool = False
68
- ) -> None:
69
- self.logger = logging.getLogger(name)
70
- self.logger.setLevel(level)
71
- self.logger.addHandler(self._get_error_handler(json=json))
72
- self.logger.addHandler(self._get_warning_handler(json=json))
73
- self.logger.addHandler(self._get_info_handler(json=json))
74
- self.logger.addHandler(self._get_critical_handler(json=json))
75
- self.logger.addHandler(self._get_debug_handler(json=json))
76
- self.logger.propagate = False
77
-
78
- def _get_error_handler(self, json: bool = False) -> logging.Handler:
79
- handler = logging.StreamHandler(sys.stderr)
80
- handler.setLevel(logging.ERROR)
81
- handler.addFilter(LogFilter(logging.ERROR))
82
- handler.setFormatter(
83
- MultilineFormatter(
84
- self.error(CONSOLE_FORMAT.format(colorama.Fore.RED, colorama.Style.RESET_ALL))
85
- )
86
- )
87
-
88
- if json:
89
- handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
90
-
91
- return handler
92
-
93
- def _get_warning_handler(self, json: bool = False) -> logging.Handler:
94
- handler = logging.StreamHandler(sys.stdout)
95
- handler.setLevel(logging.WARNING)
96
- handler.addFilter(LogFilter(logging.WARNING))
97
- handler.setFormatter(
98
- MultilineFormatter(
99
- self.warning(CONSOLE_FORMAT.format(colorama.Fore.YELLOW, colorama.Style.RESET_ALL))
100
- )
101
- )
102
-
103
- if json:
104
- handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
105
-
106
- return handler
107
-
108
- def _get_info_handler(self, json: bool = False) -> logging.Handler:
109
- handler = logging.StreamHandler(sys.stdout)
110
- handler.setLevel(logging.INFO)
111
- handler.addFilter(LogFilter(logging.INFO))
112
- handler.setFormatter(
113
- MultilineFormatter(
114
- self.info(CONSOLE_FORMAT.format(colorama.Fore.CYAN, colorama.Style.RESET_ALL))
115
- )
116
- )
117
-
118
- if json:
119
- handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
120
-
121
- return handler
122
-
123
- def _get_critical_handler(self, json: bool = False) -> logging.Handler:
124
- handler = logging.StreamHandler(sys.stderr)
125
- handler.setLevel(logging.CRITICAL)
126
- handler.addFilter(LogFilter(logging.CRITICAL))
127
- handler.setFormatter(
128
- MultilineFormatter(
129
- self.critical(CONSOLE_FORMAT.format(colorama.Fore.RED, colorama.Style.RESET_ALL))
130
- )
131
- )
132
-
133
- if json:
134
- handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
135
-
136
- return handler
137
-
138
- def _get_debug_handler(self, json: bool = False) -> logging.Handler:
139
- handler = logging.StreamHandler(sys.stderr)
140
- handler.setLevel(logging.DEBUG)
141
- handler.addFilter(LogFilter(logging.DEBUG))
142
- handler.setFormatter(
143
- MultilineFormatter(
144
- self.critical(CONSOLE_FORMAT.format(colorama.Fore.BLUE, colorama.Style.RESET_ALL))
145
- )
146
- )
147
-
148
- if json:
149
- handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
150
-
151
- return handler
152
-
153
- def set_level(self, s: str | int) -> None:
154
- """Set log level."""
155
- self.logger.setLevel(s)
156
-
157
- def debug(self, msg: str) -> str:
158
- """Format info messages and return string."""
159
- return msg
160
-
161
- def critical(self, msg: str) -> str:
162
- """Format critical messages and return string."""
163
- return msg
164
-
165
- def error(self, msg: str) -> str:
166
- """Format error messages and return string."""
167
- return msg
168
-
169
- def warning(self, msg: str) -> str:
170
- """Format warning messages and return string."""
171
- return msg
172
-
173
- def info(self, msg: str) -> str:
174
- """Format info messages and return string."""
175
- return msg
176
-
177
- def _color_text(self, color: str, msg: str) -> str:
178
- """
179
- Colorize strings.
180
-
181
- :param color: colorama color settings
182
- :param msg: string to colorize
183
- :returns: string
184
-
185
- """
186
- return f"{color}{msg}{colorama.Style.RESET_ALL}"
187
-
188
- def sysexit(self, code: int = 1) -> NoReturn:
189
- """Exit running program with given exit code."""
190
- sys.exit(code)
191
-
192
- def sysexit_with_message(self, msg: Any, code: int = 1) -> NoReturn:
193
- """Exit running program with given exit code and message."""
194
- self.logger.critical(str(msg))
195
- self.sysexit(code)
196
-
197
-
198
- class SingleLog(Log, metaclass=Singleton):
199
- """Singleton logger object."""
200
-
201
- pass
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Global utility methods and classes."""
3
-
4
- from typing import Any
5
-
6
-
7
- def strtobool(value: str) -> bool:
8
- """Convert a string representation of truth to true or false."""
9
-
10
- _map = {
11
- "y": True,
12
- "yes": True,
13
- "t": True,
14
- "true": True,
15
- "on": True,
16
- "1": True,
17
- "n": False,
18
- "no": False,
19
- "f": False,
20
- "false": False,
21
- "off": False,
22
- "0": False,
23
- }
24
-
25
- try:
26
- return _map[str(value).lower()]
27
- except KeyError as err:
28
- raise ValueError(f'"{value}" is not a valid bool value') from err
29
-
30
-
31
- def to_bool(string: str | bool) -> bool:
32
- return bool(strtobool(str(string)))
33
-
34
-
35
- def dict_intersect(d1: dict[str, Any], d2: dict[str, Any]) -> dict[str, Any]:
36
- return {
37
- k: dict_intersect(d1[k], d2[k]) if isinstance(d1[k], dict) else d1[k]
38
- for k in d1.keys() & d2.keys()
39
- }
40
-
41
-
42
- class Singleton(type):
43
- """Singleton metaclass."""
44
-
45
- _instances: dict[Any, Any] = {}
46
-
47
- def __call__(cls, *args: Any, **kwargs: Any) -> Any:
48
- if cls not in cls._instances:
49
- cls._instances[cls] = super().__call__(*args, **kwargs)
50
- return cls._instances[cls]
File without changes
File without changes