jacl4py 0.0.1__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.
jacl4py/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """jacl4py standalone logging package."""
2
+
3
+ __version__ = "0.0.1"
jacl4py/exceptions.py ADDED
@@ -0,0 +1,5 @@
1
+ """Exceptions raised by logger lifecycle operations."""
2
+
3
+
4
+ class ResourceNotInitializedError(RuntimeError):
5
+ """Raised when a deleted logger is used to log a message."""
jacl4py/logger.py ADDED
@@ -0,0 +1,260 @@
1
+ """
2
+ Logging utilities: ``JacLogger`` subclass with project-specific levels and
3
+ a ``get_logger`` factory that attaches console + rotating file handlers.
4
+ """
5
+ import logging
6
+ import os
7
+ import re
8
+ import tempfile
9
+ import warnings
10
+ from enum import Enum
11
+ from functools import wraps
12
+ from logging.handlers import TimedRotatingFileHandler
13
+ from pathlib import Path
14
+ from threading import RLock
15
+ from uuid import uuid4
16
+
17
+ import coloredlogs
18
+
19
+ from . import __version__ as _package_version
20
+ from .exceptions import ResourceNotInitializedError
21
+
22
+ __version__ = _package_version
23
+
24
+ INTERNAL_DEBUG = 5
25
+ INTERNAL_INFO = 7
26
+ logging.addLevelName(INTERNAL_DEBUG, "INTERNAL_DEBUG")
27
+ logging.addLevelName(INTERNAL_INFO, "INTERNAL_INFO")
28
+
29
+
30
+ class LogDir(Enum):
31
+ """Special log directory settings."""
32
+
33
+ DEFAULT = "default"
34
+
35
+
36
+ class JacLogger(logging.Logger):
37
+ """Logger subclass that adds INTERNAL_INFO (7) and INTERNAL_DEBUG (5) levels below DEBUG."""
38
+
39
+ _deleted: bool = False
40
+
41
+ def isEnabledFor(self, level: int) -> bool:
42
+ """Check the level, raising ResourceNotInitializedError after deletion."""
43
+ if self._deleted:
44
+ raise ResourceNotInitializedError(
45
+ f"Logger '{self.name}' has been deleted. "
46
+ "Obtain a new logger with get_logger() before logging."
47
+ )
48
+ return super().isEnabledFor(level)
49
+
50
+ def iinfo(self, message, *args, **kwargs):
51
+ """Log at INTERNAL_INFO level (7) --- framework lifecycle events, below DEBUG."""
52
+ if self.isEnabledFor(INTERNAL_INFO):
53
+ self._log(INTERNAL_INFO, message, args, **kwargs)
54
+
55
+ def idebug(self, message, *args, **kwargs):
56
+ """
57
+ Log at INTERNAL_DEBUG level (5) --- fine-grained framework tracing, below INTERNAL_INFO.
58
+ """
59
+ if self.isEnabledFor(INTERNAL_DEBUG):
60
+ self._log(INTERNAL_DEBUG, message, args, **kwargs)
61
+
62
+
63
+ _manager = logging.Manager(logging.root)
64
+ _manager.setLoggerClass(JacLogger)
65
+ _lifecycle_lock = RLock()
66
+
67
+
68
+ def _after_fork() -> None:
69
+ global _manager, _lifecycle_lock # pylint: disable=global-statement
70
+ for logger in _manager.loggerDict.values():
71
+ if isinstance(logger, JacLogger):
72
+ logger._deleted = True # pylint: disable=protected-access
73
+ logger.disabled = True
74
+ _manager = logging.Manager(logging.root)
75
+ _manager.setLoggerClass(JacLogger)
76
+ _lifecycle_lock = RLock()
77
+
78
+
79
+ if hasattr(os, "register_at_fork"):
80
+ os.register_at_fork(after_in_child=_after_fork)
81
+
82
+
83
+ def _synchronized(function):
84
+ """Serialize package logger lifecycle operations."""
85
+ @wraps(function)
86
+ def wrapped(*args, **kwargs):
87
+ """Run the operation while holding the lifecycle lock."""
88
+ with _lifecycle_lock:
89
+ return function(*args, **kwargs)
90
+ return wrapped
91
+
92
+
93
+ def _create_log_path(name: str, log_dir: str | Path | LogDir) -> Path:
94
+ """Reserve a unique sanitized log path without reusing an existing writer."""
95
+ safe_name = re.sub(r"[^A-Za-z0-9_.-]", "_", name).strip(".")[:64] or "logger"
96
+ if log_dir is not LogDir.DEFAULT:
97
+ directory = Path(log_dir)
98
+ directory.mkdir(parents=True, exist_ok=True)
99
+ while True:
100
+ identifier = safe_name + uuid4().hex[:8]
101
+ try:
102
+ if log_dir is LogDir.DEFAULT:
103
+ directory = Path(tempfile.gettempdir()) / identifier
104
+ directory.mkdir(mode=0o700)
105
+ return directory / f"{safe_name}.log"
106
+ filename = directory / f"{identifier}.log"
107
+ with filename.open("x", encoding="utf-8"):
108
+ pass
109
+ return filename
110
+ except FileExistsError:
111
+ continue
112
+
113
+
114
+ @_synchronized
115
+ def delete_logger(name: str = "default") -> bool:
116
+ """Close and unregister a package logger, keeping existing log files.
117
+
118
+ Return False if no named logger exists. Deleted JacLogger references
119
+ raise ResourceNotInitializedError when used to log;
120
+ package child loggers remain usable. Standard logging loggers are untouched.
121
+ """
122
+ manager = _manager
123
+ logger = manager.loggerDict.get(name)
124
+ if not isinstance(logger, logging.Logger):
125
+ return False
126
+
127
+ if isinstance(logger, JacLogger):
128
+ logger._deleted = True # pylint: disable=protected-access
129
+ logger.disabled = True
130
+ for handler in list(logger.handlers):
131
+ logger.removeHandler(handler)
132
+ handler.close()
133
+ del manager.loggerDict[name]
134
+ for child in list(manager.loggerDict.values()):
135
+ if isinstance(child, logging.Logger) and child.parent is logger:
136
+ manager._fixupParents(child) # pylint: disable=protected-access
137
+ return True
138
+
139
+
140
+ @_synchronized
141
+ def get_logger( # pylint: disable=too-many-arguments
142
+ name: str = "default",
143
+ *,
144
+ console_log_level: int | None = logging.INFO,
145
+ log_dir: str | Path | LogDir | None = LogDir.DEFAULT,
146
+ file_log_level: int | None = INTERNAL_DEBUG,
147
+ use_colored_output: bool = True,
148
+ reinit_if_exists: bool = False
149
+ ) -> JacLogger:
150
+ """
151
+ Get or create a configured logger instance.
152
+
153
+ Parameters
154
+ ----------
155
+ name : str
156
+ Logger name (default: "default")
157
+ console_log_level : int | None
158
+ Console handler level. Set to None to disable console logging.
159
+ log_dir : str | Path | LogDir | None
160
+ Directory for log files. LogDir.DEFAULT uses the system temp directory
161
+ under the sanitized name plus eight UUID4 hex characters. Explicit
162
+ directories use a unique filename. Set to None to disable file logging.
163
+ file_log_level : int | None
164
+ File handler level. Set to None to disable file logging.
165
+ use_colored_output : bool
166
+ Whether to use colored console output (default: True)
167
+ reinit_if_exists : bool
168
+ Delete an existing named logger and apply new settings (default: False).
169
+ Existing log files are preserved.
170
+
171
+ Returns
172
+ -------
173
+ JacLogger
174
+ Configured package-owned logger, separate from logging.getLogger(name).
175
+ After fork, obtain a fresh logger in the child with get_logger().
176
+ """
177
+ if not isinstance(name, str) or not name:
178
+ raise ValueError("Logger name must be a non-empty string.")
179
+ if reinit_if_exists:
180
+ delete_logger(name)
181
+ logger = _manager.getLogger(name)
182
+
183
+ # Avoid adding handlers multiple times
184
+ if logger.handlers:
185
+ warnings.warn(
186
+ f"Logger '{name}' already configured; returning cached logger "\
187
+ "and ignoring new settings.",
188
+ stacklevel=2
189
+ )
190
+ return logger
191
+
192
+ # Logger level must be INTERNAL to allow file handler to capture everything
193
+ # Individual handlers control their own output levels
194
+ logger.setLevel(INTERNAL_DEBUG)
195
+ logger.propagate = False
196
+
197
+ # Create file formatter
198
+ file_formatter = logging.Formatter(
199
+ "%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(message)s",
200
+ datefmt="%Y-%m-%d %H:%M:%S"
201
+ )
202
+
203
+ # Console handler with coloredlogs
204
+ if console_log_level is not None:
205
+ if use_colored_output:
206
+ # coloredlogs installs directly on the logger with field-level colors
207
+ coloredlogs.install(
208
+ level=console_log_level,
209
+ logger=logger,
210
+ fmt="%(asctime)s.%(msecs)03d %(hostname)s %(name)s[%(process)d] "\
211
+ "%(levelname)s %(message)s",
212
+ datefmt="%Y-%m-%d %H:%M:%S",
213
+ level_styles={
214
+ "internal_debug": {"color": "blue"},
215
+ "internal_info": {"color": "white"},
216
+ "debug": {"color": "cyan"},
217
+ "info": {"color": "green"},
218
+ "warning": {"color": "yellow", "bold": True},
219
+ "error": {"color": "red", "bold": True},
220
+ "critical": {"color": "red", "bold": True, "background": "white"}
221
+ },
222
+ field_styles={
223
+ "asctime": {"color": "white", "bold": True},
224
+ "hostname": {"color": "magenta"},
225
+ "levelname": {"color": "white", "bold": True},
226
+ "name": {"color": "cyan"},
227
+ "programname": {"color": "cyan"},
228
+ "process": {"color": "magenta"}
229
+ }
230
+ )
231
+ else:
232
+ console_handler = logging.StreamHandler()
233
+ console_handler.setLevel(console_log_level)
234
+ console_handler.setFormatter(file_formatter)
235
+ logger.addHandler(console_handler)
236
+
237
+ # File handler with rotation
238
+ if log_dir is not None and file_log_level is not None:
239
+ try:
240
+ log_filename = _create_log_path(name, log_dir)
241
+ file_handler = TimedRotatingFileHandler(
242
+ log_filename,
243
+ when="midnight",
244
+ interval=1,
245
+ backupCount=30,
246
+ encoding="utf-8"
247
+ )
248
+ file_handler.setLevel(file_log_level)
249
+ file_handler.setFormatter(file_formatter)
250
+ logger.addHandler(file_handler)
251
+ logger.info("Logging files to %s", log_filename.parent)
252
+
253
+ except (PermissionError, OSError) as e:
254
+ warnings.warn(
255
+ f"Could not create log file for '{name}' in {log_dir!r}: {e}. "
256
+ "File logging disabled.",
257
+ stacklevel=2
258
+ )
259
+
260
+ return logger
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: jacl4py
3
+ Version: 0.0.1
4
+ Summary: Just another custom logger for Python.
5
+ Author-Email: Fabian Krumm <ghub_fk@gmx.de>
6
+ License-Expression: BSD-3-Clause
7
+ License-File: LICENSE
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: coloredlogs>=15.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # jacl4py
15
+
16
+ Just another custom logger for Python.
17
+
18
+ `jacl4py` is a standalone package containing the custom Python logger previously
19
+ used by pookiepy.
20
+
21
+ ## Release Status
22
+
23
+ Releases before `1.0.0` are testing versions and may introduce breaking changes
24
+ without a major version increment. Semantic versioning will begin with `1.0.0`.
25
+
26
+ ## Installation
27
+
28
+ Install from PyPI:
29
+
30
+ ```powershell
31
+ pip install jacl4py
32
+ ```
33
+
34
+ Or install the local package with uv:
35
+
36
+ ```powershell
37
+ uv pip install .
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ from jacl4py.logger import INTERNAL_INFO, get_logger
44
+
45
+ logger = get_logger("service")
46
+ logger.iinfo("framework lifecycle event")
47
+ logger.log(INTERNAL_INFO, "another internal event")
48
+ logger.info("service started")
49
+ ```
50
+
51
+ Only `get_logger()` is needed to configure a logger. Package loggers use their
52
+ own registry, so importing the module does not replace Python's global logger
53
+ class. `logging.getLogger("service")` and `get_logger("service")` are separate
54
+ objects and do not change each other's handlers. The `INTERNAL_DEBUG` and
55
+ `INTERNAL_INFO` level names are registered with `logging`.
56
+
57
+ The console handler defaults to `INFO`; the file handler defaults to
58
+ `INTERNAL_DEBUG`. The default `log_dir=LogDir.DEFAULT` writes log files under the
59
+ system temporary directory in `<safe-name><uuid8>/<safe-name>.log`. For example,
60
+ `get_logger("test")` might write to `test12345678/test.log` under the temporary
61
+ directory. The suffix is the first eight hexadecimal characters of UUID4;
62
+ existing paths are never reused on a collision. Names are restricted to ASCII
63
+ letters, digits, dots, underscores, and hyphens for filesystem use, with other
64
+ characters replaced by underscores and the name component limited to 64
65
+ characters. The original logger name is preserved in log records.
66
+
67
+ Each writer has one stable active filename. Files rotate daily and retain up to
68
+ 30 backups for that writer, not necessarily 30 calendar days. Reinitialization
69
+ and new processes receive fresh paths. Old run directories and file families
70
+ are preserved, not automatically pruned. The custom `iinfo()` and `idebug()`
71
+ methods emit at levels 7 and 5 respectively.
72
+
73
+ Pass `log_dir=None` to disable file logging, or a string or `Path` for a custom
74
+ directory. An explicit directory is used as given, with a unique filename
75
+ `<safe-name><uuid8>.log` inside it. Pass `None` for `console_log_level` or
76
+ `file_log_level` to disable that handler. Use an application-owned directory
77
+ when file permissions or durable retention matter.
78
+
79
+ ```python
80
+ from jacl4py.logger import LogDir, get_logger
81
+
82
+ logger = get_logger("service", log_dir=LogDir.DEFAULT)
83
+ console_only = get_logger("console", log_dir=None)
84
+ ```
85
+
86
+ `JacLogger.setLevel()` follows standard logging behavior: it changes only the
87
+ logger threshold, preserving independent console and file handler levels.
88
+ Reusing a configured name returns the cached logger, emits a warning, and
89
+ ignores new configuration arguments. Initialization and deletion are serialized
90
+ across threads; ordinary log calls do not acquire the package lifecycle lock.
91
+
92
+ Pass `reinit_if_exists=True` to replace an existing logger and apply new settings
93
+ without the cached-logger warning:
94
+
95
+ ```python
96
+ from jacl4py.logger import delete_logger, get_logger
97
+
98
+ log = get_logger("my_test11", reinit_if_exists=True)
99
+ delete_logger("my_test11")
100
+ ```
101
+
102
+ `delete_logger(name)` closes and removes the package logger's handlers and
103
+ unregisters it without touching standard loggers. It returns `True` if a package
104
+ logger was deleted, otherwise `False`. Existing log
105
+ files are preserved. Logging through a deleted `JacLogger` reference raises
106
+ `ResourceNotInitializedError`, a subclass of `RuntimeError`, even for messages
107
+ below the configured level. Import it from `jacl4py.logger` or
108
+ `jacl4py.exceptions`. Use the new object returned by `get_logger()` after
109
+ reinitialization. Package child loggers remain usable and reconnect when the
110
+ named logger is recreated. Use `get_logger()` for child names too.
111
+
112
+ In each worker process, call `get_logger()` to obtain a separate writer. After
113
+ `fork`, inherited package logger references are invalidated automatically; do
114
+ not continue logging through an inherited reference. No queue or listener is
115
+ required. Logging is synchronous, including disk writes and rollover, so it can
116
+ block application threads or an async event loop. Configure or replace loggers
117
+ before starting work rather than while other threads are emitting records.
118
+
119
+ The version is available from the imported logger module:
120
+
121
+ ```python
122
+ from jacl4py import logger
123
+
124
+ print(logger.__version__)
125
+ ```
126
+
127
+ For a release, edit only `__version__` in `jacl4py/__init__.py`. PDM reads that
128
+ constant for wheel and source-distribution metadata; `jacl4py.__version__` and
129
+ `jacl4py.logger.__version__` expose the same value.
130
+
131
+ ## Development
132
+
133
+ This standalone repository targets Python 3.10 or newer. It is authored by
134
+ Fabian Krumm and distributed under the BSD 3-Clause License from the original
135
+ project.
136
+
137
+ Run the unit tests with uv:
138
+
139
+ ```powershell
140
+ uv pip install -r requirements.txt
141
+ uv run --with-requirements requirements.txt python -m unittest discover -s tests -p "test_*.py"
142
+ ```
143
+
144
+ Install development tools, including Pylint and coverage:
145
+
146
+ ```powershell
147
+ uv pip install -r requirements-dev.txt
148
+ ```
149
+
150
+ Run the same Pylint check as the push and pull request workflow:
151
+
152
+ ```powershell
153
+ uv run --with-requirements requirements-dev.txt python -m pylint --jobs=1 jacl4py tests
154
+ ```
155
+
156
+ Check test coverage, including branches and missing lines:
157
+
158
+ ```powershell
159
+ uv run --with-requirements requirements-dev.txt python -m coverage run --branch --source=jacl4py -m unittest discover -s tests -p "test_*.py"
160
+ uv run --with-requirements requirements-dev.txt python -m coverage report -m
161
+ ```
162
+
163
+ Build with:
164
+
165
+ ```powershell
166
+ uv build
167
+ ```
168
+
169
+ `requirements.txt` contains runtime-only dependencies and mirrors
170
+ `pyproject.toml` project dependencies. The workflows use it before tests, build,
171
+ and publication preflight; the requirements guard test detects drift, so update
172
+ both files together. PDM's declared build backend supplies `uv build` build
173
+ dependencies, while the generated wheel and source-distribution metadata causes
174
+ pip installs to resolve runtime dependencies automatically. The requirements
175
+ files are included in source distributions. `requirements-dev.txt` includes
176
+ the runtime requirements and adds development-only tools. PDM includes the package source and
177
+ test/workflow sources directly, so a `MANIFEST.in` file is unnecessary.
178
+
179
+ ## Publishing
180
+
181
+ GitHub Actions discovers workflows only from the repository root's
182
+ `.github/workflows` directory; workflows nested under `logger-package` are not
183
+ run automatically in this monorepo. When this package is published from its own
184
+ repository, keep these workflow files at that repository root.
185
+
186
+ `pypi_publish.yml` runs on pushes to `master`, not GitHub Release publication. A
187
+ read-only build job runs preflight unit tests, builds wheel and sdist, checks
188
+ them with Twine, smoke-tests each installed artifact, and uploads the validated
189
+ distributions. Only then does the `pypi-release` environment job publish those
190
+ artifacts through PyPI Trusted Publishing (OIDC, no stored API token), with
191
+ `skip-existing: true`, `print-hash: false`, and `attestations: true`.
192
+
193
+ After successful publication, it creates an annotated `v<version>` tag from
194
+ `jacl4py/__init__.py`. An existing tag on the same commit is an idempotent no-op;
195
+ a tag on another commit is never moved and causes failure. Edit the local
196
+ version constant manually before a new release: neither tags nor commit messages
197
+ infer or automatically bump the version. PDM reads that constant for metadata.
198
+
199
+ Configure the PyPI trusted publisher for project `jacl4py`, your GitHub
200
+ owner/repository, workflow filename `pypi_publish.yml`, and environment `pypi-release`.
201
+ Keep that filename to preserve the trusted publisher identity and retain the
202
+ environment URL `https://pypi.org/project/jacl4py/`. Confirm project availability
203
+ and choose an unpublished version before pushing. Skipping existing files allows
204
+ retries; it does not permit replacing a published version.
205
+
206
+ Publication depends on its own build job, not a mandatory CI `needs` barrier.
207
+ Protect `master` with required supported-version tests, lint, build and workflow
208
+ audit checks; protect the environment with reviewers and branch restrictions.
209
+ Floating `3.x` probes are nonblocking, not support guarantees. CI runs Python
210
+ 3.10-3.14 on five runners, reports coverage without a minimum, and uses Pylint
211
+ 3.2.6 with the default fail-under of 10 and no disabled messages or custom
212
+ threshold. See [.github/CI.md](.github/CI.md) for the matrix, weekly
213
+ checks, reference differences, and repository settings.
214
+
215
+ # todos
216
+
217
+ ## ai findings
218
+
219
+ - [ ] Medium (#6): Benchmark synchronous logging under representative load.
220
+ Document an optional bounded queue/listener recipe only if needed, including
221
+ overflow behavior and flushing at shutdown. UUID paths isolate writers but do
222
+ not remove I/O latency.
223
+ - [ ] Medium (#7): Make intentionally disabled outputs silent with a
224
+ `NullHandler`; track configuration independently of handler presence so
225
+ Python's `lastResort` handler cannot unexpectedly emit to stderr.
226
+ - [ ] Medium (#8): Validate replacement settings and build handlers before
227
+ invalidating the working logger. Finish cleanup if one handler fails to close,
228
+ and consider an opt-in strict policy for required file logging.
229
+ - [ ] Medium (#9): Increment `stacklevel` in `iinfo()` and `idebug()` so caller
230
+ metadata identifies application code rather than the wrapper.
231
+ - [ ] Medium (#10): Extend regression coverage for output silence, caller
232
+ attribution, custom global logger factories, and file open/write/close
233
+ failures; add branch coverage reporting in CI. High-severity regression tests
234
+ have been added but still need execution.
235
+ - [ ] Medium (#11): Pin the PyPI publishing action to a reviewed commit; lock
236
+ release tooling, automate dependency updates and vulnerability audits, and
237
+ verify the release tag against built metadata. Keep consumer dependency ranges
238
+ separate from release-tool reproducibility.
239
+ - [ ] Low (#12): Add real project/support URLs, tested Python classifiers,
240
+ contribution and private security-reporting guidance, and a compatibility and
241
+ deprecation policy. Expand privacy, permissions, and application integration
242
+ guidance as the public API settles.
243
+ - [ ] Retention follow-up: Define opt-in cleanup of completed runs if total
244
+ disk usage must be bounded. Per-writer rotation does not delete older UUID
245
+ directories or file families; do not delete other writers' files implicitly.
@@ -0,0 +1,8 @@
1
+ jacl4py-0.0.1.dist-info/METADATA,sha256=QMvS8Uexhh38Yz4CHeIDcPasmBRnDYDHPbDLobuIfww,10959
2
+ jacl4py-0.0.1.dist-info/WHEEL,sha256=IuIk3wbaEnFH1cx75iM2R1CmliRI3dFLXTbRakgvgFQ,90
3
+ jacl4py-0.0.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ jacl4py-0.0.1.dist-info/licenses/LICENSE,sha256=D5tHuFx3CsUmFQTursu3R7LmOAgCWqlytpquqGKWXdY,1499
5
+ jacl4py/__init__.py,sha256=aMADOnze8GsUON5ijtJD51SYZjnvFQQ6rz2uOOlp3M0,65
6
+ jacl4py/exceptions.py,sha256=9I1nP9oute88E6j0C6CTYhWd15urC6OAREv6tCRros8,172
7
+ jacl4py/logger.py,sha256=5mBE1Ve__XmhnmjloZDC5mWZ9SU7whCKwLL6nmRpF08,9339
8
+ jacl4py-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.5.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+
3
+ [gui_scripts]
4
+
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Fabian Krumm
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.