mreg-cli 1.7.2__tar.gz → 1.7.3__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.
Files changed (99) hide show
  1. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/CHANGELOG.md +10 -0
  2. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/PKG-INFO +1 -1
  3. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/_version.py +3 -3
  4. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/logging.py +14 -12
  5. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/config.py +15 -4
  6. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/log.py +8 -1
  7. mreg_cli-1.7.3/mreg_cli/utilities/fs.py +86 -0
  8. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli.egg-info/PKG-INFO +1 -1
  9. mreg_cli-1.7.2/mreg_cli/utilities/fs.py +0 -57
  10. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/.github/workflows/publish.yml +0 -0
  11. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/.github/workflows/test.yml +0 -0
  12. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/.gitignore +0 -0
  13. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/.markdownlint.json +0 -0
  14. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/AUTHORS +0 -0
  15. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/CONTRIBUTING +0 -0
  16. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/LICENSE +0 -0
  17. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/README.md +0 -0
  18. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/ci/Dockerfile +0 -0
  19. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/ci/diff.py +0 -0
  20. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/ci/docker-compose.yml +0 -0
  21. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/ci/run_testsuite_and_record.sh +0 -0
  22. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/ci/run_testsuite_and_record_V2.sh +0 -0
  23. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/ci/testsuite +0 -0
  24. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/ci/testsuite-result.json +0 -0
  25. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/data/mreg-cli.conf +0 -0
  26. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/__about__.py +0 -0
  27. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/__init__.py +0 -0
  28. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/__main__.py +0 -0
  29. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/api/__init__.py +0 -0
  30. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/api/abstracts.py +0 -0
  31. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/api/endpoints.py +0 -0
  32. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/api/errors.py +0 -0
  33. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/api/fields.py +0 -0
  34. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/api/history.py +0 -0
  35. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/api/models.py +0 -0
  36. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/cache.py +0 -0
  37. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/cli.py +0 -0
  38. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/__init__.py +0 -0
  39. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/base.py +0 -0
  40. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/cache.py +0 -0
  41. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/dhcp.py +0 -0
  42. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/group.py +0 -0
  43. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/help.py +0 -0
  44. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/host.py +0 -0
  45. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/host_submodules/__init__.py +0 -0
  46. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/host_submodules/a_aaaa.py +0 -0
  47. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/host_submodules/bacnet.py +0 -0
  48. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/host_submodules/cname.py +0 -0
  49. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/host_submodules/core.py +0 -0
  50. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/host_submodules/rr.py +0 -0
  51. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/label.py +0 -0
  52. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/network.py +0 -0
  53. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/permission.py +0 -0
  54. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/policy.py +0 -0
  55. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/recording.py +0 -0
  56. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/registry.py +0 -0
  57. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/root.py +0 -0
  58. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/commands/zone.py +0 -0
  59. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/dirs.py +0 -0
  60. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/errorbuilder.py +0 -0
  61. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/exceptions.py +0 -0
  62. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/help_formatter.py +0 -0
  63. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/main.py +0 -0
  64. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/outputmanager.py +0 -0
  65. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/prompt.py +0 -0
  66. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/py.typed +0 -0
  67. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/tags.txt +0 -0
  68. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/tokenfile.py +0 -0
  69. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/types.py +0 -0
  70. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/utilities/__init__.py +0 -0
  71. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/utilities/api.py +0 -0
  72. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/utilities/shared.py +0 -0
  73. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli/utilities/validators.py +0 -0
  74. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli.egg-info/SOURCES.txt +0 -0
  75. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli.egg-info/dependency_links.txt +0 -0
  76. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli.egg-info/entry_points.txt +0 -0
  77. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli.egg-info/requires.txt +0 -0
  78. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/mreg_cli.egg-info/top_level.txt +0 -0
  79. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/pyproject.toml +0 -0
  80. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/run.py +0 -0
  81. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/setup.cfg +0 -0
  82. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/__init__.py +0 -0
  83. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/api/__init__.py +0 -0
  84. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/api/test_fields.py +0 -0
  85. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/api/test_models.py +0 -0
  86. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/conftest.py +0 -0
  87. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/test_cache.py +0 -0
  88. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/test_config.py +0 -0
  89. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/test_errorbuilder.py +0 -0
  90. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/test_exceptions.py +0 -0
  91. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/test_outputmanager.py +0 -0
  92. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/test_prompt.py +0 -0
  93. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/test_tokenfile.py +0 -0
  94. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/test_types.py +0 -0
  95. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/utilities/__init__.py +0 -0
  96. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/utilities/test_api.py +0 -0
  97. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tests/utilities/test_fs.py +0 -0
  98. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/tox.ini +0 -0
  99. {mreg_cli-1.7.2 → mreg_cli-1.7.3}/uv.lock +0 -0
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- ## Unreleased -->
9
9
 
