Add computer monitor; dummy; fix

This commit is contained in:
Xavier Bustamante Talavera 2018-06-20 23:18:15 +02:00
parent 7306055d28
commit 21d1a96aff
23 changed files with 531 additions and 97 deletions

View file

@ -44,12 +44,23 @@ $ sudo -u postgres -i
postgres$ createdb dh-db1 postgres$ createdb dh-db1
``` ```
Then execute, in the same directory where `app.py` is:
```bash
$ flask init-db
```
This creates the tables in the database you created before.
And then execute, in the same directory where `app.py` is: Finally, run the app:
```bash ```bash
$ flask run $ flask run
``` ```
See the [Flask quickstart](http://flask.pocoo.org/docs/1.0/quickstart/) See the [Flask quickstart](http://flask.pocoo.org/docs/1.0/quickstart/)
for more info. for more info.
Devicehub has many commands that allows you to administrate it. You
can, for example, create a dummy database of devices with ``flask dummy``
or create users with ``flask create-user``. See all the
available commands by just executing ``flask``.

View file

@ -1,14 +1,14 @@
from distutils.version import StrictVersion from distutils.version import StrictVersion
from typing import Set from typing import Set
from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DataStorageDef, \ from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, ComputerMonitorDef, \
DesktopDef, DeviceDef, GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, \ DataStorageDef, DesktopDef, DeviceDef, GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, \
MotherboardDef, NetbookDef, NetworkAdapterDef, ProcessorDef, RamModuleDef, ServerDef, \ MotherboardDef, NetbookDef, NetworkAdapterDef, ProcessorDef, RamModuleDef, ServerDef, \
SolidStateDriveDef SolidStateDriveDef
from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, BenchmarkDataStorageDef, \ from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, AppRateDef, \
BenchmarkDef, BenchmarkProcessorDef, BenchmarkProcessorSysbenchDef, BenchmarkRamSysbenchDef, \ BenchmarkDataStorageDef, BenchmarkDef, BenchmarkProcessorDef, BenchmarkProcessorSysbenchDef, \
BenchmarkWithRateDef, EraseBasicDef, EraseSectorsDef, EventDef, InstallDef, \ BenchmarkRamSysbenchDef, BenchmarkWithRateDef, EraseBasicDef, EraseSectorsDef, EventDef, \
PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \ InstallDef, PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \
StepRandomDef, StepZeroDef, StressTestDef, TestDataStorageDef, TestDef, WorkbenchRateDef StepRandomDef, StepZeroDef, StressTestDef, TestDataStorageDef, TestDef, WorkbenchRateDef
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
@ -19,13 +19,14 @@ from teal.config import Config
class DevicehubConfig(Config): class DevicehubConfig(Config):
RESOURCE_DEFINITIONS = { RESOURCE_DEFINITIONS = {
DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef,
MicrotowerDef, ComponentDef, GraphicCardDef, DataStorageDef, SolidStateDriveDef, MicrotowerDef, ComputerMonitorDef, ComponentDef, GraphicCardDef, DataStorageDef,
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,
PhotoboxUserDef, PhotoboxSystemRateDef, InstallDef, SnapshotDef, TestDef, PhotoboxUserDef, PhotoboxSystemRateDef, InstallDef, SnapshotDef, TestDef,
TestDataStorageDef, StressTestDef, WorkbenchRateDef, InventoryDef, BenchmarkDef, TestDataStorageDef, StressTestDef, WorkbenchRateDef, InventoryDef, BenchmarkDef,
BenchmarkDataStorageDef, BenchmarkWithRateDef, BenchmarkProcessorDef, BenchmarkDataStorageDef, BenchmarkWithRateDef, AppRateDef, BenchmarkProcessorDef,
BenchmarkProcessorSysbenchDef, BenchmarkRamSysbenchDef BenchmarkProcessorSysbenchDef, BenchmarkRamSysbenchDef
} }
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str] PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]

View file

