pywemo 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywemo/README.md +69 -0
- pywemo/__init__.py +33 -0
- pywemo/color.py +79 -0
- pywemo/discovery.py +194 -0
- pywemo/exceptions.py +94 -0
- pywemo/ouimeaux_device/LICENSE +12 -0
- pywemo/ouimeaux_device/__init__.py +679 -0
- pywemo/ouimeaux_device/api/__init__.py +1 -0
- pywemo/ouimeaux_device/api/attributes.py +131 -0
- pywemo/ouimeaux_device/api/db_orm.py +197 -0
- pywemo/ouimeaux_device/api/long_press.py +168 -0
- pywemo/ouimeaux_device/api/rules_db.py +467 -0
- pywemo/ouimeaux_device/api/service.py +363 -0
- pywemo/ouimeaux_device/api/wemo_services.py +25 -0
- pywemo/ouimeaux_device/api/wemo_services.pyi +241 -0
- pywemo/ouimeaux_device/api/xsd/__init__.py +1 -0
- pywemo/ouimeaux_device/api/xsd/device.py +3888 -0
- pywemo/ouimeaux_device/api/xsd/device.xsd +95 -0
- pywemo/ouimeaux_device/api/xsd/service.py +3872 -0
- pywemo/ouimeaux_device/api/xsd/service.xsd +93 -0
- pywemo/ouimeaux_device/api/xsd_types.py +222 -0
- pywemo/ouimeaux_device/bridge.py +506 -0
- pywemo/ouimeaux_device/coffeemaker.py +92 -0
- pywemo/ouimeaux_device/crockpot.py +157 -0
- pywemo/ouimeaux_device/dimmer.py +70 -0
- pywemo/ouimeaux_device/humidifier.py +223 -0
- pywemo/ouimeaux_device/insight.py +191 -0
- pywemo/ouimeaux_device/lightswitch.py +11 -0
- pywemo/ouimeaux_device/maker.py +54 -0
- pywemo/ouimeaux_device/motion.py +6 -0
- pywemo/ouimeaux_device/outdoor_plug.py +6 -0
- pywemo/ouimeaux_device/switch.py +32 -0
- pywemo/py.typed +0 -0
- pywemo/ssdp.py +372 -0
- pywemo/subscribe.py +782 -0
- pywemo/util.py +139 -0
- pywemo-1.4.0.dist-info/LICENSE +54 -0
- pywemo-1.4.0.dist-info/METADATA +192 -0
- pywemo-1.4.0.dist-info/RECORD +40 -0
- pywemo-1.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Access and manipulate the on-device rules sqlite database."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import contextlib
|
|
5
|
+
import io
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sqlite3
|
|
9
|
+
import tempfile
|
|
10
|
+
import zipfile
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
from typing import FrozenSet, List, Mapping, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
from pywemo.exceptions import HTTPNotOkException, RulesDbQueryError
|
|
15
|
+
|
|
16
|
+
from .db_orm import DatabaseRow, PrimaryKey, SQLType
|
|
17
|
+
|
|
18
|
+
LOG = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RulesRow(DatabaseRow):
|
|
22
|
+
"""Row schema for the RULES table."""
|
|
23
|
+
|
|
24
|
+
TABLE_NAME = "RULES"
|
|
25
|
+
FIELDS = {
|
|
26
|
+
"RuleID": PrimaryKey(int, sql_type=""),
|
|
27
|
+
"Name": SQLType(str, not_null=True),
|
|
28
|
+
"Type": SQLType(str, not_null=True),
|
|
29
|
+
"RuleOrder": int,
|
|
30
|
+
"StartDate": str,
|
|
31
|
+
"EndDate": str,
|
|
32
|
+
"State": str,
|
|
33
|
+
# Sync has type INTEGER in the database, but contains the
|
|
34
|
+
# value 'NOSYNC', it is represented as a string in Python.
|
|
35
|
+
"Sync": SQLType(str, sql_type="INTEGER"),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RuleDevicesRow(DatabaseRow):
|
|
40
|
+
"""Row schema for the RULEDEVICES table."""
|
|
41
|
+
|
|
42
|
+
TABLE_NAME = "RULEDEVICES"
|
|
43
|
+
FIELDS = {
|
|
44
|
+
"RuleDevicePK": PrimaryKey(int, auto_increment=True),
|
|
45
|
+
"RuleID": int,
|
|
46
|
+
"DeviceID": str,
|
|
47
|
+
"GroupID": int,
|
|
48
|
+
"DayID": int,
|
|
49
|
+
"StartTime": int,
|
|
50
|
+
"RuleDuration": int,
|
|
51
|
+
"StartAction": float,
|
|
52
|
+
"EndAction": float,
|
|
53
|
+
"SensorDuration": int,
|
|
54
|
+
"Type": int,
|
|
55
|
+
"Value": int,
|
|
56
|
+
"Level": int,
|
|
57
|
+
"ZBCapabilityStart": str,
|
|
58
|
+
"ZBCapabilityEnd": str,
|
|
59
|
+
"OnModeOffset": int,
|
|
60
|
+
"OffModeOffset": int,
|
|
61
|
+
"CountdownTime": int,
|
|
62
|
+
"EndTime": int,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TargetDevicesRow(DatabaseRow):
|
|
67
|
+
"""Row schema for the TARGETDEVICES table."""
|
|
68
|
+
|
|
69
|
+
TABLE_NAME = "TARGETDEVICES"
|
|
70
|
+
FIELDS = {
|
|
71
|
+
"TargetDevicesPK": PrimaryKey(int, auto_increment=True),
|
|
72
|
+
"RuleID": int,
|
|
73
|
+
"DeviceID": str,
|
|
74
|
+
"DeviceIndex": int,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DeviceCombinationRow(DatabaseRow):
|
|
79
|
+
"""Row schema for the DEVICECOMBINATION table."""
|
|
80
|
+
|
|
81
|
+
TABLE_NAME = "DEVICECOMBINATION"
|
|
82
|
+
FIELDS = {
|
|
83
|
+
"DeviceCombinationPK": PrimaryKey(int, auto_increment=True),
|
|
84
|
+
"RuleID": int,
|
|
85
|
+
"SensorID": str,
|
|
86
|
+
"SensorGroupID": int,
|
|
87
|
+
"DeviceID": str,
|
|
88
|
+
"DeviceGroupID": int,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class GroupDevicesRow(DatabaseRow):
|
|
93
|
+
"""Row schema for the GROUPDEVICES table."""
|
|
94
|
+
|
|
95
|
+
TABLE_NAME = "GROUPDEVICES"
|
|
96
|
+
FIELDS = {
|
|
97
|
+
"GroupDevicePK": PrimaryKey(int, auto_increment=True),
|
|
98
|
+
"GroupID": int,
|
|
99
|
+
"DeviceID": str,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class LocationInfoRow(DatabaseRow):
|
|
104
|
+
"""Row schema for the LOCATIONINFO table."""
|
|
105
|
+
|
|
106
|
+
TABLE_NAME = "LOCATIONINFO"
|
|
107
|
+
FIELDS = {
|
|
108
|
+
"LocationPk": PrimaryKey(int, auto_increment=True),
|
|
109
|
+
"cityName": str,
|
|
110
|
+
"countryName": str,
|
|
111
|
+
"latitude": str,
|
|
112
|
+
"longitude": str,
|
|
113
|
+
"countryCode": str,
|
|
114
|
+
"region": str,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class BlockedRulesRow(DatabaseRow):
|
|
119
|
+
"""Row schema for the BLOCKEDRULES table."""
|
|
120
|
+
|
|
121
|
+
TABLE_NAME = "BLOCKEDRULES"
|
|
122
|
+
FIELDS = {
|
|
123
|
+
"Primarykey": PrimaryKey(int, auto_increment=True),
|
|
124
|
+
"ruleId": str,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class RulesNotifyMessageRow(DatabaseRow):
|
|
129
|
+
"""Row schema for the RULESNOTIFYMESSAGE table."""
|
|
130
|
+
|
|
131
|
+
TABLE_NAME = "RULESNOTIFYMESSAGE"
|
|
132
|
+
FIELDS = {
|
|
133
|
+
"RuleID": PrimaryKey(int, auto_increment=True),
|
|
134
|
+
"NotifyRuleID": int,
|
|
135
|
+
"Message": str,
|
|
136
|
+
"Frequency": int,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class SensorNotificationRow(DatabaseRow):
|
|
141
|
+
"""Row schema for the SENSORNOTIFICATION table."""
|
|
142
|
+
|
|
143
|
+
TABLE_NAME = "SENSORNOTIFICATION"
|
|
144
|
+
FIELDS = {
|
|
145
|
+
"SensorNotificationPK": PrimaryKey(int, auto_increment=True),
|
|
146
|
+
"RuleID": int,
|
|
147
|
+
"NotifyRuleID": int,
|
|
148
|
+
"NotificationMessage": str,
|
|
149
|
+
"NotificationDuration": int,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
ALL_TABLES = [
|
|
154
|
+
RulesRow,
|
|
155
|
+
RuleDevicesRow,
|
|
156
|
+
DeviceCombinationRow,
|
|
157
|
+
GroupDevicesRow,
|
|
158
|
+
LocationInfoRow,
|
|
159
|
+
BlockedRulesRow,
|
|
160
|
+
RulesNotifyMessageRow,
|
|
161
|
+
SensorNotificationRow,
|
|
162
|
+
TargetDevicesRow,
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RulesDb:
|
|
167
|
+
"""Methods to access and manipulate the `rules` sqlite database."""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self, sql_db: sqlite3.Connection, default_udn: str, device_name: str
|
|
171
|
+
):
|
|
172
|
+
"""Preparse tables for device."""
|
|
173
|
+
self._db = sql_db
|
|
174
|
+
self._default_udn = default_udn
|
|
175
|
+
self._device_name = device_name
|
|
176
|
+
self.modified = False
|
|
177
|
+
cursor = sql_db.cursor()
|
|
178
|
+
self._rules = _index_by_primary_key(RulesRow.select_all(cursor))
|
|
179
|
+
self._rule_devices = _index_by_primary_key(
|
|
180
|
+
RuleDevicesRow.select_all(cursor)
|
|
181
|
+
)
|
|
182
|
+
self._target_devices = _index_by_primary_key(
|
|
183
|
+
TargetDevicesRow.select_all(cursor)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def db(self) -> sqlite3.Connection: # pylint: disable=invalid-name
|
|
188
|
+
"""Return the sqlite3 connection instance."""
|
|
189
|
+
return self._db
|
|
190
|
+
|
|
191
|
+
def cursor(self) -> sqlite3.Cursor:
|
|
192
|
+
"""Return a cursor for the underlying sqlite3 database."""
|
|
193
|
+
return self.db.cursor()
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def rules(self) -> Mapping[int, RulesRow]:
|
|
197
|
+
"""Contents of the RULES table, keyed by RuleID."""
|
|
198
|
+
return MappingProxyType(self._rules)
|
|
199
|
+
|
|
200
|
+
def add_rule(self, rule: RulesRow) -> RulesRow:
|
|
201
|
+
"""Add a new entry to the RULES table."""
|
|
202
|
+
if not hasattr(rule, "RuleID"):
|
|
203
|
+
rule.RuleID = max(self._rules.keys(), default=0) + 1
|
|
204
|
+
rule.update_db(self.cursor())
|
|
205
|
+
self._rules[rule.RuleID] = rule
|
|
206
|
+
self.modified = True
|
|
207
|
+
return rule
|
|
208
|
+
|
|
209
|
+
def remove_rule(self, rule: RulesRow) -> None:
|
|
210
|
+
"""Remove an entry to the RULES table."""
|
|
211
|
+
del self._rules[rule.RuleID]
|
|
212
|
+
rule.remove_from_db(self.cursor())
|
|
213
|
+
self.modified = True
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def rule_devices(self) -> Mapping[int, RuleDevicesRow]:
|
|
217
|
+
"""Contents of the RULEDEVICES table, keyed by RuleDevicePK."""
|
|
218
|
+
return MappingProxyType(self._rule_devices)
|
|
219
|
+
|
|
220
|
+
def add_rule_devices(self, rule_devices: RuleDevicesRow) -> RuleDevicesRow:
|
|
221
|
+
"""Add a new entry to the RULEDEVICES table."""
|
|
222
|
+
rule_devices.update_db(self.cursor())
|
|
223
|
+
self._rule_devices[rule_devices.RuleDevicePK] = rule_devices
|
|
224
|
+
self.modified = True
|
|
225
|
+
return rule_devices
|
|
226
|
+
|
|
227
|
+
def remove_rule_devices(self, rule_devices: RuleDevicesRow) -> None:
|
|
228
|
+
"""Remove an entry to the RULEDEVICES table."""
|
|
229
|
+
del self._rule_devices[rule_devices.RuleDevicePK]
|
|
230
|
+
rule_devices.remove_from_db(self.cursor())
|
|
231
|
+
self.modified = True
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def target_devices(self) -> Mapping[int, TargetDevicesRow]:
|
|
235
|
+
"""Contents of the TARGETDEVICES table, keyed by TargetDevicesPK."""
|
|
236
|
+
return MappingProxyType(self._target_devices)
|
|
237
|
+
|
|
238
|
+
def add_target_devices(
|
|
239
|
+
self, target_devices: TargetDevicesRow
|
|
240
|
+
) -> TargetDevicesRow:
|
|
241
|
+
"""Add a new entry to the TARGETDEVICES table."""
|
|
242
|
+
target_devices.update_db(self.cursor())
|
|
243
|
+
self._target_devices[target_devices.TargetDevicesPK] = target_devices
|
|
244
|
+
self.modified = True
|
|
245
|
+
return target_devices
|
|
246
|
+
|
|
247
|
+
def remove_target_devices(self, target_devices: TargetDevicesRow) -> None:
|
|
248
|
+
"""Remove an entry to the TARGETDEVICES table."""
|
|
249
|
+
del self._target_devices[target_devices.TargetDevicesPK]
|
|
250
|
+
target_devices.remove_from_db(self.cursor())
|
|
251
|
+
self.modified = True
|
|
252
|
+
|
|
253
|
+
def update_if_modified(self) -> bool:
|
|
254
|
+
"""Sync the contents with the sqlite database.
|
|
255
|
+
|
|
256
|
+
Return True if the database was modified.
|
|
257
|
+
"""
|
|
258
|
+
modified = self.modified
|
|
259
|
+
cursor = self.cursor()
|
|
260
|
+
|
|
261
|
+
def update(rows):
|
|
262
|
+
nonlocal modified
|
|
263
|
+
for row in rows:
|
|
264
|
+
if row.modified:
|
|
265
|
+
row.update_db(cursor)
|
|
266
|
+
modified = True
|
|
267
|
+
|
|
268
|
+
update(self._rules.values())
|
|
269
|
+
update(self._rule_devices.values())
|
|
270
|
+
update(self._target_devices.values())
|
|
271
|
+
return modified
|
|
272
|
+
|
|
273
|
+
def rules_for_device(
|
|
274
|
+
self,
|
|
275
|
+
*,
|
|
276
|
+
device_udn: Optional[str] = None,
|
|
277
|
+
rule_type: Optional[str] = None,
|
|
278
|
+
) -> List[Tuple[RulesRow, RuleDevicesRow]]:
|
|
279
|
+
"""Fetch the current rules for a particular device."""
|
|
280
|
+
if device_udn is None:
|
|
281
|
+
device_udn = self._default_udn
|
|
282
|
+
values = []
|
|
283
|
+
for device in self.rule_devices.values():
|
|
284
|
+
if device_udn and device.DeviceID != device_udn:
|
|
285
|
+
continue
|
|
286
|
+
rule = self.rules[device.RuleID]
|
|
287
|
+
if rule_type and rule.Type != rule_type:
|
|
288
|
+
continue
|
|
289
|
+
values.append((rule, device))
|
|
290
|
+
|
|
291
|
+
return values
|
|
292
|
+
|
|
293
|
+
def get_target_devices_for_rule(self, rule: RulesRow) -> FrozenSet[str]:
|
|
294
|
+
"""Return the target DeviceIDs that are associated with the rule."""
|
|
295
|
+
return frozenset(
|
|
296
|
+
[
|
|
297
|
+
target.DeviceID
|
|
298
|
+
for target in self.target_devices.values()
|
|
299
|
+
if target.RuleID == rule.RuleID
|
|
300
|
+
]
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def add_target_device_to_rule(
|
|
304
|
+
self,
|
|
305
|
+
rule: RulesRow,
|
|
306
|
+
device_id: str,
|
|
307
|
+
*,
|
|
308
|
+
device_index: Optional[int] = None,
|
|
309
|
+
):
|
|
310
|
+
"""Add a new target DeviceID to the rule."""
|
|
311
|
+
if device_index is None:
|
|
312
|
+
target_device_index = (
|
|
313
|
+
target.DeviceIndex
|
|
314
|
+
for target in self.target_devices.values()
|
|
315
|
+
if target.RuleID == rule.RuleID
|
|
316
|
+
)
|
|
317
|
+
device_index = max(target_device_index, default=-1) + 1
|
|
318
|
+
self.add_target_devices(
|
|
319
|
+
TargetDevicesRow(
|
|
320
|
+
RuleID=rule.RuleID,
|
|
321
|
+
DeviceID=device_id,
|
|
322
|
+
DeviceIndex=device_index,
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def remove_target_device_from_rule(self, rule: RulesRow, device_id: str):
|
|
327
|
+
"""Remove a target DeviceID from a rule."""
|
|
328
|
+
targets = [
|
|
329
|
+
target
|
|
330
|
+
for target in self.target_devices.values()
|
|
331
|
+
if target.RuleID == rule.RuleID and target.DeviceID == device_id
|
|
332
|
+
]
|
|
333
|
+
if len(targets) != 1:
|
|
334
|
+
raise NameError(
|
|
335
|
+
f"device {device_id} not found in target devices for rule"
|
|
336
|
+
)
|
|
337
|
+
self.remove_target_devices(targets[0])
|
|
338
|
+
|
|
339
|
+
def clear_all(self) -> None:
|
|
340
|
+
"""Clear all data from the database."""
|
|
341
|
+
cursor = self.cursor()
|
|
342
|
+
for table in ALL_TABLES:
|
|
343
|
+
cursor.execute(f"DELETE FROM {table.TABLE_NAME}")
|
|
344
|
+
self.modified = True
|
|
345
|
+
self._rules = _index_by_primary_key(RulesRow.select_all(cursor))
|
|
346
|
+
self._rule_devices = _index_by_primary_key(
|
|
347
|
+
RuleDevicesRow.select_all(cursor)
|
|
348
|
+
)
|
|
349
|
+
self._target_devices = _index_by_primary_key(
|
|
350
|
+
TargetDevicesRow.select_all(cursor)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@contextlib.contextmanager
|
|
355
|
+
def rules_db_from_device(device) -> RulesDb:
|
|
356
|
+
"""Yield a RuleDb instance for the rules on a device.
|
|
357
|
+
|
|
358
|
+
Usage:
|
|
359
|
+
with rules_db.rules_db_from_device(device) as rules:
|
|
360
|
+
...
|
|
361
|
+
|
|
362
|
+
The sqlite3.Connection object can be accessed via the '.db' property in the
|
|
363
|
+
returned RulesDb instance. If the database is modified directly, setting
|
|
364
|
+
the `.modified` attribute to True will cause the database to be sent to the
|
|
365
|
+
WeMo device. Any updates that take place via the RulesDb helper methods
|
|
366
|
+
will also be propagated back to the WeMo device.
|
|
367
|
+
"""
|
|
368
|
+
fetch = device.rules.FetchRules()
|
|
369
|
+
version = int(fetch["ruleDbVersion"])
|
|
370
|
+
rule_db_url = fetch["ruleDbPath"]
|
|
371
|
+
try:
|
|
372
|
+
response = device.session.get(rule_db_url)
|
|
373
|
+
except HTTPNotOkException:
|
|
374
|
+
response = None
|
|
375
|
+
|
|
376
|
+
with tempfile.TemporaryDirectory(prefix="wemorules_") as temp_dir:
|
|
377
|
+
local_file_name = os.path.join(temp_dir, "rules.db")
|
|
378
|
+
# Create a new db, or extract the current db.
|
|
379
|
+
if response is None:
|
|
380
|
+
inner_file_name = _create_empty_db(local_file_name)
|
|
381
|
+
else:
|
|
382
|
+
inner_file_name = _unpack_db(response.content, local_file_name)
|
|
383
|
+
|
|
384
|
+
# Open the DB.
|
|
385
|
+
conn = sqlite3.connect(local_file_name)
|
|
386
|
+
try:
|
|
387
|
+
conn.row_factory = sqlite3.Row
|
|
388
|
+
try:
|
|
389
|
+
rules = RulesDb(
|
|
390
|
+
conn, default_udn=device.udn, device_name=device.name
|
|
391
|
+
)
|
|
392
|
+
yield rules
|
|
393
|
+
except sqlite3.Error as err:
|
|
394
|
+
try:
|
|
395
|
+
db_dump = list(conn.iterdump())
|
|
396
|
+
LOG.debug("Rules: %s", repr(db_dump))
|
|
397
|
+
fw_version = device.firmwareupdate.GetFirmwareVersion()
|
|
398
|
+
LOG.debug("Firmware: %s", repr(fw_version))
|
|
399
|
+
except Exception: # pylint: disable=broad-except
|
|
400
|
+
# Ignore any additional errors that occur as a result of
|
|
401
|
+
# outputting this debug information.
|
|
402
|
+
pass
|
|
403
|
+
raise RulesDbQueryError(f"sqlite3 exception {err}") from err
|
|
404
|
+
|
|
405
|
+
if rules.update_if_modified():
|
|
406
|
+
LOG.debug("Rules for %s updated. Storing rules.", device.name)
|
|
407
|
+
conn.commit()
|
|
408
|
+
conn.close()
|
|
409
|
+
conn = None
|
|
410
|
+
body = _pack_db(local_file_name, inner_file_name)
|
|
411
|
+
device.rules.StoreRules(
|
|
412
|
+
ruleDbVersion=version + 1,
|
|
413
|
+
processDb=1,
|
|
414
|
+
ruleDbBody="<![CDATA[" + body + "]]>",
|
|
415
|
+
)
|
|
416
|
+
finally:
|
|
417
|
+
if conn is not None:
|
|
418
|
+
conn.close()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _unpack_db(content, local_file_name):
|
|
422
|
+
"""Unpack the sqlite database from a .zip file content."""
|
|
423
|
+
zip_contents = io.BytesIO(content)
|
|
424
|
+
with zipfile.ZipFile(zip_contents) as zip_file:
|
|
425
|
+
inner_file_name = zip_file.namelist()[0]
|
|
426
|
+
with zip_file.open(inner_file_name) as zipped_db_file:
|
|
427
|
+
with open(local_file_name, "w+b") as db_file:
|
|
428
|
+
db_file.write(zipped_db_file.read())
|
|
429
|
+
return inner_file_name
|
|
430
|
+
raise RuntimeError("Could not find database within zip file")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _pack_db(local_file_name, inner_file_name):
|
|
434
|
+
"""Pack the sqlite database as a base64(zipped(db))."""
|
|
435
|
+
zip_contents = io.BytesIO()
|
|
436
|
+
with zipfile.ZipFile(
|
|
437
|
+
zip_contents, mode="w", compression=zipfile.ZIP_DEFLATED
|
|
438
|
+
) as zip_file:
|
|
439
|
+
zip_file.write(local_file_name, arcname=inner_file_name)
|
|
440
|
+
return base64.b64encode(zip_contents.getvalue()).decode("utf-8")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _index_by_primary_key(rows):
|
|
444
|
+
"""Return a dict of Rows indexed by the primary key."""
|
|
445
|
+
result = {}
|
|
446
|
+
for row in rows:
|
|
447
|
+
value = row.primary_key_value()
|
|
448
|
+
if value is None:
|
|
449
|
+
LOG.debug("Skipping row with NULL primary key: %s", row)
|
|
450
|
+
continue
|
|
451
|
+
result[value] = row
|
|
452
|
+
return result
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _create_empty_db(file_name):
|
|
456
|
+
"""Create an empty sqlite database.
|
|
457
|
+
|
|
458
|
+
Returns the name of the database file that would be inside the zip.
|
|
459
|
+
"""
|
|
460
|
+
conn = sqlite3.connect(file_name)
|
|
461
|
+
try:
|
|
462
|
+
for row_class in ALL_TABLES:
|
|
463
|
+
row_class.create_sqlite_table_from_row_schema(conn.cursor())
|
|
464
|
+
conn.commit()
|
|
465
|
+
finally:
|
|
466
|
+
conn.close()
|
|
467
|
+
return "temppluginRules.db"
|