Skip to content

Commit 8eeb6c0

Browse files
bosdbosd
authored andcommitted
[FIX]: Allow to write export sh scripts
1 parent b57b9ce commit 8eeb6c0

File tree

2 files changed

+145
-100
lines changed

2 files changed

+145
-100
lines changed

src/odoo_data_flow/lib/internal/io.py

Lines changed: 93 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -35,87 +35,110 @@ def write_csv(
3535
log.error(f"Failed to write to file {filename}: {e}")
3636

3737

38+
def _build_import_command(
39+
filename: str, model: str, worker: int, batch_size: int, **kwargs: Any
40+
) -> list[str]:
41+
"""Builds the command parts for an 'import' shell command."""
42+
model_name = (
43+
os.path.basename(filename).replace(".csv", "").replace("_", ".")
44+
if model == "auto"
45+
else model
46+
)
47+
command_parts = [
48+
"odoo-data-flow",
49+
"import",
50+
"--config",
51+
shlex.quote(kwargs.get("conf_file", "conf/connection.conf")),
52+
"--file",
53+
shlex.quote(filename),
54+
"--model",
55+
shlex.quote(model_name),
56+
"--encoding",
57+
shlex.quote(kwargs.get("encoding", "utf-8")),
58+
"--worker",
59+
str(worker),
60+
"--size",
61+
str(batch_size),
62+
"--sep",
63+
shlex.quote(kwargs.get("sep", ";")),
64+
]
65+
if kwargs.get("groupby"):
66+
command_parts.extend(["--groupby", shlex.quote(kwargs["groupby"])])
67+
if kwargs.get("ignore"):
68+
command_parts.extend(["--ignore", shlex.quote(kwargs["ignore"])])
69+
if kwargs.get("context"):
70+
command_parts.extend(["--context", shlex.quote(str(kwargs["context"]))])
71+
return command_parts
72+
73+
74+
def _build_export_command(filename: str, model: str, **kwargs: Any) -> list[str]:
75+
"""Builds the command parts for an 'export' shell command."""
76+
return [
77+
"odoo-data-flow",
78+
"export",
79+
"--config",
80+
shlex.quote(kwargs.get("conf_file", "conf/connection.conf")),
81+
"--file",
82+
shlex.quote(filename),
83+
"--model",
84+
shlex.quote(model),
85+
"--fields",
86+
shlex.quote(kwargs.get("fields", "")),
87+
"--domain",
88+
shlex.quote(kwargs.get("domain", "[]")),
89+
"--sep",
90+
shlex.quote(kwargs.get("sep", ";")),
91+
"--encoding",
92+
shlex.quote(kwargs.get("encoding", "utf-8")),
93+
]
94+
95+
3896
def write_file(
3997
filename: Optional[str] = None,
4098
header: Optional[list[str]] = None,
4199
data: Optional[list[list[Any]]] = None,
42100
fail: bool = False,
43101
model: str = "auto",
44102
launchfile: str = "import_auto.sh",
45-
worker: int = 1,
46-
batch_size: int = 10,
47-
init: bool = False,
48-
encoding: str = "utf-8",
49-
groupby: str = "",
50-
sep: str = ";",
51-
context: Optional[dict[str, Any]] = None,
52-
ignore: str = "",
53-
**kwargs: Any, # to catch other unused params
103+
command: str = "import",
104+
**kwargs: Any,
54105
) -> None:
55-
"""Filewriter.
106+
"""Writes data to a CSV and generates a corresponding shell script.
107+
108+
This function can generate scripts for both `import` and `export` commands
109+
based on the `command` parameter.
56110
57-
Writes data to a CSV file and generates a corresponding shell script
58-
to import that file using the odoo-data-flow CLI.
111+
Args:
112+
filename: The path to the data file to be written or referenced.
113+
header: A list of strings for the header row.
114+
data: A list of lists representing the data rows.
115+
fail: If True (and command is 'import'), includes a second command
116+
with the --fail flag.
117+
model: The technical name of the Odoo model.
118+
launchfile: The path where the shell script will be saved.
119+
command: The command to generate in the script ('import' or 'export').
120+
**kwargs: Catches other command-specific params like 'worker', 'fields', etc.
59121
"""
60-
# Step 1: Write the actual data file
61122
if filename and header is not None and data is not None:
62-
write_csv(filename, header, data, encoding=encoding)
123+
write_csv(filename, header, data, encoding=kwargs.get("encoding", "utf-8"))
124+
125+
if not launchfile or not filename:
126+
return
63127

64-
# Step 2: If no launchfile is specified, we are done.
65-
if not launchfile:
128+
command_parts: list[str]
129+
if command == "import":
130+
command_parts = _build_import_command(filename, model, **kwargs)
131+
elif command == "export":
132+
command_parts = _build_export_command(filename, model, **kwargs)
133+
else:
134+
log.error(f"Invalid command type '{command}' provided to write_file.")
66135
return
67136

68-
# Step 3: Only generate the import script if a filename was provided.
69-
if filename:
70-
# Determine the target model name
71-
if model == "auto":
72-
model_name = (
73-
os.path.basename(filename).replace(".csv", "").replace("_", ".")
74-
)
75-
else:
76-
model_name = model
77-
78-
# Build the base command with its arguments
79-
# We use shlex.quote to ensure all arguments
80-
# are safely escaped for the shell.
81-
command_parts = [
82-
"odoo-data-flow",
83-
"import",
84-
"--config",
85-
shlex.quote(kwargs.get("conf_file", "conf/connection.conf")),
86-
"--file",
87-
shlex.quote(filename),
88-
"--model",
89-
shlex.quote(model_name),
90-
"--encoding",
91-
shlex.quote(encoding),
92-
"--worker",
93-
str(worker),
94-
"--size",
95-
str(batch_size),
96-
"--sep",
97-
shlex.quote(sep),
98-
]
99-
100-
# Add optional arguments if they have a value
101-
if groupby:
102-
command_parts.extend(["--groupby", shlex.quote(groupby)])
103-
if ignore:
104-
command_parts.extend(["--ignore", shlex.quote(ignore)])
105-
if context:
106-
command_parts.extend(["--context", shlex.quote(str(context))])
107-
108-
# Write the command(s) to the shell script
109-
mode = "w" if init else "a"
110-
try:
111-
with open(launchfile, mode, encoding="utf-8") as f:
112-
# Write the main import command
113-
f.write(" ".join(command_parts) + "\n")
114-
115-
# If fail mode is enabled,
116-
# write the second command with the --fail flag
117-
if fail:
118-
fail_command_parts = [*command_parts, "--fail"]
119-
f.write(" ".join(fail_command_parts) + "\n")
120-
except OSError as e:
121-
log.error(f"Failed to write to launch file {launchfile}: {e}")
137+
mode = "w" if kwargs.get("init") else "a"
138+
try:
139+
with open(launchfile, mode, encoding="utf-8") as f:
140+
f.write(" ".join(command_parts) + "\n")
141+
if fail and command == "import":
142+
f.write(" ".join([*command_parts, "--fail"]) + "\n")
143+
except OSError as e:
144+
log.error(f"Failed to write to launch file {launchfile}: {e}")

tests/test_io.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Test the IO Handling functionalities."""
22

3-
# tests/test_io.py
4-
53
import shlex
64
from pathlib import Path
75
from unittest.mock import MagicMock, patch
@@ -38,61 +36,52 @@ def test_write_file_writes_csv_data(tmp_path: Path) -> None:
3836
filename=str(data_file),
3937
header=["id", "name"],
4038
data=[["1", "test"]],
41-
launchfile="", # Correctly pass an empty string instead of None
39+
launchfile="",
4240
)
4341
mock_write_csv.assert_called_once_with(
4442
str(data_file), ["id", "name"], [["1", "test"]], encoding="utf-8"
4543
)
4644