@ -5,12 +5,14 @@ from flask_sqlalchemy import SQLAlchemy
from ereuse_devicehub.auth import Auth from ereuse_devicehub.auth import Auth
from ereuse_devicehub.client import Client from ereuse_devicehub.client import Client
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.dummy.dummy import Dummy
from teal.config import Config as ConfigClass from teal.config import Config as ConfigClass
from teal.teal import Teal from teal.teal import Teal
class Devicehub(Teal): class Devicehub(Teal):
test_client_class = Client test_client_class = Client
Dummy = Dummy
def __init__(self, def __init__(self,
config: ConfigClass, config: ConfigClass,
@ -29,5 +31,4 @@ class Devicehub(Teal):
super().__init__(config, db, import_name, static_url_path, static_folder, static_host, super().__init__(config, db, import_name, static_url_path, static_folder, static_host,
host_matching, subdomain_matching, template_folder, instance_path, host_matching, subdomain_matching, template_folder, instance_path,
instance_relative_config, root_path, Auth) instance_relative_config, root_path, Auth)
self.dummy = Dummy(self)

View file

View file

@ -0,0 +1,62 @@
from pathlib import Path
import click
import click_spinner
import yaml
from tqdm import tqdm
from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.event.models import Snapshot
from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.resources.user import User
class Dummy:
SNAPSHOTS = (
'workbench-server-1',
'computer-monitor'
)
TAGS = (
'tag1',
'tag2',
'tag3'
)
def __init__(self, app) -> None:
super().__init__()
self.app = app
self.app.cli.command('dummy',
short_help='Creates dummy devices and users.')(self.run)
@click.confirmation_option(prompt='This command deletes the DB in the process. '
'Do you want to continue?')
def run(self):
print('Preparing the database...')
with click_spinner.spinner():
self.app.init_db(erase=True)
user = self.user_client('user@dhub.com', '1234')
user.post(res=Tag, query=[('ids', i) for i in self.TAGS], data={})
print('Creating devices...')
for file_name in tqdm(self.SNAPSHOTS):
snapshot = self.file(file_name)
user.post(res=Snapshot, data=snapshot)
print('Done :-)')
def user_client(self, email: str, password: str):
user = User(email=email, password=password)
db.session.add(user)
db.session.commit()
client = UserClient(application=self.app,
response_wrapper=self.app.response_class,
email=user.email,
password=password)
client.user, _ = client.login(client.email, client.password)
return client
def file(self, name: str):
with Path(__file__) \
.parent \
.joinpath('files') \
.joinpath(name + '.snapshot.yaml').open() as f:
return yaml.load(f)

View file

@ -0,0 +1,17 @@
type: Snapshot
software: AndroidApp
version: '1.0'
device:
type: ComputerMonitor
technology: LCD
manufacturer: Dell
model: 1707FPF
serialNumber: CN0FP446728728541C8S
resolutionWidth: 1920
resolutionHeight: 1080
size: 21.5
events:
- type: AppRate
appearanceRange: A
functionalityRange: C
labelling: False

View file

@ -0,0 +1,118 @@
# A Snapshot Phase 1 with a device
# and 1 GraphicCard, 2 RamModule, 1 Processor, 1 SSD, 1 HDD, 1 Motherboard
# Prerequisites:
# - 2 tags: tag1 and tag2 from the default org
# All numbers are invented
type: Snapshot
uuid: cb8ce6b5-6a1b-4084-b5b9-d8fadad2a015
version: '11.0'
software: Workbench
elapsed: 500
device:
type: Microtower
serialNumber: d1s
model: d1ml
manufacturer: d1mr
tags:
- type: Tag
id: tag1
events:
- type: WorkbenchRate
appearanceRange: A
functionalityRange: B
- type: BenchmarkRamSysbench
rate: 2444
- type: StressTest
elapsed: 300
error: False
components:
- type: GraphicCard
serialNumber: gc1-1s
model: gc1-1ml
manufacturer: gc1-1mr
- type: RamModule
serialNumber: rm1-1s
model: rm1-1ml
manufacturer: rm1-1mr
- type: RamModule
serialNumber: rm2-1s
model: rm2-1ml
manufacturer: rm2-1mr
- type: Processor
model: p1-1s
manufacturer: p1-1mr
events:
- type: BenchmarkProcessor
rate: 2410
- type: BenchmarkProcessorSysbench
rate: 4400
- type: SolidStateDrive
serialNumber: ssd1-1s
model: ssd1-1ml
manufacturer: ssd1-1mr
events:
- type: BenchmarkDataStorage
readSpeed: 20
writeSpeed: 15
elapsed: 21
- type: TestDataStorage
elapsed: 233
firstError: 0
error: False
status: Completed without error
length: Short
lifetime: 99
passedLifetime: 99
assessment: True
powerCycleCount: 11
reallocatedSectorCount: 2
powerCycleCount: 4
reportedUncorrectableErrors: 1
commandTimeout: 11
currentPendingSectorCount: 1
offlineUncorrectable: 33
remainingLifetimePercentage: 1
- type: EraseSectors
error: False
cleanWithZeros: False
startTime: 2018-01-01T10:10:10
endTime: 2018-01-01T12:10:10
secureRandomSteps: 0
steps:
- type: StepRandom
startTime: 2018-01-01T10:10:10
endTime: 2018-01-01T12:10:10
error: False
cleanWithZeros: False
secureRandomSteps: 0
- type: HardDrive
serialNumber: hdd1-1s
model: hdd1-1ml
manufacturer: hdd1-1mr
events:
- type: BenchmarkDataStorage
readSpeed: 10
writeSpeed: 5
- type: EraseSectors
error: False
cleanWithZeros: False
startTime: 2018-01-01T10:10:10
endTime: 2018-01-01T12:10:10
secureRandomSteps: 0
steps:
- type: StepRandom
startTime: 2018-01-01T10:10:10
endTime: 2018-01-01T12:10:10
error: False
cleanWithZeros: False
secureRandomSteps: 0
- type: Install
elapsed: 420
error: False
name: LinuxMint 18.01 32b
- type: Motherboard
serialNumber: mb1-1s
model: mb1-1ml
manufacturer: mb1-1mr

View file

@ -1,6 +1,6 @@
from ereuse_devicehub.resources.device.schemas import Component, Computer, DataStorage, Desktop, \ from ereuse_devicehub.resources.device.schemas import Component, Computer, ComputerMonitor, \
Device, GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, \ DataStorage, Desktop, Device, GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, \
Processor, RamModule, Server, SolidStateDrive NetworkAdapter, Processor, RamModule, Server, SolidStateDrive
from ereuse_devicehub.resources.device.views import DeviceView from ereuse_devicehub.resources.device.views import DeviceView
from teal.resource import Converters, Resource from teal.resource import Converters, Resource
@ -36,6 +36,10 @@ class MicrotowerDef(ComputerDef):
SCHEMA = Microtower SCHEMA = Microtower
class ComputerMonitorDef(DeviceDef):
SCHEMA = ComputerMonitor
class ComponentDef(DeviceDef): class ComponentDef(DeviceDef):
SCHEMA = Component SCHEMA = Component

View file

@ -10,7 +10,8 @@ from sqlalchemy.orm import ColumnProperty, backref, relationship
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from sqlalchemy_utils import ColorType from sqlalchemy_utils import ColorType
from ereuse_devicehub.resources.enums import DataStorageInterface, RamFormat, RamInterface from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, DataStorageInterface, \
RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
from ereuse_utils.naming import Naming from ereuse_utils.naming import Naming
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range
@ -18,14 +19,30 @@ from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, c
class Device(Thing): class Device(Thing):
id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
id.comment = """
The identifier of the device for this database.
"""
type = Column(Unicode(STR_SM_SIZE), nullable=False) type = Column(Unicode(STR_SM_SIZE), nullable=False)
hid = Column(Unicode(STR_BIG_SIZE), unique=True) hid = Column(Unicode(STR_BIG_SIZE), unique=True)
hid.comment = """
The Hardware ID (HID) is the unique ID traceability systems
use to ID a device globally.
"""
model = Column(Unicode(STR_BIG_SIZE)) model = Column(Unicode(STR_BIG_SIZE))
manufacturer = Column(Unicode(STR_SIZE)) manufacturer = Column(Unicode(STR_SIZE))
serial_number = Column(Unicode(STR_SIZE)) serial_number = Column(Unicode(STR_SIZE))
weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 3)) weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 3))
weight.comment = """
The weight of the device in Kgm.
"""
width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 3)) width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 3))
width.comment = """
The width of the device in meters.
"""
height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 3)) height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 3))
height.comment = """
The height of the device in meters.
"""
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)
@ -108,6 +125,28 @@ class Microtower(Computer):
pass pass
class ComputerMonitor(Device):
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
size = Column(Float(decimal_return_scale=2), check_range('size', 2, 150))
size.comment = """
The size of the monitor in inches.
"""
technology = Column(DBEnum(ComputerMonitorTechnologies))
technology.comment = """
The technology the monitor uses to display the image.
"""
resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000))
resolution_width.comment = """
The maximum horizontal resolution the monitor can natively support
in pixels.
"""
resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000))
resolution_height.comment = """
The maximum vertical resolution the monitor can natively support
in pixels.
"""
class Component(Device): class Component(Device):
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)

