Skip to content

Archive File API Reference

API documentation

hands_scaphoid.objects.ArchiveFile

Archive module for hands-scaphoid package.

This module provides the Archive class for managing archive operations with context manager support and hierarchical path resolution. ---yaml File: name: ArchiveFile.py uuid: 14a7856f-b8d0-43b7-a647-5ccbfc45c0e8 date: 2025-09-16

Description

Archive context manager for hierarchical file system operations

Project

name: hands_scaphoid uuid: 2945ba3b-2d66-4dff-b898-672c386f03f4 url: https://github.com/42sol-eu/hands_scaphoid

Authors: ["Andreas Häberle"]

Classes

ArchiveFile

Bases: FileObject

Pure archive operations class without context management.

This class provides static methods for archive operations that can be used independently of any context manager. All methods operate on explicit archive paths and do not maintain any state.

Supported formats: ZIP, TAR, TAR.GZ, TAR.BZ2

Example
# Direct archive operations
ArchiveFile.create_zip_archive(Path("backup.zip"), Path("source_dir"))
ArchiveFile.add_file_to_zip(Path("backup.zip"), Path("new_file.txt"))
files = ArchiveFile.list_archive_contents(Path("backup.zip"))
ArchiveFile.extract_archive(Path("backup.zip"), Path("extracted_dir"))

Attributes: name (str): The name of the archive file. path (str): The path of the archive file in the filesystem.

