Add inventory and missing fields

This commit is contained in:
Xavier Bustamante Talavera 2018-06-12 16:50:05 +02:00
parent 02a0332dd6
commit b56fbeeca7
14 changed files with 235 additions and 28 deletions

View File

@ -139,12 +139,12 @@ is as follows:
``Snapshot``. ``Snapshot``.
3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot`` 3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot``
and ``component`` IDs from 1, linking it to them. It repeats and ``component`` IDs from 1, linking it to them. It repeats
this for all the erased data storage devices; **T2+Tn** being this for all the erased data storage devices; **T3+Tn** being
*n* the erased data storage devices. *n* the erased data storage devices.
4. WorkbenchServer does like in 3. but for the event ``Install``, 4. WorkbenchServer does like in 3. but for the event ``Install``,
finishing in **T2+Tn+Tx**, being *x* the number of data storage finishing in **T3+Tn+Tx**, being *x* the number of data storage
devices with an OS installed into. devices with an OS installed into.
5. In **T2+Tn+Tx**, when all *expected events* have been performed, 5. In **T3+Tn+Tx**, when all *expected events* have been performed,
Devicehub **closes** the ``Snapshot`` from 1. Devicehub **closes** the ``Snapshot`` from 1.
Optionally, Devicehub understands receiving a ``Snapshot`` with all Optionally, Devicehub understands receiving a ``Snapshot`` with all

22
docs/getting.rst Normal file
View File

@ -0,0 +1,22 @@
Getting
=======
Devicehub uses the same path to get devices and lots.
To get the lot information ::
GET /inventory/24
You can specifically filter devices::
GET /inventory?devices?
GET /inventory/24?type=24&type=44&status={"name": "Reserved", "updated": "2018-01-01"}
GET /inventory/25?price=24&price=21
GET /devices/4?
Returns devices that matches the filters and the lots that contain them.
If the filters are applied to the lots, it returns the matched lots
and the devices that contain them.
You can join filters.

View File

@ -9,6 +9,7 @@ from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, EventDef,
PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \ PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \
StepRandomDef, StepZeroDef, TestDataStorageDef, TestDef, WorkbenchRateDef, EraseBasicDef, \ StepRandomDef, StepZeroDef, TestDataStorageDef, TestDef, WorkbenchRateDef, EraseBasicDef, \
EraseSectorsDef EraseSectorsDef
from ereuse_devicehub.resources.inventory import InventoryDef
from ereuse_devicehub.resources.tag import TagDef from ereuse_devicehub.resources.tag import TagDef
from ereuse_devicehub.resources.user import OrganizationDef, UserDef from ereuse_devicehub.resources.user import OrganizationDef, UserDef
from teal.config import Config from teal.config import Config
@ -22,7 +23,7 @@ class DevicehubConfig(Config):
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, WorkbenchRateDef TestDataStorageDef, WorkbenchRateDef, InventoryDef
} }
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str] PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' # type: str SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' # type: str

View File

@ -4,12 +4,13 @@ from operator import attrgetter
from typing import Dict, Set from typing import Dict, Set
from sqlalchemy import BigInteger, Column, Float, ForeignKey, Integer, Sequence, SmallInteger, \ from sqlalchemy import BigInteger, Column, Float, ForeignKey, Integer, Sequence, SmallInteger, \
Unicode, inspect Unicode, inspect, Enum as DBEnum
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import ColumnProperty, backref, relationship 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, RamInterface, RamFormat
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
@ -151,6 +152,7 @@ class GraphicCard(JoinedComponentTableMixin, Component):
class DataStorage(JoinedComponentTableMixin, Component): class DataStorage(JoinedComponentTableMixin, Component):
size = Column(Integer, check_range('size', min=1, max=10 ** 8)) size = Column(Integer, check_range('size', min=1, max=10 ** 8))
interface = Column(DBEnum(DataStorageInterface))
class HardDrive(DataStorage): class HardDrive(DataStorage):
@ -182,3 +184,5 @@ class Processor(JoinedComponentTableMixin, Component):
class RamModule(JoinedComponentTableMixin, Component): class RamModule(JoinedComponentTableMixin, Component):
size = Column(SmallInteger, check_range('size', min=128, max=17000)) size = Column(SmallInteger, check_range('size', min=128, max=17000))
speed = Column(Float, check_range('speed', min=100, max=10000)) speed = Column(Float, check_range('speed', min=100, max=10000))
interface = Column(DBEnum(RamInterface))
format = Column(DBEnum(RamFormat))