View file

@ -1,9 +1,10 @@
from typing import Dict, List, Set from typing import Dict, List, Set
from colour import Color from colour import Color
from sqlalchemy import Column from sqlalchemy import Column, Integer
from ereuse_devicehub.resources.enums import DataStorageInterface, RamFormat, RamInterface from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, DataStorageInterface, \
RamFormat, RamInterface
from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \ from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \
EventWithOneDevice EventWithOneDevice
from ereuse_devicehub.resources.image.models import ImageList from ereuse_devicehub.resources.image.models import ImageList
@ -72,6 +73,20 @@ class Microtower(Computer):
pass pass
class ComputerMonitor(Device):
technology = ... # type: Column
size = ... # type: Column
resolution_width = ... # type: Column
resolution_height = ... # type: Column
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
technology = ... # type: ComputerMonitorTechnologies
size = ... # type: Integer
resolution_width = ... # type: int
resolution_height = ... # type: int
class Component(Device): class Component(Device):
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)

View file

@ -1,36 +1,28 @@
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.enums import RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
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 marshmallow_enum import EnumField
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.device import models as m
from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
from teal.marshmallow import ValidationError from teal.marshmallow import ValidationError
class Device(Thing): class Device(Thing):
# todo id is dump_only except when in Snapshot id = Integer(description=m.Device.id, dump_only=True)
id = Integer(description='The identifier of the device for this database.') hid = Str(dump_only=True, description=m.Device.hid)
hid = Str(dump_only=True,
description='The Hardware ID is the unique ID traceability systems '
'use to ID a device globally.')
tags = NestedOn('Tag', many=True, collection_class=OrderedSet) tags = NestedOn('Tag', many=True, collection_class=OrderedSet)
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') 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='The weight of the device in Kgm.') height = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.height)
width = Float(validate=Range(0.1, 3),
unit=UnitCodes.m,
description='The width of the device in meters.')
height = Float(validate=Range(0.1, 3),
unit=UnitCodes.m,
description='The height of the device in meters.')
events = NestedOn('Event', many=True, dump_only=True) events = NestedOn('Event', many=True, dump_only=True)
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet) events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
@ -61,7 +53,6 @@ class Device(Thing):
class Computer(Device): class Computer(Device):
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet) components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet)
pass
class Desktop(Computer): class Desktop(Computer):
@ -84,6 +75,18 @@ class Microtower(Computer):
pass pass
class ComputerMonitor(Device):
size = Float(description=m.ComputerMonitor.size.comment, validate=Range(2, 150))
technology = EnumField(ComputerMonitorTechnologies,
description=m.ComputerMonitor.technology.comment)
resolution_width = Integer(data_key='resolutionWidth',
validate=Range(10, 20000),
description=m.ComputerMonitor.resolution_width.comment)
resolution_height = Integer(data_key='resolutionHeight',
validate=Range(10, 20000),
description=m.ComputerMonitor.resolution_height.comment)
class Component(Device): class Component(Device):
parent = NestedOn(Device, dump_only=True) parent = NestedOn(Device, dump_only=True)

