Improve api generation; remove unneeded views

This commit is contained in:
Xavier Bustamante Talavera 2018-06-24 16:57:49 +02:00
parent ac6c94ebe4
commit 10f3aa7d35
13 changed files with 172 additions and 59 deletions

View File

@ -13,6 +13,7 @@ from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, AppRateDe
from ereuse_devicehub.resources.inventory import InventoryDef from ereuse_devicehub.resources.inventory import InventoryDef
from ereuse_devicehub.resources.tag import TagDef from ereuse_devicehub.resources.tag import TagDef
from ereuse_devicehub.resources.user import OrganizationDef, UserDef from ereuse_devicehub.resources.user import OrganizationDef, UserDef
from teal.auth import TokenAuth
from teal.config import Config from teal.config import Config
@ -20,7 +21,7 @@ class DevicehubConfig(Config):
RESOURCE_DEFINITIONS = { RESOURCE_DEFINITIONS = {
DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef,
MicrotowerDef, ComputerMonitorDef, ComponentDef, GraphicCardDef, DataStorageDef, MicrotowerDef, ComputerMonitorDef, ComponentDef, GraphicCardDef, DataStorageDef,
SolidStateDriveDef, SolidStateDriveDef,
HardDriveDef, MotherboardDef, NetworkAdapterDef, RamModuleDef, ProcessorDef, UserDef, HardDriveDef, MotherboardDef, NetworkAdapterDef, RamModuleDef, ProcessorDef, UserDef,
OrganizationDef, TagDef, EventDef, AddDef, RemoveDef, EraseBasicDef, EraseSectorsDef, OrganizationDef, TagDef, EventDef, AddDef, RemoveDef, EraseBasicDef, EraseSectorsDef,
StepDef, StepZeroDef, StepRandomDef, RateDef, AggregateRateDef, WorkbenchRateDef, StepDef, StepZeroDef, StepRandomDef, RateDef, AggregateRateDef, WorkbenchRateDef,
@ -43,6 +44,14 @@ class DevicehubConfig(Config):
It is used by default, for example, when creating tags. It is used by default, for example, when creating tags.
""" """
API_DOC_CONFIG_TITLE = 'Devicehub'
API_DOC_CONFIG_VERSION = '0.2'
API_DOC_CONFIG_COMPONENTS = {
'securitySchemes': {
'bearerAuth': TokenAuth.API_DOCS
}
}
API_DOC_CLASS_DISCRIMINATOR = 'type'
def __init__(self, db: str = None) -> None: def __init__(self, db: str = None) -> None:
if not self.ORGANIZATION_NAME or not self.ORGANIZATION_TAX_ID: if not self.ORGANIZATION_NAME or not self.ORGANIZATION_TAX_ID:

View File

@ -13,64 +13,80 @@ class DeviceDef(Resource):
class ComputerDef(DeviceDef): class ComputerDef(DeviceDef):
VIEW = None
SCHEMA = Computer SCHEMA = Computer
class DesktopDef(ComputerDef): class DesktopDef(ComputerDef):
VIEW = None
SCHEMA = Desktop SCHEMA = Desktop
class LaptopDef(ComputerDef): class LaptopDef(ComputerDef):
VIEW = None
SCHEMA = Laptop SCHEMA = Laptop
class NetbookDef(ComputerDef): class NetbookDef(ComputerDef):
VIEW = None
SCHEMA = Netbook SCHEMA = Netbook
class ServerDef(ComputerDef): class ServerDef(ComputerDef):
VIEW = None
SCHEMA = Server SCHEMA = Server
class MicrotowerDef(ComputerDef): class MicrotowerDef(ComputerDef):
VIEW = None
SCHEMA = Microtower SCHEMA = Microtower
class ComputerMonitorDef(DeviceDef): class ComputerMonitorDef(DeviceDef):
VIEW = None
SCHEMA = ComputerMonitor SCHEMA = ComputerMonitor
class ComponentDef(DeviceDef): class ComponentDef(DeviceDef):
VIEW = None
SCHEMA = Component SCHEMA = Component
class GraphicCardDef(ComponentDef): class GraphicCardDef(ComponentDef):
VIEW = None
SCHEMA = GraphicCard SCHEMA = GraphicCard
class DataStorageDef(ComponentDef): class DataStorageDef(ComponentDef):
VIEW = None
SCHEMA = DataStorage SCHEMA = DataStorage
class HardDriveDef(DataStorageDef): class HardDriveDef(DataStorageDef):
VIEW = None
SCHEMA = HardDrive SCHEMA = HardDrive
class SolidStateDriveDef(DataStorageDef): class SolidStateDriveDef(DataStorageDef):
VIEW = None
SCHEMA = SolidStateDrive SCHEMA = SolidStateDrive
class MotherboardDef(ComponentDef): class MotherboardDef(ComponentDef):
VIEW = None
SCHEMA = Motherboard SCHEMA = Motherboard
class NetworkAdapterDef(ComponentDef): class NetworkAdapterDef(ComponentDef):
VIEW = None
SCHEMA = NetworkAdapter SCHEMA = NetworkAdapter
class RamModuleDef(ComponentDef): class RamModuleDef(ComponentDef):
VIEW = None
SCHEMA = RamModule SCHEMA = RamModule
class ProcessorDef(ComponentDef): class ProcessorDef(ComponentDef):
VIEW = None
SCHEMA = Processor SCHEMA = Processor

View File

@ -18,6 +18,10 @@ from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, c
class Device(Thing): class Device(Thing):
"""
Base class for any type of physical object that can be identified.
"""
id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
id.comment = """ id.comment = """
The identifier of the device for this database. The identifier of the device for this database.
@ -45,12 +49,18 @@ class Device(Thing):
""" """
depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 3)) depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 3))
color = Column(ColorType) color = Column(ColorType)
color.comment = """
"""
@property @property
def events(self) -> list: def events(self) -> list:
""" """
All the events performed to the device, All the events where the device participated, including
ordered by ascending creation time. 1) events performed directly to the device, 2) events performed
to a component, and 3) events performed to a parent device.
Events are returned by ascending creation time.
""" """
return sorted(chain(self.events_multiple, self.events_one), key=attrgetter('created')) return sorted(chain(self.events_multiple, self.events_one), key=attrgetter('created'))

