6565
6666from __future__ import annotations
6767
68- import json
6968from pathlib import Path
7069from typing import TYPE_CHECKING , Any
7170
124123 """For example, --config='{password: "SECRET:MY_PASSWORD"}'."""
125124)
126125
126+ PIP_URL_HELP = (
127+ "This can be anything pip accepts, including: a PyPI package name, a local path, "
128+ "a git repository, a git branch ref, etc. Use '.' to install from the current local "
129+ "directory."
130+ )
131+
127132
128133def _resolve_config (
129134 config : str ,
@@ -150,7 +155,7 @@ def _inject_secrets(config_dict: dict[str, Any]) -> None:
150155 message = "Config file not found." ,
151156 input_value = str (config_path ),
152157 )
153- config_dict = json . loads (config_path .read_text (encoding = "utf-8" ))
158+ config_dict = yaml . safe_load (config_path .read_text (encoding = "utf-8" ))
154159
155160 _inject_secrets (config_dict )
156161 return config_dict
@@ -447,6 +452,93 @@ def benchmark(
447452 )
448453
449454
455+ @click .command ()
456+ @click .option (
457+ "--source" ,
458+ type = str ,
459+ help = (
460+ "The source name, with an optional version declaration. "
461+ "If the name contains a colon (':'), it will be interpreted as a docker image and tag. "
462+ ),
463+ )
464+ @click .option (
465+ "--destination" ,
466+ type = str ,
467+ help = (
468+ "The destination name, with an optional version declaration. "
469+ "If a path is provided, it will be interpreted as a path to the local executable. "
470+ ),
471+ )
472+ @click .option (
473+ "--streams" ,
474+ type = str ,
475+ help = (
476+ "A comma-separated list of stream names to select for reading. If set to '*', all streams "
477+ "will be selected. Defaults to '*'."
478+ ),
479+ )
480+ @click .option (
481+ "--Sconfig" ,
482+ "source_config" ,
483+ type = str ,
484+ help = "The source config. " + CONFIG_HELP ,
485+ )
486+ @click .option (
487+ "--Dconfig" ,
488+ "destination_config" ,
489+ type = str ,
490+ help = "The destination config. " + CONFIG_HELP ,
491+ )
492+ @click .option (
493+ "--Spip-url" ,
494+ "source_pip_url" ,
495+ type = str ,
496+ help = "Optional pip URL for the source (Python connectors only). " + PIP_URL_HELP ,
497+ )
498+ @click .option (
499+ "--Dpip-url" ,
500+ "destination_pip_url" ,
501+ type = str ,
502+ help = "Optional pip URL for the destination (Python connectors only). " + PIP_URL_HELP ,
503+ )
504+ def sync (
505+ * ,
506+ source : str ,
507+ source_config : str | None = None ,
508+ source_pip_url : str | None = None ,
509+ destination : str ,
510+ destination_config : str | None = None ,
511+ destination_pip_url : str | None = None ,
512+ streams : str | None = None ,
513+ ) -> None :
514+ """Run a sync operation.
515+
516+ Currently, this only supports full refresh syncs. Incremental syncs are not yet supported.
517+ Custom catalog syncs are not yet supported.
518+ """
519+ destination_obj : Destination
520+ source_obj : Source
521+
522+ source_obj = _resolve_source_job (
523+ source = source ,
524+ config = source_config ,
525+ streams = streams ,
526+ pip_url = source_pip_url ,
527+ )
528+ destination_obj = _resolve_destination_job (
529+ destination = destination ,
530+ config = destination_config ,
531+ pip_url = destination_pip_url ,
532+ )
533+
534+ click .echo ("Running sync..." )
535+ destination_obj .write (
536+ source_data = source_obj ,
537+ cache = False ,
538+ state_cache = False ,
539+ )
540+
541+
450542@click .group ()
451543def cli () -> None :
452544 """PyAirbyte CLI."""
@@ -455,6 +547,7 @@ def cli() -> None:
455547
456548cli .add_command (validate )
457549cli .add_command (benchmark )
550+ cli .add_command (sync )
458551
459552if __name__ == "__main__" :
460553 cli ()
0 commit comments