Skip to content

Commit c48442d

Browse files
committed
Simplify db/model handling
1 parent 0847f71 commit c48442d

10 files changed

Lines changed: 58 additions & 147 deletions

File tree

bin/indoktrinator

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ from twisted.web.server import Site
1414
from twisted.python.threadpool import ThreadPool
1515
from twisted.python import log
1616

17-
# Data are accessed through SQLSoup, using SQLAlchemy.
18-
from sqlalchemy.orm import scoped_session, sessionmaker
1917
from sqlalchemy import create_engine
20-
from indoktrinator.sqlalchemy_db import SQLSoup
18+
from indoktrinator.sqlalchemy_db import SQLAlchemyDB
2119

2220
# Command line arguments follow the GNU conventions.
2321
from getopt import gnu_getopt
@@ -65,9 +63,7 @@ if __name__ == '__main__':
6563
notifier = Notifier(reactor, db_url)
6664

6765
# Prepare database connection with table reflection.
68-
engine = create_engine(db_url)
69-
session = scoped_session(sessionmaker())
70-
db = SQLSoup(engine, session=session)
66+
db = SQLAlchemyDB(create_engine(db_url))
7167

7268
# Prepare a 0MQ router instance for communication with the
7369
# leader that publishes our indoctrination schedule.
@@ -139,14 +135,10 @@ if __name__ == '__main__':
139135
config_path = v
140136

141137
# Load the configuration from file.
138+
config = ConfigParser()
142139
if action not in (do_help, do_version):
143-
config = ConfigParser()
144140
config.read(config_path)
145141

146-
# Load the configuration from file.
147-
config = ConfigParser()
148-
config.read(config_path)
149-
150142
# Perform the selected action.
151143
action(config=config)
152144

bin/indoktrinator-harvester

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ from twisted.internet import reactor
66
from twisted.web.wsgi import WSGIResource
77
from twisted.python import log
88

9-
# Data are accessed through SQLSoup, using SQLAlchemy.
10-
from sqlalchemy.orm import scoped_session, sessionmaker
119
from sqlalchemy import create_engine
12-
from indoktrinator.sqlalchemy_db import SQLSoup
10+
from indoktrinator.sqlalchemy_db import SQLAlchemyDB
1311

1412
# Command line arguments follow the GNU conventions.
1513
from getopt import gnu_getopt
@@ -34,9 +32,7 @@ if __name__ == '__main__':
3432
path = config.get('media', 'path', fallback='/var/lib/indoktrinator')
3533

3634
# Prepare database connection with table reflection.
37-
engine = create_engine(db_url)
38-
session = scoped_session(sessionmaker(autoflush=True))
39-
db = SQLSoup(engine, session=session)
35+
db = SQLAlchemyDB(create_engine(db_url))
4036

4137
# Extract harvester options, sans the pool_size we handle here.
4238
harvester_opts = dict(config.items('harvester'))

indoktrinator/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from psycopg2 import STRING
77
from psycopg2.extras import RangeCaster
8-
from psycopg2.extensions import AsIs, register_type, new_type
8+
from psycopg2.extensions import register_type, new_type
99
from sqlalchemy.types import UserDefinedType
1010
from sqlalchemy.dialects.postgresql.base import ischema_names
1111

indoktrinator/harvester/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def probe_done(info):
200200

201201
if d is not None:
202202
d.addCallback(probe_done)
203+
d.addErrback(lambda f: log.err(f, 'Failed to process {!r}'.format(path)))
203204

204205
@with_session
205206
def update_item_with_info(self, playlist, item, node, info):

indoktrinator/harvester/shadow_tree.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ def scan(self):
9999
self.add_child(fname, False)
100100

101101
for child in missing:
102-
child = self.children.pop(missing)
103-
child.lost()
102+
self.children.pop(child).lost()
104103

105104
break
106105

indoktrinator/manager/__init__.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from uuid import uuid4
1111
from time import time
1212