View File

@ -1,7 +1,6 @@
from marshmallow import post_load, pre_load from marshmallow import post_load, pre_load
from marshmallow.fields import Float, Integer, Str from marshmallow.fields import Float, Integer, Str
from marshmallow.validate import Length, OneOf, Range from marshmallow.validate import Length, OneOf, Range
from marshmallow_enum import EnumField
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.marshmallow import NestedOn
@ -9,21 +8,29 @@ from ereuse_devicehub.resources.device import models as m
from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, RamFormat, RamInterface from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes from ereuse_devicehub.resources.schemas import Thing, UnitCodes
from teal.marshmallow import ValidationError from teal.marshmallow import EnumField, ValidationError
class Device(Thing): class Device(Thing):
id = Integer(description=m.Device.id, dump_only=True) id = Integer(description=m.Device.id.comment.strip(), dump_only=True)
hid = Str(dump_only=True, description=m.Device.hid) hid = Str(dump_only=True, description=m.Device.hid.comment.strip())
tags = NestedOn('Tag', many=True, collection_class=OrderedSet) tags = NestedOn('Tag',
many=True,
collection_class=OrderedSet,
description='The set of tags that identify the device.')
model = Str(validate=Length(max=STR_BIG_SIZE)) model = Str(validate=Length(max=STR_BIG_SIZE))
manufacturer = Str(validate=Length(max=STR_SIZE)) manufacturer = Str(validate=Length(max=STR_SIZE))
serial_number = Str(data_key='serialNumber') serial_number = Str(data_key='serialNumber')
product_id = Str(data_key='productId') weight = Float(validate=Range(0.1, 3),
weight = Float(validate=Range(0.1, 3), unit=UnitCodes.kgm, description=m.Device.weight) unit=UnitCodes.kgm,
width = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.width) description=m.Device.weight.comment.strip())
height = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.height) width = Float(validate=Range(0.1, 3),
events = NestedOn('Event', many=True, dump_only=True) unit=UnitCodes.m,
description=m.Device.width.comment.strip())
height = Float(validate=Range(0.1, 3),
unit=UnitCodes.m,
description=m.Device.height.comment.strip())
events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__)
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet) events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
@pre_load @pre_load
@ -76,15 +83,15 @@ class Microtower(Computer):
class ComputerMonitor(Device): class ComputerMonitor(Device):
size = Float(description=m.ComputerMonitor.size.comment, validate=Range(2, 150)) size = Float(description=m.ComputerMonitor.size.comment.strip(), validate=Range(2, 150))
technology = EnumField(ComputerMonitorTechnologies, technology = EnumField(ComputerMonitorTechnologies,
description=m.ComputerMonitor.technology.comment) description=m.ComputerMonitor.technology.comment.strip())
resolution_width = Integer(data_key='resolutionWidth', resolution_width = Integer(data_key='resolutionWidth',
validate=Range(10, 20000), validate=Range(10, 20000),
description=m.ComputerMonitor.resolution_width.comment) description=m.ComputerMonitor.resolution_width.comment.strip())
resolution_height = Integer(data_key='resolutionHeight', resolution_height = Integer(data_key='resolutionHeight',
validate=Range(10, 20000), validate=Range(10, 20000),
description=m.ComputerMonitor.resolution_height.comment) description=m.ComputerMonitor.resolution_height.comment.strip())
class Component(Device): class Component(Device):
@ -101,9 +108,6 @@ class DataStorage(Component):
size = Integer(validate=Range(0, 10 ** 8), size = Integer(validate=Range(0, 10 ** 8),
unit=UnitCodes.mbyte, unit=UnitCodes.mbyte,
description='The size of the hard-drive in MB.') description='The size of the hard-drive in MB.')
erasure = NestedOn('EraseBasic', load_only=True)
tests = NestedOn('TestHardDrive', many=True, load_only=True)
benchmarks = NestedOn('BenchmarkHardDrive', load_only=True, many=True)
class HardDrive(DataStorage): class HardDrive(DataStorage):

