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:
``POST /lots/<parent-lot-id>/devices/?id=<device-id-1>&id=<device-id-2>``;
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 pathlib
from contextlib import suppress
from fractions import Fraction
from itertools import chain
from operator import attrgetter
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, \
Sequence, SmallInteger, Unicode, inspect, text
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
from sqlalchemy.util import OrderedSet
from sqlalchemy_utils import ColorType
@ -23,8 +25,8 @@ from teal.marshmallow import ValidationError
from teal.resource import url_for_resource
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
PrinterTechnology, RamFormat, RamInterface, Severity
from ereuse_devicehub.resources.enums import BatteryTechnology, ComputerChassis, \
DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface, Severity
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
@ -58,14 +60,15 @@ class Device(Thing):
so it can re-generated *offline*.
""" + HID_CONVERSION_DOC
model = Column(Unicode(), check_lower('model'))
model.comment = """The model or brand of the device in lower case.
model = Column(Unicode, check_lower('model'))
model.comment = """The model of the device in lower case.
Devices usually report one of both (model or brand). This value
must be consistent through time.
The model is the unambiguous, as technical as possible, denomination
for the product. This field, among others, is used to identify
the product.
"""
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.
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.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.comment = """
The weight of the device.
"""
width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5))
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.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.comment = """
The depth of the device.
The depth of the device in meters.
"""
color = Column(ColorType)
color.comment = """The predominant color of the device."""
production_date = Column(db.TIMESTAMP(timezone=True))
production_date.comment = """The date of production of the device."""
production_date = Column(db.DateTime)
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 = {
'id',
@ -107,7 +122,11 @@ class Device(Thing):
'width',
'height',
'depth',
'weight'
'weight',
'brand',
'generation',
'production_date',
'variant'
}
__table_args__ = (
@ -129,7 +148,7 @@ class Device(Thing):
2. Events performed to a component.
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)
@ -260,6 +279,7 @@ class Device(Thing):
:raise LookupError: Device has not an event of the given type.
"""
try:
# noinspection PyTypeHints
return next(e for e in reversed(self.events) if isinstance(e, types))
except StopIteration:
raise LookupError('{!r} does not contain events of types {}.'.format(self, types))
@ -288,12 +308,8 @@ class Device(Thing):
class DisplayMixin:
"""
Aspect ratio can be computed as in
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))
"""Base class for the Display Component and the Monitor Device."""
size = Column(Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=False)
size.comment = """
The size of the monitor in inches.
"""
@ -301,27 +317,59 @@ class DisplayMixin:
technology.comment = """
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 = """
The maximum horizontal resolution the monitor can natively support
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 = """
The maximum vertical resolution the monitor can natively support
in pixels.
"""
refresh_rate = Column(SmallInteger, check_range('refresh_rate', 10, 1000))
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."""
@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:
v = ''
if 't' in format_spec:
v += '{0.t} {0.model}'.format(self)
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
@ -444,14 +492,16 @@ class Mobile(Device):
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
imei = Column(BigInteger)
imei.comment = """
The International Mobile Equipment Identity of the smartphone
as an integer.
imei.comment = """The International Mobile Equipment Identity of
the smartphone as an integer.
"""
meid = Column(Unicode)
meid.comment = """
The Mobile Equipment Identifier as a hexadecimal string.
meid.comment = """The Mobile Equipment Identifier as a hexadecimal
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')
def validate_imei(self, _, value: int):
@ -577,6 +627,8 @@ class Motherboard(JoinedComponentTableMixin, Component):
firewire = Column(SmallInteger, check_range('firewire', min=0))
serial = Column(SmallInteger, check_range('serial', 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:
@ -610,6 +662,8 @@ class Processor(JoinedComponentTableMixin, Component):
threads.comment = """The number of threads per core."""
address = Column(SmallInteger, check_range('address', 8, 256))
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):
@ -635,6 +689,24 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component):
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):
"""Computer peripherals and similar accessories."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)

View File

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

View File

@ -22,9 +22,17 @@ class Device(Thing):
many=True,
collection_class=OrderedSet,
description='A set of tags that identify the device.')
model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE))
manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE))
serial_number = SanitizedStr(lower=True, data_key='serialNumber')
model = SanitizedStr(lower=True,
validate=Length(max=STR_BIG_SIZE),
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)
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)
@ -113,8 +121,8 @@ class Desktop(Computer):
class Laptop(Computer):
layout = EnumField(Layouts, description=m.Laptop.layout.comment)
__doc__ = m.Laptop.__doc__
layout = EnumField(Layouts, description=m.Laptop.layout.comment)
class Server(Computer):
@ -123,19 +131,22 @@ class Server(Computer):
class DisplayMixin:
__doc__ = m.DisplayMixin.__doc__
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150))
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150), required=True)
technology = EnumField(enums.DisplayTech,
description=m.DisplayMixin.technology.comment)
resolution_width = Integer(data_key='resolutionWidth',
validate=Range(10, 20000),
description=m.DisplayMixin.resolution_width.comment)
description=m.DisplayMixin.resolution_width.comment,
required=True)
resolution_height = Integer(data_key='resolutionHeight',
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))
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:
@ -164,6 +175,14 @@ class Mobile(Device):
imei = Integer(description=m.Mobile.imei.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
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)
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}),
description=m.Processor.address.comment)
abi = SanitizedStr(lower=True, description=m.Processor.abi.comment)
class RamModule(Component):
@ -268,6 +288,14 @@ class Display(DisplayMixin, Component):
__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):
__doc__ = m.Manufacturer.__doc__

View File

@ -272,6 +272,26 @@ class PrinterTechnology(Enum):
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):
"""A flag evaluating the event execution. Ex. failed events
have the value `Severity.Error`. Devicehub uses 4 severity levels:

View File

@ -397,7 +397,9 @@ class ErasePhysical(EraseBasic):
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)
num = Column(SmallInteger, primary_key=True)
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)
request = Column(JSON, nullable=False)
snapshot = relationship(Snapshot,
backref=backref('request',
lazy=True,
uselist=False,
@ -668,6 +669,27 @@ class TestMixin:
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):
"""
The act of testing the data storage.

View File

@ -145,6 +145,14 @@ class Test(EventWithOneDevice):
__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):
__doc__ = m.TestDataStorage.__doc__
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)

View File

@ -5,7 +5,7 @@ click==6.7
click-spinner==0.1.8
colorama==0.3.9
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-Cors==3.0.6
Flask-SQLAlchemy==2.3.2

View File

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

View File

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