Skip to content

Commit fe6b383

Browse files
bosdbosd
authored andcommitted
This commit refactors the CLI and core logic of the odoo-data-flow tool.
The CLI now defaults to a project-based workflow, looking for `flows.yml`. Single-action commands like `export` and `import` are still available but now require a `--connection-file` argument for clarity. The core logic has been updated so that functions like `run_export` can be called with a dictionary, allowing for easier integration as a library within an Odoo module. Tests have been updated and added to ensure coverage for the new functionality.
1 parent 23bc6fc commit fe6b383

18 files changed

+925
-960
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ The `transform.py` script generates a `load.sh` file containing the correct CLI
8080

8181
```bash
8282
# Contents of the generated load.sh
83-
odoo-data-flow import --config conf/connection.conf --file data/products_clean.csv --model product.product ...
83+
odoo-data-flow import --connection-file conf/connection.conf --file data/products_clean.csv --model product.product ...
8484
```
8585

8686
Then execute the script.

src/odoo_data_flow/__main__.py

Lines changed: 94 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import ast
44
from importlib.metadata import version as get_version
5+
from pathlib import Path
56
from typing import Any, Optional
67

78
import click
@@ -21,6 +22,15 @@
2122
from .writer import run_write
2223

2324

25+
def run_project_flow(flow_file: str, flow_name: Optional[str]) -> None:
26+
"""Placeholder for running a project flow."""
27+
log.info(f"Running project flow from '{flow_file}'")
28+
if flow_name:
29+
log.info(f"Executing specific flow: '{flow_name}'")
30+
else:
31+
log.info("Executing all flows defined in the file.")
32+
33+
2434
@click.group(
2535
context_settings=dict(help_option_names=["-h", "--help"]),
2636
invoke_without_command=True,
@@ -35,12 +45,44 @@
3545
type=click.Path(),
3646
help="Path to a file to write logs to, in addition to the console.",
3747
)
48+
@click.option(
49+
"--flow-file",
50+
type=click.Path(exists=True, dir_okay=False),
51+
help="Path to the YAML flow file. Defaults to 'flows.yml' in current directory.",
52+
)
53+
@click.option(
54+
"--run",
55+
"flow_name",
56+
help="Name of a specific flow to run from the flow file.",
57+
)
3858
@click.pass_context
39-
def cli(ctx: click.Context, verbose: bool, log_file: Optional[str]) -> None:
59+
def cli(
60+
ctx: click.Context,
61+
verbose: bool,
62+
log_file: Optional[str],
63+
flow_file: Optional[str],
64+
flow_name: Optional[str],
65+
) -> None:
4066
"""Odoo Data Flow: A tool for importing, exporting, and processing data."""
4167
setup_logging(verbose, log_file)
42-
if ctx.invoked_subcommand is None:
43-
click.echo(ctx.get_help())
68+
69+
# If a subcommand is invoked, it's Single-Action mode. Let it proceed.
70+
if ctx.invoked_subcommand is not None:
71+
return
72+
73+
# --- Project Mode Logic ---
74+
effective_flow_file = flow_file
75+
if not effective_flow_file:
76+
default_flow_file = Path("flows.yml")
77+
if default_flow_file.exists():
78+
log.info("No --flow-file specified, using default 'flows.yml'.")
79+
effective_flow_file = str(default_flow_file)
80+
else:
81+
# No subcommand, no --flow-file, and no default flows.yml -> show help.
82+
click.echo(ctx.get_help())
83+
return
84+
85+
run_project_flow(effective_flow_file, flow_name)
4486

4587

4688
# --- Module Management Command Group ---
@@ -52,24 +94,22 @@ def module_group() -> None:
5294

5395
@module_group.command(name="update-list")
5496
@click.option(
55-
"-c",
56-
"--config",
57-
default="conf/connection.conf",
58-
show_default=True,
59-
help="Path to the connection configuration file.",
97+
"--connection-file",
98+
required=True,
99+
type=click.Path(exists=True, dir_okay=False),
100+
help="Path to the Odoo connection file.",
60101
)
61-
def update_module_list_cmd(config: str) -> None:
102+
def update_module_list_cmd(connection_file: str) -> None:
62103
"""Scans the addons path and updates the list of available modules."""
63-
run_update_module_list(config=config)
104+
run_update_module_list(config=connection_file)
64105