View File

@ -3,12 +3,29 @@ from teal.resource import View
class DeviceView(View): class DeviceView(View):
def get(self, id):
"""
Devices view
---
description: Gets a device or multiple devices.
parameters:
- name: id
type: integer
in: path
description: The identifier of the device.
responses:
200:
description: The device or devices.
"""
return super().get(id)
def one(self, id: int): def one(self, id: int):
"""Gets one device.""" """Gets one device."""
device = Device.query.filter_by(id=id).one() device = Device.query.filter_by(id=id).one()
return self.schema.jsonify(device) return self.schema.jsonify(device)
def find(self, args: dict): def find(self, args: dict):
"""Gets many devices""" """Gets many devices."""
devices = Device.query.all() devices = Device.query.all()
return self.schema.jsonify(devices, many=True) return self.schema.jsonify(devices, many=True)

View File

@ -18,62 +18,77 @@ class EventDef(Resource):
class AddDef(EventDef): class AddDef(EventDef):
VIEW = None
SCHEMA = Add SCHEMA = Add
class RemoveDef(EventDef): class RemoveDef(EventDef):
VIEW = None
SCHEMA = Remove SCHEMA = Remove
class EraseBasicDef(EventDef): class EraseBasicDef(EventDef):
VIEW = None
SCHEMA = EraseBasic SCHEMA = EraseBasic
class EraseSectorsDef(EraseBasicDef): class EraseSectorsDef(EraseBasicDef):
VIEW = None
SCHEMA = EraseSectors SCHEMA = EraseSectors
class StepDef(Resource): class StepDef(Resource):
VIEW = None
SCHEMA = Step SCHEMA = Step
class StepZeroDef(StepDef): class StepZeroDef(StepDef):
VIEW = None
SCHEMA = StepZero SCHEMA = StepZero
class StepRandomDef(StepDef): class StepRandomDef(StepDef):
VIEW = None
SCHEMA = StepRandom SCHEMA = StepRandom
class RateDef(EventDef): class RateDef(EventDef):
VIEW = None
SCHEMA = Rate SCHEMA = Rate
class AggregateRateDef(RateDef): class AggregateRateDef(RateDef):
VIEW = None
SCHEMA = AggregateRate SCHEMA = AggregateRate
class WorkbenchRateDef(RateDef): class WorkbenchRateDef(RateDef):
VIEW = None
SCHEMA = WorkbenchRate SCHEMA = WorkbenchRate
class PhotoboxUserDef(RateDef): class PhotoboxUserDef(RateDef):
VIEW = None
SCHEMA = PhotoboxUserRate SCHEMA = PhotoboxUserRate
class PhotoboxSystemRateDef(RateDef): class PhotoboxSystemRateDef(RateDef):
VIEW = None
SCHEMA = PhotoboxSystemRate SCHEMA = PhotoboxSystemRate
class AppRateDef(RateDef): class AppRateDef(RateDef):
VIEW = None
SCHEMA = AppRate SCHEMA = AppRate
class InstallDef(EventDef): class InstallDef(EventDef):
VIEW = None
SCHEMA = Install SCHEMA = Install
class SnapshotDef(EventDef): class SnapshotDef(EventDef):
VIEW = None
SCHEMA = Snapshot SCHEMA = Snapshot
VIEW = SnapshotView VIEW = SnapshotView
@ -86,36 +101,45 @@ class SnapshotDef(EventDef):
class TestDef(EventDef): class TestDef(EventDef):
VIEW = None
SCHEMA = Test SCHEMA = Test
class TestDataStorageDef(TestDef): class TestDataStorageDef(TestDef):
VIEW = None
SCHEMA = TestDataStorage SCHEMA = TestDataStorage
class StressTestDef(TestDef): class StressTestDef(TestDef):
VIEW = None
SCHEMA = StressTest SCHEMA = StressTest
class BenchmarkDef(EventDef): class BenchmarkDef(EventDef):
VIEW = None
SCHEMA = Benchmark SCHEMA = Benchmark
class BenchmarkDataStorageDef(BenchmarkDef): class BenchmarkDataStorageDef(BenchmarkDef):
VIEW = None
SCHEMA = BenchmarkDataStorage SCHEMA = BenchmarkDataStorage
class BenchmarkWithRateDef(BenchmarkDef): class BenchmarkWithRateDef(BenchmarkDef):
VIEW = None
SCHEMA = BenchmarkWithRate SCHEMA = BenchmarkWithRate
class BenchmarkProcessorDef(BenchmarkWithRateDef): class BenchmarkProcessorDef(BenchmarkWithRateDef):
VIEW = None
SCHEMA = BenchmarkProcessor SCHEMA = BenchmarkProcessor
class BenchmarkProcessorSysbenchDef(BenchmarkProcessorDef): class BenchmarkProcessorSysbenchDef(BenchmarkProcessorDef):
VIEW = None
SCHEMA = BenchmarkProcessorSysbench SCHEMA = BenchmarkProcessorSysbench
class BenchmarkRamSysbenchDef(BenchmarkWithRateDef): class BenchmarkRamSysbenchDef(BenchmarkWithRateDef):
VIEW = None
SCHEMA = BenchmarkRamSysbench SCHEMA = BenchmarkRamSysbench

