Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Travis CI Configuration #94

Merged
merged 12 commits into from
Jan 23, 2018
8 changes: 8 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
language: python
python:
- "3.6"
install:
- pip install -r requirements.txt
script:
- pyflakes assigner
- pylint assigner
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave pylint commented out until I fix all the remaining warnings! I'll start working on that but it'll be a bit (got some 5201 stuff to do first).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already did it, sorry. It's soothing as hell.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're a madman!

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Devel

- Removed remaining lint as specified by pylint.
- Removed old baserepo standalone code.
- Added Travis CI config for pylint and pyflakes.

## 1.0.0

- Rename `token` to `gitlab-token` in the configuration file
Expand Down
8 changes: 4 additions & 4 deletions assigner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ def manage_repos(conf, args, action):
student_section = student["section"]
if "id" not in student:
logging.warning(
"Student {} does not have a gitlab account.".format(username)
"Student %s does not have a gitlab account.", username
)
continue
full_name = StudentRepo.name(semester, student_section,
hw_name, username)
full_name = StudentRepo.build_name(semester, student_section,
hw_name, username)

try:
repo = StudentRepo(host, namespace, full_name, token)
Expand Down Expand Up @@ -164,7 +164,7 @@ def main():
if args.tracebacks:
raise e
if isinstance(e, KeyError):
logger.error(str(e) + " is missing")
logger.error("%s is missing", e)
else:
logger.error(str(e))
raise SystemExit(1) from e
Expand Down
75 changes: 31 additions & 44 deletions assigner/baserepo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
import re
import requests
import tempfile

from enum import Enum
from requests.exceptions import HTTPError
Expand Down Expand Up @@ -67,7 +66,7 @@ def from_url(cls, url, token):
namespace, name, token, url)

logging.debug(json.dumps(self.info))
logging.debug("{} is valid.".format(self.name_with_namespace))
logging.debug("%s is valid.", self.name_with_namespace)

return self

Expand Down Expand Up @@ -145,7 +144,8 @@ def info(self):
self._info = self._gl_get(url)
except HTTPError as e:
if e.response.status_code == 404:
logging.debug("Could not find repo with url {}/api/v4{}.".format(self.url_base,url))
logging.debug("Could not find repo with url %s/api/v4%s.",
self.url_base, url)
self._info = None
else:
raise
Expand Down Expand Up @@ -178,7 +178,7 @@ def get_head(self, branch):
if head.name == branch:
return head

return self.repo.create_head(b, "origin/{}".format(b))
return self.repo.create_head(branch, "origin/{}".format(branch))

def checkout(self, branch):
return self.get_head(branch).checkout()
Expand All @@ -190,18 +190,18 @@ def pull(self, branch):
self.repo.remote().pull(branch)

def clone_to(self, dir_name, branch=None):
logging.debug("Cloning {}...".format(self.ssh_url))
logging.debug("Cloning %s...", self.ssh_url)
try:
if branch:
self._repo = git.Repo.clone_from(self.ssh_url, dir_name,
branch=branch)
branch=branch)
for b in branch:
self._repo.create_head(b, "origin/{}".format(b))

logging.debug(self._repo.heads)
else:
self._repo = git.Repo.clone_from(self.ssh_url, dir_name)
logging.debug("Cloned {}.".format(self.name))
logging.debug("Cloned %s.", self.name)
except git.exc.GitCommandError as e:
# GitPython may delete this directory
# and the caller may have opinions about that,
Expand All @@ -213,40 +213,39 @@ def clone_to(self, dir_name, branch=None):

def add_local_copy(self, dir_name):
if self.repo is not None:
logging.warn("You already have a local copy associated with this repo")
logging.warning("You already have a local copy associated with this repo")
return

logging.debug("Using {} for the local repo...".format(dir_name))
logging.debug("Using %s for the local repo...", dir_name)
self._repo = git.Repo(dir_name)

def delete(self):
self._gl_delete("/projects/{}".format(self.id))
logging.debug("Deleted {}.".format(self.name))
logging.debug("Deleted %s.", self.name)

# TODO: this should really go elsewhere
@classmethod
def get_user_id(cls, username, url_base, token):
data = cls._cls_gl_get(url_base, "/users", token,
params={"search": username})

