Skip to content

Commit 024b5b4

Browse files
authored
Refactor: define CLI with click (#38)
2 parents 457a32b + ace71ba commit 024b5b4

36 files changed

+782
-1657
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ default_install_hook_types:
88

99
repos:
1010
- repo: https://github.com/compilerla/conventional-pre-commit
11-
rev: v3.6.0
11+
rev: v4.0.0
1212
hooks:
1313
- id: conventional-pre-commit
1414
stages: [commit-msg]

compiler_admin/commands/info.py

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
from compiler_admin import __version__ as version, RESULT_SUCCESS, RESULT_FAILURE
2-
from compiler_admin.services.google import CallGAMCommand, CallGYBCommand
1+
import click
32

3+
from compiler_admin import __version__ as version
4+
from compiler_admin.services.google import CallGAMCommand, CallGYBCommand
45

5-
def info(*args, **kwargs) -> int:
6-
"""Print information about this package and the GAM environment.
76

8-
Returns:
9-
A value indicating if the operation succeeded or failed.
7+
@click.command()
8+
def info():
109
"""
11-
print(f"compiler-admin: {version}")
12-
13-
res = CallGAMCommand(("version",))
14-
res += CallGAMCommand(("info", "domain"))
15-
res += CallGYBCommand(("--version",))
10+
Print information about the configured environment.
11+
"""
12+
click.echo(f"compiler-admin, version {version}")
1613

17-
return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
14+
CallGAMCommand(("version",))
15+
CallGAMCommand(("info", "domain"))
16+
CallGYBCommand(("--version",))

compiler_admin/commands/init.py

+20-31
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from argparse import Namespace
21
import os
32
from pathlib import Path
43
from shutil import rmtree
54
import subprocess
65

7-
from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS
6+
import click
7+
88
from compiler_admin.services.google import USER_ARCHIVE, CallGAMCommand
99

1010

@@ -22,48 +22,37 @@ def _clean_config_dir(config_dir: Path) -> None:
2222
rmtree(path)
2323

2424

25-
def init(args: Namespace, *extras) -> int:
26-
"""Initialize a new GAM project.
27-
28-
See https://github.com/taers232c/GAMADV-XTD3/wiki/How-to-Install-Advanced-GAM
29-
30-
Args:
31-
username (str): The Compiler admin with which to initialize a new project.
32-
33-
gam (bool): If True, initialize a new GAM project.
25+
@click.command()
26+
@click.option("--gam", "init_gam", is_flag=True)
27+
@click.option("--gyb", "init_gyb", is_flag=True)
28+
@click.argument("username")
29+
def init(username: str, init_gam: bool = False, init_gyb: bool = False):
30+
"""Initialize a new GAM and/or GYB project.
3431
35-
gyb (bool): If True, initialize a new GYB project.
32+
See:
3633
37-
Returns:
38-
A value indicating if the operation succeeded or failed.
34+
- https://github.com/taers232c/GAMADV-XTD3/wiki/How-to-Install-Advanced-GAM
35+
- https://github.com/GAM-team/got-your-back/wiki
3936
"""
40-
if not hasattr(args, "username"):
41-
raise ValueError("username is required")
42-
43-
admin_user = args.username
44-
res = RESULT_SUCCESS
45-
46-
if getattr(args, "gam", False):
37+
if init_gam:
4738
_clean_config_dir(GAM_CONFIG_PATH)
4839
# GAM is already installed via pyproject.toml
49-
res += CallGAMCommand(("config", "drive_dir", str(GAM_CONFIG_PATH), "verify"))
50-
res += CallGAMCommand(("create", "project"))
51-
res += CallGAMCommand(("oauth", "create"))
52-
res += CallGAMCommand(("user", admin_user, "check", "serviceaccount"))
40+
CallGAMCommand(("config", "drive_dir", str(GAM_CONFIG_PATH), "verify"))
41+
CallGAMCommand(("create", "project"))
42+
CallGAMCommand(("oauth", "create"))
43+
CallGAMCommand(("user", username, "check", "serviceaccount"))
5344

54-
if getattr(args, "gyb", False):
45+
if init_gyb:
5546
_clean_config_dir(GYB_CONFIG_PATH)
5647
# download GYB installer to config directory
5748
gyb = GYB_CONFIG_PATH / "gyb-install.sh"
5849
with gyb.open("w+") as dest:
59-
res += subprocess.call(("curl", "-s", "-S", "-L", "https://gyb-shortn.jaylee.us/gyb-install"), stdout=dest)
50+
subprocess.call(("curl", "-s", "-S", "-L", "https://gyb-shortn.jaylee.us/gyb-install"), stdout=dest)
6051

61-
res += subprocess.call(("chmod", "+x", str(gyb.absolute())))
52+
subprocess.call(("chmod", "+x", str(gyb.absolute())))
6253

6354
# install, giving values to some options
6455
# https://github.com/GAM-team/got-your-back/blob/main/install-gyb.sh
6556
#
6657
# use GYB_CONFIG_PATH.parent for the install directory option, otherwise we get a .config/gyb/gyb directory structure
67-
res += subprocess.call((gyb, "-u", admin_user, "-r", USER_ARCHIVE, "-d", str(GYB_CONFIG_PATH.parent)))
68-
69-
return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE
58+
subprocess.call((gyb, "-u", username, "-r", USER_ARCHIVE, "-d", str(GYB_CONFIG_PATH.parent)))
+12-12
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
from argparse import Namespace
1+
import click
22

3-
from compiler_admin.commands.time.convert import convert # noqa: F401
4-
from compiler_admin.commands.time.download import download # noqa: F401
3+
from compiler_admin.commands.time.convert import convert
4+
from compiler_admin.commands.time.download import download
55

66

7-
def time(args: Namespace, *extra):
8-
# try to call the subcommand function directly from global (module) symbols
9-
# if the subcommand function was imported above, it should exist in globals()
10-
global_env = globals()
7+
@click.group
8+
def time():
9+
"""
10+
Work with Compiler time entries.
11+
"""
12+
pass
1113

12-
if args.subcommand in global_env:
13-
cmd_func = global_env[args.subcommand]
14-
cmd_func(args, *extra)
15-
else:
16-
raise NotImplementedError(f"Unknown time subcommand: {args.subcommand}")
14+
15+
time.add_command(convert)
16+
time.add_command(download)

compiler_admin/commands/time/convert.py

+46-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
from argparse import Namespace
1+
import os
2+
import sys
3+
from typing import TextIO
4+
5+
import click
26

3-
from compiler_admin import RESULT_SUCCESS
47
from compiler_admin.services.harvest import CONVERTERS as HARVEST_CONVERTERS
58
from compiler_admin.services.toggl import CONVERTERS as TOGGL_CONVERTERS
69

@@ -21,9 +24,46 @@ def _get_source_converter(from_fmt: str, to_fmt: str):
2124
)
2225

