diff --git a/README.md b/README.md index db54c0af..a02b5a04 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Our main objectives are: - To highly integrate with existing IT Asset Management Systems. - To be decentralized. -Devicehub is built with [Teal](https://github.com/bustawin/teal) and +Devicehub is built with [Teal](https://github.com/ereuse/teal) and [Flask](http://flask.pocoo.org). ## Installing diff --git a/docs/actions.rst b/docs/actions.rst index 59fa7b33..45b19b7e 100644 --- a/docs/actions.rst +++ b/docs/actions.rst @@ -1,15 +1,14 @@ -Actions and states -################## - Actions -******* - +####### Actions are events performed to devices, changing their **state**. Actions can have attributes defining **where** it happened, **who** performed them, **when**, etc. Actions are stored in a log for each device. An exemplifying action can be ``Repair``, which dictates that a device has been repaired, -after this action, the device is in the ``repaired`` state. +after this action, the device is in the ``repaired`` state. Another +example is performing a ``Sell`` to agent 1 (now this agent *owns* +the device), and then performing another ``Sell`` to agent 2 (now +agent 2 is the owner). Devicehub actions inherit from `schema actions `_, are written in Pascal case and using @@ -19,14 +18,14 @@ is going to be / must be repaired, whereas ``Repair`` states that the reparation happened. The former actions have the preposition *To* prefixing the verb. -Actions and states affect devices in different ways or **dimensions**. +:ref:`actions:Actions` and :ref:`states:States` affect devices in +different ways or **dimensions**. For example, ``Repair`` affects the **physical** dimension of a device, and ``Sell`` the **political** dimension of a device. A device can be in several states at the same time, one per dimension; ie. a device can be ``repaired`` (physical) and ``reserved`` (political), but not ``repaired`` and ``disposed`` at the same time: - - Physical actions: The following actions describe and react on the Physical condition of the devices. @@ -37,7 +36,7 @@ but not ``repaired`` and ``disposed`` at the same time: - DisposeWaste, Recover - Association actions: Actions that change the associations users have with devices; - ie. the **owners**, **usufructuarees**, **reservees**, + ie. the **owners**, **usufructuarees** (*from usufruct*), **reservees** (*from reserve*), and **physical possessors**. - Trade @@ -60,15 +59,8 @@ but not ``repaired`` and ``disposed`` at the same time: The following index has all the actions (please note we are moving from calling them ``Event`` to call them ``Action``): +Schema +****** + .. dhlist:: :module: ereuse_devicehub.resources.event.schemas - - -States -****** -.. autoclass:: ereuse_devicehub.resources.device.states.State - -.. uml:: states.puml - -.. autoclass:: ereuse_devicehub.resources.device.states.Trading -.. autoclass:: ereuse_devicehub.resources.device.states.Physical diff --git a/docs/devices.rst b/docs/devices.rst index 697802a7..1974eebd 100644 --- a/docs/devices.rst +++ b/docs/devices.rst @@ -1,6 +1,19 @@ Devices -######### +####### +Devices are objects that can be identified, and they are the +main entity in a Devicehub. Refer to :ref:`devices:Device` for more +info. +Schema +****** +The following schema represents all the device types and their +properties. + +.. dhlist:: + :module: ereuse_devicehub.resources.device.schemas + +API +*** You can retrieve devices using ``GET /devices/``, or a specific device by ``GET /devices/24``. @@ -46,5 +59,4 @@ The result is a JSON object with the following fields: - **next**: The number of the next page, if any. - **last**: The number of the last page, if any. -.. dhlist:: - :module: ereuse_devicehub.resources.device.schemas + diff --git a/docs/index.rst b/docs/index.rst index 0269992a..a8547ff9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,19 +14,36 @@ reusing devices, created under the project Our main objectives are: -- To offer a common IT Asset Management for donors, receivers and IT - professionals so they can manage devices and exchange them. - This is, reusing –and ultimately recycling. +- To offer a common IT Asset Management for distributors, refurbishers, + receivers and other IT professionals so they can manage devices and exchange them. + This is, reusing —and ultimately recycling. - To automatically recollect, analyse, process and share (controlling privacy) metadata about devices with other tools of the eReuse ecosystem to guarantee traceability, and to provide inputs for the indicators which measure circularity. -- To highly integrate with existing IT Asset Management Systems. -- To be decentralized. -Devicehub is built with `Teal `_ and -`Flask `_. +The main entity of a Devicehub are :ref:`devices:Devices`, which is any object that +can be identified. Devices are divided in *types* (like ``Computer``), +and each one defines *properties*, like serial number, weight, +quality rating, pricing, or a list of owners. +We perform :ref:`actions:Actions` on devices, which are events that +change their *state* and *properties*. Examples are sales, reparations, +quality diagnostics, data wiping, and location. +Actions are stored in the traceability log of the device. + +Devicehub is decentralized, and each instance is an inventory. We can +share and exchange devices between inventories —like in real live between +organizations. + +:ref:`tags:Tags` identify devices through those organizations and their +internal systems. With Devicehub we can manage and print smart tags with +QR and NFC capabilities, operating devices by literally scanning them. + +Devicehub is a REST API built with `Teal `_ and +`Flask `_ using `PostgreSQL `_. +`DevicehubClient `_ is the +front–end that consumes this API. .. toctree:: :maxdepth: 2 @@ -34,6 +51,7 @@ Devicehub is built with `Teal `_ and api devices actions + states tags lots diff --git a/docs/states.rst b/docs/states.rst new file mode 100644 index 00000000..7c253e9a --- /dev/null +++ b/docs/states.rst @@ -0,0 +1,48 @@ +States +###### +.. note:: In construction. + +A mutable property of a device result of applying an +:ref:`actions:Action` to it. + +States are represented as properties in :ref:`devices:Device` and +sub–types. They can be steps in a workflow +(like ``sold`` and ``payed``, part of a trading), or properties +describing computed values from applying events (like a list of owners, +or a quality rating). + +There are three types of states: + +* **Trading**: a workflow of states resulting from applying the action + :ref:`actions:Trade`. +* **Physical**: a workflow of states resulting from applying + physical actions (ref. :ref:`actions:Actions`). +* **Attributes**: miscellaneous device properties that are not part of + a workflow. + +.. uml:: states.puml + +Trading +******* + Trading states. + +:cvar Reserved: The device has been reserved. +:cvar Cancelled: The device has been cancelled. +:cvar Sold: The device has been sold. +:cvar Donated: The device is donated. +:cvar Renting: The device is in renting +:cvar ToBeDisposed: The device is disposed. + This is the end of life of a device. +:cvar ProductDisposed: The device has been removed + from the facility. It does not mean end-of-life. + +Physical +******** + Physical states. + +:cvar ToBeRepaired: The device has been selected for reparation. +:cvar Repaired: The device has been repaired. +:cvar Preparing: The device is going to be or being prepared. +:cvar Prepared: The device has been prepared. +:cvar ReadyToBeUsed: The device is in working conditions. +:cvar InUse: The device is being reported to be in active use. diff --git a/ereuse_devicehub/db.py b/ereuse_devicehub/db.py index 5fc091cb..b82e714f 100644 --- a/ereuse_devicehub/db.py +++ b/ereuse_devicehub/db.py @@ -36,6 +36,7 @@ class SQLAlchemy(SchemaSQLAlchemy): # manually import them all the time UUID = postgresql.UUID CIText = citext.CIText + PSQL_INT_MAX = 2147483648 def drop_all(self, bind='__all__', app=None, common_schema=True): """A faster nuke-like option to drop everything.""" diff --git a/ereuse_devicehub/resources/device/states.py b/ereuse_devicehub/resources/device/states.py index 1cadfb39..93c5a2af 100644 --- a/ereuse_devicehub/resources/device/states.py +++ b/ereuse_devicehub/resources/device/states.py @@ -6,6 +6,10 @@ from ereuse_devicehub.resources.event import models as e class State(Enum): + """A mutable property of a device result of applying an + :ref:`actions:Action` to it. + """ + @classmethod def events(cls): """Events participating in this state.""" diff --git a/ereuse_devicehub/resources/documents/device_row.py b/ereuse_devicehub/resources/documents/device_row.py new file mode 100644 index 00000000..a75ed60e --- /dev/null +++ b/ereuse_devicehub/resources/documents/device_row.py @@ -0,0 +1,119 @@ +from collections import OrderedDict + +from flask import current_app + +from ereuse_devicehub.resources.device import models as d +from ereuse_devicehub.resources.event.models import TestDataStorage, BenchmarkDataStorage + + +class DeviceRow(OrderedDict): + NUMS = { + d.Display.t: 1, + d.Processor.t: 2, + d.GraphicCard.t: 2, + d.Motherboard.t: 1, + d.NetworkAdapter.t: 2, + d.SoundCard.t: 2 + } + + # TODO Add more fields information + def __init__(self, device: d.Device) -> None: + super().__init__() + self.device = device + # General information about device + self['Type'] = device.t + if isinstance(device, d.Computer): + self['Chassis'] = device.chassis + else: + self['Chassis'] = '' + self['Tag 1'] = self['Tag 2'] = self['Tag 3'] = '' + for i, tag in zip(range(1, 3), device.tags): + self['Tag {}'.format(i)] = format(tag) + self['Serial Number'] = device.serial_number + self['Model'] = device.model + self['Manufacturer'] = device.manufacturer + # self['State'] = device.last_event_of() + self['Registered in'] = format(device.created, '%c') + self['Price'] = device.price + if isinstance(device, d.Computer): + self['Processor'] = device.processor_model + self['RAM (GB)'] = device.ram_size + self['Data Storage Size (MB)'] = device.data_storage_size + rate = device.rate + if rate: + self['Rate'] = rate.rating + self['Range'] = rate.rating_range + self['Processor Rate'] = rate.processor + self['Processor Range'] = rate.workbench.processor_range + self['RAM Rate'] = rate.ram + self['RAM Range'] = rate.workbench.ram_range + self['Data Storage Rate'] = rate.data_storage + self['Data Storage Range'] = rate.workbench.data_storage_range + # More specific information about components + if isinstance(device, d.Computer): + self.components() + + def components(self): + """ + Function to get all components information of a device + """ + assert isinstance(self.device, d.Computer) + # todo put an input specific order (non alphabetic) & where are a list of types components + for type in sorted(current_app.resources[d.Component.t].subresources_types): # type: str + max = self.NUMS.get(type, 4) + if type not in ['Component', 'HardDrive', 'SolidStateDrive']: + i = 1 + for component in (r for r in self.device.components if r.type == type): + self.fill_component(type, i, component) + i += 1 + if i > max: + break + while i <= max: + self.fill_component(type, i) + i += 1 + + def fill_component(self, type, i, component=None): + """ + Function to put specific information of components in OrderedDict (csv) + :param type: type of component + :param component: device.components + """ + self['{} {}'.format(type, i)] = format(component) if component else '' + self['{} {} Manufacturer'.format(type, i)] = component.serial_number if component else '' + self['{} {} Model'.format(type, i)] = component.serial_number if component else '' + self['{} {} Serial Number'.format(type, i)] = component.serial_number if component else '' + + """ Particular fields for component GraphicCard """ + if isinstance(component, d.GraphicCard): + self['{} {} Memory (MB)'.format(type, i)] = component.memory + + """ Particular fields for component DataStorage.t -> (HardDrive, SolidStateDrive) """ + if isinstance(component, d.DataStorage): + self['{} {} Size (MB)'.format(type, i)] = component.size + self['{} {} Privacy'.format(type, i)] = component.privacy + try: + self['{} {} Lifetime'.format(type, i)] = component.last_event_of(TestDataStorage).lifetime + except: + self['{} {} Lifetime'.format(type, i)] = '' + try: + self['{} {} Reading speed'.format(type, i)] = component.last_event_of(BenchmarkDataStorage).read_speed + except: + self['{} {} Reading speed'.format(type, i)] = '' + try: + self['{} {} Writing speed'.format(type, i)] = component.last_event_of(BenchmarkDataStorage).write_speed + except: + self['{} {} Writing speed'.format(type, i)] = '' + + """ Particular fields for component Processor """ + if isinstance(component, d.Processor): + self['{} {} Number of cores'.format(type, i)] = component.cores + self['{} {} Speed (GHz)'.format(type, i)] = component.speed + + """ Particular fields for component RamModule """ + if isinstance(component, d.RamModule): + self['{} {} Size (MB)'.format(type, i)] = component.size + self['{} {} Speed (MHz)'.format(type, i)] = component.speed + + # todo add Display size, ... + # todo add NetworkAdapter speedLink? + # todo add some ComputerAccessories diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index 34121d8c..72582a21 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -1,5 +1,8 @@ +import csv +import datetime import enum import uuid +from io import StringIO from typing import Callable, Iterable, Tuple import boltons @@ -7,11 +10,14 @@ import flask import flask_weasyprint import teal.marshmallow from boltons import urlutils +from flask import make_response +from teal.cache import cache from teal.resource import Resource from ereuse_devicehub.db import db from ereuse_devicehub.resources.device import models as devs from ereuse_devicehub.resources.device.views import DeviceView +from ereuse_devicehub.resources.documents.device_row import DeviceRow from ereuse_devicehub.resources.event import models as evs @@ -97,6 +103,33 @@ class DocumentView(DeviceView): return flask.render_template('documents/erasure.html', **params) +class DevicesDocumentView(DeviceView): + @cache(datetime.timedelta(minutes=1)) + def find(self, args: dict): + query = self.query(args) + return self.generate_post_csv(query) + + def generate_post_csv(self, query): + """ + Get device query and put information in csv format + :param query: + :return: + """ + data = StringIO() + cw = csv.writer(data) + first = True + for device in query: + d = DeviceRow(device) + if first: + cw.writerow(name for name in d.keys()) + first = False + cw.writerow(v for v in d.values()) + output = make_response(data.getvalue()) + output.headers['Content-Disposition'] = 'attachment; filename=export.csv' + output.headers['Content-type'] = 'text/csv' + return output + + class DocumentDef(Resource): __type__ = 'Document' SCHEMA = None @@ -124,3 +157,9 @@ class DocumentDef(Resource): self.add_url_rule('/erasures/', defaults=d, view_func=view, methods=get) self.add_url_rule('/erasures/<{}:{}>'.format(self.ID_CONVERTER.value, self.ID_NAME), view_func=view, methods=get) + devices_view = DevicesDocumentView.as_view('devicesDocumentView', + definition=self, + auth=app.auth) + if self.AUTH: + devices_view = app.auth.requires_auth(devices_view) + self.add_url_rule('/devices/', defaults=d, view_func=devices_view, methods=get) diff --git a/ereuse_devicehub/resources/event/__init__.py b/ereuse_devicehub/resources/event/__init__.py index 2acc7fd9..a9f12950 100644 --- a/ereuse_devicehub/resources/event/__init__.py +++ b/ereuse_devicehub/resources/event/__init__.py @@ -4,7 +4,7 @@ from teal.resource import Converters, Resource from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.event import schemas -from ereuse_devicehub.resources.event.views import EventView, SnapshotView +from ereuse_devicehub.resources.event.views import EventView class EventDef(Resource): @@ -90,13 +90,14 @@ class InstallDef(EventDef): class SnapshotDef(EventDef): - VIEW = SnapshotView + VIEW = None SCHEMA = schemas.Snapshot def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()): + url_prefix = '/{}'.format(EventDef.resource) super().__init__(app, import_name, static_folder, static_url_path, template_folder, url_prefix, subdomain, url_defaults, root_path, cli_commands) self.sync = Sync() diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index bdd86af4..35a453c0 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -546,7 +546,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, @@ -686,6 +685,15 @@ class TestDataStorage(Test): t += self.description return t + @property + def reported_uncorrectable_errors(self): + return self._reported_uncorrectable_errors + + @reported_uncorrectable_errors.setter + def reported_uncorrectable_errors(self, value): + # There is no value for a stratospherically big number + self._reported_uncorrectable_errors = min(value, db.PSQL_INT_MAX) + class StressTest(Test): """The act of stressing (putting to the maximum capacity) @@ -809,7 +817,12 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice): """ rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE)) - rating.comment = """The rating for the content.""" + rating.comment = """The rating for the content. + + This value is automatically set by rating algorithms. In case that + no algorithm is defined per the device and type of rate, this + value is None. + """ software = Column(DBEnum(RatingSoftware)) software.comment = """The algorithm used to produce this rating.""" version = Column(StrictVersionType) @@ -817,8 +830,7 @@ class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice): @property def rating_range(self) -> RatingRange: - if self.rating: - return RatingRange.from_score(self.rating) + return RatingRange.from_score(self.rating) if self.rating else None @declared_attr def __mapper_args__(cls): @@ -1224,7 +1236,7 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice): @classmethod def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal: """Returns a Decimal value with the correct scale for Price.price.""" - if isinstance(value, float): + if isinstance(value, (float, int)): value = Decimal(value) # equation from marshmallow.fields.Decimal return value.quantize(Decimal((0, (1,), -cls.SCALE)), rounding=rounding) @@ -1308,8 +1320,8 @@ class EreusePrice(Price): self.warranty2 = EreusePrice.Type(rate[self.WARRANTY2][role], price) def __init__(self, rating: AggregateRate, **kwargs) -> None: - if rating.rating_range == RatingRange.VERY_LOW: - raise ValueError('Cannot compute price for Range.VERY_LOW') + if not rating.rating_range or rating.rating_range == RatingRange.VERY_LOW: + raise InvalidRangeForPrice() # We pass ROUND_UP strategy so price is always greater than what refurbisher... amounts price = self.to_price(rating.rating * self.MULTIPLIER[rating.device.__class__], ROUND_UP) super().__init__(rating=rating, @@ -1643,3 +1655,7 @@ def update_parent(target: Union[EraseBasic, Test, Install], device: Device, _, _ target.parent = None if isinstance(device, Component): target.parent = device.parent + + +class InvalidRangeForPrice(ValueError): + pass diff --git a/ereuse_devicehub/resources/event/rate/main.py b/ereuse_devicehub/resources/event/rate/main.py index 69213ba7..d8879bd9 100644 --- a/ereuse_devicehub/resources/event/rate/main.py +++ b/ereuse_devicehub/resources/event/rate/main.py @@ -4,8 +4,8 @@ from typing import Set, Union from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.enums import RatingSoftware -from ereuse_devicehub.resources.event.models import AggregateRate, EreusePrice, Rate, \ - WorkbenchRate +from ereuse_devicehub.resources.event.models import AggregateRate, EreusePrice, \ + InvalidRangeForPrice, Rate, WorkbenchRate from ereuse_devicehub.resources.event.rate.workbench import v1_0 RATE_TYPES = { @@ -72,7 +72,6 @@ def main(rating_model: WorkbenchRate, if soft == software and vers == version: aggregation = AggregateRate.from_workbench_rate(rating) events.add(aggregation) - with suppress(ValueError): - # We will have exception if range == VERY_LOW + with suppress(InvalidRangeForPrice): # We will have exception if range == VERY_LOW events.add(EreusePrice(aggregation)) return events diff --git a/ereuse_devicehub/resources/event/views.py b/ereuse_devicehub/resources/event/views.py index 0d6e846c..e0247bfe 100644 --- a/ereuse_devicehub/resources/event/views.py +++ b/ereuse_devicehub/resources/event/views.py @@ -12,14 +12,21 @@ from ereuse_devicehub.resources.device.models import Component, Computer from ereuse_devicehub.resources.enums import SnapshotSoftware from ereuse_devicehub.resources.event.models import Event, Snapshot, WorkbenchRate +SUPPORTED_WORKBENCH = StrictVersion('11.0') + class EventView(View): def post(self): """Posts an event.""" json = request.get_json(validate=False) - if 'type' not in json: + if not json or 'type' not in json: raise ValidationError('Resource needs a type.') - e = app.resources[json['type']].schema.load(json) + # todo there should be a way to better get subclassess resource + # defs + resource_def = app.resources[json['type']] + e = resource_def.schema.load(json) + if json['type'] == Snapshot.t: + return self.snapshot(e, resource_def) Model = db.Model._decl_class_registry.data[json['type']]() event = Model(**e) db.session.add(event) @@ -34,25 +41,20 @@ class EventView(View): event = Event.query.filter_by(id=id).one() return self.schema.jsonify(event) - -SUPPORTED_WORKBENCH = StrictVersion('11.0') - - -class SnapshotView(View): - def post(self): + def snapshot(self, snapshot_json: dict, resource_def): """ Performs a Snapshot. See `Snapshot` section in docs for more info. """ - s = request.get_json() # Note that if we set the device / components into the snapshot # model object, when we flush them to the db we will flush # snapshot, and we want to wait to flush snapshot at the end - device = s.pop('device') # type: Computer - components = s.pop('components') \ - if s['software'] == SnapshotSoftware.Workbench else None # type: List[Component] - snapshot = Snapshot(**s) + device = snapshot_json.pop('device') # type: Computer + components = None + if snapshot_json['software'] == SnapshotSoftware.Workbench: + components = snapshot_json.pop('components') # type: List[Component] + snapshot = Snapshot(**snapshot_json) # Remove new events from devices so they don't interfere with sync events_device = set(e for e in device.events_one) @@ -62,10 +64,9 @@ class SnapshotView(View): for component in components: component.events_one.clear() - # noinspection PyArgumentList assert not device.events_one assert all(not c.events_one for c in components) if components else True - db_device, remove_events = self.resource_def.sync.run(device, components) + db_device, remove_events = resource_def.sync.run(device, components) snapshot.device = db_device snapshot.events |= remove_events | events_device # Set events to snapshot # commit will change the order of the components by what diff --git a/ereuse_devicehub/resources/inventory/schema.py b/ereuse_devicehub/resources/inventory/schema.py index 7d7a7dea..57b157d5 100644 --- a/ereuse_devicehub/resources/inventory/schema.py +++ b/ereuse_devicehub/resources/inventory/schema.py @@ -8,4 +8,3 @@ class Inventory(Thing): id = mf.String(dump_only=True) name = mf.String(dump_only=True) tag_provider = teal.marshmallow.URL(dump_only=True, data_key='tagProvider') - tag_token = mf.UUID(dump_only=True, data_key='tagToken') diff --git a/ereuse_devicehub/resources/user/models.py b/ereuse_devicehub/resources/user/models.py index da5555dd..b471724a 100644 --- a/ereuse_devicehub/resources/user/models.py +++ b/ereuse_devicehub/resources/user/models.py @@ -24,6 +24,7 @@ class User(Thing): backref=db.backref('users', lazy=True, collection_class=set), secondary=lambda: UserInventory.__table__, collection_class=set) + # todo set restriction that user has, at least, one active db def __init__(self, email, password=None, inventories=None) -> None: @@ -41,6 +42,10 @@ class User(Thing): def __repr__(self) -> str: return ''.format(self) + @property + def type(self) -> str: + return self.__class__.__name__ + @property def individual(self): """The individual associated for this database, or None.""" diff --git a/requirements.txt b/requirements.txt index bf9d37e1..d81f1b7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ requests[security]==2.19.1 requests-mock==1.5.2 SQLAlchemy==1.2.17 SQLAlchemy-Utils==0.33.11 -teal==0.2.0a36 +teal==0.2.0a38 webargs==4.0.0 Werkzeug==0.14.1 sqlalchemy-citext==1.3.post0 diff --git a/setup.py b/setup.py index fb53a90d..6468ee7c 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', install_requires=[ - 'teal>=0.2.0a36', # teal always first + 'teal>=0.2.0a38', # teal always first 'click', 'click-spinner', 'ereuse-utils[naming, test, session, cli]>=0.4b21', diff --git a/tests/files/1-device-with-components.snapshot.yaml b/tests/files/1-device-with-components.snapshot.yaml index 97b0dc98..afc0ce20 100644 --- a/tests/files/1-device-with-components.snapshot.yaml +++ b/tests/files/1-device-with-components.snapshot.yaml @@ -5,20 +5,21 @@ device: type: Desktop chassis: Tower components: -- manufacturer: p1c1m - serialNumber: p1c1s - type: Motherboard -- manufacturer: p1c2m - serialNumber: p1c2s - model: p1c2 - speed: 1.23 - cores: 2 - type: Processor -- manufacturer: p1c3m - serialNumber: p1c3s - type: GraphicCard - memory: 1.5 + - manufacturer: p1c1m + serialNumber: p1c1s + type: Motherboard + - manufacturer: p1c2m + serialNumber: p1c2s + model: p1c2 + speed: 1.23 + cores: 2 + type: Processor + - manufacturer: p1c3m + serialNumber: p1c3s + type: GraphicCard + memory: 1.5 elapsed: 25 software: Workbench uuid: 76860eca-c3fd-41f6-a801-6af7bd8cf832 version: '11.0' +type: Snapshot diff --git a/tests/files/2-second-device-with-components-of-first.snapshot.yaml b/tests/files/2-second-device-with-components-of-first.snapshot.yaml index 5062da56..0ff6c2b0 100644 --- a/tests/files/2-second-device-with-components-of-first.snapshot.yaml +++ b/tests/files/2-second-device-with-components-of-first.snapshot.yaml @@ -5,16 +5,17 @@ device: type: Desktop chassis: Microtower components: -- manufacturer: p2c1m - serialNumber: p2c1s - type: Motherboard -- manufacturer: p1c2m - serialNumber: p1c2s - model: p1c2 - speed: 1.23 - cores: 2 - type: Processor + - manufacturer: p2c1m + serialNumber: p2c1s + type: Motherboard + - manufacturer: p1c2m + serialNumber: p1c2s + model: p1c2 + speed: 1.23 + cores: 2 + type: Processor elapsed: 25 software: Workbench uuid: f2e02261-87a1-4a50-b9b7-92c0e476e5f2 version: '11.0' +type: Snapshot diff --git a/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml b/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml index bdf72b37..2a3ea83b 100644 --- a/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml +++ b/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml @@ -5,17 +5,18 @@ device: type: Desktop chassis: Microtower components: -- manufacturer: p1c2m - serialNumber: p1c2s - model: p1c2 - type: Processor - cores: 2 - speed: 1.23 -- manufacturer: p1c3m - serialNumber: p1c3s - type: GraphicCard - memory: 1.5 + - manufacturer: p1c2m + serialNumber: p1c2s + model: p1c2 + type: Processor + cores: 2 + speed: 1.23 + - manufacturer: p1c3m + serialNumber: p1c3s + type: GraphicCard + memory: 1.5 elapsed: 30 software: Workbench uuid: 3be271b6-5ef4-47d8-8237-5e1133eebfc6 version: '11.0' +type: Snapshot diff --git a/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml b/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml index 463f9b20..ba20cc28 100644 --- a/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml +++ b/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml @@ -5,16 +5,17 @@ device: type: Desktop chassis: Tower components: -- manufacturer: p1c4m - serialNumber: p1c4s - type: NetworkAdapter - speed: 1000 - wireless: False -- manufacturer: p1c3m - serialNumber: p1c3s - type: GraphicCard - memory: 1.5 + - manufacturer: p1c4m + serialNumber: p1c4s + type: NetworkAdapter + speed: 1000 + wireless: False + - manufacturer: p1c3m + serialNumber: p1c3s + type: GraphicCard + memory: 1.5 elapsed: 25 software: Workbench uuid: fd007eb4-48e3-454a-8763-169491904c6e version: '11.0' +type: Snapshot diff --git a/tests/files/basic.csv b/tests/files/basic.csv new file mode 100644 index 00000000..f78453e9 --- /dev/null +++ b/tests/files/basic.csv @@ -0,0 +1,2 @@ +Type,Chassis,Tag 1,Tag 2,Tag 3,Serial Number,Model,Manufacturer,Registered in,Price,Processor,RAM (GB),Data Storage Size (MB),Rate,Range,Processor Rate,Processor Range,RAM Rate,RAM Range,Data Storage Rate,Data Storage Range,DataStorage 1,DataStorage 1 Manufacturer,DataStorage 1 Model,DataStorage 1 Serial Number,DataStorage 2,DataStorage 2 Manufacturer,DataStorage 2 Model,DataStorage 2 Serial Number,DataStorage 3,DataStorage 3 Manufacturer,DataStorage 3 Model,DataStorage 3 Serial Number,DataStorage 4,DataStorage 4 Manufacturer,DataStorage 4 Model,DataStorage 4 Serial Number,Display 1,Display 1 Manufacturer,Display 1 Model,Display 1 Serial Number,GraphicCard 1,GraphicCard 1 Manufacturer,GraphicCard 1 Model,GraphicCard 1 Serial Number,GraphicCard 1 Memory (MB),GraphicCard 2,GraphicCard 2 Manufacturer,GraphicCard 2 Model,GraphicCard 2 Serial Number,Motherboard 1,Motherboard 1 Manufacturer,Motherboard 1 Model,Motherboard 1 Serial Number,NetworkAdapter 1,NetworkAdapter 1 Manufacturer,NetworkAdapter 1 Model,NetworkAdapter 1 Serial Number,NetworkAdapter 2,NetworkAdapter 2 Manufacturer,NetworkAdapter 2 Model,NetworkAdapter 2 Serial Number,Processor 1,Processor 1 Manufacturer,Processor 1 Model,Processor 1 Serial Number,Processor 1 Number of cores,Processor 1 Speed (GHz),Processor 2,Processor 2 Manufacturer,Processor 2 Model,Processor 2 Serial Number,RamModule 1,RamModule 1 Manufacturer,RamModule 1 Model,RamModule 1 Serial Number,RamModule 1 Size (MB),RamModule 1 Speed (MHz),RamModule 2,RamModule 2 Manufacturer,RamModule 2 Model,RamModule 2 Serial Number,RamModule 3,RamModule 3 Manufacturer,RamModule 3 Model,RamModule 3 Serial Number,RamModule 4,RamModule 4 Manufacturer,RamModule 4 Model,RamModule 4 Serial Number,SoundCard 1,SoundCard 1 Manufacturer,SoundCard 1 Model,SoundCard 1 Serial Number,SoundCard 2,SoundCard 2 Manufacturer,SoundCard 2 Model,SoundCard 2 Serial Number +Desktop,Microtower,,,,d1s,d1ml,d1mr,Tue Mar 5 19:54:18 2019,,p1ml,0,0,0.8,Very low,1.0,Very low,1.0,Very low,1.0,Very low,,,,,,,,,,,,,,,,,,,,,"GraphicCard 2: model gc1ml, S/N gc1s",gc1s,gc1s,gc1s,,,,,,,,,,,,,,,,,,"Processor 4: model p1ml, S/N p1s",p1s,p1s,p1s,,1.6,,,,,"RamModule 3: model rm1ml, S/N rm1s",rm1s,rm1s,rm1s,,1333,,,,,,,,,,,,,,,,,,,, diff --git a/tests/files/computer-monitor.csv b/tests/files/computer-monitor.csv new file mode 100644 index 00000000..0c529700 --- /dev/null +++ b/tests/files/computer-monitor.csv @@ -0,0 +1,2 @@ +Type,Chassis,Tag 1,Tag 2,Tag 3,Serial Number,Model,Manufacturer,Registered in,Price +ComputerMonitor,,,,,cn0fp446728728541c8s,1707fpf,dell,Wed Oct 24 20:57:18 2018 \ No newline at end of file diff --git a/tests/files/desktop-9644w8n-lenovo-0169622.snapshot.yaml b/tests/files/desktop-9644w8n-lenovo-0169622.snapshot.yaml new file mode 100644 index 00000000..f5cf71f7 --- /dev/null +++ b/tests/files/desktop-9644w8n-lenovo-0169622.snapshot.yaml @@ -0,0 +1,121 @@ +{ + "closed": true, + "components": [ + { + "events": [], + "manufacturer": "Intel Corporation", + "model": "NM10/ICH7 Family High Definition Audio Controller", + "serialNumber": null, + "type": "SoundCard" + }, + { + "events": [], + "manufacturer": "Broadcom Inc. and subsidiaries", + "model": "NetLink BCM5786 Gigabit Ethernet PCI Express", + "serialNumber": "00:1a:6b:5e:7f:10", + "speed": 1000, + "type": "NetworkAdapter", + "wireless": false + }, + { + "events": [], + "format": "DIMM", + "interface": "DDR", + "manufacturer": null, + "model": null, + "serialNumber": null, + "size": 1024, + "speed": 133.0, + "type": "RamModule" + }, + { + "events": [], + "format": "DIMM", + "interface": "DDR", + "manufacturer": null, + "model": null, + "serialNumber": null, + "size": 1024, + "speed": 133.0, + "type": "RamModule" + }, + { + "address": 64, + "events": [ + { + "elapsed": 33, + "rate": 32.9274, + "type": "BenchmarkProcessorSysbench" + }, + { + "elapsed": 0, + "rate": 8771.5, + "type": "BenchmarkProcessor" + } + ], + "manufacturer": "Intel Corp.", + "model": "Intel Core2 Duo CPU E4500 @ 2.20GHz", + "serialNumber": null, + "speed": 1.1, + "threads": 2, + "type": "Processor" + }, + { + "events": [], + "manufacturer": "Intel Corporation", + "memory": 256.0, + "model": "82946GZ/GL Integrated Graphics Controller", + "serialNumber": null, + "type": "GraphicCard" + }, + { + "events": [], + "firewire": 0, + "manufacturer": "LENOVO", + "model": "LENOVO", + "pcmcia": 0, + "serial": 1, + "serialNumber": null, + "slots": 0, + "type": "Motherboard", + "usb": 5 + } + ], + "device": { + "chassis": "Microtower", + "events": [ + { + "appearanceRange": "D", + "biosRange": "E", + "functionalityRange": "D", + "type": "WorkbenchRate" + }, + { + "elapsed": 300, + "severity": "Info", + "type": "StressTest" + }, + { + "elapsed": 2, + "rate": 1.4968, + "type": "BenchmarkRamSysbench" + } + ], + "manufacturer": "LENOVO", + "model": "9644W8N", + "serialNumber": "0169622", + "type": "Desktop" + }, + "elapsed": 338, + "endTime": "2019-02-13T11:57:31.378330+00:00", + "expectedEvents": [ + "Benchmark", + "TestDataStorage", + "StressTest", + "Install" + ], + "software": "Workbench", + "type": "Snapshot", + "uuid": "d7904bd3-7d0f-4918-86b1-e21bfab738f9", + "version": "11.0b5" +} diff --git a/tests/files/keyboard.csv b/tests/files/keyboard.csv new file mode 100644 index 00000000..097a48fc --- /dev/null +++ b/tests/files/keyboard.csv @@ -0,0 +1,2 @@ +Type,Chassis,Tag 1,Tag 2,Tag 3,Serial Number,Model,Manufacturer,Registered in,Price +Keyboard,,,,,bar,foo,baz,Wed Oct 24 21:01:48 2018 diff --git a/tests/files/laptop-hp_255_g3_notebook-hewlett-packard-cnd52270fw.snapshot.yaml b/tests/files/laptop-hp_255_g3_notebook-hewlett-packard-cnd52270fw.snapshot.yaml new file mode 100644 index 00000000..4824615a --- /dev/null +++ b/tests/files/laptop-hp_255_g3_notebook-hewlett-packard-cnd52270fw.snapshot.yaml @@ -0,0 +1,170 @@ +{ + "closed": true, + "components": [ + { + "events": [], + "manufacturer": "Qualcomm Atheros", + "model": "QCA9565 / AR9565 Wireless Network Adapter", + "serialNumber": "ac:e0:10:c2:e3:ac", + "type": "NetworkAdapter", + "wireless": true + }, + { + "events": [], + "manufacturer": "Realtek Semiconductor Co., Ltd.", + "model": "RTL810xE PCI Express Fast Ethernet controller", + "serialNumber": "30:8d:99:25:6c:d9", + "speed": 100, + "type": "NetworkAdapter", + "wireless": false + }, + { + "events": [], + "manufacturer": "Advanced Micro Devices, Inc. AMD/ATI", + "model": "Kabini HDMI/DP Audio", + "serialNumber": null, + "type": "SoundCard" + }, + { + "events": [], + "manufacturer": "Chicony Electronics Co.,Ltd.", + "model": "HP Webcam", + "serialNumber": "0x0001", + "type": "SoundCard" + }, + { + "events": [], + "manufacturer": "Advanced Micro Devices, Inc. AMD", + "model": "FCH Azalia Controller", + "serialNumber": null, + "type": "SoundCard" + }, + { + "events": [], + "format": "SODIMM", + "interface": "DDR3", + "manufacturer": "Hynix", + "model": "HMT451S6AFR8A-PB", + "serialNumber": "11743764", + "size": 4096, + "speed": 667.0, + "type": "RamModule" + }, + { + "address": 64, + "cores": 2, + "events": [ + { + "elapsed": 0, + "rate": 3992.32, + "type": "BenchmarkProcessor" + }, + { + "elapsed": 65, + "rate": 65.3007, + "type": "BenchmarkProcessorSysbench" + } + ], + "manufacturer": "Advanced Micro Devices AMD", + "model": "AMD E1-2100 APU with Radeon HD Graphics", + "serialNumber": null, + "speed": 0.9, + "threads": 2, + "type": "Processor" + }, + { + "events": [ + { + "elapsed": 12, + "readSpeed": 90.0, + "type": "BenchmarkDataStorage", + "writeSpeed": 30.7 + }, + { + "assessment": true, + "commandTimeout": 1341, + "currentPendingSectorCount": 0, + "elapsed": 113, + "length": "Short", + "lifetime": 1782, + "offlineUncorrectable": 0, + "powerCycleCount": 806, + "reallocatedSectorCount": 224, + "reportedUncorrectableErrors": 9961472, + "severity": "Info", + "status": "Completed without error", + "type": "TestDataStorage" + }, + { + "address": 32, + "elapsed": 690, + "name": "LinuxMint-19-x86-es-2018-12.fsa", + "severity": "Info", + "type": "Install" + } + ], + "interface": "ATA", + "manufacturer": null, + "model": "HGST HTS545050A7", + "serialNumber": "TE85134N34LNSN", + "size": 476940, + "type": "HardDrive" + }, + { + "events": [], + "manufacturer": "Advanced Micro Devices, Inc. AMD/ATI", + "memory": 256.0, + "model": "Kabini Radeon HD 8210", + "serialNumber": null, + "type": "GraphicCard" + }, + { + "events": [], + "firewire": 0, + "manufacturer": "Hewlett-Packard", + "model": "21F7", + "pcmcia": 0, + "serial": 1, + "serialNumber": "PEHERF41U8P9TV", + "slots": 0, + "type": "Motherboard", + "usb": 5 + } + ], + "device": { + "chassis": "Netbook", + "events": [ + { + "appearanceRange": "A", + "functionalityRange": "A", + "type": "WorkbenchRate" + }, + { + "elapsed": 300, + "severity": "Info", + "type": "StressTest" + }, + { + "elapsed": 6, + "rate": 5.8783, + "type": "BenchmarkRamSysbench" + } + ], + "manufacturer": "Hewlett-Packard", + "model": "HP 255 G3 Notebook", + "serialNumber": "CND52270FW", + "type": "Laptop" + }, + "elapsed": 1194, + "endTime": "2019-02-13T10:13:50.535387+00:00", + "expectedEvents": [ + "Benchmark", + "TestDataStorage", + "StressTest", + "Install" + ], + "software": "Workbench", + "type": "Snapshot", + "uuid": "ca564895-567e-4ac2-9a0d-2d1402528687", + "version": "11.0b5" +} diff --git a/tests/files/multiples_devices.csv b/tests/files/multiples_devices.csv new file mode 100644 index 00000000..6a093d0d --- /dev/null +++ b/tests/files/multiples_devices.csv @@ -0,0 +1,17 @@ +Type,Chassis,Tag 1,Tag 2,Tag 3,Serial Number,Model,Manufacturer,Registered in,Price,Processor,RAM (GB),Data Storage Size (MB),Rate,Range,Processor Rate,Processor Range,RAM Rate,RAM Range,Data Storage Rate,Data Storage Range,DataStorage 1,DataStorage 1 Manufacturer,DataStorage 1 Model,DataStorage 1 Serial Number,DataStorage 2,DataStorage 2 Manufacturer,DataStorage 2 Model,DataStorage 2 Serial Number,DataStorage 3,DataStorage 3 Manufacturer,DataStorage 3 Model,DataStorage 3 Serial Number,DataStorage 4,DataStorage 4 Manufacturer,DataStorage 4 Model,DataStorage 4 Serial Number,Display 1,Display 1 Manufacturer,Display 1 Model,Display 1 Serial Number,GraphicCard 1,GraphicCard 1 Manufacturer,GraphicCard 1 Model,GraphicCard 1 Serial Number,GraphicCard 1 Memory (MB),GraphicCard 2,GraphicCard 2 Manufacturer,GraphicCard 2 Model,GraphicCard 2 Serial Number,Motherboard 1,Motherboard 1 Manufacturer,Motherboard 1 Model,Motherboard 1 Serial Number,NetworkAdapter 1,NetworkAdapter 1 Manufacturer,NetworkAdapter 1 Model,NetworkAdapter 1 Serial Number,NetworkAdapter 2,NetworkAdapter 2 Manufacturer,NetworkAdapter 2 Model,NetworkAdapter 2 Serial Number,Processor 1,Processor 1 Manufacturer,Processor 1 Model,Processor 1 Serial Number,Processor 1 Number of cores,Processor 1 Speed (GHz),Processor 2,Processor 2 Manufacturer,Processor 2 Model,Processor 2 Serial Number,RamModule 1,RamModule 1 Manufacturer,RamModule 1 Model,RamModule 1 Serial Number,RamModule 1 Size (MB),RamModule 1 Speed (MHz),RamModule 2,RamModule 2 Manufacturer,RamModule 2 Model,RamModule 2 Serial Number,RamModule 3,RamModule 3 Manufacturer,RamModule 3 Model,RamModule 3 Serial Number,RamModule 4,RamModule 4 Manufacturer,RamModule 4 Model,RamModule 4 Serial Number,SoundCard 1,SoundCard 1 Manufacturer,SoundCard 1 Model,SoundCard 1 Serial Number,SoundCard 2,SoundCard 2 Manufacturer,SoundCard 2 Model,SoundCard 2 Serial Number +Laptop,Netbook,,,,b8oaas048286,1001pxd,asustek computer inc.,Wed Mar 6 18:22:05 2019,,intel atom cpu n455 @ 1.66ghz,1024,238475,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium,,,,,,,,,,,,,,,,,,,,,"GraphicCard 5: model atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller, S/N None",,,,256,,,,,"Motherboard 10: model 1001pxd, S/N eee0123456789",eee0123456789,eee0123456789,eee0123456789,"NetworkAdapter 2: model ar9285 wireless network adapter, S/N 74:2f:68:8b:fd:c8",74:2f:68:8b:fd:c8,74:2f:68:8b:fd:c8,74:2f:68:8b:fd:c8,"NetworkAdapter 3: model ar8152 v2.0 fast ethernet, S/N 14:da:e9:42:f6:7c",14:da:e9:42:f6:7c,14:da:e9:42:f6:7c,14:da:e9:42:f6:7c,"Processor 4: model intel atom cpu n455 @ 1.66ghz, S/N None",,,,1,1.667,,,,,"RamModule 8: model None, S/N None",,,,1024,667,,,,,,,,,,,,,"SoundCard 6: model nm10/ich7 family high definition audio controller, S/N None",,,,"SoundCard 7: model usb 2.0 uvc vga webcam, S/N 0x0001",0x0001,0x0001,0x0001 +NetworkAdapter,,,,,74:2f:68:8b:fd:c8,ar9285 wireless network adapter,qualcomm atheros,Wed Mar 6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +NetworkAdapter,,,,,14:da:e9:42:f6:7c,ar8152 v2.0 fast ethernet,qualcomm atheros,Wed Mar 6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +Processor,,,,,,intel atom cpu n455 @ 1.66ghz,intel corp.,Wed Mar 6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +GraphicCard,,,,,,atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller,intel corporation,Wed Mar 6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +SoundCard,,,,,,nm10/ich7 family high definition audio controller,intel corporation,Wed Mar 6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +SoundCard,,,,,0x0001,usb 2.0 uvc vga webcam,azurewave,Wed Mar 6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +RamModule,,,,,,,,Wed Mar 6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +HardDrive,,,,,e2024242cv86hj,hts54322,hitachi,Wed Mar6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +Motherboard,,,,,eee0123456789,1001pxd,asustek computer inc.,Wed Mar 6 18:22:05 2019,,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium +Desktop,Microtower,,,,d1s,d1ml,d1mr,Wed Mar 6 18:22:06 2019,,p1ml,0,0,0.8,Very low,1.0,Very low,1.0,Very low,1.0,Very low,,,,,,,,,,,,,,,,,,,,,"GraphicCard 12: model gc1ml, S/N gc1s",gc1s,gc1s,gc1s,,,,,,,,,,,,,,,,,,"Processor 14: model p1ml, S/N p1s",p1s,p1s,p1s,,1.6,,,,,"RamModule 13: model rm1ml, S/N rm1s",rm1s,rm1s,rm1s,,1333,,,,,,,,,,,,,,,,,,,, +GraphicCard,,,,,gc1s,gc1ml,gc1mr,Wed Mar 6 18:22:06 2019,,0.8,Very low,1.0,Very low,1.0,Very low,1.0,Very low +RamModule,,,,,rm1s,rm1ml,rm1mr,Wed Mar 6 18:22:06 2019,,0.8,Very low,1.0,Very low,1.0,Very low,1.0,Very low +Processor,,,,,p1s,p1ml,p1mr,Wed Mar 6 18:22:06 2019,,0.8,Very low,1.0,Very low,1.0,Very low,1.0,Very low +Keyboard,,,,,bar,foo,baz,Wed Mar 6 18:22:06 2019, +ComputerMonitor,,,,,cn0fp446728728541c8s,1707fpf,dell,Wed Mar 6 18:22:06 2019, diff --git a/tests/files/real-eee-1001pxd.csv b/tests/files/real-eee-1001pxd.csv new file mode 100644 index 00000000..ef09a24b --- /dev/null +++ b/tests/files/real-eee-1001pxd.csv @@ -0,0 +1,2 @@ +Type,Chassis,Tag 1,Tag 2,Tag 3,Serial Number,Model,Manufacturer,Registered in,Price,Processor,RAM (GB),Data Storage Size (MB),Rate,Range,Processor Rate,Processor Range,RAM Rate,RAM Range,Data Storage Rate,Data Storage Range,DataStorage 1,DataStorage 1 Manufacturer,DataStorage 1 Model,DataStorage 1 Serial Number,DataStorage 2,DataStorage 2 Manufacturer,DataStorage 2 Model,DataStorage 2 Serial Number,DataStorage 3,DataStorage 3 Manufacturer,DataStorage 3 Model,DataStorage 3 Serial Number,DataStorage 4,DataStorage 4 Manufacturer,DataStorage 4 Model,DataStorage 4 Serial Number,Display 1,Display 1 Manufacturer,Display 1 Model,Display 1 Serial Number,GraphicCard 1,GraphicCard 1 Manufacturer,GraphicCard 1 Model,GraphicCard 1 Serial Number,GraphicCard 1 Memory (MB),GraphicCard 2,GraphicCard 2 Manufacturer,GraphicCard 2 Model,GraphicCard 2 Serial Number,Motherboard 1,Motherboard 1 Manufacturer,Motherboard 1 Model,Motherboard 1 Serial Number,NetworkAdapter 1,NetworkAdapter 1 Manufacturer,NetworkAdapter 1 Model,NetworkAdapter 1 Serial Number,NetworkAdapter 2,NetworkAdapter 2 Manufacturer,NetworkAdapter 2 Model,NetworkAdapter 2 Serial Number,Processor 1,Processor 1 Manufacturer,Processor 1 Model,Processor 1 Serial Number,Processor 1 Number of cores,Processor 1 Speed (GHz),Processor 2,Processor 2 Manufacturer,Processor 2 Model,Processor 2 Serial Number,RamModule 1,RamModule 1 Manufacturer,RamModule 1 Model,RamModule 1 Serial Number,RamModule 1 Size (MB),RamModule 1 Speed (MHz),RamModule 2,RamModule 2 Manufacturer,RamModule 2 Model,RamModule 2 Serial Number,RamModule 3,RamModule 3 Manufacturer,RamModule 3 Model,RamModule 3 Serial Number,RamModule 4,RamModule 4 Manufacturer,RamModule 4 Model,RamModule 4 Serial Number,SoundCard 1,SoundCard 1 Manufacturer,SoundCard 1 Model,SoundCard 1 Serial Number,SoundCard 2,SoundCard 2 Manufacturer,SoundCard 2 Model,SoundCard 2 Serial Number +Laptop,Netbook,,,,b8oaas048286,1001pxd,asustek computer inc.,Tue Mar 5 19:56:08 2019,,intel atom cpu n455 @ 1.66ghz,1024,238475,1.73,Very low,1.0,Very low,1.53,Very low,3.76,Medium,,,,,,,,,,,,,,,,,,,,,"GraphicCard 5: model atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller, S/N None",,,,256,,,,,"Motherboard 10: model 1001pxd, S/N eee0123456789",eee0123456789,eee0123456789,eee0123456789,"NetworkAdapter 2: model ar9285 wireless network adapter, S/N 74:2f:68:8b:fd:c8",74:2f:68:8b:fd:c8,74:2f:68:8b:fd:c8,74:2f:68:8b:fd:c8,"NetworkAdapter 3: model ar8152 v2.0 fast ethernet, S/N 14:da:e9:42:f6:7c",14:da:e9:42:f6:7c,14:da:e9:42:f6:7c,14:da:e9:42:f6:7c,"Processor 4: model intel atom cpu n455 @ 1.66ghz, S/N None",,,,1,1.667,,,,,"RamModule 8: model None, S/N None",,,,1024,667,,,,,,,,,,,,,"SoundCard 6: model nm10/ich7 family high definition audio controller, S/N None",,,,"SoundCard 7: model usb 2.0 uvc vga webcam, S/N 0x0001",0x0001,0x0001,0x0001 diff --git a/tests/test_basic.py b/tests/test_basic.py index e1f3daab..f211503e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -21,7 +21,6 @@ def test_api_docs(client: Client): '/users/', '/devices/', '/tags/', - '/snapshots/', '/users/login/', '/events/', '/lots/', @@ -29,6 +28,7 @@ def test_api_docs(client: Client): '/lots/{id}/children', '/lots/{id}/devices', '/documents/erasures/', + '/documents/devices/', '/documents/static/{filename}', '/tags/{tag_id}/device/{device_id}', '/devices/static/{filename}' diff --git a/tests/test_db.py b/tests/test_db.py new file mode 100644 index 00000000..e5c24510 --- /dev/null +++ b/tests/test_db.py @@ -0,0 +1,30 @@ +import datetime +from uuid import UUID + +from teal.db import UniqueViolation + + +def test_unique_violation(): + class IntegrityErrorMock: + def __init__(self) -> None: + self.params = { + 'uuid': UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9'), + 'version': '11.0', + 'software': 'Workbench', 'elapsed': datetime.timedelta(0, 4), + 'expected_events': None, + 'id': UUID('dbdef3d8-2cac-48cb-adb8-419bc3e59687') + } + + def __str__(self): + return """(psycopg2.IntegrityError) duplicate key value violates unique constraint "snapshot_uuid_key" + DETAIL: Key (uuid)=(f5efd26e-8754-46bc-87bf-fbccc39d60d9) already exists. + [SQL: 'INSERT INTO snapshot (uuid, version, software, elapsed, expected_events, id) + VALUES (%(uuid)s, %(version)s, %(software)s, %(elapsed)s, CAST(%(expected_events)s + AS snapshotexpectedevents[]), %(id)s)'] [parameters: {'uuid': UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9'), + 'version': '11.0', 'software': 'Workbench', 'elapsed': datetime.timedelta(0, 4), 'expected_events': None, + 'id': UUID('dbdef3d8-2cac-48cb-adb8-419bc3e59687')}] (Background on this error at: http://sqlalche.me/e/gkpj)""" + + u = UniqueViolation(IntegrityErrorMock()) + assert u.constraint == 'snapshot_uuid_key' + assert u.field_name == 'uuid' + assert u.field_value == UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9') diff --git a/tests/test_reports.py b/tests/test_reports.py new file mode 100644 index 00000000..fe053760 --- /dev/null +++ b/tests/test_reports.py @@ -0,0 +1,169 @@ +import csv +from datetime import datetime +from io import StringIO +from pathlib import Path + +from ereuse_devicehub.client import UserClient +from ereuse_devicehub.resources.documents import documents +from ereuse_devicehub.resources.event.models import Snapshot +from tests.conftest import file + + +def test_export_basic_snapshot(user: UserClient): + """ + Test export device information in a csv file + """ + snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot) + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='devices/', + accept='text/csv', + query=[('filter', {'type': ['Computer']})]) + f = StringIO(csv_str) + obj_csv = csv.reader(f, f) + export_csv = list(obj_csv) + + # Open fixture csv and transform to list + with Path(__file__).parent.joinpath('files').joinpath('basic.csv').open() as csv_file: + obj_csv = csv.reader(csv_file) + fixture_csv = list(obj_csv) + + assert isinstance(datetime.strptime(export_csv[1][8], '%c'), datetime), \ + 'Register in field is not a datetime' + + # Pop dates fields from csv lists to compare them + fixture_csv[1] = fixture_csv[1][:8] + fixture_csv[1][9:] + export_csv[1] = export_csv[1][:8] + export_csv[1][9:] + + assert fixture_csv[0] == export_csv[0], 'Headers are not equal' + assert fixture_csv[1] == export_csv[1], 'Computer information are not equal' + + +def test_export_full_snapshot(user: UserClient): + """ + Test a export device with all information and a lot of components + """ + snapshot, _ = user.post(file('real-eee-1001pxd.snapshot.11'), res=Snapshot) + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='devices/', + accept='text/csv', + query=[('filter', {'type': ['Computer']})]) + f = StringIO(csv_str) + obj_csv = csv.reader(f, f) + export_csv = list(obj_csv) + + # Open fixture csv and transform to list + with Path(__file__).parent.joinpath('files').joinpath('real-eee-1001pxd.csv').open() \ + as csv_file: + obj_csv = csv.reader(csv_file) + fixture_csv = list(obj_csv) + + assert isinstance(datetime.strptime(export_csv[1][8], '%c'), datetime), \ + 'Register in field is not a datetime' + + # Pop dates fields from csv lists to compare them + fixture_csv[1] = fixture_csv[1][:8] + fixture_csv[1][9:] + export_csv[1] = export_csv[1][:8] + export_csv[1][9:] + + assert fixture_csv[0] == export_csv[0], 'Headers are not equal' + assert fixture_csv[1] == export_csv[1], 'Computer information are not equal' + + +def test_export_empty(user: UserClient): + """ + Test to check works correctly exporting csv without any information (no snapshot) + """ + csv_str, _ = user.get(res=documents.DocumentDef.t, + accept='text/csv', + item='devices/') + f = StringIO(csv_str) + obj_csv = csv.reader(f, f) + export_csv = list(obj_csv) + + assert len(export_csv) == 0, 'Csv is not empty' + + +def test_export_computer_monitor(user: UserClient): + """ + Test a export device type computer monitor + """ + snapshot, _ = user.post(file('computer-monitor.snapshot'), res=Snapshot) + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='devices/', + accept='text/csv', + query=[('filter', {'type': ['ComputerMonitor']})]) + f = StringIO(csv_str) + obj_csv = csv.reader(f, f) + export_csv = list(obj_csv) + # Open fixture csv and transform to list + with Path(__file__).parent.joinpath('files').joinpath('computer-monitor.csv').open() \ + as csv_file: + obj_csv = csv.reader(csv_file) + fixture_csv = list(obj_csv) + + # Pop dates fields from csv lists to compare them + fixture_csv[1] = fixture_csv[1][:8] + export_csv[1] = export_csv[1][:8] + + assert fixture_csv[0] == export_csv[0], 'Headers are not equal' + assert fixture_csv[1] == export_csv[1], 'Component information are not equal' + + +def test_export_keyboard(user: UserClient): + """ + Test a export device type keyboard + """ + snapshot, _ = user.post(file('keyboard.snapshot'), res=Snapshot) + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='devices/', + accept='text/csv', + query=[('filter', {'type': ['Keyboard']})]) + f = StringIO(csv_str) + obj_csv = csv.reader(f, f) + export_csv = list(obj_csv) + # Open fixture csv and transform to list + with Path(__file__).parent.joinpath('files').joinpath('keyboard.csv').open() as csv_file: + obj_csv = csv.reader(csv_file) + fixture_csv = list(obj_csv) + + # Pop dates fields from csv lists to compare them + fixture_csv[1] = fixture_csv[1][:8] + export_csv[1] = export_csv[1][:8] + + assert fixture_csv[0] == export_csv[0], 'Headers are not equal' + assert fixture_csv[1] == export_csv[1], 'Component information are not equal' + + +def test_export_multiple_devices(user: UserClient): + """ + Test a export multiple devices (Computers and other types) with different information + """ + # Post all devices snapshots + snapshot_pc, _ = user.post(file('real-eee-1001pxd.snapshot.11'), res=Snapshot) + snapshot_empty, _ = user.post(file('basic.snapshot'), res=Snapshot) + snapshot_keyboard, _ = user.post(file('keyboard.snapshot'), res=Snapshot) + snapshot_monitor, _ = user.post(file('computer-monitor.snapshot'), res=Snapshot) + + # need query param?? + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='devices/', + accept='text/csv') + f = StringIO(csv_str) + obj_csv = csv.reader(f, f) + export_csv = list(obj_csv) + + # Open fixture csv and transform to list + with Path(__file__).parent.joinpath('files').joinpath('multiples_devices.csv').open() \ + as csv_file: + obj_csv = csv.reader(csv_file) + fixture_csv = list(obj_csv) + + assert fixture_csv[0] == export_csv[0], 'Headers are not equal' + + max_range = max(len(export_csv), len(fixture_csv)) - 1 + # check if all devices information is correct + for i in range(1, max_range): + if isinstance(datetime.strptime(export_csv[i][8], '%c'), datetime): + export_csv[i] = export_csv[i][:8] + export_csv[i][9:] + fixture_csv[i] = fixture_csv[i][:8] + fixture_csv[i][9:] + + assert fixture_csv[i] == export_csv[i], 'Some fields are not equal' diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 2b7ac98d..ce7b5f37 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -453,3 +453,14 @@ def test_snapshot_keyboard(user: UserClient): snapshot = snapshot_and_check(user, s, event_types=('ManualRate',)) keyboard = snapshot['device'] assert keyboard['layout'] == 'ES' + + +def test_pc_rating_rate_none(user: UserClient): + """Tests a Snapshot with EraseSectors.""" + s = file('desktop-9644w8n-lenovo-0169622.snapshot') + snapshot, _ = user.post(res=Snapshot, data=s) + + +def test_pc_2(user: UserClient): + s = file('laptop-hp_255_g3_notebook-hewlett-packard-cnd52270fw.snapshot') + snapshot, _ = user.post(res=Snapshot, data=s)