provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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 (163) hide show
  1. provide/foundation/__init__.py +36 -10
  2. provide/foundation/archive/__init__.py +1 -1
  3. provide/foundation/archive/base.py +15 -14
  4. provide/foundation/archive/bzip2.py +40 -40
  5. provide/foundation/archive/gzip.py +42 -42
  6. provide/foundation/archive/operations.py +93 -96
  7. provide/foundation/archive/tar.py +33 -31
  8. provide/foundation/archive/zip.py +52 -50
  9. provide/foundation/asynctools/__init__.py +20 -0
  10. provide/foundation/asynctools/core.py +126 -0
  11. provide/foundation/cli/__init__.py +2 -2
  12. provide/foundation/cli/commands/deps.py +15 -9
  13. provide/foundation/cli/commands/logs/__init__.py +3 -3
  14. provide/foundation/cli/commands/logs/generate.py +2 -2
  15. provide/foundation/cli/commands/logs/query.py +4 -4
  16. provide/foundation/cli/commands/logs/send.py +3 -3
  17. provide/foundation/cli/commands/logs/tail.py +3 -3
  18. provide/foundation/cli/decorators.py +11 -11
  19. provide/foundation/cli/main.py +1 -1
  20. provide/foundation/cli/testing.py +2 -40
  21. provide/foundation/cli/utils.py +21 -18
  22. provide/foundation/config/__init__.py +35 -2
  23. provide/foundation/config/base.py +2 -2
  24. provide/foundation/config/converters.py +477 -0
  25. provide/foundation/config/defaults.py +67 -0
  26. provide/foundation/config/env.py +6 -20
  27. provide/foundation/config/loader.py +10 -4
  28. provide/foundation/config/sync.py +8 -6
  29. provide/foundation/config/types.py +5 -5
  30. provide/foundation/config/validators.py +4 -4
  31. provide/foundation/console/input.py +5 -5
  32. provide/foundation/console/output.py +36 -14
  33. provide/foundation/context/__init__.py +8 -4
  34. provide/foundation/context/core.py +88 -110
  35. provide/foundation/crypto/certificates/__init__.py +9 -5
  36. provide/foundation/crypto/certificates/base.py +2 -2
  37. provide/foundation/crypto/certificates/certificate.py +48 -19
  38. provide/foundation/crypto/certificates/factory.py +26 -18
  39. provide/foundation/crypto/certificates/generator.py +24 -23
  40. provide/foundation/crypto/certificates/loader.py +24 -16
  41. provide/foundation/crypto/certificates/operations.py +17 -10
  42. provide/foundation/crypto/certificates/trust.py +21 -21
  43. provide/foundation/env/__init__.py +28 -0
  44. provide/foundation/env/core.py +218 -0
  45. provide/foundation/errors/__init__.py +3 -3
  46. provide/foundation/errors/decorators.py +0 -234
  47. provide/foundation/errors/types.py +0 -98
  48. provide/foundation/eventsets/display.py +13 -14
  49. provide/foundation/eventsets/registry.py +61 -31
  50. provide/foundation/eventsets/resolver.py +50 -46
  51. provide/foundation/eventsets/sets/das.py +8 -8
  52. provide/foundation/eventsets/sets/database.py +14 -14
  53. provide/foundation/eventsets/sets/http.py +21 -21
  54. provide/foundation/eventsets/sets/llm.py +16 -16
  55. provide/foundation/eventsets/sets/task_queue.py +13 -13
  56. provide/foundation/eventsets/types.py +7 -7
  57. provide/foundation/file/directory.py +14 -23
  58. provide/foundation/file/lock.py +4 -3
  59. provide/foundation/hub/components.py +75 -389
  60. provide/foundation/hub/config.py +157 -0
  61. provide/foundation/hub/discovery.py +63 -0
  62. provide/foundation/hub/handlers.py +89 -0
  63. provide/foundation/hub/lifecycle.py +195 -0
  64. provide/foundation/hub/manager.py +7 -4
  65. provide/foundation/hub/processors.py +49 -0
  66. provide/foundation/integrations/__init__.py +11 -0
  67. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  68. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
  70. provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
  71. provide/foundation/integrations/openobserve/config.py +37 -0
  72. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  73. provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
  74. provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
  75. provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
  76. provide/foundation/logger/__init__.py +0 -1
  77. provide/foundation/logger/config/base.py +1 -1
  78. provide/foundation/logger/config/logging.py +69 -299
  79. provide/foundation/logger/config/telemetry.py +39 -121
  80. provide/foundation/logger/factories.py +2 -2
  81. provide/foundation/logger/processors/main.py +12 -10
  82. provide/foundation/logger/ratelimit/limiters.py +4 -4
  83. provide/foundation/logger/ratelimit/processor.py +1 -1
  84. provide/foundation/logger/setup/coordinator.py +39 -25
  85. provide/foundation/logger/setup/processors.py +3 -3
  86. provide/foundation/logger/setup/testing.py +14 -0
  87. provide/foundation/logger/trace.py +5 -5
  88. provide/foundation/metrics/__init__.py +1 -1
  89. provide/foundation/metrics/otel.py +3 -1
  90. provide/foundation/observability/__init__.py +3 -3
  91. provide/foundation/process/__init__.py +9 -0
  92. provide/foundation/process/exit.py +48 -0
  93. provide/foundation/process/lifecycle.py +69 -46
  94. provide/foundation/resilience/__init__.py +36 -0
  95. provide/foundation/resilience/circuit.py +166 -0
  96. provide/foundation/resilience/decorators.py +236 -0
  97. provide/foundation/resilience/fallback.py +208 -0
  98. provide/foundation/resilience/retry.py +327 -0
  99. provide/foundation/serialization/__init__.py +16 -0
  100. provide/foundation/serialization/core.py +70 -0
  101. provide/foundation/streams/config.py +78 -0
  102. provide/foundation/streams/console.py +4 -5
  103. provide/foundation/streams/core.py +5 -2
  104. provide/foundation/streams/file.py +12 -2
  105. provide/foundation/testing/__init__.py +29 -9
  106. provide/foundation/testing/archive/__init__.py +7 -7
  107. provide/foundation/testing/archive/fixtures.py +58 -54
  108. provide/foundation/testing/cli.py +30 -20
  109. provide/foundation/testing/common/__init__.py +13 -15
  110. provide/foundation/testing/common/fixtures.py +27 -57
  111. provide/foundation/testing/file/__init__.py +15 -15
  112. provide/foundation/testing/file/content_fixtures.py +289 -0
  113. provide/foundation/testing/file/directory_fixtures.py +107 -0
  114. provide/foundation/testing/file/fixtures.py +42 -516
  115. provide/foundation/testing/file/special_fixtures.py +145 -0
  116. provide/foundation/testing/logger.py +89 -8
  117. provide/foundation/testing/mocking/__init__.py +21 -21
  118. provide/foundation/testing/mocking/fixtures.py +80 -67
  119. provide/foundation/testing/process/__init__.py +23 -23
  120. provide/foundation/testing/process/async_fixtures.py +414 -0
  121. provide/foundation/testing/process/fixtures.py +48 -571
  122. provide/foundation/testing/process/subprocess_fixtures.py +210 -0
  123. provide/foundation/testing/threading/__init__.py +17 -17
  124. provide/foundation/testing/threading/basic_fixtures.py +105 -0
  125. provide/foundation/testing/threading/data_fixtures.py +101 -0
  126. provide/foundation/testing/threading/execution_fixtures.py +278 -0
  127. provide/foundation/testing/threading/fixtures.py +32 -502
  128. provide/foundation/testing/threading/sync_fixtures.py +100 -0
  129. provide/foundation/testing/time/__init__.py +11 -11
  130. provide/foundation/testing/time/fixtures.py +95 -83
  131. provide/foundation/testing/transport/__init__.py +9 -9
  132. provide/foundation/testing/transport/fixtures.py +54 -54
  133. provide/foundation/time/__init__.py +18 -0
  134. provide/foundation/time/core.py +63 -0
  135. provide/foundation/tools/__init__.py +2 -2
  136. provide/foundation/tools/base.py +68 -67
  137. provide/foundation/tools/cache.py +69 -74
  138. provide/foundation/tools/downloader.py +68 -62
  139. provide/foundation/tools/installer.py +51 -57
  140. provide/foundation/tools/registry.py +38 -45
  141. provide/foundation/tools/resolver.py +70 -68
  142. provide/foundation/tools/verifier.py +39 -50
  143. provide/foundation/tracer/spans.py +2 -14
  144. provide/foundation/transport/__init__.py +26 -33
  145. provide/foundation/transport/base.py +32 -30
  146. provide/foundation/transport/client.py +44 -49
  147. provide/foundation/transport/config.py +36 -107
  148. provide/foundation/transport/errors.py +13 -27
  149. provide/foundation/transport/http.py +69 -55
  150. provide/foundation/transport/middleware.py +113 -114
  151. provide/foundation/transport/registry.py +29 -27
  152. provide/foundation/transport/types.py +6 -6
  153. provide/foundation/utils/deps.py +17 -14
  154. provide/foundation/utils/parsing.py +49 -4
  155. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
  156. provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
  157. provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
  158. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  159. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  160. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
  161. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
  162. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
  163. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,17 @@
