├── .gitignore ├── README.md └── ReminderStore.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReminderStore 2 | 3 | Key-value store using iOS Reminders for persistence and distribution across iOS devices. 4 | 5 | ## Introduction 6 | 7 | ReminderStore is a [Pythonista](http://omz-software.com/pythonista/) persistence provider. It uses the `reminders` module to store the values in a specific list in the iOS Reminders app. If you set that list to be distributed across your iOS devices, you get a free cloud-based storage for your app data, suitable for prototyping different distributed use cases. 8 | 9 | ## API 10 | 11 | Create a store object providing the name of the Reminders list to be created/used: 12 | 13 | `store = ReminderStore(namespace, to_json=False, cache=False)` 14 | 15 | Using the store API is similar to using a dict: 16 | 17 | * Store values: `store['key'] = 'value'` 18 | * Retrieve values: `store['key']` 19 | * Iterate through all items: `for key in store: print store[key]` 20 | * Delete items: `del store['key']` 21 | * Count of items in store: `len(store)` 22 | * Check existence: `'key' in store` 23 | * Print all contents: `print str(store)` 24 | 25 | A convenience method `new_item(value='')` creates a new item in the store and returns an 8-character random key for it. 26 | 27 | If you want to store structures instead of plain strings, set `to_json=True`. Store and retrieval operations will then serialize and restore the values to and from JSON. 28 | 29 | Setting `cache=True` reduces direct access to the Reminders app. Use the `refresh_cache()` method to refresh the cache and get information on any background changes. See Notes below for more details. 30 | 31 | ## Notes 32 | 33 | * Key is stored as the title of the reminder, and value in the notes field. 34 | * Reminder titles are not unique, but ReminderStore uses an intermediate dict to enforce uniqueness. Results are likely to be unproductive if you create duplicate titles manually. 35 | * Key is used as-is if it is a plain or unicode string. Otherwise `str()` is called on key before using it. 36 | * By default, ReminderStore goes to the Reminders app to get the latest information. This is somewhat inefficient since checking for new items always requires loading the full list. There are two cases where you might to prefer to activate the intermediate caching instead: 37 | * If you have no-one making any updates remotely, cache will be more efficient and will never need manual refreshing. 38 | * If you want to get more control over when any background changes are applied in your app, use the caching and call `refresh_cache` to check and process changes. The method returns None if there are no changes, or a dict with `added`, `deleted` and `changed` sets if there are changes. Each set contains the keys of the stored items that were inserted, deleted or had their contents modified, respectively. You can use this info to e.g. remove deleted items from the UI, or inform the user of a conflict and let them decide which version to keep. 39 | * Note that iOS Reminders app provides no support for complex atomic transactions or referential integrity between items. -------------------------------------------------------------------------------- /ReminderStore.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import reminders 3 | import json 4 | import uuid 5 | 6 | class ReminderStore(): 7 | 8 | def __init__(self, namespace = 'Pythonista', to_json = False, cache = False): 9 | self.json = to_json 10 | self.cached = cache 11 | self.cache = {} 12 | self.list_calendar = None 13 | all_calendars = reminders.get_all_calendars() 14 | for calendar in all_calendars: 15 | if calendar.title == namespace: 16 | self.list_calendar = calendar 17 | break 18 | if not self.list_calendar: 19 | new_calendar = reminders.Calendar() 20 | new_calendar.title = namespace 21 | new_calendar.save() 22 | self.list_calendar = new_calendar 23 | self.items = {} 24 | self._refresh(force = True) 25 | 26 | def _refresh(self, force = False): 27 | if self.cached and not force: 28 | return 29 | list_reminders = reminders.get_reminders(self.list_calendar) 30 | for item in list_reminders: 31 | self.items[item.title] = item 32 | if self.cached: 33 | self.cache[item.title] = item.notes 34 | 35 | def new_item(self, value = ''): 36 | new_id = '' 37 | while True: 38 | full_string = str(uuid.uuid4()) 39 | new_id = full_string.split('-')[0] 40 | if not new_id in self: 41 | break 42 | self[new_id] = value 43 | return new_id 44 | 45 | def refresh_cache(self): 46 | delta = { "added": set(), "deleted": set(), "changed": set() } 47 | has_delta = False 48 | list_reminders = reminders.get_reminders(self.list_calendar) 49 | self.items = {} 50 | new_cache = {} 51 | for item in list_reminders: 52 | self.items[item.title] = item 53 | new_cache[item.title] = item.notes 54 | if not item.title in self.cache: 55 | has_delta = True 56 | delta['added'].add(item.title) 57 | else: 58 | if item.notes != self.cache[item.title]: 59 | has_delta = True 60 | delta['changed'].add(item.title) 61 | del self.cache[item.title] 62 | if len(self.cache) > 0: 63 | has_delta = True 64 | for key in self.cache: 65 | delta['deleted'].add(key) 66 | self.cache = new_cache 67 | return delta if has_delta else None 68 | 69 | def __setitem__(self, id, content): 70 | id = self._effective_id(id) 71 | r = self.items[id] if id in self else reminders.Reminder(self.list_calendar) 72 | r.title = id 73 | r.notes = json.dumps(content, ensure_ascii=False) if self.json else content 74 | self.items[id] = r 75 | r.save() 76 | if self.cached: 77 | self.cache[id] = r.notes 78 | 79 | def __getitem__(self, id): 80 | id = self._effective_id(id) 81 | if id in self: 82 | content = self.cache[id] if self.cached else self.items[id].notes 83 | return json.loads(content) if self.json else content 84 | else: 85 | return None 86 | 87 | def __delitem__(self, id): 88 | id = self._effective_id(id) 89 | if id in self: 90 | reminders.delete_reminder(self.items[id]) 91 | del self.items[id] 92 | if self.cached: 93 | del self.cache[id] 94 | else: 95 | raise KeyError 96 | 97 | def _effective_id(self, id): 98 | return id if isinstance(id, basestring) else str(id) 99 | 100 | def __len__(self): 101 | self._refresh() 102 | return len(self.items) 103 | 104 | def __str__(self): 105 | printable = {} 106 | self._refresh() 107 | for key in self: 108 | printable[key] = self[key] 109 | return str(printable) 110 | 111 | def __iter__(self): 112 | self._refresh() 113 | return iter(self.items.keys()) 114 | 115 | def __contains__(self, id): 116 | if not id in self.items: 117 | self._refresh() 118 | return id in self.items 119 | 120 | if __name__ == "__main__": 121 | 122 | content = '''ReminderStore is a [Pythonista](http://omz-software.com/pythonista/) persistence provider. It uses the ```reminders``` module to store the values in a specific list in the iOS Reminders app. 123 | ''' 124 | 125 | namespace = 'ReminderStore Demo' 126 | store = ReminderStore(namespace) 127 | 128 | id = 'intro' 129 | content = '''ReminderStore is a [Pythonista](http://omz-software.com/pythonista/) persistence provider. It uses the ```reminders``` module to store the values in a specific list in the iOS Reminders app. 130 | ''' 131 | 132 | store[id] = content 133 | new_id = store.new_item(content) 134 | print 'NEW ITEM ID: ' + new_id 135 | print 136 | print 'ITEMS IN STORE: ' + str(len(store)) 137 | print 138 | print 'PRINT CONTENTS' 139 | print store 140 | print 141 | if id in store: print 'VALIDATED: ' + id + ' in store' 142 | print 143 | print 'ITERATE ALL CONTENTS' 144 | for key in store: 145 | print 'Key: ' + key + ' - ' + store[key] 146 | del store[id] 147 | del store[new_id] 148 | 149 | print 150 | 151 | store = ReminderStore(namespace, to_json=True) 152 | 153 | id = { 'not': 'string' } 154 | content = { 'complex': 'structure serialization' } 155 | 156 | store[id] = content 157 | print 'JSON: ' + store[id]['complex'] 158 | del store[id] 159 | 160 | store = ReminderStore(namespace, cache = True) 161 | 162 | store['to be deleted'] = 'sample' 163 | store['to be changed'] = 'sample' 164 | 165 | # Simulate changes to the Reminders synced from another device 166 | 167 | # Add 168 | a = reminders.Reminder(store.list_calendar) 169 | a.title = 'has been added' 170 | a.notes = 'sample' 171 | a.save() 172 | 173 | # Change 174 | list_reminders = reminders.get_reminders(store.list_calendar) 175 | for item in list_reminders: 176 | if item.title == 'to be changed': 177 | item.notes = 'new value' 178 | item.save() 179 | 180 | # Delete 181 | reminders.delete_reminder(store.items['to be deleted']) 182 | 183 | diff = store.refresh_cache() 184 | 185 | print 186 | print 'DIFF: ' + str(diff) 187 | 188 | del store['to be changed'] 189 | del store['has been added'] 190 | 191 | print 192 | print 'ITEMS IN STORE: ' + str(len(store)) 193 | print 194 | --------------------------------------------------------------------------------