mdzip-core (Python)
Read and write MDZip (.mdz) archives in Python: one or more Markdown documents and their images and other assets, packaged in a single ZIP file.
This library implements the MDZip specification 1.2.0. It's a standalone implementation (not a binding to the JavaScript or C# cores) that uses only the Python standard library, and it's tested against the specification's shared fixtures.
- Python 3.10+
- No dependencies
- Install
mdzip-core, importmdzip_core(notmdzip: an unrelated PyPI project namedmdzip, for molecular-dynamics data, installs a module with that name)
Reading
import mdzip_core
with mdzip_core.open("guide.mdz") as archive:
print(archive.title, archive.mode) # "My Guide", "document"
entry = archive.entry_point # e.g. "index.md" (Section 5.5)
text = archive.read_text(entry)
logo = archive.read_bytes("images/logo.png")
for doc in archive.documents: # every Markdown file
...
for finding in archive.validate(): # conformance notes, never raises
print(finding.level, finding.code, finding.message)
mdzip_core.open accepts a path, bytes, or a binary file object. The archive is read into memory; nothing is extracted to disk.
Opening an archive is safe for untrusted input:
- It rejects invalid or encrypted ZIPs, entry paths that are absolute, contain
..or reserved characters, and entries that duplicate each other after Unicode NFC normalization. - It rejects a
spec.versionfrom a newer major version, and unrecognizedmodevalues. mdzip_core.Limitscaps entry count, total and per-entry size, and compression ratio. Sizes are checked against the ZIP directory, then again while reading, since declared sizes can lie.- Symbolic links are never followed or read. They're listed in
archive.special_files(not infiles), with a warning. - Corrupt entry data (a bad checksum, broken compression, or data longer than declared) fails the read with
ERR_ZIP_INVALIDorERR_LIMIT_EXCEEDED.
archive.files lists file entries. Directory entries the archive stores, such as an empty folder, are in archive.directories (each ends in /); archive.find_directory("docs") finds one with or without the slash.
Path lookups follow the spec's matching rule: case-sensitive, after NFC normalization. So café.md stored either way is found, and Index.md doesn't match index.md.
The manifest is a plain dict (archive.manifest), so fields this library doesn't know are kept.
Writing
data = mdzip_core.create(
{"index.md": "# My Guide\n\nSee .\n", "images/logo.png": png_bytes},
title="My Guide", # defaults to the entry point's first heading
application={"name": "my-tool", "version": "1.0"},
)
open("guide.mdz", "wb").write(data)
data = mdzip_core.create_from_directory("my-guide/") # Markdown + images by default; see DEFAULT_FILTERS
Every archive gets a manifest.json with spec.version (1.2.0), entryPoint, title and mode, as the spec requires of producers. Paths are written in NFC with UTF-8 names. Markdown files get LF line endings; other files are written exactly as given, since some formats (Windows batch files, RFC 4180 CSV) depend on CRLF. Pass normalize_line_endings=False to keep CRLF in Markdown too.
Updating
data = mdzip_core.update(
"guide.mdz",
write={"images/new.png": png_bytes, "index.md": new_text},
remove=["old.md"],
manifest={"title": "Second edition"}, # None as a value deletes a field
)
update follows the spec's rules for modifying archives:
- Entries you don't touch are kept byte for byte, with their timestamps and compression method.
- Unknown manifest fields are kept, at every level.
- Directory entries are kept, including empty folders. Name one in
remove=to drop it. - Symbolic link entries are kept unchanged too, so the archive stays non-conforming and the warning from opening it still applies. Pass
drop_special_files=Trueor name them inremove=once the user agrees to remove them. - A manifest is added if the archive has none.
- The entry point must resolve to the same file afterwards. Pass
entry_point=to change it on purpose; otherwise the update fails withERR_ENTRYPOINT_CHANGED.
Links
target = archive.resolve_link("chapters/intro.md", "../../Notes.md")
target.kind # "internal", "host", "archive", "url" or "unresolved"
Links resolve per spec Sections 9.3–9.5:
- links inside the archive;
- relative links that climb out of it to files next to the
.mdz; - links into archives, like
Project.mdz/docs/design.md.
They're never clamped at the archive root. Without a host, links can reach the archive's own folder when it was opened from a file, and are unresolved when it was opened from bytes. Pass mdzip_core.FilesystemHost(root) to resolve against a larger folder, or implement LinkHost for anything else.
The host's folder is the trust boundary (spec Section 9.6): targets outside it are denied, whether or not they exist. Narrow it with FilesystemHost(root, trust_root="Notes"), or list targets the user allowed with allowed_paths=[...]. Pass use="embed" when resolving a resource the document loads automatically, such as an image. Embeds that leave the archive are denied unless the host sets load_outside_resources=True, and even then only inside the boundary.
Errors
Failures raise mdzip_core.MdzError with a stable code. Codes follow the spec's suggested categories (Section 16.5), for example ERR_ENTRYPOINT_INVALID, ERR_ENTRYPOINT_MISSING, ERR_DUPLICATE_PATH and ERR_ZIP_ENCRYPTED, plus ERR_LIMIT_EXCEEDED (in the spec's list since 1.2.1) and a few specific to this library (ERR_ENTRYPOINT_CHANGED, ERR_NOT_FOUND).
Core contract
How this library covers the required operations in the MDZip core library contract:
| Contract | Python |
|---|---|
| R1 open/read | mdzip_core.open / MdzArchive.open (path, bytes or file object) |
| R2 entries and metadata | files, directories, special_files, find, find_directory, in, info, read_bytes, read_text, manifest, entry_point, mode |
| R3 links | resolve_link |
| R4 validation and errors | validate() diagnostics; MdzError.code; open-time rejections |
| R5 creation | create, create_from_directory |
| R6 modification | update(write=, remove=, manifest=, entry_point=) |
| R7 safety | Path, duplicate, symlink and link rules on open; Limits (built-in defaults, configurable) |
Development
PYTHONPATH=src python -m unittest discover -s tests -t .
tests/fixtures/ is a copy of the spec's shared fixtures, made with the spec's sync tool and checked by SPEC-FIXTURES.json. Don't edit it by hand. To refresh it:
node ../mdzip-spec/tools/sync-fixtures.mjs tests/fixtures --ref v1.2.1
License
Apache-2.0
Metadata
Release files for mdzip-core 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mdzip_core-0.1.0.tar.gz | 38.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mdzip_core-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.0 kB
Release files / mdzip_core-0.1.0.tar.gz
| Download URL | mdzip_core-0.1.0.tar.gz |
|---|---|
| Size | 38.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5c27a94cb7a33bbae80e059aca51d7f0ecf4d44e1be9652e8eacc99798481633
|
|
BLAKE2b-256 checksum How to use checksums |
8b58e329867e102c87612a1dd9b109d5b2a7667348ef63330b1d98280c1ad1ba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Release files / mdzip_core-0.1.0-py3-none-any.whl
| Download URL | mdzip_core-0.1.0-py3-none-any.whl |
|---|---|
| Size | 31.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
06b5cbcb059bef39aca8dc407d28f4b59bc9a2a0b9676b67898355b81b7ac37d
|
|
BLAKE2b-256 checksum How to use checksums |
b19f20f597bf5e0a8737d6f4d779825de0ce3b1f199bebc2ccefab9fc0762b83
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Security for mdzip-core 0.1.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.