Source code in src/hands_scaphoid/objects/ArchiveFile.py
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
class ArchiveFile(FileObject):
    """
    Pure archive operations class without context management.

    This class provides static methods for archive operations that can be used
    independently of any context manager. All methods operate on explicit
    archive paths and do not maintain any state.

    Supported formats: ZIP, TAR, TAR.GZ, TAR.BZ2

    Example:
        ```python
        # Direct archive operations
        ArchiveFile.create_zip_archive(Path("backup.zip"), Path("source_dir"))
        ArchiveFile.add_file_to_zip(Path("backup.zip"), Path("new_file.txt"))
        files = ArchiveFile.list_archive_contents(Path("backup.zip"))
        ArchiveFile.extract_archive(Path("backup.zip"), Path("extracted_dir"))
        ```
    Attributes:
        name (str): The name of the archive file.
        path (str): The path of the archive file in the filesystem.
    """

    def __init__(self, name: str, path: str):
        super().__init__(name, path)
        self.item_type = ItemType.ARCHIVE

    @classmethod
    def compression_type(cls, name: str) -> str:
        """
        Determines the compression type based on the file extension.

        Args:
            name (str): The name of the archive file.

        Returns:
            str: The compression type (e.g., 'zip', 'tar', 'gz', etc.) or 'unknown' if not recognized.
        """
        extension = get_file_extension(name)

        compression_types = {
            "zip": CompressionType.ZIP,
            "tar": CompressionType.TAR,
            "tar.gz": CompressionType.TAR_GZ,
            "tar.bz2": CompressionType.TAR_BZ2,
            "tar.xz": CompressionType.TAR_XZ,
            "gz": CompressionType.GZIP,
            "bz2": CompressionType.BZIP2,
            "xz": CompressionType.XZ,
            "7z": CompressionType.SEVEN_Z,
            "rar": CompressionType.RAR,
        }
        return compression_types.get(extension, CompressionType.UNKNOWN)

    def __repr__(self):
        return f"ArchiveFile(name={self.name}, path={self.path})"

    @staticmethod
    def add_archive_type(name: str, extract_function, compress_function) -> bool:
        """
        Add a user defined archive type (also used for types like *.whl, *.app)
        """
        done = CompressionType.add_archvive_type(
            name, extract_function, compress_function
        )
        return done

    @staticmethod
    def detect_archive_type(archive_path: PathLike) -> str:
        """
        Detect the archive type from file extension.

        Args:
            archive_path: Path to the archive file

        Returns:
            Archive type ('zip', 'tar', 'tar.gz', 'tar.bz2')

        Raises:
            ValueError: If archive type cannot be determined
        """
        path = Path(archive_path)

        if path.suffix.lower() == ".zip":
            return "zip"
        elif path.suffix.lower() == ".gz":
            return "gz"
        elif path.suffix.lower() == ".bz2":
            return "bz2"
        elif path.suffix.lower() == ".7z":
            return "7z"
        elif path.suffix.lower() == ".rar":
            return "rar"
        elif path.suffixes[-2:] == [".tar", ".gz"]:
            return "tar.gz"
        elif path.suffixes[-2:] == [".tar", ".bz2"]:
            return "tar.bz2"
        elif path.suffixes[-2:] == [".tar", ".xz"]:
            return "tar.xz"
        elif path.suffix.lower() == ".tar":
            return "tar"
        elif path.suffix.lower() == ".xz":
            return "xz"
        else:
            # check for project specific archive types
            # TODO: !!! implement whl, app and more

            raise ValueError(f"Unsupported archive type for file: {path}")

    @staticmethod
    def is_archive_file(file_path: PathLike) -> bool:
        """
        Check if a file is an archive based on its extension.

        Args:
            file_path: Path to check

        Returns:
            True if file appears to be an archive, False otherwise
        """
        try:
            ArchiveFile.detect_archive_type(file_path)
            return True
        except ValueError:
            return False

    @staticmethod
    def create_zip_archive(
        archive_path: PathLike, source_path: Optional[PathLike] = None
    ) -> None:
        """
        Create a new ZIP ArchiveFile.

        Args:
            archive_path: Path for the new archive
            source_path: Optional source directory/file to add initially

        Raises:
            PermissionError: If lacking create permissions
        """
        path = Path(archive_path)
        try:
            # Create parent directories if needed
            path.parent.mkdir(parents=True, exist_ok=True)

            with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
                if source_path:
                    source = Path(source_path)
                    if source.is_file():
                        zf.write(source, source.name)
                    elif source.is_dir():
                        for file_path in source.rglob("*"):
                            if file_path.is_file():
                                arcname = file_path.relative_to(source.parent)
                                zf.write(file_path, arcname)

            console.print(f"[green]Created ZIP archive:[/green] {path}")
        except PermissionError:
            console.print(f"[red]Permission denied creating archive:[/red] {path}")
            raise
        except Exception as e:
            console.print(f"[red]Error creating ZIP archive {path}:[/red] {e}")
            raise

    @staticmethod
    def create_tar_archive(
        archive_path: PathLike,
        source_path: Optional[PathLike] = None,
        compression: Optional[str] = None,
    ) -> None:
        """
        Create a new TAR ArchiveFile.

        Args:
            archive_path: Path for the new archive
            source_path: Optional source directory/file to add initially
            compression: Compression type ('gz', 'bz2', or None)

        Raises:
            PermissionError: If lacking create permissions
        """
        path = Path(archive_path)
        try:
            # Create parent directories if needed
            path.parent.mkdir(parents=True, exist_ok=True)

            mode = "w"
            if compression == "gz":
                mode = "w:gz"
            elif compression == "bz2":
                mode = "w:bz2"

            with tarfile.open(path, mode) as tf:
                if source_path:
                    source = Path(source_path)
                    if source.exists():
                        tf.add(source, arcname=source.name)

            console.print(f"[green]Created TAR archive:[/green] {path}")
        except PermissionError:
            console.print(f"[red]Permission denied creating archive:[/red] {path}")
            raise
        except Exception as e:
            console.print(f"[red]Error creating TAR archive {path}:[/red] {e}")
            raise

    @staticmethod
    def add_file_to_zip(
        archive_path: PathLike, file_path: PathLike, archive_name: Optional[str] = None
    ) -> None:
        """
        Add a file to an existing ZIP ArchiveFile.

        Args:
            archive_path: Path to the ZIP archive
            file_path: Path to the file to add
            archive_name: Name to use in archive (defaults to file_name from file_path)

        Raises:
            FileNotFoundError: If archive or file doesn't exist
        """
        archive = Path(archive_path)
        file_to_add = Path(file_path)

        if not ArchiveFile.exists():
            raise FileNotFoundError(f"Archive not found: {archive}")
        if not file_to_add.exists():
            raise FileNotFoundError(f"File not found: {file_to_add}")

        try:
            arcname = archive_name or file_to_add.name

            with zipfile.ZipFile(archive, "a", zipfile.ZIP_DEFLATED) as zf:
                zf.write(file_to_add, arcname)

            console.print(f"[green]Added file to ZIP archive:[/green] {arcname}")
        except Exception as e:
            console.print(f"[red]Error adding file to ZIP archive:[/red] {e}")
            raise

    @staticmethod
    def add_directory_to_zip(
        archive_path: PathLike, dir_path: PathLike, archive_name: Optional[str] = None
    ) -> None:
        """
        Add a directory to an existing ZIP ArchiveFile.

        Args:
            archive_path: Path to the ZIP archive
            dir_path: Path to the directory to add
            archive_name: Name to use in archive (defaults to directory name)
        """
        archive = Path(archive_path)
        directory = Path(dir_path)

        if not ArchiveFile.exists():
            raise FileNotFoundError(f"Archive not found: {archive}")
        if not directory.exists():
            raise FileNotFoundError(f"Directory not found: {directory}")
        if not directory.is_dir():
            raise NotADirectoryError(f"Path is not a directory: {directory}")

        try:
            base_name = archive_name or directory.name

            with zipfile.ZipFile(archive, "a", zipfile.ZIP_DEFLATED) as zf:
                for file_path in directory.rglob("*"):
                    if file_path.is_file():
                        relative_path = file_path.relative_to(directory)
                        arcname = f"{base_name}/{relative_path}"
                        zf.write(file_path, arcname)

            console.print(f"[green]Added directory to ZIP archive:[/green] {base_name}")
        except Exception as e:
            console.print(f"[red]Error adding directory to ZIP archive:[/red] {e}")
            raise

    @staticmethod
    def add_file_to_tar(
        archive_path: PathLike, file_path: PathLike, archive_name: Optional[str] = None
    ) -> None:
        """
        Add a file to an existing TAR ArchiveFile.

        Args:
            archive_path: Path to the TAR archive
            file_path: Path to the file to add
            archive_name: Name to use in archive (defaults to file_name from file_path)
        """
        archive = Path(archive_path)
        file_to_add = Path(file_path)

        if not ArchiveFile.exists():
            raise FileNotFoundError(f"Archive not found: {archive}")
        if not file_to_add.exists():
            raise FileNotFoundError(f"File not found: {file_to_add}")

        try:
            # Determine TAR mode based on archive type
            archive_type = ArchiveFile.detect_archive_type(archive)
            if archive_type == "tar.gz":
                mode = "a:gz"
            elif archive_type == "tar.bz2":
                mode = "a:bz2"
            else:
                mode = "a"

            arcname = archive_name or file_to_add.name

            with tarfile.open(archive, mode) as tf:
                tf.add(file_to_add, arcname=arcname)

            console.print(f"[green]Added file to TAR archive:[/green] {arcname}")
        except Exception as e:
            console.print(f"[red]Error adding file to TAR archive:[/red] {e}")
            raise

    @staticmethod
    def list_archive_contents(archive_path: PathLike) -> List[str]:
        """
        List contents of an ArchiveFile.

        Args:
            archive_path: Path to the archive

        Returns:
            List of file paths in the archive

        Raises:
            FileNotFoundError: If archive doesn't exist
        """
        path = Path(archive_path)

        if not path.exists():
            raise FileNotFoundError(f"Archive not found: {path}")

        try:
            archive_type = ArchiveFile.detect_archive_type(path)

            if archive_type == "zip":
                with zipfile.ZipFile(path, "r") as zf:
                    contents = zf.namelist()
            else:
                # TAR archive
                with tarfile.open(path, "r") as tf:
                    contents = tf.getnames()

            console.print(
                f"[blue]Listed {len(contents)} items in archive:[/blue] {path}"
            )
            return contents

        except Exception as e:
            console.print(f"[red]Error listing archive contents {path}:[/red] {e}")
            raise

    @staticmethod
    def extract_archive(
        archive_path: PathLike, target_path: Optional[PathLike] = None
    ) -> None:
        """
        Extract all contents from an ArchiveFile.

        Args:
            archive_path: Path to the archive to extract
            target_path: Target directory for extraction (defaults to current directory)

        Raises:
            FileNotFoundError: If archive doesn't exist
        """
        archive = Path(archive_path)
        target = Path(target_path) if target_path else Path.cwd()

        if not ArchiveFile.exists():
            raise FileNotFoundError(f"Archive not found: {archive}")

        try:
            # Create target directory if it doesn't exist
            target.mkdir(parents=True, exist_ok=True)

            archive_type = ArchiveFile.detect_archive_type(archive)

            if archive_type == "zip":
                with zipfile.ZipFile(archive, "r") as zf:
                    zf.extractall(target)
            else:
                # TAR archive
                with tarfile.open(archive, "r") as tf:
                    tf.extractall(target)

            console.print(f"[green]Extracted archive:[/green] {archive}{target}")

        except Exception as e:
            console.print(f"[red]Error extracting archive {archive}:[/red] {e}")
            raise

    @staticmethod
    def extract_file_from_archive(
        archive_path: PathLike, file_name: str, target_path: Optional[PathLike] = None
    ) -> None:
        """
        Extract a specific file from an ArchiveFile.

        Args:
            archive_path: Path to the archive
            file_name: Name of file to extract from archive
            target_path: Target directory for extraction (defaults to current directory)

        Raises:
            FileNotFoundError: If archive doesn't exist or file not in archive
        """
        archive = Path(archive_path)
        target = Path(target_path) if target_path else Path.cwd()

        if not ArchiveFile.exists():
            raise FileNotFoundError(f"Archive not found: {archive}")

        try:
            # Create target directory if it doesn't exist
            target.mkdir(parents=True, exist_ok=True)

            archive_type = ArchiveFile.detect_archive_type(archive)

            if archive_type == "zip":
                with zipfile.ZipFile(archive, "r") as zf:
                    if file_name not in zf.namelist():
                        raise FileNotFoundError(
                            f"File '{file_name}' not found in archive"
                        )
                    zf.extract(file_name, target)
            else:
                # TAR archive
                with tarfile.open(archive, "r") as tf:
                    if file_name not in tf.getnames():
                        raise FileNotFoundError(
                            f"File '{file_name}' not found in archive"
                        )
                    tf.extract(file_name, target)

            console.print(f"[green]Extracted file:[/green] {file_name}{target}")

        except Exception as e:
            console.print(f"[red]Error extracting file from archive:[/red] {e}")
            raise

    @staticmethod
    def archive_info(archive_path: PathLike) -> dict[str, Any]:
        """
        Get information about an ArchiveFile.

        Args:
            archive_path: Path to the archive

        Returns:
            Dictionary with archive information

        Raises:
            FileNotFoundError: If archive doesn't exist
        """
        path = Path(archive_path)

        if not path.exists():
            raise FileNotFoundError(f"Archive not found: {path}")

        try:
            archive_type = ArchiveFile.detect_archive_type(path)
            file_size = path.stat().st_size

            if archive_type == "zip":
                with zipfile.ZipFile(path, "r") as zf:
                    file_count = len(zf.namelist())
                    info = zf.infolist()
                    total_uncompressed = sum(f.file_size for f in info)
            else:
                # TAR archive
                with tarfile.open(path, "r") as tf:
                    members = tf.getmembers()
                    file_count = len([m for m in members if m.isfile()])
                    total_uncompressed = sum(m.size for m in members if m.isfile())

            return {
                "type": archive_type,
                "file_count": file_count,
                "compressed_size": file_size,
                "uncompressed_size": total_uncompressed,
                "compression_ratio": (file_size / total_uncompressed)
                if total_uncompressed > 0
                else 0,
            }

        except Exception as e:
            console.print(f"[red]Error getting archive info {path}:[/red] {e}")
            raise
