submine 0.1.1__cp312-cp312-macosx_11_0_arm64.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.
Files changed (47) hide show
  1. submine/__init__.py +37 -0
  2. submine/algorithms/__init__.py +23 -0
  3. submine/algorithms/base.py +143 -0
  4. submine/algorithms/gspan.py +156 -0
  5. submine/algorithms/gspan_cpp.cpython-312-darwin.so +0 -0
  6. submine/algorithms/sopagrami.py +250 -0
  7. submine/algorithms/sopagrami_cpp.cpython-312-darwin.so +0 -0
  8. submine/api.py +134 -0
  9. submine/backends/__init__.py +0 -0
  10. submine/backends/gspan/CMakeLists.txt +65 -0
  11. submine/backends/gspan/dfs.cpp +98 -0
  12. submine/backends/gspan/graph.cpp +165 -0
  13. submine/backends/gspan/gspan.cpp +776 -0
  14. submine/backends/gspan/gspan.h +296 -0
  15. submine/backends/gspan/ismin.cpp +124 -0
  16. submine/backends/gspan/main.cpp +106 -0
  17. submine/backends/gspan/misc.cpp +177 -0
  18. submine/backends/gspan/python_bindings.cpp +133 -0
  19. submine/backends/sopagrami/cpp/CMakeLists.txt +44 -0
  20. submine/backends/sopagrami/cpp/include/alg.hpp +150 -0
  21. submine/backends/sopagrami/cpp/include/common/timer.hpp +18 -0
  22. submine/backends/sopagrami/cpp/src/alg.cpp +805 -0
  23. submine/backends/sopagrami/cpp/src/dump.cpp +262 -0
  24. submine/backends/sopagrami/cpp/src/main.cpp +94 -0
  25. submine/backends/sopagrami/cpp/src/python_bindings.cpp +123 -0
  26. submine/cli/__init__.py +6 -0
  27. submine/cli/main.py +87 -0
  28. submine/core/__init__.py +12 -0
  29. submine/core/graph.py +179 -0
  30. submine/core/result.py +121 -0
  31. submine/datasets/__init__.py +11 -0
  32. submine/datasets/loaders.py +145 -0
  33. submine/errors.py +41 -0
  34. submine/io/__init__.py +30 -0
  35. submine/io/common.py +173 -0
  36. submine/io/gexf.py +88 -0
  37. submine/io/gspan.py +268 -0
  38. submine/io/sopagrami.py +143 -0
  39. submine/io/transcode.py +147 -0
  40. submine/registry.py +8 -0
  41. submine/utils/__init__.py +6 -0
  42. submine/utils/checks.py +115 -0
  43. submine/utils/logging.py +41 -0
  44. submine-0.1.1.dist-info/METADATA +178 -0
  45. submine-0.1.1.dist-info/RECORD +47 -0
  46. submine-0.1.1.dist-info/WHEEL +6 -0
  47. submine-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,115 @@
