Skip to content

Commit 80cd01d

Browse files
committed
Add tool to track current issues we are working on
1 parent 5555629 commit 80cd01d

File tree

11 files changed

+730
-0
lines changed

11 files changed

+730
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Daily Issue Tracking Update
2+
3+
on:
4+
schedule:
5+
# Run daily at 9:00 AM UTC (adjust as needed)
6+
- cron: '0 9 * * *'
7+
workflow_dispatch: # Allow manual triggering
8+
9+
jobs:
10+
update-issue-tracking:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
with:
17+
token: ${{ secrets.GITHUB_TOKEN }}
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.10'
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
make init
28+
29+
- name: Set up environment variables
30+
env:
31+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32+
run: |
33+
echo "GITHUB_TOKEN=$GITHUB_TOKEN" > .env
34+
35+
- name: Generate issue report
36+
working-directory: bin
37+
run: |
38+
python -c "from issue_tracking import gen_issue_report; gen_issue_report()"
39+
40+
- name: Generate markdown output
41+
working-directory: bin
42+
run: |
43+
python -c "from issue_tracking_output import generate_issue_tracking_index; generate_issue_tracking_index()"
44+
45+
- name: Commit and push changes
46+
run: |
47+
git config --local user.email "action@github.com"
48+
git config --local user.name "GitHub Action"
49+
make commit-issue-tracking

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,5 @@ dist
295295
.python-version
296296
.vscode
297297
package-lock.json
298+
299+
cache/

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# current git branch
2+
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
3+
14
init::
25
python -m pip install --upgrade pip
36
python -m pip install pip-tools
@@ -8,3 +11,11 @@ init::
811

912
checks:
1013
python3 bin/check.py
14+
15+
16+
status:
17+
git status --ignored
18+
19+
commit-issue-tracking::
20+
git add .
21+
git diff --quiet && git diff --staged --quiet || (git commit -m "Latest issue tracking updates $(shell date +%F)"; git push origin $(BRANCH))
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
github_label,module
2+
"component: Type of proposed advertisement",advertisement-types
3+
"component: advertisement period",advert-period
4+
"component: agent contact details",agent-contact
5+
"component: applicant contact details",applicant-contact
6+
"component: application for tree works - checklist",checklist
7+
"component: application reqs - checklist",checklist
8+
"component: checklist",checklist
9+
"component: declaration",declaration
10+
"component: demolition",demolition
11+
"component: designated area",designated-areas
12+
"component: details proposed adverts",proposed-advert-details
13+
"component: employment",employment
14+
"component: foul sewage",foul-sewage
15+
"component: pedestrian and vehicle access",access-rights-of-way
16+
"component: planning application req - checklist",checklist
17+
"component: processes and machinery",processes-machinery-waste
18+
"component: res units",res-units
19+
"component: site area",site-area
20+
"component: trade effluent",trade-effluent
21+
"component: trees location",trees-location
22+
"component: type of dev",dev-type
23+
"component: vehicle parking",vehicle-parking
24+
"component: waste and storage",waste-storage-collection
25+
"component:agent name and address",agent-details
26+
"component:applicant name and address",applicant-details
27+
"component:assessment of flood risk",flood-risk-assessment
28+
"component:authority employee / member",conflict-of-interest
29+
"component:bio geo conservation",bio-geo-arch-con
30+
"component:biodiversity net gain",bng
31+
"component:description of the proposal",proposal-details
32+
"component:development description",proposal-details
33+
"component:eligibility",eligibility
34+
"component:eligibility current building",eligibility-current-building
35+
"component:ground for app",grounds-for-application
36+
"component:grounds for app ldc",grounds-ldc
37+
"component:immunity from listing",immunity-from-listing
38+
"component:location of advertisement(s)",advert-location
39+
"component:materials",materials
40+
"component:neighbour and community consultation",community-consultation
41+
"component:non-res floorspace",non-res-floorspace
42+
"component:ownership certificates",ownership-certs
43+
"component:ownership certs + agr",ownership-certs
44+
"component:pre-application advice",pre-app-advice
45+
"component:site address details",site-details
46+
"component:site visit",site-visit

bin/csv_helpers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import csv
2+
3+
4+
def read_csv(filename, encoding="utf-8", as_dict=False, include_row_num=False):
5+
# Read the CSV file
6+
with open(filename, newline="", encoding=encoding) as csvfile:
7+
data = []
8+
if as_dict:
9+
reader = csv.DictReader(csvfile)
10+
# Start row numbering at 1
11+
for i, row in enumerate(reader, start=1):
12+
13+
if include_row_num:
14+
row["_row_num"] = i
15+
data.append(row)
16+
else:
17+
reader = csv.reader(csvfile)
18+
# Start row numbering at 1
19+
for i, row in enumerate(reader, start=1):
20+
if include_row_num:
21+
# Insert the row number at the start of the row
22+
row.insert(0, i)
23+
data.append(row)
24+
25+
return data