1
1
  """Archive operation chains and helpers."""
2
2
 
3
- import tempfile
4
3
  from pathlib import Path
5
- from typing import Callable, Optional
4
+ import tempfile
6
5
 
7
6
  from attrs import define, field
8
7
 
9
8
  from provide.foundation.archive.base import ArchiveError
10
- from provide.foundation.archive.tar import TarArchive
11
- from provide.foundation.archive.gzip import GzipCompressor
12
9
  from provide.foundation.archive.bzip2 import Bzip2Compressor
10
+ from provide.foundation.archive.gzip import GzipCompressor
11
+ from provide.foundation.archive.tar import TarArchive
13
12
  from provide.foundation.archive.zip import ZipArchive
14
13
  from provide.foundation.file import ensure_parent_dir
14
+ from provide.foundation.file.safe import safe_delete
15
15
  from provide.foundation.logger import get_logger
16
16
 
17
17
  logger = get_logger(__name__)
@@ -21,30 +21,30 @@ logger = get_logger(__name__)
21
21
  class OperationChain:
22
22
  """
23
23
  Chain multiple archive operations together.
24
-
24
+
25
25
  Enables complex operations like tar.gz, tar.bz2, etc.
26
26
  Operations are executed in order for creation, reversed for extraction.
27
27
  """