Functions
compression_type(name) classmethod

Determines the compression type based on the file extension.

Parameters:

Name Type Description Default
name str

The name of the archive file.

required

Returns:

Name Type Description
str str

The compression type (e.g., 'zip', 'tar', 'gz', etc.) or 'unknown' if not recognized.

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@classmethod
def compression_type(cls, name: str) -> str:
    """
    Determines the compression type based on the file extension.

    Args:
        name (str): The name of the archive file.

    Returns:
        str: The compression type (e.g., 'zip', 'tar', 'gz', etc.) or 'unknown' if not recognized.
    """
    extension = get_file_extension(name)

    compression_types = {
        "zip": CompressionType.ZIP,
        "tar": CompressionType.TAR,
        "tar.gz": CompressionType.TAR_GZ,
        "tar.bz2": CompressionType.TAR_BZ2,
        "tar.xz": CompressionType.TAR_XZ,
        "gz": CompressionType.GZIP,
        "bz2": CompressionType.BZIP2,
        "xz": CompressionType.XZ,
        "7z": CompressionType.SEVEN_Z,
        "rar": CompressionType.RAR,
    }
    return compression_types.get(extension, CompressionType.UNKNOWN)
add_archive_type(name, extract_function, compress_function) staticmethod

Add a user defined archive type (also used for types like .whl, .app)

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def add_archive_type(name: str, extract_function, compress_function) -> bool:
    """
    Add a user defined archive type (also used for types like *.whl, *.app)
    """
    done = CompressionType.add_archvive_type(
        name, extract_function, compress_function
    )
    return done
