copier-project 0.28.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,9 @@
1
+ """Copier template for software projects with modern tooling."""
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as import_version
5
+
6
+ try:
7
+ version = import_version(__name__)
8
+ except PackageNotFoundError: # pragma: no cover
9
+ version = "0.0.0"
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import TYPE_CHECKING, Annotated
8
+
9
+ from rich import print # noqa: A004
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+ from typer import Argument, Context, Exit, Option, Typer
15
+
16
+ from .repo import RepoTarget
17
+ from .update import UpdateAction
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Sequence
21
+
22
+
23
+ console = Console()
24
+
25
+
26
+ class Args:
27
+ Repos = Annotated[
28
+ list[str],
29
+ Argument(
30
+ help=(
31
+ "Repositories to update. Accepts gh:user/repo"
32
+ " or github.com/user/repo."
33
+ )
34
+ ),
35
+ ]
36
+ DryRun = Annotated[
37
+ bool, Option("--dry-run", "-n", help="Skip push and PR creation.")
38
+ ]
39
+
40
+
41
+ cli = Typer(
42
+ help="copier-project utilities",
43
+ add_completion=False,
44
+ no_args_is_help=True,
45
+ pretty_exceptions_enable=False,
46
+ )
47
+
48
+
49
+ @cli.callback()
50
+ def setup(ctx: Context) -> None:
51
+ pass
52
+
53
+
54
+ class UpdateStatus(Enum):
55
+ UPDATED = "bold green"
56
+ CURRENT = "bold blue"
57
+ FAILED = "bold red"
58
+
59
+ @property
60
+ def formatted_name(self) -> Text:
61
+ return Text(f"{self.name:<7}", style=self.value)
62
+
63
+
64
+ @dataclass
65
+ class UpdateResult:
66
+ repo: RepoTarget
67
+ status: UpdateStatus
68
+ exception: Exception | None = None
69
+ pr_url: str | None = None
70
+
71
+
72
+ @cli.command()
73
+ def update(
74
+ repos: Args.Repos,
75
+ *,
76
+ vcs_ref: Annotated[
77
+ str | None, Option("--vcs-ref", "-r", help="Template ref to update to")
78
+ ] = None,
79
+ dry_run: Args.DryRun = False,
80
+ branch: Annotated[str, Option(help="Branch name to create.")] = "updates",
81
+ ) -> None:
82
+ """Apply copier-project template updates to downstream repositories."""
83
+ results = []
84
+
85
+ repo_targets = {(target := RepoTarget(repo)).url: target for repo in repos}
86
+ for target in repo_targets.values():
87
+ try:
88
+ pr_url = UpdateAction(
89
+ target, branch=branch, vcs_ref=vcs_ref, dry_run=dry_run
90
+ )()
91
+ if pr_url:
92
+ results.append(
93
+ UpdateResult(target, UpdateStatus.UPDATED, pr_url=pr_url)
94
+ )
95
+ else:
96
+ results.append(UpdateResult(target, UpdateStatus.CURRENT))
97
+ except Exception as exc: # noqa: BLE001
98
+ console.print_exception()
99
+ results.append(
100
+ UpdateResult(target, status=UpdateStatus.FAILED, exception=exc)
101
+ )
102
+
103
+ _print_summary(results, dry_run=dry_run)
104
+ if any(r.status == UpdateStatus.FAILED for r in results):
105
+ raise Exit(1)
106
+
107
+
108
+ def _print_summary(results: Sequence[UpdateResult], *, dry_run: bool) -> None:
109
+ if not results:
110
+ return
111
+ grid = Table.grid(padding=(0, 1), expand=True)
112
+ grid.add_column()
113
+ grid.add_column()
114
+ grid.add_column()
115
+ for result in sorted(results, key=lambda r: r.repo.github_repo):
116
+ grid.add_row(
117
+ result.status.formatted_name,
118
+ Text.from_markup(
119
+ f"[link={result.repo.url}]{result.repo.github_repo}[/link]",
120
+ style="bold",
121
+ ),
122
+ Text(str(result.pr_url or result.exception)),
123
+ )
124
+ title = Text("Update Results", style="bold")
125
+ if dry_run:
126
+ title += Text(" (dry run)", style="green")
127
+ print(
128
+ Panel(grid, title=title, title_align="left", expand=False, padding=1)
129
+ )
130
+
131
+
132
+ def setup_env() -> None:
133
+ os.environ.pop("VIRTUAL_ENV", default=None)
134
+ os.environ["TERMINAL_WIDTH"] = str(
135
+ min(shutil.get_terminal_size().columns, 100)
136
+ )
137
+
138
+
139
+ def main() -> None:
140
+ setup_env()
141
+ cli()
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
File without changes
copier_project/repo.py ADDED
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import tempfile
7
+ from contextlib import contextmanager, suppress
8
+ from dataclasses import InitVar, dataclass, field
9
+ from functools import cached_property
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any, ClassVar, Self
12
+
13
+ import yaml
14
+ from rich import print # noqa: A004
15
+ from rich.panel import Panel
16
+ from rich.text import Text
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Generator, Sequence
20
+
21
+
22
+ @dataclass(unsafe_hash=True)
23
+ class RepoTarget:
24
+ arg: InitVar[str | Path]
25
+ github_repo: str = field(init=False)
26
+
27
+ _GITHUB_REGEXES: ClassVar[Sequence[str]] = [
28
+ r"(\.git)?/?$",
29
+ r"^gh\:",
30
+ r"^(https?://)?github.com/",
31
+ r"^((git\+)?ssh://)?([a-zA-Z0-9]+)@github.com:",
32
+ ]
33
+
34
+ def __post_init__(self, arg: str | Path) -> None:
35
+ arg = str(arg).strip()
36
+ if ((path := Path(arg)).is_dir()) and (root := self.repo_root(path)):
37
+ arg = root
38
+ for regex in self._GITHUB_REGEXES:
39
+ arg = re.sub(regex, "", arg)
40
+ if not re.fullmatch(r"[\w-]+\/[\w-]+", arg):
41
+ raise ValueError(arg)
42
+ self.github_repo = arg
43
+
44
+ def repo_root(self, path: Path) -> str | None:
45
+ with suppress(subprocess.CalledProcessError):
46
+ return subprocess.check_output(
47
+ ["git", "config", "--local", "remote.origin.url"],
48
+ text=True,
49
+ cwd=path,
50
+ ).strip()
51
+ return None
52
+
53
+ @cached_property
54
+ def name(self) -> str:
55
+ return self.github_repo.split("/")[-1]
56
+
57
+ @property
58
+ def url(self) -> str:
59
+ return f"https://github.com/{self.github_repo}"
60
+
61
+ @cached_property
62
+ def push_url(self) -> str:
63
+ return f"git@github.com:{self.github_repo}.git"
64
+
65
+
66
+ @dataclass
67
+ class RepoWorktree:
68
+ path: Path
69
+ repo: RepoTarget
70
+ branch: str
71
+
72
+ @classmethod
73
+ @contextmanager
74
+ def clone(
75
+ cls, repo: RepoTarget, branch: str
76
+ ) -> Generator[Self, None, None]:
77
+ with tempfile.TemporaryDirectory() as td:
78
+ repo_dir = Path(td) / "worktree"
79
+ cls.run_in(["git", "clone", repo.url, str(repo_dir)], repo=repo)
80
+ cls.run_in(
81
+ [
82
+ *["git", "remote", "set-url", "--push", "origin"],
83
+ repo.push_url,
84
+ ],
85
+ repo=repo,
86
+ cwd=repo_dir,
87
+ )
88
+ cls.run_in(
89
+ (
90
+ ["mise", "install"]
91
+ if (repo_dir / "mise.toml").is_file()
92
+ else ["poe", "setup"]
93
+ ),
94
+ repo=repo,
95
+ cwd=repo_dir,
96
+ )
97
+ cls.run_in(
98
+ ["git", "checkout", "-b", branch], repo=repo, cwd=repo_dir
99
+ )
100
+ yield cls(path=repo_dir, repo=repo, branch=branch)
101
+
102
+ @classmethod
103
+ def run_in(
104
+ cls, cmd: list[str], *, repo: RepoTarget, **kwargs: Any
105
+ ) -> subprocess.CompletedProcess[str]:
106
+ kwargs.setdefault("check", True)
107
+ kwargs.setdefault("text", True)
108
+ cmd_text = Text(" ".join(cmd), style="color(153)")
109
+ txt = Text.assemble(
110
+ Text(f"{repo.github_repo} => ", style="bold"),
111
+ cmd_text,
112
+ )
113
+ print(Panel(txt, expand=False, border_style="white dim"))
114
+ return subprocess.run(cmd, **kwargs) # noqa: S603 PLW1510
115
+
116
+ def run(
117
+ self, cmd: list[str], **kwargs: Any
118
+ ) -> subprocess.CompletedProcess[str]:
119
+ kwargs.setdefault("cwd", self.path)
120
+ return self.run_in(cmd, repo=self.repo, **kwargs)
121
+
122
+ def run_task(
123
+ self, name: str, **kwargs: Any
124
+ ) -> subprocess.CompletedProcess[str]:
125
+ task_runner = (
126
+ ["mise", "run"]
127
+ if (self.path / "mise.toml").is_file()
128
+ else ["uv", "run", "poe"]
129
+ )
130
+ return self.run([*task_runner, name], **kwargs)
131
+
132
+ def git_status(self) -> list[str]:
133
+ return self.run(
134
+ ["git", "status", "--porcelain"], capture_output=True
135
+ ).stdout.splitlines()
136
+
137
+ @staticmethod
138
+ def has_conflicts(status: list[str]) -> bool:
139
+ conflict_codes = {"UU", "AA", "DD", "AU", "UA", "DU", "UD"}
140
+ return any(line[:2] in conflict_codes for line in status)
141
+
142
+ @property
143
+ def template_ref(self) -> str:
144
+ return yaml.safe_load( # type: ignore[no-any-return]
145
+ (self.path / ".copier-answers.yml").read_text()
146
+ ).get("_commit")
147
+
148
+ def shell(self) -> None:
149
+ self.run([os.environ.get("SHELL", "/bin/bash")])
150
+
151
+ def open_pr(self, title: str, body: str) -> str:
152
+ result = self.run(
153
+ [
154
+ "gh",
155
+ "pr",
156
+ "create",
157
+ "--title",
158
+ title,
159
+ "--body",
160
+ body,
161
+ "--head",
162
+ self.branch,
163
+ ],
164
+ capture_output=True,
165
+ check=False,
166
+ )
167
+ if result.returncode == 0:
168
+ return result.stdout.strip()
169
+ view = self.run(
170
+ ["gh", "pr", "view", "--json", "url", "--jq", ".url"],
171
+ capture_output=True,
172
+ check=False,
173
+ )
174
+ if view.returncode == 0:
175
+ return view.stdout.strip()
176
+ raise RuntimeError(f"gh pr create failed: {result.stderr.strip()}")
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING
8
+
9
+ from .repo import RepoWorktree
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Generator
13
+
14
+ from .repo import RepoTarget
15
+
16
+ TEMPLATE_REPO_URL = "https://github.com/smkent/copier-project"
17
+
18
+
19
+ @dataclass
20
+ class UpdateAction:
21
+ repo: RepoTarget
22
+ branch: str
23
+ vcs_ref: str | None = None
24
+ dry_run: bool = field(default=False, kw_only=True)
25
+
26
+ def __call__(self) -> str | None:
27
+ with RepoWorktree.clone(self.repo, branch=self.branch) as worktree:
28
+ return self._run(worktree)
29
+
30
+ @contextmanager
31
+ def _success_or_shell(
32
+ self, repo: RepoWorktree, name: str
33
+ ) -> Generator[None]:
34
+ try:
35
+ yield
36
+ except subprocess.CalledProcessError:
37
+ print( # noqa: T201
38
+ f"{name} failed. Fix errors and exit the shell to continue."
39
+ )
40
+ repo.shell()
41
+
42
+ def _run(self, repo: RepoWorktree) -> str | None:
43
+ """Run copier update in the repo worktree."""
44
+ copier_status = json.loads(
45
+ repo.run(
46
+ ["copier", "check-update", "--output-format", "json"],
47
+ capture_output=True,
48
+ ).stdout.strip()
49
+ )
50
+ start_ref = "v" + copier_status["current_version"]
51
+ end_ref = self.vcs_ref or ("v" + copier_status["latest_version"])
52
+ if not copier_status.get("update_available", False):
53
+ return None
54
+ repo.run(["copier", "update", "--skip-answered", "--vcs-ref", end_ref])
55
+
56
+ status = repo.git_status()
57
+ if not status:
58
+ return None
59
+
60
+ if repo.has_conflicts(status):
61
+ print( # noqa: T201
62
+ "Conflicts detected."
63
+ " Resolve them and exit the shell to continue."
64
+ )
65
+ repo.shell()
66
+ status = repo.git_status()
67
+ if repo.has_conflicts(status):
68
+ raise RuntimeError("Conflicts remain, aborting")
69
+
70
+ with self._success_or_shell(repo, "`mise install`"):
71
+ repo.run(["mise", "install"])
72
+ with self._success_or_shell(repo, "Lint/test"):
73
+ repo.run_task("lt")
74
+
75
+ title = f"Apply template updates from {start_ref} to {end_ref}"
76
+ body = ""
77
+ if start_ref and end_ref and start_ref != end_ref:
78
+ body = f"{TEMPLATE_REPO_URL}/compare/{start_ref}...{end_ref}"
79
+ repo.run(["git", "add", "-A"])
80
+ repo.run(["git", "commit", "-m", f"{title}\n\n{body}".strip()])
81
+
82
+ if self.dry_run:
83
+ return None
84
+
85
+ repo.run(
86
+ ["git", "push", "-u", "origin", repo.branch, "--force-with-lease"]
87
+ )
88
+ return repo.open_pr(title, body)
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.5
2
+ Name: copier-project
3
+ Version: 0.28.0
4
+ Summary: Copier template for software projects with modern tooling
5
+ Project-URL: Homepage, https://smkent.github.io/copier-project
6
+ Project-URL: Repository, https://github.com/smkent/copier-project
7
+ Project-URL: Issues, https://github.com/smkent/copier-project/issues
8
+ Author-email: Stephen Kent <smkent@smkent.net>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
+ Classifier: Topic :: Documentation
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Topic :: Utilities
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: pyyaml>=6
26
+ Requires-Dist: rich>=15
27
+ Requires-Dist: typer>=0.9
28
+ Requires-Dist: typing-extensions>=4.1; python_version < '3.12'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # copier-project
32
+
33
+ [Copier][copier] template for software projects with modern tooling
34
+
35
+ [![Copier][copier-badge]][copier]
36
+ [![License](https://img.shields.io/github/license/smkent/copier-project)](https://github.com/smkent/copier-project/blob/main/LICENSE)
37
+ [![PyPI](https://img.shields.io/pypi/v/copier-project)](https://pypi.org/project/copier-project/)
38
+ [![Python](https://img.shields.io/pypi/pyversions/copier-project)](https://pypi.org/project/copier-project/)
39
+ [![GitHub stars](https://img.shields.io/github/stars/smkent/copier-project?style=social)](https://github.com/smkent/copier-project)
40
+
41
+ ## Quick start
42
+
43
+ Install required software:
44
+
45
+ - [**mise-en-place**][mise]: `curl https://mise.run | sh` or
46
+ [alternate installation method][mise-installation]
47
+ - [**Copier**][copier]: `mise use -g copier` or `uv tool install copier`
48
+
49
+ Create a new project from the template:
50
+
51
+ ```sh
52
+ copier copy "gh:smkent/copier-project" /new/project/path
53
+ ```
54
+
55
+ **[For more details, see the documentation!][docs-usage]**
56
+
57
+ ## Variants and features
58
+
59
+ The **Generic** project variant is a language-agnostic project template with
60
+ features common to all project variants:
61
+
62
+ * [**mise-en-place**][mise] ("mise") for development toolchain management
63
+
64
+ [![mise][mise-badge]][mise]
65
+
66
+ * [**prek**][prek] for [pre-commit][pre-commit] compatible git hooks, with
67
+ [**GitHub Actions**][github-actions] CI workflow
68
+
69
+ [![prek][prek-badge]][prek]
70
+
71
+ * Contribution terms including selectable LLM use policy, with commit message
72
+ checks by [**commit-check**][commit-check]
73
+
74
+ [![commit-check][commit-check-badge]][commit-check]
75
+
76
+ * Update automation with [**Renovate**][renovate] and
77
+ [**`copier update`**][copier-update]
78
+
79
+ [![Copier][copier-badge]][copier-update]
80
+ [![renovate][renovate-badge]][renovate]
81
+
82
+ ### Python
83
+
84
+ The **Python** project variant adds Python-specific features:
85
+
86
+ * [Astral][astral] toolset: [**uv**][uv], [**ruff**][ruff], and [**ty**][ty]
87
+
88
+ [![uv][uv-badge]][uv]
89
+ [![ruff][ruff-badge]][ruff]
90
+ [![ty][ty-badge]][ty]
91
+
92
+ * [**PyPI package**][pypi] and [**container image**][ghcr-docs] release
93
+ automation via [**GitHub Actions**][github-actions]
94
+ * [**pytest**][pytest] test framework with [**codecov.io**][codecovio] support
95
+ * [**Zensical**][zensical] project documentation with automatic deployment to
96
+ [**GitHub Pages**][github-pages]
97
+
98
+ ## Credits
99
+
100
+ This template was inspired by [pawamoy/copier-uv][copier-uv] and
101
+ [ritwiktiwari/copier-astral][copier-astral] -- Thanks!
102
+
103
+ ## License
104
+
105
+ Copyright (C) 2026 Stephen Kent and contributors
106
+
107
+ Licensed under the MIT License
108
+ ([`MIT`](https://github.com/smkent/copier-project/blob/main/LICENSE)).
109
+
110
+ [astral]: https://astral.sh
111
+ [codecovio]: https://codecov.io
112
+ [commit-check]: https://commit-check.com
113
+ [commit-check-badge]: https://commit-check.com/badge.svg
114
+ [copier-astral]: https://ritwiktiwari.github.io/copier-astral/
115
+ [copier-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-purple.json
116
+ [copier-update]: https://copier.readthedocs.io/en/stable/updating/
117
+ [copier-uv]: https://pawamoy.github.io/copier-uv/
118
+ [copier]: https://copier.readthedocs.io
119
+ [docs-usage]: https://smkent.github.io/copier-project/requirements/
120
+ [ghcr-docs]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
121
+ [github-actions]: https://github.com/features/actions
122
+ [github-pages]: https://pages.github.com
123
+ [mise]: https://mise.jdx.dev
124
+ [mise-installation]: https://mise.jdx.dev/installing-mise.html
125
+ [mise-badge]: https://mise-versions.jdx.dev/badge.svg
126
+ [pre-commit]: https://pre-commit.com/
127
+ [prek-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json
128
+ [prek]: https://prek.j178.dev/
129
+ [pypi]: https://pypi.org
130
+ [pytest]: https://docs.pytest.org
131
+ [renovate-badge]: https://img.shields.io/badge/renovate-enabled-brightgreen.svg?logo=renovatebot
132
+ [renovate]: https://docs.renovatebot.com/
133
+ [ruff-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
134
+ [ruff]: https://docs.astral.sh/ruff/
135
+ [ty-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json
136
+ [ty]: https://docs.astral.sh/ty/
137
+ [uv-badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json
138
+ [uv]: https://docs.astral.sh/uv/
139
+ [zensical]: https://zensical.org/
@@ -0,0 +1,10 @@
1
+ copier_project/__init__.py,sha256=jRKv1l0yl90HBs3GkeINdWYF6kfAdq9ccfxaFl2lp7g,291
2
+ copier_project/__main__.py,sha256=yilLgz6CIf9sOio4PR-GpPXIlU7vTA198tOUgBrgDRk,3713
3
+ copier_project/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ copier_project/repo.py,sha256=SMnF0l1CQGPb2bAy9V8--6lvwVpfu8Jg_heYQN1IRWI,5351
5
+ copier_project/update.py,sha256=ZKfwuKU14o3b0Souiozi_VUJEmD9SGMr8jwrYXEtxps,2805
6
+ copier_project-0.28.0.dist-info/METADATA,sha256=Of1xWBbHqDHq7FYRsD6oOt5PiNrUa79Nsmu2h95uw0E,5707
7
+ copier_project-0.28.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
8
+ copier_project-0.28.0.dist-info/entry_points.txt,sha256=CrIFWRD5L4OXhmtXuUmUeVB42d_Pteh3P-an4PjU6go,64
9
+ copier_project-0.28.0.dist-info/licenses/LICENSE,sha256=lwNXeTMjiyUh8CtIKWnb2SOTeliVKCJfs_NZB6P23BE,1086
10
+ copier_project-0.28.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ copier-project = copier_project.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stephen Kent and 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.