pydexcom

PyPI Python versions Pre-commit Tests Docs

A simple Python API to interact with Dexcom Share service. Used to get real-time Dexcom CGM sensor data.

Quick-start

  1. Download the Dexcom G7 / G6 / G5 / G4 mobile app and enable the Share service.

The Dexcom Share service requires setup of at least one follower to enable the share service, but pydexcom will use your (or the dependent's) credentials, not the follower's or manager's.

  1. Install the pydexcom package.

pip install pydexcom

  1. Profit.
>>> from pydexcom import Dexcom
>>> dexcom = Dexcom(username="username", password="password") # `region="ous"` if outside of US, `region="jp"` if Japan
>>> dexcom = Dexcom(username="+11234567890", password="password") # phone number
>>> dexcom = Dexcom(username="user@email.com", password="password") # email address
>>> dexcom = Dexcom(account_id="12345678-90ab-cdef-1234-567890abcdef", password="password") # account ID (advanced)
>>> glucose_reading = dexcom.get_current_glucose_reading()
>>> print(glucose_reading)
85

>>> glucose_reading.value
85

>>> glucose_reading.mmol_l
4.7

>>> glucose_reading.trend
4

>>> glucose_reading.trend_direction
'Flat'

>>> glucose_reading.trend_description
'steady'

>>> glucose_reading.trend_arrow
'→'

>>> print(bg.datetime)
2023-08-07 20:40:58

>>> glucose_reading.json
{'WT': 'Date(1691455258000)', 'ST': 'Date(1691455258000)', 'DT': 'Date(1691455258000-0400)', 'Value': 85, 'Trend': 'Flat'}

Documentation

https://gagebenne.github.io/pydexcom/pydexcom.html

Frequently Asked Questions

Why is my password not working?

The Dexcom Share API understandably reports limited information during account validation. If anything is incorrect, the API simply reports back invalid password ( pydexcom.errors.AccountErrorEnum ). However, there could be many reasons you are getting this error:

1. Ensure your credentials are valid.

Validate your Dexcom account credentials by logging on to the Dexcom Account Management website for your region:

For users in the United States: uam1.dexcom.com. For users outside of the United States: uam2.dexcom.com. For users in the Asia-Pacific: uam.dexcom.jp.

2. Use the correct Dexcom Share API endpoint.

For users in the United States: use the default, or set region="us" when initializing Dexcom. For users outside of the United States: be sure to set region="ous" when initializing Dexcom . For users in Japan: be sure to set region="jp" when initializing Dexcom.

3. Ensure your username is correctly formatted.

Format phone numbers with a +, your country code, then your phone number. For example, a US phone number of (123)-456-7890 would be supplied as a username="+11234567890".

4. Use _your_ Dexcom Share credentials, not the _follower's_ credentials.

Use the same credentials used to login to the Dexcom mobile application publishing the glucose readings.

5. Ensure you have at least one follower on Dexcom Share.

The Dexcom Share service requires setup of at least one follower to enable the service, as does this package.

6. Try using your account ID.

You can find your account ID by logging in to Dexcom Account Management website for your region. After logging in, note the UUID in the URL -- this is your account ID.

Format account IDs (UUIDs) with hyphens. For example, an account ID of 1234567890abcdef1234567890abcdef found in the URL after logging in would be supplied as account_id="12345678-90ab-cdef-1234-567890abcdef".

7. Report it!

The Dexcom Share API sometimes changes. If you believe there is an issue with pydexcom , feel free to create an issue if one has not been created yet already.

Why not use the official Dexcom Developer API?

The official Dexcom API is a great tool to view trends, statistics, and day-by-day data, but is not suitable for real time fetching of glucose readings as it is a retrospective API.

Can I use the Dexcom Stelo with this package?

No, the Dexcom Stelo isn't compatible with the Dexcom Share service, so this package can't retrieve its readings.

How can I let you know of suggestions or issues?

By all means submit a pull request if you have a feature you would like to see in the next release. Alternatively, you may create an issue if you have a suggestion or bug you'd like to report.

Where is this package being used?

Primarily this package is used in the Home Assistant Dexcom integration, but it's fantastic to see community projects involving pydexcom :

  1"""
  2.. include:: ../README.md
  3"""  # noqa: D200, D212, D400, D415
  4
  5from __future__ import annotations
  6
  7import logging
  8import re
  9from datetime import datetime
 10from typing import Any
 11from uuid import UUID
 12
 13import requests
 14
 15from .const import (
 16    DEFAULT_UUID,
 17    DEXCOM_APPLICATION_IDS,
 18    DEXCOM_AUTHENTICATE_ENDPOINT,
 19    DEXCOM_BASE_URLS,
 20    DEXCOM_GLUCOSE_READINGS_ENDPOINT,
 21    DEXCOM_LOGIN_ID_ENDPOINT,
 22    DEXCOM_TREND_DIRECTIONS,
 23    MAX_MAX_COUNT,
 24    MAX_MINUTES,
 25    MMOL_L_CONVERSION_FACTOR,
 26    TREND_ARROWS,
 27    TREND_DESCRIPTIONS,
 28    Region,
 29)
 30from .errors import (
 31    AccountError,
 32    AccountErrorEnum,
 33    ArgumentError,
 34    ArgumentErrorEnum,
 35    DexcomError,
 36    SessionError,
 37    SessionErrorEnum,
 38)
 39
 40_LOGGER = logging.getLogger("pydexcom")
 41
 42
 43class GlucoseReading:
 44    """Class for parsing glucose reading from Dexcom Share API."""
 45
 46    def __init__(self, json_glucose_reading: dict[str, Any]) -> None:
 47        """Initialize `GlucoseReading` with JSON glucose reading from Dexcom Share API.
 48
 49        :param json_glucose_reading: JSON glucose reading from Dexcom Share API
 50        """
 51        self._json = json_glucose_reading
 52        try:
 53            self._value = int(json_glucose_reading["Value"])
 54            self._trend_direction: str = json_glucose_reading["Trend"]
 55            # Dexcom Share API returns `str` direction now, previously `int` trend
 56            self._trend: int = DEXCOM_TREND_DIRECTIONS[self._trend_direction]
 57
 58            match = re.match(
 59                r"Date\((?P<timestamp>\d+)(?P<timezone>[+-]\d{4})\)",
 60                json_glucose_reading["DT"],
 61            )
 62            if match:
 63                self._datetime = datetime.fromtimestamp(
 64                    int(match.group("timestamp")) / 1000.0,
 65                    tz=datetime.strptime(match.group("timezone"), "%z").tzinfo,
 66                )
 67        except (KeyError, TypeError, ValueError) as error:
 68            raise ArgumentError(ArgumentErrorEnum.GLUCOSE_READING_INVALID) from error
 69
 70    @property
 71    def value(self) -> int:
 72        """Blood glucose value in mg/dL."""
 73        return self._value
 74
 75    @property
 76    def mg_dl(self) -> int:
 77        """Blood glucose value in mg/dL."""
 78        return self._value
 79
 80    @property
 81    def mmol_l(self) -> float:
 82        """Blood glucose value in mmol/L."""
 83        return round(self.value * MMOL_L_CONVERSION_FACTOR, 1)
 84
 85    @property
 86    def trend(self) -> int:
 87        """Blood glucose trend information.
 88
 89        Value of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`.
 90        """
 91        return self._trend
 92
 93    @property
 94    def trend_direction(self) -> str:
 95        """Blood glucose trend direction.
 96
 97        Key of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`.
 98        """
 99        return self._trend_direction
100
101    @property
102    def trend_description(self) -> str | None:
103        """Blood glucose trend information description.
104
105        See `pydexcom.const.TREND_DESCRIPTIONS`.
106        """
107        return TREND_DESCRIPTIONS[self._trend]
108
109    @property
110    def trend_arrow(self) -> str:
111        """Blood glucose trend as unicode arrow (`pydexcom.const.TREND_ARROWS`)."""
112        return TREND_ARROWS[self._trend]
113
114    @property
115    def datetime(self) -> datetime:
116        """Glucose reading recorded time as datetime."""
117        return self._datetime
118
119    @property
120    def json(self) -> dict[str, Any]:
121        """JSON glucose reading from Dexcom Share API."""
122        return self._json
123
124    def __str__(self) -> str:
125        """Blood glucose value as in mg/dL."""
126        return str(self._value)
127
128
129def valid_uuid(uuid: str | None) -> bool:
130    """Check if UUID is valid."""
131    try:
132        UUID(str(uuid))
133    except ValueError:
134        return False
135    else:
136        return True
137
138
139class Dexcom:
140    """Class for communicating with Dexcom Share API."""
141
142    def __init__(
143        self,
144        *,
145        password: str,
146        account_id: str | None = None,
147        username: str | None = None,
148        region: Region = Region.US,
149    ) -> None:
150        """Initialize `Dexcom` with Dexcom Share credentials.
151
152        :param username: username for the Dexcom Share user, *not follower*.
153        :param account_id: account ID for the Dexcom Share user, *not follower*.
154        :param password: password for the Dexcom Share user.
155        :param region: the region to use, one of `"us"`, `"ous"`, `"jp"`.
156        """
157        user_ids = sum(user_id is not None for user_id in [account_id, username])
158        if user_ids == 0:
159            raise ArgumentError(ArgumentErrorEnum.NONE_USER_ID_PROVIDED)
160        if user_ids != 1:
161            raise ArgumentError(ArgumentErrorEnum.TOO_MANY_USER_ID_PROVIDED)
162
163        self._base_url = DEXCOM_BASE_URLS[region]
164        self._application_id = DEXCOM_APPLICATION_IDS[region]
165        self._password = password
166        self._username: str | None = username
167        self._account_id: str | None = account_id
168        self._session_id: str | None = None
169        self.__session = requests.Session()
170        self._session()
171
172    def _post(
173        self,
174        endpoint: str,
175        params: dict[str, Any] | None = None,
176        json: dict[str, Any] | None = None,
177    ) -> Any:  # noqa: ANN401
178        """Send post request to Dexcom Share API.
179
180        :param endpoint: URL of the post request
181        :param params: `dict` to send in the query string of the post request
182        :param json: JSON to send in the body of the post request
183        """
184        response = self.__session.post(
185            f"{self._base_url}/{endpoint}",
186            headers={"Accept-Encoding": "application/json"},
187            params=params,
188            json={} if json is None else json,
189        )
190
191        try:
192            response.raise_for_status()
193            return response.json()
194        except requests.HTTPError as http_error:
195            error = self._handle_response(response)
196            if error:
197                raise error from http_error
198            _LOGGER.exception("%s", response.text)
199            raise
200
201    def _handle_response(self, response: requests.Response) -> DexcomError | None:  # noqa: C901
202        error: DexcomError | None = None
203        """
204        Parse `requests.Response` for `pydexcom.errors.DexcomError`.
205
206        :param response: `requests.Response` to parse
207        """
208        if response.json():
209            _LOGGER.debug("%s", response.json())
210            code = response.json().get("Code", None)
211            message = response.json().get("Message", None)
212            if code == "SessionIdNotFound":
213                error = SessionError(SessionErrorEnum.NOT_FOUND)
214            elif code == "SessionNotValid":
215                error = SessionError(SessionErrorEnum.INVALID)
216            elif code == "AccountPasswordInvalid":  # defunct
217                error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION)
218            elif code == "SSO_AuthenticateMaxAttemptsExceeded":
219                error = AccountError(AccountErrorEnum.MAX_ATTEMPTS)
220            elif code == "SSO_InternalError":
221                if message and (
222                    "Cannot Authenticate by AccountName" in message
223                    or "Cannot Authenticate by AccountId" in message
224                ):
225                    error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION)
226            elif code == "InvalidArgument":
227                if message and "accountName" in message:
228                    error = ArgumentError(ArgumentErrorEnum.USERNAME_INVALID)
229                elif message and "password" in message:
230                    error = ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID)
231                elif message and "UUID" in message:
232                    error = ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID)
233            elif code and message:
234                _LOGGER.debug("%s: %s", code, message)
235        return error
236
237    def _validate_session_id(self) -> None:
238        """Validate session ID."""
239        if any(
240            [
241                not isinstance(self._session_id, str),
242                not self._session_id,
243                not valid_uuid(self._session_id),
244            ],
245        ):
246            raise ArgumentError(ArgumentErrorEnum.SESSION_ID_INVALID)
247        if self._session_id == DEFAULT_UUID:
248            raise ArgumentError(ArgumentErrorEnum.SESSION_ID_DEFAULT)
249
250    def _validate_username(self) -> None:
251        """Validate username."""
252        if any([not isinstance(self._username, str), not self._username]):
253            raise ArgumentError(ArgumentErrorEnum.USERNAME_INVALID)
254
255    def _validate_password(self) -> None:
256        """Validate password."""
257        if any([not isinstance(self._password, str), not self._password]):
258            raise ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID)
259
260    def _validate_account_id(self) -> None:
261        """Validate account ID."""
262        if any(
263            [
264                not isinstance(self._account_id, str),
265                not self._account_id,
266                not valid_uuid(self._account_id),
267            ],
268        ):
269            raise ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID)
270        if self._account_id == DEFAULT_UUID:
271            raise ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_DEFAULT)
272
273    def _get_account_id(self) -> str:
274        """Retrieve account ID from the authentication endpoint.
275
276        See `pydexcom.const.DEXCOM_AUTHENTICATE_ENDPOINT`.
277        """
278        _LOGGER.debug("Retrieve account ID from the authentication endpoint")
279        return self._post(
280            DEXCOM_AUTHENTICATE_ENDPOINT,
281            json={
282                "accountName": self._username,
283                "password": self._password,
284                "applicationId": self._application_id,
285            },
286        )
287
288    def _get_session_id(self) -> str:
289        """Retrieve session ID from the login endpoint.
290
291        See `pydexcom.const.DEXCOM_LOGIN_ID_ENDPOINT`.
292        """
293        _LOGGER.debug("Retrieve session ID from the login endpoint")
294        return self._post(
295            DEXCOM_LOGIN_ID_ENDPOINT,
296            json={
297                "accountId": self._account_id,
298                "password": self._password,
299                "applicationId": self._application_id,
300            },
301        )
302
303    def _session(self) -> None:
304        """Create Dexcom Share API session."""
305        self._validate_password()
306
307        if self._account_id is None:
308            self._validate_username()
309            self._account_id = self._get_account_id()
310
311        self._validate_account_id()
312        self._session_id = self._get_session_id()
313        self._validate_session_id()
314
315    def _get_glucose_readings(
316        self,
317        minutes: int = MAX_MINUTES,
318        max_count: int = MAX_MAX_COUNT,
319    ) -> list[dict[str, Any]]:
320        """Retrieve glucose readings from the glucose readings endpoint.
321
322        See `pydexcom.const.DEXCOM_GLUCOSE_READINGS_ENDPOINT`.
323        """
324        if not isinstance(minutes, int) or any([minutes < 0, minutes > MAX_MINUTES]):
325            raise ArgumentError(ArgumentErrorEnum.MINUTES_INVALID)
326        if not isinstance(max_count, int) or any(
327            [max_count < 0, max_count > MAX_MAX_COUNT],
328        ):
329            raise ArgumentError(ArgumentErrorEnum.MAX_COUNT_INVALID)
330
331        _LOGGER.debug("Retrieve glucose readings from the glucose readings endpoint")
332        return self._post(
333            DEXCOM_GLUCOSE_READINGS_ENDPOINT,
334            params={
335                "sessionId": self._session_id,
336                "minutes": minutes,
337                "maxCount": max_count,
338            },
339        )
340
341    def get_glucose_readings(
342        self,
343        minutes: int = MAX_MINUTES,
344        max_count: int = MAX_MAX_COUNT,
345    ) -> list[GlucoseReading]:
346        """Get `max_count` glucose readings within specified number of `minutes`.
347
348        Catches one instance of a thrown `pydexcom.errors.SessionError` if session ID
349        expired, attempts to get a new session ID and retries.
350
351        :param minutes: Number of minutes to retrieve glucose readings from (1-1440)
352        :param max_count: Maximum number of glucose readings to retrieve (1-288)
353        """
354        json_glucose_readings: list[dict[str, Any]] = []
355
356        try:
357            # Requesting glucose reading with DEFAULT_UUID returns non-JSON empty string
358            self._validate_session_id()
359
360            json_glucose_readings = self._get_glucose_readings(minutes, max_count)
361        except SessionError:
362            # Attempt to update expired session ID
363            self._session()
364
365            json_glucose_readings = self._get_glucose_readings(minutes, max_count)
366
367        return [GlucoseReading(json_reading) for json_reading in json_glucose_readings]
368
369    def get_latest_glucose_reading(self) -> GlucoseReading | None:
370        """Get latest available glucose reading, within the last 24 hours."""
371        glucose_readings = self.get_glucose_readings(max_count=1)
372        return glucose_readings[0] if glucose_readings else None
373
374    def get_current_glucose_reading(self) -> GlucoseReading | None:
375        """Get current available glucose reading, within the last 10 minutes."""
376        glucose_readings = self.get_glucose_readings(minutes=10, max_count=1)
377        return glucose_readings[0] if glucose_readings else None
class GlucoseReading:
 44class GlucoseReading:
 45    """Class for parsing glucose reading from Dexcom Share API."""
 46
 47    def __init__(self, json_glucose_reading: dict[str, Any]) -> None:
 48        """Initialize `GlucoseReading` with JSON glucose reading from Dexcom Share API.
 49
 50        :param json_glucose_reading: JSON glucose reading from Dexcom Share API
 51        """
 52        self._json = json_glucose_reading
 53        try:
 54            self._value = int(json_glucose_reading["Value"])
 55            self._trend_direction: str = json_glucose_reading["Trend"]
 56            # Dexcom Share API returns `str` direction now, previously `int` trend
 57            self._trend: int = DEXCOM_TREND_DIRECTIONS[self._trend_direction]
 58
 59            match = re.match(
 60                r"Date\((?P<timestamp>\d+)(?P<timezone>[+-]\d{4})\)",
 61                json_glucose_reading["DT"],
 62            )
 63            if match:
 64                self._datetime = datetime.fromtimestamp(
 65                    int(match.group("timestamp")) / 1000.0,
 66                    tz=datetime.strptime(match.group("timezone"), "%z").tzinfo,
 67                )
 68        except (KeyError, TypeError, ValueError) as error:
 69            raise ArgumentError(ArgumentErrorEnum.GLUCOSE_READING_INVALID) from error
 70
 71    @property
 72    def value(self) -> int:
 73        """Blood glucose value in mg/dL."""
 74        return self._value
 75
 76    @property
 77    def mg_dl(self) -> int:
 78        """Blood glucose value in mg/dL."""
 79        return self._value
 80
 81    @property
 82    def mmol_l(self) -> float:
 83        """Blood glucose value in mmol/L."""
 84        return round(self.value * MMOL_L_CONVERSION_FACTOR, 1)
 85
 86    @property
 87    def trend(self) -> int:
 88        """Blood glucose trend information.
 89
 90        Value of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`.
 91        """
 92        return self._trend
 93
 94    @property
 95    def trend_direction(self) -> str:
 96        """Blood glucose trend direction.
 97
 98        Key of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`.
 99        """
