dokaz-api 0.1.0__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,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dokaz Industries
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.
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.5
2
+ Name: dokaz-api
3
+ Version: 0.1.0
4
+ Summary: Official client for the Dokaz API: invoice PDFs, QR codes, barcodes, Markdown to PDF, CSV/JSON/Excel, calendar .ics files, image metadata removal, email verification, site intel and text AI.
5
+ Project-URL: Homepage, https://api.dokaz.net
6
+ Project-URL: Documentation, https://api.dokaz.net/docs
7
+ Project-URL: Pricing, https://api.dokaz.net/#pricing
8
+ Project-URL: OpenAPI, https://api.dokaz.net/openapi.json
9
+ Author: Dokaz Industries
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,barcode,csv,dokaz,email-verification,exif,ics,invoice,markdown,pdf,qr-code,xlsx
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+
29
+ # dokaz-api
30
+
31
+ The official Python client for the [Dokaz API](https://api.dokaz.net): invoice PDFs, Markdown to
32
+ PDF, QR codes and barcodes, CSV/JSON/Excel conversion, calendar (.ics) files, photo metadata
33
+ removal, email verification, website intel and text AI, all from one key.
34
+
35
+ - Standard library only (urllib). No dependencies.
36
+ - Python 3.9+, fully typed (`py.typed`, TypedDicts for requests and responses).
37
+ - One method per API endpoint. PDFs, PNGs, JPEGs and spreadsheets come back as `bytes`; SVG, CSV
38
+ and iCalendar as `str`; everything else as parsed JSON.
39
+
40
+ ```sh
41
+ pip install dokaz-api
42
+ ```
43
+
44
+ ```python
45
+ from dokaz_api import Dokaz
46
+ ```
47
+
48
+ ## Free tier and plans
49
+
50
+ **100 calls a day are free with no key and no signup.** Just call it. For more, get a key at
51
+ **[api.dokaz.net/#pricing](https://api.dokaz.net/#pricing)**: Starter $9/month (10,000 calls),
52
+ Pro $29/month (100,000), Business $79/month (500,000). Every plan covers every endpoint. The key
53
+ is shown straight after checkout.
54
+
55
+ ```python
56
+ dokaz = Dokaz() # uses DOKAZ_API_KEY if set, else the free tier
57
+ dokaz = Dokaz("dk_...") # or pass the key
58
+ ```
59
+
60
+ ## Quick start
61
+
62
+ ### Invoice PDF
63
+
64
+ ```python
65
+ pdf = dokaz.invoice_pdf({
66
+ "number": "INV-2026-0142",
67
+ "date": "2026-09-06",
68
+ "due": "2026-10-06",
69
+ "from": {"name": "Northwind Studio LLC", "address": ["1200 Market Street", "San Francisco, CA 94102"]},
70
+ "to": {"name": "Acme Robotics Inc.", "email": "ap@acme-robotics.example"},
71
+ "items": [
72
+ {"description": "Brand identity design", "qty": 1, "unit_price": 2400},
73
+ {"description": "Front-end implementation (hourly)", "qty": 32, "unit_price": 110},
74
+ ],
75
+ "tax_rate": 8.25,
76
+ })
77
+ open("invoice.pdf", "wb").write(pdf)
78
+
79
+ totals = dokaz.invoice_preview(same_body) # {"subtotal", "tax", "total", "line_items", ...}
80
+ ```
81
+
82
+ ### Markdown to PDF
83
+
84
+ ```python
85
+ pdf = dokaz.pdf_markdown("# 2.4\n\n- Faster **exports**", title="Release notes", page_size="a4")
86
+ ```
87
+
88
+ ### QR codes and barcodes
89
+
90
+ ```python
91
+ svg = dokaz.qr("https://example.com", size=256, ec="Q") # str
92
+ png = dokaz.qr("https://example.com", format="png") # bytes
93
+ wifi = dokaz.qr_wifi("Cafe Guest", password="latte123", format="png")
94
+ card = dokaz.qr_vcard("Ada Lovelace", email="ada@example.com")
95
+ ean = dokaz.barcode("400638133393", type="ean13", text=True) # SVG str
96
+ ```
97
+
98
+ ### CSV, JSON and Excel
99
+
100
+ ```python
101
+ rows = dokaz.convert_csv_to_json('name,city\n"Hopper, Grace",Arlington') # [{"name": ..., "city": ...}]
102
+ csv = dokaz.convert_json_to_csv([{"name": "Ada", "born": 1815}]) # RFC 4180 text
103
+ xlsx = dokaz.convert_json_to_xlsx({"rows": [{"order": "A-1001", "total": 129.5}], "sheet_name": "Orders"})
104
+ ```
105
+
106
+ ### Calendar (.ics) files
107
+
108
+ ```python
109
+ ics = dokaz.calendar_ics_post({
110
+ "title": "Kitchen remodel consult",
111
+ "start": "2026-10-05T14:00:00-07:00",
112
+ "end": "2026-10-05T15:00:00-07:00",
113
+ "reminder_minutes": 60,
114
+ })
115
+ # An "Add to calendar" link for an email, with no request made and no key in it:
116
+ link = dokaz.calendar_ics_link("Webinar", "2026-10-08T17:00:00Z")
117
+ ```
118
+
119
+ ### Strip photo metadata (EXIF, GPS, XMP)
120
+
121
+ ```python
122
+ clean = dokaz.image_strip(open("photo.jpg", "rb").read()) # same pixels, no metadata
123
+ no_icc = dokaz.image_strip(data, keep_icc=False)
124
+ ```
125
+
126
+ ### Email verification and website intel
127
+
128
+ ```python
129
+ r = dokaz.email_verify("sales@gmial.com") # r["valid"], r["score"], r["checks"]["typo_suggestion"]
130
+ batch = dokaz.email_verify_batch(["a@example.org", "b@mailinator.com"]) # up to 50, one call
131
+ site = dokaz.site_intel("https://example.com") # title, OpenGraph, tech, contacts, socials
132
+ ```
133
+
134
+ ### Text AI
135
+
136
+ ```python
137
+ dokaz.text_summarize(text, sentences=2, style="bullets")
138
+ dokaz.text_sentiment(text)
139
+ dokaz.text_keywords(text, max=10)
140
+ dokaz.text_classify(text, ["billing", "bug report", "other"])
141
+ dokaz.text_extract(text, ["name", "company", "phone"])
142
+ dokaz.text_rewrite(text, "professional")
143
+ ```
144
+
145
+ ## Errors
146
+
147
+ Every non-2xx response raises `DokazError` carrying the API's error body:
148
+
149
+ ```python
150
+ from dokaz_api import DokazError
151
+
152
+ try:
153
+ dokaz.invoice_pdf(body)
154
+ except DokazError as e:
155
+ e.status # 400, 401, 404, 413, 429, 503 ...
156
+ e.error # the API's message (e.body is the whole JSON)
157
+ e.errors # every validation problem, when listed
158
+ e.upgrade # on 429: where to upgrade
159
+ e.retry_after # on 503 (text AI capacity): seconds to wait
160
+ ```
161
+
162
+ A request that gets no response at all (DNS, connection, timeout) raises `DokazConnectionError`,
163
+ a `DokazError` with `status` 0.
164
+
165
+ ## Quota, options and raw responses
166
+
167
+ ```python
168
+ dokaz = Dokaz(
169
+ "dk_...", # default: DOKAZ_API_KEY; None forces the anonymous free tier
170
+ timeout=30, # seconds, default 60
171
+ on_response=lambda i: print(i.operation, i.status, i.quota), # Quota(plan, limit, used)
172
+ )
173
+
174
+ # Any call with its status and headers (x-pdf-pages, x-csv-rows, x-metadata-removed ...):
175
+ r = dokaz.request("pdf_markdown", json={"markdown": "# Hi"})
176
+ r.headers["x-pdf-pages"], r.data
177
+ ```
178
+
179
+ ## Methods
180
+
181
+ | Method | Endpoint | Returns |
182
+ |---|---|---|
183
+ | `invoice_pdf(invoice)` | `POST /v1/invoice/pdf` | `bytes` (PDF) |
184
+ | `invoice_preview(invoice)` | `POST /v1/invoice/preview` | totals |
185
+ | `invoice_sample()` | `GET /v1/invoice/sample` | `bytes` (PDF) |
186
+ | `email_verify(email)` | `GET /v1/email/verify` | result |
187
+ | `email_verify_post(email)` | `POST /v1/email/verify` | result |
188
+ | `email_verify_batch(emails)` | `POST /v1/email/verify/batch` | `{"count", "results"}` |
189
+ | `site_intel(url)` | `GET /v1/site/intel` | dict |
190
+ | `site_intel_post(url)` | `POST /v1/site/intel` | dict |
191
+ | `text_summarize` `text_sentiment` `text_keywords` `text_classify` `text_extract` `text_rewrite` | `POST /v1/text/*` | dict |
192
+ | `qr(data, ...)` / `qr_post(data, ...)` | `GET` / `POST /v1/qr` | SVG `str`, PNG `bytes` or matrix |
193
+ | `qr_wifi(ssid, ...)` | `GET /v1/qr/wifi` | as `qr` |
194
+ | `qr_vcard(name, ...)` | `GET /v1/qr/vcard` | as `qr` |
195
+ | `barcode(data, ...)` | `GET /v1/barcode` | SVG `str` |
196
+ | `pdf_markdown(markdown, ...)` | `POST /v1/pdf/markdown` | `bytes` (PDF) |
197
+ | `convert_csv_to_json(csv, ...)` | `POST /v1/convert/csv-to-json` | rows |
198
+ | `convert_json_to_csv(rows, ...)` | `POST /v1/convert/json-to-csv` | CSV `str` |
199
+ | `convert_json_to_xlsx(body)` | `POST /v1/convert/json-to-xlsx` | `bytes` (.xlsx) |
200
+ | `calendar_ics(title, start, ...)` | `GET /v1/calendar/ics` | iCalendar `str` |
201
+ | `calendar_ics_post(event)` | `POST /v1/calendar/ics` | iCalendar `str` |
202
+ | `image_strip(data, ...)` | `POST /v1/image/strip` | `bytes` (image) |
203
+
204
+ Field names are the API's own, so the guides at [api.dokaz.net/docs](https://api.dokaz.net/docs)
205
+ and the [OpenAPI document](https://api.dokaz.net/openapi.json) apply unchanged.
206
+
207
+ ## License
208
+
209
+ MIT
@@ -0,0 +1,181 @@
1
+ # dokaz-api
2
+
3
+ The official Python client for the [Dokaz API](https://api.dokaz.net): invoice PDFs, Markdown to
4
+ PDF, QR codes and barcodes, CSV/JSON/Excel conversion, calendar (.ics) files, photo metadata
5
+ removal, email verification, website intel and text AI, all from one key.
6
+
7
+ - Standard library only (urllib). No dependencies.
8
+ - Python 3.9+, fully typed (`py.typed`, TypedDicts for requests and responses).
9
+ - One method per API endpoint. PDFs, PNGs, JPEGs and spreadsheets come back as `bytes`; SVG, CSV
10
+ and iCalendar as `str`; everything else as parsed JSON.
11
+
12
+ ```sh
13
+ pip install dokaz-api
14
+ ```
15
+
16
+ ```python
17
+ from dokaz_api import Dokaz
18
+ ```
19
+
20
+ ## Free tier and plans
21
+
22
+ **100 calls a day are free with no key and no signup.** Just call it. For more, get a key at
23
+ **[api.dokaz.net/#pricing](https://api.dokaz.net/#pricing)**: Starter $9/month (10,000 calls),
24
+ Pro $29/month (100,000), Business $79/month (500,000). Every plan covers every endpoint. The key
25
+ is shown straight after checkout.
26
+
27
+ ```python
28
+ dokaz = Dokaz() # uses DOKAZ_API_KEY if set, else the free tier
29
+ dokaz = Dokaz("dk_...") # or pass the key
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ ### Invoice PDF
35
+
36
+ ```python
37
+ pdf = dokaz.invoice_pdf({
38
+ "number": "INV-2026-0142",
39
+ "date": "2026-09-06",
40
+ "due": "2026-10-06",
41
+ "from": {"name": "Northwind Studio LLC", "address": ["1200 Market Street", "San Francisco, CA 94102"]},
42
+ "to": {"name": "Acme Robotics Inc.", "email": "ap@acme-robotics.example"},
43
+ "items": [
44
+ {"description": "Brand identity design", "qty": 1, "unit_price": 2400},
45
+ {"description": "Front-end implementation (hourly)", "qty": 32, "unit_price": 110},
46
+ ],
47
+ "tax_rate": 8.25,
48
+ })
49
+ open("invoice.pdf", "wb").write(pdf)
50
+
51
+ totals = dokaz.invoice_preview(same_body) # {"subtotal", "tax", "total", "line_items", ...}
52
+ ```
53
+
54
+ ### Markdown to PDF
55
+
56
+ ```python
57
+ pdf = dokaz.pdf_markdown("# 2.4\n\n- Faster **exports**", title="Release notes", page_size="a4")
58
+ ```
59
+
60
+ ### QR codes and barcodes
61
+
62
+ ```python
63
+ svg = dokaz.qr("https://example.com", size=256, ec="Q") # str
64
+ png = dokaz.qr("https://example.com", format="png") # bytes
65
+ wifi = dokaz.qr_wifi("Cafe Guest", password="latte123", format="png")
66
+ card = dokaz.qr_vcard("Ada Lovelace", email="ada@example.com")
67
+ ean = dokaz.barcode("400638133393", type="ean13", text=True) # SVG str
68
+ ```
69
+
70
+ ### CSV, JSON and Excel
71
+
72
+ ```python
73
+ rows = dokaz.convert_csv_to_json('name,city\n"Hopper, Grace",Arlington') # [{"name": ..., "city": ...}]
74
+ csv = dokaz.convert_json_to_csv([{"name": "Ada", "born": 1815}]) # RFC 4180 text
75
+ xlsx = dokaz.convert_json_to_xlsx({"rows": [{"order": "A-1001", "total": 129.5}], "sheet_name": "Orders"})
76
+ ```
77
+
78
+ ### Calendar (.ics) files
79
+
80
+ ```python
81
+ ics = dokaz.calendar_ics_post({
82
+ "title": "Kitchen remodel consult",
83
+ "start": "2026-10-05T14:00:00-07:00",
84
+ "end": "2026-10-05T15:00:00-07:00",
85
+ "reminder_minutes": 60,
86
+ })
87
+ # An "Add to calendar" link for an email, with no request made and no key in it:
88
+ link = dokaz.calendar_ics_link("Webinar", "2026-10-08T17:00:00Z")
89
+ ```
90
+
91
+ ### Strip photo metadata (EXIF, GPS, XMP)
92
+
93
+ ```python
94
+ clean = dokaz.image_strip(open("photo.jpg", "rb").read()) # same pixels, no metadata
95
+ no_icc = dokaz.image_strip(data, keep_icc=False)
96
+ ```
97
+
98
+ ### Email verification and website intel
99
+
100
+ ```python
101
+ r = dokaz.email_verify("sales@gmial.com") # r["valid"], r["score"], r["checks"]["typo_suggestion"]
102
+ batch = dokaz.email_verify_batch(["a@example.org", "b@mailinator.com"]) # up to 50, one call
103
+ site = dokaz.site_intel("https://example.com") # title, OpenGraph, tech, contacts, socials
104
+ ```
105
+
106
+ ### Text AI
107
+
108
+ ```python
109
+ dokaz.text_summarize(text, sentences=2, style="bullets")
110
+ dokaz.text_sentiment(text)
111
+ dokaz.text_keywords(text, max=10)
112
+ dokaz.text_classify(text, ["billing", "bug report", "other"])
113
+ dokaz.text_extract(text, ["name", "company", "phone"])
114
+ dokaz.text_rewrite(text, "professional")
115
+ ```
116
+
117
+ ## Errors
118
+
119
+ Every non-2xx response raises `DokazError` carrying the API's error body:
120
+
121
+ ```python
122
+ from dokaz_api import DokazError
123
+
124
+ try:
125
+ dokaz.invoice_pdf(body)
126
+ except DokazError as e:
127
+ e.status # 400, 401, 404, 413, 429, 503 ...
128
+ e.error # the API's message (e.body is the whole JSON)
129
+ e.errors # every validation problem, when listed
130
+ e.upgrade # on 429: where to upgrade
131
+ e.retry_after # on 503 (text AI capacity): seconds to wait
132
+ ```
133
+
134
+ A request that gets no response at all (DNS, connection, timeout) raises `DokazConnectionError`,
135
+ a `DokazError` with `status` 0.
136
+
137
+ ## Quota, options and raw responses
138
+
139
+ ```python
140
+ dokaz = Dokaz(
141
+ "dk_...", # default: DOKAZ_API_KEY; None forces the anonymous free tier
142
+ timeout=30, # seconds, default 60
143
+ on_response=lambda i: print(i.operation, i.status, i.quota), # Quota(plan, limit, used)
144
+ )
145
+
146
+ # Any call with its status and headers (x-pdf-pages, x-csv-rows, x-metadata-removed ...):
147
+ r = dokaz.request("pdf_markdown", json={"markdown": "# Hi"})
148
+ r.headers["x-pdf-pages"], r.data
149
+ ```
150
+
151
+ ## Methods
152
+
153
+ | Method | Endpoint | Returns |
154
+ |---|---|---|
155
+ | `invoice_pdf(invoice)` | `POST /v1/invoice/pdf` | `bytes` (PDF) |
156
+ | `invoice_preview(invoice)` | `POST /v1/invoice/preview` | totals |
157
+ | `invoice_sample()` | `GET /v1/invoice/sample` | `bytes` (PDF) |
158
+ | `email_verify(email)` | `GET /v1/email/verify` | result |
159
+ | `email_verify_post(email)` | `POST /v1/email/verify` | result |
160
+ | `email_verify_batch(emails)` | `POST /v1/email/verify/batch` | `{"count", "results"}` |
161
+ | `site_intel(url)` | `GET /v1/site/intel` | dict |
162
+ | `site_intel_post(url)` | `POST /v1/site/intel` | dict |
163
+ | `text_summarize` `text_sentiment` `text_keywords` `text_classify` `text_extract` `text_rewrite` | `POST /v1/text/*` | dict |
164
+ | `qr(data, ...)` / `qr_post(data, ...)` | `GET` / `POST /v1/qr` | SVG `str`, PNG `bytes` or matrix |
165
+ | `qr_wifi(ssid, ...)` | `GET /v1/qr/wifi` | as `qr` |
166
+ | `qr_vcard(name, ...)` | `GET /v1/qr/vcard` | as `qr` |
167
+ | `barcode(data, ...)` | `GET /v1/barcode` | SVG `str` |
168
+ | `pdf_markdown(markdown, ...)` | `POST /v1/pdf/markdown` | `bytes` (PDF) |
169
+ | `convert_csv_to_json(csv, ...)` | `POST /v1/convert/csv-to-json` | rows |
170
+ | `convert_json_to_csv(rows, ...)` | `POST /v1/convert/json-to-csv` | CSV `str` |
171
+ | `convert_json_to_xlsx(body)` | `POST /v1/convert/json-to-xlsx` | `bytes` (.xlsx) |
172
+ | `calendar_ics(title, start, ...)` | `GET /v1/calendar/ics` | iCalendar `str` |
173
+ | `calendar_ics_post(event)` | `POST /v1/calendar/ics` | iCalendar `str` |
174
+ | `image_strip(data, ...)` | `POST /v1/image/strip` | `bytes` (image) |
175
+
176
+ Field names are the API's own, so the guides at [api.dokaz.net/docs](https://api.dokaz.net/docs)
177
+ and the [OpenAPI document](https://api.dokaz.net/openapi.json) apply unchanged.
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dokaz-api"
7
+ version = "0.1.0"
8
+ description = "Official client for the Dokaz API: invoice PDFs, QR codes, barcodes, Markdown to PDF, CSV/JSON/Excel, calendar .ics files, image metadata removal, email verification, site intel and text AI."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Dokaz Industries" }]
14
+ keywords = ["dokaz", "api", "invoice", "pdf", "markdown", "qr-code", "barcode", "csv", "xlsx", "ics", "exif", "email-verification"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Internet :: WWW/HTTP",
27
+ "Topic :: Software Development :: Libraries",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.urls]
33
+ Homepage = "https://api.dokaz.net"
34
+ Documentation = "https://api.dokaz.net/docs"
35
+ Pricing = "https://api.dokaz.net/#pricing"
36
+ "OpenAPI" = "https://api.dokaz.net/openapi.json"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/dokaz_api"]
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = ["src/dokaz_api", "README.md", "LICENSE", "pyproject.toml"]
@@ -0,0 +1,33 @@
1
+ """Official Python client for the Dokaz API (https://api.dokaz.net).
2
+
3
+ from dokaz_api import Dokaz
4
+ dokaz = Dokaz() # DOKAZ_API_KEY if set, else the free tier (100 calls/day, no key)
5
+ pdf = dokaz.pdf_markdown("# Hello")
6
+ """
7
+
8
+ from ._client import (
9
+ DEFAULT_BASE_URL,
10
+ OPERATIONS,
11
+ PRICING_URL,
12
+ Dokaz,
13
+ DokazResponse,
14
+ Quota,
15
+ ResponseInfo,
16
+ __version__,
17
+ read_quota,
18
+ )
19
+ from ._errors import DokazConnectionError, DokazError
20
+
21
+ __all__ = [
22
+ "DEFAULT_BASE_URL",
23
+ "OPERATIONS",
24
+ "PRICING_URL",
25
+ "Dokaz",
26
+ "DokazConnectionError",
27
+ "DokazError",
28
+ "DokazResponse",
29
+ "Quota",
30
+ "ResponseInfo",
31
+ "__version__",
32
+ "read_quota",
33
+ ]
@@ -0,0 +1,462 @@
1
+ # mypy: disable-error-code="no-any-return"
2
+ """The Dokaz API client. Standard library only (urllib)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json as _json
7
+ import os
8
+ import re
9
+ import socket
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+ from dataclasses import dataclass
14
+ from email.message import Message
15
+ from typing import Any, Callable, Dict, Generic, List, Literal, Mapping, Optional, Tuple, TypeVar, Union, overload
16
+
17
+ from ._errors import DokazConnectionError, DokazError
18
+ from .types import (
19
+ BarcodeType,
20
+ CalendarRequest,
21
+ ClassifyResult,
22
+ EcLevel,
23
+ EmailBatchResult,
24
+ EmailResult,
25
+ ExtractResult,
26
+ InvoiceRequest,
27
+ InvoiceTotals,
28
+ KeywordsResult,
29
+ QrMatrix,
30
+ QrResult,
31
+ Rows,
32
+ RewriteResult,
33
+ SentimentResult,
34
+ SiteIntel,
35
+ SummarizeResult,
36
+ Tone,
37
+ XlsxRequest,
38
+ )
39
+
40
+ __version__ = "0.1.0"
41
+
42
+ DEFAULT_BASE_URL = "https://api.dokaz.net"
43
+ PRICING_URL = "https://api.dokaz.net/#pricing"
44
+
45
+ #: Every API operation this client implements: method name -> (HTTP method, path). The drift test
46
+ #: checks this table against the API's OpenAPI document, so an endpoint added to the API fails the
47
+ #: test until it gets a method here. Naming: the path after /v1 in snake_case, plus "_post" for the
48
+ #: POST form of a path that also has a GET.
49
+ OPERATIONS: Dict[str, Tuple[str, str]] = {
50
+ "invoice_pdf": ("POST", "/v1/invoice/pdf"),
51
+ "invoice_preview": ("POST", "/v1/invoice/preview"),
52
+ "invoice_sample": ("GET", "/v1/invoice/sample"),
53
+ "email_verify": ("GET", "/v1/email/verify"),
54
+ "email_verify_post": ("POST", "/v1/email/verify"),
55
+ "email_verify_batch": ("POST", "/v1/email/verify/batch"),
56
+ "site_intel": ("GET", "/v1/site/intel"),
57
+ "site_intel_post": ("POST", "/v1/site/intel"),
58
+ "text_summarize": ("POST", "/v1/text/summarize"),
59
+ "text_sentiment": ("POST", "/v1/text/sentiment"),
60
+ "text_keywords": ("POST", "/v1/text/keywords"),
61
+ "text_classify": ("POST", "/v1/text/classify"),
62
+ "text_extract": ("POST", "/v1/text/extract"),
63
+ "text_rewrite": ("POST", "/v1/text/rewrite"),
64
+ "qr": ("GET", "/v1/qr"),
65
+ "qr_post": ("POST", "/v1/qr"),
66
+ "qr_wifi": ("GET", "/v1/qr/wifi"),
67
+ "qr_vcard": ("GET", "/v1/qr/vcard"),
68
+ "barcode": ("GET", "/v1/barcode"),
69
+ "pdf_markdown": ("POST", "/v1/pdf/markdown"),
70
+ "convert_csv_to_json": ("POST", "/v1/convert/csv-to-json"),
71
+ "convert_json_to_csv": ("POST", "/v1/convert/json-to-csv"),
72
+ "convert_json_to_xlsx": ("POST", "/v1/convert/json-to-xlsx"),
73
+ "calendar_ics": ("GET", "/v1/calendar/ics"),
74
+ "calendar_ics_post": ("POST", "/v1/calendar/ics"),
75
+ "image_strip": ("POST", "/v1/image/strip"),
76
+ }
77
+
78
+ T = TypeVar("T")
79
+ Query = Mapping[str, Union[str, int, float, bool, None]]
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class Quota:
84
+ """The plan and usage headers every /v1 response carries."""
85
+
86
+ #: free | starter | pro | business | rapid:<plan>
87
+ plan: Optional[str]
88
+ #: Calls allowed in the window (per day on free, per month on paid); "rapidapi" via RapidAPI.
89
+ limit: Optional[str]
90
+ used: Optional[int]
91
+
92
+
93
+ def read_quota(headers: Message) -> Quota:
94
+ used = headers.get("x-quota-used")
95
+ return Quota(plan=headers.get("x-plan"), limit=headers.get("x-quota-limit"), used=int(used) if used and used.isdigit() else None)
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class ResponseInfo:
100
+ operation: str
101
+ status: int
102
+ headers: Message
103
+ quota: Quota
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class DokazResponse(Generic[T]):
108
+ """A decoded response: the value plus the status and (case-insensitive) headers."""
109
+
110
+ data: T
111
+ status: int
112
+ headers: Message
113
+
114
+
115
+ class _Unset:
116
+ def __repr__(self) -> str:
117
+ return "<from DOKAZ_API_KEY>"
118
+
119
+
120
+ _UNSET = _Unset()
121
+ _JSON_TYPE = re.compile(r"^application/(?:[\w.+-]+\+)?json\b", re.I)
122
+ _TEXT_TYPE = re.compile(r"^(?:text/|image/svg\+xml\b)", re.I)
123
+
124
+
125
+ def _env(name: str) -> Optional[str]:
126
+ v = os.environ.get(name)
127
+ return v if v else None
128
+
129
+
130
+ def _query_string(q: Optional[Query]) -> str:
131
+ if not q:
132
+ return ""
133
+ pairs = []
134
+ for k, v in q.items():
135
+ if v is None:
136
+ continue
137
+ if isinstance(v, bool):
138
+ v = "1" if v else "0"
139
+ pairs.append((k, str(v)))
140
+ return "?" + urllib.parse.urlencode(pairs) if pairs else ""
141
+
142
+
143
+ def _sniff_image(data: bytes) -> str:
144
+ if data[:4] == b"\x89PNG":
145
+ return "image/png"
146
+ if data[:2] == b"\xff\xd8":
147
+ return "image/jpeg"
148
+ return "application/octet-stream"
149
+
150
+
151
+ def _drop_none(d: Mapping[str, Any]) -> Dict[str, Any]:
152
+ return {k: v for k, v in d.items() if v is not None}
153
+
154
+
155
+ class Dokaz:
156
+ """Client for https://api.dokaz.net.
157
+
158
+ ``api_key``: your key (dk_...). Default: the DOKAZ_API_KEY environment variable when set.
159
+ Pass ``None`` (or "") to call the free tier anonymously: 100 calls a day per IP, no key.
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ api_key: Union[str, None, _Unset] = _UNSET,
165
+ *,
166
+ base_url: Optional[str] = None,
167
+ timeout: float = 60.0,
168
+ on_response: Optional[Callable[[ResponseInfo], None]] = None,
169
+ ) -> None:
170
+ key = _env("DOKAZ_API_KEY") if isinstance(api_key, _Unset) else api_key
171
+ self.__api_key: Optional[str] = key or None
172
+ self.base_url: str = (base_url or _env("DOKAZ_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
173
+ self.timeout = timeout
174
+ self.on_response = on_response
175
+
176
+ @property
177
+ def has_api_key(self) -> bool:
178
+ return self.__api_key is not None
179
+
180
+ def __repr__(self) -> str:
181
+ return f"Dokaz({self.base_url!r}, {'key' if self.__api_key else 'anonymous'})"
182
+
183
+ # ------------------------------------------------------------------ transport
184
+
185
+ def request(
186
+ self,
187
+ operation: str,
188
+ *,
189
+ query: Optional[Query] = None,
190
+ json: Any = None,
191
+ body: Optional[bytes] = None,
192
+ content_type: Optional[str] = None,
193
+ timeout: Optional[float] = None,
194
+ ) -> DokazResponse[Any]:
195
+ """Low-level call: any operation, returning the decoded body with its status and headers.
196
+
197
+ JSON responses are parsed, text (SVG, CSV, iCalendar) is ``str``, anything else ``bytes``.
198
+ """
199
+ if operation not in OPERATIONS:
200
+ raise ValueError(f"dokaz: unknown operation {operation!r}")
201
+ method, path = OPERATIONS[operation]
202
+ url = self.base_url + path + _query_string(query)
203
+ headers = {"Accept": "*/*", "User-Agent": f"dokaz-api-python/{__version__}"}
204
+ if self.__api_key:
205
+ headers["X-Api-Key"] = self.__api_key
206
+ data: Optional[bytes] = None
207
+ if json is not None:
208
+ data = _json.dumps(json).encode("utf-8")
209
+ headers["Content-Type"] = "application/json"
210
+ elif body is not None:
211
+ data = bytes(body)
212
+ headers["Content-Type"] = content_type or "application/octet-stream"
213
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
214
+ limit = self.timeout if timeout is None else timeout
215
+
216
+ try:
217
+ with urllib.request.urlopen(req, timeout=limit) as res: # noqa: S310 - https URL built above
218
+ status, resp_headers, raw = res.status, res.headers, res.read()
219
+ except urllib.error.HTTPError as e:
220
+ status, resp_headers = e.code, e.headers
221
+ try:
222
+ raw = e.read()
223
+ except Exception: # pragma: no cover - a body cut off mid-read
224
+ raw = b""
225
+ except (socket.timeout, TimeoutError) as e:
226
+ raise DokazConnectionError(f"dokaz: {method} {path} failed: timed out after {limit} s", operation) from e
227
+ except (urllib.error.URLError, OSError) as e:
228
+ reason = getattr(e, "reason", e)
229
+ raise DokazConnectionError(f"dokaz: {method} {path} failed: {reason}", operation) from e
230
+
231
+ if self.on_response is not None:
232
+ self.on_response(ResponseInfo(operation, status, resp_headers, read_quota(resp_headers)))
233
+ ctype = resp_headers.get("content-type") or ""
234
+
235
+ if status >= 400:
236
+ text = raw.decode("utf-8", "replace")
237
+ try:
238
+ parsed = _json.loads(text)
239
+ err_body = parsed if isinstance(parsed, dict) and isinstance(parsed.get("error"), str) else {"error": text[:500]}
240
+ except ValueError:
241
+ err_body = {"error": text[:500] or f"HTTP {status}"}
242
+ raise DokazError(f"dokaz: {method} {path} -> {status}: {err_body['error']}", status, err_body, resp_headers, operation)
243
+
244
+ value: Any
245
+ if _JSON_TYPE.match(ctype):
246
+ value = _json.loads(raw.decode("utf-8"))
247
+ elif _TEXT_TYPE.match(ctype):
248
+ value = raw.decode("utf-8")
249
+ else:
250
+ value = raw
251
+ return DokazResponse(value, status, resp_headers)
252
+
253
+ def _call(self, operation: str, **kw: Any) -> Any:
254
+ return self.request(operation, **kw).data
255
+
256
+ # ------------------------------------------------------------------ Invoice PDF
257
+
258
+ def invoice_pdf(self, invoice: InvoiceRequest) -> bytes:
259
+ """Render an invoice, estimate or receipt as PDF bytes."""
260
+ return self._call("invoice_pdf", json=invoice)
261
+
262
+ def invoice_preview(self, invoice: InvoiceRequest) -> InvoiceTotals:
263
+ """The computed totals and per-line amounts, to check the math before rendering."""
264
+ return self._call("invoice_preview", json=invoice)
265
+
266
+ def invoice_sample(self) -> bytes:
267
+ """A sample invoice PDF rendered from built-in demo data."""
268
+ return self._call("invoice_sample")
269
+
270
+ # ------------------------------------------------------------------ Email Verify
271
+
272
+ def email_verify(self, email: str) -> EmailResult:
273
+ """Verify one address: syntax, MX, disposable, role account, typo suggestion; score 0-100."""
274
+ return self._call("email_verify", query={"email": email})
275
+
276
+ def email_verify_post(self, email: str) -> EmailResult:
277
+ """Same as email_verify, sent as a JSON body (keeps the address out of URLs and logs)."""
278
+ return self._call("email_verify_post", json={"email": email})
279
+
280
+ def email_verify_batch(self, emails: List[str]) -> EmailBatchResult:
281
+ """Verify up to 50 addresses in one call; results in request order."""
282
+ return self._call("email_verify_batch", json={"emails": list(emails)})
283
+
284
+ # ------------------------------------------------------------------ Site Intel
285
+
286
+ def site_intel(self, url: str) -> SiteIntel:
287
+ """Title/meta/OpenGraph, tech stack, contacts, socials and more for a public web page."""
288
+ return self._call("site_intel", query={"url": url})
289
+
290
+ def site_intel_post(self, url: str) -> SiteIntel:
291
+ """Same as site_intel, as a JSON body; a scheme-less "example.com" is treated as https."""
292
+ return self._call("site_intel_post", json={"url": url})
293
+
294
+ # ------------------------------------------------------------------ Text AI
295
+
296
+ def text_summarize(self, text: str, *, sentences: Optional[int] = None, style: Optional[Literal["paragraph", "bullets"]] = None) -> SummarizeResult:
297
+ return self._call("text_summarize", json=_drop_none({"text": text, "sentences": sentences, "style": style}))
298
+
299
+ def text_sentiment(self, text: str) -> SentimentResult:
300
+ return self._call("text_sentiment", json={"text": text})
301
+
302
+ def text_keywords(self, text: str, *, max: Optional[int] = None) -> KeywordsResult: # noqa: A002 - the API's name
303
+ return self._call("text_keywords", json=_drop_none({"text": text, "max": max}))
304
+
305
+ def text_classify(self, text: str, labels: List[str]) -> ClassifyResult:
306
+ return self._call("text_classify", json={"text": text, "labels": list(labels)})
307
+
308
+ def text_extract(self, text: str, fields: List[str]) -> ExtractResult:
309
+ return self._call("text_extract", json={"text": text, "fields": list(fields)})
310
+
311
+ def text_rewrite(self, text: str, tone: Tone) -> RewriteResult:
312
+ return self._call("text_rewrite", json={"text": text, "tone": tone})
313
+
314
+ # ------------------------------------------------------------------ QR codes
315
+ # format "svg" (default) returns str, "png" bytes, "json" the module matrix.
316
+
317
+ @overload
318
+ def qr(self, data: str, *, format: Literal["png"], size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> bytes: ...
319
+ @overload
320
+ def qr(self, data: str, *, format: Literal["json"], size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> QrMatrix: ...
321
+ @overload
322
+ def qr(self, data: str, *, format: Optional[Literal["svg"]] = ..., size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> str: ...
323
+ def qr(self, data: str, *, format: Optional[str] = None, size: Optional[int] = None, margin: Optional[int] = None, ec: Optional[str] = None, fg: Optional[str] = None, bg: Optional[str] = None, rounded: Optional[bool] = None) -> QrResult:
324
+ """A QR code for any text or URL (up to 2953 bytes)."""
325
+ return self._call("qr", query=dict(data=data, format=format, size=size, margin=margin, ec=ec, fg=fg, bg=bg, rounded=rounded))
326
+
327
+ @overload
328
+ def qr_post(self, data: str, *, format: Literal["png"], size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> bytes: ...
329
+ @overload
330
+ def qr_post(self, data: str, *, format: Literal["json"], size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> QrMatrix: ...
331
+ @overload
332
+ def qr_post(self, data: str, *, format: Optional[Literal["svg"]] = ..., size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> str: ...
333
+ def qr_post(self, data: str, *, format: Optional[str] = None, size: Optional[int] = None, margin: Optional[int] = None, ec: Optional[str] = None, fg: Optional[str] = None, bg: Optional[str] = None, rounded: Optional[bool] = None) -> QrResult:
334
+ """Same as qr, sent as a JSON body."""
335
+ return self._call("qr_post", json=_drop_none(dict(data=data, format=format, size=size, margin=margin, ec=ec, fg=fg, bg=bg, rounded=rounded)))
336
+
337
+ @overload
338
+ def qr_wifi(self, ssid: str, *, password: Optional[str] = ..., type: Optional[Literal["WPA", "WEP", "nopass"]] = ..., hidden: Optional[bool] = ..., format: Literal["png"], size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> bytes: ...
339
+ @overload
340
+ def qr_wifi(self, ssid: str, *, password: Optional[str] = ..., type: Optional[Literal["WPA", "WEP", "nopass"]] = ..., hidden: Optional[bool] = ..., format: Literal["json"], size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> QrMatrix: ...
341
+ @overload
342
+ def qr_wifi(self, ssid: str, *, password: Optional[str] = ..., type: Optional[Literal["WPA", "WEP", "nopass"]] = ..., hidden: Optional[bool] = ..., format: Optional[Literal["svg"]] = ..., size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> str: ...
343
+ def qr_wifi(self, ssid: str, *, password: Optional[str] = None, type: Optional[str] = None, hidden: Optional[bool] = None, format: Optional[str] = None, size: Optional[int] = None, margin: Optional[int] = None, ec: Optional[str] = None, fg: Optional[str] = None, bg: Optional[str] = None, rounded: Optional[bool] = None) -> QrResult: # noqa: A002
344
+ """A QR code that joins a Wi-Fi network when scanned."""
345
+ q = dict(ssid=ssid, password=password, type=type, hidden=hidden, format=format, size=size, margin=margin, ec=ec, fg=fg, bg=bg, rounded=rounded)
346
+ return self._call("qr_wifi", query=q)
347
+
348
+ @overload
349
+ def qr_vcard(self, name: str, *, phone: Optional[str] = ..., email: Optional[str] = ..., org: Optional[str] = ..., url: Optional[str] = ..., format: Literal["png"], size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> bytes: ...
350
+ @overload
351
+ def qr_vcard(self, name: str, *, phone: Optional[str] = ..., email: Optional[str] = ..., org: Optional[str] = ..., url: Optional[str] = ..., format: Literal["json"], size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> QrMatrix: ...
352
+ @overload
353
+ def qr_vcard(self, name: str, *, phone: Optional[str] = ..., email: Optional[str] = ..., org: Optional[str] = ..., url: Optional[str] = ..., format: Optional[Literal["svg"]] = ..., size: Optional[int] = ..., margin: Optional[int] = ..., ec: Optional[EcLevel] = ..., fg: Optional[str] = ..., bg: Optional[str] = ..., rounded: Optional[bool] = ...) -> str: ...
354
+ def qr_vcard(self, name: str, *, phone: Optional[str] = None, email: Optional[str] = None, org: Optional[str] = None, url: Optional[str] = None, format: Optional[str] = None, size: Optional[int] = None, margin: Optional[int] = None, ec: Optional[str] = None, fg: Optional[str] = None, bg: Optional[str] = None, rounded: Optional[bool] = None) -> QrResult:
355
+ """A QR code holding a vCard 3.0 contact."""
356
+ q = dict(name=name, phone=phone, email=email, org=org, url=url, format=format, size=size, margin=margin, ec=ec, fg=fg, bg=bg, rounded=rounded)
357
+ return self._call("qr_vcard", query=q)
358
+
359
+ # ------------------------------------------------------------------ Barcodes
360
+
361
+ def barcode(
362
+ self,
363
+ data: str,
364
+ *,
365
+ type: Optional[BarcodeType] = None, # noqa: A002 - the API's name
366
+ text: Optional[bool] = None,
367
+ scale: Optional[int] = None,
368
+ height: Optional[int] = None,
369
+ margin: Optional[int] = None,
370
+ fg: Optional[str] = None,
371
+ bg: Optional[str] = None,
372
+ ) -> str:
373
+ """A Code 128 (default), EAN-13 or UPC-A barcode as SVG text."""
374
+ q = dict(data=data, type=type, text=text, scale=scale, height=height, margin=margin, fg=fg, bg=bg)
375
+ return self._call("barcode", query=q)
376
+
377
+ # ------------------------------------------------------------------ Markdown to PDF
378
+
379
+ def pdf_markdown(self, markdown: str, *, title: Optional[str] = None, page_size: Optional[Literal["letter", "a4"]] = None) -> bytes:
380
+ """Render Markdown as PDF bytes."""
381
+ return self._call("pdf_markdown", json=_drop_none({"markdown": markdown, "title": title, "page_size": page_size}))
382
+
383
+ # ------------------------------------------------------------------ CSV / JSON / Excel
384
+
385
+ def convert_csv_to_json(self, csv: str, *, delimiter: Optional[str] = None, header: Optional[bool] = None) -> List[Any]:
386
+ """CSV text to rows: dicts keyed by the header (default), or lists when header=False."""
387
+ return self._call("convert_csv_to_json", json=_drop_none({"csv": csv, "delimiter": delimiter, "header": header}))
388
+
389
+ def convert_json_to_csv(self, rows: Rows, *, delimiter: Optional[str] = None, header: Optional[bool] = None) -> str:
390
+ """Rows (dicts, or lists) to RFC 4180 CSV text."""
391
+ return self._call("convert_json_to_csv", json=_drop_none({"rows": rows, "delimiter": delimiter, "header": header}))
392
+
393
+ def convert_json_to_xlsx(self, body: XlsxRequest) -> bytes:
394
+ """Rows, {"rows", "sheet_name"}, or {"sheets": [...]} to an Excel workbook (.xlsx bytes)."""
395
+ return self._call("convert_json_to_xlsx", json=body)
396
+
397
+ # ------------------------------------------------------------------ Calendar (.ics)
398
+
399
+ def calendar_ics(
400
+ self,
401
+ title: str,
402
+ start: str,
403
+ *,
404
+ end: Optional[str] = None,
405
+ description: Optional[str] = None,
406
+ location: Optional[str] = None,
407
+ url: Optional[str] = None,
408
+ uid: Optional[str] = None,
409
+ status: Optional[Literal["confirmed", "tentative", "cancelled"]] = None,
410
+ reminder_minutes: Optional[int] = None,
411
+ ) -> str:
412
+ """One event from query parameters, as iCalendar text."""
413
+ return self._call("calendar_ics", query=self._ics_query(title, start, end, description, location, url, uid, status, reminder_minutes))
414
+
415
+ def calendar_ics_post(self, event: CalendarRequest) -> str:
416
+ """One event, or {"name", "events": [...]} with organizer, attendees and repeats, as iCalendar text."""
417
+ return self._call("calendar_ics_post", json=event)
418
+
419
+ def calendar_ics_link(
420
+ self,
421
+ title: str,
422
+ start: str,
423
+ *,
424
+ end: Optional[str] = None,
425
+ description: Optional[str] = None,
426
+ location: Optional[str] = None,
427
+ url: Optional[str] = None,
428
+ uid: Optional[str] = None,
429
+ status: Optional[Literal["confirmed", "tentative", "cancelled"]] = None,
430
+ reminder_minutes: Optional[int] = None,
431
+ ) -> str:
432
+ """An "Add to calendar" link (the GET form of calendar_ics) for an email or a page.
433
+
434
+ Makes no request and never includes your key: whoever clicks it uses their own free call.
435
+ """
436
+ q = self._ics_query(title, start, end, description, location, url, uid, status, reminder_minutes)
437
+ return self.base_url + OPERATIONS["calendar_ics"][1] + _query_string(q)
438
+
439
+ @staticmethod
440
+ def _ics_query(*values: Any) -> Dict[str, Any]:
441
+ keys = ("title", "start", "end", "description", "location", "url", "uid", "status", "reminder_minutes")
442
+ return dict(zip(keys, values))
443
+
444
+ # ------------------------------------------------------------------ Image metadata
445
+
446
+ def image_strip(self, image: bytes, *, keep_icc: Optional[bool] = None, keep_orientation: Optional[bool] = None) -> bytes:
447
+ """A JPEG or PNG with EXIF/GPS/XMP/IPTC/comments removed; the pixels are untouched."""
448
+ data = bytes(image)
449
+ q = {"keep_icc": keep_icc, "keep_orientation": keep_orientation}
450
+ return self._call("image_strip", query=q, body=data, content_type=_sniff_image(data))
451
+
452
+
453
+ __all__ = [
454
+ "DEFAULT_BASE_URL",
455
+ "Dokaz",
456
+ "DokazResponse",
457
+ "OPERATIONS",
458
+ "PRICING_URL",
459
+ "Quota",
460
+ "ResponseInfo",
461
+ "read_quota",
462
+ ]
@@ -0,0 +1,51 @@
1
+ """Errors raised by the Dokaz client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from email.message import Message
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ class DokazError(Exception):
10
+ """A non-2xx response. ``status`` is the HTTP status; ``body`` the API's error JSON.
11
+
12
+ The API's error body always has ``error`` (a message) and sometimes more: ``errors`` (every
13
+ validation problem), ``upgrade`` (on a 429, where to upgrade), ``retry_after`` (on a 503 from
14
+ the text AI endpoints, seconds to wait).
15
+ """
16
+
17
+ def __init__(self, message: str, status: int, body: Dict[str, Any], headers: Optional[Message], operation: str) -> None:
18
+ super().__init__(message)
19
+ self.status = status
20
+ self.body = body
21
+ self.headers = headers
22
+ self.operation = operation
23
+
24
+ @property
25
+ def error(self) -> str:
26
+ return str(self.body.get("error", ""))
27
+
28
+ @property
29
+ def errors(self) -> Optional[List[str]]:
30
+ v = self.body.get("errors")
31
+ return v if isinstance(v, list) else None
32
+
33
+ @property
34
+ def retry_after(self) -> Optional[int]:
35
+ v = self.body.get("retry_after")
36
+ if isinstance(v, int):
37
+ return v
38
+ h = self.headers.get("retry-after") if self.headers is not None else None
39
+ return int(h) if h and h.isdigit() else None
40
+
41
+ @property
42
+ def upgrade(self) -> Optional[str]:
43
+ v = self.body.get("upgrade")
44
+ return v if isinstance(v, str) else None
45
+
46
+
47
+ class DokazConnectionError(DokazError):
48
+ """The request got no HTTP response: DNS, connection, TLS or timeout. ``status`` is 0."""
49
+
50
+ def __init__(self, message: str, operation: str) -> None:
51
+ super().__init__(message, 0, {"error": message}, None, operation)
File without changes
@@ -0,0 +1,239 @@
1
+ """Request and response types for the Dokaz API.
2
+
3
+ Field names are exactly the API's (snake_case), so the endpoint docs at
4
+ https://api.dokaz.net/docs apply to these dicts unchanged. Every TypedDict here is
5
+ ``total=False`` (Python 3.9 has no ``Required``); the docstrings say which keys the API requires.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
11
+
12
+ # ---------------------------------------------------------------- invoice
13
+
14
+
15
+ class InvoiceParty(TypedDict, total=False):
16
+ """``name`` is required. ``address``: up to 5 lines, a list or one newline-separated string."""
17
+
18
+ name: str
19
+ address: Union[List[str], str]
20
+ email: str
21
+ phone: str
22
+
23
+
24
+ class InvoiceItem(TypedDict, total=False):
25
+ """``description``, ``qty`` and ``unit_price`` are required."""
26
+
27
+ description: str
28
+ qty: float
29
+ unit_price: float
30
+ tax_rate: float
31
+
32
+
33
+ InvoiceRequest = TypedDict(
34
+ "InvoiceRequest",
35
+ {
36
+ "kind": Literal["invoice", "estimate", "receipt"],
37
+ "number": str,
38
+ "date": str,
39
+ "due": str,
40
+ "currency": Literal["USD", "EUR", "GBP", "CAD", "AUD"],
41
+ "from": InvoiceParty,
42
+ "to": InvoiceParty,
43
+ "items": List[InvoiceItem],
44
+ "tax_rate": float,
45
+ "discount": float,
46
+ "notes": str,
47
+ "terms": str,
48
+ "paid": bool,
49
+ "logo_text": str,
50
+ },
51
+ total=False,
52
+ )
53
+ """Required: number, date (YYYY-MM-DD), from, to, items (1..60)."""
54
+
55
+
56
+ class InvoiceLineTotal(TypedDict):
57
+ description: str
58
+ qty: float
59
+ unit_price: float
60
+ tax_rate: float
61
+ amount: float
62
+ tax: float
63
+
64
+
65
+ class InvoiceTotals(TypedDict):
66
+ subtotal: float
67
+ tax: float
68
+ discount: float
69
+ total: float
70
+ currency: str
71
+ line_items: List[InvoiceLineTotal]
72
+
73
+
74
+ # ---------------------------------------------------------------- email
75
+
76
+
77
+ class EmailChecks(TypedDict):
78
+ syntax: bool
79
+ domain_has_mx: Optional[bool]
80
+ mx_hosts: List[str]
81
+ disposable: bool
82
+ role_account: bool
83
+ free_provider: bool
84
+ typo_suggestion: Optional[str]
85
+ gibberish_local: bool
86
+
87
+
88
+ class EmailResult(TypedDict):
89
+ email: str
90
+ normalized: str
91
+ valid: bool
92
+ score: int
93
+ checks: EmailChecks
94
+ reason: str
95
+
96
+
97
+ class EmailBatchResult(TypedDict):
98
+ count: int
99
+ results: List[EmailResult]
100
+
101
+
102
+ # ---------------------------------------------------------------- site
103
+
104
+ SiteIntel = Dict[str, Any]
105
+ """url, final_url, status, redirect_chain, title, description, og, tech, contacts, socials, ..."""
106
+
107
+ # ---------------------------------------------------------------- text AI
108
+
109
+
110
+ class SummarizeResult(TypedDict, total=False):
111
+ summary: str
112
+ sentences: int
113
+ input_chars: int
114
+ model: str
115
+ fallback_reason: str
116
+
117
+
118
+ class SentimentResult(TypedDict, total=False):
119
+ label: str
120
+ score: float
121
+ confidence: float
122
+ model: str
123
+ fallback_reason: str
124
+
125
+
126
+ class Keyword(TypedDict):
127
+ term: str
128
+ weight: float
129
+
130
+
131
+ class KeywordsResult(TypedDict, total=False):
132
+ keywords: List[Keyword]
133
+ model: str
134
+ fallback_reason: str
135
+
136
+
137
+ class ClassifyResult(TypedDict, total=False):
138
+ label: str
139
+ scores: Dict[str, float]
140
+ model: str
141
+ fallback_reason: str
142
+
143
+
144
+ class ExtractResult(TypedDict, total=False):
145
+ data: Dict[str, Any]
146
+ model: str
147
+ fallback_reason: str
148
+
149
+
150
+ class RewriteResult(TypedDict, total=False):
151
+ text: str
152
+ model: str
153
+ fallback_reason: str
154
+
155
+
156
+ Tone = Literal["professional", "casual", "concise", "friendly", "formal"]
157
+
158
+ # ---------------------------------------------------------------- QR and barcode
159
+
160
+ QrFormat = Literal["svg", "png", "json"]
161
+ EcLevel = Literal["L", "M", "Q", "H"]
162
+
163
+
164
+ class QrMatrix(TypedDict):
165
+ """format="json": the raw module matrix, one string of 1/0 per row."""
166
+
167
+ version: int
168
+ ec: str
169
+ mode: str
170
+ mask: int
171
+ modules: int
172
+ matrix: List[str]
173
+
174
+
175
+ QrResult = Union[str, bytes, QrMatrix]
176
+ BarcodeType = Literal["code128", "ean13", "upca"]
177
+
178
+ # ---------------------------------------------------------------- convert
179
+
180
+ Row = Dict[str, Any]
181
+ Rows = Union[List[Row], List[List[Any]]]
182
+
183
+
184
+ class XlsxSheet(TypedDict, total=False):
185
+ """``name`` and ``rows`` are required."""
186
+
187
+ name: str
188
+ rows: Rows
189
+ header: bool
190
+
191
+
192
+ XlsxRequest = Union[Rows, Dict[str, Any]]
193
+ """Rows; or {"rows": ..., "sheet_name"?: ..., "header"?: ...}; or {"sheets": [XlsxSheet, ...]}."""
194
+
195
+ # ---------------------------------------------------------------- calendar
196
+
197
+
198
+ class CalendarPerson(TypedDict, total=False):
199
+ """``email`` is required."""
200
+
201
+ name: str
202
+ email: str
203
+
204
+
205
+ class CalendarRepeat(TypedDict, total=False):
206
+ """``freq`` is required."""
207
+
208
+ freq: Literal["daily", "weekly", "monthly", "yearly"]
209
+ interval: int
210
+ count: int
211
+ until: str
212
+ by_day: List[Literal["MO", "TU", "WE", "TH", "FR", "SA", "SU"]]
213
+
214
+
215
+ class CalendarEvent(TypedDict, total=False):
216
+ """``title`` and ``start`` are required. start: "2026-10-05T14:00:00-07:00" or "2026-10-05"."""
217
+
218
+ title: str
219
+ start: str
220
+ end: str
221
+ description: str
222
+ location: str
223
+ url: str
224
+ uid: str
225
+ status: Literal["confirmed", "tentative", "cancelled"]
226
+ reminder_minutes: int
227
+ organizer: CalendarPerson
228
+ attendees: List[CalendarPerson]
229
+ repeat: CalendarRepeat
230
+
231
+
232
+ class CalendarFile(TypedDict, total=False):
233
+ """Several events in one file. ``events`` (1..100) is required."""
234
+
235
+ name: str
236
+ events: List[CalendarEvent]
237
+
238
+
239
+ CalendarRequest = Union[CalendarEvent, CalendarFile]