formepdf 0.7.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ __pycache__/
2
+ *.pyc
3
+ forme/forme.wasm
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: formepdf
3
+ Version: 0.7.9
4
+ Summary: Python SDK for the Forme hosted PDF API
5
+ Project-URL: Homepage, https://formepdf.com
6
+ Project-URL: Documentation, https://docs.formepdf.com
7
+ Project-URL: Repository, https://github.com/formepdf/forme
8
+ Author-email: Dan Molitor <danmolitor91@gmail.com>
9
+ License-Expression: MIT
10
+ Keywords: api,forme,pdf,sdk
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.8
23
+ Provides-Extra: local
24
+ Requires-Dist: wasmtime>=20.0; extra == 'local'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # forme
28
+
29
+ Python SDK for the [Forme](https://formepdf.com) hosted PDF API.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install forme
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from forme import Forme
41
+
42
+ client = Forme("forme_sk_...")
43
+
44
+ # Render a template to PDF bytes
45
+ pdf = client.render("invoice", {"customer": "Acme", "total": 100})
46
+
47
+ with open("invoice.pdf", "wb") as f:
48
+ f.write(pdf)
49
+ ```
50
+
51
+ ## API Reference
52
+
53
+ ### `Forme(api_key, base_url="https://api.formepdf.com")`
54
+
55
+ Create a client instance.
56
+
57
+ ### `client.render(slug, data=None, *, s3=None)`
58
+
59
+ Render a template synchronously. Returns `bytes` (PDF), or a `dict` with `{"url": "..."}` when `s3` is provided.
60
+
61
+ ```python
62
+ # Direct PDF bytes
63
+ pdf = client.render("invoice", {"customer": "Acme"})
64
+
65
+ # Upload to S3
66
+ result = client.render("invoice", {"customer": "Acme"}, s3={
67
+ "bucket": "my-bucket",
68
+ "key": "invoices/001.pdf",
69
+ "accessKeyId": "AK...",
70
+ "secretAccessKey": "SK...",
71
+ })
72
+ print(result["url"])
73
+ ```
74
+
75
+ ### `client.render_async(slug, data=None, *, webhook_url=None)`
76
+
77
+ Start an asynchronous render job. Returns `{"jobId": "...", "status": "pending"}`.
78
+
79
+ ```python
80
+ job = client.render_async("report", data, webhook_url="https://example.com/hook")
81
+ print(job["jobId"])
82
+ ```
83
+
84
+ ### `client.get_job(job_id)`
85
+
86
+ Poll the status of an async job.
87
+
88
+ ```python
89
+ result = client.get_job("job-123")
90
+ if result["status"] == "complete":
91
+ pdf_b64 = result["pdfBase64"]
92
+ ```
93
+
94
+ ### `client.extract(pdf_bytes)`
95
+
96
+ Extract embedded data from a PDF. Returns the data dict, or `None` if none is embedded.
97
+
98
+ ```python
99
+ data = client.extract(pdf_bytes)
100
+ ```
101
+
102
+ ## Error Handling
103
+
104
+ All methods raise `FormeError` on non-2xx responses:
105
+
106
+ ```python
107
+ from forme import Forme, FormeError
108
+
109
+ try:
110
+ pdf = client.render("invoice", data)
111
+ except FormeError as e:
112
+ print(f"Error {e.status}: {e.message}")
113
+ ```
114
+
115
+ ## Requirements
116
+
117
+ - Python 3.8+
118
+ - No dependencies (stdlib only)
@@ -0,0 +1,92 @@
1
+ # forme
2
+
3
+ Python SDK for the [Forme](https://formepdf.com) hosted PDF API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install forme
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from forme import Forme
15
+
16
+ client = Forme("forme_sk_...")
17
+
18
+ # Render a template to PDF bytes
19
+ pdf = client.render("invoice", {"customer": "Acme", "total": 100})
20
+
21
+ with open("invoice.pdf", "wb") as f:
22
+ f.write(pdf)
23
+ ```
24
+
25
+ ## API Reference
26
+
27
+ ### `Forme(api_key, base_url="https://api.formepdf.com")`
28
+
29
+ Create a client instance.
30
+
31
+ ### `client.render(slug, data=None, *, s3=None)`
32
+
33
+ Render a template synchronously. Returns `bytes` (PDF), or a `dict` with `{"url": "..."}` when `s3` is provided.
34
+
35
+ ```python
36
+ # Direct PDF bytes
37
+ pdf = client.render("invoice", {"customer": "Acme"})
38
+
39
+ # Upload to S3
40
+ result = client.render("invoice", {"customer": "Acme"}, s3={
41
+ "bucket": "my-bucket",
42
+ "key": "invoices/001.pdf",
43
+ "accessKeyId": "AK...",
44
+ "secretAccessKey": "SK...",
45
+ })
46
+ print(result["url"])
47
+ ```
48
+
49
+ ### `client.render_async(slug, data=None, *, webhook_url=None)`
50
+
51
+ Start an asynchronous render job. Returns `{"jobId": "...", "status": "pending"}`.
52
+
53
+ ```python
54
+ job = client.render_async("report", data, webhook_url="https://example.com/hook")
55
+ print(job["jobId"])
56
+ ```
57
+
58
+ ### `client.get_job(job_id)`
59
+
60
+ Poll the status of an async job.
61
+
62
+ ```python
63
+ result = client.get_job("job-123")
64
+ if result["status"] == "complete":
65
+ pdf_b64 = result["pdfBase64"]
66
+ ```
67
+
68
+ ### `client.extract(pdf_bytes)`
69
+
70
+ Extract embedded data from a PDF. Returns the data dict, or `None` if none is embedded.
71
+
72
+ ```python
73
+ data = client.extract(pdf_bytes)
74
+ ```
75
+
76
+ ## Error Handling
77
+
78
+ All methods raise `FormeError` on non-2xx responses:
79
+
80
+ ```python
81
+ from forme import Forme, FormeError
82
+
83
+ try:
84
+ pdf = client.render("invoice", data)
85
+ except FormeError as e:
86
+ print(f"Error {e.status}: {e.message}")
87
+ ```
88
+
89
+ ## Requirements
90
+
91
+ - Python 3.8+
92
+ - No dependencies (stdlib only)
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # Build the Forme engine as a WASI WASM module for use with Python wasmtime.
3
+ # Output: forme/forme.wasm
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ ENGINE_DIR="$SCRIPT_DIR/../../engine"
8
+
9
+ echo "Building Forme WASM (wasm32-wasip1, release)..."
10
+ cargo build \
11
+ --manifest-path "$ENGINE_DIR/Cargo.toml" \
12
+ --lib \
13
+ --target wasm32-wasip1 \
14
+ --release \
15
+ --features wasm-raw
16
+
17
+ WASM_SRC="$ENGINE_DIR/target/wasm32-wasip1/release/forme.wasm"
18
+ WASM_DST="$SCRIPT_DIR/forme/forme.wasm"
19
+
20
+ cp "$WASM_SRC" "$WASM_DST"
21
+ echo "Copied to $WASM_DST ($(du -h "$WASM_DST" | cut -f1))"
@@ -0,0 +1,40 @@
1
+ """Forme Python SDK — client for the Forme hosted PDF API and local rendering."""
2
+
3
+ from .client import Forme, FormeError
4
+ from .templates import (
5
+ Document,
6
+ Page,
7
+ View,
8
+ Text,
9
+ Image,
10
+ Table,
11
+ Row,
12
+ Cell,
13
+ Svg,
14
+ QrCode,
15
+ Barcode,
16
+ PageBreak,
17
+ Fixed,
18
+ Watermark,
19
+ )
20
+
21
+ __all__ = [
22
+ # API client
23
+ "Forme",
24
+ "FormeError",
25
+ # Template components
26
+ "Document",
27
+ "Page",
28
+ "View",
29
+ "Text",
30
+ "Image",
31
+ "Table",
32
+ "Row",
33
+ "Cell",
34
+ "Svg",
35
+ "QrCode",
36
+ "Barcode",
37
+ "PageBreak",
38
+ "Fixed",
39
+ "Watermark",
40
+ ]
@@ -0,0 +1,151 @@
1
+ """Forme API client. Zero dependencies — uses stdlib urllib + json."""
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.request
6
+ from typing import Any, Dict, Optional, Union
7
+
8
+
9
+ class FormeError(Exception):
10
+ """Raised on non-2xx responses from the Forme API."""
11
+
12
+ def __init__(self, status: int, message: str) -> None:
13
+ self.status = status
14
+ self.message = message
15
+ super().__init__(message)
16
+
17
+
18
+ class Forme:
19
+ """Client for the Forme hosted PDF API.
20
+
21
+ Args:
22
+ api_key: API key (e.g. ``"forme_sk_..."``).
23
+ base_url: Base URL of the API. Defaults to ``https://api.formepdf.com``.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ api_key: str,
29
+ base_url: str = "https://api.formepdf.com",
30
+ ) -> None:
31
+ self._api_key = api_key
32
+ self._base_url = base_url.rstrip("/")
33
+
34
+ # ------------------------------------------------------------------
35
+ # Public API
36
+ # ------------------------------------------------------------------
37
+
38
+ def render(
39
+ self,
40
+ slug: str,
41
+ data: Any = None,
42
+ *,
43
+ s3: Optional[Dict[str, Any]] = None,
44
+ ) -> Union[bytes, Dict[str, Any]]:
45
+ """Render a template to PDF (synchronous).
46
+
47
+ Returns raw PDF bytes, or a dict with ``{"url": "..."}`` when *s3*
48
+ is provided.
49
+ """
50
+ body: Dict[str, Any] = dict(data) if data is not None else {}
51
+ if s3 is not None:
52
+ body["s3"] = s3
53
+
54
+ resp_body, content_type = self._request(
55
+ "POST",
56
+ "/v1/render/{}".format(slug),
57
+ body=body,
58
+ )
59
+
60
+ if content_type is not None and "application/json" in content_type:
61
+ return json.loads(resp_body) # type: ignore[no-any-return]
62
+ return resp_body
63
+
64
+ def render_async(
65
+ self,
66
+ slug: str,
67
+ data: Any = None,
68
+ *,
69
+ webhook_url: Optional[str] = None,
70
+ ) -> Dict[str, Any]:
71
+ """Start an asynchronous render job.
72
+
73
+ Returns ``{"jobId": "...", "status": "pending"}``.
74
+ """
75
+ body: Dict[str, Any] = dict(data) if data is not None else {}
76
+ if webhook_url is not None:
77
+ body["webhookUrl"] = webhook_url
78
+
79
+ resp_body, _ = self._request(
80
+ "POST",
81
+ "/v1/render/{}/async".format(slug),
82
+ body=body,
83
+ )
84
+ return json.loads(resp_body) # type: ignore[no-any-return]
85
+
86
+ def get_job(self, job_id: str) -> Dict[str, Any]:
87
+ """Poll the status of an async render job."""
88
+ resp_body, _ = self._request("GET", "/v1/jobs/{}".format(job_id))
89
+ return json.loads(resp_body) # type: ignore[no-any-return]
90
+
91
+ def extract(self, pdf_bytes: bytes) -> Any:
92
+ """Extract embedded data from a PDF.
93
+
94
+ Returns the embedded data dict, or ``None`` if the PDF has no
95
+ embedded data.
96
+ """
97
+ try:
98
+ resp_body, _ = self._request(
99
+ "POST",
100
+ "/v1/extract",
101
+ body=pdf_bytes,
102
+ content_type="application/pdf",
103
+ )
104
+ result = json.loads(resp_body)
105
+ return result.get("data")
106
+ except FormeError as exc:
107
+ if exc.status == 404 and "no embedded data" in exc.message.lower():
108
+ return None
109
+ raise
110
+
111
+ # ------------------------------------------------------------------
112
+ # Internal helpers
113
+ # ------------------------------------------------------------------
114
+
115
+ def _request(
116
+ self,
117
+ method: str,
118
+ path: str,
119
+ *,
120
+ body: Any = None,
121
+ content_type: str = "application/json",
122
+ ) -> tuple:
123
+ """Send an HTTP request and return ``(response_bytes, content_type)``."""
124
+ url = self._base_url + path
125
+
126
+ if isinstance(body, bytes):
127
+ data = body
128
+ elif body is not None:
129
+ data = json.dumps(body).encode("utf-8")
130
+ else:
131
+ data = None
132
+
133
+ req = urllib.request.Request(url, data=data, method=method)
134
+ req.add_header("Authorization", "Bearer {}".format(self._api_key))
135
+ if data is not None:
136
+ req.add_header("Content-Type", content_type)
137
+
138
+ try:
139
+ resp = urllib.request.urlopen(req)
140
+ resp_content_type = resp.headers.get("Content-Type")
141
+ return resp.read(), resp_content_type
142
+ except urllib.error.HTTPError as exc:
143
+ status = exc.code
144
+ message = "Request failed with status {}".format(status)
145
+ try:
146
+ err_body = json.loads(exc.read())
147
+ if isinstance(err_body, dict):
148
+ message = err_body.get("error") or err_body.get("message") or message
149
+ except (ValueError, TypeError):
150
+ pass
151
+ raise FormeError(status, message) from None
File without changes