pagebar 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.
pagebar/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """pagebar — a tiny prod-safe perf footer for ASGI apps.
2
+
3
+ Public surface lives in :mod:`pagebar._core`; this module just re-exports it.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pagebar._core import PagebarMiddleware, __version__, pagebar_html
9
+
10
+ __all__ = ["PagebarMiddleware", "__version__", "pagebar_html"]
pagebar/_core.py ADDED
@@ -0,0 +1,392 @@
1
+ """pagebar implementation. Public surface is re-exported from ``pagebar``.
2
+
3
+ Drop ``PagebarMiddleware`` into the ASGI stack, drop ``pagebar_html()`` into
4
+ the host's base template. Per-request state lives in ``ContextVar``\\ s so
5
+ the helper can read what the middleware wrote without threading anything
6
+ through.
7
+
8
+ The full set of surfaced fields is fixed by the README — adding to it is a
9
+ security review, not a feature request.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import gc
15
+ import logging
16
+ import os
17
+ import re
18
+ import sys
19
+ import threading
20
+ import time
21
+ from contextvars import ContextVar
22
+ from html import escape
23
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Callable
28
+
29
+ __version__ = "0.1.0"
30
+
31
+ logger = logging.getLogger("pagebar")
32
+
33
+ # Time units, named so PLR2004 (magic-value-comparison) is honest.
34
+ _MS_PER_SEC = 1000
35
+ _SEC_PER_MIN = 60
36
+ _SEC_PER_HOUR = 3600
37
+ _SEC_PER_DAY = 86400
38
+ _BYTES_PER_MIB = 1024 * 1024
39
+ _SQL_TEXT_MAX = 500
40
+ _SQL_PARAMS_MAX = 200
41
+
42
+ # Process uptime baseline. Captured at import — fine for the worker-lifetime
43
+ # view; on multi-worker setups each worker has its own.
44
+ _BOOT_TIME = time.monotonic()
45
+
46
+ # Per-request state. ContextVars are the simplest way to hand data from
47
+ # middleware to the template helper without coupling them.
48
+ _start: ContextVar[float] = ContextVar("pagebar_start", default=0.0)
49
+ _queries: ContextVar[int] = ContextVar("pagebar_queries", default=0)
50
+ _method: ContextVar[str] = ContextVar("pagebar_method", default="")
51
+ _path: ContextVar[str] = ContextVar("pagebar_path", default="")
52
+ _version: ContextVar[str] = ContextVar("pagebar_version", default="dev")
53
+
54
+ # Unsafe mode — opt-in surfacing of SQL text + params. Default off.
55
+ _unsafe: ContextVar[bool] = ContextVar("pagebar_unsafe", default=False)
56
+ _sql_log: ContextVar[list] = ContextVar("pagebar_sql_log") # set per request
57
+
58
+
59
+ # Optional SQLAlchemy listener. Registered globally — same pattern as every
60
+ # other tracing/perf tool. +1 to an int per cursor execute. If SQLAlchemy
61
+ # isn't installed the counter just stays at zero.
62
+ def _on_query(
63
+ _c: Any, _cur: Any, statement: str, parameters: Any, _ctx: Any, _many: Any
64
+ ) -> None:
65
+ _queries.set(_queries.get() + 1)
66
+ if _unsafe.get():
67
+ _sql_log.get().append({
68
+ "sql": statement,
69
+ "params": parameters,
70
+ "_t0": time.perf_counter(),
71
+ })
72
+
73
+
74
+ def _on_query_done(*_args: Any, **_kwargs: Any) -> None:
75
+ if _unsafe.get():
76
+ log = _sql_log.get()
77
+ if log and "_t0" in log[-1]:
78
+ log[-1]["ms"] = (time.perf_counter() - log[-1].pop("_t0")) * 1000
79
+
80
+
81
+ def _register_sqlalchemy_listener() -> None:
82
+ """Hook the SQL counter into SQLAlchemy if it's importable.
83
+
84
+ Keeps the imports local so ty doesn't see a module-level name typed both
85
+ as ``type[Engine]`` and ``None``.
86
+ """
87
+ try:
88
+ from sqlalchemy import event # noqa: PLC0415
89
+ from sqlalchemy.engine import Engine # noqa: PLC0415
90
+ except ImportError: # pragma: no cover
91
+ return
92
+ event.listens_for(Engine, "before_cursor_execute")(_on_query)
93
+ event.listens_for(Engine, "after_cursor_execute")(_on_query_done)
94
+
95
+
96
+ _register_sqlalchemy_listener()
97
+
98
+
99
+ _BOT_RE = re.compile(r"bot|crawler|spider|googlebot|bingbot", re.IGNORECASE)
100
+
101
+
102
+ def _is_bot(scope: dict) -> bool:
103
+ for name, val in scope.get("headers", ()):
104
+ if name == b"user-agent":
105
+ return bool(_BOT_RE.search(val.decode("latin-1", "replace")))
106
+ return False
107
+
108
+
109
+ def _resolve_version(package: str, explicit: str) -> str:
110
+ if explicit:
111
+ return explicit
112
+ if not package:
113
+ return "dev"
114
+ try:
115
+ return _pkg_version(package)
116
+ except PackageNotFoundError:
117
+ logger.warning(
118
+ "pagebar: distribution %r not installed; falling back to 'dev'", package
119
+ )
120
+ return "dev"
121
+
122
+
123
+ class PagebarMiddleware:
124
+ """ASGI middleware: timer + bot/disable gate + SQL log under unsafe."""
125
+
126
+ def __init__( # noqa: PLR0913 — config-heavy by design
127
+ self,
128
+ app: Any,
129
+ *,
130
+ package: str = "",
131
+ version: str = "",
132
+ enabled: bool | Callable[[dict], bool] = True,
133
+ unsafe: bool | Callable[[dict], bool] = False,
134
+ query_budget: int = 0,
135
+ ) -> None:
136
+ self.app = app
137
+ self.package = package
138
+ self.version = _resolve_version(package, version)
139
+ self.enabled = enabled
140
+ self.unsafe = unsafe
141
+ self.query_budget = query_budget
142
+
143
+ @classmethod
144
+ def bound(cls, **kwargs: Any) -> type[PagebarMiddleware]:
145
+ """Return a subclass with config kwargs baked in.
146
+
147
+ Litestar (and similar frameworks) instantiate middleware as
148
+ ``mw_cls(app)`` with no further kwargs. Use this when you can't
149
+ thread keyword args through:
150
+
151
+ middleware=[
152
+ PagebarMiddleware.bound(package="my-app", unsafe=app.debug),
153
+ ...,
154
+ ]
155
+
156
+ For Starlette/FastAPI, prefer the plain
157
+ ``Middleware(PagebarMiddleware, package=..., unsafe=...)`` form —
158
+ no wrapper needed.
159
+ """
160
+
161
+ # ty rejects ``type[Self]`` as a class base; cast to ``Any``.
162
+ base: Any = cls
163
+
164
+ class _Bound(base):
165
+ def __init__(self, app: Any) -> None:
166
+ super().__init__(app, **kwargs)
167
+
168
+ _Bound.__name__ = f"{cls.__name__}Bound"
169
+ _Bound.__qualname__ = _Bound.__name__
170
+ return _Bound
171
+
172
+ async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
173
+ if scope["type"] != "http":
174
+ await self.app(scope, receive, send)
175
+ return
176
+
177
+ on = self.enabled(scope) if callable(self.enabled) else self.enabled
178
+ if not on or _is_bot(scope):
179
+ await self.app(scope, receive, send)
180
+ return
181
+
182
+ _start.set(time.perf_counter())
183
+ _queries.set(0)
184
+ _method.set(scope.get("method", ""))
185
+ _path.set(scope.get("path", ""))
186
+ _version.set(self.version)
187
+ _unsafe.set(self.unsafe(scope) if callable(self.unsafe) else self.unsafe)
188
+ _sql_log.set([])
189
+
190
+ await self.app(scope, receive, send)
191
+
192
+ if self.query_budget > 0 and _queries.get() > self.query_budget:
193
+ logger.warning(
194
+ "pagebar: SQL budget exceeded: %s %s — %d queries "
195
+ "(budget=%d, elapsed=%.3fs)",
196
+ _method.get(),
197
+ _path.get(),
198
+ _queries.get(),
199
+ self.query_budget,
200
+ time.perf_counter() - _start.get(),
201
+ )
202
+
203
+
204
+ # Bottom-right pill. Single dark color; opens upward into a small panel.
205
+ _STYLE = """\
206
+ .pagebar {
207
+ position: fixed;
208
+ bottom: 8px;
209
+ right: 8px;
210
+ z-index: 2147483647;
211
+ font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
212
+ background: #1f2937;
213
+ color: #e5e7eb;
214
+ border-radius: 6px;
215
+ box-shadow: 0 4px 12px rgba(0, 0, 0, .2);
216
+ padding: 4px 10px;
217
+ cursor: pointer;
218
+ user-select: none;
219
+ max-width: 90vw;
220
+ }
221
+ .pagebar-panel {
222
+ margin-top: 6px;
223
+ padding: 8px 10px;
224
+ background: #111827;
225
+ border-radius: 4px;
226
+ display: none;
227
+ }
228
+ .pagebar.--open .pagebar-panel {
229
+ display: block;
230
+ }
231
+ .pagebar-row {
232
+ display: flex;
233
+ justify-content: space-between;
234
+ gap: 1em;
235
+ margin: 2px 0;
236
+ white-space: nowrap;
237
+ }
238
+ .pagebar-row span:last-child {
239
+ color: #9ca3af;
240
+ }
241
+ .pagebar-sql {
242
+ margin-top: 8px;
243
+ padding-top: 8px;
244
+ border-top: 1px solid #374151;
245
+ max-height: 40vh;
246
+ overflow: auto;
247
+ }
248
+ .pagebar-sql-item {
249
+ margin: 4px 0;
250
+ padding: 4px;
251
+ background: #0b1220;
252
+ border-radius: 3px;
253
+ }
254
+ .pagebar-sql-meta {
255
+ color: #f59e0b;
256
+ font-size: 11px;
257
+ }
258
+ .pagebar-sql-text {
259
+ white-space: pre-wrap;
260
+ word-break: break-word;
261
+ margin-top: 2px;
262
+ }
263
+ .pagebar-sql-params {
264
+ color: #9ca3af;
265
+ font-size: 11px;
266
+ margin-top: 2px;
267
+ }
268
+ .pagebar-unsafe-tag {
269
+ color: #fca5a5;
270
+ font-size: 10px;
271
+ margin-left: 4px;
272
+ background: #7f1d1d;
273
+ padding: 1px 4px;
274
+ border-radius: 3px;
275
+ }
276
+ """
277
+
278
+ # Closes on click outside the panel; ESC closes too. State in localStorage.
279
+ _SCRIPT = """\
280
+ (() => {
281
+ const bar = document.currentScript.previousElementSibling;
282
+ const KEY = 'pagebar.open';
283
+ const apply = () => {
284
+ bar.classList.toggle('--open', localStorage.getItem(KEY) === '1');
285
+ };
286
+
287
+ bar.addEventListener('click', (e) => {
288
+ if (e.target.closest('.pagebar-panel')) return;
289
+ const next = bar.classList.contains('--open') ? '0' : '1';
290
+ localStorage.setItem(KEY, next);
291
+ apply();
292
+ });
293
+
294
+ document.addEventListener('keydown', (e) => {
295
+ if (e.key === 'Escape') {
296
+ localStorage.setItem(KEY, '0');
297
+ apply();
298
+ }
299
+ });
300
+
301
+ apply();
302
+ })();
303
+ """
304
+
305
+
306
+ def _fmt_elapsed(seconds: float) -> str:
307
+ ms = seconds * _MS_PER_SEC
308
+ if ms < _MS_PER_SEC:
309
+ return f"{ms:.0f}ms"
310
+ return f"{ms / _MS_PER_SEC:.2f}s"
311
+
312
+
313
+ def _fmt_uptime(seconds: float) -> str:
314
+ if seconds < _SEC_PER_MIN:
315
+ return f"{seconds:.0f}s"
316
+ if seconds < _SEC_PER_HOUR:
317
+ return f"{seconds / _SEC_PER_MIN:.0f}m"
318
+ if seconds < _SEC_PER_DAY:
319
+ return f"{seconds / _SEC_PER_HOUR:.1f}h"
320
+ return f"{seconds / _SEC_PER_DAY:.1f}d"
321
+
322
+
323
+ def _rss_mib() -> str:
324
+ """Process resident set size in MiB, or '—' if unavailable (Windows)."""
325
+ try:
326
+ import resource # noqa: PLC0415
327
+
328
+ rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
329
+ except ImportError: # Windows: no `resource` module
330
+ return "—"
331
+ # ru_maxrss is bytes on macOS, kibibytes on Linux/BSD.
332
+ bytes_ = rss if sys.platform == "darwin" else rss * 1024
333
+ return f"{bytes_ / _BYTES_PER_MIB:.1f} MiB"
334
+
335
+
336
+ def _render_sql_log() -> str:
337
+ log = _sql_log.get()
338
+ if not log:
339
+ return ""
340
+ items = []
341
+ for i, e in enumerate(log, 1):
342
+ sql = escape(str(e.get("sql", "")))[:_SQL_TEXT_MAX]
343
+ params = escape(repr(e.get("params", "")))[:_SQL_PARAMS_MAX]
344
+ ms = e.get("ms")
345
+ meta = f"#{i} · {ms:.1f}ms" if ms is not None else f"#{i}"
346
+ items.append(
347
+ f'<div class="pagebar-sql-item">'
348
+ f'<div class="pagebar-sql-meta">{meta}</div>'
349
+ f'<div class="pagebar-sql-text">{sql}</div>'
350
+ f'<div class="pagebar-sql-params">{params}</div>'
351
+ f"</div>"
352
+ )
353
+ return f'<div class="pagebar-sql">{"".join(items)}</div>'
354
+
355
+
356
+ def pagebar_html(*, nonce: str = "") -> str:
357
+ """Return the bar's HTML fragment. Safe to inject as-is."""
358
+ elapsed = time.perf_counter() - _start.get() if _start.get() else 0.0
359
+ q = _queries.get()
360
+ method = escape(_method.get())
361
+ path = escape(_path.get())
362
+ ver = escape(_version.get())
363
+ unsafe = _unsafe.get()
364
+
365
+ rows = (
366
+ ("Version", ver),
367
+ ("Time", _fmt_elapsed(elapsed)),
368
+ ("SQL", f"{q} quer{'y' if q == 1 else 'ies'}"),
369
+ ("Request", f"{method} {path}"),
370
+ ("Memory", _rss_mib()),
371
+ ("Uptime", _fmt_uptime(time.monotonic() - _BOOT_TIME)),
372
+ ("Threads", str(threading.active_count())),
373
+ ("GC", "/".join(str(n) for n in gc.get_count())),
374
+ ("Python", f"{sys.version_info.major}.{sys.version_info.minor}"),
375
+ ("PID", str(os.getpid())),
376
+ )
377
+ rows_html = "".join(
378
+ f'<div class="pagebar-row"><span>{k}</span><span>{v}</span></div>'
379
+ for k, v in rows
380
+ )
381
+ sql_html = _render_sql_log() if unsafe else ""
382
+ unsafe_tag = '<span class="pagebar-unsafe-tag">UNSAFE</span>' if unsafe else ""
383
+ n = f' nonce="{escape(nonce)}"' if nonce else ""
384
+ pill = f"{ver} · {_fmt_elapsed(elapsed)} · {q} SQL · {method} {path}"
385
+ return (
386
+ f"<style{n}>{_STYLE}</style>"
387
+ f'<div class="pagebar">'
388
+ f"<span>{pill}{unsafe_tag}</span>"
389
+ f'<div class="pagebar-panel">{rows_html}{sql_html}</div>'
390
+ f"</div>"
391
+ f"<script{n}>{_SCRIPT}</script>"
392
+ )
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: pagebar
3
+ Version: 0.1.0
4
+ Summary: A tiny prod-safe perf footer & toolbar for ASGI apps.
5
+ Author-email: Stéfane Fermigier <sf@fermigier.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: asgi,footer,monitoring,performance,sql
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.10
19
+ Provides-Extra: sqlalchemy
20
+ Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # pagebar
24
+
25
+ A tiny prod-safe perf footer for ASGI apps. One line at the bottom of every page, click to expand. No SQL text, no env vars, no stack traces — nothing that could leak.
26
+
27
+ ```
28
+ v2026.6.19 · 22ms · 4 SQL · GET /editeurs · 200
29
+ ```
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ pip install pagebar # core only
35
+ pip install pagebar[sqlalchemy] # with SQL query counter
36
+ ```
37
+
38
+ ## Use
39
+
40
+ ### Starlette / FastAPI
41
+
42
+ ```python
43
+ from pagebar import PagebarMiddleware, pagebar_html
44
+ from starlette.applications import Starlette
45
+ from starlette.middleware import Middleware
46
+
47
+ app = Starlette(
48
+ middleware=[Middleware(PagebarMiddleware, package="my-app")],
49
+ routes=[...],
50
+ )
51
+ ```
52
+
53
+ ### Litestar (and other `mw_cls(app)`-style frameworks)
54
+
55
+ Litestar instantiates middleware as `cls(app)` with no further kwargs, so config has to be baked in. Use `PagebarMiddleware.bound(...)`:
56
+
57
+ ```python
58
+ from litestar import Litestar
59
+ from pagebar import PagebarMiddleware
60
+
61
+ app = Litestar(
62
+ middleware=[
63
+ PagebarMiddleware.bound(package="my-app", unsafe=False),
64
+ ],
65
+ route_handlers=[...],
66
+ )
67
+ ```
68
+
69
+ `.bound(**kwargs)` returns a thin subclass with the kwargs baked into `__init__`. Same fields as the regular constructor.
70
+
71
+ ### Template
72
+
73
+ In your base template, anywhere inside `<body>`:
74
+
75
+ ```html
76
+ {{ pagebar_html() | safe }}
77
+ </body>
78
+ ```
79
+
80
+ That's it. One name for the middleware, one for the helper.
81
+
82
+ ## What's on screen
83
+
84
+ | Field | Source |
85
+ |---------|-------------------------------------------------------------------|
86
+ | Version | `importlib.metadata.version(package)` — or explicit `version=` |
87
+ | Time | `time.perf_counter()` delta |
88
+ | SQL | SQLAlchemy `before_cursor_execute` listener (if installed) |
89
+ | Request | method + path |
90
+ | Memory | `resource.getrusage().ru_maxrss` — RSS in MiB |
91
+ | Uptime | `time.monotonic()` since worker import |
92
+ | Threads | `threading.active_count()` |
93
+ | GC | `gc.get_count()` — gen0/gen1/gen2 pending |
94
+ | Python | `sys.version_info` |
95
+ | PID | `os.getpid()` |
96
+
97
+ No SQL text. No params. No env. No traces. That's the whole surface.
98
+
99
+ ## Knobs
100
+
101
+ ```python
102
+ PagebarMiddleware(
103
+ app,
104
+ package="my-app", # distribution name (PyPI / pyproject.toml)
105
+ version="", # escape hatch: bypass importlib.metadata lookup
106
+ enabled=True, # bool or callable(scope) -> bool
107
+ unsafe=False, # bool or callable(scope) -> bool — see below
108
+ query_budget=20, # >0 → log a WARNING when SQL count exceeds this
109
+ )
110
+ ```
111
+
112
+ ## Unsafe mode
113
+
114
+ In dev, you usually *want* to see the SQL text. Flip `unsafe=True` and the bar adds, per request: each statement (truncated), bound parameters, and per-statement timing. A red `UNSAFE` badge in the pill makes the mode obvious.
115
+
116
+ ```python
117
+ import os
118
+ PagebarMiddleware(app, package="my-app", unsafe=bool(os.getenv("DEBUG")))
119
+ # or framework-driven
120
+ PagebarMiddleware(app, package="my-app", unsafe=app.debug)
121
+ ```
122
+
123
+ `unsafe` defaults to `False` and **only** takes its value from constructor wiring — no URL parameter, no header, no cookie. The host's deploy config is the authority.
124
+
125
+ `enabled` lets you hide the bar from JSON endpoints, healthchecks, or admin routes:
126
+
127
+ ```python
128
+ def show(scope):
129
+ return not scope["path"].startswith(("/api/", "/health"))
130
+
131
+ Middleware(PagebarMiddleware, package="my-app", enabled=show)
132
+ ```
133
+
134
+ Bots are skipped automatically (`User-Agent` matching `bot|crawler|spider|googlebot|bingbot`).
135
+
136
+ ## CSP
137
+
138
+ `pagebar_html()` accepts a `nonce` keyword that's applied to the inline `<style>` and `<script>`:
139
+
140
+ ```html
141
+ {{ pagebar_html(nonce=csp_nonce) | safe }}
142
+ ```
143
+
144
+ Otherwise the host needs `'unsafe-inline'` for both directives. No external assets, no third-party requests.
145
+
146
+ ## Why not the Django/Flask/Litestar debug toolbars?
147
+
148
+ They reveal SQL text, environment, settings, request bodies, stack traces — fine in dev, unacceptable in production. pagebar surfaces a fixed, public-safe set of fields. The shape is the security model.
149
+
150
+ ## Non-goals
151
+
152
+ No panels system, no plugins, no per-framework adapters, no APM, no time series, no SQL EXPLAIN, no profiler. Pure ASGI middleware + one helper function in one file.
153
+
154
+ ## License
155
+
156
+ MIT.
@@ -0,0 +1,6 @@
1
+ pagebar/__init__.py,sha256=Gb-s-v6ESCbq4IYeE3LmBKzxslLXyRGZvqSZNOQrZ0Y,314
2
+ pagebar/_core.py,sha256=ckTQcvF_H7NezL3rdTPAqomoH96yH9h-fUPNiAUy8c0,11955
3
+ pagebar-0.1.0.dist-info/METADATA,sha256=UlKixCnAOk5dcoihpZ2e9KJTUsLvAq6u9uPnxrDJD4E,5354
4
+ pagebar-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ pagebar-0.1.0.dist-info/licenses/LICENSE,sha256=vUkf6nsiiAu8kQuDkDVPwiP3XBYOrTmN7hiJFKiC0Lc,1089
6
+ pagebar-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stéfane Fermigier / Abilian SAS
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.