Unify Snapshot's POST view with Devicehub's; bugfixes
This commit is contained in:
parent
32e696c57c
commit
07e8be829e
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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',
|
||||||
|
|
|
@ -5,16 +5,16 @@ device:
|
||||||
type: Desktop
|
type: Desktop
|
||||||
chassis: Tower
|
chassis: Tower
|
||||||
components:
|
components:
|
||||||
- manufacturer: p1c1m
|
- manufacturer: p1c1m
|
||||||
serialNumber: p1c1s
|
serialNumber: p1c1s
|
||||||
type: Motherboard
|
type: Motherboard
|
||||||
- manufacturer: p1c2m
|
- manufacturer: p1c2m
|
||||||
serialNumber: p1c2s
|
serialNumber: p1c2s
|
||||||
model: p1c2
|
model: p1c2
|
||||||
speed: 1.23
|
speed: 1.23
|
||||||
cores: 2
|
cores: 2
|
||||||
type: Processor
|
type: Processor
|
||||||
- manufacturer: p1c3m
|
- manufacturer: p1c3m
|
||||||
serialNumber: p1c3s
|
serialNumber: p1c3s
|
||||||
type: GraphicCard
|
type: GraphicCard
|
||||||
memory: 1.5
|
memory: 1.5
|
||||||
|
@ -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
|
||||||
|
|
|
@ -5,10 +5,10 @@ device:
|
||||||
type: Desktop
|
type: Desktop
|
||||||
chassis: Microtower
|
chassis: Microtower
|
||||||
components:
|
components:
|
||||||
- manufacturer: p2c1m
|
- manufacturer: p2c1m
|
||||||
serialNumber: p2c1s
|
serialNumber: p2c1s
|
||||||
type: Motherboard
|
type: Motherboard
|
||||||
- manufacturer: p1c2m
|
- manufacturer: p1c2m
|
||||||
serialNumber: p1c2s
|
serialNumber: p1c2s
|
||||||
model: p1c2
|
model: p1c2
|
||||||
speed: 1.23
|
speed: 1.23
|
||||||
|
@ -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
|
||||||
|
|
|
@ -5,13 +5,13 @@ device:
|
||||||
type: Desktop
|
type: Desktop
|
||||||
chassis: Microtower
|
chassis: Microtower
|
||||||
components:
|
components:
|
||||||
- manufacturer: p1c2m
|
- manufacturer: p1c2m
|
||||||
serialNumber: p1c2s
|
serialNumber: p1c2s
|
||||||
model: p1c2
|
model: p1c2
|
||||||
type: Processor
|
type: Processor
|
||||||
cores: 2
|
cores: 2
|
||||||
speed: 1.23
|
speed: 1.23
|
||||||
- manufacturer: p1c3m
|
- manufacturer: p1c3m
|
||||||
serialNumber: p1c3s
|
serialNumber: p1c3s
|
||||||
type: GraphicCard
|
type: GraphicCard
|
||||||
memory: 1.5
|
memory: 1.5
|
||||||
|
@ -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
|
||||||
|
|
|
@ -5,12 +5,12 @@ device:
|
||||||
type: Desktop
|
type: Desktop
|
||||||
chassis: Tower
|
chassis: Tower
|
||||||
components:
|
components:
|
||||||
- manufacturer: p1c4m
|
- manufacturer: p1c4m
|
||||||
serialNumber: p1c4s
|
serialNumber: p1c4s
|
||||||
type: NetworkAdapter
|
type: NetworkAdapter
|
||||||
speed: 1000
|
speed: 1000
|
||||||
wireless: False
|
wireless: False
|
||||||
- manufacturer: p1c3m
|
- manufacturer: p1c3m
|
||||||
serialNumber: p1c3s
|
serialNumber: p1c3s
|
||||||
type: GraphicCard
|
type: GraphicCard
|
||||||
memory: 1.5
|
memory: 1.5
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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/',
|
||||||
|
|
|
@ -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')
|
Reference in New Issue