100        return self._trend_direction
101
102    @property
103    def trend_description(self) -> str | None:
104        """Blood glucose trend information description.
105
106        See `pydexcom.const.TREND_DESCRIPTIONS`.
107        """
108        return TREND_DESCRIPTIONS[self._trend]
109
110    @property
111    def trend_arrow(self) -> str:
112        """Blood glucose trend as unicode arrow (`pydexcom.const.TREND_ARROWS`)."""
113        return TREND_ARROWS[self._trend]
114
115    @property
116    def datetime(self) -> datetime:
117        """Glucose reading recorded time as datetime."""
118        return self._datetime
119
120    @property
121    def json(self) -> dict[str, Any]:
122        """JSON glucose reading from Dexcom Share API."""
123        return self._json
124
125    def __str__(self) -> str:
126        """Blood glucose value as in mg/dL."""
127        return str(self._value)

Class for parsing glucose reading from Dexcom Share API.

GlucoseReading(json_glucose_reading: dict[str, typing.Any])
47    def __init__(self, json_glucose_reading: dict[str, Any]) -> None:
48        """Initialize `GlucoseReading` with JSON glucose reading from Dexcom Share API.
49
50        :param json_glucose_reading: JSON glucose reading from Dexcom Share API
51        """
52        self._json = json_glucose_reading
53        try:
54            self._value = int(json_glucose_reading["Value"])
55            self._trend_direction: str = json_glucose_reading["Trend"]
56            # Dexcom Share API returns `str` direction now, previously `int` trend
57            self._trend: int = DEXCOM_TREND_DIRECTIONS[self._trend_direction]
58
59            match = re.match(
60                r"Date\((?P<timestamp>\d+)(?P<timezone>[+-]\d{4})\)",
61                json_glucose_reading["DT"],
62            )
63            if match:
64                self._datetime = datetime.fromtimestamp(
65                    int(match.group("timestamp")) / 1000.0,
66                    tz=datetime.strptime(match.group("timezone"), "%z").tzinfo,
67                )
68        except (KeyError, TypeError, ValueError) as error:
69            raise ArgumentError(ArgumentErrorEnum.GLUCOSE_READING_INVALID) from error

