Skip to content

Commit 7a82bb0

Browse files
authored
feat: Add filter-by-user functionality (#65)
Signed-off-by: Roger Barker <roger.barker@swirldslabs.com>
1 parent 0fa97cf commit 7a82bb0

5 files changed

Lines changed: 241 additions & 18 deletions

File tree

current-implemented-CLI.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
- `temp-dir` - The temporary directory to write JSON response files
77

88
- `include-team` - Include a team to the query
9+
- `include-user` - Include a user to the query
910

1011

1112
## To Be Implemented
1213

13-
- `include-user` - Include a specific user to the query
1414
- `include-repository` - Include specific repo to the query
1515
- `include-organization` - Include specific org to a query
1616
- `include-organization-repository` - Include specific org/repo to the query

src/queryFilter.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""
2+
query_filter.py
3+
4+
This module defines the QueryFilter class, which is used to filter GitHub projects based on various criteria.
5+
It is used in the Version2Query class to filter projects based on user input.
6+
"""
7+
8+
class QueryFilter:
9+
def __init__(
10+
self,
11+
include_teams:list[str]=None,
12+
include_users:list[str]=None,
13+
include_repositories:list[str]=None,
14+
include_organizations:list[str]=None,
15+
include_organization_repositories:list[str]=None,
16+
include_labels:list[str]=None,
17+
exclude_teams:list[str]=None,
18+
exclude_users:list[str]=None,
19+
exclude_repositories:list[str]=None,
20+
exclude_organizations:list[str]=None,
21+
exclude_organization_repositories:list[str]=None,
22+
exclude_label:list[str]=None
23+
):
24+
self.include_teams:list[str] = include_teams
25+
self.include_users:list[str] = include_users
26+
self.include_repositories:list[str] = include_repositories
27+
self.include_organizations:list[str] = include_organizations
28+
self.include_organization_repositories:list[str] = include_organization_repositories
29+
self.include_labels:list[str] = include_labels
30+
self.exclude_teams:list[str] = exclude_teams
31+
self.exclude_users:list[str] = exclude_users
32+
self.exclude_repositories:list[str] = exclude_repositories
33+
self.exclude_organizations:list[str] = exclude_organizations
34+
self.exclude_organization_repositories:list[str] = exclude_organization_repositories
35+
self.exclude_label:list[str] = exclude_label
36+
37+
@property
38+
def include_teams(self) -> list[str]:
39+
return self._include_teams
40+
@include_teams.setter
41+
def include_teams(self, value:list[str]) -> None:
42+
self._include_teams=value
43+
44+
@property
45+
def include_users(self) -> list[str]:
46+
return self._include_users
47+
@include_users.setter
48+
def include_users(self, value:list[str]) -> None:
49+
self._include_users=value
50+
51+
@property
52+
def include_repositories(self) -> list[str]:
53+
return self._include_repositories
54+
@include_repositories.setter
55+
def include_repositories(self, value:list[str]) -> None:
56+
self._include_repositories=value
57+
58+
@property
59+
def include_organizations(self) -> list[str]:
60+
return self._include_organizations
61+
@include_organizations.setter
62+
def include_organizations(self, value:list[str]) -> None:
63+
self._include_organizations=value
64+
65+
@property
66+
def include_organization_repositories(self) -> list[str]:
67+
return self._include_organization_repositories
68+
@include_organization_repositories.setter
69+
def include_organization_repositories(self, value:list[str]) -> None:
70+
self._include_organization_repositories=value
71+
72+
@property
73+
def include_labels(self) -> list[str]:
74+
return self._include_labels
75+
@include_labels.setter
76+
def include_labels(self, value:list[str]) -> None:
77+
self._include_labels=value
78+
79+
@property
80+
def exclude_teams(self) -> list[str]:
81+
return self._exclude_teams
82+
@exclude_teams.setter
83+
def exclude_teams(self, value:list[str]) -> None:
84+
self._exclude_teams=value
85+
86+
@property
87+
def exclude_users(self) -> list[str]:
88+
return self._exclude_users
89+
@exclude_users.setter
90+
def exclude_users(self, value:list[str]) -> None:
91+
self._exclude_users=value
92+
93+
@property
94+
def exclude_repositories(self) -> list[str]:
95+
return self._exclude_repositories
96+
@exclude_repositories.setter
97+
def exclude_repositories(self, value:list[str]) -> None:
98+
self._exclude_repositories=value
99+
100+
@property
101+
def exclude_organizations(self) -> list[str]:
102+
return self._exclude_organizations
103+
@exclude_organizations.setter
104+
def exclude_organizations(self, value:list[str]) -> None:
105+
self._exclude_organizations=value
106+
107+
@property
108+
def exclude_organization_repositories(self) -> list[str]:
109+
return self._exclude_organization_repositories
110+
@exclude_organization_repositories.setter
111+
def exclude_organization_repositories(self, value:list[str]) -> None:
112+
self._exclude_organization_repositories=value
113+
114+
@property
115+
def exclude_label(self) -> list[str]:
116+
return self._exclude_label
117+
@exclude_label.setter
118+
def exclude_label(self, value:list[str]) -> None:
119+
self._exclude_label=value
120+
121+
def print_filters(self) -> None:
122+
"""Print the filters for debugging purposes."""
123+
print(f"[bold green]Filters:[/bold green]")
124+
print(f"Include Teams: {self.include_teams}")
125+
print(f"Include Users: {self.include_users}")
126+
print(f"Include Repositories: {self.include_repositories}")
127+
print(f"Include Organizations: {self.include_organizations}")
128+
print(f"Include Organization Repositories: {self.include_organization_repositories}")
129+
print(f"Include Labels: {self.include_labels}")
130+
print(f"Exclude Teams: {self.exclude_teams}")
131+
print(f"Exclude Users: {self.exclude_users}")
132+
print(f"Exclude Repositories: {self.exclude_repositories}")
133+
print(f"Exclude Organizations: {self.exclude_organizations}")
134+
print(f"Exclude Organization Repositories: {self.exclude_organization_repositories}")
135+
print(f"Exclude Label: {self.exclude_label}")
136+
137+
def get_filters(self) -> dict[str,list[str]]:
138+
"""Get the filters as a dictionary."""
139+
return {
140+
"include_teams": self.include_teams,
141+
"include_users": self.include_users,
142+
"include_repositories": self.include_repositories,
143+
"include_organizations": self.include_organizations,
144+
"include_organization_repositories": self.include_organization_repositories,
145+
"include_labels": self.include_labels,
146+
"exclude_teams": self.exclude_teams,
147+
"exclude_users": self.exclude_users,
148+
"exclude_repositories": self.exclude_repositories,
149+
"exclude_organizations": self.exclude_organizations,
150+
"exclude_organization_repositories": self.exclude_organization_repositories,
151+
"exclude_label": self.exclude_label
152+
}
153+
154+
def set_filters_from_dict(self, filters:dict[str,list[str]]):
155+
"""Set the filters from a dictionary."""
156+
self.include_teams = filters.get("include_teams", [])
157+
self.include_users = filters.get("include_users", [])
158+
self.include_repositories = filters.get("include_repositories", [])
159+
self.include_organizations = filters.get("include_organizations", [])
160+
self.include_organization_repositories = filters.get("include_organization_repositories", [])
161+
self.include_labels = filters.get("include_labels", [])
162+
self.exclude_teams = filters.get("exclude_teams", [])
163+
self.exclude_users = filters.get("exclude_users", [])
164+
self.exclude_repositories = filters.get("exclude_repositories", [])
165+
self.exclude_organizations = filters.get("exclude_organizations", [])
166+
self.exclude_organization_repositories = filters.get("exclude_organization_repositories", [])
167+
self.exclude_label = filters.get("exclude_label", [])

src/version2config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def init_parser(self):
5050
dest="include_user",
5151
action="append",
5252
type=str,
53-
nargs="+",
5453
help="Include all issues and PRs for the provided user [Required Parameter]"
5554
)
5655

src/version2query.py

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,26 @@
77
from pathlib import Path
88
from ghapi.all import GhApi
99
from rich import print
10+
from .queryFilter import QueryFilter
1011

1112
class Version2Query:
12-
def __init__(self, temp_dir:str = "tmp.dir", output_file:str="output.items.json"):
13+
def __init__(self, temp_dir:str="tmp.dir", output_file:str="output.items.json"):
1314
self.temp_dir:Path = Path(temp_dir)
1415
self.output_file:str = output_file
1516
self.api:GhApi = GhApi()
1617
self._validate_token()
18+
self.filters:QueryFilter = QueryFilter()
1719

1820
def _validate_token(self) -> None:
1921
"""Validate Github token exists in the environment."""
2022
if not os.getenv("GITHUB_TOKEN"):
2123
raise ValueError("GITHUB_TOKEN environment variable is not set.")
2224

25+
def set_filters(self, filters:dict) -> None:
26+
"""Set filters for the query."""
27+
self.filters.set_filters_from_dict(filters)
28+
self.filters.print_filters()
29+
2330
def get_github_token(self) -> str:
2431
return os.getenv("GITHUB_TOKEN")
2532

@@ -81,6 +88,31 @@ def filter_projects_by_team(self, project_list:list[dict], teams:list[str]) -> l
8188
print(f"[green]Found {len(matching_projects)} matching projects for team '{team}'[/green]")
8289
return filtered_projects
8390

91+
def filter_items_by_user(self) -> None:
92+
"""Filter items by user."""
93+
94+
filtered_items:list[dict] = []
95+
96+
# pull the data out of output.projects.json and write to that file again
97+
with open(self.output_file) as f:
98+
items = json.load(f)
99+
for item in items:
100+
for user in self.filters.include_users:
101+
if user in item.get("assignees", []):
102+
filtered_items.append(item)
103+
break # avoid appending the item multiple times if it matches multiple users.
104+
105+
# move self.output_file to output.items.json.tmp
106+
tmp_file = self.output_file + ".tmp"
107+
108+
# write the new output_file with the filter_items_by_user
109+
with open(self.output_file, "w") as f:
110+
json.dump(filtered_items, f, indent=2)
111+
112+
# remove the tmp_file
113+
if Path(tmp_file).exists():
114+
os.remove(tmp_file)
115+
84116
def fetch_project_items(self, projects:list[dict]) -> bool:
85117
"""Fetch all items on each project."""
86118

@@ -138,7 +170,7 @@ def cleanup(self) -> bool:
138170
if self.temp_dir.exists():
139171
try:
140172
shutil.rmtree(self.temp_dir)
141-
print(f"[bold red]Cleaned up temporary files in {self.temp_dir}[/bold red]")
173+
print(f"[bold purple]Cleaned up temporary files in {self.temp_dir}[/bold purple]")
142174
except Exception as e:
143175
print(f"[red]Error cleaning up temporary files: {e}[/red]")
144176
rv = False
@@ -147,14 +179,11 @@ def cleanup(self) -> bool:
147179

148180
return rv
149181

150-
def process(self, teams:list[str] = None) -> bool:
182+
def process(self) -> bool:
151183
"""Main processing method for the Version2Query class."""
152-
if not teams:
153-
print("[red]No team names provided. Exiting...[/red]")
154-
return False
155184

156185
if self.temp_dir.exists():
157-
cleanup()
186+
self.cleanup()
158187

159188
orgs = self.get_github_orgs()
160189
if not orgs:
@@ -166,10 +195,15 @@ def process(self, teams:list[str] = None) -> bool:
166195
print("[red]No projects found. Exiting...[/red]")
167196
return False
168197

169-
filtered_projects = self.filter_projects_by_team(all_projects, teams)
170-
if not filtered_projects:
171-
print("[red]No projects found matching the team names. Exiting...[/red]")
172-
return False
198+
# filter by team names
199+
filtered_projects = all_projects
200+
if self.filters.include_teams is not None:
201+
teams = self.filters.include_teams
202+
filtered_projects_by_teams = self.filter_projects_by_team(all_projects, teams)
203+
if not filtered_projects_by_teams:
204+
print("[red]No projects found matching the team names. Exiting...[/red]")
205+
return False
206+
filtered_projects = filtered_projects_by_teams
173207

174208
if not self.fetch_project_items(filtered_projects):
175209
print("[red]Failed to fetch project items. Exiting...[/red]")
@@ -179,19 +213,24 @@ def process(self, teams:list[str] = None) -> bool:
179213
print("[red]Failed to consolidate items. Exiting...[/red]")
180214
return False
181215

182-
return self.cleanup()
216+
# filter items by user
217+
if self.filters.include_users is not None:
218+
self.filter_items_by_user()
183219

220+
return self.cleanup()
184221

222+
# This main method is used for testing out the version2query class
185223
def main():
186224
"""The primary method for the version2query.py script."""
187225
teams:list = input("Enter team name(s) to filter projects: ").split(",")
188226
test_temp_dir:Path = Path(f"tmp.dir")
189227
test_output_file:str = f"output.items.json"
190228

191229
query = Version2Query(temp_dir=test_temp_dir, output_file=test_output_file)
192-
if not query.process(teams):
230+
query.filters.include_teams = teams
231+
if not query.process():
193232
print("[red]Processing failed.[/red]")
194-
return
233+
raise RunTimeError("Processing failed.")
195234

196235
if __name__ == "__main__":
197236
main()

version2.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,32 @@ def main():
1010
config.display_config()
1111
temp_dir:Path = Path(config.temp_dir)
1212
output_file:str = config.output_file
13-
teams:list[str] = config.include_team
13+
14+
# Get parameters for filters
15+
filters:dict[str,list[str]] = {
16+
"include_teams": config.include_team,
17+
"include_users": config.include_user,
18+
"include_repositories": config.include_repository,
19+
"include_organizations": config.include_organization,
20+
"include_organization_repositories": config.include_organization_repository,
21+
"include_labels": config.include_label,
22+
"exclude_teams": config.exclude_team,
23+
"exclude_users": config.exclude_user,
24+
"exclude_repositories": config.exclude_repository,
25+
"exclude_organizations": config.exclude_organization,
26+
"exclude_organization_repositories": config.exclude_organization_repository,
27+
"exclude_label": config.exclude_label
28+
}
1429

1530
query:VersionTwoQuery = Version2Query(temp_dir=temp_dir, output_file=output_file)
1631
ss_gen = StaticSiteGenerator()
1732

33+
# Set filters
34+
query.set_filters(filters=filters)
35+
1836
# Generate output_file or die
1937
logging.info("Querying GitHub API...")
20-
if not query.process(teams):
38+
if not query.process():
2139
logging.error("Failed to process query.")
2240
return
2341

0 commit comments

Comments
 (0)