Skip to content

Commit 50dcc6b

Browse files
committed
refactor(convert): explicit from/to format args
allows for more than just harvest<>toggl conversions
1 parent c1cbe65 commit 50dcc6b

File tree

8 files changed

+155
-39
lines changed

8 files changed

+155
-39
lines changed

compiler_admin/commands/time/convert.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
from argparse import Namespace
22

3-
import pandas as pd
4-
53
from compiler_admin import RESULT_SUCCESS
6-
from compiler_admin.services.harvest import HARVEST_COLUMNS, convert_to_toggl
7-
from compiler_admin.services.toggl import TOGGL_COLUMNS, convert_to_harvest
4+
from compiler_admin.services.harvest import CONVERTERS as HARVEST_CONVERTERS
5+
from compiler_admin.services.toggl import CONVERTERS as TOGGL_CONVERTERS
6+
7+
8+
CONVERTERS = {"harvest": HARVEST_CONVERTERS, "toggl": TOGGL_CONVERTERS}
89

910

10-
def _get_source_converter(source):
11-
columns = pd.read_csv(source, nrows=0).columns.tolist()
11+
def _get_source_converter(from_fmt: str, to_fmt: str):
12+
from_fmt = from_fmt.lower().strip() if from_fmt else ""
13+
to_fmt = to_fmt.lower().strip() if to_fmt else ""
14+
converter = CONVERTERS.get(from_fmt, {}).get(to_fmt)
1215

13-
if set(HARVEST_COLUMNS) <= set(columns):
14-
return convert_to_toggl
15-
elif set(TOGGL_COLUMNS) <= set(columns):
16-
return convert_to_harvest
16+
if converter:
17+
return converter
1718
else:
18-
raise NotImplementedError("A converter for the given source data does not exist.")
19+
raise NotImplementedError(
20+
f"A converter for the given source and target formats does not exist: {from_fmt} to {to_fmt}"
21+
)
1922

2023

2124
def convert(args: Namespace, *extras):
22-
converter = _get_source_converter(args.input)
25+
converter = _get_source_converter(args.from_fmt, args.to_fmt)
2326

2427
converter(source_path=args.input, output_path=args.output, client_name=args.client)
2528

compiler_admin/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from compiler_admin.commands.info import info
1010
from compiler_admin.commands.init import init
1111
from compiler_admin.commands.time import time
12+
from compiler_admin.commands.time.convert import CONVERTERS
1213
from compiler_admin.commands.user import user
1314
from compiler_admin.commands.user.convert import ACCOUNT_TYPE_OU
1415

@@ -78,6 +79,20 @@ def setup_time_command(cmd_parsers: _SubParsersAction):
7879
default=os.environ.get("HARVEST_DATA", sys.stdout),
7980
help="The path to the file where converted data should be written. Defaults to $HARVEST_DATA or stdout.",
8081
)
82+
time_convert.add_argument(
83+
"--from",
84+
default="toggl",
85+
choices=sorted(CONVERTERS.keys()),
86+
dest="from_fmt",
87+
help="The format of the source data. Defaults to 'toggl'.",
88+
)
89+
time_convert.add_argument(
90+
"--to",
91+
default="harvest",
92+
choices=sorted([to_fmt for sub in CONVERTERS.values() for to_fmt in sub.keys()]),
93+
dest="to_fmt",
94+
help="The format of the converted data. Defaults to 'harvest'.",
95+
)
8196
time_convert.add_argument("--client", default=None, help="The name of the client to use in converted data.")
8297

8398
time_download = add_sub_cmd(time_subcmds, "download", help="Download a Toggl report in CSV format.")