Initialize GlucoseReading with JSON glucose reading from Dexcom Share API.

Parameters
  • json_glucose_reading: JSON glucose reading from Dexcom Share API
value: int
71    @property
72    def value(self) -> int:
73        """Blood glucose value in mg/dL."""
74        return self._value

Blood glucose value in mg/dL.

mg_dl: int
76    @property
77    def mg_dl(self) -> int:
78        """Blood glucose value in mg/dL."""
79        return self._value

Blood glucose value in mg/dL.

mmol_l: float
81    @property
82    def mmol_l(self) -> float:
83        """Blood glucose value in mmol/L."""
84        return round(self.value * MMOL_L_CONVERSION_FACTOR, 1)

Blood glucose value in mmol/L.

trend: int
86    @property
87    def trend(self) -> int:
88        """Blood glucose trend information.
89
90        Value of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`.
91        """
92        return self._trend

Blood glucose trend information.

Value of pydexcom.const.DEXCOM_TREND_DIRECTIONS.

trend_direction: str
 94    @property
 95    def trend_direction(self) -> str:
 96        """Blood glucose trend direction.
 97
 98        Key of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`.
 99        """
100        return self._trend_direction

Blood glucose trend direction.

Key of pydexcom.const.DEXCOM_TREND_DIRECTIONS.

