diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index 5bfcd079..2188cd28 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -13,6 +13,7 @@ from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, AppRateDe from ereuse_devicehub.resources.inventory import InventoryDef from ereuse_devicehub.resources.tag import TagDef from ereuse_devicehub.resources.user import OrganizationDef, UserDef +from teal.auth import TokenAuth from teal.config import Config @@ -20,7 +21,7 @@ class DevicehubConfig(Config): RESOURCE_DEFINITIONS = { DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, MicrotowerDef, ComputerMonitorDef, ComponentDef, GraphicCardDef, DataStorageDef, - SolidStateDriveDef, + SolidStateDriveDef, HardDriveDef, MotherboardDef, NetworkAdapterDef, RamModuleDef, ProcessorDef, UserDef, OrganizationDef, TagDef, EventDef, AddDef, RemoveDef, EraseBasicDef, EraseSectorsDef, StepDef, StepZeroDef, StepRandomDef, RateDef, AggregateRateDef, WorkbenchRateDef, @@ -43,6 +44,14 @@ class DevicehubConfig(Config): It is used by default, for example, when creating tags. """ + API_DOC_CONFIG_TITLE = 'Devicehub' + API_DOC_CONFIG_VERSION = '0.2' + API_DOC_CONFIG_COMPONENTS = { + 'securitySchemes': { + 'bearerAuth': TokenAuth.API_DOCS + } + } + API_DOC_CLASS_DISCRIMINATOR = 'type' def __init__(self, db: str = None) -> None: if not self.ORGANIZATION_NAME or not self.ORGANIZATION_TAX_ID: diff --git a/ereuse_devicehub/resources/device/__init__.py b/ereuse_devicehub/resources/device/__init__.py index 0689629a..e7b80bb4 100644 --- a/ereuse_devicehub/resources/device/__init__.py +++ b/ereuse_devicehub/resources/device/__init__.py @@ -13,64 +13,80 @@ class DeviceDef(Resource): class ComputerDef(DeviceDef): + VIEW = None SCHEMA = Computer class DesktopDef(ComputerDef): + VIEW = None SCHEMA = Desktop class LaptopDef(ComputerDef): + VIEW = None SCHEMA = Laptop class NetbookDef(ComputerDef): + VIEW = None SCHEMA = Netbook class ServerDef(ComputerDef): + VIEW = None SCHEMA = Server class MicrotowerDef(ComputerDef): + VIEW = None SCHEMA = Microtower class ComputerMonitorDef(DeviceDef): + VIEW = None SCHEMA = ComputerMonitor class ComponentDef(DeviceDef): + VIEW = None SCHEMA = Component class GraphicCardDef(ComponentDef): + VIEW = None SCHEMA = GraphicCard class DataStorageDef(ComponentDef): + VIEW = None SCHEMA = DataStorage class HardDriveDef(DataStorageDef): + VIEW = None SCHEMA = HardDrive class SolidStateDriveDef(DataStorageDef): + VIEW = None SCHEMA = SolidStateDrive class MotherboardDef(ComponentDef): + VIEW = None SCHEMA = Motherboard class NetworkAdapterDef(ComponentDef): + VIEW = None SCHEMA = NetworkAdapter class RamModuleDef(ComponentDef): + VIEW = None SCHEMA = RamModule class ProcessorDef(ComponentDef): + VIEW = None SCHEMA = Processor diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 6762caca..c443dad0 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -18,6 +18,10 @@ from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, c class Device(Thing): + """ + Base class for any type of physical object that can be identified. + """ + id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id.comment = """ The identifier of the device for this database. @@ -45,12 +49,18 @@ class Device(Thing): """ depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 3)) color = Column(ColorType) + color.comment = """ + + """ @property def events(self) -> list: """ - All the events performed to the device, - ordered by ascending creation time. + All the events where the device participated, including + 1) events performed directly to the device, 2) events performed + to a component, and 3) events performed to a parent device. + + Events are returned by ascending creation time. """ return sorted(chain(self.events_multiple, self.events_one), key=attrgetter('created')) diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 831d33a4..43e1b25e 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -1,7 +1,6 @@ from marshmallow import post_load, pre_load 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 @@ -9,21 +8,29 @@ from ereuse_devicehub.resources.device import models as m from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, RamFormat, RamInterface from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.schemas import Thing, UnitCodes -from teal.marshmallow import ValidationError +from teal.marshmallow import EnumField, ValidationError class Device(Thing): - id = Integer(description=m.Device.id, dump_only=True) - hid = Str(dump_only=True, description=m.Device.hid) - tags = NestedOn('Tag', many=True, collection_class=OrderedSet) + id = Integer(description=m.Device.id.comment.strip(), dump_only=True) + hid = Str(dump_only=True, description=m.Device.hid.comment.strip()) + tags = NestedOn('Tag', + many=True, + collection_class=OrderedSet, + description='The set of tags that identify the device.') model = Str(validate=Length(max=STR_BIG_SIZE)) manufacturer = Str(validate=Length(max=STR_SIZE)) serial_number = Str(data_key='serialNumber') - product_id = Str(data_key='productId') - weight = Float(validate=Range(0.1, 3), unit=UnitCodes.kgm, description=m.Device.weight) - width = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.width) - height = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.height) - events = NestedOn('Event', many=True, dump_only=True) + weight = Float(validate=Range(0.1, 3), + unit=UnitCodes.kgm, + description=m.Device.weight.comment.strip()) + width = Float(validate=Range(0.1, 3), + unit=UnitCodes.m, + description=m.Device.width.comment.strip()) + height = Float(validate=Range(0.1, 3), + unit=UnitCodes.m, + description=m.Device.height.comment.strip()) + events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__) events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet) @pre_load @@ -76,15 +83,15 @@ class Microtower(Computer): class ComputerMonitor(Device): - size = Float(description=m.ComputerMonitor.size.comment, validate=Range(2, 150)) + size = Float(description=m.ComputerMonitor.size.comment.strip(), validate=Range(2, 150)) technology = EnumField(ComputerMonitorTechnologies, - description=m.ComputerMonitor.technology.comment) + description=m.ComputerMonitor.technology.comment.strip()) resolution_width = Integer(data_key='resolutionWidth', validate=Range(10, 20000), - description=m.ComputerMonitor.resolution_width.comment) + description=m.ComputerMonitor.resolution_width.comment.strip()) resolution_height = Integer(data_key='resolutionHeight', validate=Range(10, 20000), - description=m.ComputerMonitor.resolution_height.comment) + description=m.ComputerMonitor.resolution_height.comment.strip()) class Component(Device): @@ -101,9 +108,6 @@ class DataStorage(Component): size = Integer(validate=Range(0, 10 ** 8), unit=UnitCodes.mbyte, description='The size of the hard-drive in MB.') - erasure = NestedOn('EraseBasic', load_only=True) - tests = NestedOn('TestHardDrive', many=True, load_only=True) - benchmarks = NestedOn('BenchmarkHardDrive', load_only=True, many=True) class HardDrive(DataStorage): diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index c8b90009..645b9fd1 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -3,12 +3,29 @@ from teal.resource import View class DeviceView(View): + + def get(self, id): + """ + Devices view + --- + description: Gets a device or multiple devices. + parameters: + - name: id + type: integer + in: path + description: The identifier of the device. + responses: + 200: + description: The device or devices. + """ + return super().get(id) + def one(self, id: int): """Gets one device.""" device = Device.query.filter_by(id=id).one() return self.schema.jsonify(device) def find(self, args: dict): - """Gets many devices""" + """Gets many devices.""" devices = Device.query.all() return self.schema.jsonify(devices, many=True) diff --git a/ereuse_devicehub/resources/event/__init__.py b/ereuse_devicehub/resources/event/__init__.py index 12e38732..8c71fe76 100644 --- a/ereuse_devicehub/resources/event/__init__.py +++ b/ereuse_devicehub/resources/event/__init__.py @@ -18,62 +18,77 @@ class EventDef(Resource): class AddDef(EventDef): + VIEW = None SCHEMA = Add class RemoveDef(EventDef): + VIEW = None SCHEMA = Remove class EraseBasicDef(EventDef): + VIEW = None SCHEMA = EraseBasic class EraseSectorsDef(EraseBasicDef): + VIEW = None SCHEMA = EraseSectors class StepDef(Resource): + VIEW = None SCHEMA = Step class StepZeroDef(StepDef): + VIEW = None SCHEMA = StepZero class StepRandomDef(StepDef): + VIEW = None SCHEMA = StepRandom class RateDef(EventDef): + VIEW = None SCHEMA = Rate class AggregateRateDef(RateDef): + VIEW = None SCHEMA = AggregateRate class WorkbenchRateDef(RateDef): + VIEW = None SCHEMA = WorkbenchRate class PhotoboxUserDef(RateDef): + VIEW = None SCHEMA = PhotoboxUserRate class PhotoboxSystemRateDef(RateDef): + VIEW = None SCHEMA = PhotoboxSystemRate class AppRateDef(RateDef): + VIEW = None SCHEMA = AppRate class InstallDef(EventDef): + VIEW = None SCHEMA = Install class SnapshotDef(EventDef): + VIEW = None SCHEMA = Snapshot VIEW = SnapshotView @@ -86,36 +101,45 @@ class SnapshotDef(EventDef): class TestDef(EventDef): + VIEW = None SCHEMA = Test class TestDataStorageDef(TestDef): + VIEW = None SCHEMA = TestDataStorage class StressTestDef(TestDef): + VIEW = None SCHEMA = StressTest class BenchmarkDef(EventDef): + VIEW = None SCHEMA = Benchmark class BenchmarkDataStorageDef(BenchmarkDef): + VIEW = None SCHEMA = BenchmarkDataStorage class BenchmarkWithRateDef(BenchmarkDef): + VIEW = None SCHEMA = BenchmarkWithRate class BenchmarkProcessorDef(BenchmarkWithRateDef): + VIEW = None SCHEMA = BenchmarkProcessor class BenchmarkProcessorSysbenchDef(BenchmarkProcessorDef): + VIEW = None SCHEMA = BenchmarkProcessorSysbench class BenchmarkRamSysbenchDef(BenchmarkWithRateDef): + VIEW = None SCHEMA = BenchmarkRamSysbench diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 5ba10cc0..df5a9e58 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -46,8 +46,7 @@ class Event(Thing): closed.comment = """ Whether the author has finished the event. After this is set to True, no modifications are allowed. - - By default are events are closed when performed. + By default events are closed when performed. """ error = Column(Boolean, default=False, nullable=False) error.comment = """ diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 4a478400..3deec116 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -3,7 +3,6 @@ from marshmallow import ValidationError, validates_schema from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \ UUID from marshmallow.validate import Length, Range -from marshmallow_enum import EnumField from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.device.schemas import Component, Device @@ -13,21 +12,23 @@ 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 -from teal.marshmallow import Version +from teal.marshmallow import EnumField, Version from teal.resource import Schema class Event(Thing): id = UUID(dump_only=True) - 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) + name = String(default='', + validate=Length(STR_BIG_SIZE), + description=m.Event.name.comment.strip()) + date = DateTime('iso', description=m.Event.date.comment.strip()) + error = Boolean(default=False, description=m.Event.error.comment.strip()) + incidence = Boolean(default=False, description=m.Event.incidence.comment.strip()) snapshot = NestedOn('Snapshot', dump_only=True) components = NestedOn(Component, dump_only=True, many=True) - description = String(default='', description=m.Event.description.comment) + description = String(default='', description=m.Event.description.comment.strip()) author = NestedOn(User, dump_only=True, exclude=('token',)) - closed = Boolean(missing=True, description=m.Event.closed.comment) + closed = Boolean(missing=True, description=m.Event.closed.comment.strip()) class EventWithOneDevice(Event): diff --git a/ereuse_devicehub/resources/event/views.py b/ereuse_devicehub/resources/event/views.py index c38e4f9e..81b0109b 100644 --- a/ereuse_devicehub/resources/event/views.py +++ b/ereuse_devicehub/resources/event/views.py @@ -8,8 +8,7 @@ from sqlalchemy.util import OrderedSet from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.models import Component, Computer from ereuse_devicehub.resources.enums import RatingSoftware, SnapshotSoftware -from ereuse_devicehub.resources.event.models import Event, ManualRate, Snapshot, TestDataStorage, \ - WorkbenchRate +from ereuse_devicehub.resources.event.models import Event, ManualRate, Snapshot, WorkbenchRate from teal.resource import View @@ -85,16 +84,3 @@ class SnapshotView(View): ret = self.schema.jsonify(snapshot) # transform it back ret.status_code = 201 return ret - - -class TestHardDriveView(View): - def post(self): - t = request.get_json() # type: dict - # noinspection PyArgumentList - test = TestDataStorage(snapshot_id=t.pop('snapshot'), device_id=t.pop('device'), **t) - return test - - -class StressTestView(View): - def post(self): - t = request.get_json() # type: dict diff --git a/ereuse_devicehub/resources/inventory.py b/ereuse_devicehub/resources/inventory.py index c79c46d0..cf614e15 100644 --- a/ereuse_devicehub/resources/inventory.py +++ b/ereuse_devicehub/resources/inventory.py @@ -58,13 +58,39 @@ class InventoryView(View): sort = Nested(Sorting, missing=[Device.created.desc()]) page = Integer(validate=Range(min=1), missing=1) - def find(self, args: dict): + def get(self, id): + """Inventory view + --- + description: Supports the inventory view of ``devicehub-client``; returns + all the devices, groups and widgets of this Devicehub instance. + responses: + 200: + description: The inventory. + schema: + type: object + properties: + devices: + type: array + items: + $ref: '#/definitions/Device' + pagination: + type: object + properties: + page: + type: integer + minimum: 0 + perPage: + type: integer + minimum: 0 + total: + type: integer + minimum: 0 """ - Supports the inventory view of ``devicehub-client``; returns - all the devices, groups and widgets of this Devicehub instance. + # todo .format(yaml.load(schema2parameters(self.FindArgs, default_in='path', name='path'))) + return super().get(id) - The result can be filtered, sorted, and paginated. - """ + def find(self, args: dict): + """See :meth:`.get` above.""" devices = Device.query \ .filter(*args['filter']) \ .order_by(*args['sort']) \ diff --git a/ereuse_devicehub/resources/schemas.py b/ereuse_devicehub/resources/schemas.py index 7c166d3a..ee658d3c 100644 --- a/ereuse_devicehub/resources/schemas.py +++ b/ereuse_devicehub/resources/schemas.py @@ -22,8 +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, description=m.Thing.updated) - created = DateTime('iso', dump_only=True, description=m.Thing.created) + updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment.strip()) + created = DateTime('iso', dump_only=True, description=m.Thing.created.comment.strip()) @post_load def remove_type(self, data: dict): diff --git a/tests/test_basic.py b/tests/test_basic.py index c848f82b..f4e939e9 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,6 +1,6 @@ import pytest -from ereuse_devicehub.devicehub import Devicehub +from ereuse_devicehub.client import Client def test_dependencies(): @@ -12,6 +12,27 @@ def test_dependencies(): # noinspection PyArgumentList -def test_init(app: Devicehub): - """Tests app initialization.""" - pass +def test_api_docs(client: Client): + """Tests /apidocs correct initialization.""" + docs, _ = client.get('/apidocs') + assert set(docs['paths'].keys()) == { + '/tags/{id}/device', + '/inventories/', + '/apidocs', + '/users/', + '/devices/', + '/tags/', + '/snapshots/', + '/users/login', + '/events/' + } + assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'} + assert docs['components']['securitySchemes']['bearerAuth'] == { + 'description': 'Basic scheme with token.', + 'in': 'header', + 'description:': 'HTTP Basic scheme', + 'type': 'http', + 'scheme': 'basic', + 'name': 'Authorization' + } + assert len(docs['definitions']) == 46 diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 14fe80e0..07c8f7ef 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -313,13 +313,13 @@ def test_erase(user: UserClient): snapshot = snapshot_and_check(user, s, ('EraseSectors',), perform_second_snapshot=True) storage, *_ = snapshot['components'] assert storage['type'] == 'SolidStateDrive', 'Components must be ordered by input order' - storage, _ = user.get(res=SolidStateDrive, item=storage['id']) # Let's get storage events too + storage, _ = user.get(res=Device, item=storage['id']) # Let's get storage events too # order: creation time descending _snapshot1, erasure1, _snapshot2, erasure2 = storage['events'] assert erasure1['type'] == erasure2['type'] == 'EraseSectors' assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot' assert snapshot == user.get(res=Event, item=_snapshot2['id'])[0] - erasure, _ = user.get(res=EraseBasic, item=erasure1['id']) + erasure, _ = user.get(res=Event, item=erasure1['id']) assert len(erasure['steps']) == 2 assert erasure['steps'][0]['startTime'] == '2018-06-01T08:15:00+00:00' assert erasure['steps'][0]['endTime'] == '2018-06-01T09:16:00+00:00'