gh-issue-lens 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammad El-Shennawy
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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include src *.py
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: gh-issue-lens
3
+ Version: 0.1.0
4
+ Summary: Find beginner-friendly GitHub issues and export them as JSONL for LLM analysis.
5
+ Author-email: Mohammad El-Shennawy <mohamedwork216@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Mohammad El-Shennawy
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://github.com/MohamedM216/gh-issue-lens
28
+ Project-URL: Repository, https://github.com/MohamedM216/gh-issue-lens
29
+ Project-URL: Issues, https://github.com/MohamedM216/gh-issue-lens/issues
30
+ Keywords: github,open-source,issues,llm,cli,developer-tools
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Environment :: Console
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Software Development :: Version Control :: Git
42
+ Classifier: Topic :: Utilities
43
+ Requires-Python: >=3.9
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: requests>=2.31.0
47
+ Provides-Extra: dev
48
+ Requires-Dist: build>=1.2.0; extra == "dev"
49
+ Requires-Dist: twine>=5.0.0; extra == "dev"
50
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
51
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ # Easier open source contribution process for newcomers
55
+
56
+ Stop digging through stale GitHub issues. **gh-issue-lens** finds unassigned, beginner-friendly open source issues in any repository and prepares them for LLM analysis in seconds. It bypasses API rate limits by grabbing issues, linked Pull Requests, assignees, and non-bot comments in a single network pass.
57
+
58
+ # How to use it?
59
+
60
+ ## Option 1: Install from PyPI
61
+
62
+ ```bash
63
+ pip install gh-issue-lens
64
+ ```
65
+ **Export your GitHub Personal Access Token:**
66
+
67
+ First of all, create your personal github token from your github account then use it.
68
+
69
+ ```bash
70
+ export GITHUB_TOKEN="your_personal_access_token"
71
+ ```
72
+
73
+ **Fetch issues:**
74
+
75
+ ```bash
76
+ gh-issue-lens kubernetes kubernetes -d 200
77
+ ```
78
+
79
+ **Filter by labels:**
80
+
81
+ ```bash
82
+ gh-issue-lens kubernetes kubernetes -l "good first issue" "help wanted" -d 50
83
+ ```
84
+
85
+ And for more commands, you can see the sections below. The guidelines are the same. The only difference is to use `gh-issue-lens` directly instead of `python3 cli.py`.
86
+
87
+ ## Option 2: Build it manually
88
+
89
+ **Clone the repo:**
90
+
91
+ ```bash
92
+ git clone https://github.com/MohamedM216/gh-issue-lens.git
93
+ ```
94
+
95
+ **Install requirements:**
96
+
97
+ ```bash
98
+ pip3 install requests
99
+ ```
100
+
101
+ **Export your GitHub Personal Access Token:**
102
+
103
+ First of all, create your personal github token from your github account then use it.
104
+
105
+ ```bash
106
+ export GITHUB_TOKEN="your_personal_access_token"
107
+ ```
108
+
109
+ **Verify it works:**
110
+
111
+ ```bash
112
+ cd src/gh_issue_lens/
113
+ python3 cli.py --help
114
+ ```
115
+
116
+ **Fetch 100 issues from a repository:**
117
+
118
+ 100 issues is the default depth you can change it using the -d option.
119
+
120
+ ```bash
121
+ python3 cli.py kubernetes kubernetes
122
+ ```
123
+
124
+ ### Filter by up to 5 labels (ORed) and set a depth limit:
125
+
126
+ This fetches a maximum of 50 issues that have EITHER the "good first issue", "help wanted", or "bug" label.
127
+
128
+ ```bash
129
+ python3 cli.py kubernetes kubernetes -l "good first issue" "help wanted" "bug" -d 50
130
+ ```
131
+
132
+ ### Save to a custom JSON file:
133
+
134
+ If you want to use a custom name for the output file, you can run:
135
+
136
+ ```bash
137
+ python3 cli.py kubernetes kubernetes -l "feature" -d 10 -o custom_dataset.json
138
+ ```
139
+
140
+ ### Incremental Saving & Resumability (Checkpointing):
141
+
142
+ When scraping massive repositories (like Kubernetes), hitting GitHub's API rate limits or network timeouts is common. You no longer have to worry about losing your progress!
143
+ * **JSONL Format:** Issues are now saved line-by-line instantly as they are fetched, ensuring zero data loss.
144
+ * **Auto-Resume:** The tool creates a hidden `.checkpoint` file tracking your progress. If the script crashes or you stop it, simply run the **exact same command** again. It will automatically resume from the exact page it left off, saving your API budget and time.
145
+
146
+ ### Exclude issues with open PRs:
147
+
148
+ ```bash
149
+ python3 cli.py kubernetes kubernetes --exclude-open-prs
150
+ ```
151
+
152
+ ### SQLite caching:
153
+
154
+ `gh-issue-lens` can cache already-exported issues in a local SQLite database. This prevents duplicate issues from being written again when you run the same export multiple times.
155
+
156
+ Enable caching:
157
+
158
+ ```bash
159
+ python3 cli.py kubernetes kubernetes -d 200 --cache
160
+ ```
161
+
162
+ By default, the cache is stored in `.gh_issue_lens_cache.db`. Use a custom cache database:
163
+
164
+ ```bash
165
+ python3 cli.py kubernetes kubernetes -d 200 --cache --cache-db my_cache.db
166
+ ```
167
+
168
+ Run again with caching enabled. Issues that were already exported in previous runs will be skipped. You can also enable early stopping when the tool keeps finding only cached issues:
169
+
170
+ ```bash
171
+ python3 cli.py kubernetes kubernetes -d 200 --cache --cache-stop
172
+ ```
173
+
174
+ This is useful when re-running the tool against the same repository and you only want newly discovered issues. To reset the cache, delete the cache file:
175
+
176
+ ```bash
177
+ rm .gh_issue_lens_cache.db
178
+ ```
179
+
180
+ Caching is different from checkpointing: checkpointing helps resume a crashed or interrupted run, while caching helps avoid exporting the same issues again across separate runs.
181
+
182
+ ## What's next?
183
+
184
+ The generated JSON file is suitable to be used as context for Large Language Models (like Gemini, Claude, or ChatGPT). You can upload the output file to an LLM and use targeted prompts to find the perfect issue to work on.
185
+
186
+ **Example Prompts to try:**
187
+
188
+ > "I am a newcomer to this repository. Based on the attached JSON, find an issue that is easy to implement, has no active PRs, and doesn't have a long, debated comment history."
189
+
190
+ > "Filter these issues and list only the ones related to [backend / frontend / database / specific component]."
191
+
192
+ > "Read the comments on these issues. Which maintainers seem the most responsive and welcoming to beginners? Point me to issues authored or reviewed by them."
193
+
194
+ > "Summarize the technical requirements for issue #123 based on its description and comment thread. What files should I look at first?"
195
+
196
+ *Don't forget to STAR the repository :)*
197
+
198
+ *Happy hacking!*
@@ -0,0 +1,145 @@
1
+ # Easier open source contribution process for newcomers
2
+
3
+ Stop digging through stale GitHub issues. **gh-issue-lens** finds unassigned, beginner-friendly open source issues in any repository and prepares them for LLM analysis in seconds. It bypasses API rate limits by grabbing issues, linked Pull Requests, assignees, and non-bot comments in a single network pass.
4
+
5
+ # How to use it?
6
+
7
+ ## Option 1: Install from PyPI
8
+
9
+ ```bash
10
+ pip install gh-issue-lens
11
+ ```
12
+ **Export your GitHub Personal Access Token:**
13
+
14
+ First of all, create your personal github token from your github account then use it.
15
+
16
+ ```bash
17
+ export GITHUB_TOKEN="your_personal_access_token"
18
+ ```
19
+
20
+ **Fetch issues:**
21
+
22
+ ```bash
23
+ gh-issue-lens kubernetes kubernetes -d 200
24
+ ```
25
+
26
+ **Filter by labels:**
27
+
28
+ ```bash
29
+ gh-issue-lens kubernetes kubernetes -l "good first issue" "help wanted" -d 50
30
+ ```
31
+
32
+ And for more commands, you can see the sections below. The guidelines are the same. The only difference is to use `gh-issue-lens` directly instead of `python3 cli.py`.
33
+
34
+ ## Option 2: Build it manually
35
+
36
+ **Clone the repo:**
37
+
38
+ ```bash
39
+ git clone https://github.com/MohamedM216/gh-issue-lens.git
40
+ ```
41
+
42
+ **Install requirements:**
43
+
44
+ ```bash
45
+ pip3 install requests
46
+ ```
47
+
48
+ **Export your GitHub Personal Access Token:**
49
+
50
+ First of all, create your personal github token from your github account then use it.
51
+
52
+ ```bash
53
+ export GITHUB_TOKEN="your_personal_access_token"
54
+ ```
55
+
56
+ **Verify it works:**
57
+
58
+ ```bash
59
+ cd src/gh_issue_lens/
60
+ python3 cli.py --help
61
+ ```
62
+
63
+ **Fetch 100 issues from a repository:**
64
+
65
+ 100 issues is the default depth you can change it using the -d option.
66
+
67
+ ```bash
68
+ python3 cli.py kubernetes kubernetes
69
+ ```
70
+
71
+ ### Filter by up to 5 labels (ORed) and set a depth limit:
72
+
73
+ This fetches a maximum of 50 issues that have EITHER the "good first issue", "help wanted", or "bug" label.
74
+
75
+ ```bash
76
+ python3 cli.py kubernetes kubernetes -l "good first issue" "help wanted" "bug" -d 50
77
+ ```
78
+
79
+ ### Save to a custom JSON file:
80
+
81
+ If you want to use a custom name for the output file, you can run:
82
+
83
+ ```bash
84
+ python3 cli.py kubernetes kubernetes -l "feature" -d 10 -o custom_dataset.json
85
+ ```
86
+
87
+ ### Incremental Saving & Resumability (Checkpointing):
88
+
89
+ When scraping massive repositories (like Kubernetes), hitting GitHub's API rate limits or network timeouts is common. You no longer have to worry about losing your progress!
90
+ * **JSONL Format:** Issues are now saved line-by-line instantly as they are fetched, ensuring zero data loss.
91
+ * **Auto-Resume:** The tool creates a hidden `.checkpoint` file tracking your progress. If the script crashes or you stop it, simply run the **exact same command** again. It will automatically resume from the exact page it left off, saving your API budget and time.
92
+
93
+ ### Exclude issues with open PRs:
94
+
95
+ ```bash
96
+ python3 cli.py kubernetes kubernetes --exclude-open-prs
97
+ ```
98
+
99
+ ### SQLite caching:
100
+
101
+ `gh-issue-lens` can cache already-exported issues in a local SQLite database. This prevents duplicate issues from being written again when you run the same export multiple times.
102
+
103
+ Enable caching:
104
+
105
+ ```bash
106
+ python3 cli.py kubernetes kubernetes -d 200 --cache
107
+ ```
108
+
109
+ By default, the cache is stored in `.gh_issue_lens_cache.db`. Use a custom cache database:
110
+
111
+ ```bash
112
+ python3 cli.py kubernetes kubernetes -d 200 --cache --cache-db my_cache.db
113
+ ```
114
+
115
+ Run again with caching enabled. Issues that were already exported in previous runs will be skipped. You can also enable early stopping when the tool keeps finding only cached issues:
116
+
117
+ ```bash
118
+ python3 cli.py kubernetes kubernetes -d 200 --cache --cache-stop
119
+ ```
120
+
121
+ This is useful when re-running the tool against the same repository and you only want newly discovered issues. To reset the cache, delete the cache file:
122
+
123
+ ```bash
124
+ rm .gh_issue_lens_cache.db
125
+ ```
126
+
127
+ Caching is different from checkpointing: checkpointing helps resume a crashed or interrupted run, while caching helps avoid exporting the same issues again across separate runs.
128
+
129
+ ## What's next?
130
+
131
+ The generated JSON file is suitable to be used as context for Large Language Models (like Gemini, Claude, or ChatGPT). You can upload the output file to an LLM and use targeted prompts to find the perfect issue to work on.
132
+
133
+ **Example Prompts to try:**
134
+
135
+ > "I am a newcomer to this repository. Based on the attached JSON, find an issue that is easy to implement, has no active PRs, and doesn't have a long, debated comment history."
136
+
137
+ > "Filter these issues and list only the ones related to [backend / frontend / database / specific component]."
138
+
139
+ > "Read the comments on these issues. Which maintainers seem the most responsive and welcoming to beginners? Point me to issues authored or reviewed by them."
140
+
141
+ > "Summarize the technical requirements for issue #123 based on its description and comment thread. What files should I look at first?"
142
+
143
+ *Don't forget to STAR the repository :)*
144
+
145
+ *Happy hacking!*
@@ -0,0 +1,100 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=68",
4
+ "wheel",
5
+ ]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [project]
9
+ name = "gh-issue-lens"
10
+ description = "Find beginner-friendly GitHub issues and export them as JSONL for LLM analysis."
11
+ dynamic = [
12
+ "version",
13
+ "readme",
14
+ ]
15
+ requires-python = ">=3.9"
16
+ license = { file = "LICENSE" }
17
+ authors = [
18
+ { name = "Mohammad El-Shennawy", email = "mohamedwork216@gmail.com" },
19
+ ]
20
+ keywords = [
21
+ "github",
22
+ "open-source",
23
+ "issues",
24
+ "llm",
25
+ "cli",
26
+ "developer-tools",
27
+ ]
28
+ classifiers = [
29
+ "Development Status :: 4 - Beta",
30
+ "Environment :: Console",
31
+ "Intended Audience :: Developers",
32
+ "License :: OSI Approved :: MIT License",
33
+ "Operating System :: OS Independent",
34
+ "Programming Language :: Python :: 3",
35
+ "Programming Language :: Python :: 3.9",
36
+ "Programming Language :: Python :: 3.10",
37
+ "Programming Language :: Python :: 3.11",
38
+ "Programming Language :: Python :: 3.12",
39
+ "Topic :: Software Development :: Version Control :: Git",
40
+ "Topic :: Utilities",
41
+ ]
42
+ dependencies = [
43
+ "requests>=2.31.0",
44
+ ]
45
+
46
+ [project.optional-dependencies]
47
+ dev = [
48
+ "build>=1.2.0",
49
+ "twine>=5.0.0",
50
+ "pytest>=8.0.0",
51
+ "ruff>=0.5.0",
52
+ ]
53
+
54
+ [project.scripts]
55
+ gh-issue-lens = "gh_issue_lens.cli:main"
56
+
57
+ [project.urls]
58
+ Homepage = "https://github.com/MohamedM216/gh-issue-lens"
59
+ Repository = "https://github.com/MohamedM216/gh-issue-lens"
60
+ Issues = "https://github.com/MohamedM216/gh-issue-lens/issues"
61
+
62
+ [tool.setuptools.dynamic]
63
+ version = { attr = "gh_issue_lens.__version__" }
64
+ readme = { file = "README.md", content-type = "text/markdown" }
65
+
66
+ [tool.setuptools.package-dir]
67
+ "" = "src"
68
+
69
+ [tool.setuptools.packages.find]
70
+ where = [
71
+ "src",
72
+ ]
73
+
74
+ [tool.ruff]
75
+ line-length = 100
76
+ target-version = "py311"
77
+ exclude = [
78
+ ".git",
79
+ ".venv",
80
+ "venv",
81
+ "__pycache__",
82
+ ".mypy_cache",
83
+ ".ruff_cache",
84
+ "node_modules",
85
+ "dist",
86
+ "build",
87
+ ]
88
+
89
+ [tool.ruff.lint]
90
+ select = [
91
+ "E",
92
+ "F",
93
+ "W",
94
+ "I",
95
+ ]
96
+ fixable = ["ALL"]
97
+
98
+ [tool.ruff.format]
99
+ indent-style = "space"
100
+ quote-style = "double"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """gh-issue-lens package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,136 @@
1
+ """Command line interface for gh-issue-lens."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ from . import __version__
8
+ from .core import DEFAULT_CACHE_DB, fetch_issues
9
+
10
+
11
+ def main():
12
+ parser = argparse.ArgumentParser(
13
+ prog="gh-issue-lens",
14
+ description=(
15
+ "Easier open source contribution process for newcomers. "
16
+ "Fetch open issues from a GitHub repository and export them "
17
+ "as JSONL for LLM analysis."
18
+ ),
19
+ )
20
+
21
+ parser.add_argument(
22
+ "--version",
23
+ action="version",
24
+ version=f"%(prog)s {__version__}",
25
+ )
26
+
27
+ parser.add_argument(
28
+ "owner",
29
+ help="GitHub repository owner (e.g., 'kubernetes')",
30
+ )
31
+
32
+ parser.add_argument(
33
+ "repo",
34
+ help="GitHub repository name (e.g., 'kubernetes')",
35
+ )
36
+
37
+ parser.add_argument(
38
+ "-l",
39
+ "--labels",
40
+ nargs="+",
41
+ help=(
42
+ "Filter by up to 5 labels. Labels are ORed "
43
+ "(e.g., -l 'good first issue' 'help wanted')"
44
+ ),
45
+ default=[],
46
+ )
47
+
48
+ parser.add_argument(
49
+ "-d",
50
+ "--depth",
51
+ type=int,
52
+ help="Maximum number of issues to fetch (default: 100)",
53
+ default=100,
54
+ )
55
+
56
+ parser.add_argument(
57
+ "-o",
58
+ "--output",
59
+ help="Output JSONL filename",
60
+ default=None,
61
+ )
62
+
63
+ parser.add_argument(
64
+ "--exclude-open-prs",
65
+ action="store_true",
66
+ help="Skip issues that already have an open pull request against them.",
67
+ )
68
+
69
+ parser.add_argument(
70
+ "--cache",
71
+ action="store_true",
72
+ help="Enable SQLite caching to avoid exporting issues seen in previous runs.",
73
+ )
74
+
75
+ parser.add_argument(
76
+ "--cache-db",
77
+ default=DEFAULT_CACHE_DB,
78
+ help=f"Path to SQLite cache file. Default: {DEFAULT_CACHE_DB}",
79
+ )
80
+
81
+ parser.add_argument(
82
+ "--cache-stop",
83
+ action="store_true",
84
+ help=(
85
+ "Stop early when multiple consecutive pages contain "
86
+ "only cached eligible issues."
87
+ ),
88
+ )
89
+
90
+ args = parser.parse_args()
91
+
92
+ if len(args.labels) > 5:
93
+ print("Error: You can specify a maximum of 5 labels.")
94
+ sys.exit(1)
95
+
96
+ token = os.getenv("GITHUB_TOKEN")
97
+
98
+ if not token:
99
+ print("Error: GITHUB_TOKEN environment variable not set.")
100
+ print("Please run: export GITHUB_TOKEN='your_personal_access_token'")
101
+ sys.exit(1)
102
+
103
+ output_filename = args.output if args.output else f"{args.repo}_issues.jsonl"
104
+ checkpoint_file = f".checkpoint_{args.owner}_{args.repo}.json"
105
+
106
+ # If cache is disabled and there is no checkpoint, start with a fresh output file.
107
+ # If cache is enabled, keep the old output file and append only new issues.
108
+ if not os.path.exists(checkpoint_file) and not args.cache:
109
+ open(output_filename, "w", encoding="utf-8").close()
110
+
111
+ try:
112
+ total_fetched = fetch_issues(
113
+ owner=args.owner,
114
+ repo=args.repo,
115
+ labels=args.labels,
116
+ depth=args.depth,
117
+ token=token,
118
+ output_filename=output_filename,
119
+ exclude_open_prs=args.exclude_open_prs,
120
+ use_cache=args.cache,
121
+ cache_db=args.cache_db,
122
+ cache_stop=args.cache_stop,
123
+ )
124
+
125
+ print(f"\nDone! Scraped {total_fetched} issues in this run.")
126
+ print(f"Data saved to {output_filename} (JSONL format)")
127
+
128
+ except KeyboardInterrupt:
129
+ print("\nProcess interrupted by user.")
130
+ print("Progress has been saved to the checkpoint file.")
131
+ print("Run the same command again to resume.")
132
+ sys.exit(0)
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
@@ -0,0 +1,390 @@
1
+ """Core logic for gh-issue-lens."""
2
+
3
+ import json
4
+ import os
5
+ import sqlite3
6
+ import sys
7
+ from datetime import datetime, timezone
8
+
9
+ import requests
10
+
11
+ DEFAULT_CACHE_DB = ".gh_issue_lens_cache.db"
12
+
13
+
14
+ def get_graphql_query():
15
+ """
16
+ GraphQL query used to fetch open issues.
17
+
18
+ The page size is intentionally small to reduce the chance of timeouts
19
+ and GitHub 502 responses on very large repositories.
20
+ """
21
+ return """
22
+ query($owner: String!, $repo: String!, $cursor: String) {
23
+ repository(owner: $owner, name: $repo) {
24
+ issues(
25
+ first: 20,
26
+ after: $cursor,
27
+ states: OPEN,
28
+ orderBy: {
29
+ field: CREATED_AT,
30
+ direction: DESC
31
+ }
32
+ ) {
33
+ pageInfo {
34
+ hasNextPage
35
+ endCursor
36
+ }
37
+ nodes {
38
+ number
39
+ title
40
+ url
41
+ createdAt
42
+ body
43
+ labels(first: 10) {
44
+ nodes {
45
+ name
46
+ }
47
+ }
48
+ assignees(first: 10) {
49
+ nodes {
50
+ login
51
+ }
52
+ }
53
+ comments(first: 20) {
54
+ nodes {
55
+ author {
56
+ login
57
+ __typename
58
+ }
59
+ body
60
+ }
61
+ }
62
+ timelineItems(itemTypes: [CROSS_REFERENCED_EVENT], first: 10) {
63
+ nodes {
64
+ ... on CrossReferencedEvent {
65
+ source {
66
+ ... on PullRequest {
67
+ state
68
+ number
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ """
79
+
80
+
81
+ def init_cache(cache_db):
82
+ """Create or open the SQLite cache."""
83
+ conn = sqlite3.connect(cache_db)
84
+
85
+ conn.execute(
86
+ """
87
+ CREATE TABLE IF NOT EXISTS cached_issues (
88
+ repo TEXT NOT NULL,
89
+ issue_number INTEGER NOT NULL,
90
+ url TEXT,
91
+ cached_at TEXT NOT NULL,
92
+ PRIMARY KEY (repo, issue_number)
93
+ )
94
+ """
95
+ )
96
+
97
+ conn.commit()
98
+ return conn
99
+
100
+
101
+ def is_issue_cached(conn, repo_full_name, issue_number):
102
+ """Return True if this issue number is already cached."""
103
+ cur = conn.execute(
104
+ """
105
+ SELECT 1
106
+ FROM cached_issues
107
+ WHERE repo = ? AND issue_number = ?
108
+ LIMIT 1
109
+ """,
110
+ (repo_full_name.lower(), issue_number),
111
+ )
112
+
113
+ return cur.fetchone() is not None
114
+
115
+
116
+ def cache_issues(conn, repo_full_name, issues):
117
+ """Insert newly exported issues into the cache."""
118
+ if not issues:
119
+ return
120
+
121
+ now = datetime.now(timezone.utc).isoformat()
122
+
123
+ rows = [
124
+ (
125
+ repo_full_name.lower(),
126
+ issue["number"],
127
+ issue.get("url"),
128
+ now,
129
+ )
130
+ for issue in issues
131
+ ]
132
+
133
+ conn.executemany(
134
+ """
135
+ INSERT OR IGNORE INTO cached_issues (
136
+ repo,
137
+ issue_number,
138
+ url,
139
+ cached_at
140
+ )
141
+ VALUES (?, ?, ?, ?)
142
+ """,
143
+ rows,
144
+ )
145
+
146
+ conn.commit()
147
+
148
+
149
+ def fetch_issues(
150
+ owner,
151
+ repo,
152
+ labels,
153
+ depth,
154
+ token,
155
+ output_filename,
156
+ exclude_open_prs=False,
157
+ use_cache=False,
158
+ cache_db=DEFAULT_CACHE_DB,
159
+ cache_stop=False,
160
+ ):
161
+ """
162
+ Fetch issues from GitHub and append them to a JSONL file.
163
+
164
+ Returns the number of newly exported issues in this run.
165
+ """
166
+ url = "https://api.github.com/graphql"
167
+
168
+ headers = {
169
+ "Authorization": f"Bearer {token}",
170
+ "User-Agent": "gh-issue-lens",
171
+ }
172
+
173
+ query = get_graphql_query()
174
+
175
+ has_labels = bool(labels and len(labels) > 0)
176
+ target_labels_lower = [label.lower() for label in labels] if has_labels else []
177
+
178
+ has_next_page = True
179
+ cursor = None
180
+ page_num = 1
181
+ extracted_count = 0
182
+
183
+ checkpoint_file = f".checkpoint_{owner}_{repo}.json"
184
+
185
+ conn = init_cache(cache_db) if use_cache else None
186
+ repo_key = f"{owner}/{repo}".lower()
187
+
188
+ cached_skipped = 0
189
+ no_new_pages = 0
190
+
191
+ if os.path.exists(checkpoint_file):
192
+ try:
193
+ with open(checkpoint_file, "r", encoding="utf-8") as f:
194
+ cp_data = json.load(f)
195
+
196
+ cursor = cp_data.get("cursor")
197
+ page_num = cp_data.get("page_num", 1)
198
+ extracted_count = cp_data.get("extracted_count", 0)
199
+
200
+ print(
201
+ f"Resuming from checkpoint. Starting at page {page_num}, "
202
+ f"{extracted_count} issues already fetched."
203
+ )
204
+ except Exception as e:
205
+ print(f"Warning: Could not read checkpoint file: {e}")
206
+
207
+ label_text = f" (Filtering by ANY of: {', '.join(labels)})" if has_labels else ""
208
+
209
+ print(f"Starting extraction for {owner}/{repo}{label_text}...")
210
+ print(f"Target depth: {depth} matches.")
211
+
212
+ try:
213
+ while has_next_page and extracted_count < depth:
214
+ variables = {
215
+ "owner": owner,
216
+ "repo": repo,
217
+ "cursor": cursor,
218
+ }
219
+
220
+ print(f" -> Scanning page {page_num} (Fetching up to 20 open issues from GitHub)...")
221
+
222
+ try:
223
+ response = requests.post(
224
+ url,
225
+ json={
226
+ "query": query,
227
+ "variables": variables,
228
+ },
229
+ headers=headers,
230
+ timeout=60,
231
+ )
232
+ except requests.exceptions.RequestException as exc:
233
+ print(f"Network error: {exc}")
234
+ print("Progress saved to checkpoint. Run the command again to resume.")
235
+ sys.exit(1)
236
+
237
+ if response.status_code != 200:
238
+ print(f"Error {response.status_code}: {response.text}")
239
+ print("Progress saved to checkpoint. Run the command again to resume.")
240
+ sys.exit(1)
241
+
242
+ try:
243
+ data = response.json()
244
+ except ValueError as exc:
245
+ print(f"Could not parse GitHub response: {exc}")
246
+ print("Progress saved to checkpoint. Run the command again to resume.")
247
+ sys.exit(1)
248
+
249
+ if "errors" in data:
250
+ print(f"GraphQL Error: {data['errors'][0]['message']}")
251
+ print("Progress saved to checkpoint. Run the command again to resume.")
252
+ sys.exit(1)
253
+
254
+ repository = data.get("data", {}).get("repository")
255
+
256
+ if not repository:
257
+ print(f"Repository {owner}/{repo} not found.")
258
+ sys.exit(1)
259
+
260
+ issues_data = repository.get("issues", {})
261
+ nodes = issues_data.get("nodes", [])
262
+
263
+ page_issues = []
264
+ eligible_before_cache = 0
265
+
266
+ for issue in nodes:
267
+ if not issue:
268
+ continue
269
+
270
+ issue_labels = [
271
+ label_node["name"].lower()
272
+ for label_node in issue.get("labels", {}).get("nodes", [])
273
+ if label_node
274
+ ]
275
+
276
+ if has_labels:
277
+ if not any(label in issue_labels for label in target_labels_lower):
278
+ continue
279
+
280
+ assignees = [
281
+ assignee["login"]
282
+ for assignee in issue.get("assignees", {}).get("nodes", [])
283
+ if assignee
284
+ ]
285
+
286
+ comments = []
287
+
288
+ for comment in issue.get("comments", {}).get("nodes", []):
289
+ if not comment:
290
+ continue
291
+
292
+ author = comment.get("author")
293
+
294
+ if author and author.get("__typename") != "Bot":
295
+ comments.append(
296
+ {
297
+ "author": author.get("login"),
298
+ "body": comment.get("body"),
299
+ }
300
+ )
301
+
302
+ has_open_pr = False
303
+
304
+ for event in issue.get("timelineItems", {}).get("nodes", []):
305
+ if not event:
306
+ continue
307
+
308
+ source = event.get("source")
309
+
310
+ if source and source.get("state") == "OPEN":
311
+ has_open_pr = True
312
+ break
313
+
314
+ if exclude_open_prs and has_open_pr:
315
+ continue
316
+
317
+ eligible_before_cache += 1
318
+
319
+ if conn is not None and is_issue_cached(conn, repo_key, issue.get("number")):
320
+ cached_skipped += 1
321
+ continue
322
+
323
+ page_issues.append(
324
+ {
325
+ "number": issue.get("number"),
326
+ "title": issue.get("title"),
327
+ "url": issue.get("url"),
328
+ "created_at": issue.get("createdAt"),
329
+ "is_assigned": len(assignees) > 0,
330
+ "assigned_to": assignees,
331
+ "has_open_pr_against_it": has_open_pr,
332
+ "labels": issue_labels,
333
+ "body": issue.get("body"),
334
+ "comments": comments,
335
+ }
336
+ )
337
+
338
+ if extracted_count + len(page_issues) >= depth:
339
+ break
340
+
341
+ with open(output_filename, "a", encoding="utf-8") as f:
342
+ for item in page_issues:
343
+ f.write(json.dumps(item) + "\n")
344
+
345
+ if conn is not None and page_issues:
346
+ cache_issues(conn, repo_key, page_issues)
347
+
348
+ extracted_count += len(page_issues)
349
+
350
+ page_info = issues_data.get("pageInfo", {})
351
+ has_next_page = page_info.get("hasNextPage", False)
352
+ cursor = page_info.get("endCursor")
353
+ page_num += 1
354
+
355
+ cache_should_stop = False
356
+
357
+ if conn is not None and cache_stop:
358
+ if eligible_before_cache > 0 and len(page_issues) == 0:
359
+ no_new_pages += 1
360
+
361
+ if no_new_pages >= 3:
362
+ cache_should_stop = True
363
+ else:
364
+ no_new_pages = 0
365
+
366
+ if has_next_page and extracted_count < depth and not cache_should_stop:
367
+ with open(checkpoint_file, "w", encoding="utf-8") as f:
368
+ json.dump(
369
+ {
370
+ "cursor": cursor,
371
+ "page_num": page_num,
372
+ "extracted_count": extracted_count,
373
+ },
374
+ f,
375
+ )
376
+ else:
377
+ if os.path.exists(checkpoint_file):
378
+ os.remove(checkpoint_file)
379
+
380
+ if cache_should_stop:
381
+ print("Stopping early: 3 consecutive pages contained only cached eligible issues.")
382
+ break
383
+
384
+ if use_cache:
385
+ print(f"Cached issues skipped: {cached_skipped}")
386
+
387
+ return extracted_count
388
+ finally:
389
+ if conn is not None:
390
+ conn.close()
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: gh-issue-lens
3
+ Version: 0.1.0
4
+ Summary: Find beginner-friendly GitHub issues and export them as JSONL for LLM analysis.
5
+ Author-email: Mohammad El-Shennawy <mohamedwork216@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Mohammad El-Shennawy
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://github.com/MohamedM216/gh-issue-lens
28
+ Project-URL: Repository, https://github.com/MohamedM216/gh-issue-lens
29
+ Project-URL: Issues, https://github.com/MohamedM216/gh-issue-lens/issues
30
+ Keywords: github,open-source,issues,llm,cli,developer-tools
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Environment :: Console
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Software Development :: Version Control :: Git
42
+ Classifier: Topic :: Utilities
43
+ Requires-Python: >=3.9
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: requests>=2.31.0
47
+ Provides-Extra: dev
48
+ Requires-Dist: build>=1.2.0; extra == "dev"
49
+ Requires-Dist: twine>=5.0.0; extra == "dev"
50
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
51
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ # Easier open source contribution process for newcomers
55
+
56
+ Stop digging through stale GitHub issues. **gh-issue-lens** finds unassigned, beginner-friendly open source issues in any repository and prepares them for LLM analysis in seconds. It bypasses API rate limits by grabbing issues, linked Pull Requests, assignees, and non-bot comments in a single network pass.
57
+
58
+ # How to use it?
59
+
60
+ ## Option 1: Install from PyPI
61
+
62
+ ```bash
63
+ pip install gh-issue-lens
64
+ ```
65
+ **Export your GitHub Personal Access Token:**
66
+
67
+ First of all, create your personal github token from your github account then use it.
68
+
69
+ ```bash
70
+ export GITHUB_TOKEN="your_personal_access_token"
71
+ ```
72
+
73
+ **Fetch issues:**
74
+
75
+ ```bash
76
+ gh-issue-lens kubernetes kubernetes -d 200
77
+ ```
78
+
79
+ **Filter by labels:**
80
+
81
+ ```bash
82
+ gh-issue-lens kubernetes kubernetes -l "good first issue" "help wanted" -d 50
83
+ ```
84
+
85
+ And for more commands, you can see the sections below. The guidelines are the same. The only difference is to use `gh-issue-lens` directly instead of `python3 cli.py`.
86
+
87
+ ## Option 2: Build it manually
88
+
89
+ **Clone the repo:**
90
+
91
+ ```bash
92
+ git clone https://github.com/MohamedM216/gh-issue-lens.git
93
+ ```
94
+
95
+ **Install requirements:**
96
+
97
+ ```bash
98
+ pip3 install requests
99
+ ```
100
+
101
+ **Export your GitHub Personal Access Token:**
102
+
103
+ First of all, create your personal github token from your github account then use it.
104
+
105
+ ```bash
106
+ export GITHUB_TOKEN="your_personal_access_token"
107
+ ```
108
+
109
+ **Verify it works:**
110
+
111
+ ```bash
112
+ cd src/gh_issue_lens/
113
+ python3 cli.py --help
114
+ ```
115
+
116
+ **Fetch 100 issues from a repository:**
117
+
118
+ 100 issues is the default depth you can change it using the -d option.
119
+
120
+ ```bash
121
+ python3 cli.py kubernetes kubernetes
122
+ ```
123
+
124
+ ### Filter by up to 5 labels (ORed) and set a depth limit:
125
+
126
+ This fetches a maximum of 50 issues that have EITHER the "good first issue", "help wanted", or "bug" label.
127
+
128
+ ```bash
129
+ python3 cli.py kubernetes kubernetes -l "good first issue" "help wanted" "bug" -d 50
130
+ ```
131
+
132
+ ### Save to a custom JSON file:
133
+
134
+ If you want to use a custom name for the output file, you can run:
135
+
136
+ ```bash
137
+ python3 cli.py kubernetes kubernetes -l "feature" -d 10 -o custom_dataset.json
138
+ ```
139
+
140
+ ### Incremental Saving & Resumability (Checkpointing):
141
+
142
+ When scraping massive repositories (like Kubernetes), hitting GitHub's API rate limits or network timeouts is common. You no longer have to worry about losing your progress!
143
+ * **JSONL Format:** Issues are now saved line-by-line instantly as they are fetched, ensuring zero data loss.
144
+ * **Auto-Resume:** The tool creates a hidden `.checkpoint` file tracking your progress. If the script crashes or you stop it, simply run the **exact same command** again. It will automatically resume from the exact page it left off, saving your API budget and time.
145
+
146
+ ### Exclude issues with open PRs:
147
+
148
+ ```bash
149
+ python3 cli.py kubernetes kubernetes --exclude-open-prs
150
+ ```
151
+
152
+ ### SQLite caching:
153
+
154
+ `gh-issue-lens` can cache already-exported issues in a local SQLite database. This prevents duplicate issues from being written again when you run the same export multiple times.
155
+
156
+ Enable caching:
157
+
158
+ ```bash
159
+ python3 cli.py kubernetes kubernetes -d 200 --cache
160
+ ```
161
+
162
+ By default, the cache is stored in `.gh_issue_lens_cache.db`. Use a custom cache database:
163
+
164
+ ```bash
165
+ python3 cli.py kubernetes kubernetes -d 200 --cache --cache-db my_cache.db
166
+ ```
167
+
168
+ Run again with caching enabled. Issues that were already exported in previous runs will be skipped. You can also enable early stopping when the tool keeps finding only cached issues:
169
+
170
+ ```bash
171
+ python3 cli.py kubernetes kubernetes -d 200 --cache --cache-stop
172
+ ```
173
+
174
+ This is useful when re-running the tool against the same repository and you only want newly discovered issues. To reset the cache, delete the cache file:
175
+
176
+ ```bash
177
+ rm .gh_issue_lens_cache.db
178
+ ```
179
+
180
+ Caching is different from checkpointing: checkpointing helps resume a crashed or interrupted run, while caching helps avoid exporting the same issues again across separate runs.
181
+
182
+ ## What's next?
183
+
184
+ The generated JSON file is suitable to be used as context for Large Language Models (like Gemini, Claude, or ChatGPT). You can upload the output file to an LLM and use targeted prompts to find the perfect issue to work on.
185
+
186
+ **Example Prompts to try:**
187
+
188
+ > "I am a newcomer to this repository. Based on the attached JSON, find an issue that is easy to implement, has no active PRs, and doesn't have a long, debated comment history."
189
+
190
+ > "Filter these issues and list only the ones related to [backend / frontend / database / specific component]."
191
+
192
+ > "Read the comments on these issues. Which maintainers seem the most responsive and welcoming to beginners? Point me to issues authored or reviewed by them."
193
+
194
+ > "Summarize the technical requirements for issue #123 based on its description and comment thread. What files should I look at first?"
195
+
196
+ *Don't forget to STAR the repository :)*
197
+
198
+ *Happy hacking!*
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/gh_issue_lens/__init__.py
6
+ src/gh_issue_lens/cli.py
7
+ src/gh_issue_lens/core.py
8
+ src/gh_issue_lens.egg-info/PKG-INFO
9
+ src/gh_issue_lens.egg-info/SOURCES.txt
10
+ src/gh_issue_lens.egg-info/dependency_links.txt
11
+ src/gh_issue_lens.egg-info/entry_points.txt
12
+ src/gh_issue_lens.egg-info/requires.txt
13
+ src/gh_issue_lens.egg-info/top_level.txt
14
+ tests/test_smoke.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gh-issue-lens = gh_issue_lens.cli:main
@@ -0,0 +1,7 @@
1
+ requests>=2.31.0
2
+
3
+ [dev]
4
+ build>=1.2.0
5
+ twine>=5.0.0
6
+ pytest>=8.0.0
7
+ ruff>=0.5.0
@@ -0,0 +1 @@
1
+ gh_issue_lens
@@ -0,0 +1,17 @@
1
+ from gh_issue_lens import __version__
2
+ from gh_issue_lens.core import get_graphql_query
3
+
4
+
5
+ def test_version_exists():
6
+ assert __version__
7
+ assert isinstance(__version__, str)
8
+
9
+
10
+ def test_graphql_query_contains_expected_fields():
11
+ query = get_graphql_query()
12
+
13
+ assert "repository" in query
14
+ assert "issues" in query
15
+ assert "pageInfo" in query
16
+ assert "hasNextPage" in query
17
+ assert "endCursor" in query