Skip to content

Commit

Permalink
Basic api endpoints added
Browse files Browse the repository at this point in the history
  • Loading branch information
PTST committed Feb 21, 2024
1 parent b47eada commit 6ff05bc
Show file tree
Hide file tree
Showing 16 changed files with 274 additions and 4 deletions.
Empty file added LibreView/LibreView.py
Empty file.
1 change: 1 addition & 0 deletions LibreView/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from LibreView.utils import *
20 changes: 20 additions & 0 deletions LibreView/models/Connection.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions LibreView/models/Device.py
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions LibreView/models/GlucoseMeasurement.py
Original file line number Diff line number Diff line change
@@ -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")
16 changes: 16 additions & 0 deletions LibreView/models/Practice.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions LibreView/models/Sensor.py
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions LibreView/models/User.py
Original file line number Diff line number Diff line change
@@ -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]
Empty file added LibreView/models/__init__.py
Empty file.
93 changes: 93 additions & 0 deletions LibreView/utils/API.py
Original file line number Diff line number Diff line change
@@ -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"])
1 change: 1 addition & 0 deletions LibreView/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .API import API
19 changes: 19 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -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"
Empty file added Tests/__init__.py
Empty file.
31 changes: 31 additions & 0 deletions Tests/test_api.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 4 additions & 2 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -59,4 +61,4 @@
shell=True,
)
if result.returncode != 0:
raise Exception("Could not upload build")
raise Exception("Could not upload build")
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
)

0 comments on commit 6ff05bc

Please sign in to comment.