Skip to content
Commits on Source (3)
......@@ -16,6 +16,7 @@ import click
from datetime import datetime
import pytz
from teach_utils.common_requests import parse_repo_url, get_project_id
from teach_utils.utils import ingest_token
def validate_iso_date(date: str) -> str:
......@@ -134,17 +135,12 @@ def format_fork_metadata(
)
@click.argument("repo_url", type=str)
@click.option(
"--token",
"--token-path",
type=click.Path(exists=True),
help="Path to a file containing the Gitlab API token. If not provided, you will be prompted for the token.",
)
def main(repo_url, token, deadline=None):
if token is None:
token = click.prompt(
"Please enter your Gitlab API token", hide_input=True, err=True
)
else:
token = open(token).read().strip()
def main(repo_url, token_path, deadline=None):
token = ingest_token(token_path)
# Check for valid deadline format
if deadline is not None:
validate_iso_date(deadline)
......
......@@ -6,6 +6,7 @@ from typing import Dict
import requests
import click
from teach_utils.common_requests import parse_repo_url, get_group_id
from teach_utils.utils import ingest_token
def invite_student_email(
......@@ -24,10 +25,10 @@ def find_email(line: str) -> str:
"""Find and return email address in input string.
Raises an error if none found and only return the first
email if there are more than one.
Examples
--------
>>> find_email('robert,smith,robert.smith@no.where,123')
'robert.smith@no.where'
>>> find_email('mike.rock@yes.no,mrock@yes.no')
......@@ -47,20 +48,15 @@ def find_email(line: str) -> str:
)
@click.argument("group-url")
@click.option(
"--token",
"--token-path",
type=click.Path(exists=True),
help="Path to a file containing the Gitlab API token. If not provided, you will be prompted for the token.",
)
def main(emails_file, group_url, token):
def main(emails_file, group_url, token_path):
"""
Send invitations to join input gitlab group to all emails in the emails_file.
The file should have one email per line."""
if token is None:
token = click.prompt(
"Please enter your Gitlab API token", hide_input=True, err=True
)
else:
token = open(token).read().strip()
token = ingest_token(token_path)
header = {"PRIVATE-TOKEN": token}
# Recover the email on each line, in case there are multiple columns
......
......@@ -8,6 +8,7 @@ import requests
import pandas as pd
import click
from teach_utils.common_requests import get_group_id, parse_group_url
from teach_utils.utils import ingest_token
def create_student_group(
......@@ -52,23 +53,17 @@ def generate_student_code(first_name: str, last_name: str) -> str:
@click.argument("student_table")
@click.argument("parent_url")
@click.option(
"--token",
"--token-path",
type=click.Path(exists=True),
help="Path to a file containing the Gitlab API token. If not provided, you will be prompted for the token.",
)
def main(student_table, parent_url, token):
def main(student_table, parent_url, token_path):
"""Given a parent-group URL and a CSV file of students exported from Moodle,
create one private subgroup for each student in the parent group and invite students
in their personal group. The student table must contain columns: "Email address", "First name"
and "Surname"
"""
if token is None:
token = click.prompt(
"Please enter your Gitlab API token", hide_input=True, err=True
)
else:
token = open(token).read().strip()
token = ingest_token(token_path)
header = {"PRIVATE-TOKEN": token}
students = pd.read_csv(student_table)
......
......@@ -10,11 +10,13 @@
import sys
import json
from string import Formatter
from typing import Dict
from typing import Dict, Optional
import requests
from pathlib import Path
import click
import pandas as pd
from teach_utils.common_requests import parse_repo_url
from teach_utils.utils import ingest_token
FEEDBACK_TEMPLATE = """
......@@ -58,10 +60,39 @@ def fill_template(content: Dict[str, str], template: str) -> str:
raise ValueError(f"Fields missing from content: {missing_flds}") from None
def gather_fill_data(fork: Dict, feedback_csv: Optional[Path] = None) -> Dict:
"""Gather metadata and feedback for a single fork into a dictionary
to use for filling the template. The resulting dictionary contains
fields: project, group, members, url, commit and visibility from the fork
metadata, as well as all columns present in the feedback csv file.
"""
# Get basic fields in fork metadata
_, group, repo = parse_repo_url(fork["url"])
members = "\n".join([f"* {member['name']}" for member in fork["members"]])
fill_data = {
"project": repo.removesuffix(".git"),
"group": group,
"members": members,
}
# add commit, url and visibility fields from fork data
fill_data |= {key: fork.get(key) for key in ["url", "commit", "visibility"]}
# If a CSV file with feedback is provided, read the fields
# of the current fork (row) to the fill data.
if feedback_csv:
fill_data |= (
pd.read_csv(feedback_csv)
.query(f'id == {fork["id"]}')
.iloc[0]
.drop("id")
.to_dict()
)
return fill_data
@click.command()
@click.argument("forks", type=click.File(mode="r"), default=sys.stdin)
@click.option(
"--token",
"--token-path",
type=click.Path(exists=True),
help="Path to a file containing the Gitlab API token. If not provided, you will be prompted for the token.",
)
......@@ -74,29 +105,22 @@ def fill_template(content: Dict[str, str], template: str) -> str:
default=None,
)
@click.option(
"--feedback",
"--feedback-csv",
type=click.Path(exists=True),
help="CSV file with column 'id', and additional columns matching fields in the template"
"The id column should match a gitlab project ID from the JSON input.",
default=None,
)
@click.option(
"--dry-run",
is_flag=True,
help="Print feedback to stdout instead of opening issues"
"--dry-run", is_flag=True, help="Print feedback to stdout instead of opening issues"
)
def main(forks, feedback, token, template, dry_run):
def main(forks, feedback_csv, token_path, template, dry_run):
"""
Send feedback to repositories in input JSON by opening issues.
"""
# Build header with authentication token
if token is None:
token = click.prompt(
"Please enter your Gitlab API token", hide_input=True, err=True
)
else:
token = open(token).read().strip()
token = ingest_token(token_path)
header = {"PRIVATE-TOKEN": token}
forks = json.load(forks)
......@@ -108,30 +132,11 @@ def main(forks, feedback, token, template, dry_run):
feedback_template = FEEDBACK_TEMPLATE
for fork in forks:
# Get basic fields from repo metadata in json
base, group, repo = parse_repo_url(fork["url"])
members = "\n".join([f"* {member['name']}" for member in fork["members"]])
fill_data = {
"project": repo.removesuffix(".git"),
"group": group,
"members": members,
}
# add commit, url and visibility fields from fork data
fill_data |= {key: fork.get(key) for key in ["url", "commit", "visibility"]}
# If provided, get additional fields from feedback table
if feedback:
feedback_fields = (
pd.read_csv(feedback)
.query(f'id == {fork["id"]}')
.iloc[0]
.drop("id")
.to_dict()
)
fill_data |= feedback_fields
base = parse_repo_url(fork["url"])[0]
fill_data = gather_fill_data(fork, feedback_csv)
# Replace fields in the template by their value for the current fork
description = fill_template(fill_data, feedback_template)
# Create issue
# Create issue, or print to stdout if dry run
if dry_run:
print(fork)
print(description)
......
from typing import Optional
from pathlib import Path
import click
def ingest_token(token_path: Optional[Path]=None) -> str:
"""Read token from target file if provided.
If input is None, prompt for token."""
if token_path is None:
token = click.prompt(
"Please enter your Gitlab API token", hide_input=True, err=True
)
else:
token = open(token_path).read().strip()
return token