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.
- gh_issue_lens-0.1.0/LICENSE +21 -0
- gh_issue_lens-0.1.0/MANIFEST.in +3 -0
- gh_issue_lens-0.1.0/PKG-INFO +198 -0
- gh_issue_lens-0.1.0/README.md +145 -0
- gh_issue_lens-0.1.0/pyproject.toml +100 -0
- gh_issue_lens-0.1.0/setup.cfg +4 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens/__init__.py +3 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens/cli.py +136 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens/core.py +390 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens.egg-info/PKG-INFO +198 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens.egg-info/SOURCES.txt +14 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens.egg-info/dependency_links.txt +1 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens.egg-info/entry_points.txt +2 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens.egg-info/requires.txt +7 -0
- gh_issue_lens-0.1.0/src/gh_issue_lens.egg-info/top_level.txt +1 -0
- gh_issue_lens-0.1.0/tests/test_smoke.py +17 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -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
|