├── .gitignore ├── .travis.yml ├── README.md ├── nightscout ├── .gitignore ├── __init__.py ├── api.py ├── models.py └── schedule_test.py ├── requirements.txt ├── setup.py └── tests └── test_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | __pycache__/ 3 | *.pyc 4 | python_nightscout.egg-info 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies 5 | install: 6 | - pip install . 7 | - pip install -r requirements.txt 8 | # command to run tests 9 | script: py.test 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Nightscout client 2 | 3 | [![Build Status](https://travis-ci.org/ps2/python-nightscout.svg?branch=master)](https://travis-ci.org/ps2/python-nightscout) 4 | 5 | A simple python client for accessing data stored in [Nightscout](https://github.com/nightscout/cgm-remote-monitor) 6 | 7 | ## Install 8 | 9 | Clone this repo, and inside the main directory, run: 10 | 11 | ``` 12 | pip install . 13 | ``` 14 | 15 | 16 | ## Example Usage 17 | 18 | To create an instance of the nightscout.Api class, with no authentication: 19 | 20 | import nightscout 21 | api = nightscout.Api('https://yournightscoutsite.herokuapp.com/') 22 | 23 | To use authentication, instantiate the nightscout.Api class with your 24 | api secret: 25 | 26 | api = nightscout.Api('https://yournightscoutsite.herokuapp.com/', api_secret='your api secret') 27 | 28 | ### Glucose Values 29 | To fetch recent sensor glucose values (SGVs): 30 | 31 | entries = api.get_sgvs() 32 | print([entry.sgv for entry in entries]) 33 | 34 | Specify time ranges: 35 | 36 | api.get_sgvs({'count':0, 'find[dateString][$gte]': '2017-03-07T01:10:26.000Z'}) 37 | 38 | ### Treatments 39 | To fetch recent treatments (boluses, temp basals): 40 | 41 | treatments = api.get_treatments() 42 | print([treatment.eventType for treatment in treatments]) 43 | 44 | ### Profiles 45 | 46 | profile_definition_set = api.get_profiles() 47 | 48 | profile_definition = profile_definition_set.get_profile_definition_active_at(datetime.now()) 49 | 50 | profile = profile_definition.get_default_profile() 51 | 52 | print "Duration of insulin action = %d" % profile.dia 53 | 54 | five_thirty_pm = datetime(2017, 3, 24, 17, 30) 55 | five_thirty_pm = profile.timezone.localize(five_thirty_pm) 56 | print "Scheduled basal rate at 5:30pm is = %f" % profile.basal.value_at_date(five_thirty_pm) 57 | -------------------------------------------------------------------------------- /nightscout/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /nightscout/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .models import ( 3 | SGV, 4 | Treatment, 5 | ProfileDefinition, 6 | ProfileDefinitionSet, 7 | ) 8 | 9 | from .api import Api 10 | -------------------------------------------------------------------------------- /nightscout/api.py: -------------------------------------------------------------------------------- 1 | """A library that provides a Python interface to Nightscout""" 2 | import requests 3 | import hashlib 4 | from nightscout import ( 5 | SGV, 6 | Treatment, 7 | ProfileDefinition, 8 | ProfileDefinitionSet, 9 | ) 10 | 11 | class Api(object): 12 | """A python interface into Nightscout 13 | 14 | Example usage: 15 | To create an instance of the nightscout.Api class, with no authentication: 16 | >>> import nightscout 17 | >>> api = nightscout.Api('https://yournightscoutsite.herokuapp.com/') 18 | To use authentication, instantiate the nightscout.Api class with your 19 | api secret: 20 | >>> api = nightscout.Api('https://yournightscoutsite.herokuapp.com/', api_secret='your api secret') 21 | To fetch recent sensor glucose values (SGVs): 22 | >>> entries = api.get_sgvs() 23 | >>> print([entry.sgv for entry in entries]) 24 | """ 25 | 26 | def __init__(self, site_url, api_secret=None): 27 | """Instantiate a new Api object.""" 28 | self.site_url = site_url 29 | self.api_secret = api_secret 30 | 31 | def request_headers(self): 32 | headers = { 33 | 'Content-Type': 'application/json', 34 | 'Accept': 'application/json' 35 | } 36 | if self.api_secret: 37 | headers['api-secret'] = hashlib.sha1(self.api_secret.encode('utf-8')).hexdigest() 38 | return headers 39 | 40 | def get_sgvs(self, params={}): 41 | """Fetch sensor glucose values 42 | Args: 43 | params: 44 | Mongodb style query params. For example, you can do things like: 45 | get_sgvs({'count':0, 'find[dateString][$gte]': '2017-03-07T01:10:26.000Z'}) 46 | Returns: 47 | A list of SGV objects 48 | """ 49 | r = requests.get(self.site_url + 'api/v1/entries/sgv.json', headers=self.request_headers(), params=params) 50 | return [SGV.new_from_json_dict(x) for x in r.json()] 51 | 52 | def get_treatments(self, params={}): 53 | """Fetch treatments 54 | Args: 55 | params: 56 | Mongodb style query params. For example, you can do things like: 57 | get_treatments({'count':0, 'find[timestamp][$gte]': '2017-03-07T01:10:26.000Z'}) 58 | Returns: 59 | A list of Treatments 60 | """ 61 | r = requests.get(self.site_url + 'api/v1/treatments.json', headers=self.request_headers(), params=params) 62 | if len(r.content) > 0: 63 | return [Treatment.new_from_json_dict(x) for x in r.json()] 64 | else: 65 | return [] 66 | 67 | def get_profiles(self, params={}): 68 | """Fetch profiles 69 | Args: 70 | params: 71 | Mongodb style query params. For example, you can do things like: 72 | get_profiles({'count':0, 'find[startDate][$gte]': '2017-03-07T01:10:26.000Z'}) 73 | Returns: 74 | ProfileDefinitionSet 75 | """ 76 | r = requests.get(self.site_url + 'api/v1/profile.json', headers=self.request_headers(), params=params) 77 | return ProfileDefinitionSet.new_from_json_array(r.json()) 78 | -------------------------------------------------------------------------------- /nightscout/models.py: -------------------------------------------------------------------------------- 1 | import dateutil.parser 2 | from datetime import datetime, timedelta 3 | import pytz 4 | 5 | class BaseModel(object): 6 | def __init__(self, **kwargs): 7 | self.param_defaults = {} 8 | 9 | @classmethod 10 | def json_transforms(cls, json_data): 11 | pass 12 | 13 | @classmethod 14 | def new_from_json_dict(cls, data, **kwargs): 15 | json_data = data.copy() 16 | if kwargs: 17 | for key, val in kwargs.items(): 18 | json_data[key] = val 19 | 20 | cls.json_transforms(json_data) 21 | 22 | c = cls(**json_data) 23 | c._json = data 24 | return c 25 | 26 | 27 | class SGV(BaseModel): 28 | """Sensor Glucose Value 29 | 30 | Represents a single glucose measurement and direction at a specific time. 31 | 32 | Attributes: 33 | sgv (int): Glucose measurement value in mg/dl. 34 | date (datetime): The time of the measurement 35 | direction (string): One of ['DoubleUp', 'SingleUp', 'FortyFiveUp', 'Flat', 'FortyFiveDown', 'SingleDown', 'DoubleDown'] 36 | device (string): the source of the measurement. For example, 'share2', if pulled from Dexcom Share servers 37 | """ 38 | def __init__(self, **kwargs): 39 | self.param_defaults = { 40 | 'sgv': None, 41 | 'date': None, 42 | 'direction': None, 43 | 'device': None, 44 | 'trend': None, 45 | 'trendRate': None, 46 | 'direction': None, 47 | 'isCalibration': None 48 | } 49 | 50 | for (param, default) in self.param_defaults.items(): 51 | setattr(self, param, kwargs.get(param, default)) 52 | 53 | @classmethod 54 | def json_transforms(cls, json_data): 55 | if json_data.get('dateString'): 56 | json_data['date'] = dateutil.parser.parse(json_data['dateString']) 57 | 58 | def to_dict(self): 59 | return { 60 | 'sgv': self.sgv, 61 | 'date': self.date, 62 | 'direction': self.direction, 63 | 'device': self.device, 64 | 'trend': self.trend, 65 | 'trendRate': self.trendRate, 66 | 'direction': self.direction, 67 | 'isCalibration': self.isCalibration 68 | } 69 | 70 | 71 | class Treatment(BaseModel): 72 | """Nightscout Treatment 73 | 74 | Represents an entry in the Nightscout treatments store, such as boluses, carb entries, 75 | temp basals, etc. Many of the following attributes will be set to None, depending on 76 | the type of entry. 77 | 78 | Attributes: 79 | eventType (string): The event type. Examples: ['Temp Basal', 'Correction Bolus', 'Meal Bolus', 'BG Check'] 80 | timestamp (datetime): The time of the treatment 81 | insulin (float): The amount of insulin delivered 82 | programmed (float): The amount of insulin programmed. May differ from insulin if the pump was suspended before delivery was finished. 83 | carbs (int): Amount of carbohydrates in grams consumed 84 | rate (float): Rate of insulin delivery for a temp basal, in U/hr. 85 | duration (int): Duration in minutes for a temp basal. 86 | enteredBy (string): The person who gave the treatment if entered in Care Portal, or the device that fetched the treatment from the pump. 87 | glucose (int): Glucose value for a BG check, in mg/dl. 88 | """ 89 | def __init__(self, **kwargs): 90 | self.param_defaults = { 91 | 'temp': None, 92 | 'enteredBy': None, 93 | 'eventType': None, 94 | 'glucose': None, 95 | 'glucoseType': None, 96 | 'units': None, 97 | 'device': None, 98 | 'created_at': None, 99 | 'timestamp': None, 100 | 'absolute': None, 101 | 'rate': None, 102 | 'duration': None, 103 | 'carbs': None, 104 | 'insulin': None, 105 | 'unabsorbed': None, 106 | 'suspended': None, 107 | 'type': None, 108 | 'programmed': None, 109 | 'foodType': None, 110 | 'absorptionTime': None, 111 | } 112 | 113 | for (param, default) in self.param_defaults.items(): 114 | setattr(self, param, kwargs.get(param, default)) 115 | 116 | def __repr__(self): 117 | return "%s %s" % (self.timestamp, self.eventType) 118 | 119 | @classmethod 120 | def json_transforms(cls, json_data): 121 | timestamp = json_data.get('timestamp') 122 | if timestamp: 123 | if type(timestamp) == int: 124 | json_data['timestamp'] = datetime.fromtimestamp(timestamp / 1000.0, pytz.utc) 125 | else: 126 | json_data['timestamp'] = dateutil.parser.parse(timestamp) 127 | if json_data.get('created_at'): 128 | json_data['created_at'] = dateutil.parser.parse(json_data['created_at']) 129 | 130 | 131 | class ScheduleEntry(BaseModel): 132 | """ScheduleEntry 133 | 134 | Represents a change point in one of the schedules on a Nightscout profile. 135 | 136 | Attributes: 137 | offset (timedelta): The start offset of the entry 138 | value (float): The value of the entry. 139 | """ 140 | def __init__(self, offset, value): 141 | self.offset = offset 142 | self.value = value 143 | 144 | @classmethod 145 | def new_from_json_dict(cls, data): 146 | seconds_offset = data.get('timeAsSeconds') 147 | if seconds_offset == None: 148 | hours, minutes = data.get('time').split(":") 149 | seconds_offset = int(hours) * 60 * 60 + int(minutes) * 60 150 | offset_in_seconds = int(seconds_offset) 151 | return cls(timedelta(seconds=offset_in_seconds), float(data['value'])) 152 | 153 | class AbsoluteScheduleEntry(BaseModel): 154 | def __init__(self, start_date, value): 155 | self.start_date = start_date 156 | self.value = value 157 | 158 | def __repr__(self): 159 | return "%s = %s" % (self.start_date, self.value) 160 | 161 | class Schedule(object): 162 | """Schedule 163 | 164 | Represents a schedule on a Nightscout profile. 165 | 166 | """ 167 | def __init__(self, entries, timezone): 168 | self.entries = entries 169 | self.entries.sort(key=lambda e: e.offset) 170 | self.timezone = timezone 171 | 172 | # Expects a localized timestamp here 173 | def value_at_date(self, local_date): 174 | """Get scheduled value at given date 175 | 176 | Args: 177 | local_date: The datetime of interest. 178 | 179 | Returns: 180 | The value of the schedule at the given time. 181 | 182 | """ 183 | offset = (local_date - local_date.replace(hour=0, minute=0, second=0, microsecond=0)) 184 | return [e.value for e in self.entries if e.offset <= offset][-1] 185 | 186 | def between(self, start_date, end_date): 187 | """Returns entries between given dates as AbsoluteScheduleEntry objects 188 | 189 | Times passed in should be timezone aware. Times returned will have a tzinfo 190 | matching the schedule timezone. 191 | 192 | Args: 193 | start_date: The start datetime of the period to retrieve entries for. 194 | end_date: The end datetime of the period to retrieve entries for. 195 | 196 | Returns: 197 | An array of AbsoluteScheduleEntry objects. 198 | 199 | """ 200 | if start_date > end_date: 201 | return [] 202 | 203 | start_date = start_date.astimezone(self.timezone) 204 | end_date = end_date.astimezone(self.timezone) 205 | 206 | reference_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) 207 | start_offset = (start_date - reference_date) 208 | end_offset = start_offset + (end_date - start_date) 209 | if end_offset > timedelta(days=1): 210 | boundary_date = start_date + (timedelta(days=1) - start_offset) 211 | return self.between(start_date, boundary_date) + self.between(boundary_date, end_date) 212 | 213 | start_index = 0 214 | end_index = len(self.entries) 215 | 216 | for index, item in enumerate(self.entries): 217 | if start_offset >= item.offset: 218 | start_index = index 219 | if end_offset < item.offset: 220 | end_index = index 221 | break 222 | 223 | return [AbsoluteScheduleEntry(reference_date + entry.offset, entry.value) for entry in self.entries[start_index:end_index]] 224 | 225 | @classmethod 226 | def new_from_json_array(cls, data, timezone): 227 | entries = [ScheduleEntry.new_from_json_dict(d) for d in data] 228 | return cls(entries, timezone) 229 | 230 | 231 | class Profile(BaseModel): 232 | """Profile 233 | 234 | Represents a Nightscout profile. 235 | 236 | Attributes: 237 | dia (float): The duration of insulin action, in hours. 238 | carb_ratio (Schedule): A schedule of carb ratios, which are in grams/U. 239 | sens (Schedule): A schedule of insulin sensitivity values, which are in mg/dl/U. 240 | timezone (timezone): The timezone of the schedule. 241 | basal (Schedule): A schedule of basal rates, which are in U/hr. 242 | target_low (Schedule): A schedule the low end of the target range, in mg/dl. 243 | target_high (Schedule): A schedule the high end of the target range, in mg/dl. 244 | """ 245 | def __init__(self, **kwargs): 246 | self.param_defaults = { 247 | 'dia': None, 248 | 'carb_ratio': None, 249 | 'carbs_hr': None, 250 | 'delay': None, 251 | 'sens': None, 252 | 'timezone': None, 253 | 'basal': None, 254 | 'target_low': None, 255 | 'target_high': None, 256 | } 257 | 258 | for (param, default) in self.param_defaults.items(): 259 | setattr(self, param, kwargs.get(param, default)) 260 | 261 | @classmethod 262 | def json_transforms(cls, json_data): 263 | timezone = None 264 | if json_data.get('timezone'): 265 | timezone = pytz.timezone(json_data.get('timezone')) 266 | json_data['timezone'] = timezone 267 | if json_data.get('carbratio'): 268 | json_data['carbratio'] = Schedule.new_from_json_array(json_data.get('carbratio'), timezone) 269 | if json_data.get('sens'): 270 | json_data['sens'] = Schedule.new_from_json_array(json_data.get('sens'), timezone) 271 | if json_data.get('target_low'): 272 | json_data['target_low'] = Schedule.new_from_json_array(json_data.get('target_low'), timezone) 273 | if json_data.get('target_high'): 274 | json_data['target_high'] = Schedule.new_from_json_array(json_data.get('target_high'), timezone) 275 | if json_data.get('basal'): 276 | json_data['basal'] = Schedule.new_from_json_array(json_data.get('basal'), timezone) 277 | if json_data.get('dia'): 278 | json_data['dia'] = float(json_data['dia']) 279 | 280 | class ProfileDefinition(BaseModel): 281 | """ProfileDefinition 282 | 283 | Represents a Nightscout profile definition, which can have multiple named profiles. 284 | 285 | Attributes: 286 | startDate (datetime): The time these profiles start at. 287 | """ 288 | def __init__(self, **kwargs): 289 | self.param_defaults = { 290 | 'defaultProfile': None, 291 | 'store': None, 292 | 'startDate': None, 293 | 'created_at': None, 294 | } 295 | 296 | for (param, default) in self.param_defaults.items(): 297 | setattr(self, param, kwargs.get(param, default)) 298 | 299 | def get_default_profile(self): 300 | return self.store[self.defaultProfile] 301 | 302 | @classmethod 303 | def json_transforms(cls, json_data): 304 | if json_data.get('startDate'): 305 | json_data['startDate'] = dateutil.parser.parse(json_data['startDate']) 306 | if json_data.get('created_at'): 307 | json_data['created_at'] = dateutil.parser.parse(json_data['created_at']) 308 | if json_data.get('store'): 309 | store = {} 310 | for profile_name in json_data['store']: 311 | store[profile_name] = Profile.new_from_json_dict(json_data['store'][profile_name]) 312 | json_data['store'] = store 313 | 314 | class ProfileDefinitionSet(object): 315 | """ProfileDefinitionSet 316 | 317 | Represents a set of Nightscout profile definitions, each covering a range of time 318 | from its start time, to the start time of the next profile definition, or until 319 | now if there are no newer profile defitions. 320 | 321 | """ 322 | def __init__(self, profile_definitions): 323 | self.profile_definitions = profile_definitions 324 | self.profile_definitions.sort(key=lambda d: d.startDate) 325 | 326 | def get_profile_definition_active_at(self, date): 327 | """Get the profile definition active at a given datetime 328 | 329 | Args: 330 | date: The profile definition containing this time will be returned. 331 | 332 | Returns: 333 | A ProfileDefinition object valid for the specified time. 334 | 335 | """ 336 | return [d for d in self.profile_definitions if d.startDate <= date][-1] 337 | 338 | @classmethod 339 | def new_from_json_array(cls, data): 340 | defs = [ProfileDefinition.new_from_json_dict(d) for d in data] 341 | return cls(defs) 342 | -------------------------------------------------------------------------------- /nightscout/schedule_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from models import * 3 | from datetime import datetime, timedelta 4 | import pytz 5 | from dateutil import tz 6 | 7 | class ScheduleTestCase(unittest.TestCase): 8 | def test_schedule_conversion_to_absolute_time(self): 9 | # Schedule should be fixed offset, to match the pump's date math 10 | schedule_tz = tz.tzoffset(None, -(5*60*60)) # UTC-5 11 | schedule = Schedule([ 12 | ScheduleEntry(timedelta(hours=0), 1), 13 | ScheduleEntry(timedelta(hours=6), 0.7), 14 | ScheduleEntry(timedelta(hours=12), 0.8), 15 | ScheduleEntry(timedelta(hours=22), 0.9), 16 | ], schedule_tz) 17 | # Queries against the schedule are typically in utc 18 | items = schedule.between(datetime(2017,7,7,20,tzinfo=pytz.utc), datetime(2017,7,8,6,tzinfo=pytz.utc)) 19 | 20 | expected = [ 21 | AbsoluteScheduleEntry(datetime(2017,7,7,12,tzinfo=schedule_tz), 0.8), 22 | AbsoluteScheduleEntry(datetime(2017,7,7,22,tzinfo=schedule_tz), 0.9), 23 | AbsoluteScheduleEntry(datetime(2017,7,8,0,tzinfo=schedule_tz), 1), 24 | ] 25 | 26 | self.assertEqual(len(items), len(expected)) 27 | 28 | for item, expected_item in zip(items, expected): 29 | self.assertEqual(item.start_date, expected_item.start_date) 30 | self.assertEqual(item.value, expected_item.value) 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | python-dateutil 3 | coverage 4 | httmock 5 | pytest 6 | pytz 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Python Nightscout api client 2 | 3 | See: 4 | https://github.com/ps2/python-nightscout 5 | https://github.com/nightscout/cgm-remote-monitor 6 | """ 7 | 8 | # Always prefer setuptools over distutils 9 | from setuptools import setup, find_packages 10 | # To use a consistent encoding 11 | from codecs import open 12 | from os import path 13 | try: 14 | from pypandoc import convert 15 | read_md = lambda f: convert(f, 'rst') 16 | except ImportError: 17 | print("warning: pypandoc module not found, could not convert Markdown to RST") 18 | read_md = lambda f: open(f, 'r').read() 19 | 20 | here = path.abspath(path.dirname(__file__)) 21 | 22 | # Get the long description from the README file 23 | long_description=read_md('README.md') 24 | 25 | setup( 26 | name='python-nightscout', 27 | 28 | # Versions should comply with PEP440. For a discussion on single-sourcing 29 | # the version across setup.py and the project code, see 30 | # https://packaging.python.org/en/latest/single_source_version.html 31 | version='1.0.0', 32 | 33 | description='A library that provides a Python interface to Nightscout', 34 | long_description=long_description, 35 | 36 | # The project's main homepage. 37 | url='https://github.com/ps2/python-nightscout', 38 | 39 | # Author details 40 | author='Pete Schwamb', 41 | author_email='pete@schwamb.net', 42 | 43 | # Choose your license 44 | license='MIT', 45 | 46 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 47 | classifiers=[ 48 | # How mature is this project? Common values are 49 | # 3 - Alpha 50 | # 4 - Beta 51 | # 5 - Production/Stable 52 | 'Development Status :: 3 - Alpha', 53 | 54 | # Indicate who your project is intended for 55 | 'Intended Audience :: Developers', 56 | 'Topic :: Software Development :: API Client', 57 | 58 | # Pick your license as you wish (should match "license" above) 59 | 'License :: OSI Approved :: MIT License', 60 | 61 | # Specify the Python versions you support here. In particular, ensure 62 | # that you indicate whether you support Python 2, Python 3 or both. 63 | 'Programming Language :: Python :: 2.7', 64 | ], 65 | 66 | # What does your project relate to? 67 | keywords='nightscout api client development', 68 | 69 | # You can just specify the packages manually here if your project is 70 | # simple. Or you can use find_packages(). 71 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 72 | 73 | # Alternatively, if you want to distribute just a my_module.py, uncomment 74 | # this: 75 | # py_modules=["my_module"], 76 | 77 | # List run-time dependencies here. These will be installed by pip when 78 | # your project is installed. For an analysis of "install_requires" vs pip's 79 | # requirements files see: 80 | # https://packaging.python.org/en/latest/requirements.html 81 | install_requires=['requests', 'python-dateutil', 'pytz'], 82 | 83 | # List additional groups of dependencies here (e.g. development 84 | # dependencies). You can install these using the following syntax, 85 | # for example: 86 | # $ pip install -e .[dev,test] 87 | extras_require={ 88 | 'dev': ['check-manifest'], 89 | 'test': ['coverage', 'httmock'], 90 | }, 91 | 92 | # If there are data files included in your packages that need to be 93 | # installed, specify them here. If using Python 2.6 or less, then these 94 | # have to be included in MANIFEST.in as well. 95 | # package_data={ 96 | # 'sample': ['package_data.dat'], 97 | # }, 98 | 99 | # Although 'package_data' is the preferred approach, in some case you may 100 | # need to place data files outside of your packages. See: 101 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 102 | # In this case, 'data_file' will be installed into '/my_data' 103 | # data_files=[('my_data', ['data/data_file'])], 104 | 105 | # To provide executable scripts, use entry points in preference to the 106 | # "scripts" keyword. Entry points provide cross-platform support and allow 107 | # pip to create the appropriate form of executable for the target platform. 108 | # entry_points={ 109 | # 'console_scripts': [ 110 | # 'sample=sample:main', 111 | # ], 112 | # }, 113 | ) 114 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import nightscout 3 | from datetime import datetime 4 | from dateutil.tz import tzutc 5 | from httmock import all_requests, HTTMock 6 | import pytz 7 | 8 | @all_requests 9 | def sgv_response(url, request): 10 | return '[{"_id":"58be818183ab6d6632419687","sgv":184,"date":1488879925000,"dateString":"2017-03-07T09:45:25.000Z","trend":3,"direction":"FortyFiveUp","device":"share2","type":"sgv"},{"_id":"58be805583ab6d6632419684","sgv":168,"date":1488879625000,"dateString":"2017-03-07T09:40:25.000Z","trend":3,"direction":"FortyFiveUp","device":"share2","type":"sgv"},{"_id":"58be7f2983ab6d6632419681","sgv":169,"date":1488879325000,"dateString":"2017-03-07T09:35:25.000Z","trend":3,"direction":"FortyFiveUp","device":"share2","type":"sgv"}]' 11 | 12 | def treatments_response(url, request): 13 | return '[{"_id":"58be816483ab6d6632419686","temp":"absolute","enteredBy":"loop://Riley\'s iphone","eventType":"Temp Basal","created_at":"2017-03-07T09:38:35Z","timestamp":"2017-03-07T09:38:35Z","absolute":0.7,"rate":0.7,"duration":30,"carbs":null,"insulin":null},{"_id":"58be803d83ab6d6632419683","temp":"absolute","enteredBy":"loop://Riley\'s iphone","eventType":"Temp Basal","created_at":"2017-03-07T09:33:30Z","timestamp":"2017-03-07T09:33:30Z","absolute":1.675,"rate":1.675,"duration":30,"carbs":null,"insulin":null},{"_id":"58be7f0d83ab6d6632419680","temp":"absolute","enteredBy":"loop://Riley\'s iphone","eventType":"Temp Basal","created_at":"2017-03-07T09:28:30Z","timestamp":"2017-03-07T09:28:30Z","absolute":1.775,"rate":1.775,"duration":30,"carbs":null,"insulin":null}]' 14 | 15 | def profile_response(url, request): 16 | return '[{"_id":"58c0e02447d5af0c00e37593","defaultProfile":"Default","store":{"Default":{"dia":"4","carbratio":[{"time":"00:00","value":"20","timeAsSeconds":"0"},{"time":"06:00","value":"10","timeAsSeconds":"21600"},{"time":"11:00","value":"18","timeAsSeconds":"39600"},{"time":"16:00","value":"12","timeAsSeconds":"57600"},{"time":"21:00","value":"18","timeAsSeconds":"75600"}],"carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"90","timeAsSeconds":"0"},{"time":"06:00","value":"85","timeAsSeconds":"21600"},{"time":"09:00","value":"95","timeAsSeconds":"32400"}],"timezone":"US/Central","basal":[{"time":"00:00","value":"0.45","timeAsSeconds":"0"},{"time":"02:00","value":"0.3","timeAsSeconds":"7200"},{"time":"04:30","value":"0.45","timeAsSeconds":"16200"},{"time":"07:00","value":"0.6","timeAsSeconds":"25200"},{"time":"10:00","value":"0.4","timeAsSeconds":"36000"},{"time":"12:00","value":"0.4","timeAsSeconds":"43200"},{"time":"15:00","value":"0.4","timeAsSeconds":"54000"},{"time":"17:00","value":"0.4","timeAsSeconds":"61200"},{"time":"20:30","value":"0.4","timeAsSeconds":"73800"}],"target_low":[{"time":"00:00","value":"110","timeAsSeconds":"0"}],"target_high":[{"time":"00:00","value":"130","timeAsSeconds":"0"}],"startDate":"1970-01-01T00:00:00.000Z","units":"mg/dl"},"Test2":{"dia":"4","carbratio":[{"time":"00:00","value":"20","timeAsSeconds":"0"},{"time":"06:00","value":"10","timeAsSeconds":"21600"},{"time":"11:00","value":"18","timeAsSeconds":"39600"},{"time":"16:00","value":"12","timeAsSeconds":"57600"},{"time":"21:00","value":"18","timeAsSeconds":"75600"}],"carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"90","timeAsSeconds":"0"},{"time":"06:00","value":"85","timeAsSeconds":"21600"},{"time":"09:00","value":"95","timeAsSeconds":"32400"}],"timezone":"US/Central","basal":[{"time":"00:00","value":"0.45","timeAsSeconds":"0"},{"time":"02:00","value":"0.3","timeAsSeconds":"7200"},{"time":"04:30","value":"0.45","timeAsSeconds":"16200"},{"time":"07:00","value":"0.6","timeAsSeconds":"25200"},{"time":"10:00","value":"0.4","timeAsSeconds":"36000"},{"time":"12:00","value":"0.4","timeAsSeconds":"43200"},{"time":"15:00","value":"0.4","timeAsSeconds":"54000"},{"time":"17:00","value":"0.4","timeAsSeconds":"61200"},{"time":"20:30","value":"0.4","timeAsSeconds":"73800"}],"target_low":[{"time":"00:00","value":"110","timeAsSeconds":"0"}],"target_high":[{"time":"00:00","value":"130","timeAsSeconds":"0"}],"startDate":"1970-01-01T00:00:00.000Z","units":"mg/dl"}},"startDate":"2017-03-24T03:54:00.000Z","mills":"1489035240000","units":"mg/dl","created_at":"2016-10-31T12:58:43.800Z"},{"_id":"58b7777cdfb94b0c00366c7e","defaultProfile":"Default","store":{"Default":{"dia":"4","carbratio":[{"time":"00:00","value":"20","timeAsSeconds":"0"},{"time":"06:00","value":"10","timeAsSeconds":"21600"},{"time":"11:00","value":"18","timeAsSeconds":"39600"},{"time":"16:00","value":"12","timeAsSeconds":"57600"},{"time":"21:00","value":"18","timeAsSeconds":"75600"}],"carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"90","timeAsSeconds":"0"},{"time":"06:00","value":"85","timeAsSeconds":"21600"},{"time":"09:00","value":"95","timeAsSeconds":"32400"}],"timezone":"US/Central","basal":[{"time":"00:00","value":"0.45","timeAsSeconds":"0"},{"time":"02:00","value":"0.3","timeAsSeconds":"7200"},{"time":"04:30","value":"0.45","timeAsSeconds":"16200"},{"time":"07:00","value":"0.6","timeAsSeconds":"25200"},{"time":"10:00","value":"0.4","timeAsSeconds":"36000"},{"time":"12:00","value":"0.4","timeAsSeconds":"43200"},{"time":"15:00","value":"0.4","timeAsSeconds":"54000"},{"time":"17:00","value":"0.6","timeAsSeconds":"61200"},{"time":"20:30","value":"0.6","timeAsSeconds":"73800"}],"target_low":[{"time":"00:00","value":"110","timeAsSeconds":"0"}],"target_high":[{"time":"00:00","value":"130","timeAsSeconds":"0"}],"startDate":"1970-01-01T00:00:00.000Z","units":"mg/dl"}},"startDate":"2017-03-02T01:37:00.000Z","mills":"1488418620000","units":"mg/dl","created_at":"2016-10-31T12:58:43.800Z"},{"_id":"5719b2aa5c3e080b000dbfb1","defaultProfile":"Default","store":{"Default":{"dia":"4","carbratio":[{"time":"00:00","value":"18","timeAsSeconds":"0"},{"time":"06:00","value":"10","timeAsSeconds":"21600"},{"time":"11:00","value":"18","timeAsSeconds":"39600"},{"time":"16:00","value":"12","timeAsSeconds":"57600"},{"time":"21:00","value":"18","timeAsSeconds":"75600"}],"carbs_hr":"20","delay":"20","sens":[{"time":"00:00","value":"90","timeAsSeconds":"0"},{"time":"06:00","value":"85","timeAsSeconds":"21600"},{"time":"09:00","value":"95","timeAsSeconds":"32400"}],"timezone":"US/Central","basal":[{"time":"00:00","value":"0.45","timeAsSeconds":"0"},{"time":"02:00","value":"0.3","timeAsSeconds":"7200"},{"time":"04:30","value":"0.45","timeAsSeconds":"16200"},{"time":"07:00","value":"0.6","timeAsSeconds":"25200"},{"time":"10:00","value":"0.4","timeAsSeconds":"36000"},{"time":"12:00","value":"0.4","timeAsSeconds":"43200"},{"time":"15:00","value":"0.4","timeAsSeconds":"54000"},{"time":"17:00","value":"0.6","timeAsSeconds":"61200"},{"time":"20:30","value":"0.6","timeAsSeconds":"73800"}],"target_low":[{"time":"00:00","value":"110","timeAsSeconds":"0"}],"target_high":[{"time":"00:00","value":"130","timeAsSeconds":"0"}],"startDate":"1970-01-01T00:00:00.000Z","units":"mg/dl"}},"startDate":"2016-04-22T05:06:00.000Z","mills":"1461301560000","units":"mg/dl","created_at":"2016-10-31T12:58:43.800Z"}]' 17 | 18 | class TestAPI(unittest.TestCase): 19 | 20 | def setUp(self): 21 | self.api = nightscout.Api('http://testns.example.com/') 22 | 23 | def test_get_sgv(self): 24 | with HTTMock(sgv_response): 25 | entries = self.api.get_sgvs() 26 | 27 | self.assertEqual(3, len(entries)) 28 | self.assertEqual(184, entries[0].sgv) 29 | self.assertEqual("FortyFiveUp", entries[0].direction) 30 | self.assertEqual(datetime(2017, 3, 7, 9, 45, 25, tzinfo=tzutc()), entries[0].date) 31 | 32 | def test_get_treatments(self): 33 | with HTTMock(treatments_response): 34 | treatments = self.api.get_treatments() 35 | 36 | self.assertEqual(3, len(treatments)) 37 | self.assertEqual("absolute", treatments[0].temp) 38 | self.assertEqual("Temp Basal", treatments[0].eventType) 39 | timestamp = datetime(2017, 3, 7, 9, 38, 35, tzinfo=tzutc()) 40 | self.assertEqual(timestamp, treatments[0].timestamp) 41 | self.assertEqual(timestamp, treatments[0].created_at) 42 | 43 | def test_get_profile(self): 44 | with HTTMock(profile_response): 45 | profile_definition_set = self.api.get_profiles() 46 | 47 | profile_definition = profile_definition_set.get_profile_definition_active_at(datetime(2017, 3, 5, 0, 0, tzinfo=tzutc())) 48 | self.assertEqual(datetime(2017, 3, 2, 1, 37, tzinfo=tzutc()), profile_definition.startDate) 49 | 50 | profile = profile_definition.get_default_profile() 51 | self.assertEqual(pytz.timezone('US/Central'), profile.timezone) 52 | self.assertEqual(4, profile.dia) 53 | 54 | five_thirty_pm = datetime(2017, 3, 24, 17, 30) 55 | five_thirty_pm = profile.timezone.localize(five_thirty_pm) 56 | self.assertEqual(0.6, profile.basal.value_at_date(five_thirty_pm)) 57 | 58 | if __name__ == '__main__': 59 | unittest.main() 60 | --------------------------------------------------------------------------------