10
+ ## [1.7.3](https://github.com/unioslo/mreg-cli/releases/tag/1.7.3) - 2025-11-18
11
+
12
+ ### Changed
13
+
14
+ - No longer requires a writable log file to start the application. In extreme cases where no writable log file directory can be found, logging will be disabled.
15
+
16
+ ### Fixed
17
+
18
+ - Temporary file creation failing due to non-unique filename in `/tmp` directory. Now creates a unique temporary directory for the temporary file.
19
+
10
20
  ## [1.7.2](https://github.com/unioslo/mreg-cli/releases/tag/1.7.2) - 2025-11-17
11
21
 
12
22
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mreg-cli
3
- Version: 1.7.2
3
+ Version: 1.7.3
4
4
  Summary: MREG interactive command line client
5
5
  Author: Others (see AUTHORS)
6
6
  Author-email: Øyvind Kolbu <oyvind.kolbu@usit.uio.no>, Safet A <amedov@usit.uio.no>, Magnus Hirth <mhmorgan42@gmail.com>, Nico <nicolaydmohebi@gmail.com>, Marius Bakke <marius.bakke@usit.uio.no>, Fredrik Larsen <fredrik.larsen@usit.uio.no>, Paal Braathen <paal.braathen@usit.uio.no>, Nils Hiorth <nils@nls.me>, Dmytro Karpenko <dmytrok@usit.uio.no>
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.7.2'
32
- __version_tuple__ = version_tuple = (1, 7, 2)
31
+ __version__ = version = '1.7.3'
32
+ __version_tuple__ = version_tuple = (1, 7, 3)
33
33
 
34
- __commit_id__ = commit_id = 'g0bceea78e'
34
+ __commit_id__ = commit_id = 'g9939333a3'
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ from pathlib import Path
6
7
  from typing import Any
7
8
 
8
9
  from mreg_cli.commands.base import BaseCommand
@@ -66,10 +67,9 @@ def start_logging(args: argparse.Namespace) -> None:
66
67
  status = log.status
67
68
  # Abort if already logging with same params
68
69
  if status.enabled and status.file == log_file and status.level == level:
69
- raise InputFailure(f"Logging already enabled: {level} > {log_file}")
70
-
70
+ raise InputFailure(f"Logging already enabled: {status.as_str()}")
71
71
  log.start_logging(log_file, level)
72
- OutputManager().add_line(f"Logging started: {level} > {log_file}")
72
+ OutputManager().add_line(f"Logging started: {status.as_str()}")
73
73
 
74
74
 
75
75
  @command_registry.register_command(
@@ -81,7 +81,7 @@ def stop_logging(_: argparse.Namespace):
81
81
  """Stop logging."""
82
82
  log = MregCliLogger()
83
83
  if not log.status.enabled:
84
- raise InputFailure("Logging is not enabled.")
84
+ raise InputFailure("Logging is already disabled.")
85
85
 
86
86
  log.stop_logging()
87
87
  OutputManager().add_line("Logging stopped.")
@@ -110,7 +110,7 @@ def set_logging_level(args: argparse.Namespace) -> None:
110
110
  raise InputFailure("Logging is not enabled, cannot set level.")
111
111
 
112
112
  # Determine logging params
113
- log_file = log.status.file or config.log_file # fallback should never be reachable...
113
+ log_file = log.status.file or config.log_file
114
114
  level = LogLevel(args.level)
115
115
  log.start_logging(log_file, level)
116
116
 
@@ -125,19 +125,21 @@ def logging_status(_: argparse.Namespace):
125
125
  conf = MregCliConfig()
126
126
 
127
127
  status = MregCliLogger().status
128
+ log_file = status.file or conf.log_file # fallback should never be reachable...
129
+
128
130
  if not status.enabled:
129
131
  raise InputFailure("Logging is disabled.")
130
-
131
- level = status.level
132
- file = status.file or conf.log_file # fallback should never be reachable...
132
+ elif not log_file:
133
+ OutputManager().add_line(status.as_str())
134
+ return
133
135
 
134
136
  lines_in_logfile = 0
135
137
  try:
136
- with open(file) as f:
138
+ with open(log_file) as f:
137
139
  lines_in_logfile = sum(1 for _ in f)
138
140
  except OSError as e:
139
- raise FileError(f"Unable to read log file {file}: {e}") from e
141
+ raise FileError(f"Unable to read log file {log_file}: {e}") from e
140
142
 
141
- filesize = sizeof_fmt(file.stat().st_size)
143
+ filesize = sizeof_fmt(log_file.stat().st_size)
142
144
 
143
- OutputManager().add_line(f"{level} > {file} ({lines_in_logfile} lines, {filesize})")
145
+ OutputManager().add_line(f"{status.as_str()} ({lines_in_logfile} lines, {filesize})")
@@ -157,7 +157,14 @@ class IniConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin):
157
157
  return f"{self.__class__.__name__}()"
