SymageDocs Python SDK
Generate synthetic documents, identities, and tabular datasets for testing, ML training, and compliance.
Installation
pip install symagedocs
For progress bars during long jobs:
pip install symagedocs[progress]
Quick Start
from symagedocs import Client
client = Client(api_key="sk_live_...")
# List available forms
forms = client.forms.list()
for f in forms:
print(f"{f.id}: {f.name} ({f.credit_cost} credits)")
# Generate 100 W-2 documents
# JSON ground truth and CSV are always included in the dataset zip — no need to request them.
job = client.generate.create(
"irs_w2_single_page_2025",
quantity=100,
output_formats=["pdf_typed"], # see "Output formats" for all valid tokens
# Augmentation knobs. `degradation_profile` affects credit cost —
# `scanned`/`faxed` add 20%, `photographed` 30%, `mixed` 25% (`clean` = no surcharge).
# `coherence_mode` controls cross-form identity correlation in multi-form jobs.
degradation_profile="scanned",
coherence_mode="coherent",
)
result = client.generate.wait(job.job_id) # polls until complete
# "dataset" = one zip with every artifact + manifest.json; "json" and "csv"
# fetch just that ground-truth slice — see "Downloading results".
client.generate.download(job.job_id, "dataset", "./w2_documents.zip")
# Per-item training data
job = client.generate.create(
form_id="irs_w2_single_page_2025",
quantity=10,
output_formats=["pdf_typed", "bio"],
idempotency_key="my-retry-safe-key",
)
client.generate.wait(job.job_id)
for example in client.generate.iter_training_examples(job.job_id, format="bio"):
print(example.item_id, len(example.bio.tokens))
# Generate tabular data from a description
schema = client.tabular.parse("name, age, SSN, city, state, annual income")
tab_job = client.tabular.generate(columns=schema.columns, quantity=5000)
client.tabular.wait(tab_job.job_id)
client.tabular.download(tab_job.job_id, "csv", "./dataset.csv")
# Check credit balance
balance = client.account.balance()
print(f"Credits used: {balance.credits_used}")
Authentication
Get your API key at symagedocs.ai/account?tab=api.
# Pass directly
client = Client(api_key="sk_live_...")
# Or set environment variable
# export SYMAGEDOCS_API_KEY=sk_live_...
client = Client() # reads from env
Async Support
from symagedocs import AsyncClient
async with AsyncClient(api_key="sk_live_...") as client:
forms = await client.forms.list()
job = await client.generate.create("irs_w2_single_page_2025", quantity=10)
result = await client.generate.wait(job.job_id)
Configuration
client = Client(
api_key="sk_live_...",
base_url="https://symagedocs.ai", # custom server
timeout=30.0, # request timeout (seconds)
max_retries=3, # retry on 429/5xx
)
Method Reference
Forms
| Method | Description |
|---|---|
forms.list(category=None) |
List available forms, optionally filtered by category |
forms.get(form_id) |
Get detailed form info including field definitions |
Generation
| Method | Description |
|---|---|
generate.create(form_id=None, *, form_ids=None, quantity=1, output_formats=["pdf_typed"], config=None, seed=None, webhook_url=None, ink_color=None, ink_color_distribution=None, writer_consistency=None, degradation_profile=None, coherence_mode=None, fill_scenarios=None, idempotency_key=None) |
Create an async generation job. Pass either form_id (single form) or form_ids (coherent multi-form generation across the same identity). output_formats values and their pairing rules are listed under output formats. ink_color must be "black", "blue", or "red"; ink_color_distribution (when set) is a weight map over those same colors that must sum to exactly 100 and overrides ink_color. writer_consistency is "per_document" (default) or "per_field". degradation_profile and coherence_mode are typed kwargs over what used to live inside config={...} — see the augmentation knobs section; config={"label_scheme": ...} selects the ML label vocabulary — see training data. fill_scenarios is a typed kwarg for the declarative partial-fill / payer policy (list of FillScenario) — see partial fill. idempotency_key attaches an Idempotency-Key header so retries within 24 hours return the original job_id and don't double-charge. The deprecated realism_level API field is intentionally not exposed; call the REST API directly if you need it. |
generate.list_jobs(limit=50, cursor=None, status=None) |
List generation jobs (cursor-paginated) |
generate.get_job(job_id) |
Get full job status and progress |
generate.list_downloads(job_id) |
List per-artifact presigned download URLs for a completed job |
generate.download(job_id, format="dataset", path=".") |
Download job output to a local file. format is exactly one of "dataset" (default), "json", "csv" — anything else raises ValueError client-side. Allowed for terminal-but-not-completed jobs (CANCELED / FAILED / EXPIRED) so partial output is recoverable. Details under downloading results. |
generate.download_dataset(job_id, out_dir, parallel=8, resume=True) |
Download a dataset (single or sharded layout) into a directory: manifest, README, and archive(s), with parallel shard fetch and size verification. resume=True skips shards already on disk. See downloading results. |
generate.download_readme(job_id, path=".") |
Download the job's dataset card on its own, as README_<job_id>.md when path is a directory (job-scoped, so it cannot overwrite your own README.md). The same bytes download_dataset() writes into its output directory. Raises NotFoundError if the job never finished packaging. |
generate.wait(job_id, poll_interval=3.0) |
Poll until the job reaches a terminal state. Returns the final Job on completion; raises ConflictError if the job failed. Shows a progress bar when tqdm is installed (pip install symagedocs[progress]). |
generate.cancel(job_id) |
Cancel a running job. Idempotent. Items rendered before the cancel observed remain downloadable via download(format="dataset"). |
generate.list_items(job_id, limit=50, cursor=None) |
List per-item records for a job. Cursor-paginated; each item carries its presigned download URLs. |
generate.download_item(job_id, item_id) |
Presigned S3 URLs for one item's files. |
generate.get_bio_labels(job_id, item_id) |
Client-side helper: fetches the item's _bio.json sidecar and returns a parsed BioDataset. |
generate.get_word_annotations(job_id, item_id) |
Client-side helper: fetches the item's _words.json sidecar and returns parsed WordAnnotations. |
generate.iter_training_examples(job_id, format="bio") |
Client-side helper: iterates all items, yielding training examples in the chosen format ("bio" (default), "funsd", "donut"). |
client.generationalias.client.generationandclient.generatereference the same resource — use whichever name you prefer.
Identities
| Method | Description |
|---|---|
identities.generate(quantity=1, config=None, seed=None) |
Generate raw synthetic identities as JSON |
Tabular
| Method | Description |
|---|---|
tabular.parse(prompt) |
Convert natural language to a column schema (LLM-powered) |
tabular.generate(columns, quantity=100, output_formats=["csv"], seed=None) |
Create a tabular generation job |
tabular.status(job_id) |
Get tabular job progress and ETA |
tabular.download(job_id, format, path) |
Download tabular output to a local file. format is "csv" or "json". |
tabular.wait(job_id, poll_interval=2.0) |
Poll until tabular job completes or fails |
Account
| Method | Description |
|---|---|
account.balance() |
Get credit balance (credits_used, credits_allocated) |
account.usage(days=30) |
Get usage summary for the specified period |
Pricing
The pricing endpoints are public/unauthenticated on the backend, but the SDK still requires an API key at construction time for consistency; the auth header is sent and ignored by these routes.
| Method | Description |
|---|---|
pricing.rates() |
Get the current credit rate constants (CSV per-row rate, PDF base + surcharge bands, multipliers, …) |
pricing.estimate(*, field_count, output_formats, record_count, degradation_profile=None) |
Estimate the credit cost of a hypothetical job before submitting it |
Health
| Method | Description |
|---|---|
client.health() |
Lightweight reachability probe (GET /api/v1/health). Returns the parsed JSON body. Works on both Client and AsyncClient. |
Output formats
generate.create(output_formats=[...]) accepts exactly these tokens; any other value is rejected with 400 code=invalid_output_format:
| Token | Produces |
|---|---|
pdf_typed |
Filled PDF with typed text |
pdf_handwritten |
Filled PDF rendered in synthetic handwriting |
pdf_filled |
Completed PDF with values in live, editable AcroForm widgets (fillable forms only) |
png_typed |
Per-page PNG rasterizations of pdf_typed (requires pdf_typed) |
png_handwritten |
Per-page PNG rasterizations of pdf_handwritten (requires pdf_handwritten) |
bio |
BIO-tagged tokens with spatial positions (ML) |
coco |
COCO object-detection annotations (ML) |
yolo |
YOLO detection annotations (ML) |
donut |
Donut gt_parse ground truth (ML) |
Rules enforced at job creation:
- Foundational ground truth is always included. Per-instance JSON, tabular CSV, and FUNSD per-page annotations ship in every dataset automatically;
"csv","json", and"funsd"are not requestable tokens and return400. - PNG travels with its PDF.
png_typedrequirespdf_typedin the same request;png_handwrittenrequirespdf_handwritten. - ML formats are feature-gated.
bio/coco/yolo/donutrequire theml-output-formats-enabledfeature flag on your account (400 code=ml_formats_disabledotherwise) and at least one render format (pdf_typed,pdf_handwritten, orpng_typed) in the same request, since annotations are derived from the render pipeline. pdf_filledis fillable-forms-only and not a render surface. Requesting it for a non-fillable form rejects the whole submission with400 code=format_unsupported_for_form(error.details.unsupported_form_idslists the offenders). It delivers live, editable AcroForm widgets rather than a flattened render, so it cannot satisfy the ML-format render dependency, is never paired with a PNG, and is unaffected bydegradation_profile. Filled PDFs land underpdfs/filled/in the bundle.
Downloading results
generate.download(job_id, format="dataset", path=".") accepts exactly three formats:
dataset(default) — one zip with every artifact the job produced (PDFs, PNGs, ML annotations, per-item JSON ground truth, tabular CSV) plus amanifest.jsondescribing the contents. The response is streamed to disk in 64 KiB chunks, so multi-GB datasets download with flat memory use. Readmanifest.splitsfor the train/validation/test partition rather than assuming all three exist: the fixed 80/10/10 ratios are floor-allocated, so a small job can floor a split to zero items, and any split that did is omitted frommanifest.splitsand named in the manifest'somitted_splits.json— flat per-instance JSON array (no images/PDFs).csv— tabular identity data.
Anything else raises ValueError client-side before a request is made. (The pre-rename token bundle is not accepted by the SDK.)
When path is a directory (the default "."), a filename is appended automatically: symagedocs_<job_id>.zip for dataset, <job_id>.json for json, <job_id>.csv for csv, and README_<job_id>.md for download_readme(). (download_dataset() writes a plain README.md into its output directory, which is job-scoped by construction.)
Sharded datasets. Very large jobs are stored as a sharded dataset, which has no single archive; download(format="dataset") then raises ValueError pointing you at download_dataset(). download_dataset(job_id, out_dir, parallel=8, resume=True) is the universal accessor and works for both layouts: it fetches manifest.json and README.md (the latter best-effort — a job can finish with no dataset card, and a missing card does not fail the download; use download_readme() if you need the 404 surfaced), then either dataset.zip (single layout) or preview.zip followed by shards/shard_NNNNN.zip downloaded in parallel with size verification against the manifest (sharded layout). With resume=True, shards already on disk whose size matches the manifest are skipped, so a partially-failed download can be retried cheaply.
Job states. Downloads are allowed for terminal-but-not-completed jobs (CANCELED / FAILED / EXPIRED) so partial output is recoverable; downloading a job that is still running returns 409.
Tabular jobs have their own surface: tabular.download(job_id, format, path) accepts "csv" or "json".
Job visibility and account scoping
Jobs belong to the account that created them. generate.list_jobs(),
generate.get_job(), generate.list_downloads(), generate.download() /
download_dataset(), generate.list_items() / download_item() and
generate.cancel() are all filtered to the jobs owned by the account behind the
API key the client is using. There is no cross-account view.
There are two ways a job is created, and each stamps the owning account:
| Created via | How | Owner |
|---|---|---|
web_session |
Submitted from the SymageDocs web app while signed in | The signed-in user's account |
api_key |
client.generate.create(...) (POST /v1/generate) |
The account the key belongs to |
Your key sees your own web-app jobs. It does not see jobs created in someone
else's browser session or with someone else's key: those return 404 NotFoundError with the message "Job not found in this account" — the same
404 a job id that never existed returns, because existence is not leaked
across accounts. A 404 on a job id someone handed you usually means the job
lives in their account.
To let a partner pull a job with their own key, create the job with their
key — have them issue a key on their account and run
client.generate.create(...) with it. A job submitted from your browser
session, or with your key, can never be listed or downloaded with theirs; you
would have to download the archive yourself and transfer it out of band.
The delivered bundle's manifest.json records the channel in its top-level
created_via field ("web_session" or "api_key"); it is absent on jobs
created before the field existed.
The manifest's top-level generator block records which build produced the
dataset: git_shas lists every commit that produced a per-build record for the job
(more than one means at least two builds worked on it, usually because it
straddled a deploy and the documents mix both revisions),
render_versions the rendering-contract hashes, finalizer_git_sha the build
that packaged it, and stamps_seen / stamps_unreadable / guard_key how
strong that evidence is (stamps_seen comes from one listing of the per-build
records, taken after the dataset's files are fetched and immediately before
its manifest is written — not a guarantee that every chunk was accounted for).
The block always carries scope: "dataset": it describes the whole dataset
even when you read it out of a preview or a single shard's manifest, where the
counts around it are scoped to that archive. Two datasets came from the same
generator code when
their git_shas are equal, non-empty, free of "dev" (every local build
reports "dev"), and both report stamps_unreadable: 0; otherwise the
artifact cannot answer the question. That one listing feeds every copy of the
block for a given dataset — the in-zip manifest, the top-level manifest, and
every shard fragment — and publication of that listing is claimed by a single
finalizer at a time, so in normal operation the copies agree; if the listing
itself turns up genuine evidence of more than one render build, the job fails
outright with a full refund rather than shipping the inconsistency. That claim
can still be taken over — from a finalizer that stalls past its lease, or an
operator's reset of a job back to PENDING while a message for it is still in
flight — so a mismatch between copies remains possible in those cases and is
still worth checking for if you're auditing a dataset you don't trust.
Within that one listing the count can still be wrong in both directions: too
low when a record could not be read, too high when a retried part of the job
leaves the superseded attempt's record behind while its documents are
replaced. Read two or more git_shas as "at least two builds produced
records for this job" — grounds to refuse, not proof that two are present in
the bytes. download_dataset() writes manifest.json
to the output directory but does not return a parsed model — read the file, or
validate it with the DatasetManifest / DatasetManifestGenerator models if
you want typed access.
Download links are short-lived presigned S3 URLs, separate from the 48-hour
retention window on job output: 1 hour for the per-file URLs returned by
GET /v1/jobs/{job_id}/downloads and the per-item download endpoint, and 15
minutes for the redirects behind download(format="dataset") and the
dataset/manifest, dataset/preview, dataset/annotations and
dataset/shards/{n} endpoints. The SDK follows those redirects immediately, so
the limit only matters if you capture a URL and use it later.
generate.list_items() and generate.download_item() are the one exception
to the 48-hour retention window every other download surface on a job has:
they stay available until the underlying files are deleted by storage's own
7-day lifecycle rule, not 48 hours after the job completes. Don't rely on
that as a permanent archive.
Training data
Request ML annotation formats alongside a render format, then iterate per-item training examples:
job = client.generate.create(
"irs_w2_single_page_2025",
quantity=50,
output_formats=["pdf_typed", "bio"],
config={"label_scheme": "nist3"}, # default: "semantic_concept"
)
client.generate.wait(job.job_id)
for ex in client.generate.iter_training_examples(job.job_id, format="bio"):
print(ex.item_id, len(ex.bio.tokens)) # BIO tags + word boxes
iter_training_examples(job_id, format=...)yields"bio"(default),"funsd"(one example per page, withpage_indexset), or"donut"examples.config["label_scheme"]selects the annotation vocabulary:semantic_concept(default — concept names likesocial_security_number),nist3(3-classname/ssn/data),field_id(form field IDs), orfield_type(e.g.ssn,currency,text). Unknown values return400 code=invalid_label_scheme.- Per-item sidecars are also fetchable directly:
get_bio_labels(job_id, item_id)andget_word_annotations(job_id, item_id).
See the API User Manual's Training Data section for the full annotation schemas and Donut consumer conventions.
Seeds and sibling jobs
seed makes a job reproducible within a generator version. It is spent as an
offset: under the default coherent mode (and under shuffled) the
identity for item i is seeded seed + i, so a job occupies the contiguous
seed range [seed, seed + quantity).
That means two jobs whose seeds are closer together than their quantity
generate the same people — item 0 of a job seeded s + 1 is item 1 of a job
seeded s. Each dataset is internally distinct, so this only shows up when you
pool sibling jobs into one training corpus. Space the seeds of jobs you intend
to pool at least quantity apart:
QUANTITY = 250
BASE_SEED = 20260910
# 20260910, 20261160, 20261410, 20261660 — no shared identities.
for n in range(4):
client.generate.create(
"bank_statement_national_2026",
quantity=QUANTITY,
output_formats=["pdf_typed"],
seed=BASE_SEED + n * QUANTITY,
)
coherence_mode="random" mixes (seed, item_index, form_id) instead of
offsetting, so the rule above is stated for coherent and shuffled. A
multi-statement packet (sequence_length) does not change it: quantity
counts items, and one item is one identity seed however many statements it
delivers.
Augmentation knobs
Two of the most-used keys in the freeform config={...} dict on
generate.create are also exposed as typed kwargs:
degradation_profile: Literal["clean", "scanned", "faxed", "photographed", "mixed"] | Nonecoherence_mode: Literal["coherent", "shuffled", "random"] | None
Why bother? Two reasons:
degradation_profileaffects credit cost. Non-cleanprofiles need extra rendering work (rasterization, noise, paper warp), so the billing engine applies a multiplier:scanned/faxedare billed at 1.2×,mixedat 1.25×, andphotographedat 1.3×. A typo on the freeformconfig={...}form silently falls back to the default 1.0× multiplier — meaning you don't get the degradation you asked for AND the typo isn't caught until you notice the artifacts (or don't). The typed kwarg form catches typos at type-check time.- Pre-flight validation. The Literal types fence off unknown
values at edit time in any IDE that supports type checking. The
backend also rejects unknown values with
400for both knobs, so even untyped callers get a fast failure — but the typed form catches the mistake before the network round-trip.
One value-specific constraint: coherence_mode="shuffled" requires
quantity > 1. A single-item job cannot be shuffled (a 1-element
permutation is the identity), so the API rejects the combination with
400 code=invalid_coherence_mode instead of silently returning
coherent output.
The SDK exports the canonical value tuples too:
from symagedocs import DEGRADATION_PROFILES, COHERENCE_MODES
assert "scanned" in DEGRADATION_PROFILES
assert "coherent" in COHERENCE_MODES
If you pass a value via both forms (e.g. config={"degradation_profile": "X"} AND degradation_profile="Y"), the value in config wins and a
RuntimeWarning is emitted so the conflict isn't silent.
# Typed kwarg form — recommended.
job = client.generate.create(
"irs_w2_single_page_2025",
quantity=100,
degradation_profile="scanned", # billed at 1.2× — see above
coherence_mode="coherent",
)
# Equivalent freeform form — still supported, but typos cost money.
job = client.generate.create(
"irs_w2_single_page_2025",
quantity=100,
config={"degradation_profile": "scanned", "coherence_mode": "coherent"},
)
Partial fill (fill_scenarios)
fill_scenarios is a declarative partial-fill / payer policy: instead of
filling every field, you describe named, weighted scenarios and the job
quantity is split across them deterministically by seed. It is the typed
kwarg alias for config["fill_scenarios"] (ADR-072). The canonical use case is
healthcare intake: a payer or EHR prefills the patient, insurance, and
diagnosis/procedure blocks, and the provider completes (or leaves blank) the
rest.
Each scenario assigns every field one of three stages:
prefilled— value synthesized (or pinned viavalues); rendered as typed text on every surface, including inside handwritten output. Models machine prefill by the payer/EHR.completed— value synthesized, rendered surface-native. The default stage and the pre-existing behavior.blank— no value; renders empty and appears as""in the answer key, absent from every bbox label surface.
Fields are addressed in prefilled / blank by exact field id, or by a
semantic-concept glob "concept:<glob>" matched against the field's
semantic_concept (fetch the addressable set from
client.forms.get(form_id) — each field carries semantic_concept,
structural_role, and entity_role). values pins exact field ids to scalar
literals (which forces those fields to prefilled). default_stage
("completed" or "blank") covers every field not matched by a selector or
pin. The same scenario definition therefore produces the intake artifact
(default_stage="blank") or the completed document (default_stage="completed").
from symagedocs import Client, FillScenario
client = Client(api_key="sk_live_...")
# Weighted: 3-in-4 documents are payer-prefilled intake artifacts (everything
# the provider hasn't filled yet is genuinely blank); 1-in-4 are fully
# completed. One shared prefill set, two default stages.
scenarios: list[FillScenario] = [
{
"name": "payer-intake",
"weight": 3,
"default_stage": "blank",
"prefilled": [
"concept:healthcare.patient.*",
"concept:insurance.*",
"concept:healthcare.diagnosis.*",
"concept:healthcare.procedure.*",
],
"values": {"box11c_plan_name": "AETNA PPO"}, # pin a literal
},
{
"name": "completed",
"weight": 1,
"default_stage": "completed",
"prefilled": ["concept:healthcare.patient.*", "concept:insurance.*"],
},
]
job = client.generate.create(
"cms_1500_standard_02_12",
quantity=1000,
output_formats=["pdf_typed"],
seed=42,
fill_scenarios=scenarios,
)
Ground truth records the policy: each per-document JSON carries
_metadata.fill = {"scenario": <name>, "stages": {field_id: stage}}, so
blank-by-policy is distinguishable from blank-because-unset.
Two rules to know:
- Duplicate is an error. Passing
fill_scenariosvia both the kwarg andconfig={"fill_scenarios": [...]}raisesValueErrorclient-side (unlike the scalar augmentation knobs, which resolve to a config-winsRuntimeWarning). Rawconfig["fill_scenarios"]passthrough with no kwarg still works. - v1 restriction.
fill_scenariosis mutually exclusive withcoherence_mode="shuffled"(both drive the per-field override channel); the API rejects the combination with422 code=invalid_fill_scenarios.
See the API User Manual's "Partial fill and payer scenarios" section for the full selector grammar and multi-scenario examples.
Error Handling
The SDK raises typed exceptions for API errors and retries automatically on 429 and 5xx:
from symagedocs import Client, AuthenticationError, RateLimitError, NotFoundError
try:
forms = client.forms.list()
except AuthenticationError:
print("Invalid API key")
except RateLimitError:
print("Too many requests — SDK retries automatically")
except NotFoundError:
print("Resource not found")
All error classes:
| Exception | HTTP Code | Description |
|---|---|---|
SymageDocsError |
— | Base exception for all SDK errors |
AuthenticationError |
401 | Invalid or revoked API key |
PermissionDeniedError |
403 | Key missing required scope |
NotFoundError |
404 | Resource not found |
ValidationError |
400 | Invalid request parameters |
InsufficientCreditsError |
402 | Not enough credits for the operation |
ConflictError |
409 | Resource in unexpected state (e.g., downloading incomplete job) |
RateLimitError |
429 | Rate limit exceeded (SDK retries automatically) |
ServerError |
5xx | Server-side error (SDK retries automatically) |
Examples
The examples/ directory (in the repository and the source distribution; not installed with the wheel) contains complete working scripts:
list_forms.py— Browse available forms and credit costsgenerate_w2s.py— Full pipeline: create job, wait, download the dataset ziptabular_dataset.py— Parse NL description, generate 5k rows, download CSVtrain_kie_model.py— Create a job with NIST3 labels and BIO output, iterate training examples, fetch word annotations
Documentation
- API User Manual — long-form guide with worked examples
- API Explorer — interactive Swagger UI
- API Reference — three-panel ReDoc reference
License
MIT
Release files for symagedocs 1.0.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| symagedocs-1.0.7.tar.gz | 108.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| symagedocs-1.0.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 161.0 kB
Release files / symagedocs-1.0.7.tar.gz
| Download URL | symagedocs-1.0.7.tar.gz |
|---|---|
| Size | 108.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fc8257f0bc423014a42e29e880349e713a4f043c73812b1dfc17452bf05593f5
|
|
BLAKE2b-256 checksum How to use checksums |
89330be95e7d9bd694a6656ea0d1e4ca63bf8dc106918d83832e98d3d01f5c0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / symagedocs-1.0.7-py3-none-any.whl
| Download URL | symagedocs-1.0.7-py3-none-any.whl |
|---|---|
| Size | 52.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b548b10dfa39e21fdf81373176250465315a7a72bf547fa11bd5884a669ba6af
|
|
BLAKE2b-256 checksum How to use checksums |
5b536a99fc927fc5cfadf6505416649a460476ea2f3dc988ec04bb48f80e1f7a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|