From 3eda545eed5cf57cb43fa98a89bd6189e211aafd Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Sun, 13 Oct 2024 10:13:19 +0100 Subject: [PATCH 1/2] feat: Update version header 4.7.0 -> 4.12.0 Update the version string used in request headers from `4.7.0` to `4 .12.0`, as per the [comment here](https://gist.github.com/khskekec/6c13ba01b10d3018d816706a32ae8ab2?permalink_comment_id=5330276#gistcomment-5330276) Also add an `Account-Id` header, using a sha256 hashed user id --- src/pylibrelinkup/pylibrelinkup.py | 54 +++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/src/pylibrelinkup/pylibrelinkup.py b/src/pylibrelinkup/pylibrelinkup.py index 700dae1..ac1a085 100644 --- a/src/pylibrelinkup/pylibrelinkup.py +++ b/src/pylibrelinkup/pylibrelinkup.py @@ -6,10 +6,12 @@ from __future__ import annotations +import hashlib import warnings from uuid import UUID import requests +from pydantic import ValidationError from .api_url import APIUrl from .decorators import authenticated @@ -22,27 +24,29 @@ ) from .models.connection import GraphResponse, LogbookResponse from .models.data import GlucoseMeasurement, Patient -from .models.login import LoginArgs +from .models.login import LoginArgs, LoginResponse from .utilities import coerce_patient_id __all__ = ["PyLibreLinkUp"] +HEADERS: dict[str, str] = { + "accept-encoding": "gzip", + "cache-control": "no-cache", + "connection": "Keep-Alive", + "content-type": "application/json", + "product": "llu.android", + "version": "4.12.0", +} + + class PyLibreLinkUp: """PyLibreLinkUp class to request data from the LibreLinkUp API.""" email: str password: str token: str | None - - _HEADERS = { - "accept-encoding": "gzip", - "cache-control": "no-cache", - "connection": "Keep-Alive", - "content-type": "application/json", - "product": "llu.android", - "version": "4.7.0", - } + account_id_hash: str | None def __init__(self, email: str, password: str, api_url: APIUrl = APIUrl.US) -> None: """ @@ -60,6 +64,7 @@ def __init__(self, email: str, password: str, api_url: APIUrl = APIUrl.US) -> No self.email = email or "" self.password = password or "" self.token = None + self.account_id_hash = None self.api_url: str = api_url.value def authenticate(self) -> None: @@ -69,7 +74,7 @@ def authenticate(self) -> None: """ r = requests.post( url=f"{self.api_url}/llu/auth/login", - headers=self._HEADERS, + headers=self._get_headers(), json=self.login_args.model_dump(), ) r.raise_for_status() @@ -89,11 +94,24 @@ def authenticate(self) -> None: raise EmailVerificationError() try: - self.token = data["data"]["authTicket"]["token"] - self._HEADERS.update({"authorization": "Bearer " + self.token}) - - except KeyError: + login_response = LoginResponse.model_validate(data) + except ValidationError: raise AuthenticationError("Invalid login credentials") + self._set_token(login_response.data.authTicket.token) + self._set_account_id_hash(login_response.data.user.id) + + def _set_token(self, token: str): + """Saves the token for future requests.""" + self.token = token + + def _get_headers(self) -> dict: + """Returns the headers for the request.""" + headers = HEADERS.copy() + if self.token: + headers.update({"authorization": "Bearer " + self.token}) + if self.account_id_hash: + headers.update({"account-id": self.account_id_hash}) + return headers def _call_api(self, url: str = None) -> dict: """Calls the LibreLinkUp API and returns the response @@ -101,7 +119,7 @@ def _call_api(self, url: str = None) -> dict: :type url: str :rtype: object """ - r = requests.get(url=url, headers=self._HEADERS) + r = requests.get(url=url, headers=self._get_headers()) r.raise_for_status() data = r.json() return data @@ -204,3 +222,7 @@ def logbook( response_json = self._get_logbook_json(patient_id) return LogbookResponse.model_validate(response_json).data + + def _set_account_id_hash(self, account_id: str): + """Saves the account_id_hash for future requests.""" + self.account_id_hash = hashlib.sha256(account_id.encode()).hexdigest() From b67ddfbcd1f4556400a2eb456946713ecc7c7f7a Mon Sep 17 00:00:00 2001 From: Rob Berwick Date: Tue, 10 Dec 2024 20:26:03 +0000 Subject: [PATCH 2/2] refactor: re-arrange class methods tidy up method order a little --- src/pylibrelinkup/pylibrelinkup.py | 92 +++++++++++++++--------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/pylibrelinkup/pylibrelinkup.py b/src/pylibrelinkup/pylibrelinkup.py index ac1a085..7f7c09c 100644 --- a/src/pylibrelinkup/pylibrelinkup.py +++ b/src/pylibrelinkup/pylibrelinkup.py @@ -67,6 +67,52 @@ def __init__(self, email: str, password: str, api_url: APIUrl = APIUrl.US) -> No self.account_id_hash = None self.api_url: str = api_url.value + def _call_api(self, url: str = None) -> dict: + """Calls the LibreLinkUp API and returns the response + + :type url: str + :rtype: object + """ + r = requests.get(url=url, headers=self._get_headers()) + r.raise_for_status() + data = r.json() + return data + + def _set_token(self, token: str): + """Saves the token for future requests.""" + self.token = token + + def _set_account_id_hash(self, account_id: str): + """Saves the account_id_hash for future requests.""" + self.account_id_hash = hashlib.sha256(account_id.encode()).hexdigest() + + def _get_graph_data_json(self, patient_id: UUID) -> dict: + """Requests and returns patient graph data + + :param patient_id: UUID + :return: + """ + return self._call_api(url=f"{self.api_url}/llu/connections/{patient_id}/graph") + + def _get_headers(self) -> dict: + """Returns the headers for the request.""" + headers = HEADERS.copy() + if self.token: + headers.update({"authorization": "Bearer " + self.token}) + if self.account_id_hash: + headers.update({"account-id": self.account_id_hash}) + return headers + + def _get_logbook_json(self, patient_id: UUID) -> dict: + """Requests and returns patient logbook data + + :param patient_id: UUID + :return: + """ + return self._call_api( + url=f"{self.api_url}/llu/connections/{patient_id}/logbook" + ) + def authenticate(self) -> None: """Authenticate with the LibreLinkUp API @@ -100,30 +146,6 @@ def authenticate(self) -> None: self._set_token(login_response.data.authTicket.token) self._set_account_id_hash(login_response.data.user.id) - def _set_token(self, token: str): - """Saves the token for future requests.""" - self.token = token - - def _get_headers(self) -> dict: - """Returns the headers for the request.""" - headers = HEADERS.copy() - if self.token: - headers.update({"authorization": "Bearer " + self.token}) - if self.account_id_hash: - headers.update({"account-id": self.account_id_hash}) - return headers - - def _call_api(self, url: str = None) -> dict: - """Calls the LibreLinkUp API and returns the response - - :type url: str - :rtype: object - """ - r = requests.get(url=url, headers=self._get_headers()) - r.raise_for_status() - data = r.json() - return data - def get_patients(self) -> list[Patient]: """Requests and returns patient data @@ -133,24 +155,6 @@ def get_patients(self) -> list[Patient]: data = self._call_api(url=f"{self.api_url}/llu/connections") return [Patient.model_validate(patient) for patient in data["data"]] - def _get_graph_data_json(self, patient_id: UUID) -> dict: - """Requests and returns patient graph data - - :param patient_id: UUID - :return: - """ - return self._call_api(url=f"{self.api_url}/llu/connections/{patient_id}/graph") - - def _get_logbook_json(self, patient_id: UUID) -> dict: - """Requests and returns patient logbook data - - :param patient_id: UUID - :return: - """ - return self._call_api( - url=f"{self.api_url}/llu/connections/{patient_id}/logbook" - ) - @authenticated def read(self, patient_identifier: UUID | str | Patient) -> GraphResponse: """ @@ -222,7 +226,3 @@ def logbook( response_json = self._get_logbook_json(patient_id) return LogbookResponse.model_validate(response_json).data - - def _set_account_id_hash(self, account_id: str): - """Saves the account_id_hash for future requests.""" - self.account_id_hash = hashlib.sha256(account_id.encode()).hexdigest()