View File

@ -1,8 +1,10 @@
from marshmallow.fields import Float, Integer, Str from marshmallow.fields import Float, Integer, Str
from marshmallow.validate import Length, OneOf, Range from marshmallow.validate import Length, OneOf, Range
from marshmallow_enum import EnumField
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.enums import RamInterface, RamFormat
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes from ereuse_devicehub.resources.schemas import Thing, UnitCodes
@ -105,3 +107,5 @@ class Processor(Component):
class RamModule(Component): class RamModule(Component):
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte) size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz) speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
interface = EnumField(RamInterface)
format = EnumField(RamFormat)

View File

@ -3,7 +3,6 @@ from enum import Enum, IntEnum, unique
from typing import Union from typing import Union
@unique @unique
class SnapshotSoftware(Enum): class SnapshotSoftware(Enum):
"""The algorithm_software used to perform the Snapshot.""" """The algorithm_software used to perform the Snapshot."""
@ -125,3 +124,28 @@ class SnapshotExpectedEvents(Enum):
BOX_RATE_5 = 1, 5 BOX_RATE_5 = 1, 5
BOX_RATE_3 = 1, 3 BOX_RATE_3 = 1, 3
# After looking at own databases
@unique
class RamInterface(Enum):
DDR = 'DDR'
DDR2 = 'DDR2'
DDR3 = 'DDR3'
DDR4 = 'DDR4'
DDR5 = 'DDR5'
DDR6 = 'DDR6'
@unique
class RamFormat(Enum):
DIMM = 'DIMM'
SODIMM = 'SODIMM'
@unique
class DataStorageInterface(Enum):
ATA = 'ATA'
USB = 'USB'
PCI = 'PCI'

View File

@ -30,18 +30,37 @@ class JoinedTableMixin:
class Event(Thing): class Event(Thing):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
title = Column(Unicode(STR_BIG_SIZE), default='', nullable=False) name = Column(Unicode(STR_BIG_SIZE), default='', nullable=False)
name.comment = """
A name or title for the event. Used when searching for events.
"""
type = Column(Unicode) type = Column(Unicode)
incidence = Column(Boolean, default=False, nullable=False) incidence = Column(Boolean, default=False, nullable=False)
closed = Column(Boolean, default=True, nullable=False) incidence.comment = """
Should this event be reviewed due some anomaly?
""" """
closed = Column(Boolean, default=True, nullable=False)
closed.comment = """
Whether the author has finished the event. Whether the author has finished the event.
After this is set to True, no modifications are allowed. After this is set to True, no modifications are allowed.
""" """
error = Column(Boolean, default=False, nullable=False) error = Column(Boolean, default=False, nullable=False)
error.comment = """
Did the event fail?
For example, a failure in ``Erase`` means that the data storage
unit did not erase correctly.
"""
description = Column(Unicode, default='', nullable=False) description = Column(Unicode, default='', nullable=False)
description.comment = """
A comment about the event.
"""
date = Column(DateTime) date = Column(DateTime)
date.comment = """
When this event happened.
Leave it blank if it is happening now
(the field ``created`` is used instead).
This is used for example when creating events retroactively.
"""
snapshot_id = Column(UUID(as_uuid=True), ForeignKey('snapshot.id', snapshot_id = Column(UUID(as_uuid=True), ForeignKey('snapshot.id',
use_alter=True, use_alter=True,
name='snapshot_events')) name='snapshot_events'))
@ -347,6 +366,31 @@ class StressTest(Test):
pass pass
class Benchmark(EventWithOneDevice):
pass
class BenchmarkDataStorage(Benchmark):
readSpeed = Column(Float(decimal_return_scale=2), nullable=False)
writeSpeed = Column(Float(decimal_return_scale=2), nullable=False)
class BenchmarkWithRate(Benchmark):
rate = Column(SmallInteger, nullable=False)
class BenchmarkProcessor(BenchmarkWithRate):
pass
class BenchmarkProcessorSysbench(BenchmarkProcessor):
pass
class BenchmarkRamSysbench(BenchmarkWithRate):
pass
# Listeners # Listeners
@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True) @event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True)
@event.listens_for(Install.device, 'set', retval=True, propagate=True) @event.listens_for(Install.device, 'set', retval=True, propagate=True)

