diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2e2ebab..9f53e15 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,7 +8,7 @@ default_install_hook_types:
repos:
- repo: https://github.com/compilerla/conventional-pre-commit
- rev: v3.6.0
+ rev: v4.0.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
diff --git a/compiler_admin/commands/info.py b/compiler_admin/commands/info.py
index 9a75e3e..fb6d543 100644
--- a/compiler_admin/commands/info.py
+++ b/compiler_admin/commands/info.py
@@ -1,17 +1,16 @@
-from compiler_admin import __version__ as version, RESULT_SUCCESS, RESULT_FAILURE
-from compiler_admin.services.google import CallGAMCommand, CallGYBCommand
+import click
+from compiler_admin import __version__ as version
+from compiler_admin.services.google import CallGAMCommand, CallGYBCommand
-def info(*args, **kwargs) -> int:
- """Print information about this package and the GAM environment.
- Returns:
- A value indicating if the operation succeeded or failed.
+@click.command()
+def info():
"""
- print(f"compiler-admin: {version}")
-
- res = CallGAMCommand(("version",))
- res += CallGAMCommand(("info", "domain"))
- res += CallGYBCommand(("--version",))
+ Print information about the configured environment.
+ """
+ click.echo(f"compiler-admin, version {version}")
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ CallGAMCommand(("version",))
+ CallGAMCommand(("info", "domain"))
+ CallGYBCommand(("--version",))
diff --git a/compiler_admin/commands/init.py b/compiler_admin/commands/init.py
index e628938..5266d42 100644
--- a/compiler_admin/commands/init.py
+++ b/compiler_admin/commands/init.py
@@ -1,10 +1,10 @@
-from argparse import Namespace
import os
from pathlib import Path
from shutil import rmtree
import subprocess
-from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
+import click
+
from compiler_admin.services.google import USER_ARCHIVE, CallGAMCommand
@@ -22,48 +22,37 @@ def _clean_config_dir(config_dir: Path) -> None:
rmtree(path)
-def init(args: Namespace, *extras) -> int:
- """Initialize a new GAM project.
-
- See https://github.com/taers232c/GAMADV-XTD3/wiki/How-to-Install-Advanced-GAM
-
- Args:
- username (str): The Compiler admin with which to initialize a new project.
-
- gam (bool): If True, initialize a new GAM project.
+@click.command()
+@click.option("--gam", "init_gam", is_flag=True)
+@click.option("--gyb", "init_gyb", is_flag=True)
+@click.argument("username")
+def init(username: str, init_gam: bool = False, init_gyb: bool = False):
+ """Initialize a new GAM and/or GYB project.
- gyb (bool): If True, initialize a new GYB project.
+ See:
- Returns:
- A value indicating if the operation succeeded or failed.
+ - https://github.com/taers232c/GAMADV-XTD3/wiki/How-to-Install-Advanced-GAM
+ - https://github.com/GAM-team/got-your-back/wiki
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
-
- admin_user = args.username
- res = RESULT_SUCCESS
-
- if getattr(args, "gam", False):
+ if init_gam:
_clean_config_dir(GAM_CONFIG_PATH)
# GAM is already installed via pyproject.toml
- res += CallGAMCommand(("config", "drive_dir", str(GAM_CONFIG_PATH), "verify"))
- res += CallGAMCommand(("create", "project"))
- res += CallGAMCommand(("oauth", "create"))
- res += CallGAMCommand(("user", admin_user, "check", "serviceaccount"))
+ CallGAMCommand(("config", "drive_dir", str(GAM_CONFIG_PATH), "verify"))
+ CallGAMCommand(("create", "project"))
+ CallGAMCommand(("oauth", "create"))
+ CallGAMCommand(("user", username, "check", "serviceaccount"))
- if getattr(args, "gyb", False):
+ if init_gyb:
_clean_config_dir(GYB_CONFIG_PATH)
# download GYB installer to config directory
gyb = GYB_CONFIG_PATH / "gyb-install.sh"
with gyb.open("w+") as dest:
- res += subprocess.call(("curl", "-s", "-S", "-L", "https://gyb-shortn.jaylee.us/gyb-install"), stdout=dest)
+ subprocess.call(("curl", "-s", "-S", "-L", "https://gyb-shortn.jaylee.us/gyb-install"), stdout=dest)
- res += subprocess.call(("chmod", "+x", str(gyb.absolute())))
+ subprocess.call(("chmod", "+x", str(gyb.absolute())))
# install, giving values to some options
# https://github.com/GAM-team/got-your-back/blob/main/install-gyb.sh
#
# use GYB_CONFIG_PATH.parent for the install directory option, otherwise we get a .config/gyb/gyb directory structure
- res += subprocess.call((gyb, "-u", admin_user, "-r", USER_ARCHIVE, "-d", str(GYB_CONFIG_PATH.parent)))
-
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ subprocess.call((gyb, "-u", username, "-r", USER_ARCHIVE, "-d", str(GYB_CONFIG_PATH.parent)))
diff --git a/compiler_admin/commands/time/__init__.py b/compiler_admin/commands/time/__init__.py
index a56c97d..ca8bc21 100644
--- a/compiler_admin/commands/time/__init__.py
+++ b/compiler_admin/commands/time/__init__.py
@@ -1,16 +1,16 @@
-from argparse import Namespace
+import click
-from compiler_admin.commands.time.convert import convert # noqa: F401
-from compiler_admin.commands.time.download import download # noqa: F401
+from compiler_admin.commands.time.convert import convert
+from compiler_admin.commands.time.download import download
-def time(args: Namespace, *extra):
- # try to call the subcommand function directly from global (module) symbols
- # if the subcommand function was imported above, it should exist in globals()
- global_env = globals()
+@click.group
+def time():
+ """
+ Work with Compiler time entries.
+ """
+ pass
- if args.subcommand in global_env:
- cmd_func = global_env[args.subcommand]
- cmd_func(args, *extra)
- else:
- raise NotImplementedError(f"Unknown time subcommand: {args.subcommand}")
+
+time.add_command(convert)
+time.add_command(download)
diff --git a/compiler_admin/commands/time/convert.py b/compiler_admin/commands/time/convert.py
index 955b1ef..52e24b3 100644
--- a/compiler_admin/commands/time/convert.py
+++ b/compiler_admin/commands/time/convert.py
@@ -1,6 +1,9 @@
-from argparse import Namespace
+import os
+import sys
+from typing import TextIO
+
+import click
-from compiler_admin import RESULT_SUCCESS
from compiler_admin.services.harvest import CONVERTERS as HARVEST_CONVERTERS
from compiler_admin.services.toggl import CONVERTERS as TOGGL_CONVERTERS
@@ -21,9 +24,46 @@ def _get_source_converter(from_fmt: str, to_fmt: str):
)
-def convert(args: Namespace, *extras):
- converter = _get_source_converter(args.from_fmt, args.to_fmt)
+@click.command()
+@click.option(
+ "--input",
+ default=os.environ.get("TOGGL_DATA", sys.stdin),
+ help="The path to the source data for conversion. Defaults to $TOGGL_DATA or stdin.",
+)
+@click.option(
+ "--output",
+ default=os.environ.get("HARVEST_DATA", sys.stdout),
+ help="The path to the file where converted data should be written. Defaults to $HARVEST_DATA or stdout.",
+)
+@click.option(
+ "--from",
+ "from_fmt",
+ default="toggl",
+ help="The format of the source data.",
+ show_default=True,
+ type=click.Choice(sorted(CONVERTERS.keys()), case_sensitive=False),
+)
+@click.option(
+ "--to",
+ "to_fmt",
+ default="harvest",
+ help="The format of the converted data.",
+ show_default=True,
+ type=click.Choice(sorted([to_fmt for sub in CONVERTERS.values() for to_fmt in sub.keys()]), case_sensitive=False),
+)
+@click.option("--client", help="The name of the client to use in converted data.")
+def convert(
+ input: str | TextIO = os.environ.get("TOGGL_DATA", sys.stdin),
+ output: str | TextIO = os.environ.get("HARVEST_DATA", sys.stdout),
+ from_fmt="toggl",
+ to_fmt="harvest",
+ client="",
+):
+ """
+ Convert a time report from one format into another.
+ """
+ converter = _get_source_converter(from_fmt, to_fmt)
- converter(source_path=args.input, output_path=args.output, client_name=args.client)
+ click.echo(f"Converting data from format: {from_fmt} to format: {to_fmt}")
- return RESULT_SUCCESS
+ converter(source_path=input, output_path=output, client_name=client)
diff --git a/compiler_admin/commands/time/download.py b/compiler_admin/commands/time/download.py
index 4cb3e3b..d8a6cbc 100644
--- a/compiler_admin/commands/time/download.py
+++ b/compiler_admin/commands/time/download.py
@@ -1,23 +1,125 @@
-from argparse import Namespace
+from datetime import datetime, timedelta
+import os
+from typing import List
+
+import click
+from pytz import timezone
-from compiler_admin import RESULT_SUCCESS
from compiler_admin.services.toggl import TOGGL_COLUMNS, download_time_entries
-def download(args: Namespace, *extras):
- params = dict(
- start_date=args.start, end_date=args.end, output_path=args.output, output_cols=TOGGL_COLUMNS, billable=args.billable
- )
+TZINFO = timezone(os.environ.get("TZ_NAME", "America/Los_Angeles"))
- if args.client_ids:
- params.update(dict(client_ids=args.client_ids))
- if args.project_ids:
- params.update(dict(project_ids=args.project_ids))
- if args.task_ids:
- params.update(dict(task_ids=args.task_ids))
- if args.user_ids:
- params.update(dict(user_ids=args.user_ids))
- download_time_entries(**params)
+def local_now():
+ return datetime.now(tz=TZINFO)
+
+
+def prior_month_end():
+ now = local_now()
+ first = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
+ return first - timedelta(days=1)
+
+
+def prior_month_start():
+ end = prior_month_end()
+ return end.replace(day=1)
- return RESULT_SUCCESS
+
+@click.command()
+@click.option(
+ "--start",
+ metavar="YYYY-MM-DD",
+ default=prior_month_start(),
+ callback=lambda ctx, param, val: datetime.strptime(val, "%Y-%m-%d %H:%M:%S%z"),
+ help="The start date of the reporting period. Defaults to the beginning of the prior month.",
+)
+@click.option(
+ "--end",
+ metavar="YYYY-MM-DD",
+ default=prior_month_end(),
+ callback=lambda ctx, param, val: datetime.strptime(val, "%Y-%m-%d %H:%M:%S%z"),
+ help="The end date of the reporting period. Defaults to the end of the prior month.",
+)
+@click.option(
+ "--output",
+ help="The path to the file where downloaded data should be written. Defaults to a path calculated from the date range.",
+)
+@click.option(
+ "--all",
+ "billable",
+ is_flag=True,
+ default=True,
+ help="Download all time entries. The default is to download only billable time entries.",
+)
+@click.option(
+ "-c",
+ "--client",
+ "client_ids",
+ envvar="TOGGL_CLIENT_ID",
+ help="An ID for a Toggl Client to filter for in reports. Can be supplied more than once.",
+ metavar="CLIENT_ID",
+ multiple=True,
+ type=int,
+)
+@click.option(
+ "-p",
+ "--project",
+ "project_ids",
+ help="An ID for a Toggl Project to filter for in reports. Can be supplied more than once.",
+ metavar="PROJECT_ID",
+ multiple=True,
+ type=int,
+)
+@click.option(
+ "-t",
+ "--task",
+ "task_ids",
+ help="An ID for a Toggl Project Task to filter for in reports. Can be supplied more than once.",
+ metavar="TASK_ID",
+ multiple=True,
+ type=int,
+)
+@click.option(
+ "-u",
+ "--user",
+ "user_ids",
+ help="An ID for a Toggl User to filter for in reports. Can be supplied more than once.",
+ metavar="USER_ID",
+ multiple=True,
+ type=int,
+)
+def download(
+ start: datetime,
+ end: datetime,
+ output: str = "",
+ billable: bool = True,
+ client_ids: List[int] = [],
+ project_ids: List[int] = [],
+ task_ids: List[int] = [],
+ user_ids: List[int] = [],
+):
+ """
+ Download a Toggl time report in CSV format.
+ """
+ if not output:
+ output = f"Toggl_time_entries_{start.strftime('%Y-%m-%d')}_{end.strftime('%Y-%m-%d')}.csv"
+
+ params = dict(start_date=start, end_date=end, output_path=output, output_cols=TOGGL_COLUMNS)
+
+ if billable:
+ params.update(dict(billable=billable))
+ if client_ids:
+ params.update(dict(client_ids=client_ids))
+ if project_ids:
+ params.update(dict(project_ids=project_ids))
+ if task_ids:
+ params.update(dict(task_ids=task_ids))
+ if user_ids:
+ params.update(dict(user_ids=user_ids))
+
+ click.echo("Downloading Toggl time entries with parameters:")
+ for k, v in params.items():
+ click.echo(f" {k}: {v}")
+
+ download_time_entries(**params)
diff --git a/compiler_admin/commands/user/__init__.py b/compiler_admin/commands/user/__init__.py
index aeb21b1..855a225 100644
--- a/compiler_admin/commands/user/__init__.py
+++ b/compiler_admin/commands/user/__init__.py
@@ -1,22 +1,28 @@
-from argparse import Namespace
+import click
-from compiler_admin.commands.user.alumni import alumni # noqa: F401
-from compiler_admin.commands.user.create import create # noqa: F401
-from compiler_admin.commands.user.convert import convert # noqa: F401
-from compiler_admin.commands.user.delete import delete # noqa: F401
-from compiler_admin.commands.user.offboard import offboard # noqa: F401
-from compiler_admin.commands.user.reset import reset # noqa: F401
-from compiler_admin.commands.user.restore import restore # noqa: F401
-from compiler_admin.commands.user.signout import signout # noqa: F401
+from compiler_admin.commands.user.alumni import alumni
+from compiler_admin.commands.user.convert import convert
+from compiler_admin.commands.user.create import create
+from compiler_admin.commands.user.delete import delete
+from compiler_admin.commands.user.offboard import offboard
+from compiler_admin.commands.user.reset import reset
+from compiler_admin.commands.user.restore import restore
+from compiler_admin.commands.user.signout import signout
-def user(args: Namespace, *extra):
- # try to call the subcommand function directly from global (module) symbols
- # if the subcommand function was imported above, it should exist in globals()
- global_env = globals()
+@click.group
+def user():
+ """
+ Work with users in the Compiler org.
+ """
+ pass
- if args.subcommand in global_env:
- cmd_func = global_env[args.subcommand]
- cmd_func(args, *extra)
- else:
- raise NotImplementedError(f"Unknown user subcommand: {args.subcommand}")
+
+user.add_command(alumni)
+user.add_command(convert)
+user.add_command(create)
+user.add_command(delete)
+user.add_command(offboard)
+user.add_command(reset)
+user.add_command(restore)
+user.add_command(signout)
diff --git a/compiler_admin/commands/user/alumni.py b/compiler_admin/commands/user/alumni.py
index bfa72d3..93e6d9a 100644
--- a/compiler_admin/commands/user/alumni.py
+++ b/compiler_admin/commands/user/alumni.py
@@ -1,10 +1,9 @@
-from argparse import Namespace
+import click
from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
from compiler_admin.commands.user.reset import reset
from compiler_admin.services.google import (
OU_ALUMNI,
- USER_HELLO,
CallGAMCommand,
move_user_ou,
user_account_name,
@@ -12,105 +11,63 @@
)
-def alumni(args: Namespace) -> int:
- """Convert a user to a Compiler alumni.
-
- Optionally notify an email address with the new randomly generated password.
-
- Args:
- username (str): the user account to convert.
-
- notify (str): an email address to send the new password notification.
- Returns:
- A value indicating if the operation succeeded or failed.
+@click.command()
+@click.option("-f", "--force", is_flag=True, help="Don't ask for confirmation.")
+@click.option("-n", "--notify", help="An email address to send the new password notification.")
+@click.option(
+ "-e",
+ "--recovery-email",
+ help="An email address to use as the new recovery email. Without a value, clears the recovery email.",
+)
+@click.option(
+ "-p",
+ "--recovery-phone",
+ help="A phone number to use as the new recovery phone number. Without a value, clears the recovery phone number.",
+)
+@click.argument("username")
+@click.pass_context
+def alumni(
+ ctx: click.Context, username: str, force: bool = False, recovery_email: str = "", recovery_phone: str = "", **kwargs
+):
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
-
- account = user_account_name(args.username)
+ Convert a user to a Compiler alumni.
+ """
+ account = user_account_name(username)
if not user_exists(account):
- print(f"User does not exist: {account}")
- return RESULT_FAILURE
+ click.echo(f"User does not exist: {account}")
+ raise SystemExit(RESULT_FAILURE)
- if getattr(args, "force", False) is False:
- cont = input(f"Convert account to alumni: {account}? (Y/n) ")
+ if not force:
+ cont = input(f"Convert account to alumni for {account}? (Y/n): ")
if not cont.lower().startswith("y"):
- print("Aborting conversion.")
- return RESULT_SUCCESS
+ click.echo("Aborting conversion.")
+ raise SystemExit(RESULT_SUCCESS)
- res = RESULT_SUCCESS
+ click.echo(f"User exists, converting to alumni: {account}")
- print("Removing from groups")
- res += CallGAMCommand(("user", account, "delete", "groups"))
+ click.echo("Removing from groups")
+ CallGAMCommand(("user", account, "delete", "groups"))
- print(f"Moving to OU: {OU_ALUMNI}")
- res += move_user_ou(account, OU_ALUMNI)
+ click.echo(f"Moving to OU: {OU_ALUMNI}")
+ move_user_ou(account, OU_ALUMNI)
# reset password, sign out
- res += reset(args)
+ ctx.forward(reset)
- print("Clearing user profile info")
+ click.echo("Clearing user profile info")
for prop in ["address", "location", "otheremail", "phone"]:
command = ("update", "user", account, prop, "clear")
- res += CallGAMCommand(command)
+ CallGAMCommand(command)
- print("Resetting recovery email")
- recovery = getattr(args, "recovery_email", "")
- command = ("update", "user", account, "recoveryemail", recovery)
- res += CallGAMCommand(command)
+ click.echo("Resetting recovery email")
+ command = ("update", "user", account, "recoveryemail", recovery_email)
+ CallGAMCommand(command)
- print("Resetting recovery phone")
- recovery = getattr(args, "recovery_phone", "")
- command = ("update", "user", account, "recoveryphone", recovery)
- res += CallGAMCommand(command)
+ click.echo("Resetting recovery phone")
+ command = ("update", "user", account, "recoveryphone", recovery_phone)
+ CallGAMCommand(command)
- print("Turning off 2FA")
+ click.echo("Turning off 2FA")
command = ("user", account, "turnoff2sv")
- res += CallGAMCommand(command)
-
- print("Resetting email signature")
- # https://github.com/taers232c/GAMADV-XTD3/wiki/Users-Gmail-Send-As-Signature-Vacation#manage-signature
- command = (
- "user",
- account,
- "signature",
- f"Compiler LLC
https://compiler.la
{USER_HELLO}",
- "replyto",
- USER_HELLO,
- "default",
- "treatasalias",
- "false",
- "name",
- "Compiler LLC",
- "primary",
- )
- res += CallGAMCommand(command)
-
- print("Turning on email autoresponder")
- # https://github.com/taers232c/GAMADV-XTD3/wiki/Users-Gmail-Send-As-Signature-Vacation#manage-vacation
- message = (
- "Thank you for contacting Compiler. This inbox is no longer actively monitored.
"
- + f"Please reach out to {USER_HELLO} if you need to get a hold of us."
- )
- command = (
- "user",
- account,
- "vacation",
- "true",
- "subject",
- "[This inbox is no longer active]",
- "message",
- message,
- "contactsonly",
- "false",
- "domainonly",
- "false",
- "start",
- "Started",
- "end",
- "2999-12-31",
- )
- res += CallGAMCommand(command)
-
- return res
+ CallGAMCommand(command)
diff --git a/compiler_admin/commands/user/convert.py b/compiler_admin/commands/user/convert.py
index f078ac2..f215beb 100644
--- a/compiler_admin/commands/user/convert.py
+++ b/compiler_admin/commands/user/convert.py
@@ -1,6 +1,6 @@
-from argparse import Namespace
+import click
-from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE
+from compiler_admin import RESULT_FAILURE
from compiler_admin.commands.user.alumni import alumni
from compiler_admin.services.google import (
GROUP_PARTNERS,
@@ -22,62 +22,63 @@
ACCOUNT_TYPE_OU = {"alumni": OU_ALUMNI, "contractor": OU_CONTRACTORS, "partner": OU_PARTNERS, "staff": OU_STAFF}
-def convert(args: Namespace) -> int:
- f"""Convert a user of one type to another.
- Args:
- username (str): The account to convert. Must exist already.
-
- account_type (str): One of {", ".join(ACCOUNT_TYPE_OU.keys())}
- Returns:
- A value indicating if the operation succeeded or failed.
+@click.command()
+@click.option("-f", "--force", is_flag=True, help="Don't ask for confirmation.")
+@click.option(
+ "-n", "--notify", help="An email address to send the new password notification. Only valid for alumni conversion."
+)
+@click.option(
+ "-e",
+ "--recovery-email",
+ help="An email address to use as the new recovery email. Only valid for alumni conversion.",
+)
+@click.option(
+ "-p",
+ "--recovery-phone",
+ help="A phone number to use as the new recovery phone number. Only valid for alumni conversion.",
+)
+@click.argument("username")
+@click.argument("account_type", type=click.Choice(ACCOUNT_TYPE_OU.keys(), case_sensitive=False))
+@click.pass_context
+def convert(ctx: click.Context, username: str, account_type: str, **kwargs):
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
- if not hasattr(args, "account_type"):
- raise ValueError("account_type is required")
-
- account = user_account_name(args.username)
- account_type = args.account_type
+ Convert a user of one type to another.
+ """
+ account = user_account_name(username)
if not user_exists(account):
- print(f"User does not exist: {account}")
- return RESULT_FAILURE
+ click.echo(f"User does not exist: {account}")
+ raise SystemExit(RESULT_FAILURE)
- if account_type not in ACCOUNT_TYPE_OU:
- print(f"Unknown account type for conversion: {account_type}")
- return RESULT_FAILURE
-
- print(f"User exists, converting to: {account_type} for {account}")
- res = RESULT_SUCCESS
+ click.echo(f"User exists, converting to: {account_type} for {account}")
if account_type == "alumni":
- res = alumni(args)
+ # call the alumni command
+ ctx.forward(alumni)
elif account_type == "contractor":
if user_is_partner(account):
- res += remove_user_from_group(account, GROUP_PARTNERS)
- res += remove_user_from_group(account, GROUP_STAFF)
+ remove_user_from_group(account, GROUP_PARTNERS)
+ remove_user_from_group(account, GROUP_STAFF)
elif user_is_staff(account):
- res = remove_user_from_group(account, GROUP_STAFF)
+ remove_user_from_group(account, GROUP_STAFF)
elif account_type == "staff":
if user_is_partner(account):
- res += remove_user_from_group(account, GROUP_PARTNERS)
+ remove_user_from_group(account, GROUP_PARTNERS)
elif user_is_staff(account):
- print(f"User is already staff: {account}")
- return RESULT_FAILURE
- res += add_user_to_group(account, GROUP_STAFF)
+ click.echo(f"User is already staff: {account}")
+ raise SystemExit(RESULT_FAILURE)
+ add_user_to_group(account, GROUP_STAFF)
elif account_type == "partner":
if user_is_partner(account):
- print(f"User is already partner: {account}")
- return RESULT_FAILURE
+ click.echo(f"User is already partner: {account}")
+ raise SystemExit(RESULT_FAILURE)
if not user_is_staff(account):
- res += add_user_to_group(account, GROUP_STAFF)
- res += add_user_to_group(account, GROUP_PARTNERS)
-
- res += move_user_ou(account, ACCOUNT_TYPE_OU[account_type])
+ add_user_to_group(account, GROUP_STAFF)
+ add_user_to_group(account, GROUP_PARTNERS)
- print(f"Account conversion complete for: {account}")
+ move_user_ou(account, ACCOUNT_TYPE_OU[account_type])
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ click.echo(f"Account conversion complete for: {account}")
diff --git a/compiler_admin/commands/user/create.py b/compiler_admin/commands/user/create.py
index 6576e26..06e0dfa 100644
--- a/compiler_admin/commands/user/create.py
+++ b/compiler_admin/commands/user/create.py
@@ -1,47 +1,46 @@
-from argparse import Namespace
-from typing import Sequence
-
-from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE
-from compiler_admin.services.google import GROUP_TEAM, add_user_to_group, CallGAMCommand, user_account_name, user_exists
-
-
-def create(args: Namespace, *extra: Sequence[str]) -> int:
- """Create a new user account in Compiler.
+import click
+
+from compiler_admin import RESULT_FAILURE
+from compiler_admin.services.google import (
+ GROUP_TEAM,
+ USER_HELLO,
+ add_user_to_group,
+ CallGAMCommand,
+ user_account_name,
+ user_exists,
+)
+
+
+@click.command(context_settings={"ignore_unknown_options": True})
+@click.option("-n", "--notify", help="An email address to send the new password notification.")
+@click.argument("username")
+@click.argument("gam_args", nargs=-1, type=click.UNPROCESSED)
+def create(username: str, notify: str = "", gam_args: list = []):
+ """Create a new user account.
The user's password is randomly generated and requires reset on first login.
Extra args are passed along to GAM as options.
- See https://github.com/taers232c/GAMADV-XTD3/wiki/Users#create-a-user
- for the complete list of options supported.
- Args:
- username (str): The account to create. Must not exist already.
- Returns:
- A value indicating if the operation succeeded or failed.
+ https://github.com/taers232c/GAMADV-XTD3/wiki/Users#create-a-user
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
-
- account = user_account_name(args.username)
+ account = user_account_name(username)
if user_exists(account):
- print(f"User already exists: {account}")
- return RESULT_FAILURE
+ click.echo(f"User already exists: {account}")
+ raise SystemExit(RESULT_FAILURE)
- print(f"User does not exist, continuing: {account}")
+ click.echo(f"User does not exist, continuing: {account}")
command = ("create", "user", account, "password", "random", "changepassword")
- notify = getattr(args, "notify", None)
if notify:
- command += ("notify", notify, "from", "hello@compiler.la")
-
- command += (*extra,)
+ command += ("notify", notify, "from", USER_HELLO)
- res = CallGAMCommand(command)
+ command += (*gam_args,)
- res += add_user_to_group(account, GROUP_TEAM)
+ CallGAMCommand(command)
- print(f"User created successfully: {account}")
+ add_user_to_group(account, GROUP_TEAM)
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ click.echo(f"User created successfully: {account}")
diff --git a/compiler_admin/commands/user/delete.py b/compiler_admin/commands/user/delete.py
index ac5efc1..89d17dc 100644
--- a/compiler_admin/commands/user/delete.py
+++ b/compiler_admin/commands/user/delete.py
@@ -1,34 +1,28 @@
-from argparse import Namespace
+import click
-from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE
+from compiler_admin import RESULT_FAILURE
from compiler_admin.services.google import CallGAMCommand, user_account_name, user_exists
-def delete(args: Namespace) -> int:
- """Delete the user account.
-
- Args:
- username (str): The account to delete. Must exist and not be an alias.
- Returns:
- A value indicating if the operation succeeded or failed.
+@click.command()
+@click.option("-f", "--force", is_flag=True, help="Don't ask for confirmation.")
+@click.argument("username")
+def delete(username: str, force: bool = False):
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
-
- account = user_account_name(args.username)
+ Delete a user account.
+ """
+ account = user_account_name(username)
if not user_exists(account):
- print(f"User does not exist: {account}")
- return RESULT_FAILURE
+ click.echo(f"User does not exist: {account}")
+ raise SystemExit(RESULT_FAILURE)
- if getattr(args, "force", False) is False:
- cont = input(f"Delete account {account}? (Y/n)")
+ if not force:
+ cont = input(f"Delete account {account}? (Y/n): ")
if not cont.lower().startswith("y"):
- print("Aborting delete.")
- return RESULT_SUCCESS
-
- print(f"User exists, deleting: {account}")
+ click.echo("Aborting delete.")
+ return
- res = CallGAMCommand(("delete", "user", account, "noactionifalias"))
+ click.echo(f"User exists, deleting: {account}")
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ CallGAMCommand(("delete", "user", account, "noactionifalias"))
diff --git a/compiler_admin/commands/user/offboard.py b/compiler_admin/commands/user/offboard.py
index f9bd2c5..d17c385 100644
--- a/compiler_admin/commands/user/offboard.py
+++ b/compiler_admin/commands/user/offboard.py
@@ -1,7 +1,8 @@
-from argparse import Namespace
from tempfile import NamedTemporaryFile
-from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE
+import click
+
+from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
from compiler_admin.commands.user.alumni import alumni
from compiler_admin.commands.user.delete import delete
from compiler_admin.services.google import (
@@ -13,7 +14,13 @@
)
-def offboard(args: Namespace) -> int:
+@click.command()
+@click.option("-a", "--alias", help="Another account to assign username as an alias.")
+@click.option("-f", "--force", is_flag=True, help="Don't ask for confirmation.")
+@click.option("-n", "--notify", help="An email address to send the new password notification.")
+@click.argument("username")
+@click.pass_context
+def offboard(ctx: click.Context, username: str, alias: str = "", force: bool = False, **kwargs):
"""Fully offboard a user from Compiler.
Args:
@@ -23,55 +30,49 @@ def offboard(args: Namespace) -> int:
Returns:
A value indicating if the operation succeeded or failed.
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
-
- username = args.username
account = user_account_name(username)
if not user_exists(account):
- print(f"User does not exist: {account}")
- return RESULT_FAILURE
+ click.echo(f"User does not exist: {account}")
+ raise SystemExit(RESULT_FAILURE)
- alias = getattr(args, "alias", None)
alias_account = user_account_name(alias)
- if alias_account is not None and not user_exists(alias_account):
- print(f"Alias target user does not exist: {alias_account}")
- return RESULT_FAILURE
+ if alias_account and not user_exists(alias_account):
+ click.echo(f"Alias target user does not exist: {alias_account}")
+ raise SystemExit(RESULT_FAILURE)
- if getattr(args, "force", False) is False:
- cont = input(f"Offboard account {account} {' (assigning alias to ' + alias_account + ')' if alias else ''}? (Y/n)")
+ if not force:
+ cont = input(f"Offboard account {account} {' (assigning alias to ' + alias_account + ')' if alias else ''}? (Y/n): ")
if not cont.lower().startswith("y"):
- print("Aborting offboard.")
- return RESULT_SUCCESS
+ click.echo("Aborting offboard.")
+ raise SystemExit(RESULT_SUCCESS)
- print(f"User exists, offboarding: {account}")
- res = RESULT_SUCCESS
+ click.echo(f"User exists, offboarding: {account}")
- res += alumni(args)
+ # call the alumni command
+ ctx.forward(alumni)
- print("Backing up email")
- res += CallGYBCommand(("--service-account", "--email", account, "--action", "backup"))
+ click.echo("Backing up email")
+ CallGYBCommand(("--service-account", "--email", account, "--action", "backup"))
- print("Starting Drive and Calendar transfer")
- res += CallGAMCommand(("create", "transfer", account, "calendar,drive", USER_ARCHIVE, "all", "releaseresources"))
+ click.echo("Starting Drive and Calendar transfer")
+ CallGAMCommand(("create", "transfer", account, "calendar,drive", USER_ARCHIVE, "all", "releaseresources"))
status = ""
with NamedTemporaryFile("w+") as stdout:
while "Overall Transfer Status: completed" not in status:
- print("Transfer in progress")
- res += CallGAMCommand(("show", "transfers", "olduser", username), stdout=stdout.name, stderr="stdout")
+ click.echo("Transfer in progress")
+ CallGAMCommand(("show", "transfers", "olduser", username), stdout=stdout.name, stderr="stdout")
status = " ".join(stdout.readlines())
stdout.seek(0)
- res += CallGAMCommand(("user", account, "deprovision", "popimap"))
+ CallGAMCommand(("user", account, "deprovision", "popimap"))
- res += delete(args)
+ # call the delete command
+ ctx.forward(delete)
if alias_account:
- print(f"Adding an alias to account: {alias_account}")
- res += CallGAMCommand(("create", "alias", account, "user", alias_account))
-
- print(f"Offboarding for user complete: {account}")
+ click.echo(f"Adding an alias to account: {alias_account}")
+ CallGAMCommand(("create", "alias", account, "user", alias_account))
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ click.echo(f"Offboarding for user complete: {account}")
diff --git a/compiler_admin/commands/user/reset.py b/compiler_admin/commands/user/reset.py
index ecdfdda..25792bf 100644
--- a/compiler_admin/commands/user/reset.py
+++ b/compiler_admin/commands/user/reset.py
@@ -1,46 +1,38 @@
-from argparse import Namespace
+import click
-from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE
+from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
from compiler_admin.commands.user.signout import signout
from compiler_admin.services.google import USER_HELLO, CallGAMCommand, user_account_name, user_exists
-def reset(args: Namespace) -> int:
- """Reset a user's password.
-
- Optionally notify an email address with the new randomly generated password.
-
- Args:
- username (str): the user account to reset.
-
- notify (str): an email address to send the new password notification.
- Returns:
- A value indicating if the operation succeeded or failed.
+@click.command()
+@click.option("-f", "--force", is_flag=True, help="Don't ask for confirmation.")
+@click.option("-n", "--notify", help="An email address to send the new password notification.")
+@click.argument("username")
+@click.pass_context
+def reset(ctx: click.Context, username: str, force: bool = False, notify: str = "", **kwargs):
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
-
- account = user_account_name(args.username)
+ Reset a user's password.
+ """
+ account = user_account_name(username)
if not user_exists(account):
- print(f"User does not exist: {account}")
- return RESULT_FAILURE
+ click.echo(f"User does not exist: {account}")
+ raise SystemExit(RESULT_FAILURE)
- if getattr(args, "force", False) is False:
- cont = input(f"Reset password for {account}? (Y/n)")
+ if not force:
+ cont = input(f"Reset password for {account}? (Y/n): ")
if not cont.lower().startswith("y"):
- print("Aborting password reset.")
- return RESULT_SUCCESS
+ click.echo("Aborting password reset.")
+ raise SystemExit(RESULT_SUCCESS)
- command = ("update", "user", account, "password", "random", "changepassword")
+ click.echo(f"User exists, resetting password: {account}")
- notify = getattr(args, "notify", None)
+ command = ("update", "user", account, "password", "random", "changepassword")
if notify:
command += ("notify", notify, "from", USER_HELLO)
- print(f"User exists, resetting password: {account}")
-
- res = CallGAMCommand(command)
- res += signout(args)
+ CallGAMCommand(command)
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ # call the signout command
+ ctx.forward(signout)
diff --git a/compiler_admin/commands/user/restore.py b/compiler_admin/commands/user/restore.py
index 1c56e95..d0fa5da 100644
--- a/compiler_admin/commands/user/restore.py
+++ b/compiler_admin/commands/user/restore.py
@@ -1,31 +1,27 @@
-from argparse import Namespace
import pathlib
-from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE
-from compiler_admin.services.google import USER_ARCHIVE, CallGYBCommand, user_account_name
+import click
+from compiler_admin import RESULT_FAILURE
+from compiler_admin.services.google import USER_ARCHIVE, CallGYBCommand, user_account_name
-def restore(args: Namespace) -> int:
- """Restore an email backup from a prior offboarding.
- Args:
- username (str): the user account with a local email backup to restore.
- Returns:
- A value indicating if the operation succeeded or failed.
+@click.command()
+@click.argument("username")
+def restore(username: str):
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
-
- account = user_account_name(args.username)
+ Restore an email backup from a prior offboarding.
+ """
+ account = user_account_name(username)
backup_dir = f"GYB-GMail-Backup-{account}"
if not pathlib.Path(backup_dir).exists():
- print(f"Couldn't find a local backup: {backup_dir}")
- return RESULT_FAILURE
+ click.echo(f"Couldn't find a local backup: {backup_dir}")
+ raise SystemExit(RESULT_FAILURE)
- print(f"Found backup, starting restore process with dest: {USER_ARCHIVE} for account: {account}")
+ click.echo(f"Found backup, starting restore process with dest: {USER_ARCHIVE} for account: {account}")
- res = CallGYBCommand(
+ CallGYBCommand(
(
"--service-account",
"--email",
@@ -39,6 +35,4 @@ def restore(args: Namespace) -> int:
)
)
- print(f"Email restore complete for: {account}")
-
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ click.echo(f"Email restore complete for: {account}")
diff --git a/compiler_admin/commands/user/signout.py b/compiler_admin/commands/user/signout.py
index e204578..e9c8764 100644
--- a/compiler_admin/commands/user/signout.py
+++ b/compiler_admin/commands/user/signout.py
@@ -1,34 +1,28 @@
-from argparse import Namespace
+import click
-from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE
+from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
from compiler_admin.services.google import CallGAMCommand, user_account_name, user_exists
-def signout(args: Namespace) -> int:
- """Signs the user out from all active sessions.
-
- Args:
- username (str): The account to sign out. Must exist already.
- Returns:
- A value indicating if the operation succeeded or failed.
+@click.command()
+@click.option("-f", "--force", is_flag=True, help="Don't ask for confirmation.")
+@click.argument("username")
+def signout(username: str, force: bool = False, **kwargs):
"""
- if not hasattr(args, "username"):
- raise ValueError("username is required")
-
- account = user_account_name(args.username)
+ Sign a user out from all active sessions.
+ """
+ account = user_account_name(username)
if not user_exists(account):
- print(f"User does not exist: {account}")
- return RESULT_FAILURE
+ click.echo(f"User does not exist: {account}")
+ raise SystemExit(RESULT_FAILURE)
- if getattr(args, "force", False) is False:
- cont = input(f"Signout account {account} from all active sessions? (Y/n)")
+ if not force:
+ cont = input(f"Signout account {account} from all active sessions? (Y/n): ")
if not cont.lower().startswith("y"):
- print("Aborting signout.")
- return RESULT_SUCCESS
-
- print(f"User exists, signing out from all active sessions: {account}")
+ click.echo("Aborting signout.")
+ raise SystemExit(RESULT_SUCCESS)
- res = CallGAMCommand(("user", account, "signout"))
+ click.echo(f"User exists, signing out from all active sessions: {account}")
- return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
+ CallGAMCommand(("user", account, "signout"))
diff --git a/compiler_admin/main.py b/compiler_admin/main.py
index bc16512..86a4340 100644
--- a/compiler_admin/main.py
+++ b/compiler_admin/main.py
@@ -1,231 +1,24 @@
-from argparse import ArgumentParser, _SubParsersAction
-from datetime import datetime, timedelta
-import os
-import sys
+import click
-from pytz import timezone
+from compiler_admin import __version__
-from compiler_admin import __version__ as version
from compiler_admin.commands.info import info
from compiler_admin.commands.init import init
from compiler_admin.commands.time import time
-from compiler_admin.commands.time.convert import CONVERTERS
from compiler_admin.commands.user import user
-from compiler_admin.commands.user.convert import ACCOUNT_TYPE_OU
-TZINFO = timezone(os.environ.get("TZ_NAME", "America/Los_Angeles"))
+@click.group
+@click.version_option(__version__, prog_name="compiler-admin")
+def main():
+ """Compiler's command line interface."""
+ pass
-def local_now():
- return datetime.now(tz=TZINFO)
-
-
-def prior_month_end():
- now = local_now()
- first = now.replace(day=1)
- return first - timedelta(days=1)
-
-
-def prior_month_start():
- end = prior_month_end()
- return end.replace(day=1)
-
-
-def add_sub_cmd_parser(parser: ArgumentParser, dest="subcommand", help=None):
- """Helper adds a subparser for the given dest."""
- return parser.add_subparsers(dest=dest, help=help)
-
-
-def add_sub_cmd(cmd: _SubParsersAction, subcmd, help) -> ArgumentParser:
- """Helper creates a new subcommand parser."""
- return cmd.add_parser(subcmd, help=help)
-
-
-def add_sub_cmd_with_username_arg(cmd: _SubParsersAction, subcmd, help) -> ArgumentParser:
- """Helper creates a new subcommand parser with a required username arg."""
- sub_cmd = add_sub_cmd(cmd, subcmd, help=help)
- sub_cmd.add_argument("username", help="A Compiler user account name, sans domain.")
- return sub_cmd
-
-
-def setup_info_command(cmd_parsers: _SubParsersAction):
- info_cmd = add_sub_cmd(cmd_parsers, "info", help="Print configuration and debugging information.")
- info_cmd.set_defaults(func=info)
-
-
-def setup_init_command(cmd_parsers: _SubParsersAction):
- init_cmd = add_sub_cmd_with_username_arg(
- cmd_parsers, "init", help="Initialize a new admin project. This command should be run once before any others."
- )
- init_cmd.add_argument("--gam", action="store_true", help="If provided, initialize a new GAM project.")
- init_cmd.add_argument("--gyb", action="store_true", help="If provided, initialize a new GYB project.")
- init_cmd.set_defaults(func=init)
-
-
-def setup_time_command(cmd_parsers: _SubParsersAction):
- time_cmd = add_sub_cmd(cmd_parsers, "time", help="Work with Compiler time entries.")
- time_cmd.set_defaults(func=time)
- time_subcmds = add_sub_cmd_parser(time_cmd, help="The time command to run.")
-
- time_convert = add_sub_cmd(time_subcmds, "convert", help="Convert a time report from one format into another.")
- time_convert.add_argument(
- "--input",
- default=os.environ.get("TOGGL_DATA", sys.stdin),
- help="The path to the source data for conversion. Defaults to $TOGGL_DATA or stdin.",
- )
- time_convert.add_argument(
- "--output",
- default=os.environ.get("HARVEST_DATA", sys.stdout),
- help="The path to the file where converted data should be written. Defaults to $HARVEST_DATA or stdout.",
- )
- time_convert.add_argument(
- "--from",
- default="toggl",
- choices=sorted(CONVERTERS.keys()),
- dest="from_fmt",
- help="The format of the source data. Defaults to 'toggl'.",
- )
- time_convert.add_argument(
- "--to",
- default="harvest",
- choices=sorted([to_fmt for sub in CONVERTERS.values() for to_fmt in sub.keys()]),
- dest="to_fmt",
- help="The format of the converted data. Defaults to 'harvest'.",
- )
- time_convert.add_argument("--client", default=None, help="The name of the client to use in converted data.")
-
- time_download = add_sub_cmd(time_subcmds, "download", help="Download a Toggl report in CSV format.")
- time_download.add_argument(
- "--start",
- metavar="YYYY-MM-DD",
- default=prior_month_start(),
- type=lambda s: TZINFO.localize(datetime.strptime(s, "%Y-%m-%d")),
- help="The start date of the reporting period. Defaults to the beginning of the prior month.",
- )
- time_download.add_argument(
- "--end",
- metavar="YYYY-MM-DD",
- default=prior_month_end(),
- type=lambda s: TZINFO.localize(datetime.strptime(s, "%Y-%m-%d")),
- help="The end date of the reporting period. Defaults to the end of the prior month.",
- )
- time_download.add_argument(
- "--output",
- default=os.environ.get("TOGGL_DATA", sys.stdout),
- help="The path to the file where downloaded data should be written. Defaults to $TOGGL_DATA or stdout.",
- )
- time_download.add_argument(
- "--all",
- default=True,
- action="store_false",
- dest="billable",
- help="Download all time entries. The default is to download only billable time entries.",
- )
- time_download.add_argument(
- "--client",
- dest="client_ids",
- metavar="CLIENT_ID",
- action="append",
- type=int,
- help="An ID for a Toggl Client to filter for in reports. Can be supplied more than once.",
- )
- time_download.add_argument(
- "--project",
- dest="project_ids",
- metavar="PROJECT_ID",
- action="append",
- type=int,
- help="An ID for a Toggl Project to filter for in reports. Can be supplied more than once.",
- )
- time_download.add_argument(
- "--task",
- dest="task_ids",
- metavar="TASK_ID",
- action="append",
- type=int,
- help="An ID for a Toggl Project Task to filter for in reports. Can be supplied more than once.",
- )
- time_download.add_argument(
- "--user",
- dest="user_ids",
- metavar="USER_ID",
- action="append",
- type=int,
- help="An ID for a Toggl User to filter for in reports. Can be supplied more than once.",
- )
-
-
-def setup_user_command(cmd_parsers: _SubParsersAction):
- user_cmd = add_sub_cmd(cmd_parsers, "user", help="Work with users in the Compiler org.")
- user_cmd.set_defaults(func=user)
- user_subcmds = add_sub_cmd_parser(user_cmd, help="The user command to run.")
-
- user_alumni = add_sub_cmd_with_username_arg(user_subcmds, "alumni", help="Convert a user account to a Compiler alumni.")
- user_alumni.add_argument("--notify", help="An email address to send the alumni's new password.")
- user_alumni.add_argument(
- "--force", action="store_true", default=False, help="Don't ask for confirmation before conversion."
- )
-
- user_create = add_sub_cmd_with_username_arg(user_subcmds, "create", help="Create a new user in the Compiler domain.")
- user_create.add_argument("--notify", help="An email address to send the newly created account info.")
-
- user_convert = add_sub_cmd_with_username_arg(user_subcmds, "convert", help="Convert a user account to a new type.")
- user_convert.add_argument("account_type", choices=ACCOUNT_TYPE_OU.keys(), help="Target account type for this conversion.")
- user_convert.add_argument(
- "--force", action="store_true", default=False, help="Don't ask for confirmation before conversion."
- )
- user_convert.add_argument("--notify", help="An email address to send the alumni's new password.")
-
- user_delete = add_sub_cmd_with_username_arg(user_subcmds, "delete", help="Delete a user account.")
- user_delete.add_argument("--force", action="store_true", default=False, help="Don't ask for confirmation before deletion.")
-
- user_offboard = add_sub_cmd_with_username_arg(user_subcmds, "offboard", help="Offboard a user account.")
- user_offboard.add_argument("--alias", help="Account to assign username as an alias.")
- user_offboard.add_argument(
- "--force", action="store_true", default=False, help="Don't ask for confirmation before offboarding."
- )
-
- user_reset = add_sub_cmd_with_username_arg(
- user_subcmds, "reset", help="Reset a user's password to a randomly generated string."
- )
- user_reset.add_argument("--force", action="store_true", default=False, help="Don't ask for confirmation before reset.")
- user_reset.add_argument("--notify", help="An email address to send the newly generated password.")
-
- add_sub_cmd_with_username_arg(user_subcmds, "restore", help="Restore an email backup from a prior offboarding.")
-
- user_signout = add_sub_cmd_with_username_arg(user_subcmds, "signout", help="Signs a user out from all active sessions.")
- user_signout.add_argument("--force", action="store_true", default=False, help="Don't ask for confirmation before signout.")
-
-
-def main(argv=None):
- argv = argv if argv is not None else sys.argv[1:]
- parser = ArgumentParser(prog="compiler-admin")
-
- # https://stackoverflow.com/a/8521644/812183
- parser.add_argument(
- "-v",
- "--version",
- action="version",
- version=f"%(prog)s {version}",
- )
-
- cmd_parsers = add_sub_cmd_parser(parser, dest="command", help="The command to run")
- setup_info_command(cmd_parsers)
- setup_init_command(cmd_parsers)
- setup_time_command(cmd_parsers)
- setup_user_command(cmd_parsers)
-
- if len(argv) == 0:
- argv = ["info"]
-
- args, extra = parser.parse_known_args(argv)
-
- if args.func:
- return args.func(args, *extra)
- else:
- raise ValueError("Unrecognized command")
-
+main.add_command(init)
+main.add_command(info)
+main.add_command(time)
+main.add_command(user)
if __name__ == "__main__":
raise SystemExit(main())
diff --git a/compiler_admin/services/toggl.py b/compiler_admin/services/toggl.py
index 06c4bfa..84f4d1e 100644
--- a/compiler_admin/services/toggl.py
+++ b/compiler_admin/services/toggl.py
@@ -192,12 +192,6 @@ def download_time_entries(
Returns:
None. Either prints the resulting CSV data or writes to output_path.
"""
- env_client_id = os.environ.get("TOGGL_CLIENT_ID")
- if env_client_id:
- env_client_id = int(env_client_id)
- if ("client_ids" not in kwargs or not kwargs["client_ids"]) and isinstance(env_client_id, int):
- kwargs["client_ids"] = [env_client_id]
-
token = os.environ.get("TOGGL_API_TOKEN")
workspace = os.environ.get("TOGGL_WORKSPACE_ID")
toggl = Toggl(token, workspace)
diff --git a/pyproject.toml b/pyproject.toml
index 433e4ba..43ad7fe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,6 +12,7 @@ authors = [
requires-python = ">=3.11"
dependencies = [
"advanced-gam-for-google-workspace @ git+https://github.com/taers232c/GAMADV-XTD3.git@v7.00.38#subdirectory=src",
+ "click==8.1.7",
"pandas==2.2.3",
"tzdata",
]
diff --git a/tests/commands/test_info.py b/tests/commands/test_info.py
index 4f9f3f7..6c705c1 100644
--- a/tests/commands/test_info.py
+++ b/tests/commands/test_info.py
@@ -1,8 +1,7 @@
import pytest
-from compiler_admin import RESULT_SUCCESS
+from compiler_admin import RESULT_SUCCESS, __version__
from compiler_admin.commands.info import info, __name__ as MODULE
-from compiler_admin.services.google import DOMAIN
@pytest.fixture
@@ -15,21 +14,10 @@ def mock_google_CallGYBCommand(mock_google_CallGYBCommand):
return mock_google_CallGYBCommand(MODULE)
-def test_info(mock_google_CallGAMCommand, mock_google_CallGYBCommand):
- info()
+def test_info(cli_runner, mock_google_CallGAMCommand, mock_google_CallGYBCommand):
+ result = cli_runner.invoke(info)
+ assert result.exit_code == RESULT_SUCCESS
+ assert f"compiler-admin, version {__version__}" in result.output
assert mock_google_CallGAMCommand.call_count > 0
mock_google_CallGYBCommand.assert_called_once()
-
-
-@pytest.mark.e2e
-def test_info_e2e(capfd):
- res = info()
- captured = capfd.readouterr()
-
- assert res == RESULT_SUCCESS
- assert "compiler-admin:" in captured.out
- assert "GAMADV-XTD3" in captured.out
- assert f"Primary Domain: {DOMAIN}" in captured.out
- assert "Got Your Back" in captured.out
- assert "WARNING: Config File:" not in captured.err
diff --git a/tests/commands/test_init.py b/tests/commands/test_init.py
index cbe8a8b..c6cfef6 100644
--- a/tests/commands/test_init.py
+++ b/tests/commands/test_init.py
@@ -1,6 +1,6 @@
-from argparse import Namespace
import pytest
+from compiler_admin import RESULT_SUCCESS
from compiler_admin.commands.init import _clean_config_dir, init, __name__ as MODULE
@@ -52,35 +52,28 @@ def test_clean_config_dir(mocker, mock_GAM_CONFIG_PATH, mock_rmtree):
assert mock_dir in mock_rmtree.call_args.args
-def test_init_admin_user_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- init(args)
-
-
-def test_init_default(mock_clean_config_dir, mock_google_CallGAMCommand, mock_subprocess_call):
- args = Namespace(username="username")
- init(args)
+def test_init_default(cli_runner, mock_clean_config_dir, mock_google_CallGAMCommand, mock_subprocess_call):
+ result = cli_runner.invoke(init, ["username"])
+ assert result.exit_code == RESULT_SUCCESS
assert mock_clean_config_dir.call_count == 0
assert mock_google_CallGAMCommand.call_count == 0
assert mock_subprocess_call.call_count == 0
-def test_init_gam(mock_GAM_CONFIG_PATH, mock_clean_config_dir, mock_google_CallGAMCommand):
- args = Namespace(username="username", gam=True, gyb=False)
- init(args)
+def test_init_gam(cli_runner, mock_GAM_CONFIG_PATH, mock_clean_config_dir, mock_google_CallGAMCommand):
+ result = cli_runner.invoke(init, ["--gam", "username"])
+ assert result.exit_code == RESULT_SUCCESS
mock_clean_config_dir.assert_called_once()
assert mock_GAM_CONFIG_PATH in mock_clean_config_dir.call_args.args
assert mock_google_CallGAMCommand.call_count > 0
-def test_init_gyb(mock_GYB_CONFIG_PATH, mock_clean_config_dir, mock_subprocess_call):
- args = Namespace(username="username", gam=False, gyb=True)
- init(args)
+def test_init_gyb(cli_runner, mock_GYB_CONFIG_PATH, mock_clean_config_dir, mock_subprocess_call):
+ result = cli_runner.invoke(init, ["--gyb", "username"])
+ assert result.exit_code == RESULT_SUCCESS
mock_clean_config_dir.assert_called_once()
assert mock_GYB_CONFIG_PATH in mock_clean_config_dir.call_args.args
assert mock_subprocess_call.call_count > 0
diff --git a/tests/commands/time/test__init__.py b/tests/commands/time/test__init__.py
deleted file mode 100644
index db439fb..0000000
--- a/tests/commands/time/test__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from argparse import Namespace
-
-import pytest
-
-from compiler_admin.commands.time import time
-
-
-def test_time_subcommand_exists(mocker):
- args = Namespace(subcommand="subcmd")
- subcmd = mocker.patch("compiler_admin.commands.time.globals", return_value={"subcmd": mocker.Mock()})
-
- time(args, 1, 2, 3)
-
- subcmd.assert_called_once()
- subcmd.return_value["subcmd"].assert_called_once_with(args, 1, 2, 3)
-
-
-def test_time_subcommand_doesnt_exists(mocker):
- args = Namespace(subcommand="subcmd")
- subcmd = mocker.patch("compiler_admin.commands.time.globals", return_value={})
-
- with pytest.raises(NotImplementedError, match="Unknown time subcommand: subcmd"):
- time(args, 1, 2, 3)
-
- subcmd.assert_called_once()
diff --git a/tests/commands/time/test_convert.py b/tests/commands/time/test_convert.py
index 68fc580..b5e4276 100644
--- a/tests/commands/time/test_convert.py
+++ b/tests/commands/time/test_convert.py
@@ -1,4 +1,3 @@
-from argparse import Namespace
import pytest
from compiler_admin import RESULT_SUCCESS
@@ -40,14 +39,15 @@ def test_get_source_converter_mismatch():
_get_source_converter("toggl", "nope")
-def test_convert(mock_get_source_converter):
- args = Namespace(input="input", output="output", client="client", from_fmt="from", to_fmt="to")
- res = convert(args)
+def test_convert(cli_runner, mock_get_source_converter):
+ result = cli_runner.invoke(
+ convert, ["--input", "input", "--output", "output", "--client", "client", "--from", "harvest", "--to", "toggl"]
+ )
- assert res == RESULT_SUCCESS
- mock_get_source_converter.assert_called_once_with(args.from_fmt, args.to_fmt)
+ assert result.exit_code == RESULT_SUCCESS
+ mock_get_source_converter.assert_called_once_with("harvest", "toggl")
mock_get_source_converter.return_value.assert_called_once_with(
- source_path=args.input, output_path=args.output, client_name=args.client
+ source_path="input", output_path="output", client_name="client"
)
diff --git a/tests/commands/time/test_download.py b/tests/commands/time/test_download.py
index fe9f80b..851289c 100644
--- a/tests/commands/time/test_download.py
+++ b/tests/commands/time/test_download.py
@@ -1,9 +1,32 @@
-from argparse import Namespace
from datetime import datetime
import pytest
from compiler_admin import RESULT_SUCCESS
-from compiler_admin.commands.time.download import __name__ as MODULE, download, TOGGL_COLUMNS
+from compiler_admin.commands.time.download import (
+ __name__ as MODULE,
+ download,
+ TOGGL_COLUMNS,
+ TZINFO,
+ prior_month_end,
+ prior_month_start,
+)
+
+
+@pytest.fixture
+def mock_local_now(mocker):
+ dt = datetime(2024, 9, 25, tzinfo=TZINFO)
+ mocker.patch(f"{MODULE}.local_now", return_value=dt)
+ return dt
+
+
+@pytest.fixture
+def mock_start(mock_local_now):
+ return datetime(2024, 8, 1, tzinfo=TZINFO)
+
+
+@pytest.fixture
+def mock_end(mock_local_now):
+ return datetime(2024, 8, 31, tzinfo=TZINFO)
@pytest.fixture
@@ -11,31 +34,98 @@ def mock_download_time_entries(mocker):
return mocker.patch(f"{MODULE}.download_time_entries")
-@pytest.mark.parametrize("billable", [True, False])
-def test_download(mock_download_time_entries, billable):
- date = datetime.now()
- args = Namespace(
- start=date,
- end=date,
- output="output",
- billable=billable,
- client_ids=["c1", "c2"],
- project_ids=["p1", "p2"],
- task_ids=["t1", "t2"],
- user_ids=["u1", "u2"],
- )
+def test_prior_month_start(mock_start):
+ start = prior_month_start()
+
+ assert start == mock_start
+
+
+def test_prior_month_end(mock_end):
+ end = prior_month_end()
+
+ assert end == mock_end
+
- res = download(args)
+def test_download(cli_runner, mock_download_time_entries):
+ date = datetime.now(tz=TZINFO).replace(hour=0, minute=0, second=0, microsecond=0)
+ args = [
+ "--start",
+ date.strftime("%Y-%m-%d %H:%M:%S%z"),
+ "--end",
+ date.strftime("%Y-%m-%d %H:%M:%S%z"),
+ "--output",
+ "output",
+ "-c",
+ 1,
+ "-c",
+ 2,
+ "-p",
+ 3,
+ "-p",
+ 4,
+ "-t",
+ 5,
+ "-t",
+ 6,
+ "-u",
+ 7,
+ "-u",
+ 8,
+ ]
- assert res == RESULT_SUCCESS
+ result = cli_runner.invoke(download, args)
+
+ assert result.exit_code == RESULT_SUCCESS
mock_download_time_entries.assert_called_once_with(
- start_date=args.start,
- end_date=args.end,
- output_path=args.output,
+ start_date=date,
+ end_date=date,
+ output_path="output",
+ billable=True,
output_cols=TOGGL_COLUMNS,
- billable=args.billable,
- client_ids=args.client_ids,
- project_ids=args.project_ids,
- task_ids=args.task_ids,
- user_ids=args.user_ids,
+ client_ids=(1, 2),
+ project_ids=(3, 4),
+ task_ids=(5, 6),
+ user_ids=(7, 8),
+ )
+
+
+def test_download_client_envvar(cli_runner, monkeypatch, mock_download_time_entries):
+ monkeypatch.setenv("TOGGL_CLIENT_ID", 1234)
+
+ date = datetime.now(tz=TZINFO).replace(hour=0, minute=0, second=0, microsecond=0)
+ args = [
+ "--start",
+ date.strftime("%Y-%m-%d %H:%M:%S%z"),
+ "--end",
+ date.strftime("%Y-%m-%d %H:%M:%S%z"),
+ "--output",
+ "output",
+ ]
+
+ result = cli_runner.invoke(download, args)
+
+ assert result.exit_code == RESULT_SUCCESS
+ mock_download_time_entries.assert_called_once_with(
+ start_date=date, end_date=date, output_path="output", output_cols=TOGGL_COLUMNS, billable=True, client_ids=(1234,)
+ )
+
+
+def test_download_all(cli_runner, monkeypatch, mock_download_time_entries):
+ monkeypatch.delenv("TOGGL_CLIENT_ID", raising=False)
+ date = datetime.now(tz=TZINFO).replace(hour=0, minute=0, second=0, microsecond=0)
+ args = [
+ "--start",
+ date.strftime("%Y-%m-%d %H:%M:%S%z"),
+ "--end",
+ date.strftime("%Y-%m-%d %H:%M:%S%z"),
+ "--output",
+ "output",
+ "--all",
+ ]
+
+ result = cli_runner.invoke(download, args)
+
+ assert result.exit_code == RESULT_SUCCESS
+ mock_download_time_entries.assert_called_once_with(
+ start_date=date, end_date=date, output_path="output", output_cols=TOGGL_COLUMNS
)
diff --git a/tests/commands/time/test_init.py b/tests/commands/time/test_init.py
new file mode 100644
index 0000000..3181ad7
--- /dev/null
+++ b/tests/commands/time/test_init.py
@@ -0,0 +1,8 @@
+import pytest
+
+from compiler_admin.commands.time import time
+
+
+@pytest.mark.parametrize("command", ["convert", "download"])
+def test_time_commands(command):
+ assert command in time.commands
diff --git a/tests/commands/user/test__init__.py b/tests/commands/user/test__init__.py
deleted file mode 100644
index fd55421..0000000
--- a/tests/commands/user/test__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from argparse import Namespace
-
-import pytest
-
-from compiler_admin.commands.user import user
-
-
-def test_user_subcommand_exists(mocker):
- args = Namespace(subcommand="subcmd")
- subcmd = mocker.patch("compiler_admin.commands.user.globals", return_value={"subcmd": mocker.Mock()})
-
- user(args, 1, 2, 3)
-
- subcmd.assert_called()
- subcmd.return_value["subcmd"].assert_called_once_with(args, 1, 2, 3)
-
-
-def test_time_subcommand_doesnt_exists(mocker):
- args = Namespace(subcommand="subcmd")
- subcmd = mocker.patch("compiler_admin.commands.user.globals", return_value={})
-
- with pytest.raises(NotImplementedError, match="Unknown user subcommand: subcmd"):
- user(args, 1, 2, 3)
-
- subcmd.assert_called_once()
diff --git a/tests/commands/user/test_alumni.py b/tests/commands/user/test_alumni.py
index 16df231..51db376 100644
--- a/tests/commands/user/test_alumni.py
+++ b/tests/commands/user/test_alumni.py
@@ -1,4 +1,3 @@
-from argparse import Namespace
import pytest
from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
@@ -45,58 +44,50 @@ def mock_google_user_exists(mock_google_user_exists):
return mock_google_user_exists(MODULE)
-def test_alumni_username_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- alumni(args)
-
-
-def test_alumni_user_does_not_exists(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_alumni_user_does_not_exists(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
mock_google_user_exists.return_value = False
- args = Namespace(username="username")
- res = alumni(args)
+ result = cli_runner.invoke(alumni, ["username"])
- assert res == RESULT_FAILURE
+ assert result.exit_code == RESULT_FAILURE
+ assert result.exception
+ assert "User does not exist: username@compiler.la" in result.output
mock_google_CallGAMCommand.assert_not_called()
@pytest.mark.usefixtures("mock_input_yes")
def test_alumni_confirm_yes(
- mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou
+ cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_google_move_user_ou, mock_commands_reset
):
mock_google_user_exists.return_value = True
- args = Namespace(username="username", force=False)
- res = alumni(args)
+ result = cli_runner.invoke(alumni, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called()
mock_google_move_user_ou.assert_called_once_with("username@compiler.la", OU_ALUMNI)
- mock_commands_reset.assert_called_once_with(args)
@pytest.mark.usefixtures("mock_input_no")
-def test_alumni_confirm_no(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou):
+def test_alumni_confirm_no(
+ cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_google_move_user_ou, mock_commands_reset
+):
mock_google_user_exists.return_value = True
- args = Namespace(username="username", force=False)
- res = alumni(args)
+ result = cli_runner.invoke(alumni, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_not_called()
- mock_commands_reset.assert_not_called()
mock_google_move_user_ou.assert_not_called()
-def test_alumni_force(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou):
+def test_alumni_force(
+ cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_google_move_user_ou, mock_commands_reset
+):
mock_google_user_exists.return_value = True
- args = Namespace(username="username", force=True)
- res = alumni(args)
+ result = cli_runner.invoke(alumni, ["--force", "username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called()
mock_google_move_user_ou.assert_called_once_with("username@compiler.la", OU_ALUMNI)
- mock_commands_reset.assert_called_once_with(args)
diff --git a/tests/commands/user/test_convert.py b/tests/commands/user/test_convert.py
index ddbbc99..8228029 100644
--- a/tests/commands/user/test_convert.py
+++ b/tests/commands/user/test_convert.py
@@ -1,7 +1,6 @@
-from argparse import Namespace
import pytest
-from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
+from compiler_admin import RESULT_SUCCESS
from compiler_admin.commands.user.convert import convert, __name__ as MODULE
@@ -70,76 +69,56 @@ def mock_google_user_is_staff_false(mock_google_user_is_staff):
return mock_google_user_is_staff
-def test_convert_user_username_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- convert(args)
-
-
-def test_convert_user_account_type_required():
- args = Namespace(username="username")
-
- with pytest.raises(ValueError, match="account_type is required"):
- convert(args)
-
-
-def test_convert_user_does_not_exists(mock_google_user_exists, mock_google_move_user_ou):
+def test_convert_user_does_not_exists(cli_runner, mock_google_user_exists, mock_google_move_user_ou):
mock_google_user_exists.return_value = False
- args = Namespace(username="username", account_type="account_type")
- res = convert(args)
+ result = cli_runner.invoke(convert, ["username", "staff"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
assert mock_google_move_user_ou.call_count == 0
@pytest.mark.usefixtures("mock_google_user_exists_true")
-def test_convert_user_exists_bad_account_type(mock_google_move_user_ou):
- args = Namespace(username="username", account_type="account_type")
- res = convert(args)
+def test_convert_user_exists_bad_account_type(cli_runner, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "bad_account_type"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
assert mock_google_move_user_ou.call_count == 0
@pytest.mark.usefixtures("mock_google_user_exists_true")
-def test_convert_alumni(mock_commands_alumni, mock_google_move_user_ou):
- args = Namespace(username="username", account_type="alumni")
- res = convert(args)
+def test_convert_alumni(cli_runner, mock_commands_alumni, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "alumni"])
- assert res == RESULT_SUCCESS
- mock_commands_alumni.assert_called_once_with(args)
+ assert result.exit_code == RESULT_SUCCESS
+ mock_commands_alumni.callback.assert_called_once()
mock_google_move_user_ou.assert_called_once()
@pytest.mark.usefixtures(
"mock_google_user_exists_true", "mock_google_user_is_partner_false", "mock_google_user_is_staff_false"
)
-def test_convert_contractor(mock_google_move_user_ou):
- args = Namespace(username="username", account_type="contractor")
- res = convert(args)
+def test_convert_contractor(cli_runner, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "contractor"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_move_user_ou.assert_called_once()
@pytest.mark.usefixtures("mock_google_user_exists_true", "mock_google_user_is_partner_true", "mock_google_user_is_staff_false")
-def test_convert_contractor_user_is_partner(mock_google_remove_user_from_group, mock_google_move_user_ou):
- args = Namespace(username="username", account_type="contractor")
- res = convert(args)
+def test_convert_contractor_user_is_partner(cli_runner, mock_google_remove_user_from_group, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "contractor"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
assert mock_google_remove_user_from_group.call_count == 2
mock_google_move_user_ou.assert_called_once()
@pytest.mark.usefixtures("mock_google_user_exists_true", "mock_google_user_is_partner_false", "mock_google_user_is_staff_true")
-def test_convert_contractor_user_is_staff(mock_google_remove_user_from_group, mock_google_move_user_ou):
- args = Namespace(username="username", account_type="contractor")
- res = convert(args)
+def test_convert_contractor_user_is_staff(cli_runner, mock_google_remove_user_from_group, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "contractor"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_remove_user_from_group.assert_called_once()
mock_google_move_user_ou.assert_called_once()
@@ -147,34 +126,31 @@ def test_convert_contractor_user_is_staff(mock_google_remove_user_from_group, mo
@pytest.mark.usefixtures(
"mock_google_user_exists_true", "mock_google_user_is_partner_false", "mock_google_user_is_staff_false"
)
-def test_convert_staff(mock_google_add_user_to_group, mock_google_move_user_ou):
- args = Namespace(username="username", account_type="staff")
- res = convert(args)
+def test_convert_staff(cli_runner, mock_google_add_user_to_group, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "staff"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_add_user_to_group.assert_called_once()
mock_google_move_user_ou.assert_called_once()
@pytest.mark.usefixtures("mock_google_user_exists_true", "mock_google_user_is_partner_true", "mock_google_user_is_staff_false")
def test_convert_staff_user_is_partner(
- mock_google_add_user_to_group, mock_google_remove_user_from_group, mock_google_move_user_ou
+ cli_runner, mock_google_add_user_to_group, mock_google_remove_user_from_group, mock_google_move_user_ou
):
- args = Namespace(username="username", account_type="staff")
- res = convert(args)
+ result = cli_runner.invoke(convert, ["username", "staff"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_remove_user_from_group.assert_called_once()
mock_google_add_user_to_group.assert_called_once()
mock_google_move_user_ou.assert_called_once()
@pytest.mark.usefixtures("mock_google_user_exists_true", "mock_google_user_is_partner_false", "mock_google_user_is_staff_true")
-def test_convert_staff_user_is_staff(mock_google_add_user_to_group, mock_google_move_user_ou):
- args = Namespace(username="username", account_type="staff")
- res = convert(args)
+def test_convert_staff_user_is_staff(cli_runner, mock_google_add_user_to_group, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "staff"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
assert mock_google_add_user_to_group.call_count == 0
assert mock_google_move_user_ou.call_count == 0
@@ -182,30 +158,27 @@ def test_convert_staff_user_is_staff(mock_google_add_user_to_group, mock_google_
@pytest.mark.usefixtures(
"mock_google_user_exists_true", "mock_google_user_is_partner_false", "mock_google_user_is_staff_false"
)
-def test_convert_partner(mock_google_add_user_to_group, mock_google_move_user_ou):
- args = Namespace(username="username", account_type="partner")
- res = convert(args)
+def test_convert_partner(cli_runner, mock_google_add_user_to_group, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "partner"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
assert mock_google_add_user_to_group.call_count == 2
mock_google_move_user_ou.assert_called_once()
@pytest.mark.usefixtures("mock_google_user_exists_true", "mock_google_user_is_partner_true", "mock_google_user_is_staff_false")
-def test_convert_partner_user_is_partner(mock_google_add_user_to_group, mock_google_move_user_ou):
- args = Namespace(username="username", account_type="partner")
- res = convert(args)
+def test_convert_partner_user_is_partner(cli_runner, mock_google_add_user_to_group, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "partner"])
- assert res == RESULT_FAILURE
+ assert result != RESULT_SUCCESS
assert mock_google_add_user_to_group.call_count == 0
assert mock_google_move_user_ou.call_count == 0
@pytest.mark.usefixtures("mock_google_user_exists_true", "mock_google_user_is_partner_false", "mock_google_user_is_staff_true")
-def test_convert_partner_user_is_staff(mock_google_add_user_to_group, mock_google_move_user_ou):
- args = Namespace(username="username", account_type="partner")
- res = convert(args)
+def test_convert_partner_user_is_staff(cli_runner, mock_google_add_user_to_group, mock_google_move_user_ou):
+ result = cli_runner.invoke(convert, ["username", "partner"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_add_user_to_group.assert_called_once()
mock_google_move_user_ou.assert_called_once()
diff --git a/tests/commands/user/test_create.py b/tests/commands/user/test_create.py
index 3d839a3..cebe38d 100644
--- a/tests/commands/user/test_create.py
+++ b/tests/commands/user/test_create.py
@@ -1,7 +1,6 @@
-from argparse import Namespace
import pytest
-from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
+from compiler_admin import RESULT_SUCCESS
from compiler_admin.commands.user.create import create, __name__ as MODULE
from compiler_admin.services.google import USER_HELLO
@@ -21,29 +20,22 @@ def mock_google_add_user_to_group(mock_google_add_user_to_group):
return mock_google_add_user_to_group(MODULE)
-def test_create_user_username_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- create(args)
-
-
-def test_create_user_exists(mock_google_user_exists):
+def test_create_user_exists(cli_runner, mock_google_user_exists):
mock_google_user_exists.return_value = True
- args = Namespace(username="username")
- res = create(args)
+ result = cli_runner.invoke(create, ["username"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
-def test_create_user_does_not_exists(mock_google_user_exists, mock_google_CallGAMCommand, mock_google_add_user_to_group):
+def test_create_user_does_not_exists(
+ cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_google_add_user_to_group
+):
mock_google_user_exists.return_value = False
- args = Namespace(username="username")
- res = create(args)
+ result = cli_runner.invoke(create, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called_once()
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
@@ -53,13 +45,12 @@ def test_create_user_does_not_exists(mock_google_user_exists, mock_google_CallGA
mock_google_add_user_to_group.assert_called_once()
-def test_create_user_notify(mock_google_user_exists, mock_google_CallGAMCommand, mock_google_add_user_to_group):
+def test_create_user_notify(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_google_add_user_to_group):
mock_google_user_exists.return_value = False
- args = Namespace(username="username", notify="notification@example.com")
- res = create(args)
+ result = cli_runner.invoke(create, ["--notify", "notification@example.com", "username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called_once()
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
@@ -70,13 +61,12 @@ def test_create_user_notify(mock_google_user_exists, mock_google_CallGAMCommand,
mock_google_add_user_to_group.assert_called_once()
-def test_create_user_extras(mock_google_user_exists, mock_google_CallGAMCommand, mock_google_add_user_to_group):
+def test_create_user_extras(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_google_add_user_to_group):
mock_google_user_exists.return_value = False
- args = Namespace(username="username")
- res = create(args, "extra1", "extra2")
+ result = cli_runner.invoke(create, ["username", "extra1", "extra2"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called_once()
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
diff --git a/tests/commands/user/test_delete.py b/tests/commands/user/test_delete.py
index 143402a..cbd98ba 100644
--- a/tests/commands/user/test_delete.py
+++ b/tests/commands/user/test_delete.py
@@ -1,7 +1,6 @@
-from argparse import Namespace
import pytest
-from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
+from compiler_admin import RESULT_SUCCESS
from compiler_admin.commands.user.delete import delete, __name__ as MODULE
@@ -29,21 +28,13 @@ def mock_google_CallGAMCommand(mock_google_CallGAMCommand):
return mock_google_CallGAMCommand(MODULE)
-def test_delete_user_username_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- delete(args)
-
-
@pytest.mark.usefixtures("mock_input_yes")
-def test_delete_confirm_yes(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_delete_confirm_yes(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
mock_google_user_exists.return_value = True
- args = Namespace(username="username")
- res = delete(args)
+ result = cli_runner.invoke(delete, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called_once()
call_args = mock_google_CallGAMCommand.call_args.args[0]
assert "delete" in call_args
@@ -52,21 +43,19 @@ def test_delete_confirm_yes(mock_google_user_exists, mock_google_CallGAMCommand)
@pytest.mark.usefixtures("mock_input_no")
-def test_delete_confirm_no(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_delete_confirm_no(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
mock_google_user_exists.return_value = True
- args = Namespace(username="username")
- res = delete(args)
+ result = cli_runner.invoke(delete, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_not_called()
-def test_delete_user_does_not_exist(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_delete_user_does_not_exist(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
mock_google_user_exists.return_value = False
- args = Namespace(username="username")
- res = delete(args)
+ result = cli_runner.invoke(delete, ["username"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
assert mock_google_CallGAMCommand.call_count == 0
diff --git a/tests/commands/user/test_init.py b/tests/commands/user/test_init.py
new file mode 100644
index 0000000..6faabb6
--- /dev/null
+++ b/tests/commands/user/test_init.py
@@ -0,0 +1,8 @@
+import pytest
+
+from compiler_admin.commands.user import user
+
+
+@pytest.mark.parametrize("command", ["alumni", "convert", "create", "delete", "offboard", "reset", "restore", "signout"])
+def test_user_commands(command):
+ assert command in user.commands
diff --git a/tests/commands/user/test_offboard.py b/tests/commands/user/test_offboard.py
index 5fa06b0..5ed54d6 100644
--- a/tests/commands/user/test_offboard.py
+++ b/tests/commands/user/test_offboard.py
@@ -1,7 +1,6 @@
-from argparse import Namespace
import pytest
-from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
+from compiler_admin import RESULT_SUCCESS
from compiler_admin.commands.user.offboard import offboard, __name__ as MODULE
@@ -49,15 +48,9 @@ def mock_google_CallGYBCommand(mock_google_CallGYBCommand):
return mock_google_CallGYBCommand(MODULE)
-def test_offboard_user_username_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- offboard(args)
-
-
@pytest.mark.usefixtures("mock_input_yes")
def test_offboard_confirm_yes(
+ cli_runner,
mock_google_user_exists,
mock_google_CallGAMCommand,
mock_google_CallGYBCommand,
@@ -67,23 +60,20 @@ def test_offboard_confirm_yes(
):
mock_google_user_exists.return_value = True
- args = Namespace(username="username")
- res = offboard(args)
+ result = cli_runner.invoke(offboard, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
assert mock_google_CallGAMCommand.call_count > 0
mock_google_CallGYBCommand.assert_called_once()
mock_NamedTemporaryFile.assert_called_once()
- mock_commands_alumni.assert_called_once()
- assert args in mock_commands_alumni.call_args.args
-
- mock_commands_delete.assert_called_once()
- assert args in mock_commands_delete.call_args.args
+ mock_commands_alumni.callback.assert_called_once()
+ mock_commands_delete.callback.assert_called_once()
@pytest.mark.usefixtures("mock_input_no")
def test_offboard_confirm_no(
+ cli_runner,
mock_google_user_exists,
mock_google_CallGAMCommand,
mock_google_CallGYBCommand,
@@ -92,35 +82,32 @@ def test_offboard_confirm_no(
):
mock_google_user_exists.return_value = True
- args = Namespace(username="username")
- res = offboard(args)
+ result = cli_runner.invoke(offboard, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_not_called()
mock_google_CallGYBCommand.assert_not_called()
- mock_commands_alumni.assert_not_called()
- mock_commands_delete.assert_not_called()
+ mock_commands_alumni.callback.assert_not_called()
+ mock_commands_delete.callback.assert_not_called()
-def test_offboard_user_does_not_exist(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_offboard_user_does_not_exist(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
mock_google_user_exists.return_value = False
- args = Namespace(username="username")
- res = offboard(args)
+ result = cli_runner.invoke(offboard, ["username"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
assert mock_google_CallGAMCommand.call_count == 0
-def test_offboard_alias_user_does_not_exist(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_offboard_alias_user_does_not_exist(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
# the first call returns True (the user exists), the second False (the alias user does not)
# https://stackoverflow.com/a/24897297
mock_google_user_exists.side_effect = [True, False]
- args = Namespace(username="username", alias="alias_username")
- res = offboard(args)
+ result = cli_runner.invoke(offboard, ["--alias", "alias_username", "username"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
assert mock_google_user_exists.call_count == 2
assert mock_google_CallGAMCommand.call_count == 0
diff --git a/tests/commands/user/test_reset.py b/tests/commands/user/test_reset.py
index b6eb98c..b09721e 100644
--- a/tests/commands/user/test_reset.py
+++ b/tests/commands/user/test_reset.py
@@ -1,7 +1,6 @@
-from argparse import Namespace
import pytest
-from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
+from compiler_admin import RESULT_SUCCESS
from compiler_admin.commands.user.reset import reset, __name__ as MODULE
from compiler_admin.services.google import USER_HELLO
@@ -35,74 +34,58 @@ def mock_google_CallGAMCommand(mock_google_CallGAMCommand):
return mock_google_CallGAMCommand(MODULE)
-def test_reset_user_username_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- reset(args)
-
-
-def test_reset_user_does_not_exist(mock_google_user_exists):
+def test_reset_user_does_not_exist(cli_runner, mock_google_user_exists):
mock_google_user_exists.return_value = False
- args = Namespace(username="username")
- res = reset(args)
+ result = cli_runner.invoke(reset, ["username"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
@pytest.mark.usefixtures("mock_input_yes")
-def test_reset_confirm_yes(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
+def test_reset_confirm_yes(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
mock_google_user_exists.return_value = True
- args = Namespace(username="username", force=False)
- res = reset(args)
+ result = cli_runner.invoke(reset, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called_once()
- mock_commands_signout.assert_called_once_with(args)
+ mock_commands_signout.callback.assert_called_once()
@pytest.mark.usefixtures("mock_input_no")
-def test_reset_confirm_no(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
+def test_reset_confirm_no(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
mock_google_user_exists.return_value = True
- args = Namespace(username="username", force=False)
- res = reset(args)
+ result = cli_runner.invoke(reset, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_not_called()
- mock_commands_signout.assert_not_called()
+ mock_commands_signout.callback.assert_not_called()
-def test_reset_user_exists(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
+def test_reset_user_exists(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
mock_google_user_exists.return_value = True
- args = Namespace(username="username", force=True)
- res = reset(args)
-
- assert res == RESULT_SUCCESS
+ result = cli_runner.invoke(reset, ["--force", "username"])
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called_once()
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
assert "update user" in call_args
assert "password random changepassword" in call_args
+ mock_commands_signout.callback.assert_called_once()
- mock_commands_signout.assert_called_once_with(args)
-
-def test_reset_notify(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
+def test_reset_notify(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
mock_google_user_exists.return_value = True
- args = Namespace(username="username", notify="notification@example.com", force=True)
- res = reset(args)
-
- assert res == RESULT_SUCCESS
+ result = cli_runner.invoke(reset, ["--force", "--notify", "notification@example.com", "username"])
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called_once()
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
assert "update user" in call_args
assert "password random changepassword" in call_args
assert f"notify notification@example.com from {USER_HELLO}" in call_args
-
- mock_commands_signout.assert_called_once_with(args)
+ mock_commands_signout.callback.assert_called_once()
diff --git a/tests/commands/user/test_restore.py b/tests/commands/user/test_restore.py
index 05366c4..0b8e38b 100644
--- a/tests/commands/user/test_restore.py
+++ b/tests/commands/user/test_restore.py
@@ -1,7 +1,6 @@
-from argparse import Namespace
import pytest
-from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
+from compiler_admin import RESULT_SUCCESS
from compiler_admin.commands.user.restore import restore, __name__ as MODULE
@@ -15,28 +14,19 @@ def mock_google_CallGYBCommand(mock_google_CallGYBCommand):
return mock_google_CallGYBCommand(MODULE)
-def test_restore_user_username_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- restore(args)
-
-
-def test_restore_backup_exists(mock_Path_exists, mock_google_CallGYBCommand):
+def test_restore_backup_exists(cli_runner, mock_Path_exists, mock_google_CallGYBCommand):
mock_Path_exists.return_value = True
- args = Namespace(username="username")
- res = restore(args)
+ result = cli_runner.invoke(restore, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGYBCommand.assert_called_once()
-def test_restore_backup_does_not_exist(mocker, mock_Path_exists, mock_google_CallGYBCommand):
+def test_restore_backup_does_not_exist(cli_runner, mock_Path_exists, mock_google_CallGYBCommand):
mock_Path_exists.return_value = False
- args = Namespace(username="username")
- res = restore(args)
+ result = cli_runner.invoke(restore, ["username"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
assert mock_google_CallGYBCommand.call_count == 0
diff --git a/tests/commands/user/test_signout.py b/tests/commands/user/test_signout.py
index 472a1dd..078241d 100644
--- a/tests/commands/user/test_signout.py
+++ b/tests/commands/user/test_signout.py
@@ -1,7 +1,6 @@
-from argparse import Namespace
import pytest
-from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
+from compiler_admin import RESULT_SUCCESS
from compiler_admin.commands.user.signout import signout, __name__ as MODULE
@@ -29,42 +28,32 @@ def mock_google_CallGAMCommand(mock_google_CallGAMCommand):
return mock_google_CallGAMCommand(MODULE)
-def test_signout_user_username_required():
- args = Namespace()
-
- with pytest.raises(ValueError, match="username is required"):
- signout(args)
-
-
@pytest.mark.usefixtures("mock_input_yes")
-def test_signout_confirm_yes(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_signout_confirm_yes(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
mock_google_user_exists.return_value = True
- args = Namespace(username="username")
- res = signout(args)
+ result = cli_runner.invoke(signout, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_called_once()
call_args = mock_google_CallGAMCommand.call_args.args[0]
assert "user" in call_args and "signout" in call_args
@pytest.mark.usefixtures("mock_input_no")
-def test_signout_confirm_no(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_signout_confirm_no(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
mock_google_user_exists.return_value = True
- args = Namespace(username="username")
- res = signout(args)
+ result = cli_runner.invoke(signout, ["username"])
- assert res == RESULT_SUCCESS
+ assert result.exit_code == RESULT_SUCCESS
mock_google_CallGAMCommand.assert_not_called()
-def test_signout_user_does_not_exist(mock_google_user_exists, mock_google_CallGAMCommand):
+def test_signout_user_does_not_exist(cli_runner, mock_google_user_exists, mock_google_CallGAMCommand):
mock_google_user_exists.return_value = False
- args = Namespace(username="username")
- res = signout(args)
+ result = cli_runner.invoke(signout, ["username"])
- assert res == RESULT_FAILURE
+ assert result.exit_code != RESULT_SUCCESS
mock_google_CallGAMCommand.assert_not_called()
diff --git a/tests/conftest.py b/tests/conftest.py
index 0625742..2457a35 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,6 @@
+import click
+from click.testing import CliRunner
+
import pytest
from pytest_socket import disable_socket
@@ -33,76 +36,44 @@ def mock_input(mock_module_name):
return mock_module_name("input")
-@pytest.fixture
-def mock_commands_alumni(mock_module_name):
- """Fixture returns a function that patches the alumni function in a given module."""
- return mock_module_name("alumni")
+@click.command
+def dummy_command(**kwargs):
+ return RESULT_SUCCESS
@pytest.fixture
-def mock_commands_create(mock_module_name):
- """Fixture returns a function that patches the create function in a given module."""
- return mock_module_name("create")
+def mock_command(mocker):
+ def _mock_command(command):
+ def __mock_command(module):
+ return mocker.patch(f"{module}.{command}", new=mocker.MagicMock(spec=dummy_command))
+ return __mock_command
-@pytest.fixture
-def mock_commands_convert(mock_module_name):
- """Fixture returns a function that patches the convert command function in a given module."""
- return mock_module_name("convert")
+ return _mock_command
@pytest.fixture
-def mock_commands_delete(mock_module_name):
- """Fixture returns a function that patches the delete command function in a given module."""
- return mock_module_name("delete")
-
-
-@pytest.fixture
-def mock_commands_info(mock_module_name):
- """Fixture returns a function that patches the info command function in a given module."""
- return mock_module_name("info")
-
-
-@pytest.fixture
-def mock_commands_init(mock_module_name):
- """Fixture returns a function that patches the init command function in a given module."""
- return mock_module_name("init")
+def mock_commands_alumni(mock_command):
+ """Fixture returns a function that patches the alumni function in a given module."""
+ return mock_command("alumni")
@pytest.fixture
-def mock_commands_offboard(mock_module_name):
- """Fixture returns a function that patches the offboard command function in a given module."""
- return mock_module_name("offboard")
+def mock_commands_delete(mock_command):
+ """Fixture returns a function that patches the delete command function in a given module."""
+ return mock_command("delete")
@pytest.fixture
-def mock_commands_reset(mock_module_name):
+def mock_commands_reset(mock_command):
"""Fixture returns a function that patches the reset command function in a given module."""
- return mock_module_name("reset")
+ return mock_command("reset")
@pytest.fixture
-def mock_commands_restore(mock_module_name):
- """Fixture returns a function that patches the restore command function in a given module."""
- return mock_module_name("restore")
-
-
-@pytest.fixture
-def mock_commands_signout(mock_module_name):
+def mock_commands_signout(mock_command):
"""Fixture returns a function that patches the signout command function in a given module."""
- return mock_module_name("signout")
-
-
-@pytest.fixture
-def mock_commands_time(mock_module_name):
- """Fixture returns a function that patches the time command function in a given module."""
- return mock_module_name("time")
-
-
-@pytest.fixture
-def mock_commands_user(mock_module_name):
- """Fixture returns a function that patches the user command function in a given module."""
- return mock_module_name("user")
+ return mock_command("signout")
@pytest.fixture
@@ -186,6 +157,11 @@ def _mock_NamedTemporaryFile(module, readlines=[""], **kwargs):
return _mock_NamedTemporaryFile
+@pytest.fixture
+def cli_runner():
+ return CliRunner()
+
+
@pytest.fixture
def harvest_file():
return "notebooks/data/harvest-sample.csv"
diff --git a/tests/test_main.py b/tests/test_main.py
index fcec85f..9830eee 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,644 +1,8 @@
-from argparse import Namespace
-from datetime import datetime
-import subprocess
-import sys
-
import pytest
-import compiler_admin.main
-from compiler_admin.main import main, prior_month_start, prior_month_end, TZINFO, __name__ as MODULE
-from compiler_admin.services.google import DOMAIN
-
-
-@pytest.fixture(autouse=True)
-def reset_env(monkeypatch):
- monkeypatch.delenv("HARVEST_DATA", raising=False)
- monkeypatch.delenv("TOGGL_DATA", raising=False)
-
-
-@pytest.fixture
-def mock_local_now(mocker):
- dt = datetime(2024, 9, 25, tzinfo=TZINFO)
- mocker.patch(f"{MODULE}.local_now", return_value=dt)
- return dt
-
-
-@pytest.fixture
-def mock_start(mock_local_now):
- return datetime(2024, 8, 1, tzinfo=TZINFO)
-
-
-@pytest.fixture
-def mock_end(mock_local_now):
- return datetime(2024, 8, 31, tzinfo=TZINFO)
-
-
-@pytest.fixture
-def mock_commands_info(mock_commands_info):
- return mock_commands_info(MODULE)
-
-
-@pytest.fixture
-def mock_commands_init(mock_commands_init):
- return mock_commands_init(MODULE)
-
-
-@pytest.fixture
-def mock_commands_time(mock_commands_time):
- return mock_commands_time(MODULE)
-
-
-@pytest.fixture
-def mock_commands_user(mock_commands_user):
- return mock_commands_user(MODULE)
-
-
-def test_prior_month_start(mock_start):
- start = prior_month_start()
-
- assert start == mock_start
-
-
-def test_prior_month_end(mock_end):
- end = prior_month_end()
-
- assert end == mock_end
-
-
-def test_main_info(mock_commands_info):
- main(argv=["info"])
-
- mock_commands_info.assert_called_once()
-
-
-def test_main_info_default(mock_commands_info):
- main(argv=[])
-
- mock_commands_info.assert_called_once()
-
-
-def test_main_init_default(mock_commands_init):
- main(argv=["init", "username"])
-
- mock_commands_init.assert_called_once()
- call_args = mock_commands_init.call_args.args
- assert Namespace(func=mock_commands_init, command="init", username="username", gam=False, gyb=False) in call_args
-
-
-def test_main_init_gam(mock_commands_init):
- main(argv=["init", "username", "--gam"])
-
- mock_commands_init.assert_called_once()
- call_args = mock_commands_init.call_args.args
- assert Namespace(func=mock_commands_init, command="init", username="username", gam=True, gyb=False) in call_args
-
-
-def test_main_init_gyb(mock_commands_init):
- main(argv=["init", "username", "--gyb"])
-
- mock_commands_init.assert_called_once()
- call_args = mock_commands_init.call_args.args
- assert Namespace(func=mock_commands_init, command="init", username="username", gam=False, gyb=True) in call_args
-
-
-def test_main_init_no_username(mock_commands_init):
- with pytest.raises(SystemExit):
- main(argv=["init"])
- assert mock_commands_init.call_count == 0
-
-
-def test_main_time_convert_default(mock_commands_time):
- main(argv=["time", "convert"])
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="convert",
- client=None,
- input=sys.stdin,
- output=sys.stdout,
- from_fmt="toggl",
- to_fmt="harvest",
- )
- in call_args
- )
-
-
-def test_main_time_convert_env(monkeypatch, mock_commands_time):
- monkeypatch.setenv("HARVEST_DATA", "harvest")
- monkeypatch.setenv("TOGGL_DATA", "toggl")
-
- main(argv=["time", "convert"])
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="convert",
- client=None,
- input="toggl",
- output="harvest",
- from_fmt="toggl",
- to_fmt="harvest",
- )
- in call_args
- )
-
-
-@pytest.mark.usefixtures("mock_local_now")
-def test_main_time_download_default(mock_commands_time, mock_start, mock_end):
- main(argv=["time", "download"])
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="download",
- start=mock_start,
- end=mock_end,
- output=sys.stdout,
- billable=True,
- client_ids=None,
- project_ids=None,
- task_ids=None,
- user_ids=None,
- )
- in call_args
- )
-
-
-def test_main_time_download_args(mock_commands_time):
- main(
- argv=[
- "time",
- "download",
- "--start",
- "2024-01-01",
- "--end",
- "2024-01-31",
- "--output",
- "file.csv",
- "--all",
- "--client",
- "1",
- "--client",
- "2",
- "--client",
- "3",
- "--project",
- "1",
- "--project",
- "2",
- "--project",
- "3",
- "--task",
- "1",
- "--task",
- "2",
- "--task",
- "3",
- "--user",
- "1",
- "--user",
- "2",
- "--user",
- "3",
- ]
- )
-
- expected_start = TZINFO.localize(datetime(2024, 1, 1))
- expected_end = TZINFO.localize(datetime(2024, 1, 31))
- ids = [1, 2, 3]
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="download",
- start=expected_start,
- end=expected_end,
- output="file.csv",
- billable=False,
- client_ids=ids,
- project_ids=ids,
- task_ids=ids,
- user_ids=ids,
- )
- in call_args
- )
-
-
-def test_main_time_convert_client(mock_commands_time):
- main(argv=["time", "convert", "--client", "client123"])
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="convert",
- client="client123",
- input=sys.stdin,
- output=sys.stdout,
- from_fmt="toggl",
- to_fmt="harvest",
- )
- in call_args
- )
-
-
-def test_main_time_convert_input(mock_commands_time):
- main(argv=["time", "convert", "--input", "file.csv"])
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="convert",
- client=None,
- input="file.csv",
- output=sys.stdout,
- from_fmt="toggl",
- to_fmt="harvest",
- )
- in call_args
- )
-
-
-def test_main_time_convert_output(mock_commands_time):
- main(argv=["time", "convert", "--output", "file.csv"])
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="convert",
- client=None,
- input=sys.stdin,
- output="file.csv",
- from_fmt="toggl",
- to_fmt="harvest",
- )
- in call_args
- )
-
-
-def test_main_time_convert_from(mock_commands_time):
- main(argv=["time", "convert", "--from", "harvest"])
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="convert",
- client=None,
- input=sys.stdin,
- output=sys.stdout,
- from_fmt="harvest",
- to_fmt="harvest",
- )
- in call_args
- )
-
- with pytest.raises(SystemExit):
- main(argv=["time", "convert", "--from", "nope"])
- # it should not have been called an additional time from the first
- mock_commands_time.assert_called_once()
-
-
-def test_main_time_convert_to(mock_commands_time):
- main(argv=["time", "convert", "--to", "toggl"])
-
- mock_commands_time.assert_called_once()
- call_args = mock_commands_time.call_args.args
- assert (
- Namespace(
- func=mock_commands_time,
- command="time",
- subcommand="convert",
- client=None,
- input=sys.stdin,
- output=sys.stdout,
- from_fmt="toggl",
- to_fmt="toggl",
- )
- in call_args
- )
-
- with pytest.raises(SystemExit):
- main(argv=["time", "convert", "--to", "nope"])
- # it should not have been called an additional time from the first
- mock_commands_time.assert_called_once()
-
-
-def test_main_user_alumni(mock_commands_user):
- main(argv=["user", "alumni", "username"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="alumni", username="username", force=False, notify=None)
- in call_args
- )
-
-
-def test_main_user_alumni_notify(mock_commands_user):
- main(argv=["user", "alumni", "username", "--notify", "notification"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(
- func=mock_commands_user,
- command="user",
- subcommand="alumni",
- username="username",
- force=False,
- notify="notification",
- )
- in call_args
- )
-
-
-def test_main_user_alumni_force(mock_commands_user):
- main(argv=["user", "alumni", "username", "--force"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(
- func=mock_commands_user,
- command="user",
- subcommand="alumni",
- username="username",
- force=True,
- notify=None,
- )
- in call_args
- )
-
-
-def test_main_user_create(mock_commands_user):
- main(argv=["user", "create", "username"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="create", username="username", notify=None) in call_args
- )
-
-
-def test_main_user_create_notify(mock_commands_user):
- main(argv=["user", "create", "username", "--notify", "notification"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="create", username="username", notify="notification")
- in call_args
- )
-
-
-def test_main_user_create_extras(mock_commands_user):
- main(argv=["user", "create", "username", "extra1", "extra2"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="create", username="username", notify=None) in call_args
- )
- assert "extra1" in call_args
- assert "extra2" in call_args
-
-
-def test_main_user_create_no_username(mock_commands_user):
- with pytest.raises(SystemExit):
- main(argv=["user", "create"])
- assert mock_commands_user.call_count == 0
-
-
-def test_main_user_convert(mock_commands_user):
- main(argv=["user", "convert", "username", "contractor"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(
- func=mock_commands_user,
- command="user",
- subcommand="convert",
- username="username",
- account_type="contractor",
- force=False,
- notify=None,
- )
- in call_args
- )
-
-
-def test_main_user_convert_no_username(mock_commands_user):
- with pytest.raises(SystemExit):
- main(argv=["user", "convert"])
- assert mock_commands_user.call_count == 0
-
-
-def test_main_user_convert_bad_account_type(mock_commands_user):
- with pytest.raises(SystemExit):
- main(argv=["user", "convert", "username", "account_type"])
- assert mock_commands_user.call_count == 0
-
-
-def test_main_user_delete(mock_commands_user):
- main(argv=["user", "delete", "username"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="delete", username="username", force=False) in call_args
- )
-
-
-def test_main_user_delete_force(mock_commands_user):
- main(argv=["user", "delete", "username", "--force"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="delete", username="username", force=True) in call_args
- )
-
-
-def test_main_user_delete_no_username(mock_commands_user):
- with pytest.raises(SystemExit):
- main(argv=["user", "delete"])
- assert mock_commands_user.call_count == 0
-
-
-def test_main_user_offboard(mock_commands_user):
- main(argv=["user", "offboard", "username"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="offboard", username="username", alias=None, force=False)
- in call_args
- )
-
-
-def test_main_user_offboard_force(mock_commands_user):
- main(argv=["user", "offboard", "username", "--force"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="offboard", username="username", alias=None, force=True)
- in call_args
- )
-
-
-def test_main_user_offboard_with_alias(mock_commands_user):
- main(argv=["user", "offboard", "username", "--alias", "anotheruser"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(
- func=mock_commands_user,
- command="user",
- subcommand="offboard",
- username="username",
- alias="anotheruser",
- force=False,
- )
- in call_args
- )
-
-
-def test_main_user_offboard_no_username(mock_commands_user):
- with pytest.raises(SystemExit):
- main(argv=["user", "offboard"])
- assert mock_commands_user.call_count == 0
-
-
-def test_main_user_reset(mock_commands_user):
- main(argv=["user", "reset", "username"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="reset", username="username", force=False, notify=None)
- in call_args
- )
-
-
-def test_main_user_reset_notify(mock_commands_user):
- main(argv=["user", "reset", "username", "--notify", "notification"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(
- func=mock_commands_user,
- command="user",
- subcommand="reset",
- username="username",
- notify="notification",
- force=False,
- )
- in call_args
- )
-
-
-def test_main_user_reset_force(mock_commands_user):
- main(argv=["user", "reset", "username", "--force"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="reset", username="username", force=True, notify=None)
- in call_args
- )
-
-
-def test_main_user_reset_no_username(mock_commands_user):
- with pytest.raises(SystemExit):
- main(argv=["user", "reset"])
- assert mock_commands_user.call_count == 0
-
-
-def test_main_user_restore(mock_commands_user):
- main(argv=["user", "restore", "username"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert Namespace(func=mock_commands_user, command="user", subcommand="restore", username="username") in call_args
-
-
-def test_main_user_restore_no_username(mock_commands_user):
- with pytest.raises(SystemExit):
- main(argv=["user", "restore"])
- assert mock_commands_user.call_count == 0
-
-
-def test_main_user_signout(mock_commands_user):
- main(argv=["user", "signout", "username"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="signout", username="username", force=False) in call_args
- )
-
-
-def test_main_user_signout_force(mock_commands_user):
- main(argv=["user", "signout", "username", "--force"])
-
- mock_commands_user.assert_called_once()
- call_args = mock_commands_user.call_args.args
- assert (
- Namespace(func=mock_commands_user, command="user", subcommand="signout", username="username", force=True) in call_args
- )
-
-
-def test_main_user_signout_no_username(mock_commands_user):
- with pytest.raises(SystemExit):
- main(argv=["user", "signout"])
- assert mock_commands_user.call_count == 0
-
-
-@pytest.mark.e2e
-def test_main_e2e(mocker):
- spy_info = mocker.spy(compiler_admin.main, "info")
- res = main(argv=[])
-
- assert res == 0
- spy_info.assert_called_once()
-
+from compiler_admin.main import main
-@pytest.mark.e2e
-def test_run_compiler_admin(capfd):
- # call CLI command as a subprocess
- res = subprocess.call(["compiler-admin"])
- captured = capfd.readouterr()
- assert res == 0
- assert "compiler-admin:" in captured.out
- assert "GAMADV-XTD3" in captured.out
- assert f"Primary Domain: {DOMAIN}" in captured.out
- assert "WARNING: Config File:" not in captured.err
+@pytest.mark.parametrize("command", ["info", "init", "time", "user"])
+def test_main_commands(command):
+ assert command in main.commands