pylibrelinkup 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pylibrelinkup-0.0.1/LICENSE +21 -0
- pylibrelinkup-0.0.1/PKG-INFO +88 -0
- pylibrelinkup-0.0.1/README.md +42 -0
- pylibrelinkup-0.0.1/pyproject.toml +48 -0
- pylibrelinkup-0.0.1/setup.cfg +4 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/__init__.py +0 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/api_url.py +17 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/client.py +77 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/exceptions.py +17 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/models/__init__.py +0 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/models/config.py +36 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/models/connection.py +85 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/models/data.py +145 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/models/hardware.py +55 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup/models/login.py +100 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup.egg-info/PKG-INFO +88 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup.egg-info/SOURCES.txt +19 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup.egg-info/dependency_links.txt +1 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup.egg-info/requires.txt +12 -0
- pylibrelinkup-0.0.1/src/pylibrelinkup.egg-info/top_level.txt +1 -0
- pylibrelinkup-0.0.1/tests/test_client_authentication.py +105 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Rob Berwick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pylibrelinkup
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A client for the Abbott LibreLinkUp API
|
|
5
|
+
Author-email: Rob Berwick <rob.berwick@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Rob Berwick
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/robberwick/pylibrelinkup
|
|
28
|
+
Project-URL: Issues, https://github.com/robberwick/pylibrelinkup/issues
|
|
29
|
+
Keywords: librelink,librelinkup,abbott,diabetes,glucose,api,client,health,medical,blood-sugar,continuous-glucose-monitoring,cgm
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Requires-Python: >=3.11
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: requests>=2.31.0
|
|
37
|
+
Requires-Dist: pydantic>=2.5.0
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: black; extra == "dev"
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest; extra == "test"
|
|
42
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
43
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
44
|
+
Requires-Dist: polyfactory; extra == "test"
|
|
45
|
+
Requires-Dist: responses; extra == "test"
|
|
46
|
+
|
|
47
|
+
# pylibrelinkup
|
|
48
|
+
|
|
49
|
+
`pylibrelinkup` is a Python client for the LibreLinkUp API, which allows you to interact with the LibreLinkUp service to retrieve glucose data and other related information. This project is a Python implementation inspired by the [libre-link-up-api-client](https://github.com/DiaKEM/libre-link-up-api-client) project.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
To install `pylibrelinkup`, you can use `pip`:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install pylibrelinkup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### Initialization
|
|
62
|
+
|
|
63
|
+
First, you need to import the necessary modules, initialize the client, and authenticate with your LibreLinkUp credentials:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from pylibrelinkup.client import LibreLinkUpClient
|
|
67
|
+
|
|
68
|
+
client = LibreLinkUpClient(username='your_username', password='your_password')
|
|
69
|
+
client.authenticate()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Getting Patient List
|
|
73
|
+
|
|
74
|
+
You can fetch the patient list using the `get_patient_list` method:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
patient_list = client.get_patient_list()
|
|
78
|
+
print(patient_list)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Getting Patient data
|
|
82
|
+
|
|
83
|
+
Retrieve patient data using the `get_graph_data` method:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
patient = client.get_patient_list()[0]
|
|
87
|
+
patient_data = client.get_graph_data(patient_id=patient.patient_id)
|
|
88
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# pylibrelinkup
|
|
2
|
+
|
|
3
|
+
`pylibrelinkup` is a Python client for the LibreLinkUp API, which allows you to interact with the LibreLinkUp service to retrieve glucose data and other related information. This project is a Python implementation inspired by the [libre-link-up-api-client](https://github.com/DiaKEM/libre-link-up-api-client) project.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install `pylibrelinkup`, you can use `pip`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pylibrelinkup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Initialization
|
|
16
|
+
|
|
17
|
+
First, you need to import the necessary modules, initialize the client, and authenticate with your LibreLinkUp credentials:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from pylibrelinkup.client import LibreLinkUpClient
|
|
21
|
+
|
|
22
|
+
client = LibreLinkUpClient(username='your_username', password='your_password')
|
|
23
|
+
client.authenticate()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Getting Patient List
|
|
27
|
+
|
|
28
|
+
You can fetch the patient list using the `get_patient_list` method:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
patient_list = client.get_patient_list()
|
|
32
|
+
print(patient_list)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Getting Patient data
|
|
36
|
+
|
|
37
|
+
Retrieve patient data using the `get_graph_data` method:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
patient = client.get_patient_list()[0]
|
|
41
|
+
patient_data = client.get_graph_data(patient_id=patient.patient_id)
|
|
42
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pylibrelinkup"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "A client for the Abbott LibreLinkUp API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name="Rob Berwick", email="rob.berwick@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
license = { file="LICENSE" }
|
|
14
|
+
keywords = [
|
|
15
|
+
"librelink",
|
|
16
|
+
"librelinkup",
|
|
17
|
+
"abbott",
|
|
18
|
+
"diabetes",
|
|
19
|
+
"glucose",
|
|
20
|
+
"api",
|
|
21
|
+
"client",
|
|
22
|
+
"health",
|
|
23
|
+
"medical",
|
|
24
|
+
"blood-sugar",
|
|
25
|
+
"continuous-glucose-monitoring",
|
|
26
|
+
"cgm"
|
|
27
|
+
]
|
|
28
|
+
classifiers = [
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"License :: OSI Approved :: MIT License",
|
|
31
|
+
"Operating System :: OS Independent",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"requests>=2.31.0",
|
|
35
|
+
"pydantic>= 2.5.0",
|
|
36
|
+
]
|
|
37
|
+
requires-python = ">=3.11"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = ["black"]
|
|
41
|
+
test = ["pytest", "pytest-cov", "pytest-mock", "polyfactory", "responses"]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/robberwick/pylibrelinkup"
|
|
45
|
+
Issues = "https://github.com/robberwick/pylibrelinkup/issues"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Type, cast
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class APIUrl(StrEnum):
|
|
8
|
+
EU = "https://api-eu.libreview.io"
|
|
9
|
+
EU2 = "https://api-eu2.libreview.io"
|
|
10
|
+
US = "https://api.libreview.io"
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_string(cls: Type[APIUrl], value: str) -> APIUrl:
|
|
14
|
+
for member in cls:
|
|
15
|
+
if member.lower() == value.lower():
|
|
16
|
+
return cast(APIUrl, member)
|
|
17
|
+
raise ValueError(f"{value} is not a valid {cls.__name__}")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from .api_url import APIUrl
|
|
9
|
+
from .exceptions import AuthenticationError, RedirectError
|
|
10
|
+
from .models.connection import ConnectionResponse
|
|
11
|
+
from .models.data import Patient
|
|
12
|
+
from .models.login import LoginArgs, LoginResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Client:
|
|
16
|
+
"""Client class to request data from the LibreLinkUp API."""
|
|
17
|
+
|
|
18
|
+
email: str
|
|
19
|
+
password: str
|
|
20
|
+
token: str | None
|
|
21
|
+
|
|
22
|
+
HEADERS = {
|
|
23
|
+
"accept-encoding": "gzip",
|
|
24
|
+
"cache-control": "no-cache",
|
|
25
|
+
"connection": "Keep-Alive",
|
|
26
|
+
"content-type": "application/json",
|
|
27
|
+
"product": "llu.android",
|
|
28
|
+
"version": "4.7.0",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def __init__(self, email: str, password: str, api_url: APIUrl = APIUrl.US):
|
|
32
|
+
self.login_args: LoginArgs = LoginArgs(email=email, password=password)
|
|
33
|
+
self.email = email or ""
|
|
34
|
+
self.password = password or ""
|
|
35
|
+
self.token = None
|
|
36
|
+
self.api_url: str = api_url.value
|
|
37
|
+
|
|
38
|
+
def authenticate(self) -> None:
|
|
39
|
+
r = requests.post(
|
|
40
|
+
url=f"{self.api_url}/llu/auth/login",
|
|
41
|
+
headers=self.HEADERS,
|
|
42
|
+
json=self.login_args.model_dump(),
|
|
43
|
+
)
|
|
44
|
+
r.raise_for_status()
|
|
45
|
+
data = r.json()
|
|
46
|
+
# Response to login can either be a request to use a different regional host, just successful, or a request
|
|
47
|
+
# to first accept terms.
|
|
48
|
+
# Here we handle the first two cases.
|
|
49
|
+
# TODO: Handle the case where the user needs to accept terms.
|
|
50
|
+
try:
|
|
51
|
+
data_dict = data.get("data", {})
|
|
52
|
+
if data_dict.get("redirect", False):
|
|
53
|
+
raise RedirectError(APIUrl.from_string(data_dict["region"]))
|
|
54
|
+
else:
|
|
55
|
+
login_response = LoginResponse.model_validate(data)
|
|
56
|
+
self.token = login_response.data.authTicket.token
|
|
57
|
+
self.HEADERS.update({"authorization": "Bearer " + self.token})
|
|
58
|
+
|
|
59
|
+
except ValidationError:
|
|
60
|
+
raise AuthenticationError("Invalid login credentials")
|
|
61
|
+
|
|
62
|
+
def get_patients(self) -> list[Patient]:
|
|
63
|
+
"""Requests and returns patient data"""
|
|
64
|
+
r = requests.get(url=f"{self.api_url}/llu/connections", headers=self.HEADERS)
|
|
65
|
+
r.raise_for_status()
|
|
66
|
+
data = r.json()
|
|
67
|
+
return [Patient.model_validate(patient) for patient in data["data"]]
|
|
68
|
+
|
|
69
|
+
def get_graph_data(self, patient_id: UUID) -> ConnectionResponse:
|
|
70
|
+
"""Requests and returns patient graph data"""
|
|
71
|
+
r = requests.get(
|
|
72
|
+
url=f"{self.api_url}/llu/connections/{patient_id}/graph",
|
|
73
|
+
headers=self.HEADERS,
|
|
74
|
+
)
|
|
75
|
+
r.raise_for_status()
|
|
76
|
+
|
|
77
|
+
return ConnectionResponse.model_validate(r.json())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .api_url import APIUrl
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthenticationError(Exception):
|
|
5
|
+
"""Raised when authentication fails."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RedirectError(Exception):
|
|
11
|
+
"""Raised when a redirect is encountered during authentication. This is a signal to retry the request with the new region.
|
|
12
|
+
The new region is stored in the `region` attribute of the exception, which is an APIUrl enum value.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, region: APIUrl):
|
|
16
|
+
self.region = region
|
|
17
|
+
super().__init__(f"Redirected to {region}")
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
from pydantic.alias_generators import to_camel
|
|
3
|
+
|
|
4
|
+
from pylibrelinkup.models.data import H, F, Nd, Std, L
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AlarmRules(BaseModel):
|
|
8
|
+
"""AlarmRules class to store alarm rules data."""
|
|
9
|
+
|
|
10
|
+
model_config = ConfigDict(
|
|
11
|
+
str_strip_whitespace=True,
|
|
12
|
+
alias_generator=to_camel,
|
|
13
|
+
populate_by_name=True,
|
|
14
|
+
from_attributes=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
c: bool
|
|
18
|
+
h: H
|
|
19
|
+
f: F
|
|
20
|
+
l: L
|
|
21
|
+
nd: Nd
|
|
22
|
+
p: int
|
|
23
|
+
r: int
|
|
24
|
+
std: Std
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FixedLowAlarmValues(BaseModel):
|
|
28
|
+
"""FixedLowAlarmValues class to store fixed alarm values."""
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(
|
|
31
|
+
populate_by_name=True,
|
|
32
|
+
from_attributes=True,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
mgdl: int
|
|
36
|
+
mmoll: float
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from uuid import UUID
|
|
2
|
+
|
|
3
|
+
from pydantic import (
|
|
4
|
+
BaseModel,
|
|
5
|
+
ConfigDict,
|
|
6
|
+
Field,
|
|
7
|
+
)
|
|
8
|
+
from pydantic.alias_generators import to_camel
|
|
9
|
+
|
|
10
|
+
from .config import AlarmRules
|
|
11
|
+
from .data import GlucoseItem
|
|
12
|
+
from .hardware import Sensor, PatientDevice, ActiveSensor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Connection(BaseModel):
|
|
16
|
+
"""Connection class to store connection data."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(
|
|
19
|
+
str_strip_whitespace=True,
|
|
20
|
+
alias_generator=to_camel,
|
|
21
|
+
populate_by_name=True,
|
|
22
|
+
from_attributes=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
id: UUID
|
|
26
|
+
patient_id: UUID
|
|
27
|
+
country: str
|
|
28
|
+
status: int
|
|
29
|
+
first_name: str
|
|
30
|
+
last_name: str
|
|
31
|
+
target_low: int
|
|
32
|
+
target_high: int
|
|
33
|
+
uom: int
|
|
34
|
+
sensor: Sensor
|
|
35
|
+
alarm_rules: AlarmRules
|
|
36
|
+
glucose_measurement: GlucoseItem
|
|
37
|
+
glucose_item: GlucoseItem
|
|
38
|
+
glucose_alarm: None
|
|
39
|
+
patient_device: PatientDevice
|
|
40
|
+
created: int
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Data(BaseModel):
|
|
44
|
+
"""Data class to store connection data."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(
|
|
47
|
+
str_strip_whitespace=True,
|
|
48
|
+
alias_generator=to_camel,
|
|
49
|
+
populate_by_name=True,
|
|
50
|
+
from_attributes=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
connection: Connection
|
|
54
|
+
active_sensors: list[ActiveSensor] = Field(alias="activeSensors")
|
|
55
|
+
graph_data: list[GlucoseItem] = Field(alias="graphData")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Ticket(BaseModel):
|
|
59
|
+
"""TicketData class to store ticket data."""
|
|
60
|
+
|
|
61
|
+
model_config = ConfigDict(
|
|
62
|
+
str_strip_whitespace=True,
|
|
63
|
+
alias_generator=to_camel,
|
|
64
|
+
populate_by_name=True,
|
|
65
|
+
from_attributes=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
token: str
|
|
69
|
+
expires: int
|
|
70
|
+
duration: int
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ConnectionResponse(BaseModel):
|
|
74
|
+
"""ConnectionResponse class to store API connection endpoint response."""
|
|
75
|
+
|
|
76
|
+
model_config = ConfigDict(
|
|
77
|
+
str_strip_whitespace=True,
|
|
78
|
+
alias_generator=to_camel,
|
|
79
|
+
populate_by_name=True,
|
|
80
|
+
from_attributes=True,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
status: int
|
|
84
|
+
data: Data
|
|
85
|
+
ticket: Ticket
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from datetime import datetime, UTC
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
6
|
+
from pydantic.alias_generators import to_camel, to_pascal
|
|
7
|
+
from pydantic_core.core_schema import ValidationInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Patient(BaseModel):
|
|
11
|
+
"""Patient class to store patient data."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(
|
|
14
|
+
str_strip_whitespace=True,
|
|
15
|
+
alias_generator=to_camel,
|
|
16
|
+
populate_by_name=True,
|
|
17
|
+
from_attributes=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
id: UUID
|
|
21
|
+
patient_id: UUID
|
|
22
|
+
first_name: str
|
|
23
|
+
last_name: str
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return f"{self.first_name} {self.last_name}: {self.id}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Trend(IntEnum):
|
|
30
|
+
DOWN_FAST: int = 1
|
|
31
|
+
DOWN_SLOW: int = 2
|
|
32
|
+
STABLE: int = 3
|
|
33
|
+
UP_SLOW: int = 4
|
|
34
|
+
UP_FAST: int = 5
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GlucoseItem(BaseModel):
|
|
38
|
+
"""GlucoseMeasurement class to store glucose measurement data."""
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(
|
|
41
|
+
str_strip_whitespace=True,
|
|
42
|
+
alias_generator=to_pascal,
|
|
43
|
+
populate_by_name=True,
|
|
44
|
+
from_attributes=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
factory_timestamp: datetime = Field(None)
|
|
48
|
+
timestamp: datetime = Field(None)
|
|
49
|
+
type: int
|
|
50
|
+
value_in_mg_per_dl: float
|
|
51
|
+
measurement_color: int
|
|
52
|
+
glucose_units: int
|
|
53
|
+
value: float
|
|
54
|
+
is_high: bool = Field(alias="isHigh")
|
|
55
|
+
is_low: bool = Field(alias="isLow")
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return f"{self.timestamp}: {self.value}"
|
|
59
|
+
|
|
60
|
+
@field_validator("timestamp", "factory_timestamp", mode="before")
|
|
61
|
+
@classmethod
|
|
62
|
+
def parse_timestamp(cls, v: str, info: ValidationInfo):
|
|
63
|
+
if isinstance(v, str):
|
|
64
|
+
datetime_value = datetime.strptime(v, "%m/%d/%Y %I:%M:%S %p")
|
|
65
|
+
elif isinstance(v, datetime):
|
|
66
|
+
datetime_value = v
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Invalid type for {info.field_name}: {type(v)}. Expected str or datetime."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if info.field_name == "factory_timestamp":
|
|
73
|
+
# factory_timestamp is in UTC
|
|
74
|
+
datetime_value = datetime_value.replace(tzinfo=UTC)
|
|
75
|
+
return datetime_value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class GlucoseItemTrend(GlucoseItem):
|
|
79
|
+
trend_arrow: Trend
|
|
80
|
+
|
|
81
|
+
def __str__(self):
|
|
82
|
+
return f"{super().__str__()} {self.trend_arrow}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class F(BaseModel):
|
|
86
|
+
"""F class to store F data."""
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(
|
|
89
|
+
from_attributes=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
th: int
|
|
93
|
+
thmm: float
|
|
94
|
+
d: int
|
|
95
|
+
tl: int
|
|
96
|
+
tlmm: float
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class L(BaseModel):
|
|
100
|
+
"""L class to store L data."""
|
|
101
|
+
|
|
102
|
+
model_config = ConfigDict(
|
|
103
|
+
from_attributes=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
th: int
|
|
107
|
+
thmm: float
|
|
108
|
+
d: int
|
|
109
|
+
tl: int
|
|
110
|
+
tlmm: float
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class H(BaseModel):
|
|
114
|
+
"""H class to store H data."""
|
|
115
|
+
|
|
116
|
+
model_config = ConfigDict(
|
|
117
|
+
from_attributes=True,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
th: int
|
|
121
|
+
thmm: float
|
|
122
|
+
d: int
|
|
123
|
+
f: float
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Nd(BaseModel):
|
|
127
|
+
"""Nd class to store Nd data."""
|
|
128
|
+
|
|
129
|
+
model_config = ConfigDict(
|
|
130
|
+
from_attributes=True,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
i: int
|
|
134
|
+
r: int
|
|
135
|
+
l: int
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Std(BaseModel):
|
|
139
|
+
"""Std class to store Std data."""
|
|
140
|
+
|
|
141
|
+
...
|
|
142
|
+
model_config = ConfigDict(
|
|
143
|
+
from_attributes=True,
|
|
144
|
+
)
|
|
145
|
+
sd: bool
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
from pydantic.alias_generators import to_camel
|
|
3
|
+
|
|
4
|
+
from .config import FixedLowAlarmValues
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Sensor(BaseModel):
|
|
8
|
+
"""Sensor class to store sensor data."""
|
|
9
|
+
|
|
10
|
+
model_config = ConfigDict(
|
|
11
|
+
str_strip_whitespace=True,
|
|
12
|
+
alias_generator=to_camel,
|
|
13
|
+
populate_by_name=True,
|
|
14
|
+
from_attributes=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
device_id: str
|
|
18
|
+
sn: str
|
|
19
|
+
a: int
|
|
20
|
+
w: int
|
|
21
|
+
pt: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PatientDevice(BaseModel):
|
|
25
|
+
"""PatientDevice class to store device data."""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(
|
|
28
|
+
str_strip_whitespace=True,
|
|
29
|
+
alias_generator=to_camel,
|
|
30
|
+
populate_by_name=True,
|
|
31
|
+
from_attributes=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
did: str
|
|
35
|
+
dtid: int
|
|
36
|
+
v: str
|
|
37
|
+
ll: int
|
|
38
|
+
hl: int
|
|
39
|
+
u: int
|
|
40
|
+
fixed_low_alarm_values: FixedLowAlarmValues
|
|
41
|
+
alarms: bool
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ActiveSensor(BaseModel):
|
|
45
|
+
"""ActiveSensor class to store active sensor data."""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(
|
|
48
|
+
str_strip_whitespace=True,
|
|
49
|
+
alias_generator=to_camel,
|
|
50
|
+
populate_by_name=True,
|
|
51
|
+
from_attributes=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
sensor: Sensor
|
|
55
|
+
device: PatientDevice
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Llu(BaseModel):
|
|
7
|
+
policyAccept: int
|
|
8
|
+
touAccept: int
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Consents(BaseModel):
|
|
12
|
+
llu: Llu
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SystemMessages(BaseModel):
|
|
16
|
+
firstUsePhoenix: int
|
|
17
|
+
firstUsePhoenixReportsDataMerged: int
|
|
18
|
+
lluGettingStartedBanner: int
|
|
19
|
+
lluNewFeatureModal: int
|
|
20
|
+
lluOnboarding: int
|
|
21
|
+
lvWebPostRelease: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class System(BaseModel):
|
|
25
|
+
messages: SystemMessages
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class User(BaseModel):
|
|
29
|
+
id: str
|
|
30
|
+
firstName: str
|
|
31
|
+
lastName: str
|
|
32
|
+
email: str
|
|
33
|
+
country: str
|
|
34
|
+
uiLanguage: str
|
|
35
|
+
communicationLanguage: str
|
|
36
|
+
accountType: str
|
|
37
|
+
uom: str
|
|
38
|
+
dateFormat: str
|
|
39
|
+
timeFormat: str
|
|
40
|
+
emailDay: List[int]
|
|
41
|
+
system: System
|
|
42
|
+
details: dict
|
|
43
|
+
created: int
|
|
44
|
+
lastLogin: int
|
|
45
|
+
programs: dict
|
|
46
|
+
dateOfBirth: int
|
|
47
|
+
practices: dict
|
|
48
|
+
devices: dict
|
|
49
|
+
consents: Consents
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Notifications(BaseModel):
|
|
53
|
+
unresolved: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DataMessages(BaseModel):
|
|
57
|
+
unread: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AuthTicket(BaseModel):
|
|
61
|
+
token: str
|
|
62
|
+
expires: int
|
|
63
|
+
duration: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Data(BaseModel):
|
|
67
|
+
user: User
|
|
68
|
+
messages: DataMessages
|
|
69
|
+
notifications: Notifications
|
|
70
|
+
authTicket: AuthTicket
|
|
71
|
+
invitations: List[str]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LoginResponse(BaseModel):
|
|
75
|
+
status: int
|
|
76
|
+
data: Data
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ErrorMessage(BaseModel):
|
|
80
|
+
message: str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class LoginResponseUnauthenticated(BaseModel):
|
|
84
|
+
status: int
|
|
85
|
+
error: ErrorMessage
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class LoginRedirectData(BaseModel):
|
|
89
|
+
redirect: bool
|
|
90
|
+
region: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LoginRedirectResponse(BaseModel):
|
|
94
|
+
status: int
|
|
95
|
+
data: LoginRedirectData
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LoginArgs(BaseModel):
|
|
99
|
+
email: str = ""
|
|
100
|
+
password: str = ""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pylibrelinkup
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A client for the Abbott LibreLinkUp API
|
|
5
|
+
Author-email: Rob Berwick <rob.berwick@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Rob Berwick
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/robberwick/pylibrelinkup
|
|
28
|
+
Project-URL: Issues, https://github.com/robberwick/pylibrelinkup/issues
|
|
29
|
+
Keywords: librelink,librelinkup,abbott,diabetes,glucose,api,client,health,medical,blood-sugar,continuous-glucose-monitoring,cgm
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Requires-Python: >=3.11
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: requests>=2.31.0
|
|
37
|
+
Requires-Dist: pydantic>=2.5.0
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: black; extra == "dev"
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest; extra == "test"
|
|
42
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
43
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
44
|
+
Requires-Dist: polyfactory; extra == "test"
|
|
45
|
+
Requires-Dist: responses; extra == "test"
|
|
46
|
+
|
|
47
|
+
# pylibrelinkup
|
|
48
|
+
|
|
49
|
+
`pylibrelinkup` is a Python client for the LibreLinkUp API, which allows you to interact with the LibreLinkUp service to retrieve glucose data and other related information. This project is a Python implementation inspired by the [libre-link-up-api-client](https://github.com/DiaKEM/libre-link-up-api-client) project.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
To install `pylibrelinkup`, you can use `pip`:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install pylibrelinkup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### Initialization
|
|
62
|
+
|
|
63
|
+
First, you need to import the necessary modules, initialize the client, and authenticate with your LibreLinkUp credentials:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from pylibrelinkup.client import LibreLinkUpClient
|
|
67
|
+
|
|
68
|
+
client = LibreLinkUpClient(username='your_username', password='your_password')
|
|
69
|
+
client.authenticate()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Getting Patient List
|
|
73
|
+
|
|
74
|
+
You can fetch the patient list using the `get_patient_list` method:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
patient_list = client.get_patient_list()
|
|
78
|
+
print(patient_list)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Getting Patient data
|
|
82
|
+
|
|
83
|
+
Retrieve patient data using the `get_graph_data` method:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
patient = client.get_patient_list()[0]
|
|
87
|
+
patient_data = client.get_graph_data(patient_id=patient.patient_id)
|
|
88
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pylibrelinkup/__init__.py
|
|
5
|
+
src/pylibrelinkup/api_url.py
|
|
6
|
+
src/pylibrelinkup/client.py
|
|
7
|
+
src/pylibrelinkup/exceptions.py
|
|
8
|
+
src/pylibrelinkup.egg-info/PKG-INFO
|
|
9
|
+
src/pylibrelinkup.egg-info/SOURCES.txt
|
|
10
|
+
src/pylibrelinkup.egg-info/dependency_links.txt
|
|
11
|
+
src/pylibrelinkup.egg-info/requires.txt
|
|
12
|
+
src/pylibrelinkup.egg-info/top_level.txt
|
|
13
|
+
src/pylibrelinkup/models/__init__.py
|
|
14
|
+
src/pylibrelinkup/models/config.py
|
|
15
|
+
src/pylibrelinkup/models/connection.py
|
|
16
|
+
src/pylibrelinkup/models/data.py
|
|
17
|
+
src/pylibrelinkup/models/hardware.py
|
|
18
|
+
src/pylibrelinkup/models/login.py
|
|
19
|
+
tests/test_client_authentication.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pylibrelinkup
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import requests
|
|
5
|
+
import responses
|
|
6
|
+
from polyfactory.factories.pydantic_factory import ModelFactory
|
|
7
|
+
|
|
8
|
+
from pylibrelinkup.client import Client, AuthenticationError
|
|
9
|
+
from pylibrelinkup.api_url import APIUrl
|
|
10
|
+
from pylibrelinkup.models.connection import ConnectionResponse
|
|
11
|
+
from pylibrelinkup.models.data import Patient
|
|
12
|
+
from pylibrelinkup.models.login import (
|
|
13
|
+
LoginResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LoginResponseFactory(ModelFactory[LoginResponse]):
|
|
18
|
+
__model__ = LoginResponse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConnectionResponseFactory(ModelFactory[ConnectionResponse]):
|
|
22
|
+
__model__ = ConnectionResponse
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PatientFactory(ModelFactory[Patient]):
|
|
26
|
+
__model__ = Patient
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mocked_responses():
|
|
31
|
+
with responses.RequestsMock() as rsps:
|
|
32
|
+
yield rsps
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize("api_url", APIUrl)
|
|
36
|
+
def test_authenticate_raises_error_on_incorrect_login(
|
|
37
|
+
mocked_responses, api_url: APIUrl
|
|
38
|
+
):
|
|
39
|
+
"""Test that the authenticate method raises an error when the login fails."""
|
|
40
|
+
mocked_responses.add(
|
|
41
|
+
responses.POST,
|
|
42
|
+
f"{api_url.value}/llu/auth/login",
|
|
43
|
+
json={"not": "important"},
|
|
44
|
+
status=200,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
client = Client(email="parp", password="parp", api_url=api_url)
|
|
48
|
+
|
|
49
|
+
with pytest.raises(AuthenticationError):
|
|
50
|
+
client.authenticate()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize("api_url", APIUrl)
|
|
54
|
+
def test_authenticate_sets_token_on_correct_login(mocked_responses, api_url: APIUrl):
|
|
55
|
+
"""Test that the authenticate method sets the token on a successful login."""
|
|
56
|
+
response = LoginResponseFactory.build()
|
|
57
|
+
response.data.authTicket.token = "parp"
|
|
58
|
+
assert isinstance(response, LoginResponse)
|
|
59
|
+
|
|
60
|
+
mocked_responses.add(
|
|
61
|
+
responses.POST,
|
|
62
|
+
f"{api_url.value}/llu/auth/login",
|
|
63
|
+
json=json.loads(response.model_dump_json()),
|
|
64
|
+
status=200,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
client = Client(email="parp", password="parp", api_url=api_url)
|
|
68
|
+
|
|
69
|
+
client.authenticate()
|
|
70
|
+
|
|
71
|
+
assert client.token == "parp"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize("api_url", APIUrl)
|
|
75
|
+
def test_authenticate_raises_error_on_invalid_response(
|
|
76
|
+
mocked_responses, api_url: APIUrl
|
|
77
|
+
):
|
|
78
|
+
"""Test that the authenticate method raises an error on invalid response."""
|
|
79
|
+
mocked_responses.add(
|
|
80
|
+
responses.POST,
|
|
81
|
+
f"{api_url.value}/llu/auth/login",
|
|
82
|
+
json={"invalid": "response"},
|
|
83
|
+
status=200,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
client = Client(email="parp", password="parp", api_url=api_url)
|
|
87
|
+
|
|
88
|
+
with pytest.raises(AuthenticationError):
|
|
89
|
+
client.authenticate()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize("api_url", APIUrl)
|
|
93
|
+
def test_authenticate_raises_error_on_http_error(mocked_responses, api_url: APIUrl):
|
|
94
|
+
"""Test that the authenticate method raises an error on HTTP error."""
|
|
95
|
+
mocked_responses.add(
|
|
96
|
+
responses.POST,
|
|
97
|
+
f"{api_url.value}/llu/auth/login",
|
|
98
|
+
json={"error": "Unauthorized"},
|
|
99
|
+
status=401,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
client = Client(email="parp", password="parp", api_url=api_url)
|
|
103
|
+
|
|
104
|
+
with pytest.raises(requests.exceptions.HTTPError):
|
|
105
|
+
client.authenticate()
|