Unify Snapshot's POST view with Devicehub's; bugfixes

This commit is contained in:
Xavier Bustamante Talavera 2019-02-18 12:43:50 +01:00
parent 32e696c57c
commit 07e8be829e
12 changed files with 2647 additions and 61 deletions

View File

@ -4,7 +4,7 @@ from teal.resource import Converters, Resource
from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.device.sync import Sync
from ereuse_devicehub.resources.event import schemas from ereuse_devicehub.resources.event import schemas
from ereuse_devicehub.resources.event.views import EventView, SnapshotView from ereuse_devicehub.resources.event.views import EventView
class EventDef(Resource): class EventDef(Resource):
@ -90,13 +90,14 @@ class InstallDef(EventDef):
class SnapshotDef(EventDef): class SnapshotDef(EventDef):
VIEW = SnapshotView VIEW = None
SCHEMA = schemas.Snapshot SCHEMA = schemas.Snapshot
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None, def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
static_url_path=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()): root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
url_prefix = '/{}'.format(EventDef.resource)
super().__init__(app, import_name, static_folder, static_url_path, template_folder, super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands) url_prefix, subdomain, url_defaults, root_path, cli_commands)
self.sync = Sync() self.sync = Sync()

View File

@ -12,14 +12,21 @@ from ereuse_devicehub.resources.device.models import Component, Computer
from ereuse_devicehub.resources.enums import SnapshotSoftware from ereuse_devicehub.resources.enums import SnapshotSoftware
from ereuse_devicehub.resources.event.models import Event, Snapshot, WorkbenchRate from ereuse_devicehub.resources.event.models import Event, Snapshot, WorkbenchRate
SUPPORTED_WORKBENCH = StrictVersion('11.0')
class EventView(View): class EventView(View):
def post(self): def post(self):
"""Posts an event.""" """Posts an event."""
json = request.get_json(validate=False) json = request.get_json(validate=False)
if 'type' not in json: if not json or 'type' not in json:
raise ValidationError('Resource needs a type.') raise ValidationError('Resource needs a type.')
e = app.resources[json['type']].schema.load(json) # todo there should be a way to better get subclassess resource
# defs
resource_def = app.resources[json['type']]
e = resource_def.schema.load(json)
if json['type'] == Snapshot.t:
return self.snapshot(e, resource_def)
Model = db.Model._decl_class_registry.data[json['type']]() Model = db.Model._decl_class_registry.data[json['type']]()
event = Model(**e) event = Model(**e)
db.session.add(event) db.session.add(event)
@ -34,25 +41,20 @@ class EventView(View):
event = Event.query.filter_by(id=id).one() event = Event.query.filter_by(id=id).one()
return self.schema.jsonify(event) return self.schema.jsonify(event)
def snapshot(self, snapshot_json: dict, resource_def):
SUPPORTED_WORKBENCH = StrictVersion('11.0')
class SnapshotView(View):
def post(self):
""" """
Performs a Snapshot. Performs a Snapshot.
See `Snapshot` section in docs for more info. See `Snapshot` section in docs for more info.
""" """
s = request.get_json()
# Note that if we set the device / components into the snapshot # Note that if we set the device / components into the snapshot
# model object, when we flush them to the db we will flush # model object, when we flush them to the db we will flush
# snapshot, and we want to wait to flush snapshot at the end # snapshot, and we want to wait to flush snapshot at the end
device = s.pop('device') # type: Computer device = snapshot_json.pop('device') # type: Computer
components = s.pop('components') \ components = None
if s['software'] == SnapshotSoftware.Workbench else None # type: List[Component] if snapshot_json['software'] == SnapshotSoftware.Workbench:
snapshot = Snapshot(**s) components = snapshot_json.pop('components') # type: List[Component]
snapshot = Snapshot(**snapshot_json)
# Remove new events from devices so they don't interfere with sync # Remove new events from devices so they don't interfere with sync
events_device = set(e for e in device.events_one) events_device = set(e for e in device.events_one)
@ -62,10 +64,9 @@ class SnapshotView(View):
for component in components: for component in components:
component.events_one.clear() component.events_one.clear()
# noinspection PyArgumentList
assert not device.events_one assert not device.events_one
assert all(not c.events_one for c in components) if components else True assert all(not c.events_one for c in components) if components else True
db_device, remove_events = self.resource_def.sync.run(device, components) db_device, remove_events = resource_def.sync.run(device, components)
snapshot.device = db_device snapshot.device = db_device
snapshot.events |= remove_events | events_device # Set events to snapshot snapshot.events |= remove_events | events_device # Set events to snapshot
# commit will change the order of the components by what # commit will change the order of the components by what

View File

