Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#11 As part of being a good API citizen, instead of getting a new token at expiry, we should refresh the one we have #12

Merged
merged 2 commits into from
Apr 7, 2024
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
10 changes: 0 additions & 10 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ on:
branches:
- main
- dev
tags:
- '*'

jobs:
tests:
Expand Down Expand Up @@ -39,11 +37,3 @@ jobs:
with:
name: code-coverage-report
path: htmlcov/*
- name: Build release package
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
run: make package
- name: Publish package
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
44 changes: 44 additions & 0 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Push actions

on:
push:
tags:
- '*'

jobs:
tests:
runs-on: "ubuntu-latest"
name: Test and release
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v3"
- name: Setup Python
uses: "actions/setup-python@v4"
with:
python-version: "3.10"
- name: Install requirements
run: python3 -m pip install -r requirements_test.txt -r requirements_release.txt
- name: Run tests
run: |
python3 -m pytest \
-vv \
-qq \
--timeout=9 \
--durations=10 \
--cov podpointclient \
--cov-report term \
--cov-report html \
-o console_output_style=count \
-p no:sugar \
tests
- name: Archive code coverage results
uses: actions/upload-artifact@v3
with:
name: code-coverage-report
path: htmlcov/*
- name: Build release package
run: make package
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Pod Point Client Changelog

## v1.5.0

* Add support for refreshing expired tokens, rather than grabbing new ones each time
* Update example.py to demonstrate token expiry

## v1.4.3

* Remove additional / from pod point api calls
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ spec:
--cov-report html \
-o console_output_style=count \
-p no:sugar \
-s \
tests

lint:
Expand All @@ -25,4 +26,4 @@ package:
python3 setup.py sdist

publish: clean package
twine upload dist/* --verbose
twine upload dist/* --verbose
22 changes: 19 additions & 3 deletions example.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime, timedelta
from podpointclient.client import PodPointClient
import asyncio
import aiohttp
Expand All @@ -19,10 +20,14 @@ async def main(username: str, password: str, http_debug: bool = False, loop=None

# Verify credentials work
verified = await client.async_credentials_verified()
print(f" Credentials verified: {verified}")
print(f"Credentials verified: {verified}")
print(f" Token expiry: {client.auth.access_token_expiry}")

print("Sleeping 2s")
time.sleep(2)

print("Getting user details")
# Get user information
print("Getting user details")
user = await client.async_get_user()
print(f" Account balance {user.account.balance}p")

Expand All @@ -35,7 +40,7 @@ async def main(username: str, password: str, http_debug: bool = False, loop=None
pod = pods[0]
print(f"Selecting first pod: {pod.ppid}")

# Get firmware information for the pod
# Get firmware information for the pod
firmwares = await client.async_get_firmware(pod=pod)
firmware = firmwares[0]
print(f"Gettnig firmware data for {pod.ppid}")
Expand All @@ -58,6 +63,17 @@ async def main(username: str, password: str, http_debug: bool = False, loop=None
energy_used = charges[0].kwh_used
print(f" kW charged: {energy_used}")

# Expire token and exchange a refresh
print("Expiring token and refreshing...")
client.auth.access_token_expiry = datetime.now() - timedelta(minutes=10)
updated = await client.auth.async_update_access_token()
print(f" Token updated? {updated} - New expiry: {client.auth.access_token_expiry}")

# Get user information again
print("Getting user details with new token")
user = await client.async_get_user()
print(f" Account balance {user.account.balance}p")

if __name__ == "__main__":
import time
import argparse
Expand Down
4 changes: 4 additions & 0 deletions podpointclient/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"""Google endpoint, used for auth"""
GOOGLE_KEY = '?key=AIzaSyCwhF8IOl_7qHXML0pOd5HmziYP46IZAGU'
PASSWORD_VERIFY = f"/verifyPassword{GOOGLE_KEY}"
TOKEN = f"/token{GOOGLE_KEY}"

GOOGLE_BASE = 'www.googleapis.com/identitytoolkit/v3/relyingparty'
GOOGLE_BASE_URL = f"https://{GOOGLE_BASE}"

GOOGLE_TOKEN_BASE = 'securetoken.googleapis.com/v1'
GOOGLE_TOKEN_BASE_URL = f"https://{GOOGLE_TOKEN_BASE}"
20 changes: 14 additions & 6 deletions podpointclient/helpers/api_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,20 @@ async def __wrapper(
)

elif method == "post":
response = await self._session.post(
url,
headers=headers,
params=params,
json=data
)
if isinstance(data, str):
response = await self._session.post(
url,
headers=headers,
params=params,
data=data
)
else:
response = await self._session.post(
url,
headers=headers,
params=params,
json=data
)

elif method == "delete":
response = await self._session.delete(url, headers=headers, params=params)
Expand Down
43 changes: 35 additions & 8 deletions podpointclient/helpers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ..errors import APIError, AuthError, SessionError
from .session import Session
from ..endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY
from ..endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY, GOOGLE_TOKEN_BASE_URL, TOKEN
from .functions import HEADERS
from .api_wrapper import APIWrapper

Expand Down Expand Up @@ -61,7 +61,9 @@ async def async_update_access_token(self) -> bool:

try:
_LOGGER.debug('Updating access token')
access_token_updated: bool = await self.__update_access_token()
access_token_updated: bool = await self.__update_access_token(
refresh=self.access_token_expired()
)

_LOGGER.debug(
"Updated access token. New expiration: %s",
Expand Down Expand Up @@ -90,23 +92,45 @@ async def async_update_access_token(self) -> bool:

async def __update_access_token(self, refresh: bool = False) -> bool:
return_value = False
id_token_response = 'idToken'
refresh_token_response = 'refreshToken'
expires_in_response = 'expiresIn'

try:
wrapper = APIWrapper(session=self._session)

if refresh:
_LOGGER.debug('Refreshing access token')
headers = HEADERS.copy()
headers["Content-type"] = 'application/x-www-form-urlencoded'

url = f"{GOOGLE_TOKEN_BASE_URL}{TOKEN}"
body = f"grant_type=refresh_token&refresh_token={self.refresh_token}"

id_token_response = 'id_token'
refresh_token_response = 'refresh_token'
expires_in_response = 'expires_in'
else:
_LOGGER.debug('Getting a new access token')

url = f"{GOOGLE_BASE_URL}{PASSWORD_VERIFY}"
body = {"email": self.email, "returnSecureToken": True, "password": self.password}
headers = HEADERS.copy()

response = await wrapper.post(
url=f"{GOOGLE_BASE_URL}{PASSWORD_VERIFY}",
body={"email": self.email, "returnSecureToken": True, "password": self.password},
headers=HEADERS,
url=url,
body=body,
headers=headers,
exception_class=AuthError)

if response.status != 200:
await self.__handle_response_error(response, AuthError)

json = await response.json()
self.access_token = json["idToken"]
self.refresh_token = json["refreshToken"]
self.access_token = json[id_token_response]
self.refresh_token = json[refresh_token_response]
self.access_token_expiry = datetime.now() + timedelta(
seconds=int(json["expiresIn"]) - 10
seconds=int(json[expires_in_response]) - 10
)
return_value = True

Expand Down Expand Up @@ -134,4 +158,7 @@ async def __handle_response_error(self, response, error_class):
status = response.status
response = await response.text()

if self._http_debug:
_LOGGER.debug(response)

raise error_class(status, response)
2 changes: 1 addition & 1 deletion podpointclient/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version for the podpointclient library"""

__version__ = "1.4.3"
__version__ = "1.5.0"
8 changes: 8 additions & 0 deletions tests/fixtures/refresh.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"access_token": "1234",
"id_token": "1234",
"refresh_token": "1234",
"expires_in": "1234",
"token_type": "Bearer",
"user_id": "11111111-1111-1111-1111-11111111111"
}
7 changes: 6 additions & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import json
import os

from podpointclient.endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY, API_BASE_URL, AUTH, CHARGE_SCHEDULES, PODS, SESSIONS, UNITS, USERS, CHARGES, FIRMWARE
from podpointclient.endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY, API_BASE_URL, AUTH, CHARGE_SCHEDULES, PODS, SESSIONS, UNITS, USERS, CHARGES, FIRMWARE, GOOGLE_TOKEN_BASE_URL, TOKEN

class Mocks:
def __init__(self, m = None) -> None:
self.m = m

def happy_path(self, include_timestamp=False):
auth_response = self.auth_response()
refresh_response = self.refresh_response()
session_response = self.session_response()
pods_response = self.pods_response()
pods_response_schedule_disabled = self.pods_response_schedule_disabled()
Expand All @@ -25,6 +26,7 @@ def happy_path(self, include_timestamp=False):
question_timestamp = f'?{timestamp}'

self.m.post(f'{GOOGLE_BASE_URL}{PASSWORD_VERIFY}', payload=auth_response)
self.m.post(f'{GOOGLE_TOKEN_BASE_URL}{TOKEN}', payload=refresh_response)
self.m.post(f'{API_BASE_URL}{SESSIONS}', payload=session_response)
self.m.get(f'{API_BASE_URL}{USERS}/1234{PODS}?perpage=1&page=1{and_timestamp}', payload=pods_response)
self.m.get(f'{API_BASE_URL}{USERS}/1234{PODS}?perpage=5&page=1&include=statuses,price,model,unit_connectors,charge_schedules,charge_override{and_timestamp}', payload=pods_response)
Expand All @@ -37,6 +39,9 @@ def happy_path(self, include_timestamp=False):
def auth_response(self):
return self.__json_load_fixture('auth')

def refresh_response(self):
return self.__json_load_fixture('refresh')

def session_response(self):
return self.__json_load_fixture('session')

Expand Down
13 changes: 12 additions & 1 deletion tests/test_api_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async def test_get(aiohttp_client):
assert "OK" == await result.text()

@pytest.mark.asyncio
async def test_post(aiohttp_client):
async def test_post_with_dictionary_data(aiohttp_client):
with aioresponses() as m:
m.post('https://google.com/api/v1/test', body="OK")

Expand All @@ -27,6 +27,17 @@ async def test_post(aiohttp_client):
assert 200 == result.status
assert "OK" == await result.text()

@pytest.mark.asyncio
async def test_post_with_string_data(aiohttp_client):
with aioresponses() as m:
m.post('https://google.com/api/v1/test', body="OK")

async with aiohttp.ClientSession() as session:
wrapper = APIWrapper(session)
async with await wrapper.post("https://google.com/api/v1/test", body="foo", headers={}) as result:
assert 200 == result.status
assert "OK" == await result.text()

@pytest.mark.asyncio
async def test_put(aiohttp_client):
with aioresponses() as m:
Expand Down
Loading
Loading