View file

@ -149,3 +149,14 @@ class DataStorageInterface(Enum):
ATA = 'ATA' ATA = 'ATA'
USB = 'USB' USB = 'USB'
PCI = 'PCI' PCI = 'PCI'
@unique
class ComputerMonitorTechnologies(Enum):
CRT = 'Cathode ray tube (CRT)'
TFT = 'Thin-film-transistor liquid-crystal (TFT)'
LED = 'LED-backlit (LED)'
PDP = 'Plasma display panel (Plasma)'
LCD = 'Liquid-crystal display (any of TFT, LED, Blue Phase, IPS)'
OLED = 'Organic light-emitting diode (OLED)'
AMOLED = 'Organic light-emitting diode (AMOLED)'

View file

@ -1,7 +1,7 @@
from typing import Callable, Iterable, Tuple from typing import Callable, Iterable, Tuple
from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.device.sync import Sync
from ereuse_devicehub.resources.event.schemas import Add, AggregateRate, Benchmark, \ from ereuse_devicehub.resources.event.schemas import Add, AggregateRate, AppRate, Benchmark, \
BenchmarkDataStorage, BenchmarkProcessor, BenchmarkProcessorSysbench, BenchmarkRamSysbench, \ BenchmarkDataStorage, BenchmarkProcessor, BenchmarkProcessorSysbench, BenchmarkRamSysbench, \
BenchmarkWithRate, EraseBasic, EraseSectors, Event, Install, PhotoboxSystemRate, \ BenchmarkWithRate, EraseBasic, EraseSectors, Event, Install, PhotoboxSystemRate, \
PhotoboxUserRate, Rate, Remove, Snapshot, Step, StepRandom, StepZero, StressTest, Test, \ PhotoboxUserRate, Rate, Remove, Snapshot, Step, StepRandom, StepZero, StressTest, Test, \
@ -65,6 +65,10 @@ class PhotoboxSystemRateDef(RateDef):
SCHEMA = PhotoboxSystemRate SCHEMA = PhotoboxSystemRate
class AppRateDef(RateDef):
SCHEMA = AppRate
class InstallDef(EventDef): class InstallDef(EventDef):
SCHEMA = Install SCHEMA = Install

View file

