Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions camply/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import logging
import os
import sys
from dataclasses import dataclass
from datetime import date, timedelta
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down
98 changes: 51 additions & 47 deletions camply/utils/yaml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
----------
Expand All @@ -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
28 changes: 28 additions & 0 deletions docs/command_line_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions tests/cli/test_campsites.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
11 changes: 11 additions & 0 deletions tests/yaml/multi_document.yaml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions tests/yaml/yosemite_search.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
provider: RecreationDotGov
recreation_area: 2991
start_date: 2023-09-15
end_date: 2023-09-17
continuous: false