1
+ """Environment and safety checks for submine.
2
+
3
+ This module centralizes lightweight validation utilities that protect the
4
+ library from common failure modes (corrupt inputs, missing binaries) and
5
+ from avoidable abuse (e.g., attempting to load arbitrarily large files into
6
+ memory, unsafe subprocess execution defaults).
7
+
8
+ These are not meant to be a sandbox. They are meant to provide sensible,
9
+ defensive defaults for a publishable OSS library.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import shutil
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+
20
+ DEFAULT_MAX_INPUT_MB = int(os.getenv("SUBMINE_MAX_INPUT_MB", "128"))
21
+ DEFAULT_MAX_INPUT_BYTES = DEFAULT_MAX_INPUT_MB * 1024 * 1024
22
+
23
+
24
+ DEFAULT_MAX_LINES = int(os.getenv("SUBMINE_MAX_LINES", "5000000")) # 5M lines
25
+ DEFAULT_MAX_LINE_BYTES = int(os.getenv("SUBMINE_MAX_LINE_BYTES", "1048576")) # 1 MiB per line
26
+
27
+
28
+ def iter_text_lines(
29
+ path: str | Path,
30
+ *,
31
+ encoding: str = "utf-8",
32
+ max_lines: int | None = None,
33
+ max_line_bytes: int | None = None,
34
+ ):
35
+ """Yield decoded lines from *path* with hard limits.
36
+
37
+ This is intended for parsers that stream large graph files. It protects
38
+ against:
39
+ - extremely long lines (often accidental corruption or malicious inputs)
40
+ - unbounded files that could consume excessive CPU time
41
+
42
+ Notes:
43
+ - Lines are decoded with ``errors='replace'`` to avoid UnicodeDecodeError.
44
+ - The returned lines are stripped of trailing ``\n``.
45
+ """
46
+ # Resolve limits at call time so test suites (and embedding apps) can
47
+ # override them via environment variables without requiring a reload.
48
+ if max_lines is None:
49
+ max_lines = int(os.getenv("SUBMINE_MAX_LINES", str(DEFAULT_MAX_LINES)))
50
+ if max_line_bytes is None:
51
+ max_line_bytes = int(os.getenv("SUBMINE_MAX_LINE_BYTES", str(DEFAULT_MAX_LINE_BYTES)))
52
+
53
+ p = assert_regular_file(path)
54
+ count = 0
55
+ with p.open("rb") as f:
56
+ for raw in f:
57
+ count += 1
58
+ if count > max_lines:
59
+ from ..errors import ResourceLimitError
60
+ raise ResourceLimitError(
61
+ f"Refusing to parse {p}: exceeds max line count limit ({max_lines}). "
62
+ "Set SUBMINE_MAX_LINES to increase the limit if you trust this input."
63
+ )
64
+ if len(raw) > max_line_bytes:
65
+ from ..errors import ResourceLimitError
66
+ raise ResourceLimitError(
67
+ f"Refusing to parse {p}: line {count} exceeds max line length ({max_line_bytes} bytes). "
68
+ "Set SUBMINE_MAX_LINE_BYTES to increase the limit if you trust this input."
69
+ )
70
+ yield raw.decode(encoding, errors="replace").rstrip("\n")
71
+
72
+ def is_tool_available(name: str) -> bool:
73
+ """Return True if a given executable exists on the system PATH."""
74
+ return shutil.which(name) is not None
75
+
76
+
77
+ def assert_regular_file(path: str | Path, *, must_exist: bool = True) -> Path:
78
+ """Validate that *path* points to a regular file.
79
+
80
+ We reject directories and (by default) require existence. We also resolve
81
+ symlinks to avoid surprises.
82
+ """
83
+ p = Path(path).expanduser()
84
+ if must_exist and not p.exists():
85
+ raise FileNotFoundError(f"File does not exist: {p}")
86
+ if must_exist and not p.is_file():
87
+ raise ValueError(f"Expected a regular file path, got: {p}")
88
+ # Resolve to eliminate '..' segments and follow symlinks.
89
+ return p.resolve()
90
+
91
+
92
+ def assert_file_size_under(path: str | Path, *, max_bytes: int = DEFAULT_MAX_INPUT_BYTES) -> None:
93
+ """Raise if the file exceeds *max_bytes*.
94
+
95
+ This is primarily used to protect code paths that necessarily read the full
96
+ file into memory (e.g., certain bindings).
97
+ """
98
+ p = Path(path)
99
+ try:
100
+ size = p.stat().st_size
101
+ except OSError as e:
102
+ raise OSError(f"Unable to stat file: {p}") from e
103
+ if size > max_bytes:
104
+ from ..errors import ResourceLimitError
105
+ raise ResourceLimitError(
106
+ f"Refusing to load {p} ({size} bytes): exceeds configured limit of {max_bytes} bytes. "
107
+ "Set SUBMINE_MAX_INPUT_MB to increase the limit if you trust this input."
108
+ )
109
+
110
+
111
+ def safe_read_text(path: str | Path, *, encoding: str = "utf-8", max_bytes: int = DEFAULT_MAX_INPUT_BYTES) -> str:
112
+ """Read a text file with a hard cap on bytes."""
113
+ p = assert_regular_file(path)
114
+ assert_file_size_under(p, max_bytes=max_bytes)
115
+ return p.read_text(encoding=encoding, errors="replace")
@@ -0,0 +1,41 @@
1
+ """Logging utilities for submine.
2
+
3
+ This module configures a simple hierarchical logging setup that
4
+ suppresses log output by default. Users can enable verbose logging
5
+ per algorithm by passing ``verbose=True`` to the constructor. All
6
+ modules within submine should obtain their logger via
7
+ :func:`get_logger` rather than calling :func:`logging.getLogger`
8
+ directly.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Optional
15
+
16
+
17
+ def get_logger(name: Optional[str] = None) -> logging.Logger:
18
+ """Return a logger with a default configuration.
19
+
20
+ If called for the first time this function sets up a root logger
21
+ with a basic configuration that logs messages with level WARNING and
22
+ above to stderr. Subsequent calls return child loggers that
23
+ propagate messages to the root.
24
+
25
+ Parameters
26
+ ----------
27
+ name: str, optional
28
+ Name of the logger. If omitted, a root logger is returned.
29
+
30
+ Returns
31
+ -------
32
+ logging.Logger
33
+ Configured logger instance.
34
+ """
35
+ if not logging.getLogger().handlers:
36
+ # Configure root logger once
37
+ logging.basicConfig(
38
+ level=logging.WARNING,
39
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
40
+ )
41
+ return logging.getLogger(name)
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.2
2
+ Name: submine
3
+ Version: 0.1.1
4
+ Summary: Modular subgraph mining library with unified API
5
+ Keywords: graph-mining,subgraph-mining,gspan,frequent-subgraph-mining
6
+ Author: Ridwan Amure
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Project-URL: Homepage, https://github.com/instabaines/submine
13
+ Project-URL: Repository, https://github.com/instabaines/submine
14
+ Project-URL: Issues, https://github.com/instabaines/submine/issues
15
+ Requires-Python: >=3.9
16
+ Requires-Dist: networkx>=2.8
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7; extra == "dev"
19
+ Requires-Dist: pytest-cov>=4; extra == "dev"
20
+ Requires-Dist: build>=1; extra == "dev"
21
+ Requires-Dist: twine>=5; extra == "dev"
22
+ Requires-Dist: ruff>=0.5; extra == "dev"
23
+ Description-Content-Type: text/markdown
24
+
25
+ # submine
26
+
27
+ *submine* is a modular Python library for frequent subgraph mining that provides a unified, safe, and extensible interface over heterogeneous mining algorithms implemented in Python, C/C++, and Java.
28
+
29
+ The library is designed to support research-grade reproducibility and production-safe execution, while remaining lightweight and backend-agnostic.
30
+
31
+ ## Core Design Principles
32
+
33
+ * Algorithm-agnostic API
34
+ Users select an algorithm; submine handles format adaptation and execution.
35
+
36
+ * No redundant graph rewrites
37
+ Input graphs are converted directly into the format required by the selected algorithm.
38
+
39
+ * Strict input validation & safety
40
+ Resource limits, parameter validation, and hardened subprocess execution are enforced by default.
41
+
42
+ * Clean extensibility model
43
+ New algorithms can be plugged in without modifying core logic.
44
+
45
+ ## Package Layout
46
+ ```
47
+ submine/
48
+ ├── api.py # Public API entrypoint
49
+ ├── errors.py # Structured exception hierarchy
50
+ ├── core/
51
+ │ ├── graph.py # Canonical graph representation
52
+ │ └── result.py # Mining result containers
53
+ ├── algorithms/
54
+ │ ├── base.py # Base miner abstraction
55
+ │ ├── gspan.py # gSpan wrapper / implementation
56
+ │ ├── sopagrami.py # SoPaGraMi backend wrapper
57
+ │ └── ...
58
+ ├── io/
59
+ │ ├── common.py # Shared readers / writers
60
+ │ ├── transcode.py # Format detection & conversion
61
+ │ ├── gspan.py # gSpan I/O
62
+ │ ├── sopagrami.py # .lg I/O
63
+ │ └── ...
64
+ ├── utils/
65
+ │ └── checks.py # Input validation & resource limits
66
+ └── tests/
67
+ ├── unit/
68
+ └── functional/
69
+ ```
70
+
71
+ ## Supported Algorithms
72
+
73
+ | Algorithm | Graph Type | Backend | Notes |
74
+ | --------- | ------------------ | ------------ | ------------------------ |
75
+ | gSpan | Multiple graphs | Python / C++ | Frequent subgraph mining |
76
+ | SoPaGraMi | Single large graph | C++ | Social pattern mining |
77
+
78
+ Each algorithm declares:
79
+
80
+ * required input format,
81
+ * parameter schema,
82
+ * execution strategy (in-process vs subprocess).
83
+
84
+ ## Supported Input Formats
85
+
86
+ submine accepts graphs in multiple formats and converts them directly into the format required by the selected algorithm:
87
+
88
+ * Edge list (.txt, .edgelist)
89
+
90
+ * gSpan datasets (.data, .data.x, .data.N)
91
+
92
+ * SoPaGraMi .lg
93
+
94
+ * GEXF (.gexf)
95
+
96
+ * Format detection is automatic and deterministic.
97
+
98
+ ## Installation
99
+ ### Runtime installation
100
+
101
+
102
+ ```bash
103
+ pip install submine
104
+
105
+ ```
106
+ ### Dev installation
107
+
108
+ ```bash
109
+ pip install -e ".[dev]"
110
+ ```
111
+
112
+ ## Basic Usage
113
+
114
+ ```python
115
+ from submine.api import mine_subgraphs
116
+
117
+ results = mine_subgraphs(
118
+ data="graph.data",
119
+ algorithm="gspan",
120
+ min_support=5
121
+ )
122
+
123
+ ```
124
+
125
+ For SoPaGraMi:
126
+ ```python
127
+ results = mine_subgraphs("citeseer.lg", algorithm="sopagrami",
128
+ min_support=100,
129
+ sorted_seeds=4,
130
+ dump_images_csv=True, #if you want to dump the images
131
+ dump_sample_embeddings=True, #if you want to dump the embeddings, not implemented yet
132
+ out_dir= "." # /path/to/dir to save the images and embeddings default is ./sopagrami_result
133
+ )
134
+ ```
135
+
136
+ ## TODO
137
+ * Include more algorithms
138
+ * Added more utils for cross-usage between other graph/network libraries
139
+
140
+ # Citation
141
+
142
+ If you use gspan kindly cite the paper
143
+
144
+ ```
145
+ @inproceedings{yan2002gspan,
146
+ title={gspan: Graph-based substructure pattern mining},
147
+ author={Yan, Xifeng and Han, Jiawei},
148
+ booktitle={2002 IEEE International Conference on Data Mining, 2002. Proceedings.},
149
+ pages={721--724},
150
+ year={2002},
151
+ organization={IEEE}
152
+ }
153
+ ```
154
+
155
+ if you use sopagrami, kindly cite the paper
156
+
157
+ ```
158
+ @article{nguyen2020fast,
159
+ title={Fast and scalable algorithms for mining subgraphs in a single large graph},
160
+ author={Nguyen, Lam BQ and Vo, Bay and Le, Ngoc-Thao and Snasel, Vaclav and Zelinka, Ivan},
161
+ journal={Engineering Applications of Artificial Intelligence},
162
+ volume={90},
163
+ pages={103539},
164
+ year={2020},
165
+ publisher={Elsevier}
166
+ }
167
+ ```
168
+
169
+ You can cite this implementation as well
170
+
171
+ ```
172
+ @misc{amure_submine,
173
+ title = {submine: A Subgraph Mining Library},
174
+ author = {Amure, Ridwan},
175
+ year = {2025},
176
+ url = {https://github.com/instabaines/submine}
177
+ }
178
+ ```
@@ -0,0 +1,47 @@
1
+ submine-0.1.1.dist-info/RECORD,,
2
+ submine-0.1.1.dist-info/WHEEL,sha256=735A17gG5ShyLwkIV8AnDG0hqZcskErhdy-3EckFfDc,141
3
+ submine-0.1.1.dist-info/METADATA,sha256=jMDdclv55XPqpJm7qoRrTGbqWx9H19Pu7Z83VqjYzbE,5321
4
+ submine-0.1.1.dist-info/licenses/LICENSE,sha256=KNAADYhXKAIGySYjfCVq6P4ZDhIUFfHxeZFYa3-32ec,1055
5
+ submine/registry.py,sha256=Jr8tbfbU_j5L5waH7gl4dqtrNB0X_boHnu3A0bVMmuo,254
6
+ submine/__init__.py,sha256=tnuVK76tHfOrZPntQLD-iUugR5PyyGGaE0kyrY2VRjI,899
7
+ submine/api.py,sha256=mGy1ynLFyWnj4yv1piDbBkebNE6GEw4FztwhrKsp4Ao,4369
8
+ submine/errors.py,sha256=YaEtNKkN4qpyrbhSLaN3Vkcot6sfG-9atiYPo8HWds8,1112
9
+ submine/core/graph.py,sha256=7yzUfEyuRuOmGPACFg3AR51UoHkvgiGkE43ceXY5HLA,6739
10
+ submine/core/__init__.py,sha256=prISqLxgIC4zUO39LDC69AhSrK4tGl1iC0BmKzuxL_A,443
11
+ submine/core/result.py,sha256=t9D1sYYWr-vdVJGw5AX-CSbiuSioVrrmbY3EeT54mX8,3653
12
+ submine/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ submine/backends/gspan/CMakeLists.txt,sha256=etRcErf0JK9w7xj_OghoFGszpiQooETHPZE4UIu3RvA,1586
14
+ submine/backends/gspan/misc.cpp,sha256=w5Z2XvogkSNqskZezxauvii-1H0lqZ_RWIpEWfQK5LI,4961
15
+ submine/backends/gspan/graph.cpp,sha256=zhtw3vXcOIFk1sZpoIOQUFIey6teLmPvCeQ_2LiD98w,4265
16
+ submine/backends/gspan/gspan.h,sha256=w9rPfrOB1JIi1M1AlD02PKsrELkRwC3uvcFc0jrLIz4,8247
17
+ submine/backends/gspan/dfs.cpp,sha256=sBsixi3CzhmnClbzmFDj9DOYQ-eaeplL3zVIO7q1lhI,2593
18
+ submine/backends/gspan/ismin.cpp,sha256=MKYMYgnMIMaiAKZLIdNjEe7eUQ2E_mGzo6phOcDmiuM,4083
19
+ submine/backends/gspan/gspan.cpp,sha256=9iud5jf23oyUyjNWe_z_r94f48tnqK5EWQceWUfGIgk,21101
20
+ submine/backends/gspan/python_bindings.cpp,sha256=IrOhBJiYjMOa90GEzncxeBZ1VkZ5-wkJHZB-cqNHZsI,3912
21
+ submine/backends/gspan/main.cpp,sha256=ipp3kPAssz-BBu_7rDi6GfPWwKjJfkJYIv74Rl5DH_A,3414
22
+ submine/backends/sopagrami/cpp/CMakeLists.txt,sha256=WXOoHkclzxmcBCyRBe_D-wV1bXM48CQeKioE-ZvR0Rk,1263
23
+ submine/backends/sopagrami/cpp/include/alg.hpp,sha256=D8krOTYxX47DS3ur_2fwyYYROmv1nXuM6rDiMx5spw8,5284
24
+ submine/backends/sopagrami/cpp/include/common/timer.hpp,sha256=h3wUdSJU_tpoXf5NVtmcUj92wBcNgUPUKEuLcCGiz3Q,360
25
+ submine/backends/sopagrami/cpp/src/alg.cpp,sha256=wd8CTdfUS7iemG6OCWn3mNtujJNplaf561Q0SCzynGo,28710
26
+ submine/backends/sopagrami/cpp/src/python_bindings.cpp,sha256=e9OsCPhaFdkBmqTYSVMXLwFTrg2ig0nebw5dKUxtBqM,3356
27
+ submine/backends/sopagrami/cpp/src/main.cpp,sha256=oiI4I73bKqj_IbuNjEI4WHZL8M9pvIVlJwhcGgSGbHA,3197
28
+ submine/backends/sopagrami/cpp/src/dump.cpp,sha256=0knEeEzPQXVOCJhvU3i4ELIfXdeJsTQUG1OTHxVXtN4,8942
29
+ submine/datasets/loaders.py,sha256=USiGS45-5DwshmJsx0f788Pb-471xcB7np31OUzkm5E,4952
30
+ submine/datasets/__init__.py,sha256=JggD0frQ9zZh9iXINpNMGvTkFac1DZa3U-Gf8umvnmE,384
31
+ submine/io/gexf.py,sha256=jdJYTgf3WyLIhIP-8m4b9JJbGGo2_PvAQ1ddwVGKBLU,2695
32
+ submine/io/transcode.py,sha256=9lj5QOZVz1Q6SQTCWGWlxEzjOfVTqFNzFQ026hVotG4,4428
33
+ submine/io/__init__.py,sha256=VAuJCGXE29KGGSq3jHGOV0mtYLS_yMcg_v-7ESHQze4,697
34
+ submine/io/gspan.py,sha256=B49RPwsSurN-rbPJUZppzJ_5hFzGgwBF9UYsTv3h_wI,8499
35
+ submine/io/common.py,sha256=afDqYSBco4DH8vuhRIZd0SKy_AqFPrrSOWNja5Of7lU,4905
36
+ submine/io/sopagrami.py,sha256=qYqZvKpUmSxj8HDzn7IxNnhu11LRFcMX4NCKro4NIpw,4950
37
+ submine/algorithms/sopagrami_cpp.cpython-312-darwin.so,sha256=h6JndK0C4OWj0Tns-tVKvnupN1cxPLmHRwxpPvxx718,274192
38
+ submine/algorithms/__init__.py,sha256=2OvRa_DdtsGbE-Yf_FF0Q6XadS9wTSh2mlYfrc6Yl9s,953
39
+ submine/algorithms/gspan.py,sha256=wa3kyEkNGN1GaEq8z4SglL4FUbXvYGkc1CwF96fmugo,5616
40
+ submine/algorithms/gspan_cpp.cpython-312-darwin.so,sha256=0xWYe_-tU-8m2SMLxFwXdVLnzOPaVXYIEfaKNg6X7sQ,188192
41
+ submine/algorithms/sopagrami.py,sha256=GDqPmZ-XRjxEAlzYDpRJTqpxRrNN1ccpP_0_PLPDLX4,9157
42
+ submine/algorithms/base.py,sha256=YW2X021ZfHIhnggwMkzklh5KMdnN1HoQ1XB7MTfwmRk,5381
43
+ submine/utils/logging.py,sha256=zcx-noJ20Cl2sbaH1HZkKSZZyOXMfHB9JRbbVACJpIM,1275
44
+ submine/utils/checks.py,sha256=fFlxFyPNRfhztgoYXzvfaoELzHN8qQOCmuHUaUbLX2E,4519
45
+ submine/utils/__init__.py,sha256=pF9-ISTFv1XH4g7RiVz2Dapt1EdJBUNleJ7mhpELlGA,196
46
+ submine/cli/__init__.py,sha256=k3MAv7_6UTA7O0Kp1FaMBd0veB0I3MNHdOsHql2oYG8,246
47
+ submine/cli/main.py,sha256=VkdlRALYUzSrgMOL3mBpNsrrfgluajvNKbPsScNL-lQ,2555
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 0.11.6
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-macosx_11_0_arm64
5
+ Generator: delocate 0.13.0
6
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
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.