Metadata-Version: 2.5
Name: envstamp
Version: 0.2.0
Summary: Atomic, reproducible fingerprints of installed Python distributions.
Project-URL: Repository, https://github.com/Chamkang6093/envstamp
Author: Cham
License-Expression: MIT
License-File: LICENSE
Keywords: content-hashing,installed-packages,version-tracking
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# envstamp

Atomic, reproducible fingerprints of installed Python distributions.

`envstamp` is a small stdlib-only package for recording installed package versions and content
fingerprints in an atomic JSON stamp.

## Installation

```bash
pip install envstamp
```

`envstamp` requires Python 3.12 or newer.

## CLI

Get a stamp for one or more installed distributions and print it:

```bash
envstamp get numpy pandas
```

Search explicit distribution paths:

```bash
envstamp get numpy pandas --paths /path/to/site-packages
```

Attach string metadata by repeating `--metadata`:

```bash
envstamp get numpy pandas \
    --metadata service=worker \
    --metadata environment=production
```

Every stamp contains a metadata object, which may be empty. Its keys and values are strings, keys
are stored in sorted order, and repeated CLI keys are rejected.

Atomically write the stamp to a file:

```bash
envstamp get numpy pandas --output /tmp/envstamp.json
```

Read the saved stamp:

```bash
envstamp read /tmp/envstamp.json
```

Print package names and versions on one line in canonical-name order:

```bash
envstamp get numpy pandas --summary
# numpy==2.5.3, pandas==3.0.6
```

`--summary` and `--output` are mutually exclusive.

Each stamp's top-level fingerprint contains the algorithm and aggregate SHA-256 of its package
records. Each package record contains its canonical name, installed version, package-level
SHA-256, and the number of files included in that digest. Package records are sorted by canonical
name.

## Python API

```python
import sys

from envstamp.stamp import get_stamp, read_stamp, write_stamp


stamp = get_stamp(
    ["numpy", "pandas"],
    paths=sys.path,
    metadata={"service": "worker", "environment": "production"},
)
write_stamp("/tmp/envstamp.json", stamp)

saved = read_stamp("/tmp/envstamp.json")
print(saved.summary)
print(saved.fingerprint["sha256"])
for package in saved.packages:
    print(package.canonical_name, package.version, package.sha256)
```

Pass the distribution search paths explicitly through `get_stamp(..., paths=...)`.
Metadata does not participate in package or stamp content hashes.

`envstamp` supports regular, non-editable installations. It reads
`.dist-info/direct_url.json` when present and rejects the distribution when
`dir_info.editable` is `true`, because that metadata points at a mutable source tree rather than a
stable installed payload.

## Fingerprint protocol

The `envstamp-sha256-v1` algorithm produces a content fingerprint for each package and an
aggregate fingerprint for the complete package set.

### Package fingerprint

A package fingerprint starts from its installed distribution manifest, but only includes entries
that resolve inside its installation root, normally `site-packages`. This covers Python code,
native libraries such as `.so` and `.pyd`, and package data. Distribution metadata, bytecode
caches, `.pth` and `.egg-link` path files, launch scripts, and external data are excluded.

RECORD hashes are not used as an inclusion filter. When an included entry has no RECORD hash,
`envstamp` still reads and hashes the installed file directly. If that file is missing,
fingerprinting fails instead of silently producing a partial digest.

File paths are sorted before the package digest hashes the protocol tag followed by
`path\0sha256\0size\n` records. The read buffer size does not affect the resulting digest.

### Stamp fingerprint

Package records are sorted by canonical name before the stamp digest hashes the protocol tag
followed by `name\0version\0sha256\0count\n` records. The resulting algorithm and aggregate digest
are stored in `fingerprint.algorithm` and `fingerprint.sha256`.

`get_stamp()` fingerprints the complete package set twice and only returns when both results are
identical. If the installed environment changes while the stamp is being generated, it raises
`FingerprintError` instead of returning a mixed snapshot.

## Atomic writes

Each stamp file belongs to one writer and contains one complete stamp. A write creates a
same-directory temporary file, flushes it, and atomically replaces the previous stamp file.
On POSIX, readers therefore observe either the previous complete stamp or the new complete stamp.
Concurrent writers to the same path are not supported; use a separate file for each process.
When the stamp's parent directory does not exist, `envstamp` creates it with mode `0755`; existing
directories keep their current permissions. Completed stamp files use mode `0444`. Failed writes
remove their temporary file, and the next write removes any temporary file left by interruption.