detect_archive_type(archive_path) staticmethod

Detect the archive type from file extension.

Parameters:

Name Type Description Default
archive_path PathLike

Path to the archive file

required

Returns:

Type Description
str

Archive type ('zip', 'tar', 'tar.gz', 'tar.bz2')

Raises:

Type Description
ValueError

If archive type cannot be determined

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def detect_archive_type(archive_path: PathLike) -> str:
    """
    Detect the archive type from file extension.

    Args:
        archive_path: Path to the archive file

    Returns:
        Archive type ('zip', 'tar', 'tar.gz', 'tar.bz2')

    Raises:
        ValueError: If archive type cannot be determined
    """
    path = Path(archive_path)

    if path.suffix.lower() == ".zip":
        return "zip"
    elif path.suffix.lower() == ".gz":
        return "gz"
    elif path.suffix.lower() == ".bz2":
        return "bz2"
    elif path.suffix.lower() == ".7z":
        return "7z"
    elif path.suffix.lower() == ".rar":
        return "rar"
    elif path.suffixes[-2:] == [".tar", ".gz"]:
        return "tar.gz"
    elif path.suffixes[-2:] == [".tar", ".bz2"]:
        return "tar.bz2"
    elif path.suffixes[-2:] == [".tar", ".xz"]:
        return "tar.xz"
    elif path.suffix.lower() == ".tar":
        return "tar"
    elif path.suffix.lower() == ".xz":
        return "xz"
    else:
        # check for project specific archive types
        # TODO: !!! implement whl, app and more

        raise ValueError(f"Unsupported archive type for file: {path}")