trend_description: str | None
102    @property
103    def trend_description(self) -> str | None:
104        """Blood glucose trend information description.
105
106        See `pydexcom.const.TREND_DESCRIPTIONS`.
107        """
108        return TREND_DESCRIPTIONS[self._trend]

Blood glucose trend information description.

See pydexcom.const.TREND_DESCRIPTIONS.

trend_arrow: str
110    @property
111    def trend_arrow(self) -> str:
112        """Blood glucose trend as unicode arrow (`pydexcom.const.TREND_ARROWS`)."""
113        return TREND_ARROWS[self._trend]

Blood glucose trend as unicode arrow (pydexcom.const.TREND_ARROWS).

datetime: <property object at 0x7fca53ca1e40>
115    @property
116    def datetime(self) -> datetime:
117        """Glucose reading recorded time as datetime."""
118        return self._datetime

Glucose reading recorded time as datetime.

json: dict[str, typing.Any]
120    @property
121    def json(self) -> dict[str, Any]:
122        """JSON glucose reading from Dexcom Share API."""
123        return self._json

JSON glucose reading from Dexcom Share API.

def valid_uuid(uuid: str | None) -> bool:
130def valid_uuid(uuid: str | None) -> bool:
131    """Check if UUID is valid."""
132    try:
133        UUID(str(uuid))
134    except ValueError:
135        return False
136    else:
137        return True