View File

@ -46,8 +46,7 @@ class Event(Thing):
closed.comment = """ closed.comment = """
Whether the author has finished the event. Whether the author has finished the event.
After this is set to True, no modifications are allowed. After this is set to True, no modifications are allowed.
By default events are closed when performed.
By default are events are closed when performed.
""" """
error = Column(Boolean, default=False, nullable=False) error = Column(Boolean, default=False, nullable=False)
error.comment = """ error.comment = """

View File

@ -3,7 +3,6 @@ from marshmallow import ValidationError, validates_schema
from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \ from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \
UUID UUID
from marshmallow.validate import Length, Range from marshmallow.validate import Length, Range
from marshmallow_enum import EnumField
from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.device.schemas import Component, Device from ereuse_devicehub.resources.device.schemas import Component, Device
@ -13,21 +12,23 @@ from ereuse_devicehub.resources.event import models as m
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.user.schemas import User from ereuse_devicehub.resources.user.schemas import User
from teal.marshmallow import Version from teal.marshmallow import EnumField, Version
from teal.resource import Schema from teal.resource import Schema
class Event(Thing): class Event(Thing):
id = UUID(dump_only=True) id = UUID(dump_only=True)
name = String(default='', validate=Length(STR_BIG_SIZE), description=m.Event.name.comment) name = String(default='',
date = DateTime('iso', description=m.Event.date.comment) validate=Length(STR_BIG_SIZE),
error = Boolean(default=False, description=m.Event.error.comment) description=m.Event.name.comment.strip())
incidence = Boolean(default=False, description=m.Event.incidence.comment) date = DateTime('iso', description=m.Event.date.comment.strip())
error = Boolean(default=False, description=m.Event.error.comment.strip())
incidence = Boolean(default=False, description=m.Event.incidence.comment.strip())
snapshot = NestedOn('Snapshot', dump_only=True) snapshot = NestedOn('Snapshot', dump_only=True)
components = NestedOn(Component, dump_only=True, many=True) components = NestedOn(Component, dump_only=True, many=True)
description = String(default='', description=m.Event.description.comment) description = String(default='', description=m.Event.description.comment.strip())
author = NestedOn(User, dump_only=True, exclude=('token',)) author = NestedOn(User, dump_only=True, exclude=('token',))
closed = Boolean(missing=True, description=m.Event.closed.comment) closed = Boolean(missing=True, description=m.Event.closed.comment.strip())
class EventWithOneDevice(Event): class EventWithOneDevice(Event):

