diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5d26377..2e731ab 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,25 +1,33 @@ name: Docs + on: push: branches: - - master + - master - main + permissions: contents: write + jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v4 with: - python-version: 3.x - - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - - uses: actions/cache@v3 + python-version: "3.11" + + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + + - uses: actions/cache@v4 with: - key: mkdocs-material-${{ env.cache_id }} - path: .cache + key: uv-mkdocs-material-${{ env.cache_id }} + path: ~/.cache/uv restore-keys: | - mkdocs-material- - - run: pip install .[docs] - - run: mkdocs gh-deploy --force + uv-mkdocs-material- + + - run: uv sync --group docs + + - run: uv run mkdocs gh-deploy --force diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 8456044..cb24e47 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -3,37 +3,33 @@ name: Publish 📦 to PyPI on: push: tags: - - '*' + - "*" jobs: build-and-publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - - name: Install pypa/build - run: >- - python3 -m - pip install - build - --user + - name: Install uv + uses: astral-sh/setup-uv@v6 - name: Build a binary wheel and a source tarball run: >- - python3 -m + uv build --sdist --wheel - --outdir dist/ - . -# - name: Publish distribution 📦 to Test PyPI -# uses: pypa/gh-action-pypi-publish@release/v1 -# with: -# password: ${{ secrets.TEST_PYPI_API_TOKEN }} -# repository-url: https://test.pypi.org/legacy/ + --out-dir dist/ + # - name: Publish distribution 📦 to Test PyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # password: ${{ secrets.TEST_PYPI_API_TOKEN }} + # repository-url: https://test.pypi.org/legacy/ - name: Publish distribution 📦 to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9fed846..8040e8d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,21 +6,23 @@ jobs: build: strategy: matrix: - os: ['ubuntu-latest', 'macos-13', 'windows-latest'] + os: ["ubuntu-latest", "macos-14", "windows-latest"] python-version: ["3.8", "3.11"] runs-on: ${{ matrix.os }} + steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Install dependencies with uv run: | - python -m pip install --upgrade pip - pip install .[all] + uv python install ${{ matrix.python-version }} + uv sync --python ${{ matrix.python-version }} --extra all --all-groups + - name: Test with pytest env: SOLCAST_API_KEY: ${{ secrets.SOLCAST_API_KEY }} run: | - pytest tests + uv run --python ${{ matrix.python-version }} --group test pytest tests diff --git a/.gitignore b/.gitignore index cfe62b5..6d391e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ public +dist +uv.lock +.python-version .idea *.ipynb_checkpoints .pytest_cache diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d969f96 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.testing.pytestArgs": ["tests"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bfc58..5a4d388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ # Changelog -## [1.3.0] - 2024-07-10 +## [1.3.1] - 2025-11-19 + +- Add the `Kimber` and `HSU` to live, forecast, and historic module -- Add the `aggregations` module. No tests as we are yet to expose unmetered aggregations. +## [1.3.0] - 2024-07-10 +- Add the `aggregations` module. No tests as we are yet to expose unmetered aggregations. ## [1.2.5] - 2024-07-05 diff --git a/README.md b/README.md index f5f1618..f57fd7f 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,13 @@ **Documentation**: https://solcast.github.io/solcast-api-python-sdk/ ## Install + ```commandline pip install solcast ``` -or from source: + +or from source: + ```commandline git clone https://github.com/Solcast/solcast-api-python-sdk.git cd solcast-api-python-sdk @@ -22,7 +25,8 @@ pip install . ``` The vanilla version doesn't have any dependency. For full functionality, -for example for getting the data into `DataFrames`, and for development, use the `[all]` tag: +for example for getting the data into `DataFrames`, and for development, use the `[all]` tag: + ```commandline pip install .[all] for the dev libs ``` @@ -39,15 +43,32 @@ df = live.radiation_and_weather( ).to_pandas() ``` -Don't forget to set your [account Api Key](https://toolkit.solcast.com.au/register) with: -```export SOLCAST_API_KEY={your commercial api_key}``` +Don't forget to set your [account Api Key](https://toolkit.solcast.com.au/register) with: +`export SOLCAST_API_KEY={your commercial api_key}` --- ## Contributing -Tests are run against the Solcast API, you will need an API key to run them. + +Tests are run against the Solcast API, you will need an API key to run them. They are executed on `unmetered locations` and as such won't consume your requests. ```commandline pytest tests ``` + +### Formatters and Linters + +| Language | Formatter/Linter | +| -------- | ---------------- | +| `yaml` | `yamlls` | +| `toml` | `taplo` | +| `python` | `black` | + +### Recommended Python Development Version + +Develop on the oldest supported `Python` version. + +```bash +uv python pin 3.8 +``` diff --git a/pyproject.toml b/pyproject.toml index 7a85254..da09372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,28 +6,28 @@ build-backend = "hatchling.build" name = "solcast" description = "a simple Python SDK for the Solcast API" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = "Apache-2.0" classifiers = [ - "Intended Audience :: Information Technology", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python", - "Topic :: Internet", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development", - "Topic :: Scientific/Engineering", - "Typing :: Typed", - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", + "Intended Audience :: Information Technology", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", + "Topic :: Scientific/Engineering", + "Typing :: Typed", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] dynamic = ["version"] @@ -38,21 +38,7 @@ Documentation = "https://solcast.github.io/solcast-api-python-sdk" Repository = "https://github.com/Solcast/solcast-api-python-sdk" [project.optional-dependencies] -docs = [ - "pytest", - "mkdocs", - "mkdocs-material", - "mkdocstrings[python]==0.25", - "mkdocs-jupyter", - "kaleido" -] -all = [ - "notebook", - "matplotlib", - "pandas", - "black", - "solcast[docs]" -] +all = ["notebook", "matplotlib", "pandas"] [tool.hatch.version] path = "solcast/__init__.py" @@ -65,9 +51,16 @@ junit_family = "xunit2" [tool.coverage.run] parallel = true -source = [ - "docs", - "tests", - "solcast" -] +source = ["docs", "tests", "solcast"] context = '${CONTEXT}' + +[dependency-groups] +docs = [ + "kaleido>=1.2.0", + "mkdocs>=1.6.1", + "mkdocs-jupyter>=0.24.8", + "mkdocs-material>=9.7.0", + "mkdocstrings[python]==0.26.1", +] +lint = ["black>=24.8.0", "isort>=5.13.2"] +test = ["pytest>=8.3.5"] diff --git a/solcast/__init__.py b/solcast/__init__.py index 813a1cb..260711c 100644 --- a/solcast/__init__.py +++ b/solcast/__init__.py @@ -1,15 +1,13 @@ -__version__ = "1.3.0" +__version__ = "1.3.1" from . import ( - api, + aggregations, forecast, historic, live, + pv_power_sites, tmy, - aggregations, unmetered_locations, - urls, - pv_power_sites, ) __all__ = [ diff --git a/solcast/aggregations.py b/solcast/aggregations.py index f8dd736..631f013 100644 --- a/solcast/aggregations.py +++ b/solcast/aggregations.py @@ -1,11 +1,7 @@ from typing import Optional from .api import Client, PandafiableResponse -from .urls import ( - base_url, - forecast_grid_aggregations, - live_grid_aggregations, -) +from .urls import base_url, forecast_grid_aggregations, live_grid_aggregations def live( diff --git a/solcast/api.py b/solcast/api.py index 5d7e01d..ecaba75 100644 --- a/solcast/api.py +++ b/solcast/api.py @@ -1,21 +1,14 @@ import copy import json import os -from dataclasses import dataclass -from urllib.request import urlopen, Request -import urllib.parse import urllib.error -from typing import Optional +import urllib.parse +from dataclasses import dataclass +from typing import Optional, Tuple +from urllib.request import Request, urlopen import solcast -try: - import pandas as pd -except ImportError: - _PANDAS = False -else: - _PANDAS = True - @dataclass class Response: @@ -34,10 +27,13 @@ def __repr__(self): def to_dict(self): if self.code not in [200, 204]: raise Exception(self.exception) - if self.code == 204: # Case of valid no content + if self.code == 204: return dict() return json.loads(self.data) + def __call__(self, *args, **kwargs) -> "Response": + return type(self)(*args, **kwargs) + class PandafiableResponse(Response): """Class to handle API response from the Solcast API, with pandas integration.""" @@ -48,9 +44,13 @@ def to_pandas(self): like casting the datetime columns and setting them as index. """ # not ideal to run this for every Response - assert _PANDAS, ImportError( - "Pandas needs to be installed for this functionality." - ) + + try: + import pandas as pd + except ImportError as e: + raise ImportError( + f"Pandas needs to be installed for this functionality. {e}" + ) if self.code != 200: raise Exception(self.exception) @@ -63,18 +63,26 @@ def to_pandas(self): # to make it work with different Pandas versions if dfs.index.tz is None: - dfs.index.tz = "UTC" + dfs.index = dfs.index.tz_localize("UTC") dfs.index.name = "period_end" dfs = dfs.drop(columns=["period_end", "period"]) return dfs + def __call__(self, *args, **kwargs) -> "PandafiableResponse": + return type(self)(*args, **kwargs) + class Client: """Handles all API get requests for the different endpoints.""" - def __init__(self, base_url: str, endpoint: str, response_type: Response): + def __init__( + self, + base_url: str, + endpoint: str, + response_type: Response, + ): """ Args: base_url: the base URL to Solcast API @@ -87,7 +95,7 @@ def __init__(self, base_url: str, endpoint: str, response_type: Response): self.url = self.make_url() @staticmethod - def _check_params(params: dict) -> (dict, str): + def _check_params(params: dict) -> Tuple[dict, str]: """Run basic checks on the parameters that will be passed to the HTTP request.""" assert isinstance(params, dict), "parameters needs to be a dict" params = copy.deepcopy(params) @@ -225,7 +233,7 @@ def _make_request(self, params: dict, method: str) -> Response: except urllib.error.HTTPError as e: try: exception_message = json.loads(e.read())["response_status"]["message"] - except: + except Exception: exception_message = "Undefined Error" return self.response( code=e.code, diff --git a/solcast/forecast.py b/solcast/forecast.py index 3ec2762..3081d1b 100644 --- a/solcast/forecast.py +++ b/solcast/forecast.py @@ -3,9 +3,11 @@ from .api import Client, PandafiableResponse from .urls import ( base_url, - forecast_rooftop_pv_power, - forecast_radiation_and_weather, forecast_advanced_pv_power, + forecast_radiation_and_weather, + forecast_rooftop_pv_power, + forecast_soiling_hsu, + forecast_soiling_kimber, ) @@ -29,7 +31,7 @@ def radiation_and_weather( client = Client( base_url=base_url, endpoint=forecast_radiation_and_weather, - response_type=PandafiableResponse, + response_type=PandafiableResponse, # type: ignore[arg-type] ) return client.get( @@ -63,7 +65,7 @@ def rooftop_pv_power( client = Client( base_url=base_url, endpoint=forecast_rooftop_pv_power, - response_type=PandafiableResponse, + response_type=PandafiableResponse, # type: ignore[arg-type] ) return client.get( @@ -93,7 +95,80 @@ def advanced_pv_power(resource_id: int, **kwargs) -> PandafiableResponse: client = Client( base_url=base_url, endpoint=forecast_advanced_pv_power, - response_type=PandafiableResponse, + response_type=PandafiableResponse, # type: ignore[arg-type] ) return client.get({"resource_id": resource_id, "format": "json", **kwargs}) + + +def soiling_kimber( + latitude: float, + longitude: float, + **kwargs, +) -> PandafiableResponse: + """Get hourly soiling loss forecast using the Kimber model. + + Returns a time series of forecast cumulative soiling / cleanliness state for the + requested location based on Pvlib's Kimber model. + + Args: + latitude: Decimal degrees, between -90 and 90 (north positive). + longitude: Decimal degrees, between -180 and 180 (east positive). + **kwargs: Additional query parameters accepted by the endpoint (e.g. depo_veloc_pm10, initial_soiling). + + Returns: + PandafiableResponse: Response object; call `.to_pandas()` for a DataFrame. + + See https://docs.solcast.com.au/ for full parameter details. + """ + url = kwargs.get("base_url", base_url) + + client = Client( + base_url=url, + endpoint=forecast_soiling_kimber, + response_type=PandafiableResponse, # type: ignore[arg-type] + ) + return client.get( + { + "latitude": latitude, + "longitude": longitude, + "format": "json", + **kwargs, + } + ) + + +def soiling_hsu( + latitude: float, + longitude: float, + **kwargs, +) -> PandafiableResponse: + """Get hourly soiling loss forecast using the HSU model. + + Returns a time series of forecast cumulative soiling / cleanliness state for the + requested location based on Solcast's HSU model. + + Args: + latitude: Decimal degrees, between -90 and 90 (north positive). + longitude: Decimal degrees, between -180 and 180 (east positive). + **kwargs: Additional query parameters accepted by the endpoint (e.g. depo_veloc_pm10, initial_soiling). + + Returns: + PandafiableResponse: Response object; call `.to_pandas()` for a DataFrame. + + See https://docs.solcast.com.au/ for full parameter details. + """ + url = kwargs.get("base_url", base_url) + client = Client( + base_url=url, + endpoint=forecast_soiling_hsu, + response_type=PandafiableResponse, # type: ignore[arg-type] + ) + return client.get( + { + "latitude": latitude, + "longitude": longitude, + "format": "json", + **kwargs, + } + ) diff --git a/solcast/historic.py b/solcast/historic.py index 4703638..adbc776 100644 --- a/solcast/historic.py +++ b/solcast/historic.py @@ -1,9 +1,13 @@ +from typing import Optional + from .api import Client, PandafiableResponse from .urls import ( base_url, + historic_advanced_pv_power, historic_radiation_and_weather, historic_rooftop_pv_power, - historic_advanced_pv_power, + historic_soiling_hsu, + historic_soiling_kimber, ) @@ -11,8 +15,8 @@ def radiation_and_weather( latitude: float, longitude: float, start: str, - end: str = None, - duration: str = None, + end: Optional[str] = None, + duration: Optional[str] = None, **kwargs, ) -> PandafiableResponse: """ @@ -36,7 +40,7 @@ def radiation_and_weather( client = Client( base_url=base_url, endpoint=historic_radiation_and_weather, - response_type=PandafiableResponse, + response_type=PandafiableResponse, # type: ignore[arg-type] ) params = { @@ -59,8 +63,8 @@ def rooftop_pv_power( latitude: float, longitude: float, start: str, - end: str = None, - duration: str = None, + end: Optional[str] = None, + duration: Optional[str] = None, **kwargs, ) -> PandafiableResponse: """ @@ -83,7 +87,7 @@ def rooftop_pv_power( client = Client( base_url=base_url, endpoint=historic_rooftop_pv_power, - response_type=PandafiableResponse, + response_type=PandafiableResponse, # type: ignore[arg-type] ) assert (end is None and duration is not None) | ( @@ -107,7 +111,11 @@ def rooftop_pv_power( def advanced_pv_power( - resource_id: int, start: str, end: str = None, duration: str = None, **kwargs + resource_id: int, + start: str, + end: Optional[str] = None, + duration: Optional[str] = None, + **kwargs, ) -> PandafiableResponse: """ Get historical high spec PV power estimated actuals for the requested site, @@ -127,7 +135,7 @@ def advanced_pv_power( client = Client( base_url=base_url, endpoint=historic_advanced_pv_power, - response_type=PandafiableResponse, + response_type=PandafiableResponse, # type: ignore[arg-type] ) assert (end is None and duration is not None) | ( @@ -138,6 +146,114 @@ def advanced_pv_power( "resource_id": resource_id, "start": start, "format": "json", + "format": "json", + **kwargs, + } + + if end is not None: + params["end"] = end + if duration is not None: + params["duration"] = duration + + return client.get(params) + + +def soiling_kimber( + latitude: float, + longitude: float, + start: str, + end: Optional[str] = None, + duration: Optional[str] = None, + **kwargs, +) -> PandafiableResponse: + """Get hourly historical soiling loss using the Kimber model. + + Returns a time series of estimated historical cumulative soiling / cleanliness state + for the requested location based on Pvlib's Kimber model. + + Args: + latitude: Decimal degrees, between -90 and 90 (north positive). + longitude: Decimal degrees, between -180 and 180 (east positive). + start: Datetime-like (YYYY-MM-DD or ISO8601) start of period. + end: Optional, end of requested period (mutually exclusive with duration). + duration: Optional, ISO8601 duration within 31 days of start (mutually exclusive with end). + **kwargs: Additional query parameters accepted by the endpoint (e.g. depo_veloc_pm10, initial_soiling). + + Returns: + PandafiableResponse: Response object; call `.to_pandas()` for a DataFrame. + + See https://docs.solcast.com.au/ for full parameter details. + """ + assert (end is None and duration is not None) | ( + duration is None and end is not None + ), "only one of duration or end" + + url = kwargs.get("base_url", base_url) + client = Client( + base_url=url, + endpoint=historic_soiling_kimber, + response_type=PandafiableResponse, # type: ignore[arg-type] + ) + + params = { + "latitude": latitude, + "longitude": longitude, + "start": start, + "format": "json", + **kwargs, + } + + if end is not None: + params["end"] = end + if duration is not None: + params["duration"] = duration + + return client.get(params) + + +def soiling_hsu( + latitude: float, + longitude: float, + start: str, + end: Optional[str] = None, + duration: Optional[str] = None, + base_url=base_url, + **kwargs, +) -> PandafiableResponse: + """Get hourly historical soiling loss using the HSU model. + + Returns a time series of estimated historical cumulative soiling / cleanliness state + for the requested location based on Solcast's HSU model. + + Args: + latitude: Decimal degrees, between -90 and 90 (north positive). + longitude: Decimal degrees, between -180 and 180 (east positive). + start: Datetime-like (YYYY-MM-DD or ISO8601) start of period. + end: Optional, end of requested period (mutually exclusive with duration). + duration: Optional, ISO8601 duration within 31 days of start (mutually exclusive with end). + **kwargs: Additional query parameters accepted by the endpoint (e.g. depo_veloc_pm10, initial_soiling). + + Returns: + PandafiableResponse: Response object; call `.to_pandas()` for a DataFrame. + + See https://docs.solcast.com.au/ for full parameter details. + """ + assert (end is None and duration is not None) | ( + duration is None and end is not None + ), "only one of duration or end" + + url = kwargs.get("base_url", base_url) + client = Client( + base_url=url, + endpoint=historic_soiling_hsu, + response_type=PandafiableResponse, # type: ignore[arg-type] + ) + + params = { + "latitude": latitude, + "longitude": longitude, + "start": start, + "format": "json", **kwargs, } diff --git a/solcast/live.py b/solcast/live.py index a85227a..15746d8 100644 --- a/solcast/live.py +++ b/solcast/live.py @@ -3,9 +3,9 @@ from .api import Client, PandafiableResponse from .urls import ( base_url, + live_advanced_pv_power, live_radiation_and_weather, live_rooftop_pv_power, - live_advanced_pv_power, ) @@ -89,3 +89,80 @@ def advanced_pv_power(resource_id: int, **kwargs) -> PandafiableResponse: ) return client.get({"resource_id": resource_id, "format": "json", **kwargs}) + + +def soiling_hsu( + latitude: float, + longitude: float, + **kwargs, +): + """Get hourly soiling loss using the HSU model. + + Returns a time series of estimated cumulative soiling / cleanliness state for the + requested location based on Solcast's HSU model. + + Args: + latitude: Decimal degrees, between -90 and 90 (north positive). + longitude: Decimal degrees, between -180 and 180 (east positive). + **kwargs: Additional query parameters accepted by the endpoint (e.g. depo_veloc_pm10, initial_soiling). + + Returns: + PandafiableResponse: Response object; call `.to_pandas()` for a DataFrame. + + See https://docs.solcast.com.au/ for full parameter details. + """ + from solcast.urls import live_soiling_hsu + + url = kwargs.get("base_url", base_url) + client = Client( + base_url=url, + endpoint=live_soiling_hsu, + response_type=PandafiableResponse, + ) + return client.get( + { + "latitude": latitude, + "longitude": longitude, + "format": "json", + **kwargs, + } + ) + + +def soiling_kimber( + latitude: float, + longitude: float, + base_url=base_url, + **kwargs, +) -> PandafiableResponse: + """Get hourly soiling loss using the Kimber model. + + Returns a time series of estimated cumulative soiling / cleanliness state for the + requested location based on Pvlib's Kimber model. + + Args: + latitude: Decimal degrees, between -90 and 90 (north positive). + longitude: Decimal degrees, between -180 and 180 (east positive). + **kwargs: Additional query parameters accepted by the endpoint (e.g. depo_veloc_pm10, initial_soiling). + + Returns: + PandafiableResponse: Response object; call `.to_pandas()` for a DataFrame. + + See https://docs.solcast.com.au/ for full parameter details. + """ + from solcast.urls import live_soiling_kimber + + url = kwargs.get("base_url", base_url) + client = Client( + base_url=url, + endpoint=live_soiling_kimber, + response_type=PandafiableResponse, # type: ignore[arg-type] + ) + return client.get( + { + "latitude": latitude, + "longitude": longitude, + "format": "json", + **kwargs, + } + ) diff --git a/solcast/urls.py b/solcast/urls.py index 367ea26..ce7e236 100644 --- a/solcast/urls.py +++ b/solcast/urls.py @@ -3,13 +3,19 @@ live_rooftop_pv_power = "data/live/rooftop_pv_power" live_advanced_pv_power = "data/live/advanced_pv_power" live_grid_aggregations = "data/live/aggregations" +live_soiling_kimber = "data/live/soiling/kimber" +live_soiling_hsu = "data/live/soiling/hsu" historic_radiation_and_weather = "data/historic/radiation_and_weather" historic_rooftop_pv_power = "data/historic/rooftop_pv_power" historic_advanced_pv_power = "data/historic/advanced_pv_power" +historic_soiling_kimber = "data/historic/soiling/kimber" +historic_soiling_hsu = "data/historic/soiling/hsu" forecast_radiation_and_weather = "data/forecast/radiation_and_weather" forecast_rooftop_pv_power = "data/forecast/rooftop_pv_power" forecast_advanced_pv_power = "data/forecast/advanced_pv_power" forecast_grid_aggregations = "data/forecast/aggregations" +forecast_soiling_kimber = "data/forecast/soiling/kimber" +forecast_soiling_hsu = "data/forecast/soiling/hsu" tmy_radiation_and_weather = "data/tmy/radiation_and_weather" tmy_rooftop_pv_power = "data/tmy/rooftop_pv_power" pv_power_site = "resources/pv_power_site" diff --git a/tests/test_client.py b/tests/test_client.py index 2250c5f..f07f195 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,10 +1,11 @@ +import pytest + from solcast.api import Client, PandafiableResponse, Response from solcast.urls import ( base_url, - live_radiation_and_weather, historic_radiation_and_weather, + live_radiation_and_weather, ) -import pytest @pytest.fixture diff --git a/tests/test_forecast.py b/tests/test_forecast.py index 4d63770..90ac179 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -1,7 +1,8 @@ +import pandas as pd from solcast import forecast from solcast.unmetered_locations import ( - load_test_locations_coordinates, UNMETERED_LOCATIONS, + load_test_locations_coordinates, ) @@ -38,3 +39,30 @@ def test_advanced_pv_power(): capacity=1, ) assert res.success is True + + +def test_soiling_kimber(): + lats, longs = load_test_locations_coordinates() + res = forecast.soiling_kimber( + latitude=lats[0], + longitude=longs[0], + manual_washdates=["2024-01-01"], + ) + assert res.success is True + assert res.to_dict()["forecasts"][0]["period"] == "PT30M" + df = res.to_pandas() + assert isinstance(df, pd.DataFrame) + assert df.shape[0] > 0 + + +def test_soiling_hsu(): + lats, longs = load_test_locations_coordinates() + res = forecast.soiling_hsu( + latitude=lats[1], + longitude=longs[1], + ) + assert res.success is True + assert res.to_dict()["forecasts"][0]["period"] == "PT30M" + df = res.to_pandas() + assert isinstance(df, pd.DataFrame) + assert df.shape[0] > 0 diff --git a/tests/test_historic.py b/tests/test_historic.py index 9cfc5ce..618bcdf 100644 --- a/tests/test_historic.py +++ b/tests/test_historic.py @@ -1,11 +1,11 @@ +import pandas as pd import pytest from solcast import historic from solcast.unmetered_locations import ( - load_test_locations_coordinates, UNMETERED_LOCATIONS, + load_test_locations_coordinates, ) -import pandas as pd def test_radiation_and_weather(): @@ -53,3 +53,36 @@ def test_advanced_pv_power(): assert res.success is True assert len(res.to_dict()["estimated_actuals"]) == 3 * 48 + 1 assert ~res.to_pandas().isna().any().all() + + +def test_soiling_kimber(): + lats, longs = load_test_locations_coordinates() + res = historic.soiling_kimber( + latitude=lats[2], + longitude=longs[2], + start="2022-10-25T14:45:00.00Z", + duration="P3D", + manual_washdates=["2022-10-01"], + ) + assert res.success is True + first = res.to_dict()["estimated_actuals"][0] + assert first["period"] == "PT30M" + df = res.to_pandas() + assert isinstance(df, pd.DataFrame) + assert df.shape[0] > 0 + + +def test_soiling_hsu(): + lats, longs = load_test_locations_coordinates() + res = historic.soiling_hsu( + latitude=lats[3], + longitude=longs[3], + start="2022-10-25T14:45:00.00Z", + duration="P3D", + ) + assert res.success is True + first = res.to_dict()["estimated_actuals"][0] + assert first["period"] == "PT30M" + df = res.to_pandas() + assert isinstance(df, pd.DataFrame) + assert df.shape[0] > 0 diff --git a/tests/test_live.py b/tests/test_live.py index d91edc1..1add13e 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -1,7 +1,8 @@ +import pandas as pd from solcast import live from solcast.unmetered_locations import ( - load_test_locations_coordinates, UNMETERED_LOCATIONS, + load_test_locations_coordinates, ) @@ -30,7 +31,7 @@ def test_fail_rooftop_pv_power(): res = live.rooftop_pv_power(latitude=lats[0], longitude=longs[0]) assert res.success is False assert res.code == 400 - assert res.exception == "'Capacity' must be greater than '0'." + assert res.exception == "'capacity' must be greater than '0'." def test_advanced_pv_power(): @@ -39,3 +40,30 @@ def test_advanced_pv_power(): ) assert res.success is True + + +def test_soiling_kimber_live(): + lats, longs = load_test_locations_coordinates() + res = live.soiling_kimber( + latitude=lats[0], + longitude=longs[0], + manual_washdates=["2024-01-01"], + ) + assert res.success is True + assert res.to_dict()["estimated_actuals"][0]["period"] == "PT30M" + df = res.to_pandas() + assert isinstance(df, pd.DataFrame) + assert df.shape[0] > 0 + + +def test_soiling_hsu_live(): + lats, longs = load_test_locations_coordinates() + res = live.soiling_hsu( + latitude=lats[1], + longitude=longs[1], + ) + assert res.success is True + assert res.to_dict()["estimated_actuals"][0]["period"] == "PT30M" + df = res.to_pandas() + assert isinstance(df, pd.DataFrame) + assert df.shape[0] > 0 diff --git a/tests/test_tmy.py b/tests/test_tmy.py index a22a718..cc42539 100644 --- a/tests/test_tmy.py +++ b/tests/test_tmy.py @@ -23,8 +23,10 @@ def test_rooftop_pv_power(): def test_fail_rooftop_pv_power(): lats, longs = load_test_locations_coordinates() - res = tmy.rooftop_pv_power(latitude=lats[0], longitude=longs[0], array_type="wrong") + res = tmy.rooftop_pv_power( + latitude=lats[0], longitude=longs[0], array_type="wrong", capacity=3 + ) assert res.success is False assert res.code == 400 - assert res.exception == "The specified condition was not met for 'Array Type'." + assert res.exception == "The specified condition was not met for 'array_type'."