28
-
28
+
29
29
  operations: list[str] = field(factory=list)
30
-
30
+
31
31
  def execute(self, source: Path, output: Path) -> Path:
32
32
  """
33
33
  Execute operation chain on source.
34
-
34
+
35
35
  Args:
36
36
  source: Source file or directory
37
37
  output: Final output path
38
-
38
+
39
39
  Returns:
40
40
  Path to final output
41
-
41
+
42
42
  Raises:
43
43
  ArchiveError: If any operation fails
44
44
  """
45
45
  current = source
46
46
  temp_files = []
47
-
47
+
48
48
  try:
49
49
  for i, op in enumerate(self.operations):
50
50
  # Determine output for this operation
@@ -58,57 +58,54 @@ class OperationChain:
58
58
  temp.close()
59
59
  next_output = Path(temp.name)
60
60
  temp_files.append(next_output)
61
-
61
+
62
62
  # Execute operation
63
63
  current = self._execute_operation(op, current, next_output)
64
64
  logger.debug(f"Executed operation '{op}': {current}")
65
-
65
+
66
66
  return current
67
-
67
+
68
68
  except Exception as e:
69
69
  raise ArchiveError(f"Operation chain failed: {e}") from e
70
70
  finally:
71
- # Clean up temp files
71
+ # Clean up temp files using Foundation's safe file operations
72
72
  for temp in temp_files:
73
- try:
74
- temp.unlink()
75
- except Exception:
76
- pass
77
-
73
+ safe_delete(temp, missing_ok=True)
74
+
78
75
  def reverse(self, source: Path, output: Path) -> Path:
79
76
  """
80
77
  Reverse operation chain (extract/decompress).
81
-
78
+
82
79
  Args:
83
80
  source: Source archive
84
81
  output: Final output path
85
-
82
+
86
83
  Returns:
87
84
  Path to final output
88
-
85
+
89
86
  Raises:
90
87
  ArchiveError: If any operation fails
91
88
  """
92
89
  # Reverse the operations and invert them
93
90
  reverse_map = {
94
- 'tar': 'untar',
95
- 'untar': 'tar',
96
- 'gzip': 'gunzip',
97
- 'gunzip': 'gzip',
98
- 'bzip2': 'bunzip2',
99
- 'bunzip2': 'bzip2',
100
- 'zip': 'unzip',
101
- 'unzip': 'zip',
91
+ "tar": "untar",
92
+ "untar": "tar",
93
+ "gzip": "gunzip",
94
+ "gunzip": "gzip",
95
+ "bzip2": "bunzip2",
96
+ "bunzip2": "bzip2",
97
+ "zip": "unzip",
98
+ "unzip": "zip",
102
99
  }
103
-
100
+
104
101
  reversed_ops = []
105
102
  for op in reversed(self.operations):
106
103
  reversed_op = reverse_map.get(op.lower(), op)
107
104
  reversed_ops.append(reversed_op)
108
-
105
+
109
106
  reversed_chain = OperationChain(operations=reversed_ops)
110
107
  return reversed_chain.execute(source, output)
111
-
108
+
112
109
  def _execute_operation(self, operation: str, source: Path, output: Path) -> Path:
113
110
  """Execute a single operation."""
114
111
  match operation.lower():
@@ -138,7 +135,7 @@ class OperationChain:
138
135
  return zip_archive.extract(source, output)
139
136
  case _:
140
137
  raise ArchiveError(f"Unknown operation: {operation}")
141
-
138
+
142
139
  def _get_suffix_for_operation(self, operation: str) -> str:
143
140
  """Get file suffix for operation."""
