diff --git a/.github/workflows/check-version.yml b/.github/workflows/check-version.yml deleted file mode 100644 index d9f75b2..0000000 --- a/.github/workflows/check-version.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: Check Issue Version - -on: - issues: - types: [opened] - -jobs: - check_version: - runs-on: ubuntu-latest - permissions: - issues: write # Needed to comment on issues - contents: read # Needed to get latest release info - - steps: - - name: Parse issue form data - id: parse_issue - uses: peter-murray/issue-forms-body-parser@22e5d90e6a607073d08891bc036461971cbcfb45 # v4 # Or zentered/issue-forms-body-parser@v2 - with: - issue_id: ${{ github.event.issue.number }} - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Debug: Print parsed payload - run: | - echo "Raw parsed payload: ${{ steps.parse_issue.outputs.payload }}" - shell: bash - - - name: Get latest release version - id: get_latest_release - uses: pozetroninc/github-action-get-latest-release@2a61c339ea7ef0a336d1daa35ef0cb1418e7676c # v0.8.0 # Or rez0n/actions-github-release@v2.0 - with: - repository: cloneofghosts/python-pirate-weather # Your repo - excludes: "prerelease,draft" # Ensure you get stable releases - - - name: Clean up latest release version (remove 'v' prefix) - id: clean_release_version - run: | - RAW_LATEST_VERSION="${{ steps.get_latest_release.outputs.release }}" - # Remove 'v' prefix if it exists - CLEANED_LATEST_VERSION="${RAW_LATEST_VERSION#v}" - echo "CLEANED_LATEST_VERSION=$CLEANED_LATEST_VERSION" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Compare versions with Python and comment - env: - ISSUE_VERSION: ${{ steps.parse_issue.outputs.payload.version }} - LATEST_VERSION: ${{ steps.clean_release_version.outputs.CLEANED_LATEST_VERSION }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Install Python and packaging library if not already in runner image - python -m pip install packaging - - # Start of Python script - import os - import tempfile - import subprocess - from packaging.version import parse - - issue_version = os.environ['ISSUE_VERSION'] - latest_version = os.environ['LATEST_VERSION'] - issue_number = os.environ['ISSUE_NUMBER'] - github_token = os.environ['GITHUB_TOKEN'] - github_repository = os.environ.get('GITHUB_REPOSITORY') - - print(f'Issue version: {issue_version}') - print(f'Latest release version: {latest_version}') - - # Ensure the issue version is parseable (e.g., if user inputs garbage) - try: - parsed_issue_version = parse(issue_version) - except Exception as e: - print(f'Could not parse issue version: {issue_version}. Error: {e}. Skipping version comparison.') - exit(0) # Exit gracefully if version cannot be parsed - - try: - parsed_latest_version = parse(latest_version) - except Exception as e: - print(f'Could not parse latest release version: {latest_version}. Error: {e}. Skipping version comparison.') - exit(0) # Exit gracefully - - if parsed_issue_version < parsed_latest_version: - print('Version mismatch detected. Commenting on issue.') - # Define update_instructions as a list of lines to avoid YAML parsing issues - update_instructions_lines = [ - f'It looks like you are running an older version of `python-pirate-weather` (`{issue_version}`).', - '', - f'**Please update to the latest stable version (`{latest_version}`) before reporting issues, as your issue might already be resolved.**', - '', - 'You can update by running:', - '\`\`\`bash', - 'pip install --upgrade python-pirate-weather', - '\`\`\`', - '', - 'If the issue persists after updating, please provide the new version number in a comment.' - ] - update_instructions = '\n'.join(update_instructions_lines) - - # Write the comment body to a temporary file - with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as temp_file: - temp_file.write(update_instructions) - temp_file_path = temp_file.name - - # Use subprocess.run to execute the gh CLI command - # This is more robust against quoting issues - try: - subprocess.run( - ['gh', 'issue', 'comment', str(issue_number), '--repo', github_repository, '--body-file', temp_file_path], - check=True, # Raise an exception for non-zero exit codes - capture_output=True, # Capture stdout/stderr - text=True # Decode stdout/stderr as text - ) - print(f"Successfully commented on issue #{issue_number}") - except subprocess.CalledProcessError as e: - print(f"Error commenting on issue: {e}") - print(f"Stdout: {e.stdout}") - print(f"Stderr: {e.stderr}") - finally: - os.remove(temp_file_path) # Clean up the temporary file - else: - print('User is running the latest version or a newer/unreleased version.') - shell: python # Explicitly set the shell to python diff --git a/.ruff.toml b/.ruff.toml index 05a8d10..506ea3f 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -113,8 +113,6 @@ ignore = [ "SIM115", # Use context handler for opening files "TRY003", # Avoid specifying long messages outside the exception class "TRY400", # Use `logging.exception` instead of `logging.error` - # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 - "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", diff --git a/README.md b/README.md index d4a61c6..6e30dd9 100644 --- a/README.md +++ b/README.md @@ -67,12 +67,13 @@ Parameters: - **latitude** - The latitude of the location for the forecast - **longitude** - The longitude of the location for the forecast - **time** - (optional) A datetime object for the forecast either in the past or future - see How Timezones Work below for the details on how timezones are handled in this library. -- **units** - (optional) A string of the preferred units of measurement, "us" is the default. "us","ca","uk","si" are also available. See the API Docs (https://pirateweather.net/en/latest/API/#units) for exactly what each unit means. +- **units** - (optional) A string of the preferred units of measurement, "auto" is the default. "us","ca","uk","si" are also available. See the API Docs (https://pirateweather.net/en/latest/API/#units) for exactly what each unit means. - **lang** - (optional) A string of the desired language. See https://pirateweather.net/en/latest/API/#language for supported languages. - **lazy** - (optional) Defaults to `false`. If `true` the function will request the json data as it is needed. Results in more requests, but maybe a faster response time. - **extend** - (optional) Defaults to `false`. If `"hourly"` the API will hourly data for 168 hours instead of the standard 48 hours. - **version** - (optional) Defaults to `1`. If set to `2` the API will return fields that were not part of the Dark Sky API. - **icon** - (optional) Defaults to `darksky`. If set to `pirate` the API will return icons which aren't apart of the default Dark Sky icon set. +- **extraVars** - (optional) Is used to add additional parameters to the API response. The only extra parameter at the moment is `stationPressure` but more may be added in the future. - **callback** - (optional) Pass a function to be used as a callback. If used, load_forecast() will use an asynchronous HTTP call and **will not return the forecast object directly**, instead it will be passed to the callback function. Make sure it can accept it. ---------------------------------------------------- diff --git a/pirateweather/api.py b/pirateweather/api.py index c0542e4..caa007a 100644 --- a/pirateweather/api.py +++ b/pirateweather/api.py @@ -12,13 +12,14 @@ def load_forecast( lat, lng, time=None, - units="us", + units="auto", lang="en", lazy=False, callback=None, extend=None, version=1, icon="darksky", + extraVars=None, ): """Build the request url and loads some or all of the needed json depending on lazy is True. @@ -27,7 +28,7 @@ def load_forecast( time: A datetime.datetime object representing the desired time of the forecast. If no timezone is present, the API assumes local time at the provided latitude and longitude. - units: A string of the preferred units of measurement, "us" is + units: A string of the preferred units of measurement, "auto" is default. also ca,uk,si is available lang: Return summary properties in the desired language lazy: Defaults to false. The function will only request the json @@ -37,13 +38,15 @@ def load_forecast( of the standard 48 hours. version: If set to 2 the API will return fields that were not part of the Dark Sky API. icon: If set to pirate the API will return icons which aren't apart of the default Dark Sky icon set + extraVars: Is used to add additional parameters to the API response. """ if time is None: - url = ( - f"https://api.pirateweather.net/forecast/{key}/{lat},{lng}" - f"?units={units}&lang={lang}&extend={extend}&version={version}&icon={icon}" - ) + url = f"https://api.pirateweather.net/forecast/{key}/{lat},{lng}?units={units}&lang={lang}&version={version}&icon={icon}" + if extend: + url += f"&extend={extend}" + if extraVars: + url += f"&extraVars={extraVars}" else: url_time = time.replace( microsecond=0 diff --git a/pirateweather/models.py b/pirateweather/models.py index f955e9a..77f580f 100644 --- a/pirateweather/models.py +++ b/pirateweather/models.py @@ -108,6 +108,9 @@ def __init__(self, d=None): self.nearestStation = d.get("nearest-station") self.sources = list(d.get("sources")) self.sourceTimes = d.get("sourceTimes") + self.processTime = d.get("processTime") + self.ingestVersion = d.get("ingestVersion") + self.nearestCity = d.get("nearestCity") def __unicode__(self): """Return a string representation of the data block.""" diff --git a/setup.py b/setup.py index aa33ef7..b79f715 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def read(fname): setup( name="python-pirateweather", - version="1.1.2", + version="1.2.0", author="cloneofghosts", description=("A thin Python Wrapper for the Pirate Weather API"), license="BSD 2-clause", diff --git a/tests/test_pirateweather.py b/tests/test_pirateweather.py index 068a35d..bb8ec7e 100644 --- a/tests/test_pirateweather.py +++ b/tests/test_pirateweather.py @@ -18,6 +18,10 @@ def setUp(self): """Set up the API key, location and time for the tests.""" self.api_key = os.environ.get("PIRATEWEATHER_API_KEY") + if not self.api_key: + # Skip end-to-end tests when the API key isn't available in CI/local dev + # They require network access and a valid key. + self.skipTest("PIRATEWEATHER_API_KEY not set") self.lat = 52.370235 self.lng = 4.903549 self.time = datetime(2015, 2, 27, 6, 0, 0) @@ -58,7 +62,7 @@ def test_extend(self): hourl_data = forecast.hourly() assert forecast.response.status_code == 200 - assert len(hourl_data.data) == 169 + assert len(hourl_data.data) == 168 def test_version(self): """Test querying the API endpoint.""" @@ -81,17 +85,33 @@ def test_version_data_point(self): assert forecast.response.status_code == 200 assert fc_cur.fireIndex + def test_extra_vars(self): + """Test querying the API endpoint.""" + + forecast = pirateweather.load_forecast( + self.api_key, self.lat, self.lng, units="us", extraVars="stationPressure" + ) + fc_cur = forecast.currently() + + assert forecast.response.status_code == 200 + assert fc_cur.stationPressure + def test_flags(self): """Test the data returned by the flags block.""" - forecast = pirateweather.load_forecast(self.api_key, self.lat, self.lng) + forecast = pirateweather.load_forecast( + self.api_key, self.lat, self.lng, version=2 + ) flags = forecast.flags() assert len(flags.sources) == 3 assert len(flags.sourceTimes) == 2 assert flags.nearestStation == 0 - assert flags.units == "us" + assert flags.units == "si" assert flags.sourceTimes.get("gfs") + assert flags.processTime + assert flags.ingestVersion + assert flags.nearestCity == "Amsterdam" def test_invalid_key(self): """Test querying the API endpoint with a invalid API key.""" @@ -128,15 +148,25 @@ class BasicFunctionality(unittest.TestCase): @responses.activate def setUp(self): """Set up the data to use in the next tests.""" - - URL = "https://api.pirateweather.net/forecast/foo/50.0,10.0?units=us&lang=en&extend=None&version=1" + URL = "https://api.pirateweather.net/forecast/foo/50.0,10.0?units=auto&lang=en&version=1&icon=darksky" responses.add( responses.GET, URL, - body=open(os.path.join("./fixtures/test.json")).read(), + body=open( + os.path.join(os.path.dirname(__file__), "fixtures", "test.json") + ).read(), status=200, content_type="application/json", - match_querystring=True, + match=[ + responses.matchers.query_param_matcher( + { + "units": "auto", + "lang": "en", + "version": "1", + "icon": "darksky", + } + ) + ], ) api_key = "foo" @@ -229,15 +259,27 @@ class ForecastsWithAlerts(unittest.TestCase): @responses.activate def setUp(self): """Set up the test data with alerts to use in the next tests.""" - - URL = "https://api.pirateweather.net/forecast/foo/50.0,10.0?units=us&lang=en&extend=None&version=1" + URL = "https://api.pirateweather.net/forecast/foo/50.0,10.0?units=auto&lang=en&version=1&icon=darksky" responses.add( responses.GET, URL, - body=open(os.path.join("./fixtures/test_with_alerts.json")).read(), + body=open( + os.path.join( + os.path.dirname(__file__), "fixtures", "test_with_alerts.json" + ) + ).read(), status=200, content_type="application/json", - match_querystring=True, + match=[ + responses.matchers.query_param_matcher( + { + "units": "auto", + "lang": "en", + "version": "1", + "icon": "darksky", + } + ) + ], ) api_key = "foo" @@ -306,10 +348,11 @@ def setUp(self): responses.add( responses.GET, URL, - body=open(os.path.join("./fixtures/test.json")).read(), + body=open( + os.path.join(os.path.dirname(__file__), "fixtures", "test.json") + ).read(), status=200, content_type="application/json", - match_querystring=True, ) self.forecast = pirateweather.manual("http://test_url.com/")