Merge branch 'master' into rate

# Conflicts:
#	ereuse_devicehub/resources/event/models.py
#	ereuse_devicehub/resources/event/models.pyi
#	ereuse_devicehub/resources/event/schemas.py
This commit is contained in:
Xavier Bustamante Talavera 2019-05-03 15:02:09 +02:00
commit e6d496872c
10 changed files with 289 additions and 76 deletions

View File

@ -27,3 +27,45 @@ To remove children lots the idea is the same:
And for devices is all the same: And for devices is all the same:
``POST /lots/<parent-lot-id>/devices/?id=<device-id-1>&id=<device-id-2>``; ``POST /lots/<parent-lot-id>/devices/?id=<device-id-1>&id=<device-id-2>``;
idem for removing devices. idem for removing devices.
Sharing lots
************
Sharing a lot means giving certain permissions to users, like reading
the characteristics of devices, or performing certain events.
Linking lots
============
Linking lots means setting a reference to a lot in another Devicehub
which supposedly have the same devices. One of both lots is considered
to be the outgoing lot and the other one the ingoing, in the sense
that the lifetime of the devices of the outgoing lot precedes the
incoming lot; in other way, the events of the devices in the outgoing
lot should (but not enforced) happen before that the events in the
incoming lot.
The lot has two fields that uses to keep a link: the Devicehub URL and
the ID of the other lot (internal ID I suppose...).
A device can only be in one incoming lot and in one outgoing lot.
A device inside a linked lot, when it is confirmed the existence of
an equal device in the other DB, is considered to be linked.
A linked lot defines a both way share of READ Characteristics, at
least.
Users can sync the characteristics of devices + selected tags + selected rules,
copying (but not deleting) devices between them. This ensures no
traceability warnings when generating the lifetime of devices. In first
iterations this process could be done manually (to allow user select
tags and rules)
Why linking lots
----------------
* Get the lifetime of devices (reporting). This way a Devicehub
traverses the linked devices (as long as it has perm).
* Send devices to another Devicehub (characteristics).
* Ensure rules over linked devices.

View File

