Skip to content

Commit 560f08a

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

File tree

8 files changed

+136
-30
lines changed

8 files changed

+136
-30
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}
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: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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,6 +287,48 @@ 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", "from_fmt"])
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="from_fmt",
311+
to_fmt="harvest",
312+
)
313+
in call_args
314+
)
315+
316+
317+
def test_main_time_convert_to(mock_commands_time):
318+
main(argv=["time", "convert", "--to", "to_fmt"])
319+
320+
mock_commands_time.assert_called_once()
321+
call_args = mock_commands_time.call_args.args
322+
assert (
323+
Namespace(
324+
func=mock_commands_time,
325+
command="time",
326+
subcommand="convert",
327+
client=None,
328+
input=sys.stdin,
329+
output=sys.stdout,
330+
from_fmt="toggl",
331+
to_fmt="to_fmt",
270332
)
271333
in call_args
272334
)

0 commit comments

Comments
 (0)