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.
- provide/foundation/__init__.py +20 -20
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +90 -91
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +4 -4
- provide/foundation/cli/commands/logs/__init__.py +2 -2
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +3 -3
- provide/foundation/cli/commands/logs/send.py +2 -2
- provide/foundation/cli/commands/logs/tail.py +2 -2
- provide/foundation/cli/decorators.py +0 -1
- provide/foundation/cli/testing.py +0 -5
- provide/foundation/cli/utils.py +1 -2
- provide/foundation/config/__init__.py +19 -19
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +81 -83
- provide/foundation/config/defaults.py +1 -1
- provide/foundation/config/env.py +2 -1
- provide/foundation/config/loader.py +1 -1
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/output.py +7 -7
- provide/foundation/context/core.py +19 -17
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -2
- provide/foundation/errors/decorators.py +0 -3
- provide/foundation/errors/types.py +0 -1
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +1 -1
- provide/foundation/file/lock.py +2 -3
- provide/foundation/hub/components.py +19 -21
- provide/foundation/hub/config.py +25 -19
- provide/foundation/hub/discovery.py +5 -4
- provide/foundation/hub/handlers.py +13 -5
- provide/foundation/hub/lifecycle.py +10 -9
- provide/foundation/hub/manager.py +3 -0
- provide/foundation/hub/processors.py +8 -3
- provide/foundation/integrations/__init__.py +1 -1
- provide/foundation/integrations/openobserve/client.py +2 -2
- provide/foundation/integrations/openobserve/commands.py +9 -9
- provide/foundation/integrations/openobserve/config.py +2 -2
- provide/foundation/integrations/openobserve/otlp.py +2 -2
- provide/foundation/integrations/openobserve/search.py +1 -2
- provide/foundation/integrations/openobserve/streaming.py +1 -1
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +19 -19
- provide/foundation/logger/config/telemetry.py +11 -13
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +38 -24
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +1 -1
- provide/foundation/process/__init__.py +1 -1
- provide/foundation/process/exit.py +6 -5
- provide/foundation/process/lifecycle.py +41 -18
- provide/foundation/resilience/__init__.py +6 -5
- provide/foundation/resilience/circuit.py +32 -30
- provide/foundation/resilience/decorators.py +58 -42
- provide/foundation/resilience/fallback.py +55 -40
- provide/foundation/resilience/retry.py +67 -65
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +8 -9
- provide/foundation/streams/console.py +3 -3
- provide/foundation/streams/core.py +2 -2
- provide/foundation/streams/file.py +1 -1
- provide/foundation/testing/__init__.py +22 -7
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +3 -6
- provide/foundation/testing/common/__init__.py +13 -13
- provide/foundation/testing/common/fixtures.py +27 -30
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +65 -92
- provide/foundation/testing/file/directory_fixtures.py +19 -19
- provide/foundation/testing/file/fixtures.py +14 -17
- provide/foundation/testing/file/special_fixtures.py +34 -42
- provide/foundation/testing/logger.py +28 -23
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +89 -80
- provide/foundation/testing/process/fixtures.py +11 -13
- provide/foundation/testing/process/subprocess_fixtures.py +41 -40
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +21 -17
- provide/foundation/testing/threading/data_fixtures.py +18 -16
- provide/foundation/testing/threading/execution_fixtures.py +67 -52
- provide/foundation/testing/threading/fixtures.py +10 -14
- provide/foundation/testing/threading/sync_fixtures.py +21 -18
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +91 -79
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +62 -69
- provide/foundation/tools/downloader.py +51 -56
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +1 -13
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +11 -13
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +86 -81
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +3 -2
- provide/foundation/utils/parsing.py +7 -7
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev2.dist-info/RECORD +0 -225
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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(
|
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=
|
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(
|
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=
|
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(
|
306
|
-
return [
|
307
|
-
elif name.endswith(
|
308
|
-
return [
|
309
|
-
elif name.endswith(
|
310
|
-
return [
|
311
|
-
elif name.endswith(
|
312
|
-
return [
|
313
|
-
elif name.endswith(
|
314
|
-
return [
|
315
|
-
elif name.endswith(
|
316
|
-
return [
|
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,
|
319
|
+
with open(file, "rb") as f:
|
321
320
|
magic = f.read(4)
|
322
|
-
|
323
|
-
if magic[:2] == b
|
324
|
-
return [
|
325
|
-
elif magic[:3] == b
|
326
|
-
return [
|
327
|
-
elif magic[:4] == b
|
328
|
-
return [
|
329
|
-
elif magic[:3] == b
|
330
|
-
return [
|
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
|
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(
|
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)
|