ocr-software 0.1.1__tar.gz → 0.1.2__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.
- {ocr_software-0.1.1 → ocr_software-0.1.2}/INSTALL.txt +4 -4
- {ocr_software-0.1.1/ocr_software.egg-info → ocr_software-0.1.2}/PKG-INFO +6 -6
- {ocr_software-0.1.1 → ocr_software-0.1.2}/README.md +4 -4
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software/__init__.py +1 -1
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software/advice.py +6 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software/cli.py +22 -6
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software/client.py +10 -6
- {ocr_software-0.1.1 → ocr_software-0.1.2/ocr_software.egg-info}/PKG-INFO +6 -6
- {ocr_software-0.1.1 → ocr_software-0.1.2}/pyproject.toml +2 -2
- {ocr_software-0.1.1 → ocr_software-0.1.2}/tests/test_cli.py +56 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/tests/test_client.py +10 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/LICENSE +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software/__main__.py +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software.egg-info/SOURCES.txt +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software.egg-info/dependency_links.txt +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software.egg-info/entry_points.txt +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software.egg-info/requires.txt +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/ocr_software.egg-info/top_level.txt +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/setup.cfg +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/tests/test_advice.py +0 -0
- {ocr_software-0.1.1 → ocr_software-0.1.2}/tests/test_release_files.py +0 -0
|
@@ -23,11 +23,11 @@ Environment variables; the command line flag wins.
|
|
|
23
23
|
|
|
24
24
|
| Variable | Meaning |
|
|
25
25
|
|---|---|
|
|
26
|
-
| `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs. |
|
|
26
|
+
| `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs; if both are set, `OCR_SOFTWARE_API_KEY` wins. |
|
|
27
27
|
| `OCR_SOFTWARE_BASE_URL` | Default `https://api.ocr-software.com`; own server e.g. `http://127.0.0.1:8080` (also `--base-url`). `OCR_BASE_URL` is read too. |
|
|
28
28
|
| `OCR_SOFTWARE_ALLOWED_DIR` | Optional: read documents only from below this folder (symlinks and `..` do not escape). |
|
|
29
29
|
|
|
30
|
-
The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr; `--list-pipelines` marks it. In Python: `client.ocr(path, client.default_pipeline())`.
|
|
30
|
+
The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr (`-q` silences that line for bulk runs; `-o NUL` or `-o /dev/null` discards the result); `--list-pipelines` marks it. Warnings of the service (`dpi_ignored`, `job_budget`, `free_limit`, `balance_too_low`) are printed as `warning CODE: text` lines on stderr by `--dry-run` and before a job that uses `--dpi` or `--job-budget` (that job asks once with a dry run first). In Python: `client.ocr(path, client.default_pipeline())`.
|
|
31
31
|
|
|
32
32
|
## Command line
|
|
33
33
|
|
|
@@ -49,7 +49,7 @@ Options:
|
|
|
49
49
|
- `--dpi 150|200|300` (PDF render resolution)
|
|
50
50
|
- `--timeout SECONDS` (wait for the job, default 3600)
|
|
51
51
|
- `--job-budget MILLICENTS` (cap; ignored where billing is off)
|
|
52
|
-
- `--dry-run`, `--version`
|
|
52
|
+
- `-q`, `--dry-run`, `--version`
|
|
53
53
|
|
|
54
54
|
Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the file appears whole or not at all, an existing file is replaced, and the input file itself is refused as output.
|
|
55
55
|
|
|
@@ -68,7 +68,7 @@ Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the fil
|
|
|
68
68
|
```python
|
|
69
69
|
from ocr_software import Client, PagesUnread
|
|
70
70
|
client = Client() # key and address from the environment
|
|
71
|
-
text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes; raises PagesUnread (with .data) if a page was not read
|
|
71
|
+
text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes (text.decode("utf-8") for a string); raises PagesUnread (with .data) if a page was not read
|
|
72
72
|
pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
|
|
73
73
|
job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
|
|
74
74
|
client.wait(job["job_id"]); client.result(job["job_id"], "json")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ocr-software
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Client and command line for the ocr-software.com OCR API: text, word boxes, layout, searchable PDF.
|
|
5
5
|
License-Expression: MIT
|
|
6
|
-
Project-URL: Homepage, https://ocr-software.com/
|
|
6
|
+
Project-URL: Homepage, https://ocr-software.com/integrations/python/
|
|
7
7
|
Project-URL: Repository, https://github.com/ocr-software/ocr-software-python
|
|
8
8
|
Keywords: ocr,pdf,searchable-pdf,hocr,markdown,document-ai
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -40,11 +40,11 @@ Environment variables; the command line flag wins.
|
|
|
40
40
|
|
|
41
41
|
| Variable | Meaning |
|
|
42
42
|
|---|---|
|
|
43
|
-
| `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs. |
|
|
43
|
+
| `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs; if both are set, `OCR_SOFTWARE_API_KEY` wins. |
|
|
44
44
|
| `OCR_SOFTWARE_BASE_URL` | Default `https://api.ocr-software.com`; own server e.g. `http://127.0.0.1:8080` (also `--base-url`). `OCR_BASE_URL` is read too. |
|
|
45
45
|
| `OCR_SOFTWARE_ALLOWED_DIR` | Optional: read documents only from below this folder (symlinks and `..` do not escape). |
|
|
46
46
|
|
|
47
|
-
The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr; `--list-pipelines` marks it. In Python: `client.ocr(path, client.default_pipeline())`.
|
|
47
|
+
The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr (`-q` silences that line for bulk runs; `-o NUL` or `-o /dev/null` discards the result); `--list-pipelines` marks it. Warnings of the service (`dpi_ignored`, `job_budget`, `free_limit`, `balance_too_low`) are printed as `warning CODE: text` lines on stderr by `--dry-run` and before a job that uses `--dpi` or `--job-budget` (that job asks once with a dry run first). In Python: `client.ocr(path, client.default_pipeline())`.
|
|
48
48
|
|
|
49
49
|
## Command line
|
|
50
50
|
|
|
@@ -66,7 +66,7 @@ Options:
|
|
|
66
66
|
- `--dpi 150|200|300` (PDF render resolution)
|
|
67
67
|
- `--timeout SECONDS` (wait for the job, default 3600)
|
|
68
68
|
- `--job-budget MILLICENTS` (cap; ignored where billing is off)
|
|
69
|
-
- `--dry-run`, `--version`
|
|
69
|
+
- `-q`, `--dry-run`, `--version`
|
|
70
70
|
|
|
71
71
|
Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the file appears whole or not at all, an existing file is replaced, and the input file itself is refused as output.
|
|
72
72
|
|
|
@@ -85,7 +85,7 @@ Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the fil
|
|
|
85
85
|
```python
|
|
86
86
|
from ocr_software import Client, PagesUnread
|
|
87
87
|
client = Client() # key and address from the environment
|
|
88
|
-
text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes; raises PagesUnread (with .data) if a page was not read
|
|
88
|
+
text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes (text.decode("utf-8") for a string); raises PagesUnread (with .data) if a page was not read
|
|
89
89
|
pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
|
|
90
90
|
job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
|
|
91
91
|
client.wait(job["job_id"]); client.result(job["job_id"], "json")
|
|
@@ -23,11 +23,11 @@ Environment variables; the command line flag wins.
|
|
|
23
23
|
|
|
24
24
|
| Variable | Meaning |
|
|
25
25
|
|---|---|
|
|
26
|
-
| `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs. |
|
|
26
|
+
| `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs; if both are set, `OCR_SOFTWARE_API_KEY` wins. |
|
|
27
27
|
| `OCR_SOFTWARE_BASE_URL` | Default `https://api.ocr-software.com`; own server e.g. `http://127.0.0.1:8080` (also `--base-url`). `OCR_BASE_URL` is read too. |
|
|
28
28
|
| `OCR_SOFTWARE_ALLOWED_DIR` | Optional: read documents only from below this folder (symlinks and `..` do not escape). |
|
|
29
29
|
|
|
30
|
-
The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr; `--list-pipelines` marks it. In Python: `client.ocr(path, client.default_pipeline())`.
|
|
30
|
+
The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr (`-q` silences that line for bulk runs; `-o NUL` or `-o /dev/null` discards the result); `--list-pipelines` marks it. Warnings of the service (`dpi_ignored`, `job_budget`, `free_limit`, `balance_too_low`) are printed as `warning CODE: text` lines on stderr by `--dry-run` and before a job that uses `--dpi` or `--job-budget` (that job asks once with a dry run first). In Python: `client.ocr(path, client.default_pipeline())`.
|
|
31
31
|
|
|
32
32
|
## Command line
|
|
33
33
|
|
|
@@ -49,7 +49,7 @@ Options:
|
|
|
49
49
|
- `--dpi 150|200|300` (PDF render resolution)
|
|
50
50
|
- `--timeout SECONDS` (wait for the job, default 3600)
|
|
51
51
|
- `--job-budget MILLICENTS` (cap; ignored where billing is off)
|
|
52
|
-
- `--dry-run`, `--version`
|
|
52
|
+
- `-q`, `--dry-run`, `--version`
|
|
53
53
|
|
|
54
54
|
Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the file appears whole or not at all, an existing file is replaced, and the input file itself is refused as output.
|
|
55
55
|
|
|
@@ -68,7 +68,7 @@ Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the fil
|
|
|
68
68
|
```python
|
|
69
69
|
from ocr_software import Client, PagesUnread
|
|
70
70
|
client = Client() # key and address from the environment
|
|
71
|
-
text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes; raises PagesUnread (with .data) if a page was not read
|
|
71
|
+
text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes (text.decode("utf-8") for a string); raises PagesUnread (with .data) if a page was not read
|
|
72
72
|
pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
|
|
73
73
|
job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
|
|
74
74
|
client.wait(job["job_id"]); client.result(job["job_id"], "json")
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
from .advice import default_pipeline, price_sentence
|
|
3
3
|
from .client import Client, OcrError, PagesUnread, split_pages, unread_message
|
|
4
4
|
|
|
5
|
-
__version__ = "0.1.
|
|
5
|
+
__version__ = "0.1.2"
|
|
6
6
|
__all__ = ["Client", "OcrError", "PagesUnread", "split_pages", "unread_message", "default_pipeline", "price_sentence", "__version__"]
|
|
@@ -43,6 +43,12 @@ def price_sentence(q, show_list_price=True):
|
|
|
43
43
|
return text
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def warning_lines(q):
|
|
47
|
+
"""One line per warning of a dry run (dpi_ignored, job_budget, free_limit, balance, balance_too_low), for stderr:
|
|
48
|
+
price_sentence says what it costs, these say why fewer pages run or why an option does nothing."""
|
|
49
|
+
return ["warning %s: %s" % (w.get("code", "?"), w.get("message", "")) for w in q.get("warnings") or []]
|
|
50
|
+
|
|
51
|
+
|
|
46
52
|
def cost(p):
|
|
47
53
|
"""Price per page in millicents, comparable between pipelines: the catalog price, else factor * 100 (the catalog hides the
|
|
48
54
|
price while billing is off, the factor stays)."""
|
|
@@ -11,7 +11,7 @@ import os
|
|
|
11
11
|
import sys
|
|
12
12
|
|
|
13
13
|
from . import __version__
|
|
14
|
-
from .advice import default_pipeline, price_sentence, summary
|
|
14
|
+
from .advice import default_pipeline, price_sentence, summary, warning_lines
|
|
15
15
|
from .client import DEFAULT_WAIT_TIMEOUT_S, FORMATS, Client, OcrError, PagesUnread
|
|
16
16
|
|
|
17
17
|
EXIT_ERROR, EXIT_USAGE, EXIT_UNREAD, EXIT_TIMEOUT = 1, 2, 3, 4
|
|
@@ -26,6 +26,7 @@ def _parser():
|
|
|
26
26
|
p.add_argument("--format", default="text", choices=FORMATS, help="default text")
|
|
27
27
|
p.add_argument("--dpi", type=int, choices=(150, 200, 300), help="render resolution of a PDF (default 150)")
|
|
28
28
|
p.add_argument("--job-budget", type=int, help="cap in millicents (100,000 = 1 EUR; one page of the cheapest pipeline = 100). Where billing is off it is ignored")
|
|
29
|
+
p.add_argument("-q", "--quiet", action="store_true", help="do not name the default pipeline on stderr (for bulk runs); errors and warnings stay")
|
|
29
30
|
p.add_argument("--dry-run", action="store_true", help="show the page count and the price in euros, run nothing, cost nothing")
|
|
30
31
|
p.add_argument("--timeout", type=int, default=DEFAULT_WAIT_TIMEOUT_S, help="seconds to wait for the job (default %(default)s)")
|
|
31
32
|
p.add_argument("--base-url", help="address of the service (default OCR_SOFTWARE_BASE_URL, else the hosted API)")
|
|
@@ -54,7 +55,19 @@ def _same_file(a, b):
|
|
|
54
55
|
return False
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
def _is_devnull(path):
|
|
59
|
+
# "-o NUL" on Windows, "-o /dev/null" everywhere (also typed on Windows from Git Bash, where no folder \dev exists)
|
|
60
|
+
return bool(path) and os.path.normcase(path) in (os.path.normcase(os.devnull), os.path.normcase("/dev/null"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _print_warnings(answer):
|
|
64
|
+
for line in warning_lines(answer): # an older service sends none: nothing to print
|
|
65
|
+
print("ocr-software: " + line, file=sys.stderr)
|
|
66
|
+
|
|
67
|
+
|
|
57
68
|
def _emit(data, output):
|
|
69
|
+
if _is_devnull(output):
|
|
70
|
+
return # discard: a .part file next to the null device cannot exist
|
|
58
71
|
if output:
|
|
59
72
|
_write(output, data)
|
|
60
73
|
else:
|
|
@@ -83,21 +96,24 @@ def main(argv=None):
|
|
|
83
96
|
parser.error("IN is required (--list-pipelines shows the pipelines)")
|
|
84
97
|
if args.format == "pdf" and not args.output:
|
|
85
98
|
parser.error("--format pdf needs -o OUT (a PDF is not written to the terminal)")
|
|
86
|
-
if args.output and _same_file(args.input, args.output):
|
|
99
|
+
if args.output and not _is_devnull(args.output) and _same_file(args.input, args.output):
|
|
87
100
|
parser.error("OUT is the input file; it would be overwritten")
|
|
88
|
-
if args.output and not os.path.isdir(os.path.dirname(os.path.abspath(args.output))):
|
|
101
|
+
if args.output and not _is_devnull(args.output) and not os.path.isdir(os.path.dirname(os.path.abspath(args.output))):
|
|
89
102
|
parser.error("folder of %s does not exist; nothing was sent" % args.output) # before the job, not after it
|
|
90
103
|
pipeline = args.pipeline
|
|
91
104
|
if not pipeline: # the default is named, never silent (ziel.md: fastest and cheapest available; stdout stays the result)
|
|
92
105
|
pipeline = client.default_pipeline()
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
if not args.quiet:
|
|
107
|
+
print("ocr-software: no --pipeline given, using %s (the fastest and cheapest available one; --list-pipelines shows the others)" % pipeline,
|
|
108
|
+
file=sys.stderr)
|
|
95
109
|
if args.dry_run:
|
|
96
110
|
q = client.quote(args.input, pipeline, args.dpi)
|
|
97
111
|
q.setdefault("pipeline", pipeline)
|
|
98
112
|
print(price_sentence(q))
|
|
113
|
+
_print_warnings(q)
|
|
99
114
|
return 0
|
|
100
|
-
data = client.ocr(args.input, pipeline, args.format, timeout=args.timeout, dpi=args.dpi, job_budget=args.job_budget
|
|
115
|
+
data = client.ocr(args.input, pipeline, args.format, timeout=args.timeout, dpi=args.dpi, job_budget=args.job_budget,
|
|
116
|
+
on_accepted=_print_warnings) # the service names the warnings in its answer: no extra dry run
|
|
101
117
|
_emit(data, args.output)
|
|
102
118
|
return 0
|
|
103
119
|
except PagesUnread as e:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Client for the ocr-software.com API (standard library only, Python 3.9 or newer).
|
|
2
2
|
|
|
3
|
-
Settings: OCR_SOFTWARE_API_KEY (or OCR_KEY, the name used in the API docs), OCR_SOFTWARE_BASE_URL (or OCR_BASE_URL),
|
|
3
|
+
Settings (the OCR_SOFTWARE_ name wins if both are set): OCR_SOFTWARE_API_KEY (or OCR_KEY, the name used in the API docs), OCR_SOFTWARE_BASE_URL (or OCR_BASE_URL),
|
|
4
4
|
OCR_SOFTWARE_ALLOWED_DIR (optional). The key is never printed, never in an error message, never in a log.
|
|
5
5
|
Spec of every path, field and error code: <base>/openapi.json.
|
|
6
6
|
"""
|
|
@@ -233,7 +233,7 @@ class Client:
|
|
|
233
233
|
head = dict(headers or {})
|
|
234
234
|
if key_needed:
|
|
235
235
|
if not self._key:
|
|
236
|
-
raise OcrError("API key missing: set OCR_SOFTWARE_API_KEY (the key starts with ocr_)", "no_key")
|
|
236
|
+
raise OcrError("API key missing: set OCR_SOFTWARE_API_KEY or OCR_KEY (the key starts with ocr_)", "no_key")
|
|
237
237
|
if not KEY_CHARS.fullmatch(self._key): # fullmatch: "$" would let a trailing newline pass # a bad character would make http.client echo the key
|
|
238
238
|
raise OcrError("API key contains characters that are not allowed in a header", "no_key")
|
|
239
239
|
head["Authorization"] = "Bearer " + self._key
|
|
@@ -345,13 +345,17 @@ class Client:
|
|
|
345
345
|
data, _ = self._call("GET", "/v1/jobs/%s/result" % job_id, query={"format": format})
|
|
346
346
|
return data
|
|
347
347
|
|
|
348
|
-
def ocr(self, path, pipeline, format="text", timeout=DEFAULT_WAIT_TIMEOUT_S, **options):
|
|
349
|
-
"""Submit, wait, fetch: the result bytes. options go to submit (dpi, client_ref, job_budget, formats, features).
|
|
350
|
-
Raises PagesUnread (with .data, the result anyway) if a page failed or was not run: never silent.
|
|
348
|
+
def ocr(self, path, pipeline, format="text", timeout=DEFAULT_WAIT_TIMEOUT_S, on_accepted=None, **options):
|
|
349
|
+
"""Submit, wait, fetch: the result as bytes, also for text, md, json and hocr (use .decode("utf-8") for a string). options go to submit (dpi, client_ref, job_budget, formats, features).
|
|
350
|
+
Raises PagesUnread (with .data, the result anyway) if a page failed or was not run: never silent.
|
|
351
|
+
on_accepted(answer) is called with the answer to the submit; its "warnings" (absent from an older service) say why fewer pages run."""
|
|
351
352
|
if format not in FORMATS:
|
|
352
353
|
raise OcrError("format must be one of %s" % ", ".join(FORMATS), "bad_input")
|
|
353
354
|
source = read_document(path, self.allowed_dir) # read once: PDF output needs the same bytes again
|
|
354
|
-
|
|
355
|
+
accepted = self._submit(source, pipeline, **options)
|
|
356
|
+
if on_accepted:
|
|
357
|
+
on_accepted(accepted)
|
|
358
|
+
job_id = accepted.get("job_id")
|
|
355
359
|
if not job_id:
|
|
356
360
|
raise OcrError("the answer of %s has no job_id: check OCR_SOFTWARE_BASE_URL" % self.base_url, "bad_response")
|
|
357
361
|
done = self.wait(job_id, timeout)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ocr-software
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Client and command line for the ocr-software.com OCR API: text, word boxes, layout, searchable PDF.
|
|
5
5
|
License-Expression: MIT
|
|
6
|
-
Project-URL: Homepage, https://ocr-software.com/
|
|
6
|
+
Project-URL: Homepage, https://ocr-software.com/integrations/python/
|
|
7
7
|
Project-URL: Repository, https://github.com/ocr-software/ocr-software-python
|
|
8
8
|
Keywords: ocr,pdf,searchable-pdf,hocr,markdown,document-ai
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -40,11 +40,11 @@ Environment variables; the command line flag wins.
|
|
|
40
40
|
|
|
41
41
|
| Variable | Meaning |
|
|
42
42
|
|---|---|
|
|
43
|
-
| `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs. |
|
|
43
|
+
| `OCR_SOFTWARE_API_KEY` | API key (starts with `ocr_`). `OCR_KEY` is read too, it is the name used in the API docs; if both are set, `OCR_SOFTWARE_API_KEY` wins. |
|
|
44
44
|
| `OCR_SOFTWARE_BASE_URL` | Default `https://api.ocr-software.com`; own server e.g. `http://127.0.0.1:8080` (also `--base-url`). `OCR_BASE_URL` is read too. |
|
|
45
45
|
| `OCR_SOFTWARE_ALLOWED_DIR` | Optional: read documents only from below this folder (symlinks and `..` do not escape). |
|
|
46
46
|
|
|
47
|
-
The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr; `--list-pipelines` marks it. In Python: `client.ocr(path, client.default_pipeline())`.
|
|
47
|
+
The key is never printed and never part of an error message. Without `--pipeline` the default is used: the fastest and cheapest available pipeline (no previews). It is named on stderr (`-q` silences that line for bulk runs; `-o NUL` or `-o /dev/null` discards the result); `--list-pipelines` marks it. Warnings of the service (`dpi_ignored`, `job_budget`, `free_limit`, `balance_too_low`) are printed as `warning CODE: text` lines on stderr by `--dry-run` and before a job that uses `--dpi` or `--job-budget` (that job asks once with a dry run first). In Python: `client.ocr(path, client.default_pipeline())`.
|
|
48
48
|
|
|
49
49
|
## Command line
|
|
50
50
|
|
|
@@ -66,7 +66,7 @@ Options:
|
|
|
66
66
|
- `--dpi 150|200|300` (PDF render resolution)
|
|
67
67
|
- `--timeout SECONDS` (wait for the job, default 3600)
|
|
68
68
|
- `--job-budget MILLICENTS` (cap; ignored where billing is off)
|
|
69
|
-
- `--dry-run`, `--version`
|
|
69
|
+
- `-q`, `--dry-run`, `--version`
|
|
70
70
|
|
|
71
71
|
Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the file appears whole or not at all, an existing file is replaced, and the input file itself is refused as output.
|
|
72
72
|
|
|
@@ -85,7 +85,7 @@ Without `-o` the result goes to stdout as UTF-8 (not for pdf). With `-o` the fil
|
|
|
85
85
|
```python
|
|
86
86
|
from ocr_software import Client, PagesUnread
|
|
87
87
|
client = Client() # key and address from the environment
|
|
88
|
-
text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes; raises PagesUnread (with .data) if a page was not read
|
|
88
|
+
text = client.ocr("scan.pdf", "NAME@VERSION", "md") # bytes (text.decode("utf-8") for a string); raises PagesUnread (with .data) if a page was not read
|
|
89
89
|
pdf = client.ocr("scan.pdf", "NAME@VERSION", "pdf") # searchable PDF
|
|
90
90
|
job = client.submit("scan.pdf", "NAME@VERSION") # step by step: submit, wait, result
|
|
91
91
|
client.wait(job["job_id"]); client.result(job["job_id"], "json")
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ocr-software"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Client and command line for the ocr-software.com OCR API: text, word boxes, layout, searchable PDF."
|
|
9
9
|
readme = {file = "INSTALL.txt", content-type = "text/markdown"}
|
|
10
10
|
license = "MIT"
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
test = ["pytest==9.0.2"]
|
|
22
22
|
|
|
23
23
|
[project.urls]
|
|
24
|
-
Homepage = "https://ocr-software.com/"
|
|
24
|
+
Homepage = "https://ocr-software.com/integrations/python/"
|
|
25
25
|
Repository = "https://github.com/ocr-software/ocr-software-python"
|
|
26
26
|
|
|
27
27
|
[project.scripts]
|
|
@@ -221,3 +221,59 @@ def test_ctrl_c_ends_without_a_stack_trace(fake, doc, monkeypatch, capsysbinary)
|
|
|
221
221
|
monkeypatch.setattr(cli.Client, "ocr", interrupted)
|
|
222
222
|
code, out, errtext = run([doc, "--pipeline", "p@1"], capsysbinary)
|
|
223
223
|
assert code == 130 and out == b"" and "interrupted" in errtext and "Traceback" not in errtext
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_quiet_drops_the_default_pipeline_line_only(fake, doc, capsysbinary):
|
|
227
|
+
fake.scripted[("GET", "/v1/catalog")] = [(200, {}, {"pipelines": [{"pipeline": "p@1", "available": True, "price_per_page_millicents": 100}]})]
|
|
228
|
+
job_flow(fake, result=b"x")
|
|
229
|
+
code, _, errtext = run([doc, "-q"], capsysbinary)
|
|
230
|
+
assert code == 0 and errtext == ""
|
|
231
|
+
job_flow(fake, result=b"x")
|
|
232
|
+
code, _, errtext = run([doc], capsysbinary)
|
|
233
|
+
assert code == 0 and "no --pipeline given, using p@1" in errtext
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_output_to_the_null_device_is_accepted_and_discards(fake, doc, capsysbinary):
|
|
237
|
+
job_flow(fake, result=b"gone")
|
|
238
|
+
code, out, errtext = run([doc, "--pipeline", "p@1", "-o", os.devnull], capsysbinary)
|
|
239
|
+
assert (code, out, errtext) == (0, b"", "")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@pytest.mark.parametrize("target", [os.devnull, "/dev/null"])
|
|
243
|
+
def test_both_null_device_spellings_discard_on_every_system(fake, doc, capsysbinary, target):
|
|
244
|
+
job_flow(fake, result=b"gone")
|
|
245
|
+
code, out, errtext = run([doc, "--pipeline", "p@1", "-o", target], capsysbinary)
|
|
246
|
+
assert (code, out, errtext) == (0, b"", "")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
DPI_WARNING = {"code": "dpi_ignored", "message": "dpi applies to the pages of a PDF."}
|
|
250
|
+
DRY_WITH_WARNING = {"dry_run": True, "pages": 1, "price_per_page_millicents": 100, "price_millicents": 100, "max_charge_millicents": 0,
|
|
251
|
+
"pages_covered": 1, "warnings": [DPI_WARNING]}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_dry_run_shows_every_warning_as_its_own_stderr_line(fake, doc, capsysbinary):
|
|
255
|
+
fake.scripted[("POST", "/v1/jobs")] = [(200, {}, dict(DRY_WITH_WARNING, warnings=[DPI_WARNING, {"code": "job_budget", "message": "covers 1 of 2"}]))]
|
|
256
|
+
code, out, errtext = run([doc, "--pipeline", "a@1", "--dry-run", "--dpi", "300"], capsysbinary)
|
|
257
|
+
assert code == 0 and b"1 page with a@1" in out
|
|
258
|
+
assert errtext.splitlines() == ["ocr-software: warning dpi_ignored: dpi applies to the pages of a PDF.",
|
|
259
|
+
"ocr-software: warning job_budget: covers 1 of 2"]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_a_real_job_shows_the_warnings_of_the_answer_in_one_upload(fake, doc, capsysbinary):
|
|
263
|
+
job_flow(fake, result=b"text")
|
|
264
|
+
fake.scripted[("POST", "/v1/jobs")] = [(202, {}, {"job_id": "j1", "state": "running", "warnings": [DPI_WARNING]})]
|
|
265
|
+
code, out, errtext = run([doc, "--pipeline", "a@1", "--dpi", "300", "--job-budget", "1"], capsysbinary)
|
|
266
|
+
assert code == 0 and out == b"text" and errtext == "ocr-software: warning dpi_ignored: dpi applies to the pages of a PDF.\n"
|
|
267
|
+
posts = [c for c in fake.log if c["method"] == "POST" and "/result" not in c["path"]]
|
|
268
|
+
assert len(posts) == 1 and "dry_run" not in posts[0]["query"]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_an_older_service_without_warnings_breaks_nothing(fake, doc, capsysbinary):
|
|
272
|
+
job_flow(fake, result=b"text")
|
|
273
|
+
assert run([doc, "--pipeline", "a@1", "--dpi", "300"], capsysbinary) == (0, b"text", "")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_a_plain_real_job_stays_one_upload(fake, doc, capsysbinary):
|
|
277
|
+
job_flow(fake)
|
|
278
|
+
run([doc, "--pipeline", "a@1"], capsysbinary)
|
|
279
|
+
assert sum(1 for c in fake.log if c["method"] == "POST" and "/result" not in c["path"]) == 1
|
|
@@ -510,3 +510,13 @@ def test_split_pages_text_and_markdown():
|
|
|
510
510
|
md = "## Page 1\n\nfirst\n\n## Page 2\n\n## Page 7\n\nnot a page\n\n## Page 3\n\nlast\n"
|
|
511
511
|
assert split_pages(md, "md") == [(1, "first\n"), (2, "## Page 7\n\nnot a page\n"), (3, "last\n")]
|
|
512
512
|
assert split_pages("no headings", "md") == [(1, "no headings")]
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_both_names_set_the_ocr_software_name_wins_for_key_url_and_folder(monkeypatch):
|
|
516
|
+
for short, long_, bad, good in (("OCR_KEY", "OCR_SOFTWARE_API_KEY", "ocr_bogus", "ocr_good"),
|
|
517
|
+
("OCR_BASE_URL", "OCR_SOFTWARE_BASE_URL", "http://bad:1", "http://good:1"),
|
|
518
|
+
("OCR_ALLOWED_DIR", "OCR_SOFTWARE_ALLOWED_DIR", "/bad", "/good")):
|
|
519
|
+
monkeypatch.setenv(short, bad)
|
|
520
|
+
monkeypatch.setenv(long_, good)
|
|
521
|
+
c = Client()
|
|
522
|
+
assert (c._key, c.base_url, c.allowed_dir) == ("ocr_good", "http://good:1", "/good")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|