diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 88815344..a98755d5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,4 @@ blank_issues_enabled: false contact_links: - name: Have a question about how to use camply? url: https://github.com/juftin/camply/discussions - about: Check out camply discussions. There's lots of useful stuff there. \ No newline at end of file + about: Check out camply discussions. There's lots of useful stuff there. diff --git a/.github/config/yamllint.yml b/.github/config/yamllint.yml index a8bc4117..fe1a696d 100644 --- a/.github/config/yamllint.yml +++ b/.github/config/yamllint.yml @@ -18,5 +18,3 @@ rules: max: 120 truthy: check-keys: false -ignore: | - tests/models/cassettes/ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cca818c5..94b9ee6b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,20 +19,27 @@ jobs: - name: Setup Node Dependency uses: actions/setup-node@v2 - name: Install Python Dependencies - run: | - python -m pip install --upgrade pip wheel - python -m pip install tox + run: | + python -m pip install --upgrade pip wheel + python -m pip install tox - name: Install Node Dependencies - run: | - npm i -g eslint eslint-plugin-markdown - npm install eslint-plugin-markdown@latest --save-dev - - name: Lint with Flake8 - id: flake8 + run: | + npm i -g eslint eslint-plugin-markdown + npm install eslint-plugin-markdown@latest --save-dev + - name: Lint with ESLint + id: eslint continue-on-error: true - run: | - echo "::add-matcher::.github/matchers/flake8.json" - tox -e flake8 - echo "::remove-matcher owner=flake8::" + run: | + echo "::add-matcher::.github/matchers/eslint.json" + eslint . --config "${{ github.workspace }}/.github/config/.eslintrc.js" + echo "::remove-matcher owner=eslint::" + - name: Lint with Flake8 + id: flake8 + continue-on-error: true + run: | + echo "::add-matcher::.github/matchers/flake8.json" + tox -e flake8 + echo "::remove-matcher owner=flake8::" - name: Lint with YAML-Lint id: yamllint continue-on-error: true @@ -47,22 +54,15 @@ jobs: echo "::add-matcher::.github/matchers/mypy.json" tox -e mypy echo "::remove-matcher owner=mypy::" - - name: Lint with ESLint - id: eslint - continue-on-error: true - run: | - echo "::add-matcher::.github/matchers/eslint.json" - eslint . --config "${{ github.workspace }}/.github/config/.eslintrc.js" - echo "::remove-matcher owner=eslint::" - - name: (Don't actualy) Raise Errors For Linting Failures - if: | - steps.flake8.outcome != 'success' || - steps.yamllint.outcome != 'success' || - steps.mypy.outcome != 'success' || - steps.eslint.outcome != 'success' - run: | - echo "Flake8: ${{ steps.flake8.outcome }}" - echo "YAML-Lint: ${{ steps.yamllint.outcome }}" - echo "MyPy: ${{ steps.mypy.outcome }}" - echo "ESLint: ${{ steps.eslint.outcome }}" - echo "I Should be Raising an Error" + - name: Raise Errors For Linting Failures + if: | + steps.flake8.outcome != 'success' || + steps.yamllint.outcome != 'success' || + steps.eslint.outcome != 'success' + run: | + echo "Flake8: ${{ steps.flake8.outcome }}" + echo "MyPy: ${{ steps.mypy.outcome }}" + echo "YamlLint: ${{ steps.yamllint.outcome }}" + echo "EsLint: ${{ steps.eslint.outcome }}" + echo "I Should Really Be Enforcing MyPy Errors + exit 1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 81b42a44..2712318d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -65,5 +65,5 @@ jobs: id: docker_build uses: docker/build-push-action@v2 with: - push: True + push: true tags: juftin/camply:latest,juftin/camply:${{ env.CAMPLY_VERSION }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4aff2a22..5103f331 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,9 @@ on: - main pull_request: branches: [ "**" ] + paths: + - "camply/**" + - "!camply/README.md" schedule: - cron: "0 12 1 * *" jobs: diff --git a/camply/_version.py b/camply/_version.py index 9bc1a05f..b6dcc334 100644 --- a/camply/_version.py +++ b/camply/_version.py @@ -2,4 +2,4 @@ camply __version__ file """ -__version__ = "0.1.14" +__version__ = "0.1.15" diff --git a/camply/config/__init__.py b/camply/config/__init__.py index 400cca23..6a9bfc90 100644 --- a/camply/config/__init__.py +++ b/camply/config/__init__.py @@ -13,3 +13,19 @@ from .notification_config import EmailConfig, PushbulletConfig, PushoverConfig from .search_config import SearchConfig from .yellowstone_config import YellowstoneConfig + +__all__ = [ + "RecreationBookingConfig", + "RIDBConfig", + "STANDARD_HEADERS", + "USER_AGENTS", + "CommandLineConfig", + "CampsiteContainerFields", + "DataColumns", + "FileConfig", + "EmailConfig", + "PushbulletConfig", + "PushoverConfig", + "SearchConfig", + "YellowstoneConfig", +] diff --git a/camply/config/api_config.py b/camply/config/api_config.py index 674f7c1b..56236fd7 100644 --- a/camply/config/api_config.py +++ b/camply/config/api_config.py @@ -7,7 +7,7 @@ """ from os import getenv -from typing import Dict, List +from typing import Dict, List, Union from dotenv import load_dotenv @@ -187,9 +187,10 @@ class RIDBConfig: https://ridb.recreation.gov/docs """ + _camply_ridb_service_account_api_token: bytes = \ b'YTc0MTY0NzEtMWI1ZC00YTY0LWFkM2QtYTIzM2U3Y2I1YzQ0' - _api_key: str = getenv("RIDB_API_KEY", _camply_ridb_service_account_api_token) + _api_key: Union[str, bytes] = getenv("RIDB_API_KEY", _camply_ridb_service_account_api_token) API_KEY = _camply_ridb_service_account_api_token if _api_key == "" else _api_key RIDB_SCHEME: str = "https" @@ -229,6 +230,7 @@ class RecreationBookingConfig: """ Variable Storage Class for Recreation.gov Booking API """ + API_SCHEME: str = "https" API_NET_LOC = "www.recreation.gov" API_BASE_PATH: str = "api/camps/availability/campground/" diff --git a/camply/config/cli_config.py b/camply/config/cli_config.py index a822aa86..31b012a2 100644 --- a/camply/config/cli_config.py +++ b/camply/config/cli_config.py @@ -22,6 +22,7 @@ class CommandLineActions: """ ArgParse Actions """ + VERSION: str = "version" STORE: str = "store" STORE_TRUE: str = "store_true" @@ -32,6 +33,7 @@ class CommandLineArguments: """ Argument Config """ + VERSION_ARGUMENT: str = "--version" STATE_ARGUMENT: str = "--state" @@ -81,7 +83,7 @@ class CommandLineArguments: POLLING_INTERVAL_ARGUMENT: str = "--polling-interval" POLLING_INTERVAL_DESTINATION: str = "polling_interval" - POLLING_INTERVAL_DEFAULT: str = SearchConfig.RECOMMENDED_POLLING_INTERVAL + POLLING_INTERVAL_DEFAULT: int = SearchConfig.RECOMMENDED_POLLING_INTERVAL POLLING_INTERVAL_HELP: str = ("If --continuous is activated, how often to wait in between " "checks (in minutes). Defaults to 10, cannot be less than 5.") @@ -118,6 +120,7 @@ class CommandLineValidation: """ Camply CLI Validation Config """ + ERROR_NO_ARGUMENT_FOUND: str = "You must provide an argument to the Camply CLI" ERROR_MESSAGE_RECREATION_AREA: str = ("You must add a --search or --state parameter to search " "for Recreation Areas.") diff --git a/camply/config/data_columns.py b/camply/config/data_columns.py index 6bb97cbd..0406607a 100644 --- a/camply/config/data_columns.py +++ b/camply/config/data_columns.py @@ -11,6 +11,7 @@ class DataColumns: """ Variable Storage Class """ + CAMPSITE_ID_COLUMN: str = "campsite_code" BOOKING_DATE_COLUMN: str = "booking_date" BOOKING_END_DATE_COLUMN: str = "booking_end_date" @@ -30,6 +31,7 @@ class CampsiteContainerFields: """ String Variable Storage Class """ + CAMPSITE_ID: str = "campsite_id" CAMPGROUND_ID: str = "facility_id" BOOKING_DATE: str = "booking_date" diff --git a/camply/config/file_config.py b/camply/config/file_config.py index 696c7df0..d0da162e 100644 --- a/camply/config/file_config.py +++ b/camply/config/file_config.py @@ -9,13 +9,13 @@ from collections import OrderedDict from os.path import abspath, join from pathlib import Path -from typing import List class FileConfig: """ File Path Storage Class """ + HOME_PATH = abspath(Path.home()) DOT_CAMPLY_FILE = join(HOME_PATH, ".camply") _file_config_file = Path(abspath(__file__)) @@ -24,7 +24,7 @@ class FileConfig: CAMPLY_DIRECTORY = _config_dir.parent ROOT_DIRECTORY = CAMPLY_DIRECTORY.parent - DOT_CAMPLY_FIELDS: List[str] = OrderedDict( + DOT_CAMPLY_FIELDS = OrderedDict( PUSHOVER_PUSH_USER=dict(default="", notes="Enables Pushover Notifications"), PUSHBULLET_API_TOKEN=dict(default="", notes="Enables Pushbullet Notifications"), EMAIL_TO_ADDRESS=dict(default="", notes="Email Notifications will be sent here"), diff --git a/camply/config/notification_config.py b/camply/config/notification_config.py index 7e9d2d53..2b554115 100644 --- a/camply/config/notification_config.py +++ b/camply/config/notification_config.py @@ -8,7 +8,7 @@ import logging from os import environ, getenv -from typing import List +from typing import List, Optional from dotenv import load_dotenv @@ -39,15 +39,16 @@ class EmailConfig: """ Email Notification Config Class """ - EMAIL_TO_ADDRESS: str = getenv("EMAIL_TO_ADDRESS", None) + + EMAIL_TO_ADDRESS: Optional[str] = getenv("EMAIL_TO_ADDRESS", None) DEFAULT_FROM_ADDRESS: str = "camply@juftin.com" EMAIL_FROM_ADDRESS: str = getenv("EMAIL_FROM_ADDRESS", DEFAULT_FROM_ADDRESS) DEFAULT_SUBJECT_LINE: str = "Camply Notification" EMAIL_SUBJECT_LINE: str = getenv("EMAIL_SUBJECT_LINE", DEFAULT_SUBJECT_LINE) DEFAULT_SMTP_SERVER: str = "smtp.gmail.com" EMAIL_SMTP_SERVER: str = getenv("EMAIL_SMTP_SERVER", DEFAULT_SMTP_SERVER) - EMAIL_USERNAME: str = getenv("EMAIL_USERNAME", None) - EMAIL_PASSWORD: str = getenv("EMAIL_PASSWORD", None) + EMAIL_USERNAME: Optional[str] = getenv("EMAIL_USERNAME", None) + EMAIL_PASSWORD: Optional[str] = getenv("EMAIL_PASSWORD", None) DEFAULT_SMTP_PORT: int = 465 EMAIL_SMTP_PORT: int = int(getenv("EMAIL_SMTP_PORT", DEFAULT_SMTP_PORT)) diff --git a/camply/config/search_config.py b/camply/config/search_config.py index a28c1b32..82b6f50a 100644 --- a/camply/config/search_config.py +++ b/camply/config/search_config.py @@ -11,6 +11,7 @@ class SearchConfig: """ File Path Storage Class """ + POLLING_INTERVAL_MINIMUM: int = 5 # 5 MINUTES RECOMMENDED_POLLING_INTERVAL: int = 10 # 10 MINUTES ERROR_MESSAGE: str = "No search days configured. Exiting" diff --git a/camply/containers.py b/camply/containers.py index 5de97b3a..46a3e627 100644 --- a/camply/containers.py +++ b/camply/containers.py @@ -17,15 +17,18 @@ class SearchWindow(NamedTuple): """ Search Window for Campsite Search """ + start_date: datetime end_date: datetime class AvailableCampsite(NamedTuple): """ - Campsite Storage, this container should be universal regardless of - API Provider + Campsite Storage + + This container should be universal regardless of API Provider """ + campsite_id: int booking_date: datetime booking_end_date: datetime @@ -47,6 +50,7 @@ class CampgroundFacility(NamedTuple): """ Campground Facility Data Storage """ + facility_name: str recreation_area: str facility_id: int @@ -57,6 +61,7 @@ class RecreationArea(NamedTuple): """ Recreation Area Data Storage """ + recreation_area: str recreation_area_id: int recreation_area_location: str diff --git a/camply/notifications/email_notifications.py b/camply/notifications/email_notifications.py index 389d69ff..675607b2 100755 --- a/camply/notifications/email_notifications.py +++ b/camply/notifications/email_notifications.py @@ -61,6 +61,9 @@ def __init__(self): _email_server.quit() def __repr__(self): + """ + String Representation + """ return "" @staticmethod diff --git a/camply/notifications/pushbullet.py b/camply/notifications/pushbullet.py index 7ebae79d..35973604 100755 --- a/camply/notifications/pushbullet.py +++ b/camply/notifications/pushbullet.py @@ -28,20 +28,24 @@ class PushbulletNotifications(BaseNotifications, ABC): def __init__(self): if any([PushbulletConfig.API_TOKEN is None, PushbulletConfig.API_TOKEN == ""]): - warning_message = ("Pushbullet is not configured properly. To send Pushbullet messages " - "make sure to run `camply configure` or set the " - "proper environment variable: `PUSHBULLET_API_TOKEN`.") + warning_message = ( + "Pushbullet is not configured properly. To send Pushbullet messages " + "make sure to run `camply configure` or set the " + "proper environment variable: `PUSHBULLET_API_TOKEN`.") logger.error(warning_message) raise EnvironmentError(warning_message) def __repr__(self): + """ + String Representation + """ return "" @staticmethod def send_message(message: str, **kwargs) -> Optional[requests.Response]: """ Send a message via PushBullet - if environment variables are configured -` + Parameters ---------- message: str @@ -54,7 +58,8 @@ def send_message(message: str, **kwargs) -> Optional[requests.Response]: pushbullet_headers.update({"Access-Token": PushbulletConfig.API_TOKEN}) message_type = kwargs.pop("type", "note") message_title = kwargs.pop("title", "Camply Notification") - message_json = dict(type=message_type, title=message_title, body=message, **kwargs) + message_json = dict(type=message_type, title=message_title, body=message, + **kwargs) logger.debug(message_json) response = requests.post(url=PushbulletConfig.PUSHBULLET_API_ENDPOINT, headers=pushbullet_headers, @@ -87,5 +92,6 @@ def send_campsites(campsites: List[AvailableCampsite], **kwargs): composed_message = "\n".join(fields) message_title = (f"{campsite.recreation_area} | {campsite.facility_name} | " f"{campsite.booking_date.strftime('%Y-%m-%d')}") - PushbulletNotifications.send_message(message=composed_message, title=message_title, + PushbulletNotifications.send_message(message=composed_message, + title=message_title, type="note") diff --git a/camply/notifications/pushover.py b/camply/notifications/pushover.py index 7671ed62..2ec8fb84 100755 --- a/camply/notifications/pushover.py +++ b/camply/notifications/pushover.py @@ -38,6 +38,9 @@ def __init__(self, level: Optional[int] = logging.INFO): raise EnvironmentError(warning_message) def __repr__(self): + """ + String Representation + """ return "" @staticmethod diff --git a/camply/notifications/silent_notifications.py b/camply/notifications/silent_notifications.py index 970f3a71..c811ed47 100755 --- a/camply/notifications/silent_notifications.py +++ b/camply/notifications/silent_notifications.py @@ -9,7 +9,7 @@ from abc import ABC import logging from pprint import pformat -from typing import List +from typing import Iterable, List from camply.containers import AvailableCampsite from camply.notifications.base_notifications import BaseNotifications @@ -29,16 +29,19 @@ def __init__(self): logger.info(f"{self} enabled. I hope you're watching these logs.") def __repr__(self): + """ + String Representation + """ return "" @staticmethod - def send_message(message: str, **kwargs) -> None: + def send_message(message: Iterable, **kwargs) -> None: """ Send a message via Email Parameters ---------- - message: str + message: Iterable Email Body **kwargs kwargs are disregarded diff --git a/camply/providers/__init__.py b/camply/providers/__init__.py index a25b97e5..f57a05f7 100644 --- a/camply/providers/__init__.py +++ b/camply/providers/__init__.py @@ -9,3 +9,9 @@ from .base_provider import BaseProvider from .recreation_dot_gov.campsite_search import RecreationDotGov from .xanterra.yellowstone_lodging import YellowstoneLodging + +__all__ = [ + "BaseProvider", + "RecreationDotGov", + "YellowstoneLodging", +] diff --git a/camply/providers/recreation_dot_gov/campsite_search.py b/camply/providers/recreation_dot_gov/campsite_search.py index a6858dc8..48e0f92a 100644 --- a/camply/providers/recreation_dot_gov/campsite_search.py +++ b/camply/providers/recreation_dot_gov/campsite_search.py @@ -112,7 +112,6 @@ def find_campgrounds(self, search_string: str = None, facilities: List[CampgroundFacility] Array of Matching Campsites """ - if campground_id not in [None, list()]: facilities = self._find_facilities_from_campgrounds(campground_id=campground_id) elif rec_area_id is not None: @@ -240,8 +239,6 @@ def _ridb_get_data(self, path: str, params: Optional[dict] = None) -> Union[dict """ Find Matching Campsites Based on Search String - Parameters - ---------- Parameters ---------- path: str @@ -323,7 +320,7 @@ def _filter_facilities_responses(cls, responses=List[dict]) -> List[dict]: Returns ------- - + List[dict] """ filtered_responses = list() for possible_match in responses: @@ -465,6 +462,7 @@ def _rec_availability_get_endpoint(cls, path: str) -> str: def _make_recdotgov_request(self, campground_id: int, month: datetime) -> requests.Response: """ Make a request to the RecreationDotGov API - Handle Exponential Backoff + Parameters ---------- campground_id @@ -472,7 +470,7 @@ def _make_recdotgov_request(self, campground_id: int, month: datetime) -> reques Returns ------- - Returns + requests.Response """ try: formatted_month = month.strftime("%Y-%m-01T00:00:00.000Z") diff --git a/camply/providers/xanterra/yellowstone_lodging.py b/camply/providers/xanterra/yellowstone_lodging.py index 831fb1ae..5c26467b 100755 --- a/camply/providers/xanterra/yellowstone_lodging.py +++ b/camply/providers/xanterra/yellowstone_lodging.py @@ -55,20 +55,25 @@ def _get_monthly_availability(self, month: datetime, nights: int = None) -> dict rate_code=YellowstoneConfig.RATE_CODE) if nights is not None: query_dict.update(dict(nights=nights)) - api_endpoint = self._get_api_endpoint(url_path=YellowstoneConfig.YELLOWSTONE_LODGING_PATH, - query=None) - logger.info(f"Searching for Yellowstone Lodging Availability: {month.strftime('%B, %Y')}") - all_resort_availability_data = self.make_yellowstone_request(endpoint=api_endpoint, - params=query_dict) + api_endpoint = self._get_api_endpoint( + url_path=YellowstoneConfig.YELLOWSTONE_LODGING_PATH, + query=None) + logger.info( + f"Searching for Yellowstone Lodging Availability: {month.strftime('%B, %Y')}") + all_resort_availability_data = self.make_yellowstone_request( + endpoint=api_endpoint, + params=query_dict) return all_resort_availability_data @staticmethod @tenacity.retry(wait=tenacity.wait_random_exponential(multiplier=3, max=1800), stop=tenacity.stop.stop_after_delay(6000)) - def _try_retry_get_data(endpoint: str, params: Optional[dict] = None): + def _try_retry_get_data(endpoint: str, params: Optional[dict] = None) -> dict: """ - Try and Retry Fetching Data from the Yellowstone API. Unfortunately this is a required - method to request the data since the Yellowstone API doesn't always return data. + Try and Retry Fetching Data from the Yellowstone API. + + Unfortunately this is a required method to request the data since the + Yellowstone API doesn't always return data. Parameters ---------- @@ -78,7 +83,7 @@ def _try_retry_get_data(endpoint: str, params: Optional[dict] = None): Returns ------- - + dict """ yellowstone_headers = choice(USER_AGENTS) yellowstone_headers.update(STANDARD_HEADERS) @@ -95,10 +100,12 @@ def _try_retry_get_data(endpoint: str, params: Optional[dict] = None): raise RuntimeError(error_message) @staticmethod - def make_yellowstone_request(endpoint: str, params: Optional[dict] = None): + def make_yellowstone_request(endpoint: str, params: Optional[dict] = None) -> dict: """ - Try and Retry Fetching Data from the Yellowstone API. Unfortunately this is a required - method to request the data since the Yellowstone API doesn't always return data. + Try and Retry Fetching Data from the Yellowstone API. + + Unfortunately this is a required method to request the data since the + Yellowstone API doesn't always return data. Parameters ---------- @@ -108,10 +115,11 @@ def make_yellowstone_request(endpoint: str, params: Optional[dict] = None): Returns ------- - + dict """ try: - content = YellowstoneLodging._try_retry_get_data(endpoint=endpoint, params=params) + content = YellowstoneLodging._try_retry_get_data(endpoint=endpoint, + params=params) except RuntimeError as re: raise RuntimeError(f"error_message: {re}") return content @@ -214,12 +222,16 @@ def _compile_campground_availabilities(cls, availability: dict) -> List[dict]: f"{len(available_campsites)} sites found.") return available_campsites - def _gather_campsite_specific_availability(self, available_campsites: List[dict], - month: datetime, - nights: Optional[int] = None) -> List[dict]: + def _gather_campsite_specific_availability( + self, + available_campsites: List[dict], + month: datetime, + nights: Optional[int] = None) -> List[dict]: """ - Given a DataFrame of campsite availability, return updated Data with details about the - actual campsites that are available (i.e Tent Size, RV Length, Etc) + Get campsite extra information + + Given a DataFrame of campsite availability, return updated Data with details + about the actual campsites that are available (i.e Tent Size, RV Length, Etc) Parameters ---------- @@ -236,15 +248,17 @@ def _gather_campsite_specific_availability(self, available_campsites: List[dict] availability_df = DataFrame(data=available_campsites) if availability_df.empty is True: return available_room_array - for facility_id, facility_df in availability_df.groupby(YellowstoneConfig.FACILITY_ID): + for facility_id, _facility_df in availability_df.groupby( + YellowstoneConfig.FACILITY_ID): api_endpoint = self._get_api_endpoint( url_path=YellowstoneConfig.YELLOWSTONE_CAMPSITE_AVAILABILITY, query=None) params = dict(date=self._ensure_current_month(month=month), limit=31) if nights is not None: params.update(dict(nights=nights)) - campsite_data = self.make_yellowstone_request(endpoint=f"{api_endpoint}/{facility_id}", - params=params) + campsite_data = self.make_yellowstone_request( + endpoint=f"{api_endpoint}/{facility_id}", + params=params) campsite_availability = campsite_data[YellowstoneConfig.BOOKING_AVAILABILITY] booking_dates = campsite_availability.keys() availabilities = self._process_daily_availability( @@ -256,7 +270,8 @@ def _gather_campsite_specific_availability(self, available_campsites: List[dict] @classmethod def _process_daily_availability(cls, booking_dates: List[str], - campsite_availability: dict, facility_id: str) -> List[dict]: + campsite_availability: dict, facility_id: str) -> \ + List[dict]: """ Process Monthly Availability @@ -285,7 +300,8 @@ def _process_daily_availability(cls, booking_dates: List[str], booking_date=booking_date_str, facility_id=facility_id, campsite_code=room[YellowstoneConfig.FACILITY_ROOM_CODE], - available=room[YellowstoneConfig.FACILITY_AVAILABLE_QUALIFIER], + available=room[ + YellowstoneConfig.FACILITY_AVAILABLE_QUALIFIER], price=room[YellowstoneConfig.FACILITY_PRICE])) return daily_availabilities @@ -310,7 +326,8 @@ def _get_property_information(self, available_rooms: List[dict]) -> List[dict]: api_endpoint = self._get_api_endpoint( url_path=YellowstoneConfig.YELLOWSTONE_PROPERTY_INFO, query=None) - campsite_info = self.make_yellowstone_request(endpoint=f"{api_endpoint}/{facility_id}") + campsite_info = self.make_yellowstone_request( + endpoint=f"{api_endpoint}/{facility_id}") campsite_codes = campsite_info.keys() for campsite_code in campsite_codes: campsite_data = campsite_info[campsite_code] @@ -343,9 +360,11 @@ def get_monthly_campsites(self, month: datetime, now = datetime.now() search_date = month.replace(day=1) if month <= now: - logger.info("Cannot input search dates before today, adjusting search parameters.") + logger.info( + "Cannot input search dates before today, adjusting search parameters.") search_date = search_date.replace(year=now.year, month=now.month, day=now.day) - availability_found = self._get_monthly_availability(month=search_date, nights=nights) + availability_found = self._get_monthly_availability(month=search_date, + nights=nights) monthly_campsites = self._compile_campground_availabilities( availability=availability_found[YellowstoneConfig.BOOKING_AVAILABILITY]) campsite_data = DataFrame(monthly_campsites, @@ -370,7 +389,8 @@ def get_monthly_campsites(self, month: datetime, nights_param = dict(nights=1) booking_nights = nights_param.get("nights") merged_campsites[YellowstoneConfig.BOOKING_END_DATE_COLUMN] = \ - merged_campsites[YellowstoneConfig.BOOKING_DATE_COLUMN] + timedelta(days=booking_nights) + merged_campsites[YellowstoneConfig.BOOKING_DATE_COLUMN] + timedelta( + days=booking_nights) merged_campsites[YellowstoneConfig.BOOKING_NIGHTS_COLUMN] = booking_nights final_campsites = merged_campsites.merge( campsite_data, on=YellowstoneConfig.FACILITY_ID_COLUMN).sort_values( @@ -432,7 +452,8 @@ def _ensure_current_month(cls, month: datetime) -> datetime: """ yellowstone_timezone = timezone(YellowstoneConfig.YELLOWSTONE_TIMEZONE) yellowstone_current_time = datetime.now(yellowstone_timezone) - today = datetime(year=yellowstone_current_time.year, month=yellowstone_current_time.month, + today = datetime(year=yellowstone_current_time.year, + month=yellowstone_current_time.month, day=yellowstone_current_time.day) if today > month: month = today diff --git a/camply/search/__init__.py b/camply/search/__init__.py index 240ad160..39ea3943 100644 --- a/camply/search/__init__.py +++ b/camply/search/__init__.py @@ -16,3 +16,9 @@ "RecreationDotGov": SearchRecreationDotGov, "Yellowstone": SearchYellowstone } + +__all__ = [ + "BaseCampingSearch", + "SearchYellowstone", + "SearchRecreationDotGov", +] diff --git a/camply/search/base_search.py b/camply/search/base_search.py index 50a917d4..a96c95fe 100644 --- a/camply/search/base_search.py +++ b/camply/search/base_search.py @@ -33,7 +33,7 @@ class SearchError(Exception): """ -class CampsiteNotFound(SearchError): +class CampsiteNotFoundError(SearchError): """ Campsite not found Error """ @@ -76,7 +76,9 @@ def __init__(self, provider: Union[RecreationDotGov, @abstractmethod def get_all_campsites(self) -> List[AvailableCampsite]: """ - Perform the Search and Return Matching Availabilities. This method must be implemented + Perform the Search and Return Matching Availabilities. + + This method must be implemented on all sub-classes. Returns @@ -86,6 +88,7 @@ def get_all_campsites(self) -> List[AvailableCampsite]: def _get_intersection_date_overlap(self, date: datetime, periods: int) -> bool: """ + Find Date Overlap Parameters ---------- @@ -169,7 +172,7 @@ def _search_matching_campsites_available(self, log: bool = False, if len(matching_campgrounds) == 0 and raise_error is True: campsite_availability_message = "No Campsites were found, we'll continue checking" logger.info(campsite_availability_message) - raise CampsiteNotFound(campsite_availability_message) + raise CampsiteNotFoundError(campsite_availability_message) return matching_campgrounds @classmethod @@ -230,7 +233,7 @@ def _continuous_search_retry(self, log: bool, verbose: bool, polling_interval: i logger.info(f"Searching for campsites every {polling_interval_minutes} minutes. " f"Notifications active via {notifier}") retryer = tenacity.Retrying( - retry=tenacity.retry_if_exception_type(CampsiteNotFound), + retry=tenacity.retry_if_exception_type(CampsiteNotFoundError), wait=tenacity.wait.wait_fixed(int(polling_interval_minutes) * 60)) matching_campsites = retryer.__call__(self._search_matching_campsites_available, False, False, True) @@ -441,7 +444,7 @@ def _consolidate_campsites(cls, campsite_df: DataFrame, group_identifier = consecutive_nights.cumsum() campsite_grouping[CampsiteContainerFields.CAMPSITE_GROUP] = group_identifier # USE THE ASSEMBLED GROUPS TO CREATE UPDATED CAMPSITES AND REMOVE DUPLICATES - for campsite_group, campsite_group_slice in campsite_grouping.groupby( + for _campsite_group, campsite_group_slice in campsite_grouping.groupby( [CampsiteContainerFields.CAMPSITE_GROUP]): composed_grouping = campsite_group_slice.sort_values( by=CampsiteContainerFields.BOOKING_DATE, @@ -478,8 +481,9 @@ def _consecutive_subseq(cls, iterable: Iterable, length: int) -> Generator: @classmethod def _find_consecutive_nights(cls, dataframe: DataFrame, nights: int) -> DataFrame: """ - Given a DataFrame of Consecutive Nightly Campsite Availabilities, explode it into - all unique possibilities given the length of the stay. + Explode a DataFrame of Consecutive Nightly Campsite Availabilities, + + Expand to all unique possibilities given the length of the stay. Parameters ---------- diff --git a/camply/search/search_recreationdotgov.py b/camply/search/search_recreationdotgov.py index 62f62e6e..c1abf86a 100644 --- a/camply/search/search_recreationdotgov.py +++ b/camply/search/search_recreationdotgov.py @@ -59,8 +59,10 @@ def __init__(self, search_window: Union[SearchWindow, List[SearchWindow]], def _get_searchable_campgrounds(self) -> List[CampgroundFacility]: """ - Return a List of Campgrounds to search, this handles scenarios - where a recreation area is provided instead of a campground list + Return a List of Campgrounds to search + + This handles scenarios where a recreation area is provided instead + of a campground list Returns ------- @@ -119,9 +121,10 @@ def get_all_campsites(self) -> List[AvailableCampsite]: logger.info(f"Searching across {len(self.campgrounds)} campgrounds") for index, campground in enumerate(self.campgrounds): for month in self.search_months: - logger.info(f"Searching {campground.facility_name}, {campground.recreation_area} " - f"({campground.facility_id}) for availability: " - f"{month.strftime('%B, %Y')}") + logger.info( + f"Searching {campground.facility_name}, {campground.recreation_area} " + f"({campground.facility_id}) for availability: " + f"{month.strftime('%B, %Y')}") availabilities = self.campsite_finder.get_recdotgov_data( campground_id=campground.facility_id, month=month) campsites = self.campsite_finder.process_campsite_availability( @@ -136,7 +139,8 @@ def get_all_campsites(self) -> List[AvailableCampsite]: sleep(round(uniform(*RecreationBookingConfig.RATE_LIMITING), 2)) campsite_df = self.campsites_to_df(campsites=found_campsites) campsite_df_validated = self._filter_date_overlap(campsites=campsite_df) - compiled_campsite_df = self._consolidate_campsites(campsite_df=campsite_df_validated, - nights=self.nights) + compiled_campsite_df = self._consolidate_campsites( + campsite_df=campsite_df_validated, + nights=self.nights) compiled_campsites = self.df_to_campsites(campsite_df=compiled_campsite_df) return compiled_campsites diff --git a/camply/utils/__init__.py b/camply/utils/__init__.py index 6c63b096..a80ba1e4 100644 --- a/camply/utils/__init__.py +++ b/camply/utils/__init__.py @@ -8,3 +8,9 @@ from .api_utils import filter_json, generate_url from .logging_utils import log_camply + +__all__ = [ + "filter_json", + "generate_url", + "log_camply", +] diff --git a/camply/utils/camply_cli.py b/camply/utils/camply_cli.py index b22648d6..f1e8e821 100644 --- a/camply/utils/camply_cli.py +++ b/camply/utils/camply_cli.py @@ -24,11 +24,12 @@ def main(): """ - Run the Camply CLI. The CLI is wrapped in a main() function to - interface with Poetry + Run the Camply CLI. + + The CLI is wrapped in a main() function to interface with entrypoints Returns - ------ + ------- None """ logging.basicConfig(format="%(asctime)s [%(levelname)8s]: %(message)s", @@ -47,7 +48,6 @@ class CommandLineError(Exception): """ Generic CLI Error """ - pass class CamplyCommandLine: @@ -62,7 +62,6 @@ def __init__(self): """ Initialized CLI """ - self.parser = ArgumentParser(description=CommandLineConfig.CAMPLY_LONG_DESCRIPTION, prog=self.__name__, epilog=CommandLineConfig.CAMPLY_EPILOG) diff --git a/camply/utils/configure_camply.py b/camply/utils/configure_camply.py index 6f192fec..84bcbe96 100644 --- a/camply/utils/configure_camply.py +++ b/camply/utils/configure_camply.py @@ -20,6 +20,7 @@ def get_log_input(message: str): """ Create a log message with a nice log format :) + Parameters ---------- message: str @@ -60,8 +61,9 @@ def double_check(message: str) -> bool: def check_dot_camply_file() -> bool: """ - Check to see if the `.camply` file already exists, and return the - file existence status + Check to see if the `.camply` file already exists + + Return the file existence status Returns ------- @@ -110,7 +112,7 @@ def write_config_to_file(config_dict: OrderedDict) -> None: """ string_list = [ "# CAMPLY CONFIGURATION FILE. ", - "# SEE https://github.com/juftin/camply/blob/main/docs/examples/example.camply ""FOR MORE DETAILS", + "# SEE https://github.com/juftin/camply/blob/main/docs/examples/example.camply", "" ] for config_key, config_value in config_dict.items(): diff --git a/camply/utils/logging_utils.py b/camply/utils/logging_utils.py index bb1d52e2..3165d336 100644 --- a/camply/utils/logging_utils.py +++ b/camply/utils/logging_utils.py @@ -35,7 +35,9 @@ def get_emoji(obj: list) -> str: def log_camply(self: logging.Logger, message: str, *args, **kwargs) -> None: """ Custom Logging Notification Level for Pushover Logging - between logging.ERROR and logging.CRITICAL (45) + + Between logging.ERROR and logging.CRITICAL (45) + Parameters ---------- self: logging.Logger diff --git a/camply/utils/yaml_utils.py b/camply/utils/yaml_utils.py index f43edadc..ecdf14d5 100644 --- a/camply/utils/yaml_utils.py +++ b/camply/utils/yaml_utils.py @@ -23,16 +23,19 @@ def read_yml(path: str = None): """ + Read a YAML File + Load a yaml configuration file_path (path) or data object (data) and resolve any environment variables. The environment variables must be in this format to be parsed: ${VAR_NAME}. + Parameters ---------- path: str File Path of YAML Object to Read Examples - ---------- + -------- database: host: ${HOST} port: ${PORT} @@ -50,8 +53,10 @@ def read_yml(path: str = None): def env_var_constructor(safe_loader: object, node: object): """ Extracts the environment variable from the node's value + :param yaml.Loader safe_loader: the yaml loader :param node: the current node in the yaml + :return: the parsed string that contains the value of the environment variable """ diff --git a/docs/examples/example_search.yml b/docs/examples/example_search.yml index 8d51cc25..c94e2aa7 100644 --- a/docs/examples/example_search.yml +++ b/docs/examples/example_search.yml @@ -1,14 +1,14 @@ -provider: RecreationDotGov # RecreationDotGov IF NOT PROVIDED -recreation_area: # (LIST OR SINGLE ENTRY) - - 2991 # Yosemite National Park, CA (All Campgrounds) - - 1074 # Sierra National Forest, CA (All Campgrounds) -campgrounds: null # OVERRIDES RECREATION AREA (LIST OR SINGLE ENTRY) -start_date: 2022-09-12 # YYYY-MM-DD -end_date: 2022-09-13 # YYYY-MM-DD -weekends: False # FALSE BY DEFAULT -nights: 1 # 1 BY DEFAULT -continuous: False # DEFAULTS TO TRUE -polling_interval: 5 # DEFAULTS TO 10 , CAN'T BE LESS THAN 5 -notifications: email # (silent, email, pushover, pushbullet), DEFAULTS TO `silent` -search_forever: True # FALSE BY DEFAULT -notify_first_try: False # FALSE BY DEFAULT +provider: RecreationDotGov # RecreationDotGov IF NOT PROVIDED +recreation_area: # (LIST OR SINGLE ENTRY) + - 2991 # Yosemite National Park, CA (All Campgrounds) + - 1074 # Sierra National Forest, CA (All Campgrounds) +campgrounds: null # OVERRIDES RECREATION AREA (LIST OR SINGLE ENTRY) +start_date: 2022-09-12 # YYYY-MM-DD +end_date: 2022-09-13 # YYYY-MM-DD +weekends: false # FALSE BY DEFAULT +nights: 1 # 1 BY DEFAULT +continuous: false # DEFAULTS TO TRUE +polling_interval: 5 # DEFAULTS TO 10 , CAN'T BE LESS THAN 5 +notifications: email # (silent, email, pushover, pushbullet), DEFAULTS TO `silent` +search_forever: true # FALSE BY DEFAULT +notify_first_try: false # FALSE BY DEFAULT diff --git a/setup.py b/setup.py index e2931d99..52ee77a9 100755 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ """ Python Packaging Configuration File + Package Settings configured and inferred from setup.cfg """ @@ -14,7 +15,8 @@ def parse_requirements_file(filename: str) -> List[str]: """ Parse a Requirements File Into Package Dependency List - while ignoring comments (on their own line or after the dependency) + + Ignore comments (on their own line or after the dependency) and empty lines """ requirements_list = [] diff --git a/tests/yml/example_search.yml b/tests/yml/example_search.yml index 400c8e57..2375ed39 100644 --- a/tests/yml/example_search.yml +++ b/tests/yml/example_search.yml @@ -1,8 +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: 2022-09-10 # YYYY-MM-DD -end_date: 2022-09-11 # YYYY-MM-DD -weekends: False # FALSE BY DEFAULT -continuous: False # DEFAULTS TO TRUE +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: 2022-09-10 # YYYY-MM-DD +end_date: 2022-09-11 # YYYY-MM-DD +weekends: false # FALSE BY DEFAULT +continuous: false # DEFAULTS TO TRUE diff --git a/tox.ini b/tox.ini index 25c83d67..8b2e5e23 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py flake8 commandline + yamllint [testenv] extras = @@ -56,3 +57,36 @@ ignore = D401 per-file-ignores = camply/config/api_config.py:E501 + +[testenv:mypy] +changedir = {toxinidir} +deps = + mypy +commands = + mypy \ + --config-file {toxinidir}/tox.ini \ + --install-types \ + --strict-optional \ + --non-interactive \ + {toxinidir}/camply + +[mypy] + +;follow_imports = silent +;warn_redundant_casts = True +;warn_unused_ignores = True +;disallow_any_generics = True +;check_untyped_defs = True +;no_implicit_reexport = True +;disallow_untyped_defs = True + +[mypy-camply.utils.camply_cli] +ignore_errors = True + +[testenv:yamllint] +changedir = {toxinidir} +skip_install = true +deps = + yamllint +commands = + yamllint {toxinidir} -c {toxinidir}/.github/config/yamllint.yml