Skip to main content

Shojiku for Python

Python bindings for Shojiku — a document engine that turns a YAML template plus your data into a deterministic PDF, then signs and verifies the result.

Install

pip install shojiku

Wheels carry a prebuilt engine binary, so there is no build step on the supported platforms (Linux and macOS on x86-64 and arm64, Windows on x86-64). The binding itself is ctypes from the standard library, so the package has no runtime dependencies at all and works on every supported interpreter without compiling anything.

Usage

import shojiku

client = shojiku.Client(templates="app/templates")

result = client.generate("receipt", {"customer": {"name": "Yamada Shoji K.K."}})

if result.success:
    result.artifact.write("receipt.pdf")
else:
    for diagnostic in result.failure.diagnostics:
        print(diagnostic)

params may be a dict, or a string you already hold — the engine parses JSON or YAML, and a string is passed through verbatim:

client.generate("receipt", "customer:\n  name: Yamada Shoji K.K.\n")

To render one template in several locales, pass lang per call. It beats the client's own locale for that call only:

client.generate("receipt", params, lang="ja-JP")

Sources you already hold

When a template does not live in a directory this package can see — fetched from object storage, read out of a database, written inline — hand the sources over directly:

result = client.generate_source(
    template=template_yaml,        # source TEXT, never a path
    definitions=definitions_yaml,  # optional
    assets_dir="/srv/assets",      # optional; without it bundled images are off
    params={"customer": {"name": "Yamada Shoji K.K."}},
)

Fetching them stays your application's act — nothing here opens a socket. The template argument is source text, so a path-shaped value is a template that fails to parse: this entrance never reads a file, because an SDK that "helpfully" opened it would make every containment rule below bypassable by spelling the same thing differently.

Root containment does not apply to caller-supplied bytes — there is no root to be contained by. A deployment that wants to forbid this entrance entirely declares strict.

To re-enter a document you archived earlier, so it can be verified or signed again:

artifact = client.artifact(pdf_bytes)
artifact.verify(anchors="ca.pem")

Results, not exceptions

No lifecycle operation raises in the normal flow. A template that will not render, a key that will not sign, a signature that does not verify are all data you query:

result = client.generate("receipt", params)

result.success          # bool
result.failed           # bool
result.artifact         # the DocumentArtifact, or None
result.diagnostics      # what the engine noticed — on SUCCESS too
result.errors           # the diagnostics that are errors
result.warnings         # the ones that are warnings
result.failure          # the trace, on failure

Diagnostics ride on a success as well: a render that worked can still have warned about an overflowing box, and a caller that only looks at failures never sees them. Each one carries the engine's stable code and its typed args untranslated, so you can render your own message from them.

A failure is a value, not a control-flow event:

failure = result.failure
failure.step          # "generate" | "sign" | "verify" — this SDK's own step
failure.kind          # a stable machine-readable class
failure.message
failure.diagnostics
failure.causes        # this failure and everything under it, outermost first

For a script that would rather have a traceback than a branch, unwrap() raises instead of returning:

artifact = client.generate("receipt", params).unwrap()

Calling it on a failed result is programmer misuse — a caller who has not checked success is asserting the operation worked.

What does raise is programmer misuse (shojiku.UsageError) and an environment with no engine in it (shojiku.LibraryNotFoundError).

Templates

A template name is an identifier, never a path. Names resolve against the configured template root, laid out as <root>/<name>/templates.yml plus an optional definitions.yml and assets/ directory — the same shape as this repository's examples/*/.

Rejection rules are the union across platforms, not the host's: absolute paths, .. traversal, both separators, drive-relative names like C:receipt, UNC paths, control characters and reserved DOS device names (CON, NUL, COM1…) are refused on every platform, so a name that is valid on one machine is valid on all of them. The resolved directory must still be inside the root after canonicalization, which is what stops a symlink that a name-shape rule cannot see.

A refused name is a failed result, not an exception — a hostile name is a fact about the request, not a bug in your program. A name that is not a string at all is a bug, and raises.

Configuration

shojiku.configure(templates="app/templates", lang="ja-JP")

This feeds the constructor; it never adds a precedence level of its own:

explicit argument  >  shojiku.configure  >  SHOJIKU_*