65106

66107
@module_group.command(name="install")
67108
@click.option(
68-
"-c",
69-
"--config",
70-
default="conf/connection.conf",
71-
show_default=True,
72-
help="Path to the connection configuration file.",
109+
"--connection-file",
110+
required=True,
111+
type=click.Path(exists=True, dir_okay=False),
112+
help="Path to the Odoo connection file.",
73113
)
74114
@click.option(
75115
"-m",
@@ -78,19 +118,18 @@ def update_module_list_cmd(config: str) -> None:
78118
required=True,
79119
help="A comma-separated list of module names to install or upgrade.",
80120
)
81-
def install_modules_cmd(config: str, modules_str: str) -> None:
121+
def install_modules_cmd(connection_file: str, modules_str: str) -> None:
82122
"""Installs or upgrades a list of Odoo modules."""
83-
modules_list = [mod.strip() for mod in modules_str.split(",")]
84-
run_module_installation(config=config, modules=modules_list)
123+
modules_list = [mod.strip() for mod in modules_str.split(",") if mod.strip()]
124+
run_module_installation(config=connection_file, modules=modules_list)
85125

86126

87127
@module_group.command(name="uninstall")
88128
@click.option(
89-
"-c",
90-
"--config",
91-
default="conf/connection.conf",
92-
show_default=True,
93-
help="Path to the connection configuration file.",
129+
"--connection-file",
130+
required=True,
131+
type=click.Path(exists=True, dir_okay=False),
132+
help="Path to the Odoo connection file.",
94133
)
95134
@click.option(
96135
"-m",
@@ -99,19 +138,18 @@ def install_modules_cmd(config: str, modules_str: str) -> None:
99138
required=True,
100139
help="A comma-separated list of module names to uninstall.",
101140
)
102-
def uninstall_modules_cmd(config: str, modules_str: str) -> None:
141+
def uninstall_modules_cmd(connection_file: str, modules_str: str) -> None:
103142
"""Uninstalls a list of Odoo modules."""
104143
modules_list = [mod.strip() for mod in modules_str.split(",")]
105-
run_module_uninstallation(config=config, modules=modules_list)
144+
run_module_uninstallation(config=connection_file, modules=modules_list)
106145

107146

108147
@module_group.command(name="install-languages")
109148
@click.option(
110-
"-c",
111-
"--config",
112-
default="conf/connection.conf",
113-
show_default=True,
114-
help="Path to the connection configuration file.",
149+
"--connection-file",
150+
required=True,
151+
type=click.Path(exists=True, dir_okay=False),
152+
help="Path to the Odoo connection file.",
115153
)
116154
@click.option(
117155
"-l",
@@ -120,10 +158,10 @@ def uninstall_modules_cmd(config: str, modules_str: str) -> None:
120158
required=True,
121159
help="A comma-separated list of language codes to install (e.g., 'nl_BE,fr_FR').",
122160
)
123-
def install_languages_cmd(config: str, languages_str: str) -> None:
161+
def install_languages_cmd(connection_file: str, languages_str: str) -> None:
124162
"""Installs one or more languages in the Odoo database."""
125163
languages_list = [lang.strip() for lang in languages_str.split(",")]
126-
run_language_installation(config=config, languages=languages_list)
164+
run_language_installation(config=connection_file, languages=languages_list)
127165

128166