compiler_admin/services/harvest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,6 @@ def convert_to_toggl(
9090
output_data.sort_values(["Start date", "Start time", "Email"], inplace=True)
9191

9292
files.write_csv(output_path, output_data, output_cols)
93+
94+
95+
CONVERTERS = {"toggl": convert_to_toggl}

compiler_admin/services/toggl.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,6 @@ def download_time_entries(
217217

218218
df = pd.read_csv(io.StringIO(csv))
219219
files.write_csv(output_path, df, columns=output_cols)
220+
221+
222+
CONVERTERS = {"harvest": convert_to_harvest, "justworks": convert_to_justworks}

tests/commands/time/test_convert.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,56 @@
11
from argparse import Namespace
2-
from io import StringIO
32
import pytest
43

54
from compiler_admin import RESULT_SUCCESS
65
from compiler_admin.commands.time.convert import (
76
__name__ as MODULE,
7+
CONVERTERS,
88
_get_source_converter,
9-
convert_to_harvest,
10-
convert_to_toggl,
119
convert,
1210
)
11+
from compiler_admin.services.harvest import CONVERTERS as HARVEST_CONVERTERS
12+
from compiler_admin.services.toggl import CONVERTERS as TOGGL_CONVERTERS
1313

1414

1515
@pytest.fixture
1616
def mock_get_source_converter(mocker):
1717
return mocker.patch(f"{MODULE}._get_source_converter")
1818

1919

20-
def test_get_source_converter_match_toggl(toggl_file):
21-
result = _get_source_converter(toggl_file)
22-
23-
assert result == convert_to_harvest
20+
@pytest.fixture
21+
def mock_converters(mocker):
22+
return mocker.patch(f"{MODULE}.CONVERTERS", new={})
2423

2524

26-
def test_get_source_converter_match_harvest(harvest_file):
27-
result = _get_source_converter(harvest_file)
25+
def test_get_source_converter_match(mock_converters):
26+
mock_converters["toggl"] = {"test_fmt": "converter"}
27+
result = _get_source_converter("toggl", "test_fmt")
2828

29-
assert result == convert_to_toggl
29+
assert result == "converter"
3030

3131

3232
def test_get_source_converter_mismatch():
33-
data = StringIO("one,two,three\n1,2,3")
34-
35-
with pytest.raises(NotImplementedError, match="A converter for the given source data does not exist."):
36-
_get_source_converter(data)
33+
with pytest.raises(
34+
NotImplementedError, match="A converter for the given source and target formats does not exist: nope to toggl"
35+
):
36+
_get_source_converter("nope", "toggl")
37+
with pytest.raises(
38+
NotImplementedError, match="A converter for the given source and target formats does not exist: toggl to nope"
39+
):
40+
_get_source_converter("toggl", "nope")
3741

3842

3943
def test_convert(mock_get_source_converter):
40-
args = Namespace(input="input", output="output", client="client")
44+
args = Namespace(input="input", output="output", client="client", from_fmt="from", to_fmt="to")
4145
res = convert(args)
4246

4347
assert res == RESULT_SUCCESS
44-
mock_get_source_converter.assert_called_once_with(args.input)
48+
mock_get_source_converter.assert_called_once_with(args.from_fmt, args.to_fmt)
4549
mock_get_source_converter.return_value.assert_called_once_with(
4650
source_path=args.input, output_path=args.output, client_name=args.client
4751
)
52+
53+
54+
def test_converters():
55+
assert CONVERTERS.get("harvest") == HARVEST_CONVERTERS
56+
assert CONVERTERS.get("toggl") == TOGGL_CONVERTERS

tests/services/test_harvest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
files,
1313
HARVEST_COLUMNS,
1414
TOGGL_COLUMNS,
15+
CONVERTERS,
1516
_calc_start_time,
1617
_duration_str,
1718
_toggl_client_name,
@@ -109,3 +110,7 @@ def test_convert_to_toggl_sample(harvest_file, toggl_file):
109110
assert set(output_df.columns.to_list()) <= set(sample_output_df.columns.to_list())
110111
assert output_df["Client"].eq("Test Client 123").all()
111112
assert output_df["Project"].eq("Test Client 123").all()
113+
114+
115+
def test_converters():
116+
assert CONVERTERS.get("toggl") == convert_to_toggl

tests/services/test_toggl.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import compiler_admin.services.toggl
1111
from compiler_admin.services.toggl import (
12+
CONVERTERS,
1213
__name__ as MODULE,
1314
_get_first_name,
1415
_get_last_name,
@@ -251,3 +252,8 @@ def test_download_time_entries(toggl_file):
251252
# as corresponding column values from the mock DataFrame
252253
for col in response_df.columns:
253254
assert response_df[col].equals(mock_df[col])
255+
256+
257+
def test_converters():
258+
assert CONVERTERS.get("harvest") == convert_to_harvest
259+
assert CONVERTERS.get("justworks") == convert_to_justworks

tests/test_main.py

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def test_main_init_gyb(mock_commands_init):
104104
def test_main_init_no_username(mock_commands_init):
105105
with pytest.raises(SystemExit):
106106
main(argv=["init"])
107-
assert mock_commands_init.call_count == 0
107+
assert mock_commands_init.call_count == 0
108108

109109

110110
def test_main_time_convert_default(mock_commands_time):
@@ -114,7 +114,14 @@ def test_main_time_convert_default(mock_commands_time):
114114
call_args = mock_commands_time.call_args.args
115115
assert (
116116
Namespace(
117-
func=mock_commands_time, command="time", subcommand="convert", client=None, input=sys.stdin, output=sys.stdout
117+
func=mock_commands_time,
118+
command="time",
119+
subcommand="convert",
120+
client=None,
121+
input=sys.stdin,
122+
output=sys.stdout,
123+
from_fmt="toggl",
124+
to_fmt="harvest",
118125
)
119126
in call_args
120127
)
@@ -129,7 +136,16 @@ def test_main_time_convert_env(monkeypatch, mock_commands_time):
129136
mock_commands_time.assert_called_once()
130137
call_args = mock_commands_time.call_args.args
131138
assert (
132-
Namespace(func=mock_commands_time, command="time", subcommand="convert", client=None, input="toggl", output="harvest")
139+
Namespace(
140+
func=mock_commands_time,
141+
command="time",
142+
subcommand="convert",
143+
client=None,
144+
input="toggl",
145+
output="harvest",
146+
from_fmt="toggl",
147+
to_fmt="harvest",
148+
)
133149
in call_args
134150
)
135151

@@ -231,6 +247,8 @@ def test_main_time_convert_client(mock_commands_time):
231247
client="client123",
232248
input=sys.stdin,
233249
output=sys.stdout,
250+
from_fmt="toggl",
251+
to_fmt="harvest",
234252
)
235253
in call_args
236254
)
@@ -249,6 +267,8 @@ def test_main_time_convert_input(mock_commands_time):
249267
client=None,
250268
input="file.csv",
251269
output=sys.stdout,
270+
from_fmt="toggl",
271+
to_fmt="harvest",
252272
)
253273
in call_args
254274
)
@@ -267,10 +287,62 @@ def test_main_time_convert_output(mock_commands_time):
267287
client=None,
268288
input=sys.stdin,
269289
output="file.csv",
290+
from_fmt="toggl",
291+
to_fmt="harvest",
292+
)
293+
in call_args
294+
)
295+
296+
297+
def test_main_time_convert_from(mock_commands_time):
298+
main(argv=["time", "convert", "--from", "harvest"])
299+
300+
mock_commands_time.assert_called_once()
301+
call_args = mock_commands_time.call_args.args
302+
assert (
303+
Namespace(
304+
func=mock_commands_time,
305+
command="time",
306+
subcommand="convert",
307+
client=None,
308+
input=sys.stdin,
309+
output=sys.stdout,
310+
from_fmt="harvest",
311+
to_fmt="harvest",
270312
)
271313
in call_args
272314
)
273315

