pppandas 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.
ppandas/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ppandas: browse a configured GitHub repository from the terminal."""
2
+
3
+ __version__ = "0.1.0"
ppandas/cli.py ADDED
@@ -0,0 +1,241 @@
1
+ """Command-line interface for browsing a GitHub repository."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ import zipfile
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ DEFAULT_REPO = "PratikChavan05/LP3"
16
+ API_BASE = "https://api.github.com"
17
+ RAW_BASE = "https://raw.githubusercontent.com"
18
+
19
+
20
+ def request_bytes(url: str) -> bytes:
21
+ """Fetch a URL with a descriptive user-agent."""
22
+ req = urllib.request.Request(
23
+ url,
24
+ headers={
25
+ "User-Agent": "ppandas-cli/0.1.0",
26
+ "Accept": "application/vnd.github+json",
27
+ },
28
+ )
29
+ try:
30
+ with urllib.request.urlopen(req, timeout=30) as response:
31
+ return response.read()
32
+ except urllib.error.HTTPError as exc:
33
+ if exc.code == 404:
34
+ raise RuntimeError(f"Not found: {url}") from exc
35
+ if exc.code == 403:
36
+ raise RuntimeError(
37
+ "GitHub rate limit or access restriction. Try again later, "
38
+ "or configure a GitHub token using PPANDAS_GITHUB_TOKEN."
39
+ ) from exc
40
+ raise RuntimeError(f"GitHub request failed (HTTP {exc.code}): {url}") from exc
41
+ except urllib.error.URLError as exc:
42
+ raise RuntimeError(f"Network error: {exc.reason}") from exc
43
+
44
+
45
+ def headers_with_token() -> dict[str, str]:
46
+ token = os.environ.get("PPANDAS_GITHUB_TOKEN")
47
+ headers = {"User-Agent": "ppandas-cli/0.1.0", "Accept": "application/vnd.github+json"}
48
+ if token:
49
+ headers["Authorization"] = f"Bearer {token}"
50
+ return headers
51
+
52
+
53
+ def github_json(url: str) -> Any:
54
+ req = urllib.request.Request(url, headers=headers_with_token())
55
+ try:
56
+ with urllib.request.urlopen(req, timeout=30) as response:
57
+ return json.loads(response.read().decode("utf-8"))
58
+ except urllib.error.HTTPError as exc:
59
+ if exc.code == 404:
60
+ raise RuntimeError(f"Repository, branch, or path not found: {url}") from exc
61
+ if exc.code == 403:
62
+ raise RuntimeError(
63
+ "GitHub API rate limit/access restriction. Set PPANDAS_GITHUB_TOKEN "
64
+ "to a GitHub token if needed."
65
+ ) from exc
66
+ raise RuntimeError(f"GitHub API request failed (HTTP {exc.code}): {url}") from exc
67
+ except urllib.error.URLError as exc:
68
+ raise RuntimeError(f"Network error: {exc.reason}") from exc
69
+
70
+
71
+ def parse_repo(value: str) -> str:
72
+ value = value.strip().rstrip("/")
73
+ if value.startswith("https://github.com/"):
74
+ value = value[len("https://github.com/"):]
75
+ value = value.removesuffix(".git")
76
+ parts = value.split("/")
77
+ if len(parts) < 2 or not all(parts[:2]):
78
+ raise argparse.ArgumentTypeError(
79
+ "Repository must be OWNER/REPO or a GitHub repository URL."
80
+ )
81
+ return f"{parts[0]}/{parts[1]}"
82
+
83
+
84
+ def get_tree(repo: str, branch: str) -> tuple[str, list[dict[str, Any]]]:
85
+ encoded_branch = urllib.parse.quote(branch, safe="")
86
+ data = github_json(f"{API_BASE}/repos/{repo}/git/trees/{encoded_branch}?recursive=1")
87
+ if data.get("truncated"):
88
+ print("Warning: GitHub returned a truncated file tree.", file=sys.stderr)
89
+ return data.get("sha", ""), data.get("tree", [])
90
+
91
+
92
+ def cmd_list(args: argparse.Namespace) -> int:
93
+ repo = parse_repo(args.repo)
94
+ _, tree = get_tree(repo, args.branch)
95
+ entries = [item for item in tree if item.get("type") == "blob"]
96
+ for item in entries:
97
+ print(item["path"])
98
+ print(f"\n{len(entries)} files")
99
+ return 0
100
+
101
+
102
+ def cmd_show(args: argparse.Namespace) -> int:
103
+ repo = parse_repo(args.repo)
104
+ path = args.path.lstrip("/")
105
+ _, tree = get_tree(repo, args.branch)
106
+ match = next(
107
+ (item for item in tree if item.get("type") == "blob" and item.get("path") == path),
108
+ None,
109
+ )
110
+ if not match:
111
+ raise RuntimeError(f"File not found in {repo}@{args.branch}: {path}")
112
+ if match.get("size", 0) > args.max_bytes:
113
+ raise RuntimeError(
114
+ f"File is {match['size']} bytes, exceeding the {args.max_bytes}-byte limit. "
115
+ "Use --max-bytes to increase the limit."
116
+ )
117
+ encoded_path = "/".join(urllib.parse.quote(part, safe="") for part in path.split("/"))
118
+ url = f"{RAW_BASE}/{repo}/{urllib.parse.quote(args.branch, safe='')}/{encoded_path}"
119
+ content = request_bytes(url)
120
+ try:
121
+ print(content.decode("utf-8"))
122
+ except UnicodeDecodeError as exc:
123
+ raise RuntimeError("This file is not UTF-8 text and cannot be displayed.") from exc
124
+ return 0
125
+
126
+
127
+ def cmd_search(args: argparse.Namespace) -> int:
128
+ repo = parse_repo(args.repo)
129
+ _, tree = get_tree(repo, args.branch)
130
+ files = [
131
+ item for item in tree
132
+ if item.get("type") == "blob"
133
+ and (not args.extension or item["path"].lower().endswith(args.extension.lower()))
134
+ and item.get("size", 0) <= args.max_bytes
135
+ ]
136
+ needle = args.keyword.casefold()
137
+ found = 0
138
+ for item in files:
139
+ path = item["path"]
140
+ encoded_path = "/".join(urllib.parse.quote(part, safe="") for part in path.split("/"))
141
+ url = f"{RAW_BASE}/{repo}/{urllib.parse.quote(args.branch, safe='')}/{encoded_path}"
142
+ try:
143
+ text = request_bytes(url).decode("utf-8")
144
+ except (RuntimeError, UnicodeDecodeError):
145
+ continue
146
+ for line_number, line in enumerate(text.splitlines(), start=1):
147
+ if needle in line.casefold():
148
+ print(f"{path}:{line_number}: {line.strip()}")
149
+ found += 1
150
+ if found >= args.limit:
151
+ print(f"Search result limit reached ({args.limit}).")
152
+ return 0
153
+ if found == 0:
154
+ print("No matches found.")
155
+ else:
156
+ print(f"\n{found} match(es)")
157
+ return 0
158
+
159
+
160
+ def cmd_download(args: argparse.Namespace) -> int:
161
+ repo = parse_repo(args.repo)
162
+ branch = args.branch
163
+ archive_url = f"https://github.com/{repo}/archive/refs/heads/{urllib.parse.quote(branch, safe='')}.zip"
164
+ archive = request_bytes(archive_url)
165
+ destination = Path(args.output).expanduser().resolve()
166
+ destination.mkdir(parents=True, exist_ok=True)
167
+ with zipfile.ZipFile(__import__("io").BytesIO(archive)) as zf:
168
+ # Extract into a single repository folder, rejecting unsafe paths.
169
+ root = destination.resolve()
170
+ for member in zf.infolist():
171
+ target = (destination / member.filename).resolve()
172
+ if target != root and root not in target.parents:
173
+ raise RuntimeError("Archive contains an unsafe path; refusing to extract.")
174
+ zf.extractall(destination)
175
+ print(f"Downloaded {repo}@{branch} to {destination}")
176
+ return 0
177
+
178
+
179
+ def cmd_update(args: argparse.Namespace) -> int:
180
+ # For this CLI, update refreshes the repository by downloading its latest archive.
181
+ return cmd_download(args)
182
+
183
+
184
+ def add_repo_options(parser: argparse.ArgumentParser) -> None:
185
+ parser.add_argument(
186
+ "--repo", default=DEFAULT_REPO,
187
+ help=f"GitHub repository as OWNER/REPO or URL (default: {DEFAULT_REPO})",
188
+ )
189
+ parser.add_argument("--branch", default="main", help="Branch or tag (default: main)")
190
+
191
+
192
+ def build_parser() -> argparse.ArgumentParser:
193
+ parser = argparse.ArgumentParser(
194
+ prog="ppandas",
195
+ description="Browse and download the LP3 GitHub repository from your terminal.",
196
+ )
197
+ subparsers = parser.add_subparsers(dest="command", required=True)
198
+
199
+ list_parser = subparsers.add_parser("list", help="List repository files")
200
+ add_repo_options(list_parser)
201
+ list_parser.set_defaults(func=cmd_list)
202
+
203
+ show_parser = subparsers.add_parser("show", help="Print one file's source code")
204
+ show_parser.add_argument("path", help="Path relative to the repository root")
205
+ add_repo_options(show_parser)
206
+ show_parser.add_argument("--max-bytes", type=int, default=1_000_000)
207
+ show_parser.set_defaults(func=cmd_show)
208
+
209
+ search_parser = subparsers.add_parser("search", help="Search text across repository files")
210
+ search_parser.add_argument("keyword", help="Text to search for")
211
+ add_repo_options(search_parser)
212
+ search_parser.add_argument("--extension", help="Limit to an extension, e.g. .py or .cpp")
213
+ search_parser.add_argument("--max-bytes", type=int, default=500_000)
214
+ search_parser.add_argument("--limit", type=int, default=100)
215
+ search_parser.set_defaults(func=cmd_search)
216
+
217
+ download_parser = subparsers.add_parser("download", help="Download and extract repository ZIP")
218
+ add_repo_options(download_parser)
219
+ download_parser.add_argument("--output", default=".", help="Directory to extract into")
220
+ download_parser.set_defaults(func=cmd_download)
221
+
222
+ update_parser = subparsers.add_parser("update", help="Download the latest repository archive")
223
+ add_repo_options(update_parser)
224
+ update_parser.add_argument("--output", default=".", help="Directory to extract into")
225
+ update_parser.set_defaults(func=cmd_update)
226
+
227
+ return parser
228
+
229
+
230
+ def main() -> int:
231
+ parser = build_parser()
232
+ args = parser.parse_args()
233
+ try:
234
+ return int(args.func(args))
235
+ except (RuntimeError, OSError, zipfile.BadZipFile) as exc:
236
+ print(f"ppandas: error: {exc}", file=sys.stderr)
237
+ return 1
238
+
239
+
240
+ if __name__ == "__main__":
241
+ raise SystemExit(main())
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: pppandas
3
+ Version: 0.1.0
4
+ Summary: A terminal tool to browse and download the PratikChavan05/LP3 GitHub repository
5
+ Author: PratikChavan05
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # ppandas
13
+
14
+ `ppandas` is a terminal CLI for listing, viewing, searching, and downloading the public
15
+ GitHub repository `PratikChavan05/LP3`.
16
+
17
+ ## Install locally
18
+
19
+ ```bash
20
+ python3 -m venv .venv
21
+ source .venv/bin/activate
22
+ python -m pip install --upgrade pip
23
+ python -m pip install .
24
+ ppandas --help
25
+ ```
26
+
27
+ ## Commands
28
+
29
+ ```bash
30
+ ppandas list
31
+ ppandas show path/to/file.cpp
32
+ ppandas search "binary search"
33
+ ppandas search "main" --extension .cpp
34
+ ppandas download --output ./downloads
35
+ ppandas update --output ./downloads
36
+ ```
37
+
38
+ Override the repository or branch:
39
+
40
+ ```bash
41
+ ppandas list --repo OWNER/REPO --branch main
42
+ ```
43
+
44
+ `show` and `search` fetch files from GitHub when used, so an internet connection is required.
45
+ The default repository is public. For higher GitHub API rate limits, set
46
+ `PPANDAS_GITHUB_TOKEN` to a GitHub token in your shell. Do not commit a token to source control.
47
+
48
+ ## Publish to PyPI
49
+
50
+ Check package-name availability and complete the PyPI account setup first. Then build and
51
+ upload a release using the official Python packaging instructions.
@@ -0,0 +1,8 @@
1
+ ppandas/__init__.py,sha256=MT07VHlxgjA1iutLdAHzBIUNWIDPsRhS1N1xdwd9O8g,95
2
+ ppandas/cli.py,sha256=kQ6mthdmgaeFObIBXpuAtcfhC3FRahBGvxjR2lHRiGI,9334
3
+ pppandas-0.1.0.dist-info/licenses/LICENSE,sha256=IAMcIK9VEEi6Ay3oIgiuaQdTVCpIbLG0kOQu0ZRKv64,812
4
+ pppandas-0.1.0.dist-info/METADATA,sha256=QBukhkYy4yHCG7f-Q6RN6aWOYnmkS7yOBqqAc7AO6AU,1330
5
+ pppandas-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
6
+ pppandas-0.1.0.dist-info/entry_points.txt,sha256=uSh1-ssG4mzu8PsDDIrc_QEByGLPMORqkxqxrogSDaE,45
7
+ pppandas-0.1.0.dist-info/top_level.txt,sha256=5CXoUq_TlC3Fp44175sX7zfKcI6sozJapj2nNT75Vf0,8
8
+ pppandas-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ppandas = ppandas.cli:main
@@ -0,0 +1,17 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PratikChavan05
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.
@@ -0,0 +1 @@
1
+ ppandas