158
158
 
159
159
 
160
- ResolvedPath = Annotated[Path, AfterValidator(to_path)]
160
+ def to_path_optional(value: Any) -> Path | None:
161
+ """Convert a value to a Path object with expanded user and resolved symlinks, or None."""
162
+ if value is None:
163
+ return None
164
+ return to_path(value)
165
+
166
+
167
+ ResolvedPath = Annotated[Path, AfterValidator(to_path_optional)]
161
168
  """Path type that is user expanded (~) and resolved (absolute+symlinks) after validation."""
162
169
 
163
170
 
@@ -179,7 +186,7 @@ class MregCliConfig(BaseSettings):
179
186
  token_only: bool = False
180
187
  source: ResolvedPath | None = None
181
188
  verbose: bool = False
182
- log_file: ResolvedPath = Field(
189
+ log_file: ResolvedPath | None = Field(
183
190
  LOG_FILE_DEFAULT,
184
191
  # New and old names both valid
185
192
  validation_alias=AliasChoices("logfile", "log_file"),
@@ -247,9 +254,13 @@ class MregCliConfig(BaseSettings):
247
254
  return []
248
255
 
249
256
  @field_validator("log_file", mode="after")
250
- def _ensure_log_file_writable(cls, v: Path) -> Path:
257
+ def _ensure_log_file_writable(cls, v: Path) -> Path | None:
251
258
  """Ensure we have a writable log file."""
252
- return get_writable_file_or_tempfile(v)
259
+ try:
260
+ return get_writable_file_or_tempfile(v)
261
+ except OSError as e:
262
+ logger.error(str(e))
263
+ return None
253
264
 
254
265
  @override
255
266
  @classmethod
@@ -23,6 +23,10 @@ class LoggingStatus(NamedTuple):
23
23
  file: Path | None
24
24
  level: LogLevel
25
25
 
26
+ def as_str(self) -> str:
27
+ """Get a string representation of the logging status."""
28
+ return f"{self.level} > {self.file or 'stderr'}" if self.enabled else "disabled"
29
+
26
30
 
27
31
  class MregCliLogger:
28
32
  """Singleton that manages logging state."""
@@ -44,7 +48,10 @@ class MregCliLogger:
44
48
  return LoggingStatus(self._is_logging, self._file, self._level)
45
49
 
46
50
  def start_logging(
47
- self, logfile: Path, level: LogLevel | str = LogLevel.INFO, fmt: str = LOGGING_FORMAT
51
+ self,
52
+ logfile: Path | None,
53
+ level: LogLevel | str = LogLevel.INFO,
54
+ fmt: str = LOGGING_FORMAT,
48
55
  ) -> None:
49
56
  """Start logging to the specified file. Disables logging if already enabled."""
50
57
  level = LogLevel(level)
@@ -0,0 +1,86 @@
1
+ """File system utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import functools
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from mreg_cli.exceptions import InputFailure
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def get_writable_file_or_tempfile(path: Path) -> Path:
17
+ """Ensure a writable file path exists, creating it if necessary, or fall back to a temporary file.
18
+
19
+ :param path: The desired file path.
20
+ :returns: The original path if writable, otherwise a temporary file path.
21
+ """
22
+ try:
23
+ path.parent.mkdir(parents=True, exist_ok=True)
24
+ if path.exists() and path.is_dir():
25
+ raise IsADirectoryError(f"Path {path} is a directory, not a file")
26
+ if path.exists():
27
+ check_writable(path)
28
+ else:
29
+ # Create new file if it doesn't exist (very defensively)
30
+ path.touch(exist_ok=True)
31
+ except OSError as e:
32
+ import time # noqa: PLC0415
33
+
34
+ logger.error(
35
+ "Unable to create file at %s: %s",
36
+ path,
37
+ e,
38
+ )
39
+
40
+ filename = path.name or f"mreg-cli-temp-{int(time.time())}"
41
+ # NOTE: Not really a temporary dir, since we never delete it!
42
+ tempdir = get_temp_dir()
43
+ tmpfile = tempdir / filename
44
+ try:
45
+ tmpfile.touch(exist_ok=True)
46
+ check_writable(tmpfile)
47
+ except Exception as tmp_err:
48
+ raise OSError(f"Unable to create temporary file at {tmpfile}: {tmp_err}") from tmp_err
49
+ logger.warning("Using temporary file at %s", tmpfile)
50
+ return tmpfile
51
+ return path
52
+
53
+
54
+ def check_writable(path: Path) -> None:
55
+ """Check if the given path is writable.
56
+
57
+ HACKY: Tests write access on file by opening in append mode.
58
+ """
59
+ # Check write privileges without overwriting existing content
60
+ with path.open("a"):
61
+ pass
62
+
63
+
64
+ @functools.cache
65
+ def get_temp_dir() -> Path:
66
+ """Get a temporary directory for use by the CLI.
67
+
68
+ Always returns the same directory for the lifetime of the process.
69
+ """
70
+ import tempfile # noqa: PLC0415
71
+
72
+ temp_dir = tempfile.mkdtemp(prefix="mreg-cli.", suffix="." + str(os.getuid()))
73
+ return Path(temp_dir)
74
+
75
+
76
+ def to_path(value: Any) -> Path:
77
+ """Convert a value to a Path object with expanded user and resolved symlinks."""
78
+ try:
79
+ p = Path(value)
80
+ try:
81
+ p = p.expanduser()
82
+ except RuntimeError: # no homedir
83
+ pass
84
+ return p.resolve()
85
+ except Exception as e:
86
+ raise InputFailure(f"Invalid path {value}: {e}") from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mreg-cli
3
- Version: 1.7.2
3
+ Version: 1.7.3
4
4
  Summary: MREG interactive command line client
5
5
  Author: Others (see AUTHORS)
6
6
  Author-email: Øyvind Kolbu <oyvind.kolbu@usit.uio.no>, Safet A <amedov@usit.uio.no>, Magnus Hirth <mhmorgan42@gmail.com>, Nico <nicolaydmohebi@gmail.com>, Marius Bakke <marius.bakke@usit.uio.no>, Fredrik Larsen <fredrik.larsen@usit.uio.no>, Paal Braathen <paal.braathen@usit.uio.no>, Nils Hiorth <nils@nls.me>, Dmytro Karpenko <dmytrok@usit.uio.no>
@@ -1,57 +0,0 @@
1
- """File system utilities."""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- from mreg_cli.exceptions import InputFailure
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- def get_writable_file_or_tempfile(path: Path) -> Path:
15
- """Ensure a file path exists and is writable, otherwise return a temporary file."""
16
- try:
17
- path.parent.mkdir(parents=True, exist_ok=True)
18
- if path.exists() and path.is_dir():
19
- raise IsADirectoryError(f"Path {path} is a directory, not a file")
20
- # Check write privileges without overwriting existing content
21
- if path.exists():
22
- # Test write access on existing file by opening in append mode
23
- with path.open("a"):
24
- pass
25
- else:
26
- # Create new file if it doesn't exist
27
- path.touch()
28
- except OSError as e:
29
- import tempfile # noqa: PLC0415 # only import if we really need it
30
- import time # noqa: PLC0415
31
-
32
- # NOTE: we might not be able to write to the temp file either
33
- # but in that case we are so far outside the normal
34
- # usage patterns that we should probably just fail
35
- tmpfile = Path(tempfile.gettempdir()) / path.name or f"mreg-cli-temp-{int(time.time())}"
36
- logger.error(
37
- "Unable to create file at %s due to: %s; falling back to temporary file at %s",
38
- path,
39
- e,
40
- tmpfile,
41
- )
42
- tmpfile.touch()
43
- return tmpfile
44
- return path
45
-
46
-
47
- def to_path(value: Any) -> Path:
48
- """Convert a value to a Path object with expanded user and resolved symlinks."""
49
- try:
50
- p = Path(value)
51
- try:
52
- p = p.expanduser()
53
- except RuntimeError: # no homedir
54
- pass
55
- return p.resolve()
56
- except Exception as e:
57
- raise InputFailure(f"Invalid path {value}: {e}") from e
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes