codefetch-cli 1.2.2__tar.gz → 1.2.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codefetch-cli
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Quickly fetch code files from GitHub repositories with simple commands
5
5
  Home-page: https://github.com/InzamamulQureshi/Codefetch
6
6
  Author: Inzamamul Qureshi
@@ -24,7 +24,7 @@ Dynamic: requires-python
24
24
 
25
25
  Quickly fetch code files from GitHub repositories with simple commands.
26
26
 
27
- CodeFetch is a lightweight command-line utility designed to quickly inspect, preview, and download individual source code files directly from any GitHub repository without needing to clone full repositories.
27
+ CodeFetch is a lightweight command-line utility and Python library designed to quickly inspect, preview, and download individual source code files directly from any GitHub repository without needing to clone full repositories.
28
28
 
29
29
  ---
30
30
 
@@ -35,6 +35,7 @@ CodeFetch is a lightweight command-line utility designed to quickly inspect, pre
35
35
  - **Automatic Path & Case Resolution**: Automatically resolves case mismatches and finds files nested inside subdirectories.
36
36
  - **Repository Discovery**: List all public repositories for any GitHub user with `--repos`.
37
37
  - **Tree Exploration**: List all supported code files within a repository with `--list`.
38
+ - **Python Library Support**: Import and use programmatically with `import codefetch as cf`.
38
39
  - **Optional Persistent Defaults**: Save a default repository or user with `--set-default` if you frequently work with the same repository.
39
40
  - **Zero Runtime Dependencies**: Built using only the Python standard library with zero third-party dependencies.
40
41
 
@@ -130,6 +131,28 @@ codefetch --reset-config
130
131
 
131
132
  ---
132
133
 
134
+ ## Python API Usage
135
+
136
+ You can also use CodeFetch programmatically within your Python scripts:
137
+
138
+ ```python
139
+ import codefetch as cf
140
+
141
+ # 1. Fetch file content as a string directly in memory
142
+ makefile_text = cf.get("torvalds/linux", "Makefile")
143
+ # Or shorthand single argument syntax:
144
+ readme_text = cf.get("psf/requests/README.md")
145
+
146
+ # 2. Download a file to disk
147
+ cf.download("torvalds/linux", "Makefile", output="linux_Makefile")
148
+
149
+ # 3. List all code files in a repository
150
+ files = cf.list("torvalds/linux")
151
+ print(f"Found {len(files)} files")
152
+ ```
153
+
154
+ ---
155
+
133
156
  ## Command Reference
134
157
 
135
158
  | Flag | Description | Example |
@@ -2,7 +2,7 @@
2
2
 
3
3
  Quickly fetch code files from GitHub repositories with simple commands.
4
4
 
5
- CodeFetch is a lightweight command-line utility designed to quickly inspect, preview, and download individual source code files directly from any GitHub repository without needing to clone full repositories.
5
+ CodeFetch is a lightweight command-line utility and Python library designed to quickly inspect, preview, and download individual source code files directly from any GitHub repository without needing to clone full repositories.
6
6
 
7
7
  ---
8
8
 
@@ -13,6 +13,7 @@ CodeFetch is a lightweight command-line utility designed to quickly inspect, pre
13
13
  - **Automatic Path & Case Resolution**: Automatically resolves case mismatches and finds files nested inside subdirectories.
14
14
  - **Repository Discovery**: List all public repositories for any GitHub user with `--repos`.
15
15
  - **Tree Exploration**: List all supported code files within a repository with `--list`.
16
+ - **Python Library Support**: Import and use programmatically with `import codefetch as cf`.
16
17
  - **Optional Persistent Defaults**: Save a default repository or user with `--set-default` if you frequently work with the same repository.
17
18
  - **Zero Runtime Dependencies**: Built using only the Python standard library with zero third-party dependencies.
18
19
 
@@ -108,6 +109,28 @@ codefetch --reset-config
108
109
 
109
110
  ---
110
111
 
112
+ ## Python API Usage
113
+
114
+ You can also use CodeFetch programmatically within your Python scripts:
115
+
116
+ ```python
117
+ import codefetch as cf
118
+
119
+ # 1. Fetch file content as a string directly in memory
120
+ makefile_text = cf.get("torvalds/linux", "Makefile")
121
+ # Or shorthand single argument syntax:
122
+ readme_text = cf.get("psf/requests/README.md")
123
+
124
+ # 2. Download a file to disk
125
+ cf.download("torvalds/linux", "Makefile", output="linux_Makefile")
126
+
127
+ # 3. List all code files in a repository
128
+ files = cf.list("torvalds/linux")
129
+ print(f"Found {len(files)} files")
130
+ ```
131
+
132
+ ---
133
+
111
134
  ## Command Reference
112
135
 
113
136
  | Flag | Description | Example |
@@ -0,0 +1,146 @@
1
+ """
2
+ CodeFetch - Easily preview and download code files from GitHub repositories.
3
+
4
+ Usage in Python:
5
+ import codefetch as cf
6
+
7
+ # 1. Fetch file content as string:
8
+ content = cf.get("torvalds/linux", "Makefile")
9
+ # or shorthand target string:
10
+ content = cf.get("torvalds/linux/Makefile")
11
+
12
+ # 2. Download file to local disk:
13
+ cf.download("torvalds/linux", "Makefile", output="linux_makefile.txt")
14
+
15
+ # 3. List available code files:
16
+ files = cf.list("torvalds/linux")
17
+ """
18
+
19
+ __version__ = "1.2.3"
20
+
21
+
22
+ def get(target, filename=None, branch="main"):
23
+ """
24
+ Fetch and return the text content of a file from GitHub.
25
+
26
+ Args:
27
+ target: 'owner/repo' or full path 'owner/repo/path/to/file' or full GitHub URL.
28
+ filename: Optional path to file in repo if target is 'owner/repo'.
29
+ branch: Branch name (default: 'main', automatically falls back to 'master').
30
+
31
+ Returns:
32
+ str: File content.
33
+
34
+ Raises:
35
+ FileNotFoundError: If the file does not exist in the repository.
36
+ ValueError: If arguments cannot be resolved into a repository and filename.
37
+ """
38
+ import urllib.request
39
+ from .cli import (
40
+ fetch_raw_direct,
41
+ get_repo_tree_with_branch,
42
+ resolve_file_in_tree,
43
+ parse_repo_identifier,
44
+ resolve_invocation,
45
+ )
46
+
47
+ if filename:
48
+ owner, repo = parse_repo_identifier(target)
49
+ file_path = filename.strip("/\\")
50
+ else:
51
+ owner, repo, resolved_branch, file_path = resolve_invocation(
52
+ [target], None, None, branch, None, None, "main"
53
+ )
54
+ if resolved_branch:
55
+ branch = resolved_branch
56
+
57
+ if not owner or not repo or not file_path:
58
+ raise ValueError(f"Could not resolve owner, repo, and filename from target='{target}', filename='{filename}'")
59
+
60
+ # 1. Try fast direct raw fetch
61
+ found_branch, content = fetch_raw_direct(owner, repo, branch, file_path)
62
+ if content is not None:
63
+ return content
64
+
65
+ # 2. Try repository tree search
66
+ actual_branch, tree_data = get_repo_tree_with_branch(owner, repo, branch)
67
+ matched = resolve_file_in_tree(tree_data, file_path)
68
+ if matched:
69
+ url = f"https://raw.githubusercontent.com/{owner}/{repo}/{actual_branch}/{matched}"
70
+ req = urllib.request.Request(url, headers={"User-Agent": "codefetch"})
71
+ with urllib.request.urlopen(req, timeout=15) as resp:
72
+ return resp.read().decode("utf-8")
73
+
74
+ raise FileNotFoundError(f"File '{file_path}' not found in {owner}/{repo} ({actual_branch})")
75
+
76
+
77
+ def download(target, filename=None, output=None, branch="main"):
78
+ """
79
+ Download a file from GitHub and save it locally.
80
+
81
+ Args:
82
+ target: 'owner/repo' or full path 'owner/repo/path/to/file'.
83
+ filename: Optional path to file in repo if target is 'owner/repo'.
84
+ output: Destination filename/path on local disk (default: original filename).
85
+ branch: Branch name (default: 'main').
86
+
87
+ Returns:
88
+ str: Path to the downloaded file.
89
+ """
90
+ content = get(target, filename=filename, branch=branch)
91
+ dest_name = output
92
+ if not dest_name:
93
+ resolved_file = filename if filename else target.rsplit("/", 1)[-1]
94
+ dest_name = resolved_file.rsplit("/", 1)[-1]
95
+
96
+ with open(dest_name, "w", encoding="utf-8") as f:
97
+ f.write(content)
98
+
99
+ return dest_name
100
+
101
+
102
+ def list(target, branch="main"):
103
+ """
104
+ List supported code files in a repository.
105
+
106
+ Args:
107
+ target: 'owner/repo' or repository URL.
108
+ branch: Branch name (default: 'main').
109
+
110
+ Returns:
111
+ list[str]: Relative paths of code files in the repository.
112
+ """
113
+ from .cli import (
114
+ parse_repo_identifier,
115
+ get_repo_tree_with_branch,
116
+ CODE_EXTENSIONS,
117
+ NAMED_FILES,
118
+ )
119
+
120
+ owner, repo = parse_repo_identifier(target)
121
+ if not owner or not repo:
122
+ raise ValueError(f"Invalid repository identifier: '{target}'")
123
+
124
+ actual_branch, tree_data = get_repo_tree_with_branch(owner, repo, branch)
125
+ files = []
126
+ for item in tree_data.get("tree", []):
127
+ if item.get("type") != "blob":
128
+ continue
129
+ path = item.get("path", "")
130
+ base_name = path.rsplit("/", 1)[-1].lower()
131
+ suffix = ""
132
+ if "." in base_name:
133
+ suffix = "." + base_name.rsplit(".", 1)[-1]
134
+ if suffix in CODE_EXTENSIONS or base_name in NAMED_FILES:
135
+ files.append(path)
136
+
137
+ files.sort(key=str.lower)
138
+ return files
139
+
140
+
141
+ __all__ = [
142
+ "__version__",
143
+ "get",
144
+ "download",
145
+ "list",
146
+ ]
@@ -18,6 +18,8 @@ DEFAULT_BRANCH = "main"
18
18
 
19
19
  CONFIG_FILE = Path.home() / ".codefetch.json"
20
20
 
21
+ __version__ = "1.2.3"
22
+
21
23
  # Supported code, data, and config extensions
22
24
  CODE_EXTENSIONS = {
23
25
  # Data & Tables
@@ -56,85 +58,81 @@ CodeFetch - Download single code files from GitHub without cloning entire repos.
56
58
 
57
59
  USAGE:
58
60
  codefetch <owner>/<repo> <filename> Download a file from any GitHub repository
59
- codefetch <filename> Download a file (requires default repository)
60
-
61
- QUICK EXAMPLES:
62
- Download a file:
63
- codefetch torvalds/linux Makefile Save Makefile to current directory
64
- codefetch owner/repo main.py Save main.py from a repository
65
- codefetch owner/repo data.csv -o out.csv Save with a custom output filename
66
-
67
- View code in terminal (without downloading):
68
- codefetch -s owner/repo main.py Display main.py in terminal
69
- codefetch -s torvalds/linux Makefile Display Makefile in terminal
70
-
71
- Browse files and repositories:
72
- codefetch -l owner/repo List all code files in a repository
73
- codefetch -R -u <username> List all public repositories of a user
74
- codefetch -R <username> List all public repositories of a user
75
-
76
- SETTINGS & DEFAULTS (Optional):
77
- codefetch --set-default <owner/repo> Set a permanent default repository
78
- codefetch --set-default-user <username> Set a permanent default GitHub user
79
- codefetch --config View current saved settings
80
- codefetch --reset-config Clear saved settings
81
-
82
- OPTIONS:
83
- -s, --show View file contents in terminal instead of saving
84
- -o, --output <file> Custom filename or path to save the downloaded file
85
- -l, --list [repo] List available code files in a repository
86
- -R, --repos [user] List all public repositories for a user
87
- -r, --repo <name> Specify repository name or URL
88
- -u, --user <name> Specify GitHub username
89
- -b, --branch <name> Specify branch name (default: main)
90
- -v, --version Show version number
91
- -h, --help Show this help guide
61
+ codefetch <filename> Download a file (uses saved default repo)
62
+ codefetch -s <owner>/<repo> <filename> Preview file in terminal without saving
63
+ codefetch -l <owner>/<repo> List all code files in a repository
64
+ codefetch -R <username> List public repositories for a user
65
+
66
+ POPULAR EXAMPLES:
67
+ # Download from any repository:
68
+ codefetch owner/repo main.py
69
+ codefetch torvalds/linux Makefile
70
+
71
+ # Preview code directly in terminal:
72
+ codefetch -s owner/repo app.js
73
+ codefetch -s torvalds/linux Makefile
74
+
75
+ # Save with a custom name or path:
76
+ codefetch owner/repo data.csv -o my_data.csv
77
+
78
+ # Explore repositories and files:
79
+ codefetch -l torvalds/linux # list files in a repo
80
+ codefetch -R torvalds # list repos belonging to user
81
+
82
+ SETTING A DEFAULT REPOSITORY (OPTIONAL):
83
+ If you frequently work with the same repo, you can set it as default:
84
+ codefetch --set-default owner/repo # sets default repo
85
+ codefetch script.py # now you don't need to type owner/repo!
86
+ codefetch --config # view active configuration
87
+ codefetch --reset-config # reset all defaults
88
+
89
+ FLAGS & OPTIONS:
90
+ -s, --show Print file content to terminal instead of downloading
91
+ -o, --output <file> Specify output filename or path
92
+ -l, --list List available code files in the repository
93
+ -R, --repos List public repositories for a GitHub user
94
+ -r, --repo <repo> Specify repository name or URL
95
+ -u, --user <user> Specify GitHub username / organization
96
+ -b, --branch <branch> Specify branch name (default: main)
97
+ --set-default <repo> Save default repository (e.g. owner/repo)
98
+ --set-default-user <user> Save default user/organization
99
+ --config Show current saved configuration
100
+ --reset-config Reset configuration back to defaults
101
+ -v, --version Show version number
102
+ -h, --help Show this help message
92
103
  """
93
104
 
94
105
 
95
106
  def load_config():
96
- """Load configuration from environment variables and local config file."""
97
- config = {
98
- "owner": os.environ.get("CODEFETCH_USER", DEFAULT_OWNER),
99
- "repo": os.environ.get("CODEFETCH_REPO", DEFAULT_REPO),
100
- "branch": os.environ.get("CODEFETCH_BRANCH", DEFAULT_BRANCH),
101
- }
107
+ """Load user configuration from ~/.codefetch.json."""
102
108
  if CONFIG_FILE.exists():
103
109
  try:
104
110
  with open(CONFIG_FILE, "r", encoding="utf-8") as f:
105
- saved = json.load(f)
106
- if isinstance(saved, dict):
107
- if saved.get("owner"):
108
- config["owner"] = saved["owner"]
109
- if saved.get("repo"):
110
- config["repo"] = saved["repo"]
111
- if saved.get("branch"):
112
- config["branch"] = saved["branch"]
111
+ return json.load(f)
113
112
  except Exception:
114
- pass
115
- return config
113
+ return {}
114
+ return {}
116
115
 
117
116
 
118
117
  def save_config(owner=None, repo=None, branch=None):
119
- """Save configuration to ~/.codefetch.json."""
120
- config = load_config()
121
- if owner:
122
- config["owner"] = owner
123
- if repo:
124
- config["repo"] = repo
125
- if branch:
126
- config["branch"] = branch
118
+ """Save user configuration to ~/.codefetch.json."""
119
+ cfg = load_config()
120
+ if owner is not None:
121
+ cfg["owner"] = owner
122
+ if repo is not None:
123
+ cfg["repo"] = repo
124
+ if branch is not None:
125
+ cfg["branch"] = branch
126
+
127
127
  try:
128
128
  with open(CONFIG_FILE, "w", encoding="utf-8") as f:
129
- json.dump(config, f, indent=2)
130
- return True
129
+ json.dump(cfg, f, indent=2)
131
130
  except Exception as e:
132
- print(f"Error saving configuration: {e}")
133
- return False
131
+ print(f"Warning: Could not save configuration: {e}")
134
132
 
135
133
 
136
134
  def get_headers():
137
- """Get HTTP headers for GitHub requests."""
135
+ """Build request headers, attaching GITHUB_TOKEN if present in environment."""
138
136
  headers = {
139
137
  "User-Agent": "codefetch",
140
138
  "Accept": "application/vnd.github+json",
@@ -146,10 +144,10 @@ def get_headers():
146
144
 
147
145
 
148
146
  def get_json(url):
149
- """Fetch and decode JSON from a URL."""
150
- request = urllib.request.Request(url, headers=get_headers())
151
- with urllib.request.urlopen(request, timeout=15) as response:
152
- return json.loads(response.read().decode("utf-8"))
147
+ """Fetch JSON from GitHub API with rate limit handling."""
148
+ req = urllib.request.Request(url, headers=get_headers())
149
+ with urllib.request.urlopen(req, timeout=15) as resp:
150
+ return json.loads(resp.read().decode("utf-8"))
153
151
 
154
152
 
155
153
  def parse_repo_identifier(repo_str, default_owner=None):
@@ -170,37 +168,89 @@ def parse_repo_identifier(repo_str, default_owner=None):
170
168
  return default_owner, repo_str.removesuffix(".git")
171
169
 
172
170
 
173
- def get_repo_tree(owner, repo, branch="main"):
174
- """Fetch the Git tree for a repository, with fallback to master if main is absent."""
175
- url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1"
171
+ def fetch_raw_direct(owner, repo, branch, clean_filename):
172
+ """
173
+ Direct raw fetch from raw.githubusercontent.com.
174
+ Tries requested branch first. If branch is 'main', also tries 'master',
175
+ and vice versa.
176
+ Returns (branch_used, content_str) or (None, None).
177
+ """
178
+ branches_to_try = [branch]
179
+ if branch == "main":
180
+ branches_to_try.append("master")
181
+ elif branch == "master":
182
+ branches_to_try.append("main")
183
+
184
+ for b in branches_to_try:
185
+ url = f"https://raw.githubusercontent.com/{owner}/{repo}/{b}/{clean_filename}"
186
+ try:
187
+ req = urllib.request.Request(url, headers={"User-Agent": "codefetch"})
188
+ with urllib.request.urlopen(req, timeout=15) as resp:
189
+ return b, resp.read().decode("utf-8")
190
+ except urllib.error.HTTPError as e:
191
+ if e.code == 404:
192
+ continue
193
+ raise
194
+ return None, None
195
+
196
+
197
+ def get_repo_tree_with_branch(owner, repo, branch="main"):
198
+ """
199
+ Fetch the Git tree for a repository, returning (actual_branch, tree_data).
200
+ Tries branch first, then fallback to master/main, then checks repository info for default_branch.
201
+ """
202
+ branches_to_try = [branch]
203
+ if branch == "main":
204
+ branches_to_try.append("master")
205
+ elif branch == "master":
206
+ branches_to_try.append("main")
207
+
208
+ for b in branches_to_try:
209
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{b}?recursive=1"
210
+ try:
211
+ return b, get_json(url)
212
+ except urllib.error.HTTPError as e:
213
+ if e.code == 404:
214
+ continue
215
+ raise
216
+
217
+ # Query repository info to discover actual default branch (e.g. develop, trunk, etc.)
176
218
  try:
177
- return get_json(url)
178
- except urllib.error.HTTPError as e:
179
- if e.code == 404 and branch == "main":
180
- url_master = f"https://api.github.com/repos/{owner}/{repo}/git/trees/master?recursive=1"
181
- try:
182
- return get_json(url_master)
183
- except Exception:
184
- pass
185
- raise
219
+ info_url = f"https://api.github.com/repos/{owner}/{repo}"
220
+ info = get_json(info_url)
221
+ def_branch = info.get("default_branch")
222
+ if def_branch and def_branch not in branches_to_try:
223
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{def_branch}?recursive=1"
224
+ return def_branch, get_json(url)
225
+ except Exception:
226
+ pass
227
+
228
+ raise urllib.error.HTTPError(
229
+ f"https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}",
230
+ 404,
231
+ "Branch tree not found",
232
+ {},
233
+ None,
234
+ )
235
+
236
+
237
+ def get_repo_tree(owner, repo, branch="main"):
238
+ """Fetch the Git tree for a repository (backwards compatible helper)."""
239
+ _, tree_data = get_repo_tree_with_branch(owner, repo, branch)
240
+ return tree_data
186
241
 
187
242
 
188
243
  def get_user_repos(owner):
189
- """Fetch public repositories for a given GitHub owner."""
244
+ """Fetch public repositories list for a user."""
190
245
  url = f"https://api.github.com/users/{owner}/repos?per_page=100&sort=updated"
191
- try:
192
- return get_json(url)
193
- except Exception:
194
- return []
246
+ return get_json(url)
195
247
 
196
248
 
197
249
  def list_repos(owner=None, default_repo=None):
198
- """List all public repositories for the owner."""
250
+ """List all public repositories for a GitHub user."""
199
251
  if not owner:
200
- print("Error: No GitHub user specified.\n")
201
- print("Usage:")
202
- print(" codefetch -R <username>")
203
- print(" codefetch -R -u <username>")
252
+ print("Error: No GitHub username specified.")
253
+ print("Usage: codefetch --repos <username>")
204
254
  return
205
255
 
206
256
  try:
@@ -209,15 +259,17 @@ def list_repos(owner=None, default_repo=None):
209
259
  print(f"No public repositories found for user '{owner}'.")
210
260
  return
211
261
 
212
- print(f"Repositories for '{owner}':\n")
213
- for r in repos:
262
+ print(f"Public repositories for '{owner}':\n")
263
+ for i, r in enumerate(repos, 1):
214
264
  name = r.get("name", "")
215
- desc = r.get("description") or ""
265
+ desc = r.get("description") or "No description"
216
266
  is_def = " (default)" if default_repo and name.lower() == default_repo.lower() else ""
217
267
  marker = "*" if is_def else "-"
218
- desc_text = f" - {desc}" if desc else ""
219
- print(f" {marker} {name}{is_def}{desc_text}")
268
+ print(f" {marker} {name:<22}{is_def}")
269
+ if desc and desc != "No description":
270
+ print(f" {desc}")
220
271
 
272
+ print(f"\nTotal: {len(repos)} repositories")
221
273
  print("\nCommands:")
222
274
  print(f" codefetch -l {owner}/<repo> List files in a repository")
223
275
  print(f" codefetch {owner}/<repo> <filename> Download a file from a repository")
@@ -241,7 +293,7 @@ def list_files(owner, repo, branch="main"):
241
293
  return
242
294
 
243
295
  try:
244
- data = get_repo_tree(owner, repo, branch)
296
+ actual_branch, data = get_repo_tree_with_branch(owner, repo, branch)
245
297
  if data.get("truncated"):
246
298
  print("Warning: GitHub returned a truncated file list.")
247
299
 
@@ -262,10 +314,10 @@ def list_files(owner, repo, branch="main"):
262
314
  files.sort(key=str.lower)
263
315
 
264
316
  if not files:
265
- print(f"No supported code files found in {owner}/{repo}.")
317
+ print(f"No supported code files found in {owner}/{repo} ({actual_branch}).")
266
318
  return
267
319
 
268
- print(f"Available files in {owner}/{repo} ({branch}):\n")
320
+ print(f"Available files in {owner}/{repo} ({actual_branch}):\n")
269
321
  for i, path in enumerate(files, 1):
270
322
  print(f"{i:>3}. {path}")
271
323
 
@@ -368,50 +420,64 @@ def fetch_file(filename, owner=None, repo=None, branch=DEFAULT_BRANCH,
368
420
  with urllib.request.urlopen(req, timeout=15) as resp:
369
421
  return resp.read().decode("utf-8")
370
422
 
371
- # 1. Direct raw fetch first (fastest, preserves GitHub API rate limit)
372
- raw_base = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}"
373
- url = f"{raw_base}/{clean_filename}"
374
-
375
423
  content = None
376
424
  resolved_path = clean_filename
425
+ actual_branch = branch
377
426
 
427
+ # 1. Direct raw fetch first (fastest, preserves GitHub API rate limit)
378
428
  try:
379
- content = download_url(url)
429
+ found_branch, raw_content = fetch_raw_direct(owner, repo, branch, clean_filename)
430
+ if raw_content is not None:
431
+ actual_branch = found_branch
432
+ content = raw_content
433
+ except UnicodeDecodeError:
434
+ print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
435
+ return
380
436
  except urllib.error.HTTPError as e:
381
- if e.code == 404:
382
- # 2. Direct fetch 404ed. Try smart resolution with repo tree
383
- try:
384
- tree_data = get_repo_tree(owner, repo, branch)
385
- matched = resolve_file_in_tree(tree_data, clean_filename)
386
- if matched:
387
- resolved_path = matched
388
- content = download_url(f"{raw_base}/{resolved_path}")
389
- if resolved_path != clean_filename:
390
- print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
391
- else:
392
- # Check other public repositories for the same user
393
- other_repo, other_match = find_file_in_other_repos(owner, repo, clean_filename)
394
- if other_repo:
395
- print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
396
- print(f"\n[Tip] Found '{other_match}' in repository '{other_repo}'!")
397
- print(f"To fetch it, run:\n codefetch {owner}/{other_repo} {other_match}")
398
- return
399
- else:
400
- print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
401
- print(f"Run 'codefetch -l {owner}/{repo}' to see available files.")
402
- return
403
- except Exception:
404
- print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
405
- return
406
- else:
407
- print(f"Download failed: HTTP {e.code}")
408
- return
437
+ print(f"Download failed: HTTP {e.code}")
438
+ return
409
439
  except urllib.error.URLError:
410
440
  print("Could not connect to GitHub.")
411
441
  return
412
- except UnicodeDecodeError:
413
- print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
414
- return
442
+
443
+ # 2. If direct fetch didn't find the file (404), fall back to smart repository tree search
444
+ if content is None:
445
+ try:
446
+ actual_branch, tree_data = get_repo_tree_with_branch(owner, repo, branch)
447
+ matched = resolve_file_in_tree(tree_data, clean_filename)
448
+ if matched:
449
+ resolved_path = matched
450
+ raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{actual_branch}/{resolved_path}"
451
+ content = download_url(raw_url)
452
+ if resolved_path != clean_filename:
453
+ print(f"(Resolved '{clean_filename}' -> '{resolved_path}')")
454
+ else:
455
+ # Check other public repositories for the same user
456
+ other_repo, other_match = find_file_in_other_repos(owner, repo, clean_filename)
457
+ if other_repo:
458
+ print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
459
+ print(f"\n[Tip] Found '{other_match}' in repository '{other_repo}'!")
460
+ print(f"To fetch it, run:\n codefetch {owner}/{other_repo} {other_match}")
461
+ return
462
+ else:
463
+ print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
464
+ print(f"Run 'codefetch -l {owner}/{repo}' to see available files.")
465
+ return
466
+ except urllib.error.HTTPError as e:
467
+ if e.code == 404:
468
+ print(f"Repository or branch not found: {owner}/{repo} ({branch})")
469
+ else:
470
+ print(f"Could not search repository: HTTP {e.code}")
471
+ return
472
+ except urllib.error.URLError:
473
+ print("Could not connect to GitHub.")
474
+ return
475
+ except UnicodeDecodeError:
476
+ print("The file is not UTF-8 text, so it cannot be displayed or saved as text.")
477
+ return
478
+ except Exception:
479
+ print(f"File not found: '{clean_filename}' in {owner}/{repo}.")
480
+ return
415
481
 
416
482
  if content is None:
417
483
  return
@@ -423,7 +489,7 @@ def fetch_file(filename, owner=None, repo=None, branch=DEFAULT_BRANCH,
423
489
  try:
424
490
  with open(out_name, "w", encoding="utf-8") as f:
425
491
  f.write(content)
426
- print(f"Downloaded: {out_name} (from {owner}/{repo})")
492
+ print(f"Downloaded: {out_name} (from {owner}/{repo}@{actual_branch})")
427
493
  except Exception as e:
428
494
  print(f"Error saving file '{out_name}': {e}")
429
495
 
@@ -440,76 +506,27 @@ class CodefetchParser(argparse.ArgumentParser):
440
506
 
441
507
 
442
508
  def build_parser():
443
- """Build argument parser with full CLI options."""
444
509
  parser = CodefetchParser(
445
510
  prog="codefetch",
446
- add_help=True,
447
- )
448
- parser.add_argument(
449
- "args",
450
- nargs="*",
451
- help="Target specification: <owner>/<repo> <filename> or <filename>",
452
- )
453
- parser.add_argument(
454
- "-r", "--repo",
455
- default=None,
456
- help="Repository name or URL (e.g. repo or owner/repo)",
457
- )
458
- parser.add_argument(
459
- "-u", "--user",
460
- default=None,
461
- help="GitHub user/org name",
462
- )
463
- parser.add_argument(
464
- "-b", "--branch",
465
- default=None,
466
- help=f"Branch name (default: {DEFAULT_BRANCH})",
467
- )
468
- parser.add_argument(
469
- "-l", "--list",
470
- action="store_true",
471
- help="List available code files in the repository",
472
- )
473
- parser.add_argument(
474
- "-R", "--repos",
475
- action="store_true",
476
- help="List all public repositories for a user",
477
- )
478
- parser.add_argument(
479
- "-s", "--show",
480
- action="store_true",
481
- help="Display file content in terminal without saving to disk",
482
- )
483
- parser.add_argument(
484
- "-o", "--output",
485
- default=None,
486
- help="Custom destination filename or output path",
487
- )
488
- parser.add_argument(
489
- "--set-default",
490
- metavar="REPO",
491
- help="Set persistent default repository in local config",
492
- )
493
- parser.add_argument(
494
- "--set-default-user",
495
- metavar="USER",
496
- help="Set persistent default GitHub user in local config",
497
- )
498
- parser.add_argument(
499
- "--config",
500
- action="store_true",
501
- help="Display current CodeFetch configuration",
502
- )
503
- parser.add_argument(
504
- "--reset-config",
505
- action="store_true",
506
- help="Reset configuration back to defaults",
507
- )
508
- parser.add_argument(
509
- "-v", "--version",
510
- action="version",
511
- version="codefetch 1.2.2",
511
+ description="Fetch code files from GitHub repositories.",
512
+ add_help=False,
512
513
  )
514
+
515
+ parser.add_argument("args", nargs="*", help="Repository and filename targets")
516
+ parser.add_argument("-s", "--show", action="store_true", help="Print file content to terminal")
517
+ parser.add_argument("-l", "--list", action="store_true", help="List code files in repository")
518
+ parser.add_argument("-R", "--repos", action="store_true", help="List public repositories for a user")
519
+ parser.add_argument("-r", "--repo", type=str, help="Repository name or URL")
520
+ parser.add_argument("-u", "--user", type=str, help="GitHub username")
521
+ parser.add_argument("-b", "--branch", type=str, help="Branch name (default: main)")
522
+ parser.add_argument("-o", "--output", type=str, help="Custom output filename or path")
523
+ parser.add_argument("--set-default", type=str, help="Save default repository (e.g. owner/repo)")
524
+ parser.add_argument("--set-default-user", type=str, help="Save default GitHub user")
525
+ parser.add_argument("--config", action="store_true", help="Display active configuration")
526
+ parser.add_argument("--reset-config", action="store_true", help="Reset configuration back to defaults")
527
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
528
+ parser.add_argument("-h", "--help", action="help", help="Show help message")
529
+
513
530
  return parser
514
531
 
515
532
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codefetch-cli
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Quickly fetch code files from GitHub repositories with simple commands
5
5
  Home-page: https://github.com/InzamamulQureshi/Codefetch
6
6
  Author: Inzamamul Qureshi
@@ -24,7 +24,7 @@ Dynamic: requires-python
24
24
 
25
25
  Quickly fetch code files from GitHub repositories with simple commands.
26
26
 
27
- CodeFetch is a lightweight command-line utility designed to quickly inspect, preview, and download individual source code files directly from any GitHub repository without needing to clone full repositories.
27
+ CodeFetch is a lightweight command-line utility and Python library designed to quickly inspect, preview, and download individual source code files directly from any GitHub repository without needing to clone full repositories.
28
28
 
29
29
  ---
30
30
 
@@ -35,6 +35,7 @@ CodeFetch is a lightweight command-line utility designed to quickly inspect, pre
35
35
  - **Automatic Path & Case Resolution**: Automatically resolves case mismatches and finds files nested inside subdirectories.
36
36
  - **Repository Discovery**: List all public repositories for any GitHub user with `--repos`.
37
37
  - **Tree Exploration**: List all supported code files within a repository with `--list`.
38
+ - **Python Library Support**: Import and use programmatically with `import codefetch as cf`.
38
39
  - **Optional Persistent Defaults**: Save a default repository or user with `--set-default` if you frequently work with the same repository.
39
40
  - **Zero Runtime Dependencies**: Built using only the Python standard library with zero third-party dependencies.
40
41
 
@@ -130,6 +131,28 @@ codefetch --reset-config
130
131
 
131
132
  ---
132
133
 
134
+ ## Python API Usage
135
+
136
+ You can also use CodeFetch programmatically within your Python scripts:
137
+
138
+ ```python
139
+ import codefetch as cf
140
+
141
+ # 1. Fetch file content as a string directly in memory
142
+ makefile_text = cf.get("torvalds/linux", "Makefile")
143
+ # Or shorthand single argument syntax:
144
+ readme_text = cf.get("psf/requests/README.md")
145
+
146
+ # 2. Download a file to disk
147
+ cf.download("torvalds/linux", "Makefile", output="linux_Makefile")
148
+
149
+ # 3. List all code files in a repository
150
+ files = cf.list("torvalds/linux")
151
+ print(f"Found {len(files)} files")
152
+ ```
153
+
154
+ ---
155
+
133
156
  ## Command Reference
134
157
 
135
158
  | Flag | Description | Example |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codefetch-cli"
7
- version = "1.2.2"
7
+ version = "1.2.3"
8
8
  description = "Quickly fetch code files from GitHub repositories with simple commands"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,7 +7,7 @@ long_description = readme_path.read_text(encoding="utf-8") if readme_path.exists
7
7
 
8
8
  setup(
9
9
  name="codefetch-cli",
10
- version="1.2.2",
10
+ version="1.2.3",
11
11
  description="Quickly fetch code files from GitHub repositories with simple commands",
12
12
  long_description=long_description,
13
13
  long_description_content_type="text/markdown",
@@ -1,5 +0,0 @@
1
- """
2
- CodeFetch - CLI tool to fetch code files from GitHub repositories.
3
- """
4
-
5
- __version__ = "1.2.2"
File without changes
File without changes