Metadata-Version: 2.4
Name: gz2zip
Version: 0.2.0
Summary: Fast GZIP-to-ZIP/ZIP64 conversion without recompression
License-Expression: MIT
Project-URL: Homepage, https://github.com/45deg/gz2zip
Project-URL: Repository, https://github.com/45deg/gz2zip
Project-URL: Issues, https://github.com/45deg/gz2zip/issues
Keywords: gzip,zip,zip64,deflate,streaming,repack
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Archiving :: Compression
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# gz2zip

[![CI](https://github.com/45deg/gz2zip/actions/workflows/ci.yaml/badge.svg)](https://github.com/45deg/gz2zip/actions/workflows/ci.yaml)
[![PyPI version](https://img.shields.io/pypi/v/gz2zip.svg)](https://pypi.org/project/gz2zip/)
[![Python versions](https://img.shields.io/pypi/pyversions/gz2zip.svg)](https://pypi.org/project/gz2zip/)

`gz2zip` converts **GZIP to ZIP/ZIP64 without recompression** by reusing the original Deflate payload.

This is much faster than decompressing and re-zipping, and it supports both:
1. **seekable file conversion** (`gzip_to_zip`)
2. **streaming conversion** (`stream_gzip_to_zip`) for pipes and async workloads

## Benchmark

```
Benchmark: 100 MiB text, 5 iterations
Platform: macOS 26.0.1 (arm64)
CPU: Apple M1 Pro
Memory: 32.0 GiB
Python: 3.12.0
Input: 100.00 MiB text -> 30.49 MiB gzip

gz2zip.core.gzip_to_zip  mean 9.73 ms  median 9.69 ms  min 9.32 ms  max 10.48 ms
python gzip + zipfile    mean 3316.88 ms  median 3313.52 ms  min 3299.16 ms  max 3335.05 ms
shell gzip -dc | zip     mean 3390.16 ms  median 3385.02 ms  min 3361.35 ms  max 3435.71 ms
```

See `benchmarks/speed_experiment.py` for the benchmark script.

## Installation

```bash
pip install gz2zip
```

Install as an isolated CLI tool with `pipx`:

```bash
pipx install gz2zip
```

or with `uv`:

```bash
uv tool install gz2zip
```

Run without installing globally:

```bash
pipx run gz2zip --help
uvx gz2zip --help
```

## Supported Python versions

Based on the official Python version support table, this package targets:

- Python 3.10
- Python 3.11
- Python 3.12
- Python 3.13
- Python 3.14

## CLI Usage

The package installs the `gz2zip` command:

```bash
gz2zip INPUT_GZ [-o OUTPUT_ZIP] [-n NAME_IN_ZIP] [-s UNCOMPRESSED_SIZE] [-t ISO_TIMESTAMP] [-q]
```

### Common examples

Convert a file and auto-derive output name (`input.gz` -> `input.zip`):

```bash
gz2zip input.gz
```

Set output path and internal filename:

```bash
gz2zip input.gz -o output.zip -n data.csv
```

Pipe from stdin to stdout:

```bash
cat input.gz | gz2zip - -n data.csv > output.zip
```

Large-file case (>4 GiB original content): provide exact uncompressed size:

```bash
gz2zip huge.sql.gz -o huge.zip -n huge.sql -s 5368709120
```

Run as a module:

```bash
python -m gz2zip input.gz -o output.zip
```

### CLI options

| Option | Description |
|---|---|
| `input` | Input `.gz` file. Use `-` (or omit) to read from stdin. |
| `-o, --output` | Output ZIP file. Use `-` for stdout. |
| `-n, --name` | Filename to store inside ZIP. |
| `-s, --size` | Known uncompressed size (recommended for >4 GiB input). |
| `-t, --timestamp` | Override timestamp in ISO 8601, e.g. `2025-12-31T23:59:58`. |
| `-q, --quiet` | Suppress non-error logs. |

## API Usage

### Synchronous (seekable files)

```python
from gz2zip import gzip_to_zip

with open("input.gz", "rb") as f_in, open("output.zip", "wb") as f_out:
    gzip_to_zip(f_in, f_out, filename_in_zip="data.csv")
```

### Asynchronous streaming

```python
import asyncio
from gz2zip import stream_gzip_to_zip

async def gzip_chunks():
    with open("input.gz", "rb") as f:
        while chunk := f.read(64 * 1024):
            yield chunk

async def main():
    with open("output.zip", "wb") as out:
        async for chunk in stream_gzip_to_zip(gzip_chunks(), "data.csv"):
            out.write(chunk)

asyncio.run(main())
```

## ZIP64 note

GZIP stores `ISIZE` modulo `2^32`, so files larger than 4 GiB need an explicit
`known_uncompressed_size` (`-s` in CLI) if exact size metadata is required.

## Limitation: concatenated GZIP members

Concatenated GZIP streams are valid (for example: `cat a.gz b.gz > joined.gz`),
but this project currently does **not** support that multi-member format
correctly. Use single-member `.gz` input files.

## Changes in 0.2.0

- File output is written to a temporary file in the destination directory and
  replaces the destination only after conversion and closing succeed. Failed
  conversions preserve existing output files.
- File input can be converted directly to a stdout pipe with `-o -`.
- `stream_gzip_to_zip` consistently yields `bytes`, including payload chunks.

File replacement creates a new file with private temporary-file permissions;
existing destination permissions and hard-link relationships are not preserved.
If the destination is a symbolic link, the link itself is replaced.
Special destinations such as FIFOs, devices, and sockets (including symlinks
to them) are rejected without replacement. To stream to one, use stdout and
shell redirection, for example `gz2zip input.gz -o - > output.fifo`.

## Development

Run tests:

```bash
uv run pytest
```
