Add sphinx extension dhclass; generate better API docs; remove cache for lots

This commit is contained in:
Xavier Bustamante Talavera 2019-02-03 17:12:53 +01:00
parent eabf6aad54
commit 198a89a0b1
17 changed files with 609 additions and 382 deletions

View File

@ -1,6 +1,9 @@
Actions and states Actions and states
################## ##################
Actions
*******
Actions are events performed to devices, changing their **state**. Actions are events performed to devices, changing their **state**.
Actions can have attributes defining Actions can have attributes defining
**where** it happened, **who** performed them, **when**, etc. **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, 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.
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 Devicehub actions inherit from `schema actions
<http://schema.org/Action>`_, are written in Pascal case and using <http://schema.org/Action>`_, are written in Pascal case and using
a verb in infinitive. Some verbs represent the willingness or 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 that the reparation happened. The former actions have the preposition
*To* prefixing the verb. *To* prefixing the verb.
In the following section we define the actions and states. Actions and states affect devices in different ways or **dimensions**.
To see how to perform actions to the Devicehub API head For example, ``Repair`` affects the **physical** dimension of a device,
to the `Swagger docs and ``Sell`` the **political** dimension of a device. A device
<https://app.swaggerhub.com/apis/ereuse/devicehub/0.2>`_. can be in several states at the same time, one per dimension; ie. a
device can be ``repaired`` (physical) and ``reserved`` (political),
.. toctree:: but not ``repaired`` and ``disposed`` at the same time:
:maxdepth: 4
actions
.. uml:: actions.puml
Physical Actions - Physical actions: The following actions describe and react on the
**************** Physical condition of the devices.
The following actions describe and react on the
:class:`ereuse_devicehub.resources.device.states.Physical` condition
of the devices.
ToPrepare and Prepare - ToPrepare and prepare.
================== - ToRepair, Repair
Prepare - ReadyToUse
------- - Live
.. autoclass:: ereuse_devicehub.resources.event.models.Prepare - DisposeWaste, Recover
ToPrepare
---------
.. autoclass:: ereuse_devicehub.resources.event.models.ToPrepare
ToRepair, Repair - Association actions: Actions that change the associations users have with devices;
================ ie. the **owners**, **usufructuarees**, **reservees**,
Repair and **physical possessors**.
------
.. autoclass:: ereuse_devicehub.resources.event.models.Repair
ToRepair
--------
.. autoclass:: ereuse_devicehub.resources.event.models.ToRepair
ReadyToUse - Trade
========== - Transfer
.. autoclass:: ereuse_devicehub.resources.event.models.ReadyToUse - Organize
Live - Internal state actions: Actions providing metadata about devices that don't usually change
==== their state.
.. autoclass:: ereuse_devicehub.resources.event.models.Live
DisposeWaste, Recover - Snapshot
===================== - Add, remove
``RecyclingCenter`` users have two extra special events: - Erase
- ``DisposeWaste``: The device has been disposed in an unspecified - Install
manner. - Test
- ``Recover``: The device has been scrapped and its materials have - Benchmark
been recovered under a new product. - Rate
- Price
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
Add, Remove The following index has all the actions (please note we are moving from calling them
=========== ``Event`` to call them ``Action``):
.. autoclass:: ereuse_devicehub.resources.event.models.Add
.. autoclass:: ereuse_devicehub.resources.event.models.Remove
Erase .. dhlist::
===== :module: ereuse_devicehub.resources.event.schemas
.. 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 !!
States States
@ -266,8 +71,4 @@ States
.. uml:: states.puml .. uml:: states.puml
.. autoclass:: ereuse_devicehub.resources.device.states.Trading .. autoclass:: ereuse_devicehub.resources.device.states.Trading
:members:
:undoc-members:
.. autoclass:: ereuse_devicehub.resources.device.states.Physical .. autoclass:: ereuse_devicehub.resources.device.states.Physical
:members:
:undoc-members:

70
docs/api.rst Normal file
View File

@ -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 <inventory>/devices/
Accept: application/json
Authorization: Basic <token>
And an example is::
GET acme/devices/
Accept: application/json
Authorization: Basic myTokenInBase64
Let's go through the variables:
- ``<inventory>`` 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.
- ``<token>`` is the token of the account.
See :ref:`devices:devices` for more information on how to query
devices.

View File

@ -18,6 +18,19 @@
# -- Project information ----------------------------------------------------- # -- 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' project = 'Devicehub'
copyright = '2018, eReuse.org team' copyright = '2018, eReuse.org team'
@ -176,3 +189,123 @@ html_favicon = 'img/favicon.ico'
# autosectionlabel # autosectionlabel
autosectionlabel_prefix_document = True autosectionlabel_prefix_document = True
autodoc_member_order = 'bysource' 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'}

View File

@ -37,19 +37,14 @@ Result
****** ******
The result is a JSON object with the following fields: The result is a JSON object with the following fields:
- **devices**: A list of devices. - **items**: A list of devices.
- **groups**: A list of groups. - **pagination**:
- **widgets**: A dictionary of widgets.
- **pagination**: Pagination information:
- **page**: The page you requested in the ``page`` param of the query, - **page**: The page you requested in the ``page`` param of the query,
or ``1``. or ``1``.
- **perPage**: How many devices are in every page, fixed to ``30``. - **perPage**: How many devices are in every page, fixed to ``30``.
- **total**: How many total devices passed the filters. - **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 .. dhlist::
****** :module: ereuse_devicehub.resources.device.schemas
.. automodule:: ereuse_devicehub.resources.device.models
:members:
:member-order: bysource

