plotwork 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.
- plotwork/__init__.py +90 -0
- plotwork/_generated.py +2201 -0
- plotwork/_request.py +187 -0
- plotwork/_version.py +3 -0
- plotwork/py.typed +0 -0
- plotwork-0.1.0.dist-info/METADATA +141 -0
- plotwork-0.1.0.dist-info/RECORD +9 -0
- plotwork-0.1.0.dist-info/WHEEL +4 -0
- plotwork-0.1.0.dist-info/licenses/LICENSE +21 -0
plotwork/_request.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""The one transport every operation calls.
|
|
2
|
+
|
|
3
|
+
It sends a request to the Plotwork API with ``urllib.request`` and reads the answer. A 2xx answer is the result; a mistake in a
|
|
4
|
+
diagram is part of that result (``valid: False``, ``errors`` with line, column and hint), never an exception. Anything else is a
|
|
5
|
+
``PlotworkApiError`` with the API's stable ``error_code``. A call the API may answer again safely is retried on 429 and 503,
|
|
6
|
+
after the ``Retry-After`` it gives; publishing is never retried.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import socket
|
|
14
|
+
import time
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
from typing import Any, Callable, Mapping, Optional
|
|
19
|
+
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
|
|
22
|
+
#: The longest the client waits before one retry, whatever ``Retry-After`` says, in seconds.
|
|
23
|
+
MAX_RETRY_WAIT = 30.0
|
|
24
|
+
|
|
25
|
+
#: Opens a ``urllib.request.Request`` and returns the response, called like ``urllib.request.urlopen(request, timeout=...)``.
|
|
26
|
+
#: It may raise ``urllib.error.HTTPError`` for a status that is not 2xx, as ``urlopen`` does, or return that response.
|
|
27
|
+
Opener = Callable[..., Any]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PlotworkApiError(Exception):
|
|
31
|
+
"""The API's error envelope (``ErrorResponse``), or what the client knew when there was none."""
|
|
32
|
+
|
|
33
|
+
#: The HTTP status, or 0 when no answer came (offline, refused, timed out).
|
|
34
|
+
status: int
|
|
35
|
+
#: A stable key such as ``auth.invalid_key``, ``rate_limit.exceeded`` or ``request.timeout``.
|
|
36
|
+
error_code: str
|
|
37
|
+
message: str
|
|
38
|
+
details: Any
|
|
39
|
+
#: The API's trace id, to quote to support.
|
|
40
|
+
request_id: Optional[str]
|
|
41
|
+
|
|
42
|
+
def __init__(self, status: int, error_code: str, message: str, details: Any = None, request_id: Optional[str] = None) -> None:
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
self.status = status
|
|
45
|
+
self.error_code = error_code
|
|
46
|
+
self.message = message
|
|
47
|
+
self.details = details
|
|
48
|
+
self.request_id = request_id
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
return f"PlotworkApiError(status={self.status!r}, error_code={self.error_code!r}, message={self.message!r}, request_id={self.request_id!r})"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def retry_delay(retry_after: Optional[str], attempt: int) -> float:
|
|
55
|
+
"""How many seconds to wait before retry number ``attempt`` (from 1): the API's ``Retry-After``, else a doubling half second."""
|
|
56
|
+
seconds = math.nan
|
|
57
|
+
if retry_after is not None:
|
|
58
|
+
try:
|
|
59
|
+
seconds = float(retry_after)
|
|
60
|
+
except ValueError:
|
|
61
|
+
seconds = math.nan
|
|
62
|
+
wait = seconds if math.isfinite(seconds) and seconds >= 0 else 0.5 * 2 ** (attempt - 1)
|
|
63
|
+
return min(wait, MAX_RETRY_WAIT)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _query_value(value: Any) -> str:
|
|
67
|
+
if value is True:
|
|
68
|
+
return "true"
|
|
69
|
+
if value is False:
|
|
70
|
+
return "false"
|
|
71
|
+
return str(value)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _status_of(response: Any) -> int:
|
|
75
|
+
status = getattr(response, "status", None)
|
|
76
|
+
return int(status if status is not None else response.getcode())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _header(response: Any, name: str) -> Optional[str]:
|
|
80
|
+
headers = getattr(response, "headers", None)
|
|
81
|
+
if headers is None:
|
|
82
|
+
return None
|
|
83
|
+
value = headers.get(name)
|
|
84
|
+
return None if value is None else str(value)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _is_timeout(error: BaseException) -> bool:
|
|
88
|
+
reason = getattr(error, "reason", None)
|
|
89
|
+
return isinstance(error, (TimeoutError, socket.timeout)) or isinstance(reason, (TimeoutError, socket.timeout))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _no_answer(error: BaseException) -> PlotworkApiError:
|
|
93
|
+
if _is_timeout(error):
|
|
94
|
+
return PlotworkApiError(0, "request.timeout", "The Plotwork API did not answer in time")
|
|
95
|
+
return PlotworkApiError(0, "request.failed", "The Plotwork API could not be reached")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _error_of(status: int, raw: bytes, request_id: Optional[str]) -> PlotworkApiError:
|
|
99
|
+
try:
|
|
100
|
+
body = json.loads(raw.decode("utf-8"))
|
|
101
|
+
except ValueError:
|
|
102
|
+
body = None # Not JSON (a proxy's page): the status says enough.
|
|
103
|
+
if isinstance(body, dict) and isinstance(body.get("errorCode"), str):
|
|
104
|
+
message = body.get("message")
|
|
105
|
+
envelope_id = body.get("requestId")
|
|
106
|
+
return PlotworkApiError(
|
|
107
|
+
status,
|
|
108
|
+
body["errorCode"],
|
|
109
|
+
message if isinstance(message, str) else f"HTTP {status}",
|
|
110
|
+
body.get("details"),
|
|
111
|
+
envelope_id if isinstance(envelope_id, str) else request_id,
|
|
112
|
+
)
|
|
113
|
+
return PlotworkApiError(status, f"http.{status}", f"The Plotwork API answered HTTP {status}", None, request_id)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _default_opener() -> Opener:
|
|
117
|
+
director = urllib.request.build_opener()
|
|
118
|
+
return director.open
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Transport:
|
|
122
|
+
"""How the client sends requests: the address, the headers, the timeout, the retries, and the opener."""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
base_url: str,
|
|
128
|
+
headers: Mapping[str, str],
|
|
129
|
+
timeout: float,
|
|
130
|
+
max_retries: int,
|
|
131
|
+
opener: Optional[Opener] = None,
|
|
132
|
+
sleep: Optional[Callable[[float], Any]] = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
self.base_url = base_url
|
|
135
|
+
self.headers = dict(headers)
|
|
136
|
+
self.timeout = timeout
|
|
137
|
+
self.max_retries = max(0, max_retries)
|
|
138
|
+
self.opener: Opener = opener if opener is not None else _default_opener()
|
|
139
|
+
self.sleep: Callable[[float], Any] = sleep if sleep is not None else time.sleep
|
|
140
|
+
|
|
141
|
+
def url_of(self, path: str, query: Optional[Mapping[str, Any]] = None) -> str:
|
|
142
|
+
"""The address of ``path`` on the base address, with every query value that is not ``None``."""
|
|
143
|
+
base = self.base_url if self.base_url.endswith("/") else f"{self.base_url}/"
|
|
144
|
+
url = urllib.parse.urljoin(base, path)
|
|
145
|
+
pairs = [(name, _query_value(value)) for name, value in (query or {}).items() if value is not None]
|
|
146
|
+
return f"{url}?{urllib.parse.urlencode(pairs)}" if pairs else url
|
|
147
|
+
|
|
148
|
+
def request(self, method: str, path: str, *, idempotent: bool, body: Any = None, query: Optional[Mapping[str, Any]] = None) -> Any:
|
|
149
|
+
"""Sends one request and returns the decoded JSON answer, or raises ``PlotworkApiError``."""
|
|
150
|
+
retries = self.max_retries if idempotent else 0
|
|
151
|
+
headers = {"Accept": "application/json", "User-Agent": f"plotwork-python/{__version__}"}
|
|
152
|
+
data = None
|
|
153
|
+
if body is not None:
|
|
154
|
+
headers["Content-Type"] = "application/json"
|
|
155
|
+
data = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
156
|
+
headers.update(self.headers)
|
|
157
|
+
url = self.url_of(path, query)
|
|
158
|
+
attempt = 0
|
|
159
|
+
while True:
|
|
160
|
+
prepared = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
161
|
+
try:
|
|
162
|
+
response = self.opener(prepared, timeout=self.timeout)
|
|
163
|
+
except urllib.error.HTTPError as answer:
|
|
164
|
+
response = answer
|
|
165
|
+
except (urllib.error.URLError, OSError) as error:
|
|
166
|
+
raise _no_answer(error) from None
|
|
167
|
+
try:
|
|
168
|
+
status = _status_of(response)
|
|
169
|
+
if 200 <= status < 300:
|
|
170
|
+
try:
|
|
171
|
+
raw = response.read()
|
|
172
|
+
except OSError as error:
|
|
173
|
+
raise _no_answer(error) from None
|
|
174
|
+
return None if status == 204 or raw == b"" else json.loads(raw.decode("utf-8"))
|
|
175
|
+
if status in (429, 503) and attempt < retries:
|
|
176
|
+
attempt += 1
|
|
177
|
+
self.sleep(retry_delay(_header(response, "retry-after"), attempt))
|
|
178
|
+
continue
|
|
179
|
+
try:
|
|
180
|
+
raw = response.read()
|
|
181
|
+
except OSError:
|
|
182
|
+
raw = b""
|
|
183
|
+
raise _error_of(status, raw, _header(response, "x-request-id"))
|
|
184
|
+
finally:
|
|
185
|
+
close = getattr(response, "close", None)
|
|
186
|
+
if close is not None:
|
|
187
|
+
close()
|
plotwork/_version.py
ADDED
plotwork/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plotwork
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Plotwork API from Python: validate, draw, render, import and publish diagrams, on the standard library alone.
|
|
5
|
+
Project-URL: Homepage, https://plotwork.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.plotwork.dev/sdks
|
|
7
|
+
Project-URL: Source, https://github.com/plotwork-official/plotwork-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/plotwork-official/plotwork-sdk/issues
|
|
9
|
+
Author: Plotwork
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api-client,architecture,diagrams,diagrams-as-code,plotwork,svg
|
|
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.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Documentation
|
|
24
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
25
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://plotwork.dev">
|
|
32
|
+
<picture>
|
|
33
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://plotwork.dev/brand/logo/plotwork-lockup-on-dark.svg">
|
|
34
|
+
<img alt="Plotwork" src="https://plotwork.dev/brand/logo/plotwork-lockup-on-light.svg" width="240">
|
|
35
|
+
</picture>
|
|
36
|
+
</a>
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
# plotwork
|
|
40
|
+
|
|
41
|
+
<p>
|
|
42
|
+
<a href="https://pypi.org/project/plotwork/"><img alt="PyPI" src="https://img.shields.io/pypi/v/plotwork?logo=pypi&logoColor=white&color=0f766e"></a>
|
|
43
|
+
<img alt="Python" src="https://img.shields.io/pypi/pyversions/plotwork?logo=python&logoColor=white&color=0f766e">
|
|
44
|
+
<a href="https://pypi.org/project/plotwork/#provenance"><img alt="Provenance" src="https://img.shields.io/badge/provenance-attested-0f766e?logo=sigstore&logoColor=white"></a>
|
|
45
|
+
<img alt="Runtime dependencies" src="https://img.shields.io/badge/runtime%20dependencies-0-0f766e?logo=dependabot&logoColor=white">
|
|
46
|
+
<img alt="Typed" src="https://img.shields.io/badge/types-generated%20from%20OpenAPI-3776ab?logo=python&logoColor=white">
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
The hosted Plotwork API from Python. Every `/v1` operation (validate, draw, render, import a file, publish, patterns, icons) is a typed method generated from the API's OpenAPI document, sent with `urllib` from the standard library alone. It runs on Python 3.10 and later.
|
|
50
|
+
|
|
51
|
+
## TL;DR
|
|
52
|
+
|
|
53
|
+
| Question | Answer |
|
|
54
|
+
|---|---|
|
|
55
|
+
| What does it install? | Nothing else. |
|
|
56
|
+
| Where do the types come from? | The API's OpenAPI document, kept in the SDK repository as [`spec/openapi.json`](https://github.com/plotwork-official/plotwork-sdk/blob/main/spec/openapi.json). Every request and answer is a `TypedDict`. |
|
|
57
|
+
| How do I sign in? | An API key from the Plotwork portal (`api_key`), or an OAuth access token (`access_token`). |
|
|
58
|
+
| Does a mistake in a diagram raise? | No. It is part of the answer, with a line, a column and a hint. Only a failed request raises, as `PlotworkApiError` with a stable `error_code`. |
|
|
59
|
+
| Does it retry? | A safe call is retried twice on 429 and 503, after the API's `Retry-After`. Publishing is never retried. |
|
|
60
|
+
| Is there a TypeScript client? | Yes, [`@plotwork.official/sdk`](https://www.npmjs.com/package/@plotwork.official/sdk), with the same calls in camelCase. |
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install plotwork
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Use
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import os
|
|
72
|
+
|
|
73
|
+
from plotwork import Plotwork
|
|
74
|
+
|
|
75
|
+
plotwork = Plotwork(api_key=os.environ["PLOTWORK_API_KEY"])
|
|
76
|
+
|
|
77
|
+
source = 'flow "Checkout"\nnode web service "Web tier"\nnode api service "Orders API"\nweb -> api : "places order"\n'
|
|
78
|
+
|
|
79
|
+
checked = plotwork.diagrams.validate(source=source)
|
|
80
|
+
if not checked["valid"]:
|
|
81
|
+
for error in checked["errors"]:
|
|
82
|
+
print(error["line"], error["column"], error["message"], error.get("hint"))
|
|
83
|
+
|
|
84
|
+
published = plotwork.diagrams.publish(source=source, embedSource=True)
|
|
85
|
+
# A diagram an organisation's policy keeps private has no public link.
|
|
86
|
+
if "url" in published:
|
|
87
|
+
print(published["url"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A method takes the request's fields as keyword arguments, named as the API names them (`embedSource`, `documentPath`), and returns the answer as a plain `dict` typed by a `TypedDict`. A body you already have as a `dict` is passed with `**body`. Keep the key in an environment variable or your CI's secret store, never in a file you commit.
|
|
91
|
+
|
|
92
|
+
| Group | Calls |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `diagrams` | `validate`, `draw`, `render`, `outline`, `read`, `edit`, `compare`, `suggest`, `convert_mermaid`, `publish` |
|
|
95
|
+
| `import_source` | A diagram from Terraform, OpenAPI, SQL, Kubernetes, Compose and the other formats the API reads |
|
|
96
|
+
| `markdown` | `sync`, `plan` |
|
|
97
|
+
| `patterns` | `find`, `get`, `draw`, `check` |
|
|
98
|
+
| `icons` | `search`, `map` |
|
|
99
|
+
| `vocabulary` | The language's statements and limits, for a kind or a topic |
|
|
100
|
+
|
|
101
|
+
## Errors
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from plotwork import PlotworkApiError
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
plotwork.diagrams.render(source=source)
|
|
108
|
+
except PlotworkApiError as error:
|
|
109
|
+
print(error.status, error.error_code, error.request_id)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| `error_code` | Means |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `auth.missing`, `auth.invalid_key` | No key, or one the API does not accept |
|
|
115
|
+
| `rate_limit.exceeded` | Too many calls; already retried for a safe call |
|
|
116
|
+
| `http.<status>` | An answer that was not the API's error envelope, such as a proxy's page |
|
|
117
|
+
| `request.timeout`, `request.failed` | No answer came; `status` is 0 |
|
|
118
|
+
|
|
119
|
+
## Options
|
|
120
|
+
|
|
121
|
+
| Option | Default |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `api_key` | none |
|
|
124
|
+
| `access_token` | none |
|
|
125
|
+
| `base_url` | `https://api.plotwork.dev` (`https://api-preview.plotwork.dev` for preview, or a self-hosted engine) |
|
|
126
|
+
| `timeout` | `60` seconds |
|
|
127
|
+
| `max_retries` | `2` |
|
|
128
|
+
| `opener` | `urllib.request.build_opener().open`; pass your own for a proxy or a test |
|
|
129
|
+
| `sleep` | `time.sleep`, the wait before a retry |
|
|
130
|
+
|
|
131
|
+
`plotwork.with_options(timeout=120)` gives a copy with another timeout or number of retries, for a slow call such as a large import.
|
|
132
|
+
|
|
133
|
+
## See also
|
|
134
|
+
|
|
135
|
+
| Page | Why |
|
|
136
|
+
|---|---|
|
|
137
|
+
| [SDKs guide](https://docs.plotwork.dev/sdks) | Every Plotwork SDK, and which to choose |
|
|
138
|
+
| [API reference](https://docs.plotwork.dev) | What each operation does |
|
|
139
|
+
| [TypeScript client](https://www.npmjs.com/package/@plotwork.official/sdk) | The same API from TypeScript |
|
|
140
|
+
| [Viewer](https://www.npmjs.com/package/@plotwork.official/viewer) | Show what you publish, interactively, in a web page |
|
|
141
|
+
| [Source](https://github.com/plotwork-official/plotwork-sdk/tree/main/python) | This package, its tests and its generator |
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
plotwork/__init__.py,sha256=eTQnRuuT5YAYs9rdw0cc8zE2RKtpskUoE4jS2mGs38M,3464
|
|
2
|
+
plotwork/_generated.py,sha256=B0ZaWhjwyexxDZhzg0OlSRsKRp5pC4iLHHfGqR8nvd8,114022
|
|
3
|
+
plotwork/_request.py,sha256=itD8pvNNioW-DYrNcSCuWapDEQGE-aU1YsLtP32dJp0,7747
|
|
4
|
+
plotwork/_version.py,sha256=XR92RURavhZFjFy8y6Atyjs7_paojt-E7BKhSavuEbU,138
|
|
5
|
+
plotwork/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
plotwork-0.1.0.dist-info/METADATA,sha256=8kAYBZlyme17saQqfHgF90Hq51rM96UUVtuKWTP1U7k,6661
|
|
7
|
+
plotwork-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
plotwork-0.1.0.dist-info/licenses/LICENSE,sha256=qwged3eiPcCNEOysunrXSLtz_IQpKD4xzLQlqmNEfO0,1065
|
|
9
|
+
plotwork-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Plotwork
|
|
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.
|