2326

24-
def convert(args: Namespace, *extras):
25-
converter = _get_source_converter(args.from_fmt, args.to_fmt)
27+
@click.command()
28+
@click.option(
29+
"--input",
30+
default=os.environ.get("TOGGL_DATA", sys.stdin),
31+
help="The path to the source data for conversion. Defaults to $TOGGL_DATA or stdin.",
32+
)
33+
@click.option(
34+
"--output",
35+
default=os.environ.get("HARVEST_DATA", sys.stdout),
36+
help="The path to the file where converted data should be written. Defaults to $HARVEST_DATA or stdout.",
37+
)
38+
@click.option(
39+
"--from",
40+
"from_fmt",
41+
default="toggl",
42+
help="The format of the source data.",
43+
show_default=True,
44+
type=click.Choice(sorted(CONVERTERS.keys()), case_sensitive=False),
45+
)
46+
@click.option(
47+
"--to",
48+
"to_fmt",
49+
default="harvest",
50+
help="The format of the converted data.",
51+
show_default=True,
52+
type=click.Choice(sorted([to_fmt for sub in CONVERTERS.values() for to_fmt in sub.keys()]), case_sensitive=False),
53+
)
54+
@click.option("--client", help="The name of the client to use in converted data.")
55+
def convert(
56+
input: str | TextIO = os.environ.get("TOGGL_DATA", sys.stdin),
57+
output: str | TextIO = os.environ.get("HARVEST_DATA", sys.stdout),
58+
from_fmt="toggl",
59+
to_fmt="harvest",
60+
client="",
61+
):
62+
"""
63+
Convert a time report from one format into another.
64+
"""
65+
converter = _get_source_converter(from_fmt, to_fmt)
2666