for the template root and the pack directories. The engine library resolves the other way round — SHOJIKU_LIBRARY beats both — because where the engine lives is a deployment decision that has to be able to win over application code.

env=False disables every SHOJIKU_* lookup at once (the template root, the pack directories and the library path). One flag rather than one per variable: an application that wants a hermetic configuration wants all of it off.

strict is the one setting configure wins outright — see below.

Locking down where signable input comes from

Once this SDK signs what it renders, template input is a security boundary: whoever controls the bytes controls what gets signed. A strict client narrows where signable input may come from.

shojiku.configure(
    strict=True,
    providers={"invoice": shojiku.LocalPem(key="signer.key", cert="signer.crt")},
)

client.sign(artifact, "invoice")   # by NAME, not by object

A strict client refuses generate_source; signs only a document it rendered from the template root (an artifact carries its origin, and signing inherits it, so appending a revision cannot launder provenance); and takes signing material only as the name of a provider registered in configuration, so a key path never appears in request-handling code.

Verification is never restricted. Verifying bytes of unknown provenance is the point of verify, and a locked-down deployment is precisely the one that must check an archived document it did not produce.

A refusal raises UsageError rather than returning a failed result: strict disables an entrance, so calling it is the program contradicting its own deployment — not a fact about a document — and a failed result is something an if result.success: check can swallow.

Signing and verification

provider = shojiku.LocalPem(key="signer.key", cert="signer.crt")
signed = artifact.sign(provider)

report = signed.artifact.verify(anchors="ca.pem").report
report.valid
report.signature, report.coverage, report.certificate_validity, report.trust_chain
report.not_checked        # what this release did NOT look at

Material is explicit, never sniffed, in both directions: paths go to key / cert / anchors, bytes go to key_pem / cert_pem / anchors_pem, and passing both forms of the same thing raises rather than silently preferring one. A provider redacts itself when printed, because the default repr would dump the private key and passphrase into consoles and exception reporters.

Anchors are required — there is no fallback to the machine's trust store, because the engine never consults one and a default would answer a different question than you asked.

Verification fails closed. A signature that does not verify is a failed result, so a caller who checks only success is never told a forgery is fine. The report rides that failed result anyway, because not_checked must reach you either way. A document that cannot be evaluated at all — no signature, unreadable container — has no report, which is a different fact from an empty one.

Logging

Optional, silent by default, and host-side only. Any object with a debug method is accepted, so this package grows no logging dependency:

import logging

client = shojiku.Client(templates="app/templates", logger=logging.getLogger("shojiku"))

It reports what the binding did — which library it loaded and which lookup position won, the ABI revision, which lifecycle step ran, how long it took, whether it worked. Never params, document bytes, key material, a passphrase, or the engine's diagnostics: a log line is the easiest way for a secret to leave a process, and the diagnostics belong to the result you already hold.

Threads

Every operation may be called from several threads at once, and concurrent calls produce identical bytes for identical input. This binding loads the engine with ctypes.CDLL, which releases the GIL around each foreign call, so a long render does not block the rest of your process.

Signing with a key this process never holds

When the private key lives in a cloud KMS, an HSM or a smartcard, use ExternalSigner instead. Shojiku hands out the bytes a signature has to cover; your code signs them wherever the key is and hands the signature back, so the key never enters your application:

provider = shojiku.ExternalSigner(
    lambda to_be_signed: kms.sign(
        KeyId=os.environ["KEY_ID"],
        Message=to_be_signed,
        MessageType="RAW",
        SigningAlgorithm="ECDSA_SHA_256",
    )["Signature"],
    cert="signer.crt",
    algorithm=shojiku.Algorithm.ECDSA_P256_SHA256,
)
signed = client.sign(artifact, provider)

The call site does not change — which provider you pass is the only difference, and a provider registered by name works the same way under a strict client. This package ships no cloud client of its own: the callback is whichever client your application already uses.

Two details worth getting right. The bytes you are handed are the CMS signed attributes, not the document's digest — a service that signs a digest must hash these bytes with SHA-256 itself. And the signature is that operation's raw output: PKCS#1 v1.5 bytes for rsa-pkcs1-sha256, an ASN.1 DER sequence for ecdsa-p256-sha256, which is what AWS KMS and Google Cloud KMS both return unchanged.

