Skip to content

Commit

Permalink
Fix: refactor github methods out of contributor mod
Browse files Browse the repository at this point in the history
Fix: refactor github methods out of contributor mod
  • Loading branch information
lwasser authored Mar 19, 2024
2 parents 6773b23 + 34254ff commit 9f08b17
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 46 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
- Fix: Rename and organize `clean.py` module into `utils_parse` and `utils_clean` (@lwasser, @willingc, #121)
- Fix: Add tests for all utils functions (@lwasser, #122)
- Fix: Bug where date_accepted is removed (@lwasser, #129)
- Fix: Refactor all GitHub related methods move to gh_client module (@lwasser, #125)
- Fix: Refactor all issue related GitHub methods to gh_client module (@lwasser, #125)
- Add: support for partners and emeritus_editor in contributor model (@lwasser, #133)
- Fix: Refactor all contributor GitHub related methods into gh_client module from contributors module (@lwasser, #125)


## [v0.2.3] - 2024-02-29
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ version.source = "vcs"
build.hooks.vcs.version-file = "src/pyosmeta/_version.py"

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

[tool.hatch.envs.test.scripts]
run-coverage = "pytest --cov-config=pyproject.toml --cov=pyosmeta --cov=tests/*"
Expand Down
8 changes: 5 additions & 3 deletions src/pyosmeta/cli/update_contributors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from pyosmeta.contributors import ProcessContributors
from pyosmeta.file_io import create_paths, load_pickle, open_yml_file
from pyosmeta.github_api import GitHubAPI
from pyosmeta.models import PersonModel

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

# Create a list of all contributors across repositories
process_contribs = ProcessContributors(json_files)
github_api = GitHubAPI()
process_contribs = ProcessContributors(github_api, json_files)
bot_all_contribs = process_contribs.combine_json_data()

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

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

# TODO: turn this into a small update method
existing = all_contribs[user].model_dump()
Expand Down
47 changes: 15 additions & 32 deletions src/pyosmeta/contributors.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import json
import os

import requests
from dataclasses import dataclass
from dotenv import load_dotenv
from typing import List, Optional, Tuple
from typing import Any, List, Optional, Tuple

from .github_api import GitHubAPI


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

def __init__(self, json_files: List) -> None:
def __init__(self, github_api: GitHubAPI, json_files: List) -> None:
"""
Parameters
----------
github_api : str
Instantiated instance of a GitHubAPI object
json_files : list
A list of string objects each of which represents a URL to a JSON
file to be parsed
GITHUB_TOKEN : str
A string containing your API token needed to access the github API
"""

self.github_api = github_api
self.json_files = json_files
# self.GITHUB_TOKEN = GITHUB_TOKEN

self.update_keys = [
"twitter",
"website",
Expand All @@ -52,18 +52,6 @@ def __init__(self, json_files: List) -> None:
],
}

def get_token(self) -> str:
"""Fetches the GitHub API key from the users environment. If running
local from an .env file.
Returns
-------
str
The provided API key in the .env file.
"""
load_dotenv()
return os.environ["GITHUB_TOKEN"]

def check_contrib_type(self, json_file: str):
"""
Determine the type of contribution the person
Expand Down Expand Up @@ -94,6 +82,7 @@ def check_contrib_type(self, json_file: str):
contrib_type = "community"
return contrib_type

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

def get_user_info(
self, username: str, aname: Optional[str] = None
) -> dict:
def return_user_info(
self, gh_handle: str, name: Optional[str] = None
) -> dict[str, Any]:
"""
Get a single user's information from their GitHub username using the
GitHub API
# https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
Parameters
----------
username : string
gh_handle : string
Github username to retrieve data for
aname : str default=None
name : str default=None
A user's name from the contributors.yml file.
https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-a-user
Expand All @@ -174,14 +163,8 @@ def get_user_info(
Dict with updated user data grabbed from the GH API
"""

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

# TODO: make an attribute and call it here?
update_keys = {
"name": "name",
"location": "location",
Expand Down
24 changes: 16 additions & 8 deletions src/pyosmeta/file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,24 @@ def _list_to_dict(a_list: List, a_key: str) -> Dict:


def create_paths(repos: Union[list[str], str]) -> Union[list[str], str]:
""" """
"""Construct URLs for .all-contributorsrc file on GitHub for pyos repos.
We add new contributors to each repo using the all contributors bot. This
generates urls for all of the files across all of our repos where people
contribute to our content and processes.
Parameters:
----------
repos : Union[List[str], str]
A list of GitHub repository names or a single repository name.
Returns:
-------
Union[List[str], str]
A list of URLs if `repos` is a list, or a single URL if `repos` is a string.
"""
base_url = "https://raw.githubusercontent.com/pyOpenSci/"
end_url = "/main/.all-contributorsrc"
repos = [
"python-package-guide",
"software-peer-review",
"pyopensci.github.io",
"software-review",
"update-web-metadata",
]
if isinstance(repos, list):
all_paths = [base_url + repo + end_url for repo in repos]
else:
Expand Down
33 changes: 32 additions & 1 deletion src/pyosmeta/github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import requests
from dataclasses import dataclass
from dotenv import load_dotenv
from typing import Any
from typing import Any, Optional, Union


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

return date

def get_user_info(
self, gh_handle: str, name: Optional[str] = None
) -> dict[str, Union[str, Any]]:
"""
Get a single user's information from their GitHub username using the
GitHub API
# https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
Parameters
----------
gh_handle : string
Github username to retrieve data for
name : str default=None
A user's name from the contributors.yml file.
https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-a-user
Returns
-------
Dict with updated user data grabbed from the GH API
"""

url = f"https://api.github.com/users/{gh_handle}"
headers = {"Authorization": f"Bearer {self.get_token()}"}
response = requests.get(url, headers=headers)

if response.status_code == 401:
raise ValueError(
"Oops, I couldn't authenticate. Please check your token."
)
return response.json()
31 changes: 31 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import pytest

from pyosmeta.contributors import ProcessContributors
from pyosmeta.github_api import GitHubAPI
from pyosmeta.parse_issues import ProcessIssues


@pytest.fixture
def ghuser_response():
"""This is the initial github response. I changed the username to
create this object"""
expected_response = {
"login": "chayadecacao",
"id": 123456,
"node_id": "MDQ6VXNlcjU3ODU0Mw==",
"avatar_url": "https://avatars.githubusercontent.com/u/123456?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/cacao",
"html_url": "https://github.com/cacao",
}
return expected_response


@pytest.fixture
def mock_github_api(mocker, ghuser_response):
mock_api = mocker.Mock(spec=GitHubAPI)
mock_api.get_user_info.return_value = ghuser_response
return mock_api


@pytest.fixture
def process_contribs(contrib_github_api):
"""A fixture that creates a"""
return ProcessContributors(contrib_github_api)


@pytest.fixture
def github_api():
"""A fixture that instantiates an instance of the GitHubAPI object"""
return GitHubAPI(
org="pyopensci", repo="pyosmeta", labels=["label1", "label2"]
)
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/test_contributors_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pyosmeta.contributors import ProcessContributors
from pyosmeta.github_api import GitHubAPI


def test_init(mocker):
"""Test that the ProcessContributors object instantiates as
expected"""

# Create a mock GitHubAPI object
github_api_mock = mocker.MagicMock(spec=GitHubAPI)
json_files = ["file1.json", "file2.json"]

process_contributors = ProcessContributors(github_api_mock, json_files)

assert process_contributors.github_api == github_api_mock
assert process_contributors.json_files == json_files


def test_return_user_info(mock_github_api, ghuser_response):
"""Test that return from github API user info returns expected
GH username."""

process_contributors = ProcessContributors(mock_github_api, [])
gh_handle = "chayadecacao"
user_info = process_contributors.return_user_info(gh_handle)

assert user_info["github_username"] == gh_handle
28 changes: 28 additions & 0 deletions tests/unit/test_github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,31 @@ def test_api_endpoint(github_api):
"issues?labels=label1,label2&state=all&per_page=100"
)
assert github_api.api_endpoint == expected_endpoint


def test_get_user_info_successful(mocker, ghuser_response):
"""Test that an expected response returns properly"""

expected_response = ghuser_response
mock_response = mocker.Mock()
mock_response.status_code = 200
mock_response.json.return_value = expected_response
mocker.patch("requests.get", return_value=mock_response)

github_api_instance = GitHubAPI()
user_info = github_api_instance.get_user_info("example_user")

assert user_info == expected_response


def test_get_user_info_bad_credentials(mocker):
"""Test that a value error is raised when the GH token is not
valid."""
mock_response = mocker.Mock()
mock_response.status_code = 401
mocker.patch("requests.get", return_value=mock_response)

github_api = GitHubAPI()

with pytest.raises(ValueError, match="Oops, I couldn't authenticate"):
github_api.get_user_info("example_user")

0 comments on commit 9f08b17

Please sign in to comment.