provide-foundation 0.0.0.dev0__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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
"""File operations with safety, atomicity, and format support.
|
2
|
+
|
3
|
+
This module provides comprehensive file operations including:
|
4
|
+
- Atomic writes to prevent corruption
|
5
|
+
- Safe operations with error handling
|
6
|
+
- Directory management utilities
|
7
|
+
- Format-specific helpers for JSON, YAML, TOML
|
8
|
+
- File locking for concurrent access
|
9
|
+
- Various utility functions
|
10
|
+
"""
|
11
|
+
|
12
|
+
from provide.foundation.file.atomic import (
|
13
|
+
atomic_replace,
|
14
|
+
atomic_write,
|
15
|
+
atomic_write_text,
|
16
|
+
)
|
17
|
+
from provide.foundation.file.directory import (
|
18
|
+
ensure_dir,
|
19
|
+
ensure_parent_dir,
|
20
|
+
safe_rmtree,
|
21
|
+
temp_dir,
|
22
|
+
)
|
23
|
+
from provide.foundation.file.formats import (
|
24
|
+
read_json,
|
25
|
+
read_toml,
|
26
|
+
read_yaml,
|
27
|
+
write_json,
|
28
|
+
write_toml,
|
29
|
+
write_yaml,
|
30
|
+
)
|
31
|
+
from provide.foundation.file.lock import FileLock, LockError
|
32
|
+
from provide.foundation.file.safe import (
|
33
|
+
safe_copy,
|
34
|
+
safe_delete,
|
35
|
+
safe_move,
|
36
|
+
safe_read,
|
37
|
+
safe_read_text,
|
38
|
+
)
|
39
|
+
from provide.foundation.file.utils import (
|
40
|
+
backup_file,
|
41
|
+
find_files,
|
42
|
+
get_mtime,
|
43
|
+
get_size,
|
44
|
+
touch,
|
45
|
+
)
|
46
|
+
|
47
|
+
__all__ = [
|
48
|
+
# From lock
|
49
|
+
"FileLock",
|
50
|
+
"LockError",
|
51
|
+
"atomic_replace",
|
52
|
+
# From atomic
|
53
|
+
"atomic_write",
|
54
|
+
"atomic_write_text",
|
55
|
+
"backup_file",
|
56
|
+
# From directory
|
57
|
+
"ensure_dir",
|
58
|
+
"ensure_parent_dir",
|
59
|
+
"find_files",
|
60
|
+
"get_mtime",
|
61
|
+
# From utils
|
62
|
+
"get_size",
|
63
|
+
# From formats
|
64
|
+
"read_json",
|
65
|
+
"read_toml",
|
66
|
+
"read_yaml",
|
67
|
+
"safe_copy",
|
68
|
+
"safe_delete",
|
69
|
+
"safe_move",
|
70
|
+
# From safe
|
71
|
+
"safe_read",
|
72
|
+
"safe_read_text",
|
73
|
+
"safe_rmtree",
|
74
|
+
"temp_dir",
|
75
|
+
"touch",
|
76
|
+
"write_json",
|
77
|
+
"write_toml",
|
78
|
+
"write_yaml",
|
79
|
+
]
|
@@ -0,0 +1,157 @@
|
|
1
|
+
"""Atomic file operations using temp file + rename pattern."""
|
2
|
+
|
3
|
+
import contextlib
|
4
|
+
import os
|
5
|
+
from pathlib import Path
|
6
|
+
import tempfile
|
7
|
+
|
8
|
+
from provide.foundation.logger import get_logger
|
9
|
+
|
10
|
+
log = get_logger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
def atomic_write(
|
14
|
+
path: Path | str,
|
15
|
+
data: bytes,
|
16
|
+
mode: int | None = None,
|
17
|
+
backup: bool = False,
|
18
|
+
preserve_mode: bool = True,
|
19
|
+
) -> None:
|
20
|
+
"""Write file atomically using temp file + rename.
|
21
|
+
|
22
|
+
This ensures that the file is either fully written or not written at all,
|
23
|
+
preventing partial writes or corruption.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
path: Target file path
|
27
|
+
data: Binary data to write
|
28
|
+
mode: Optional file permissions (e.g., 0o644)
|
29
|
+
backup: Create .bak file before overwrite
|
30
|
+
preserve_mode: Whether to preserve existing file permissions when mode is None
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
OSError: If file operation fails
|
34
|
+
"""
|
35
|
+
path = Path(path)
|
36
|
+
|
37
|
+
# Create backup if requested and file exists
|
38
|
+
if backup and path.exists():
|
39
|
+
backup_path = path.with_suffix(path.suffix + ".bak")
|
40
|
+
try:
|
41
|
+
path.rename(backup_path)
|
42
|
+
log.debug("Created backup", backup=str(backup_path))
|
43
|
+
except OSError as e:
|
44
|
+
log.warning("Failed to create backup", error=str(e))
|
45
|
+
|
46
|
+
# Ensure parent directory exists
|
47
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
48
|
+
|
49
|
+
# Create temp file in same directory for atomic rename
|
50
|
+
# Note: mkstemp creates files with 0o600 by default for security
|
51
|
+
fd, temp_path = tempfile.mkstemp(
|
52
|
+
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
|
53
|
+
)
|
54
|
+
|
55
|
+
try:
|
56
|
+
with os.fdopen(fd, "wb") as f:
|
57
|
+
f.write(data)
|
58
|
+
f.flush()
|
59
|
+
os.fsync(f.fileno())
|
60
|
+
|
61
|
+
# Set permissions if specified
|
62
|
+
if mode is not None:
|
63
|
+
os.chmod(temp_path, mode)
|
64
|
+
elif preserve_mode and path.exists():
|
65
|
+
# Preserve existing permissions if requested
|
66
|
+
try:
|
67
|
+
existing_mode = path.stat().st_mode
|
68
|
+
os.chmod(temp_path, existing_mode)
|
69
|
+
except OSError:
|
70
|
+
pass
|
71
|
+
elif not preserve_mode:
|
72
|
+
# When not preserving, set to standard default permissions
|
73
|
+
# mkstemp creates with 0o600, but we want standard defaults
|
74
|
+
# Apply umask manually since os.chmod doesn't respect it
|
75
|
+
default_mode = 0o666
|
76
|
+
current_umask = os.umask(0) # Get current umask
|
77
|
+
os.umask(current_umask) # Restore it
|
78
|
+
os.chmod(temp_path, default_mode & ~current_umask)
|
79
|
+
|
80
|
+
# Atomic rename
|
81
|
+
os.replace(temp_path, path)
|
82
|
+
|
83
|
+
log.debug(
|
84
|
+
"Atomically wrote file",
|
85
|
+
path=str(path),
|
86
|
+
size=len(data),
|
87
|
+
mode=oct(mode) if mode else None,
|
88
|
+
)
|
89
|
+
except Exception:
|
90
|
+
# Clean up temp file on error
|
91
|
+
with contextlib.suppress(OSError):
|
92
|
+
os.unlink(temp_path)
|
93
|
+
raise
|
94
|
+
|
95
|
+
|
96
|
+
def atomic_write_text(
|
97
|
+
path: Path | str,
|
98
|
+
text: str,
|
99
|
+
encoding: str = "utf-8",
|
100
|
+
mode: int | None = None,
|
101
|
+
backup: bool = False,
|
102
|
+
preserve_mode: bool = True,
|
103
|
+
) -> None:
|
104
|
+
"""Write text file atomically.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
path: Target file path
|
108
|
+
text: Text content to write
|
109
|
+
encoding: Text encoding (default: utf-8)
|
110
|
+
mode: Optional file permissions
|
111
|
+
backup: Create .bak file before overwrite
|
112
|
+
preserve_mode: Whether to preserve existing file permissions when mode is None
|
113
|
+
|
114
|
+
Raises:
|
115
|
+
OSError: If file operation fails
|
116
|
+
UnicodeEncodeError: If text cannot be encoded
|
117
|
+
"""
|
118
|
+
data = text.encode(encoding)
|
119
|
+
atomic_write(path, data, mode=mode, backup=backup, preserve_mode=preserve_mode)
|
120
|
+
|
121
|
+
|
122
|
+
def atomic_replace(
|
123
|
+
path: Path | str,
|
124
|
+
data: bytes,
|
125
|
+
preserve_mode: bool = True,
|
126
|
+
) -> None:
|
127
|
+
"""Replace existing file atomically, preserving permissions.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
path: Target file path (must exist)
|
131
|
+
data: Binary data to write
|
132
|
+
preserve_mode: Whether to preserve file permissions
|
133
|
+
|
134
|
+
Raises:
|
135
|
+
FileNotFoundError: If file doesn't exist
|
136
|
+
OSError: If file operation fails
|
137
|
+
"""
|
138
|
+
path = Path(path)
|
139
|
+
|
140
|
+
if not path.exists():
|
141
|
+
raise FileNotFoundError(f"File does not exist: {path}")
|
142
|
+
|
143
|
+
mode = None
|
144
|
+
if preserve_mode:
|
145
|
+
with contextlib.suppress(OSError):
|
146
|
+
mode = path.stat().st_mode
|
147
|
+
|
148
|
+
# When preserve_mode is False, we explicitly pass preserve_mode=False to atomic_write
|
149
|
+
# and let it handle the non-preservation (atomic_write won't preserve even if file exists)
|
150
|
+
atomic_write(path, data, mode=mode, backup=False, preserve_mode=preserve_mode)
|
151
|
+
|
152
|
+
|
153
|
+
__all__ = [
|
154
|
+
"atomic_replace",
|
155
|
+
"atomic_write",
|
156
|
+
"atomic_write_text",
|
157
|
+
]
|
@@ -0,0 +1,134 @@
|
|
1
|
+
"""Directory operations and utilities."""
|
2
|
+
|
3
|
+
from contextlib import contextmanager
|
4
|
+
from typing import Generator
|
5
|
+
from pathlib import Path
|
6
|
+
import shutil
|
7
|
+
import tempfile
|
8
|
+
|
9
|
+
from provide.foundation.logger import get_logger
|
10
|
+
|
11
|
+
log = get_logger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
def ensure_dir(
|
15
|
+
path: Path | str,
|
16
|
+
mode: int = 0o755,
|
17
|
+
parents: bool = True,
|
18
|
+
) -> Path:
|
19
|
+
"""Ensure directory exists with proper permissions.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
path: Directory path
|
23
|
+
mode: Directory permissions
|
24
|
+
parents: Create parent directories if needed
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Path object for the directory
|
28
|
+
"""
|
29
|
+
path = Path(path)
|
30
|
+
|
31
|
+
if not path.exists():
|
32
|
+
path.mkdir(mode=mode, parents=parents, exist_ok=True)
|
33
|
+
log.debug("Created directory", path=str(path), mode=oct(mode))
|
34
|
+
elif not path.is_dir():
|
35
|
+
raise NotADirectoryError(f"Path exists but is not a directory: {path}")
|
36
|
+
|
37
|
+
return path
|
38
|
+
|
39
|
+
|
40
|
+
def ensure_parent_dir(
|
41
|
+
file_path: Path | str,
|
42
|
+
mode: int = 0o755,
|
43
|
+
) -> Path:
|
44
|
+
"""Ensure parent directory of file exists.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
file_path: File path whose parent to ensure
|
48
|
+
mode: Directory permissions
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Path object for the parent directory
|
52
|
+
"""
|
53
|
+
file_path = Path(file_path)
|
54
|
+
parent = file_path.parent
|
55
|
+
|
56
|
+
if parent and parent != Path():
|
57
|
+
return ensure_dir(parent, mode=mode, parents=True)
|
58
|
+
|
59
|
+
return parent
|
60
|
+
|
61
|
+
|
62
|
+
@contextmanager
|
63
|
+
def temp_dir(
|
64
|
+
prefix: str = "provide_",
|
65
|
+
cleanup: bool = True,
|
66
|
+
) -> Generator[Path, None, None]:
|
67
|
+
"""Create temporary directory with automatic cleanup.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
prefix: Directory name prefix
|
71
|
+
cleanup: Whether to remove directory on exit
|
72
|
+
|
73
|
+
Yields:
|
74
|
+
Path object for the temporary directory
|
75
|
+
"""
|
76
|
+
temp_path = None
|
77
|
+
try:
|
78
|
+
temp_path = Path(tempfile.mkdtemp(prefix=prefix))
|
79
|
+
log.debug("Created temp directory", path=str(temp_path))
|
80
|
+
yield temp_path
|
81
|
+
finally:
|
82
|
+
if cleanup and temp_path and temp_path.exists():
|
83
|
+
try:
|
84
|
+
shutil.rmtree(temp_path)
|
85
|
+
log.debug("Cleaned up temp directory", path=str(temp_path))
|
86
|
+
except Exception as e:
|
87
|
+
log.warning(
|
88
|
+
"Failed to cleanup temp directory",
|
89
|
+
path=str(temp_path),
|
90
|
+
error=str(e),
|
91
|
+
)
|
92
|
+
|
93
|
+
|
94
|
+
def safe_rmtree(
|
95
|
+
path: Path | str,
|
96
|
+
missing_ok: bool = True,
|
97
|
+
) -> bool:
|
98
|
+
"""Remove directory tree safely.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
path: Directory to remove
|
102
|
+
missing_ok: If True, don't raise error if doesn't exist
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
True if removed, False if didn't exist
|
106
|
+
|
107
|
+
Raises:
|
108
|
+
OSError: If removal fails and directory exists
|
109
|
+
"""
|
110
|
+
path = Path(path)
|
111
|
+
|
112
|
+
try:
|
113
|
+
if path.exists():
|
114
|
+
shutil.rmtree(path)
|
115
|
+
log.debug("Removed directory tree", path=str(path))
|
116
|
+
return True
|
117
|
+
elif missing_ok:
|
118
|
+
log.debug("Directory already absent", path=str(path))
|
119
|
+
return False
|
120
|
+
else:
|
121
|
+
raise FileNotFoundError(f"Directory does not exist: {path}")
|
122
|
+
except Exception as e:
|
123
|
+
if not path.exists() and missing_ok:
|
124
|
+
return False
|
125
|
+
log.error("Failed to remove directory tree", path=str(path), error=str(e))
|
126
|
+
raise
|
127
|
+
|
128
|
+
|
129
|
+
__all__ = [
|
130
|
+
"ensure_dir",
|
131
|
+
"ensure_parent_dir",
|
132
|
+
"safe_rmtree",
|
133
|
+
"temp_dir",
|
134
|
+
]
|
@@ -0,0 +1,236 @@
|
|
1
|
+
"""Format-specific file operations for JSON, YAML, TOML."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from provide.foundation.file.atomic import atomic_write_text
|
8
|
+
from provide.foundation.file.safe import safe_read_text
|
9
|
+
from provide.foundation.logger import get_logger
|
10
|
+
|
11
|
+
log = get_logger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
def read_json(
|
15
|
+
path: Path | str,
|
16
|
+
default: Any = None,
|
17
|
+
encoding: str = "utf-8",
|
18
|
+
) -> Any:
|
19
|
+
"""Read JSON file with error handling.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
path: JSON file path
|
23
|
+
default: Default value if file doesn't exist or is invalid
|
24
|
+
encoding: Text encoding
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Parsed JSON data or default value
|
28
|
+
"""
|
29
|
+
content = safe_read_text(path, default="", encoding=encoding)
|
30
|
+
|
31
|
+
if not content:
|
32
|
+
log.debug("Empty or missing JSON file, returning default", path=str(path))
|
33
|
+
return default
|
34
|
+
|
35
|
+
try:
|
36
|
+
return json.loads(content)
|
37
|
+
except json.JSONDecodeError as e:
|
38
|
+
log.warning("Invalid JSON file", path=str(path), error=str(e))
|
39
|
+
return default
|
40
|
+
|
41
|
+
|
42
|
+
def write_json(
|
43
|
+
path: Path | str,
|
44
|
+
data: Any,
|
45
|
+
indent: int = 2,
|
46
|
+
sort_keys: bool = False,
|
47
|
+
atomic: bool = True,
|
48
|
+
encoding: str = "utf-8",
|
49
|
+
) -> None:
|
50
|
+
"""Write JSON file, optionally atomic.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
path: JSON file path
|
54
|
+
data: Data to serialize
|
55
|
+
indent: Indentation level (None for compact)
|
56
|
+
sort_keys: Whether to sort dictionary keys
|
57
|
+
atomic: Use atomic write
|
58
|
+
encoding: Text encoding
|
59
|
+
"""
|
60
|
+
path = Path(path)
|
61
|
+
|
62
|
+
try:
|
63
|
+
content = json.dumps(
|
64
|
+
data, indent=indent, sort_keys=sort_keys, ensure_ascii=False
|
65
|
+
)
|
66
|
+
|
67
|
+
if atomic:
|
68
|
+
atomic_write_text(path, content, encoding=encoding)
|
69
|
+
else:
|
70
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
71
|
+
path.write_text(content, encoding=encoding)
|
72
|
+
|
73
|
+
log.debug("Wrote JSON file", path=str(path), atomic=atomic)
|
74
|
+
except Exception as e:
|
75
|
+
log.error("Failed to write JSON file", path=str(path), error=str(e))
|
76
|
+
raise
|
77
|
+
|
78
|
+
|
79
|
+
def read_yaml(
|
80
|
+
path: Path | str,
|
81
|
+
default: Any = None,
|
82
|
+
encoding: str = "utf-8",
|
83
|
+
) -> Any:
|
84
|
+
"""Read YAML file with error handling.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
path: YAML file path
|
88
|
+
default: Default value if file doesn't exist or is invalid
|
89
|
+
encoding: Text encoding
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Parsed YAML data or default value
|
93
|
+
"""
|
94
|
+
try:
|
95
|
+
import yaml
|
96
|
+
except ImportError:
|
97
|
+
log.warning("PyYAML not installed, returning default")
|
98
|
+
return default
|
99
|
+
|
100
|
+
content = safe_read_text(path, default="", encoding=encoding)
|
101
|
+
|
102
|
+
if not content:
|
103
|
+
log.debug("Empty or missing YAML file, returning default", path=str(path))
|
104
|
+
return default
|
105
|
+
|
106
|
+
try:
|
107
|
+
return yaml.safe_load(content)
|
108
|
+
except yaml.YAMLError as e:
|
109
|
+
log.warning("Invalid YAML file", path=str(path), error=str(e))
|
110
|
+
return default
|
111
|
+
|
112
|
+
|
113
|
+
def write_yaml(
|
114
|
+
path: Path | str,
|
115
|
+
data: Any,
|
116
|
+
atomic: bool = True,
|
117
|
+
encoding: str = "utf-8",
|
118
|
+
default_flow_style: bool = False,
|
119
|
+
) -> None:
|
120
|
+
"""Write YAML file, optionally atomic.
|
121
|
+
|
122
|
+
Args:
|
123
|
+
path: YAML file path
|
124
|
+
data: Data to serialize
|
125
|
+
atomic: Use atomic write
|
126
|
+
encoding: Text encoding
|
127
|
+
default_flow_style: Use flow style (JSON-like) instead of block style
|
128
|
+
"""
|
129
|
+
try:
|
130
|
+
import yaml
|
131
|
+
except ImportError:
|
132
|
+
raise ImportError("PyYAML is required for YAML operations")
|
133
|
+
|
134
|
+
path = Path(path)
|
135
|
+
|
136
|
+
try:
|
137
|
+
content = yaml.dump(
|
138
|
+
data,
|
139
|
+
default_flow_style=default_flow_style,
|
140
|
+
allow_unicode=True,
|
141
|
+
sort_keys=False,
|
142
|
+
)
|
143
|
+
|
144
|
+
if atomic:
|
145
|
+
atomic_write_text(path, content, encoding=encoding)
|
146
|
+
else:
|
147
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
148
|
+
path.write_text(content, encoding=encoding)
|
149
|
+
|
150
|
+
log.debug("Wrote YAML file", path=str(path), atomic=atomic)
|
151
|
+
except Exception as e:
|
152
|
+
log.error("Failed to write YAML file", path=str(path), error=str(e))
|
153
|
+
raise
|
154
|
+
|
155
|
+
|
156
|
+
def read_toml(
|
157
|
+
path: Path | str,
|
158
|
+
default: Any = None,
|
159
|
+
encoding: str = "utf-8",
|
160
|
+
) -> dict[str, Any]:
|
161
|
+
"""Read TOML file with error handling.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
path: TOML file path
|
165
|
+
default: Default value if file doesn't exist or is invalid
|
166
|
+
encoding: Text encoding
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
Parsed TOML data or default value
|
170
|
+
"""
|
171
|
+
try:
|
172
|
+
import tomllib
|
173
|
+
except ImportError:
|
174
|
+
try:
|
175
|
+
import tomli as tomllib
|
176
|
+
except ImportError:
|
177
|
+
log.warning("tomllib/tomli not available, returning default")
|
178
|
+
return default if default is not None else {}
|
179
|
+
|
180
|
+
content = safe_read_text(path, default="", encoding=encoding)
|
181
|
+
|
182
|
+
if not content:
|
183
|
+
log.debug("Empty or missing TOML file, returning default", path=str(path))
|
184
|
+
return default if default is not None else {}
|
185
|
+
|
186
|
+
try:
|
187
|
+
return tomllib.loads(content)
|
188
|
+
except Exception as e:
|
189
|
+
log.warning("Invalid TOML file", path=str(path), error=str(e))
|
190
|
+
return default if default is not None else {}
|
191
|
+
|
192
|
+
|
193
|
+
def write_toml(
|
194
|
+
path: Path | str,
|
195
|
+
data: dict[str, Any],
|
196
|
+
atomic: bool = True,
|
197
|
+
encoding: str = "utf-8",
|
198
|
+
) -> None:
|
199
|
+
"""Write TOML file, optionally atomic.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
path: TOML file path
|
203
|
+
data: Data to serialize (must be a dictionary)
|
204
|
+
atomic: Use atomic write
|
205
|
+
encoding: Text encoding
|
206
|
+
"""
|
207
|
+
try:
|
208
|
+
import tomli_w
|
209
|
+
except ImportError:
|
210
|
+
raise ImportError("tomli-w is required for TOML write operations")
|
211
|
+
|
212
|
+
path = Path(path)
|
213
|
+
|
214
|
+
try:
|
215
|
+
content = tomli_w.dumps(data)
|
216
|
+
|
217
|
+
if atomic:
|
218
|
+
atomic_write_text(path, content, encoding=encoding)
|
219
|
+
else:
|
220
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
221
|
+
path.write_text(content, encoding=encoding)
|
222
|
+
|
223
|
+
log.debug("Wrote TOML file", path=str(path), atomic=atomic)
|
224
|
+
except Exception as e:
|
225
|
+
log.error("Failed to write TOML file", path=str(path), error=str(e))
|
226
|
+
raise
|
227
|
+
|
228
|
+
|
229
|
+
__all__ = [
|
230
|
+
"read_json",
|
231
|
+
"read_toml",
|
232
|
+
"read_yaml",
|
233
|
+
"write_json",
|
234
|
+
"write_toml",
|
235
|
+
"write_yaml",
|
236
|
+
]
|