codai-sdk 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.
codai/__init__.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""codai — official Python SDK for the codai AI gateway.
|
|
2
|
+
|
|
3
|
+
Zero-dependency (stdlib urllib) client mirroring @codai/sdk:
|
|
4
|
+
|
|
5
|
+
from codai import Codai
|
|
6
|
+
|
|
7
|
+
client = Codai(api_key="ck-...", session_id="my-session")
|
|
8
|
+
result = client.chat([{"role": "user", "content": "hello"}])
|
|
9
|
+
print(result.content, result.routed_to)
|
|
10
|
+
|
|
11
|
+
for delta in client.chat_stream([{"role": "user", "content": "hi"}]):
|
|
12
|
+
print(delta, end="")
|
|
13
|
+
|
|
14
|
+
run = client.agents_run("summarize the repo structure")
|
|
15
|
+
client.feedback(result.request_id, 1)
|
|
16
|
+
|
|
17
|
+
Memory tools (memory_recall/memory_remember) and skills are GATEWAY-NATIVE:
|
|
18
|
+
pass a stable session_id and the model uses them server-side.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import time
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.request
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Any, Dict, Generator, List, Optional
|
|
29
|
+
|
|
30
|
+
__all__ = ["Codai", "CodaiError", "ChatResult", "AgentRunResult"]
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
_RETRYABLE = {429, 500, 502, 503, 504}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CodaiError(Exception):
|
|
37
|
+
def __init__(self, message: str, status: int = 0, body: Any = None) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.status = status
|
|
40
|
+
self.body = body
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ChatResult:
|
|
45
|
+
content: str
|
|
46
|
+
raw: Dict[str, Any]
|
|
47
|
+
request_id: Optional[str]
|
|
48
|
+
routed_to: Optional[str]
|
|
49
|
+
exec_verify: Optional[str]
|
|
50
|
+
usage: Optional[Dict[str, int]]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class AgentRunResult:
|
|
55
|
+
result: str
|
|
56
|
+
model: str
|
|
57
|
+
event_id: Optional[str]
|
|
58
|
+
usage: Optional[Dict[str, Any]] = field(default=None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Codai:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
api_key: str,
|
|
65
|
+
base_url: str = "https://ai.codai.ro",
|
|
66
|
+
session_id: Optional[str] = None,
|
|
67
|
+
timeout: float = 120.0,
|
|
68
|
+
max_retries: int = 2,
|
|
69
|
+
) -> None:
|
|
70
|
+
if not api_key:
|
|
71
|
+
raise ValueError("Codai: api_key is required")
|
|
72
|
+
self.api_key = api_key
|
|
73
|
+
self.base_url = base_url.rstrip("/")
|
|
74
|
+
self.session_id = session_id
|
|
75
|
+
self.timeout = timeout
|
|
76
|
+
self.max_retries = max_retries
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------- chat
|
|
79
|
+
def chat(
|
|
80
|
+
self,
|
|
81
|
+
messages: List[Dict[str, Any]],
|
|
82
|
+
model: str = "codai",
|
|
83
|
+
temperature: Optional[float] = None,
|
|
84
|
+
max_tokens: Optional[int] = None,
|
|
85
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
86
|
+
agent_mode: bool = False,
|
|
87
|
+
compact: Optional[str] = None,
|
|
88
|
+
best_of: Optional[int] = None,
|
|
89
|
+
session_id: Optional[str] = None,
|
|
90
|
+
) -> ChatResult:
|
|
91
|
+
body: Dict[str, Any] = {"model": model, "messages": messages, "stream": False}
|
|
92
|
+
if temperature is not None:
|
|
93
|
+
body["temperature"] = temperature
|
|
94
|
+
if max_tokens is not None:
|
|
95
|
+
body["max_tokens"] = max_tokens
|
|
96
|
+
if tools:
|
|
97
|
+
body["tools"] = tools
|
|
98
|
+
headers = self._chat_headers(agent_mode, compact, best_of, session_id)
|
|
99
|
+
status, resp_headers, data = self._request(
|
|
100
|
+
"/v1/chat/completions", body, extra_headers=headers
|
|
101
|
+
)
|
|
102
|
+
raw = json.loads(data)
|
|
103
|
+
choices = raw.get("choices") or []
|
|
104
|
+
usage_raw = raw.get("usage") or {}
|
|
105
|
+
return ChatResult(
|
|
106
|
+
content=str((choices[0].get("message") or {}).get("content") or "")
|
|
107
|
+
if choices
|
|
108
|
+
else "",
|
|
109
|
+
raw=raw,
|
|
110
|
+
request_id=resp_headers.get("x-request-id"),
|
|
111
|
+
routed_to=resp_headers.get("x-codai-routed-to"),
|
|
112
|
+
exec_verify=resp_headers.get("x-codai-exec-verify"),
|
|
113
|
+
usage=(
|
|
114
|
+
{
|
|
115
|
+
"prompt_tokens": int(usage_raw.get("prompt_tokens") or 0),
|
|
116
|
+
"completion_tokens": int(usage_raw.get("completion_tokens") or 0),
|
|
117
|
+
}
|
|
118
|
+
if usage_raw
|
|
119
|
+
else None
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def chat_stream(
|
|
124
|
+
self,
|
|
125
|
+
messages: List[Dict[str, Any]],
|
|
126
|
+
model: str = "codai",
|
|
127
|
+
temperature: Optional[float] = None,
|
|
128
|
+
max_tokens: Optional[int] = None,
|
|
129
|
+
agent_mode: bool = False,
|
|
130
|
+
session_id: Optional[str] = None,
|
|
131
|
+
) -> Generator[str, None, None]:
|
|
132
|
+
body: Dict[str, Any] = {"model": model, "messages": messages, "stream": True}
|
|
133
|
+
if temperature is not None:
|
|
134
|
+
body["temperature"] = temperature
|
|
135
|
+
if max_tokens is not None:
|
|
136
|
+
body["max_tokens"] = max_tokens
|
|
137
|
+
headers = self._chat_headers(agent_mode, None, None, session_id)
|
|
138
|
+
req = self._build_request("/v1/chat/completions", body, headers)
|
|
139
|
+
try:
|
|
140
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
141
|
+
buffer = b""
|
|
142
|
+
for chunk in iter(lambda: resp.read(1024), b""):
|
|
143
|
+
buffer += chunk
|
|
144
|
+
while b"\n\n" in buffer:
|
|
145
|
+
frame, buffer = buffer.split(b"\n\n", 1)
|
|
146
|
+
for line in frame.split(b"\n"):
|
|
147
|
+
if not line.startswith(b"data:"):
|
|
148
|
+
continue
|
|
149
|
+
payload = line[5:].strip()
|
|
150
|
+
if payload == b"[DONE]":
|
|
151
|
+
return
|
|
152
|
+
try:
|
|
153
|
+
parsed = json.loads(payload)
|
|
154
|
+
delta = (
|
|
155
|
+
(parsed.get("choices") or [{}])[0]
|
|
156
|
+
.get("delta", {})
|
|
157
|
+
.get("content")
|
|
158
|
+
)
|
|
159
|
+
if delta:
|
|
160
|
+
yield delta
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
continue
|
|
163
|
+
except urllib.error.HTTPError as e: # pragma: no cover
|
|
164
|
+
raise CodaiError(f"codai stream → {e.code}", e.code, e.read()) from e
|
|
165
|
+
|
|
166
|
+
# ------------------------------------------------------------ agents
|
|
167
|
+
def agents_run(
|
|
168
|
+
self,
|
|
169
|
+
task: str,
|
|
170
|
+
context: Optional[str] = None,
|
|
171
|
+
system: Optional[str] = None,
|
|
172
|
+
model: Optional[str] = None,
|
|
173
|
+
) -> AgentRunResult:
|
|
174
|
+
body: Dict[str, Any] = {"task": task}
|
|
175
|
+
if context:
|
|
176
|
+
body["context"] = context
|
|
177
|
+
if system:
|
|
178
|
+
body["system"] = system
|
|
179
|
+
if model:
|
|
180
|
+
body["model"] = model
|
|
181
|
+
_, _, data = self._request("/v1/agents/run", body)
|
|
182
|
+
raw = json.loads(data)
|
|
183
|
+
return AgentRunResult(
|
|
184
|
+
result=str(raw.get("result") or ""),
|
|
185
|
+
model=str(raw.get("model") or ""),
|
|
186
|
+
event_id=raw.get("event_id"),
|
|
187
|
+
usage=raw.get("usage"),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------- feedback
|
|
191
|
+
def feedback(self, request_id: str, rating: int, comment: Optional[str] = None) -> None:
|
|
192
|
+
body: Dict[str, Any] = {"event_id": request_id, "rating": rating}
|
|
193
|
+
if comment:
|
|
194
|
+
body["comment"] = comment
|
|
195
|
+
self._request("/v1/feedback", body)
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------ models
|
|
198
|
+
def models(self) -> List[Dict[str, Any]]:
|
|
199
|
+
_, _, data = self._request("/v1/models", None, method="GET")
|
|
200
|
+
return json.loads(data).get("data") or []
|
|
201
|
+
|
|
202
|
+
# ------------------------------------------------------------ tokens
|
|
203
|
+
def mint_token(self, scope: str, ttl_seconds: Optional[int] = None) -> Dict[str, Any]:
|
|
204
|
+
"""Mint an ephemeral scoped token ('realtime'|'audio'|'embeddings').
|
|
205
|
+
|
|
206
|
+
Returns {"token", "expires_at", "scope"}. Hand the token to a
|
|
207
|
+
browser/webview client; it expires in 60-3600s and only works on
|
|
208
|
+
its scope's surface.
|
|
209
|
+
"""
|
|
210
|
+
body: Dict[str, Any] = {"scope": scope}
|
|
211
|
+
if ttl_seconds is not None:
|
|
212
|
+
body["ttl_seconds"] = ttl_seconds
|
|
213
|
+
_, _, data = self._request("/v1/tokens", body)
|
|
214
|
+
return json.loads(data)
|
|
215
|
+
|
|
216
|
+
# -------------------------------------------------------- embeddings
|
|
217
|
+
def embeddings(
|
|
218
|
+
self,
|
|
219
|
+
input: Any,
|
|
220
|
+
model: str = "codai-embed",
|
|
221
|
+
dimensions: Optional[int] = None,
|
|
222
|
+
) -> List[List[float]]:
|
|
223
|
+
body: Dict[str, Any] = {"model": model, "input": input}
|
|
224
|
+
if dimensions is not None:
|
|
225
|
+
body["dimensions"] = dimensions
|
|
226
|
+
_, _, data = self._request("/v1/embeddings", body)
|
|
227
|
+
return [d["embedding"] for d in json.loads(data).get("data") or []]
|
|
228
|
+
|
|
229
|
+
# ------------------------------------------------------------- audio
|
|
230
|
+
def transcribe(
|
|
231
|
+
self,
|
|
232
|
+
file_bytes: bytes,
|
|
233
|
+
filename: str = "audio.webm",
|
|
234
|
+
model: str = "codai-transcribe",
|
|
235
|
+
) -> str:
|
|
236
|
+
"""Speech-to-text via multipart upload (max 25 MB). Returns transcript."""
|
|
237
|
+
boundary = f"----codai{int(time.time() * 1000)}"
|
|
238
|
+
parts = []
|
|
239
|
+
parts.append(
|
|
240
|
+
f"--{boundary}\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\n{model}\r\n".encode()
|
|
241
|
+
)
|
|
242
|
+
parts.append(
|
|
243
|
+
(
|
|
244
|
+
f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; "
|
|
245
|
+
f"filename=\"{filename}\"\r\nContent-Type: application/octet-stream\r\n\r\n"
|
|
246
|
+
).encode()
|
|
247
|
+
)
|
|
248
|
+
parts.append(file_bytes)
|
|
249
|
+
parts.append(f"\r\n--{boundary}--\r\n".encode())
|
|
250
|
+
payload = b"".join(parts)
|
|
251
|
+
req = urllib.request.Request(
|
|
252
|
+
f"{self.base_url}/v1/audio/transcriptions",
|
|
253
|
+
data=payload,
|
|
254
|
+
headers={
|
|
255
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
256
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
257
|
+
},
|
|
258
|
+
method="POST",
|
|
259
|
+
)
|
|
260
|
+
try:
|
|
261
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
262
|
+
return str(json.loads(resp.read()).get("text") or "")
|
|
263
|
+
except urllib.error.HTTPError as e:
|
|
264
|
+
try:
|
|
265
|
+
err_body = json.loads(e.read())
|
|
266
|
+
except Exception: # noqa: BLE001
|
|
267
|
+
err_body = None
|
|
268
|
+
raise CodaiError(f"codai /v1/audio/transcriptions → {e.code}", e.code, err_body) from e
|
|
269
|
+
|
|
270
|
+
def speech(
|
|
271
|
+
self,
|
|
272
|
+
input: str,
|
|
273
|
+
model: str = "codai-tts",
|
|
274
|
+
voice: str = "alloy",
|
|
275
|
+
) -> bytes:
|
|
276
|
+
"""Text-to-speech. Returns audio bytes (default mp3)."""
|
|
277
|
+
_, _, data = self._request(
|
|
278
|
+
"/v1/audio/speech", {"model": model, "input": input, "voice": voice}
|
|
279
|
+
)
|
|
280
|
+
return data
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------- internal
|
|
283
|
+
def _chat_headers(
|
|
284
|
+
self,
|
|
285
|
+
agent_mode: bool,
|
|
286
|
+
compact: Optional[str],
|
|
287
|
+
best_of: Optional[int],
|
|
288
|
+
session_id: Optional[str],
|
|
289
|
+
) -> Dict[str, str]:
|
|
290
|
+
h: Dict[str, str] = {}
|
|
291
|
+
session = session_id or self.session_id
|
|
292
|
+
if session:
|
|
293
|
+
h["x-codai-session-id"] = session
|
|
294
|
+
if agent_mode:
|
|
295
|
+
h["x-codai-mode"] = "agent"
|
|
296
|
+
if compact:
|
|
297
|
+
h["x-codai-compact"] = compact
|
|
298
|
+
if best_of is not None:
|
|
299
|
+
h["x-codai-best-of"] = str(best_of)
|
|
300
|
+
return h
|
|
301
|
+
|
|
302
|
+
def _build_request(
|
|
303
|
+
self, path: str, body: Any, extra_headers: Optional[Dict[str, str]] = None, method: str = "POST"
|
|
304
|
+
) -> urllib.request.Request:
|
|
305
|
+
headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
306
|
+
data = None
|
|
307
|
+
if body is not None:
|
|
308
|
+
headers["Content-Type"] = "application/json"
|
|
309
|
+
data = json.dumps(body).encode()
|
|
310
|
+
headers.update(extra_headers or {})
|
|
311
|
+
return urllib.request.Request(
|
|
312
|
+
f"{self.base_url}{path}", data=data, headers=headers, method=method
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def _request(
|
|
316
|
+
self, path: str, body: Any, extra_headers: Optional[Dict[str, str]] = None, method: str = "POST"
|
|
317
|
+
) -> tuple:
|
|
318
|
+
last_err: Optional[Exception] = None
|
|
319
|
+
for attempt in range(self.max_retries + 1):
|
|
320
|
+
req = self._build_request(path, body, extra_headers, method)
|
|
321
|
+
try:
|
|
322
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
323
|
+
return resp.status, dict(resp.headers), resp.read()
|
|
324
|
+
except urllib.error.HTTPError as e:
|
|
325
|
+
if e.code in _RETRYABLE and attempt < self.max_retries:
|
|
326
|
+
time.sleep(0.5 * 2**attempt)
|
|
327
|
+
continue
|
|
328
|
+
try:
|
|
329
|
+
err_body = json.loads(e.read())
|
|
330
|
+
except Exception: # noqa: BLE001
|
|
331
|
+
err_body = None
|
|
332
|
+
raise CodaiError(f"codai {path} → {e.code}", e.code, err_body) from e
|
|
333
|
+
except Exception as e: # noqa: BLE001 — network-level
|
|
334
|
+
last_err = e
|
|
335
|
+
if attempt < self.max_retries:
|
|
336
|
+
time.sleep(0.5 * 2**attempt)
|
|
337
|
+
continue
|
|
338
|
+
raise CodaiError(f"codai {path} failed after retries: {last_err}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codai-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the codai AI gateway — chat, streaming, agents, feedback, models.
|
|
5
|
+
Project-URL: Homepage, https://codai.ro
|
|
6
|
+
Project-URL: Repository, https://github.com/dragoscv/codai-sdk-python
|
|
7
|
+
Project-URL: Issues, https://github.com/dragoscv/codai-sdk-python/issues
|
|
8
|
+
Author-email: codai <support@codai.ro>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,codai,gateway,llm,openai-compatible
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# codai-sdk — Python SDK
|
|
23
|
+
|
|
24
|
+
[](https://pypi.org/project/codai-sdk/)
|
|
25
|
+
[](https://pypi.org/project/codai-sdk/)
|
|
26
|
+
[](./LICENSE)
|
|
27
|
+
|
|
28
|
+
Official Python client for the [codai](https://codai.ro) AI gateway.
|
|
29
|
+
Zero dependencies (stdlib only), Python 3.9+.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install codai-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> You need a codai API key. Get one at **[codai.ro](https://codai.ro)**.
|
|
36
|
+
> The import name stays `codai`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from codai import Codai
|
|
40
|
+
|
|
41
|
+
client = Codai(api_key="ck-...", session_id="my-project")
|
|
42
|
+
|
|
43
|
+
# Chat (OpenAI-compatible, smart-routed)
|
|
44
|
+
result = client.chat([{"role": "user", "content": "Explain asyncio.gather"}])
|
|
45
|
+
print(result.content)
|
|
46
|
+
print(result.routed_to) # which model actually served
|
|
47
|
+
|
|
48
|
+
# Streaming
|
|
49
|
+
for delta in client.chat_stream([{"role": "user", "content": "hi"}]):
|
|
50
|
+
print(delta, end="", flush=True)
|
|
51
|
+
|
|
52
|
+
# Server-side agent loop
|
|
53
|
+
run = client.agents_run("Find and summarize the TODOs in this codebase")
|
|
54
|
+
print(run.result)
|
|
55
|
+
|
|
56
|
+
# Feedback (improves routing for everyone)
|
|
57
|
+
client.feedback(result.request_id, 1)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## codai extensions
|
|
61
|
+
|
|
62
|
+
| Option | Effect |
|
|
63
|
+
| ----------------- | ------------------------------------ |
|
|
64
|
+
| `session_id` | Session memory + routing stickiness |
|
|
65
|
+
| `agent_mode=True` | Plan-and-execute loop (Pro+) |
|
|
66
|
+
| `compact="auto"` | Server-side context compaction |
|
|
67
|
+
| `best_of=0/3` | Disable / force best-of-N ensembling |
|
|
68
|
+
|
|
69
|
+
MIT © codai
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
codai/__init__.py,sha256=UxrukVprtpHNc_I-4ys6ZjsM9JlO2xMGbd-MazQ1mmw,12778
|
|
2
|
+
codai_sdk-0.1.0.dist-info/METADATA,sha256=UY8teAnt3GknAM2VvYVM2VxPEq06Q3pRMW5pt4-lgws,2464
|
|
3
|
+
codai_sdk-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
4
|
+
codai_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=hS2xIQEKlVIu-hRrMxe4NTasTiucytr6_IZv9SBJH6g,1090
|
|
5
|
+
codai_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dragos Catalin Vladulescu (codai)
|
|
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.
|