A failure inside your own code is not swallowed into a failed result: an outage at your key service is not a fact about the document.

Development

Nobody needs Python installed to work on this package — the gates run in a container, like every other gate in this repository:

make sdk:python:verify

sdk:python:test and sdk:python:lint are the faster slices. The engine library is injected already compiled; make engine:capi-lib builds it.

Requirements

Python 3.11 or newer.

Documentation

License

Licensed under any of Apache-2.0, MIT, or BSD-3-Clause, at your option.

Metadata

Release files for shojiku 0.3.0

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

Built distributions (wheels)

Table of built distributions (wheels) for shojiku 0.3.0
File
shojiku-0.3.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
shojiku-0.3.0-py3-none-manylinux_2_36_x86_64.whl Python 3 none Linux glibc 2.36+ x86-64 Details
shojiku-0.3.0-py3-none-manylinux_2_36_aarch64.whl Python 3 none Linux glibc 2.36+ ARM64 Details
shojiku-0.3.0-py3-none-macosx_11_0_x86_64.whl Python 3 none macOS 11.0+ x86-64 Details
shojiku-0.3.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
shojiku-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 30.5 MB

Release files / shojiku-0.3.0-py3-none-win_amd64.whl

Download URL shojiku-0.3.0-py3-none-win_amd64.whl
Size 11.3 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
fd71b1accea4af58c8e08a58e8ab947df04e9049a451638e53a9ecf9868f58f1
BLAKE2b-256 checksum
How to use checksums
df9ce6cc112f8904916812dd235b9d67fcc8c5baf08bd6ba3f71114368557ce7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / shojiku-0.3.0-py3-none-manylinux_2_36_x86_64.whl

Download URL shojiku-0.3.0-py3-none-manylinux_2_36_x86_64.whl
Size 5.1 MB
Tags Linux glibc 2.36+ x86-64 Python 3
SHA-256 checksum
How to use checksums
4eda2809db5e2b2113d389da9f144284c1db9f1f9326f7e1c515043d6d5ab5e2
BLAKE2b-256 checksum
How to use checksums
8fbf212e70a1533345022a103c2cf9792c779bd466679d6325351534f2c3d358
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / shojiku-0.3.0-py3-none-manylinux_2_36_aarch64.whl

Download URL shojiku-0.3.0-py3-none-manylinux_2_36_aarch64.whl
Size 4.9 MB
Tags Linux glibc 2.36+ ARM64 Python 3
SHA-256 checksum
How to use checksums
90a9855cae26010a809e8aaf242502ff9fc2686dda715eeeda044be1d947087d
BLAKE2b-256 checksum
How to use checksums
cf90260efdfbe525fdd3fc531c1bdc3035e819199e4ddb99245e016af8ce9fe7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / shojiku-0.3.0-py3-none-macosx_11_0_x86_64.whl

Download URL shojiku-0.3.0-py3-none-macosx_11_0_x86_64.whl
Size 4.7 MB
Tags Python 3 macOS 11.0+ x86-64
SHA-256 checksum
How to use checksums
eec04507412fb9e658424c047ea5ae2094f0a8a717c5318b5ab26219a8c36832
BLAKE2b-256 checksum
How to use checksums
be7fe6816faa6f71d3b34bd25e068f7540ef3ccab6b514f9a0514874e7301d1c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / shojiku-0.3.0-py3-none-macosx_11_0_arm64.whl

Download URL shojiku-0.3.0-py3-none-macosx_11_0_arm64.whl
Size 4.5 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
95fd2a804ca916983173386b80ade699a25263b03a3be202f6857f698bb438a1
BLAKE2b-256 checksum
How to use checksums
5dc172e7f08ef5ea5c4e6a6e2a9f6bd33dd88aef87f1d93daf53988715fd5eff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / shojiku-0.3.0-py3-none-any.whl

Download URL shojiku-0.3.0-py3-none-any.whl
Size 44.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
037b71c34b680c990755dba68e1259968568bf023c4327a35bbd97d787f006f3
BLAKE2b-256 checksum
How to use checksums
5e58006fbd1aacde060e04f0cb9ee5286cda3b36e40146b4349d3a22bc0b21c3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.3.0 This release

6 release files

0.2.0

6 release files

0.1.0

6 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