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