Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
# PV_Forecast-API
A Python implementation of the PV_Forecast web API. See [www.solar.sheffield.ac.uk/pvforecast/](https://www.solar.sheffield.ac.uk/pvforecast/) and [api.solar.sheffield.ac.uk](https://api.solar.sheffield.ac.uk/)

**Latest Version: 0.4**
**Latest Version: 0.5*

**New! Updated 2022-08-01 to use PV_Forecast API v4.**
**New! Updated 2022-12-12 to add support for proxy connections and add command line arguments.**

## About this repository

Expand Down Expand Up @@ -85,6 +86,52 @@ pvf = PVForecast(user_id="", api_key="") # Enter your user_id and api_key here!
|Get the nationally aggregated GB PV outturn forecast with forecast base `2021-02-23T07:00:00Z` as a DataFrame|`pvf.get_forecast(datetime(2021, 2, 23, 7, 0, tzinfo=pytz.utc), dataframe=True))`|![Screenshot of output](/misc/code_example_output3.png?raw=true)|
|Get all 07:00 nationally aggregated GB PV outturn forecasts between `2021-02-23T01:00:00Z` and `2021-02-23T07:00:00Z`, as a DataFrame|`pvf.get_forecasts(datetime(2021, 2, 23, 1, 0, tzinfo=pytz.utc), datetime(2021, 2, 23, 7, 0, tzinfo=pytz.utc), forecast_base_times=["07:00"], dataframe=True))`|![Screenshot of output](/misc/code_example_output4.png?raw=true)|

## Command Line Utilities

### pvforecast

This utility can be used to download data to a CSV file:

```
usage: pvforecast.py [-h] --user_id <user_id> [--api_key <api_key>]
[-s "<yyyy-mm-dd HH:MM:SS>"] [-e "<yyyy-mm-dd HH:MM:SS>"]
[--entity_type <entity_type>] [--entity_id <entity_id>]
[-q] [-o </path/to/output/file>] [-http <http_proxy>]
[-https <https_proxy>]

This is a command line interface (CLI) for the PVForecast API module

optional arguments:
-h, --help show this help message and exit
--user_id <user_id> PVForecast user id.
--api_key <api_key> Your PVForecast API key. If not path passed, will
check environment variables for `PVForecastAPIKey` and
finally a config file in same directory named
`.pvforecast_credentials`
-s "<yyyy-mm-dd HH:MM:SS>", --start "<yyyy-mm-dd HH:MM:SS>"
Specify a UTC start date in 'yyyy-mm-dd HH:MM:SS'
format (inclusive), default behaviour is to retrieve
the latest outturn.
-e "<yyyy-mm-dd HH:MM:SS>", --end "<yyyy-mm-dd HH:MM:SS>"
Specify a UTC end date in 'yyyy-mm-dd HH:MM:SS' format
(inclusive), default behaviour is to retrieve the
latest outturn.
--entity_type <entity_type>
Specify an entity type, either 'gsp' or 'pes'. Default
is 'gsp'.
--entity_id <entity_id>
Specify an entity ID, default is 0 (i.e. national).
-q, --quiet Specify to not print anything to stdout.
-o </path/to/output/file>, --outfile </path/to/output/file>
Specify a CSV file to write results to.
-http <http_proxy>, -http-proxy <http_proxy>
HTTP Proxy address
-https <https_proxy>, -https-proxy <https_proxy>
HTTPS Proxy address

Jamie Taylor & Ethan Jones, 2022-12-10
```

## Documentation

* [https://sheffieldsolar.github.io/PV_Forecast-API/](https://sheffieldsolar.github.io/PV_Forecast-API/)
Expand Down
122 changes: 113 additions & 9 deletions pvforecast_api/pvforecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
A Python interface for the PV_Forecast web API from Sheffield Solar.

- Jamie Taylor <jamie.taylor@sheffield.ac.uk>
- Ethan Jones <ejones18@sheffield.ac.uk>
- First Authored: 2018-08-31
- Updated: 2022-12-10 to provide support for proxy connections & CLI.
"""

import os
import sys
from datetime import datetime, timedelta, date, time
from math import ceil
Expand All @@ -13,9 +16,12 @@
import json
import pytz
import requests
import argparse

from numpy import nan, int64
import pandas as pd

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))

class PVForecastException(Exception):
"""An Exception specific to the PVForecast class."""
Expand All @@ -42,14 +48,15 @@ class PVForecast:
`retries` : int
Optionally specify the number of retries to use should the API respond with anything
other than status code 200. Exponential back-off applies inbetween retries.

`proxies` : Dict
Optionally specify a Dict of proxies for http and https requests in the format:
{"http": "<address>", "https": "<address>"}
Notes
-----
To obtain a User ID and API key, please visit the `PV_Forecast API website <https://api.solar.sheffield.ac.uk/pvforecast/>`_.
"""
def __init__(self, user_id, api_key, retries=3):
if not user_id or not api_key:
raise PVForecastException("You must pass a valid user_id and api_key.")
def __init__(self, user_id, api_key, retries=3, proxies=None):
self.proxies = proxies
self.base_url = "https://api0.solar.sheffield.ac.uk/pvforecast/api/v4/"
self.retries = retries
self.params = {"user_id": user_id, "key": api_key, "data_format": "json"}
Expand Down Expand Up @@ -296,7 +303,7 @@ def _fetch_url(self, url):
while not success and try_counter < self.retries + 1:
try_counter += 1
try:
page = requests.get(url)
page = requests.get(url, proxies=self.proxies)
page.raise_for_status()
if page.status_code == 200 and "Your api key is not valid" in page.text:
raise PVForecastException("The user_id and/or api_key entered are invalid.")
Expand Down Expand Up @@ -357,10 +364,107 @@ def _convert_tuple_to_df(self, data, columns):
data.datetime_gmt = pd.to_datetime(data.datetime_gmt)
return data

def main():
"""Placeholder for CLI to be added in future release."""
print("There is no CLI for this module yet.")
sys.exit()
def parse_options():
"""Parse command line options."""
parser = argparse.ArgumentParser(description=("This is a command line interface (CLI) for the "
"PVForecast API module"),
epilog="Jamie Taylor & Ethan Jones, 2022-12-10")
parser.add_argument("--user_id", metavar="<user_id>", dest="user_id", action="store",
type=str, required=True, help="PVForecast user id.")
parser.add_argument("--api_key", metavar="<api_key>", dest="api_key", action="store",
type=str, required=False, help="Your PVForecast API key. "
"If not path passed, will check environment variables for `PVForecastAPIKey` "
"and finally a config file in same directory named `.pvforecast_credentials` ")
parser.add_argument("-s", "--start", metavar="\"<yyyy-mm-dd HH:MM:SS>\"", dest="start",
action="store", type=str, required=False, default=None,
help="Specify a UTC start date in 'yyyy-mm-dd HH:MM:SS' format "
"(inclusive), default behaviour is to retrieve the latest outturn.")
parser.add_argument("-e", "--end", metavar="\"<yyyy-mm-dd HH:MM:SS>\"", dest="end",
action="store", type=str, required=False, default=None,
help="Specify a UTC end date in 'yyyy-mm-dd HH:MM:SS' format (inclusive), "
"default behaviour is to retrieve the latest outturn.")
parser.add_argument("--entity_type", metavar="<entity_type>", dest="entity_type",
action="store", type=str, required=False, default="gsp",
choices=["gsp", "pes"],
help="Specify an entity type, either 'gsp' or 'pes'. Default is 'gsp'.")
parser.add_argument("--entity_id", metavar="<entity_id>", dest="entity_id", action="store",
type=int, required=False, default=0,
help="Specify an entity ID, default is 0 (i.e. national).")
parser.add_argument("-q", "--quiet", dest="quiet", action="store_true",
required=False, help="Specify to not print anything to stdout.")
parser.add_argument("-o", "--outfile", metavar="</path/to/output/file>", dest="outfile",
action="store", type=str, required=False,
help="Specify a CSV file to write results to.")
parser.add_argument('-http', '-http-proxy', metavar="<http_proxy>", dest="http",
type=str, required=False, default=None, action="store",
help="HTTP Proxy address")
parser.add_argument('-https', '-https-proxy', metavar="<https_proxy>", dest="https",
type=str, required=False, default=None, action="store",
help="HTTPS Proxy address")
options = parser.parse_args()

def handle_options(options):
"""Validate command line args and pre-process where necessary."""
if options.api_key is None:
key = os.environ.get('PVForecastAPIKey')
if key is None:
config_path = os.path.join(SCRIPT_DIR, ".pvforecast_credentials")
if os.path.exists(config_path):
with open(config_path) as f:
key = f.read()
if key is None:
raise Exception("OptionsError: Couldn't fetch API Key, ensure either the file path is "
"passed via the CLI, the `PVForecastAPIKey` environment variable is "
"defined or it exists in the `C:\PVForecastAPIKey.yaml` config file.")
else:
options.api_key = key
if (options.outfile is not None and os.path.exists(options.outfile)) and not options.quiet:
try:
input(f"The output file '{options.outfile}' already exists and will be "
"overwritten, are you sure you want to continue? Press enter to continue or "
"ctrl+c to abort.")
except KeyboardInterrupt:
print()
print("Aborting...")
sys.exit(0)
if options.start is not None:
try:
options.start = pytz.utc.localize(
datetime.strptime(options.start, "%Y-%m-%d %H:%M:%S")
)
except:
raise Exception("OptionsError: Failed to parse start datetime, make sure you use "
"'yyyy-mm-dd HH:MM:SS' format.")
if options.end is not None:
try:
options.end = pytz.utc.localize(datetime.strptime(options.end, "%Y-%m-%d %H:%M:%S"))
except:
raise Exception("OptionsError: Failed to parse end datetime, make sure you use "
"'yyyy-mm-dd HH:MM:SS' format.")
proxies = {}
if options.http is not None:
proxies.update({"http": options.http})
if options.https is not None:
proxies.update({"https": options.https})
options.proxies = proxies
return options
return handle_options(options)

def main():
options = parse_options()
pvforecast = PVForecast(options.user_id, options.api_key, proxies=options.proxies)
if options.start is None and options.end is None:
data = pvforecast.latest(entity_type=options.entity_type, entity_id=options.entity_id,
dataframe=True)
else:
start = datetime(2014, 1, 1, 0, 30, tzinfo=pytz.utc) if options.start is None \
else options.start
end = pytz.utc.localize(datetime.utcnow()) if options.end is None else options.end
data = pvforecast.get_forecasts(start, end, entity_type=options.entity_type,
entity_id=options.entity_id, dataframe=True)
if options.outfile is not None:
data.to_csv(options.outfile, float_format="%.3f", index=False)
if not options.quiet:
print(data)
if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pytz
requests
numpy
pandas
pandas
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# https://packaging.python.org/en/latest/single_source_version.html
version="0.4",
version="0.5",

description="A Python interface for the PV_Forecast web API from Sheffield Solar.",
long_description=long_description,
Expand Down