View File

@ -8,8 +8,7 @@ from sqlalchemy.util import OrderedSet
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Component, Computer from ereuse_devicehub.resources.device.models import Component, Computer
from ereuse_devicehub.resources.enums import RatingSoftware, SnapshotSoftware from ereuse_devicehub.resources.enums import RatingSoftware, SnapshotSoftware
from ereuse_devicehub.resources.event.models import Event, ManualRate, Snapshot, TestDataStorage, \ from ereuse_devicehub.resources.event.models import Event, ManualRate, Snapshot, WorkbenchRate
WorkbenchRate
from teal.resource import View from teal.resource import View
@ -85,16 +84,3 @@ class SnapshotView(View):
ret = self.schema.jsonify(snapshot) # transform it back ret = self.schema.jsonify(snapshot) # transform it back
ret.status_code = 201 ret.status_code = 201
return ret return ret
class TestHardDriveView(View):
def post(self):
t = request.get_json() # type: dict
# noinspection PyArgumentList
test = TestDataStorage(snapshot_id=t.pop('snapshot'), device_id=t.pop('device'), **t)
return test
class StressTestView(View):
def post(self):
t = request.get_json() # type: dict

View File

@ -58,13 +58,39 @@ class InventoryView(View):
sort = Nested(Sorting, missing=[Device.created.desc()]) sort = Nested(Sorting, missing=[Device.created.desc()])
page = Integer(validate=Range(min=1), missing=1) page = Integer(validate=Range(min=1), missing=1)
def find(self, args: dict): def get(self, id):
"""Inventory view
---
description: Supports the inventory view of ``devicehub-client``; returns
all the devices, groups and widgets of this Devicehub instance.
responses:
200:
description: The inventory.
schema:
type: object
properties:
devices:
type: array
items:
$ref: '#/definitions/Device'
pagination:
type: object
properties:
page:
type: integer
minimum: 0
perPage:
type: integer
minimum: 0
total:
type: integer
minimum: 0
""" """
Supports the inventory view of ``devicehub-client``; returns # todo .format(yaml.load(schema2parameters(self.FindArgs, default_in='path', name='path')))
all the devices, groups and widgets of this Devicehub instance. return super().get(id)
The result can be filtered, sorted, and paginated. def find(self, args: dict):
""" """See :meth:`.get` above."""
devices = Device.query \ devices = Device.query \
.filter(*args['filter']) \ .filter(*args['filter']) \
.order_by(*args['sort']) \ .order_by(*args['sort']) \

