kes_tools

Python SDK for kes-tools: convert PDFs and Kurzweil 3000 .kes files into .kes files with OCR, layout and reading order from Datalab, through the kes-tools website.

pip install kes-tools-sdk
from kes_tools import Client

with Client() as client:
    for result in client.convert(["book.pdf", "chapter.kes"], output="converted"):
        print(result.file, "->", result.output if result.ok else result.error)

Conversions run on the kes-tools website under your account and are billed to the Datalab account whose API key you set up there. Client() uses the sign-in from kes-tools login (with pip install 'kes-tools-sdk[keyring]'), or $KES_TOOLS_REFRESH_TOKEN; Client(refresh_token=...) uses the given one, and Client.login signs in through the browser. All errors derive from KesToolsError.

DEFAULT_URL = 'https://dbqmri38t4eq6.cloudfront.net'
@dataclass(frozen=True)
class Account:

The signed-in account. key_hint is the last four characters of the Datalab API key, if one is set up.

Account(email: str | None, has_datalab_key: bool, key_hint: str | None)
email: str | None
has_datalab_key: bool
key_hint: str | None
class ApiError(kes_tools.KesToolsError):

The kes-tools website answered a request with an error.

ApiError(status: int, message: str)
status
class AuthError(kes_tools.KesToolsError):

Not signed in, the sign-in expired or was revoked, or signing in failed.

class Client:

A signed-in connection to the kes-tools website.

url defaults to $KES_TOOLS_URL or the public site. Without refresh_token, the sign-in comes from $KES_TOOLS_REFRESH_TOKEN or the one kes-tools login (or Client.login) stored in the system keychain. Use it as a context manager, or call close(), to release its connections.

Client( url: str | None = None, refresh_token: str | None = None, *, http: httpx.Client | None = None, store: kes_tools.auth.Store | None = None, timeout: float = 60)
url
http
def close(self) -> None:

Closes the client's connections.

def login( self, notify: Callable[[str], NoneType] = <built-in function print>) -> str | None:

Signs in through the browser, stores the sign-in in the keychain and returns the account's email.

def logout(self) -> None:

Revokes the stored sign-in and removes it from the keychain.

def account(self) -> Account:

Who is signed in, and whether their Datalab API key is set up.

def jobs(self, status: str | None = None) -> list[Job]:

Your conversions, newest first, optionally only those with status.

def job(self, job: Job | str) -> Job:

One conversion, as it is now. For a done job this includes a fresh download_url.

def submit( self, path: str | os.PathLike, mode: str = 'balanced', image: str = 'color') -> Job:

Uploads one .pdf or .kes file and starts converting it; returns its job without waiting.

def wait( self, jobs: Iterable[Job | str], timeout: float = 3600, poll: float | None = None) -> list[Job]:

Waits until the jobs are done or failed, or timeout seconds have passed; returns them in the same order, with the status they had last.

def download( self, job: Job | str, folder: str | os.PathLike = '.', overwrite: bool = False) -> pathlib.Path:

Saves a done job's result in folder and returns its path. An existing file is kept and the new one gets " (2)" etc. unless overwrite.

def delete(self, jobs: Iterable[Job | str]) -> int:

Deletes conversions and their files on the server; returns how many.

def convert( self, files: Iterable[str | os.PathLike], output: str | os.PathLike = '.', mode: str = 'balanced', image: str = 'color', timeout: float = 3600, overwrite: bool = False, wait: bool = True, progress: Callable[[str], NoneType] | None = None) -> list[ConversionResult]:

Converts .pdf and .kes files and saves the results in output, one result per file in the same order.

Raises MissingDatalabKey if the account has no Datalab API key. A file that fails to upload or convert gets a result with error instead of raising. With wait=False the files are only uploaded and started; the results then hold the jobs, for wait and download later. progress receives messages for people.

@dataclass(frozen=True)
class ConversionResult:

The outcome of converting one file with Client.convert: output is set when the converted file was saved, error otherwise. job is None when the file could not be uploaded.

ConversionResult( file: pathlib.Path, job: Job | None, output: pathlib.Path | None = None, error: str | None = None)
file: pathlib.Path
job: Job | None
output: pathlib.Path | None = None
error: str | None = None
ok: bool

Whether the converted file was saved.

@dataclass(frozen=True)
class Job:

A conversion on the kes-tools website.

status is one of "awaiting_upload", "queued", "processing", "done" and "failed". cost_cents is what Datalab billed, in US cents. download_url is set only on a done job fetched with Client.job and expires after a few minutes; use Client.download instead of fetching it yourself.

Job( id: str, status: str, filename: str, kind: str, mode: str, image: str, pages: int | None = None, cost_cents: float | None = None, error: str | None = None, created_at: datetime.datetime | None = None, download_url: str | None = None, raw: dict = <factory>)
id: str
status: str
filename: str
kind: str
mode: str
image: str
pages: int | None = None
cost_cents: float | None = None
error: str | None = None
created_at: datetime.datetime | None = None
download_url: str | None = None
raw: dict
running: bool

Still uploading, waiting in the queue or converting.

done: bool

Converted; the result can be downloaded.

failed: bool

The conversion failed; error says why.

@classmethod
def from_api(cls, data: dict) -> Job:

A job from the website's JSON for it.

class KesToolsError(builtins.Exception):

Base class of the errors kes-tools raises.

class MissingDatalabKey(kes_tools.KesToolsError):

The account has no Datalab API key; it can only be added in Settings on the website.

__version__ = '0.1.1'