View File

@ -31,10 +31,9 @@ Devicehub is built with `Teal <https://github.com/bustawin/teal>`_ and
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
processes api
actions
agents
devices devices
actions
tags tags
lots lots

View File

@ -27,11 +27,11 @@ class Dummy:
) )
"""Tags to create.""" """Tags to create."""
ET = ( ET = (
('A0000000000001', 'DT-AAAAA'), ('DT-AAAAA', 'A0000000000001'),
('A0000000000002', 'DT-BBBBB'), ('DT-BBBBB', 'A0000000000002'),
('A0000000000003', 'DT-CCCCC'), ('DT-CCCCC', 'A0000000000003'),
('04970DA2A15984', 'DT-BRRAB'), ('DT-BRRAB', '04970DA2A15984'),
('04e4bc5af95980', 'DT-XXXXX') ('DT-XXXXX', '04e4bc5af95980')
) )
"""eTags to create.""" """eTags to create."""
ORG = 'eReuse.org CAT', '-t', 'G-60437761', '-c', 'ES' ORG = 'eReuse.org CAT', '-t', 'G-60437761', '-c', 'ES'

View File

@ -29,44 +29,75 @@ from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
class Device(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 <https
://schema.org/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') EVENT_SORT_KEY = attrgetter('created')
id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
id.comment = """ 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) type = Column(Unicode(STR_SM_SIZE), nullable=False, index=True)
hid = Column(Unicode(), check_lower('hid'), unique=True) hid = Column(Unicode(), check_lower('hid'), unique=True)
hid.comment = """ hid.comment = """
The Hardware ID (HID) is the unique ID traceability systems 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 = 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 = 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 = 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 = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5))
weight.comment = """ 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 = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5))
width.comment = """ 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 = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5))
height.comment = """ 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 = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5))
depth.comment = """ depth.comment = """
The depth of the device in meters. The depth of the device.
""" """
color = Column(ColorType) color = Column(ColorType)
color.comment = """The predominant color of the device.""" color.comment = """The predominant color of the device."""
production_date = Column(db.TIMESTAMP(timezone=True)) 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 = { _NON_PHYSICAL_PROPS = {
'id', 'id',
@ -91,11 +122,13 @@ class Device(Thing):
@property @property
def events(self) -> list: def events(self) -> list:
""" """
All the events where the device participated, including 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. 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) 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. device is working if the list is empty.
This property returns, for the last test performed of each type, 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. test has been executed.
""" """
from ereuse_devicehub.resources.event.models import Test from ereuse_devicehub.resources.event.models import Test
@ -292,8 +325,18 @@ class DisplayMixin:
class Computer(Device): 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) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
chassis = Column(DBEnum(ComputerChassis), nullable=False) 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: def __init__(self, chassis, **kwargs) -> None:
chassis = ComputerChassis(chassis) chassis = ComputerChassis(chassis)
@ -342,7 +385,7 @@ class Computer(Device):
@property @property
def privacy(self): def privacy(self):
"""Returns the privacy of all DataStorage components when """Returns the privacy of all ``DataStorage`` components when
it is not None. it is not None.
""" """
return set( return set(
@ -395,6 +438,8 @@ class Projector(Monitor):
class Mobile(Device): class Mobile(Device):
"""A mobile device consisting of smartphones, tablets, and cellphones."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
imei = Column(BigInteger) imei = Column(BigInteger)
imei.comment = """ imei.comment = """
@ -432,6 +477,7 @@ class Cellphone(Mobile):
class Component(Device): class Component(Device):
"""A device that can be inside another device."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True) parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True)
@ -481,6 +527,7 @@ class GraphicCard(JoinedComponentTableMixin, Component):
class DataStorage(JoinedComponentTableMixin, Component): class DataStorage(JoinedComponentTableMixin, Component):
"""A device that stores information."""
size = Column(Integer, check_range('size', min=1, max=10 ** 8)) size = Column(Integer, check_range('size', min=1, max=10 ** 8))
size.comment = """ size.comment = """
The size of the data-storage in MB. The size of the data-storage in MB.
@ -548,14 +595,21 @@ class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
class Processor(JoinedComponentTableMixin, Component): class Processor(JoinedComponentTableMixin, Component):
"""The CPU."""
speed = Column(Float, check_range('speed', 0.1, 15)) speed = Column(Float, check_range('speed', 0.1, 15))
speed.comment = """The regular CPU speed."""
cores = Column(SmallInteger, check_range('cores', 1, 10)) cores = Column(SmallInteger, check_range('cores', 1, 10))
cores.comment = """The number of regular cores."""
threads = Column(SmallInteger, check_range('threads', 1, 20)) 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 = 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): class RamModule(JoinedComponentTableMixin, Component):
"""A stick of RAM."""
size = Column(SmallInteger, check_range('size', min=128, max=17000)) 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)) speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
interface = Column(DBEnum(RamInterface)) interface = Column(DBEnum(RamInterface))
format = Column(DBEnum(RamFormat)) format = Column(DBEnum(RamFormat))
@ -568,14 +622,15 @@ class SoundCard(JoinedComponentTableMixin, Component):
class Display(JoinedComponentTableMixin, DisplayMixin, Component): class Display(JoinedComponentTableMixin, DisplayMixin, Component):
""" """
The display of a device. This is used in all devices that have The display of a device. This is used in all devices that have
displays but that it is not their main treat, like laptops, displays but that it is not their main part, like laptops,
mobiles, smart-watches, and so on; excluding then ComputerMonitor mobiles, smart-watches, and so on; excluding ``ComputerMonitor``
and Television Set. and ``TelevisionSet``.
""" """
pass pass
class ComputerAccessory(Device): class ComputerAccessory(Device):
"""Computer peripherals and similar accessories."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
pass pass
@ -597,6 +652,7 @@ class MemoryCardReader(ComputerAccessory):
class Networking(NetworkMixin, Device): class Networking(NetworkMixin, Device):
"""Routers, switches, hubs..."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
@ -641,6 +697,7 @@ class Microphone(Sound):
class Video(Device): class Video(Device):
"""Devices related to video treatment."""
pass pass
@ -653,6 +710,7 @@ class Videoconference(Video):
class Cooking(Device): class Cooking(Device):
"""Cooking devices."""
pass pass
@ -661,6 +719,11 @@ class Mixer(Cooking):
class Manufacturer(db.Model): 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'} __table_args__ = {'schema': 'common'}
CSV_DELIMITER = csv.get_dialect('excel').delimiter CSV_DELIMITER = csv.get_dialect('excel').delimiter
@ -668,8 +731,11 @@ class Manufacturer(db.Model):
primary_key=True, primary_key=True,
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/ # from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
index=db.Index('name', text('name gin_trgm_ops'), postgresql_using='gin')) 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 = db.Column(URL(), unique=True)
url.comment = """An URL to a page describing the manufacturer."""
logo = db.Column(URL()) logo = db.Column(URL())
logo.comment = """An URL pointing to the logo of the manufacturer."""
@classmethod @classmethod
def add_all_to_session(cls, session: db.Session): def add_all_to_session(cls, session: db.Session):