if len(data) == 0:
logging.warn(
"Did not find any users matching {}.".format(username)
if not data:
logging.warning(
"Did not find any users matching %s.", username
)
raise RepoError("No user {}.".format(username))

for result in data:
if result["username"] == username:
logging.info(
"Got id {} for user {}.".format(data[0]["id"], username)
"Got id %s for user %s.", data[0]["id"], username
)
return result["id"]

# Fall back to first result if all else fails
logging.warn("Got {} users for {}.".format(len(data), username))
logging.warn("Failed to find an exact match for {}.".format(username))
logging.warning("Got %s users for %s.", len(data), username)
logging.warning("Failed to find an exact match for %s.", username)
logging.info(
"Got id {} for user {}.".format(data[0]["id"], data[0]["username"])
"Got id %s for user %s.", data[0]["id"], data[0]["username"]
)
return data[0]["id"]

Expand Down Expand Up @@ -332,27 +331,30 @@ def archive(self):
def unarchive(self):
return self._gl_post("/projects/{}/unarchive".format(self.id))

def protect(self, branch="master", developer_push=True, developer_merge=True): # NOTE: these are not the same defaults that Gitlab uses
# NOTE: these are not the same defaults that Gitlab uses
def protect(self, branch="master", developer_push=True, developer_merge=True):
params = {
"developers_can_push": developer_push,
"developers_can_merge": developer_merge,
}
return self._gl_put("/projects/{}/repository/branches/{}/protect".format(self.id, branch), params)
}
return self._gl_put("/projects/{}/repository/branches/{}/protect"
.format(self.id, branch), params)

def unprotect(self, branch="master"):
return self._gl_put("/projects/{}/repository/branches/{}/unprotect".format(self.id, branch))
return self._gl_put("/projects/{}/repository/branches/{}/unprotect"
.format(self.id, branch))

def _gl_get(self, path, params={}):
return self.__class__._cls_gl_get(
self.url_base, path, self.token, params
)