@ -1,6 +1,7 @@
import csv import csv
import pathlib import pathlib
from contextlib import suppress from contextlib import suppress
from fractions import Fraction
from itertools import chain from itertools import chain
from operator import attrgetter from operator import attrgetter
from typing import Dict, List, Set from typing import Dict, List, Set
@ -12,6 +13,7 @@ from more_itertools import unique_everseen
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \ from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
Sequence, SmallInteger, Unicode, inspect, text Sequence, SmallInteger, Unicode, inspect, text
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import ColumnProperty, backref, relationship, validates from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from sqlalchemy_utils import ColorType from sqlalchemy_utils import ColorType
@ -23,8 +25,8 @@ from teal.marshmallow import ValidationError
from teal.resource import url_for_resource from teal.resource import url_for_resource
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \ from ereuse_devicehub.resources.enums import BatteryTechnology, ComputerChassis, \
PrinterTechnology, RamFormat, RamInterface, Severity DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface, Severity
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
@ -58,14 +60,15 @@ class Device(Thing):
so it can re-generated *offline*. so it can re-generated *offline*.
""" + HID_CONVERSION_DOC """ + HID_CONVERSION_DOC
model = Column(Unicode(), check_lower('model')) model = Column(Unicode, check_lower('model'))
model.comment = """The model or brand of the device in lower case. model.comment = """The model of the device in lower case.
Devices usually report one of both (model or brand). This value The model is the unambiguous, as technical as possible, denomination
must be consistent through time. for the product. This field, among others, is used to identify
the product.
""" """
manufacturer = Column(Unicode(), check_lower('manufacturer')) manufacturer = Column(Unicode(), check_lower('manufacturer'))
manufacturer.comment = """The normalized name of the manufacturer manufacturer.comment = """The normalized name of the manufacturer,
in lower case. in lower case.
Although as of now Devicehub does not enforce normalization, Although as of now Devicehub does not enforce normalization,
@ -74,26 +77,38 @@ class Device(Thing):
""" """
serial_number = Column(Unicode(), check_lower('serial_number')) serial_number = Column(Unicode(), check_lower('serial_number'))
serial_number.comment = """The serial number of the device in lower case.""" serial_number.comment = """The serial number of the device in lower case."""
brand = db.Column(CIText())
brand.comment = """A naming for consumers. This field can represent
several models, so it can be ambiguous, and it is not used to
identify the product.
"""
generation = db.Column(db.SmallInteger, check_range('generation', 0))
generation.comment = """The generation of the device."""
weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5)) weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5))
weight.comment = """ weight.comment = """
The weight of the device. The weight of the device.
""" """
width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5)) width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5))
width.comment = """ width.comment = """
The width of the device. The width of the device in meters.
""" """
height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5)) height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5))
height.comment = """ height.comment = """
The height of the device. The height of the device in meters.
""" """
depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5)) depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5))
depth.comment = """ depth.comment = """
The depth of the device. The depth of the device in meters.
""" """
color = Column(ColorType) color = Column(ColorType)
color.comment = """The predominant color of the device.""" color.comment = """The predominant color of the device."""
production_date = Column(db.TIMESTAMP(timezone=True)) production_date = Column(db.DateTime)
production_date.comment = """The date of production of the device.""" production_date.comment = """The date of production of the device.
This is timezone naive, as Workbench cannot report this data
with timezone information.
"""
variant = Column(Unicode)
variant.comment = """A variant or sub-model of the device."""
_NON_PHYSICAL_PROPS = { _NON_PHYSICAL_PROPS = {
'id', 'id',
@ -107,7 +122,11 @@ class Device(Thing):
'width', 'width',
'height', 'height',
'depth', 'depth',
'weight' 'weight',
'brand',
'generation',
'production_date',
'variant'
} }
__table_args__ = ( __table_args__ = (
@ -129,7 +148,7 @@ class Device(Thing):
2. Events performed to a component. 2. Events performed to a component.
3. Events performed to a parent device. 3. Events performed to a parent device.
Events are returned by ascending ``created`` time. Events are returned by descending ``created`` time.
""" """
return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY) return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY)
@ -260,6 +279,7 @@ class Device(Thing):
:raise LookupError: Device has not an event of the given type. :raise LookupError: Device has not an event of the given type.
""" """
try: try:
# noinspection PyTypeHints
return next(e for e in reversed(self.events) if isinstance(e, types)) return next(e for e in reversed(self.events) if isinstance(e, types))
except StopIteration: except StopIteration:
raise LookupError('{!r} does not contain events of types {}.'.format(self, types)) raise LookupError('{!r} does not contain events of types {}.'.format(self, types))
@ -288,12 +308,8 @@ class Device(Thing):
class DisplayMixin: class DisplayMixin:
""" """Base class for the Display Component and the Monitor Device."""
Aspect ratio can be computed as in size = Column(Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=False)
https://github.com/mirukan/whratio/blob/master/whratio/ratio.py and
could be a future property.
"""
size = Column(Float(decimal_return_scale=2), check_range('size', 2, 150))
size.comment = """ size.comment = """
The size of the monitor in inches. The size of the monitor in inches.
""" """
@ -301,27 +317,59 @@ class DisplayMixin:
technology.comment = """ technology.comment = """
The technology the monitor uses to display the image. The technology the monitor uses to display the image.
""" """
resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000)) resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000),
nullable=False)
resolution_width.comment = """ resolution_width.comment = """
The maximum horizontal resolution the monitor can natively support The maximum horizontal resolution the monitor can natively support
in pixels. in pixels.
""" """
resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000)) resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000),
nullable=False)
resolution_height.comment = """ resolution_height.comment = """
The maximum vertical resolution the monitor can natively support The maximum vertical resolution the monitor can natively support
in pixels. in pixels.
""" """
refresh_rate = Column(SmallInteger, check_range('refresh_rate', 10, 1000)) refresh_rate = Column(SmallInteger, check_range('refresh_rate', 10, 1000))
contrast_ratio = Column(SmallInteger, check_range('contrast_ratio', 100, 100000)) contrast_ratio = Column(SmallInteger, check_range('contrast_ratio', 100, 100000))
touchable = Column(Boolean, nullable=False, default=False) touchable = Column(Boolean)
touchable.comment = """Whether it is a touchscreen.""" touchable.comment = """Whether it is a touchscreen."""
@hybrid_property
def aspect_ratio(self):
"""The aspect ratio of the display, as a fraction: ``X/Y``.
Regular values are ``4/3``, ``5/4``, ``16/9``, ``21/9``,
``14/10``, ``19/10``, ``16/10``.
"""
return Fraction(self.resolution_width, self.resolution_height)
# noinspection PyUnresolvedReferences
@aspect_ratio.expression
def aspect_ratio(cls):
# The aspect ratio to use as SQL in the DB
# This allows comparing resolutions
return db.func.round(cls.resolution_width / cls.resolution_height, 2)
@hybrid_property
def widescreen(self):
"""Whether the monitor is considered to be widescreen.
Widescreen monitors are those having a higher aspect ratio
greater than 4/3.
"""
# We add a tiny extra to 4/3 to avoid precision errors
return self.aspect_ratio > 4.001 / 3
def __str__(self) -> str:
return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self)
def __format__(self, format_spec: str) -> str: def __format__(self, format_spec: str) -> str:
v = '' v = ''
if 't' in format_spec: if 't' in format_spec:
v += '{0.t} {0.model}'.format(self) v += '{0.t} {0.model}'.format(self)
if 's' in format_spec: if 's' in format_spec:
v += '({0.manufacturer}) S/N {0.serial_number} {0.size}in {0.technology}' v += '({0.manufacturer}) S/N {0.serial_number}'.format(self)
v += ' {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self)
return v return v
@ -444,14 +492,16 @@ class Mobile(Device):
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
imei = Column(BigInteger) imei = Column(BigInteger)
imei.comment = """ imei.comment = """The International Mobile Equipment Identity of
The International Mobile Equipment Identity of the smartphone the smartphone as an integer.
as an integer.
""" """
meid = Column(Unicode) meid = Column(Unicode)
meid.comment = """ meid.comment = """The Mobile Equipment Identifier as a hexadecimal
The Mobile Equipment Identifier as a hexadecimal string. string.
""" """
ram_size = db.Column(db.Integer, check_range(1, ))
ram_size.comment = """The total of RAM of the device in MB."""
data_storage_size = db.Column(db.Integer)
@validates('imei') @validates('imei')
def validate_imei(self, _, value: int): def validate_imei(self, _, value: int):
@ -577,6 +627,8 @@ class Motherboard(JoinedComponentTableMixin, Component):
firewire = Column(SmallInteger, check_range('firewire', min=0)) firewire = Column(SmallInteger, check_range('firewire', min=0))
serial = Column(SmallInteger, check_range('serial', min=0)) serial = Column(SmallInteger, check_range('serial', min=0))
pcmcia = Column(SmallInteger, check_range('pcmcia', min=0)) pcmcia = Column(SmallInteger, check_range('pcmcia', min=0))
bios_date = Column(db.Date)
bios_date.comment = """The date of the BIOS version."""
class NetworkMixin: class NetworkMixin:
@ -610,6 +662,8 @@ class Processor(JoinedComponentTableMixin, Component):
threads.comment = """The number of threads per core.""" threads.comment = """The number of threads per core."""
address = Column(SmallInteger, check_range('address', 8, 256)) address = Column(SmallInteger, check_range('address', 8, 256))
address.comment = """The address of the CPU: 8, 16, 32, 64, 128 or 256 bits.""" address.comment = """The address of the CPU: 8, 16, 32, 64, 128 or 256 bits."""
abi = Column(Unicode, check_lower('abi'))
abi.comment = """The Application Binary Interface of the processor."""
class RamModule(JoinedComponentTableMixin, Component): class RamModule(JoinedComponentTableMixin, Component):
@ -635,6 +689,24 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component):
pass pass
class Battery(JoinedComponentTableMixin, Component):
wireless = db.Column(db.Boolean)
wireless.comment = """If the battery can be charged wirelessly."""
technology = db.Column(db.Enum(BatteryTechnology))
size = db.Column(db.Integer, nullable=False)
size.comment = """Maximum battery capacity by design, in mAh.
Use BatteryTest's "size" to get the actual size of the battery.
"""
@property
def capacity(self) -> float:
"""The quantity of """
from ereuse_devicehub.resources.event.models import MeasureBattery
real_size = self.last_event_of(MeasureBattery).size
return real_size / self.size if real_size and self.size else None
class ComputerAccessory(Device): class ComputerAccessory(Device):
"""Computer peripherals and similar accessories.""" """Computer peripherals and similar accessories."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)

View File

@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from fractions import Fraction
from operator import attrgetter from operator import attrgetter
from typing import Dict, Generator, Iterable, List, Optional, Set, Type from typing import Dict, Generator, Iterable, List, Optional, Set, Type, TypeVar
from boltons import urlutils from boltons import urlutils
from boltons.urlutils import URL from boltons.urlutils import URL
@ -12,15 +13,16 @@ from teal.enums import Layouts
from ereuse_devicehub.resources.agent.models import Agent from ereuse_devicehub.resources.agent.models import Agent
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \ from ereuse_devicehub.resources.enums import BatteryTechnology, ComputerChassis, \
PrinterTechnology, RamFormat, RamInterface DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface
from ereuse_devicehub.resources.event import models as e from ereuse_devicehub.resources.event import models as e
from ereuse_devicehub.resources.image.models import ImageList
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.models import Thing
from ereuse_devicehub.resources.tag import Tag from ereuse_devicehub.resources.tag import Tag
from ereuse_devicehub.resources.tag.model import Tags from ereuse_devicehub.resources.tag.model import Tags
E = TypeVar('E', bound=e.Event)
class Device(Thing): class Device(Thing):
EVENT_SORT_KEY = attrgetter('created') EVENT_SORT_KEY = attrgetter('created')
@ -38,27 +40,32 @@ class Device(Thing):
color = ... # type: Column color = ... # type: Column
lots = ... # type: relationship lots = ... # type: relationship
production_date = ... # type: Column production_date = ... # type: Column
brand = ... # type: Column
generation = ... # type: Column
variant = ... # type: Column
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.id = ... # type: int self.id = ... # type: int
self.type = ... # type: str self.type = ... # type: str
self.hid = ... # type: str self.hid = ... # type: Optional[str]
self.model = ... # type: str self.model = ... # type: Optional[str]
self.manufacturer = ... # type: str self.manufacturer = ... # type: Optional[str]
self.serial_number = ... # type: str self.serial_number = ... # type: Optional[str]
self.weight = ... # type: float self.weight = ... # type: Optional[float]
self.width = ... # type:float self.width = ... # type:Optional[float]
self.height = ... # type: float self.height = ... # type: Optional[float]
self.depth = ... # type: float self.depth = ... # type: Optional[float]
self.color = ... # type: Color self.color = ... # type: Optional[Color]
self.physical_properties = ... # type: Dict[str, object or None] self.physical_properties = ... # type: Dict[str, object or None]
self.events_multiple = ... # type: Set[e.EventWithMultipleDevices] self.events_multiple = ... # type: Set[e.EventWithMultipleDevices]
self.events_one = ... # type: Set[e.EventWithOneDevice] self.events_one = ... # type: Set[e.EventWithOneDevice]
self.images = ... # type: ImageList
self.tags = ... # type: Tags[Tag] self.tags = ... # type: Tags[Tag]
self.lots = ... # type: Set[Lot] self.lots = ... # type: Set[Lot]
self.production_date = ... # type: datetime self.production_date = ... # type: Optional[datetime]
self.brand = ... # type: Optional[str]
self.generation = ... # type: Optional[int]
self.variant = ... # type: Optional[str]
@property @property
def events(self) -> List[e.Event]: def events(self) -> List[e.Event]:
@ -96,7 +103,7 @@ class Device(Thing):
def working(self) -> List[e.Test]: def working(self) -> List[e.Test]:
pass pass
def last_event_of(self, *types: Type[e.Event]) -> e.Event: def last_event_of(self, *types: Type[E]) -> E:
pass pass
def _warning_events(self, events: Iterable[e.Event]) -> Generator[e.Event]: def _warning_events(self, events: Iterable[e.Event]) -> Generator[e.Event]:
@ -118,9 +125,11 @@ class DisplayMixin:
self.size = ... # type: Integer self.size = ... # type: Integer
self.resolution_width = ... # type: int self.resolution_width = ... # type: int
self.resolution_height = ... # type: int self.resolution_height = ... # type: int
self.refresh_rate = ... # type: int self.refresh_rate = ... # type: Optional[int]
self.contrast_ratio = ... # type: int self.contrast_ratio = ... # type: Optional[int]
self.touchable = ... # type: bool self.touchable = ... # type: Optional[bool]
self.aspect_ratio = ... #type: Fraction
self.widescreen = ... # type: bool
class Computer(DisplayMixin, Device): class Computer(DisplayMixin, Device):
@ -193,11 +202,15 @@ class TelevisionSet(Monitor):
class Mobile(Device): class Mobile(Device):
imei = ... # type: Column imei = ... # type: Column
meid = ... # type: Column meid = ... # type: Column
ram_size = ... # type: Column
data_storage_size = ... # type: Column
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.imei = ... # type: int self.imei = ... # type: Optional[int]
self.meid = ... # type: str self.meid = ... # type: Optional[str]
self.ram_size = ... # type: Optional[int]
self.data_storage_size = ... # type: Optional[int]
class Smartphone(Mobile): class Smartphone(Mobile):
@ -288,13 +301,15 @@ class Processor(Component):
cores = ... # type: Column cores = ... # type: Column
address = ... # type: Column address = ... # type: Column
threads = ... # type: Column threads = ... # type: Column
abi = ... # type: Column
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.speed = ... # type: float self.speed = ... # type: Optional[float]
self.cores = ... # type: int self.cores = ... # type: Optional[int]
self.threads = ... # type: int self.threads = ... # type: Optional[int]
self.address = ... # type: int self.address = ... # type: Optional[int]
self.abi = ... # type: Optional[str]
class RamModule(Component): class RamModule(Component):
@ -319,6 +334,18 @@ class Display(DisplayMixin, Component):
pass pass
class Battery(Component):
wireless = ... # type: Column
technology = ... # type: Column
size = ... # type: Column
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.wireless = ... # type: Optional[bool]
self.technology = ... # type: Optional[BatteryTechnology]
self.size = ... # type: bool
class ComputerAccessory(Device): class ComputerAccessory(Device):
pass pass

View File

@ -22,9 +22,17 @@ class Device(Thing):
many=True, many=True,
collection_class=OrderedSet, collection_class=OrderedSet,
description='A set of tags that identify the device.') description='A set of tags that identify the device.')
model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE)) model = SanitizedStr(lower=True,
manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE)) validate=Length(max=STR_BIG_SIZE),
serial_number = SanitizedStr(lower=True, data_key='serialNumber') description=m.Device.model.comment)
manufacturer = SanitizedStr(lower=True,
validate=Length(max=STR_SIZE),
description=m.Device.manufacturer.comment)
serial_number = SanitizedStr(lower=True,
validate=Length(max=STR_BIG_SIZE),
data_key='serialNumber')
brand = SanitizedStr(validate=Length(max=STR_BIG_SIZE), description=m.Device.brand.comment)
generation = Integer(validate=Range(1, 100), description=m.Device.generation.comment)
weight = Float(validate=Range(0.1, 5), unit=UnitCodes.kgm, description=m.Device.weight.comment) weight = Float(validate=Range(0.1, 5), unit=UnitCodes.kgm, description=m.Device.weight.comment)
width = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.width.comment) width = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.width.comment)
height = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.height.comment) height = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.height.comment)
@ -113,8 +121,8 @@ class Desktop(Computer):
class Laptop(Computer): class Laptop(Computer):
layout = EnumField(Layouts, description=m.Laptop.layout.comment)
__doc__ = m.Laptop.__doc__ __doc__ = m.Laptop.__doc__
layout = EnumField(Layouts, description=m.Laptop.layout.comment)
class Server(Computer): class Server(Computer):
@ -123,19 +131,22 @@ class Server(Computer):
class DisplayMixin: class DisplayMixin:
__doc__ = m.DisplayMixin.__doc__ __doc__ = m.DisplayMixin.__doc__
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150), required=True)
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150))
technology = EnumField(enums.DisplayTech, technology = EnumField(enums.DisplayTech,
description=m.DisplayMixin.technology.comment) description=m.DisplayMixin.technology.comment)
resolution_width = Integer(data_key='resolutionWidth', resolution_width = Integer(data_key='resolutionWidth',
validate=Range(10, 20000), validate=Range(10, 20000),
description=m.DisplayMixin.resolution_width.comment) description=m.DisplayMixin.resolution_width.comment,
required=True)
resolution_height = Integer(data_key='resolutionHeight', resolution_height = Integer(data_key='resolutionHeight',
validate=Range(10, 20000), validate=Range(10, 20000),
description=m.DisplayMixin.resolution_height.comment) description=m.DisplayMixin.resolution_height.comment,
required=True)
refresh_rate = Integer(data_key='refreshRate', validate=Range(10, 1000)) refresh_rate = Integer(data_key='refreshRate', validate=Range(10, 1000))
contrast_ratio = Integer(data_key='contrastRatio', validate=Range(100, 100000)) contrast_ratio = Integer(data_key='contrastRatio', validate=Range(100, 100000))
touchable = Boolean(missing=False, description=m.DisplayMixin.touchable.comment) touchable = Boolean(description=m.DisplayMixin.touchable.comment)
aspect_ratio = String(dump_only=True, description=m.DisplayMixin.aspect_ratio.__doc__)
widescreen = Boolean(dump_only=True, description=m.DisplayMixin.widescreen.__doc__)
class NetworkMixin: class NetworkMixin:
@ -164,6 +175,14 @@ class Mobile(Device):
imei = Integer(description=m.Mobile.imei.comment) imei = Integer(description=m.Mobile.imei.comment)
meid = Str(description=m.Mobile.meid.comment) meid = Str(description=m.Mobile.meid.comment)
ram_size = Integer(validate=Range(min=128, max=36000),
data_key='ramSize',
unit=UnitCodes.mbyte,
description=m.Mobile.ram_size.comment)
data_storage_size = Integer(validate=Range(0, 10 ** 8),
data_key='dataStorageSize',
description=m.Mobile.data_storage_size)
@pre_load @pre_load
def convert_check_imei(self, data): def convert_check_imei(self, data):
@ -247,6 +266,7 @@ class Processor(Component):
threads = Integer(validate=Range(min=1, max=20), description=m.Processor.threads.comment) threads = Integer(validate=Range(min=1, max=20), description=m.Processor.threads.comment)
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}), address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}),
description=m.Processor.address.comment) description=m.Processor.address.comment)
abi = SanitizedStr(lower=True, description=m.Processor.abi.comment)
class RamModule(Component): class RamModule(Component):
@ -268,6 +288,14 @@ class Display(DisplayMixin, Component):
__doc__ = m.Display.__doc__ __doc__ = m.Display.__doc__
class Battery(Component):
__doc__ = m.Battery
wireless = Boolean(description=m.Battery.wireless.comment)
technology = EnumField(enums.BatteryTechnology, description=m.Battery.technology.comment)
size = Integer(required=True, description=m.Battery.size.comment)
class Manufacturer(Schema): class Manufacturer(Schema):
__doc__ = m.Manufacturer.__doc__ __doc__ = m.Manufacturer.__doc__

View File

@ -272,6 +272,26 @@ class PrinterTechnology(Enum):
Thermal = 'Thermal' Thermal = 'Thermal'
@unique
class BatteryHealth(Enum):
"""The battery health status as in Android."""
Cold = 'Cold'
Dead = 'Dead'
Good = 'Good'
Overheat = 'Overheat'
OverVoltage = 'OverVoltage'
UnspecifiedValue = 'UnspecifiedValue'
@unique
class BatteryTechnology(Enum):
"""The technology of the Battery."""
LiIon = 'Lithium-ion'
NiCad = 'Nickel-Cadmium'
NiMH = 'Nickel-metal hydride'
Al = 'Alkaline'
class Severity(IntEnum): class Severity(IntEnum):
"""A flag evaluating the event execution. Ex. failed events """A flag evaluating the event execution. Ex. failed events
have the value `Severity.Error`. Devicehub uses 4 severity levels: have the value `Severity.Error`. Devicehub uses 4 severity levels:

View File

@ -397,7 +397,9 @@ class ErasePhysical(EraseBasic):
class Step(db.Model): class Step(db.Model):
erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True) erasure_id = Column(UUID(as_uuid=True),
ForeignKey(EraseBasic.id, ondelete='CASCADE'),
primary_key=True)
type = Column(Unicode(STR_SM_SIZE), nullable=False) type = Column(Unicode(STR_SM_SIZE), nullable=False)
num = Column(SmallInteger, primary_key=True) num = Column(SmallInteger, primary_key=True)
severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False) severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False)
@ -559,7 +561,6 @@ class SnapshotRequest(db.Model):
id = Column(UUID(as_uuid=True), ForeignKey(Snapshot.id), primary_key=True) id = Column(UUID(as_uuid=True), ForeignKey(Snapshot.id), primary_key=True)
request = Column(JSON, nullable=False) request = Column(JSON, nullable=False)
snapshot = relationship(Snapshot, snapshot = relationship(Snapshot,
backref=backref('request', backref=backref('request',
lazy=True, lazy=True,
uselist=False, uselist=False,
@ -668,6 +669,27 @@ class TestMixin:
return Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True) return Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True)
class MeasureBattery(TestMixin, Test):
"""A sample of the status of the battery.
Operative Systems keep a record of several aspects of a battery.
This is a sample of those.
"""
size = db.Column(db.Integer, nullable=False)
size.comment = """Maximum battery capacity, in mAh."""
voltage = db.Column(db.Integer, nullable=False)
voltage.comment = """The actual voltage of the battery, in mV."""
cycle_count = db.Column(db.Integer)
cycle_count.comment = """The number of full charges discharges
cycles.
"""
health = db.Column(db.Enum(BatteryHealth))
health.comment = """The health of the Battery.
Only reported in Android.
"""
class TestDataStorage(TestMixin, Test): class TestDataStorage(TestMixin, Test):
""" """
The act of testing the data storage. The act of testing the data storage.

View File

@ -145,6 +145,14 @@ class Test(EventWithOneDevice):
__doc__ = m.Test.__doc__ __doc__ = m.Test.__doc__
class MeasureBattery(Test):
__doc__ = m.MeasureBattery.__doc__
size = Integer(required=True, description=m.MeasureBattery.size.comment)
voltage = Integer(required=True, description=m.MeasureBattery.voltage.comment)
cycle_count = Integer(required=True, description=m.MeasureBattery.cycle_count.comment)
health = EnumField(enums.BatteryHealth, description=m.MeasureBattery.health.comment)
class TestDataStorage(Test): class TestDataStorage(Test):
__doc__ = m.TestDataStorage.__doc__ __doc__ = m.TestDataStorage.__doc__
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)

View File

@ -5,7 +5,7 @@ click==6.7
click-spinner==0.1.8 click-spinner==0.1.8
colorama==0.3.9 colorama==0.3.9
colour==0.1.5 colour==0.1.5
ereuse-utils[naming, test, session, cli]==0.4.0b21 ereuse-utils[naming,test,session,cli]==0.4.0b49
Flask==1.0.2 Flask==1.0.2
Flask-Cors==3.0.6 Flask-Cors==3.0.6
Flask-SQLAlchemy==2.3.2 Flask-SQLAlchemy==2.3.2

View File

@ -1,10 +1,8 @@
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path
from setuptools import find_packages, setup from setuptools import find_packages, setup
with open('README.md', encoding='utf8') as f:
long_description = f.read()
test_requires = [ test_requires = [
'pytest', 'pytest',
'requests_mock' 'requests_mock'
@ -26,13 +24,13 @@ setup(
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
python_requires='>=3.5.3', python_requires='>=3.5.3',
long_description=long_description, long_description=Path('README.md').read_text('utf8'),
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
install_requires=[ install_requires=[
'teal>=0.2.0a38', # teal always first 'teal>=0.2.0a38', # teal always first
'click', 'click',
'click-spinner', 'click-spinner',
'ereuse-utils[naming, test, session, cli]>=0.4b21', 'ereuse-utils[naming,test,session,cli]>=0.4b49',
'hashids', 'hashids',
'marshmallow_enum', 'marshmallow_enum',
'psycopg2-binary', 'psycopg2-binary',

View File

@ -116,6 +116,7 @@ def test_physical_properties():
'serial': None, 'serial': None,
'firewire': None, 'firewire': None,
'manufacturer': 'mr', 'manufacturer': 'mr',
'bios_date': None
} }
assert pc.physical_properties == { assert pc.physical_properties == {
'model': 'foo', 'model': 'foo',
@ -453,11 +454,6 @@ def test_computer_monitor():
db.session.commit() db.session.commit()
@pytest.mark.xfail(reason='Make test')
def test_computer_with_display():
pass
def test_manufacturer(user: UserClient): def test_manufacturer(user: UserClient):
m, r = user.get(res='Manufacturer', query=[('search', 'asus')]) m, r = user.get(res='Manufacturer', query=[('search', 'asus')])
assert m == {'items': [{'name': 'Asus', 'url': 'https://en.wikipedia.org/wiki/Asus'}]} assert m == {'items': [{'name': 'Asus', 'url': 'https://en.wikipedia.org/wiki/Asus'}]}