useragentgen 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.
@@ -0,0 +1,18 @@
1
+ """
2
+ useragentgen
3
+ ============
4
+ Random User-Agent string generator with embedded templates.
5
+
6
+ Quick start
7
+ -----------
8
+ >>> from useragentgen import generate, generate_many, init
9
+ >>> init(42) # optional seed for reproducibility
10
+ >>> generate() # random browser
11
+ >>> generate("firefox") # specific browser
12
+ >>> generate_many(5) # list of 5 random UAs
13
+ """
14
+
15
+ from .generator import generate, generate_many, init
16
+ from .parser import init_parser, parse_line
17
+
18
+ __all__ = ["generate", "generate_many", "init", "parse_line", "init_parser"]
@@ -0,0 +1 @@
1
+ # required for importlib.resources
@@ -0,0 +1,79 @@
1
+ {
2
+ "chrome": {
3
+ "os_exclude": [
4
+ "ios"
5
+ ],
6
+ "engine": "webkit_chrome",
7
+ "version_prefix": "Chrome/",
8
+ "mobile_token": {
9
+ "if": "android",
10
+ "value": "android_chrome"
11
+ },
12
+ "suffix": "Safari/537.36"
13
+ },
14
+ "firefox": {
15
+ "os_allow": "*",
16
+ "engine": "gecko",
17
+ "version_prefix": [
18
+ "Firefox/",
19
+ {
20
+ "prefix": "rv:",
21
+ "os_exclude": [
22
+ "android",
23
+ "ios"
24
+ ]
25
+ }
26
+ ],
27
+ "mobile_token": null,
28
+ "suffix": null
29
+ },
30
+ "safari": {
31
+ "os_allow": [
32
+ "macos",
33
+ "ios",
34
+ "windows"
35
+ ],
36
+ "engine": "webkit_safari",
37
+ "version_prefix": "Version/",
38
+ "mobile_token": {
39
+ "if": "ios",
40
+ "value": "ios_safari"
41
+ },
42
+ "suffix": {
43
+ "method": "_safari_suffix"
44
+ }
45
+ },
46
+ "edge": {
47
+ "os_exclude": [
48
+ "ios"
49
+ ],
50
+ "engine": "webkit_chrome",
51
+ "version_prefix": "Edg/",
52
+ "mobile_token": {
53
+ "if": "android",
54
+ "value": "android_chrome"
55
+ },
56
+ "suffix": {
57
+ "template": "Chrome/{rnd}.0.0.0 Safari/537.36",
58
+ "rnd": [
59
+ 115,
60
+ 134
61
+ ]
62
+ }
63
+ },
64
+ "opera": {
65
+ "os_exclude": [
66
+ "ios"
67
+ ],
68
+ "engine": "webkit_chrome",
69
+ "version_prefix": "OPR/",
70
+ "mobile_token": null,
71
+ "suffix": {
72
+ "template": "Chrome/{rnd}.0.0.0 Safari/537.36",
73
+ "rnd": [
74
+ 115,
75
+ 134
76
+ ]
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,14 @@
1
+ # Engine strings by name
2
+ # Referenced in configs.json as "engine": "name"
3
+
4
+ # Chrome / Edge / Opera
5
+ webkit_chrome = AppleWebKit/537.36 (KHTML, like Gecko)
6
+
7
+ # Safari (desktop & iOS)
8
+ webkit_safari = AppleWebKit/605.1.15 (KHTML, like Gecko)
9
+
10
+ # Firefox (desktop)
11
+ gecko = Gecko/20100101
12
+
13
+ # Firefox (Android variant — version inline, no separate engine token)
14
+ gecko_android = Gecko/{115-122}.0
@@ -0,0 +1,25 @@
1
+ # Browser-OS exclusion rules
2
+ # Format: browser:os_substring_pattern
3
+ # Lines starting with # are ignored
4
+
5
+ # Safari was discontinued on Windows
6
+ safari:Windows NT
7
+
8
+ # Edge and Opera don't exist on iOS
9
+ edge:iPhone
10
+ edge:iPad
11
+ opera:iPhone
12
+ opera:iPad
13
+
14
+ # Firefox on iOS uses WebKit (different UA format)
15
+ # So exclude from our desktop Firefox generator
16
+ firefox:iPhone
17
+ firefox:iPad
18
+
19
+ # Safari on Linux doesn't exist
20
+ safari:Linux
21
+
22
+ # Chrome on iOS uses WebKit (still valid but different)
23
+ # Keep if you want, or uncomment to exclude:
24
+ # chrome:iPhone
25
+ # chrome:iPad
@@ -0,0 +1,13 @@
1
+ # Mobile token strings by name
2
+ # Referenced in configs.json as "mobile_token": {"value": "name"}
3
+
4
+ # iOS Safari (iPhone & iPad)
5
+ ios_safari = Mobile/15E148
6
+
7
+ # Chrome on Android
8
+ android_chrome = Mobile Safari/537.36
9
+
10
+ # Safari fallback tokens (desktop & iOS)
11
+ safari_604 = Safari/604.1
12
+ safari_605 = Safari/605.1.15
13
+ safari_537 = Safari/537.36
@@ -0,0 +1,18 @@
1
+ # Windows
2
+ Windows NT 10.0; Win64; x64
3
+ Windows NT 6.3; Win64; x64
4
+ Windows NT 6.1; Win64; x64
5
+
6
+ # macOS
7
+ Macintosh; Intel Mac OS X {10|11|12|13|14|15}_{0|1|2|3|4|5|6|7}_0
8
+
9
+ # Linux
10
+ X11; {|Ubuntu; |Fedora; |Linux Mint; }Linux {x86_64|i686}
11
+
12
+ # Android
13
+ Linux; Android {11-15}; {Pixel 6|Pixel 7|Pixel 8|Pixel 9|Pixel 10}
14
+ Linux; Android {11-15}; {SM-G991B|SM-G998B|SM-S918B|SM-A546B|SM-F946B}
15
+
16
+ # iOS
17
+ iPhone; CPU iPhone OS {15|16|17|18}_{0|1|2|3|4|5|6} like Mac OS X
18
+ iPad; CPU OS {15|16|17|18}_{0|1|2|3|4|5|6} like Mac OS X
@@ -0,0 +1 @@
1
+ Mozilla/5.0
@@ -0,0 +1,17 @@
1
+ # Chrome
2
+ Chrome/{115-134}.0.{5000-6999}.{0-250}
3
+
4
+ # Firefox
5
+ Firefox/{115-135}.0
6
+
7
+ # Firefox (rv tag, desktop only)
8
+ rv:{115-135}.0
9
+
10
+ # Safari
11
+ Version/{15-18}.{0-6}
12
+
13
+ # Edge (Edg token)
14
+ Edg/{115-134}.0.{1000-2500}.{0-150}
15
+
16
+ # Opera (OPR token)
17
+ OPR/{100-117}.0.{4000-5500}.{0-150}
@@ -0,0 +1,269 @@
1
+ import json
2
+ import random
3
+ from importlib.resources import files
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from . import parser
7
+
8
+ _DATA_PKG = "useragentgen.data"
9
+
10
+ # Module-level state
11
+ _rng = random.Random()
12
+ _data: Dict[str, Any] = {}
13
+ _configs: Dict[str, Any] = {}
14
+ _exclusions: Dict[str, List[str]] = {}
15
+ _engines: Dict[str, str] = {}
16
+ _mobile_tokens: Dict[str, str] = {}
17
+ _initialized = False
18
+
19
+
20
+ def _read_resource(filename: str) -> str:
21
+ return files(_DATA_PKG).joinpath(filename).read_text(encoding="utf-8")
22
+
23
+
24
+ def _parse_single(filename: str) -> str:
25
+ for raw in _read_resource(filename).splitlines():
26
+ line = raw.strip()
27
+ if line and not line.startswith("#"):
28
+ return parser.parse_line(line)
29
+ return ""
30
+
31
+
32
+ def _parse_list(filename: str) -> List[str]:
33
+ results = []
34
+ for raw in _read_resource(filename).splitlines():
35
+ line = raw.strip()
36
+ if not line or line.startswith("#"):
37
+ continue
38
+ results.append(parser.parse_line(line))
39
+ return results
40
+
41
+
42
+ def _parse_lookup(filename: str) -> Dict[str, str]:
43
+ results = {}
44
+ for raw in _read_resource(filename).splitlines():
45
+ line = raw.strip()
46
+ if not line or line.startswith("#"):
47
+ continue
48
+ if "=" not in line:
49
+ continue
50
+ key, value = line.split("=", 1)
51
+ results[key.strip()] = parser.parse_line(value.strip())
52
+ return results
53
+
54
+
55
+ # OS detection helpers (same as before)
56
+ def _is_windows(s: str) -> bool:
57
+ return s.startswith("Windows")
58
+
59
+
60
+ def _is_macos(s: str) -> bool:
61
+ return s.startswith("Macintosh")
62
+
63
+
64
+ def _is_android(s: str) -> bool:
65
+ return "Android" in s and "iPhone" not in s and "iPad" not in s
66
+
67
+
68
+ def _is_ios(s: str) -> bool:
69
+ return "iPhone" in s or "iPad" in s
70
+
71
+
72
+ def _is_excluded(browser: str, os_str: str) -> bool:
73
+ for pattern in _exclusions.get(browser, []):
74
+ if pattern in os_str:
75
+ return True
76
+ return False
77
+
78
+
79
+ def _safari_suffix(os_str: str) -> str:
80
+ if _is_ios(os_str):
81
+ return _rng.choice(["Safari/604.1", "Safari/605.1.15"])
82
+ return "Safari/605.1.15"
83
+
84
+
85
+ # Rule interpreters (same logic, using module _rng)
86
+ def _check_os(rule: Dict[str, Any], os_str: str) -> bool:
87
+ if "os_allow" in rule:
88
+ allowed = rule["os_allow"]
89
+ if allowed == "*":
90
+ return True
91
+ return any(globals()[f"_is_{opt}"](os_str) for opt in allowed)
92
+ if "os_exclude" in rule:
93
+ excluded = rule["os_exclude"]
94
+ return not any(globals()[f"_is_{opt}"](os_str) for opt in excluded)
95
+ return True
96
+
97
+
98
+ def _check_version(rule: Dict[str, Any], version: str, os_str: str) -> bool:
99
+ """Evaluate version_prefix rules, with optional OS conditions."""
100
+ prefixes = rule["version_prefix"]
101
+ if isinstance(prefixes, str):
102
+ prefixes = [prefixes]
103
+
104
+ for p in prefixes:
105
+ if isinstance(p, str):
106
+ # Simple string prefix
107
+ if version.startswith(p):
108
+ return True
109
+ else:
110
+ # Dict with OS conditions: {"prefix": "rv:", "os_exclude": ["android"]}
111
+ if not version.startswith(p["prefix"]):
112
+ continue
113
+
114
+ # Check OS conditions if present
115
+ excluded = p.get("os_exclude", [])
116
+ if any(globals()[f"_is_{opt}"](os_str) for opt in excluded):
117
+ continue
118
+ allowed = p.get("os_allow", [])
119
+ if allowed: # Only check if explicitly defined
120
+ if not any(globals()[f"_is_{opt}"](os_str) for opt in allowed):
121
+ continue
122
+ return True
123
+ else:
124
+ return True # No restrictions means it's allowed
125
+ return False
126
+
127
+
128
+ def _get_mobile_token(rule: Dict[str, Any], os_str: str) -> Optional[str]:
129
+ mobile_cfg = rule.get("mobile_token")
130
+ if not mobile_cfg:
131
+ return None
132
+ condition = mobile_cfg["if"]
133
+ if globals()[f"_is_{condition}"](os_str):
134
+ token_name = mobile_cfg["value"]
135
+ return _mobile_tokens.get(token_name, token_name)
136
+ return None
137
+
138
+
139
+ def _get_suffix(rule: Dict[str, Any], version: str, os_str: str) -> Optional[str]:
140
+ suffix_rule = rule.get("suffix")
141
+ if suffix_rule is None:
142
+ return None
143
+ if isinstance(suffix_rule, str):
144
+ return suffix_rule
145
+ if "method" in suffix_rule:
146
+ return globals()[suffix_rule["method"]](os_str)
147
+ if "template" in suffix_rule:
148
+ template = suffix_rule["template"]
149
+ result = template
150
+ for key, val in suffix_rule.items():
151
+ if key == "template":
152
+ continue
153
+ if isinstance(val, list) and len(val) == 2:
154
+ result = result.replace(f"{{{key}}}", str(_rng.randint(val[0], val[1])))
155
+ return result
156
+ return None
157
+
158
+
159
+ def init(seed: Optional[int] = None) -> None:
160
+ """Initialize or reinitialize the generator with an optional seed."""
161
+ global _initialized, _rng, _data, _configs, _exclusions, _engines, _mobile_tokens
162
+
163
+ _rng = random.Random(seed)
164
+ parser.init_parser(seed) # Sync parser RNG
165
+
166
+ # Core data
167
+ _data = {
168
+ "prefix": _parse_single("prefix.txt"),
169
+ "os_list": _parse_list("os.txt"),
170
+ "versions": _parse_list("version.txt"),
171
+ }
172
+
173
+ # Browser configs from JSON
174
+ _configs = json.loads(_read_resource("configs.json"))
175
+
176
+ # Engine and mobile token lookups
177
+ _engines = _parse_lookup("engine.txt")
178
+ _mobile_tokens = _parse_lookup("mobile_token.txt")
179
+
180
+ # Exclusion rules
181
+ browsers = ("chrome", "firefox", "safari", "edge", "opera")
182
+ _exclusions = {b: [] for b in browsers}
183
+ try:
184
+ content = _read_resource("exclusions.txt")
185
+ for line in content.splitlines():
186
+ line = line.strip()
187
+ if not line or line.startswith("#") or ":" not in line:
188
+ continue
189
+ browser, pattern = line.split(":", 1)
190
+ browser = browser.strip().lower()
191
+ if browser in _exclusions:
192
+ _exclusions[browser].append(pattern.strip())
193
+ except FileNotFoundError:
194
+ pass
195
+
196
+ _initialized = True
197
+
198
+
199
+ def _ensure_init() -> None:
200
+ if not _initialized:
201
+ init()
202
+
203
+
204
+ def generate(browser: Optional[str] = None) -> str:
205
+ """Generate a single User-Agent string."""
206
+ _ensure_init()
207
+
208
+ browsers = ("chrome", "firefox", "safari", "edge", "opera")
209
+ if browser is None:
210
+ browser = _rng.choice(browsers)
211
+ else:
212
+ browser = browser.lower()
213
+
214
+ cfg = _configs.get(browser)
215
+ if not cfg:
216
+ raise ValueError(f"Unknown browser: {browser!r}. Choose from: {browsers}")
217
+
218
+ valid_os = [
219
+ os_str
220
+ for os_str in _data["os_list"]
221
+ if _check_os(cfg, os_str) and not _is_excluded(browser, os_str)
222
+ ]
223
+ if not valid_os:
224
+ raise ValueError(f"No valid OS found for browser: {browser!r}")
225
+ os_str = _rng.choice(valid_os)
226
+
227
+
228
+
229
+ # In generate() function, after selecting OS:
230
+ if browser == "firefox":
231
+ # Firefox needs both rv: (in parens) and Firefox/ (after engine)
232
+ version_num = _rng.randint(115, 135)
233
+ rv_version = f"rv:{version_num}.0"
234
+ fx_version = f"Firefox/{version_num}.0"
235
+
236
+ # Insert rv: into OS parentheses
237
+ os_str = os_str.rstrip(')') + f"; {rv_version})"
238
+ engine = _engines.get(cfg["engine"], cfg["engine"])
239
+ parts = [_data["prefix"], f"({os_str}", engine, fx_version]
240
+ else:
241
+ # valid_versions = [v for v in _data["versions"] if _check_version(cfg, v)]
242
+ valid_versions = [v for v in _data["versions"] if _check_version(cfg, v, os_str)]
243
+
244
+ if not valid_versions:
245
+ raise ValueError(f"No valid version tokens found for browser: {browser!r}")
246
+ version = _rng.choice(valid_versions)
247
+
248
+ parts = [_data["prefix"], f"({os_str})"]
249
+
250
+ if cfg.get("engine"):
251
+ engine = _engines.get(cfg["engine"], cfg["engine"])
252
+ parts.append(engine)
253
+
254
+ mobile = _get_mobile_token(cfg, os_str)
255
+ if mobile:
256
+ parts.append(mobile)
257
+
258
+ parts.append(version)
259
+
260
+ suffix = _get_suffix(cfg, version, os_str)
261
+ if suffix:
262
+ parts.append(suffix)
263
+
264
+ return " ".join(parts)
265
+
266
+
267
+ def generate_many(count: int = 10, browser: Optional[str] = None) -> List[str]:
268
+ """Generate multiple User-Agent strings."""
269
+ return [generate(browser) for _ in range(count)]
useragentgen/parser.py ADDED
@@ -0,0 +1,31 @@
1
+ import re
2
+ import random
3
+ from typing import Optional
4
+
5
+ # Module-level RNG (can be seeded via init_parser)
6
+ _rng: random.Random = random.Random()
7
+
8
+ CHOICE_PATTERN = re.compile(r"\{([^}]+)\}")
9
+ RANGE_PATTERN = re.compile(r"^(\d+)-(\d+)$")
10
+
11
+
12
+ def init_parser(seed: Optional[int] = None) -> None:
13
+ """Initialize or reseed the parser's RNG."""
14
+ global _rng
15
+ _rng = random.Random(seed)
16
+
17
+
18
+ def parse_line(line: str) -> str:
19
+ """Expand all {a|b} and {1-10} patterns in a template string."""
20
+
21
+ def replace_match(match: re.Match) -> str:
22
+ content = match.group(1)
23
+ range_match = RANGE_PATTERN.match(content)
24
+ if range_match:
25
+ start, end = int(range_match.group(1)), int(range_match.group(2))
26
+ return str(_rng.randint(start, end))
27
+ if "|" in content:
28
+ return _rng.choice(content.split("|"))
29
+ return content
30
+
31
+ return CHOICE_PATTERN.sub(replace_match, line)
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: useragentgen
3
+ Version: 1.0
4
+ Summary: Random User-Agent string generator with embedded browser templates
5
+ Project-URL: Homepage, https://github.com/Masrkai/useragentgen
6
+ Project-URL: Repository, https://github.com/Masrkai/useragentgen
7
+ Project-URL: Issues, https://github.com/Masrkai/useragentgen/issues
8
+ Author: Masrkai
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: browser,fake-user-agent,http,scraping,testing,user-agent
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+
26
+ # useragentgen
27
+
28
+ Random User-Agent string generator for Chrome, Firefox, Safari, Edge, and Opera.
29
+ All browser templates are **embedded inside the package** — no external files needed after install.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install useragentgen
35
+ ```
36
+
37
+ ## Quick start
38
+
39
+ ```python
40
+ import useragentgen
41
+
42
+ # Single UA, random browser
43
+ useragentgen.generate()
44
+
45
+ # Single UA, specific browser
46
+ useragentgen.generate("chrome")
47
+ useragentgen.generate("firefox")
48
+ useragentgen.generate("safari")
49
+ useragentgen.generate("edge")
50
+ useragentgen.generate("opera")
51
+
52
+ # Batch generation
53
+ useragentgen.generate_many(10) # 10 random-browser UAs
54
+ useragentgen.generate_many(5, "chrome") # 5 Chrome UAs
55
+ ```
56
+
57
+ Reproducible output with a seed:
58
+
59
+ ```python
60
+ import useragentgen
61
+
62
+ useragentgen.init(42)
63
+ print(useragentgen.generate("chrome")) # same result every run
64
+ ```
65
+
66
+ ## Supported browsers
67
+
68
+ | Key | Covers |
69
+ |------------|-------------------------------|
70
+ | `chrome` | Windows, macOS, Linux, Android |
71
+ | `firefox` | All platforms |
72
+ | `safari` | macOS, iOS, Windows |
73
+ | `edge` | Windows, macOS, Linux, Android |
74
+ | `opera` | Windows, macOS, Linux |
75
+
76
+ ## License
77
+
78
+ [MIT License Copyright (c) 2026 Masrkai, rights reserved](LICENSE)
@@ -0,0 +1,15 @@
1
+ useragentgen/__init__.py,sha256=aicxXG3_DYmzg4k66nxRj9NU0lT4W6yC-KmpY5CvLO0,576
2
+ useragentgen/generator.py,sha256=9HuC7jlSjbyq3gD5v9scm7m_h0nDenPhb5Qur8Wa75g,8318
3
+ useragentgen/parser.py,sha256=2Krk84WtbnYKUjPdXyRIzmKBxXjVgB-WH5GnHIiT1ek,924
4
+ useragentgen/data/__init__.py,sha256=-5BSKlwMjdZiBFTzS0QHwhgyVOj3cY-tlVsBMLNh5-k,35
5
+ useragentgen/data/configs.json,sha256=WGQPyP16diyvbYpYBWyDNz487AAfxb5GlW2t3eFVvk0,1386
6
+ useragentgen/data/engine.txt,sha256=fpcs8FGxD5BGdp0-qiTMjVepOwmYwvrWZdn75xvMXho,389
7
+ useragentgen/data/exclusions.txt,sha256=P_fnrDhGQAnpf7BUyVoM7yKhd6NTL25Xe8CP5hErTrg,548
8
+ useragentgen/data/mobile_token.txt,sha256=8Jhzf_bDCs9yHNR_4PkGlljW_UO2Us7Pe5gY0mxH0qI,337
9
+ useragentgen/data/os.txt,sha256=pj9Z4TJkrM3FJBxA0UbQbyCB0r8BSVwEtyVT8tJ2zG8,512
10
+ useragentgen/data/prefix.txt,sha256=EGa0giS7GIzrlVYF9Pz_mIk74miNfpZa-wTTbRfn8Nc,11
11
+ useragentgen/data/version.txt,sha256=LmbFPuLJcDc-DBDkE03Cm0NcHRXaLv2zA92DTZrL6c0,272
12
+ useragentgen-1.0.dist-info/METADATA,sha256=rRVxxLTzHXglCdq6n4fX30o72nbacIeJ8xKH4__v0nE,2349
13
+ useragentgen-1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
14
+ useragentgen-1.0.dist-info/licenses/LICENSE,sha256=14L0qu87Blgdz8aKrx_U6Sw4XML5eAy3_9DdTng7lQY,1066
15
+ useragentgen-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 [Masrkai]
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.