View File

@ -17,9 +17,10 @@ from teal.db import Model
class Event(Thing): class Event(Thing):
id = ... # type: Column id = ... # type: Column
title = ... # type: Column name = ... # type: Column
date = ... # type: Column date = ... # type: Column
type = ... # type: Column type = ... # type: Column
error = ... # type: Column
incidence = ... # type: Column incidence = ... # type: Column
description = ... # type: Column description = ... # type: Column
finalized = ... # type: Column finalized = ... # type: Column
@ -32,7 +33,7 @@ class Event(Thing):
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.id = ... # type: UUID self.id = ... # type: UUID
self.title = ... # type: str self.name = ... # type: str
self.type = ... # type: str self.type = ... # type: str
self.incidence = ... # type: bool self.incidence = ... # type: bool
self.closed = ... # type: bool self.closed = ... # type: bool
@ -108,6 +109,9 @@ class SnapshotRequest(Model):
class Rate(EventWithOneDevice): class Rate(EventWithOneDevice):
rating = ... # type: Column
appearance = ... # type: Column
functionality = ... # type: Column
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.rating = ... # type: float self.rating = ... # type: float

View File

@ -8,6 +8,7 @@ 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, \
RATE_POSITIVE, RatingSoftware, SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength RATE_POSITIVE, RatingSoftware, SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength
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
@ -17,18 +18,14 @@ from teal.resource import Schema
class Event(Thing): class Event(Thing):
id = Integer(dump_only=True) id = Integer(dump_only=True)
title = String(default='', name = String(default='', validate=Length(STR_BIG_SIZE), description=m.Event.name.comment)
validate=Length(STR_BIG_SIZE), date = DateTime('iso', description=m.Event.date.comment)
description='A name or title for the event. Used when searching for events.') error = Boolean(default=False, description=m.Event.error.comment)
date = DateTime('iso', description='When this event happened. ' incidence = Boolean(default=False, description=m.Event.incidence.comment)
'Leave it blank if it is happening now. '
'This is used when creating events retroactively.')
error = Boolean(default=False, description='Did the event fail?')
incidence = Boolean(default=False,
description='Should this event be reviewed due some anomaly?')
snapshot = NestedOn('Snapshot', dump_only=True) snapshot = NestedOn('Snapshot', dump_only=True)
components = NestedOn(Component, dump_only=True, many=True) components = NestedOn(Component, dump_only=True, many=True)
description = String(default='', description='A comment about the event.') description = String(default='', description=m.Event.description.comment)
author = NestedOn(User, dump_only=True, exclude=('token',))
class EventWithOneDevice(Event): class EventWithOneDevice(Event):

View File

@ -0,0 +1,52 @@
from flask import current_app as app, jsonify
from marshmallow import Schema as MarshmallowSchema
from marshmallow.fields import Float, Nested, Str
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.event.models import Rate
from ereuse_devicehub.resources.tag import Tag
from teal.marshmallow import IsType
from teal.query import Between, Equal, ILike, Or, Query
from teal.resource import Resource, Schema, View
class Inventory(Schema):
pass
class RateQ(Query):
rating = Between(Rate.rating, Float())
appearance = Between(Rate.appearance, Float())
functionality = Between(Rate.functionality, Float())
class TagQ(Query):
id = Or(ILike(Tag.id), required=True)
org = ILike(Tag.org)
class Filters(Query):
type = Or(Equal(Device.type, Str(validate=IsType(Device.t))))
model = ILike(Device.model)
manufacturer = ILike(Device.manufacturer)
serialNumber = ILike(Device.serial_number)
rating = Nested(RateQ) # todo db join
tag = Nested(TagQ) # todo db join
class InventoryView(View):
class FindArgs(MarshmallowSchema):
where = Nested(Filters, default={})
def find(self, args):
devices = Device.query.filter_by()
inventory = {
'devices': app.resources[Device.t].schema.dump()
}
return jsonify(inventory)
class InventoryDef(Resource):
SCHEMA = Inventory
VIEW = InventoryView
AUTH = True