is_archive_file(file_path) staticmethod

Check if a file is an archive based on its extension.

Parameters:

Name Type Description Default
file_path PathLike

Path to check

required

Returns:

Type Description
bool

True if file appears to be an archive, False otherwise

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def is_archive_file(file_path: PathLike) -> bool:
    """
    Check if a file is an archive based on its extension.

    Args:
        file_path: Path to check

    Returns:
        True if file appears to be an archive, False otherwise
    """
    try:
        ArchiveFile.detect_archive_type(file_path)
        return True
    except ValueError:
        return False
create_zip_archive(archive_path, source_path=None) staticmethod

Create a new ZIP ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path for the new archive

required
source_path Optional[PathLike]

Optional source directory/file to add initially

None

Raises:

Type Description
PermissionError

If lacking create permissions

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def create_zip_archive(
    archive_path: PathLike, source_path: Optional[PathLike] = None
) -> None:
    """
    Create a new ZIP ArchiveFile.

    Args:
        archive_path: Path for the new archive
        source_path: Optional source directory/file to add initially

    Raises:
        PermissionError: If lacking create permissions
    """
    path = Path(archive_path)
    try:
        # Create parent directories if needed
        path.parent.mkdir(parents=True, exist_ok=True)

        with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
            if source_path:
                source = Path(source_path)
                if source.is_file():
                    zf.write(source, source.name)
                elif source.is_dir():
                    for file_path in source.rglob("*"):
                        if file_path.is_file():
                            arcname = file_path.relative_to(source.parent)
                            zf.write(file_path, arcname)

        console.print(f"[green]Created ZIP archive:[/green] {path}")
    except PermissionError:
        console.print(f"[red]Permission denied creating archive:[/red] {path}")
        raise
    except Exception as e:
        console.print(f"[red]Error creating ZIP archive {path}:[/red] {e}")
        raise
