diffnc 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.
@@ -0,0 +1,158 @@
1
+ """Junos set-form vendor parser.
2
+
3
+ Handles the flat ``set ...`` form produced by ``display set``. Documents are modelled as a
4
+ flat list of leaves (one per logical ``set`` statement); duplicate statements collapse to
5
+ one. ``activate <path>`` and ``deactivate <path>`` are treated as a tri-state per path
6
+ (activated / deactivated / unspecified), applied in input order — the last toggle wins and
7
+ only one state node per path remains in the tree. ``delete <path>`` is applied in-order:
8
+ it removes any earlier ``set``/``activate``/``deactivate`` whose path equals or starts
9
+ with ``<path>`` at a token boundary, mirroring the Junos CLI semantics. Subsequent ``set``
10
+ statements may re-add state.
11
+
12
+ Line comments (``#``, ``//`` to end of line) are stripped during parsing.
13
+
14
+ The hierarchical (``show configuration``) form is handled by a separate vendor, see
15
+ :mod:`diffnc.vendors.junos`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Iterable, Iterator
21
+ from dataclasses import dataclass
22
+ from typing import TYPE_CHECKING
23
+
24
+ from diffnc.errors import ParseError
25
+ from diffnc.ir import ConfigNode, ConfigTree
26
+ from diffnc.vendors.base import VendorParser
27
+
28
+ if TYPE_CHECKING:
29
+ from diffnc.reconcile import ReconcileEvent
30
+
31
+ _SET_PREFIXES = ("set ", "deactivate ", "activate ")
32
+ _DELETE_PREFIX = "delete "
33
+ _TOGGLE_PREFIXES = ("activate ", "deactivate ")
34
+ _VALID_SET_PREFIXES = (*_SET_PREFIXES, _DELETE_PREFIX)
35
+
36
+
37
+ @dataclass
38
+ class _JunosSetParser:
39
+ name: str = "junos_set"
40
+
41
+ def parse(self, text: str) -> ConfigTree:
42
+ tree = ConfigTree.empty(vendor=self.name)
43
+ for lineno, raw in enumerate(text.splitlines(), start=1):
44
+ stripped = _strip_line_comment(raw).strip()
45
+ if not stripped:
46
+ continue
47
+ if not stripped.startswith(_VALID_SET_PREFIXES):
48
+ raise ParseError(
49
+ f"line {lineno}: expected 'set'/'activate'/'deactivate'/'delete' line, "
50
+ f"got {stripped!r}"
51
+ )
52
+ if stripped.startswith(_DELETE_PREFIX):
53
+ path = stripped[len(_DELETE_PREFIX) :].strip()
54
+ if not path:
55
+ raise ParseError(f"line {lineno}: 'delete' requires a path")
56
+ _apply_delete(tree.root, path)
57
+ continue
58
+ if stripped.startswith(_TOGGLE_PREFIXES):
59
+ op, _, path = stripped.partition(" ")
60
+ path = path.strip()
61
+ if not path:
62
+ raise ParseError(f"line {lineno}: {op!r} requires a path")
63
+ _apply_state_toggle(tree.root, path, stripped)
64
+ continue
65
+ tree.root.add_child(ConfigNode(line=stripped))
66
+ return tree
67
+
68
+ def format(self, tree: ConfigTree) -> list[str]:
69
+ return [child.line for child in tree.root.children]
70
+
71
+ def render_open(self, node: ConfigNode, depth: int) -> str:
72
+ # Set form has no sections, so this should never be invoked by the diff engine.
73
+ raise ParseError("render_open is not applicable to Junos set form")
74
+
75
+ def render_close(self, node: ConfigNode, depth: int) -> str | None:
76
+ return None
77
+
78
+ def render_leaf(self, node: ConfigNode, depth: int) -> str:
79
+ return node.line
80
+
81
+ def is_order_sensitive(self, path: tuple[str, ...]) -> bool:
82
+ """Set form has no semantically ordered children.
83
+
84
+ :meth:`parse` already replays the activate/deactivate/delete operations to a
85
+ canonical final state, so the IR's residual order is just whatever the input
86
+ happened to leave behind — never load-bearing.
87
+ """
88
+
89
+ return False
90
+
91
+ def render_reconcile(self, events: Iterable[ReconcileEvent]) -> Iterator[str]:
92
+ from diffnc.reconcile import ReconcileAdd, ReconcileDelete
93
+
94
+ for ev in events:
95
+ if isinstance(ev, ReconcileAdd):
96
+ yield ev.node.line
97
+ elif isinstance(ev, ReconcileDelete):
98
+ yield f"delete {_strip_set_prefix(ev.node.line)}"
99
+
100
+
101
+ def _apply_state_toggle(root: ConfigNode, path: str, new_line: str) -> None:
102
+ """Toggle the activate/deactivate state for ``path``.
103
+
104
+ Replaces any existing ``activate <path>`` / ``deactivate <path>`` child line in place
105
+ (preserving first-occurrence position), or appends a new state node when none exists.
106
+ """
107
+
108
+ activate_line = f"activate {path}"
109
+ deactivate_line = f"deactivate {path}"
110
+ for child in root.children:
111
+ if child.line == activate_line or child.line == deactivate_line:
112
+ child.line = new_line
113
+ return
114
+ root.children.append(ConfigNode(line=new_line))
115
+
116
+
117
+ def _strip_set_prefix(line: str) -> str:
118
+ for prefix in _SET_PREFIXES:
119
+ if line.startswith(prefix):
120
+ return line[len(prefix) :]
121
+ return line
122
+
123
+
124
+ def _apply_delete(root: ConfigNode, delete_path: str) -> None:
125
+ """Drop children whose set-form path equals or token-prefix-matches ``delete_path``."""
126
+
127
+ boundary = delete_path + " "
128
+ surviving: list[ConfigNode] = []
129
+ for child in root.children:
130
+ path = _strip_set_prefix(child.line)
131
+ if path == delete_path or path.startswith(boundary):
132
+ continue
133
+ surviving.append(child)
134
+ root.children = surviving
135
+
136
+
137
+ def _strip_line_comment(line: str) -> str:
138
+ """Truncate at the first ``#`` or ``//`` that lies outside a double-quoted string."""
139
+
140
+ in_quote = False
141
+ i = 0
142
+ n = len(line)
143
+ while i < n:
144
+ ch = line[i]
145
+ if ch == '"':
146
+ in_quote = not in_quote
147
+ i += 1
148
+ continue
149
+ if not in_quote:
150
+ if ch == "#":
151
+ return line[:i]
152
+ if ch == "/" and i + 1 < n and line[i + 1] == "/":
153
+ return line[:i]
154
+ i += 1
155
+ return line
156
+
157
+
158
+ PARSER: VendorParser = _JunosSetParser()
diffnc/vendors/nxos.py ADDED
@@ -0,0 +1,22 @@
1
+ """NX-OS vendor parser.
2
+
3
+ NX-OS uses significant indentation (2-space unit) under section heads such as
4
+ ``interface Ethernet1/1`` or ``vrf context FOO``. Shutdown toggling, ``default``
5
+ semantics, and ``!``/``#`` comment handling are shared with the other Cisco-style
6
+ vendors via :mod:`diffnc.vendors._cisco_like`.
7
+
8
+ Unlike IOS, saved NX-OS configurations do not typically include ``end`` / ``exit``
9
+ terminator lines, so we leave the ``terminators`` set empty here to keep the existing
10
+ behaviour where such lines would be parsed as ordinary config statements if present.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from diffnc.vendors._cisco_like import CiscoLikeParser
16
+ from diffnc.vendors.base import VendorParser
17
+
18
+ PARSER: VendorParser = CiscoLikeParser(
19
+ name="nxos",
20
+ indent_unit=2,
21
+ terminators=frozenset(),
22
+ )
@@ -0,0 +1,283 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffnc
3
+ Version: 0.0.1
4
+ Summary: Structural diff library for network device configurations
5
+ Author-email: minefuto <46558834+minefuto@users.noreply.github.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 minefuto
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Classifier: Programming Language :: Python :: 3
29
+ Classifier: Programming Language :: Python :: 3.11
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Programming Language :: Python :: 3.14
33
+ Requires-Python: >=3.11
34
+ Description-Content-Type: text/markdown
35
+
36
+ # diffnc(DIFF for Network device Configurations)
37
+
38
+ A Python library and CLI that diffs network device configurations **with structural awareness**, exposed through a `difflib`-like API.
39
+
40
+ * Duplicate same-name blocks (e.g. `interface eth1` appearing more than once) are merged at parse time
41
+ * **Only sections where order carries meaning emit order diffs** (Junos `firewall filter` / `policy-statement` terms, Cisco `access-list` / `policy-map`, etc.). Everywhere else, reordering alone produces no diff
42
+ * Vendor is auto-detected. Diffing across vendors raises an error
43
+ * Supported vendors: **Cisco NX-OS**, **Cisco IOS**, **Cisco IOS-XE**, **Cisco IOS-XR**, **Arista EOS**, **Junos** (hierarchical), **Junos set** (`display set` format)
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install diffnc
49
+ ```
50
+
51
+ For development:
52
+
53
+ ```bash
54
+ uv sync
55
+ ```
56
+
57
+ ## Library usage
58
+
59
+ ```python
60
+ from diffnc import unified_diff, ndiff
61
+
62
+ with open("router-before.conf") as f:
63
+ a = f.read()
64
+ with open("router-after.conf") as f:
65
+ b = f.read()
66
+
67
+ # Structural unified diff (shows changed lines and their parent sections only)
68
+ for line in unified_diff(a, b, fromfile="before", tofile="after"):
69
+ print(line, end="")
70
+
71
+ # Full ndiff
72
+ for line in ndiff(a, b):
73
+ print(line, end="")
74
+ ```
75
+
76
+ To force a specific vendor:
77
+
78
+ ```python
79
+ unified_diff(a, b, vendor="junos_set")
80
+ ```
81
+
82
+ To only run detection:
83
+
84
+ ```python
85
+ from diffnc import detect_vendor
86
+ detect_vendor(open("config.conf").read()) # -> "nxos"
87
+ ```
88
+
89
+ ### `reconcile` (experimental)
90
+
91
+ > **Experimental.** The output shape and exact command sequences may change in future releases. Always review the generated commands before applying them to a live device.
92
+
93
+ `reconcile(a, b)` returns the bare config-mode command lines that, when entered on a device currently running config *A*, bring it to the state described by config *B*.
94
+
95
+ ```python
96
+ from diffnc import reconcile
97
+
98
+ for line in reconcile(a, b):
99
+ print(line)
100
+ ```
101
+
102
+ Output is config-mode commands only — no `configure terminal` / `end` / `commit` wrappers, no indentation. Pipe through your own session manager.
103
+
104
+ * **Cisco-like (NX-OS / IOS / IOS-XE / IOS-XR / EOS):** emits section navigation plus `<line>` for adds and `no <line>` for deletes (with `no no foo` collapsed to `foo`, so `no shutdown` ↔ `shutdown` toggles correctly).
105
+ * **Junos hierarchical:** emits flat `set <path>` and `delete <path>` lines.
106
+ * **Junos set:** emits `<line>` verbatim for adds and `delete <path>` (with the `set ` / `activate ` / `deactivate ` prefix stripped) for deletes.
107
+ * **Order-sensitive sections** (ACL, `policy-map`, Junos `firewall filter` / `policy-statement` terms): on any change, the entire section is deleted and recreated from *B* — partial in-place edits are not attempted.
108
+
109
+ Exceptions:
110
+
111
+ | Exception | When it is raised |
112
+ | --- | --- |
113
+ | `VendorMismatchError` | The two configs are detected as different vendors (e.g. Junos set vs. Junos hierarchical is also rejected here) |
114
+ | `ParseError` | Vendor detection failed, syntax error, etc. |
115
+
116
+ ## CLI
117
+
118
+ ```
119
+ diffnc [OPTIONS] FILE_A FILE_B
120
+
121
+ -u, --unified Structural unified diff (default)
122
+ -n, --ndiff Full ndiff output
123
+ -r, --reconcile Emit config-mode commands that transform FILE_A into FILE_B (experimental)
124
+ --vendor {junos,junos_set,nxos,ios,iosxe,iosxr,eos}
125
+ Skip auto-detection and use the given vendor
126
+ --color {auto,always,never}
127
+ Colorize +/- lines (auto = tty detection)
128
+ --version
129
+ ```
130
+
131
+ Exit codes follow `diff(1)`: `0` = no differences, `1` = differences found, `2` = error.
132
+
133
+ Example:
134
+
135
+ ```bash
136
+ $ diffnc before.conf after.conf
137
+ --- before.conf
138
+ +++ after.conf
139
+ +feature ospf
140
+ interface Ethernet1/1
141
+ - description uplink
142
+ + description uplink-to-spine
143
+ ```
144
+
145
+ Or, in reconcile mode (**experimental**):
146
+
147
+ ```bash
148
+ $ diffnc before.conf after.conf -r
149
+ interface Ethernet1/1
150
+ no description uplink
151
+ description uplink-to-spine
152
+ feature ospf
153
+ ```
154
+
155
+ ## Example: normalizing duplicate blocks
156
+
157
+ Input A:
158
+
159
+ ```
160
+ interface eth1
161
+ no shut
162
+ ip address 1.1.1.1/24
163
+ stp
164
+ ```
165
+
166
+ Input B (the same `interface eth1` appears twice):
167
+
168
+ ```
169
+ interface eth1
170
+ shut
171
+ ip address 1.1.1.1/24
172
+
173
+ interface eth1
174
+ stp
175
+ ```
176
+
177
+ `ndiff` output:
178
+
179
+ ```
180
+ interface eth1
181
+ - no shut
182
+ + shut
183
+ ip address 1.1.1.1/24
184
+ stp
185
+ ```
186
+
187
+ ## How order is handled
188
+
189
+ Network device configurations mix "sections whose semantics don't depend on order" with "sections where order determines behavior." diffnc diffs **order-insensitively by default** and only does **position-based comparison for parent paths where order carries meaning**.
190
+
191
+ ### Order-insensitive (reorder ≠ diff)
192
+
193
+ Most containers fall into this bucket. Examples: `system`, `interfaces`, `routing-options`, `vrf context`, top-level `interface ...`, `route-map FOO permit <seq>`, and so on. Reshuffling the children alone produces an empty diff.
194
+
195
+ ```
196
+ # A
197
+ system {
198
+ host-name foo;
199
+ domain-name example.com;
200
+ }
201
+
202
+ # B
203
+ system {
204
+ domain-name example.com;
205
+ host-name foo;
206
+ }
207
+
208
+ $ diffnc a.conf b.conf # → no diff, exit 0
209
+ ```
210
+
211
+ ### Order-sensitive (reorder = diff)
212
+
213
+ The paths below are evaluated in declaration order by the device, so swapping term/ACE/class order produces diff output.
214
+
215
+ | Vendor | Parent path | Children |
216
+ | --- | --- | --- |
217
+ | Junos | `firewall.filter <name>` | `term <name>` |
218
+ | Junos | `firewall.family <fam>.filter <name>` | `term <name>` |
219
+ | Junos | `policy-options.policy-statement <name>` | `term <name>` |
220
+ | Cisco-like (IOS / IOS-XE / IOS-XR / NX-OS / EOS) | `ip access-list <name>`, `ipv6 access-list <name>`, `mac access-list <name>` | ACE lines |
221
+ | Cisco-like (same as above) | `policy-map <name>` | `class <name>` blocks |
222
+
223
+ Pure reorders (children whose rendered subtree is byte-identical on both sides, just in a different position) are surfaced with a `!` marker, once per moved subtree. Children whose contents also changed continue to use `-` / `+` pairs.
224
+
225
+ Example: swapping two byte-identical terms inside a Junos firewall filter
226
+
227
+ ```diff
228
+ firewall {
229
+ filter F {
230
+ ! term B {
231
+ ! then discard;
232
+ ! }
233
+ }
234
+ }
235
+ ```
236
+
237
+ Example: a reorder of one term plus a content change in another term
238
+
239
+ ```diff
240
+ firewall {
241
+ filter F {
242
+ ! term A {
243
+ ! then accept;
244
+ ! }
245
+ term B {
246
+ - then discard;
247
+ + then reject;
248
+ }
249
+ }
250
+ }
251
+ ```
252
+
253
+ ### Customizing the behavior for a new vendor
254
+
255
+ The `VendorParser` protocol exposes `is_order_sensitive(path: tuple[str, ...]) -> bool`. `path` is the tuple of `line` values from the root down to "the parent node whose children are being compared." Returning `True` makes the children compared positionally via `SequenceMatcher`; returning `False` (the default) falls back to set-style key comparison. If you're subclassing the Cisco family, the shortest path is to pass `order_sensitive_predicate` to `CiscoLikeParser(...)`.
256
+
257
+ ## Development
258
+
259
+ ```bash
260
+ uv sync --extra dev
261
+ uv run pytest # tests
262
+ uv run ruff check . # lint
263
+ uv run ruff format . # format
264
+ uv run ty check # type check
265
+ ```
266
+
267
+ ## Adding a new vendor
268
+
269
+ Create a new module under `src/diffnc/vendors/`, expose an implementation of the `VendorParser` protocol (`src/diffnc/vendors/base.py`) as `PARSER`, call `register(_yourvendor.PARSER)` from `src/diffnc/vendors/__init__.py`, and add the corresponding case to the detection logic in `src/diffnc/detect.py`.
270
+
271
+ `VendorParser` requires the following methods:
272
+
273
+ * `parse(text) -> ConfigTree`
274
+ * `format(tree) -> list[str]`
275
+ * `render_open(node, depth) -> str`
276
+ * `render_close(node, depth) -> str | None`
277
+ * `render_leaf(node, depth) -> str`
278
+ * `is_order_sensitive(path) -> bool` (optional; treated as always `False` if not implemented. See the "How order is handled" section.)
279
+ * `render_reconcile(events) -> Iterator[str]` (optional; required only to support `reconcile`. Receives a sequence of `ReconcileAdd` / `ReconcileDelete` / `ReconcileRecreate` events from `diffnc.reconcile` and yields the corresponding CLI lines.)
280
+
281
+ ## License
282
+
283
+ MIT
@@ -0,0 +1,24 @@
1
+ diffnc/__init__.py,sha256=yavFeCIll2bGmhWcWVto-7okLVvIbjwHh7b8nCgxWFA,943
2
+ diffnc/__main__.py,sha256=hMk6DO5C-rRNRXCLgWgounKjoKZFY-z3e8RGksfkCqo,179
3
+ diffnc/cli.py,sha256=3deyuQqb0-vQf6QuhuI7OyTbps-sk8NF9LGxUYwMvys,4006
4
+ diffnc/detect.py,sha256=Bg1bRDsNvzEEC8kABL8aNCDk16N2uH5DyV84I49iW1s,5191
5
+ diffnc/diff.py,sha256=pnR62dCgN1dLUtu1laUx0noHh4ewsHF3rr6IaWFqd4E,12082
6
+ diffnc/errors.py,sha256=PfEvBZAEA7jeYzoyCNdZpvo1jhCF8OoH_dZZdPT-b9k,648
7
+ diffnc/ir.py,sha256=gN06VRvIGArvMPOOpeNZbQ6zEbvrfRfcowUtU0k-eWE,1979
8
+ diffnc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ diffnc/reconcile.py,sha256=kGlRBiFpAfd6bgflrM2oZCFwv89Uo7jRMaiLf5OhEo4,4714
10
+ diffnc/vendors/__init__.py,sha256=s5db9NNpV337_ov11PWlmGoyGp8iqtnm1i8JE3q7kuo,1726
11
+ diffnc/vendors/_cisco_like.py,sha256=jCS3g1b_jYLHgMWC0bJG8-qsb8UA2p9E-vL8VtTzk_I,7250
12
+ diffnc/vendors/base.py,sha256=DxHcmYd4Tj2VbI2xnPwAtLzH56Z2wOMrG7_FiwpN-Nw,3305
13
+ diffnc/vendors/eos.py,sha256=DizzGfb5Qcytj3CEYMPPw3uG78n428RuTQBLZs2ZOpU,704
14
+ diffnc/vendors/ios.py,sha256=jEvlUlV3XOJ04Q5K_oJtcmflBvnBi3gjOS4-Q4PFzXk,694
15
+ diffnc/vendors/iosxe.py,sha256=xsdyjALeJi66Zp17U2grdbBxQfiP6yzB60APakHbRNo,757
16
+ diffnc/vendors/iosxr.py,sha256=ZoKXFYmnM0wIDPxqMZAQ5XXIdrluOTAfIvx63_WqrX4,734
17
+ diffnc/vendors/junos.py,sha256=rufK-hmz8kvRa30OeU4LxYRtan95xIpRvrQ5x9jxINo,5896
18
+ diffnc/vendors/junos_set.py,sha256=y-_NaK4_fjRJbcjQlEXVRWMa_dg4j2EMi6-IGxvghSM,5807
19
+ diffnc/vendors/nxos.py,sha256=Kt9sSSf_5vfVWgq9GipcZSgghdut6yK0J23wLCx4h2s,817
20
+ diffnc-0.0.1.dist-info/METADATA,sha256=2--nghNCTvJJFsusR8XBKUn6xVpRzr1MdRdD6cxEXEk,9748
21
+ diffnc-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
22
+ diffnc-0.0.1.dist-info/entry_points.txt,sha256=F79q2jhkvDItSS2e4LhCFhPRy7w6PZ-F2aR9sg9LXvs,43
23
+ diffnc-0.0.1.dist-info/licenses/LICENSE,sha256=nQ7lrLsasMOfMg78sASYIcvRRhqCvGUYV7_1dXBbsBc,1065
24
+ diffnc-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ diffnc = diffnc.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 minefuto
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.