Add inventory and missing fields
This commit is contained in:
parent
02a0332dd6
commit
b56fbeeca7
|
@ -139,12 +139,12 @@ is as follows:
|
|||
``Snapshot``.
|
||||
3. In **T3**, WorkbenchServer submits the ``Erase`` with the ``Snapshot``
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
Optionally, Devicehub understands receiving a ``Snapshot`` with all
|
||||
|
|
22
docs/getting.rst
Normal file
22
docs/getting.rst
Normal 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.
|
||||
|
|
@ -9,6 +9,7 @@ from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, EventDef,
|
|||
PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \
|
||||
StepRandomDef, StepZeroDef, TestDataStorageDef, TestDef, WorkbenchRateDef, EraseBasicDef, \
|
||||
EraseSectorsDef
|
||||
from ereuse_devicehub.resources.inventory import InventoryDef
|
||||
from ereuse_devicehub.resources.tag import TagDef
|
||||
from ereuse_devicehub.resources.user import OrganizationDef, UserDef
|
||||
from teal.config import Config
|
||||
|
@ -22,7 +23,7 @@ class DevicehubConfig(Config):
|
|||
OrganizationDef, TagDef, EventDef, AddDef, RemoveDef, EraseBasicDef, EraseSectorsDef,
|
||||
StepDef, StepZeroDef, StepRandomDef, RateDef, AggregateRateDef, WorkbenchRateDef,
|
||||
PhotoboxUserDef, PhotoboxSystemRateDef, InstallDef, SnapshotDef, TestDef,
|
||||
TestDataStorageDef, WorkbenchRateDef
|
||||
TestDataStorageDef, WorkbenchRateDef, InventoryDef
|
||||
}
|
||||
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' # type: str
|
||||
|
|
|
@ -4,12 +4,13 @@ from operator import attrgetter
|
|||
from typing import Dict, Set
|
||||
|
||||
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.orm import ColumnProperty, backref, relationship
|
||||
from sqlalchemy.util import OrderedSet
|
||||
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_utils.naming import Naming
|
||||
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range
|
||||
|
@ -151,6 +152,7 @@ class GraphicCard(JoinedComponentTableMixin, Component):
|
|||
|
||||
class DataStorage(JoinedComponentTableMixin, Component):
|
||||
size = Column(Integer, check_range('size', min=1, max=10 ** 8))
|
||||
interface = Column(DBEnum(DataStorageInterface))
|
||||
|
||||
|
||||
class HardDrive(DataStorage):
|
||||
|
@ -182,3 +184,5 @@ class Processor(JoinedComponentTableMixin, Component):
|
|||
class RamModule(JoinedComponentTableMixin, Component):
|
||||
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
||||
speed = Column(Float, check_range('speed', min=100, max=10000))
|
||||
interface = Column(DBEnum(RamInterface))
|
||||
format = Column(DBEnum(RamFormat))
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from marshmallow.fields import Float, Integer, Str
|
||||
from marshmallow.validate import Length, OneOf, Range
|
||||
from marshmallow_enum import EnumField
|
||||
from sqlalchemy.util import OrderedSet
|
||||
|
||||
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.schemas import Thing, UnitCodes
|
||||
|
||||
|
@ -105,3 +107,5 @@ class Processor(Component):
|
|||
class RamModule(Component):
|
||||
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
|
||||
speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
|
||||
interface = EnumField(RamInterface)
|
||||
format = EnumField(RamFormat)
|
||||
|
|
|
@ -3,7 +3,6 @@ from enum import Enum, IntEnum, unique
|
|||
from typing import Union
|
||||
|
||||
|
||||
|
||||
@unique
|
||||
class SnapshotSoftware(Enum):
|
||||
"""The algorithm_software used to perform the Snapshot."""
|
||||
|
@ -125,3 +124,28 @@ class SnapshotExpectedEvents(Enum):
|
|||
|
||||
BOX_RATE_5 = 1, 5
|
||||
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'
|
||||
|
|
|
@ -30,18 +30,37 @@ class JoinedTableMixin:
|
|||
|
||||
class Event(Thing):
|
||||
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)
|
||||
incidence = Column(Boolean, default=False, nullable=False)
|
||||
closed = Column(Boolean, default=True, nullable=False)
|
||||
incidence.comment = """
|
||||
Should this event be reviewed due some anomaly?
|
||||
"""
|
||||
Whether the author has finished the event.
|
||||
After this is set to True, no modifications are allowed.
|
||||
closed = Column(Boolean, default=True, nullable=False)
|
||||
closed.comment = """
|
||||
Whether the author has finished the event.
|
||||
After this is set to True, no modifications are allowed.
|
||||
"""
|
||||
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.comment = """
|
||||
A comment about the event.
|
||||
"""
|
||||
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',
|
||||
use_alter=True,
|
||||
name='snapshot_events'))
|
||||
|
@ -347,6 +366,31 @@ class StressTest(Test):
|
|||
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
|
||||
@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True)
|
||||
@event.listens_for(Install.device, 'set', retval=True, propagate=True)
|
||||
|
|
|
@ -17,9 +17,10 @@ from teal.db import Model
|
|||
|
||||
class Event(Thing):
|
||||
id = ... # type: Column
|
||||
title = ... # type: Column
|
||||
name = ... # type: Column
|
||||
date = ... # type: Column
|
||||
type = ... # type: Column
|
||||
error = ... # type: Column
|
||||
incidence = ... # type: Column
|
||||
description = ... # type: Column
|
||||
finalized = ... # type: Column
|
||||
|
@ -32,7 +33,7 @@ class Event(Thing):
|
|||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.id = ... # type: UUID
|
||||
self.title = ... # type: str
|
||||
self.name = ... # type: str
|
||||
self.type = ... # type: str
|
||||
self.incidence = ... # type: bool
|
||||
self.closed = ... # type: bool
|
||||
|
@ -108,6 +109,9 @@ class SnapshotRequest(Model):
|
|||
|
||||
|
||||
class Rate(EventWithOneDevice):
|
||||
rating = ... # type: Column
|
||||
appearance = ... # type: Column
|
||||
functionality = ... # type: Column
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.rating = ... # type: float
|
||||
|
|
|
@ -8,6 +8,7 @@ from ereuse_devicehub.marshmallow import NestedOn
|
|||
from ereuse_devicehub.resources.device.schemas import Component, Device
|
||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||
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.schemas import Thing
|
||||
from ereuse_devicehub.resources.user.schemas import User
|
||||
|
@ -17,18 +18,14 @@ from teal.resource import Schema
|
|||
|
||||
class Event(Thing):
|
||||
id = Integer(dump_only=True)
|
||||
title = String(default='',
|
||||
validate=Length(STR_BIG_SIZE),
|
||||
description='A name or title for the event. Used when searching for events.')
|
||||
date = DateTime('iso', description='When this event happened. '
|
||||
'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?')
|
||||
name = String(default='', validate=Length(STR_BIG_SIZE), description=m.Event.name.comment)
|
||||
date = DateTime('iso', description=m.Event.date.comment)
|
||||
error = Boolean(default=False, description=m.Event.error.comment)
|
||||
incidence = Boolean(default=False, description=m.Event.incidence.comment)
|
||||
snapshot = NestedOn('Snapshot', dump_only=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):
|
||||
|
|
52
ereuse_devicehub/resources/inventory.py
Normal file
52
ereuse_devicehub/resources/inventory.py
Normal 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
|
|
@ -11,4 +11,10 @@ STR_XSM_SIZE = 16
|
|||
class Thing(db.Model):
|
||||
__abstract__ = True
|
||||
updated = db.Column(db.DateTime, onupdate=datetime.utcnow)
|
||||
updated.comment = """
|
||||
When this was last changed.
|
||||
"""
|
||||
created = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
created.comment = """
|
||||
When Devicehub created this.
|
||||
"""
|
||||
|
|
|
@ -3,7 +3,7 @@ from enum import Enum
|
|||
from marshmallow import post_load
|
||||
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
|
||||
|
||||
|
||||
|
@ -22,9 +22,8 @@ class Thing(Schema):
|
|||
type = String(description='Only required when it is nested.')
|
||||
url = URL(dump_only=True, description='The URL of the resource.')
|
||||
same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs')
|
||||
updated = DateTime('iso', dump_only=True)
|
||||
created = DateTime('iso', dump_only=True)
|
||||
author = NestedOn('User', dump_only=True, exclude=('token',))
|
||||
updated = DateTime('iso', dump_only=True, description=m.Thing.updated)
|
||||
created = DateTime('iso', dump_only=True, description=m.Thing.created)
|
||||
|
||||
@post_load
|
||||
def remove_type(self, data: dict):
|
||||
|
|
3
setup.py
3
setup.py
|
@ -8,6 +8,7 @@ setup(
|
|||
license='Affero',
|
||||
author='eReuse.org team',
|
||||
author_email='x.bustamante@ereuse.org',
|
||||
include_package_data=True,
|
||||
description='A system to manage devices focusing reuse.',
|
||||
install_requires=[
|
||||
'teal>=0.2.0a1',
|
||||
|
@ -34,5 +35,5 @@ setup(
|
|||
'Topic :: Internet :: WWW/HTTP :: HTTP Servers',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'License :: OSI Approved :: GNU Affero General Public License v3'
|
||||
],
|
||||
]
|
||||
)
|
||||
|
|
49
tests/test_inventory.py
Normal file
49
tests/test_inventory.py
Normal 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'
|
Reference in a new issue