def _convert_404_to_not_found_error(msg): try: yield except gitlab.exceptions.GitlabError as exc: if exc.response_code == 404: raise plug.NotFoundError(msg) raise plug.PlatformError(str(exc), status=exc.response_code) from exc
def _try_api_request(ignore_statuses: Optional[Iterable[int]] = None): """Context manager for trying API requests. Args: ignore_statuses: One or more status codes to ignore (only applicable if the exception is a gitlab.exceptions.GitlabError). """ try: yield except gitlab.exceptions.GitlabError as e: if ignore_statuses and e.response_code in ignore_statuses: return if e.response_code == 404: raise plug.NotFoundError(str(e), status=404) from e elif e.response_code == 401: raise plug.BadCredentials( "credentials rejected, verify that token has correct access.", status=401, ) from e else: raise plug.PlatformError(str(e), status=e.response_code) from e except (exception.RepoBeeException, plug.PlugError): raise except Exception as e: raise plug.UnexpectedException( f"a {type(e).__name__} occured unexpectedly: {str(e)}") from e
def _convert_404_to_not_found_error(msg): try: yield except gitlab.exceptions.GitlabError as exc: if exc.response_code == 404: raise plug.NotFoundError(msg) raise plug.UnexpectedException( f"An unexpected exception occured. {type(exc).__name__}: {exc}")
def get_repo(self, repo_name: str, team_name: Optional[str]) -> plug.Repo: """See :py:meth:`repobee_plug.PlatformAPI.get_repo`.""" repos = (self._get_team(team_name).repos if team_name else self._repos[self._org_name].values()) for repo in repos: if repo.name == repo_name: return repo.to_plug_repo() raise plug.NotFoundError(f"{team_name} has no repository {repo_name}")
def _get_organization(self, org_name): matches = [ g for g in self._gitlab.groups.list(search=org_name) if g.path == org_name ] if not matches: raise plug.NotFoundError(org_name, status=404) return matches[0]
def _convert_404_to_not_found_error(msg): """Catch a github.GithubException with status 404 and convert to plug.NotFoundError with the provided message. If the GithubException does not have status 404, instead raise plug.UnexpectedException. """ try: yield except github.GithubException as exc: if exc.status == 404: raise plug.NotFoundError(msg) raise plug.UnexpectedException( f"An unexpected exception occured. {type(exc).__name__}: {exc}")
def delete_repo(self, repo: plug.Repo) -> None: """See :py:meth:`repobee_plug.PlatformAPI.delete_repo`.""" repo_bucket = self._repos.get(self._org_name, {}) if repo.name not in repo_bucket: raise plug.NotFoundError( f"no such repo '{self._org_name}/{repo.name}'") repo_path = self._repodir / self._org_name / repo.name shutil.rmtree(repo_path) del repo_bucket[repo.name] for team in self._teams[self._org_name].values(): try: team.repos.remove(repo.implementation) except KeyError: pass
def _verify_group(group_name: str, gl: gitlab.Gitlab) -> None: """Check that the group exists and that the user is an owner.""" user = gl.user.username plug.echo(f"Trying to fetch group {group_name}") slug_matched = [ group for group in gl.groups.list(search=group_name) if group.path == group_name ] if not slug_matched: raise plug.NotFoundError( f"Could not find group with slug {group_name}. Verify that " f"you have access to the group, and that you've provided " f"the slug (the name in the address bar).") group = slug_matched[0] plug.echo(f"SUCCESS: Found group {group.name}") plug.echo( f"Verifying that user {user} is an owner of group {group_name}") GitLabAPI._verify_membership(user, group)
def _connect_to_api(base_url: str, token: str, org_name: str, user: str) -> plug.PlatformAPI: """Return an API instance connected to the specified API endpoint.""" required_args = plug.manager.hook.api_init_requires() kwargs = {} if "base_url" in required_args: kwargs["base_url"] = base_url if "token" in required_args: kwargs["token"] = token if "org_name" in required_args: kwargs["org_name"] = org_name if "user" in required_args: kwargs["user"] = user api_class = plug.manager.hook.get_api_class() try: return api_class(**kwargs) except plug.NotFoundError: # more informative message raise plug.NotFoundError("either organization {} could not be found, " "or the base url '{}' is incorrect".format( org_name, base_url))
def _try_api_request(ignore_statuses: Optional[Iterable[int]] = None): """Context manager for trying API requests. Args: ignore_statuses: One or more status codes to ignore (only applicable if the exception is a github.GithubException). Raises: plug.NotFoundError plug.BadCredentials plug.PlatformError plug.ServiceNotFoundError plug.UnexpectedException """ try: yield except plug.PlugError: raise except github.GithubException as e: if ignore_statuses and e.status in ignore_statuses: return if e.status == 404: raise plug.NotFoundError(str(e), status=404) elif e.status == 401: raise plug.BadCredentials( "credentials rejected, verify that token has correct access.", status=401, ) else: raise plug.PlatformError(str(e), status=e.status) except gaierror: raise plug.ServiceNotFoundError( "GitHub service could not be found, check the url" ) except Exception as e: raise plug.UnexpectedException( "a {} occured unexpectedly: {}".format(type(e).__name__, str(e)) )
def _get_team(self, team_id: str) -> Team: if team_id not in self._teams[self._org_name]: raise plug.NotFoundError(f"invalid team id: {team_id}") return self._teams[self._org_name][team_id]
def _get_user(self, username: str) -> User: if username not in self._users: raise plug.NotFoundError(f"no such user: {username}") return self._users[username]
def assign_peer_reviews( assignment_names: Iterable[str], teams: Iterable[plug.StudentTeam], num_reviews: int, issue: Optional[plug.Issue], double_blind_key: Optional[str], api: plug.PlatformAPI, ) -> None: """Assign peer reviewers among the students to each student repo. Each student is assigned to review num_reviews repos, and consequently, each repo gets reviewed by num_reviews reviewers. In practice, each student repo has a review team generated (called <student-repo-name>-review), to which num_reviews _other_ students are assigned. The team itself is given pull-access to the student repo, so that reviewers can view code and open issues, but cannot modify the contents of the repo. Args: assignment_names: Names of assginments. teams: Team objects specifying student groups. num_reviews: Amount of reviews each student should perform (consequently, the amount of reviews of each repo) issue: An issue with review instructions to be opened in the considered repos. double_blind_key: If provided, use key to make double-blind review allocation. api: An implementation of :py:class:`repobee_plug.PlatformAPI` used to interface with the platform (e.g. GitHub or GitLab) instance. """ issue = issue or DEFAULT_REVIEW_ISSUE expected_repo_names = set(plug.generate_repo_names(teams, assignment_names)) fetched_teams = progresswrappers.get_teams(teams, api, desc="Fetching teams and repos") team_repo_tuples = [(team, list(api.get_team_repos(team))) for team in fetched_teams] fetched_repos = list( itertools.chain.from_iterable(repos for _, repos in team_repo_tuples)) fetched_repo_dict = {r.name: r for r in fetched_repos} missing = expected_repo_names - fetched_repo_dict.keys() if missing: raise plug.NotFoundError(f"Can't find repos: {', '.join(missing)}") if double_blind_key: plug.log.info(f"Creating anonymous repos with key: {double_blind_key}") fetched_repo_dict = _create_anonymized_repos( [(team, _only_expected_repos(repos, expected_repo_names)) for team, repos in team_repo_tuples], double_blind_key, api, ) allocations_for_output = [] for assignment_name in assignment_names: plug.echo("Allocating reviews") allocations = plug.manager.hook.generate_review_allocations( teams=teams, num_reviews=num_reviews) # adjust names of review teams review_team_specs, reviewed_team_names = list( zip(*[( plug.StudentTeam( members=alloc.review_team.members, name=_review_team_name( alloc.reviewed_team, assignment_name, key=double_blind_key, ), ), alloc.reviewed_team, ) for alloc in allocations])) review_teams = _repobee.command.teams.create_teams( review_team_specs, plug.TeamPermission.PULL, api) review_teams_progress = plug.cli.io.progress_bar( review_teams, desc="Creating review teams", total=len(review_team_specs), ) for review_team, reviewed_team_name in zip(review_teams_progress, reviewed_team_names): reviewed_repo = fetched_repo_dict[plug.generate_repo_name( reviewed_team_name, assignment_name)] review_teams_progress.write( # type: ignore f"Assigning {' and '.join(review_team.members)} " f"to review {reviewed_repo.name}") api.assign_repo(review_team, reviewed_repo, plug.TeamPermission.PULL) api.create_issue( issue.title, issue.body, reviewed_repo, # It's not possible to assign users with read-access in Gitea # FIXME redesign so Gitea does not require special handling assignees=review_team.members if not isinstance(api, _repobee.ext.gitea.GiteaAPI) else None, ) allocations_for_output.append({ "reviewed_repo": { "name": reviewed_repo.name, "url": reviewed_repo.url, }, "review_team": { "name": review_team.name, "members": review_team.members, }, }) if featflags.is_feature_enabled( featflags.FeatureFlag.REPOBEE_4_REVIEW_COMMANDS): output = dict(allocations=allocations_for_output, num_reviews=num_reviews) pathlib.Path("review_allocations.json").write_text( json.dumps(output, indent=4), encoding=sys.getdefaultencoding(), )
def assign_peer_reviews( assignment_names: Iterable[str], teams: Iterable[plug.StudentTeam], num_reviews: int, issue: Optional[plug.Issue], api: plug.PlatformAPI, ) -> None: """Assign peer reviewers among the students to each student repo. Each student is assigned to review num_reviews repos, and consequently, each repo gets reviewed by num_reviews reviewers. In practice, each student repo has a review team generated (called <student-repo-name>-review), to which num_reviews _other_ students are assigned. The team itself is given pull-access to the student repo, so that reviewers can view code and open issues, but cannot modify the contents of the repo. Args: assignment_names: Names of assginments. teams: Team objects specifying student groups. num_reviews: Amount of reviews each student should perform (consequently, the amount of reviews of each repo) issue: An issue with review instructions to be opened in the considered repos. api: An implementation of :py:class:`repobee_plug.PlatformAPI` used to interface with the platform (e.g. GitHub or GitLab) instance. """ issue = issue or DEFAULT_REVIEW_ISSUE expected_repo_names = plug.generate_repo_names(teams, assignment_names) fetched_teams = progresswrappers.get_teams(teams, api, desc="Fetching teams and repos") fetched_repos = list( itertools.chain.from_iterable(map(api.get_team_repos, fetched_teams))) fetched_repo_dict = {r.name: r for r in fetched_repos} missing = set(expected_repo_names) - set(fetched_repo_dict.keys()) if missing: raise plug.NotFoundError(f"Can't find repos: {', '.join(missing)}") for assignment_name in assignment_names: plug.echo("Allocating reviews") allocations = plug.manager.hook.generate_review_allocations( teams=teams, num_reviews=num_reviews) # adjust names of review teams review_team_specs, reviewed_team_names = list( zip(*[( plug.StudentTeam( members=alloc.review_team.members, name=plug.generate_review_team_name( str(alloc.reviewed_team), assignment_name), ), alloc.reviewed_team, ) for alloc in allocations])) review_teams = _repobee.command.teams.create_teams( review_team_specs, plug.TeamPermission.PULL, api) review_teams_progress = plug.cli.io.progress_bar( review_teams, desc="Creating review teams", total=len(review_team_specs), ) for review_team, reviewed_team_name in zip(review_teams_progress, reviewed_team_names): reviewed_repo = fetched_repo_dict[plug.generate_repo_name( reviewed_team_name, assignment_name)] review_teams_progress.write( # type: ignore f"Assigning {' and '.join(review_team.members)} " f"to review {reviewed_repo.name}") api.assign_repo(review_team, reviewed_repo, plug.TeamPermission.PULL) api.create_issue( issue.title, issue.body, reviewed_repo, assignees=review_team.members, )