144
141
  suffixes = {
@@ -153,34 +150,34 @@ class OperationChain:
153
150
  class ArchiveOperations:
154
151
  """
155
152
  Helper class for common archive operation patterns.
156
-
153
+
157
154
  Provides convenient methods for common archive formats.
158
155
  """
159
-
156
+
160
157
  @staticmethod
161
158
  def create_tar_gz(source: Path, output: Path, deterministic: bool = True) -> Path:
162
159
  """
163
160
  Create .tar.gz archive in one step.
164
-
161
+
165
162
  Args:
166
163
  source: Source file or directory
167
164
  output: Output path (should end with .tar.gz)
168
165
  deterministic: Create reproducible archive
169
-
166
+
170
167
  Returns:
171
168
  Path to created archive
172
-
169
+
173
170
  Raises:
174
171
  ArchiveError: If creation fails
175
172
  """
176
173
  ensure_parent_dir(output)
177
-
174
+
178
175
  # Create temp tar file
179
- temp_tar = output.with_suffix('.tar')
176
+ temp_tar = output.with_suffix(".tar")
180
177
  try:
181
178
  tar = TarArchive(deterministic=deterministic)
182
179
  tar.create(source, temp_tar)
183
-
180
+
184
181
  # Compress to final output
185
182
  gzip = GzipCompressor()
186
183
  return gzip.compress_file(temp_tar, output)
@@ -188,32 +185,32 @@ class ArchiveOperations:
188
185
  # Clean up temp file
189
186
  if temp_tar.exists():
190
187
  temp_tar.unlink()
191
-
188
+
192
189
  @staticmethod
193
190
  def extract_tar_gz(archive: Path, output: Path) -> Path:
194
191
  """
195
192
  Extract .tar.gz archive in one step.
196
-
193
+
197
194
  Args:
198
195
  archive: Archive path
199
196
  output: Output directory
200
-
197
+
201
198
  Returns:
202
199
  Path to extraction directory
203
-
200
+
204
201
  Raises:
205
202
  ArchiveError: If extraction fails
206
203
  """
207
204
  output.mkdir(parents=True, exist_ok=True)
208
-
205
+
209
206
  # Decompress to temp file
210
- with tempfile.NamedTemporaryFile(suffix='.tar', delete=False) as temp:
207
+ with tempfile.NamedTemporaryFile(suffix=".tar", delete=False) as temp:
211
208
  temp_tar = Path(temp.name)
212
-
209
+
213
210
  try:
214
211
  gzip = GzipCompressor()
215
212
  gzip.decompress_file(archive, temp_tar)
216
-
213
+
217
214
  # Extract tar
218
215
  tar = TarArchive()
219
216
  return tar.extract(temp_tar, output)
@@ -221,31 +218,31 @@ class ArchiveOperations:
221
218
  # Clean up temp file
222
219
  if temp_tar.exists():
223
220
  temp_tar.unlink()
224
-
221
+
225
222
  @staticmethod
226
223
  def create_tar_bz2(source: Path, output: Path, deterministic: bool = True) -> Path:
227
224
  """
228
225
  Create .tar.bz2 archive in one step.
229
-
226
+
230
227
  Args:
231
228
  source: Source file or directory
232
229
  output: Output path (should end with .tar.bz2)
233
230
  deterministic: Create reproducible archive
234
-
231
+
235
232
  Returns:
236
233
  Path to created archive
237
-
234
+
238
235
  Raises:
239
236
  ArchiveError: If creation fails
240
237
  """
241
238
  ensure_parent_dir(output)
242
-
239
+
243
240
  # Create temp tar file
244
- temp_tar = output.with_suffix('.tar')
241
+ temp_tar = output.with_suffix(".tar")
245
242
  try:
246
243
  tar = TarArchive(deterministic=deterministic)
247
244
  tar.create(source, temp_tar)
248
-
245
+
249
246
  # Compress to final output
250
247
  bz2 = Bzip2Compressor()
251
248
  return bz2.compress_file(temp_tar, output)
@@ -253,32 +250,32 @@ class ArchiveOperations:
253
250
  # Clean up temp file
254
251
  if temp_tar.exists():
255
252
  temp_tar.unlink()
256
-
253
+
257
254
  @staticmethod
258
255
  def extract_tar_bz2(archive: Path, output: Path) -> Path:
259
256
  """
260
257
  Extract .tar.bz2 archive in one step.
261
-
258
+
262
259
  Args:
263
260
  archive: Archive path
264
261
  output: Output directory
265
-
262
+
266
263
  Returns:
267
264
  Path to extraction directory
268
-
265
+
269
266
  Raises:
270
267
  ArchiveError: If extraction fails
271
268
  """
272
269
  output.mkdir(parents=True, exist_ok=True)
273
-
270
+
274
271
  # Decompress to temp file
275
- with tempfile.NamedTemporaryFile(suffix='.tar', delete=False) as temp:
272
+ with tempfile.NamedTemporaryFile(suffix=".tar", delete=False) as temp:
276
273
  temp_tar = Path(temp.name)
277
-
274
+
278
275
  try:
279
276
  bz2 = Bzip2Compressor()
280
277
  bz2.decompress_file(archive, temp_tar)
281
-
278
+
282
279
  # Extract tar
283
280
  tar = TarArchive()
284
281
  return tar.extract(temp_tar, output)
@@ -286,51 +283,51 @@ class ArchiveOperations:
286
283
  # Clean up temp file
287
284
  if temp_tar.exists():
288
285
  temp_tar.unlink()
289
-
286
+
290
287
  @staticmethod
291
288
  def detect_format(file: Path) -> list[str]:
292
289
  """
293
290
  Detect archive format and return operation chain.
294
-
291
+
295
292
  Args:
296
293
  file: File path to analyze
297
-
294
+
298
295
  Returns:
299
296
  List of operations needed to extract
300
-
297
+
301
298
  Raises:
302
299
  ArchiveError: If format cannot be detected
303
300
  """
304
301
  name = file.name.lower()
305
-
302
+
306
303
  # Check by extension
307
- if name.endswith('.tar.gz') or name.endswith('.tgz'):
308
- return ['gunzip', 'untar']
309
- elif name.endswith('.tar.bz2') or name.endswith('.tbz2'):
310
- return ['bunzip2', 'untar']
311
- elif name.endswith('.tar'):
312
- return ['untar']
313
- elif name.endswith('.gz'):
314
- return ['gunzip']
315
- elif name.endswith('.bz2'):
316
- return ['bunzip2']
317
- elif name.endswith('.zip'):
318
- return ['unzip']
319
-
304
+ if name.endswith(".tar.gz") or name.endswith(".tgz"):
305
+ return ["gunzip", "untar"]
306
+ elif name.endswith(".tar.bz2") or name.endswith(".tbz2"):
307
+ return ["bunzip2", "untar"]
308
+ elif name.endswith(".tar"):
309
+ return ["untar"]
310
+ elif name.endswith(".gz"):
311
+ return ["gunzip"]
312
+ elif name.endswith(".bz2"):
313
+ return ["bunzip2"]
314
+ elif name.endswith(".zip"):
315
+ return ["unzip"]
316
+
320
317
  # Check by magic numbers
321
318
  try:
322
- with open(file, 'rb') as f:
319
+ with open(file, "rb") as f:
323
320
  magic = f.read(4)
324
-
325
- if magic[:2] == b'\x1f\x8b': # gzip
326
- return ['gunzip']
327
- elif magic[:3] == b'BZh': # bzip2
328
- return ['bunzip2']
329
- elif magic[:4] == b'PK\x03\x04': # zip
330
- return ['unzip']
331
- elif magic[:3] == b'ustar': # tar (at offset 257)
332
- return ['untar']
321
+
322
+ if magic[:2] == b"\x1f\x8b": # gzip
323
+ return ["gunzip"]
324
+ elif magic[:3] == b"BZh": # bzip2
325
+ return ["bunzip2"]
326
+ elif magic[:4] == b"PK\x03\x04": # zip
327
+ return ["unzip"]
328
+ elif magic[:3] == b"ustar": # tar (at offset 257)
329
+ return ["untar"]
333
330
  except Exception:
334
331
  pass
335
-
336
- raise ArchiveError(f"Cannot detect format of {file}")
332
+
333
+ raise ArchiveError(f"Cannot detect format of {file}")
@@ -1,11 +1,11 @@
1
1
  """TAR archive implementation."""
2
2
 
3
- import tarfile
4
3
  from pathlib import Path
4
+ import tarfile
5
5
 
6
6
  from attrs import define, field
7
7
 
8
- from provide.foundation.archive.base import BaseArchive, ArchiveError
8
+ from provide.foundation.archive.base import ArchiveError, BaseArchive
9
9
  from provide.foundation.file import ensure_parent_dir
10
10
  from provide.foundation.logger import get_logger
11
11
 
@@ -16,32 +16,32 @@ logger = get_logger(__name__)
16
16
  class TarArchive(BaseArchive):
17
17
  """
18
18
  TAR archive implementation.
19
-
19
+
20
20
  Creates and extracts TAR archives with optional metadata preservation
21
21
  and deterministic output for reproducible builds.
22
22
  """
23
-
23
+
24
24
  deterministic: bool = field(default=True)
25
25
  preserve_metadata: bool = field(default=True)
26
26
  preserve_permissions: bool = field(default=True)
27
-
27
+
28
28
  def create(self, source: Path, output: Path) -> Path:
29
29
  """
30
30
  Create TAR archive from source.
31
-
31
+
32
32
  Args:
33
33
  source: Source file or directory to archive
34
34
  output: Output TAR file path
35
-
35
+
36
36
  Returns:
37
37
  Path to created archive
38
-
38
+
39
39
  Raises:
40
40
  ArchiveError: If archive creation fails
41
41
  """
42
42
  try:
43
43
  ensure_parent_dir(output)
44
-
44
+
45
45
  with tarfile.open(output, "w") as tar:
46
46
  if source.is_dir():
47
47
  # Add all files in directory
@@ -52,52 +52,52 @@ class TarArchive(BaseArchive):
52
52
  else:
53
53
  # Add single file
54
54
  self._add_file(tar, source, source.name)
55
-
55
+
56
56
  logger.debug(f"Created TAR archive: {output}")
57
57
  return output
58
-
58
+
59
59
  except Exception as e:
60
60
  raise ArchiveError(f"Failed to create TAR archive: {e}") from e
61
61
 
62
62
  def extract(self, archive: Path, output: Path) -> Path:
63
63
  """
64
64
  Extract TAR archive to output directory.
65
-
65
+
66
66
  Args:
67
67
  archive: TAR archive file path
68
68
  output: Output directory path
69
-
69
+
70
70
  Returns:
71
71
  Path to extraction directory
72
-
72
+
73
73
  Raises:
74
74
  ArchiveError: If extraction fails
75
75
  """
76
76
  try:
77
77
  output.mkdir(parents=True, exist_ok=True)
78
-
78
+
79
79
  with tarfile.open(archive, "r") as tar:
80
80
  # Security check - prevent path traversal
81
81
  for member in tar.getmembers():
82
82
  if member.name.startswith("/") or ".." in member.name:
83
83
  raise ArchiveError(f"Unsafe path in archive: {member.name}")
84
-
84
+
85
85
  # Extract all
86
86
  tar.extractall(output)
87
-
87
+
88
88
  logger.debug(f"Extracted TAR archive to: {output}")
89
89
  return output
90
-
90
+
91
91
  except Exception as e:
92
92
  raise ArchiveError(f"Failed to extract TAR archive: {e}") from e
93
93
 
94
94
  def validate(self, archive: Path) -> bool:
95
95
  """
96
96
  Validate TAR archive integrity.
97
-
97
+
98
98
  Args:
99
99
  archive: TAR archive file path
100
-
100
+
101
101
  Returns:
102
102
  True if archive is valid, False otherwise
103
103
  """
@@ -110,17 +110,17 @@ class TarArchive(BaseArchive):
110
110
  return True
111
111
  except Exception:
112
112
  return False
113
-
113
+
114
114
  def list_contents(self, archive: Path) -> list[str]:
115
115
  """
116
116
  List contents of TAR archive.
117
-
117
+
118
118
  Args:
119
119
  archive: TAR archive file path
120
-
120
+
121
121
  Returns:
122
122
  List of file paths in archive
123
-
123
+
124
124
  Raises:
125
125
  ArchiveError: If listing fails
126
126
  """
@@ -133,18 +133,20 @@ class TarArchive(BaseArchive):
133
133
  return sorted(contents)
134
134
  except Exception as e:
135
135
  raise ArchiveError(f"Failed to list TAR contents: {e}") from e
136
-
137
- def _add_file(self, tar: tarfile.TarFile, file_path: Path, arcname: str | Path) -> None:
136
+
137
+ def _add_file(
138
+ self, tar: tarfile.TarFile, file_path: Path, arcname: str | Path
139
+ ) -> None:
138
140
  """
139
141
  Add single file to TAR archive.
140
-
142
+
141
143
  Args:
142
144
  tar: Open TarFile object
143
145
  file_path: Path to file to add
144
146
  arcname: Name in archive
145
147
  """
146
148
  tarinfo = tar.gettarinfo(str(file_path), str(arcname))
147
-
149
+
148
150
  if self.deterministic:
149
151
  # Set consistent metadata for reproducible archives
150
152
  tarinfo.uid = 0
@@ -152,13 +154,13 @@ class TarArchive(BaseArchive):
152
154
  tarinfo.uname = ""
153
155
  tarinfo.gname = ""
154
156
  tarinfo.mtime = 0
155
-
157
+
156
158
  if not self.preserve_permissions:
157
159
  # Normalize permissions
158
160
  if tarinfo.isfile():
159
161
  tarinfo.mode = 0o644
160
162
  elif tarinfo.isdir():
161
163
  tarinfo.mode = 0o755
162
-
164
+
163
165
  with open(file_path, "rb") as f:
164
- tar.addfile(tarinfo, f)
166
+ tar.addfile(tarinfo, f)