diff --git a/camply/cli.py b/camply/cli.py index 0b2330c1..6bb75936 100644 --- a/camply/cli.py +++ b/camply/cli.py @@ -3,6 +3,7 @@ """ import logging +import os import sys from dataclasses import dataclass from datetime import date, timedelta @@ -441,9 +442,10 @@ def campgrounds( "--yaml-config", "--yml-config", default=None, - type=click.Path(exists=True, dir_okay=False, resolve_path=True), + type=str, help="Rather than provide arguments to the command line utility, instead " - "pass a file path to a YAML configuration file. See the documentation " + "pass a file path to a YAML configuration file. Multiple files can be " + "specified separated by commas. See the documentation " "for more information on how to structure your configuration file.", ) equipment_argument = click.option( @@ -741,11 +743,16 @@ def campsites( if context.debug is None: context.debug = debug _set_up_debug(debug=context.debug) + configs = [] if yaml_config is not None: - provider, provider_kwargs, search_kwargs = yaml_utils.yaml_file_to_arguments( - file_path=yaml_config - ) - provider = _preferred_provider(context, provider) + yaml_files = [f.strip() for f in yaml_config.split(',') if f.strip()] + for file_path in yaml_files: + if not os.path.exists(file_path): + raise click.BadParameter(f"YAML config file '{file_path}' does not exist.") + file_configs = yaml_utils.yaml_file_to_arguments(file_path=file_path) + for provider, provider_kwargs, search_kwargs in file_configs: + provider = _preferred_provider(context, provider) + configs.append((provider, provider_kwargs, search_kwargs)) else: provider = _preferred_provider(context, provider) provider_kwargs, search_kwargs = _get_provider_kwargs_from_cli( @@ -770,9 +777,11 @@ def campsites( day=day, yaml_config=yaml_config, ) - provider_class: Type[BaseCampingSearch] = CAMPSITE_SEARCH_PROVIDER[provider] - camping_finder: BaseCampingSearch = provider_class(**provider_kwargs) - camping_finder.get_matching_campsites(**search_kwargs) + configs = [(provider, provider_kwargs, search_kwargs)] + for config_provider, config_provider_kwargs, config_search_kwargs in configs: + provider_class: Type[BaseCampingSearch] = CAMPSITE_SEARCH_PROVIDER[config_provider] + camping_finder: BaseCampingSearch = provider_class(**config_provider_kwargs) + camping_finder.get_matching_campsites(**config_search_kwargs) @camply_command_line.command(cls=RichCommand) diff --git a/camply/utils/yaml_utils.py b/camply/utils/yaml_utils.py index 9c5767df..c476c63c 100644 --- a/camply/utils/yaml_utils.py +++ b/camply/utils/yaml_utils.py @@ -7,10 +7,10 @@ from enum import Enum from pathlib import Path from re import compile -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import yaml -from yaml import SafeLoader, load +from yaml import SafeLoader, load, load_all from camply.containers.search_model import YamlSearchFile from camply.utils import make_list @@ -76,14 +76,15 @@ def env_var_constructor(safe_loader: yaml.Loader, node: Any) -> Any: safe_loader.add_constructor(tag=None, constructor=env_var_constructor) with open(path) as conf_data: - return load(stream=conf_data, Loader=safe_loader) + documents = list(load_all(stream=conf_data, Loader=safe_loader)) + return documents def yaml_file_to_arguments( file_path: str, -) -> Tuple[str, Dict[str, object], Dict[str, object]]: +) -> List[Tuple[str, Dict[str, object], Dict[str, object]]]: """ - Convert YAML File into A Dictionary to be used as **kwargs + Convert YAML File into A List of Dictionaries to be used as **kwargs Parameters ---------- @@ -92,48 +93,51 @@ def yaml_file_to_arguments( Returns ------- - Tuple[str, Dict[str, object], Dict[str, object]] - Tuple containing provider string, provider **kwargs, and search **kwargs + List[Tuple[str, Dict[str, object], Dict[str, object]]] + List of tuples, each containing provider string, provider **kwargs, and search **kwargs """ - yaml_search = read_yaml(path=file_path) + yaml_documents = read_yaml(path=file_path) logger.info(f"YAML File Parsed: {Path(file_path).name}") - yaml_model = YamlSearchFile(**yaml_search) - if isinstance(yaml_model.provider, Enum): - provider = yaml_model.provider.value - else: - provider = yaml_model.provider - search_window = handle_search_windows( - start_date=yaml_model.start_date, end_date=yaml_model.end_date - ) - days_of_the_week = yaml_model.days - if days_of_the_week is not None: - lower_mapping = { - key.lower(): value for key, value in days_of_the_week_mapping.items() + configs = [] + for yaml_search in yaml_documents: + yaml_model = YamlSearchFile(**yaml_search) + if isinstance(yaml_model.provider, Enum): + provider = yaml_model.provider.value + else: + provider = yaml_model.provider + search_window = handle_search_windows( + start_date=yaml_model.start_date, end_date=yaml_model.end_date + ) + days_of_the_week = yaml_model.days + if days_of_the_week is not None: + lower_mapping = { + key.lower(): value for key, value in days_of_the_week_mapping.items() + } + days_of_the_week = [lower_mapping[item.lower()] for item in days_of_the_week] + equipment = make_list(yaml_model.equipment) + if isinstance(equipment, list): + equipment = [tuple(equip) for equip in equipment] + provider_kwargs = { + "search_window": search_window, + "recreation_area": yaml_model.recreation_area, + "campgrounds": yaml_model.campgrounds, + "campsites": yaml_model.campsites, + "weekends_only": yaml_model.weekends, + "days_of_the_week": days_of_the_week, + "nights": yaml_model.nights, + "equipment": equipment, + "offline_search": yaml_model.offline_search, + "offline_search_path": yaml_model.offline_search_path, } - days_of_the_week = [lower_mapping[item.lower()] for item in days_of_the_week] - equipment = make_list(yaml_model.equipment) - if isinstance(equipment, list): - equipment = [tuple(equip) for equip in equipment] - provider_kwargs = { - "search_window": search_window, - "recreation_area": yaml_model.recreation_area, - "campgrounds": yaml_model.campgrounds, - "campsites": yaml_model.campsites, - "weekends_only": yaml_model.weekends, - "days_of_the_week": days_of_the_week, - "nights": yaml_model.nights, - "equipment": equipment, - "offline_search": yaml_model.offline_search, - "offline_search_path": yaml_model.offline_search_path, - } - search_kwargs = { - "log": True, - "verbose": True, - "continuous": yaml_model.continuous, - "polling_interval": yaml_model.polling_interval, - "notify_first_try": yaml_model.notify_first_try, - "notification_provider": yaml_model.notifications, - "search_forever": yaml_model.search_forever, - "search_once": yaml_model.search_once, - } - return provider, provider_kwargs, search_kwargs + search_kwargs = { + "log": True, + "verbose": True, + "continuous": yaml_model.continuous, + "polling_interval": yaml_model.polling_interval, + "notify_first_try": yaml_model.notify_first_try, + "notification_provider": yaml_model.notifications, + "search_forever": yaml_model.search_forever, + "search_once": yaml_model.search_once, + } + configs.append((provider, provider_kwargs, search_kwargs)) + return configs diff --git a/docs/command_line_usage.md b/docs/command_line_usage.md index 1de47fcb..25dc336d 100644 --- a/docs/command_line_usage.md +++ b/docs/command_line_usage.md @@ -715,6 +715,34 @@ camply campsites --yaml-config example_search.yaml A JSON Schema for the YAML configuration file can be found at [docs/yaml_search.json](yaml_search.json) +#### Multiple YAML Configurations + +Camply supports searching across multiple providers or configurations in a single command. You can specify multiple YAML files separated by commas: + +```commandline +camply campsites --yaml-config config1.yaml,config2.yaml +``` + +Alternatively, you can define multiple search configurations in a single YAML file using YAML document separators (`---`): + +```yaml +provider: RecreationDotGov +recreation_area: 2907 +start_date: 2023-09-10 +end_date: 2023-09-11 +--- +provider: GoingToCamp +recreation_area: 1 +start_date: 2023-09-10 +end_date: 2023-09-11 +``` + +```commandline +camply campsites --yaml-config multi_config.yaml +``` + +This allows you to search multiple providers or different search criteria simultaneously. + ### Searching for a Campsite That Fits Your Equipment Camply can help you filter campsites to fit your specific equipment, like a Trailer or an RV. diff --git a/tests/cli/test_campsites.py b/tests/cli/test_campsites.py index ea2d27b1..586c8090 100644 --- a/tests/cli/test_campsites.py +++ b/tests/cli/test_campsites.py @@ -578,3 +578,42 @@ def test_search_by_yaml_reservecalifornia( assert "Andrew Molera SP" in result.output assert "Reservable Campsites Matching Search Preferences" in result.output cli_status_checker(result=result, exit_code_zero=True) + + +@vcr_cassette +def test_multiple_yaml_files(cli_runner: CamplyRunner) -> None: + """ + Search for Campsites using multiple YAML files + """ + test_command = """ + camply \ + campsites \ + --yaml-config \ + tests/yaml/example_search.yaml,tests/yaml/yosemite_search.yaml \ + --search-once + """ + result = cli_runner.run_camply_command(command=test_command) + assert "YAML File Parsed: example_search.yaml" in result.output + assert "YAML File Parsed: yosemite_search.yaml" in result.output + assert "Rocky Mountain National Park" in result.output + assert "Wawona Campground" in result.output + cli_status_checker(result=result, exit_code_zero=True) + + +@vcr_cassette +def test_multi_document_yaml(cli_runner: CamplyRunner) -> None: + """ + Search for Campsites using multi-document YAML + """ + test_command = """ + camply \ + campsites \ + --yaml-config \ + tests/yaml/multi_document.yaml \ + --search-once + """ + result = cli_runner.run_camply_command(command=test_command) + assert "YAML File Parsed: multi_document.yaml" in result.output + assert 'Using Camply Provider: "RecreationDotGov"' in result.output + assert 'Using Camply Provider: "GoingToCamp"' in result.output + cli_status_checker(result=result, exit_code_zero=True) diff --git a/tests/yaml/multi_document.yaml b/tests/yaml/multi_document.yaml new file mode 100644 index 00000000..f0b76722 --- /dev/null +++ b/tests/yaml/multi_document.yaml @@ -0,0 +1,11 @@ +provider: RecreationDotGov +recreation_area: 2907 +start_date: 2026-09-10 +end_date: 2026-09-11 +continuous: false +--- +provider: GoingToCamp +recreation_area: 1 +start_date: 2026-09-10 +end_date: 2026-09-11 +continuous: false \ No newline at end of file diff --git a/tests/yaml/yosemite_search.yaml b/tests/yaml/yosemite_search.yaml new file mode 100644 index 00000000..774a9e05 --- /dev/null +++ b/tests/yaml/yosemite_search.yaml @@ -0,0 +1,5 @@ +provider: RecreationDotGov +recreation_area: 2991 +start_date: 2023-09-15 +end_date: 2023-09-17 +continuous: false \ No newline at end of file