boxfab-code-interpreter 0.1.0a1__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.
@@ -0,0 +1,526 @@
1
+ import inspect
2
+ import json
3
+ import logging
4
+
5
+ from boxfab import NotFoundException, TimeoutException, SandboxException
6
+ from dataclasses import dataclass, field
7
+ from typing import (
8
+ List,
9
+ Literal,
10
+ Optional,
11
+ Iterable,
12
+ Dict,
13
+ TypeVar,
14
+ Callable,
15
+ Awaitable,
16
+ Any,
17
+ Union,
18
+ )
19
+
20
+ from httpx import Response
21
+
22
+ from .charts import Chart, _deserialize_chart
23
+
24
+ RunCodeLanguage = Union[
25
+ Literal["python", "javascript", "typescript", "r", "java", "bash"],
26
+ str,
27
+ ]
28
+
29
+ T = TypeVar("T")
30
+ OutputHandler = Union[Callable[[T], Any],]
31
+
32
+ OutputHandlerWithAsync = Union[
33
+ OutputHandler[T],
34
+ Callable[[T], Awaitable[Any]],
35
+ ]
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ @dataclass
41
+ class OutputMessage:
42
+ """
43
+ Represents an output message from the sandbox code execution.
44
+ """
45
+
46
+ line: str
47
+ """
48
+ The output line.
49
+ """
50
+ timestamp: int
51
+ """
52
+ Unix epoch in nanoseconds
53
+ """
54
+ error: bool = False
55
+ """
56
+ Whether the output is an error.
57
+ """
58
+
59
+ def __str__(self):
60
+ return self.line
61
+
62
+
63
+ @dataclass
64
+ class ExecutionError:
65
+ """
66
+ Represents an error that occurred during the execution of a cell.
67
+ The error contains the name of the error, the value of the error, and the traceback.
68
+ """
69
+
70
+ name: str
71
+ """
72
+ Name of the error.
73
+ """
74
+ value: str
75
+ """
76
+ Value of the error.
77
+ """
78
+ traceback: str
79
+ """
80
+ The raw traceback of the error.
81
+ """
82
+
83
+ def __init__(self, name: str, value: str, traceback: str, **kwargs):
84
+ self.name = name
85
+ self.value = value
86
+ self.traceback = traceback
87
+
88
+ def to_json(self) -> str:
89
+ """
90
+ Returns the JSON representation of the Error object.
91
+ """
92
+ data = {"name": self.name, "value": self.value, "traceback": self.traceback}
93
+ return json.dumps(data)
94
+
95
+
96
+ class MIMEType(str):
97
+ """
98
+ Represents a MIME type.
99
+ """
100
+
101
+
102
+ @dataclass
103
+ class Result:
104
+ """
105
+ Represents the data to be displayed as a result of executing a cell in a Jupyter notebook.
106
+ The result is similar to the structure returned by ipython kernel: https://ipython.readthedocs.io/en/stable/development/execution.html#execution-semantics
107
+
108
+ The result can contain multiple types of data, such as text, images, plots, etc. Each type of data is represented
109
+ as a string, and the result can contain multiple types of data. The display calls don't have to have text representation,
110
+ for the actual result the representation is always present for the result, the other representations are always optional.
111
+ """
112
+
113
+ def __getitem__(self, item):
114
+ return getattr(self, item)
115
+
116
+ text: Optional[str] = None
117
+ html: Optional[str] = None
118
+ markdown: Optional[str] = None
119
+ svg: Optional[str] = None
120
+ png: Optional[str] = None
121
+ jpeg: Optional[str] = None
122
+ pdf: Optional[str] = None
123
+ latex: Optional[str] = None
124
+ json: Optional[dict] = None
125
+ javascript: Optional[str] = None
126
+ data: Optional[dict] = None
127
+ chart: Optional[Chart] = None
128
+ is_main_result: bool = False
129
+ """Whether this data is the result of the cell. Data can be produced by display calls of which can be multiple in a cell."""
130
+ extra: Optional[dict] = None
131
+ """Extra data that can be included. Not part of the standard types."""
132
+
133
+ def __init__(
134
+ self,
135
+ text: Optional[str] = None,
136
+ html: Optional[str] = None,
137
+ markdown: Optional[str] = None,
138
+ svg: Optional[str] = None,
139
+ png: Optional[str] = None,
140
+ jpeg: Optional[str] = None,
141
+ pdf: Optional[str] = None,
142
+ latex: Optional[str] = None,
143
+ json: Optional[dict] = None,
144
+ javascript: Optional[str] = None,
145
+ data: Optional[dict] = None,
146
+ chart: Optional[dict] = None,
147
+ is_main_result: bool = False,
148
+ extra: Optional[dict] = None,
149
+ **kwargs, # Allows for future expansion
150
+ ):
151
+ self.text = text
152
+ self.html = html
153
+ self.markdown = markdown
154
+ self.svg = svg
155
+ self.png = png
156
+ self.jpeg = jpeg
157
+ self.pdf = pdf
158
+ self.latex = latex
159
+ self.json = json
160
+ self.javascript = javascript
161
+ self.data = data
162
+ if chart:
163
+ try:
164
+ self.chart = _deserialize_chart(chart)
165
+ except Exception as e:
166
+ logger.error(
167
+ f"Error deserializing chart, check if you are using the latest version of the library: {e}"
168
+ )
169
+ self.is_main_result = is_main_result
170
+ self.extra = extra
171
+
172
+ def formats(self) -> Iterable[str]:
173
+ """
174
+ Returns all available formats of the result.
175
+
176
+ :return: All available formats of the result in MIME types.
177
+ """
178
+ formats = []
179
+ if self.text:
180
+ formats.append("text")
181
+ if self.html:
182
+ formats.append("html")
183
+ if self.markdown:
184
+ formats.append("markdown")
185
+ if self.svg:
186
+ formats.append("svg")
187
+ if self.png:
188
+ formats.append("png")
189
+ if self.jpeg:
190
+ formats.append("jpeg")
191
+ if self.pdf:
192
+ formats.append("pdf")
193
+ if self.latex:
194
+ formats.append("latex")
195
+ if self.json:
196
+ formats.append("json")
197
+ if self.javascript:
198
+ formats.append("javascript")
199
+ if self.data:
200
+ formats.append("data")
201
+ if self.chart:
202
+ formats.append("chart")
203
+
204
+ if self.extra:
205
+ for key in self.extra:
206
+ formats.append(key)
207
+
208
+ return formats
209
+
210
+ def __str__(self) -> str:
211
+ """
212
+ Returns the text representation of the data.
213
+
214
+ :return: The text representation of the data.
215
+ """
216
+ return self.__repr__()
217
+
218
+ def __repr__(self) -> str:
219
+ if self.text:
220
+ return f"Result({self.text})"
221
+ else:
222
+ return "Result(Formats: " + ", ".join(self.formats()) + ")"
223
+
224
+ def _repr_html_(self) -> Optional[str]:
225
+ """
226
+ Returns the HTML representation of the data.
227
+
228
+ :return: The HTML representation of the data.
229
+ """
230
+ return self.html
231
+
232
+ def _repr_markdown_(self) -> Optional[str]:
233
+ """
234
+ Returns the Markdown representation of the data.
235
+
236
+ :return: The Markdown representation of the data.
237
+ """
238
+ return self.markdown
239
+
240
+ def _repr_svg_(self) -> Optional[str]:
241
+ """
242
+ Returns the SVG representation of the data.
243
+
244
+ :return: The SVG representation of the data.
245
+ """
246
+ return self.svg
247
+
248
+ def _repr_png_(self) -> Optional[str]:
249
+ """
250
+ Returns the base64 representation of the PNG data.
251
+
252
+ :return: The base64 representation of the PNG data.
253
+ """
254
+ return self.png
255
+
256
+ def _repr_jpeg_(self) -> Optional[str]:
257
+ """
258
+ Returns the base64 representation of the JPEG data.
259
+
260
+ :return: The base64 representation of the JPEG data.
261
+ """
262
+ return self.jpeg
263
+
264
+ def _repr_pdf_(self) -> Optional[str]:
265
+ """
266
+ Returns the PDF representation of the data.
267
+
268
+ :return: The PDF representation of the data.
269
+ """
270
+ return self.pdf
271
+
272
+ def _repr_latex_(self) -> Optional[str]:
273
+ """
274
+ Returns the LaTeX representation of the data.
275
+
276
+ :return: The LaTeX representation of the data.
277
+ """
278
+ return self.latex
279
+
280
+ def _repr_json_(self) -> Optional[dict]:
281
+ """
282
+ Returns the JSON representation of the data.
283
+
284
+ :return: The JSON representation of the data.
285
+ """
286
+ return self.json
287
+
288
+ def _repr_javascript_(self) -> Optional[str]:
289
+ """
290
+ Returns the JavaScript representation of the data.
291
+
292
+ :return: The JavaScript representation of the data.
293
+ """
294
+ return self.javascript
295
+
296
+
297
+ @dataclass(repr=False)
298
+ class Logs:
299
+ """
300
+ Data printed to stdout and stderr during execution, usually by print statements, logs, warnings, subprocesses, etc.
301
+ """
302
+
303
+ stdout: List[str] = field(default_factory=list)
304
+ """List of strings printed to stdout by prints, subprocesses, etc."""
305
+ stderr: List[str] = field(default_factory=list)
306
+ """List of strings printed to stderr by prints, subprocesses, etc."""
307
+
308
+ def __init__(
309
+ self,
310
+ stdout: Optional[List[str]] = None,
311
+ stderr: Optional[List[str]] = None,
312
+ **kwargs,
313
+ ):
314
+ self.stdout = stdout or []
315
+ self.stderr = stderr or []
316
+
317
+ def __repr__(self):
318
+ return f"Logs(stdout: {self.stdout}, stderr: {self.stderr})"
319
+
320
+ def to_json(self) -> str:
321
+ """
322
+ Returns the JSON representation of the Logs object.
323
+ """
324
+ data = {"stdout": self.stdout, "stderr": self.stderr}
325
+ return json.dumps(data)
326
+
327
+
328
+ def serialize_results(results: List[Result]) -> List[Dict[str, str]]:
329
+ """
330
+ Serializes the results to JSON.
331
+ """
332
+ serialized = []
333
+ for result in results:
334
+ serialized_dict = {}
335
+ for key in result.formats():
336
+ if key == "chart":
337
+ chart = result.chart
338
+ if chart is not None:
339
+ serialized_dict[key] = chart.to_dict()
340
+ else:
341
+ serialized_dict[key] = result[key]
342
+
343
+ serialized_dict["text"] = result.text
344
+ serialized.append(serialized_dict)
345
+
346
+ return serialized
347
+
348
+
349
+ @dataclass(repr=False)
350
+ class Execution:
351
+ """
352
+ Represents the result of a cell execution.
353
+ """
354
+
355
+ results: List[Result] = field(default_factory=list)
356
+ """List of the result of the cell (interactively interpreted last line), display calls (e.g. matplotlib plots)."""
357
+ logs: Logs = field(default_factory=Logs)
358
+ """Logs printed to stdout and stderr during execution."""
359
+ error: Optional[ExecutionError] = None
360
+ """Error object if an error occurred, None otherwise."""
361
+ execution_count: Optional[int] = None
362
+ """Execution count of the cell."""
363
+
364
+ def __init__(
365
+ self,
366
+ results: Optional[List[Result]] = None,
367
+ logs: Optional[Logs] = None,
368
+ error: Optional[ExecutionError] = None,
369
+ execution_count: Optional[int] = None,
370
+ **kwargs,
371
+ ):
372
+ self.results = results or []
373
+ self.logs = logs or Logs()
374
+ self.error = error
375
+ self.execution_count = execution_count
376
+
377
+ def __repr__(self):
378
+ return f"Execution(Results: {self.results}, Logs: {self.logs}, Error: {self.error})"
379
+
380
+ @property
381
+ def text(self) -> Optional[str]:
382
+ """
383
+ Returns the text representation of the result.
384
+
385
+ :return: The text representation of the result.
386
+ """
387
+ for d in self.results:
388
+ if d.is_main_result:
389
+ return d.text
390
+
391
+ def to_json(self) -> str:
392
+ """
393
+ Returns the JSON representation of the Execution object.
394
+ """
395
+ data = {
396
+ "results": serialize_results(self.results),
397
+ "logs": self.logs.to_json(),
398
+ "error": self.error.to_json() if self.error else None,
399
+ }
400
+ return json.dumps(data)
401
+
402
+
403
+ async def aextract_exception(res: Response):
404
+ if res.is_success:
405
+ return None
406
+
407
+ await res.aread()
408
+ return extract_exception(res)
409
+
410
+
411
+ def extract_exception(res: Response):
412
+ if res.is_success:
413
+ return None
414
+
415
+ res.read()
416
+ return format_exception(res)
417
+
418
+
419
+ def format_exception(res: Response):
420
+ if res.is_success:
421
+ return None
422
+
423
+ if res.status_code == 404:
424
+ return NotFoundException(res.text)
425
+ elif res.status_code == 502:
426
+ return TimeoutException(
427
+ f"{res.text}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeout' when starting the sandbox or calling '.set_timeout' on the sandbox with the desired timeout."
428
+ )
429
+ else:
430
+ body = res.text.strip()
431
+ return SandboxException(
432
+ f"{res.status_code} {res.reason_phrase}{f': {body}' if body else ''}"
433
+ )
434
+
435
+
436
+ def parse_output(
437
+ execution: Execution,
438
+ output: str,
439
+ on_stdout: Optional[OutputHandler[OutputMessage]] = None,
440
+ on_stderr: Optional[OutputHandler[OutputMessage]] = None,
441
+ on_result: Optional[OutputHandler[Result]] = None,
442
+ on_error: Optional[OutputHandler[ExecutionError]] = None,
443
+ ):
444
+ _parse_output(execution, output, on_stdout, on_stderr, on_result, on_error)
445
+
446
+
447
+ async def async_parse_output(
448
+ execution: Execution,
449
+ output: str,
450
+ on_stdout: Optional[OutputHandlerWithAsync[OutputMessage]] = None,
451
+ on_stderr: Optional[OutputHandlerWithAsync[OutputMessage]] = None,
452
+ on_result: Optional[OutputHandlerWithAsync[Result]] = None,
453
+ on_error: Optional[OutputHandlerWithAsync[ExecutionError]] = None,
454
+ ):
455
+ none_or_awaitable = _parse_output(
456
+ execution, output, on_stdout, on_stderr, on_result, on_error
457
+ )
458
+ if inspect.isawaitable(none_or_awaitable):
459
+ await none_or_awaitable
460
+
461
+
462
+ def _parse_output(
463
+ execution: Execution,
464
+ output: str,
465
+ on_stdout: Optional[OutputHandler[OutputMessage]] = None,
466
+ on_stderr: Optional[OutputHandler[OutputMessage]] = None,
467
+ on_result: Optional[OutputHandler[Result]] = None,
468
+ on_error: Optional[OutputHandler[ExecutionError]] = None,
469
+ ) -> Union[None, Awaitable[Any]]:
470
+ data = json.loads(output)
471
+ data_type = data.pop("type")
472
+
473
+ if data_type == "result":
474
+ result = Result(**data)
475
+ execution.results.append(result)
476
+ if on_result:
477
+ return on_result(result)
478
+ elif data_type == "stdout":
479
+ execution.logs.stdout.append(data["text"])
480
+ if on_stdout:
481
+ return on_stdout(OutputMessage(data["text"], data["timestamp"], False))
482
+ elif data_type == "stderr":
483
+ execution.logs.stderr.append(data["text"])
484
+ if on_stderr:
485
+ return on_stderr(OutputMessage(data["text"], data["timestamp"], True))
486
+ elif data_type == "error":
487
+ execution.error = ExecutionError(data["name"], data["value"], data["traceback"])
488
+ if on_error:
489
+ return on_error(execution.error)
490
+ elif data_type == "number_of_executions":
491
+ execution.execution_count = data["execution_count"]
492
+
493
+ return None
494
+
495
+
496
+ @dataclass
497
+ class Context:
498
+ """
499
+ Represents a context for code execution.
500
+ """
501
+
502
+ id: str
503
+ """
504
+ The ID of the context.
505
+ """
506
+ language: str
507
+ """
508
+ The language of the context.
509
+ """
510
+ cwd: str
511
+ """
512
+ The working directory of the context.
513
+ """
514
+
515
+ def __init__(self, context_id: str, language: str, cwd: str, **kwargs):
516
+ self.id = context_id
517
+ self.language = language
518
+ self.cwd = cwd
519
+
520
+ @classmethod
521
+ def from_json(cls, data: Dict[str, str]):
522
+ return cls(
523
+ context_id=data["id"],
524
+ language=data["language"],
525
+ cwd=data["cwd"],
526
+ )
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: boxfab-code-interpreter
3
+ Version: 0.1.0a1
4
+ Summary: BoxFab Code Interpreter: run AI-generated code in BoxFab sandboxes, Jupyter-style
5
+ Author: ContextFab, Inc.
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: boxfab>=0.1.0a3,<0.2
15
+ Requires-Dist: httpx>=0.20.0,<1.0.0
16
+ Requires-Dist: attrs>=21.3.0
17
+ Requires-Python: >=3.11
18
+ Project-URL: Repository, https://github.com/boxfab/python-sdk
19
+ Description-Content-Type: text/markdown
20
+
21
+ # BoxFab Code Interpreter (Python)
22
+
23
+ Run AI-generated code in isolated [BoxFab](https://boxfab.io) sandboxes, Jupyter-style:
24
+ Python, JavaScript, TypeScript, R, Java and Bash, with results such as text, tables and charts
25
+ returned as data.
26
+
27
+ ## Getting started
28
+
29
+ ### 1. Install the SDK
30
+
31
+ ```bash
32
+ pip install boxfab-code-interpreter
33
+ ```
34
+
35
+ ### 2. Get a BoxFab API key
36
+
37
+ Get a key from the [BoxFab dashboard](https://dashboard.boxfab.io) and set it as `BOXFAB_API_KEY`.
38
+ The SDK reads only `BOXFAB_*` variables.
39
+
40
+ ### 3. Execute code with code interpreter inside Sandbox
41
+
42
+ ```py
43
+ from boxfab_code_interpreter import Sandbox
44
+
45
+ with Sandbox.create() as sandbox:
46
+ sandbox.run_code("x = 1")
47
+ execution = sandbox.run_code("x+=1; x")
48
+ print(execution.text) # outputs 2
49
+ ```
50
+
51
+ ### 4. Bind the configuration to a client
52
+
53
+ The top-level `Sandbox` and `AsyncSandbox` exports read their configuration from the environment variables. To use an explicit configuration — e.g. several API keys or domains in one process — create a `BoxFab` client and use the resource classes it exposes:
54
+
55
+ ```py
56
+ from boxfab_code_interpreter import BoxFab
57
+
58
+ client = BoxFab(api_key="e2b_***", domain="boxfab.io")
59
+
60
+ with client.Sandbox.create() as sandbox:
61
+ execution = sandbox.run_code("x = 1; x += 1; x")
62
+
63
+ # The async variant is exposed as well.
64
+ async_sandbox = await client.AsyncSandbox.create()
65
+
66
+ # The core resources are bound to the client's configuration as well.
67
+ volume = client.Volume.create("my-volume")
68
+ exists = client.Template.exists("my-template")
69
+ secret = client.Secret.create("openai-api-key", "sk-***")
70
+
71
+ # The classes can be assigned and used like the top-level ones.
72
+ Sandbox = client.Sandbox
73
+ paginator = Sandbox.list()
74
+ ```
75
+
76
+ Per-call params still take precedence over the client's params, and clients are isolated from each other and from the env-configured top-level exports.
77
+
78
+ ## Under the hood
79
+
80
+ Sandboxes are created from the `code-interpreter-v1` template, which BoxFab provides to every team:
81
+ a Jupyter server with common data-science packages. BoxFab sandboxes are
82
+ [Firecracker](https://firecracker-microvm.github.io/) microVMs, run on the open-source
83
+ [E2B](https://github.com/e2b-dev/infra) sandbox infrastructure. This package is derived from the
84
+ open-source E2B Code Interpreter SDK;
85
+ [UPSTREAM.md](https://github.com/boxfab/python-sdk/blob/main/UPSTREAM.md) lists every change.
86
+
87
+ **Using the E2B Code Interpreter SDKs.** BoxFab serves the same API and provides the
88
+ `code-interpreter-v1` template, so the E2B Code Interpreter SDKs work with it as well. Set both
89
+ variables, the domain first:
90
+
91
+ ```bash
92
+ export E2B_DOMAIN=boxfab.io
93
+ export E2B_API_KEY="<your BoxFab API key>"
94
+ ```
95
+
96
+ Without `E2B_DOMAIN`, the E2B SDKs send requests, including the key, to E2B's own service.
97
+
98
+ Tested against BoxFab with `e2b-code-interpreter` 2.11.6 (Python) and `@e2b/code-interpreter` 2.9.0
99
+ (JavaScript).
100
+
101
+ ## License
102
+
103
+ MIT, as the E2B Code Interpreter SDK it is derived from; see [LICENSE](LICENSE).
104
+
105
+ BoxFab is a product of ContextFab, Inc.
@@ -0,0 +1,12 @@
1
+ boxfab_code_interpreter/__init__.py,sha256=4k96jiR2qqLbWV5sTOT05fc5P-Dv80LSwAk3pUyyYbs,349
2
+ boxfab_code_interpreter/charts.py,sha256=ivhSndofDJZAg-Nup3BxtnmkDAxwNUpaKou7Zf_5gYU,5475
3
+ boxfab_code_interpreter/client.py,sha256=6YZRemh8DO_y-CT-ifnG6_H8no49L8YVhe1TeEA8tZc,3535
4
+ boxfab_code_interpreter/code_interpreter_async.py,sha256=CUluRG364egG4Lptr3dNpdlPO6F1HPypkDhpYSxRWbU,15491
5
+ boxfab_code_interpreter/code_interpreter_sync.py,sha256=A9ekJYWNYtJwteW66ud0us0UzfU3e1NGHm4zcU-mzeU,15137
6
+ boxfab_code_interpreter/constants.py,sha256=Gs-ONgzrfrvFnCZzV9drv_LgmF2Domb56LZ_kcd9H8o,84
7
+ boxfab_code_interpreter/exceptions.py,sha256=aCrhoLKW6kpWeTy_6pRb4u74Bz7R9A8JBps8FIdevxQ,781
8
+ boxfab_code_interpreter/models.py,sha256=gbAU3IzHZ4bGjlBsZaVh5UNy2uIW_wqNqytcp3jzukY,14778
9
+ boxfab_code_interpreter-0.1.0a1.dist-info/licenses/LICENSE,sha256=Owp2F6JmvLCebzBDPeOq8ZFKqd05UisLlsTLWFFKC24,1110
10
+ boxfab_code_interpreter-0.1.0a1.dist-info/WHEEL,sha256=3OmCxFA0ffBIsX4x0o8CdfSvZAOvO3mzEG8U_z5TvV4,80
11
+ boxfab_code_interpreter-0.1.0a1.dist-info/METADATA,sha256=Fhlu62euI0uRpA4h8U7ZBSbqxSY2TyraT6imSrLOPqU,3748
12
+ boxfab_code_interpreter-0.1.0a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.8
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,10 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 FOUNDRYLABS, INC.
4
+ Copyright (c) 2026 ContextFab, Inc.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.