feat: add exclusive tag for addition and deletion
This commit is contained in:
parent
594337a54d
commit
a2f6a83b09
|
|
@ -174,7 +174,9 @@ def close_all_issues() -> None:
|
||||||
def close_issues(
|
def close_issues(
|
||||||
repo_name: str,
|
repo_name: str,
|
||||||
issue_numbers: List[int] = Argument(..., help="One or more issue numbers to close"),
|
issue_numbers: List[int] = Argument(..., help="One or more issue numbers to close"),
|
||||||
dry_run: bool = Option(False, "--dry-run/--no-dry-run", help="Enable dry run (no changes will be made)"),
|
dry_run: bool = Option(
|
||||||
|
False, "--dry-run/--no-dry-run", help="Enable dry run (no changes will be made)"
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
tea.pot.gitea.close_issues(repo_name, issue_numbers, dry_run)
|
tea.pot.gitea.close_issues(repo_name, issue_numbers, dry_run)
|
||||||
|
|
||||||
|
|
@ -187,11 +189,19 @@ def label_issues(
|
||||||
label_name: str,
|
label_name: str,
|
||||||
repo_name: str,
|
repo_name: str,
|
||||||
issue_numbers: List[int] = Argument(..., help="One or more issue numbers to label"),
|
issue_numbers: List[int] = Argument(..., help="One or more issue numbers to label"),
|
||||||
issue_color: Optional[str] = Option(None, "--color", help="Color for newly created label (hex without # or with #)"),
|
issue_color: Optional[str] = Option(
|
||||||
dry_run: bool = Option(False, "--dry-run/--no-dry-run", help="Dry run: show what would be labeled without making changes"),
|
None, "--color", help="Color for newly created label (hex without # or with #)"
|
||||||
|
),
|
||||||
|
dry_run: bool = Option(
|
||||||
|
False,
|
||||||
|
"--dry-run/--no-dry-run",
|
||||||
|
help="Dry run: show what would be labeled without making changes",
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
logger.info(f"Dry run: would add label '{label_name}' to {repo_name} issues: {issue_numbers}")
|
logger.info(
|
||||||
|
f"Dry run: would add label '{label_name}' to {repo_name} issues: {issue_numbers}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
tea.pot.gitea.label_issues(repo_name, label_name, issue_numbers, color=issue_color)
|
tea.pot.gitea.label_issues(repo_name, label_name, issue_numbers, color=issue_color)
|
||||||
|
|
||||||
|
|
@ -203,19 +213,41 @@ def label_issues(
|
||||||
def delete_label(
|
def delete_label(
|
||||||
label_name: str,
|
label_name: str,
|
||||||
repo_name: str,
|
repo_name: str,
|
||||||
issue_numbers: List[int] = Argument(None, help="Zero or more issue numbers to remove the label from (if empty and --repo is set, delete repo label)"),
|
issue_numbers: List[int] = Argument(
|
||||||
repo: bool = Option(False, "--repo", help="Delete the repo-level label when set and issue_numbers is empty"),
|
None,
|
||||||
dry_run: bool = Option(False, "--dry-run/--no-dry-run", help="Dry run: show what would be removed without making changes"),
|
help="Zero or more issue numbers to remove the label from (if empty and --repo is set, delete repo label)",
|
||||||
|
),
|
||||||
|
repo: bool = Option(
|
||||||
|
False,
|
||||||
|
"--repo",
|
||||||
|
help="Delete the repo-level label when set and issue_numbers is empty",
|
||||||
|
),
|
||||||
|
dry_run: bool = Option(
|
||||||
|
False,
|
||||||
|
"--dry-run/--no-dry-run",
|
||||||
|
help="Dry run: show what would be removed without making changes",
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
if issue_numbers:
|
if issue_numbers:
|
||||||
logger.info(f"Dry run: would remove label '{label_name}' from {repo_name} issues: {issue_numbers}")
|
logger.info(
|
||||||
|
f"Dry run: would remove label '{label_name}' from {repo_name} issues: {issue_numbers}"
|
||||||
|
)
|
||||||
elif repo:
|
elif repo:
|
||||||
logger.info(f"Dry run: would delete repo label '{label_name}' from {repo_name}")
|
logger.info(
|
||||||
|
f"Dry run: would delete repo label '{label_name}' from {repo_name}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("Dry run: no action specified (provide issue numbers or --repo to delete repo label)")
|
logger.info(
|
||||||
|
"Dry run: no action specified (provide issue numbers or --repo to delete repo label)"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
tea.pot.gitea.delete_label(repo_name, label_name, issue_numbers if issue_numbers else None, delete_repo_label=repo)
|
tea.pot.gitea.delete_label(
|
||||||
|
repo_name,
|
||||||
|
label_name,
|
||||||
|
issue_numbers if issue_numbers else None,
|
||||||
|
delete_repo_label=repo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
@app.command(
|
||||||
|
|
@ -299,18 +331,33 @@ def create_personal_channels_on_mm(
|
||||||
help="update Mattermost channels for student groups based on team information on Gitea; only add missing members",
|
help="update Mattermost channels for student groups based on team information on Gitea; only add missing members",
|
||||||
)
|
)
|
||||||
def update_group_channels_on_mm(
|
def update_group_channels_on_mm(
|
||||||
prefix: str = Option("", help="Only process repositories starting with this prefix"),
|
prefix: str = Option(
|
||||||
|
"", help="Only process repositories starting with this prefix"
|
||||||
|
),
|
||||||
suffix: str = Option("", help="Only process channels ending with this suffix"),
|
suffix: str = Option("", help="Only process channels ending with this suffix"),
|
||||||
update_teaching_team: bool = Option(True, "--update-teaching-team/--no-update-teaching-team", help="Whether to update teaching team"),
|
update_teaching_team: bool = Option(
|
||||||
dry_run: bool = Option(False, "--dry-run/--no-dry-run", help="Dry run: show what would be added without making changes"),
|
True,
|
||||||
|
"--update-teaching-team/--no-update-teaching-team",
|
||||||
|
help="Whether to update teaching team",
|
||||||
|
),
|
||||||
|
dry_run: bool = Option(
|
||||||
|
False,
|
||||||
|
"--dry-run/--no-dry-run",
|
||||||
|
help="Dry run: show what would be added without making changes",
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
groups = {
|
groups = {
|
||||||
group_name: members
|
group_name: members
|
||||||
for group_name, members in tea.pot.gitea.get_all_teams().items()
|
for group_name, members in tea.pot.gitea.get_all_teams().items()
|
||||||
if group_name.startswith(prefix)
|
if group_name.startswith(prefix)
|
||||||
}
|
}
|
||||||
logger.info(f"{len(groups)} group channel(s) to update" + (f" with suffix {suffix}" if suffix else ""))
|
logger.info(
|
||||||
tea.pot.mattermost.update_channels_for_groups(groups, suffix, update_teaching_team, dry_run)
|
f"{len(groups)} group channel(s) to update"
|
||||||
|
+ (f" with suffix {suffix}" if suffix else "")
|
||||||
|
)
|
||||||
|
tea.pot.mattermost.update_channels_for_groups(
|
||||||
|
groups, suffix, update_teaching_team, dry_run
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
@app.command(
|
||||||
|
|
@ -318,10 +365,20 @@ def update_group_channels_on_mm(
|
||||||
help="update personal Mattermost channels for every student; only add missing members",
|
help="update personal Mattermost channels for every student; only add missing members",
|
||||||
)
|
)
|
||||||
def update_personal_channels_on_mm(
|
def update_personal_channels_on_mm(
|
||||||
update_teaching_team: bool = Option(True, "--update-teaching-team/--no-update-teaching-team", help="Whether to update teaching team"),
|
update_teaching_team: bool = Option(
|
||||||
dry_run: bool = Option(False, "--dry-run/--no-dry-run", help="Dry run: show what would be added without making changes"),
|
True,
|
||||||
|
"--update-teaching-team/--no-update-teaching-team",
|
||||||
|
help="Whether to update teaching team",
|
||||||
|
),
|
||||||
|
dry_run: bool = Option(
|
||||||
|
False,
|
||||||
|
"--dry-run/--no-dry-run",
|
||||||
|
help="Dry run: show what would be added without making changes",
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
tea.pot.mattermost.update_channels_for_individuals(tea.pot.canvas.students, update_teaching_team, dry_run)
|
tea.pot.mattermost.update_channels_for_individuals(
|
||||||
|
tea.pot.canvas.students, update_teaching_team, dry_run
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
@app.command(
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,12 @@ class Canvas:
|
||||||
re.sub(r"[^\x00-\x7F]+", "", student.name).strip().title()
|
re.sub(r"[^\x00-\x7F]+", "", student.name).strip().title()
|
||||||
) # We only care english name
|
) # We only care english name
|
||||||
# Some users (like system users, announcers) might not have login_id
|
# Some users (like system users, announcers) might not have login_id
|
||||||
if hasattr(student, 'login_id') and student.login_id:
|
if hasattr(student, "login_id") and student.login_id:
|
||||||
student.sis_id = student.login_id
|
student.sis_id = student.login_id
|
||||||
student.login_id = student.email.split("@")[0]
|
student.login_id = student.email.split("@")[0]
|
||||||
else:
|
else:
|
||||||
# For users without login_id, use email prefix as both sis_id and login_id
|
# For users without login_id, use email prefix as both sis_id and login_id
|
||||||
if hasattr(student, 'email') and student.email:
|
if hasattr(student, "email") and student.email:
|
||||||
student.login_id = student.email.split("@")[0]
|
student.login_id = student.email.split("@")[0]
|
||||||
student.sis_id = student.login_id
|
student.sis_id = student.login_id
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import re
|
import re
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, cast
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
import focs_gitea
|
import focs_gitea
|
||||||
import requests
|
import requests # type: ignore
|
||||||
from urllib.parse import quote
|
|
||||||
from canvasapi.group import Group, GroupMembership
|
from canvasapi.group import Group, GroupMembership
|
||||||
from canvasapi.paginated_list import PaginatedList
|
from canvasapi.paginated_list import PaginatedList
|
||||||
from canvasapi.user import User
|
from canvasapi.user import User
|
||||||
|
|
@ -22,11 +22,13 @@ class PermissionEnum(Enum):
|
||||||
admin = "admin"
|
admin = "admin"
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
def list_all(method: Callable[..., Any], *args: Any, **kwargs: Any) -> List[Any]:
|
||||||
|
"""Call paginated API methods repeatedly and collect results.
|
||||||
|
|
||||||
|
The exact return element types vary depending on the API client. We use
|
||||||
def list_all(method: Callable[..., Iterable[T]], *args: Any, **kwargs: Any) -> List[T]:
|
``Any`` here to avoid over-constraining typing for the external client.
|
||||||
all_res = []
|
"""
|
||||||
|
all_res: List[Any] = []
|
||||||
page = 1
|
page = 1
|
||||||
while True:
|
while True:
|
||||||
res = method(*args, **kwargs, page=page)
|
res = method(*args, **kwargs, page=page)
|
||||||
|
|
@ -479,14 +481,21 @@ class Gitea:
|
||||||
self.org_name, repo_name, issue.number, body={"state": "closed"}
|
self.org_name, repo_name, issue.number, body={"state": "closed"}
|
||||||
)
|
)
|
||||||
|
|
||||||
def close_issues(self, repo_name: str, issue_numbers: List[int], dry_run: bool = True) -> None:
|
def close_issues(
|
||||||
|
self, repo_name: str, issue_numbers: List[int], dry_run: bool = True
|
||||||
|
) -> None:
|
||||||
if not issue_numbers:
|
if not issue_numbers:
|
||||||
logger.warning("No issue numbers provided to close")
|
logger.warning("No issue numbers provided to close")
|
||||||
return
|
return
|
||||||
if dry_run:
|
if dry_run:
|
||||||
logger.info("Dry run enabled. No changes will be made to issues.")
|
logger.info("Dry run enabled. No changes will be made to issues.")
|
||||||
try:
|
try:
|
||||||
issues = {issue.number: issue for issue in list_all(self.issue_api.issue_list_issues, self.org_name, repo_name)}
|
issues = {
|
||||||
|
issue.number: issue
|
||||||
|
for issue in list_all(
|
||||||
|
self.issue_api.issue_list_issues, self.org_name, repo_name
|
||||||
|
)
|
||||||
|
}
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
logger.error(f"Failed to list issues for {repo_name}: {e}")
|
logger.error(f"Failed to list issues for {repo_name}: {e}")
|
||||||
return
|
return
|
||||||
|
|
@ -503,7 +512,9 @@ class Gitea:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
logger.info(f"Would close issue #{num} in {repo_name} (dry run)")
|
logger.info(f"Would close issue #{num} in {repo_name} (dry run)")
|
||||||
continue
|
continue
|
||||||
self.issue_api.issue_edit_issue(self.org_name, repo_name, num, body={"state": "closed"})
|
self.issue_api.issue_edit_issue(
|
||||||
|
self.org_name, repo_name, num, body={"state": "closed"}
|
||||||
|
)
|
||||||
logger.info(f"Closed issue #{num} in {repo_name}")
|
logger.info(f"Closed issue #{num} in {repo_name}")
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
logger.error(f"Failed to close issue #{num} in {repo_name}: {e}")
|
logger.error(f"Failed to close issue #{num} in {repo_name}: {e}")
|
||||||
|
|
@ -579,17 +590,40 @@ class Gitea:
|
||||||
self.create_milestone(repo_name, milestone, description, due_date)
|
self.create_milestone(repo_name, milestone, description, due_date)
|
||||||
logger.info(f"Created milestone {milestone} in {repo_name}")
|
logger.info(f"Created milestone {milestone} in {repo_name}")
|
||||||
|
|
||||||
def label_issues(self, repo_name: str, label_name: str, issue_numbers: List[int], color: Optional[str] = None) -> None:
|
def label_issues(
|
||||||
|
self,
|
||||||
|
repo_name: str,
|
||||||
|
label_name: str,
|
||||||
|
issue_numbers: List[int],
|
||||||
|
color: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
if not issue_numbers:
|
if not issue_numbers:
|
||||||
logger.warning("No issue numbers provided to label")
|
logger.warning("No issue numbers provided to label")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
labels = self.issue_api.issue_list_labels(self.org_name, repo_name)
|
labels = list(
|
||||||
|
cast(
|
||||||
|
Iterable[Any],
|
||||||
|
self.issue_api.issue_list_labels(self.org_name, repo_name),
|
||||||
|
)
|
||||||
|
)
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
logger.error(f"Failed to list labels for {repo_name}: {e}")
|
logger.error(f"Failed to list labels for {repo_name}: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Build a lookup for repo labels by name to read metadata like 'exclusive'
|
||||||
|
repo_label_name_to_obj: Dict[str, Any] = {}
|
||||||
|
try:
|
||||||
|
for l in labels:
|
||||||
|
name = getattr(l, "name", None) or (
|
||||||
|
l.get("name") if isinstance(l, dict) else None
|
||||||
|
)
|
||||||
|
if isinstance(name, str):
|
||||||
|
repo_label_name_to_obj[name] = l
|
||||||
|
except Exception:
|
||||||
|
repo_label_name_to_obj = {}
|
||||||
|
|
||||||
label_obj = None
|
label_obj = None
|
||||||
for l in labels:
|
for l in labels:
|
||||||
if getattr(l, "name", None) == label_name:
|
if getattr(l, "name", None) == label_name:
|
||||||
|
|
@ -600,40 +634,106 @@ class Gitea:
|
||||||
chosen_color = None
|
chosen_color = None
|
||||||
if color:
|
if color:
|
||||||
c = color.strip()
|
c = color.strip()
|
||||||
if c.startswith('#'):
|
if c.startswith("#"):
|
||||||
c = c[1:]
|
c = c[1:]
|
||||||
if re.fullmatch(r"[0-9a-fA-F]{6}", c):
|
if re.fullmatch(r"[0-9a-fA-F]{6}", c):
|
||||||
chosen_color = c
|
chosen_color = c
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Provided color '{color}' is not a valid 3- or 6-digit hex; falling back to default")
|
logger.warning(
|
||||||
|
f"Provided color '{color}' is not a valid 3- or 6-digit hex; falling back to default"
|
||||||
|
)
|
||||||
if chosen_color is None:
|
if chosen_color is None:
|
||||||
chosen_color = "CCCCCC"
|
chosen_color = "CCCCCC"
|
||||||
try:
|
try:
|
||||||
|
# Create the label and mark it as exclusive so subsequent labels added by this
|
||||||
|
# command are treated as exclusive by Gitea (if supported by the server)
|
||||||
label_obj = self.issue_api.issue_create_label(
|
label_obj = self.issue_api.issue_create_label(
|
||||||
self.org_name,
|
self.org_name,
|
||||||
repo_name,
|
repo_name,
|
||||||
body={"name": label_name, "color": chosen_color, "exclusive": False},
|
body={"name": label_name, "color": chosen_color, "exclusive": True},
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Created label '{label_name}' in {self.org_name}/{repo_name}"
|
||||||
)
|
)
|
||||||
logger.info(f"Created label '{label_name}' in {self.org_name}/{repo_name}")
|
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
logger.error(f"Failed to create label {label_name} in {repo_name}: {e}")
|
logger.error(f"Failed to create label {label_name} in {repo_name}: {e}")
|
||||||
if hasattr(e, 'body'):
|
if hasattr(e, "body"):
|
||||||
logger.error(f"ApiException body: {getattr(e, 'body', None)}")
|
logger.error(f"ApiException body: {getattr(e, 'body', None)}")
|
||||||
return
|
return
|
||||||
|
|
||||||
label_id = getattr(label_obj, "id", None)
|
else:
|
||||||
if label_id is None:
|
|
||||||
try:
|
try:
|
||||||
|
existing_color = getattr(label_obj, "color", None) or (
|
||||||
|
label_obj.get("color") if isinstance(label_obj, dict) else None
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
existing_color
|
||||||
|
and isinstance(existing_color, str)
|
||||||
|
and existing_color.startswith("#")
|
||||||
|
):
|
||||||
|
existing_color = existing_color[1:]
|
||||||
|
is_exclusive = getattr(label_obj, "exclusive", None)
|
||||||
|
if not is_exclusive:
|
||||||
|
enc_name = quote(label_name, safe="")
|
||||||
|
path = f"/repos/{self.org_name}/{repo_name}/labels/{enc_name}"
|
||||||
|
url = f"{self.api_client.configuration.host}{path}"
|
||||||
|
token = (
|
||||||
|
getattr(self.api_client.configuration, "api_key", {}).get(
|
||||||
|
"access_token"
|
||||||
|
)
|
||||||
|
or settings.gitea_access_token
|
||||||
|
)
|
||||||
|
headers_local = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
payload = {"exclusive": True, "name": label_name}
|
||||||
|
if existing_color:
|
||||||
|
payload["color"] = existing_color
|
||||||
|
try:
|
||||||
|
resp = requests.patch(
|
||||||
|
url, headers=headers_local, json=payload, timeout=10
|
||||||
|
)
|
||||||
|
if resp.status_code in (200, 201):
|
||||||
|
logger.info(
|
||||||
|
f"Marked existing label '{label_name}' as exclusive in {repo_name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to mark existing label '{label_name}' as exclusive: status={resp.status_code}, body={getattr(resp, 'text', None)}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Error while trying to mark label '{label_name}' exclusive: {e}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.debug(
|
||||||
|
f"Could not ensure exclusive attribute for label '{label_name}' (continuing)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine numeric id of the label in a type-safe manner
|
||||||
|
label_id = None
|
||||||
|
try:
|
||||||
|
if isinstance(label_obj, dict):
|
||||||
label_id = label_obj.get("id")
|
label_id = label_obj.get("id")
|
||||||
|
else:
|
||||||
|
label_id = getattr(label_obj, "id", None)
|
||||||
except Exception:
|
except Exception:
|
||||||
label_id = None
|
label_id = None
|
||||||
|
|
||||||
if label_id is None:
|
if label_id is None:
|
||||||
logger.error(f"Unable to determine id of label '{label_name}' in {repo_name}")
|
logger.error(
|
||||||
|
f"Unable to determine id of label '{label_name}' in {repo_name}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
issues = {issue.number: issue for issue in list_all(self.issue_api.issue_list_issues, self.org_name, repo_name)}
|
issues = {
|
||||||
|
issue.number: issue
|
||||||
|
for issue in list_all(
|
||||||
|
self.issue_api.issue_list_issues, self.org_name, repo_name
|
||||||
|
)
|
||||||
|
}
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
logger.error(f"Failed to list issues for {repo_name}: {e}")
|
logger.error(f"Failed to list issues for {repo_name}: {e}")
|
||||||
return
|
return
|
||||||
|
|
@ -644,15 +744,20 @@ class Gitea:
|
||||||
logger.warning(f"Issue #{num} not found in {repo_name}")
|
logger.warning(f"Issue #{num} not found in {repo_name}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def _extract_label_names_from_issue(issue_obj) -> List[str]:
|
def _extract_label_names_from_issue(issue_obj: Any) -> List[str]:
|
||||||
try:
|
try:
|
||||||
return [getattr(l, "name", l) for l in getattr(issue_obj, "labels", []) or []]
|
return [
|
||||||
|
getattr(l, "name", l)
|
||||||
|
for l in getattr(issue_obj, "labels", []) or []
|
||||||
|
]
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
existing_label_names = _extract_label_names_from_issue(issue)
|
existing_label_names = _extract_label_names_from_issue(issue)
|
||||||
if label_name in existing_label_names:
|
if label_name in existing_label_names:
|
||||||
logger.info(f"Issue #{num} in {repo_name} already has label '{label_name}'")
|
logger.info(
|
||||||
|
f"Issue #{num} in {repo_name} already has label '{label_name}'"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
existing_label_ids: List[int] = []
|
existing_label_ids: List[int] = []
|
||||||
|
|
@ -670,17 +775,32 @@ class Gitea:
|
||||||
existing_label_ids = []
|
existing_label_ids = []
|
||||||
|
|
||||||
if label_id in existing_label_ids:
|
if label_id in existing_label_ids:
|
||||||
logger.info(f"Issue #{num} in {repo_name} already has label '{label_name}' (by id)")
|
logger.info(
|
||||||
|
f"Issue #{num} in {repo_name} already has label '{label_name}' (by id)"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# verification
|
# verification
|
||||||
def _fetch_issue_labels_via_api() -> List[str]:
|
def _fetch_issue_labels_via_api() -> List[str]:
|
||||||
try:
|
try:
|
||||||
single = self.issue_api.issue_get_issue(self.org_name, repo_name, num)
|
single = self.issue_api.issue_get_issue(
|
||||||
|
self.org_name, repo_name, num
|
||||||
|
)
|
||||||
return _extract_label_names_from_issue(single)
|
return _extract_label_names_from_issue(single)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
updated = next((i for i in list_all(self.issue_api.issue_list_issues, self.org_name, repo_name) if getattr(i, "number", None) == num), None)
|
updated = next(
|
||||||
|
(
|
||||||
|
i
|
||||||
|
for i in list_all(
|
||||||
|
self.issue_api.issue_list_issues,
|
||||||
|
self.org_name,
|
||||||
|
repo_name,
|
||||||
|
)
|
||||||
|
if getattr(i, "number", None) == num
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
if updated is None:
|
if updated is None:
|
||||||
return []
|
return []
|
||||||
return _extract_label_names_from_issue(updated)
|
return _extract_label_names_from_issue(updated)
|
||||||
|
|
@ -690,42 +810,183 @@ class Gitea:
|
||||||
issue_labels = _fetch_issue_labels_via_api()
|
issue_labels = _fetch_issue_labels_via_api()
|
||||||
|
|
||||||
# prepare low-level HTTP helpers for fallbacks
|
# prepare low-level HTTP helpers for fallbacks
|
||||||
def _do_post_labels(issue_num: int, payload) -> requests.Response:
|
def _do_post_labels(issue_num: int, payload: Any) -> Any:
|
||||||
path = f"/repos/{self.org_name}/{repo_name}/issues/{issue_num}/labels"
|
path = f"/repos/{self.org_name}/{repo_name}/issues/{issue_num}/labels"
|
||||||
full_url = f"{self.api_client.configuration.host}{path}"
|
full_url = f"{self.api_client.configuration.host}{path}"
|
||||||
token = getattr(self.api_client.configuration, 'api_key', {}) .get('access_token') or settings.gitea_access_token
|
token = (
|
||||||
|
getattr(self.api_client.configuration, "api_key", {}).get(
|
||||||
|
"access_token"
|
||||||
|
)
|
||||||
|
or settings.gitea_access_token
|
||||||
|
)
|
||||||
headers_local = {
|
headers_local = {
|
||||||
"Authorization": f"token {token}",
|
"Authorization": f"token {token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
return requests.post(full_url, headers=headers_local, json=payload, timeout=10)
|
return requests.post(
|
||||||
|
full_url, headers=headers_local, json=payload, timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
# fallback: if label still not present, POST JSON object {"labels":[name]} to add-labels endpoint
|
# fallback: if label still not present, POST JSON object {"labels":[name]} to add-labels endpoint
|
||||||
|
# determine if the target label is marked exclusive (force to bool)
|
||||||
|
target_is_exclusive: bool = False
|
||||||
|
try:
|
||||||
|
target_label_obj = repo_label_name_to_obj.get(label_name) or label_obj
|
||||||
|
_tmp_any: Any = getattr(target_label_obj, "exclusive", None) or (
|
||||||
|
target_label_obj.get("exclusive")
|
||||||
|
if isinstance(target_label_obj, dict)
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
target_is_exclusive = bool(_tmp_any)
|
||||||
|
except Exception:
|
||||||
|
target_is_exclusive = False
|
||||||
|
|
||||||
|
if target_is_exclusive:
|
||||||
|
# find other exclusive labels present on this issue and remove them
|
||||||
|
other_exclusive_label_ids: List[int] = []
|
||||||
|
try:
|
||||||
|
for lname in issue_labels or []:
|
||||||
|
if lname == label_name:
|
||||||
|
continue
|
||||||
|
lobj = repo_label_name_to_obj.get(lname)
|
||||||
|
is_ex = False
|
||||||
|
if lobj is not None:
|
||||||
|
is_ex = bool(
|
||||||
|
getattr(lobj, "exclusive", None)
|
||||||
|
or (
|
||||||
|
lobj.get("exclusive")
|
||||||
|
if isinstance(lobj, dict)
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# try to find by name in labels list
|
||||||
|
for ll in labels:
|
||||||
|
n = getattr(ll, "name", None) or (
|
||||||
|
ll.get("name") if isinstance(ll, dict) else None
|
||||||
|
)
|
||||||
|
if n == lname:
|
||||||
|
_tmp_any_ex: Any = getattr(
|
||||||
|
ll, "exclusive", None
|
||||||
|
) or (
|
||||||
|
ll.get("exclusive")
|
||||||
|
if isinstance(ll, dict)
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
is_ex = bool(_tmp_any_ex)
|
||||||
|
break
|
||||||
|
if is_ex:
|
||||||
|
# determine id
|
||||||
|
lid = None
|
||||||
|
try:
|
||||||
|
candidate = next(
|
||||||
|
(
|
||||||
|
ll
|
||||||
|
for ll in labels
|
||||||
|
if (
|
||||||
|
getattr(ll, "name", None)
|
||||||
|
or (
|
||||||
|
ll.get("name")
|
||||||
|
if isinstance(ll, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
== lname
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if candidate is not None:
|
||||||
|
lid = getattr(candidate, "id", None) or (
|
||||||
|
candidate.get("id")
|
||||||
|
if isinstance(candidate, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
lid = None
|
||||||
|
if lid is not None:
|
||||||
|
other_exclusive_label_ids.append(lid)
|
||||||
|
except Exception:
|
||||||
|
other_exclusive_label_ids = []
|
||||||
|
|
||||||
|
# delete other exclusive labels by numeric id
|
||||||
|
token = (
|
||||||
|
getattr(self.api_client.configuration, "api_key", {}).get(
|
||||||
|
"access_token"
|
||||||
|
)
|
||||||
|
or settings.gitea_access_token
|
||||||
|
)
|
||||||
|
headers_local = {"Authorization": f"token {token}"}
|
||||||
|
for other_lid in other_exclusive_label_ids:
|
||||||
|
try:
|
||||||
|
path_id = f"/repos/{self.org_name}/{repo_name}/issues/{num}/labels/{other_lid}"
|
||||||
|
url_id = f"{self.api_client.configuration.host}{path_id}"
|
||||||
|
resp = requests.delete(
|
||||||
|
url_id, headers=headers_local, timeout=10
|
||||||
|
)
|
||||||
|
if resp.status_code in (200, 204):
|
||||||
|
logger.info(
|
||||||
|
f"Removed exclusive label id {other_lid} from {repo_name}#{num}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to remove exclusive label id {other_lid} from {repo_name}#{num}: status={resp.status_code}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Error removing exclusive label id {other_lid} from {repo_name}#{num}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
if label_name not in (issue_labels or []):
|
if label_name not in (issue_labels or []):
|
||||||
try:
|
try:
|
||||||
resp = _do_post_labels(num, {"labels": [label_name]})
|
resp = _do_post_labels(num, {"labels": [label_name]})
|
||||||
if resp.status_code not in (200, 201):
|
if resp.status_code not in (200, 201):
|
||||||
logger.error(f"Failed to add label via add-labels endpoint for issue #{num}: status={resp.status_code}")
|
logger.error(
|
||||||
|
f"Failed to add label via add-labels endpoint for issue #{num}: status={resp.status_code}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to POST object payload to issue #{num}: {e}")
|
logger.error(f"Failed to POST object payload to issue #{num}: {e}")
|
||||||
|
|
||||||
# final verification
|
# final verification
|
||||||
issue_labels = _fetch_issue_labels_via_api()
|
issue_labels = _fetch_issue_labels_via_api()
|
||||||
if label_name in (issue_labels or []):
|
if label_name in (issue_labels or []):
|
||||||
logger.info(f"Label '{label_name}' attached to issue #{num} in {repo_name}")
|
logger.info(
|
||||||
|
f"Label '{label_name}' attached to issue #{num} in {repo_name}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Label '{label_name}' not attached to issue #{num} in {repo_name} after attempts")
|
logger.warning(
|
||||||
|
f"Label '{label_name}' not attached to issue #{num} in {repo_name} after attempts"
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_label(
|
||||||
def delete_label(self, repo_name: str, label_name: str, issue_numbers: Optional[List[int]] = None, delete_repo_label: bool = False) -> None:
|
self,
|
||||||
token = getattr(self.api_client.configuration, 'api_key', {}) .get('access_token') or settings.gitea_access_token
|
repo_name: str,
|
||||||
|
label_name: str,
|
||||||
|
issue_numbers: Optional[List[int]] = None,
|
||||||
|
delete_repo_label: bool = False,
|
||||||
|
) -> None:
|
||||||
|
token = (
|
||||||
|
getattr(self.api_client.configuration, "api_key", {}).get("access_token")
|
||||||
|
or settings.gitea_access_token
|
||||||
|
)
|
||||||
headers_local = {"Authorization": f"token {token}"}
|
headers_local = {"Authorization": f"token {token}"}
|
||||||
|
repo_labels: List[Any] = []
|
||||||
try:
|
try:
|
||||||
repo_labels = self.issue_api.issue_list_labels(self.org_name, repo_name)
|
repo_labels = list(
|
||||||
label_name_to_id: Dict[str, int] = {
|
cast(
|
||||||
(getattr(l, 'name', None) or (l.get('name') if isinstance(l, dict) else None)): (getattr(l, 'id', None) or (l.get('id') if isinstance(l, dict) else None))
|
Iterable[Any],
|
||||||
for l in repo_labels
|
self.issue_api.issue_list_labels(self.org_name, repo_name),
|
||||||
}
|
)
|
||||||
|
)
|
||||||
|
label_name_to_id: Dict[str, int] = {}
|
||||||
|
for l in repo_labels:
|
||||||
|
name = getattr(l, "name", None) or (
|
||||||
|
l.get("name") if isinstance(l, dict) else None
|
||||||
|
)
|
||||||
|
lid = getattr(l, "id", None) or (
|
||||||
|
l.get("id") if isinstance(l, dict) else None
|
||||||
|
)
|
||||||
|
if isinstance(name, str) and isinstance(lid, int):
|
||||||
|
label_name_to_id[name] = lid
|
||||||
except Exception:
|
except Exception:
|
||||||
label_name_to_id = {}
|
label_name_to_id = {}
|
||||||
|
|
||||||
|
|
@ -733,45 +994,70 @@ class Gitea:
|
||||||
lid = label_name_to_id.get(label_name)
|
lid = label_name_to_id.get(label_name)
|
||||||
if lid is None:
|
if lid is None:
|
||||||
try:
|
try:
|
||||||
for l in self.issue_api.issue_list_labels(self.org_name, repo_name):
|
for l in repo_labels:
|
||||||
name = getattr(l, 'name', None) or (l.get('name') if isinstance(l, dict) else None)
|
name = getattr(l, "name", None) or (
|
||||||
|
l.get("name") if isinstance(l, dict) else None
|
||||||
|
)
|
||||||
if name == label_name:
|
if name == label_name:
|
||||||
lid = getattr(l, 'id', None) or (l.get('id') if isinstance(l, dict) else None)
|
lid = getattr(l, "id", None) or (
|
||||||
|
l.get("id") if isinstance(l, dict) else None
|
||||||
|
)
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if lid is None:
|
if lid is None:
|
||||||
logger.warning(f"No numeric id found for label '{label_name}' in {repo_name}; skipping issue-level delete for issue #{issue_num}")
|
logger.warning(
|
||||||
|
f"No numeric id found for label '{label_name}' in {repo_name}; skipping issue-level delete for issue #{issue_num}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
path_id = f"/repos/{self.org_name}/{repo_name}/issues/{issue_num}/labels/{lid}"
|
path_id = (
|
||||||
|
f"/repos/{self.org_name}/{repo_name}/issues/{issue_num}/labels/{lid}"
|
||||||
|
)
|
||||||
url_id = f"{self.api_client.configuration.host}{path_id}"
|
url_id = f"{self.api_client.configuration.host}{path_id}"
|
||||||
try:
|
try:
|
||||||
resp = requests.delete(url_id, headers=headers_local, timeout=10)
|
resp = requests.delete(url_id, headers=headers_local, timeout=10)
|
||||||
if resp.status_code in (200, 204):
|
if resp.status_code in (200, 204):
|
||||||
logger.info(f"Removed label '{label_name}' from {repo_name}#{issue_num}")
|
logger.info(
|
||||||
|
f"Removed label '{label_name}' from {repo_name}#{issue_num}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
logger.warning(f"Numeric-id DELETE returned status {resp.status_code} for {repo_name}#{issue_num}: body={getattr(resp, 'text', None)}")
|
logger.warning(
|
||||||
|
f"Numeric-id DELETE returned status {resp.status_code} for {repo_name}#{issue_num}: body={getattr(resp, 'text', None)}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Numeric-id DELETE error for {repo_name}#{issue_num}: {e}")
|
logger.error(
|
||||||
|
f"Numeric-id DELETE error for {repo_name}#{issue_num}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
def _delete_repo_label() -> None:
|
def _delete_repo_label() -> None:
|
||||||
enc_name = quote(label_name, safe='')
|
enc_name = quote(label_name, safe="")
|
||||||
path = f"/repos/{self.org_name}/{repo_name}/labels/{enc_name}"
|
path = f"/repos/{self.org_name}/{repo_name}/labels/{enc_name}"
|
||||||
url = f"{self.api_client.configuration.host}{path}"
|
url = f"{self.api_client.configuration.host}{path}"
|
||||||
try:
|
try:
|
||||||
resp = requests.delete(url, headers=headers_local, timeout=10)
|
resp = requests.delete(url, headers=headers_local, timeout=10)
|
||||||
if resp.status_code in (200, 204):
|
if resp.status_code in (200, 204):
|
||||||
logger.info(f"Removed repo-level label '{label_name}' from {repo_name}")
|
logger.info(
|
||||||
|
f"Removed repo-level label '{label_name}' from {repo_name}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
logger.error(f"Failed to delete repo label '{label_name}' from {repo_name}: status={resp.status_code}, body={getattr(resp, 'text', None)}")
|
logger.error(
|
||||||
|
f"Failed to delete repo label '{label_name}' from {repo_name}: status={resp.status_code}, body={getattr(resp, 'text', None)}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting repo label '{label_name}' from {repo_name}: {e}")
|
logger.error(
|
||||||
|
f"Error deleting repo label '{label_name}' from {repo_name}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
if issue_numbers:
|
if issue_numbers:
|
||||||
try:
|
try:
|
||||||
issues = {issue.number: issue for issue in list_all(self.issue_api.issue_list_issues, self.org_name, repo_name)}
|
issues = {
|
||||||
|
issue.number: issue
|
||||||
|
for issue in list_all(
|
||||||
|
self.issue_api.issue_list_issues, self.org_name, repo_name
|
||||||
|
)
|
||||||
|
}
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
logger.error(f"Failed to list issues for {repo_name}: {e}")
|
logger.error(f"Failed to list issues for {repo_name}: {e}")
|
||||||
return
|
return
|
||||||
|
|
@ -784,7 +1070,10 @@ class Gitea:
|
||||||
if delete_repo_label:
|
if delete_repo_label:
|
||||||
_delete_repo_label()
|
_delete_repo_label()
|
||||||
else:
|
else:
|
||||||
logger.warning("No issue numbers provided and --repo not set; nothing to do for delete_label")
|
logger.warning(
|
||||||
|
"No issue numbers provided and --repo not set; nothing to do for delete_label"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
gitea = Gitea()
|
gitea = Gitea()
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,9 @@ class Mattermost:
|
||||||
for group_name, members in groups.items():
|
for group_name, members in groups.items():
|
||||||
channel_name = group_name + suffix
|
channel_name = group_name + suffix
|
||||||
try:
|
try:
|
||||||
channel = self.endpoint.channels.get_channel_by_name(self.team["id"], channel_name)
|
channel = self.endpoint.channels.get_channel_by_name(
|
||||||
|
self.team["id"], channel_name
|
||||||
|
)
|
||||||
logger.info(f"Channel {channel_name} exists, updating members")
|
logger.info(f"Channel {channel_name} exists, updating members")
|
||||||
except Exception:
|
except Exception:
|
||||||
# channel does not exist
|
# channel does not exist
|
||||||
|
|
@ -121,7 +123,9 @@ class Mattermost:
|
||||||
info_members = list(members)
|
info_members = list(members)
|
||||||
if update_teaching_team:
|
if update_teaching_team:
|
||||||
info_members = info_members + settings.mattermost_teaching_team
|
info_members = info_members + settings.mattermost_teaching_team
|
||||||
logger.info(f"Dry run: would create channel {channel_name} and add members: {info_members}")
|
logger.info(
|
||||||
|
f"Dry run: would create channel {channel_name} and add members: {info_members}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
channel = self.endpoint.channels.create_channel(
|
channel = self.endpoint.channels.create_channel(
|
||||||
|
|
@ -141,7 +145,9 @@ class Mattermost:
|
||||||
|
|
||||||
current_members = set()
|
current_members = set()
|
||||||
try:
|
try:
|
||||||
mm_members = self.endpoint.channels.get_channel_members(channel["id"]) or []
|
mm_members = (
|
||||||
|
self.endpoint.channels.get_channel_members(channel["id"]) or []
|
||||||
|
)
|
||||||
for m in mm_members:
|
for m in mm_members:
|
||||||
uname = None
|
uname = None
|
||||||
if isinstance(m, dict):
|
if isinstance(m, dict):
|
||||||
|
|
@ -169,22 +175,34 @@ class Mattermost:
|
||||||
logger.debug(f"Member {member} already in channel {channel_name}")
|
logger.debug(f"Member {member} already in channel {channel_name}")
|
||||||
continue
|
continue
|
||||||
if dry_run:
|
if dry_run:
|
||||||
logger.info(f"Dry run: would add member {member} to channel {channel_name}")
|
logger.info(
|
||||||
|
f"Dry run: would add member {member} to channel {channel_name}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
mmuser = self.endpoint.users.get_user_by_username(member)
|
mmuser = self.endpoint.users.get_user_by_username(member)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"User {member} is not found on the Mattermost server")
|
logger.warning(
|
||||||
|
f"User {member} is not found on the Mattermost server"
|
||||||
|
)
|
||||||
self.endpoint.posts.create_post(
|
self.endpoint.posts.create_post(
|
||||||
{"channel_id": channel["id"], "message": f"User {member} is not found on the Mattermost server"}
|
{
|
||||||
|
"channel_id": channel["id"],
|
||||||
|
"message": f"User {member} is not found on the Mattermost server",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
self.endpoint.channels.add_user(channel["id"], {"user_id": mmuser["id"]})
|
self.endpoint.channels.add_user(
|
||||||
|
channel["id"], {"user_id": mmuser["id"]}
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"User {member} is not in the team")
|
logger.warning(f"User {member} is not in the team")
|
||||||
self.endpoint.posts.create_post(
|
self.endpoint.posts.create_post(
|
||||||
{"channel_id": channel["id"], "message": f"User {member} is not in the team"}
|
{
|
||||||
|
"channel_id": channel["id"],
|
||||||
|
"message": f"User {member} is not in the team",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
logger.info(f"Added member {member} to channel {channel_name}")
|
logger.info(f"Added member {member} to channel {channel_name}")
|
||||||
|
|
||||||
|
|
@ -261,14 +279,18 @@ class Mattermost:
|
||||||
display_name = student.name
|
display_name = student.name
|
||||||
channel_name = student.sis_id
|
channel_name = student.sis_id
|
||||||
try:
|
try:
|
||||||
channel = self.endpoint.channels.get_channel_by_name(self.team["id"], channel_name)
|
channel = self.endpoint.channels.get_channel_by_name(
|
||||||
|
self.team["id"], channel_name
|
||||||
|
)
|
||||||
logger.info(f"Channel {channel_name} exists, updating members")
|
logger.info(f"Channel {channel_name} exists, updating members")
|
||||||
except Exception:
|
except Exception:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
members_info = [student.login_id]
|
members_info = [student.login_id]
|
||||||
if update_teaching_team:
|
if update_teaching_team:
|
||||||
members_info = members_info + settings.mattermost_teaching_team
|
members_info = members_info + settings.mattermost_teaching_team
|
||||||
logger.info(f"Dry run: would create channel {display_name} ({channel_name}) and add members: {members_info}")
|
logger.info(
|
||||||
|
f"Dry run: would create channel {display_name} ({channel_name}) and add members: {members_info}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
channel = self.endpoint.channels.create_channel(
|
channel = self.endpoint.channels.create_channel(
|
||||||
|
|
@ -279,7 +301,9 @@ class Mattermost:
|
||||||
"type": "P",
|
"type": "P",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
logger.info(f"Created channel {display_name} ({channel_name}) on Mattermost")
|
logger.info(
|
||||||
|
f"Created channel {display_name} ({channel_name}) on Mattermost"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Error when creating channel {channel_name}: {e} Perhaps channel already exists?"
|
f"Error when creating channel {channel_name}: {e} Perhaps channel already exists?"
|
||||||
|
|
@ -288,7 +312,9 @@ class Mattermost:
|
||||||
|
|
||||||
current_members = set()
|
current_members = set()
|
||||||
try:
|
try:
|
||||||
mm_members = self.endpoint.channels.get_channel_members(channel["id"]) or []
|
mm_members = (
|
||||||
|
self.endpoint.channels.get_channel_members(channel["id"]) or []
|
||||||
|
)
|
||||||
for m in mm_members:
|
for m in mm_members:
|
||||||
uname = None
|
uname = None
|
||||||
if isinstance(m, dict):
|
if isinstance(m, dict):
|
||||||
|
|
@ -316,22 +342,34 @@ class Mattermost:
|
||||||
logger.debug(f"Member {member} already in channel {channel_name}")
|
logger.debug(f"Member {member} already in channel {channel_name}")
|
||||||
continue
|
continue
|
||||||
if dry_run:
|
if dry_run:
|
||||||
logger.info(f"Dry run: would add member {member} to channel {channel_name}")
|
logger.info(
|
||||||
|
f"Dry run: would add member {member} to channel {channel_name}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
mmuser = self.endpoint.users.get_user_by_username(member)
|
mmuser = self.endpoint.users.get_user_by_username(member)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"User {member} is not found on the Mattermost server")
|
logger.warning(
|
||||||
|
f"User {member} is not found on the Mattermost server"
|
||||||
|
)
|
||||||
self.endpoint.posts.create_post(
|
self.endpoint.posts.create_post(
|
||||||
{"channel_id": channel["id"], "message": f"User {member} is not found on the Mattermost server"}
|
{
|
||||||
|
"channel_id": channel["id"],
|
||||||
|
"message": f"User {member} is not found on the Mattermost server",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
self.endpoint.channels.add_user(channel["id"], {"user_id": mmuser["id"]})
|
self.endpoint.channels.add_user(
|
||||||
|
channel["id"], {"user_id": mmuser["id"]}
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(f"User {member} is not in the team")
|
logger.warning(f"User {member} is not in the team")
|
||||||
self.endpoint.posts.create_post(
|
self.endpoint.posts.create_post(
|
||||||
{"channel_id": channel["id"], "message": f"User {member} is not in the team"}
|
{
|
||||||
|
"channel_id": channel["id"],
|
||||||
|
"message": f"User {member} is not in the team",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
logger.info(f"Added member {member} to channel {channel_name}")
|
logger.info(f"Added member {member} to channel {channel_name}")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user