Skip to content

Commit 363ac9b

Browse files
author
Leah Wasser
authored
Merge pull request #452 from kcranston/git-github-reorg
Git and GitHub reorg
2 parents abe5864 + 6ce0349 commit 363ac9b

File tree

9 files changed

+550
-527
lines changed

9 files changed

+550
-527
lines changed

abcclassroom/auth.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
abc-classroom.auth
3+
==================
4+
"""
5+
6+
# Methods for setting up authorization to the GitHub API. See
7+
# github.py for methods that use the API.
8+
9+
import requests
10+
11+
import os.path as op
12+
from ruamel.yaml import YAML
13+
14+
from .utils import get_request
15+
16+
17+
def get_github_auth():
18+
"""
19+
Check to see if there is an existing github authentication
20+
and load the authentication.
21+
22+
Returns
23+
-------
24+
ruamel.yaml.comments.CommentedMap
25+
Yaml object that contains the token and id for a github session.
26+
If yaml doesn't exists, return an empty dictionary.
27+
"""
28+
yaml = YAML()
29+
try:
30+
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
31+
config = yaml.load(f)
32+
return config["github"]
33+
34+
except FileNotFoundError:
35+
return {}
36+
37+
38+
def set_github_auth(auth_info):
39+
"""
40+
Set the github authentication information. Put the token and id
41+
authentication information into a yaml file if it doesn't already exist.
42+
43+
Parameters
44+
----------
45+
auth_info : dictionary
46+
The token and id authentication information from github stored in a
47+
dictionary object.
48+
"""
49+
yaml = YAML()
50+
config = {}
51+
if get_github_auth():
52+
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
53+
config = yaml.load(f)
54+
55+
config["github"] = auth_info
56+
57+
with open(op.expanduser("~/.abc-classroom.tokens.yml"), "w") as f:
58+
yaml.dump(config, f)
59+
60+
61+
def get_access_token():
62+
"""Get a GitHub access token for the API
63+
64+
First tries to read from local token file. If token does not exist,
65+
or is not valid, generates a new token using the OAuth Device Flow.
66+
https://docs.github.com/en/free-pro-team@latest/developers/apps/
67+
identifying-and-authorizing-users-for-github-apps#device-flow
68+
69+
Returns an access token (string).
70+
"""
71+
# first, we see if we have a saved token
72+
auth_info = get_github_auth()
73+
if auth_info:
74+
try:
75+
access_token = auth_info["access_token"]
76+
# if so, is it valid?
77+
user = _get_authenticated_user(access_token)
78+
if user is not None:
79+
print(
80+
"Access token is present and valid; successfully "
81+
"authenticated as user {}".format(user)
82+
)
83+
return access_token
84+
except KeyError:
85+
pass
86+
87+
# otherwise, generate a new token
88+
print("Generating new access token")
89+
# client id for the abc-classroom-bot GitHub App
90+
client_id = "Iv1.8df72ad9560c774c"
91+
92+
# TODO need to handle cases where the device call fails - wrong client_id,
93+
# the user could ^C or the internet could be out, or some other
94+
# unanticipated woe)
95+
device_code = _get_login_code(client_id)
96+
access_token = _poll_for_status(client_id, device_code)
97+
98+
# test the new access token
99+
user = _get_authenticated_user(access_token)
100+
if user is not None:
101+
print("""Successfully authenticated as user {}""".format(user))
102+
return access_token
103+
104+
105+
def _get_authenticated_user(token):
106+
"""Test the validity of an access token.
107+
108+
Given a github access token, test that it is valid by making an
109+
API call to get the authenticated user.
110+
111+
Returns the GitHub username of the authenticated user if token valid,
112+
otherwise returns None.
113+
"""
114+
115+
url = "https://api.github.com/user"
116+
(status, body) = get_request(url, token)
117+
try:
118+
user = body["login"]
119+
return user
120+
except KeyError:
121+
return None
122+
123+
124+
def _get_login_code(client_id):
125+
"""Prompts the user to authorize abc-classroom-bot.
126+
127+
First part of the Device Flow workflow. Asks user to visit a URL and
128+
enter the provided code. Waits for user to hit RETURN to continue.
129+
Returns the device code.
130+
"""
131+
132+
# make the device call
133+
header = {"Content-Type": "application/json", "Accept": "application/json"}
134+
payload = {"client_id": client_id}
135+
link = "https://github.com/login/device/code"
136+
r = requests.post(link, headers=header, json=payload)
137+
138+
# process the response
139+
data = r.json()
140+
status = r.status_code
141+
if status != 200:
142+
# print the response if the call failed
143+
print(r.json())
144+
return None
145+
146+
device_code = data["device_code"]
147+
uri = data["verification_uri"]
148+
user_code = data["user_code"]
149+
150+
# prompt the user to enter the code
151+
print(
152+
"To authorize this app, go to {} and enter the code {}".format(
153+
uri, user_code
154+
)
155+
)
156+
input("\nPress RETURN to continue after inputting the code successfully")
157+
158+
return device_code
159+
160+
161+
def _poll_for_status(client_id, device_code):
162+
"""Polls API to see if user entered the device code
163+
164+
This is the second step of the Device Flow. Returns an access token, and
165+
also writes the token to a file in the user's home directory.
166+
"""
167+
168+
header = {"Content-Type": "application/json", "Accept": "application/json"}
169+
payload = {
170+
"client_id": client_id,
171+
"device_code": device_code,
172+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
173+
}
174+
r = requests.post(
175+
"https://github.com/login/oauth/access_token",
176+
headers=header,
177+
json=payload,
178+
)
179+
180+
data = r.json()
181+
access_token = data["access_token"]
182+
set_github_auth({"access_token": access_token})
183+
return access_token

abcclassroom/clone.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import shutil
1010

1111
from . import config as cf
12-
from . import github as gh
12+
from . import git as abcgit
1313

1414

1515
def clone_or_update_repo(organization, repo, clone_dir, skip_existing):
@@ -40,13 +40,13 @@ def clone_or_update_repo(organization, repo, clone_dir, skip_existing):
4040
)
4141
return
4242
try:
43-
gh.pull_from_github(destination_dir)
43+
abcgit.pull_from_github(destination_dir)
4444
except RuntimeError as e:
4545
print("Error pulling repository {}".format(destination_dir))
4646
print(e)
4747
else:
4848
try:
49-
gh.clone_repo(organization, repo, clone_dir)
49+
abcgit.clone_repo(organization, repo, clone_dir)
5050
except RuntimeError as e:
5151
print("Error cloning repository {}".format(repo))
5252
print(e)

abcclassroom/config.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,10 @@
66

77
import pprint
88
from pathlib import Path
9-
import os.path as op
10-
119
from ruamel.yaml import YAML
1210
import ruamel.yaml.composer
1311

1412

15-
def get_github_auth():
16-
"""
17-
Check to see if there is an existing github authentication
18-
and load the authentication.
19-
20-
Returns
21-
-------
22-
ruamel.yaml.comments.CommentedMap
23-
Yaml object that contains the token and id for a github session.
24-
If yaml doesn't exists, return an empty dictionary.
25-
"""
26-
yaml = YAML()
27-
try:
28-
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
29-
config = yaml.load(f)
30-
return config["github"]
31-
32-
except FileNotFoundError:
33-
return {}
34-
35-
36-
def set_github_auth(auth_info):
37-
"""
38-
Set the github authentication information. Put the token and id
39-
authentication information into a yaml file if it doesn't already exist.
40-
41-
Parameters
42-
----------
43-
auth_info : dictionary
44-
The token and id authentication information from github stored in a
45-
dictionary object.
46-
"""
47-
yaml = YAML()
48-
config = {}
49-
if get_github_auth():
50-
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
51-
config = yaml.load(f)
52-
53-
config["github"] = auth_info
54-
55-
with open(op.expanduser("~/.abc-classroom.tokens.yml"), "w") as f:
56-
yaml.dump(config, f)
57-
58-
5913
def get_config(configpath=None):
6014
"""Attempts to read a config file at the provided path (or at
6115
"config.yml" if no path provided). Throws FileNotFoundError if no

abcclassroom/feedback.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import shutil
99

1010
from . import config as cf
11-
from . import github
11+
from . import git as abcgit
1212

1313
# or import just the function we need?
1414
from . import scrub_feedback as sf
@@ -99,15 +99,15 @@ def copy_feedback_files(assignment_name, push_to_github=False, scrub=False):
9999
)
100100
shutil.copy(f, destination_dir)
101101

102-
github.commit_all_changes(
102+
abcgit.commit_all_changes(
103103
destination_dir,
104104
msg="Adding feedback for assignment {}".format(
105105
assignment_name
106106
),
107107
)
108108
if push_to_github:
109109
try:
110-
github.push_to_github(destination_dir)
110+
abcgit.push_to_github(destination_dir)
111111
except RuntimeError as e:
112112
print(e)
113113
print(

0 commit comments

Comments
 (0)