Skip to main content

portable

ci

Does a data export actually contain what it promises? Checked category by category, found or missing.

A privacy policy or an export feature's own docs say "your export includes your profile, your order history, and your uploaded photos." Nobody automatically checks whether a real export actually contains all three. portable reads a real export and a declared list of categories, and reports which ones are actually there.

$ portable check export.json export-manifest.yaml
[OK] 'profile': user.email is present in the export
[OK] 'order_history': orders has 2 item(s)
[??] 'uploaded_photos' needs a directory export, but a JSON file was given

2/3 found, 1 unverified

(Real output.)

Scope -- read this before anything else

This is infrastructure for checking your own export against your own declared promises. It has no notion of any specific company, ships with no data about any real service, and makes no claim about anyone's actual compliance. Point it at an export you produced (your own app's export feature, tested against your own account) and a manifest you wrote (your own privacy policy's own claims) -- the same "sites I own" discipline witness already holds itself to for a different kind of claim-checking. Automatically testing a live third party's export feature and publishing findings about them is a different, much riskier thing this repo deliberately does not do and was not built to enable.

Install

pip install portable-evidence   # the command it installs is `portable`

(portable was already taken on PyPI -- same story as every sibling in this portfolio.)

Use

Declare what your export is supposed to contain, in export-manifest.yaml:

categories:
  - name: profile
    json_path: user.email        # a dot/bracket path into a JSON export
  - name: order_history
    json_path: orders
    min_count: 1                 # must be a list with at least this many items
  - name: uploaded_photos
    file_glob: "photos/*.jpg"    # for a directory-shaped export instead
  - name: order_history_csv
    file_glob: "Orders/*.csv"    # first non-empty match, if more than one
    csv_column: order_id         # + this column must have real values in it
    min_count: 1                 # applies to non-empty *rows* of that column

Then check a real export against it:

portable check export.json export-manifest.yaml        # a single JSON export
portable check export-directory/ export-manifest.yaml   # a directory export
portable check export.zip export-manifest.yaml          # a zip archive export

A .zip export (a Google-Takeout-style archive) is extracted to a scratch directory and checked exactly like an already-unpacked directory export -- file_glob and csv_column categories never need to know which one they got.

Each category needs exactly one of json_path (checked against a single JSON file export) or file_glob (checked against a directory or zip export, matched with pathlib.Path.glob, must resolve to at least one non-empty file). Pointing a json_path category at a directory/zip export, or a file_glob category at a JSON file, reports that one category as unverified -- not a failure, just the wrong export shape for what it needs, named clearly rather than silently skipped.

Adding csv_column to a file_glob category opens the first matching file as CSV and additionally requires that column to have at least min_count (default 1) non-empty values -- a category with the right filename but an empty or header-only CSV inside it would otherwise read as found from the filename match alone.

--json prints the full machine-readable report. Exit code is 1 only if a category comes back missing -- unverified never fails it, same convention every sibling tool in this portfolio shares.

What counts as "missing"

Not just "the key doesn't exist" -- a field that exists but is empty ("", [], {}, null) counts as missing too. The whole point of a category is proving real data landed in the export; a key that's technically present but holds nothing satisfies that no better than the key being absent.

What this does NOT do

  • No semantic understanding of what a category "should" contain. portable only checks presence and non-emptiness (and, for a list, a minimum count) -- it can't tell you whether the content at user.email is actually a real email address, or whether order_history contains every order that ever happened. That's a correctness question about the export's content, a different and harder problem than "is the category there at all."
  • No nested archives. A .zip export is extracted one level; a ZIP containing another ZIP inside it isn't recursed into. Real Google- Takeout-style exports don't nest this way, so it wasn't worth the extra code. Extraction has no size cap -- fine for the "your own export" scope this tool holds itself to (see "Scope," above), not something to point at a ZIP you don't already trust.
  • No wildcards, filters, or a real JSONPath grammar in json_path -- dot keys and [N] indices only. Every real manifest entry names one concrete field or list, not a query over the whole document.

Tests

pip install -e .
python tests/test_path.py       # the json_path resolver
python tests/test_manifest.py   # export-manifest.yaml validation
python tests/test_check.py      # found/missing/unverified classification
python tests/test_cli.py        # the real CLI entry point, real files, real argv

51 tests.

MIT licensed.

Release files for portable-evidence 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for portable-evidence 0.1.0
File Size Uploaded
portable_evidence-0.1.0.tar.gz 14.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for portable-evidence 0.1.0
File Interpreter ABI Platform
portable_evidence-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 24.2 kB

Release files / portable_evidence-0.1.0.tar.gz

Download URL portable_evidence-0.1.0.tar.gz
Size 14.1 kB
Tags Source
SHA-256 checksum
How to use checksums
d6432a5cb0f73e2dfce8f26ff8119727cdff859e2b3979f3a3c6508cd461b6d7
BLAKE2b-256 checksum
How to use checksums
8aa670bd043529f4520ccba93ea4ca0306b04db283e87583591c4d9dace66307
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.

Transparency log

Release files / portable_evidence-0.1.0-py3-none-any.whl

Download URL portable_evidence-0.1.0-py3-none-any.whl
Size 10.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8c8084dd19551126de95c0083182204b66aa4b0576c9a531214c799971eb4708
BLAKE2b-256 checksum
How to use checksums
ebce4621bdbb7d5c2827191886009ecccd76a08c4f10b02b243a6092a95ef0bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page