fastlog-io 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.
Potentially problematic release.
This version of fastlog-io might be problematic. Click here for more details.
- fastlog/__init__.py +14 -0
- fastlog/config.py +26 -0
- fastlog/core.py +125 -0
- fastlog/helpers.py +11 -0
- fastlog/intercept.py +47 -0
- fastlog/metrics.py +33 -0
- fastlog_io-0.1.0.dist-info/METADATA +144 -0
- fastlog_io-0.1.0.dist-info/RECORD +10 -0
- fastlog_io-0.1.0.dist-info/WHEEL +4 -0
- fastlog_io-0.1.0.dist-info/licenses/LICENSE +21 -0
fastlog/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""logs package – import‑time ready logger with Prometheus metrics helper.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
configure – reconfigure sinks, level, etc.
|
|
5
|
+
get_log(name=None) – get bound logger
|
|
6
|
+
log – the global loguru log
|
|
7
|
+
start_metrics_server – start a Prometheus /metrics endpoint
|
|
8
|
+
"""
|
|
9
|
+
from .core import configure, get_log
|
|
10
|
+
from .metrics import start_metrics_server
|
|
11
|
+
|
|
12
|
+
__all__ = ['configure', 'get_log', 'log', 'start_metrics_server']
|
|
13
|
+
|
|
14
|
+
log = get_log('app')
|
fastlog/config.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Config:
|
|
8
|
+
'''Holds runtime configuration for logging.'''
|
|
9
|
+
|
|
10
|
+
# Directory for log files. Defaults to None or $LOG_DIR env var
|
|
11
|
+
log_dir: Path | None = Path(os.getenv('LOG_DIR')) if os.getenv('LOG_DIR') else None
|
|
12
|
+
# Minimum log level. Can be overwritten with $LOG_LEVEL
|
|
13
|
+
level: str = os.getenv('LOG_LEVEL', 'INFO').upper()
|
|
14
|
+
# Loguru rotation policy (e.g. '100 MB' or '1 day').
|
|
15
|
+
rotation: str = os.getenv('LOG_ROTATION', '100 MB')
|
|
16
|
+
# Loguru retention policy (e.g. '30 days' or '10 files').
|
|
17
|
+
retention: str = os.getenv('LOG_RETENTION', '90 days')
|
|
18
|
+
|
|
19
|
+
format: str = (
|
|
20
|
+
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS!UTC}</green> | '
|
|
21
|
+
'<level>{level:<8}</level> | '
|
|
22
|
+
'<yellow>{extra[name]:<5}</yellow> | '
|
|
23
|
+
'<light-black>{extra[trace_id]}</light-black> | '
|
|
24
|
+
'<cyan>{module}.{function}:{line}</cyan> | '
|
|
25
|
+
'<level>{message}</level>{exception}'
|
|
26
|
+
)
|
fastlog/core.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import MethodType
|
|
6
|
+
from typing import ContextManager, Iterator, cast
|
|
7
|
+
|
|
8
|
+
from loguru import logger as _logger
|
|
9
|
+
from loguru._logger import Logger as _Logger
|
|
10
|
+
from loguru._logger import context as _context
|
|
11
|
+
|
|
12
|
+
from .config import Config
|
|
13
|
+
from .helpers import generate_id
|
|
14
|
+
from .intercept import reset_std_logging
|
|
15
|
+
from .metrics import metrics_patch
|
|
16
|
+
|
|
17
|
+
__all__ = ['configure', 'get_log', 'trace_ctx']
|
|
18
|
+
|
|
19
|
+
logger = _logger
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --------------------------------------------------------------------------- #
|
|
23
|
+
# Record patches
|
|
24
|
+
# --------------------------------------------------------------------------- #
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _patch_trace_and_name(record: dict) -> None:
|
|
28
|
+
trace_id = record['extra'].get('trace_id')
|
|
29
|
+
sub_trace_id = record['extra'].get('sub_trace_id')
|
|
30
|
+
if trace_id and sub_trace_id:
|
|
31
|
+
trace_id = f'{trace_id}:{sub_trace_id}'
|
|
32
|
+
elif sub_trace_id:
|
|
33
|
+
trace_id = sub_trace_id
|
|
34
|
+
elif not trace_id:
|
|
35
|
+
trace_id = f'-{generate_id(7)}'
|
|
36
|
+
record['extra']['trace_id'] = trace_id
|
|
37
|
+
record['extra'].setdefault('name', '-')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --------------------------------------------------------------------------- #
|
|
41
|
+
# Trace Func
|
|
42
|
+
# --------------------------------------------------------------------------- #
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@contextmanager
|
|
46
|
+
def _trace_ctx(self: _Logger, trace_id: str | None = None) -> Iterator[None]:
|
|
47
|
+
trace_id = trace_id or generate_id(8)
|
|
48
|
+
if _context.get().get('trace_id'):
|
|
49
|
+
with self.contextualize(sub_trace_id=trace_id):
|
|
50
|
+
yield
|
|
51
|
+
else:
|
|
52
|
+
with self.contextualize(trace_id=trace_id):
|
|
53
|
+
yield
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_Logger.trace_ctx = MethodType(_trace_ctx, logger)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Add static type checking support
|
|
60
|
+
class Logger(_Logger):
|
|
61
|
+
def trace_ctx(self, trace_id: str | None = None) -> ContextManager[None]: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
logger = cast(Logger, logger)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --------------------------------------------------------------------------- #
|
|
68
|
+
# Config
|
|
69
|
+
# --------------------------------------------------------------------------- #
|
|
70
|
+
|
|
71
|
+
_configure: bool = False
|
|
72
|
+
|
|
73
|
+
def configure(
|
|
74
|
+
log_dir: str | Path | None = None,
|
|
75
|
+
level: str | None = None,
|
|
76
|
+
rotation: str | None = None,
|
|
77
|
+
retention: str | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
cfg_kwargs = dict()
|
|
80
|
+
if log_dir is not None:
|
|
81
|
+
cfg_kwargs['log_dir'] = Path(log_dir)
|
|
82
|
+
if level is not None:
|
|
83
|
+
cfg_kwargs['level'] = level.upper()
|
|
84
|
+
if rotation is not None:
|
|
85
|
+
cfg_kwargs['rotation'] = rotation
|
|
86
|
+
if retention is not None:
|
|
87
|
+
cfg_kwargs['retention'] = retention
|
|
88
|
+
|
|
89
|
+
cfg = Config(**cfg_kwargs)
|
|
90
|
+
|
|
91
|
+
global logger
|
|
92
|
+
|
|
93
|
+
logger.remove()
|
|
94
|
+
|
|
95
|
+
logger = logger.patch(_patch_trace_and_name).patch(metrics_patch)
|
|
96
|
+
|
|
97
|
+
logger.add(
|
|
98
|
+
sys.stderr,
|
|
99
|
+
format=cfg.format,
|
|
100
|
+
level=cfg.level,
|
|
101
|
+
colorize=True,
|
|
102
|
+
enqueue=False,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if cfg.log_dir:
|
|
106
|
+
enqueue = sys.platform.startswith('linux')
|
|
107
|
+
logger.add(
|
|
108
|
+
cfg.log_dir / 'app.log',
|
|
109
|
+
format=cfg.format,
|
|
110
|
+
level=cfg.level,
|
|
111
|
+
rotation=cfg.rotation,
|
|
112
|
+
retention=cfg.retention,
|
|
113
|
+
enqueue=enqueue,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
reset_std_logging()
|
|
117
|
+
|
|
118
|
+
global _configure
|
|
119
|
+
_configure = True
|
|
120
|
+
|
|
121
|
+
@lru_cache
|
|
122
|
+
def get_log(name: str | None = None):
|
|
123
|
+
if not _configure:
|
|
124
|
+
configure()
|
|
125
|
+
return logger.bind(name=name) if name else logger
|
fastlog/helpers.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import string
|
|
2
|
+
|
|
3
|
+
from nanoid import generate
|
|
4
|
+
|
|
5
|
+
__all__ = ['generate_id']
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_id(size: int = 6, *, digits: bool = False) -> str:
|
|
9
|
+
'''Return a random NanoID (digits‑only when *digits* is True).'''
|
|
10
|
+
alphabet = string.digits if digits else string.ascii_letters + string.digits
|
|
11
|
+
return generate(alphabet, size=size)
|
fastlog/intercept.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
from .helpers import generate_id
|
|
6
|
+
class InterceptHandler(logging.Handler):
|
|
7
|
+
""" A handler that forwards standard logging records to Loguru. """
|
|
8
|
+
|
|
9
|
+
_cache: dict[str, str] = {}
|
|
10
|
+
|
|
11
|
+
def emit(self, record: logging.LogRecord) -> None: # noqa: D401
|
|
12
|
+
try:
|
|
13
|
+
level = logger.level(record.levelname).name
|
|
14
|
+
except ValueError:
|
|
15
|
+
level = record.levelno
|
|
16
|
+
|
|
17
|
+
frame, depth = logging.currentframe(), 2
|
|
18
|
+
while frame and frame.f_code.co_filename == logging.__file__:
|
|
19
|
+
frame = frame.f_back # type: ignore[assignment]
|
|
20
|
+
depth += 1
|
|
21
|
+
|
|
22
|
+
logger.opt(depth=depth, exception=record.exc_info).bind(
|
|
23
|
+
action=f'[{record.name}]{record.module}.{record.funcName}:{record.lineno}',
|
|
24
|
+
trace_id=self._track_id(record.name),
|
|
25
|
+
).log(level, record.getMessage())
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def _track_id(cls, name: str) -> str:
|
|
29
|
+
if name not in cls._cache:
|
|
30
|
+
cls._cache[name] = f'-{generate_id(6, digits=True)}-'
|
|
31
|
+
return cls._cache[name]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def reset_std_logging() -> None:
|
|
35
|
+
""" Replace the root logger's handlers with a single `InterceptHandler`. """
|
|
36
|
+
root = logging.getLogger()
|
|
37
|
+
root.handlers.clear()
|
|
38
|
+
root.addHandler(InterceptHandler())
|
|
39
|
+
root.setLevel(logging.WARNING)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def reset_uvicorn_logging() -> None:
|
|
43
|
+
# Capture the uvicorn log correctly to prevent duplicate output
|
|
44
|
+
for name in ('uvicorn', 'uvicorn.error', 'uvicorn.access'):
|
|
45
|
+
logging.getLogger(name).handlers.clear()
|
|
46
|
+
logging.getLogger(name).propagate = False
|
|
47
|
+
logging.getLogger('uvicorn').handlers = [InterceptHandler()]
|
fastlog/metrics.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Prometheus metrics integration for logs package.
|
|
2
|
+
|
|
3
|
+
Expose counters to let Grafana/Prometheus track log volume by level and logger name.
|
|
4
|
+
"""
|
|
5
|
+
try:
|
|
6
|
+
from prometheus_client import Counter, start_http_server
|
|
7
|
+
|
|
8
|
+
LOG_COUNTER = Counter(
|
|
9
|
+
'log_messages_total',
|
|
10
|
+
'Total log messages',
|
|
11
|
+
labelnames=('level', 'name'),
|
|
12
|
+
)
|
|
13
|
+
except ImportError: # pragma: no cover – optional dependency
|
|
14
|
+
LOG_COUNTER = None
|
|
15
|
+
Counter = None
|
|
16
|
+
|
|
17
|
+
__all__ = ['start_metrics_server', 'metrics_patch']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def metrics_patch(record: dict) -> None:
|
|
21
|
+
'''Loguru patch: increment Prometheus counter for each record.'''
|
|
22
|
+
if LOG_COUNTER is None:
|
|
23
|
+
return
|
|
24
|
+
level = record['level'].name.lower()
|
|
25
|
+
name = record['extra'].get('name', '-')
|
|
26
|
+
LOG_COUNTER.labels(level, name).inc()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def start_metrics_server(port: int = 8000):
|
|
30
|
+
'''Start a background HTTP server on *port* exposing /metrics.'''
|
|
31
|
+
if Counter is None:
|
|
32
|
+
raise RuntimeError('prometheus_client is not installed')
|
|
33
|
+
start_http_server(port)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastlog-io
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Thin wrapper around loguru with Prometheus metrics
|
|
5
|
+
Author: owq
|
|
6
|
+
License: MIT License
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
15
|
+
Classifier: Topic :: System :: Logging
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: loguru>=0.7.3
|
|
18
|
+
Requires-Dist: nanoid>=2.0.0
|
|
19
|
+
Provides-Extra: metrics
|
|
20
|
+
Requires-Dist: prometheus-client>=0.22.1; extra == 'metrics'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="https://pypi.org/project/fastlog-io" target="_blank">
|
|
25
|
+
<img src="https://img.owq.world/2025/fastlog-logo-857f7d.png" alt="FastLOG">
|
|
26
|
+
</a>
|
|
27
|
+
</p>
|
|
28
|
+
<p align="center">
|
|
29
|
+
<em>FastLOG is a lightweight wrapper around <a href="https://github.com/Delgan/loguru" target="_blank">Loguru</a> that offers Prometheus metrics, automatic `trace_id`, colourful output, and a dead‑simple configuration API.</em>
|
|
30
|
+
</p>
|
|
31
|
+
<p align="center">
|
|
32
|
+
<a href="https://pypi.org/project/fastlog-io" target="_blank">
|
|
33
|
+
<img src="https://img.shields.io/pypi/v/fastlog-io" alt="Package version">
|
|
34
|
+
</a>
|
|
35
|
+
<span> | <span>
|
|
36
|
+
<a href="https://pypi.org/project/fastlog-io" target="_blank">
|
|
37
|
+
<img src="https://img.shields.io/pypi/status/fastlog-io" alt="Development Status">
|
|
38
|
+
</a>
|
|
39
|
+
<span> | <span>
|
|
40
|
+
<a href="https://pypi.org/project/fastlog-io" target="_blank">
|
|
41
|
+
<img src="https://img.shields.io/pypi/pyversions/fastlog-io" alt="Supported Python versions">
|
|
42
|
+
</a>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ✨ Features
|
|
49
|
+
|
|
50
|
+
* **Zero‑config out of the box** – `from fastlog-io import log; log.info("hello")` works instantly.
|
|
51
|
+
* **Unified interface** – `configure()` + `get_log(name)` keep formatting consistent across modules.
|
|
52
|
+
* **Automatic `trace_id`** – generates a 7‑char NanoID when none is bound, perfect for request tracing.
|
|
53
|
+
* **Prometheus ready** – one call to `start_metrics_server()` exposes `/metrics` with log counters.
|
|
54
|
+
* **File rotation & retention** – default `rotation="100 MB"`; tune it via `configure()`.
|
|
55
|
+
* **Stdlib compatibility** – built‑in `reset_std_logging()` redirects the `logging` module to fastlog-io.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 🚀 Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install fastlog-io
|
|
63
|
+
# Use the prometheus dependency version
|
|
64
|
+
pip install fastlog-io[metrics]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Requirements**
|
|
68
|
+
|
|
69
|
+
* Python ≥ 3.12
|
|
70
|
+
* [loguru](https://pypi.org/project/loguru/)
|
|
71
|
+
* Optional: [prometheus‑client](https://pypi.org/project/prometheus-client/) for metric export
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## ⚡ Quickstart
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from fastlog import log, configure, start_metrics_server
|
|
79
|
+
|
|
80
|
+
# override defaults if needed
|
|
81
|
+
configure(level="DEBUG", log_dir="./logs")
|
|
82
|
+
start_metrics_server(port=9100)
|
|
83
|
+
|
|
84
|
+
log.info("service started")
|
|
85
|
+
|
|
86
|
+
# child logger
|
|
87
|
+
api_log = log.bind(name="api")
|
|
88
|
+
api_log.debug("new request")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Console output (colours stripped):
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
2025-06-21 09:57:56.510 | INFO | app | -cFZrY2V | main.<module>:7 | service started
|
|
95
|
+
2025-06-21 09:57:56.511 | DEBUG | api | -25bJVku | main.<module>:11 | new request
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🔧 Environment variables
|
|
101
|
+
|
|
102
|
+
| Variable | Default | Description |
|
|
103
|
+
| -------------- | --------- | ----------------------------------------------------- |
|
|
104
|
+
| `LOG_DIR` | *(empty)* | Log file directory; leave blank to log to stderr only |
|
|
105
|
+
| `LOG_LEVEL` | `INFO` | Minimum log level |
|
|
106
|
+
| `LOG_ROTATION` | `100 MB` | File rotation policy (Loguru syntax) |
|
|
107
|
+
|
|
108
|
+
> These can also be passed directly to `configure()` and override environment values.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 📊 Metrics
|
|
113
|
+
|
|
114
|
+
* **`log_messages_total{level, name}`** – total log messages, labelled by level and logger name.
|
|
115
|
+
|
|
116
|
+
Prometheus scrape example:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
targets:
|
|
120
|
+
- job_name: fastlog-io
|
|
121
|
+
static_configs:
|
|
122
|
+
- targets: ["127.0.0.1:9100"]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🛠 Development & Tests (powered by [uv](https://github.com/astral-sh/uv))
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# run tests in a temp venv with test extras installed
|
|
131
|
+
uv run --with '.[test]' pytest -q
|
|
132
|
+
|
|
133
|
+
# build wheel + sdist
|
|
134
|
+
uv build
|
|
135
|
+
|
|
136
|
+
# verify the built wheel in a clean env
|
|
137
|
+
uv run --with dist/*.whl --with pytest pytest -q
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 📄 License
|
|
143
|
+
|
|
144
|
+
[MIT](LICENSE) © 2025 OWQ
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
fastlog/__init__.py,sha256=liEBajqZD3wmqILPpAbfVZ1xyn4vLvy5LG5SOeMfAjo,487
|
|
2
|
+
fastlog/config.py,sha256=9PKil_bQxk_Y4KhL_6sdRf3HIDEECR4lfqm2RFCv-NE,987
|
|
3
|
+
fastlog/core.py,sha256=vZR6tJq8N68vtAxLdUtVvAFyjmiUZmWya0BP-Ud6_Og,3345
|
|
4
|
+
fastlog/helpers.py,sha256=ywzfhMFD1uiyXrIvZX9FTgT0y39QX6HvJGo-WfZeaMw,330
|
|
5
|
+
fastlog/intercept.py,sha256=rf9BbRWvAJ2Z6whqsLRXnzPq2l4zHDwA-mtTQ8pEdNY,1649
|
|
6
|
+
fastlog/metrics.py,sha256=g3_hA7m5jVI6scGOGC5fo9Yb71BugL8km-V9SNQa-r8,1015
|
|
7
|
+
fastlog_io-0.1.0.dist-info/METADATA,sha256=dS5JHTmhduI-zDV-4FqeWIGmc2EojIn6bdeO8HTpjOM,4427
|
|
8
|
+
fastlog_io-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
fastlog_io-0.1.0.dist-info/licenses/LICENSE,sha256=H5l4pEKXYzeobqnqWgyXrmsb2rVtlmHQWTjxY7jlW-M,1055
|
|
10
|
+
fastlog_io-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017
|
|
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.
|