Skip to content

Commit 9f08b17

Browse files
authored
Fix: refactor github methods out of contributor mod
Fix: refactor github methods out of contributor mod
2 parents 6773b23 + 34254ff commit 9f08b17

File tree

9 files changed

+157
-46
lines changed

9 files changed

+157
-46
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
- Fix: Rename and organize `clean.py` module into `utils_parse` and `utils_clean` (@lwasser, @willingc, #121)
1010
- Fix: Add tests for all utils functions (@lwasser, #122)
1111
- Fix: Bug where date_accepted is removed (@lwasser, #129)
12-
- Fix: Refactor all GitHub related methods move to gh_client module (@lwasser, #125)
12+
- Fix: Refactor all issue related GitHub methods to gh_client module (@lwasser, #125)
1313
- Add: support for partners and emeritus_editor in contributor model (@lwasser, #133)
14+
- Fix: Refactor all contributor GitHub related methods into gh_client module from contributors module (@lwasser, #125)
1415

1516

1617
## [v0.2.3] - 2024-02-29

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ version.source = "vcs"
6969
build.hooks.vcs.version-file = "src/pyosmeta/_version.py"
7070

7171
[tool.hatch.envs.test]
72-
dependencies = ["pytest", "pytest-cov", "coverage[toml]"]
72+
dependencies = ["pytest", "pytest-cov", "coverage[toml]", "pytest-mock"]
7373

7474
[tool.hatch.envs.test.scripts]
7575
run-coverage = "pytest --cov-config=pyproject.toml --cov=pyosmeta --cov=tests/*"

src/pyosmeta/cli/update_contributors.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from pyosmeta.contributors import ProcessContributors
88
from pyosmeta.file_io import create_paths, load_pickle, open_yml_file
9+
from pyosmeta.github_api import GitHubAPI
910
from pyosmeta.models import PersonModel
1011

1112
# TODO - https://stackoverflow.com
@@ -62,7 +63,8 @@ def main():
6263
print("Done processing all-contribs")
6364

6465
# Create a list of all contributors across repositories
65-
process_contribs = ProcessContributors(json_files)
66+
github_api = GitHubAPI()
67+
process_contribs = ProcessContributors(github_api, json_files)
6668
bot_all_contribs = process_contribs.combine_json_data()
6769

6870
print("Updating contrib types and searching for new users now")
@@ -71,7 +73,7 @@ def main():
7173
# Find and populate data for any new contributors
7274
if gh_user not in all_contribs.keys():
7375
print("Missing", gh_user, "Adding them now")
74-
new_contrib = process_contribs.get_user_info(gh_user)
76+
new_contrib = process_contribs.return_user_info(gh_user)
7577
new_contrib["date_added"] = datetime.now().strftime("%Y-%m-%d")
7678
all_contribs[gh_user] = PersonModel(**new_contrib)
7779

@@ -81,7 +83,7 @@ def main():
8183
if update_all:
8284
for user in all_contribs.keys():
8385
print("Updating all user info from github", user)
84-
new_gh_data = process_contribs.get_user_info(user)
86+
new_gh_data = process_contribs.return_user_info(user)
8587

8688
# TODO: turn this into a small update method
8789
existing = all_contribs[user].model_dump()

src/pyosmeta/contributors.py

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
import json
2-
import os
32

43
import requests
54
from dataclasses import dataclass
6-
from dotenv import load_dotenv
7-
from typing import List, Optional, Tuple
5+
from typing import Any, List, Optional, Tuple
6+
7+
from .github_api import GitHubAPI
88

99

1010
@dataclass
1111
class ProcessContributors:
1212
"""A class that contains some basic methods to support populating and
1313
updating contributor data."""
1414

15-
def __init__(self, json_files: List) -> None:
15+
def __init__(self, github_api: GitHubAPI, json_files: List) -> None:
1616
"""
1717
Parameters
1818
----------
19-
19+
github_api : str
20+
Instantiated instance of a GitHubAPI object
2021
json_files : list
2122
A list of string objects each of which represents a URL to a JSON
2223
file to be parsed
23-
GITHUB_TOKEN : str
24-
A string containing your API token needed to access the github API
2524
"""
2625

26+
self.github_api = github_api
2727
self.json_files = json_files
28-
# self.GITHUB_TOKEN = GITHUB_TOKEN
28+
2929
self.update_keys = [
3030
"twitter",
3131
"website",
@@ -52,18 +52,6 @@ def __init__(self, json_files: List) -> None:
5252
],
5353
}
5454

55-
def get_token(self) -> str:
56-
"""Fetches the GitHub API key from the users environment. If running
57-
local from an .env file.
58-
59-
Returns
60-
-------
61-
str
62-
The provided API key in the .env file.
63-
"""
64-
load_dotenv()
65-
return os.environ["GITHUB_TOKEN"]
66-
6755
def check_contrib_type(self, json_file: str):
6856
"""
6957
Determine the type of contribution the person
@@ -94,6 +82,7 @@ def check_contrib_type(self, json_file: str):
9482
contrib_type = "community"
9583
return contrib_type
9684

85+
# Possibly github it is a get request but it says json path
9786
def load_json(self, json_path: str) -> dict:
9887
"""
9988
Helper function that deserializes a json file to a dict.
@@ -153,19 +142,19 @@ def combine_json_data(self) -> dict:
153142
print("Oops - can't process", json_file, e)
154143
return combined_data
155144

156-
def get_user_info(
157-
self, username: str, aname: Optional[str] = None
158-
) -> dict:
145+
def return_user_info(
146+
self, gh_handle: str, name: Optional[str] = None
147+
) -> dict[str, Any]:
159148
"""
160149
Get a single user's information from their GitHub username using the
161150
GitHub API
162151
# https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
163152
164153
Parameters
165154
----------
166-
username : string
155+
gh_handle : string
167156
Github username to retrieve data for
168-
aname : str default=None
157+
name : str default=None
169158
A user's name from the contributors.yml file.
170159
https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-a-user
171160
@@ -174,14 +163,8 @@ def get_user_info(
174163
Dict with updated user data grabbed from the GH API
175164
"""
176165

177-
url = f"https://api.github.com/users/{username}"
178-
headers = {"Authorization": f"Bearer {self.get_token()}"}
179-
response = requests.get(url, headers=headers)
180-
# TODO: add check here for if credentials are bad
181-
# if message = Bad credentials
182-
response_json = response.json()
166+
response_json = self.github_api.get_user_info(gh_handle, name)
183167

184-
# TODO: make an attribute and call it here?
185168
update_keys = {
186169
"name": "name",
187170
"location": "location",

src/pyosmeta/file_io.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,24 @@ def _list_to_dict(a_list: List, a_key: str) -> Dict:
2929

3030

3131
def create_paths(repos: Union[list[str], str]) -> Union[list[str], str]:
32-
""" """
32+
"""Construct URLs for .all-contributorsrc file on GitHub for pyos repos.
33+
34+
We add new contributors to each repo using the all contributors bot. This
35+
generates urls for all of the files across all of our repos where people
36+
contribute to our content and processes.
37+
38+
Parameters:
39+
----------
40+
repos : Union[List[str], str]
41+
A list of GitHub repository names or a single repository name.
42+
43+
Returns:
44+
-------
45+
Union[List[str], str]
46+
A list of URLs if `repos` is a list, or a single URL if `repos` is a string.
47+
"""
3348
base_url = "https://raw.githubusercontent.com/pyOpenSci/"
3449
end_url = "/main/.all-contributorsrc"
35-
repos = [
36-
"python-package-guide",
37-
"software-peer-review",
38-
"pyopensci.github.io",
39-
"software-review",
40-
"update-web-metadata",
41-
]
4250
if isinstance(repos, list):
4351
all_paths = [base_url + repo + end_url for repo in repos]
4452
else:

src/pyosmeta/github_api.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import requests
1616
from dataclasses import dataclass
1717
from dotenv import load_dotenv
18-
from typing import Any
18+
from typing import Any, Optional, Union
1919

2020

2121
@dataclass
@@ -224,3 +224,34 @@ def get_last_commit(self, repo: str) -> str:
224224
date = response[0]["commit"]["author"]["date"]
225225

226226
return date
227+
228+
def get_user_info(
229+
self, gh_handle: str, name: Optional[str] = None
230+
) -> dict[str, Union[str, Any]]:
231+
"""
232+
Get a single user's information from their GitHub username using the
233+
GitHub API
234+
# https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
235+
236+
Parameters
237+
----------
238+
gh_handle : string
239+
Github username to retrieve data for
240+
name : str default=None
241+
A user's name from the contributors.yml file.
242+
https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-a-user
243+
244+
Returns
245+
-------
246+
Dict with updated user data grabbed from the GH API
247+
"""
248+
249+
url = f"https://api.github.com/users/{gh_handle}"
250+
headers = {"Authorization": f"Bearer {self.get_token()}"}
251+
response = requests.get(url, headers=headers)
252+
253+
if response.status_code == 401:
254+
raise ValueError(
255+
"Oops, I couldn't authenticate. Please check your token."
256+
)
257+
return response.json()

tests/conftest.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
11
import pytest
22

3+
from pyosmeta.contributors import ProcessContributors
34
from pyosmeta.github_api import GitHubAPI
45
from pyosmeta.parse_issues import ProcessIssues
56

67

8+
@pytest.fixture
9+
def ghuser_response():
10+
"""This is the initial github response. I changed the username to
11+
create this object"""
12+
expected_response = {
13+
"login": "chayadecacao",
14+
"id": 123456,
15+
"node_id": "MDQ6VXNlcjU3ODU0Mw==",
16+
"avatar_url": "https://avatars.githubusercontent.com/u/123456?v=4",
17+
"gravatar_id": "",
18+
"url": "https://api.github.com/users/cacao",
19+
"html_url": "https://github.com/cacao",
20+
}
21+
return expected_response
22+
23+
24+
@pytest.fixture
25+
def mock_github_api(mocker, ghuser_response):
26+
mock_api = mocker.Mock(spec=GitHubAPI)
27+
mock_api.get_user_info.return_value = ghuser_response
28+
return mock_api
29+
30+
31+
@pytest.fixture
32+
def process_contribs(contrib_github_api):
33+
"""A fixture that creates a"""
34+
return ProcessContributors(contrib_github_api)
35+
36+
737
@pytest.fixture
838
def github_api():
39+
"""A fixture that instantiates an instance of the GitHubAPI object"""
940
return GitHubAPI(
1041
org="pyopensci", repo="pyosmeta", labels=["label1", "label2"]
1142
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pyosmeta.contributors import ProcessContributors
2+
from pyosmeta.github_api import GitHubAPI
3+
4+
5+
def test_init(mocker):
6+
"""Test that the ProcessContributors object instantiates as
7+
expected"""
8+
9+
# Create a mock GitHubAPI object
10+
github_api_mock = mocker.MagicMock(spec=GitHubAPI)
11+
json_files = ["file1.json", "file2.json"]
12+
13+
process_contributors = ProcessContributors(github_api_mock, json_files)
14+
15+
assert process_contributors.github_api == github_api_mock
16+
assert process_contributors.json_files == json_files
17+
18+
19+
def test_return_user_info(mock_github_api, ghuser_response):
20+
"""Test that return from github API user info returns expected
21+
GH username."""
22+
23+
process_contributors = ProcessContributors(mock_github_api, [])
24+
gh_handle = "chayadecacao"
25+
user_info = process_contributors.return_user_info(gh_handle)
26+
27+
assert user_info["github_username"] == gh_handle

tests/unit/test_github_api.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,31 @@ def test_api_endpoint(github_api):
5151
"issues?labels=label1,label2&state=all&per_page=100"
5252
)
5353
assert github_api.api_endpoint == expected_endpoint
54+
55+
56+
def test_get_user_info_successful(mocker, ghuser_response):
57+
"""Test that an expected response returns properly"""
58+
59+
expected_response = ghuser_response
60+
mock_response = mocker.Mock()
61+
mock_response.status_code = 200
62+
mock_response.json.return_value = expected_response
63+
mocker.patch("requests.get", return_value=mock_response)
64+
65+
github_api_instance = GitHubAPI()
66+
user_info = github_api_instance.get_user_info("example_user")
67+
68+
assert user_info == expected_response
69+
70+
71+
def test_get_user_info_bad_credentials(mocker):
72+
"""Test that a value error is raised when the GH token is not
73+
valid."""
74+
mock_response = mocker.Mock()
75+
mock_response.status_code = 401
76+
mocker.patch("requests.get", return_value=mock_response)
77+
78+
github_api = GitHubAPI()
79+
80+
with pytest.raises(ValueError, match="Oops, I couldn't authenticate"):
81+
github_api.get_user_info("example_user")

0 commit comments

Comments
 (0)