View File

@ -11,4 +11,10 @@ STR_XSM_SIZE = 16
class Thing(db.Model): class Thing(db.Model):
__abstract__ = True __abstract__ = True
updated = db.Column(db.DateTime, onupdate=datetime.utcnow) updated = db.Column(db.DateTime, onupdate=datetime.utcnow)
updated.comment = """
When this was last changed.
"""
created = db.Column(db.DateTime, default=datetime.utcnow) created = db.Column(db.DateTime, default=datetime.utcnow)
created.comment = """
When Devicehub created this.
"""

View File

@ -3,7 +3,7 @@ from enum import Enum
from marshmallow import post_load from marshmallow import post_load
from marshmallow.fields import DateTime, List, String, URL from marshmallow.fields import DateTime, List, String, URL
from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources import models as m
from teal.resource import Schema from teal.resource import Schema
@ -22,9 +22,8 @@ class Thing(Schema):
type = String(description='Only required when it is nested.') type = String(description='Only required when it is nested.')
url = URL(dump_only=True, description='The URL of the resource.') url = URL(dump_only=True, description='The URL of the resource.')
same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs') same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs')
updated = DateTime('iso', dump_only=True) updated = DateTime('iso', dump_only=True, description=m.Thing.updated)
created = DateTime('iso', dump_only=True) created = DateTime('iso', dump_only=True, description=m.Thing.created)
author = NestedOn('User', dump_only=True, exclude=('token',))
@post_load @post_load
def remove_type(self, data: dict): def remove_type(self, data: dict):

View File

@ -8,6 +8,7 @@ setup(
license='Affero', license='Affero',
author='eReuse.org team', author='eReuse.org team',
author_email='x.bustamante@ereuse.org', author_email='x.bustamante@ereuse.org',
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.0a1',
@ -34,5 +35,5 @@ setup(
'Topic :: Internet :: WWW/HTTP :: HTTP Servers', 'Topic :: Internet :: WWW/HTTP :: HTTP Servers',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
'License :: OSI Approved :: GNU Affero General Public License v3' 'License :: OSI Approved :: GNU Affero General Public License v3'
], ]
) )

49
tests/test_inventory.py Normal file
View File

@ -0,0 +1,49 @@
import pytest
from sqlalchemy.sql.elements import BinaryExpression
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.inventory import Filters, InventoryView
from teal.utils import compiled
@pytest.mark.usefixtures('app_context')
def test_inventory_filters():
schema = Filters()
q = schema.load({
'type': ['Microtower', 'Laptop'],
'manufacturer': 'Dell',
'rating': {
'rating': [3, 6],
'appearance': [2, 4]
},
'tag': {
'id': ['bcn-', 'activa-02']
}
})
s, params = compiled(Device, q)
# Order between query clauses can change
assert '(device.type = %(type_1)s OR device.type = %(type_2)s)' in s
assert 'device.manufacturer ILIKE %(manufacturer_1)s' in s
assert 'rate.rating BETWEEN %(rating_1)s AND %(rating_2)s' in s
assert 'rate.appearance BETWEEN %(appearance_1)s AND %(appearance_2)s' in s
assert '(tag.id ILIKE %(id_1)s OR tag.id ILIKE %(id_2)s)' in s
assert params == {
'type_1': 'Microtower',
'rating_2': 6.0,
'manufacturer_1': 'Dell%',
'appearance_1': 2.0,
'appearance_2': 4.0,
'id_1': 'bcn-%',
'rating_1': 3.0,
'id_2': 'activa-02%',
'type_2': 'Laptop'
}
@pytest.mark.usefixtures('app_context')
def test_inventory_query():
schema = InventoryView.FindArgs()
args = schema.load({
'where': {'type': ['Computer']}
})
assert isinstance(args['where'], BinaryExpression), '``where`` must be a SQLAlchemy query'