parserbee 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.
- parserbee/__init__.py +27 -0
- parserbee/client.py +149 -0
- parserbee/errors.py +77 -0
- parserbee-0.1.0.dist-info/METADATA +145 -0
- parserbee-0.1.0.dist-info/RECORD +7 -0
- parserbee-0.1.0.dist-info/WHEEL +4 -0
- parserbee-0.1.0.dist-info/licenses/LICENSE +21 -0
parserbee/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Python client for the ParserBee document extraction API."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .client import ParserBee # noqa: E402
|
|
6
|
+
from .errors import ( # noqa: E402
|
|
7
|
+
APIConnectionError,
|
|
8
|
+
APIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
InsufficientCreditsError,
|
|
11
|
+
InvalidRequestError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
ParserBeeError,
|
|
14
|
+
PermissionDeniedError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ParserBee",
|
|
19
|
+
"ParserBeeError",
|
|
20
|
+
"APIError",
|
|
21
|
+
"APIConnectionError",
|
|
22
|
+
"AuthenticationError",
|
|
23
|
+
"InsufficientCreditsError",
|
|
24
|
+
"InvalidRequestError",
|
|
25
|
+
"NotFoundError",
|
|
26
|
+
"PermissionDeniedError",
|
|
27
|
+
]
|
parserbee/client.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""HTTP client for the ParserBee API."""
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import IO, Any, Dict, List, Optional, Union
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
from .errors import APIConnectionError, APIError, error_for_status
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://app.parserbee.com"
|
|
15
|
+
DEFAULT_TIMEOUT = 120.0
|
|
16
|
+
|
|
17
|
+
FileInput = Union[str, os.PathLike, IO[bytes]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ParserBee:
|
|
21
|
+
"""Client for the ParserBee REST API.
|
|
22
|
+
|
|
23
|
+
The API key is read from the ``api_key`` argument or, if that is not
|
|
24
|
+
given, from the ``PARSERBEE_API_KEY`` environment variable. Generate a
|
|
25
|
+
key from Settings in the ParserBee dashboard; it starts with ``pk_``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: Optional[str] = None,
|
|
31
|
+
*,
|
|
32
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
33
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
34
|
+
session: Optional[requests.Session] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
api_key = api_key or os.environ.get("PARSERBEE_API_KEY")
|
|
37
|
+
if not api_key:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"No API key given. Pass api_key=... or set PARSERBEE_API_KEY."
|
|
40
|
+
)
|
|
41
|
+
self.api_key = api_key
|
|
42
|
+
self.base_url = base_url.rstrip("/")
|
|
43
|
+
self.timeout = timeout
|
|
44
|
+
self._session = session or requests.Session()
|
|
45
|
+
self._session.headers.update(
|
|
46
|
+
{
|
|
47
|
+
"x-api-key": api_key,
|
|
48
|
+
"User-Agent": f"parserbee-python/{__version__}",
|
|
49
|
+
"Accept": "application/json",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Dict[str, Any]:
|
|
54
|
+
url = f"{self.base_url}{path}"
|
|
55
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
56
|
+
try:
|
|
57
|
+
response = self._session.request(method, url, **kwargs)
|
|
58
|
+
except requests.RequestException as exc:
|
|
59
|
+
raise APIConnectionError(f"Could not reach the ParserBee API: {exc}") from exc
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
body = response.json()
|
|
63
|
+
except ValueError:
|
|
64
|
+
body = {}
|
|
65
|
+
|
|
66
|
+
if response.status_code >= 400:
|
|
67
|
+
raise error_for_status(response.status_code, body)
|
|
68
|
+
if not isinstance(body, dict):
|
|
69
|
+
raise APIError(
|
|
70
|
+
"Unexpected response from the ParserBee API",
|
|
71
|
+
status=response.status_code,
|
|
72
|
+
)
|
|
73
|
+
return body
|
|
74
|
+
|
|
75
|
+
def extract(
|
|
76
|
+
self,
|
|
77
|
+
template_id: str,
|
|
78
|
+
file: Optional[FileInput] = None,
|
|
79
|
+
*,
|
|
80
|
+
file_url: Optional[str] = None,
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""Extract structured data from a document with a Parser Template.
|
|
83
|
+
|
|
84
|
+
Pass either ``file`` (a path or an open binary file) or ``file_url``
|
|
85
|
+
(a public link to the document). Each extraction costs 1 page credit
|
|
86
|
+
per page processed.
|
|
87
|
+
|
|
88
|
+
Returns the full API response. The extracted fields are in
|
|
89
|
+
``result["data"]``; ``result["usage"]`` holds the page count and the
|
|
90
|
+
page credits charged.
|
|
91
|
+
"""
|
|
92
|
+
if not template_id:
|
|
93
|
+
raise ValueError("template_id is required")
|
|
94
|
+
if (file is None) == (file_url is None):
|
|
95
|
+
raise ValueError("Pass exactly one of file or file_url")
|
|
96
|
+
|
|
97
|
+
if file_url is not None:
|
|
98
|
+
return self._request(
|
|
99
|
+
"POST",
|
|
100
|
+
"/api/v1/extract",
|
|
101
|
+
json={"template_id": template_id, "file_url": file_url},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if isinstance(file, (str, os.PathLike)):
|
|
105
|
+
path = Path(file)
|
|
106
|
+
with path.open("rb") as handle:
|
|
107
|
+
return self._upload(template_id, handle, path.name)
|
|
108
|
+
|
|
109
|
+
name = Path(getattr(file, "name", "document")).name
|
|
110
|
+
return self._upload(template_id, file, name)
|
|
111
|
+
|
|
112
|
+
def _upload(self, template_id: str, handle: IO[bytes], name: str) -> Dict[str, Any]:
|
|
113
|
+
content_type = mimetypes.guess_type(name)[0] or "application/octet-stream"
|
|
114
|
+
return self._request(
|
|
115
|
+
"POST",
|
|
116
|
+
"/api/v1/extract",
|
|
117
|
+
data={"template_id": template_id},
|
|
118
|
+
files={"file": (name, handle, content_type)},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def list_templates(self) -> List[Dict[str, Any]]:
|
|
122
|
+
"""Return every Parser Template in the account, newest first."""
|
|
123
|
+
return self._request("GET", "/api/v1/templates").get("items", [])
|
|
124
|
+
|
|
125
|
+
def get_template(self, template_id: str) -> Dict[str, Any]:
|
|
126
|
+
"""Return one Parser Template, with its fields."""
|
|
127
|
+
return self._request("GET", f"/api/v1/templates/{quote(template_id, safe='')}")
|
|
128
|
+
|
|
129
|
+
def get_credits(self) -> Dict[str, Any]:
|
|
130
|
+
"""Return the page credit balance and the subscription plan."""
|
|
131
|
+
return self._request("GET", "/api/v1/credits")
|
|
132
|
+
|
|
133
|
+
def list_activity(self) -> List[Dict[str, Any]]:
|
|
134
|
+
"""Return the last 10 extraction requests, newest first."""
|
|
135
|
+
return self._request("GET", "/api/v1/activity").get("items", [])
|
|
136
|
+
|
|
137
|
+
def get_activity(self, request_id: str) -> Dict[str, Any]:
|
|
138
|
+
"""Return one extraction request, including the extracted data."""
|
|
139
|
+
return self._request("GET", f"/api/v1/activity/{quote(request_id, safe='')}")
|
|
140
|
+
|
|
141
|
+
def close(self) -> None:
|
|
142
|
+
"""Close the underlying HTTP session."""
|
|
143
|
+
self._session.close()
|
|
144
|
+
|
|
145
|
+
def __enter__(self) -> "ParserBee":
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def __exit__(self, *exc: Any) -> None:
|
|
149
|
+
self.close()
|
parserbee/errors.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Exceptions raised by the ParserBee client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ParserBeeError(Exception):
|
|
7
|
+
"""Base class for every error the client raises."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
*,
|
|
13
|
+
code: Optional[str] = None,
|
|
14
|
+
status: Optional[int] = None,
|
|
15
|
+
body: Optional[Dict[str, Any]] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.message = message
|
|
19
|
+
self.code = code
|
|
20
|
+
self.status = status
|
|
21
|
+
self.body = body or {}
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
parts = [self.message]
|
|
25
|
+
if self.code:
|
|
26
|
+
parts.append(f"(code: {self.code})")
|
|
27
|
+
if self.status:
|
|
28
|
+
parts.append(f"[HTTP {self.status}]")
|
|
29
|
+
return " ".join(parts)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuthenticationError(ParserBeeError):
|
|
33
|
+
"""The API key is missing or invalid (HTTP 401)."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InsufficientCreditsError(ParserBeeError):
|
|
37
|
+
"""The account has no page credits left (HTTP 402)."""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def credits_remaining(self) -> Optional[int]:
|
|
41
|
+
return self.body.get("credits_remaining")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PermissionDeniedError(ParserBeeError):
|
|
45
|
+
"""No active subscription, or the account is locked (HTTP 403)."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NotFoundError(ParserBeeError):
|
|
49
|
+
"""The template or activity record does not exist (HTTP 404)."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InvalidRequestError(ParserBeeError):
|
|
53
|
+
"""The request is missing a field or the file is not accepted (HTTP 400)."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class APIError(ParserBeeError):
|
|
57
|
+
"""Any other error returned by the API, including server errors."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class APIConnectionError(ParserBeeError):
|
|
61
|
+
"""The request did not reach the API (network error or timeout)."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_STATUS_ERRORS = {
|
|
65
|
+
400: InvalidRequestError,
|
|
66
|
+
401: AuthenticationError,
|
|
67
|
+
402: InsufficientCreditsError,
|
|
68
|
+
403: PermissionDeniedError,
|
|
69
|
+
404: NotFoundError,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def error_for_status(status: int, body: Dict[str, Any]) -> ParserBeeError:
|
|
74
|
+
"""Build the right exception for an error response."""
|
|
75
|
+
cls = _STATUS_ERRORS.get(status, APIError)
|
|
76
|
+
message = body.get("error") or f"ParserBee API returned HTTP {status}"
|
|
77
|
+
return cls(message, code=body.get("code"), status=status, body=body)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: parserbee
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the ParserBee document extraction API
|
|
5
|
+
Project-URL: Homepage, https://parserbee.com
|
|
6
|
+
Project-URL: Documentation, https://app.parserbee.com/docs
|
|
7
|
+
Project-URL: Source, https://github.com/ParserBee/parserbee-python
|
|
8
|
+
Author-email: ParserBee <support@parserbee.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: data extraction,document extraction,invoice,ocr,parserbee,pdf
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: requests>=2.28
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# parserbee-python
|
|
24
|
+
|
|
25
|
+
Python client for the [ParserBee](https://parserbee.com) API. Send a PDF, scan or phone photo of an invoice, purchase order, receipt or bank statement, and get back structured JSON that matches your Parser Template.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install parserbee
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires Python 3.9 or later.
|
|
34
|
+
|
|
35
|
+
## Before you start
|
|
36
|
+
|
|
37
|
+
1. Create a free account at [app.parserbee.com](https://app.parserbee.com/create-account). New accounts get 25 free pages, no credit card.
|
|
38
|
+
2. Create a Parser Template: upload one sample document and the AI builds the fields for you, or pick a ready-made one from the [template library](https://parserbee.com/templates/).
|
|
39
|
+
3. Copy your API key from **Settings** (it starts with `pk_`) and the template ID from the template page.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export PARSERBEE_API_KEY=pk_your_api_key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from parserbee import ParserBee
|
|
49
|
+
|
|
50
|
+
client = ParserBee() # reads PARSERBEE_API_KEY
|
|
51
|
+
|
|
52
|
+
result = client.extract("your_template_id", "invoice.pdf")
|
|
53
|
+
print(result["data"])
|
|
54
|
+
# {'invoice_number': 'INV-2026-001', 'vendor_name': 'Acme Corp', 'total_amount': 1500.0, ...}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Extract from a public link instead of a local file:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
result = client.extract("your_template_id", file_url="https://example.com/invoice.pdf")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An open binary file works too:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
with open("receipt.jpg", "rb") as f:
|
|
67
|
+
result = client.extract("your_template_id", f)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## The response
|
|
71
|
+
|
|
72
|
+
`extract()` returns the API response as a dict:
|
|
73
|
+
|
|
74
|
+
| Key | What it holds |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `data` | The extracted fields, shaped like your Parser Template |
|
|
77
|
+
| `request_id` | The request ID (also in Activity in the dashboard) |
|
|
78
|
+
| `credits_remaining` | Page credits left after this call |
|
|
79
|
+
| `usage` | `pages_processed`, `credits_charged`, `doc_size_bytes` |
|
|
80
|
+
| `processing_time_ms` | Time taken |
|
|
81
|
+
|
|
82
|
+
Each extraction costs 1 page credit per page processed, with a minimum of 1.
|
|
83
|
+
|
|
84
|
+
## Other methods
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
client.list_templates() # every Parser Template in the account
|
|
88
|
+
client.get_template("tpl_id") # one template with its fields
|
|
89
|
+
client.get_credits() # balance, plan and subscription status
|
|
90
|
+
client.list_activity() # the last 10 extractions
|
|
91
|
+
client.get_activity("req_id") # one extraction, with its data
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Errors
|
|
95
|
+
|
|
96
|
+
Every error is a subclass of `parserbee.ParserBeeError`, with `.code` and `.status`:
|
|
97
|
+
|
|
98
|
+
| Exception | When |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `InvalidRequestError` | 400: missing field, or a file type that is not accepted |
|
|
101
|
+
| `AuthenticationError` | 401: missing or invalid API key |
|
|
102
|
+
| `InsufficientCreditsError` | 402: no page credits left (`.credits_remaining`) |
|
|
103
|
+
| `PermissionDeniedError` | 403: no active subscription |
|
|
104
|
+
| `NotFoundError` | 404: template or request not found |
|
|
105
|
+
| `APIError` | Any other API error |
|
|
106
|
+
| `APIConnectionError` | The request did not reach the API |
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from parserbee import InsufficientCreditsError
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
client.extract("your_template_id", "invoice.pdf")
|
|
113
|
+
except InsufficientCreditsError:
|
|
114
|
+
print("Top up page credits in the dashboard")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Examples
|
|
118
|
+
|
|
119
|
+
The [examples](https://github.com/ParserBee/parserbee-python/tree/main/examples) folder has scripts you can run as they are:
|
|
120
|
+
|
|
121
|
+
- `extract_invoice.py`: one file, printed as JSON
|
|
122
|
+
- `extract_from_url.py`: a document that is already online
|
|
123
|
+
- `folder_to_csv.py`: every PDF and image in a folder, one CSV row each
|
|
124
|
+
- `check_credits.py`: balance and template IDs
|
|
125
|
+
|
|
126
|
+
## Supported files
|
|
127
|
+
|
|
128
|
+
PDF, PNG, JPG, WebP and HEIC, up to 50 MB. The API also reads DOC, PPT, PPTX, XLSX, CSV, TXT, RTF, ODT, HTML and EML; see the [API reference](https://app.parserbee.com/docs) for details. Scans and phone photos work, and there is no need to run OCR first. DOCX is not accepted: save it as PDF first.
|
|
129
|
+
|
|
130
|
+
## No code needed?
|
|
131
|
+
|
|
132
|
+
If you only need the data in a spreadsheet, you do not need this library. [Data Lab](https://parserbee.com/tools/bank-statement-converter/#data-lab) in the dashboard takes a batch of documents and exports one combined Excel, CSV or Google Sheets file. To try a single invoice in the browser, use the free [invoice parser](https://parserbee.com/tools/invoice-parser/).
|
|
133
|
+
|
|
134
|
+
Choosing a tool for invoices? Our comparison of [invoice OCR software](https://parserbee.com/blog/best-invoice-ocr-software/) covers 10 options, tested on real invoices, scans and phone photos.
|
|
135
|
+
|
|
136
|
+
## Links
|
|
137
|
+
|
|
138
|
+
- [ParserBee](https://parserbee.com)
|
|
139
|
+
- [API reference](https://app.parserbee.com/docs)
|
|
140
|
+
- [Pricing](https://parserbee.com/pricing/)
|
|
141
|
+
- Support: support@parserbee.com
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
parserbee/__init__.py,sha256=DdP2GEvaMpMjZWfmRLlPf951_Yei4pHcEwHWFjXKRQg,585
|
|
2
|
+
parserbee/client.py,sha256=ieYHwaYYVvQzH3fo_Z56uKcnvHri148IHjt2C2BWwOU,5293
|
|
3
|
+
parserbee/errors.py,sha256=VV854RlMVrqta5kiYpIPzJh-Q1NaBblBav8pzuRW4HA,2174
|
|
4
|
+
parserbee-0.1.0.dist-info/METADATA,sha256=LjToRLDrGAfqZ3dvCvkY9oXWgYf40qVJlgKRzKl5yak,5347
|
|
5
|
+
parserbee-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
parserbee-0.1.0.dist-info/licenses/LICENSE,sha256=WMylYQd76UfonebFez-VIVw180ftkAN4PUC_Kc-rYVM,1066
|
|
7
|
+
parserbee-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ParserBee
|
|
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.
|