27-
converter(source_path=args.input, output_path=args.output, client_name=args.client)
67+
click.echo(f"Converting data from format: {from_fmt} to format: {to_fmt}")
2868

29-
return RESULT_SUCCESS
69+
converter(source_path=input, output_path=output, client_name=client)
+118-16
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,125 @@
1-
from argparse import Namespace
1+
from datetime import datetime, timedelta
2+
import os
3+
from typing import List
4+
5+
import click
6+
from pytz import timezone
27

3-
from compiler_admin import RESULT_SUCCESS
48
from compiler_admin.services.toggl import TOGGL_COLUMNS, download_time_entries
59

610

7-
def download(args: Namespace, *extras):
8-
params = dict(
9-
start_date=args.start, end_date=args.end, output_path=args.output, output_cols=TOGGL_COLUMNS, billable=args.billable
10-
)
11+
TZINFO = timezone(os.environ.get("TZ_NAME", "America/Los_Angeles"))
1112

12-
if args.client_ids:
13-
params.update(dict(client_ids=args.client_ids))
14-
if args.project_ids:
15-
params.update(dict(project_ids=args.project_ids))
16-
if args.task_ids:
17-
params.update(dict(task_ids=args.task_ids))
18-
if args.user_ids:
19-
params.update(dict(user_ids=args.user_ids))
2013

21-
download_time_entries(**params)
14+
def local_now():
15+
return datetime.now(tz=TZINFO)
16+
17+
18+
def prior_month_end():
19+
now = local_now()
20+
first = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
21+
return first - timedelta(days=1)
22+
23+
24+
def prior_month_start():
25+
end = prior_month_end()
26+
return end.replace(day=1)
2227

