vessel-api-python 1.0.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.
- vessel_api_python/__init__.py +183 -0
- vessel_api_python/_client.py +163 -0
- vessel_api_python/_constants.py +8 -0
- vessel_api_python/_errors.py +137 -0
- vessel_api_python/_iterator.py +120 -0
- vessel_api_python/_models.py +887 -0
- vessel_api_python/_services.py +1180 -0
- vessel_api_python/_transport.py +185 -0
- vessel_api_python/py.typed +0 -0
- vessel_api_python-1.0.0.dist-info/METADATA +168 -0
- vessel_api_python-1.0.0.dist-info/RECORD +13 -0
- vessel_api_python-1.0.0.dist-info/WHEEL +4 -0
- vessel_api_python-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""HTTP transport middleware for auth and retry logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import datetime
|
|
7
|
+
import math
|
|
8
|
+
import random
|
|
9
|
+
import time
|
|
10
|
+
from email.utils import parsedate_to_datetime
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from ._constants import MAX_BACKOFF
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Auth transports
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthTransport(httpx.BaseTransport):
|
|
22
|
+
"""Sync transport that adds Bearer token auth and User-Agent headers."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, base: httpx.BaseTransport, api_key: str, user_agent: str) -> None:
|
|
25
|
+
self._base = base
|
|
26
|
+
self._api_key = api_key
|
|
27
|
+
self._user_agent = user_agent
|
|
28
|
+
|
|
29
|
+
def handle_request(self, request: httpx.Request) -> httpx.Response:
|
|
30
|
+
request.headers["Authorization"] = f"Bearer {self._api_key}"
|
|
31
|
+
request.headers["User-Agent"] = self._user_agent
|
|
32
|
+
return self._base.handle_request(request)
|
|
33
|
+
|
|
34
|
+
def close(self) -> None:
|
|
35
|
+
self._base.close()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AsyncAuthTransport(httpx.AsyncBaseTransport):
|
|
39
|
+
"""Async transport that adds Bearer token auth and User-Agent headers."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, base: httpx.AsyncBaseTransport, api_key: str, user_agent: str) -> None:
|
|
42
|
+
self._base = base
|
|
43
|
+
self._api_key = api_key
|
|
44
|
+
self._user_agent = user_agent
|
|
45
|
+
|
|
46
|
+
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
|
47
|
+
request.headers["Authorization"] = f"Bearer {self._api_key}"
|
|
48
|
+
request.headers["User-Agent"] = self._user_agent
|
|
49
|
+
return await self._base.handle_async_request(request)
|
|
50
|
+
|
|
51
|
+
async def aclose(self) -> None:
|
|
52
|
+
await self._base.aclose()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Retry transports
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RetryTransport(httpx.BaseTransport):
|
|
61
|
+
"""Sync transport with retry logic for 429, 5xx, and transient errors.
|
|
62
|
+
|
|
63
|
+
Implements exponential backoff with jitter, respects Retry-After headers
|
|
64
|
+
(both seconds and HTTP-date formats), and caps backoff at 30 seconds.
|
|
65
|
+
Only retries non-idempotent methods (POST/PATCH) on 429.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, base: httpx.BaseTransport, max_retries: int = 3) -> None:
|
|
69
|
+
self._base = base
|
|
70
|
+
self._max_retries = max(max_retries, 0)
|
|
71
|
+
|
|
72
|
+
def handle_request(self, request: httpx.Request) -> httpx.Response:
|
|
73
|
+
for attempt in range(self._max_retries + 1):
|
|
74
|
+
try:
|
|
75
|
+
response = self._base.handle_request(request)
|
|
76
|
+
except httpx.TransportError:
|
|
77
|
+
# Retry transient network errors for idempotent methods only.
|
|
78
|
+
if attempt >= self._max_retries or not _is_idempotent(request.method):
|
|
79
|
+
raise
|
|
80
|
+
time.sleep(_calc_exp_backoff(attempt))
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if not _is_retryable(response.status_code) or attempt >= self._max_retries:
|
|
84
|
+
return response
|
|
85
|
+
|
|
86
|
+
# Don't retry non-idempotent methods on 5xx.
|
|
87
|
+
if response.status_code != 429 and not _is_idempotent(request.method):
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
wait = _calc_backoff(attempt, response)
|
|
91
|
+
# Read and discard the body to free the connection.
|
|
92
|
+
response.read()
|
|
93
|
+
response.close()
|
|
94
|
+
time.sleep(wait)
|
|
95
|
+
|
|
96
|
+
# Unreachable — the loop always returns.
|
|
97
|
+
raise RuntimeError("vesselapi: retry loop exited unexpectedly") # pragma: no cover
|
|
98
|
+
|
|
99
|
+
def close(self) -> None:
|
|
100
|
+
self._base.close()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AsyncRetryTransport(httpx.AsyncBaseTransport):
|
|
104
|
+
"""Async transport with retry logic for 429, 5xx, and transient errors.
|
|
105
|
+
|
|
106
|
+
Same logic as RetryTransport but using asyncio.sleep for async contexts.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, base: httpx.AsyncBaseTransport, max_retries: int = 3) -> None:
|
|
110
|
+
self._base = base
|
|
111
|
+
self._max_retries = max(max_retries, 0)
|
|
112
|
+
|
|
113
|
+
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
|
114
|
+
for attempt in range(self._max_retries + 1):
|
|
115
|
+
try:
|
|
116
|
+
response = await self._base.handle_async_request(request)
|
|
117
|
+
except httpx.TransportError:
|
|
118
|
+
if attempt >= self._max_retries or not _is_idempotent(request.method):
|
|
119
|
+
raise
|
|
120
|
+
await asyncio.sleep(_calc_exp_backoff(attempt))
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if not _is_retryable(response.status_code) or attempt >= self._max_retries:
|
|
124
|
+
return response
|
|
125
|
+
|
|
126
|
+
if response.status_code != 429 and not _is_idempotent(request.method):
|
|
127
|
+
return response
|
|
128
|
+
|
|
129
|
+
wait = _calc_backoff(attempt, response)
|
|
130
|
+
await response.aread()
|
|
131
|
+
await response.aclose()
|
|
132
|
+
await asyncio.sleep(wait)
|
|
133
|
+
|
|
134
|
+
raise RuntimeError("vesselapi: retry loop exited unexpectedly") # pragma: no cover
|
|
135
|
+
|
|
136
|
+
async def aclose(self) -> None:
|
|
137
|
+
await self._base.aclose()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Helpers
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _is_retryable(status_code: int) -> bool:
|
|
146
|
+
"""Return True if the status code warrants a retry."""
|
|
147
|
+
return status_code == 429 or status_code >= 500
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _is_idempotent(method: str) -> bool:
|
|
151
|
+
"""Return True for HTTP methods that are safe to retry."""
|
|
152
|
+
return method.upper() in {"GET", "HEAD", "OPTIONS", "PUT", "DELETE"}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _calc_backoff(attempt: int, response: httpx.Response) -> float:
|
|
156
|
+
"""Calculate retry wait time, respecting Retry-After header."""
|
|
157
|
+
retry_after = response.headers.get("Retry-After", "")
|
|
158
|
+
if retry_after:
|
|
159
|
+
# Try integer seconds first.
|
|
160
|
+
try:
|
|
161
|
+
seconds = int(retry_after)
|
|
162
|
+
return max(0.0, min(float(seconds), MAX_BACKOFF))
|
|
163
|
+
except ValueError:
|
|
164
|
+
pass
|
|
165
|
+
# Try HTTP-date format (RFC 7231 section 7.1.3).
|
|
166
|
+
try:
|
|
167
|
+
dt = parsedate_to_datetime(retry_after)
|
|
168
|
+
delta = (dt - _utcnow()).total_seconds()
|
|
169
|
+
return max(0.0, min(delta, MAX_BACKOFF))
|
|
170
|
+
except (ValueError, TypeError):
|
|
171
|
+
pass
|
|
172
|
+
return _calc_exp_backoff(attempt)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _calc_exp_backoff(attempt: int) -> float:
|
|
176
|
+
"""Exponential backoff with jitter, capped at MAX_BACKOFF."""
|
|
177
|
+
base = math.pow(2, attempt)
|
|
178
|
+
jitter = random.random() * base # noqa: S311
|
|
179
|
+
duration = (base + jitter) * 0.5
|
|
180
|
+
return min(duration, MAX_BACKOFF)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _utcnow() -> datetime.datetime:
|
|
184
|
+
"""Return current UTC datetime. Extracted for test patching."""
|
|
185
|
+
return datetime.datetime.now(datetime.timezone.utc)
|
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vessel-api-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for the Vessel Tracking API — maritime vessel tracking, port events, emissions, and navigation data.
|
|
5
|
+
Project-URL: Documentation, https://vesselapi.com/docs
|
|
6
|
+
Project-URL: Repository, https://github.com/vessel-api/vesselapi-python
|
|
7
|
+
Project-URL: Issues, https://github.com/vessel-api/vesselapi-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/vessel-api/vesselapi-python/releases
|
|
9
|
+
Author-email: Vessel API <support@vesselapi.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ais,maritime,sdk,tracking,vessel,vesselapi
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
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: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: eval-type-backport>=0.2.0; python_version < '3.10'
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: pydantic>=2.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# vesselapi-python
|
|
36
|
+
|
|
37
|
+
[](https://github.com/vessel-api/vesselapi-python/actions/workflows/ci.yml)
|
|
38
|
+
[](https://pypi.org/project/vessel-api-python/)
|
|
39
|
+
[](https://pypi.org/project/vessel-api-python/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
42
|
+
Python client for the [Vessel Tracking API](https://vesselapi.com) — maritime vessel tracking, port events, emissions, and navigation data.
|
|
43
|
+
|
|
44
|
+
**Resources**: [Documentation](https://vesselapi.com/docs) | [API Explorer](https://vesselapi.com/api-reference) | [Dashboard](https://dashboard.vesselapi.com) | [Contact Support](mailto:support@vesselapi.com)
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install vessel-api-python
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requires Python 3.9+.
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from vessel_api_python import VesselClient
|
|
58
|
+
|
|
59
|
+
client = VesselClient(api_key="your-api-key")
|
|
60
|
+
|
|
61
|
+
# Search for a vessel by name.
|
|
62
|
+
result = client.search.vessels(filter_name="Ever Given")
|
|
63
|
+
for v in result.vessels or []:
|
|
64
|
+
print(f"{v.name} (IMO {v.imo})")
|
|
65
|
+
|
|
66
|
+
# Get a port by UN/LOCODE.
|
|
67
|
+
port = client.ports.get("NLRTM")
|
|
68
|
+
print(port.port.name)
|
|
69
|
+
|
|
70
|
+
# Auto-paginate through port events.
|
|
71
|
+
for event in client.port_events.list_all(pagination_limit=10):
|
|
72
|
+
print(f"{event.event} at {event.timestamp}")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Async
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import asyncio
|
|
79
|
+
from vessel_api_python import AsyncVesselClient
|
|
80
|
+
|
|
81
|
+
async def main():
|
|
82
|
+
async with AsyncVesselClient(api_key="your-api-key") as client:
|
|
83
|
+
result = await client.search.vessels(filter_name="Ever Given")
|
|
84
|
+
async for event in client.port_events.list_all(pagination_limit=10):
|
|
85
|
+
print(f"{event.event} at {event.timestamp}")
|
|
86
|
+
|
|
87
|
+
asyncio.run(main())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Available Services
|
|
91
|
+
|
|
92
|
+
| Service | Methods | Description |
|
|
93
|
+
|---------|---------|-------------|
|
|
94
|
+
| `vessels` | `get`, `position`, `casualties`, `classification`, `emissions`, `eta`, `inspections`, `inspection_detail`, `ownership`, `positions` | Vessel details, positions, and records |
|
|
95
|
+
| `ports` | `get` | Port lookup by UN/LOCODE |
|
|
96
|
+
| `port_events` | `list`, `by_port`, `by_ports`, `by_vessel`, `last_by_vessel`, `by_vessels` | Vessel arrival/departure events |
|
|
97
|
+
| `emissions` | `list` | EU MRV emissions data |
|
|
98
|
+
| `search` | `vessels`, `ports`, `dgps`, `light_aids`, `modus`, `radio_beacons` | Full-text search across entity types |
|
|
99
|
+
| `location` | `vessels_bounding_box`, `vessels_radius`, `ports_bounding_box`, `ports_radius`, `dgps_bounding_box`, `dgps_radius`, `light_aids_bounding_box`, `light_aids_radius`, `modus_bounding_box`, `modus_radius`, `radio_beacons_bounding_box`, `radio_beacons_radius` | Geo queries by bounding box or radius |
|
|
100
|
+
| `navtex` | `list` | NAVTEX maritime safety messages |
|
|
101
|
+
|
|
102
|
+
**37 methods total.**
|
|
103
|
+
|
|
104
|
+
## Error Handling
|
|
105
|
+
|
|
106
|
+
All methods raise specific exception types on non-2xx responses:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from vessel_api_python import VesselAPIError
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
client.ports.get("ZZZZZ")
|
|
113
|
+
except VesselAPIError as err:
|
|
114
|
+
if err.is_not_found:
|
|
115
|
+
print("Port not found")
|
|
116
|
+
elif err.is_rate_limited:
|
|
117
|
+
print("Rate limited — back off")
|
|
118
|
+
elif err.is_auth_error:
|
|
119
|
+
print("Check API key")
|
|
120
|
+
print(err.status_code, err.message)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Auto-Pagination
|
|
124
|
+
|
|
125
|
+
Every list endpoint has an `all_*` / `list_all` variant returning an iterator:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# Sync
|
|
129
|
+
for vessel in client.search.all_vessels(filter_name="tanker"):
|
|
130
|
+
print(vessel.name)
|
|
131
|
+
|
|
132
|
+
# Async
|
|
133
|
+
async for vessel in client.search.all_vessels(filter_name="tanker"):
|
|
134
|
+
print(vessel.name)
|
|
135
|
+
|
|
136
|
+
# Collect all at once
|
|
137
|
+
vessels = client.search.all_vessels(filter_name="tanker").collect()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
client = VesselClient(
|
|
144
|
+
api_key="your-api-key",
|
|
145
|
+
base_url="https://custom-endpoint.example.com/v1",
|
|
146
|
+
timeout=60.0,
|
|
147
|
+
max_retries=5, # default: 3
|
|
148
|
+
user_agent="my-app/1.0",
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Retries use exponential backoff with jitter on 429 and 5xx responses. The `Retry-After` header is respected.
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- [API Documentation](https://vesselapi.com/docs) — endpoint guides, request/response schemas, and usage examples
|
|
157
|
+
- [API Explorer](https://vesselapi.com/api-reference) — interactive API reference
|
|
158
|
+
- [Dashboard](https://dashboard.vesselapi.com) — manage API keys and monitor usage
|
|
159
|
+
|
|
160
|
+
## Contributing & Support
|
|
161
|
+
|
|
162
|
+
Found a bug, have a feature request, or need help? You're welcome to [open an issue](https://github.com/vessel-api/vesselapi-python/issues). For API-level bugs and feature requests, please use the [main VesselAPI repository](https://github.com/vessel-api/VesselApi/issues).
|
|
163
|
+
|
|
164
|
+
For security vulnerabilities, **do not** open a public issue — email security@vesselapi.com instead. See [SECURITY.md](SECURITY.md).
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
vessel_api_python/__init__.py,sha256=K4ob0XECb6ur-lKl_h0YAJQAhQ9HO1JSTv9fCshj9gQ,4505
|
|
2
|
+
vessel_api_python/_client.py,sha256=39yKlQ4W2TnE85sTtK7KlBzFF385Pl52I5ghyq575EA,5067
|
|
3
|
+
vessel_api_python/_constants.py,sha256=wQeOziPys14EdD21f2vBgM8BKrO3WeHIUeOTQAG3BAI,244
|
|
4
|
+
vessel_api_python/_errors.py,sha256=pxDm36mbrviOoDRmJ-ogtlUIsyu-dHpC0jcs5f34WDU,4310
|
|
5
|
+
vessel_api_python/_iterator.py,sha256=SkgsGSdxLrKtw-ad8fSUPaEDDJCamAKK0PrG_P8HX-4,3198
|
|
6
|
+
vessel_api_python/_models.py,sha256=g1c--6UUhtfDSfTv4G6wDmYxGdURN8KASNNAXXQvibE,29124
|
|
7
|
+
vessel_api_python/_services.py,sha256=-3FFJo3C_5fXZDnPc8gcgXQ472BC1hScaQWIOX6qQiU,73801
|
|
8
|
+
vessel_api_python/_transport.py,sha256=_4s-65JMRwD2S435u0iuoLGH_GyLNaX8cP_uSh_u80I,6692
|
|
9
|
+
vessel_api_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
vessel_api_python-1.0.0.dist-info/METADATA,sha256=Pc35iFq8GJ5FywrNWOJKGwcQ2VS8Miv7L8ubW3T3P0s,6309
|
|
11
|
+
vessel_api_python-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
vessel_api_python-1.0.0.dist-info/licenses/LICENSE,sha256=zoj_abq99mj8cz-Bz71XCnwB-Qv3kU1VzVXeKEntuQU,1067
|
|
13
|
+
vessel_api_python-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Vessel API
|
|
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.
|