create_tar_archive(archive_path, source_path=None, compression=None) staticmethod

Create a new TAR ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path for the new archive

required
source_path Optional[PathLike]

Optional source directory/file to add initially

None
compression Optional[str]

Compression type ('gz', 'bz2', or None)

None

Raises:

Type Description
PermissionError

If lacking create permissions

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def create_tar_archive(
    archive_path: PathLike,
    source_path: Optional[PathLike] = None,
    compression: Optional[str] = None,
) -> None:
    """
    Create a new TAR ArchiveFile.

    Args:
        archive_path: Path for the new archive
        source_path: Optional source directory/file to add initially
        compression: Compression type ('gz', 'bz2', or None)

    Raises:
        PermissionError: If lacking create permissions
    """
    path = Path(archive_path)
    try:
        # Create parent directories if needed
        path.parent.mkdir(parents=True, exist_ok=True)

        mode = "w"
        if compression == "gz":
            mode = "w:gz"
        elif compression == "bz2":
            mode = "w:bz2"

        with tarfile.open(path, mode) as tf:
            if source_path:
                source = Path(source_path)
                if source.exists():
                    tf.add(source, arcname=source.name)

        console.print(f"[green]Created TAR archive:[/green] {path}")
    except PermissionError:
        console.print(f"[red]Permission denied creating archive:[/red] {path}")
        raise
    except Exception as e:
        console.print(f"[red]Error creating TAR archive {path}:[/red] {e}")
        raise
add_file_to_zip(archive_path, file_path, archive_name=None) staticmethod

Add a file to an existing ZIP ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path to the ZIP archive

required
file_path PathLike

Path to the file to add

required
archive_name Optional[str]

Name to use in archive (defaults to file_name from file_path)

None

Raises:

Type Description
FileNotFoundError

If archive or file doesn't exist

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def add_file_to_zip(
    archive_path: PathLike, file_path: PathLike, archive_name: Optional[str] = None
) -> None:
    """
    Add a file to an existing ZIP ArchiveFile.

    Args:
        archive_path: Path to the ZIP archive
        file_path: Path to the file to add
        archive_name: Name to use in archive (defaults to file_name from file_path)

    Raises:
        FileNotFoundError: If archive or file doesn't exist
    """
    archive = Path(archive_path)
    file_to_add = Path(file_path)

    if not ArchiveFile.exists():
        raise FileNotFoundError(f"Archive not found: {archive}")
    if not file_to_add.exists():
        raise FileNotFoundError(f"File not found: {file_to_add}")

    try:
        arcname = archive_name or file_to_add.name

        with zipfile.ZipFile(archive, "a", zipfile.ZIP_DEFLATED) as zf:
            zf.write(file_to_add, arcname)

        console.print(f"[green]Added file to ZIP archive:[/green] {arcname}")
    except Exception as e:
        console.print(f"[red]Error adding file to ZIP archive:[/red] {e}")
        raise
add_directory_to_zip(archive_path, dir_path, archive_name=None) staticmethod

Add a directory to an existing ZIP ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path to the ZIP archive

required
dir_path PathLike

Path to the directory to add

required
archive_name Optional[str]

Name to use in archive (defaults to directory name)