23-
return RESULT_SUCCESS
28+
29+
@click.command()
30+
@click.option(
31+
"--start",
32+
metavar="YYYY-MM-DD",
33+
default=prior_month_start(),
34+
callback=lambda ctx, param, val: datetime.strptime(val, "%Y-%m-%d %H:%M:%S%z"),
35+
help="The start date of the reporting period. Defaults to the beginning of the prior month.",
36+
)
37+
@click.option(
38+
"--end",
39+
metavar="YYYY-MM-DD",
40+
default=prior_month_end(),
41+
callback=lambda ctx, param, val: datetime.strptime(val, "%Y-%m-%d %H:%M:%S%z"),
42+
help="The end date of the reporting period. Defaults to the end of the prior month.",
43+
)
44+
@click.option(
45+
"--output",
46+
help="The path to the file where downloaded data should be written. Defaults to a path calculated from the date range.",
47+
)
48+
@click.option(
49+
"--all",
50+
"billable",
51+
is_flag=True,
52+
default=True,
53+
help="Download all time entries. The default is to download only billable time entries.",
54+
)
55+
@click.option(
56+
"-c",
57+
"--client",
58+
"client_ids",
59+
envvar="TOGGL_CLIENT_ID",
60+
help="An ID for a Toggl Client to filter for in reports. Can be supplied more than once.",
61+
metavar="CLIENT_ID",
62+
multiple=True,
63+
type=int,
64+
)
65+
@click.option(
66+
"-p",
67+
"--project",
68+
"project_ids",
69+
help="An ID for a Toggl Project to filter for in reports. Can be supplied more than once.",
70+
metavar="PROJECT_ID",
71+
multiple=True,
72+
type=int,
73+
)
74+
@click.option(
75+
"-t",
76+
"--task",
77+
"task_ids",
78+
help="An ID for a Toggl Project Task to filter for in reports. Can be supplied more than once.",
79+
metavar="TASK_ID",
80+
multiple=True,
81+
type=int,
82+
)
83+
@click.option(
84+
"-u",
85+
"--user",
86+
"user_ids",
87+
help="An ID for a Toggl User to filter for in reports. Can be supplied more than once.",
88+
metavar="USER_ID",
89+
multiple=True,
90+
type=int,
91+
)
92+
def download(
93+
start: datetime,
94+
end: datetime,
95+
output: str = "",
96+
billable: bool = True,
97+
client_ids: List[int] = [],
98+
project_ids: List[int] = [],
99+
task_ids: List[int] = [],
100+
user_ids: List[int] = [],
101+
):
102+
"""
103+
Download a Toggl time report in CSV format.
104+
"""
105+
if not output:
106+
output = f"Toggl_time_entries_{start.strftime('%Y-%m-%d')}_{end.strftime('%Y-%m-%d')}.csv"
107+
108+
params = dict(start_date=start, end_date=end, output_path=output, output_cols=TOGGL_COLUMNS)
109+
110+
if billable:
111+
params.update(dict(billable=billable))
112+
if client_ids:
113+
params.update(dict(client_ids=client_ids))
114+
if project_ids:
115+
params.update(dict(project_ids=project_ids))
116+
if task_ids:
117+
params.update(dict(task_ids=task_ids))
118+
if user_ids:
119+
params.update(dict(user_ids=user_ids))
120+
121+
click.echo("Downloading Toggl time entries with parameters:")
122+
for k, v in params.items():
123+
click.echo(f" {k}: {v}")
124+
125+
download_time_entries(**params)
+24-18
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
1-
from argparse import Namespace
1+
import click
22

3-
from compiler_admin.commands.user.alumni import alumni # noqa: F401
4-
from compiler_admin.commands.user.create import create # noqa: F401
5-
from compiler_admin.commands.user.convert import convert # noqa: F401
6-
from compiler_admin.commands.user.delete import delete # noqa: F401
7-
from compiler_admin.commands.user.offboard import offboard # noqa: F401
8-
from compiler_admin.commands.user.reset import reset # noqa: F401
9-
from compiler_admin.commands.user.restore import restore # noqa: F401
10-
from compiler_admin.commands.user.signout import signout # noqa: F401
3+
from compiler_admin.commands.user.alumni import alumni
4+
from compiler_admin.commands.user.convert import convert
5+
from compiler_admin.commands.user.create import create
6+
from compiler_admin.commands.user.delete import delete
7+
from compiler_admin.commands.user.offboard import offboard
8+
from compiler_admin.commands.user.reset import reset
9+
from compiler_admin.commands.user.restore import restore
10+
from compiler_admin.commands.user.signout import signout
1111

1212

13-
def user(args: Namespace, *extra):
14-
# try to call the subcommand function directly from global (module) symbols
15-
# if the subcommand function was imported above, it should exist in globals()
16-
global_env = globals()
13+
@click.group
14+
def user():
15+
"""
16+
Work with users in the Compiler org.
17+
"""
18+
pass
1719

18-
if args.subcommand in global_env:
19-
cmd_func = global_env[args.subcommand]
20-
cmd_func(args, *extra)
21-
else:
22-
raise NotImplementedError(f"Unknown user subcommand: {args.subcommand}")
20+
21+
user.add_command(alumni)
22+
user.add_command(convert)
23+
user.add_command(create)
24+
user.add_command(delete)
25+
user.add_command(offboard)
26+
user.add_command(reset)
27+
user.add_command(restore)
28+
user.add_command(signout)

0 commit comments

Comments
 (0)