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
- Template reference — how to write the YAML the engine renders
- SDK policy — the lifecycle contract every Shojiku SDK implements
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
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e39e7b3304a5db73313538388c1542ca2f5d957e19f01f3e7bb1f397f7989298
|
|
| MD5 |
4a2ed992e06f52d480aceaaf0190ef87
|
|
| BLAKE2b-256 |
9dcf982054d81cb69eea7ba54ed0141ea52307945a12ebb23f3405588699b8c4
|
File details
Details for the file shojiku-0.2.0-py3-none-manylinux_2_36_x86_64.whl.
File metadata
- Download URL: shojiku-0.2.0-py3-none-manylinux_2_36_x86_64.whl
- Upload date:
- Size: 5.0 MB
- Tags: Python 3, manylinux: glibc 2.36+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9398318bfa659552550c3cfec766f0285f300055bb202e36cb9d2272ce7a178b
|
|
| MD5 |
ca47225d142c6cdc1cd377824b7f3a8e
|
|
| BLAKE2b-256 |
51f7b30a407075a6e2029a0ee862953fe9c583e3935835e3c40b34514464f052
|
File details
Details for the file shojiku-0.2.0-py3-none-manylinux_2_36_aarch64.whl.
File metadata
- Download URL: shojiku-0.2.0-py3-none-manylinux_2_36_aarch64.whl
- Upload date:
- Size: 4.7 MB
- Tags: Python 3, manylinux: glibc 2.36+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
911da9caed00703404711ae5ca7b5ee58591e7bed8f7e6bc67e05204d4129103
|
|
| MD5 |
ce7abe9b59da2359f58629c8c53532a4
|
|
| BLAKE2b-256 |
0227485fdb932a4ff132ba80229e11c701d6628233943c738ab9d47d230d6c71
|
File details
Details for the file shojiku-0.2.0-py3-none-macosx_11_0_x86_64.whl.
File metadata
- Download URL: shojiku-0.2.0-py3-none-macosx_11_0_x86_64.whl
- Upload date:
- Size: 4.6 MB
- Tags: Python 3, macOS 11.0+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
70ad7e077773e72fcc238688fc6e9b5b2adbc400b5707f6f138daf810d0c9185
|
|
| MD5 |
f66f7effc9476d62c8ff99fb982d254f
|
|
| BLAKE2b-256 |
e44e42a8c6f964796c7478fe783cca44ec5e1b41d38abb93bc9928bacb62b8c2
|
File details
Details for the file shojiku-0.2.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: shojiku-0.2.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 4.3 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa7722205f33d3a057385c85ccd1fec3df1b96bbe7695355e1403ae537103594
|
|
| MD5 |
622acdc85ae11d500e011b364bf46b5f
|
|
| BLAKE2b-256 |
c790a1f565d2648069450d1617d82e37245d1efe3de9b26aae957daa891c5914
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
393dbe158330ce4481c569b2e7cc5470306160e872311911600b07464159e279
|
|
| MD5 |
250d7a5c582a702cec939248ddba6cd2
|
|
| BLAKE2b-256 |
000691c541c8d621f2ab7da6da2431aab4fe0332b42cbc3de2b9772a6e7e7616
|