Skip to content

Commit

Permalink
Merge pull request #29 from loudnate/dev
Browse files Browse the repository at this point in the history
Next version (dev)
  • Loading branch information
loudnate committed Sep 20, 2015
2 parents 34583b0 + ad8195b commit 2e43202
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 22 deletions.
36 changes: 18 additions & 18 deletions README.md
Expand Up @@ -26,26 +26,26 @@ $ python setup.py develop
### Adding to your openaps project
```bash
$ openaps vendor add openapscontrib.mmhistorytools
$ openaps device add munge mmhistorytools
$ openaps device add history mmhistorytools
```

## Usage
Use the device help menu to see available commands.
```bash
$ openaps use munge -h
usage: openaps-use munge [-h] USAGE ...
$ openaps use history -h
usage: openaps-use history [-h] USAGE ...

optional arguments:
-h, --help show this help message and exit

## Device munge:
## Device history:
vendor openapscontrib.mmhistorytools

mmhistorytools - tools for cleaning, condensing, and reformatting history data





USAGE Usage Details
trim Trims a sequence of pump history to a specified time window
Expand All @@ -61,8 +61,8 @@ optional arguments:

Use the command help menu to see available arguments.
```bash
$ openaps use munge clean -h
usage: openaps-use munge clean [-h] [--start START] [--end END] [infile]
$ openaps use history clean -h
usage: openaps-use history clean [-h] [--start START] [--end END] [infile]

Resolve inconsistencies from a sequence of pump history

Expand All @@ -85,24 +85,24 @@ Tasks performed by this pass:

Add a report flow to process pump history for analysis:
```
$ openaps report add clean_history.json JSON munge clean pump_history.json
$ openaps report add reconciled_history.json JSON munge reconcile clean_history.json
$ openaps report add resolved_history.json JSON munge resolve reconciled_history.json
$ openaps report add normalized_history.json JSON munge normalize resolved_history.json --basal-profile basal.json --zero-at read_clock.json
$ openaps report add clean_history.json JSON history clean pump_history.json
$ openaps report add reconciled_history.json JSON history reconcile clean_history.json
$ openaps report add resolved_history.json JSON history resolve reconciled_history.json
$ openaps report add normalized_history.json JSON history normalize resolved_history.json --basal-profile basal.json --zero-at read_clock.json
```

All `infile` arguments default to accept stdin, so commands can be chained to simplify testing:
```bash
$ openaps use pump iter_pump_hours 4 | openaps use munge clean | openaps use munge reconcile | openaps use munge resolve | openaps use munge normalize --basal-profile basal.json --zero-at read_clock.json
$ openaps use pump iter_pump_hours 4 | openaps use history clean | openaps use history reconcile | openaps use history resolve | openaps use history normalize --basal-profile basal.json --zero-at read_clock.json
```

## Contributing
Contributions are welcome and encouraged in the form of bugs and pull requests.

### Testing

Unit tests can be run manually via setuptools. This is also handled by TravisCI after opening a pull request.

```bash
$ python setup.py test
```
96 changes: 95 additions & 1 deletion openapscontrib/mmhistorytools/__init__.py
Expand Up @@ -13,6 +13,7 @@

from historytools import TrimHistory, CleanHistory, ReconcileHistory
from historytools import ResolveHistory, NormalizeRecords
from historytools import AppendDoseToHistory


# set_config is needed by openaps for all vendors.
Expand All @@ -38,7 +39,7 @@ def display_device(device):
# agp as a vendor. Return a list of classes which inherit from Use,
# or are compatible with it:
def get_uses(device, config):
return [trim, clean, reconcile, resolve, normalize]
return [trim, clean, reconcile, resolve, normalize, append_dose, prepare]


