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 verify:sdk:python

test:sdk:python and lint:sdk:python are the faster slices. The engine library is injected already compiled; make 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

shojiku-0.2.0-py3-none-win_amd64.whl (10.8 MB view details)

Uploaded Python 3Windows x86-64

shojiku-0.2.0-py3-none-manylinux_2_36_x86_64.whl (5.0 MB view details)

Uploaded Python 3manylinux: glibc 2.36+ x86-64

shojiku-0.2.0-py3-none-manylinux_2_36_aarch64.whl (4.7 MB view details)

Uploaded Python 3manylinux: glibc 2.36+ ARM64

shojiku-0.2.0-py3-none-macosx_11_0_x86_64.whl (4.6 MB view details)

Uploaded Python 3macOS 11.0+ x86-64

shojiku-0.2.0-py3-none-macosx_11_0_arm64.whl (4.3 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

shojiku-0.2.0-py3-none-any.whl (44.3 kB view details)

Uploaded Python 3

File details

Details for the file shojiku-0.2.0-py3-none-win_amd64.whl.

File metadata

  • Download URL: shojiku-0.2.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 10.8 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for shojiku-0.2.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 e39e7b3304a5db73313538388c1542ca2f5d957e19f01f3e7bb1f397f7989298
MD5 4a2ed992e06f52d480aceaaf0190ef87
BLAKE2b-256 9dcf982054d81cb69eea7ba54ed0141ea52307945a12ebb23f3405588699b8c4

See more details on using hashes here.

File details

Details for the file shojiku-0.2.0-py3-none-manylinux_2_36_x86_64.whl.

File metadata

File hashes

Hashes for shojiku-0.2.0-py3-none-manylinux_2_36_x86_64.whl
Algorithm Hash digest
SHA256 9398318bfa659552550c3cfec766f0285f300055bb202e36cb9d2272ce7a178b
MD5 ca47225d142c6cdc1cd377824b7f3a8e
BLAKE2b-256 51f7b30a407075a6e2029a0ee862953fe9c583e3935835e3c40b34514464f052

See more details on using hashes here.

File details

Details for the file shojiku-0.2.0-py3-none-manylinux_2_36_aarch64.whl.

File metadata

File hashes

Hashes for shojiku-0.2.0-py3-none-manylinux_2_36_aarch64.whl
Algorithm Hash digest
SHA256 911da9caed00703404711ae5ca7b5ee58591e7bed8f7e6bc67e05204d4129103
MD5 ce7abe9b59da2359f58629c8c53532a4
BLAKE2b-256 0227485fdb932a4ff132ba80229e11c701d6628233943c738ab9d47d230d6c71

See more details on using hashes here.

File details

Details for the file shojiku-0.2.0-py3-none-macosx_11_0_x86_64.whl.

File metadata

File hashes

Hashes for shojiku-0.2.0-py3-none-macosx_11_0_x86_64.whl
Algorithm Hash digest
SHA256 70ad7e077773e72fcc238688fc6e9b5b2adbc400b5707f6f138daf810d0c9185
MD5 f66f7effc9476d62c8ff99fb982d254f
BLAKE2b-256 e44e42a8c6f964796c7478fe783cca44ec5e1b41d38abb93bc9928bacb62b8c2

See more details on using hashes here.

File details

Details for the file shojiku-0.2.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for shojiku-0.2.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 fa7722205f33d3a057385c85ccd1fec3df1b96bbe7695355e1403ae537103594
MD5 622acdc85ae11d500e011b364bf46b5f
BLAKE2b-256 c790a1f565d2648069450d1617d82e37245d1efe3de9b26aae957daa891c5914

See more details on using hashes here.

File details

Details for the file shojiku-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: shojiku-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 44.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for shojiku-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 393dbe158330ce4481c569b2e7cc5470306160e872311911600b07464159e279
MD5 250d7a5c582a702cec939248ddba6cd2
BLAKE2b-256 000691c541c8d621f2ab7da6da2431aab4fe0332b42cbc3de2b9772a6e7e7616

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page