diff --git a/LibreView/LibreView.py b/LibreView/LibreView.py new file mode 100644 index 0000000..e69de29 diff --git a/LibreView/__init__.py b/LibreView/__init__.py new file mode 100644 index 0000000..4bafcec --- /dev/null +++ b/LibreView/__init__.py @@ -0,0 +1 @@ +from LibreView.utils import * diff --git a/LibreView/models/Connection.py b/LibreView/models/Connection.py new file mode 100644 index 0000000..392c560 --- /dev/null +++ b/LibreView/models/Connection.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from uuid import UUID +from dataclass_wizard import JSONWizard +from LibreView.models.Sensor import Sensor +from LibreView.models.GlucoseMeasurement import GlucoseMeasurement + + +@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 new file mode 100644 index 0000000..57cbf6d --- /dev/null +++ b/LibreView/models/Device.py @@ -0,0 +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 diff --git a/LibreView/models/GlucoseMeasurement.py b/LibreView/models/GlucoseMeasurement.py new file mode 100644 index 0000000..215f7ed --- /dev/null +++ b/LibreView/models/GlucoseMeasurement.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass +from datetime import datetime +from dataclass_wizard import JSONWizard, json_field + + +@dataclass +class GlucoseMeasurement(JSONWizard): + _timestamp: str + 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 + + @property + def factory_timestamp(self) -> datetime: + return self.parse_dt(self._factory_timestamp) + + _timestamp: str = json_field("Timestamp") # type: ignore + + @property + def timestamp(self) -> datetime: + return self.parse_dt(self._timestamp) + + trend_message: str | None = None + + 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 new file mode 100644 index 0000000..3a158dc --- /dev/null +++ b/LibreView/models/Practice.py @@ -0,0 +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 diff --git a/LibreView/models/Sensor.py b/LibreView/models/Sensor.py new file mode 100644 index 0000000..d9fb5dc --- /dev/null +++ b/LibreView/models/Sensor.py @@ -0,0 +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 diff --git a/LibreView/models/User.py b/LibreView/models/User.py new file mode 100644 index 0000000..42cafaa --- /dev/null +++ b/LibreView/models/User.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass +from typing import Any, List, Dict +from uuid import UUID +from dataclass_wizard import JSONWizard +from LibreView.models.Device import Device +from LibreView.models.Practice import 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 new file mode 100644 index 0000000..e69de29 diff --git a/LibreView/utils/API.py b/LibreView/utils/API.py new file mode 100644 index 0000000..cb5bb51 --- /dev/null +++ b/LibreView/utils/API.py @@ -0,0 +1,93 @@ +from typing import List +import requests +from LibreView.models.User import User +from LibreView.models.Connection import 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: + username: str + password: str + base_url = "https://api.libreview.io" + client = requests.session() + product = "llu.android" + version = "4.7" + + def __init__(self, username: str, password: str): + 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, + }, + ) + r.raise_for_status() + content = r.json() + + # 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}", + }, + ) + 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", + ) + 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/LibreView/utils/__init__.py b/LibreView/utils/__init__.py new file mode 100644 index 0000000..2c30b14 --- /dev/null +++ b/LibreView/utils/__init__.py @@ -0,0 +1 @@ +from .API import API diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..af691fc --- /dev/null +++ b/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +requests = "*" +dataclass-wizard = "*" + +[dev-packages] +black = "*" +twine = "*" +pytest = "*" +pylint = "*" +pip-system-certs = "*" +pytest-cov = "*" + +[requires] +python_version = "3.11" diff --git a/Tests/__init__.py b/Tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tests/test_api.py b/Tests/test_api.py new file mode 100644 index 0000000..88bf6c6 --- /dev/null +++ b/Tests/test_api.py @@ -0,0 +1,31 @@ +from datetime import datetime +import os +from uuid import UUID +import LibreView +import logging + +LOGGER = logging.getLogger(__name__) +USERNAME: str = os.environ["libre_username"] +PASSWORD: str = os.environ["libre_password"] + + +def test_logon(): + api = LibreView.API(USERNAME, PASSWORD) + assert api.client.headers.get("Authorization") != None + + +def test_get_user(): + api = LibreView.API(USERNAME, PASSWORD) + usr = api.get_user() + assert isinstance(usr.first_name, str) + assert len(usr.first_name) > 0 + + +def test_get_connections(): + api = LibreView.API(USERNAME, PASSWORD) + cons = api.get_connections() + assert len(cons) > 0 + con = cons[0] + assert isinstance(con.patient_id, UUID) + assert isinstance(con.glucose_measurement.timestamp, datetime) + assert len(con.first_name) > 0 diff --git a/build.py b/build.py index db45531..4b35f6f 100644 --- a/build.py +++ b/build.py @@ -30,7 +30,9 @@ setup_py = f.read() matches = version_regex.search(setup_py) version = StrictVersion(matches.group(1)) - version = StrictVersion(f"{version.version[0]}.{version.version[1]}.{version.version[2]+1}") + version = StrictVersion( + f"{version.version[0]}.{version.version[1]}.{version.version[2]+1}" + ) if args.version: version = StrictVersion(args.version) version = str(version) @@ -59,4 +61,4 @@ shell=True, ) if result.returncode != 0: - raise Exception("Could not upload build") \ No newline at end of file + raise Exception("Could not upload build") diff --git a/setup.py b/setup.py index ca1dfbd..1f41708 100644 --- a/setup.py +++ b/setup.py @@ -13,11 +13,11 @@ long_description_content_type="text/markdown", url="https://github.com/PTST/LibreView_Py", packages=setuptools.find_packages(), - install_requires=["requests >= 2.23.0"], + install_requires=["requests >= 2.31.0"], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires=">=3.11", -) \ No newline at end of file +)