316+
with pytest.raises(SystemExit):
317+
main(argv=["time", "convert", "--from", "nope"])
318+
# it should not have been called an additional time from the first
319+
mock_commands_time.assert_called_once()
320+
321+
322+
def test_main_time_convert_to(mock_commands_time):
323+
main(argv=["time", "convert", "--to", "toggl"])
324+
325+
mock_commands_time.assert_called_once()
326+
call_args = mock_commands_time.call_args.args
327+
assert (
328+
Namespace(
329+
func=mock_commands_time,
330+
command="time",
331+
subcommand="convert",
332+
client=None,
333+
input=sys.stdin,
334+
output=sys.stdout,
335+
from_fmt="toggl",
336+
to_fmt="toggl",
337+
)
338+
in call_args
339+
)
340+
341+
with pytest.raises(SystemExit):
342+
main(argv=["time", "convert", "--to", "nope"])
343+
# it should not have been called an additional time from the first
344+
mock_commands_time.assert_called_once()
345+
274346

275347
def test_main_user_alumni(mock_commands_user):
276348
main(argv=["user", "alumni", "username"])
@@ -355,7 +427,7 @@ def test_main_user_create_extras(mock_commands_user):
355427
def test_main_user_create_no_username(mock_commands_user):
356428
with pytest.raises(SystemExit):
357429
main(argv=["user", "create"])
358-
assert mock_commands_user.call_count == 0
430+
assert mock_commands_user.call_count == 0
359431