13-
from indoktrinator.db import with_session
1413
from indoktrinator.manager.schema import schema
1514
from indoktrinator.manager.scheduler import *
1615
from indoktrinator.manager.store import *
@@ -37,6 +36,7 @@ def __init__(self, db, notifier, router, media_path, url,
3736
self.power_down_gap = int(power_down_gap)
3837

3938
# Collection of devices we have come into contact with.
39+
# Written only from the reactor thread; HTTP threads read via dict.copy().
4040
self.devices = {}
4141

4242
# Store for database objects received via notifications.
@@ -158,8 +158,9 @@ def on_program_change(self, uuid, program):
158158
else:
159159
log.msg('Program {} deleted.'.format(uuid))
160160

161-
if uuid in self.plans:
162-
self.plans.pop(uuid)
161+
str_uuid = str(uuid)
162+
if str_uuid in self.plans:
163+
del self.plans[str_uuid]
163164

164165
self.sync_devices()
165166

@@ -178,14 +179,18 @@ def update_plans(self):
178179

179180
def on_device_change(self, id, device):
180181
"""Device has changed in the database."""
181-
self.sync_device(id)
182+
if device is None:
183+
# Device deleted — remove runtime state.
184+
self.devices.pop(id, None)
185+
else:
186+
self.sync_device(id)
182187

183188
def sync_devices(self):
184189
"""
185190
Synchronize all devices plans as needed.
186191
"""
187192

188-
for id in self.devices:
193+
for id in list(self.devices):
189194
self.sync_device(id)
190195

191196
def sync_device(self, id):
@@ -211,7 +216,12 @@ def sync_device(self, id):
211216
return
212217

213218
# Find plan for device program.
214-
plan = self.plans[str(device['program'])]
219+
plan = self.plans.get(str(device['program']))
220+
221+
if plan is None:
222+
# Plan not computed yet (e.g. during startup or after DB change).
223+
self.send(id, 'plan', EMPTY_PLAN)
224+
return
215225

216226
# Check that device plan is up to date.
217227
if status['plan'] != plan['id']:

indoktrinator/manager/scheduler.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
from re import findall
1313
from uuid import uuid4
1414

15-
from indoktrinator.db import with_session
16-
1715

1816
__all__ = ['make_plan', 'EMPTY_PLAN']
1917

indoktrinator/site/__init__.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ def api_devices(depth, **kwargs):
191191
)
192192
)
193193

194-
for devid, status in manager.devices.items():
194+
# Snapshot to avoid RuntimeError if reactor modifies devices concurrently.
195+
for devid, status in manager.devices.copy().items():
195196
if devid not in persistent:
196197
devices.insert(
197198
0,
@@ -218,8 +219,8 @@ def api_device(id, depth, **kwargs):
218219
try:
219220
device = model.device.get(id, depth=depth)
220221
except KeyError:
221-
device = manager.devices.get(id)
222-
device.update({'pending': True})
222+
device = dict(manager.devices.get(id) or {})
223+
device['pending'] = True
223224

224225
status = manager.devices.get(id, {})
225226

@@ -237,13 +238,10 @@ def api_device(id, depth, **kwargs):
237238
try:
238239
return jsonify(deleted=model.device.delete(id))
239240
except KeyError:
240-
pass
241-
finally:
242241
if id not in manager.devices:
243242
log.msg('No device with id: {id}'.format(id=id))
244-
else:
245-
del manager.devices[id]
246243
return jsonify(deleted=id)
244+
# Reactor cleans up manager.devices via on_device_change when NOTIFY arrives.
247245

248246
if 'PATCH' == request.method:
249247
patch = request.get_json(force=True)

indoktrinator/site/model.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
__all__ = ['Model']
99

10+
# Maps SQLAlchemy mapped classes to their fixup function.
11+
# Populated by Table.__init__ so dbdict() needs no magic on the ORM objects.
12+
_fixup_registry: dict = {}
13+
1014

1115
class Table:
1216
NAME = None
@@ -23,15 +27,8 @@ def __init__(self, model, db):
2327
self.db = db
2428
self.table = getattr(db, self.NAME)
2529

26-
for key, other_table in self.RELS:
27-
entity = getattr(db, other_table)
28-
ordering = []
29-
30-
for column in self.model.TABLES[other_table].ORDER_BY:
31-
ordering.append(getattr(entity, column))
32-
33-
# Fox the `dbdict` function.
34-
self.table._table.fixup = self.fixup
30+
# Register this table's fixup so dbdict() can find it by mapped class.
31+
_fixup_registry[self.table._cls] = self.fixup
3532

3633
def get(self, key, depth=0):
3734
obj = self.table.get(key)
@@ -243,7 +240,7 @@ def dbdict(obj, depth=0):
243240
Convert an SQLAlchemy object to a dictionary.
244241
"""
245242

246-
fixup = obj._table.fixup
243+
fixup = _fixup_registry.get(type(obj), lambda x: x)
247244
mapper = class_mapper(obj.__class__)
248245
data = {col.key: getattr(obj, col.key) for col in mapper.columns}
249246

0 commit comments

Comments
 (0)