Check if UUID is valid.

class Dexcom:
140class Dexcom:
141    """Class for communicating with Dexcom Share API."""
142
143    def __init__(
144        self,
145        *,
146        password: str,
147        account_id: str | None = None,
148        username: str | None = None,
149        region: Region = Region.US,
150    ) -> None:
151        """Initialize `Dexcom` with Dexcom Share credentials.
152
153        :param username: username for the Dexcom Share user, *not follower*.
154        :param account_id: account ID for the Dexcom Share user, *not follower*.
155        :param password: password for the Dexcom Share user.
156        :param region: the region to use, one of `"us"`, `"ous"`, `"jp"`.
157        """
158        user_ids = sum(user_id is not None for user_id in [account_id, username])
159        if user_ids == 0:
160            raise ArgumentError(ArgumentErrorEnum.NONE_USER_ID_PROVIDED)
161        if user_ids != 1:
162            raise ArgumentError(ArgumentErrorEnum.TOO_MANY_USER_ID_PROVIDED)
163
164        self._base_url = DEXCOM_BASE_URLS[region]
165        self._application_id = DEXCOM_APPLICATION_IDS[region]
166        self._password = password
167        self._username: str | None = username
168        self._account_id: str | None = account_id
169        self._session_id: str | None = None
170        self.__session = requests.Session()
171        self._session()
172
173    def _post(
174        self,
175        endpoint: str,
176        params: dict[str, Any] | None = None,
177        json: dict[str, Any] | None = None,
178    ) -> Any:  # noqa: ANN401
179        """Send post request to Dexcom Share API.
180
181        :param endpoint: URL of the post request
182        :param params: `dict` to send in the query string of the post request
183        :param json: JSON to send in the body of the post request
184        """
185        response = self.__session.post(
186            f"{self._base_url}/{endpoint}",
187            headers={"Accept-Encoding": "application/json"},
188            params=params,
189            json={} if json is None else json,
190        )
191
192        try:
193            response.raise_for_status()
194            return response.json()
195        except requests.HTTPError as http_error:
196            error = self._handle_response(response)
197            if error:
198                raise error from http_error
199            _LOGGER.exception("%s", response.text)
200            raise
201
202    def _handle_response(self, response: requests.Response) -> DexcomError | None:  # noqa: C901
203        error: DexcomError | None = None
204        """
205        Parse `requests.Response` for `pydexcom.errors.DexcomError`.
206
207        :param response: `requests.Response` to parse
208        """
209        if response.json():
210            _LOGGER.debug("%s", response.json())
211            code = response.json().get("Code", None)
212            message = response.json().get("Message", None)
213            if code == "SessionIdNotFound":
214                error = SessionError(SessionErrorEnum.NOT_FOUND)
215            elif code == "SessionNotValid":
216                error = SessionError(SessionErrorEnum.INVALID)
217            elif code == "AccountPasswordInvalid":  # defunct
218                error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION)
219            elif code == "SSO_AuthenticateMaxAttemptsExceeded":
220                error = AccountError(AccountErrorEnum.MAX_ATTEMPTS)
221            elif code == "SSO_InternalError":
222                if message and (
223                    "Cannot Authenticate by AccountName" in message
224                    or "Cannot Authenticate by AccountId" in message
225                ):
226                    error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION)
227            elif code == "InvalidArgument":
228                if message and "accountName" in message:
229                    error = ArgumentError(ArgumentErrorEnum.USERNAME_INVALID)
230                elif message and "password" in message:
231                    error = ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID)
232                elif message and "UUID" in message:
233                    error = ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID)
234            elif code and message:
235                _LOGGER.debug("%s: %s", code, message)
236        return error
237
238    def _validate_session_id(self) -> None:
239        """Validate session ID."""
240        if any(
241            [
242                not isinstance(self._session_id, str),
243                not self._session_id,
244                not valid_uuid(self._session_id),
245            ],
246        ):
247            raise ArgumentError(ArgumentErrorEnum.SESSION_ID_INVALID)
248        if self._session_id == DEFAULT_UUID:
249            raise ArgumentError(ArgumentErrorEnum.SESSION_ID_DEFAULT)
250
251    def _validate_username(self) -> None:
252        """Validate username."""
253        if any([not isinstance(self._username, str), not self._username]):
254            raise ArgumentError(ArgumentErrorEnum.USERNAME_INVALID)
255
256    def _validate_password(self) -> None:
257        """Validate password."""
258        if any([not isinstance(self._password, str), not self._password]):
259            raise ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID)
260
261    def _validate_account_id(self) -> None:
262        """Validate account ID."""
263        if any(
264            [
265                not isinstance(self._account_id, str),
266                not self._account_id,
267                not valid_uuid(self._account_id),
268            ],
269        ):
270            raise ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID)
271        if self._account_id == DEFAULT_UUID:
272            raise ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_DEFAULT)
273
274    def _get_account_id(self) -> str:
275        """Retrieve account ID from the authentication endpoint.
276
277        See `pydexcom.const.DEXCOM_AUTHENTICATE_ENDPOINT`.
278        """
279        _LOGGER.debug("Retrieve account ID from the authentication endpoint")
280        return self._post(
281            DEXCOM_AUTHENTICATE_ENDPOINT,
282            json={
283                "accountName": self._username,
284                "password": self._password,
285                "applicationId": self._application_id,
286            },
287        )
288
289    def _get_session_id(self) -> str:
290        """Retrieve session ID from the login endpoint.
291
292        See `pydexcom.const.DEXCOM_LOGIN_ID_ENDPOINT`.
293        """
294        _LOGGER.debug("Retrieve session ID from the login endpoint")
295        return self._post(
296            DEXCOM_LOGIN_ID_ENDPOINT,
297            json={
298                "accountId": self._account_id,
299                "password": self._password,
300                "applicationId": self._application_id,
301            },
302        )
303
304    def _session(self) -> None:
305        """Create Dexcom Share API session."""
306        self._validate_password()
307
308        if self._account_id is None:
309            self._validate_username()
310            self._account_id = self._get_account_id()
311
312        self._validate_account_id()
313        self._session_id = self._get_session_id()
314        self._validate_session_id()
315
316    def _get_glucose_readings(
317        self,
318        minutes: int = MAX_MINUTES,
319        max_count: int = MAX_MAX_COUNT,
320    ) -> list[dict[str, Any]]:
321        """Retrieve glucose readings from the glucose readings endpoint.
322
323        See `pydexcom.const.DEXCOM_GLUCOSE_READINGS_ENDPOINT`.
324        """
325        if not isinstance(minutes, int) or any([minutes < 0, minutes > MAX_MINUTES]):
326            raise ArgumentError(ArgumentErrorEnum.MINUTES_INVALID)
327        if not isinstance(max_count, int) or any(
328            [max_count < 0, max_count > MAX_MAX_COUNT],
329        ):
330            raise ArgumentError(ArgumentErrorEnum.MAX_COUNT_INVALID)
331
332        _LOGGER.debug("Retrieve glucose readings from the glucose readings endpoint")
333        return self._post(
334            DEXCOM_GLUCOSE_READINGS_ENDPOINT,
335            params={
336                "sessionId": self._session_id,
337                "minutes": minutes,
338                "maxCount": max_count,
339            },
340        )
341
342    def get_glucose_readings(
343        self,
344        minutes: int = MAX_MINUTES,
345        max_count: int = MAX_MAX_COUNT,
346    ) -> list[GlucoseReading]:
347        """Get `max_count` glucose readings within specified number of `minutes`.
348
349        Catches one instance of a thrown `pydexcom.errors.SessionError` if session ID
350        expired, attempts to get a new session ID and retries.
351
352        :param minutes: Number of minutes to retrieve glucose readings from (1-1440)
353        :param max_count: Maximum number of glucose readings to retrieve (1-288)
354        """
355        json_glucose_readings: list[dict[str, Any]] = []
356
357        try:
358            # Requesting glucose reading with DEFAULT_UUID returns non-JSON empty string
359            self._validate_session_id()
360
361            json_glucose_readings = self._get_glucose_readings(minutes, max_count)
362        except SessionError:
363            # Attempt to update expired session ID
364            self._session()
365
366            json_glucose_readings = self._get_glucose_readings(minutes, max_count)
367
368        return [GlucoseReading(json_reading) for json_reading in json_glucose_readings]
369
370    def get_latest_glucose_reading(self) -> GlucoseReading | None:
371        """Get latest available glucose reading, within the last 24 hours."""
372        glucose_readings = self.get_glucose_readings(max_count=1)
373        return glucose_readings[0] if glucose_readings else None
374
375    def get_current_glucose_reading(self) -> GlucoseReading | None:
376        """Get current available glucose reading, within the last 10 minutes."""
377        glucose_readings = self.get_glucose_readings(minutes=10, max_count=1)
378        return glucose_readings[0] if glucose_readings else None