4745

48-
@patch("odoo_data_flow.lib.internal.io.write_csv") # Mock the CSV writing part
46+
@patch("odoo_data_flow.lib.internal.io.write_csv")
4947
@patch("odoo_data_flow.lib.internal.io.open")
5048
def test_write_file_no_launchfile(
5149
mock_open: MagicMock, mock_write_csv: MagicMock, tmp_path: Path
5250
) -> None:
5351
"""Tests that write_file exits early if no launchfile is specified."""
5452
data_file = tmp_path / "data.csv"
55-
5653
write_file(
5754
filename=str(data_file),
5855
header=["id"],
5956
data=[["1"]],
60-
launchfile="", # Empty string means no script
57+
launchfile="",
6158
)
62-
63-
# Assert that write_csv was called, but open was not (for the launchfile)
6459
mock_write_csv.assert_called_once()
6560
mock_open.assert_not_called()
6661

6762

68-
def test_write_file_full_script_generation(tmp_path: Path) -> None:
69-
"""Tests that write_file generates a complete shell script with all options."""
70-
# 1. Setup
63+
def test_write_file_import_command(tmp_path: Path) -> None:
64+
"""Tests that write_file generates a complete import shell script."""
7165
script_file = tmp_path / "load.sh"
7266
data_file = tmp_path / "my_model.csv"
73-
74-
# 2. Action
7567
write_file(
7668
filename=str(data_file),
7769
header=["id", "name"],
7870
data=[["1", "test"]],
7971
launchfile=str(script_file),
72+
command="import",
8073
model="my.model",
8174
fail=True,
8275
init=True,
8376
worker=4,
8477
batch_size=50,
8578
groupby="parent_id/id",
8679
ignore="field_to_ignore",
87-
context={"active_test": False}, # Correctly pass a dict instead of a string
80+
context={"active_test": False},
8881
conf_file="conf/custom.conf",
8982
)
90-
91-
# 3. Assertions
9283
assert script_file.exists()
9384
content = script_file.read_text()
94-
95-
# Check for the main command
9685
assert "odoo-data-flow import" in content
9786
assert f"--config {shlex.quote('conf/custom.conf')}" in content
9887
assert f"--file {shlex.quote(str(data_file))}" in content
@@ -102,33 +91,55 @@ def test_write_file_full_script_generation(tmp_path: Path) -> None:
10291
assert f"--groupby {shlex.quote('parent_id/id')}" in content
10392
assert f"--ignore {shlex.quote('field_to_ignore')}" in content
10493
assert f"--context {shlex.quote(str({'active_test': False}))}" in content
105-
106-
# Check for the second command with the --fail flag
10794
assert "--fail" in content
108-
# Count occurrences to ensure both commands are present
10995
assert content.count("odoo-data-flow import") == 2
11096

