
* Fix small error in venv setup guide * Add functions for mm integration Implemented: create channels for groups Implemented: create webhooks on both sides for groups As for now these functions can only be called from the Python REPL * Add commands for mm channel/webhook creation Implemented: archive a given list of channels (unused) * Add new features to README * Add filter argument for channel/webhook creation This filter argument is optional and defaults to an empty string, meaning no filtering is required. This is helpful for excluding previous project repos or irrelevant repos. Also added detection logic to handle an exception where a student is on MM but not in the target team. (Perhaps we would want to invite that student immediately?) * Update README Clarify platform difference for venv Restructure Commands & Features section to make room for better docs * Remove unused function from Canvas worker * Add gitea domain name and suffix config items Align with the mm worker, and grant more flexibility Also changed terminology to be clearer (`domain_name` instead of `url`) * Code style and quality updates * Add domain name and suffix config items for Canvas * Return to using dicts to represent groups Removed `StudentGroup` at BoYanZh's request
168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
import functools
|
|
from datetime import datetime
|
|
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
|
|
|
from joint_teapot.config import settings
|
|
from joint_teapot.utils.logger import logger
|
|
from joint_teapot.utils.main import first
|
|
from joint_teapot.workers import Canvas, Git, Gitea, Mattermost
|
|
from joint_teapot.workers.joj import JOJ
|
|
|
|
_T = TypeVar("_T")
|
|
|
|
|
|
def for_all_methods(
|
|
decorator: Callable[[Callable[[_T], _T]], Any]
|
|
) -> Callable[[_T], _T]:
|
|
@functools.wraps(decorator)
|
|
def decorate(cls: Any) -> Any:
|
|
for attr in cls.__dict__: # there's propably a better way to do this
|
|
if callable(getattr(cls, attr)):
|
|
setattr(cls, attr, decorator(getattr(cls, attr)))
|
|
return cls
|
|
|
|
return decorate
|
|
|
|
|
|
def log_exception_in_loguru(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
@functools.wraps(func)
|
|
def decorator(*args: Any, **kwargs: Any) -> Any:
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
|
|
return decorator
|
|
|
|
|
|
@for_all_methods(log_exception_in_loguru)
|
|
class Teapot:
|
|
_canvas = None
|
|
_gitea = None
|
|
_git = None
|
|
_joj = None
|
|
_mattermost = None
|
|
|
|
@property
|
|
def canvas(self) -> Canvas:
|
|
if not self._canvas:
|
|
self._canvas = Canvas()
|
|
return self._canvas
|
|
|
|
@property
|
|
def gitea(self) -> Gitea:
|
|
if not self._gitea:
|
|
self._gitea = Gitea()
|
|
return self._gitea
|
|
|
|
@property
|
|
def git(self) -> Git:
|
|
if not self._git:
|
|
self._git = Git()
|
|
return self._git
|
|
|
|
@property
|
|
def joj(self) -> JOJ:
|
|
if not self._joj:
|
|
self._joj = JOJ()
|
|
return self._joj
|
|
|
|
@property
|
|
def mattermost(self) -> Mattermost:
|
|
if not self._mattermost:
|
|
self._mattermost = Mattermost()
|
|
return self._mattermost
|
|
|
|
def __init__(self) -> None:
|
|
logger.info(
|
|
"Settings loaded. "
|
|
f"Canvas Course ID: {settings.canvas_course_id}, "
|
|
f"Gitea Organization name: {settings.gitea_org_name}, "
|
|
f"Mattermost Team name: {settings.mattermost_team}@{settings.mattermost_domain_name}{settings.mattermost_suffix}"
|
|
)
|
|
logger.debug("Teapot initialized.")
|
|
|
|
def add_all_canvas_students_to_teams(self, team_names: List[str]) -> None:
|
|
return self.gitea.add_canvas_students_to_teams(self.canvas.students, team_names)
|
|
|
|
def create_personal_repos_for_all_canvas_students(self) -> List[str]:
|
|
return self.gitea.create_personal_repos_for_canvas_students(
|
|
self.canvas.students
|
|
)
|
|
|
|
def create_teams_and_repos_by_canvas_groups(
|
|
self, group_prefix: str = ""
|
|
) -> List[str]:
|
|
def convertor(name: str) -> Optional[str]:
|
|
if group_prefix and not name.startswith(group_prefix):
|
|
return None
|
|
team_name, number_str = name.split(" ")
|
|
number = int(number_str)
|
|
return f"{team_name}-{number:02}"
|
|
|
|
return self.gitea.create_teams_and_repos_by_canvas_groups(
|
|
self.canvas.students, self.canvas.groups, convertor, convertor
|
|
)
|
|
|
|
def get_public_key_of_all_canvas_students(self) -> Dict[str, List[str]]:
|
|
return self.gitea.get_public_key_of_canvas_students(self.canvas.students)
|
|
|
|
def clone_all_repos(self) -> None:
|
|
for i, repo_name in enumerate(self.gitea.get_all_repo_names()):
|
|
logger.info(f"{i}, {self.gitea.org_name}/{repo_name} cloning...")
|
|
self.git.repo_clean_and_checkout(repo_name, "master")
|
|
|
|
def create_issue_for_repos(
|
|
self, repo_names: List[str], title: str, body: str
|
|
) -> None:
|
|
for repo_name in repo_names:
|
|
self.gitea.create_issue(repo_name, title, body)
|
|
|
|
def create_milestone_for_repos(
|
|
self, repo_names: List[str], title: str, description: str, due_on: datetime
|
|
) -> None:
|
|
for repo_name in repo_names:
|
|
self.gitea.create_milestone(repo_name, title, description, due_on)
|
|
|
|
def check_exist_issue_by_title(
|
|
self, repo_names: List[str], title: str
|
|
) -> List[str]:
|
|
res = []
|
|
for repo_name in repo_names:
|
|
if not self.gitea.check_exist_issue_by_title(repo_name, title):
|
|
res.append(repo_name)
|
|
return res
|
|
|
|
def checkout_to_repo_by_release_name(
|
|
self, repo_name: str, release_name: str, due: datetime = datetime(3000, 1, 1)
|
|
) -> bool:
|
|
repo_releases = self.gitea.get_repo_releases(repo_name)
|
|
release = first(repo_releases, lambda item: item.name == release_name)
|
|
if release is None or release.created_at.replace(tzinfo=None) >= due:
|
|
logger.warning(
|
|
f"{self.gitea.org_name}/{repo_name} checkout to "
|
|
f"release by name {release_name} fail"
|
|
)
|
|
return False
|
|
self.git.repo_clean_and_checkout(repo_name, f"tags/{release.tag_name}")
|
|
logger.info(
|
|
f"{self.gitea.org_name}/{repo_name} checkout to "
|
|
f"tags/{release.tag_name} succeed"
|
|
)
|
|
return True
|
|
|
|
def get_repos_status(self, commit_lt: int, issue_lt: int) -> None:
|
|
for repo_name, (
|
|
commit_count,
|
|
issue_count,
|
|
) in self.gitea.get_repos_status().items():
|
|
if commit_count < commit_lt or issue_count < issue_lt:
|
|
logger.info(
|
|
f"{self.gitea.org_name}/{repo_name} has "
|
|
f"{commit_count} commit(s), {issue_count} issue(s)"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
teapot = Teapot()
|