View File

@ -287,11 +287,13 @@ class Processor(Component):
speed = ... # type: Column speed = ... # type: Column
cores = ... # type: Column cores = ... # type: Column
address = ... # type: Column address = ... # type: Column
threads = ... # type: Column
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.speed = ... # type: float self.speed = ... # type: float
self.cores = ... # type: int self.cores = ... # type: int
self.threads = ... # type: int
self.address = ... # type: int self.address = ... # type: int

View File

@ -15,12 +15,13 @@ from ereuse_devicehub.resources.schemas import Thing, UnitCodes
class Device(Thing): class Device(Thing):
__doc__ = m.Device.__doc__
id = Integer(description=m.Device.id.comment, dump_only=True) id = Integer(description=m.Device.id.comment, dump_only=True)
hid = SanitizedStr(lower=True, dump_only=True, description=m.Device.hid.comment) hid = SanitizedStr(lower=True, dump_only=True, description=m.Device.hid.comment)
tags = NestedOn('Tag', tags = NestedOn('Tag',
many=True, many=True,
collection_class=OrderedSet, 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)) model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE))
manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE)) manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE))
serial_number = SanitizedStr(lower=True, data_key='serialNumber') serial_number = SanitizedStr(lower=True, data_key='serialNumber')
@ -75,29 +76,54 @@ class Device(Thing):
class Computer(Device): class Computer(Device):
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet) __doc__ = m.Computer.__doc__
chassis = EnumField(enums.ComputerChassis, required=True) components = NestedOn('Component',
ram_size = Integer(dump_only=True, data_key='ramSize') many=True,
data_storage_size = Integer(dump_only=True, data_key='dataStorageSize') dump_only=True,
processor_model = Str(dump_only=True, data_key='processorModel') collection_class=OrderedSet,
graphic_card_model = Str(dump_only=True, data_key='graphicCardModel') description='The components that are inside this computer.')
network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds') chassis = EnumField(enums.ComputerChassis,
privacy = NestedOn('Event', many=True, dump_only=True, collection_class=set) 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): class Desktop(Computer):
pass __doc__ = m.Desktop.__doc__
class Laptop(Computer): class Laptop(Computer):
pass layout = EnumField(Layouts, description=m.Laptop.layout.comment)
__doc__ = m.Laptop.__doc__
class Server(Computer): class Server(Computer):
pass __doc__ = m.Server.__doc__
class DisplayMixin: class DisplayMixin:
__doc__ = m.DisplayMixin.__doc__
size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150)) size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150))
technology = EnumField(enums.DisplayTech, technology = EnumField(enums.DisplayTech,
description=m.DisplayMixin.technology.comment) description=m.DisplayMixin.technology.comment)
@ -113,6 +139,8 @@ class DisplayMixin:
class NetworkMixin: class NetworkMixin:
__doc__ = m.NetworkMixin.__doc__
speed = Integer(validate=Range(min=10, max=10000), speed = Integer(validate=Range(min=10, max=10000),
unit=UnitCodes.mbps, unit=UnitCodes.mbps,
description=m.NetworkAdapter.speed.comment) description=m.NetworkAdapter.speed.comment)
@ -120,18 +148,20 @@ class NetworkMixin:
class Monitor(DisplayMixin, Device): class Monitor(DisplayMixin, Device):
pass __doc__ = m.Monitor.__doc__
class ComputerMonitor(Monitor): class ComputerMonitor(Monitor):
pass __doc__ = m.ComputerMonitor.__doc__
class TelevisionSet(Monitor): class TelevisionSet(Monitor):
pass __doc__ = m.TelevisionSet.__doc__
class Mobile(Device): class Mobile(Device):
__doc__ = m.Mobile.__doc__
imei = Integer(description=m.Mobile.imei.comment) imei = Integer(description=m.Mobile.imei.comment)
meid = Str(description=m.Mobile.meid.comment) meid = Str(description=m.Mobile.meid.comment)
@ -149,28 +179,34 @@ class Mobile(Device):
class Smartphone(Mobile): class Smartphone(Mobile):
pass __doc__ = m.Smartphone.__doc__
class Tablet(Mobile): class Tablet(Mobile):
pass __doc__ = m.Tablet.__doc__
class Cellphone(Mobile): class Cellphone(Mobile):
pass __doc__ = m.Cellphone.__doc__
class Component(Device): class Component(Device):
__doc__ = m.Component.__doc__
parent = NestedOn(Device, dump_only=True) parent = NestedOn(Device, dump_only=True)
class GraphicCard(Component): class GraphicCard(Component):
__doc__ = m.GraphicCard.__doc__
memory = Integer(validate=Range(0, 10000), memory = Integer(validate=Range(0, 10000),
unit=UnitCodes.mbyte, unit=UnitCodes.mbyte,
description=m.GraphicCard.memory.comment) description=m.GraphicCard.memory.comment)
class DataStorage(Component): class DataStorage(Component):
__doc__ = m.DataStorage.__doc__
size = Integer(validate=Range(0, 10 ** 8), size = Integer(validate=Range(0, 10 ** 8),
unit=UnitCodes.mbyte, unit=UnitCodes.mbyte,
description=m.DataStorage.size.comment) description=m.DataStorage.size.comment)
@ -179,128 +215,147 @@ class DataStorage(Component):
class HardDrive(DataStorage): class HardDrive(DataStorage):
pass __doc__ = m.HardDrive.__doc__
class SolidStateDrive(DataStorage): class SolidStateDrive(DataStorage):
pass __doc__ = m.SolidStateDrive.__doc__
class Motherboard(Component): class Motherboard(Component):
__doc__ = m.Motherboard.__doc__
slots = Integer(validate=Range(0, 20), slots = Integer(validate=Range(0, 20),
description=m.Motherboard.slots.comment) description=m.Motherboard.slots.comment)
usb = Integer(validate=Range(0, 20)) usb = Integer(validate=Range(0, 20), description=m.Motherboard.usb.comment)
firewire = Integer(validate=Range(0, 20)) firewire = Integer(validate=Range(0, 20), description=m.Motherboard.firewire.comment)
serial = Integer(validate=Range(0, 20)) serial = Integer(validate=Range(0, 20), description=m.Motherboard.serial.comment)
pcmcia = Integer(validate=Range(0, 20)) pcmcia = Integer(validate=Range(0, 20), description=m.Motherboard.pcmcia.comment)
class NetworkAdapter(NetworkMixin, Component): class NetworkAdapter(NetworkMixin, Component):
pass __doc__ = m.NetworkAdapter.__doc__
class Processor(Component): class Processor(Component):
speed = Float(validate=Range(min=0.1, max=15), unit=UnitCodes.ghz) __doc__ = m.Processor.__doc__
cores = Integer(validate=Range(min=1, max=10))
threads = Integer(validate=Range(min=1, max=20)) speed = Float(validate=Range(min=0.1, max=15),
address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256})) 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): 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) speed = Integer(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)
interface = EnumField(enums.RamInterface) interface = EnumField(enums.RamInterface)
format = EnumField(enums.RamFormat) format = EnumField(enums.RamFormat)
class SoundCard(Component): class SoundCard(Component):
pass __doc__ = m.SoundCard.__doc__
class Display(DisplayMixin, Component): class Display(DisplayMixin, Component):
pass __doc__ = m.Display.__doc__
class Manufacturer(Schema): class Manufacturer(Schema):
__doc__ = m.Manufacturer.__doc__
name = String(dump_only=True) name = String(dump_only=True)
url = URL(dump_only=True) url = URL(dump_only=True)
logo = URL(dump_only=True) logo = URL(dump_only=True)
class ComputerAccessory(Device): class ComputerAccessory(Device):
pass __doc__ = m.ComputerAccessory.__doc__
class Mouse(ComputerAccessory): class Mouse(ComputerAccessory):
pass __doc__ = m.Mouse.__doc__
class MemoryCardReader(ComputerAccessory): class MemoryCardReader(ComputerAccessory):
pass __doc__ = m.MemoryCardReader.__doc__
class SAI(ComputerAccessory): class SAI(ComputerAccessory):
pass __doc__ = m.SAI.__doc__
class Keyboard(ComputerAccessory): class Keyboard(ComputerAccessory):
__doc__ = m.Keyboard.__doc__
layout = EnumField(Layouts) layout = EnumField(Layouts)
class Networking(NetworkMixin, Device): class Networking(NetworkMixin, Device):
pass __doc__ = m.Networking.__doc__
class Router(Networking): class Router(Networking):
pass __doc__ = m.Router.__doc__
class Switch(Networking): class Switch(Networking):
pass __doc__ = m.Switch.__doc__
class Hub(Networking): class Hub(Networking):
pass __doc__ = m.Hub.__doc__
class WirelessAccessPoint(Networking): class WirelessAccessPoint(Networking):
pass __doc__ = m.WirelessAccessPoint.__doc__
class Printer(Device): class Printer(Device):
wireless = Boolean(required=True, missing=False) __doc__ = m.Printer.__doc__
scanning = Boolean(required=True, missing=False)
technology = EnumField(enums.PrinterTechnology, required=True) wireless = Boolean(required=True, missing=False, description=m.Printer.wireless.comment)
monochrome = Boolean(required=True, missing=True) 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): class LabelPrinter(Printer):
pass __doc__ = m.LabelPrinter.__doc__
class Sound(Device): class Sound(Device):
pass __doc__ = m.Sound.__doc__
class Microphone(Sound): class Microphone(Sound):
pass __doc__ = m.Microphone.__doc__
class Video(Device): class Video(Device):
pass __doc__ = m.Video.__doc__
class VideoScaler(Video): class VideoScaler(Video):
pass __doc__ = m.VideoScaler.__doc__
class Videoconference(Video): class Videoconference(Video):
pass __doc__ = m.Videoconference.__doc__
class Cooking(Device): class Cooking(Device):
pass __doc__ = m.Cooking.__doc__
class Mixer(Cooking): class Mixer(Cooking):
pass __doc__ = m.Mixer.__doc__