@ -2,14 +2,6 @@ from collections import Iterable
from typing import Set, Union from typing import Set, Union
from uuid import uuid4 from uuid import uuid4
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Device
from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \
FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
from ereuse_devicehub.resources.image.models import Image
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User
from flask import g from flask import g
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \ from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \
Float, ForeignKey, Interval, JSON, SmallInteger, Unicode, event Float, ForeignKey, Interval, JSON, SmallInteger, Unicode, event
@ -20,6 +12,14 @@ from sqlalchemy.orm import backref, relationship
from sqlalchemy.orm.events import AttributeEvents as Events from sqlalchemy.orm.events import AttributeEvents as Events
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Device
from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \
FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
from ereuse_devicehub.resources.image.models import Image
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User
from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \ from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \
POLYMORPHIC_ON, StrictVersionType, check_range POLYMORPHIC_ON, StrictVersionType, check_range
@ -256,15 +256,18 @@ class StepRandom(Step):
class Snapshot(JoinedTableMixin, EventWithOneDevice): class Snapshot(JoinedTableMixin, EventWithOneDevice):
uuid = Column(UUID(as_uuid=True), nullable=False, unique=True) uuid = Column(UUID(as_uuid=True), unique=True)
version = Column(StrictVersionType(STR_SM_SIZE), nullable=False) version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
software = Column(DBEnum(SnapshotSoftware), nullable=False) software = Column(DBEnum(SnapshotSoftware), nullable=False)
elapsed = Column(Interval, nullable=False) elapsed = Column(Interval)
elapsed.comment = """
For Snapshots made with Workbench, the total amount of time
it took to complete.
"""
expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents))) expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents)))
class Install(JoinedTableMixin, EventWithOneDevice): class Install(JoinedTableMixin, EventWithOneDevice):
name = Column(Unicode(STR_BIG_SIZE), nullable=False)
elapsed = Column(Interval, nullable=False) elapsed = Column(Interval, nullable=False)
@ -290,6 +293,20 @@ class Rate(JoinedTableMixin, EventWithOneDevice):
def rating_range(self) -> RatingRange: def rating_range(self) -> RatingRange:
return RatingRange.from_score(self.rating) return RatingRange.from_score(self.rating)
@declared_attr
def __mapper_args__(cls):
"""
Defines inheritance.
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
extensions/declarative/api.html
#sqlalchemy.ext.declarative.declared_attr>`_
"""
args = {POLYMORPHIC_ID: cls.t}
if cls.t == 'Rate':
args[POLYMORPHIC_ON] = cls.type
return args
class IndividualRate(Rate): class IndividualRate(Rate):
pass pass
@ -319,18 +336,26 @@ class RateAggregateRate(db.Model):
primary_key=True) primary_key=True)
class WorkbenchRate(IndividualRate): class ManualRate(IndividualRate):
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
labelling = Column(Boolean)
appearance_range = Column(DBEnum(AppearanceRange))
functionality_range = Column(DBEnum(FunctionalityRange))
class WorkbenchRate(ManualRate):
id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id), primary_key=True)
processor = Column(Float(decimal_return_scale=2), check_range('processor', *RATE_POSITIVE)) processor = Column(Float(decimal_return_scale=2), check_range('processor', *RATE_POSITIVE))
ram = Column(Float(decimal_return_scale=2), check_range('ram', *RATE_POSITIVE)) ram = Column(Float(decimal_return_scale=2), check_range('ram', *RATE_POSITIVE))
data_storage = Column(Float(decimal_return_scale=2), data_storage = Column(Float(decimal_return_scale=2),
check_range('data_storage', *RATE_POSITIVE)) check_range('data_storage', *RATE_POSITIVE))
graphic_card = Column(Float(decimal_return_scale=2), graphic_card = Column(Float(decimal_return_scale=2),
check_range('graphic_card', *RATE_POSITIVE)) check_range('graphic_card', *RATE_POSITIVE))
labelling = Column(Boolean)
bios = Column(DBEnum(Bios)) bios = Column(DBEnum(Bios))
appearance_range = Column(DBEnum(AppearanceRange))
functionality_range = Column(DBEnum(FunctionalityRange))
class AppRate(ManualRate):
pass
class PhotoboxRate(IndividualRate): class PhotoboxRate(IndividualRate):
@ -369,6 +394,20 @@ class PhotoboxSystemRate(PhotoboxRate):
class Test(JoinedTableMixin, EventWithOneDevice): class Test(JoinedTableMixin, EventWithOneDevice):
elapsed = Column(Interval, nullable=False) elapsed = Column(Interval, nullable=False)
@declared_attr
def __mapper_args__(cls):
"""
Defines inheritance.
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
extensions/declarative/api.html
#sqlalchemy.ext.declarative.declared_attr>`_
"""
args = {POLYMORPHIC_ID: cls.t}
if cls.t == 'Test':
args[POLYMORPHIC_ON] = cls.type
return args
class TestDataStorage(Test): class TestDataStorage(Test):
id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True) id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True)
@ -396,6 +435,20 @@ class StressTest(Test):
class Benchmark(JoinedTableMixin, EventWithOneDevice): class Benchmark(JoinedTableMixin, EventWithOneDevice):
elapsed = Column(Interval) elapsed = Column(Interval)
@declared_attr
def __mapper_args__(cls):
"""
Defines inheritance.
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
extensions/declarative/api.html
#sqlalchemy.ext.declarative.declared_attr>`_
"""
args = {POLYMORPHIC_ID: cls.t}
if cls.t == 'Benchmark':
args[POLYMORPHIC_ON] = cls.type
return args
class BenchmarkDataStorage(Benchmark): class BenchmarkDataStorage(Benchmark):
id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True) id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True)

