This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
devicehub-teal/ereuse_devicehub/resources/device/models.py

416 lines
14 KiB
Python
Raw Normal View History

2018-09-30 17:40:28 +00:00
import json
import pathlib
2018-04-27 17:16:43 +00:00
from contextlib import suppress
from itertools import chain
from operator import attrgetter
2018-10-03 12:51:22 +00:00
from typing import Dict, List, Set
2018-04-27 17:16:43 +00:00
2018-09-30 17:40:28 +00:00
from boltons import urlutils
from citext import CIText
2018-09-07 10:38:02 +00:00
from ereuse_utils.naming import Naming
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
Sequence, SmallInteger, Unicode, inspect
2018-04-10 15:06:39 +00:00
from sqlalchemy.ext.declarative import declared_attr
2018-06-26 13:36:21 +00:00
from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
from sqlalchemy.util import OrderedSet
2018-06-10 16:47:49 +00:00
from sqlalchemy_utils import ColorType
2018-06-26 13:36:21 +00:00
from stdnum import imei, meid
2018-09-30 17:40:28 +00:00
from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, check_lower, \
check_range
2018-09-07 10:38:02 +00:00
from teal.marshmallow import ValidationError
2018-04-10 15:06:39 +00:00
2018-09-30 17:40:28 +00:00
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
2018-07-02 10:52:54 +00:00
2018-04-10 15:06:39 +00:00
class Device(Thing):
"""
Base class for any type of physical object that can be identified.
"""
2018-06-10 16:47:49 +00:00
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
2018-06-20 21:18:15 +00:00
id.comment = """
The identifier of the device for this database.
"""
2018-04-27 17:16:43 +00:00
type = Column(Unicode(STR_SM_SIZE), nullable=False)
hid = Column(Unicode(), check_lower('hid'), unique=True)
2018-06-20 21:18:15 +00:00
hid.comment = """
The Hardware ID (HID) is the unique ID traceability systems
use to ID a device globally.
"""
model = Column(Unicode(), check_lower('model'))
manufacturer = Column(Unicode(), check_lower('manufacturer'))
serial_number = Column(Unicode(), check_lower('serial_number'))
2018-06-10 16:47:49 +00:00
weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 3))
2018-06-20 21:18:15 +00:00
weight.comment = """
The weight of the device in Kgm.
"""
2018-06-10 16:47:49 +00:00
width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 3))
2018-06-20 21:18:15 +00:00
width.comment = """
The width of the device in meters.
"""
2018-06-10 16:47:49 +00:00
height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 3))
2018-06-20 21:18:15 +00:00
height.comment = """
The height of the device in meters.
"""
2018-06-10 16:47:49 +00:00
depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 3))
color = Column(ColorType)
color.comment = """
"""
2018-04-27 17:16:43 +00:00
@property
def events(self) -> list:
2018-06-10 16:47:49 +00:00
"""
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.
2018-06-10 16:47:49 +00:00
"""
return sorted(chain(self.events_multiple, self.events_one), key=attrgetter('created'))
2018-06-15 13:31:03 +00:00
def __init__(self, **kw) -> None:
super().__init__(**kw)
2018-04-30 17:58:19 +00:00
with suppress(TypeError):
2018-06-15 13:31:03 +00:00
self.hid = Naming.hid(self.manufacturer, self.serial_number, self.model)
2018-04-30 17:58:19 +00:00
2018-04-27 17:16:43 +00:00
@property
2018-04-30 17:58:19 +00:00
def physical_properties(self) -> Dict[str, object or None]:
2018-04-27 17:16:43 +00:00
"""
Fields that describe the physical properties of a device.
:return A generator where each value is a tuple with tho fields:
- Column.
- Actual value of the column or None.
"""
# todo ensure to remove materialized values when start using them
# todo or self.__table__.columns if inspect fails
2018-04-30 17:58:19 +00:00
return {c.key: getattr(self, c.key, None)
2018-04-27 17:16:43 +00:00
for c in inspect(self.__class__).attrs
2018-04-30 17:58:19 +00:00
if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None)
and c.key not in {'id', 'type', 'created', 'updated', 'parent_id', 'hid'}}
2018-04-10 15:06:39 +00:00
@declared_attr
def __mapper_args__(cls):
"""
Defines inheritance.
From `the guide <http://docs.sqlalchemy.org/en/latest/orm/
extensions/declarative/api.html
#sqlalchemy.ext.declarative.declared_attr>`_
"""
args = {POLYMORPHIC_ID: cls.t}
if cls.t == 'Device':
2018-04-10 15:06:39 +00:00
args[POLYMORPHIC_ON] = cls.type
return args
2018-04-30 17:58:19 +00:00
def __lt__(self, other):
return self.id < other.id
2018-04-27 17:16:43 +00:00
2018-10-03 12:51:22 +00:00
def __str__(self) -> str:
return '{0.t} {0.id}: model {0.model}, S/N {0.serial_number}'.format(self)
def __format__(self, format_spec):
if not format_spec:
return super().__format__(format_spec)
v = ''
if 't' in format_spec:
v += '{0.t} {0.model}'.format(self)
if 's' in format_spec:
v += '({0.manufacturer}) S/N {0.serial_number}'.format(self)
return v
2018-04-10 15:06:39 +00:00
2018-06-26 13:36:21 +00:00
class DisplayMixin:
size = Column(Float(decimal_return_scale=2), check_range('size', 2, 150))
size.comment = """
The size of the monitor in inches.
"""
technology = Column(DBEnum(DisplayTech))
technology.comment = """
The technology the monitor uses to display the image.
"""
resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000))
resolution_width.comment = """
The maximum horizontal resolution the monitor can natively support
in pixels.
"""
resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000))
resolution_height.comment = """
The maximum vertical resolution the monitor can natively support
in pixels.
"""
2018-10-03 12:51:22 +00:00
def __format__(self, format_spec: str) -> str:
v = ''
if 't' in format_spec:
v += '{0.t} {0.model}'.format(self)
if 's' in format_spec:
v += '({0.manufacturer}) S/N {0.serial_number} {0.size}in {0.technology}'
return v
2018-06-26 13:36:21 +00:00
2018-04-10 15:06:39 +00:00
class Computer(Device):
2018-06-10 16:47:49 +00:00
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
2018-06-26 13:36:21 +00:00
chassis = Column(DBEnum(ComputerChassis), nullable=False)
2018-04-10 15:06:39 +00:00
@property
def events(self) -> list:
return sorted(chain(super().events, self.events_parent), key=attrgetter('created'))
2018-10-03 12:51:22 +00:00
@property
def ram_size(self) -> int:
"""The total of RAM memory the computer has."""
return sum(ram.size for ram in self.components if isinstance(ram, RamModule))
@property
def data_storage_size(self) -> int:
"""The total of data storage the computer has."""
return sum(ds.size for ds in self.components if isinstance(ds, DataStorage))
@property
def processor_model(self) -> str:
"""The model of one of the processors of the computer."""
return next(p.model for p in self.components if isinstance(p, Processor))
@property
def graphic_card_model(self) -> str:
"""The model of one of the graphic cards of the computer."""
return next(p.model for p in self.components if isinstance(p, GraphicCard))
@property
def network_speeds(self) -> List[int]:
"""Returns two speeds: the first for the eth and the
second for the wifi networks, or 0 respectively if not found.
"""
speeds = [0, 0]
for net in (c for c in self.components if isinstance(c, NetworkAdapter)):
speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless])
return speeds
def __format__(self, format_spec):
if not format_spec:
return super().__format__(format_spec)
v = ''
if 't' in format_spec:
v += '{0.chassis} {0.model}'.format(self)
elif 's' in format_spec:
v += '({0.manufacturer}) S/N {0.serial_number}'.format(self)
return v
2018-04-10 15:06:39 +00:00
class Desktop(Computer):
pass
class Laptop(Computer):
pass
2018-06-26 13:36:21 +00:00
class Server(Computer):
2018-04-10 15:06:39 +00:00
pass
2018-06-26 13:36:21 +00:00
class Monitor(DisplayMixin, Device):
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
class ComputerMonitor(Monitor):
2018-04-10 15:06:39 +00:00
pass
2018-06-26 13:36:21 +00:00
class TelevisionSet(Monitor):
2018-04-10 15:06:39 +00:00
pass
2018-06-26 13:36:21 +00:00
class Mobile(Device):
2018-06-20 21:18:15 +00:00
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
2018-06-26 13:36:21 +00:00
imei = Column(BigInteger)
imei.comment = """
The International Mobile Equipment Identity of the smartphone
as an integer.
2018-06-20 21:18:15 +00:00
"""
2018-06-26 13:36:21 +00:00
meid = Column(Unicode)
meid.comment = """
The Mobile Equipment Identifier as a hexadecimal string.
2018-06-20 21:18:15 +00:00
"""
2018-06-26 13:36:21 +00:00
@validates('imei')
def validate_imei(self, _, value: int):
if not imei.is_valid(str(value)):
2018-06-26 13:36:21 +00:00
raise ValidationError('{} is not a valid imei.'.format(value))
@validates('meid')
def validate_meid(self, _, value: str):
if not meid.is_valid(value):
raise ValidationError('{} is not a valid meid.'.format(value))
class Smartphone(Mobile):
pass
class Tablet(Mobile):
pass
class Cellphone(Mobile):
pass
2018-06-20 21:18:15 +00:00
2018-04-10 15:06:39 +00:00
class Component(Device):
2018-06-10 16:47:49 +00:00
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
2018-04-10 15:06:39 +00:00
parent_id = Column(BigInteger, ForeignKey(Computer.id))
2018-04-10 15:06:39 +00:00
parent = relationship(Computer,
backref=backref('components',
lazy=True,
cascade=CASCADE,
order_by=lambda: Component.id,
collection_class=OrderedSet),
2018-06-15 13:31:03 +00:00
primaryjoin=parent_id == Computer.id)
2018-04-27 17:16:43 +00:00
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
"""
Gets a component that:
- has the same parent.
- Doesn't generate HID.
- Has same physical properties.
:param parent:
:param blacklist: A set of components to not to consider
when looking for similar ones.
"""
assert self.hid is None, 'Don\'t use this method with a component that has HID'
component = self.__class__.query \
.filter_by(parent=parent, hid=None, **self.physical_properties) \
.filter(~Component.id.in_(blacklist)) \
.first()
if not component:
raise ResourceNotFound(self.type)
return component
2018-04-10 15:06:39 +00:00
@property
def events(self) -> list:
2018-06-15 13:31:03 +00:00
return sorted(chain(super().events, self.events_components), key=attrgetter('created'))
class JoinedComponentTableMixin:
@declared_attr
def id(cls):
return Column(BigInteger, ForeignKey(Component.id), primary_key=True)
2018-04-10 15:06:39 +00:00
class GraphicCard(JoinedComponentTableMixin, Component):
2018-06-10 16:47:49 +00:00
memory = Column(SmallInteger, check_range('memory', min=1, max=10000))
2018-06-26 13:36:21 +00:00
memory.comment = """
The amount of memory of the Graphic Card in MB.
"""
2018-06-10 16:47:49 +00:00
2018-04-10 15:06:39 +00:00
2018-06-10 16:47:49 +00:00
class DataStorage(JoinedComponentTableMixin, Component):
size = Column(Integer, check_range('size', min=1, max=10 ** 8))
2018-06-26 13:36:21 +00:00
size.comment = """
The size of the data-storage in MB.
"""
2018-06-12 14:50:05 +00:00
interface = Column(DBEnum(DataStorageInterface))
2018-04-10 15:06:39 +00:00
2018-10-03 12:51:22 +00:00
def __format__(self, format_spec):
v = super().__format__(format_spec)
if 's' in format_spec:
v += ' {} GB'.format(self.size // 1000)
return v
2018-06-10 16:47:49 +00:00
class HardDrive(DataStorage):
pass
class SolidStateDrive(DataStorage):
pass
2018-04-10 15:06:39 +00:00
class Motherboard(JoinedComponentTableMixin, Component):
2018-07-02 10:52:54 +00:00
slots = Column(SmallInteger, check_range('slots', min=0))
2018-06-26 13:36:21 +00:00
slots.comment = """
PCI slots the motherboard has.
"""
2018-07-02 10:52:54 +00:00
usb = Column(SmallInteger, check_range('usb', min=0))
firewire = Column(SmallInteger, check_range('firewire', min=0))
serial = Column(SmallInteger, check_range('serial', min=0))
pcmcia = Column(SmallInteger, check_range('pcmcia', min=0))
2018-04-10 15:06:39 +00:00
2018-06-26 13:35:13 +00:00
class NetworkMixin:
2018-06-10 16:47:49 +00:00
speed = Column(SmallInteger, check_range('speed', min=10, max=10000))
2018-06-26 13:35:13 +00:00
speed.comment = """
The maximum speed this network adapter can handle, in mbps.
"""
wireless = Column(Boolean)
wireless.comment = """
Whether it is a wireless interface.
"""
2018-06-26 13:35:13 +00:00
2018-10-03 12:51:22 +00:00
def __format__(self, format_spec):
v = super().__format__(format_spec)
if 's' in format_spec:
v += ' {} Mbps'.format(self.speed)
return v
2018-06-26 13:35:13 +00:00
class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
pass
2018-04-27 17:16:43 +00:00
class Processor(JoinedComponentTableMixin, Component):
speed = Column(Float, check_range('speed', 0.1, 15))
cores = Column(SmallInteger, check_range('cores', 1, 10))
threads = Column(SmallInteger, check_range('threads', 1, 20))
address = Column(SmallInteger, check_range('address', 8, 256))
class RamModule(JoinedComponentTableMixin, Component):
2018-04-27 17:16:43 +00:00
size = Column(SmallInteger, check_range('size', min=128, max=17000))
speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
2018-06-12 14:50:05 +00:00
interface = Column(DBEnum(RamInterface))
format = Column(DBEnum(RamFormat))
2018-06-26 13:36:21 +00:00
2018-07-02 10:52:54 +00:00
class SoundCard(JoinedComponentTableMixin, Component):
pass
2018-06-26 13:36:21 +00:00
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.
"""
pass
2018-09-30 17:40:28 +00:00
class Manufacturer(db.Model):
__table_args__ = {'schema': 'common'}
CUSTOM_MANUFACTURERS = {'Belinea', 'OKI Data Corporation', 'Vivitek', 'Yuraku'}
"""A list of manufacturer names that are not from Wikipedia's JSON."""
name = db.Column(CIText(), primary_key=True)
url = db.Column(URL(), unique=True)
logo = db.Column(URL())
@classmethod
def add_all_to_session(cls, session):
"""Adds all manufacturers to session."""
with pathlib.Path(__file__).parent.joinpath('manufacturers.json').open() as f:
for m in json.load(f):
man = cls(name=m['name'],
url=urlutils.URL(m['url']),
logo=urlutils.URL(m['logo']) if m.get('logo', None) else None)
session.add(man)
for name in cls.CUSTOM_MANUFACTURERS:
session.add(cls(name=name))