pcd-cli 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.
pcd_cli/scanner.py ADDED
@@ -0,0 +1,116 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ from pcd_cli.filesystem import canonical_path
7
+ from pcd_cli.models import Project, ProjectSource
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Iterator
11
+
12
+ from pcd_cli.models import ProjectSettings
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ProjectScanner:
17
+ settings: ProjectSettings
18
+
19
+ def scan(self) -> Iterator[Project]:
20
+ for root, display_path in self._scan_roots():
21
+ yield from self._walk(root, display_path)
22
+
23
+ def _scan_roots(self) -> list[tuple[Path, Path]]:
24
+ roots = sorted(
25
+ ((canonical_path(root), root.expanduser()) for root in self.settings.roots),
26
+ key=lambda item: (len(item[0].parts), str(item[0])),
27
+ )
28
+ selected: dict[Path, Path] = {}
29
+
30
+ for root, display_path in roots:
31
+ parent = next((item for item in root.parents if item in selected), None)
32
+ if parent is not None and self._reachable_from_parent(root, parent):
33
+ continue
34
+ selected[root] = display_path
35
+
36
+ return list(selected.items())
37
+
38
+ def _reachable_from_parent(self, root: Path, parent: Path) -> bool:
39
+ relative = root.relative_to(parent)
40
+ current = parent
41
+
42
+ for part in relative.parts:
43
+ blocked = part in self.settings.excluded_names
44
+ hidden = not self.settings.include_hidden and part.startswith(".")
45
+ if blocked or hidden or _is_git_repository(current):
46
+ return False
47
+ current /= part
48
+
49
+ return True
50
+
51
+ def _walk(self, root: Path, display_path: Path) -> Iterator[Project]:
52
+ if not root.is_dir():
53
+ return
54
+
55
+ visited = set[tuple[int, int]]() if self.settings.follow_symlinks else None
56
+
57
+ for directory, directories, _files in os.walk(
58
+ root,
59
+ topdown=True,
60
+ followlinks=self.settings.follow_symlinks,
61
+ ):
62
+ current = Path(directory)
63
+ active = True if visited is None else _mark_visited(current, visited)
64
+ if not active:
65
+ directories.clear()
66
+ continue
67
+
68
+ relative = current.relative_to(root)
69
+ shown = display_path / relative
70
+
71
+ if _is_git_repository(current):
72
+ directories.clear()
73
+ path = canonical_path(current) if self.settings.follow_symlinks else current
74
+ yield Project(current.name, path, shown, ProjectSource.DISCOVERED)
75
+ continue
76
+
77
+ descend_into: list[str] = []
78
+ for name in directories:
79
+ child = current / name
80
+ blocked = name == ".git" or name in self.settings.excluded_names
81
+ hidden = not self.settings.include_hidden and name.startswith(".")
82
+ linked = not self.settings.follow_symlinks and child.is_symlink()
83
+
84
+ if blocked or hidden:
85
+ continue
86
+ if linked:
87
+ if _is_git_repository(child):
88
+ yield Project(
89
+ name,
90
+ canonical_path(child),
91
+ shown / name,
92
+ ProjectSource.DISCOVERED,
93
+ )
94
+ continue
95
+ descend_into.append(name)
96
+
97
+ # os.walk descends only into names left in directories when topdown=True.
98
+ directories[:] = descend_into
99
+
100
+
101
+ def _mark_visited(path: Path, visited: set[tuple[int, int]]) -> bool:
102
+ try:
103
+ stat = path.stat()
104
+ except OSError:
105
+ return False
106
+
107
+ key = (stat.st_dev, stat.st_ino)
108
+ if key in visited:
109
+ return False
110
+
111
+ visited.add(key)
112
+ return True
113
+
114
+
115
+ def _is_git_repository(path: Path) -> bool:
116
+ return (path / ".git").exists()
pcd_cli/search.py ADDED
@@ -0,0 +1,108 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from collections.abc import Iterable, Mapping
5
+ from pathlib import Path
6
+
7
+ from pcd_cli.models import Project, ProjectUsage
8
+
9
+ ProjectRank = tuple[int, int, float, int, int, str, str]
10
+
11
+ EXACT_WEIGHT = 16
12
+ PREFIX_WEIGHT = 12
13
+ SUBSTRING_WEIGHT = 10
14
+ MATCH_POINTS = 4
15
+ BOUNDARY_BONUS = 5
16
+ ADJACENT_BONUS = 3
17
+ MAX_GAP_PENALTY = 4
18
+
19
+
20
+ def rank_matches(
21
+ projects: Iterable[Project],
22
+ query: str,
23
+ usage: Mapping[Path, ProjectUsage] | None = None,
24
+ ) -> list[Project]:
25
+ needle = query.casefold()
26
+ matches: list[tuple[ProjectRank, Project]] = []
27
+
28
+ for project in projects:
29
+ score = _score(needle, project.name.casefold())
30
+ if score is None:
31
+ continue
32
+
33
+ match_kind, points = score
34
+ project_usage = None if usage is None else usage.get(project.path)
35
+ used_at = 0.0 if project_usage is None else project_usage.used_at
36
+ count = 0 if project_usage is None else project_usage.count
37
+ rank: ProjectRank = (
38
+ match_kind,
39
+ -points,
40
+ -used_at,
41
+ -count,
42
+ len(project.name),
43
+ project.name.casefold(),
44
+ str(project.path),
45
+ )
46
+ matches.append((rank, project))
47
+
48
+ matches.sort(key=lambda item: item[0])
49
+ return [project for _, project in matches]
50
+
51
+
52
+ def rank_recent(
53
+ projects: Iterable[Project],
54
+ usage: Mapping[Path, ProjectUsage],
55
+ ) -> list[Project]:
56
+ ranked_projects = list(projects)
57
+
58
+ def usage_rank(project: Project) -> tuple[float, int, str, str]:
59
+ project_usage = usage.get(project.path)
60
+ return (
61
+ 0.0 if project_usage is None else -project_usage.used_at,
62
+ 0 if project_usage is None else -project_usage.count,
63
+ project.name.casefold(),
64
+ str(project.path),
65
+ )
66
+
67
+ ranked_projects.sort(key=usage_rank)
68
+ return ranked_projects
69
+
70
+
71
+ def _score(query: str, name: str) -> tuple[int, int] | None:
72
+ if name == query:
73
+ return 0, len(query) * EXACT_WEIGHT
74
+ if name.startswith(query):
75
+ return 1, len(query) * PREFIX_WEIGHT - (len(name) - len(query))
76
+
77
+ position = name.find(query)
78
+ if position >= 0:
79
+ return 2, len(query) * SUBSTRING_WEIGHT - position
80
+
81
+ query_index = 0
82
+ previous = -2
83
+ points = 0
84
+
85
+ for index, char in enumerate(name):
86
+ if query_index == len(query):
87
+ break
88
+ if char != query[query_index]:
89
+ continue
90
+
91
+ points += MATCH_POINTS
92
+ points += BOUNDARY_BONUS if index == 0 or name[index - 1] in "-_./ " else 0
93
+
94
+ if previous >= 0:
95
+ points += (
96
+ ADJACENT_BONUS
97
+ if index == previous + 1
98
+ else -min(index - previous - 1, MAX_GAP_PENALTY)
99
+ )
100
+
101
+ previous = index
102
+ query_index += 1
103
+
104
+ if query_index != len(query):
105
+ return None
106
+
107
+ penalty = max(len(name) - len(query), 0) // 2
108
+ return 3, points - penalty
@@ -0,0 +1,264 @@
1
+ """Generate, install, and inspect shell integration for pcd."""
2
+
3
+ import os
4
+ import stat
5
+ from dataclasses import dataclass
6
+ from enum import StrEnum
7
+ from pathlib import Path
8
+ from textwrap import dedent
9
+ from typing import Self
10
+
11
+ from pcd_cli.filesystem import atomic_write, file_lock
12
+
13
+ SHELL_MODE_ENV = "PCD_SHELL"
14
+ # Internal success status: stdout contains the destination path when this is returned.
15
+ SHELL_CD_EXIT_CODE = 10
16
+ _MANAGED_BLOCK_START = "# >>> pcd shell integration >>>"
17
+ _MANAGED_BLOCK_END = "# <<< pcd shell integration <<<"
18
+
19
+
20
+ class ShellIntegrationError(Exception):
21
+ """Persistent shell integration cannot be inspected or managed safely."""
22
+
23
+
24
+ class Shell(StrEnum):
25
+ BASH = "bash"
26
+ ZSH = "zsh"
27
+ FISH = "fish"
28
+
29
+
30
+ class ShellIntegrationState(StrEnum):
31
+ ABSENT = "not installed"
32
+ MANUAL = "configured manually"
33
+ MANAGED = "installed by pcd"
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class ShellIntegration:
38
+ shell: Shell
39
+ config_path: Path
40
+
41
+ @classmethod
42
+ def detect(cls) -> Self:
43
+ return cls.for_shell(detect_shell())
44
+
45
+ @classmethod
46
+ def for_shell(cls, shell: Shell) -> Self:
47
+ return cls(shell=shell, config_path=shell_config_path(shell))
48
+
49
+ def state(self) -> ShellIntegrationState:
50
+ return _integration_state(self._read(), self.shell)
51
+
52
+ def install(self) -> bool:
53
+ """Install the managed block. Return whether the config changed."""
54
+ with file_lock(self.config_path):
55
+ content = self._read()
56
+ if _integration_state(content, self.shell) is not ShellIntegrationState.ABSENT:
57
+ return False
58
+
59
+ separator = "" if not content or content.endswith("\n") else "\n"
60
+ self._write(f"{content}{separator}{render_managed_block(self.shell)}")
61
+ return True
62
+
63
+ def uninstall(self) -> bool:
64
+ """Remove only a pcd-managed block, leaving manual setup untouched."""
65
+ with file_lock(self.config_path):
66
+ content = self._read()
67
+ bounds = _managed_block_bounds(content)
68
+ if bounds is None:
69
+ return False
70
+
71
+ start, end = bounds
72
+ self._write(f"{content[:start]}{content[end:]}")
73
+ return True
74
+
75
+ def _read(self) -> str:
76
+ try:
77
+ return self.config_path.read_text(encoding="utf-8")
78
+ except FileNotFoundError:
79
+ return ""
80
+ except UnicodeError as exc:
81
+ raise ShellIntegrationError(
82
+ f"Shell config is not valid UTF-8: {self.config_path}"
83
+ ) from exc
84
+
85
+ def _write(self, content: str) -> None:
86
+ target = (
87
+ self.config_path.resolve(strict=False)
88
+ if self.config_path.is_symlink()
89
+ else self.config_path
90
+ )
91
+ mode = stat.S_IMODE(target.stat().st_mode) if target.exists() else None
92
+
93
+ with atomic_write(target) as stream:
94
+ stream.write(content)
95
+
96
+ if mode is not None:
97
+ target.chmod(mode)
98
+
99
+
100
+ def detect_shell() -> Shell:
101
+ """Detect the user's login shell from the standard SHELL environment variable."""
102
+ executable = os.environ.get("SHELL", "")
103
+ name = Path(executable).name.casefold()
104
+ try:
105
+ return Shell(name)
106
+ except ValueError as exc:
107
+ supported = ", ".join(shell.value for shell in Shell)
108
+ raise ShellIntegrationError(
109
+ f"Cannot detect a supported shell from SHELL={executable!r}; choose one of: {supported}"
110
+ ) from exc
111
+
112
+
113
+ def shell_config_path(shell: Shell) -> Path:
114
+ """Return the startup file where persistent integration should be installed."""
115
+ home = Path.home()
116
+ if shell is Shell.BASH:
117
+ return home / ".bashrc"
118
+ if shell is Shell.ZSH:
119
+ zdotdir = os.environ.get("ZDOTDIR")
120
+ return (Path(zdotdir).expanduser() if zdotdir else home) / ".zshrc"
121
+
122
+ xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
123
+ config_home = Path(xdg_config_home).expanduser() if xdg_config_home else home / ".config"
124
+ return config_home / "fish" / "config.fish"
125
+
126
+
127
+ def shell_integration_active() -> bool:
128
+ return os.environ.get(SHELL_MODE_ENV) == "1"
129
+
130
+
131
+ def inactive_shell_message() -> str:
132
+ """Explain how to activate directory changes when the wrapper is not running."""
133
+ try:
134
+ integration = ShellIntegration.detect()
135
+ state = integration.state()
136
+ except OSError, ShellIntegrationError:
137
+ return (
138
+ "Shell integration is not active, so pcd cannot change this shell's directory. "
139
+ "If you have not configured it manually, run: pcd shell install"
140
+ )
141
+
142
+ if state is ShellIntegrationState.ABSENT:
143
+ return (
144
+ "Shell integration is not installed, so pcd cannot change this shell's directory. "
145
+ "Install it with: pcd shell install"
146
+ )
147
+ return (
148
+ f"Shell integration is configured in {integration.config_path} but is not active in "
149
+ f"this shell. Restart it or run: exec {integration.shell.value}"
150
+ )
151
+
152
+
153
+ def render_managed_block(shell: Shell) -> str:
154
+ """Render the small persistent block written into the shell startup file."""
155
+ command = (
156
+ f'eval "$(command pcd shell print {shell.value})"'
157
+ if shell is not Shell.FISH
158
+ else f"command pcd shell print {shell.value} | source"
159
+ )
160
+ return f"{_MANAGED_BLOCK_START}\n{command}\n{_MANAGED_BLOCK_END}\n"
161
+
162
+
163
+ def render_shell_integration(shell: Shell) -> str:
164
+ """Generate shell integration for directory changes and Click completion."""
165
+ return _fish() if shell is Shell.FISH else _posix(shell)
166
+
167
+
168
+ def _integration_state(content: str, shell: Shell) -> ShellIntegrationState:
169
+ bounds = _managed_block_bounds(content)
170
+ if bounds is not None:
171
+ return ShellIntegrationState.MANAGED
172
+
173
+ legacy = f"pcd shell-init {shell.value}"
174
+ current = f"pcd shell print {shell.value}"
175
+ if legacy in content or current in content:
176
+ return ShellIntegrationState.MANUAL
177
+ return ShellIntegrationState.ABSENT
178
+
179
+
180
+ def _managed_block_bounds(content: str) -> tuple[int, int] | None:
181
+ starts = content.count(_MANAGED_BLOCK_START)
182
+ ends = content.count(_MANAGED_BLOCK_END)
183
+ if starts == 0 and ends == 0:
184
+ return None
185
+ if starts != 1 or ends != 1:
186
+ raise ShellIntegrationError(
187
+ "Shell config contains an invalid pcd-managed integration block"
188
+ )
189
+
190
+ start = content.index(_MANAGED_BLOCK_START)
191
+ end_marker = content.index(_MANAGED_BLOCK_END)
192
+ if end_marker < start:
193
+ raise ShellIntegrationError(
194
+ "Shell config contains an invalid pcd-managed integration block"
195
+ )
196
+
197
+ end = end_marker + len(_MANAGED_BLOCK_END)
198
+ if end < len(content) and content[end] == "\n":
199
+ end += 1
200
+ return start, end
201
+
202
+
203
+ def _posix(shell: Shell) -> str:
204
+ completion = "bash_source" if shell is Shell.BASH else "zsh_source"
205
+ return dedent(
206
+ f"""\
207
+ pcd() {{
208
+ if [ -n "${{_PCD_COMPLETE:-}}" ]; then
209
+ command pcd "$@"
210
+ return $?
211
+ fi
212
+
213
+ local output code
214
+ if output="$({SHELL_MODE_ENV}=1 command pcd "$@")"; then
215
+ code=0
216
+ else
217
+ code=$?
218
+ fi
219
+
220
+ if [ "$code" -eq {SHELL_CD_EXIT_CODE} ]; then
221
+ builtin cd -- "$output"
222
+ return $?
223
+ fi
224
+ if [ "$code" -ne 0 ]; then
225
+ return "$code"
226
+ fi
227
+ if [ -n "$output" ]; then
228
+ printf '%s\n' "$output"
229
+ fi
230
+ }}
231
+
232
+ eval "$(_PCD_COMPLETE={completion} command pcd)"
233
+ """
234
+ )
235
+
236
+
237
+ def _fish() -> str:
238
+ return dedent(
239
+ f"""\
240
+ function pcd
241
+ if set -q _PCD_COMPLETE
242
+ command pcd $argv
243
+ return $status
244
+ end
245
+
246
+ set -l output (env {SHELL_MODE_ENV}=1 command pcd $argv)
247
+ set -l code $status
248
+ if test $code -eq {SHELL_CD_EXIT_CODE}
249
+ cd -- "$output"
250
+ return $status
251
+ end
252
+ if test $code -ne 0
253
+ return $code
254
+ end
255
+ if test -n "$output"
256
+ printf '%s\n' "$output"
257
+ end
258
+ end
259
+
260
+ set -lx _PCD_COMPLETE fish_source
261
+ command pcd | source
262
+ set -e _PCD_COMPLETE
263
+ """
264
+ )
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: pcd-cli
3
+ Version: 0.1.0
4
+ Summary: Fast project navigator for Bash, Zsh and Fish
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: cli,cd,projects,navigation,bash,zsh,fish
8
+ Author: pcd-cli contributors
9
+ Requires-Python: >=3.14,<4
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Utilities
17
+ Requires-Dist: click (>=8.4.2,<9)
18
+ Requires-Dist: platformdirs (>=4.10,<5)
19
+ Requires-Dist: prompt-toolkit (>=3.0.53,<4)
20
+ Requires-Dist: tomlkit (>=0.15.1,<1)
21
+ Description-Content-Type: text/plain
22
+
23
+ Project cd
24
+ ==========
25
+
26
+ pcd (project cd) - Is a fast way to jump between projects from your terminal
27
+
28
+ Instead of:
29
+ cd ~/development/github/palantir/backend/pigeon-surveillance
30
+ Just:
31
+ pcd pigeon-surveillance
32
+
33
+ Supported shells:
34
+ * Bash
35
+ * Zsh
36
+ * Fish
37
+
38
+
39
+ INSTALL
40
+ -------
41
+ pipx install --python python3.14 git+https://github.com/jestenough/pcd.git
42
+ pcd shell install
43
+ Restart your shell after installing shell integration.
44
+
45
+
46
+ QUICK START
47
+ ----------
48
+ cd <your root folder, e.g. ~/Github/>
49
+ pcd init
50
+ pcd <project name from your root folder>
51
+ pcd discovers projects inside registered scan roots.
52
+
53
+
54
+ COMMANDS
55
+ --------
56
+
57
+ pcd <project> Jump to a project
58
+ pcd Show help
59
+
60
+ pcd add <path> Add a project manually
61
+ pcd remove <project> Remove a manual project
62
+
63
+ pcd init Add the current directory as a scan root
64
+ pcd uninit Remove the current directory from scan roots
65
+ pcd roots List scan roots
66
+ pcd refresh Rescan roots and rebuild the project cache
67
+
68
+ pcd list List all known projects
69
+ pcd config-path Print the configuration file path
70
+
71
+ pcd shell install Install shell integration
72
+ pcd shell status Show shell integration status
73
+ pcd shell uninstall Remove shell integration
74
+
75
+ pcd --project <project> Jump to a project whose name matches a command
76
+ pcd --version Show version
77
+ pcd --help Show help
78
+
79
+ * Projects inside registered roots are discovered automatically.
80
+ * You can register multiple scan roots.
81
+
82
+
83
+ DEVELOPMENT
84
+ -----------
85
+ make install
86
+ make check
87
+ make build
88
+ See ./dist folder
89
+ You can install *.whl with your python package manager
90
+
91
+
92
+ SEE ALSO
93
+ --------
94
+ Features:
95
+ FEATURES.txt
96
+
97
+ Planned changes:
98
+ ROADMAP.md
99
+
100
+ Changelog:
101
+ CHANGELOG.md
102
+
103
+ License:
104
+ MIT
105
+
@@ -0,0 +1,18 @@
1
+ pcd_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pcd_cli/cache.py,sha256=tVXS4yNc5vKHwjfnpro39Fm9cSQg3qTVHYX4UHfQKo8,3655
3
+ pcd_cli/catalog.py,sha256=kGnR1hOkMmDS_N1AX5PP0PD3NJXCZuffuD2ryFZOg6c,4841
4
+ pcd_cli/cli.py,sha256=91Px-y00OC-K7-6dUKnzXCRlzjVhBgJSXRNspdUJYV8,11469
5
+ pcd_cli/config.py,sha256=U1kT6ehbklI6-YhQjWqmQWftDaskqp9MjVqtI5OmrVo,7519
6
+ pcd_cli/filesystem.py,sha256=FcUJKRm0y9ujd4vB5vRgahcIOJFt5sVTmgpVMywuZfI,2268
7
+ pcd_cli/history.py,sha256=phlefu3isprF-Xo95DRdjPRVAbspI8vZf19C3Ai1IHc,2825
8
+ pcd_cli/models.py,sha256=7oKtD_c3PEEbpXFVABCf62OqMBzvSiaHI5ZTBtm_XBQ,731
9
+ pcd_cli/navigation.py,sha256=Z9zXpMzohDxIGCwoMNAhmZaSDUAB7srIQmozDe70Ysc,2523
10
+ pcd_cli/picker.py,sha256=Jbja0SAH6H6G0nWVsDooXckdlqpA4KjNxMPKcTSYuLw,5163
11
+ pcd_cli/scanner.py,sha256=2a0tgEg-D-sx7q5OsK7yRCSlhssaQXlICmyFaTWhPAw,3869
12
+ pcd_cli/search.py,sha256=cTI7qWc35Y3vTOk9nPVzb1wzikd0fBjUUHMiF4nzWAA,2976
13
+ pcd_cli/shell_integration.py,sha256=1i_-efl4kIqh8YY5hRdLCk55T469Wb9ZGTLVM7NaJTQ,8321
14
+ pcd_cli-0.1.0.dist-info/METADATA,sha256=Oh7vjV2RDsKXDlPT4QbqQai_RbrUlvfnzrDZb3zNnHg,2752
15
+ pcd_cli-0.1.0.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
16
+ pcd_cli-0.1.0.dist-info/entry_points.txt,sha256=3ZMnsucwvrxf64c1yhIJnXmB_Xxz-4ICL74ibZdpjSU,40
17
+ pcd_cli-0.1.0.dist-info/licenses/LICENSE,sha256=X2MjaqUh65CgyVR4rWS1nWM82z1Z6FLV7R8bBaH6590,1077
18
+ pcd_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pcd=pcd_cli.cli:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pcd-cli contributors
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.