From 0a4976a6ebd8c4fd462af1973f9849d141801a58 Mon Sep 17 00:00:00 2001 From: Patrick Toft Steffensen <17211264+PTST@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:45:12 +0100 Subject: [PATCH] Fix for new german headers --- LibreView/LibreView.py | 36 ++--- LibreView/models/Connection.py | 44 +++--- LibreView/models/Device.py | 24 +-- LibreView/models/GlucoseMeasurement.py | 64 ++++---- LibreView/models/Practice.py | 32 ++-- LibreView/models/Sensor.py | 26 ++-- LibreView/models/User.py | 52 +++---- LibreView/models/__init__.py | 12 +- LibreView/utils/API.py | 208 +++++++++++++------------ setup.py | 2 +- 10 files changed, 254 insertions(+), 246 deletions(-) diff --git a/LibreView/LibreView.py b/LibreView/LibreView.py index 039c987..d3bad0b 100644 --- a/LibreView/LibreView.py +++ b/LibreView/LibreView.py @@ -1,18 +1,18 @@ -from typing import Dict, List -from uuid import UUID -from LibreView.models import Connection -from LibreView.utils import API - - -class LibreView: - client: API - - connections_dict: Dict[UUID, Connection] - - def __init__(self, username: str, password: str): - self.client = API(username, password) - - def get_connections(self) -> List[Connection]: - cons = self.client.get_connections() - self.connections_dict = {x.id: x for x in cons} - return cons +from typing import Dict, List, Optional +from uuid import UUID +from LibreView.models import Connection +from LibreView.utils import API + + +class LibreView: + client: API + + connections_dict: Dict[UUID, Connection] + + def __init__(self, username: str, password: str, region: Optional[str] = None): + self.client = API(username, password, region) + + def get_connections(self) -> List[Connection]: + cons = self.client.get_connections() + self.connections_dict = {x.id: x for x in cons} + return cons diff --git a/LibreView/models/Connection.py b/LibreView/models/Connection.py index 89478f3..fd96cb8 100644 --- a/LibreView/models/Connection.py +++ b/LibreView/models/Connection.py @@ -1,22 +1,22 @@ -from dataclasses import dataclass -from uuid import UUID -from dataclass_wizard import JSONWizard - -# from LibreView.models import Sensor, GlucoseMeasurement -from LibreView.models.GlucoseMeasurement import GlucoseMeasurement -from LibreView.models.Sensor import Sensor - - -@dataclass -class Connection(JSONWizard): - id: UUID - patient_id: UUID - country: str - status: int - first_name: str - last_name: str - target_low: int - target_high: int - uom: int - sensor: Sensor - glucose_measurement: GlucoseMeasurement +from dataclasses import dataclass +from uuid import UUID +from dataclass_wizard import JSONWizard + +# from LibreView.models import Sensor, GlucoseMeasurement +from LibreView.models.GlucoseMeasurement import GlucoseMeasurement +from LibreView.models.Sensor import Sensor + + +@dataclass +class Connection(JSONWizard): + id: UUID + patient_id: UUID + country: str + status: int + first_name: str + last_name: str + target_low: int + target_high: int + uom: int + sensor: Sensor + glucose_measurement: GlucoseMeasurement diff --git a/LibreView/models/Device.py b/LibreView/models/Device.py index 57cbf6d..395c289 100644 --- a/LibreView/models/Device.py +++ b/LibreView/models/Device.py @@ -1,12 +1,12 @@ -from dataclasses import dataclass -from uuid import UUID -from dataclass_wizard import JSONWizard - - -@dataclass -class Device(JSONWizard): - id: UUID - nickname: str - sn: UUID - type: int - upload_date: int +from dataclasses import dataclass +from uuid import UUID +from dataclass_wizard import JSONWizard + + +@dataclass +class Device(JSONWizard): + id: UUID + nickname: str + sn: UUID + type: int + upload_date: int diff --git a/LibreView/models/GlucoseMeasurement.py b/LibreView/models/GlucoseMeasurement.py index 93edeb7..1fe0e82 100644 --- a/LibreView/models/GlucoseMeasurement.py +++ b/LibreView/models/GlucoseMeasurement.py @@ -1,32 +1,32 @@ -from dataclasses import dataclass -from datetime import datetime -from dataclass_wizard import JSONWizard, json_field - - -@dataclass -class GlucoseMeasurement(JSONWizard): - type: int - value_in_mg_per_dl: int - trend_arrow: int - measurement_color: int - glucose_units: int - value: float - is_high: bool - is_low: bool - _factory_timestamp: str = json_field("FactoryTimestamp") # type: ignore - _timestamp: str = json_field("Timestamp") # type: ignore - trend_message: str | None = None - - @property - def factory_timestamp(self) -> datetime: - return self.parse_dt(self._factory_timestamp) - - @property - def timestamp(self) -> datetime: - return self.parse_dt(self._timestamp) - - def parse_dt(self, val: str) -> datetime: - splitted = val.split("/") - splitted[0] = splitted[0].zfill(2) - splitted[1] = splitted[1].zfill(2) - return datetime.strptime("/".join(splitted), "%m/%d/%Y %I:%M:%S %p") +from dataclasses import dataclass +from datetime import datetime +from dataclass_wizard import JSONWizard, json_field + + +@dataclass +class GlucoseMeasurement(JSONWizard): + type: int + value_in_mg_per_dl: int + trend_arrow: int + measurement_color: int + glucose_units: int + value: float + is_high: bool + is_low: bool + _factory_timestamp: str = json_field("FactoryTimestamp") # type: ignore + _timestamp: str = json_field("Timestamp") # type: ignore + trend_message: str | None = None + + @property + def factory_timestamp(self) -> datetime: + return self.parse_dt(self._factory_timestamp) + + @property + def timestamp(self) -> datetime: + return self.parse_dt(self._timestamp) + + def parse_dt(self, val: str) -> datetime: + splitted = val.split("/") + splitted[0] = splitted[0].zfill(2) + splitted[1] = splitted[1].zfill(2) + return datetime.strptime("/".join(splitted), "%m/%d/%Y %I:%M:%S %p") diff --git a/LibreView/models/Practice.py b/LibreView/models/Practice.py index 3a158dc..13d9255 100644 --- a/LibreView/models/Practice.py +++ b/LibreView/models/Practice.py @@ -1,16 +1,16 @@ -from dataclasses import dataclass -from uuid import UUID -from dataclass_wizard import JSONWizard - - -@dataclass -class Practice(JSONWizard): - id: UUID - practice_id: str - name: str - address1: str - city: str - state: str - zip: str - phone_number: str - address2: str | None = None +from dataclasses import dataclass +from uuid import UUID +from dataclass_wizard import JSONWizard + + +@dataclass +class Practice(JSONWizard): + id: UUID + practice_id: str + name: str + address1: str + city: str + state: str + zip: str + phone_number: str + address2: str | None = None diff --git a/LibreView/models/Sensor.py b/LibreView/models/Sensor.py index d9fb5dc..a1f2459 100644 --- a/LibreView/models/Sensor.py +++ b/LibreView/models/Sensor.py @@ -1,13 +1,13 @@ -from dataclasses import dataclass -from dataclass_wizard import JSONWizard - - -@dataclass -class Sensor(JSONWizard): - device_id: str - sn: str - a: int - w: int - pt: int - s: bool - lj: bool +from dataclasses import dataclass +from dataclass_wizard import JSONWizard + + +@dataclass +class Sensor(JSONWizard): + device_id: str + sn: str + a: int + w: int + pt: int + s: bool + lj: bool diff --git a/LibreView/models/User.py b/LibreView/models/User.py index a6d6172..f4e5ab5 100644 --- a/LibreView/models/User.py +++ b/LibreView/models/User.py @@ -1,26 +1,26 @@ -from dataclasses import dataclass -from typing import Any, List, Dict -from uuid import UUID -from dataclass_wizard import JSONWizard -from LibreView.models import Device, Practice - - -@dataclass -class User(JSONWizard): - id: UUID - first_name: str - last_name: str - email: str - country: str - ui_language: str - communication_language: str - account_type: str - uom: int - date_format: int - time_format: int - email_day: List[int] - created: int - last_login: int - date_of_birth: int - practices: Dict[UUID, Practice] - devices: Dict[str, Device] +from dataclasses import dataclass +from typing import Any, List, Dict +from uuid import UUID +from dataclass_wizard import JSONWizard +from LibreView.models import Device, Practice + + +@dataclass +class User(JSONWizard): + id: UUID + first_name: str + last_name: str + email: str + country: str + ui_language: str + communication_language: str + account_type: str + uom: int + date_format: int + time_format: int + email_day: List[int] + created: int + last_login: int + date_of_birth: int + practices: Dict[UUID, Practice] + devices: Dict[str, Device] diff --git a/LibreView/models/__init__.py b/LibreView/models/__init__.py index 54e6c20..6284f7a 100644 --- a/LibreView/models/__init__.py +++ b/LibreView/models/__init__.py @@ -1,6 +1,6 @@ -from LibreView.models.Connection import Connection -from LibreView.models.Device import Device -from LibreView.models.GlucoseMeasurement import GlucoseMeasurement -from LibreView.models.Practice import Practice -from LibreView.models.Sensor import Sensor -from LibreView.models.User import User +from LibreView.models.Connection import Connection +from LibreView.models.Device import Device +from LibreView.models.GlucoseMeasurement import GlucoseMeasurement +from LibreView.models.Practice import Practice +from LibreView.models.Sensor import Sensor +from LibreView.models.User import User diff --git a/LibreView/utils/API.py b/LibreView/utils/API.py index 7aae789..f71e2ba 100644 --- a/LibreView/utils/API.py +++ b/LibreView/utils/API.py @@ -1,100 +1,108 @@ -from typing import Optional -import requests -from LibreView.models import User, Connection - - -def reauth_on_fail(func): - def wrapper(*args): - try: - return func(*args) - except requests.HTTPError as e: - if e.response.status_code == 401: - api: API = args[0] - api.authenticate() - return func(*args) - raise e - - return wrapper - - -class API: - def __init__(self, username: str, password: str, region: Optional[str] = None): - self.base_url = "https://api.libreview.io" - if region: - self.base_url = f"https://api-{region}.libreview.io" - self.client = requests.session() - self.product = "llu.android" - self.version = "4.7" - self.username = username - self.password = password - self.client.headers["product"] = self.product - self.client.headers["version"] = self.version - self.authenticate() - - def authenticate(self): - r = self.client.post( - f"{self.base_url}/llu/auth/login", - json={ - "email": self.username, - "password": self.password, - }, - verify=False - ) - r.raise_for_status() - content = r.json() - - if content and content.get("status") == 0 and content["data"].get("redirect", False): - region = content["data"]["region"] - self.base_url = f"https://api-{region}.libreview.io" - return self.authenticate() - - # status 0 == login successfull - if content and content.get("status") == 0: - self.set_token(content["data"]["authTicket"]["token"]) - return - - # status 4 == missing term accepts - if content and content.get("status") == 4: - self.accept_terms(content["data"]["authTicket"]["token"]) - return - - error = "Unknown error occured during authentication" - if content and content.get("error") and content["error"].get("message"): - error = content["error"]["message"] - - raise Exception(error) - - def set_token(self, token): - self.client.headers["Authorization"] = f"Bearer {token}" - - def accept_terms(self, token): - r = self.client.post( - f"{self.base_url}/llu/auth/login", - headers={ - "Authorization": f"Bearer {token}", - }, - verify=False - ) - r.raise_for_status() - content = r.json() - if content and content.get("status") == 0: - self.set_token(content["data"]["authTicket"]["token"]) - return - - @reauth_on_fail - def get_user(self) -> User: - r = self.client.get( - f"{self.base_url}/user", - verify=False - ) - r.raise_for_status() - return User.from_dict(r.json()["data"]["user"]) - - @reauth_on_fail - def get_connections(self) -> list[Connection]: - r = self.client.get( - f"{self.base_url}/llu/connections", - verify=False - ) - r.raise_for_status() - return Connection.from_list(r.json()["data"]) +from typing import Optional +import requests +from LibreView.models import User, Connection +from hashlib import sha256 + + +def reauth_on_fail(func): + def wrapper(*args): + try: + return func(*args) + except requests.HTTPError as e: + if e.response.status_code == 401: + api: API = args[0] + api.authenticate() + return func(*args) + raise e + + return wrapper + + +class API: + def __init__(self, username: str, password: str, region: Optional[str] = None): + self.base_url = "https://api.libreview.io" + if region: + self.base_url = f"https://api-{region}.libreview.io" + self.client = requests.session() + self.product = "llu.android" + self.version = "4.12.0" + self.username = username + self.password = password + self.client.headers["product"] = self.product + self.client.headers["version"] = self.version + + def authenticate(self): + r = self.client.post( + f"{self.base_url}/llu/auth/login", + json={ + "email": self.username, + "password": self.password, + }, + ) + r.raise_for_status() + content = r.json() + + if ( + content + and content.get("status") == 0 + and content["data"].get("redirect", False) + ): + region = content["data"]["region"] + self.base_url = f"https://api-{region}.libreview.io" + return self.authenticate() + + # status 0 == login successfull + if content and content.get("status") == 0: + account_id = content["data"]["user"]["id"] + self.client.headers["account-id"] = sha256( + account_id.encode("utf-8") + ).hexdigest() + self.set_token(content["data"]["authTicket"]["token"]) + return + + # status 4 == missing term accepts + if content and content.get("status") == 4: + self.accept_terms(content["data"]["authTicket"]["token"]) + return + + error = "Unknown error occured during authentication" + if content and content.get("error") and content["error"].get("message"): + error = content["error"]["message"] + + raise Exception(error) + + def set_token(self, token): + self.client.headers["Authorization"] = f"Bearer {token}" + + def accept_terms(self, token): + r = self.client.post( + f"{self.base_url}/llu/auth/login", + headers={ + "Authorization": f"Bearer {token}", + }, + ) + r.raise_for_status() + content = r.json() + if content and content.get("status") == 0: + account_id = content["data"]["user"]["id"] + self.client.headers["account-id"] = sha256( + account_id.encode("utf-8") + ).hexdigest() + self.set_token(content["data"]["authTicket"]["token"]) + return + + @reauth_on_fail + def get_user(self) -> User: + r = self.client.get( + f"{self.base_url}/user", + ) + r.raise_for_status() + return User.from_dict(r.json()["data"]["user"]) + + @reauth_on_fail + def get_connections(self) -> list[Connection]: + r = self.client.get( + f"{self.base_url}/llu/connections", + ) + r.raise_for_status() + return Connection.from_list(r.json()["data"]) diff --git a/setup.py b/setup.py index 461f936..b5fea5b 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="LibreView", - version="0.1.6", + version="0.2.4", author="PTST", author_email="patrick@steffensen.io", description="API interface for LibreView / LibreLinkUp glucose readings",