quirepdf 0.1.0__py3-none-any.whl

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.
quirepdf/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ """Quire: send JSON, get back a polished PDF.
2
+
3
+ from quirepdf import Quire
4
+
5
+ client = Quire() # reads QUIRE_API_KEY and QUIRE_API_URL
6
+ client.render(data).save("invoice.pdf")
7
+
8
+ Typed template data lives in ``quirepdf.types`` (``InvoiceData``, ``ReceiptData``, ...).
9
+ """
10
+
11
+ from .client import (
12
+ CreatedKey,
13
+ KeyInfo,
14
+ Quire,
15
+ Quota,
16
+ RenderResult,
17
+ TemplateDetail,
18
+ TemplateMeta,
19
+ Usage,
20
+ ValidationResult,
21
+ __version__,
22
+ )
23
+ from .errors import FieldError, QuireError
24
+
25
+ __all__ = [
26
+ "Quire",
27
+ "QuireError",
28
+ "RenderResult",
29
+ "ValidationResult",
30
+ "Quota",
31
+ "FieldError",
32
+ "TemplateMeta",
33
+ "TemplateDetail",
34
+ "Usage",
35
+ "KeyInfo",
36
+ "CreatedKey",
37
+ "__version__",
38
+ ]
quirepdf/client.py ADDED
@@ -0,0 +1,322 @@
1
+ """Synchronous Quire API client (standard library only)."""
2
+
3
+ import decimal
4
+ import http.client
5
+ import json
6
+ import os
7
+ import pathlib
8
+ import socket
9
+ import urllib.error
10
+ import urllib.parse
11
+ import urllib.request
12
+ from dataclasses import dataclass, field
13
+ from email.message import Message
14
+ from typing import Any, Dict, List, Mapping, Optional, TypedDict, Union
15
+
16
+ from .errors import FieldError, QuireError
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ DEFAULT_BASE_URL = "https://api.quirepdf.dev"
21
+ USER_AGENT = f"quirepdf-python/{__version__}"
22
+
23
+ PathLike = Union[str, "os.PathLike[str]"]
24
+
25
+
26
+ # ---------- response shapes ----------
27
+
28
+
29
+ class TemplateMeta(TypedDict):
30
+ """One entry of ``templates.list()``."""
31
+
32
+ name: str
33
+ title: str
34
+ description: str
35
+ category: str
36
+ tags: List[str]
37
+ version: str
38
+
39
+
40
+ class TemplateDetail(TypedDict):
41
+ """``templates.get(name)``: metadata, the JSON Schema for the data, and valid sample data."""
42
+
43
+ template: TemplateMeta
44
+ schema: Dict[str, Any]
45
+ sample: Dict[str, Any]
46
+
47
+
48
+ class Usage(TypedDict):
49
+ """``usage()``: renders used in the current calendar month."""
50
+
51
+ email: str
52
+ plan: str
53
+ period: str
54
+ used: int
55
+ limit: int
56
+ remaining: int
57
+
58
+
59
+ class KeyInfo(TypedDict):
60
+ """An API key as listed by ``keys.list()`` (never includes the secret)."""
61
+
62
+ id: str
63
+ prefix: str
64
+ name: Optional[str]
65
+ created_at: int
66
+ last_used_at: Optional[int]
67
+ current: bool
68
+ """True for the key this client is authenticated with (added by the SDK)."""
69
+
70
+
71
+ class CreatedKey(TypedDict):
72
+ """``keys.create()``: the new key's info plus its secret, which is shown only once."""
73
+
74
+ key: Dict[str, Any]
75
+ api_key: str
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Quota:
80
+ """Monthly render quota after this render (accounts mode only)."""
81
+
82
+ limit: int
83
+ remaining: int
84
+
85
+
86
+ @dataclass
87
+ class RenderResult:
88
+ """A rendered document.
89
+
90
+ The file is in ``content`` rather than a field named ``bytes`` (the name the cross-language
91
+ spec uses): ``bytes`` would shadow the built-in type inside the class and read ambiguously at
92
+ call sites (``result.bytes`` next to ``bytes(...)``), and ``content`` matches the convention
93
+ Python HTTP libraries use for a binary response body.
94
+ """
95
+
96
+ content: bytes = field(repr=False)
97
+ content_type: str
98
+ pages: int
99
+ template: str
100
+ template_source: str
101
+ """``explicit`` (you named it), ``detected`` (the data matched its schema) or ``fallback``
102
+ (the generic ``document`` layout)."""
103
+ hint: Optional[str] = None
104
+ """On fallback: a template that almost matched and why, e.g. ``invoice (seller is required)``."""
105
+ render_ms: float = 0.0
106
+ quota: Optional[Quota] = None
107
+
108
+ def save(self, path: PathLike) -> pathlib.Path:
109
+ """Write the document to ``path`` and return it as a ``pathlib.Path``."""
110
+ p = pathlib.Path(path)
111
+ p.write_bytes(self.content)
112
+ return p
113
+
114
+
115
+ @dataclass
116
+ class ValidationResult:
117
+ """Outcome of ``validate()``: which template the data resolves to, and every problem with it."""
118
+
119
+ valid: bool
120
+ template: str
121
+ source: str
122
+ hint: Optional[str] = None
123
+ errors: List[FieldError] = field(default_factory=list)
124
+
125
+
126
+ # ---------- transport ----------
127
+
128
+
129
+ @dataclass
130
+ class _Response:
131
+ status: int
132
+ headers: Message
133
+ body: bytes
134
+
135
+ def json(self) -> Any:
136
+ return json.loads(self.body.decode("utf-8")) if self.body else None
137
+
138
+
139
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
140
+ """Never follow redirects: urllib would turn a POST into a GET and forward the API key to
141
+ whatever host the redirect names. A 3xx surfaces as a ``QuireError`` instead."""
142
+
143
+ def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[no-untyped-def]
144
+ return None
145
+
146
+
147
+ def _json_default(value: Any) -> Any:
148
+ if isinstance(value, decimal.Decimal):
149
+ return int(value) if value == value.to_integral_value() else float(value)
150
+ raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
151
+
152
+
153
+ # ---------- client ----------
154
+
155
+
156
+ class Quire:
157
+ """Client for the Quire API.
158
+
159
+ >>> from quirepdf import Quire
160
+ >>> client = Quire() # QUIRE_API_KEY / QUIRE_API_URL from the environment
161
+ >>> client.render({"title": "Hello", "items": [{"name": "Tea", "qty": 2}]}).save("hello.pdf")
162
+
163
+ Args:
164
+ api_key: API key. Defaults to ``QUIRE_API_KEY``. When neither is set no ``Authorization``
165
+ header is sent (fine for open-mode servers; others reply ``unauthorized``).
166
+ base_url: API root. Defaults to ``QUIRE_API_URL``, else ``https://api.quirepdf.dev``.
167
+ timeout: Seconds to wait for each request.
168
+ """
169
+
170
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, timeout: float = 30.0) -> None:
171
+ self.api_key: Optional[str] = api_key or os.environ.get("QUIRE_API_KEY") or None
172
+ self.base_url: str = (base_url or os.environ.get("QUIRE_API_URL") or DEFAULT_BASE_URL).rstrip("/")
173
+ self.timeout = timeout
174
+ self.templates = Templates(self)
175
+ self.keys = Keys(self)
176
+ self._opener = urllib.request.build_opener(_NoRedirect)
177
+
178
+ def __repr__(self) -> str:
179
+ key = f"{self.api_key[:8]}..." if self.api_key else None
180
+ return f"Quire(base_url={self.base_url!r}, api_key={key!r}, timeout={self.timeout!r})"
181
+
182
+ # ----- documents -----
183
+
184
+ def render(
185
+ self,
186
+ data: Mapping[str, Any],
187
+ template: Optional[str] = None,
188
+ format: str = "pdf",
189
+ page: Optional[int] = None,
190
+ ) -> RenderResult:
191
+ """Render ``data`` to a PDF (or one page as PNG with ``format="png"``).
192
+
193
+ Without ``template`` the API picks the best-matching gallery template, or the generic
194
+ ``document`` layout (see ``RenderResult.template_source`` and ``hint``).
195
+ """
196
+ path = f"/v1/render/{_segment(template)}" if template else "/v1/render"
197
+ query: Dict[str, Any] = {"format": format if format != "pdf" else None, "page": page}
198
+ res = self._request("POST", path, query=query, body=data)
199
+ h = res.headers
200
+ limit, remaining = _int(h.get("x-quota-limit")), _int(h.get("x-quota-remaining"))
201
+ return RenderResult(
202
+ content=res.body,
203
+ content_type=(h.get("content-type") or "").split(";")[0].strip(),
204
+ pages=_int(h.get("x-pages")) or 0,
205
+ template=h.get("x-template") or (template or ""),
206
+ template_source=h.get("x-template-source") or ("explicit" if template else ""),
207
+ hint=h.get("x-template-hint") or None,
208
+ render_ms=_float(h.get("x-render-ms")) or 0.0,
209
+ quota=Quota(limit, remaining) if limit is not None and remaining is not None else None,
210
+ )
211
+
212
+ def validate(self, data: Mapping[str, Any], template: Optional[str] = None) -> ValidationResult:
213
+ """Check ``data`` without rendering (free, not metered). Invalid data is a result, not an error."""
214
+ body = self._request("POST", "/v1/validate", query={"template": template}, body=data).json()
215
+ return ValidationResult(
216
+ valid=bool(body.get("valid")),
217
+ template=body.get("template") or "",
218
+ source=body.get("source") or "",
219
+ hint=body.get("hint") or None,
220
+ errors=[{"path": str(e.get("path", "")), "message": str(e.get("message", ""))} for e in body.get("errors") or []],
221
+ )
222
+
223
+ def usage(self) -> Usage:
224
+ """Renders used this month on your plan (accounts mode only)."""
225
+ return self._request("GET", "/v1/usage").json() # type: ignore[no-any-return]
226
+
227
+ # ----- transport -----
228
+
229
+ def _request(
230
+ self,
231
+ method: str,
232
+ path: str,
233
+ query: Optional[Mapping[str, Any]] = None,
234
+ body: Any = None,
235
+ ) -> _Response:
236
+ url = self.base_url + path
237
+ params = {k: str(v) for k, v in (query or {}).items() if v is not None}
238
+ if params:
239
+ url += "?" + urllib.parse.urlencode(params)
240
+ headers = {"User-Agent": USER_AGENT}
241
+ if self.api_key:
242
+ headers["Authorization"] = f"Bearer {self.api_key}"
243
+ data = None
244
+ if body is not None:
245
+ data = json.dumps(body, ensure_ascii=False, default=_json_default).encode("utf-8")
246
+ headers["Content-Type"] = "application/json"
247
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
248
+ try:
249
+ with self._opener.open(req, timeout=self.timeout) as resp:
250
+ return _Response(resp.status, resp.headers, resp.read())
251
+ except urllib.error.HTTPError as e:
252
+ try:
253
+ raw = e.read()
254
+ except (OSError, http.client.HTTPException):
255
+ raw = b""
256
+ finally:
257
+ e.close()
258
+ raise QuireError.from_response(e.code, raw) from None
259
+ except urllib.error.URLError as e:
260
+ if isinstance(e.reason, (socket.timeout, TimeoutError)):
261
+ raise self._timeout(method, path) from e
262
+ raise QuireError(0, "network", f"can't reach {self.base_url} ({e.reason})") from e
263
+ except (socket.timeout, TimeoutError) as e: # timed out while reading the response
264
+ raise self._timeout(method, path) from e
265
+ except (OSError, http.client.HTTPException) as e:
266
+ raise QuireError(0, "network", f"connection to {self.base_url} failed ({e or type(e).__name__})") from e
267
+
268
+ def _timeout(self, method: str, path: str) -> QuireError:
269
+ return QuireError(0, "timeout", f"{method} {path} timed out after {self.timeout:g}s")
270
+
271
+
272
+ class Templates:
273
+ """``client.templates``: the template catalog."""
274
+
275
+ def __init__(self, client: Quire) -> None:
276
+ self._client = client
277
+
278
+ def list(self) -> List[TemplateMeta]:
279
+ return self._client._request("GET", "/v1/templates").json()["templates"] # type: ignore[no-any-return]
280
+
281
+ def get(self, name: str) -> TemplateDetail:
282
+ return self._client._request("GET", f"/v1/templates/{_segment(name)}").json() # type: ignore[no-any-return]
283
+
284
+
285
+ class Keys:
286
+ """``client.keys``: API keys of the authenticated account (accounts mode only)."""
287
+
288
+ def __init__(self, client: Quire) -> None:
289
+ self._client = client
290
+
291
+ def list(self) -> List[KeyInfo]:
292
+ """Active keys. ``current`` marks the key this client uses."""
293
+ body = self._client._request("GET", "/v1/keys").json()
294
+ current = body.get("current_key_id")
295
+ return [dict(k, current=k.get("id") == current) for k in body.get("keys") or []] # type: ignore[misc]
296
+
297
+ def create(self, name: Optional[str] = None) -> CreatedKey:
298
+ """Create a key. The secret is in ``["api_key"]`` and is returned only this once."""
299
+ body = {"name": name} if name is not None else {}
300
+ return self._client._request("POST", "/v1/keys", body=body).json() # type: ignore[no-any-return]
301
+
302
+ def revoke(self, key_id: str) -> None:
303
+ """Revoke a key by id. The API refuses to revoke your only active key (``last_key``)."""
304
+ self._client._request("DELETE", f"/v1/keys/{_segment(key_id)}")
305
+
306
+
307
+ def _segment(value: str) -> str:
308
+ return urllib.parse.quote(value, safe="")
309
+
310
+
311
+ def _int(value: Optional[str]) -> Optional[int]:
312
+ try:
313
+ return int(value) if value is not None else None
314
+ except ValueError:
315
+ return None
316
+
317
+
318
+ def _float(value: Optional[str]) -> Optional[float]:
319
+ try:
320
+ return float(value) if value is not None else None
321
+ except ValueError:
322
+ return None
quirepdf/errors.py ADDED
@@ -0,0 +1,63 @@
1
+ """The one error type the SDK raises for API, network and timeout failures."""
2
+
3
+ import json
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ FieldError = Dict[str, str]
7
+ """One validation problem: ``{"path": "items[0].qty", "message": "items[0].qty must be a number"}``."""
8
+
9
+
10
+ class QuireError(Exception):
11
+ """Raised for every failed request.
12
+
13
+ Attributes:
14
+ status: HTTP status code, or ``0`` when no response arrived (network failure or timeout).
15
+ type: The API error type (``invalid_data``, ``unknown_template``, ``unauthorized``,
16
+ ``quota_exceeded``, ...), or ``"network"`` / ``"timeout"`` for client-side failures,
17
+ or ``"http_error"`` when the server sent a non-JSON error body.
18
+ message: Human-readable message from the API, e.g. ``customer.name is required``.
19
+ fields: Every validation problem as ``{"path", "message"}`` dicts (``invalid_data`` only;
20
+ empty otherwise).
21
+ """
22
+
23
+ def __init__(self, status: int, type: str, message: str, fields: Optional[List[FieldError]] = None) -> None:
24
+ super().__init__(message)
25
+ self.status = status
26
+ self.type = type
27
+ self.message = message
28
+ self.fields: List[FieldError] = list(fields or [])
29
+
30
+ def __str__(self) -> str:
31
+ lines = [self.message]
32
+ for f in self.fields:
33
+ msg = f.get("message", "")
34
+ # A single-problem error repeats that problem as its message; don't print it twice.
35
+ if msg and msg != self.message:
36
+ lines.append(f" - {msg}")
37
+ return "\n".join(lines)
38
+
39
+ def __repr__(self) -> str:
40
+ return f"QuireError(status={self.status!r}, type={self.type!r}, message={self.message!r}, fields={self.fields!r})"
41
+
42
+ @classmethod
43
+ def from_response(cls, status: int, body: bytes) -> "QuireError":
44
+ """Build an error from an HTTP error response (``{"error": {"type", "message", "fields"?}}``)."""
45
+ type_, message = "http_error", f"HTTP {status}"
46
+ fields: List[FieldError] = []
47
+ payload: Any = None
48
+ try:
49
+ payload = json.loads(body.decode("utf-8")) if body else None
50
+ except ValueError: # includes JSONDecodeError and UnicodeDecodeError
51
+ pass
52
+ err = payload.get("error") if isinstance(payload, dict) else None
53
+ if isinstance(err, dict):
54
+ type_ = str(err.get("type") or type_)
55
+ message = str(err.get("message") or message)
56
+ raw_fields = err.get("fields")
57
+ if isinstance(raw_fields, list):
58
+ fields = [
59
+ {"path": str(f.get("path", "")), "message": str(f.get("message", ""))}
60
+ for f in raw_fields
61
+ if isinstance(f, dict)
62
+ ]
63
+ return cls(status, type_, message, fields)
quirepdf/py.typed ADDED
File without changes
quirepdf/types.py ADDED
@@ -0,0 +1,698 @@
1
+ """Typed data for Quire's gallery templates.
2
+
3
+ GENERATED by scripts/gen_types.py from templates/*/schema.json. Do not edit by hand;
4
+ re-run ``python3 scripts/gen_types.py`` after changing a schema.
5
+
6
+ Each template has a ``<Name>Data`` TypedDict (``InvoiceData``, ``ReceiptData``, ...).
7
+ Python 3.9 has no ``NotRequired``, so an object with both required and optional keys is a
8
+ private required base class (``total=True``) plus a public ``total=False`` subclass.
9
+ """
10
+
11
+ from typing import Any, Dict, List, Literal, TypedDict
12
+
13
+ __all__ = [
14
+ "CertificateData",
15
+ "CertificateIssuer",
16
+ "CertificateSignature",
17
+ "CreditNoteCustomer",
18
+ "CreditNoteData",
19
+ "CreditNoteItem",
20
+ "CreditNoteOriginalInvoice",
21
+ "CreditNoteSeller",
22
+ "CreditNoteTax",
23
+ "InvoiceCustomer",
24
+ "InvoiceData",
25
+ "InvoiceDiscount",
26
+ "InvoiceItem",
27
+ "InvoicePayment",
28
+ "InvoiceSeller",
29
+ "InvoiceTax",
30
+ "QuoteCustomer",
31
+ "QuoteData",
32
+ "QuoteDiscount",
33
+ "QuoteSection",
34
+ "QuoteSectionItem",
35
+ "QuoteSeller",
36
+ "QuoteTax",
37
+ "ReceiptCustomer",
38
+ "ReceiptData",
39
+ "ReceiptDiscount",
40
+ "ReceiptItem",
41
+ "ReceiptNote",
42
+ "ReceiptPayment",
43
+ "ReceiptSeller",
44
+ "ReceiptSupport",
45
+ "ReceiptTax",
46
+ "StatementAging",
47
+ "StatementCustomer",
48
+ "StatementData",
49
+ "StatementEntry",
50
+ "StatementPayment",
51
+ "StatementSeller",
52
+ "DocumentData",
53
+ "TemplateName",
54
+ "TEMPLATE_DATA_TYPES",
55
+ ]
56
+
57
+
58
+ class _CertificateIssuerRequired(TypedDict):
59
+ """Required keys of :class:`CertificateIssuer`."""
60
+
61
+ # [1 to 60 chars]
62
+ name: str
63
+
64
+
65
+ class CertificateIssuer(_CertificateIssuerRequired, total=False):
66
+ """The organisation awarding the certificate"""
67
+
68
+ # Muted line under the issuer name, e.g. a website [max 80 chars]
69
+ tagline: str
70
+
71
+
72
+ class _CertificateSignatureRequired(TypedDict):
73
+ """Required keys of :class:`CertificateSignature`."""
74
+
75
+ # [1 to 50 chars]
76
+ name: str
77
+
78
+
79
+ class CertificateSignature(_CertificateSignatureRequired, total=False):
80
+ """``signatures[]`` in :class:`CertificateData`."""
81
+
82
+ # [max 60 chars]
83
+ title: str
84
+
85
+
86
+ class _CertificateDataRequired(TypedDict):
87
+ """Required keys of :class:`CertificateData`."""
88
+
89
+ # The organisation awarding the certificate
90
+ issuer: CertificateIssuer
91
+ # Recipient's full name. Long names are shrunk to fit, then wrapped. [1 to 100 chars]
92
+ recipient: str
93
+ # Course or achievement name [1 to 140 chars]
94
+ achievement: str
95
+ # Date of issue, pre-formatted for display [1 to 40 chars]
96
+ date: str
97
+ # One or two signature lines. With one, the date of issue takes the other slot. [1 to 2 items]
98
+ signatures: List[CertificateSignature]
99
+
100
+
101
+ class CertificateData(_CertificateDataRequired, total=False):
102
+ """Data for the ``certificate`` template (Certificate)."""
103
+
104
+ # Brand colour for the frame, seal and title, e.g. #0f766e [pattern ^#[0-9a-fA-F]{6}$]
105
+ accent: str
106
+ # e.g. Certificate of achievement [default "Certificate of completion"; 1 to 60 chars]
107
+ title: str
108
+ # [default "This certifies that"; max 60 chars]
109
+ intro: str
110
+ # Text between the name and the achievement [default "has successfully completed"; max 80 chars]
111
+ statement: str
112
+ # Muted line under the achievement, e.g. hours, grade, format [max 220 chars]
113
+ details: str
114
+ # [max 40 chars]
115
+ credential_id: str
116
+ # Verification link, shown in small monospace text [max 100 chars]
117
+ verify_url: str
118
+
119
+
120
+ class _CreditNoteOriginalInvoiceRequired(TypedDict):
121
+ """Required keys of :class:`CreditNoteOriginalInvoice`."""
122
+
123
+ # [1 to 40 chars]
124
+ number: str
125
+
126
+
127
+ class CreditNoteOriginalInvoice(_CreditNoteOriginalInvoiceRequired, total=False):
128
+ """The invoice this credit is issued against"""
129
+
130
+ # Original invoice date, pre-formatted for display
131
+ date: str
132
+ # Original invoice total
133
+ amount: float
134
+
135
+
136
+ class _CreditNoteSellerRequired(TypedDict):
137
+ """Required keys of :class:`CreditNoteSeller`."""
138
+
139
+ # [non-empty]
140
+ name: str
141
+
142
+
143
+ class CreditNoteSeller(_CreditNoteSellerRequired, total=False):
144
+ """The business issuing the credit (shown as 'From')"""
145
+
146
+ # Shown under the brand name
147
+ email: str
148
+ address: List[str]
149
+ tax_id: str
150
+
151
+
152
+ class _CreditNoteCustomerRequired(TypedDict):
153
+ """Required keys of :class:`CreditNoteCustomer`."""
154
+
155
+ # [non-empty]
156
+ name: str
157
+
158
+
159
+ class CreditNoteCustomer(_CreditNoteCustomerRequired, total=False):
160
+ """The customer receiving the credit (shown as 'Billed to')"""
161
+
162
+ company: str
163
+ address: List[str]
164
+ email: str
165
+
166
+
167
+ class _CreditNoteItemRequired(TypedDict):
168
+ """Required keys of :class:`CreditNoteItem`."""
169
+
170
+ description: str
171
+ # [>= 0]
172
+ qty: float
173
+ # [>= 0]
174
+ unit_price: float
175
+
176
+
177
+ class CreditNoteItem(_CreditNoteItemRequired, total=False):
178
+ """``items[]`` in :class:`CreditNoteData`."""
179
+
180
+ detail: str
181
+
182
+
183
+ class CreditNoteTax(TypedDict):
184
+ """Tax credited back, applied to the subtotal"""
185
+
186
+ label: str
187
+ # [0 to 1]
188
+ rate: float
189
+
190
+
191
+ class _CreditNoteDataRequired(TypedDict):
192
+ """Required keys of :class:`CreditNoteData`."""
193
+
194
+ # Credit note number, e.g. CN-2026-0019 [1 to 40 chars]
195
+ number: str
196
+ # Issue date, pre-formatted for display [non-empty]
197
+ issued: str
198
+ # The invoice this credit is issued against
199
+ original_invoice: CreditNoteOriginalInvoice
200
+ # The business issuing the credit (shown as 'From')
201
+ seller: CreditNoteSeller
202
+ # The customer receiving the credit (shown as 'Billed to')
203
+ customer: CreditNoteCustomer
204
+ # Credited lines. Use positive quantities and prices; they are shown as credit. [at least 1
205
+ # item]
206
+ items: List[CreditNoteItem]
207
+
208
+
209
+ class CreditNoteData(_CreditNoteDataRequired, total=False):
210
+ """Data for the ``credit-note`` template (Credit note)."""
211
+
212
+ # [default "issued"]
213
+ status: Literal["issued", "applied", "refunded"]
214
+ # Currency symbol prefix [default "$"; max 4 chars]
215
+ currency: str
216
+ # Brand colour, e.g. #7048e8 [pattern ^#[0-9a-fA-F]{6}$]
217
+ accent: str
218
+ # Why the credit was issued [max 600 chars]
219
+ reason: str
220
+ # Tax credited back, applied to the subtotal
221
+ tax: CreditNoteTax
222
+ # How this credit will be applied, e.g. refunded to the original card [max 600 chars]
223
+ application: str
224
+ # [max 600 chars]
225
+ notes: str
226
+
227
+
228
+ class _InvoiceSellerRequired(TypedDict):
229
+ """Required keys of :class:`InvoiceSeller`."""
230
+
231
+ # [non-empty]
232
+ name: str
233
+ address: List[str]
234
+
235
+
236
+ class InvoiceSeller(_InvoiceSellerRequired, total=False):
237
+ """``seller`` in :class:`InvoiceData`."""
238
+
239
+ email: str
240
+ tax_id: str
241
+
242
+
243
+ class _InvoiceCustomerRequired(TypedDict):
244
+ """Required keys of :class:`InvoiceCustomer`."""
245
+
246
+ # [non-empty]
247
+ name: str
248
+ address: List[str]
249
+
250
+
251
+ class InvoiceCustomer(_InvoiceCustomerRequired, total=False):
252
+ """``customer`` in :class:`InvoiceData`."""
253
+
254
+ company: str
255
+ email: str
256
+
257
+
258
+ class _InvoiceItemRequired(TypedDict):
259
+ """Required keys of :class:`InvoiceItem`."""
260
+
261
+ description: str
262
+ # [>= 0]
263
+ qty: float
264
+ unit_price: float
265
+
266
+
267
+ class InvoiceItem(_InvoiceItemRequired, total=False):
268
+ """``items[]`` in :class:`InvoiceData`."""
269
+
270
+ detail: str
271
+
272
+
273
+ class InvoiceDiscount(TypedDict):
274
+ """``discount`` in :class:`InvoiceData`."""
275
+
276
+ label: str
277
+ # [0 to 1]
278
+ rate: float
279
+
280
+
281
+ class InvoiceTax(TypedDict):
282
+ """``tax`` in :class:`InvoiceData`."""
283
+
284
+ label: str
285
+ # [0 to 1]
286
+ rate: float
287
+
288
+
289
+ class InvoicePayment(TypedDict, total=False):
290
+ """``payment`` in :class:`InvoiceData`."""
291
+
292
+ method: str
293
+ link: str
294
+ bank: str
295
+
296
+
297
+ class _InvoiceDataRequired(TypedDict):
298
+ """Required keys of :class:`InvoiceData`."""
299
+
300
+ # Invoice number, e.g. INV-2026-0142
301
+ number: str
302
+ # Issue date, pre-formatted for display
303
+ issued: str
304
+ # Due date, pre-formatted for display
305
+ due: str
306
+ seller: InvoiceSeller
307
+ customer: InvoiceCustomer
308
+ # [at least 1 item]
309
+ items: List[InvoiceItem]
310
+
311
+
312
+ class InvoiceData(_InvoiceDataRequired, total=False):
313
+ """Data for the ``invoice`` template (Invoice)."""
314
+
315
+ # [default "due"]
316
+ status: Literal["draft", "due", "paid", "overdue", "void"]
317
+ # Currency symbol prefix [default "$"; max 4 chars]
318
+ currency: str
319
+ # Brand colour, e.g. #3b5bdb [pattern ^#[0-9a-fA-F]{6}$]
320
+ accent: str
321
+ discount: InvoiceDiscount
322
+ tax: InvoiceTax
323
+ payment: InvoicePayment
324
+ notes: str
325
+
326
+
327
+ class _QuoteSellerRequired(TypedDict):
328
+ """Required keys of :class:`QuoteSeller`."""
329
+
330
+ # [non-empty]
331
+ name: str
332
+
333
+
334
+ class QuoteSeller(_QuoteSellerRequired, total=False):
335
+ """The business preparing the quote (shown as 'Prepared by')"""
336
+
337
+ # Person responsible, e.g. 'Jonas Berg, Account Director'
338
+ contact: str
339
+ email: str
340
+ phone: str
341
+ address: List[str]
342
+
343
+
344
+ class _QuoteCustomerRequired(TypedDict):
345
+ """Required keys of :class:`QuoteCustomer`."""
346
+
347
+ # [non-empty]
348
+ name: str
349
+
350
+
351
+ class QuoteCustomer(_QuoteCustomerRequired, total=False):
352
+ """The client receiving the quote (shown as 'Prepared for')"""
353
+
354
+ company: str
355
+ address: List[str]
356
+ email: str
357
+
358
+
359
+ class _QuoteSectionItemRequired(TypedDict):
360
+ """Required keys of :class:`QuoteSectionItem`."""
361
+
362
+ description: str
363
+ # [>= 0]
364
+ qty: float
365
+ unit_price: float
366
+
367
+
368
+ class QuoteSectionItem(_QuoteSectionItemRequired, total=False):
369
+ """``sections[].items[]`` in :class:`QuoteData`."""
370
+
371
+ detail: str
372
+ # Shown after the quantity, e.g. hrs, days [max 12 chars]
373
+ unit: str
374
+
375
+
376
+ class _QuoteSectionRequired(TypedDict):
377
+ """Required keys of :class:`QuoteSection`."""
378
+
379
+ # [at least 1 item]
380
+ items: List[QuoteSectionItem]
381
+
382
+
383
+ class QuoteSection(_QuoteSectionRequired, total=False):
384
+ """``sections[]`` in :class:`QuoteData`."""
385
+
386
+ title: str
387
+ # Short muted text shown next to the section title
388
+ note: str
389
+
390
+
391
+ class QuoteDiscount(TypedDict):
392
+ """``discount`` in :class:`QuoteData`."""
393
+
394
+ label: str
395
+ # [0 to 1]
396
+ rate: float
397
+
398
+
399
+ class QuoteTax(TypedDict):
400
+ """``tax`` in :class:`QuoteData`."""
401
+
402
+ label: str
403
+ # [0 to 1]
404
+ rate: float
405
+
406
+
407
+ class _QuoteDataRequired(TypedDict):
408
+ """Required keys of :class:`QuoteData`."""
409
+
410
+ # Quote number, e.g. Q-2026-0057 [non-empty]
411
+ number: str
412
+ # Issue date, pre-formatted for display [non-empty]
413
+ issued: str
414
+ # Expiry date, pre-formatted for display [non-empty]
415
+ valid_until: str
416
+ # The business preparing the quote (shown as 'Prepared by')
417
+ seller: QuoteSeller
418
+ # The client receiving the quote (shown as 'Prepared for')
419
+ customer: QuoteCustomer
420
+ # Line items grouped into sections. Use a single section without a title for an ungrouped quote.
421
+ # Titles and subtotals are shown when there is more than one section. [at least 1 item]
422
+ sections: List[QuoteSection]
423
+
424
+
425
+ class QuoteData(_QuoteDataRequired, total=False):
426
+ """Data for the ``quote`` template (Quote)."""
427
+
428
+ # [default "sent"]
429
+ status: Literal["draft", "sent", "accepted", "expired"]
430
+ # Currency symbol prefix [default "$"; max 4 chars]
431
+ currency: str
432
+ # Brand colour, e.g. #c2410c [pattern ^#[0-9a-fA-F]{6}$]
433
+ accent: str
434
+ # Short project title
435
+ project: str
436
+ # One-paragraph project summary
437
+ summary: str
438
+ discount: QuoteDiscount
439
+ tax: QuoteTax
440
+ terms: List[str]
441
+ # Text above the signature lines. Defaults to a sign-and-return instruction.
442
+ acceptance_note: str
443
+
444
+
445
+ class _ReceiptSellerRequired(TypedDict):
446
+ """Required keys of :class:`ReceiptSeller`."""
447
+
448
+ # [non-empty]
449
+ name: str
450
+
451
+
452
+ class ReceiptSeller(_ReceiptSellerRequired, total=False):
453
+ """``seller`` in :class:`ReceiptData`."""
454
+
455
+ # Shown under the brand name
456
+ email: str
457
+ address: List[str]
458
+ tax_id: str
459
+
460
+
461
+ class _ReceiptCustomerRequired(TypedDict):
462
+ """Required keys of :class:`ReceiptCustomer`."""
463
+
464
+ # [non-empty]
465
+ name: str
466
+
467
+
468
+ class ReceiptCustomer(_ReceiptCustomerRequired, total=False):
469
+ """``customer`` in :class:`ReceiptData`."""
470
+
471
+ company: str
472
+ address: List[str]
473
+ email: str
474
+
475
+
476
+ class _ReceiptPaymentRequired(TypedDict):
477
+ """Required keys of :class:`ReceiptPayment`."""
478
+
479
+ # e.g. Visa •••• 4242 [non-empty]
480
+ method: str
481
+
482
+
483
+ class ReceiptPayment(_ReceiptPaymentRequired, total=False):
484
+ """``payment`` in :class:`ReceiptData`."""
485
+
486
+ # [max 40 chars]
487
+ transaction_id: str
488
+
489
+
490
+ class _ReceiptItemRequired(TypedDict):
491
+ """Required keys of :class:`ReceiptItem`."""
492
+
493
+ description: str
494
+ # [>= 0]
495
+ qty: float
496
+ unit_price: float
497
+
498
+
499
+ class ReceiptItem(_ReceiptItemRequired, total=False):
500
+ """``items[]`` in :class:`ReceiptData`."""
501
+
502
+ detail: str
503
+
504
+
505
+ class ReceiptDiscount(TypedDict):
506
+ """``discount`` in :class:`ReceiptData`."""
507
+
508
+ label: str
509
+ # [0 to 1]
510
+ rate: float
511
+
512
+
513
+ class ReceiptTax(TypedDict):
514
+ """``tax`` in :class:`ReceiptData`."""
515
+
516
+ label: str
517
+ # [0 to 1]
518
+ rate: float
519
+
520
+
521
+ class ReceiptSupport(TypedDict, total=False):
522
+ """``support`` in :class:`ReceiptData`."""
523
+
524
+ # [default "Questions?"]
525
+ title: str
526
+ email: str
527
+ phone: str
528
+ url: str
529
+
530
+
531
+ class _ReceiptNoteRequired(TypedDict):
532
+ """Required keys of :class:`ReceiptNote`."""
533
+
534
+ text: str
535
+
536
+
537
+ class ReceiptNote(_ReceiptNoteRequired, total=False):
538
+ """``note`` in :class:`ReceiptData`."""
539
+
540
+ # e.g. Refund policy [default "Notes"]
541
+ title: str
542
+
543
+
544
+ class _ReceiptDataRequired(TypedDict):
545
+ """Required keys of :class:`ReceiptData`."""
546
+
547
+ # Receipt number, e.g. RCPT-2026-0388 [non-empty]
548
+ number: str
549
+ # Payment date, pre-formatted for display [non-empty]
550
+ paid_on: str
551
+ seller: ReceiptSeller
552
+ customer: ReceiptCustomer
553
+ payment: ReceiptPayment
554
+ # [at least 1 item]
555
+ items: List[ReceiptItem]
556
+
557
+
558
+ class ReceiptData(_ReceiptDataRequired, total=False):
559
+ """Data for the ``receipt`` template (Receipt)."""
560
+
561
+ # Currency symbol prefix [default "$"; max 4 chars]
562
+ currency: str
563
+ # Brand colour, e.g. #0b7285 [pattern ^#[0-9a-fA-F]{6}$]
564
+ accent: str
565
+ discount: ReceiptDiscount
566
+ tax: ReceiptTax
567
+ support: ReceiptSupport
568
+ note: ReceiptNote
569
+
570
+
571
+ class _StatementSellerRequired(TypedDict):
572
+ """Required keys of :class:`StatementSeller`."""
573
+
574
+ # [non-empty]
575
+ name: str
576
+
577
+
578
+ class StatementSeller(_StatementSellerRequired, total=False):
579
+ """The business issuing the statement. The address is shown under 'Remit to'."""
580
+
581
+ # Shown under the brand name
582
+ email: str
583
+ address: List[str]
584
+
585
+
586
+ class _StatementCustomerRequired(TypedDict):
587
+ """Required keys of :class:`StatementCustomer`."""
588
+
589
+ # [non-empty]
590
+ name: str
591
+
592
+
593
+ class StatementCustomer(_StatementCustomerRequired, total=False):
594
+ """The account holder (shown as 'Statement for')"""
595
+
596
+ company: str
597
+ # Customer account number [max 30 chars]
598
+ account: str
599
+ address: List[str]
600
+ email: str
601
+
602
+
603
+ class _StatementEntryRequired(TypedDict):
604
+ """Required keys of :class:`StatementEntry`."""
605
+
606
+ # Pre-formatted, e.g. 4 Sep [1 to 16 chars]
607
+ date: str
608
+ # [non-empty]
609
+ description: str
610
+
611
+
612
+ class StatementEntry(_StatementEntryRequired, total=False):
613
+ """``entries[]`` in :class:`StatementData`."""
614
+
615
+ # e.g. INV-2026-0431 [max 18 chars]
616
+ reference: str
617
+ # [>= 0]
618
+ charge: float
619
+ # [>= 0]
620
+ payment: float
621
+
622
+
623
+ class StatementAging(TypedDict):
624
+ """Outstanding balance by age"""
625
+
626
+ current: float
627
+ days_1_30: float
628
+ days_31_60: float
629
+ days_61_90: float
630
+ days_90_plus: float
631
+
632
+
633
+ class StatementPayment(TypedDict, total=False):
634
+ """Payment instructions"""
635
+
636
+ method: str
637
+ # [max 80 chars]
638
+ link: str
639
+ bank: str
640
+ # Reference the customer should quote when paying [max 30 chars]
641
+ reference: str
642
+
643
+
644
+ class _StatementDataRequired(TypedDict):
645
+ """Required keys of :class:`StatementData`."""
646
+
647
+ # Statement date, pre-formatted for display, e.g. 30 Sep 2026 [non-empty]
648
+ date: str
649
+ # Statement period, pre-formatted, e.g. 1 Sep – 30 Sep 2026 [1 to 60 chars]
650
+ period: str
651
+ # Balance at the start of the period. Negative means the customer is in credit.
652
+ opening_balance: float
653
+ # The business issuing the statement. The address is shown under 'Remit to'.
654
+ seller: StatementSeller
655
+ # The account holder (shown as 'Statement for')
656
+ customer: StatementCustomer
657
+ # Activity in the period, oldest first. The running balance is computed from opening_balance.
658
+ # Put invoices and debits in 'charge', payments and credits in 'payment'.
659
+ entries: List[StatementEntry]
660
+
661
+
662
+ class StatementData(_StatementDataRequired, total=False):
663
+ """Data for the ``statement`` template (Statement of account)."""
664
+
665
+ # When the balance is due, pre-formatted for display
666
+ due_date: str
667
+ # Badge next to the balance. Defaults to 'due' when the closing balance is positive, 'paid' when
668
+ # it is zero and 'credit' when it is negative.
669
+ status: Literal["due", "overdue", "paid", "credit"]
670
+ # Currency symbol prefix [default "$"; max 4 chars]
671
+ currency: str
672
+ # Brand colour, e.g. #b45309 [pattern ^#[0-9a-fA-F]{6}$]
673
+ accent: str
674
+ # Outstanding balance by age
675
+ aging: StatementAging
676
+ # Payment instructions
677
+ payment: StatementPayment
678
+ # [max 600 chars]
679
+ notes: str
680
+
681
+
682
+ DocumentData = Dict[str, Any]
683
+ """The ``document`` template accepts any JSON object: reserved keys shape the header, every other
684
+ key becomes a section."""
685
+
686
+ TemplateName = Literal["certificate", "credit-note", "document", "invoice", "quote", "receipt", "statement"]
687
+ """Names of the templates these types were generated from."""
688
+
689
+ TEMPLATE_DATA_TYPES: Dict[str, Any] = {
690
+ "certificate": CertificateData,
691
+ "credit-note": CreditNoteData,
692
+ "document": DocumentData,
693
+ "invoice": InvoiceData,
694
+ "quote": QuoteData,
695
+ "receipt": ReceiptData,
696
+ "statement": StatementData,
697
+ }
698
+ """Template name -> its data type."""
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.5
2
+ Name: quirepdf
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Quire API: send JSON, get back a polished PDF.
5
+ Project-URL: Homepage, https://quirepdf.dev
6
+ Project-URL: Documentation, https://quirepdf.dev/docs/sdk-python
7
+ Project-URL: Pricing, https://quirepdf.dev/pricing
8
+ Author-email: Quire PDF <support@quirepdf.dev>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: invoice,json-to-pdf,pdf,pdf-generation,quire,receipt,typst
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Office/Business
17
+ Classifier: Topic :: Printing
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # quirepdf (Python)
23
+
24
+ Send JSON, get back a polished PDF. Python 3.9+, standard library only, synchronous.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ pip install quirepdf
30
+ ```
31
+
32
+ ## Quickstart
33
+
34
+ Any JSON object renders. Quire picks the best-matching template, or a clean generic layout:
35
+
36
+ ```python
37
+ from quirepdf import Quire
38
+
39
+ client = Quire() # reads QUIRE_API_KEY (and QUIRE_API_URL)
40
+
41
+ order = {
42
+ "order_id": "ORD-88213",
43
+ "customer": {"name": "Lena Fischer", "email": "lena@example.de"},
44
+ "lines": [{"sku": "TS-BLK-M", "name": "Organic tee", "qty": 2, "price": 29.0}],
45
+ "currency": "€",
46
+ }
47
+ result = client.render(order)
48
+ result.save("order.pdf")
49
+ print(result.template, result.template_source, result.pages) # document fallback 1
50
+ if result.hint:
51
+ print("almost matched:", result.hint) # e.g. "invoice (seller is required)"
52
+ ```
53
+
54
+ Name a template and use the generated types so your editor checks the fields:
55
+
56
+ ```python
57
+ from quirepdf.types import InvoiceData
58
+
59
+ invoice: InvoiceData = {
60
+ "number": "INV-2026-0142",
61
+ "issued": "1 Oct 2026",
62
+ "due": "15 Oct 2026",
63
+ "seller": {"name": "Northwind Studio", "address": ["221 Market Street", "San Francisco, CA 94105"]},
64
+ "customer": {"name": "Ravi Kumar", "address": ["14 Residency Road", "Bengaluru 560025"]},
65
+ "items": [{"description": "Pro plan", "qty": 1, "unit_price": 49}],
66
+ "status": "due", # Literal["draft", "due", "paid", "overdue", "void"]
67
+ }
68
+
69
+ pdf = client.render(invoice, template="invoice")
70
+ png = client.render(invoice, template="invoice", format="png", page=1) # preview one page
71
+ print(pdf.content[:5], len(png.content), pdf.render_ms, pdf.quota) # Quota(limit=100, remaining=97)
72
+ ```
73
+
74
+ `RenderResult` fields: `content` (the file as `bytes`), `content_type`, `pages`, `template`,
75
+ `template_source` (`explicit` | `detected` | `fallback`), `hint`, `render_ms`, `quota`, and `save(path)`.
76
+
77
+ Other calls:
78
+
79
+ ```python
80
+ check = client.validate(invoice, template="invoice") # free, nothing rendered
81
+ check.valid, check.errors # False, [{"path": "...", "message": "..."}]
82
+
83
+ client.templates.list() # [{"name": "invoice", "title": "Invoice", ...}, ...]
84
+ client.templates.get("invoice") # {"template": {...}, "schema": {...}, "sample": {...}}
85
+ client.usage() # {"plan": "free", "used": 3, "limit": 100, "remaining": 97, ...}
86
+
87
+ new = client.keys.create(name="ci") # new["api_key"] is shown only once
88
+ client.keys.list() # [{"id", "prefix", "name", "current": bool, ...}]
89
+ client.keys.revoke(new["key"]["id"])
90
+ ```
91
+
92
+ ## Errors
93
+
94
+ Every failure raises `QuireError` with `status`, `type`, `message` and `fields`:
95
+
96
+ ```python
97
+ from quirepdf import QuireError
98
+
99
+ try:
100
+ client.render({"number": "INV-1"}, template="invoice")
101
+ except QuireError as err:
102
+ print(err.status, err.type) # 422 invalid_data
103
+ print(err) # data has 5 problems
104
+ # - issued is required
105
+ # - due is required ...
106
+ for f in err.fields:
107
+ print(f["path"], f["message"])
108
+ ```
109
+
110
+ Common types: `invalid_data`, `unknown_template`, `unauthorized`, `quota_exceeded`, `bad_request`,
111
+ `payload_too_large`, `timeout`. Client-side failures have `status == 0`: `type == "network"`
112
+ (can't connect) or `type == "timeout"` (no reply within `timeout` seconds). The SDK never retries.
113
+
114
+ ## Configuration
115
+
116
+ | Argument | Environment | Default |
117
+ |------------|-----------------|-------------------------|
118
+ | `api_key` | `QUIRE_API_KEY` | none (no `Authorization` header is sent) |
119
+ | `base_url` | `QUIRE_API_URL` | `https://api.quirepdf.dev` |
120
+ | `timeout` | | `30.0` seconds |
121
+
122
+ ```python
123
+ client = Quire(api_key="qk_...", base_url="http://127.0.0.1:8787", timeout=10)
124
+ ```
125
+
126
+ A local engine in open mode (`make serve`) needs no key.
127
+
128
+ ## Development
129
+
130
+ ```sh
131
+ python3 scripts/gen_types.py # regenerate src/quirepdf/types.py after a schema change
132
+ python3 scripts/gen_types.py --check # fail if it is stale
133
+ PYTHONPATH=src python3 -m unittest discover -s tests -v # runs against engine/target/release/quire-engine (make build)
134
+ ```
@@ -0,0 +1,9 @@
1
+ quirepdf/__init__.py,sha256=I1ZxHwLsv_gaDc3Eav5jveiVUcAwEaYTyQ7dN44HCDA,732
2
+ quirepdf/client.py,sha256=MtgKs_WXeU-9a-6vWAEHyWXphqha0RBE7h_LkV2-RPA,11494
3
+ quirepdf/errors.py,sha256=FqI6wgOW_3WuS21jVe315m4_r_bu8N_CZuCh3j9Csq8,2810
4
+ quirepdf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ quirepdf/types.py,sha256=1YbV4k9AgfU9j39R8Q-BeflbT7jokceBUgxZAus-8f8,17679
6
+ quirepdf-0.1.0.dist-info/METADATA,sha256=nBQb3s742354v09kV2PQUw2544RvffEgVeXeJFxwRa0,4948
7
+ quirepdf-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
8
+ quirepdf-0.1.0.dist-info/licenses/LICENSE,sha256=ZW-qhkKnBmVhrfiWVu5aSfxUhhwaL-LHGLcCaJNYzwo,1066
9
+ quirepdf-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quire PDF
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.