def _opt_date(timestamp):
Expand Down Expand Up @@ -215,13 +216,17 @@ class resolve(BaseUse):
- `Bolus`: Insulin delivery events in Units, or Units/hour
- `Meal`: Grams of carbohydrate
- `TempBasal`: Paced insulin delivery events in Units/hour, or Percent of scheduled basal
- `Exercise`: Exercise event
_
The following history events are parsed:
- TempBasal and TempBasalDuration are combined into TempBasal records
- PumpSuspend and PumpResume are combined into TempBasal records of 0%
- Square Bolus is converted to a Bolus record
- Normal Bolus is converted to a Bolus record
- BolusWizard carb entry is converted to a Meal record
- JournalEntryMealMarker is converted to a Meal record
- JournalEntryExerciseMarker is converted to an Exercise record
_
Events that are not related to the record types or seem to have no effect are dropped.
"""
def main(self, args, app):
Expand All @@ -239,6 +244,7 @@ class normalize(BaseUse):
If `--basal-profile` is provided, the TempBasal `amount` is replaced with a relative dose in
Units/hour. A single TempBasal record might split into multiple records to account for boundary
crossings in the basal schedule.
_
If `--zero-at` is provided, the values for the `start_at` and `end_at` keys are replaced with signed
integers representing the number of minutes from `--zero-at`.
"""
Expand Down Expand Up @@ -291,3 +297,91 @@ def main(self, args, app):
tool = NormalizeRecords(*args, **kwargs)

return tool.normalized_records


# noinspection PyPep8Naming
class append_dose(BaseUse):
"""Appends a dose record to a sequence of cleaned history
The expected dose record format is a dictionary with a key named "recieved" (sic).
If that key isn't present, or its value is false, the record is ignored.
"""

def configure_app(self, app, parser):
super(append_dose, self).configure_app(app, parser)

parser.add_argument(
'--dose',
help='JSON-encoded dosing report'
)

def get_params(self, args):
params = super(append_dose, self).get_params(args)

params.update(dose=args.dose)

return params

def get_program(self, params):
args, kwargs = super(append_dose, self).get_program(params)

args.append(_opt_json_file(params['dose']))

return args, kwargs

def main(self, args, app):
args, _ = self.get_program(self.get_params(args))

tool = AppendDoseToHistory(*args)

return tool.appended_history


# noinspection PyPep8Naming
class prepare(BaseUse):
"""Runs a sequence of commands to prepare history for use in prediction and dosing.
This command performs the following commands in sequence:
[clean] -> [reconcile] -> [resolve] -> [normalize:basal-profile]
_
Please refer to the --help documentation of each command for more information.
_
Warning: This command will not return the same level of diagnostic logging as
running all four commands separately. If there is reason to believe an issue
has occurred, output from this command may not be sufficient for debugging.
"""

def configure_app(self, app, parser):
super(prepare, self).configure_app(app, parser)

parser.add_argument(
'--basal-profile',
default=None,
help='A file containing a basal profile by which to adjust TempBasal records'
)

def get_params(self, args):
params = super(prepare, self).get_params(args)
if 'basal_profile' in args and args.basal_profile:
params.update(basal_profile=args.basal_profile)

return params

def get_program(self, params):
args, kwargs = super(prepare, self).get_program(params)

kwargs.update(
basal_schedule=_opt_json_file(params.get('basal_profile'))
)

return args, kwargs

def main(self, args, app):
args, kwargs = self.get_program(self.get_params(args))

clean_history = CleanHistory(*args).clean_history
reconciled_history = ReconcileHistory(clean_history).reconciled_history
resolved_records = ResolveHistory(reconciled_history).resolved_records
normalized_records = NormalizeRecords(resolved_records, **kwargs).normalized_records

return normalized_records
59 changes: 58 additions & 1 deletion openapscontrib/mmhistorytools/historytools.py
Expand Up @@ -4,7 +4,7 @@
from datetime import timedelta
from dateutil import parser

from .models import Bolus, Meal, TempBasal, Unit
from .models import Bolus, Meal, TempBasal, Exercise, Unit