360432

361433
def test_main_user_convert(mock_commands_user):
@@ -380,13 +452,13 @@ def test_main_user_convert(mock_commands_user):
380452
def test_main_user_convert_no_username(mock_commands_user):
381453
with pytest.raises(SystemExit):
382454
main(argv=["user", "convert"])
383-
assert mock_commands_user.call_count == 0
455+
assert mock_commands_user.call_count == 0
384456

385457

386458
def test_main_user_convert_bad_account_type(mock_commands_user):
387459
with pytest.raises(SystemExit):
388460
main(argv=["user", "convert", "username", "account_type"])
389-
assert mock_commands_user.call_count == 0
461+
assert mock_commands_user.call_count == 0
390462

391463

392464
def test_main_user_delete(mock_commands_user):
@@ -412,7 +484,7 @@ def test_main_user_delete_force(mock_commands_user):
412484
def test_main_user_delete_no_username(mock_commands_user):
413485
with pytest.raises(SystemExit):
414486
main(argv=["user", "delete"])
415-
assert mock_commands_user.call_count == 0
487+
assert mock_commands_user.call_count == 0
416488

417489

418490
def test_main_user_offboard(mock_commands_user):
@@ -458,7 +530,7 @@ def test_main_user_offboard_with_alias(mock_commands_user):
458530
def test_main_user_offboard_no_username(mock_commands_user):
459531
with pytest.raises(SystemExit):
460532
main(argv=["user", "offboard"])
461-
assert mock_commands_user.call_count == 0
533+
assert mock_commands_user.call_count == 0
462534

463535

464536
def test_main_user_reset(mock_commands_user):
@@ -504,7 +576,7 @@ def test_main_user_reset_force(mock_commands_user):
504576
def test_main_user_reset_no_username(mock_commands_user):
505577
with pytest.raises(SystemExit):
506578
main(argv=["user", "reset"])
507-
assert mock_commands_user.call_count == 0
579+
assert mock_commands_user.call_count == 0
508580

509581

510582
def test_main_user_restore(mock_commands_user):
@@ -518,7 +590,7 @@ def test_main_user_restore(mock_commands_user):
518590
def test_main_user_restore_no_username(mock_commands_user):
519591
with pytest.raises(SystemExit):
520592
main(argv=["user", "restore"])
521-
assert mock_commands_user.call_count == 0
593+
assert mock_commands_user.call_count == 0
522594

523595

524596
def test_main_user_signout(mock_commands_user):
@@ -544,7 +616,7 @@ def test_main_user_signout_force(mock_commands_user):
544616
def test_main_user_signout_no_username(mock_commands_user):
545617
with pytest.raises(SystemExit):
546618
main(argv=["user", "signout"])
547-
assert mock_commands_user.call_count == 0
619+
assert mock_commands_user.call_count == 0
548620

549621

550622
@pytest.mark.e2e

0 commit comments

Comments
 (0)