Class for communicating with Dexcom Share API.

Dexcom( *, password: str, account_id: str | None = None, username: str | None = None, region: pydexcom.const.Region = <Region.US: 'us'>)
143    def __init__(
144        self,
145        *,
146        password: str,
147        account_id: str | None = None,
148        username: str | None = None,
149        region: Region = Region.US,
150    ) -> None:
151        """Initialize `Dexcom` with Dexcom Share credentials.
152
153        :param username: username for the Dexcom Share user, *not follower*.
154        :param account_id: account ID for the Dexcom Share user, *not follower*.
155        :param password: password for the Dexcom Share user.
156        :param region: the region to use, one of `"us"`, `"ous"`, `"jp"`.
157        """
158        user_ids = sum(user_id is not None for user_id in [account_id, username])
159        if user_ids == 0:
160            raise ArgumentError(ArgumentErrorEnum.NONE_USER_ID_PROVIDED)
161        if user_ids != 1:
162            raise ArgumentError(ArgumentErrorEnum.TOO_MANY_USER_ID_PROVIDED)
163
164        self._base_url = DEXCOM_BASE_URLS[region]
165        self._application_id = DEXCOM_APPLICATION_IDS[region]
166        self._password = password
167        self._username: str | None = username
168        self._account_id: str | None = account_id
169        self._session_id: str | None = None
170        self.__session = requests.Session()
171        self._session()