def _gl_post(self, path, payload={}, params={}):
def _gl_post(self, path, payload={}, _=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should actually probably be passing params through to _cls_gl_post here...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done. This is when happens when you fix lint without reading code.

return self.__class__._cls_gl_post(
self.url_base, path, self.token, payload
)

def _gl_put(self, path, payload={}, params={}):
def _gl_put(self, path, payload={}, _=None):
return self.__class__._cls_gl_put(
self.url_base, path, self.token, payload
)
Expand All @@ -370,11 +372,11 @@ def new(cls, name, namespace, url_base, token):
namespaces = cls._cls_gl_get(url_base, "/namespaces",
token, {"search": namespace})
logging.debug(
"Got {} namespaces matching {}.".format(len(namespaces), namespace)
"Got %s namespaces matching %s.", len(namespaces), namespace
)
logging.debug(
"Using namespace " +
"{} with ID {}.".format(namespaces[0]["path"], namespaces[0]["id"])
"%s with ID %s.", namespaces[0]["path"], namespaces[0]["id"]
)

payload = {
Expand All @@ -396,7 +398,7 @@ def push_to(self, student_repo, branch="master"):
r = git.Remote.add(self.repo, student_repo.name,
student_repo.ssh_url)
r.push(branch)
logging.debug("Pushed {} to {}.".format(self.name, student_repo.name))
logging.debug("Pushed %s to %s.", self.name, student_repo.name)


class StudentRepo(Repo):
Expand All @@ -406,7 +408,7 @@ class StudentRepo(Repo):
def new(cls, base_repo, semester, section, username, token):
"""Create a new repository on GitLab"""
payload = {
"name": cls.name(semester, section, base_repo.name, username),
"name": cls.build_name(semester, section, base_repo.name, username),
"namespace_id": base_repo.namespace_id,
"issues_enabled": False,
"merge_requests_enabled": False,
Expand All @@ -422,7 +424,7 @@ def new(cls, base_repo, semester, section, username, token):
return cls.from_url(result["http_url_to_repo"], token)

@classmethod
def name(cls, semester, section, assignment, user):
def build_name(cls, semester, section, assignment, user):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method had some weird conflict with Repo.name set in init. I highly doubt this ever caused a bug.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn't work anyway... functions were missing args.

fmt = {
"semester": semester,
"section": section,
Expand All @@ -436,18 +438,3 @@ def name(cls, semester, section, assignment, user):
def push(self, base_repo, branch="master"):
"""Push base_repo code to this repo"""
base_repo.push_to(self, branch)


if __name__ == "__main__":
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed this wasn't being used and was for testing / from the old one file assigner.
@LinuxMercedes @michaelwisely confirm?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that all can go!

# Need to make this module if'n you're going to run this
from secret import token

logging.basicConfig(level=logging.INFO)

b = BaseRepo("https://git.mst.edu/2016-Spring-CS-2001/hw01.git", token)
print(json.dumps(b.info, indent=8))
with tempfile.TemporaryDirectory() as tmpdirname:
b.clone_to(tmpdirname)
print(os.listdir(tmpdirname))

StudentRepo.new(b, "2016SP", "A", "mwwcp2", token)
8 changes: 4 additions & 4 deletions assigner/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ def _request(self, method: str, url: str, params: dict = None, retries: int = 3)
header = self.REQUEST_HEADER
connection.request(method, url, (json.dumps(params) if params is not None else None), header)
return connection.getresponse()
except Exception as ex:
except http.client.HTTPException:
tries += 1
if tries > retries:
raise ex
raise
logging.warning("Caught exception in request after %d tries. Will retry %d more times.",
tries, retries - tries, exc_info=True)
time.sleep(1)
Expand Down Expand Up @@ -59,7 +59,7 @@ def _get_all_pages(self, url: str, params: dict = None) -> list:
count = len(result)
page = 1
while 'next' in links:
logging.debug("Got links:\n" + json.dumps(links, sort_keys=True, indent=2))
logging.debug("Got links:\n%s", json.dumps(links, sort_keys=True, indent=2))
page += 1
next_url = url.split('?')[0] + '?page=%s&per_page=%s' % (page, count)
logging.info("Getting next page for " + url + " via " + next_url)
Expand All @@ -71,7 +71,7 @@ def _get_all_pages(self, url: str, params: dict = None) -> list:

def get_instructor_courses(self):
get = lambda x: self._get_all_pages('/api/v1/courses',
{'enrollment_type': x, 'state': ['available']})
{'enrollment_type': x, 'state': ['available']})
result = get('teacher')
result.extend(get('ta'))
result.extend(get('grader'))
Expand Down
44 changes: 24 additions & 20 deletions assigner/commands/assign.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from requests.exceptions import HTTPError

help="Assign a base repo to students"
help = "Assign a base repo to students"

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -40,14 +40,16 @@ def assign(conf, args):
print("Assigning '{}' to {} student{} in {}.".format(
hw_name, student_count,
"s" if student_count != 1 else "",
"section " + args.section if args.section else "all sections")
)
"section " + args.section if args.section else "all sections"
))
base = BaseRepo(host, namespace, hw_name, token)
if not dry_run:
try:
base.clone_to(tmpdirname, branch)
except RepoError as e:
logging.error("Could not clone base repo (have you pushed at least one commit to it?)")
logging.error(
"Could not clone base repo (have you pushed at least one commit to it?)"
)
logging.debug(e)
return

Expand All @@ -58,8 +60,8 @@ def assign(conf, args):
for i, student in progress.enumerate(roster):
username = student["username"]
student_section = student["section"]
full_name = StudentRepo.name(semester, student_section,
hw_name, username)
full_name = StudentRepo.build_name(semester, student_section,
hw_name, username)
repo = StudentRepo(host, namespace, full_name, token)

if not repo.already_exists():
Expand All @@ -72,18 +74,20 @@ def assign(conf, args):
actual_count += 1
logging.debug("Assigned.")
elif force:
logging.info("{}: Already exists, deleting...".format(full_name))
logging.info("%s: Already exists, deleting...", full_name)
if not dry_run:
repo.delete()

# Gitlab will throw a 400 if you delete and immediately recreate a repo.
# We retry w/ exponential backoff up to 5 times
# Gitlab will throw a 400 if you delete and immediately
# recreate a repo. We retry w/ exponential backoff up
# to 5 times
wait = 0.1
retries = 0
while True:
try:
repo = StudentRepo.new(base, semester, student_section,
username, token)
repo = StudentRepo.new(
base, semester, student_section, username, token
)
logger.debug("Success!")
break
except HTTPError as e:
Expand All @@ -102,7 +106,7 @@ def assign(conf, args):
actual_count += 1
logging.debug("Assigned.")
elif args.branch:
logging.info("{}: Already exists.".format(full_name))
logging.info("%s: Already exists.", full_name)
# If we have an explicit branch, push anyways
if not dry_run:
repo.push(base, branch)
Expand All @@ -111,7 +115,7 @@ def assign(conf, args):
actual_count += 1
logging.debug("Assigned.")
else:
logging.info("{}: Already exists, skipping...".format(full_name))
logging.info("%s: Already exists, skipping...", full_name)
i += 1

progress.finish()
Expand All @@ -129,16 +133,16 @@ def assign(conf, args):

def setup_parser(parser):
parser.add_argument("name",
help="Name of the assignment to assign.")
help="Name of the assignment to assign.")
parser.add_argument("--branch", "--branches", nargs="+",
help="Branch or branches to push")
help="Branch or branches to push")
parser.add_argument("--section", nargs="?",
help="Section to assign homework to")
help="Section to assign homework to")
parser.add_argument("--student", metavar="id",
help="ID of the student to assign to.")
help="ID of the student to assign to.")
parser.add_argument("--dry-run", action="store_true",
help="Don't actually do it.")
help="Don't actually do it.")
parser.add_argument("-f", "--force", action="store_true", dest="force",
help="Delete and recreate already existing " +
"student repos.")
help="Delete and recreate already existing "
"student repos.")
parser.set_defaults(run=assign)
Loading