129167
# --- Workflow Command Group ---
@@ -136,11 +174,10 @@ def workflow_group() -> None:
136174
# --- Invoice v9 Workflow Sub-command ---
137175
@workflow_group.command(name="invoice-v9")
138176
@click.option(
139-
"-c",
140-
"--config",
141-
default="conf/connection.conf",
142-
show_default=True,
143-
help="Path to the connection configuration file.",
177+
"--connection-file",
178+
required=True,
179+
type=click.Path(exists=True, dir_okay=False),
180+
help="Path to the Odoo connection file.",
144181
)
145182
@click.option(
146183
"--action",
@@ -179,19 +216,19 @@ def workflow_group() -> None:
179216
@click.option(
180217
"--max-connection", default=4, type=int, help="Number of parallel threads."
181218
)
182-
def invoice_v9_cmd(**kwargs: Any) -> None:
219+
def invoice_v9_cmd(connection_file: str, **kwargs: Any) -> None:
183220
"""Runs the legacy Odoo v9 invoice processing workflow."""
221+
kwargs["config"] = connection_file
184222
run_invoice_v9_workflow(**kwargs)
185223

186224

187225
# --- Import Command ---
188226
@cli.command(name="import")
189227
@click.option(
190-
"-c",
191-
"--config",
192-
default="conf/connection.conf",
193-
show_default=True,
194-
help="Configuration file for connection parameters.",
228+
"--connection-file",
229+
required=True,
230+
type=click.Path(exists=True, dir_okay=False),
231+
help="Path to the Odoo connection file.",
195232
)
196233
@click.option("--file", "filename", required=True, help="File to import.")
197234
@click.option(
@@ -266,8 +303,9 @@ def invoice_v9_cmd(**kwargs: Any) -> None:
266303
help="Special handling for one-to-many imports.",
267304
)
268305
@click.option("--encoding", default="utf-8", help="Encoding of the data file.")
269-
def import_cmd(**kwargs: Any) -> None:
306+
def import_cmd(connection_file: str, **kwargs: Any) -> None:
270307
"""Runs the data import process."""
308+
kwargs["config"] = connection_file
271309
try:
272310
kwargs["context"] = ast.literal_eval(kwargs.get("context", "{}"))
273311
except (ValueError, SyntaxError) as e:
@@ -279,11 +317,10 @@ def import_cmd(**kwargs: Any) -> None:
279317
# --- Write Command (New) ---
280318
@cli.command(name="write")
281319
@click.option(
282-
"-c",
283-
"--config",
284-
default="conf/connection.conf",
285-
show_default=True,
286-
help="Configuration file for connection parameters.",
320+
"--connection-file",
321+
required=True,
322+
type=click.Path(exists=True, dir_okay=False),
323+
help="Path to the Odoo connection file.",
287324
)
288325
@click.option("--file", "filename", required=True, help="File with records to update.")
289326
@click.option("--model", required=True, help="Odoo model to write to.")
@@ -310,8 +347,9 @@ def import_cmd(**kwargs: Any) -> None:
310347
help="Odoo context as a dictionary string.",
311348
)
312349
@click.option("--encoding", default="utf-8", help="Encoding of the data file.")
313-
def write_cmd(**kwargs: Any) -> None:
350+
def write_cmd(connection_file: str, **kwargs: Any) -> None:
314351
"""Runs the batch update (write) process."""
352+
kwargs["config"] = connection_file
315353
try:
316354
kwargs["context"] = ast.literal_eval(kwargs.get("context", "{}"))
317355
except (ValueError, SyntaxError) as e:
@@ -323,11 +361,10 @@ def write_cmd(**kwargs: Any) -> None:
323361
# --- Export Command ---
324362
@cli.command(name="export")
325363
@click.option(
326-
"-c",
327-
"--config",
328-
default="conf/connection.conf",
329-
show_default=True,
330-
help="Configuration file for connection parameters.",
364+
"--connection-file",
365+
required=True,
366+
type=click.Path(exists=True, dir_okay=False),
367+
help="Path to the Odoo connection file.",
331368
)
332369
@click.option("--output", required=True, help="Output file path.")
333370
@click.option("--model", required=True, help="Odoo model to export from.")
@@ -379,8 +416,9 @@ def write_cmd(**kwargs: Any) -> None:
379416
like 'selection' or 'binary'.
380417
""",
381418
)
382-
def export_cmd(**kwargs: Any) -> None:
419+
def export_cmd(connection_file: str, **kwargs: Any) -> None:
383420
"""Runs the data export process."""
421+
kwargs["config"] = connection_file
384422
run_export(**kwargs)
385423

386424

src/odoo_data_flow/export_threaded.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,18 @@ def launch_batch(self, data_ids: list[int], batch_number: int) -> None:
292292

293293

294294
def _initialize_export(
295-
config_file: str, model_name: str, header: list[str], technical_names: bool
295+
config: Union[str, dict[str, Any]],
296+
model_name: str,
297+
header: list[str],
298+
technical_names: bool,
296299
) -> tuple[Optional[Any], Optional[Any], Optional[dict[str, dict[str, Any]]]]:
297300
"""Connects to Odoo and fetches field metadata, including relations."""
298301
log.debug("Starting metadata initialization.")
299302
try:
300-
connection = conf_lib.get_connection_from_config(config_file)
303+
if isinstance(config, dict):
304+
connection = conf_lib.get_connection_from_dict(config)
305+
else:
306+
connection = conf_lib.get_connection_from_config(config)
301307
model_obj = connection.get_model(model_name)
302308
fields_for_metadata = sorted(
303309
list(
@@ -541,7 +547,10 @@ def _process_export_batches( # noqa: C901
541547

542548

543549
def _determine_export_strategy(
544-
config_file: str, model: str, header: list[str], technical_names: bool
550+
config: Union[str, dict[str, Any]],
551+
model: str,
552+
header: list[str],
553+
technical_names: bool,
545554
) -> tuple[
546555
Optional[Any],
547556
Optional[Any],
@@ -554,7 +563,7 @@ def _determine_export_strategy(
554563
f.endswith("/.id") or f == ".id" for f in header
555564
)
556565
connection, model_obj, fields_info = _initialize_export(
557-
config_file, model, header, preliminary_read_mode
566+
config, model, header, preliminary_read_mode
558567
)
559568

560569
if not model_obj or not fields_info:
@@ -663,7 +672,7 @@ def _create_new_session(
663672

664673

665674
def export_data(
666-
config_file: str,
675+
config: Union[str, dict[str, Any]],
667676
model: str,
668677
domain: list[Any],
669678
header: list[str],
@@ -684,7 +693,7 @@ def export_data(
684693
return False, session_id, 0, None
685694

686695
connection, model_obj, fields_info, force_read_method, is_hybrid = (
687-
_determine_export_strategy(config_file, model, header, technical_names)
696+
_determine_export_strategy(config, model, header, technical_names)
688697
)
689698
if not connection or not model_obj or not fields_info:
690699
return False, session_id, 0, None

src/odoo_data_flow/exporter.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""This module contains the high-level logic for exporting data from Odoo."""
22

