Skip to content

Commit fe12294

Browse files
committed
As part of being a good API citizen, instead of getting a new token at expiry, we should refresh the one we have.
* Adds token refresh using the Firebase token refresh system * Update example to include refreshing token * Add form enccoded post capabilities to api wrapper (although implemented in a smelly way)
1 parent 30c1d18 commit fe12294

12 files changed

Lines changed: 194 additions & 50 deletions

File tree

.github/workflows/push.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ on:
55
branches:
66
- main
77
- dev
8-
tags:
9-
- '*'
108

119
jobs:
1210
tests:
@@ -39,11 +37,3 @@ jobs:
3937
with:
4038
name: code-coverage-report
4139
path: htmlcov/*
42-
- name: Build release package
43-
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
44-
run: make package
45-
- name: Publish package
46-
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
47-
uses: pypa/gh-action-pypi-publish@release/v1
48-
with:
49-
password: ${{ secrets.PYPI_API_TOKEN }}

.github/workflows/tag.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Push actions
2+
3+
on:
4+
push:
5+
tags:
6+
- '*'
7+
8+
jobs:
9+
tests:
10+
runs-on: "ubuntu-latest"
11+
name: Test and release
12+
steps:
13+
- name: Check out code from GitHub
14+
uses: "actions/checkout@v3"
15+
- name: Setup Python
16+
uses: "actions/setup-python@v4"
17+
with:
18+
python-version: "3.10"
19+
- name: Install requirements
20+
run: python3 -m pip install -r requirements_test.txt -r requirements_release.txt
21+
- name: Run tests
22+
run: |
23+
python3 -m pytest \
24+
-vv \
25+
-qq \
26+
--timeout=9 \
27+
--durations=10 \
28+
--cov podpointclient \
29+
--cov-report term \
30+
--cov-report html \
31+
-o console_output_style=count \
32+
-p no:sugar \
33+
tests
34+
- name: Archive code coverage results
35+
uses: actions/upload-artifact@v3
36+
with:
37+
name: code-coverage-report
38+
path: htmlcov/*
39+
- name: Build release package
40+
run: make package
41+
- name: Publish package
42+
uses: pypa/gh-action-pypi-publish@release/v1
43+
with:
44+
password: ${{ secrets.PYPI_API_TOKEN }}

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Pod Point Client Changelog
22

3+
## v1.5.0
4+
5+
* Add support for refreshing expired tokens, rather than grabbing new ones each time
6+
* Update example.py to demonstrate token expiry
7+
38
## v1.4.3
49

510
* Remove additional / from pod point api calls

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ package:
2525
python3 setup.py sdist
2626

2727
publish: clean package
28-
twine upload dist/* --verbose
28+
twine upload dist/* --verbose

example.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import datetime, timedelta
12
from podpointclient.client import PodPointClient
23
import asyncio
34
import aiohttp
@@ -19,10 +20,14 @@ async def main(username: str, password: str, http_debug: bool = False, loop=None
1920

2021
# Verify credentials work
2122
verified = await client.async_credentials_verified()
22-
print(f" Credentials verified: {verified}")
23+
print(f"Credentials verified: {verified}")
24+
print(f" Token expiry: {client.auth.access_token_expiry}")
25+
26+
print("Sleeping 2s")
27+
time.sleep(2)
2328

24-
print("Getting user details")
2529
# Get user information
30+
print("Getting user details")
2631
user = await client.async_get_user()
2732
print(f" Account balance {user.account.balance}p")
2833

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

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

66+
# Expire token and exchange a refresh
67+
print("Expiring token and refreshing...")
68+
client.auth.access_token_expiry = datetime.now() - timedelta(minutes=10)
69+
updated = await client.auth.async_update_access_token()
70+
print(f" Token updated? {updated} - New expiry: {client.auth.access_token_expiry}")
71+
72+
# Get user information again
73+
print("Getting user details with new token")
74+
user = await client.async_get_user()
75+
print(f" Account balance {user.account.balance}p")
76+
6177
if __name__ == "__main__":
6278
import time
6379
import argparse

podpointclient/endpoints.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
"""Google endpoint, used for auth"""
1818
GOOGLE_KEY = '?key=AIzaSyCwhF8IOl_7qHXML0pOd5HmziYP46IZAGU'
1919
PASSWORD_VERIFY = f"/verifyPassword{GOOGLE_KEY}"
20+
TOKEN = f"/token{GOOGLE_KEY}"
2021

2122
GOOGLE_BASE = 'www.googleapis.com/identitytoolkit/v3/relyingparty'
2223
GOOGLE_BASE_URL = f"https://{GOOGLE_BASE}"
24+
25+
GOOGLE_TOKEN_BASE = 'securetoken.googleapis.com/v1'
26+
GOOGLE_TOKEN_BASE_URL = f"https://{GOOGLE_TOKEN_BASE}"

podpointclient/helpers/api_wrapper.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@ async def post(
7272
headers=headers,
7373
exception_class=exception_class
7474
)
75+
async def post_form_data(
76+
self,
77+
url: str,
78+
body: Any,
79+
headers: Dict[str, Any],
80+
params: Dict[str, Any] = None,
81+
exception_class=APIError
82+
) -> aiohttp.ClientResponse:
83+
"""Make a POST request"""
84+
return await self.__wrapper(
85+
method="post_data",
86+
url=url,
87+
params=params,
88+
data=body,
89+
headers=headers,
90+
exception_class=exception_class
91+
)
7592

7693
async def delete(
7794
self,
@@ -132,6 +149,16 @@ async def __wrapper(
132149
json=data
133150
)
134151

152+
# ToDo: Fix this, we need to look again at the pattern for this, maybe determine based on data type?
153+
# THIS REALLY SMELLS
154+
elif method == "post_data":
155+
response = await self._session.post(
156+
url,
157+
headers=headers,
158+
params=params,
159+
data=data
160+
)
161+
135162
elif method == "delete":
136163
response = await self._session.delete(url, headers=headers, params=params)
137164

podpointclient/helpers/auth.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from ..errors import APIError, AuthError, SessionError
99
from .session import Session
10-
from ..endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY
10+
from ..endpoints import GOOGLE_BASE_URL, PASSWORD_VERIFY, GOOGLE_TOKEN_BASE_URL, TOKEN
1111
from .functions import HEADERS
1212
from .api_wrapper import APIWrapper
1313

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

6262
try:
6363
_LOGGER.debug('Updating access token')
64-
access_token_updated: bool = await self.__update_access_token()
64+
access_token_updated: bool = await self.__update_access_token(
65+
refresh=self.access_token_expired()
66+
)
6567

6668
_LOGGER.debug(
6769
"Updated access token. New expiration: %s",
@@ -90,23 +92,43 @@ async def async_update_access_token(self) -> bool:
9092

9193
async def __update_access_token(self, refresh: bool = False) -> bool:
9294
return_value = False
95+
id_token_response = 'idToken'
96+
refresh_token_response = 'refreshToken'
97+
expires_in_response = 'expiresIn'
9398

9499
try:
95100
wrapper = APIWrapper(session=self._session)
96-
response = await wrapper.post(
97-
url=f"{GOOGLE_BASE_URL}{PASSWORD_VERIFY}",
98-
body={"email": self.email, "returnSecureToken": True, "password": self.password},
99-
headers=HEADERS,
100-
exception_class=AuthError)
101+
102+
if refresh:
103+
_LOGGER.debug('Refreshing access token')
104+
headers = HEADERS.copy()
105+
headers["Content-type"] = 'application/x-www-form-urlencoded'
106+
107+
id_token_response = 'id_token'
108+
refresh_token_response = 'refresh_token'
109+
expires_in_response = 'expires_in'
110+
111+
response = await wrapper.post_form_data(
112+
url=f"{GOOGLE_TOKEN_BASE_URL}{TOKEN}",
113+
body=f"grant_type=refresh_token&refresh_token={self.refresh_token}",
114+
headers=headers,
115+
exception_class=AuthError)
116+
else:
117+
_LOGGER.debug('Getting a new access token')
118+
response = await wrapper.post(
119+
url=f"{GOOGLE_BASE_URL}{PASSWORD_VERIFY}",
120+
body={"email": self.email, "returnSecureToken": True, "password": self.password},
121+
headers=HEADERS,
122+
exception_class=AuthError)
101123

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

105127
json = await response.json()
106-
self.access_token = json["idToken"]
107-
self.refresh_token = json["refreshToken"]
128+
self.access_token = json[id_token_response]
129+
self.refresh_token = json[refresh_token_response]
108130
self.access_token_expiry = datetime.now() + timedelta(
109-
seconds=int(json["expiresIn"]) - 10
131+
seconds=int(json[expires_in_response]) - 10
110132
)
111133
return_value = True
112134

@@ -134,4 +156,7 @@ async def __handle_response_error(self, response, error_class):
134156
status = response.status
135157
response = await response.text()
136158

159+
if self._http_debug:
160+
_LOGGER.debug(response)
161+
137162
raise error_class(status, response)

podpointclient/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version for the podpointclient library"""
22

3-
__version__ = "1.4.3"
3+
__version__ = "1.5.0"

tests/fixtures/refresh.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"access_token": "1234",
3+
"id_token": "1234",
4+
"refresh_token": "1234",
5+
"expires_in": "1234",
6+
"token_type": "Bearer",
7+
"user_id": "11111111-1111-1111-1111-11111111111"
8+
}

0 commit comments

Comments
 (0)