class ParseHistory(object):
Expand Down Expand Up @@ -254,6 +254,7 @@ class ResolveHistory(ParseHistory):
- `Bolus`: Insulin delivery events in Units, or Units/hour
- `Meal`: Grams of carbohydrate
- `TempBasal`: Paced insulin delivery events in Units/hour, or Percent of scheduled basal
- `Exercise`: Exercise event
The following history events are parsed:
Expand All @@ -263,6 +264,7 @@ class ResolveHistory(ParseHistory):
- Normal Bolus is converted to a Bolus record
- BolusWizard carb entry is converted to a Meal record
- JournalEntryMealMarker is converted to a Meal record
- JournalEntryExerciseMarker is converted to an Exercise record
Events that are not related to the record types or seem to have no effect are dropped.
"""
Expand Down Expand Up @@ -345,6 +347,18 @@ def _decode_journalentrymealmarker(self, event):
description='{}: {}g'.format(event["_type"], carb_input)
)

def _decode_journalentryexercisemarker(self, event):
num_events = 1
start_at = self._event_datetime(event)

return Exercise(
start_at=start_at,
end_at=start_at,
amount=num_events,
unit=Unit.event,
description=event["_type"]
)

def _decode_pumpresume(self, event):
self._resume_datetime = self._event_datetime(event)

Expand Down Expand Up @@ -569,3 +583,46 @@ def _decode_tempbasal(self, event):
)

return events


class AppendDoseToHistory(ParseHistory):
"""Append a dose record or records to a list of history records.
The expected dose record format is a dictionary with a key named "recieved" (sic).
If that key isn't present, or its value is false, the record is ignored.
"""
def __init__(self, clean_history, doses):
"""Initializes a new instance of the history parser
:param clean_history: A list of pump history events in reverse-chronological order
:type clean_history: list(dict)
:param doses: A single dose event, or a list of dose events in chronological order
:type doses: list(dict)|dict
"""
self.appended_history = clean_history

if isinstance(doses, dict):
doses = [doses]

for event in doses:
if event.get('recieved', False) is True:
self.add_history_event(event)

def add_history_event(self, event):
try:
decoded = getattr(self, '_decode_{}'.format(event['type'].lower()))(event)
except AttributeError:
decoded = [event]

for decoded_event in decoded:
self.appended_history.insert(0, decoded_event)

def _decode_tempbasal(self, event):
amount_event = copy(event)
amount_event['_type'] = amount_event.pop('type')

duration_event = copy(event)
duration_event['_type'] = '{}Duration'.format(duration_event.pop('type'))
duration_event[self.DURATION_IN_MINUTES_KEY] = duration_event.pop('duration')

return [amount_event, duration_event]
5 changes: 5 additions & 0 deletions openapscontrib/mmhistorytools/models.py
Expand Up @@ -41,11 +41,16 @@ class TempBasal(BaseRecord):
pass


class Exercise(BaseRecord):
pass


class Unit(object):
grams = "g"
percent_of_basal = "percent"
units = "U"
units_per_hour = "U/hour"
event = "event"


class RecordJSONEncoder(json.JSONEncoder):
Expand Down
2 changes: 1 addition & 1 deletion openapscontrib/mmhistorytools/version.py
@@ -1 +1 @@
__version__ = '0.0.4'
__version__ = '0.1.0'
10 changes: 10 additions & 0 deletions tests/fixtures/exercise_marker.json
@@ -0,0 +1,10 @@
[
{
"_type": "JournalEntryExerciseMarker",
"_description": "JournalEntryExerciseMarker 2015-09-07T15:38:23 head[2], body[1] op[0x41]",
"timestamp": "2015-09-07T15:38:23",
"_body": "00",
"_head": "4100",
"_date": "97660f070f"
}
]
10 changes: 10 additions & 0 deletions tests/fixtures/set_dose.json
@@ -0,0 +1,10 @@
[
{
"temp": "absolute",
"recieved": true,
"rate": 1.425,
"timestamp": "2015-09-19T20:25:27.468623",
"duration": 30,
"type": "TempBasal"
}
]
18 changes: 18 additions & 0 deletions tests/fixtures/set_two_doses.json
@@ -0,0 +1,18 @@
[
{
"temp": "percent",
"recieved": true,
"rate": 100,
"timestamp": "2015-07-27T16:45:29.737754",
"duration": 0,
"type": "TempBasal"
},
{
"temp": "percent",
"recieved": true,
"rate": 140,
"timestamp": "2015-07-27T16:45:31.320183",
"duration": 30,
"type": "TempBasal"
}
]

0 comments on commit 2e43202

Please sign in to comment.