From 55b76fa76ea4e40e74c18bb3c297a61652e8ae6c Mon Sep 17 00:00:00 2001 From: Justin Flannery <49741340+juftin@users.noreply.github.com> Date: Thu, 3 Jun 2021 08:54:15 -0600 Subject: [PATCH] camply 0.1.5 (#18) * version bump -> 0.1.5 * pushbullet example * Yellowstone Campground Filtering * fix error * rmnp test * single day search * yellwsnte doc clarification * PyYaml inclusion * config strings for Yellowstone * second yml test * YML File Path expander * Changelog update * Changelog bullet * debug logging for ubuntu * let yellowstone timeout * fix parallel jobs * 30 second timeout + less frequent testing * CHANGELOG.md update * YML Cleanup --- .github/workflows/docker-image-ci.yml | 4 +- .github/workflows/pylint.yml | 2 - .github/workflows/pytest.yml | 52 +++------------ CHANGELOG.md | 18 +++++ Dockerfile | 2 +- README.md | 34 ++++++++-- camply/__init__.py | 2 +- camply/config/yellowstone_config.py | 27 +++----- .../recreation_dot_gov/campsite_search.py | 5 +- .../providers/xanterra/yellowstone_lodging.py | 2 +- camply/search/search_yellowstone.py | 65 +++++++++++++++++-- camply/utils/yaml_utils.py | 9 ++- pyproject.toml | 2 +- tests/yml/example_search.yml | 8 +++ 14 files changed, 148 insertions(+), 84 deletions(-) create mode 100644 tests/yml/example_search.yml diff --git a/.github/workflows/docker-image-ci.yml b/.github/workflows/docker-image-ci.yml index 15e2463d..40143f13 100644 --- a/.github/workflows/docker-image-ci.yml +++ b/.github/workflows/docker-image-ci.yml @@ -1,10 +1,8 @@ name: Docker Image CI on: - push: - branches: [ "**" ] pull_request: - branches: [ main ] + branches: [ "**" ] jobs: diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index ae41cf0c..512f6ce8 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -3,8 +3,6 @@ name: PyLint on: push: branches: [ "**" ] - pull_request: - branches: [ main ] jobs: build: diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 547aa2d9..b6fe8f37 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,18 +1,20 @@ name: Tests on: - push: - branches: [ "**" ] pull_request: - branches: [ main ] + branches: [ "**" ] jobs: - ubuntu: + pytest: runs-on: ubuntu-latest strategy: - fail-fast: false + fail-fast: false matrix: python-version: [ 3.6, 3.7, 3.8, 3.9 ] + max-parallel: 1 + env: + LOG_LEVEL: DEBUG + RIDB_API_KEY: ${{ secrets.RIDB_API_KEY }} steps: - uses: actions/checkout@v2 - name: Set up Python Environment ${{ matrix.python-version }} @@ -40,41 +42,5 @@ jobs: camply campgrounds --search "Fire Tower Lookout" --state CA camply campsites --rec-area 2991 --start-date 2021-09-15 --end-date 2021-09-16 camply campsites --campground 252037 --start-date 2021-09-15 --end-date 2021-09-16 - - windows: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-version: [ 3.6, 3.7, 3.8, 3.9 ] - steps: - - uses: actions/checkout@v2 - - name: Set up Python Environment ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install flake8 pytest pylint - pip install . - shell: cmd - - name: "Lint (flake8)" - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - shell: cmd - - name: "Unit Tests (pytest)" - run: | - pytest tests/ - shell: cmd - - name: "Unit Tests (commandline)" - run: | - camply recreation-areas --search "Yosemite National Park" - camply campgrounds --rec-area 2991 - camply campgrounds --search "Fire Tower Lookout" --state CA - camply campsites --rec-area 2991 --start-date 2021-09-15 --end-date 2021-09-16 - camply campsites --campground 252037 --start-date 2021-09-15 --end-date 2021-09-16 - shell: cmd \ No newline at end of file + camply campsites --provider yellowstone --start-date 2021-09-10 --end-date 2021-09-15 --campground YLYC:RV + camply campsites --yml-config tests/yml/example_search.yml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3394f8..00959e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ versioning. ## [Unreleased] +To request new features or bugfixes the +[Camply Feedback for v1.0.0 Discussion](https://github.com/juftin/camply/discussions/12) is the best +place to go. + +## [0.1.5] - 2021-06-02 + +### Added + +- Ability to filter on Yellowstone Campgrounds. See + this [discussion](https://github.com/juftin/camply/discussions/15#discussioncomment-783657) for + more detail. + +### Fixed + +- Allow endpoints to timeout after being unresponsive for 30 seconds + ## [0.1.4] - 2021-06-01 ### Added @@ -60,6 +76,8 @@ versioning. [unreleased]: https://github.com/juftin/camply/compare/main...integration +[0.1.5]: https://github.com/juftin/camply/compare/v0.1.4...v0.1.5 + [0.1.4]: https://github.com/juftin/camply/compare/v0.1.3...v0.1.4 [0.1.3]: https://github.com/juftin/camply/compare/v0.1.2...v0.1.3 diff --git a/Dockerfile b/Dockerfile index 976b3381..39b92e17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.8-slim MAINTAINER Justin Flannery -LABEL version="0.1.4" +LABEL version="0.1.5" LABEL description="camply, the campsite finder" COPY . /tmp/camply diff --git a/README.md b/README.md index 98347abe..355b5e29 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ ___________ + [Searching for a Campsite by Campground ID](#searching-for-a-campsite-by-campground-id) + [Continuously Searching for A Campsite](#continuously-searching-for-a-campsite) + [Continue Looking After The First Match Is Found](#continue-looking-after-the-first-match-is-found) + + [Send a Push Notification](#send-a-push-notification) + [Look for Weekend Campsite Availabilities](#look-for-weekend-campsite-availabilities) + [Look for a Campsite Inside of Yellowstone](#look-for-a-campsite-inside-of-yellowstone) + [Look for a Campsite Across Multiple Recreation areas](#look-for-a-campsite-across-multiple-recreation-areas) @@ -145,11 +146,11 @@ and a link to make the booking. Required parameters include `--start-date`, `--e * `--notifications`: `NOTIFICATIONS` + If `--continuous` is activated, types of notifications to receive. Options available are `email`, `pushover`, `pushbullet`, or `silent`. Defaults to `silent` - which just logs - messages to console. [**_example_](#continuously-searching-for-a-campsite) + messages to console. [**_example_](#send-a-push-notification) * `--notify-first-try` + If `--continuous` is activated, whether to send all non-silent notifications if more than 5 - matching campsites are found on the first try. Defaults to false which only sends the first - 5. [**_example_](#continuously-searching-for-a-campsite) + matching campsites are found on the first try. Defaults to false which only sends + the first 5. [**_example_](#continuously-searching-for-a-campsite) * `--search-forever` + If `--continuous` is activated, this method continues to search after the first availability has been found. The one caveat is that it will never notify about the same identical campsite @@ -299,6 +300,25 @@ camply campsites \ --search-forever ``` +#### Send a Push Notification + +camply supports notifications via `Pushbullet`, `Pushover`, and `Email`. Pushbullet is a great +option because it's +a [free and easy service to sign up for](https://www.pushbullet.com/#settings/account) +and it supports notifications across different devices and operating systems. Similar to `Pushover`, +`Pushbullet` requires that you create an account and an API token, and share that token with camply +through a [configuration file](docs/examples/example.camply) (via the `camply configure` +command) or though environment variables (`PUSHBULLET_API_TOKEN`). + +```text +camply campsites \ + --rec-area 2991 \ + --start-date 2021-09-10 \ + --end-date 2021-09-20 \ + --continuous \ + --notifications pushbullet +``` + #### Look for Weekend Campsite Availabilities This below search looks across larger periods of time, but only if a campground is available to book @@ -320,8 +340,10 @@ camply campsites \ Yellowstone doesn't use https://recreation.gov to manage its campgrounds, instead it uses its own proprietary system. In order to search the Yellowstone API for campsites, make sure to pass -the `--provider "yellowstone"` argument. This flag disables `--rec-area` and `--campground` -arguments. +the `--provider "yellowstone"` argument. This flag disables `--rec-area` argument. + +To learn more about using `camply` to find campsites at Yellowstone, check out +this [discussion](https://github.com/juftin/camply/discussions/15#discussioncomment-783657). ```text camply campsites \ @@ -580,6 +602,8 @@ dependencies: - [python-dotenv](https://github.com/theskumar/python-dotenv) - The `python-dotenv` package reads key-value pairs from a `.env` file and can set them as environment variables - this helps with the `.camply` configuration file. +- [PyYAML](https://pyyaml.org/) + - PyYAML is a YAML parsing library - this helps with the YAML file campsite searches. ___________ ___________ diff --git a/camply/__init__.py b/camply/__init__.py index 5e767783..b26fa0f7 100644 --- a/camply/__init__.py +++ b/camply/__init__.py @@ -6,4 +6,4 @@ camply __init__ file """ -__version__ = "0.1.4" +__version__ = "0.1.5" diff --git a/camply/config/yellowstone_config.py b/camply/config/yellowstone_config.py index bcd93bd2..b46a0726 100644 --- a/camply/config/yellowstone_config.py +++ b/camply/config/yellowstone_config.py @@ -7,7 +7,7 @@ """ import logging -from typing import List, Tuple +from typing import Dict, List, Tuple from .data_columns import DataColumns @@ -68,25 +68,18 @@ class YellowstoneConfig(DataColumns): YELLOWSTONE_RECREATION_AREA_ID: int = 1 YELLOWSTONE_RECREATION_AREA_NAME: str = "Yellowstone" + YELLOWSTONE_RECREATION_AREA_FORMAL_NAME: str = "Yellowstone National Park, USA" YELLOWSTONE_LOOP_NAME: str = "N/A" CAMPSITE_AVAILABILITY_STATUS: str = "Available" YELLOWSTONE_CAMPGROUND_NAME_REPLACE: Tuple[str, str] = ("CG Internet Rate", "Campground") YELLOWSTONE_TIMEZONE: str = "America/Denver" - @staticmethod - def get_polling_interval(interval: int) -> int: - """ - Ensure the Polling Interval never exceeds the minimum set - - Returns - ------- - int - """ - if interval < YellowstoneConfig.MINIMUM_POLLING_INTERVAL: - logger.warning("Polling interval is too short, setting " - f"to {YellowstoneConfig.MINIMUM_POLLING_INTERVAL} minutes") - return_interval = YellowstoneConfig.MINIMUM_POLLING_INTERVAL - else: - return_interval = interval - return return_interval + # LODGES: https://webapi.xanterra.net/v1/api/property/hotels/yellowstonenationalparklodges + YELLOWSTONE_CAMPGROUNDS: Dict[str, str] = { + "YLYC:RV": "Canyon Campground", + "YLYB:RV": "Bridge Bay Campground", + "YLYG:RV": "Grant Campground", + "YLYM:RV": "Madison Campground", + "YLYF:RV": "Fishing Bridge RV Park" + } diff --git a/camply/providers/recreation_dot_gov/campsite_search.py b/camply/providers/recreation_dot_gov/campsite_search.py index e030208c..ebee0f3a 100644 --- a/camply/providers/recreation_dot_gov/campsite_search.py +++ b/camply/providers/recreation_dot_gov/campsite_search.py @@ -256,7 +256,7 @@ def _ridb_get_data(self, path: str, params: Optional[dict] = None) -> Union[dict """ api_endpoint = self._ridb_get_endpoint(path=path) response = requests.get(url=api_endpoint, headers=self._ridb_api_headers, - params=params) + params=params, timeout=30) try: assert response.status_code == 200 except AssertionError: @@ -482,7 +482,8 @@ def _make_recdotgov_request(self, campground_id: int, month: datetime) -> reques headers.update(choice(USER_AGENTS)) headers.update(RecreationBookingConfig.API_REFERRERS) response = requests.get(url=api_endpoint, headers=headers, - params=dict(start_date=formatted_month)) + params=dict(start_date=formatted_month), + timeout=30) assert response.status_code == 200 except AssertionError: response_error = response.text diff --git a/camply/providers/xanterra/yellowstone_lodging.py b/camply/providers/xanterra/yellowstone_lodging.py index a4ea1836..693cffe2 100755 --- a/camply/providers/xanterra/yellowstone_lodging.py +++ b/camply/providers/xanterra/yellowstone_lodging.py @@ -83,7 +83,7 @@ def _try_retry_get_data(endpoint: str, params: Optional[dict] = None): yellowstone_headers.update(YellowstoneConfig.API_REFERRERS) response = requests.get(url=endpoint, headers=yellowstone_headers, - params=params) + params=params, timeout=30) if response.status_code == 200 and response.text.strip() != "": return loads(response.content) else: diff --git a/camply/search/search_yellowstone.py b/camply/search/search_yellowstone.py index ac5cb47a..d2eed7f4 100644 --- a/camply/search/search_yellowstone.py +++ b/camply/search/search_yellowstone.py @@ -8,11 +8,12 @@ from datetime import datetime import logging -from typing import List, Union +from typing import List, Optional, Set, Union +from camply.config import YellowstoneConfig from camply.containers import AvailableCampsite, SearchWindow from camply.providers import YellowstoneLodging -from camply.search.base_search import BaseCampingSearch +from camply.search.base_search import BaseCampingSearch, SearchError logger = logging.getLogger(__name__) @@ -25,6 +26,7 @@ class SearchYellowstone(BaseCampingSearch): # noinspection PyUnusedLocal def __init__(self, search_window: Union[SearchWindow, List[SearchWindow]], weekends_only: bool = False, + campgrounds: Optional[Union[List[str], str]] = None, **kwargs) -> None: """ Initialize with Search Parameters @@ -33,15 +35,16 @@ def __init__(self, search_window: Union[SearchWindow, List[SearchWindow]], ---------- search_window: Union[SearchWindow, List[SearchWindow]] Search Window tuple containing start date and End Date - campgrounds: Optional[Union[List[int], int]] - Campground ID or List of Campground IDs weekends_only: bool Whether to only search for Camping availabilities on the weekends (Friday / Saturday nights) + campgrounds: Optional[Union[List[str], str]] + Campground ID or List of Campground IDs """ super().__init__(provider=YellowstoneLodging(), search_window=search_window, weekends_only=weekends_only) + self.campgrounds = self._make_list(campgrounds) def get_all_campsites(self) -> List[AvailableCampsite]: """ @@ -52,8 +55,60 @@ def get_all_campsites(self) -> List[AvailableCampsite]: List[AvailableCampsite] """ all_campsites = list() + searchable_campgrounds = self._get_searchable_campgrounds() this_month = datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0) for month in self.search_months: if month >= this_month: all_campsites += self.campsite_finder.get_monthly_campsites(month=month) - return all_campsites + matching_campsites = self._filter_campsites_to_campgrounds( + campsites=all_campsites, searchable_campgrounds=searchable_campgrounds) + return matching_campsites + + def _get_searchable_campgrounds(self) -> Optional[Set[str]]: + """ + Return the Campgrounds for the Camping Search + + Returns + ------- + Optional[Set[str]] + """ + if self.campgrounds is None: + return None + supported_campsites = set(YellowstoneConfig.YELLOWSTONE_CAMPGROUNDS.keys()) + selected_campsites = set(self.campgrounds) + searchable_campgrounds = supported_campsites.intersection(selected_campsites) + if len(searchable_campgrounds) == 0: + campground_ids = [f"`{key}` ({value})" for key, value in + YellowstoneConfig.YELLOWSTONE_CAMPGROUNDS.items()] + error_message = ("You must supply a YellowstoneNationalParkLodges supported " + "campground ID. Current supported Campground IDs: " + f"{', '.join(campground_ids)}") + logger.error(error_message) + raise SearchError(error_message) + logger.info(f"{len(searchable_campgrounds)} Matching Campgrounds Found") + for campground in searchable_campgrounds: + logger.info(f"⛰ {YellowstoneConfig.YELLOWSTONE_RECREATION_AREA_FORMAL_NAME} " + f"(#{YellowstoneConfig.YELLOWSTONE_RECREATION_AREA_ID}) - 🏕 " + f"{YellowstoneConfig.YELLOWSTONE_CAMPGROUNDS[campground]} ({campground})") + return searchable_campgrounds + + def _filter_campsites_to_campgrounds( + self, campsites: List[AvailableCampsite], + searchable_campgrounds: Set[str]) -> List[AvailableCampsite]: + """ + Filter Campsites Down to Matching Campgrounds + + Parameters + ---------- + campsites: List[AvailableCampsite] + searchable_campgrounds: Set[str] + + Returns + ------- + List[AvailableCampsite] + """ + if self.campgrounds is None: + return campsites + matching_campsites = [campsite for campsite in campsites if + campsite.facility_id in searchable_campgrounds] + return matching_campsites diff --git a/camply/utils/yaml_utils.py b/camply/utils/yaml_utils.py index f7162dd4..20013916 100644 --- a/camply/utils/yaml_utils.py +++ b/camply/utils/yaml_utils.py @@ -8,7 +8,8 @@ from datetime import datetime import logging -from os import getenv +import os +from pathlib import Path from re import compile from typing import Dict, Tuple @@ -40,6 +41,7 @@ def read_yml(path: str = None): log_path: "/var/${LOG_PATH}" something_else: "${AWESOME_ENV_VAR}/var/${A_SECOND_AWESOME_VAR}" """ + path = os.path.abspath(path) pattern = compile(r".*?\${(\w+)}.*?") safe_loader = SafeLoader @@ -57,9 +59,9 @@ def env_var_constructor(safe_loader: object, node: object): match = pattern.findall(string=value) if match: full_value = value - for g in match: + for item in match: full_value = full_value.replace( - "${{{key}}}".format(key=g), getenv(key=g, default=g)) + "${{{key}}}".format(key=item), os.getenv(key=item, default=item)) return full_value return value @@ -83,6 +85,7 @@ def yaml_file_to_arguments(file_path: str) -> Tuple[str, Dict[str, object], Dict Tuple containing provider string, provider **kwargs, and search **kwargs """ yaml_search = read_yml(path=file_path) + logger.info(f"YML File Parsed: {Path(file_path).name}") provider = yaml_search.get("provider", "RecreationDotGov") start_date = datetime.strptime(str(yaml_search["start_date"]), "%Y-%m-%d") end_date = datetime.strptime(str(yaml_search["end_date"]), "%Y-%m-%d") diff --git a/pyproject.toml b/pyproject.toml index f5aa719d..ddf664b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "camply" -version = "0.1.4" +version = "0.1.5" description = "camply, the campsite finder ⛺️" authors = ["Justin Flannery "] maintainers = ["Justin Flannery "] diff --git a/tests/yml/example_search.yml b/tests/yml/example_search.yml new file mode 100644 index 00000000..fb3b6067 --- /dev/null +++ b/tests/yml/example_search.yml @@ -0,0 +1,8 @@ +provider: RecreationDotGov # RecreationDotGov IF NOT PROVIDED +recreation_area: # (LIST OR SINGLE ENTRY) + - 2907 # ROCKY MOUNTAIN NATIONAL PARK +campgrounds: null # OVERRIDES RECREATION AREA (LIST OR SINGLE ENTRY) +start_date: 2021-09-10 # YYYY-MM-DD +end_date: 2021-09-10 # YYYY-MM-DD +weekends: False # FALSE BY DEFAULT +continuous: False # DEFAULTS TO TRUE