11197

98+
def test_write_file_export_command(tmp_path: Path) -> None:
99+
"""Tests that write_file generates a correct export shell script."""
100+
script_file = tmp_path / "export.sh"
101+
data_file = tmp_path / "partner_export.csv"
102+
domain_str = "[('is_company', '=', True)]"
103+
write_file(
104+
filename=str(data_file),
105+
launchfile=str(script_file),
106+
command="export",
107+
model="res.partner",
108+
fields="id,name",
109+
domain=domain_str,
110+
init=True,
111+
)
112+
assert script_file.exists()
113+
content = script_file.read_text()
114+
assert "odoo-data-flow export" in content
115+
assert f"--model {shlex.quote('res.partner')}" in content
116+
assert f"--fields {shlex.quote('id,name')}" in content
117+
expected_domain_str = f"--domain {shlex.quote(domain_str)}"
118+
assert expected_domain_str in content
119+
assert "--fail" not in content
120+
121+
112122
def test_write_file_auto_model_name(tmp_path: Path) -> None:
113123
"""Tests that the model name is correctly inferred when model='auto'."""
114124
script_file = tmp_path / "load_auto.sh"
115125
data_file = tmp_path / "res.partner.csv"
116-
117126
write_file(
118127
filename=str(data_file),
119128
header=["id"],
120129
data=[["1"]],
121130
launchfile=str(script_file),
122131
model="auto",
123132
init=True,
133+
# Provide default worker and batch_size to avoid TypeError
134+
worker=1,
135+
batch_size=10,
124136
)
125-
126137
content = script_file.read_text()
127-
# The model name should be inferred from 'res.partner.csv' -> 'res.partner'
138+
# The model name should be inferred from 'res_partner.csv' -> 'res.partner'
128139
assert f"--model {shlex.quote('res.partner')}" in content
129140

130141

131-
@patch("odoo_data_flow.lib.internal.io.write_csv") # Mock the CSV part
142+
@patch("odoo_data_flow.lib.internal.io.write_csv")
132143
@patch("odoo_data_flow.lib.internal.io.open")
133144
@patch("odoo_data_flow.lib.internal.io.log.error")
134145
def test_write_file_oserror(
@@ -138,19 +149,30 @@ def test_write_file_oserror(
138149
139150
Tests that write_file logs an error if an OSError occurs during script writing.
140151
"""
141-
# 1. Setup: This time, the 'open' for the launchfile will fail
142152
mock_open.side_effect = OSError("Permission denied on script file")
143153

144-
# 2. Action
145154
write_file(
146155
filename="data.csv",
147156
header=["id"],
148157
data=[["1"]],
149158
launchfile="protected/load.sh",
150159
init=True,
160+
# Provide default worker and batch_size to avoid TypeError
161+
worker=1,
162+
batch_size=10,
151163
)
152-
153-
# 3. Assertions
154-
mock_write_csv.assert_called_once() # Ensure the CSV part was attempted
164+
mock_write_csv.assert_called_once()
155165
mock_log_error.assert_called_once()
156166
assert "Failed to write to launch file" in mock_log_error.call_args[0][0]
167+
168+
169+
@patch("odoo_data_flow.lib.internal.io.log.error")
170+
def test_write_file_invalid_command(mock_log_error: MagicMock) -> None:
171+
"""Tests that an error is logged for an invalid command type."""
172+
write_file(
173+
filename="dummy.csv",
174+
launchfile="dummy.sh",
175+
command="invalid-command",
176+
)
177+
mock_log_error.assert_called_once()
178+
assert "Invalid command type" in mock_log_error.call_args[0][0]

0 commit comments

Comments
 (0)