@ -24,6 +24,7 @@ class User(Thing):
backref=db.backref('users', lazy=True, collection_class=set), backref=db.backref('users', lazy=True, collection_class=set),
secondary=lambda: UserInventory.__table__, secondary=lambda: UserInventory.__table__,
collection_class=set) collection_class=set)
# todo set restriction that user has, at least, one active db # todo set restriction that user has, at least, one active db
def __init__(self, email, password=None, inventories=None) -> None: def __init__(self, email, password=None, inventories=None) -> None:
@ -41,6 +42,10 @@ class User(Thing):
def __repr__(self) -> str: def __repr__(self) -> str:
return '<User {0.email}>'.format(self) return '<User {0.email}>'.format(self)
@property
def type(self) -> str:
return self.__class__.__name__
@property @property
def individual(self): def individual(self):
"""The individual associated for this database, or None.""" """The individual associated for this database, or None."""

2546
file.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@ requests[security]==2.19.1
requests-mock==1.5.2 requests-mock==1.5.2
SQLAlchemy==1.2.17 SQLAlchemy==1.2.17
SQLAlchemy-Utils==0.33.11 SQLAlchemy-Utils==0.33.11
teal==0.2.0a36 teal==0.2.0a37
webargs==4.0.0 webargs==4.0.0
Werkzeug==0.14.1 Werkzeug==0.14.1
sqlalchemy-citext==1.3.post0 sqlalchemy-citext==1.3.post0

View File

@ -29,7 +29,7 @@ setup(
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
install_requires=[ install_requires=[
'teal>=0.2.0a36', # teal always first 'teal>=0.2.0a37', # teal always first
'click', 'click',
'click-spinner', 'click-spinner',
'ereuse-utils[naming, test, session, cli]>=0.4b21', 'ereuse-utils[naming, test, session, cli]>=0.4b21',

View File

@ -22,3 +22,4 @@ elapsed: 25
software: Workbench software: Workbench
uuid: 76860eca-c3fd-41f6-a801-6af7bd8cf832 uuid: 76860eca-c3fd-41f6-a801-6af7bd8cf832
version: '11.0' version: '11.0'
type: Snapshot

View File

@ -18,3 +18,4 @@ elapsed: 25
software: Workbench software: Workbench
uuid: f2e02261-87a1-4a50-b9b7-92c0e476e5f2 uuid: f2e02261-87a1-4a50-b9b7-92c0e476e5f2
version: '11.0' version: '11.0'
type: Snapshot

View File

@ -19,3 +19,4 @@ elapsed: 30
software: Workbench software: Workbench
uuid: 3be271b6-5ef4-47d8-8237-5e1133eebfc6 uuid: 3be271b6-5ef4-47d8-8237-5e1133eebfc6
version: '11.0' version: '11.0'
type: Snapshot

View File

@ -18,3 +18,4 @@ elapsed: 25
software: Workbench software: Workbench
uuid: fd007eb4-48e3-454a-8763-169491904c6e uuid: fd007eb4-48e3-454a-8763-169491904c6e
version: '11.0' version: '11.0'
type: Snapshot

View File

@ -21,7 +21,6 @@ def test_api_docs(client: Client):
'/users/', '/users/',
'/devices/', '/devices/',
'/tags/', '/tags/',
'/snapshots/',
'/users/login/', '/users/login/',
'/events/', '/events/',
'/lots/', '/lots/',

30
tests/test_db.py Normal file
View File

@ -0,0 +1,30 @@
import datetime
from uuid import UUID
from teal.db import UniqueViolation
def test_unique_violation():
class IntegrityErrorMock:
def __init__(self) -> None:
self.params = {
'uuid': UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9'),
'version': '11.0',
'software': 'Workbench', 'elapsed': datetime.timedelta(0, 4),
'expected_events': None,
'id': UUID('dbdef3d8-2cac-48cb-adb8-419bc3e59687')
}
def __str__(self):
return """(psycopg2.IntegrityError) duplicate key value violates unique constraint "snapshot_uuid_key"
DETAIL: Key (uuid)=(f5efd26e-8754-46bc-87bf-fbccc39d60d9) already exists.
[SQL: 'INSERT INTO snapshot (uuid, version, software, elapsed, expected_events, id)
VALUES (%(uuid)s, %(version)s, %(software)s, %(elapsed)s, CAST(%(expected_events)s
AS snapshotexpectedevents[]), %(id)s)'] [parameters: {'uuid': UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9'),
'version': '11.0', 'software': 'Workbench', 'elapsed': datetime.timedelta(0, 4), 'expected_events': None,
'id': UUID('dbdef3d8-2cac-48cb-adb8-419bc3e59687')}] (Background on this error at: http://sqlalche.me/e/gkpj)"""
u = UniqueViolation(IntegrityErrorMock())
assert u.constraint == 'snapshot_uuid_key'
assert u.field_name == 'uuid'
assert u.field_value == UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9')