Skip to content

Commit 6620f4c

Browse files
acaloiarojuftin
authored andcommitted
✨ GoingToCamp Provider 🏕 (#99)
* Add going to camp support * Fix type hints for _fetch_nested_keys * Add missing 'provider' argument to 'campsites()' * Fix invalid equipment parameter name * Fix provider config for recreation-areas * flake8 can get some satisfaction * Add python tests for going to camp search * Fix recursive resource fetch bug * Revert back to requipment-id instead of changing the --equipment API * Check off the final TODO list * Update camply/search/search_going_to_camp.py * Update camply/search/search_going_to_camp.py * Refactor 'self->cls' in validator * Add multiple search window support to search library * Coerce start date to 'today' when it's in the past * Skip all sites with zero capacity
1 parent 1211ae2 commit 6620f4c

15 files changed

Lines changed: 1206 additions & 33 deletions

camply/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ._version import __application__, __version__
66
from .config import EquipmentOptions
77
from .containers import AvailableCampsite, SearchWindow
8-
from .providers import RecreationDotGov, YellowstoneLodging
8+
from .providers import GoingToCampProvider, RecreationDotGov, YellowstoneLodging
99
from .search import SearchRecreationDotGov, SearchYellowstone
1010

1111
__all__ = [
@@ -18,4 +18,5 @@
1818
"SearchWindow",
1919
"AvailableCampsite",
2020
"EquipmentOptions",
21+
"GoingToCampProvider",
2122
]

camply/cli.py

Lines changed: 145 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,24 @@
1212
from rich import traceback
1313

1414
from camply import __application__, __version__
15-
from camply.config import SearchConfig
15+
from camply.config import EquipmentOptions, SearchConfig
1616
from camply.config.logging_config import set_up_logging
1717
from camply.containers import SearchWindow
18-
from camply.providers import RecreationDotGov
18+
from camply.providers import (
19+
GOING_TO_CAMP,
20+
RECREATION_DOT_GOV,
21+
YELLOWSTONE,
22+
GoingToCampProvider,
23+
RecreationDotGov,
24+
)
1925
from camply.search import CAMPSITE_SEARCH_PROVIDER, SearchYellowstone
2026
from camply.utils import configure_camply, log_camply, make_list, yaml_utils
27+
from camply.utils.logging_utils import log_sorted_response
2128

2229
logging.Logger.camply = log_camply
2330
logger = logging.getLogger(__name__)
2431

25-
DEFAULT_CAMPLY_PROVIDER: str = "RecreationDotGov"
32+
DEFAULT_CAMPLY_PROVIDER: str = RECREATION_DOT_GOV
2633

2734

2835
@dataclass
@@ -39,8 +46,9 @@ class CamplyContext:
3946
"--provider",
4047
show_default=False,
4148
default=None,
42-
help="Camping Search Provider. Options available are 'Yellowstone' and "
43-
"'RecreationDotGov'. Defaults to 'RecreationDotGov', not case-sensitive.",
49+
help="Camping Search Provider. Options available are 'Yellowstone', "
50+
"'RecreationDotGov', and 'GoingToCamp'. Defaults to 'RecreationDotGov'"
51+
", not case-sensitive.",
4452
)
4553
debug_option = click.option(
4654
"--debug/--no-debug", default=None, help="Enable extra debugging output"
@@ -62,12 +70,36 @@ def _set_up_debug(debug: Optional[bool] = None) -> None:
6270
traceback.install(show_locals=debug)
6371

6472

73+
def _preferred_provider(context: CamplyContext, command_provider: Optional[str]) -> str:
74+
"""
75+
Called to get the preferred subcommands provider.
76+
77+
It establishes rules for the "preferred" provider. That is, when multiple
78+
providers are elgigible to serve a command, the one that is most specific is
79+
chosen. Preference is in the following order:
80+
81+
1. The provider explicitly provided to a camply subcommand
82+
2. The provider associated with CamplyContext
83+
3. The default provider
84+
85+
The preferred provider is returned
86+
"""
87+
if command_provider:
88+
return command_provider.lower()
89+
elif command_provider is None and context.provider:
90+
return context.provider.lower()
91+
else:
92+
return DEFAULT_CAMPLY_PROVIDER.lower()
93+
94+
6595
@click.group()
6696
@click.version_option(version=__version__, prog_name=__application__)
67-
@provider_argument
6897
@debug_option
98+
@provider_argument
6999
@click.pass_context
70-
def camply_command_line(ctx: click.core.Context, provider: str, debug: bool) -> None:
100+
def camply_command_line(
101+
ctx: click.core.Context, debug: bool, provider: Optional[str]
102+
) -> None:
71103
"""
72104
Welcome to camply, the campsite finder.
73105
@@ -128,34 +160,91 @@ def configure(context: CamplyContext, debug: bool) -> None:
128160
)
129161

130162

163+
@camply_command_line.command()
164+
@rec_area_argument
165+
@provider_argument
166+
@click.pass_obj
167+
def equipment_types(
168+
context: CamplyContext,
169+
rec_area: Optional[int] = None,
170+
provider: str = DEFAULT_CAMPLY_PROVIDER,
171+
) -> None:
172+
"""
173+
Retrieve a list of equipment supported by the current provider/recreaton area
174+
175+
Equipment are camping equipment that can be used at a campsite. Different providers
176+
and recreation areas have different types of equipment for which reservations can be made.
177+
"""
178+
provider = _preferred_provider(context, provider)
179+
if not rec_area and provider == GOING_TO_CAMP:
180+
logger.error(
181+
"This provider requires --rec-area to be specified when listing equipment types"
182+
)
183+
exit(1)
184+
185+
if provider == GOING_TO_CAMP:
186+
GoingToCampProvider().list_equipment_types(rec_area[0])
187+
else:
188+
log_sorted_response(response_array=EquipmentOptions.__all_accepted_equipment__)
189+
190+
exit(0)
191+
192+
131193
@camply_command_line.command()
132194
@search_argument
133195
@state_argument
134196
@debug_option
197+
@provider_argument
135198
@click.pass_obj
136199
def recreation_areas(
137-
context: CamplyContext, search: Optional[str], state: Optional[str], debug: bool
200+
context: CamplyContext,
201+
search: Optional[str],
202+
state: Optional[str],
203+
debug: bool,
204+
provider: str = DEFAULT_CAMPLY_PROVIDER,
138205
) -> None:
139206
"""
140207
Search for Recreation Areas and list them
141208
142209
Search for Recreation Areas and their IDs. Recreation Areas are places like
143210
National Parks and National Forests that can contain one or many campgrounds.
144211
"""
212+
provider = _preferred_provider(context, provider)
145213
if context.debug is None:
146214
context.debug = debug
147215
_set_up_debug(debug=context.debug)
148-
if all([search is None, state is None]):
216+
217+
# Recreation dot gov and yellowstone require --state or --search, but going to
218+
# camp does not, since all of its "rec areas" are a very few.
219+
if all([search is None, state is None, provider != GOING_TO_CAMP]):
149220
logger.error(
150-
"You must add a --search or --state parameter to search "
151-
"for Recreation Areas."
221+
"You must add a --search, --state, or --provider parameter "
222+
"to search for Recreation Areas."
152223
)
153224
exit(1)
154-
camp_finder = RecreationDotGov()
225+
if all([search is None, state is not None, provider == GOING_TO_CAMP]):
226+
logger.error(
227+
"GoingToCamp does not support filtering recreation areas by state. Leave --state blank."
228+
)
229+
exit(1)
230+
231+
camp_provider = None
232+
if provider == RECREATION_DOT_GOV:
233+
camp_provider = RecreationDotGov()
234+
elif provider == GOING_TO_CAMP:
235+
camp_provider = GoingToCampProvider()
236+
else:
237+
logger.error(
238+
"The provider you specified does not exist or does notsupport"
239+
"listing recreation areas. See --help for available providers"
240+
)
241+
exit(1)
242+
155243
params = dict()
156244
if state is not None:
157245
params.update(dict(state=state))
158-
camp_finder.find_recreation_areas(search_string=search, **params)
246+
247+
camp_provider.find_recreation_areas(search_string=search, **params)
159248

160249

161250
@camply_command_line.command()
@@ -175,7 +264,7 @@ def campgrounds(
175264
rec_area: Optional[int] = None,
176265
campground: Optional[int] = None,
177266
campsite: Optional[int] = None,
178-
provider: Optional[str] = "RecreationDotGov",
267+
provider: str = DEFAULT_CAMPLY_PROVIDER,
179268
) -> None:
180269
"""
181270
Search for Campgrounds (inside of Recreation Areas) and list them
@@ -185,13 +274,12 @@ def campgrounds(
185274
multiple campsites, others are facilities like fire towers or cabins that might only
186275
contain a single 'campsite' to book.
187276
"""
277+
provider = _preferred_provider(context, provider)
188278
if context.debug is None:
189279
context.debug = debug
190280
_set_up_debug(debug=context.debug)
191-
if context.provider is None:
192-
context.provider = provider
193-
provider = DEFAULT_CAMPLY_PROVIDER if context.provider is None else context.provider
194-
if provider.lower() == "yellowstone":
281+
282+
if provider == YELLOWSTONE:
195283
SearchYellowstone.print_campgrounds()
196284
exit(0)
197285
if all(
@@ -208,6 +296,20 @@ def campgrounds(
208296
"or --rec-area parameter to search for Campgrounds."
209297
)
210298
exit(1)
299+
300+
if provider == YELLOWSTONE:
301+
SearchYellowstone.print_campgrounds()
302+
exit(0)
303+
if provider == GOING_TO_CAMP:
304+
if len(rec_area) == 0:
305+
logger.error("You must specify at least one --rec-area")
306+
exit(1)
307+
rec_area_id = int(rec_area[0])
308+
GoingToCampProvider().find_facilities_per_recreation_area(
309+
rec_area_id=rec_area_id, campground_id=campground, search_string=search
310+
)
311+
exit(0)
312+
211313
camp_finder = RecreationDotGov()
212314
params = dict()
213315
if state is not None:
@@ -310,6 +412,19 @@ def campgrounds(
310412
"equipment names include `Tent`, `RV`. `Trailer`, `Vehicle` and are "
311413
"not case-sensitive.",
312414
)
415+
equipment_id_argument = click.option(
416+
"--equipment-id",
417+
default=None,
418+
help="""
419+
Search for campsites campaitble with specific equipment categories. Going To
420+
Camp uses equipment category IDs for filtering campsites by equipment. Every
421+
recreation area has equipment categories unique to it.
422+
423+
Use `camply equipment-types --provider goingtocamp --rec-area <rec area id>`
424+
to get a listing of equipment for an area.
425+
""",
426+
)
427+
313428
offline_search_argument = click.option(
314429
"--offline-search",
315430
is_flag=True,
@@ -359,7 +474,7 @@ def _validate_campsites(
359474
"""
360475
Validate the campsites portion of the CLI
361476
"""
362-
if provider.lower() == "recreationdotgov" and all(
477+
if provider == RECREATION_DOT_GOV and all(
363478
[
364479
len(rec_area) == 0,
365480
len(campground) == 0,
@@ -401,9 +516,10 @@ def _validate_campsites(
401516
@notifications_argument
402517
@polling_interval_argument
403518
@continuous_argument
404-
@provider_argument
405519
@equipment_argument
520+
@equipment_id_argument
406521
@nights_argument
522+
@provider_argument
407523
@weekends_argument
408524
@end_date_argument
409525
@start_date_argument
@@ -422,16 +538,17 @@ def campsites(
422538
end_date: Optional[str] = None,
423539
weekends: bool = False,
424540
nights: int = 1,
425-
provider: str = "RecreationDotGov",
541+
provider: str = DEFAULT_CAMPLY_PROVIDER,
426542
continuous: bool = False,
427543
polling_interval: int = SearchConfig.RECOMMENDED_POLLING_INTERVAL,
428544
notifications: Union[str, List[str]] = "silent",
429545
notify_first_try: bool = False,
430546
search_forever: bool = False,
431547
yaml_config: Optional[str] = None,
432-
equipment: Optional[List[str]] = None,
433548
offline_search: bool = False,
434549
offline_search_path: Optional[str] = None,
550+
equipment: Optional[Union[str, int]] = None,
551+
equipment_id: Optional[Union[str, int]] = None,
435552
) -> None:
436553
"""
437554
Find available Campsites using search criteria
@@ -443,12 +560,11 @@ def campsites(
443560
functionality can be enabled with `--continuous` and notifications can be enabled using
444561
`--notifications`.
445562
"""
563+
provider = _preferred_provider(context, provider)
446564
if context.debug is None:
447565
context.debug = debug
448566
_set_up_debug(debug=context.debug)
449-
if context.provider is None:
450-
context.provider = provider
451-
provider = DEFAULT_CAMPLY_PROVIDER if context.provider is None else context.provider
567+
452568
notifications = make_list(notifications)
453569
_validate_campsites(
454570
rec_area=rec_area,
@@ -470,8 +586,8 @@ def campsites(
470586
provider, provider_kwargs, search_kwargs = yaml_utils.yaml_file_to_arguments(
471587
file_path=yaml_config
472588
)
473-
else:
474589
provider = provider.lower()
590+
else:
475591
search_window = SearchWindow(
476592
start_date=datetime.strptime(start_date, "%Y-%m-%d"),
477593
end_date=datetime.strptime(end_date, "%Y-%m-%d"),
@@ -483,9 +599,10 @@ def campsites(
483599
campsites=make_list(campsite),
484600
weekends_only=weekends,
485601
nights=int(nights),
486-
equipment=make_list(equipment),
487602
offline_search=offline_search,
488603
offline_search_path=offline_search_path,
604+
equipment=equipment,
605+
equipment_id=equipment_id,
489606
)
490607
search_kwargs = dict(
491608
log=True,
@@ -498,7 +615,7 @@ def campsites(
498615
)
499616
provider_class = {
500617
key.lower(): value for key, value in CAMPSITE_SEARCH_PROVIDER.items()
501-
}[provider.lower()]
618+
}[provider]
502619
camping_finder = provider_class(**provider_kwargs)
503620
camping_finder.get_matching_campsites(**search_kwargs)
504621

camply/containers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .base_container import CamplyModel
66
from .data_containers import (
77
AvailableCampsite,
8+
AvailableResource,
89
CampgroundFacility,
910
RecreationArea,
1011
SearchWindow,
@@ -13,6 +14,7 @@
1314
__all__ = [
1415
"CamplyModel",
1516
"AvailableCampsite",
17+
"AvailableResource",
1618
"CampgroundFacility",
1719
"RecreationArea",
1820
"SearchWindow",

camply/containers/base_container.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,17 @@ class RecDotGovEquipment(CamplyModel):
6161

6262
equipment_name: str
6363
max_length: float
64+
65+
66+
#############################
67+
# GoingToCamp Base Containers
68+
#############################
69+
70+
71+
class GoingToCampEquipment(CamplyModel):
72+
"""
73+
Model of GoingToCamp provider equipment
74+
"""
75+
76+
equipment_name: str
77+
equipment_type_id: int

0 commit comments

Comments
 (0)