Initialize Dexcom with Dexcom Share credentials.

Parameters
  • username: username for the Dexcom Share user, not follower.
  • account_id: account ID for the Dexcom Share user, not follower.
  • password: password for the Dexcom Share user.
  • region: the region to use, one of "us", "ous", "jp".
def get_glucose_readings( self, minutes: int = 1440, max_count: int = 288) -> list[GlucoseReading]:
342    def get_glucose_readings(
343        self,
344        minutes: int = MAX_MINUTES,
345        max_count: int = MAX_MAX_COUNT,
346    ) -> list[GlucoseReading]:
347        """Get `max_count` glucose readings within specified number of `minutes`.
348
349        Catches one instance of a thrown `pydexcom.errors.SessionError` if session ID
350        expired, attempts to get a new session ID and retries.
351
352        :param minutes: Number of minutes to retrieve glucose readings from (1-1440)
353        :param max_count: Maximum number of glucose readings to retrieve (1-288)
354        """
355        json_glucose_readings: list[dict[str, Any]] = []
356
357        try:
358            # Requesting glucose reading with DEFAULT_UUID returns non-JSON empty string
359            self._validate_session_id()
360
361            json_glucose_readings = self._get_glucose_readings(minutes, max_count)
362        except SessionError:
363            # Attempt to update expired session ID
364            self._session()
365
366            json_glucose_readings = self._get_glucose_readings(minutes, max_count)
367
368        return [GlucoseReading(json_reading) for json_reading in json_glucose_readings]

Get max_count glucose readings within specified number of minutes.

Catches one instance of a thrown pydexcom.errors.SessionError if session ID expired, attempts to get a new session ID and retries.

Parameters
  • minutes: Number of minutes to retrieve glucose readings from (1-1440)
  • max_count: Maximum number of glucose readings to retrieve (1-288)
def get_latest_glucose_reading(self) -> GlucoseReading | None:
370    def get_latest_glucose_reading(self) -> GlucoseReading | None:
371        """Get latest available glucose reading, within the last 24 hours."""
372        glucose_readings = self.get_glucose_readings(max_count=1)
373        return glucose_readings[0] if glucose_readings else None

Get latest available glucose reading, within the last 24 hours.

def get_current_glucose_reading(self) -> GlucoseReading | None:
375    def get_current_glucose_reading(self) -> GlucoseReading | None:
376        """Get current available glucose reading, within the last 10 minutes."""
377        glucose_readings = self.get_glucose_readings(minutes=10, max_count=1)
378        return glucose_readings[0] if glucose_readings else None

Get current available glucose reading, within the last 10 minutes.