View File

@ -16,6 +16,19 @@ class State(Enum):
class Trading(State): 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 Reserved = e.Reserve
Cancelled = e.CancelTrade Cancelled = e.CancelTrade
Sold = e.Sell Sold = e.Sell
@ -27,6 +40,16 @@ class Trading(State):
class Physical(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 ToBeRepaired = e.ToRepair
Repaired = e.Repair Repaired = e.Repair
Preparing = e.ToPrepare Preparing = e.ToPrepare

View File

@ -278,17 +278,15 @@ class PrinterTechnology(Enum):
class Severity(IntEnum): class Severity(IntEnum):
"""A flag evaluating the event execution. Ex. failed events """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 Notices are not usually that important but something
(good or bad) worth checking. (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. to check negatively affecting the event.
- Error: the event failed. * Error: the event failed.
Devicehub specially raises user awareness when an event Devicehub specially raises user awareness when an event
has a Severity of ``Warning`` or greater. has a Severity of ``Warning`` or greater.

View File

@ -44,6 +44,10 @@ class JoinedTableMixin:
class Event(Thing): class Event(Thing):
"""Event performed on a device.
This class extends `Schema's Action <https://schema.org/Action>`_.
"""
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
type = Column(Unicode, nullable=False, index=True) type = Column(Unicode, nullable=False, index=True)
name = Column(CIText(), default='', nullable=False) name = Column(CIText(), default='', nullable=False)
@ -1179,6 +1183,9 @@ class Trade(JoinedTableMixin, EventWithMultipleDevices):
Performing trade events changes the *Trading* state of the Performing trade events changes the *Trading* state of the
device :class:`ereuse_devicehub.resources.device.states.Trading`. device :class:`ereuse_devicehub.resources.device.states.Trading`.
This class and its inheritors
extend `Schema's Trade <http://schema.org/TradeAction>`_.
""" """
shipping_date = Column(DateTime) shipping_date = Column(DateTime)
shipping_date.comment = """ shipping_date.comment = """

View File

@ -10,18 +10,19 @@ from teal.resource import Schema
from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources import enums from ereuse_devicehub.resources import enums
from ereuse_devicehub.resources.agent.schemas import Agent from ereuse_devicehub.resources.agent import schemas as s_agent
from ereuse_devicehub.resources.device.schemas import Component, Computer, Device from ereuse_devicehub.resources.device import schemas as s_device
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \ from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
PhysicalErasureMethod, PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \ PhysicalErasureMethod, PriceSoftware, RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, \
Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
from ereuse_devicehub.resources.event import models as m from ereuse_devicehub.resources.event import models as m
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing 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): class Event(Thing):
__doc__ = m.Event.__doc__
id = UUID(dump_only=True) id = UUID(dump_only=True)
name = SanitizedStr(default='', name = SanitizedStr(default='',
validate=Length(max=STR_BIG_SIZE), 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) start_time = DateTime(data_key='startTime', description=m.Event.start_time.comment)
end_time = DateTime(data_key='endTime', description=m.Event.end_time.comment) end_time = DateTime(data_key='endTime', description=m.Event.end_time.comment)
snapshot = NestedOn('Snapshot', dump_only=True) snapshot = NestedOn('Snapshot', dump_only=True)
agent = NestedOn(Agent, description=m.Event.agent_id.comment) agent = NestedOn(s_agent.Agent, description=m.Event.agent_id.comment)
author = NestedOn(User, dump_only=True, exclude=('token',)) author = NestedOn(s_user.User, dump_only=True, exclude=('token',))
components = NestedOn(Component, dump_only=True, many=True) components = NestedOn(s_device.Component, dump_only=True, many=True)
parent = NestedOn(Computer, dump_only=True, description=m.Event.parent_id.comment) parent = NestedOn(s_device.Computer, dump_only=True, description=m.Event.parent_id.comment)
url = URL(dump_only=True, description=m.Event.url.__doc__) url = URL(dump_only=True, description=m.Event.url.__doc__)
class EventWithOneDevice(Event): class EventWithOneDevice(Event):
device = NestedOn(Device, only_query='id') __doc__ = m.EventWithOneDevice.__doc__
device = NestedOn(s_device.Device, only_query='id')
class EventWithMultipleDevices(Event): 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): class Add(EventWithOneDevice):
pass __doc__ = m.Add.__doc__
class Remove(EventWithOneDevice): class Remove(EventWithOneDevice):
pass __doc__ = m.Remove.__doc__
class Allocate(EventWithMultipleDevices): class Allocate(EventWithMultipleDevices):
to = NestedOn(User, __doc__ = m.Allocate.__doc__
to = NestedOn(s_user.User,
description='The user the devices are allocated to.') description='The user the devices are allocated to.')
organization = SanitizedStr(validate=Length(max=STR_SIZE), organization = SanitizedStr(validate=Length(max=STR_SIZE),
description='The organization where the ' description='The organization where the '
@ -64,7 +68,8 @@ class Allocate(EventWithMultipleDevices):
class Deallocate(EventWithMultipleDevices): class Deallocate(EventWithMultipleDevices):
from_rel = Nested(User, __doc__ = m.Deallocate.__doc__
from_rel = Nested(s_user.User,
data_key='from', data_key='from',
description='The user where the devices are not allocated to anymore.') description='The user where the devices are not allocated to anymore.')
organization = SanitizedStr(validate=Length(max=STR_SIZE), organization = SanitizedStr(validate=Length(max=STR_SIZE),
@ -73,20 +78,23 @@ class Deallocate(EventWithMultipleDevices):
class EraseBasic(EventWithOneDevice): class EraseBasic(EventWithOneDevice):
__doc__ = m.EraseBasic.__doc__
steps = NestedOn('Step', many=True) steps = NestedOn('Step', many=True)
standards = f.List(EnumField(enums.ErasureStandards), dump_only=True) standards = f.List(EnumField(enums.ErasureStandards), dump_only=True)
certificate = URL(dump_only=True) certificate = URL(dump_only=True)
class EraseSectors(EraseBasic): class EraseSectors(EraseBasic):
pass __doc__ = m.EraseSectors.__doc__
class ErasePhysical(EraseBasic): class ErasePhysical(EraseBasic):
__doc__ = m.ErasePhysical.__doc__
method = EnumField(PhysicalErasureMethod, description=PhysicalErasureMethod.__doc__) method = EnumField(PhysicalErasureMethod, description=PhysicalErasureMethod.__doc__)
class Step(Schema): class Step(Schema):
__doc__ = m.Step.__doc__
type = String(description='Only required when it is nested.') type = String(description='Only required when it is nested.')
start_time = DateTime(required=True, data_key='startTime') start_time = DateTime(required=True, data_key='startTime')
end_time = DateTime(required=True, data_key='endTime') end_time = DateTime(required=True, data_key='endTime')
@ -94,14 +102,15 @@ class Step(Schema):
class StepZero(Step): class StepZero(Step):
pass __doc__ = m.StepZero.__doc__
class StepRandom(Step): class StepRandom(Step):
pass __doc__ = m.StepRandom.__doc__
class Rate(EventWithOneDevice): class Rate(EventWithOneDevice):
__doc__ = m.Rate.__doc__
rating = Integer(validate=Range(*RATE_POSITIVE), rating = Integer(validate=Range(*RATE_POSITIVE),
dump_only=True, dump_only=True,
description=m.Rate.rating.comment) description=m.Rate.rating.comment)
@ -116,10 +125,11 @@ class Rate(EventWithOneDevice):
class IndividualRate(Rate): class IndividualRate(Rate):
pass __doc__ = m.IndividualRate.__doc__
class ManualRate(IndividualRate): class ManualRate(IndividualRate):
__doc__ = m.ManualRate.__doc__
appearance_range = EnumField(AppearanceRange, appearance_range = EnumField(AppearanceRange,
required=True, required=True,
data_key='appearanceRange', data_key='appearanceRange',
@ -132,6 +142,7 @@ class ManualRate(IndividualRate):
class WorkbenchRate(ManualRate): class WorkbenchRate(ManualRate):
__doc__ = m.WorkbenchRate.__doc__
processor = Float() processor = Float()
ram = Float() ram = Float()
data_storage = Float() data_storage = Float()
@ -147,6 +158,7 @@ class WorkbenchRate(ManualRate):
class AggregateRate(Rate): class AggregateRate(Rate):
__doc__ = m.AggregateRate.__doc__
workbench = NestedOn(WorkbenchRate, dump_only=True, workbench = NestedOn(WorkbenchRate, dump_only=True,
description=m.AggregateRate.workbench_id.comment) description=m.AggregateRate.workbench_id.comment)
manual = NestedOn(ManualRate, manual = NestedOn(ManualRate,
@ -176,6 +188,7 @@ class AggregateRate(Rate):
class Price(EventWithOneDevice): class Price(EventWithOneDevice):
__doc__ = m.Price.__doc__
currency = EnumField(Currency, required=True, description=m.Price.currency.comment) currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
price = Decimal(places=m.Price.SCALE, price = Decimal(places=m.Price.SCALE,
rounding=m.Price.ROUND, rounding=m.Price.ROUND,
@ -187,6 +200,8 @@ class Price(EventWithOneDevice):
class EreusePrice(Price): class EreusePrice(Price):
__doc__ = m.EreusePrice.__doc__
class Service(MarshmallowSchema): class Service(MarshmallowSchema):
class Type(MarshmallowSchema): class Type(MarshmallowSchema):
amount = Float() amount = Float()
@ -202,6 +217,7 @@ class EreusePrice(Price):
class Install(EventWithOneDevice): class Install(EventWithOneDevice):
__doc__ = m.Install.__doc__
name = SanitizedStr(validate=Length(min=4, max=STR_BIG_SIZE), name = SanitizedStr(validate=Length(min=4, max=STR_BIG_SIZE),
required=True, required=True,
description='The name of the OS installed.') description='The name of the OS installed.')
@ -210,6 +226,7 @@ class Install(EventWithOneDevice):
class Snapshot(EventWithOneDevice): class Snapshot(EventWithOneDevice):
__doc__ = m.Snapshot.__doc__
""" """
The Snapshot updates the state of the device with information about The Snapshot updates the state of the device with information about
its components and events performed at them. its components and events performed at them.
@ -229,7 +246,7 @@ class Snapshot(EventWithOneDevice):
'the async Snapshot.') 'the async Snapshot.')
elapsed = TimeDelta(precision=TimeDelta.SECONDS) elapsed = TimeDelta(precision=TimeDelta.SECONDS)
components = NestedOn(Component, components = NestedOn(s_device.Component,
many=True, many=True,
description='A list of components that are inside of the device' description='A list of components that are inside of the device'
'at the moment of this Snapshot.' 'at the moment of this Snapshot.'
@ -274,10 +291,12 @@ class Snapshot(EventWithOneDevice):
class Test(EventWithOneDevice): class Test(EventWithOneDevice):
__doc__ = m.Test.__doc__
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
class TestDataStorage(Test): class TestDataStorage(Test):
__doc__ = m.TestDataStorage.__doc__
length = EnumField(TestDataStorageLength, required=True) length = EnumField(TestDataStorageLength, required=True)
status = SanitizedStr(lower=True, validate=Length(max=STR_SIZE), required=True) status = SanitizedStr(lower=True, validate=Length(max=STR_SIZE), required=True)
lifetime = TimeDelta(precision=TimeDelta.HOURS) lifetime = TimeDelta(precision=TimeDelta.HOURS)
@ -292,55 +311,59 @@ class TestDataStorage(Test):
class StressTest(Test): class StressTest(Test):
pass __doc__ = m.StressTest.__doc__
class Benchmark(EventWithOneDevice): class Benchmark(EventWithOneDevice):
__doc__ = m.Benchmark.__doc__
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
class BenchmarkDataStorage(Benchmark): class BenchmarkDataStorage(Benchmark):
__doc__ = m.BenchmarkDataStorage.__doc__
read_speed = Float(required=True, data_key='readSpeed') read_speed = Float(required=True, data_key='readSpeed')
write_speed = Float(required=True, data_key='writeSpeed') write_speed = Float(required=True, data_key='writeSpeed')
class BenchmarkWithRate(Benchmark): class BenchmarkWithRate(Benchmark):
__doc__ = m.BenchmarkWithRate.__doc__
rate = Float(required=True) rate = Float(required=True)
class BenchmarkProcessor(BenchmarkWithRate): class BenchmarkProcessor(BenchmarkWithRate):
pass __doc__ = m.BenchmarkProcessor.__doc__
class BenchmarkProcessorSysbench(BenchmarkProcessor): class BenchmarkProcessorSysbench(BenchmarkProcessor):
pass __doc__ = m.BenchmarkProcessorSysbench.__doc__
class BenchmarkRamSysbench(BenchmarkWithRate): class BenchmarkRamSysbench(BenchmarkWithRate):
pass __doc__ = m.BenchmarkRamSysbench.__doc__
class ToRepair(EventWithMultipleDevices): class ToRepair(EventWithMultipleDevices):
pass __doc__ = m.ToRepair.__doc__
class Repair(EventWithMultipleDevices): class Repair(EventWithMultipleDevices):
pass __doc__ = m.Repair.__doc__
class ReadyToUse(EventWithMultipleDevices): class ReadyToUse(EventWithMultipleDevices):
pass __doc__ = m.ReadyToUse.__doc__
class ToPrepare(EventWithMultipleDevices): class ToPrepare(EventWithMultipleDevices):
pass __doc__ = m.ToPrepare.__doc__
class Prepare(EventWithMultipleDevices): class Prepare(EventWithMultipleDevices):
pass __doc__ = m.Prepare.__doc__
class Live(EventWithOneDevice): class Live(EventWithOneDevice):
__doc__ = m.Live.__doc__
ip = IP(dump_only=True) ip = IP(dump_only=True)
subdivision_confidence = Integer(dump_only=True, data_key='subdivisionConfidence') subdivision_confidence = Integer(dump_only=True, data_key='subdivisionConfidence')
subdivision = EnumField(Subdivision, dump_only=True) subdivision = EnumField(Subdivision, dump_only=True)
@ -353,60 +376,63 @@ class Live(EventWithOneDevice):
class Organize(EventWithMultipleDevices): class Organize(EventWithMultipleDevices):
pass __doc__ = m.Organize.__doc__
class Reserve(Organize): class Reserve(Organize):
pass __doc__ = m.Reserve.__doc__
class CancelReservation(Organize): class CancelReservation(Organize):
pass __doc__ = m.CancelReservation.__doc__
class Trade(EventWithMultipleDevices): class Trade(EventWithMultipleDevices):
__doc__ = m.Trade.__doc__
shipping_date = DateTime(data_key='shippingDate') shipping_date = DateTime(data_key='shippingDate')
invoice_number = SanitizedStr(validate=Length(max=STR_SIZE), data_key='invoiceNumber') invoice_number = SanitizedStr(validate=Length(max=STR_SIZE), data_key='invoiceNumber')
price = NestedOn(Price) 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) confirms = NestedOn(Organize)
class Sell(Trade): class Sell(Trade):
pass __doc__ = m.Sell.__doc__
class Donate(Trade): class Donate(Trade):
pass __doc__ = m.Donate.__doc__
class Rent(Trade): class Rent(Trade):
pass __doc__ = m.Rent.__doc__
class CancelTrade(Trade): class CancelTrade(Trade):
pass __doc__ = m.CancelTrade.__doc__
class ToDisposeProduct(Trade): class ToDisposeProduct(Trade):
pass __doc__ = m.ToDisposeProduct.__doc__
class DisposeProduct(Trade): class DisposeProduct(Trade):
pass __doc__ = m.DisposeProduct.__doc__
class Receive(EventWithMultipleDevices): class Receive(EventWithMultipleDevices):
__doc__ = m.Receive.__doc__
role = EnumField(ReceiverRole) role = EnumField(ReceiverRole)
class Migrate(EventWithMultipleDevices): class Migrate(EventWithMultipleDevices):
__doc__ = m.Migrate.__doc__
other = URL() other = URL()
class MigrateTo(Migrate): class MigrateTo(Migrate):
pass __doc__ = m.MigrateTo.__doc__
class MigrateFrom(Migrate): class MigrateFrom(Migrate):
pass __doc__ = m.MigrateFrom.__doc__

View File

@ -2,7 +2,7 @@ from marshmallow import fields as f
from teal.marshmallow import SanitizedStr, URL from teal.marshmallow import SanitizedStr, URL
from ereuse_devicehub.marshmallow import NestedOn 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.lot import models as m
from ereuse_devicehub.resources.models import STR_SIZE from ereuse_devicehub.resources.models import STR_SIZE
from ereuse_devicehub.resources.schemas import Thing 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) name = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), required=True)
description = SanitizedStr(description=m.Lot.description.comment) description = SanitizedStr(description=m.Lot.description.comment)
closed = f.Boolean(missing=False, description=m.Lot.closed.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) children = NestedOn('Lot', many=True, dump_only=True)
parents = 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__) url = URL(dump_only=True, description=m.Lot.url.__doc__)

View File

@ -9,21 +9,26 @@ STR_XSM_SIZE = 16
class Thing(db.Model): class Thing(db.Model):
"""The base class of all Devicehub resources.
This is a loose copy of
`schema.org's Thing class <https://schema.org/Thing>`_
using only needed fields.
"""
__abstract__ = True __abstract__ = True
# todo make updated to auto-update
updated = db.Column(db.TIMESTAMP(timezone=True), updated = db.Column(db.TIMESTAMP(timezone=True),
nullable=False, nullable=False,
index=True, index=True,
server_default=db.text('CURRENT_TIMESTAMP')) server_default=db.text('CURRENT_TIMESTAMP'))
updated.comment = """ updated.comment = """
When this was last changed. The last time Devicehub recorded a change for this thing.
""" """
created = db.Column(db.TIMESTAMP(timezone=True), created = db.Column(db.TIMESTAMP(timezone=True),
nullable=False, nullable=False,
index=True, index=True,
server_default=db.text('CURRENT_TIMESTAMP')) server_default=db.text('CURRENT_TIMESTAMP'))
created.comment = """ created.comment = """
When Devicehub created this. When Devicehub created this.
""" """
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:

View File

@ -1,7 +1,9 @@
from enum import Enum from enum import Enum
from typing import Any
from marshmallow import post_load from marshmallow import post_load
from marshmallow.fields import DateTime, List, String from marshmallow.fields import DateTime, List, String
from marshmallow.schema import SchemaMeta
from teal.marshmallow import URL from teal.marshmallow import URL
from teal.resource import Schema from teal.resource import Schema
@ -18,10 +20,59 @@ class UnitCodes(Enum):
kgm = 'KGM' kgm = 'KGM'
m = 'MTR' 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): class Thing(Schema):
type = String(description='Only required when it is nested.') type = String(description=_type_description)
same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs') same_as = List(URL(dump_only=True),
dump_only=True,
data_key='sameAs')
updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment) updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment)
created = DateTime('iso', dump_only=True, description=m.Thing.created.comment) created = DateTime('iso', dump_only=True, description=m.Thing.created.comment)

View File

@ -1,6 +1,3 @@
import datetime
import teal.cache
from flask import Response, current_app as app, g, redirect, request from flask import Response, current_app as app, g, redirect, request
from flask_sqlalchemy import Pagination from flask_sqlalchemy import Pagination
from teal.marshmallow import ValidationError from teal.marshmallow import ValidationError
@ -22,7 +19,6 @@ class TagView(View):
res = self._post_one() res = self._post_one()
return res return res
@teal.cache.cache(datetime.timedelta(minutes=1))
def find(self, args: dict): def find(self, args: dict):
tags = Tag.query.filter(Tag.is_printable_q()) \ tags = Tag.query.filter(Tag.is_printable_q()) \
.order_by(Tag.created.desc()) \ .order_by(Tag.created.desc()) \