View file

@ -139,17 +139,26 @@ class AggregateRate(Rate):
self.ratings = ... # type: Set[IndividualRate] self.ratings = ... # type: Set[IndividualRate]
class WorkbenchRate(IndividualRate): class ManualRate(IndividualRate):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.labelling = ... # type: bool
self.appearance_range = ... # type: AppearanceRange
self.functionality_range = ... # type: FunctionalityRange
class WorkbenchRate(ManualRate):
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.processor = ... # type: float self.processor = ... # type: float
self.ram = ... # type: float self.ram = ... # type: float
self.data_storage = ... # type: float self.data_storage = ... # type: float
self.graphic_card = ... # type: float self.graphic_card = ... # type: float
self.labelling = ... # type: bool
self.bios = ... # type: Bios self.bios = ... # type: Bios
self.appearance_range = ... # type: AppearanceRange
self.functionality_range = ... # type: FunctionalityRange
class AppRate(ManualRate):
pass
class PhotoboxRate(IndividualRate): class PhotoboxRate(IndividualRate):

View file

@ -1,3 +1,10 @@
from flask import current_app as app
from marshmallow import ValidationError, validates_schema
from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \
UUID
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
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \ from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
@ -6,13 +13,6 @@ 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 flask import current_app as app
from marshmallow import ValidationError, validates_schema
from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \
UUID
from marshmallow.validate import Length, Range
from marshmallow_enum import EnumField
from teal.marshmallow import Version from teal.marshmallow import Version
from teal.resource import Schema from teal.resource import Schema
@ -124,7 +124,7 @@ class PhotoboxRate(IndividualRate):
# todo Image # todo Image
class PhotoboxUserRate(PhotoboxRate): class PhotoboxUserRate(IndividualRate):
assembling = Integer() assembling = Integer()
parts = Integer() parts = Integer()
buttons = Integer() buttons = Integer()
@ -135,18 +135,11 @@ class PhotoboxUserRate(PhotoboxRate):
dirt = Integer() dirt = Integer()
class PhotoboxSystemRate(PhotoboxRate): class PhotoboxSystemRate(IndividualRate):
pass pass
class WorkbenchRate(IndividualRate): class ManualRate(IndividualRate):
processor = Float()
ram = Float()
data_storage = Float()
graphic_card = Float()
labelling = Boolean(description='Sets if there are labels stuck that should be removed.')
bios = EnumField(Bios, description='How difficult it has been to set the bios to '
'boot from the network.')
appearance_range = EnumField(AppearanceRange, appearance_range = EnumField(AppearanceRange,
required=True, required=True,
data_key='appearanceRange', data_key='appearanceRange',
@ -156,6 +149,20 @@ class WorkbenchRate(IndividualRate):
required=True, required=True,
data_key='functionalityRange', data_key='functionalityRange',
description='Grades the defects of a device that affect its usage.') description='Grades the defects of a device that affect its usage.')
labelling = Boolean(description='Sets if there are labels stuck that should be removed.')
class AppRate(ManualRate):
pass
class WorkbenchRate(ManualRate):
processor = Float()
ram = Float()
data_storage = Float()
graphic_card = Float()
bios = EnumField(Bios, description='How difficult it has been to set the bios to '
'boot from the network.')
class Install(EventWithOneDevice): class Install(EventWithOneDevice):
@ -172,7 +179,7 @@ class Snapshot(EventWithOneDevice):
See docs for more info. See docs for more info.
""" """
uuid = UUID(required=True) uuid = UUID()
software = EnumField(SnapshotSoftware, software = EnumField(SnapshotSoftware,
required=True, required=True,
description='The software that generated this Snapshot.') description='The software that generated this Snapshot.')
@ -185,7 +192,7 @@ class Snapshot(EventWithOneDevice):
'the async Snapshot.') 'the async Snapshot.')
device = NestedOn(Device) device = NestedOn(Device)
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) elapsed = TimeDelta(precision=TimeDelta.SECONDS)
components = NestedOn(Component, components = NestedOn(Component,
many=True, many=True,
description='A list of components that are inside of the device' description='A list of components that are inside of the device'
@ -206,10 +213,29 @@ class Snapshot(EventWithOneDevice):
@validates_schema @validates_schema
def validate_components_only_workbench(self, data: dict): def validate_components_only_workbench(self, data: dict):
if data['software'] != SnapshotSoftware.Workbench: if data['software'] != SnapshotSoftware.Workbench:
if data['components'] is not None: if data.get('components', None) is not None:
raise ValidationError('Only Workbench can add component info', raise ValidationError('Only Workbench can add component info',
field_names=['components']) field_names=['components'])
@validates_schema
def validate_only_workbench_fields(self, data: dict):
"""Ensures workbench has ``elapsed`` and ``uuid`` and no others."""
# todo test
if data['software'] == SnapshotSoftware.Workbench:
if not data.get('uuid', None):
raise ValidationError('Snapshots from Workbench must have uuid',
field_names=['uuid'])
if not data.get('elapsed', None):
raise ValidationError('Snapshots from Workbench must have elapsed',
field_names=['elapsed'])
else:
if data.get('uuid', None):
raise ValidationError('Only Snapshots from Workbench can have uuid',
field_names=['uuid'])
if data.get('elapsed', None):
raise ValidationError('Only Snapshots from Workbench can have elapsed',
field_names=['elapsed'])
class Test(EventWithOneDevice): class Test(EventWithOneDevice):
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)

View file

@ -8,7 +8,8 @@ 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, Snapshot, TestDataStorage, WorkbenchRate from ereuse_devicehub.resources.event.models import Event, ManualRate, Snapshot, TestDataStorage, \
WorkbenchRate
from teal.resource import View from teal.resource import View
@ -40,14 +41,15 @@ class SnapshotView(View):
# 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)
events_components = tuple(set(e for e in component.events_one) for component in components)
device.events_one.clear() device.events_one.clear()
for component in components: if components:
component.events_one.clear() events_components = tuple(set(e for e in c.events_one) for c in components)
for component in components:
component.events_one.clear()
# noinspection PyArgumentList # noinspection PyArgumentList
assert not device.events_one assert not device.events_one
assert all(not c.events_one for c in components) 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 = self.resource_def.sync.run(device, components)
snapshot.device = db_device snapshot.device = db_device
snapshot.events |= remove_events | events_device snapshot.events |= remove_events | events_device
@ -56,19 +58,21 @@ class SnapshotView(View):
ordered_components = OrderedSet(x for x in snapshot.components) ordered_components = OrderedSet(x for x in snapshot.components)
for event in events_device: for event in events_device:
if isinstance(event, ManualRate):
event.algorithm_software = RatingSoftware.Ereuse
event.algorithm_version = StrictVersion('1.0')
if isinstance(event, WorkbenchRate): if isinstance(event, WorkbenchRate):
# todo process workbench rate # todo process workbench rate
event.data_storage = 2 event.data_storage = 2
event.graphic_card = 4 event.graphic_card = 4
event.processor = 1 event.processor = 1
event.algorithm_software = RatingSoftware.Ereuse
event.algorithm_version = StrictVersion('1.0')
# Add the new events to the db-existing devices and components # Add the new events to the db-existing devices and components
db_device.events_one |= events_device db_device.events_one |= events_device
for component, events in zip(ordered_components, events_components): if components:
component.events_one |= events for component, events in zip(ordered_components, events_components):
snapshot.events |= events component.events_one |= events
snapshot.events |= events
db.session.add(snapshot) db.session.add(snapshot)
db.session.commit() db.session.commit()

View file

@ -1,7 +1,6 @@
from click import argument, option from click import argument, option
from flask import current_app as app from flask import current_app as app
from ereuse_devicehub import devicehub
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.user.models import Organization, User from ereuse_devicehub.resources.user.models import Organization, User
from ereuse_devicehub.resources.user.schemas import User as UserS from ereuse_devicehub.resources.user.schemas import User as UserS
@ -16,7 +15,7 @@ class UserDef(Resource):
ID_CONVERTER = Converters.uuid ID_CONVERTER = Converters.uuid
AUTH = True AUTH = True
def __init__(self, app: 'devicehub.Devicehub', import_name=__package__, static_folder=None, def __init__(self, app, import_name=__package__, static_folder=None,
static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None,
url_defaults=None, root_path=None): url_defaults=None, root_path=None):
cli_commands = ((self.create_user, 'create-user'),) cli_commands = ((self.create_user, 'create-user'),)

View file

@ -11,14 +11,16 @@ setup(
include_package_data=True, include_package_data=True,
description='A system to manage devices focusing reuse.', description='A system to manage devices focusing reuse.',
install_requires=[ install_requires=[
'teal>=0.2.0a1', 'teal>=0.2.0a2',
'marshmallow_enum', 'marshmallow_enum',
'ereuse-utils [Naming]>=0.3.0b1', 'ereuse-utils [Naming]>=0.3.0b2',
'psycopg2-binary', 'psycopg2-binary',
'sqlalchemy-utils', 'sqlalchemy-utils',
'requests', 'requests',
'requests-toolbelt', 'requests-toolbelt',
'hashids' 'hashids',
'tqdm',
'click-spinner'
], ],
tests_requires=[ tests_requires=[
'pytest', 'pytest',

View file

@ -53,11 +53,12 @@ def app_context(app: Devicehub):
def user(app: Devicehub) -> UserClient: def user(app: Devicehub) -> UserClient:
"""Gets a client with a logged-in dummy user.""" """Gets a client with a logged-in dummy user."""
with app.app_context(): with app.app_context():
user = create_user() password = 'foo'
user = create_user(password=password)
client = UserClient(application=app, client = UserClient(application=app,
response_wrapper=app.response_class, response_wrapper=app.response_class,
email=user.email, email=user.email,
password='foo') password=password)
client.user, _ = client.login(client.email, client.password) client.user, _ = client.login(client.email, client.password)
return client return client

View file

@ -0,0 +1,17 @@
type: Snapshot
software: AndroidApp
version: '1.0'
device:
type: ComputerMonitor
technology: LCD
manufacturer: Dell
model: 1707FPF
serialNumber: CN0FP446728728541C8S
resolutionWidth: 1920
resolutionHeight: 1080
size: 21.5
events:
- type: AppRate
appearanceRange: A
functionalityRange: C
labelling: False

View file

@ -10,11 +10,12 @@ from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.device.exceptions import NeedsId from ereuse_devicehub.resources.device.exceptions import NeedsId
from ereuse_devicehub.resources.device.models import Component, Computer, Desktop, Device, \ from ereuse_devicehub.resources.device.models import Component, Computer, ComputerMonitor, Desktop, \
GraphicCard, Laptop, Microtower, Motherboard, NetworkAdapter Device, GraphicCard, Laptop, Microtower, Motherboard, NetworkAdapter
from ereuse_devicehub.resources.device.schemas import Device as DeviceS from ereuse_devicehub.resources.device.schemas import Device as DeviceS
from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \ from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \
Sync Sync
from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies
from ereuse_devicehub.resources.event.models import Remove, Test from ereuse_devicehub.resources.event.models import Remove, Test
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.resources.user import User from ereuse_devicehub.resources.user import User
@ -396,3 +397,16 @@ def test_get_devices(app: Devicehub, user: UserClient):
assert tuple(d['id'] for d in devices) == (1, 2, 3, 4, 5) assert tuple(d['id'] for d in devices) == (1, 2, 3, 4, 5)
assert tuple(d['type'] for d in devices) == ('Desktop', 'Microtower', assert tuple(d['type'] for d in devices) == ('Desktop', 'Microtower',
'Laptop', 'NetworkAdapter', 'GraphicCard') 'Laptop', 'NetworkAdapter', 'GraphicCard')
@pytest.mark.usefixtures('app_context')
def test_computer_monitor():
m = ComputerMonitor(technology=ComputerMonitorTechnologies.LCD,
manufacturer='foo',
model='bar',
serial_number='foo-bar',
resolution_width=1920,
resolution_height=1080,
size=14.5)
db.session.add(m)
db.session.commit()

View file

@ -69,11 +69,13 @@ def snapshot_and_check(user: UserClient,
assert event['type'] != 'Receive', 'All Remove events must be before the Add ones' assert event['type'] != 'Receive', 'All Remove events must be before the Add ones'
assert input_snapshot['device'] assert input_snapshot['device']
assert_similar_device(input_snapshot['device'], snapshot['device']) assert_similar_device(input_snapshot['device'], snapshot['device'])
assert_similar_components(input_snapshot['components'], snapshot['components']) if input_snapshot.get('components', None):
assert_similar_components(input_snapshot['components'], snapshot['components'])
assert all(c['parent'] == snapshot['device']['id'] for c in snapshot['components']), \ assert all(c['parent'] == snapshot['device']['id'] for c in snapshot['components']), \
'Components must be in their parent' 'Components must be in their parent'
if perform_second_snapshot: if perform_second_snapshot:
input_snapshot['uuid'] = uuid4() if 'uuid' in input_snapshot:
input_snapshot['uuid'] = uuid4()
return snapshot_and_check(user, input_snapshot, event_types, perform_second_snapshot=False) return snapshot_and_check(user, input_snapshot, event_types, perform_second_snapshot=False)
else: else:
return snapshot return snapshot
@ -330,3 +332,24 @@ def test_erase(user: UserClient):
assert step['secureRandomSteps'] == 1 assert step['secureRandomSteps'] == 1
assert step['cleanWithZeros'] is True assert step['cleanWithZeros'] is True
assert 'num' not in step assert 'num' not in step
def test_snapshot_computer_monitor(user: UserClient):
s = file('computer-monitor.snapshot')
snapshot_and_check(user, s, event_types=('AppRate',))
def test_snapshot_components_none():
"""
Tests that a snapshot without components does not
remove them from the computer.
"""
# todo test
pass
def test_snapshot_components_empty():
"""
Tests that a snapshot whose components are an empty list remove
all its components.
"""