View File

@ -22,8 +22,8 @@ class Thing(Schema):
type = String(description='Only required when it is nested.') type = String(description='Only required when it is nested.')
url = URL(dump_only=True, description='The URL of the resource.') url = URL(dump_only=True, description='The URL of the resource.')
same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs') same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs')
updated = DateTime('iso', dump_only=True, description=m.Thing.updated) updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment.strip())
created = DateTime('iso', dump_only=True, description=m.Thing.created) created = DateTime('iso', dump_only=True, description=m.Thing.created.comment.strip())
@post_load @post_load
def remove_type(self, data: dict): def remove_type(self, data: dict):

View File

@ -1,6 +1,6 @@
import pytest import pytest
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.client import Client
def test_dependencies(): def test_dependencies():
@ -12,6 +12,27 @@ def test_dependencies():
# noinspection PyArgumentList # noinspection PyArgumentList
def test_init(app: Devicehub): def test_api_docs(client: Client):
"""Tests app initialization.""" """Tests /apidocs correct initialization."""
pass docs, _ = client.get('/apidocs')
assert set(docs['paths'].keys()) == {
'/tags/{id}/device',
'/inventories/',
'/apidocs',
'/users/',
'/devices/',
'/tags/',
'/snapshots/',
'/users/login',
'/events/'
}
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
assert docs['components']['securitySchemes']['bearerAuth'] == {
'description': 'Basic scheme with token.',
'in': 'header',
'description:': 'HTTP Basic scheme',
'type': 'http',
'scheme': 'basic',
'name': 'Authorization'
}
assert len(docs['definitions']) == 46

View File

@ -313,13 +313,13 @@ def test_erase(user: UserClient):
snapshot = snapshot_and_check(user, s, ('EraseSectors',), perform_second_snapshot=True) snapshot = snapshot_and_check(user, s, ('EraseSectors',), perform_second_snapshot=True)
storage, *_ = snapshot['components'] storage, *_ = snapshot['components']
assert storage['type'] == 'SolidStateDrive', 'Components must be ordered by input order' assert storage['type'] == 'SolidStateDrive', 'Components must be ordered by input order'
storage, _ = user.get(res=SolidStateDrive, item=storage['id']) # Let's get storage events too storage, _ = user.get(res=Device, item=storage['id']) # Let's get storage events too
# order: creation time descending # order: creation time descending
_snapshot1, erasure1, _snapshot2, erasure2 = storage['events'] _snapshot1, erasure1, _snapshot2, erasure2 = storage['events']
assert erasure1['type'] == erasure2['type'] == 'EraseSectors' assert erasure1['type'] == erasure2['type'] == 'EraseSectors'
assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot' assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot'
assert snapshot == user.get(res=Event, item=_snapshot2['id'])[0] assert snapshot == user.get(res=Event, item=_snapshot2['id'])[0]
erasure, _ = user.get(res=EraseBasic, item=erasure1['id']) erasure, _ = user.get(res=Event, item=erasure1['id'])
assert len(erasure['steps']) == 2 assert len(erasure['steps']) == 2
assert erasure['steps'][0]['startTime'] == '2018-06-01T08:15:00+00:00' assert erasure['steps'][0]['startTime'] == '2018-06-01T08:15:00+00:00'
assert erasure['steps'][0]['endTime'] == '2018-06-01T09:16:00+00:00' assert erasure['steps'][0]['endTime'] == '2018-06-01T09:16:00+00:00'