diff --git a/docs/events.rst b/docs/events.rst index 22e881b2..ac784374 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -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 diff --git a/docs/getting.rst b/docs/getting.rst new file mode 100644 index 00000000..62234f18 --- /dev/null +++ b/docs/getting.rst @@ -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. + diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index f25d09f1..00eecd49 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -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 diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 926ea995..1b6177eb 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -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)) diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index cb5dd1e1..9968b6f0 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -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) diff --git a/ereuse_devicehub/resources/enums.py b/ereuse_devicehub/resources/enums.py index 9803077c..cc090c5d 100644 --- a/ereuse_devicehub/resources/enums.py +++ b/ereuse_devicehub/resources/enums.py @@ -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' diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 05813919..43e14f7a 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -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) diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index 4fff2daf..aa4a1b0b 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -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 diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index a2071011..b53d6c51 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -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): diff --git a/ereuse_devicehub/resources/inventory.py b/ereuse_devicehub/resources/inventory.py new file mode 100644 index 00000000..d1d156b6 --- /dev/null +++ b/ereuse_devicehub/resources/inventory.py @@ -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 diff --git a/ereuse_devicehub/resources/models.py b/ereuse_devicehub/resources/models.py index 190b374c..7088b175 100644 --- a/ereuse_devicehub/resources/models.py +++ b/ereuse_devicehub/resources/models.py @@ -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. + """ diff --git a/ereuse_devicehub/resources/schemas.py b/ereuse_devicehub/resources/schemas.py index 6f329c3d..7c166d3a 100644 --- a/ereuse_devicehub/resources/schemas.py +++ b/ereuse_devicehub/resources/schemas.py @@ -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): diff --git a/setup.py b/setup.py index 6526bad5..76cc7ae8 100644 --- a/setup.py +++ b/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' - ], + ] ) diff --git a/tests/test_inventory.py b/tests/test_inventory.py new file mode 100644 index 00000000..164b44cf --- /dev/null +++ b/tests/test_inventory.py @@ -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'