bin/github_api.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import os
2+
from collections import Counter
3+
from datetime import datetime
4+
from typing import Any, Dict, List
5+
6+
import requests
7+
from json_helpers import load_json, save_json
8+
9+
# from dotenv import load_dotenv
10+
11+
# load_dotenv()
12+
13+
cached_issues_file = "cache/github_issues_cache.json"
14+
15+
16+
def get_open_issues(
17+
repo_owner: str = "digital-land",
18+
repo_name: str = "planning-application-data-specification",
19+
github_token: str = None,
20+
) -> List[Dict[str, Any]]:
21+
"""
22+
Fetch all open issues from a GitHub repository.
23+
24+
Args:
25+
repo_owner: GitHub repository owner
26+
repo_name: GitHub repository name
27+
github_token: GitHub token for authentication (optional)
28+
29+
Returns:
30+
List of issue dictionaries
31+
"""
32+
# if github_token is None:
33+
# github_token = os.getenv("GITHUB_TOKEN")
34+
35+
# HEADERS (use token for higher rate limit)
36+
headers = {
37+
"Accept": "application/vnd.github+json",
38+
"X-GitHub-Api-Version": "2022-11-28",
39+
}
40+
if github_token:
41+
headers["Authorization"] = f"Bearer {github_token}"
42+
43+
# PAGINATION VARIABLES
44+
issues_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/issues"
45+
params = {"state": "open", "per_page": 100, "page": 1}
46+
47+
all_issues = []
48+
49+
# PAGINATE THROUGH RESULTS
50+
while True:
51+
try:
52+
response = requests.get(issues_url, headers=headers, params=params)
53+
54+
# Handle rate limiting
55+
if response.status_code == 403:
56+
print(f"Error 403: {response.json().get('message', 'Forbidden')}")
57+
if "rate limit" in response.text.lower():
58+
print("You've hit the rate limit. Please:")
59+
print("1. Wait a bit and try again")
60+
print(
61+
"2. Add a GITHUB_TOKEN environment variable for higher limits"
62+
)
63+
break
64+
65+
response.raise_for_status()
66+
issues = response.json()
67+
68+
if not issues:
69+
break
70+
71+
for issue in issues:
72+
# Skip pull requests (they are also issues)
73+
if "pull_request" not in issue:
74+
all_issues.append(issue)
75+
76+
params["page"] += 1
77+
78+
except requests.exceptions.HTTPError as e:
79+
print(f"HTTP Error: {e}")
80+
print(f"Response: {response.text}")
81+
break
82+
except Exception as e:
83+
print(f"Unexpected error: {e}")
84+
break
85+
86+
return all_issues
87+
88+
89+
def save_issues_to_file(
90+
issues: List[Dict[str, Any]], filename: str = cached_issues_file
91+
):
92+
data = {
93+
"fetch_timestamp": datetime.now().isoformat(),
94+
"fetch_timestamp_human": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
95+
"issue_count": len(issues),
96+
"issues": issues,
97+
}
98+
save_json(data, filename)
99+
100+
101+
def load_issues_from_file(filename: str = cached_issues_file) -> Dict[str, Any]:
102+
data = load_json(filename)
103+
if not data:
104+
print("No data found.")
105+
return None
106+
107+
return data
108+
109+
110+
def get_issues_with_cache(
111+
cache_hours: int = 1, force_refresh: bool = False
112+
) -> tuple[List[Dict[str, Any]], str]:
113+
"""
114+
Get issues with caching and timestamp tracking.
115+
116+
Returns:
117+
Tuple of (issues_list, last_fetch_time)
118+
"""
119+
# Try to load existing cache
120+
cached_data = load_issues_from_file()
121+
122+
# Check if we need to refresh
123+
need_refresh = force_refresh
124+
125+
if cached_data:
126+
last_fetch_time = cached_data.get("fetch_timestamp_human", "Unknown")
127+
fetch_timestamp = datetime.fromisoformat(cached_data["fetch_timestamp"])
128+
age_hours = (datetime.now() - fetch_timestamp).total_seconds() / 3600
129+
if age_hours >= cache_hours:
130+
print(f"Cache is too old ({age_hours:.2f} hours), refreshing...")
131+
need_refresh = True
132+
else:
133+
print("No cached data found, fetching new issues...")
134+
need_refresh = True
135+
136+
if need_refresh:
137+
print("Fetching new issues from GitHub...")
138+
issues = get_open_issues()
139+
save_issues_to_file(issues)
140+
last_fetch_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
141+
return issues, last_fetch_time
142+
143+
return cached_data.get("issues", []), last_fetch_time
144+
145+
146+
def count_labels_from_issues(issues: List[Dict[str, Any]]) -> Counter:
147+
"""
148+
Count labels from a list of issues.
149+
150+
Args:
151+
issues: List of issue dictionaries
152+
153+
Returns:
154+
Counter object with label counts
155+
"""
156+
label_counter = Counter()
157+
158+
for issue in issues:
159+
for label in issue["labels"]:
160+
label_counter[label["name"]] += 1
161+
162+
return label_counter
163+
164+
165+
# USAGE EXAMPLE
166+
if __name__ == "__main__":
167+
# Get all open issues (with caching)
168+
issues, last_fetch_time = get_issues_with_cache()
169+
170+
# Count labels
171+
label_counter = count_labels_from_issues(issues)
172+
173+
# OUTPUT RESULTS
174+
print(f"Found {len(issues)} open issues")
175+
print("\nOpen issues by label:")
176+
for label, count in label_counter.most_common():
177+
print(f"{label}: {count}")
178+
179+
# You can now work with the issues list
180+
# For example, get issue titles:
181+
print("\nIssue titles:")
182+
for issue in issues[:5]: # Show first 5
183+
print(f"- {issue['title']}")
184+
185+
print(issues)

0 commit comments

Comments
 (0)