None
Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def add_directory_to_zip(
    archive_path: PathLike, dir_path: PathLike, archive_name: Optional[str] = None
) -> None:
    """
    Add a directory to an existing ZIP ArchiveFile.

    Args:
        archive_path: Path to the ZIP archive
        dir_path: Path to the directory to add
        archive_name: Name to use in archive (defaults to directory name)
    """
    archive = Path(archive_path)
    directory = Path(dir_path)

    if not ArchiveFile.exists():
        raise FileNotFoundError(f"Archive not found: {archive}")
    if not directory.exists():
        raise FileNotFoundError(f"Directory not found: {directory}")
    if not directory.is_dir():
        raise NotADirectoryError(f"Path is not a directory: {directory}")

    try:
        base_name = archive_name or directory.name

        with zipfile.ZipFile(archive, "a", zipfile.ZIP_DEFLATED) as zf:
            for file_path in directory.rglob("*"):
                if file_path.is_file():
                    relative_path = file_path.relative_to(directory)
                    arcname = f"{base_name}/{relative_path}"
                    zf.write(file_path, arcname)

        console.print(f"[green]Added directory to ZIP archive:[/green] {base_name}")
    except Exception as e:
        console.print(f"[red]Error adding directory to ZIP archive:[/red] {e}")
        raise
add_file_to_tar(archive_path, file_path, archive_name=None) staticmethod

Add a file to an existing TAR ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path to the TAR archive

required
file_path PathLike

Path to the file to add

required
archive_name Optional[str]

Name to use in archive (defaults to file_name from file_path)

None
Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def add_file_to_tar(
    archive_path: PathLike, file_path: PathLike, archive_name: Optional[str] = None
) -> None:
    """
    Add a file to an existing TAR ArchiveFile.

    Args:
        archive_path: Path to the TAR archive
        file_path: Path to the file to add
        archive_name: Name to use in archive (defaults to file_name from file_path)
    """
    archive = Path(archive_path)
    file_to_add = Path(file_path)

    if not ArchiveFile.exists():
        raise FileNotFoundError(f"Archive not found: {archive}")
    if not file_to_add.exists():
        raise FileNotFoundError(f"File not found: {file_to_add}")

    try:
        # Determine TAR mode based on archive type
        archive_type = ArchiveFile.detect_archive_type(archive)
        if archive_type == "tar.gz":
            mode = "a:gz"
        elif archive_type == "tar.bz2":
            mode = "a:bz2"
        else:
            mode = "a"

        arcname = archive_name or file_to_add.name

        with tarfile.open(archive, mode) as tf:
            tf.add(file_to_add, arcname=arcname)

        console.print(f"[green]Added file to TAR archive:[/green] {arcname}")
    except Exception as e:
        console.print(f"[red]Error adding file to TAR archive:[/red] {e}")
        raise
list_archive_contents(archive_path) staticmethod

List contents of an ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path to the archive

required

Returns:

Type Description
List[str]

List of file paths in the archive

Raises:

Type Description
FileNotFoundError

If archive doesn't exist

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def list_archive_contents(archive_path: PathLike) -> List[str]:
    """
    List contents of an ArchiveFile.

    Args:
        archive_path: Path to the archive

    Returns:
        List of file paths in the archive

    Raises:
        FileNotFoundError: If archive doesn't exist
    """
    path = Path(archive_path)

    if not path.exists():
        raise FileNotFoundError(f"Archive not found: {path}")

    try:
        archive_type = ArchiveFile.detect_archive_type(path)

        if archive_type == "zip":
            with zipfile.ZipFile(path, "r") as zf:
                contents = zf.namelist()
        else:
            # TAR archive
            with tarfile.open(path, "r") as tf:
                contents = tf.getnames()

        console.print(
            f"[blue]Listed {len(contents)} items in archive:[/blue] {path}"
        )
        return contents

    except Exception as e:
        console.print(f"[red]Error listing archive contents {path}:[/red] {e}")
        raise
extract_archive(archive_path, target_path=None) staticmethod

Extract all contents from an ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path to the archive to extract

required
target_path Optional[PathLike]

Target directory for extraction (defaults to current directory)

None

Raises:

Type Description
FileNotFoundError

