Skip to content

Commit d898180

Browse files
committed
refactor(time): describe as @click.group of commands
1 parent 4b56443 commit d898180

File tree

7 files changed

+283
-91
lines changed

7 files changed

+283
-91
lines changed
Lines changed: 12 additions & 12 deletions
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

Lines changed: 46 additions & 6 deletions
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)
Lines changed: 117 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,124 @@
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+
multiple=True,
60+
help="An ID for a Toggl Client to filter for in reports. Can be supplied more than once.",
61+
metavar="CLIENT_ID",
62+
type=int,
63+
)
64+
@click.option(
65+
"-p",
66+
"--project",
67+
"project_ids",
68+
help="An ID for a Toggl Project to filter for in reports. Can be supplied more than once.",
69+
metavar="PROJECT_ID",
70+
multiple=True,
71+
type=int,
72+
)
73+
@click.option(
74+
"-t",
75+
"--task",
76+
"task_ids",
77+
help="An ID for a Toggl Project Task to filter for in reports. Can be supplied more than once.",
78+
metavar="TASK_ID",
79+
multiple=True,
80+
type=int,
81+
)
82+
@click.option(
83+
"-u",
84+
"--user",
85+
"user_ids",
86+
help="An ID for a Toggl User to filter for in reports. Can be supplied more than once.",
87+
metavar="USER_ID",
88+
multiple=True,
89+
type=int,
90+
)
91+
def download(
92+
start: datetime,
93+
end: datetime,
94+
output: str = "",
95+
billable: bool = True,
96+
client_ids: List[int] = [],
97+
project_ids: List[int] = [],
98+
task_ids: List[int] = [],
99+
user_ids: List[int] = [],
100+
):
101+
"""
102+
Download a Toggl time report in CSV format.
103+
"""
104+
if not output:
105+
output = f"Toggl_time_entries_{start.strftime('%Y-%m-%d')}_{end.strftime('%Y-%m-%d')}.csv"
106+
107+
params = dict(start_date=start, end_date=end, output_path=output, output_cols=TOGGL_COLUMNS)
108+
109+
if billable:
110+
params.update(dict(billable=billable))
111+
if client_ids:
112+
params.update(dict(client_ids=client_ids))
113+
if project_ids:
114+
params.update(dict(project_ids=project_ids))
115+
if task_ids:
116+
params.update(dict(task_ids=task_ids))
117+
if user_ids:
118+
params.update(dict(user_ids=user_ids))
119+
120+
click.echo("Downloading Toggl time entries with parameters:")
121+
for k, v in params.items():
122+
click.echo(f" {k}: {v}")
123+
124+
download_time_entries(**params)

tests/commands/time/test__init__.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

tests/commands/time/test_convert.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from argparse import Namespace
21
import pytest
32

43
from compiler_admin import RESULT_SUCCESS
@@ -40,14 +39,15 @@ def test_get_source_converter_mismatch():
4039
_get_source_converter("toggl", "nope")
4140

4241

43-
def test_convert(mock_get_source_converter):
44-
args = Namespace(input="input", output="output", client="client", from_fmt="from", to_fmt="to")
45-
res = convert(args)
42+
def test_convert(cli_runner, mock_get_source_converter):
43+
result = cli_runner.invoke(
44+
convert, ["--input", "input", "--output", "output", "--client", "client", "--from", "harvest", "--to", "toggl"]
45+
)
4646

47-
assert res == RESULT_SUCCESS
48-
mock_get_source_converter.assert_called_once_with(args.from_fmt, args.to_fmt)
47+
assert result.exit_code == RESULT_SUCCESS
48+
mock_get_source_converter.assert_called_once_with("harvest", "toggl")
4949
mock_get_source_converter.return_value.assert_called_once_with(
50-
source_path=args.input, output_path=args.output, client_name=args.client
50+
source_path="input", output_path="output", client_name="client"
5151
)
5252

5353

0 commit comments

Comments
 (0)