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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. 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
+ ]