33
import ast
4-
from typing import Any, Optional
4+
from typing import Any, Optional, Union
55

66
import polars as pl
77
from rich.console import Console
@@ -30,7 +30,7 @@ def _show_success_panel(message: str) -> None:
3030

3131

3232
def run_export(
33-
config: str,
33+
config: Union[str, dict[str, Any]],
3434
model: str,
3535
fields: str,
3636
output: str,
@@ -71,7 +71,7 @@ def run_export(
7171
fields_list = fields.split(",")
7272

7373
success, session_id, record_count, _ = export_threaded.export_data(
74-
config_file=config,
74+
config=config,
7575
model=model,
7676
domain=parsed_domain,
7777
header=fields_list,
@@ -136,7 +136,7 @@ def run_export(
136136

137137

138138
def run_export_for_migration(
139-
config: str,
139+
config: Union[str, dict[str, Any]],
140140
model: str,
141141
fields: list[str],
142142
domain: str = "[]",
@@ -168,7 +168,7 @@ def run_export_for_migration(
168168
parsed_context = {}
169169

170170
success, _, _, result_df = export_threaded.export_data(
171-
config_file=config,
171+
config=config,
172172
model=model,
173173
domain=parsed_domain,
174174
header=fields,

0 commit comments

Comments
 (0)