From 198a89a0b160ae7d38cd1923a81fd50b6f8401e5 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Sun, 3 Feb 2019 17:12:53 +0100 Subject: [PATCH] Add sphinx extension dhclass; generate better API docs; remove cache for lots --- docs/actions.rst | 271 +++---------------- docs/api.rst | 70 +++++ docs/conf.py | 133 +++++++++ docs/devices.rst | 17 +- docs/index.rst | 5 +- ereuse_devicehub/dummy/dummy.py | 10 +- ereuse_devicehub/resources/device/models.py | 102 +++++-- ereuse_devicehub/resources/device/models.pyi | 2 + ereuse_devicehub/resources/device/schemas.py | 161 +++++++---- ereuse_devicehub/resources/device/states.py | 23 ++ ereuse_devicehub/resources/enums.py | 12 +- ereuse_devicehub/resources/event/models.py | 7 + ereuse_devicehub/resources/event/schemas.py | 104 ++++--- ereuse_devicehub/resources/lot/schemas.py | 4 +- ereuse_devicehub/resources/models.py | 11 +- ereuse_devicehub/resources/schemas.py | 55 +++- ereuse_devicehub/resources/tag/view.py | 4 - 17 files changed, 609 insertions(+), 382 deletions(-) create mode 100644 docs/api.rst diff --git a/docs/actions.rst b/docs/actions.rst index cfa87d6e..59fa7b33 100644 --- a/docs/actions.rst +++ b/docs/actions.rst @@ -1,6 +1,9 @@ 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. @@ -8,13 +11,6 @@ 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. -Actions and 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. - Devicehub actions inherit from `schema actions `_, are written in Pascal case and using a verb in infinitive. Some verbs represent the willingness or @@ -23,240 +19,49 @@ is going to be / must be repaired, whereas ``Repair`` states that the reparation happened. The former actions have the preposition *To* prefixing the verb. -In the following section we define the actions and states. -To see how to perform actions to the Devicehub API head -to the `Swagger docs -`_. - -.. toctree:: - :maxdepth: 4 - - actions - -.. uml:: actions.puml +Actions and 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 -:class:`ereuse_devicehub.resources.device.states.Physical` condition -of the devices. +- Physical actions: The following actions describe and react on the + Physical condition of the devices. -ToPrepare and Prepare -================== -Prepare -------- -.. autoclass:: ereuse_devicehub.resources.event.models.Prepare -ToPrepare ---------- -.. autoclass:: ereuse_devicehub.resources.event.models.ToPrepare + - ToPrepare and prepare. + - ToRepair, Repair + - ReadyToUse + - Live + - DisposeWaste, Recover -ToRepair, Repair -================ -Repair ------- -.. autoclass:: ereuse_devicehub.resources.event.models.Repair -ToRepair --------- -.. autoclass:: ereuse_devicehub.resources.event.models.ToRepair +- Association actions: Actions that change the associations users have with devices; + ie. the **owners**, **usufructuarees**, **reservees**, + and **physical possessors**. -ReadyToUse -========== -.. autoclass:: ereuse_devicehub.resources.event.models.ReadyToUse + - Trade + - Transfer + - Organize -Live -==== -.. autoclass:: ereuse_devicehub.resources.event.models.Live +- Internal state actions: Actions providing metadata about devices that don't usually change + their state. -DisposeWaste, Recover -===================== -``RecyclingCenter`` users have two extra special events: - - ``DisposeWaste``: The device has been disposed in an unspecified - manner. - - ``Recover``: The device has been scrapped and its materials have - been recovered under a new product. - -See `ToDisposeProduct, DisposeProduct`_. - -.. todo:: Events not developed yet. - -Association actions -******************* -Actions that change the associations users have with devices; -ie. the **owners**, **usufructuarees**, **reservees**, -and **physical possessors**. - -There are three sub-dimensions: **trade**, **transfer**, -and **organize** actions. - -.. uml:: association-events.puml - -Trade -===== - -.. todo Not fully developed. - -.. autoclass:: ereuse_devicehub.resources.event.models.Trade - -Sell ----- -.. autoclass:: ereuse_devicehub.resources.event.models.Sell - -Donate ------- -.. autoclass:: ereuse_devicehub.resources.event.models.Donate - -Rent ----- -.. autoclass:: ereuse_devicehub.resources.event.models.Rent - -CancelTrade ------------ -.. autoclass:: ereuse_devicehub.resources.event.models.CancelTrade - -ToDisposeProduct, DisposeProduct --------------------------------- -.. autoclass:: ereuse_devicehub.resources.event.models.DisposeProduct -.. autoclass:: ereuse_devicehub.resources.event.models.ToDisposeProduct - -Transfer actions -================ -The act of transferring/moving devices from one place to another. - -Receive -------- -.. autoclass:: ereuse_devicehub.resources.event.models.Receive -.. autoclass:: ereuse_devicehub.resources.enums.ReceiverRole - :members: - :undoc-members: -.. autoattribute:: ereuse_devicehub.resources.device.models.Device.physical_possessor - -Organize actions -================ -.. autoclass:: ereuse_devicehub.resources.event.models.Organize - -Reserve, CancelReservation -------------------------- -Not fully developed. - -.. autoclass:: ereuse_devicehub.resources.event.models.Reserve -.. autoclass:: ereuse_devicehub.resources.event.models.CancelReservation - -Assign, Accept, Reject ----------------------- -Not developed. - -``Assign`` allocates devices to an user. The purpose or meaning -of the association is defined by the users. - -``Accept`` and ``Reject`` allow users to accept and reject the -assignments. - -.. todo:: shall we add ``Deassign`` or make ``Assign`` - always define all active users? - Assign won't be developed until further notice. - -Internal state actions -********************** -Actions providing metadata about devices that don't usually change -their state. - -Snapshot -======== -.. autoclass:: ereuse_devicehub.resources.event.models.Snapshot + - Snapshot + - Add, remove + - Erase + - Install + - Test + - Benchmark + - Rate + - Price -Add, Remove -=========== -.. autoclass:: ereuse_devicehub.resources.event.models.Add -.. autoclass:: ereuse_devicehub.resources.event.models.Remove +The following index has all the actions (please note we are moving from calling them +``Event`` to call them ``Action``): -Erase -===== -.. autoclass:: ereuse_devicehub.resources.event.models.EraseBasic -.. autoclass:: ereuse_devicehub.resources.event.models.EraseSectors -.. autoclass:: ereuse_devicehub.resources.enums.ErasureStandards - :members: -.. autoclass:: ereuse_devicehub.resources.event.models.ErasePhysical -.. autoclass:: ereuse_devicehub.resources.enums.PhysicalErasureMethod - :members: - :undoc-members: - - -Install -======= -.. autoclass:: ereuse_devicehub.resources.event.models.Install - -Test -==== -.. autoclass:: ereuse_devicehub.resources.event.models.Test - -TestDataStorage ---------------- -.. autoclass:: ereuse_devicehub.resources.event.models.TestDataStorage - -StressTest ----------- -.. autoclass:: ereuse_devicehub.resources.event.models.StressTest - -Benchmark -========= -.. autoclass:: ereuse_devicehub.resources.event.models.Benchmark - - -BenchmarkDataStorage --------------------- -.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkDataStorage - - -BenchmarkWithRate ------------------ -.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkWithRate - - -BenchmarkProcessor ------------------- -.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkProcessor - - -BenchmarkProcessorSysbench --------------------------- -.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkProcessorSysbench - - -BenchmarkRamSysbench --------------------- -.. autoclass:: ereuse_devicehub.resources.event.models.BenchmarkRamSysbench - -Rate -==== -.. autoclass:: ereuse_devicehub.resources.event.models.Rate - -The following are the values the appearance, performance, and -functionality grade can have: - -.. autoclass:: ereuse_devicehub.resources.enums.AppearanceRange - :members: - :undoc-members: -.. autoclass:: ereuse_devicehub.resources.enums.FunctionalityRange - :members: - :undoc-members: -.. autoclass:: ereuse_devicehub.resources.enums.RatingRange - -Price -===== -.. autoclass:: ereuse_devicehub.resources.event.models.Price - -Migrate -======= -Not done. - -.. autoclass:: ereuse_devicehub.resources.event.models.Migrate - -Locate -====== -todo -.. todo !! +.. dhlist:: + :module: ereuse_devicehub.resources.event.schemas States @@ -266,8 +71,4 @@ States .. uml:: states.puml .. autoclass:: ereuse_devicehub.resources.device.states.Trading - :members: - :undoc-members: .. autoclass:: ereuse_devicehub.resources.device.states.Physical - :members: - :undoc-members: diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..fc67dbef --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,70 @@ +Using the API +############# + +Devicehub is a REST API on the web that partially extends Schema.org's +ontology and it is formatted in JSON. + +The main resource are devices. However, you do not perform operations +directly against them (there is no ``POST /device``), +as you use an Action / Event to do so (you only ``GET /devices``). +For example, to upload information of devices with tests, erasures, etcetera, use +the action/event ``POST /snapshot`` (:ref:`devices-snapshot`). + +Login +***** +To use the API, you need first to log in with an existing account from the DeviceHub. +Perform ``POST /users/login/`` with the email and password fields filled:: + + POST /users/login/ + Content-Type: application/json + Accept: application/json + { + "email": "user@dhub.com", + "password: "1234" + } + +Upon success, you are answered with the account object, containing a Token field:: + + { + "id": "...", + "token: "A base 64 codified token", + "type": "User", + "inventories": [{"type": "Inventory", id: "db1", ...}, ...], + ... + } + +From this moment, any other following operation against +the API requires the following HTTP Header: +``Authorization: Basic token``. This is, the word **Basic** +followed with a **space** and then the **token**, +obtained from the account object above, **exactly as it is**. + +.. _authenticate-requests: + + +Authenticate requests +--------------------- +To explain how to operate with resources like events or devices, we +use one as an example: obtaining devices. The template of +a request is:: + + GET /devices/ + Accept: application/json + Authorization: Basic + +And an example is:: + + GET acme/devices/ + Accept: application/json + Authorization: Basic myTokenInBase64 + +Let's go through the variables: + +- ```` is the name of the inventory where you operate. + You get this value from the ``User`` object returned from the login. + The ``inventories`` field contains a set of databases the account + can operate with, being the first inventory the default one. +- ```` is the token of the account. + +See :ref:`devices:devices` for more information on how to query +devices. diff --git a/docs/conf.py b/docs/conf.py index fd4964e9..43e4683c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,19 @@ # -- Project information ----------------------------------------------------- +import importlib +import inspect +from typing import Union + +from docutils.parsers.rst import Directive, directives +from docutils.statemachine import StringList, string2lines +from marshmallow.fields import DateTime, Field +from marshmallow.schema import SchemaMeta +from teal.enums import Country, Currency, Layouts, Subdivision +from teal.marshmallow import EnumField + +from ereuse_devicehub.marshmallow import NestedOn +from ereuse_devicehub.resources.schemas import Thing project = 'Devicehub' copyright = '2018, eReuse.org team' @@ -176,3 +189,123 @@ html_favicon = 'img/favicon.ico' # autosectionlabel autosectionlabel_prefix_document = True autodoc_member_order = 'bysource' + +import docutils.nodes as n + + +class DhlistDirective(Directive): + """Generates documentation from Devicehub Schema. + + This requires :py:class:`ereuse_devicehub.resources.schemas.SchemaMeta`. + You will find in that module more information. + """ + has_content = False + + # Definition of passed-in options + option_spec = {'module': directives.unchanged} + + def _import(self, module): + for obj in vars(module).values(): + if inspect.isclass(obj): + if isinstance(obj, SchemaMeta) and hasattr(obj, '_base_class'): + yield obj + + def run(self): + env = self.state.document.settings.env + module = importlib.import_module(self.options['module']) + things = tuple(self._import(module)) + + sections = [] + sections.append(self.links(things)) # Make index + for thng in things: # type: Thing + # Generate a section for each class, with a title, + # fields description and a paragraph + section = n.section(ids=[self._id(thng)]) + section += n.title(thng.__name__, thng.__name__) + section += self.parse('*Extends {}*'.format(thng._base_class)) + if thng.__doc__: + section += self.parse(thng.__doc__) + fields = n.field_list() + for key, f in thng._own: + name = n.field_name(text=f.data_key or key) + body = [ + self.parse('{} {}'.format(self.type(f), f.metadata.get('description', ''))) + ] + if isinstance(f, EnumField): + body.append(self._parse_enum_field(f)) + attrs = n.field_list() + if f.dump_only: + attrs += self.field('Submit', 'No.') + if f.required: + attrs += self.field('Required', f.required) + fields += n.field('', name, n.field_body('', *body, attrs)) + section += fields + sections.append(section) + return sections + + def _parse_enum_field(self, f): + from ereuse_devicehub.resources.device import states + if issubclass(f.enum, (Subdivision, Currency, Country, Layouts, states.State)): + return self.parse(f.enum.__doc__) + else: + enum_fields = n.field_list() + for el in f.enum: + enum_fields += self.field(el.name, el.value) + return enum_fields + + def field(self, name: str, body: Union[str, bool]): + """Generates a field node with a name and a paragraph body.""" + if isinstance(body, bool): + body = 'Yes.' if body else 'No.' + body = str(body) if body else '' + return n.field('', n.field_name(text=name), n.field_body('', self.parse(body))) + + def type(self, field: Field): + """Parses the type field.""" + if isinstance(field, NestedOn): + t = '' + if field.many: + t = 'List of ' + t = t + str(field.schema.t) + elif isinstance(field, EnumField): + t = field.enum.__name__ + elif isinstance(field, DateTime): + t = 'Date time (ISO 8601 with timezone)' + else: + t = field.__class__.__name__ + if 'str' in t.lower(): + t = 'Text' + if 'unit' in field.metadata: + t = t + ' ({})'.format(field.metadata['unit']) + return t + '.' + + def links(self, things, parent='Schema'): + """Generates an index of things with inheritance awareness.""" + l = n.bullet_list('') + for child in (c for c in things if c._base_class == parent): + ref = n.reference(text=child.__name__) + ref['refuri'] = '#{}'.format(self._id(child)) + p = n.paragraph() + p += ref + l += n.list_item('', p) + sub_list = self.links(things, parent=child.__name__) + if sub_list: + l += sub_list + return l + + def _id(self, thing): + """Generate an id to use as html anchors.""" + return n.make_id('dh-{}'.format(thing.__name__)) + + def parse(self, text) -> n.container: + """Parses text possibly containing ReST stuff and adds it in + a node.""" + p = n.container('') + self.state.nested_parse(StringList(string2lines(inspect.cleandoc(text))), 0, p) + return p + # return publish_doctree(text).children + + +def setup(app): + app.add_directive('dhlist', DhlistDirective) + return {'version': '0.1'} diff --git a/docs/devices.rst b/docs/devices.rst index c6ca3ea4..697802a7 100644 --- a/docs/devices.rst +++ b/docs/devices.rst @@ -37,19 +37,14 @@ Result ****** The result is a JSON object with the following fields: -- **devices**: A list of devices. -- **groups**: A list of groups. -- **widgets**: A dictionary of widgets. -- **pagination**: Pagination information: - +- **items**: A list of devices. +- **pagination**: - **page**: The page you requested in the ``page`` param of the query, or ``1``. - **perPage**: How many devices are in every page, fixed to ``30``. - **total**: How many total devices passed the filters. + - **next**: The number of the next page, if any. + - **last**: The number of the last page, if any. -Models -****** - -.. automodule:: ereuse_devicehub.resources.device.models - :members: - :member-order: bysource +.. dhlist:: + :module: ereuse_devicehub.resources.device.schemas diff --git a/docs/index.rst b/docs/index.rst index 58f9be7d..0269992a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,10 +31,9 @@ Devicehub is built with `Teal `_ and .. toctree:: :maxdepth: 2 - processes - actions - agents + api devices + actions tags lots diff --git a/ereuse_devicehub/dummy/dummy.py b/ereuse_devicehub/dummy/dummy.py index 7b24cd08..511d11a1 100644 --- a/ereuse_devicehub/dummy/dummy.py +++ b/ereuse_devicehub/dummy/dummy.py @@ -27,11 +27,11 @@ class Dummy: ) """Tags to create.""" ET = ( - ('A0000000000001', 'DT-AAAAA'), - ('A0000000000002', 'DT-BBBBB'), - ('A0000000000003', 'DT-CCCCC'), - ('04970DA2A15984', 'DT-BRRAB'), - ('04e4bc5af95980', 'DT-XXXXX') + ('DT-AAAAA', 'A0000000000001'), + ('DT-BBBBB', 'A0000000000002'), + ('DT-CCCCC', 'A0000000000003'), + ('DT-BRRAB', '04970DA2A15984'), + ('DT-XXXXX', '04e4bc5af95980') ) """eTags to create.""" ORG = 'eReuse.org CAT', '-t', 'G-60437761', '-c', 'ES' diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 1c6e219a..20639680 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -29,44 +29,75 @@ from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing class Device(Thing): - """ - Base class for any type of physical object that can be identified. + """Base class for any type of physical object that can be identified. + + Device partly extends `Schema's IndividualProduct `_, adapting it to our + use case. + + A device requires an identification method, ideally a serial number, + although it can be identified only with tags too. More ideally + both methods are used. + + Devices can contain ``Components``, which are just a type of device + (it is a recursive relationship). """ EVENT_SORT_KEY = attrgetter('created') id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id.comment = """ - The identifier of the device for this database. + The identifier of the device for this database. Used only + internally for software; users should not use this. """ type = Column(Unicode(STR_SM_SIZE), nullable=False, index=True) hid = Column(Unicode(), check_lower('hid'), unique=True) hid.comment = """ The Hardware ID (HID) is the unique ID traceability systems - use to ID a device globally. + use to ID a device globally. This field is auto-generated + from Devicehub using literal identifiers from the device, + so it can re-generated *offline*. + + The HID is the result of joining the type of device, S/N, + manufacturer name, and model. Devices that do not have one + of these fields cannot generate HID, thus not guaranteeing + global uniqueness. """ model = Column(Unicode(), check_lower('model')) + model.comment = """The model or brand of the device in lower case. + + Devices usually report one of both (model or brand). This value + must be consistent through time. + """ manufacturer = Column(Unicode(), check_lower('manufacturer')) + manufacturer.comment = """The normalized name of the manufacturer + in lower case. + + Although as of now Devicehub does not enforce normalization, + users can choose a list of normalized manufacturer names + from the own ``/manufacturers`` REST endpoint. + """ serial_number = Column(Unicode(), check_lower('serial_number')) + serial_number.comment = """The serial number of the device in lower case.""" weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5)) weight.comment = """ - The weight of the device in Kgm. + The weight of the device. """ width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5)) width.comment = """ - The width of the device in meters. + The width of the device. """ height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5)) height.comment = """ - The height of the device in meters. + The height of the device. """ depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5)) depth.comment = """ - The depth of the device in meters. + The depth of the device. """ color = Column(ColorType) color.comment = """The predominant color of the device.""" production_date = Column(db.TIMESTAMP(timezone=True)) - production_date.comment = """The date of production of the item.""" + production_date.comment = """The date of production of the device.""" _NON_PHYSICAL_PROPS = { 'id', @@ -91,11 +122,13 @@ class Device(Thing): @property def events(self) -> list: """ - 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. + All the events where the device participated, including: - Events are returned by ascending creation time. + 1. Events performed directly to the device. + 2. Events performed to a component. + 3. Events performed to a parent device. + + Events are returned by ascending ``created`` time. """ return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY) @@ -198,7 +231,7 @@ class Device(Thing): device is working if the list is empty. This property returns, for the last test performed of each type, - the one with the worst severity of them, or `None` if no + the one with the worst ``severity`` of them, or ``None`` if no test has been executed. """ from ereuse_devicehub.resources.event.models import Test @@ -292,8 +325,18 @@ class DisplayMixin: class Computer(Device): + """A chassis with components inside that can be processed + automatically with Workbench Computer. + + Computer is broadly extended by ``Desktop``, ``Laptop``, and + ``Server``. The property ``chassis`` defines it more granularly. + """ id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) chassis = Column(DBEnum(ComputerChassis), nullable=False) + chassis.comment = """The physical form of the computer. + + It is a subset of the Linux definition of DMI / DMI decode. + """ def __init__(self, chassis, **kwargs) -> None: chassis = ComputerChassis(chassis) @@ -342,7 +385,7 @@ class Computer(Device): @property def privacy(self): - """Returns the privacy of all DataStorage components when + """Returns the privacy of all ``DataStorage`` components when it is not None. """ return set( @@ -395,6 +438,8 @@ class Projector(Monitor): class Mobile(Device): + """A mobile device consisting of smartphones, tablets, and cellphones.""" + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) imei = Column(BigInteger) imei.comment = """ @@ -432,6 +477,7 @@ class Cellphone(Mobile): class Component(Device): + """A device that can be inside another device.""" id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True) @@ -481,6 +527,7 @@ class GraphicCard(JoinedComponentTableMixin, Component): class DataStorage(JoinedComponentTableMixin, Component): + """A device that stores information.""" size = Column(Integer, check_range('size', min=1, max=10 ** 8)) size.comment = """ The size of the data-storage in MB. @@ -548,14 +595,21 @@ class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component): class Processor(JoinedComponentTableMixin, Component): + """The CPU.""" speed = Column(Float, check_range('speed', 0.1, 15)) + speed.comment = """The regular CPU speed.""" cores = Column(SmallInteger, check_range('cores', 1, 10)) + cores.comment = """The number of regular cores.""" threads = Column(SmallInteger, check_range('threads', 1, 20)) + threads.comment = """The number of threads per core.""" address = Column(SmallInteger, check_range('address', 8, 256)) + address.comment = """The address of the CPU: 8, 16, 32, 64, 128 or 256 bits.""" class RamModule(JoinedComponentTableMixin, Component): + """A stick of RAM.""" size = Column(SmallInteger, check_range('size', min=128, max=17000)) + size.comment = """The capacity of the RAM stick.""" speed = Column(SmallInteger, check_range('speed', min=100, max=10000)) interface = Column(DBEnum(RamInterface)) format = Column(DBEnum(RamFormat)) @@ -568,14 +622,15 @@ class SoundCard(JoinedComponentTableMixin, Component): class Display(JoinedComponentTableMixin, DisplayMixin, Component): """ The display of a device. This is used in all devices that have - displays but that it is not their main treat, like laptops, - mobiles, smart-watches, and so on; excluding then ComputerMonitor - and Television Set. + displays but that it is not their main part, like laptops, + mobiles, smart-watches, and so on; excluding ``ComputerMonitor`` + and ``TelevisionSet``. """ pass class ComputerAccessory(Device): + """Computer peripherals and similar accessories.""" id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) pass @@ -597,6 +652,7 @@ class MemoryCardReader(ComputerAccessory): class Networking(NetworkMixin, Device): + """Routers, switches, hubs...""" id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) @@ -641,6 +697,7 @@ class Microphone(Sound): class Video(Device): + """Devices related to video treatment.""" pass @@ -653,6 +710,7 @@ class Videoconference(Video): class Cooking(Device): + """Cooking devices.""" pass @@ -661,6 +719,11 @@ class Mixer(Cooking): class Manufacturer(db.Model): + """The normalized information about a manufacturer. + + Ideally users should use the names from this list when submitting + devices. + """ __table_args__ = {'schema': 'common'} CSV_DELIMITER = csv.get_dialect('excel').delimiter @@ -668,8 +731,11 @@ class Manufacturer(db.Model): primary_key=True, # from https://niallburkley.com/blog/index-columns-for-like-in-postgres/ index=db.Index('name', text('name gin_trgm_ops'), postgresql_using='gin')) + name.comment = """The normalized name of the manufacturer.""" url = db.Column(URL(), unique=True) + url.comment = """An URL to a page describing the manufacturer.""" logo = db.Column(URL()) + logo.comment = """An URL pointing to the logo of the manufacturer.""" @classmethod def add_all_to_session(cls, session: db.Session): diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi index 7b280c4a..962e4901 100644 --- a/ereuse_devicehub/resources/device/models.pyi +++ b/ereuse_devicehub/resources/device/models.pyi @@ -287,11 +287,13 @@ class Processor(Component): speed = ... # type: Column cores = ... # type: Column address = ... # type: Column + threads = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.speed = ... # type: float self.cores = ... # type: int + self.threads = ... # type: int self.address = ... # type: int diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index b04a7111..9b3eb5b2 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -15,12 +15,13 @@ from ereuse_devicehub.resources.schemas import Thing, UnitCodes class Device(Thing): + __doc__ = m.Device.__doc__ id = Integer(description=m.Device.id.comment, dump_only=True) hid = SanitizedStr(lower=True, dump_only=True, description=m.Device.hid.comment) tags = NestedOn('Tag', many=True, collection_class=OrderedSet, - description='The set of tags that identify the device.') + description='A set of tags that identify the device.') model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE)) manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE)) serial_number = SanitizedStr(lower=True, data_key='serialNumber') @@ -75,29 +76,54 @@ class Device(Thing): class Computer(Device): - components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet) - chassis = EnumField(enums.ComputerChassis, required=True) - ram_size = Integer(dump_only=True, data_key='ramSize') - data_storage_size = Integer(dump_only=True, data_key='dataStorageSize') - processor_model = Str(dump_only=True, data_key='processorModel') - graphic_card_model = Str(dump_only=True, data_key='graphicCardModel') - network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds') - privacy = NestedOn('Event', many=True, dump_only=True, collection_class=set) + __doc__ = m.Computer.__doc__ + components = NestedOn('Component', + many=True, + dump_only=True, + collection_class=OrderedSet, + description='The components that are inside this computer.') + chassis = EnumField(enums.ComputerChassis, + required=True, + description=m.Computer.chassis.comment) + ram_size = Integer(dump_only=True, + data_key='ramSize', + description=m.Computer.ram_size.__doc__) + data_storage_size = Integer(dump_only=True, + data_key='dataStorageSize', + description=m.Computer.data_storage_size.__doc__) + processor_model = Str(dump_only=True, + data_key='processorModel', + description=m.Computer.processor_model.__doc__) + graphic_card_model = Str(dump_only=True, + data_key='graphicCardModel', + description=m.Computer.graphic_card_model.__doc__) + network_speeds = List(Integer(dump_only=True), + dump_only=True, + data_key='networkSpeeds', + description=m.Computer.network_speeds.__doc__) + privacy = NestedOn('Event', + many=True, + dump_only=True, + collection_class=set, + description=m.Computer.privacy.__doc__) class Desktop(Computer): - pass + __doc__ = m.Desktop.__doc__ class Laptop(Computer): - pass + layout = EnumField(Layouts, description=m.Laptop.layout.comment) + __doc__ = m.Laptop.__doc__ class Server(Computer): - pass + __doc__ = m.Server.__doc__ class DisplayMixin: + __doc__ = m.DisplayMixin.__doc__ + size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150)) technology = EnumField(enums.DisplayTech, description=m.DisplayMixin.technology.comment) @@ -113,6 +139,8 @@ class DisplayMixin: class NetworkMixin: + __doc__ = m.NetworkMixin.__doc__ + speed = Integer(validate=Range(min=10, max=10000), unit=UnitCodes.mbps, description=m.NetworkAdapter.speed.comment) @@ -120,18 +148,20 @@ class NetworkMixin: class Monitor(DisplayMixin, Device): - pass + __doc__ = m.Monitor.__doc__ class ComputerMonitor(Monitor): - pass + __doc__ = m.ComputerMonitor.__doc__ class TelevisionSet(Monitor): - pass + __doc__ = m.TelevisionSet.__doc__ class Mobile(Device): + __doc__ = m.Mobile.__doc__ + imei = Integer(description=m.Mobile.imei.comment) meid = Str(description=m.Mobile.meid.comment) @@ -149,28 +179,34 @@ class Mobile(Device): class Smartphone(Mobile): - pass + __doc__ = m.Smartphone.__doc__ class Tablet(Mobile): - pass + __doc__ = m.Tablet.__doc__ class Cellphone(Mobile): - pass + __doc__ = m.Cellphone.__doc__ class Component(Device): + __doc__ = m.Component.__doc__ + parent = NestedOn(Device, dump_only=True) class GraphicCard(Component): + __doc__ = m.GraphicCard.__doc__ + memory = Integer(validate=Range(0, 10000), unit=UnitCodes.mbyte, description=m.GraphicCard.memory.comment) class DataStorage(Component): + __doc__ = m.DataStorage.__doc__ + size = Integer(validate=Range(0, 10 ** 8), unit=UnitCodes.mbyte, description=m.DataStorage.size.comment) @@ -179,128 +215,147 @@ class DataStorage(Component): class HardDrive(DataStorage): - pass + __doc__ = m.HardDrive.__doc__ class SolidStateDrive(DataStorage): - pass + __doc__ = m.SolidStateDrive.__doc__ class Motherboard(Component): + __doc__ = m.Motherboard.__doc__ + slots = Integer(validate=Range(0, 20), description=m.Motherboard.slots.comment) - usb = Integer(validate=Range(0, 20)) - firewire = Integer(validate=Range(0, 20)) - serial = Integer(validate=Range(0, 20)) - pcmcia = Integer(validate=Range(0, 20)) + usb = Integer(validate=Range(0, 20), description=m.Motherboard.usb.comment) + firewire = Integer(validate=Range(0, 20), description=m.Motherboard.firewire.comment) + serial = Integer(validate=Range(0, 20), description=m.Motherboard.serial.comment) + pcmcia = Integer(validate=Range(0, 20), description=m.Motherboard.pcmcia.comment) class NetworkAdapter(NetworkMixin, Component): - pass + __doc__ = m.NetworkAdapter.__doc__ class Processor(Component): - speed = Float(validate=Range(min=0.1, max=15), unit=UnitCodes.ghz) - cores = Integer(validate=Range(min=1, max=10)) - threads = Integer(validate=Range(min=1, max=20)) - address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256})) + __doc__ = m.Processor.__doc__ + + speed = Float(validate=Range(min=0.1, max=15), + unit=UnitCodes.ghz, + description=m.Processor.speed.comment) + cores = Integer(validate=Range(min=1, max=10), description=m.Processor.cores.comment) + threads = Integer(validate=Range(min=1, max=20), description=m.Processor.threads.comment) + address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}), + description=m.Processor.address.comment) class RamModule(Component): - size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte) + __doc__ = m.RamModule.__doc__ + + size = Integer(validate=Range(min=128, max=17000), + unit=UnitCodes.mbyte, + description=m.RamModule.size.comment) speed = Integer(validate=Range(min=100, max=10000), unit=UnitCodes.mhz) interface = EnumField(enums.RamInterface) format = EnumField(enums.RamFormat) class SoundCard(Component): - pass + __doc__ = m.SoundCard.__doc__ class Display(DisplayMixin, Component): - pass + __doc__ = m.Display.__doc__ class Manufacturer(Schema): + __doc__ = m.Manufacturer.__doc__ + name = String(dump_only=True) url = URL(dump_only=True) logo = URL(dump_only=True) class ComputerAccessory(Device): - pass + __doc__ = m.ComputerAccessory.__doc__ class Mouse(ComputerAccessory): - pass + __doc__ = m.Mouse.__doc__ class MemoryCardReader(ComputerAccessory): - pass + __doc__ = m.MemoryCardReader.__doc__ class SAI(ComputerAccessory): - pass + __doc__ = m.SAI.__doc__ class Keyboard(ComputerAccessory): + __doc__ = m.Keyboard.__doc__ + layout = EnumField(Layouts) class Networking(NetworkMixin, Device): - pass + __doc__ = m.Networking.__doc__ class Router(Networking): - pass + __doc__ = m.Router.__doc__ class Switch(Networking): - pass + __doc__ = m.Switch.__doc__ class Hub(Networking): - pass + __doc__ = m.Hub.__doc__ class WirelessAccessPoint(Networking): - pass + __doc__ = m.WirelessAccessPoint.__doc__ class Printer(Device): - wireless = Boolean(required=True, missing=False) - scanning = Boolean(required=True, missing=False) - technology = EnumField(enums.PrinterTechnology, required=True) - monochrome = Boolean(required=True, missing=True) + __doc__ = m.Printer.__doc__ + + wireless = Boolean(required=True, missing=False, description=m.Printer.wireless.comment) + scanning = Boolean(required=True, missing=False, description=m.Printer.scanning.comment) + technology = EnumField(enums.PrinterTechnology, + required=True, + description=m.Printer.technology.comment) + monochrome = Boolean(required=True, missing=True, description=m.Printer.monochrome.comment) class LabelPrinter(Printer): - pass + __doc__ = m.LabelPrinter.__doc__ class Sound(Device): - pass + __doc__ = m.Sound.__doc__ class Microphone(Sound): - pass + __doc__ = m.Microphone.__doc__ class Video(Device): - pass + __doc__ = m.Video.__doc__ class VideoScaler(Video): - pass + __doc__ = m.VideoScaler.__doc__ class Videoconference(Video): - pass + __doc__ = m.Videoconference.__doc__ class Cooking(Device): - pass + __doc__ = m.Cooking.__doc__ class Mixer(Cooking): - pass + __doc__ = m.Mixer.__doc__ diff --git a/ereuse_devicehub/resources/device/states.py b/ereuse_devicehub/resources/device/states.py index 809e0a64..1cadfb39 100644 --- a/ereuse_devicehub/resources/device/states.py +++ b/ereuse_devicehub/resources/device/states.py @@ -16,6 +16,19 @@ class State(Enum): class Trading(State): + """ + 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. + """ Reserved = e.Reserve Cancelled = e.CancelTrade Sold = e.Sell @@ -27,6 +40,16 @@ class Trading(State): class Physical(State): + """ + 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. + """ ToBeRepaired = e.ToRepair Repaired = e.Repair Preparing = e.ToPrepare diff --git a/ereuse_devicehub/resources/enums.py b/ereuse_devicehub/resources/enums.py index 8788f00b..78c4e6a3 100644 --- a/ereuse_devicehub/resources/enums.py +++ b/ereuse_devicehub/resources/enums.py @@ -278,17 +278,15 @@ class PrinterTechnology(Enum): class Severity(IntEnum): """A flag evaluating the event execution. Ex. failed events - have the value `Severity.Error`. + have the value `Severity.Error`. Devicehub uses 4 severity levels: - Devicehub uses 4 severity levels: - - - Info: default neutral severity. The event succeeded. - - Notice: The event succeeded but it is raising awareness. + * Info: default neutral severity. The event succeeded. + * Notice: The event succeeded but it is raising awareness. Notices are not usually that important but something (good or bad) worth checking. - - Warning: The event succeeded but there is something important + * Warning: The event succeeded but there is something important to check negatively affecting the event. - - Error: the event failed. + * Error: the event failed. Devicehub specially raises user awareness when an event has a Severity of ``Warning`` or greater. diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 6069ca2e..7e180810 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -44,6 +44,10 @@ class JoinedTableMixin: class Event(Thing): + """Event performed on a device. + + This class extends `Schema's Action `_. + """ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) type = Column(Unicode, nullable=False, index=True) name = Column(CIText(), default='', nullable=False) @@ -1179,6 +1183,9 @@ class Trade(JoinedTableMixin, EventWithMultipleDevices): Performing trade events changes the *Trading* state of the device —:class:`ereuse_devicehub.resources.device.states.Trading`. + + This class and its inheritors + extend `Schema's Trade `_. """ shipping_date = Column(DateTime) shipping_date.comment = """ diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 3f100bab..5a1a2363 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -10,18 +10,19 @@ from teal.resource import Schema from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources import enums -from ereuse_devicehub.resources.agent.schemas import Agent -from ereuse_devicehub.resources.device.schemas import Component, Computer, Device +from ereuse_devicehub.resources.agent import schemas as s_agent +from ereuse_devicehub.resources.device import schemas as s_device from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \ PhysicalErasureMethod, PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \ Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength 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 ereuse_devicehub.resources.user import schemas as s_user class Event(Thing): + __doc__ = m.Event.__doc__ id = UUID(dump_only=True) name = SanitizedStr(default='', validate=Length(max=STR_BIG_SIZE), @@ -32,31 +33,34 @@ class Event(Thing): start_time = DateTime(data_key='startTime', description=m.Event.start_time.comment) end_time = DateTime(data_key='endTime', description=m.Event.end_time.comment) snapshot = NestedOn('Snapshot', dump_only=True) - agent = NestedOn(Agent, description=m.Event.agent_id.comment) - author = NestedOn(User, dump_only=True, exclude=('token',)) - components = NestedOn(Component, dump_only=True, many=True) - parent = NestedOn(Computer, dump_only=True, description=m.Event.parent_id.comment) + agent = NestedOn(s_agent.Agent, description=m.Event.agent_id.comment) + author = NestedOn(s_user.User, dump_only=True, exclude=('token',)) + components = NestedOn(s_device.Component, dump_only=True, many=True) + parent = NestedOn(s_device.Computer, dump_only=True, description=m.Event.parent_id.comment) url = URL(dump_only=True, description=m.Event.url.__doc__) class EventWithOneDevice(Event): - device = NestedOn(Device, only_query='id') + __doc__ = m.EventWithOneDevice.__doc__ + device = NestedOn(s_device.Device, only_query='id') class EventWithMultipleDevices(Event): - devices = NestedOn(Device, many=True, only_query='id', collection_class=OrderedSet) + __doc__ = m.EventWithMultipleDevices.__doc__ + devices = NestedOn(s_device.Device, many=True, only_query='id', collection_class=OrderedSet) class Add(EventWithOneDevice): - pass + __doc__ = m.Add.__doc__ class Remove(EventWithOneDevice): - pass + __doc__ = m.Remove.__doc__ class Allocate(EventWithMultipleDevices): - to = NestedOn(User, + __doc__ = m.Allocate.__doc__ + to = NestedOn(s_user.User, description='The user the devices are allocated to.') organization = SanitizedStr(validate=Length(max=STR_SIZE), description='The organization where the ' @@ -64,7 +68,8 @@ class Allocate(EventWithMultipleDevices): class Deallocate(EventWithMultipleDevices): - from_rel = Nested(User, + __doc__ = m.Deallocate.__doc__ + from_rel = Nested(s_user.User, data_key='from', description='The user where the devices are not allocated to anymore.') organization = SanitizedStr(validate=Length(max=STR_SIZE), @@ -73,20 +78,23 @@ class Deallocate(EventWithMultipleDevices): class EraseBasic(EventWithOneDevice): + __doc__ = m.EraseBasic.__doc__ steps = NestedOn('Step', many=True) standards = f.List(EnumField(enums.ErasureStandards), dump_only=True) certificate = URL(dump_only=True) class EraseSectors(EraseBasic): - pass + __doc__ = m.EraseSectors.__doc__ class ErasePhysical(EraseBasic): + __doc__ = m.ErasePhysical.__doc__ method = EnumField(PhysicalErasureMethod, description=PhysicalErasureMethod.__doc__) class Step(Schema): + __doc__ = m.Step.__doc__ type = String(description='Only required when it is nested.') start_time = DateTime(required=True, data_key='startTime') end_time = DateTime(required=True, data_key='endTime') @@ -94,14 +102,15 @@ class Step(Schema): class StepZero(Step): - pass + __doc__ = m.StepZero.__doc__ class StepRandom(Step): - pass + __doc__ = m.StepRandom.__doc__ class Rate(EventWithOneDevice): + __doc__ = m.Rate.__doc__ rating = Integer(validate=Range(*RATE_POSITIVE), dump_only=True, description=m.Rate.rating.comment) @@ -116,10 +125,11 @@ class Rate(EventWithOneDevice): class IndividualRate(Rate): - pass + __doc__ = m.IndividualRate.__doc__ class ManualRate(IndividualRate): + __doc__ = m.ManualRate.__doc__ appearance_range = EnumField(AppearanceRange, required=True, data_key='appearanceRange', @@ -132,6 +142,7 @@ class ManualRate(IndividualRate): class WorkbenchRate(ManualRate): + __doc__ = m.WorkbenchRate.__doc__ processor = Float() ram = Float() data_storage = Float() @@ -147,6 +158,7 @@ class WorkbenchRate(ManualRate): class AggregateRate(Rate): + __doc__ = m.AggregateRate.__doc__ workbench = NestedOn(WorkbenchRate, dump_only=True, description=m.AggregateRate.workbench_id.comment) manual = NestedOn(ManualRate, @@ -176,6 +188,7 @@ class AggregateRate(Rate): class Price(EventWithOneDevice): + __doc__ = m.Price.__doc__ currency = EnumField(Currency, required=True, description=m.Price.currency.comment) price = Decimal(places=m.Price.SCALE, rounding=m.Price.ROUND, @@ -187,6 +200,8 @@ class Price(EventWithOneDevice): class EreusePrice(Price): + __doc__ = m.EreusePrice.__doc__ + class Service(MarshmallowSchema): class Type(MarshmallowSchema): amount = Float() @@ -202,6 +217,7 @@ class EreusePrice(Price): class Install(EventWithOneDevice): + __doc__ = m.Install.__doc__ name = SanitizedStr(validate=Length(min=4, max=STR_BIG_SIZE), required=True, description='The name of the OS installed.') @@ -210,6 +226,7 @@ class Install(EventWithOneDevice): class Snapshot(EventWithOneDevice): + __doc__ = m.Snapshot.__doc__ """ The Snapshot updates the state of the device with information about its components and events performed at them. @@ -229,7 +246,7 @@ class Snapshot(EventWithOneDevice): 'the async Snapshot.') elapsed = TimeDelta(precision=TimeDelta.SECONDS) - components = NestedOn(Component, + components = NestedOn(s_device.Component, many=True, description='A list of components that are inside of the device' 'at the moment of this Snapshot.' @@ -274,10 +291,12 @@ class Snapshot(EventWithOneDevice): class Test(EventWithOneDevice): + __doc__ = m.Test.__doc__ elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) class TestDataStorage(Test): + __doc__ = m.TestDataStorage.__doc__ length = EnumField(TestDataStorageLength, required=True) status = SanitizedStr(lower=True, validate=Length(max=STR_SIZE), required=True) lifetime = TimeDelta(precision=TimeDelta.HOURS) @@ -292,55 +311,59 @@ class TestDataStorage(Test): class StressTest(Test): - pass + __doc__ = m.StressTest.__doc__ class Benchmark(EventWithOneDevice): + __doc__ = m.Benchmark.__doc__ elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) class BenchmarkDataStorage(Benchmark): + __doc__ = m.BenchmarkDataStorage.__doc__ read_speed = Float(required=True, data_key='readSpeed') write_speed = Float(required=True, data_key='writeSpeed') class BenchmarkWithRate(Benchmark): + __doc__ = m.BenchmarkWithRate.__doc__ rate = Float(required=True) class BenchmarkProcessor(BenchmarkWithRate): - pass + __doc__ = m.BenchmarkProcessor.__doc__ class BenchmarkProcessorSysbench(BenchmarkProcessor): - pass + __doc__ = m.BenchmarkProcessorSysbench.__doc__ class BenchmarkRamSysbench(BenchmarkWithRate): - pass + __doc__ = m.BenchmarkRamSysbench.__doc__ class ToRepair(EventWithMultipleDevices): - pass + __doc__ = m.ToRepair.__doc__ class Repair(EventWithMultipleDevices): - pass + __doc__ = m.Repair.__doc__ class ReadyToUse(EventWithMultipleDevices): - pass + __doc__ = m.ReadyToUse.__doc__ class ToPrepare(EventWithMultipleDevices): - pass + __doc__ = m.ToPrepare.__doc__ class Prepare(EventWithMultipleDevices): - pass + __doc__ = m.Prepare.__doc__ class Live(EventWithOneDevice): + __doc__ = m.Live.__doc__ ip = IP(dump_only=True) subdivision_confidence = Integer(dump_only=True, data_key='subdivisionConfidence') subdivision = EnumField(Subdivision, dump_only=True) @@ -353,60 +376,63 @@ class Live(EventWithOneDevice): class Organize(EventWithMultipleDevices): - pass + __doc__ = m.Organize.__doc__ class Reserve(Organize): - pass + __doc__ = m.Reserve.__doc__ class CancelReservation(Organize): - pass + __doc__ = m.CancelReservation.__doc__ class Trade(EventWithMultipleDevices): + __doc__ = m.Trade.__doc__ shipping_date = DateTime(data_key='shippingDate') invoice_number = SanitizedStr(validate=Length(max=STR_SIZE), data_key='invoiceNumber') price = NestedOn(Price) - to = NestedOn(Agent, only_query='id', required=True, comment=m.Trade.to_comment) + to = NestedOn(s_agent.Agent, only_query='id', required=True, comment=m.Trade.to_comment) confirms = NestedOn(Organize) class Sell(Trade): - pass + __doc__ = m.Sell.__doc__ class Donate(Trade): - pass + __doc__ = m.Donate.__doc__ class Rent(Trade): - pass + __doc__ = m.Rent.__doc__ class CancelTrade(Trade): - pass + __doc__ = m.CancelTrade.__doc__ class ToDisposeProduct(Trade): - pass + __doc__ = m.ToDisposeProduct.__doc__ class DisposeProduct(Trade): - pass + __doc__ = m.DisposeProduct.__doc__ class Receive(EventWithMultipleDevices): + __doc__ = m.Receive.__doc__ role = EnumField(ReceiverRole) class Migrate(EventWithMultipleDevices): + __doc__ = m.Migrate.__doc__ other = URL() class MigrateTo(Migrate): - pass + __doc__ = m.MigrateTo.__doc__ class MigrateFrom(Migrate): - pass + __doc__ = m.MigrateFrom.__doc__ diff --git a/ereuse_devicehub/resources/lot/schemas.py b/ereuse_devicehub/resources/lot/schemas.py index 2594f8b3..c6550e86 100644 --- a/ereuse_devicehub/resources/lot/schemas.py +++ b/ereuse_devicehub/resources/lot/schemas.py @@ -2,7 +2,7 @@ from marshmallow import fields as f from teal.marshmallow import SanitizedStr, URL from ereuse_devicehub.marshmallow import NestedOn -from ereuse_devicehub.resources.device.schemas import Device +from ereuse_devicehub.resources.device import schemas as s_device from ereuse_devicehub.resources.lot import models as m from ereuse_devicehub.resources.models import STR_SIZE from ereuse_devicehub.resources.schemas import Thing @@ -13,7 +13,7 @@ class Lot(Thing): name = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), required=True) description = SanitizedStr(description=m.Lot.description.comment) closed = f.Boolean(missing=False, description=m.Lot.closed.comment) - devices = NestedOn(Device, many=True, dump_only=True) + devices = NestedOn(s_device.Device, many=True, dump_only=True) children = NestedOn('Lot', many=True, dump_only=True) parents = NestedOn('Lot', many=True, dump_only=True) url = URL(dump_only=True, description=m.Lot.url.__doc__) diff --git a/ereuse_devicehub/resources/models.py b/ereuse_devicehub/resources/models.py index d0dbd2a1..98f03cbc 100644 --- a/ereuse_devicehub/resources/models.py +++ b/ereuse_devicehub/resources/models.py @@ -9,21 +9,26 @@ STR_XSM_SIZE = 16 class Thing(db.Model): + """The base class of all Devicehub resources. + + This is a loose copy of + `schema.org's Thing class `_ + using only needed fields. + """ __abstract__ = True - # todo make updated to auto-update updated = db.Column(db.TIMESTAMP(timezone=True), nullable=False, index=True, server_default=db.text('CURRENT_TIMESTAMP')) updated.comment = """ - When this was last changed. + The last time Devicehub recorded a change for this thing. """ created = db.Column(db.TIMESTAMP(timezone=True), nullable=False, index=True, server_default=db.text('CURRENT_TIMESTAMP')) created.comment = """ - When Devicehub created this. + When Devicehub created this. """ def __init__(self, **kwargs) -> None: diff --git a/ereuse_devicehub/resources/schemas.py b/ereuse_devicehub/resources/schemas.py index 75652401..0c632578 100644 --- a/ereuse_devicehub/resources/schemas.py +++ b/ereuse_devicehub/resources/schemas.py @@ -1,7 +1,9 @@ from enum import Enum +from typing import Any from marshmallow import post_load from marshmallow.fields import DateTime, List, String +from marshmallow.schema import SchemaMeta from teal.marshmallow import URL from teal.resource import Schema @@ -18,10 +20,59 @@ class UnitCodes(Enum): kgm = 'KGM' m = 'MTR' + def __str__(self): + return self.name + + +# The following SchemaMeta modifications allow us to generate +# documentation using our directive. This is their only purpose. +# Marshmallow's meta class removes variables from our defined +# classes, so we put some home made proxies in order to intercept +# those values and safe them in our classes. +# What we do is: +# 1. Make our ``Meta`` class be the superclass of Marshmallow's +# SchemaMeta and provide a new that stores in class, so we +# can save some vars. +# 2. Substitute SchemaMeta.get_declared_fields with our own method +# that saves more variables. +# Then the directive in our docs/config.py file reads these variables +# generating the documentation. + +class Meta(type): + + def __new__(cls, *args, **kw) -> Any: + base_name = args[1][0].__name__ + y = super().__new__(cls, *args, **kw) + y._base_class = base_name + return y + + +SchemaMeta.__bases__ = Meta, + + +@classmethod +def get_declared_fields(mcs, klass, cls_fields, inherited_fields, dict_cls): + klass._own = cls_fields + klass._inherited = inherited_fields + return dict_cls(inherited_fields + cls_fields) + + +SchemaMeta.get_declared_fields = get_declared_fields + +_type_description = """The name of the type of Thing, +like "Device" or "Receive". This is the same as JSON-LD ``@type``. + +This field is required when submitting values +so Devicehub knows the type of object. Devicehub always returns this +value. +""" + class Thing(Schema): - type = String(description='Only required when it is nested.') - same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs') + type = String(description=_type_description) + same_as = List(URL(dump_only=True), + dump_only=True, + data_key='sameAs') updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment) created = DateTime('iso', dump_only=True, description=m.Thing.created.comment) diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index c4678873..f337c8de 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -1,6 +1,3 @@ -import datetime - -import teal.cache from flask import Response, current_app as app, g, redirect, request from flask_sqlalchemy import Pagination from teal.marshmallow import ValidationError @@ -22,7 +19,6 @@ class TagView(View): res = self._post_one() return res - @teal.cache.cache(datetime.timedelta(minutes=1)) def find(self, args: dict): tags = Tag.query.filter(Tag.is_printable_q()) \ .order_by(Tag.created.desc()) \