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.
- provide/foundation/__init__.py +36 -10
- 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 +93 -96
- 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 +15 -9
- provide/foundation/cli/commands/logs/__init__.py +3 -3
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +4 -4
- provide/foundation/cli/commands/logs/send.py +3 -3
- provide/foundation/cli/commands/logs/tail.py +3 -3
- provide/foundation/cli/decorators.py +11 -11
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -40
- provide/foundation/cli/utils.py +21 -18
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +477 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +6 -20
- provide/foundation/config/loader.py +10 -4
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +36 -14
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +88 -110
- 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 -3
- provide/foundation/errors/decorators.py +0 -234
- provide/foundation/errors/types.py +0 -98
- 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 +14 -23
- provide/foundation/file/lock.py +4 -3
- provide/foundation/hub/components.py +75 -389
- provide/foundation/hub/config.py +157 -0
- provide/foundation/hub/discovery.py +63 -0
- provide/foundation/hub/handlers.py +89 -0
- provide/foundation/hub/lifecycle.py +195 -0
- provide/foundation/hub/manager.py +7 -4
- provide/foundation/hub/processors.py +49 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
- provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
- provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +69 -299
- provide/foundation/logger/config/telemetry.py +39 -121
- 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 +39 -25
- 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 +3 -3
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +48 -0
- provide/foundation/process/lifecycle.py +69 -46
- provide/foundation/resilience/__init__.py +36 -0
- provide/foundation/resilience/circuit.py +166 -0
- provide/foundation/resilience/decorators.py +236 -0
- provide/foundation/resilience/fallback.py +208 -0
- provide/foundation/resilience/retry.py +327 -0
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +78 -0
- provide/foundation/streams/console.py +4 -5
- provide/foundation/streams/core.py +5 -2
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +29 -9
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +30 -20
- provide/foundation/testing/common/__init__.py +13 -15
- provide/foundation/testing/common/fixtures.py +27 -57
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +289 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +42 -516
- provide/foundation/testing/file/special_fixtures.py +145 -0
- provide/foundation/testing/logger.py +89 -8
- 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 +414 -0
- provide/foundation/testing/process/fixtures.py +48 -571
- provide/foundation/testing/process/subprocess_fixtures.py +210 -0
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +105 -0
- provide/foundation/testing/threading/data_fixtures.py +101 -0
- provide/foundation/testing/threading/execution_fixtures.py +278 -0
- provide/foundation/testing/threading/fixtures.py +32 -502
- provide/foundation/testing/threading/sync_fixtures.py +100 -0
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +95 -83
- 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 +69 -74
- provide/foundation/tools/downloader.py +68 -62
- 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 +2 -14
- 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 +36 -107
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +113 -114
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +17 -14
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev1.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.dev1.dist-info/RECORD +0 -200
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
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
|
-
|
74
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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(
|
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=
|
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(
|
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=
|
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(
|
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
|
-
elif name.endswith(
|
318
|
-
return [
|
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,
|
319
|
+
with open(file, "rb") as f:
|
323
320
|
magic = f.read(4)
|
324
|
-
|
325
|
-
if magic[:2] == b
|
326
|
-
return [
|
327
|
-
elif magic[:3] == b
|
328
|
-
return [
|
329
|
-
elif magic[:4] == b
|
330
|
-
return [
|
331
|
-
elif magic[:3] == b
|
332
|
-
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"]
|
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
|
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)
|