RepoPackPy 0.1.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.
repopack/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
repopack/cli.py ADDED
@@ -0,0 +1,58 @@
1
+ import json
2
+ import typer
3
+ from pathlib import Path
4
+
5
+ from repopack.packer import pack_workspace
6
+ from repopack.unpacker import unpack_workspace
7
+ from repopack.mcp_server import run_server
8
+
9
+ app = typer.Typer(name="repopack", help="Pack/unpack any workspace into portable JSON.")
10
+
11
+
12
+ @app.command()
13
+ def pack(
14
+ directory: str = typer.Argument(".", help="The workspace to pack."),
15
+ output: str = typer.Option(None, "-o", "--output", help="Output JSON file path."),
16
+ include_binary: bool = typer.Option(False, "--include-binary", help="Include binary files."),
17
+ custom_ignore: str = typer.Option(None, "--custom-ignore", help="Comma-separated extra ignore patterns."),
18
+ ):
19
+ try:
20
+ result = pack_workspace(Path(directory), include_binary, custom_ignore)
21
+ if output:
22
+ with open(output, "w", encoding="utf-8") as f:
23
+ json.dump(result, f, indent=2)
24
+ total_files = result.get("total_files", 0)
25
+ total_bytes = result.get("total_bytes", 0)
26
+ typer.echo(f"Packed {total_files} files ({total_bytes} bytes) → {output}", err=True)
27
+ else:
28
+ print(json.dumps(result, indent=2))
29
+ except Exception as e:
30
+ typer.echo(str(e), err=True)
31
+ raise typer.Exit(1)
32
+
33
+
34
+ @app.command()
35
+ def unpack(
36
+ input: str = typer.Argument(..., help="File path or raw JSON string to unpack."),
37
+ target: str = typer.Option(".", "-t", "--target", help="Output directory."),
38
+ force: bool = typer.Option(False, "--force/--no-force", help="Overwrite existing files."),
39
+ ):
40
+ try:
41
+ result = unpack_workspace(input, Path(target), overwrite=force)
42
+ extracted = result.get("extracted", 0)
43
+ destination = result.get("destination", target)
44
+ skipped = result.get("skipped", 0)
45
+ typer.echo(f"Extracted {extracted} files to {destination} ({skipped} skipped)")
46
+ except Exception as e:
47
+ typer.echo(str(e), err=True)
48
+ raise typer.Exit(1)
49
+
50
+
51
+ @app.command()
52
+ def serve():
53
+ typer.echo("Starting RepoPack MCP server (stdio)...", err=True)
54
+ run_server()
55
+
56
+
57
+ def main():
58
+ app()
repopack/mcp_server.py ADDED
@@ -0,0 +1,41 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from mcp.server.fastmcp import FastMCP
5
+
6
+ from repopack.packer import pack_workspace
7
+ from repopack.unpacker import unpack_workspace
8
+
9
+ mcp = FastMCP("RepoPack")
10
+
11
+
12
+ @mcp.tool()
13
+ def export_workspace(
14
+ workspace_path: str = ".",
15
+ output_json_path: str | None = None,
16
+ include_binary: bool = False,
17
+ ) -> str:
18
+ result = pack_workspace(Path(workspace_path), include_binary=include_binary)
19
+ if output_json_path is not None:
20
+ with open(output_json_path, "w", encoding="utf-8") as f:
21
+ json.dump(result, f, indent=2)
22
+ return f"Packed {result['metadata']['total_files']} files ({result['metadata']['total_bytes']} bytes) to {output_json_path}"
23
+ return json.dumps(result, indent=2)
24
+
25
+
26
+ @mcp.tool()
27
+ def import_workspace(
28
+ json_input: str,
29
+ destination_path: str,
30
+ overwrite: bool = False,
31
+ ) -> str:
32
+ summary = unpack_workspace(json_input, Path(destination_path), overwrite=overwrite)
33
+ return f"Extracted {summary['extracted']} files to {summary['destination']} ({summary['skipped']} skipped)"
34
+
35
+
36
+ def run_server():
37
+ mcp.run(transport="stdio")
38
+
39
+
40
+ if __name__ == "__main__":
41
+ run_server()
repopack/packer.py ADDED
@@ -0,0 +1,96 @@
1
+ import base64
2
+ import datetime
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from repopack.utils import detect_stack, is_binary_file, is_ignored, load_gitignore_specs
8
+
9
+ _MAX_FILE_BYTES = 5 * 1024 * 1024
10
+
11
+
12
+ def pack_workspace(
13
+ directory: Path,
14
+ include_binary: bool = False,
15
+ custom_ignore: str | None = None,
16
+ ) -> dict:
17
+ directory = directory.resolve()
18
+
19
+ specs = load_gitignore_specs(directory)
20
+
21
+ custom_patterns: list[str] = []
22
+ if custom_ignore:
23
+ custom_patterns = [p.strip() for p in custom_ignore.split(",") if p.strip()]
24
+
25
+ files: list[dict] = []
26
+ total_bytes: int = 0
27
+
28
+ for root, dirs, filenames in os.walk(directory, topdown=True):
29
+ root_path = Path(root)
30
+
31
+ dirs[:] = [
32
+ d for d in dirs
33
+ if d not in {".git", ".svn", ".hg"}
34
+ and not is_ignored(root_path / d, directory, specs, custom_patterns)
35
+ ]
36
+
37
+ for filename in filenames:
38
+ file_path = root_path / filename
39
+ rel_path = file_path.relative_to(directory)
40
+
41
+ if is_ignored(file_path, directory, specs, custom_patterns):
42
+ continue
43
+
44
+ try:
45
+ file_size = file_path.stat().st_size
46
+ except OSError:
47
+ continue
48
+
49
+ if file_size > _MAX_FILE_BYTES:
50
+ print(
51
+ f"warning: skipping {rel_path.as_posix()} ({file_size} bytes > 5 MB)",
52
+ file=sys.stderr,
53
+ )
54
+ continue
55
+
56
+ binary = is_binary_file(file_path)
57
+
58
+ if binary and not include_binary:
59
+ continue
60
+
61
+ if binary:
62
+ try:
63
+ raw = file_path.read_bytes()
64
+ except OSError:
65
+ continue
66
+ content = base64.b64encode(raw).decode("ascii")
67
+ encoding = "base64"
68
+ else:
69
+ try:
70
+ content = file_path.read_text(encoding="utf-8", errors="replace")
71
+ except OSError:
72
+ continue
73
+ encoding = "utf-8"
74
+
75
+ total_bytes += file_size
76
+ files.append(
77
+ {
78
+ "path": rel_path.as_posix(),
79
+ "encoding": encoding,
80
+ "content": content,
81
+ }
82
+ )
83
+
84
+ detected_stack = detect_stack(directory)
85
+
86
+ return {
87
+ "version": "1.0",
88
+ "metadata": {
89
+ "created_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
90
+ "root_directory_name": directory.name,
91
+ "detected_stack": detected_stack,
92
+ "total_files": len(files),
93
+ "total_bytes": total_bytes,
94
+ },
95
+ "files": files,
96
+ }
repopack/unpacker.py ADDED
@@ -0,0 +1,60 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from repopack.utils import safe_relative_path
7
+
8
+
9
+ def unpack_workspace(
10
+ json_input: str,
11
+ destination_path: Path,
12
+ overwrite: bool = False,
13
+ force: bool = False,
14
+ ) -> dict:
15
+ if Path(json_input).is_file():
16
+ with open(json_input, "r", encoding="utf-8") as f:
17
+ data = json.load(f)
18
+ else:
19
+ data = json.loads(json_input)
20
+
21
+ if "version" not in data or "files" not in data:
22
+ raise ValueError("Malformed input: top-level JSON must contain 'version' and 'files' keys.")
23
+
24
+ destination_path = destination_path.resolve()
25
+
26
+ extracted = 0
27
+ skipped = 0
28
+
29
+ for entry in data["files"]:
30
+ rel = entry.get("path", "")
31
+
32
+ rel_path = Path(rel)
33
+ if rel_path.is_absolute():
34
+ raise ValueError(f"Path traversal detected: {rel}")
35
+ if any(part == ".." for part in rel_path.parts):
36
+ raise ValueError(f"Path traversal detected: {rel}")
37
+
38
+ resolved = (destination_path / rel_path).resolve()
39
+ if os.path.commonpath([str(destination_path), str(resolved)]) != str(destination_path):
40
+ raise ValueError(f"Path traversal detected: {rel}")
41
+
42
+ output_path = destination_path / rel_path
43
+
44
+ if output_path.exists() and not overwrite:
45
+ skipped += 1
46
+ continue
47
+
48
+ output_path.parent.mkdir(parents=True, exist_ok=True)
49
+
50
+ encoding = entry.get("encoding", "utf-8")
51
+ content = entry.get("content", "")
52
+
53
+ if encoding == "base64":
54
+ output_path.write_bytes(base64.b64decode(content))
55
+ else:
56
+ output_path.write_text(content, encoding="utf-8")
57
+
58
+ extracted += 1
59
+
60
+ return {"extracted": extracted, "skipped": skipped, "destination": str(destination_path)}
repopack/utils.py ADDED
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import pathspec
7
+
8
+ DEFAULT_IGNORE_PATTERNS: list[str] = [
9
+ ".git/",
10
+ ".svn/",
11
+ ".hg/",
12
+ ".DS_Store",
13
+ "Thumbs.db",
14
+ "node_modules/",
15
+ "dist/",
16
+ "build/",
17
+ ".next/",
18
+ ".nuxt/",
19
+ ".angular/",
20
+ ".cache/",
21
+ "coverage/",
22
+ ".astro/",
23
+ ".turbo/",
24
+ "out/",
25
+ "__pycache__/",
26
+ "*.pyc",
27
+ ".venv/",
28
+ "venv/",
29
+ ".pytest_cache/",
30
+ ".mypy_cache/",
31
+ ".eggs/",
32
+ "*.egg-info/",
33
+ "sdist/",
34
+ "target/",
35
+ "bin/",
36
+ "obj/",
37
+ "*.exe",
38
+ "*.so",
39
+ "*.dylib",
40
+ "*.dll",
41
+ "*.class",
42
+ "*.jar",
43
+ "*.o",
44
+ "*.a",
45
+ ".env",
46
+ ".env.local",
47
+ ".env.*.local",
48
+ "package-lock.json",
49
+ "yarn.lock",
50
+ "pnpm-lock.yaml",
51
+ "poetry.lock",
52
+ "Pipfile.lock",
53
+ "Cargo.lock",
54
+ "go.sum",
55
+ ]
56
+
57
+ _BINARY_EXTENSIONS: frozenset[str] = frozenset({
58
+ ".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp", ".bmp", ".tiff",
59
+ ".pdf",
60
+ ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar",
61
+ ".woff", ".woff2", ".ttf", ".eot", ".otf",
62
+ ".mp3", ".mp4", ".wav", ".ogg", ".avi", ".mov", ".webm",
63
+ ".pyc", ".pyo",
64
+ ".exe", ".dll", ".so", ".dylib",
65
+ ".class", ".jar",
66
+ ".o", ".a", ".lib",
67
+ ".db", ".sqlite", ".sqlite3",
68
+ })
69
+
70
+
71
+ def load_gitignore_specs(root_dir: Path) -> list[tuple[Path, pathspec.PathSpec]]:
72
+ specs: list[tuple[Path, pathspec.PathSpec]] = []
73
+ for gitignore_path in sorted(root_dir.rglob(".gitignore")):
74
+ lines = gitignore_path.read_text(encoding="utf-8", errors="replace").splitlines()
75
+ spec = pathspec.PathSpec.from_lines("gitignore", lines)
76
+ specs.append((gitignore_path.parent, spec))
77
+ return specs
78
+
79
+
80
+ def is_ignored(
81
+ path: Path,
82
+ root_dir: Path,
83
+ gitignore_specs: list[tuple[Path, pathspec.PathSpec]],
84
+ custom_patterns: list[str] | None = None,
85
+ ) -> bool:
86
+ if custom_patterns:
87
+ custom_spec = pathspec.PathSpec.from_lines("gitignore", custom_patterns)
88
+ rel = path.relative_to(root_dir)
89
+ if custom_spec.match_file(str(rel)):
90
+ return True
91
+
92
+ for spec_dir, spec in gitignore_specs:
93
+ try:
94
+ rel = path.relative_to(spec_dir)
95
+ except ValueError:
96
+ continue
97
+ if spec.match_file(str(rel)):
98
+ return True
99
+
100
+ default_spec = pathspec.PathSpec.from_lines("gitignore", DEFAULT_IGNORE_PATTERNS)
101
+ try:
102
+ rel_from_root = path.relative_to(root_dir)
103
+ except ValueError:
104
+ rel_from_root = path
105
+ if default_spec.match_file(str(rel_from_root)):
106
+ return True
107
+
108
+ return False
109
+
110
+
111
+ def is_binary_file(path: Path) -> bool:
112
+ if path.suffix.lower() in _BINARY_EXTENSIONS:
113
+ return True
114
+ try:
115
+ chunk = path.read_bytes()[:8192]
116
+ except OSError:
117
+ return False
118
+ return b"\x00" in chunk
119
+
120
+
121
+ def detect_stack(root_dir: Path) -> list[str]:
122
+ entries = {p.name: p for p in root_dir.iterdir()}
123
+ stacks: list[str] = []
124
+
125
+ if "package.json" in entries:
126
+ stacks.append("Node.js")
127
+ try:
128
+ import json
129
+ pkg = json.loads(entries["package.json"].read_text(encoding="utf-8", errors="replace"))
130
+ deps = {
131
+ **pkg.get("dependencies", {}),
132
+ **pkg.get("devDependencies", {}),
133
+ }
134
+ if "react" in deps:
135
+ stacks.append("React")
136
+ except Exception:
137
+ pass
138
+
139
+ has_ts = (
140
+ "tsconfig.json" in entries
141
+ or any(p.suffix in {".ts", ".tsx"} for p in root_dir.iterdir() if p.is_file())
142
+ )
143
+ if has_ts:
144
+ stacks.append("TypeScript")
145
+
146
+ if "angular.json" in entries:
147
+ stacks.append("Angular")
148
+
149
+ if any(e.startswith("next.config.") for e in entries):
150
+ stacks.append("Next.js")
151
+
152
+ if any(e.startswith("vue.config.") for e in entries) or any(
153
+ p.suffix == ".vue" for p in root_dir.iterdir() if p.is_file()
154
+ ):
155
+ stacks.append("Vue")
156
+
157
+ if any(e in entries for e in ("requirements.txt", "pyproject.toml", "setup.py")):
158
+ stacks.append("Python")
159
+
160
+ if "Cargo.toml" in entries:
161
+ stacks.append("Rust")
162
+
163
+ if "go.mod" in entries:
164
+ stacks.append("Go")
165
+
166
+ if "pom.xml" in entries or "build.gradle" in entries:
167
+ stacks.append("Java")
168
+
169
+ if any(p.suffix in {".csproj", ".sln"} for p in root_dir.iterdir() if p.is_file()):
170
+ stacks.append("C#")
171
+
172
+ return stacks
173
+
174
+
175
+ def safe_relative_path(base: Path, target: Path) -> Path:
176
+ resolved_base = base.resolve()
177
+ resolved_target = target.resolve()
178
+ common = os.path.commonpath([str(resolved_base), str(resolved_target)])
179
+ if common != str(resolved_base):
180
+ raise ValueError(
181
+ f"Path traversal detected: {target!r} is not under {base!r}"
182
+ )
183
+ return resolved_target.relative_to(resolved_base)
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: RepoPackPy
3
+ Version: 0.1.1
4
+ Summary: Pack/unpack any source workspace into portable JSON, with MCP server support.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: mcp[cli]>=1.0
9
+ Requires-Dist: pathspec>=0.12
10
+ Requires-Dist: typer>=0.12
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # RepoPack
16
+
17
+ Pack/unpack **any source workspace regardless of technology stack** (Python, Node.js, React, Angular, Vue, Rust, Go, Java, C#, and more) into a portable, structured JSON payload — and restore it faithfully from that payload.
18
+
19
+ ## Features
20
+
21
+ - **Universal ignore engine** — respects root and nested `.gitignore` files plus stack-agnostic defaults (node_modules, __pycache__, .venv, target/, dist/, secrets, lock files, …)
22
+ - **Polyglot stack detection** — auto-detects Node.js, React, TypeScript, Angular, Next.js, Vue, Python, Rust, Go, Java, C# from project sentinel files
23
+ - **Binary safety** — text files encoded as UTF-8; binaries skipped by default or included as Base64 with `--include-binary`
24
+ - **5 MB guard** — files larger than 5 MB are skipped with a warning
25
+ - **Path traversal shield** — `unpack` validates every path against the destination root before writing anything
26
+ - **MCP server** — expose pack/unpack as tools to any MCP-compatible AI client (Claude Desktop, etc.)
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install RepoPackPy
32
+ ```
33
+
34
+ For development:
35
+
36
+ ```bash
37
+ pip install -e ".[dev]"
38
+ ```
39
+
40
+ Requires Python 3.10+.
41
+
42
+ ## CLI Usage
43
+
44
+ ```bash
45
+ # Pack a directory into JSON
46
+ repopack pack [DIRECTORY] [-o output.json] [--include-binary] [--custom-ignore ".next,dist"]
47
+
48
+ # Unpack JSON back into a directory
49
+ repopack unpack <input.json> [-t TARGET_DIR] [--force]
50
+
51
+ # Start the MCP server (stdio transport)
52
+ repopack serve
53
+ ```
54
+
55
+ ### Examples
56
+
57
+ ```bash
58
+ # Pack current directory, write to file
59
+ repopack pack . -o workspace.json
60
+
61
+ # Pack a specific project, include binaries
62
+ repopack pack ~/projects/my-app -o my-app.json --include-binary
63
+
64
+ # Unpack into a new directory
65
+ repopack unpack workspace.json -t ./restored
66
+
67
+ # Unpack and overwrite existing files
68
+ repopack unpack workspace.json -t ./restored --force
69
+ ```
70
+
71
+ ## JSON Payload Schema
72
+
73
+ ```json
74
+ {
75
+ "version": "1.0",
76
+ "metadata": {
77
+ "created_at": "2026-07-26T12:00:00Z",
78
+ "root_directory_name": "my-app",
79
+ "detected_stack": ["Node.js", "React", "TypeScript"],
80
+ "total_files": 38,
81
+ "total_bytes": 128450
82
+ },
83
+ "files": [
84
+ { "path": "src/App.tsx", "encoding": "utf-8", "content": "..." },
85
+ { "path": "public/favicon.ico", "encoding": "base64", "content": "..." }
86
+ ]
87
+ }
88
+ ```
89
+
90
+ ## MCP Server Tools
91
+
92
+ When running `repopack serve`, two tools are registered via FastMCP:
93
+
94
+ | Tool | Description |
95
+ |---|---|
96
+ | `export_workspace(workspace_path, output_json_path, include_binary)` | Pack a directory into JSON |
97
+ | `import_workspace(json_input, destination_path, overwrite)` | Unpack JSON into a directory |
98
+
99
+ ## Project Structure
100
+
101
+ ```
102
+ repopack/
103
+ ├── pyproject.toml
104
+ ├── README.md
105
+ └── repopack/
106
+ ├── __init__.py
107
+ ├── cli.py # Typer CLI (pack / unpack / serve)
108
+ ├── mcp_server.py # FastMCP server
109
+ ├── packer.py # Directory walker & JSON builder
110
+ ├── unpacker.py # JSON extractor & path-safe reconstructor
111
+ └── utils.py # Ignore engine, binary detector, stack detection
112
+ tests/
113
+ └── test_repopack.py # 26-test pytest suite
114
+ ```
115
+
116
+ ## Running Tests
117
+
118
+ ```bash
119
+ pytest tests/ -v
120
+ ```
121
+
122
+ ## Dependencies
123
+
124
+ - [typer](https://typer.tiangolo.com/) — CLI framework
125
+ - [mcp](https://github.com/modelcontextprotocol/python-sdk) — MCP SDK (FastMCP)
126
+ - [pathspec](https://github.com/cpburnz/python-pathspec) — gitignore wildmatch pattern matching
@@ -0,0 +1,11 @@
1
+ repopack/__init__.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
2
+ repopack/cli.py,sha256=iuRVCnGrOPgEEARsFOYahFt2dUS-SmJRYjkhL73DgPQ,2090
3
+ repopack/mcp_server.py,sha256=qX4EougdL8ArnfiRoMWmV1ZSzim1xsD2MUPtdfjzxIs,1166
4
+ repopack/packer.py,sha256=2h4dC-oV2LIPLVUQ0m7WwAaA9mkpJNXfM3CLZ3GEvF0,2761
5
+ repopack/unpacker.py,sha256=RDIXprVPg9ZTcAzN2CgxGq7EbxB2ex9EDojD28TGGK8,1798
6
+ repopack/utils.py,sha256=UnKnSGl_StMlE4OQI1zCNUl--UPV6VFzW3UGTfsi6JI,4807
7
+ repopackpy-0.1.1.dist-info/METADATA,sha256=QkenIk0OVB3rqsXBk1awNAcfpvnBlYQgvE4uSVaTUxs,3783
8
+ repopackpy-0.1.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
9
+ repopackpy-0.1.1.dist-info/entry_points.txt,sha256=6_yKH4CqxdMrFaHwQH-bKM1XW0lppiBJ3ioWJ8RFlTs,47
10
+ repopackpy-0.1.1.dist-info/licenses/LICENSE,sha256=Z4Oa85qoUpnHkq4Pa4-LOGiCT5cvJaX5_aP0K8OXBNQ,1069
11
+ repopackpy-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ repopack = repopack.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shan Konduru
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.