If archive doesn't exist

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def extract_archive(
    archive_path: PathLike, target_path: Optional[PathLike] = None
) -> None:
    """
    Extract all contents from an ArchiveFile.

    Args:
        archive_path: Path to the archive to extract
        target_path: Target directory for extraction (defaults to current directory)

    Raises:
        FileNotFoundError: If archive doesn't exist
    """
    archive = Path(archive_path)
    target = Path(target_path) if target_path else Path.cwd()

    if not ArchiveFile.exists():
        raise FileNotFoundError(f"Archive not found: {archive}")

    try:
        # Create target directory if it doesn't exist
        target.mkdir(parents=True, exist_ok=True)

        archive_type = ArchiveFile.detect_archive_type(archive)

        if archive_type == "zip":
            with zipfile.ZipFile(archive, "r") as zf:
                zf.extractall(target)
        else:
            # TAR archive
            with tarfile.open(archive, "r") as tf:
                tf.extractall(target)

        console.print(f"[green]Extracted archive:[/green] {archive}{target}")

    except Exception as e:
        console.print(f"[red]Error extracting archive {archive}:[/red] {e}")
        raise
extract_file_from_archive(archive_path, file_name, target_path=None) staticmethod

Extract a specific file from an ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path to the archive

required
file_name str

Name of file to extract from archive

required
target_path Optional[PathLike]

Target directory for extraction (defaults to current directory)

None

Raises:

Type Description
FileNotFoundError

If archive doesn't exist or file not in archive

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def extract_file_from_archive(
    archive_path: PathLike, file_name: str, target_path: Optional[PathLike] = None
) -> None:
    """
    Extract a specific file from an ArchiveFile.

    Args:
        archive_path: Path to the archive
        file_name: Name of file to extract from archive
        target_path: Target directory for extraction (defaults to current directory)

    Raises:
        FileNotFoundError: If archive doesn't exist or file not in archive
    """
    archive = Path(archive_path)
    target = Path(target_path) if target_path else Path.cwd()

    if not ArchiveFile.exists():
        raise FileNotFoundError(f"Archive not found: {archive}")

    try:
        # Create target directory if it doesn't exist
        target.mkdir(parents=True, exist_ok=True)

        archive_type = ArchiveFile.detect_archive_type(archive)

        if archive_type == "zip":
            with zipfile.ZipFile(archive, "r") as zf:
                if file_name not in zf.namelist():
                    raise FileNotFoundError(
                        f"File '{file_name}' not found in archive"
                    )
                zf.extract(file_name, target)
        else:
            # TAR archive
            with tarfile.open(archive, "r") as tf:
                if file_name not in tf.getnames():
                    raise FileNotFoundError(
                        f"File '{file_name}' not found in archive"
                    )
                tf.extract(file_name, target)

        console.print(f"[green]Extracted file:[/green] {file_name}{target}")

    except Exception as e:
        console.print(f"[red]Error extracting file from archive:[/red] {e}")
        raise
archive_info(archive_path) staticmethod

Get information about an ArchiveFile.

Parameters:

Name Type Description Default
archive_path PathLike

Path to the archive

required

Returns:

Type Description
dict[str, Any]

Dictionary with archive information

Raises:

Type Description
FileNotFoundError

If archive doesn't exist

Source code in src/hands_scaphoid/objects/ArchiveFile.py
@staticmethod
def archive_info(archive_path: PathLike) -> dict[str, Any]:
    """
    Get information about an ArchiveFile.

    Args:
        archive_path: Path to the archive

    Returns:
        Dictionary with archive information

    Raises:
        FileNotFoundError: If archive doesn't exist
    """
    path = Path(archive_path)

    if not path.exists():
        raise FileNotFoundError(f"Archive not found: {path}")

    try:
        archive_type = ArchiveFile.detect_archive_type(path)
        file_size = path.stat().st_size

        if archive_type == "zip":
            with zipfile.ZipFile(path, "r") as zf:
                file_count = len(zf.namelist())
                info = zf.infolist()
                total_uncompressed = sum(f.file_size for f in info)
        else:
            # TAR archive
            with tarfile.open(path, "r") as tf:
                members = tf.getmembers()
                file_count = len([m for m in members if m.isfile()])
                total_uncompressed = sum(m.size for m in members if m.isfile())

        return {
            "type": archive_type,
            "file_count": file_count,
            "compressed_size": file_size,
            "uncompressed_size": total_uncompressed,
            "compression_ratio": (file_size / total_uncompressed)
            if total_uncompressed > 0
            else 0,
        }

    except Exception as e:
        console.print(f"[red]Error getting archive info {path}:[/red] {e}")
        raise

Functions