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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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}")
@@ -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,12 @@
1
+ requests>=2.31.0
2
+ pydantic>=2.5.0
3
+
4
+ [dev]
5
+ black
6
+
7
+ [test]
8
+ pytest
9
+ pytest-cov
10
+ pytest-mock
11
+ polyfactory
12
+ responses
@@ -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()