class Membership(Thing): """Organizations that are related to the Individual. For example, because the individual works in or because is a member of. """ id = Column(Unicode(), check_lower('id')) organization_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True) organization = relationship(Organization, backref=backref('members', collection_class=set, lazy=True), primaryjoin=organization_id == Organization.id) individual_id = Column(UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True) individual = relationship(Individual, backref=backref('member_of', collection_class=set, lazy=True), primaryjoin=individual_id == Individual.id) def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None: super().__init__(organization=organization, individual=individual, id=id) __table_args__ = (UniqueConstraint( id, organization_id, name='One member id per organization.'), )
class Agent(Thing): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) type = Column(Unicode, nullable=False) name = Column(CIText()) name.comment = """ The name of the organization or person. """ tax_id = Column(Unicode(length=STR_SM_SIZE), check_lower('tax_id')) tax_id.comment = """ The Tax / Fiscal ID of the organization, e.g. the TIN in the US or the CIF/NIF in Spain. """ country = Column(DBEnum(enums.Country)) country.comment = """ Country issuing the tax_id number. """ telephone = Column(PhoneNumberType()) email = Column(EmailType, unique=True) __table_args__ = (UniqueConstraint( tax_id, country, name='Registration Number per country.'), UniqueConstraint(tax_id, name, name='One tax ID with one name.'), db.Index('agent_type', type, postgresql_using='hash')) @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 == 'Agent': args[POLYMORPHIC_ON] = cls.type if JoinedTableMixin in cls.mro(): args[INHERIT_COND] = cls.id == Agent.id return args @property def events(self) -> list: # todo test return sorted(chain(self.events_agent, self.events_to), key=attrgetter('created')) @validates('name') def does_not_contain_slash(self, _, value: str): if '/' in value: raise ValidationError('Name cannot contain slash \'') return value def __repr__(self) -> str: return '<{0.t} {0.name}>'.format(self)
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.""" abi = Column(Unicode, check_lower('abi')) abi.comment = """The Application Binary Interface of the processor."""
class Device(Thing): """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') id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id.comment = """ 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) 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. This field is auto-generated from Devicehub using literal identifiers from the device, so it can re-generated *offline*. """ + HID_CONVERSION_DOC model = Column(Unicode, check_lower('model')) model.comment = """The model of the device in lower case. The model is the unambiguous, as technical as possible, denomination for the product. This field, among others, is used to identify the product. """ 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.""" brand = db.Column(CIText()) brand.comment = """A naming for consumers. This field can represent several models, so it can be ambiguous, and it is not used to identify the product. """ generation = db.Column(db.SmallInteger, check_range('generation', 0)) generation.comment = """The generation of the device.""" weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5)) weight.comment = """ 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. """ height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5)) height.comment = """ The height of the device in meters. """ depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5)) depth.comment = """ The depth of the device in meters. """ color = Column(ColorType) color.comment = """The predominant color of the device.""" production_date = Column(db.DateTime) production_date.comment = """The date of production of the device. This is timezone naive, as Workbench cannot report this data with timezone information. """ variant = Column(Unicode) variant.comment = """A variant or sub-model of the device.""" _NON_PHYSICAL_PROPS = { 'id', 'type', 'created', 'updated', 'parent_id', 'hid', 'production_date', 'color', # these are only user-input thus volatile 'width', 'height', 'depth', 'weight', 'brand', 'generation', 'production_date', 'variant' } __table_args__ = (db.Index('device_id', id, postgresql_using='hash'), db.Index('type_index', type, postgresql_using='hash')) def __init__(self, **kw) -> None: super().__init__(**kw) with suppress(TypeError): self.hid = Naming.hid(self.type, self.manufacturer, self.model, self.serial_number) @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. 3. Events performed to a parent device. Events are returned by descending ``created`` time. """ return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY) @property def problems(self): """Current events with severity.Warning or higher. There can be up to 3 events: current Snapshot, current Physical event, current Trading event. """ from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.event.models import Snapshot events = set() with suppress(LookupError, ValueError): events.add(self.last_event_of(Snapshot)) with suppress(LookupError, ValueError): events.add(self.last_event_of(*states.Physical.events())) with suppress(LookupError, ValueError): events.add(self.last_event_of(*states.Trading.events())) return self._warning_events(events) @property def physical_properties(self) -> Dict[str, object or None]: """ 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 return { c.key: getattr(self, c.key, None) for c in inspect(self.__class__).attrs if isinstance(c, ColumnProperty) and not getattr(c, 'foreign_keys', None) and c.key not in self._NON_PHYSICAL_PROPS } @property def url(self) -> urlutils.URL: """The URL where to GET this device.""" return urlutils.URL(url_for_resource(Device, item_id=self.id)) @property def rate(self): """The last AggregateRate of the device.""" with suppress(LookupError, ValueError): from ereuse_devicehub.resources.event.models import AggregateRate return self.last_event_of(AggregateRate) @property def price(self): """The actual Price of the device, or None if no price has ever been set.""" with suppress(LookupError, ValueError): from ereuse_devicehub.resources.event.models import Price return self.last_event_of(Price) @property def trading(self): """The actual trading state, or None if no Trade event has ever been performed to this device.""" from ereuse_devicehub.resources.device import states with suppress(LookupError, ValueError): event = self.last_event_of(*states.Trading.events()) return states.Trading(event.__class__) @property def physical(self): """The actual physical state, None otherwise.""" from ereuse_devicehub.resources.device import states with suppress(LookupError, ValueError): event = self.last_event_of(*states.Physical.events()) return states.Physical(event.__class__) @property def physical_possessor(self): """The actual physical possessor or None. The physical possessor is the Agent that has physically the device. It differs from legal owners, usufructuarees or reserves in that the physical possessor does not have a legal relation per se with the device, but it is the one that has it physically. As an example, a transporter could be a physical possessor of a device although it does not own it legally. Note that there can only be one physical possessor per device, and :class:`ereuse_devicehub.resources.event.models.Receive` changes it. """ from ereuse_devicehub.resources.event.models import Receive with suppress(LookupError): event = self.last_event_of(Receive) return event.agent @property def working(self): """A list of the current tests with warning or errors. A 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 test has been executed. """ from ereuse_devicehub.resources.event.models import Test current_tests = unique_everseen( (e for e in reversed(self.events) if isinstance(e, Test)), key=attrgetter('type')) # last test of each type return self._warning_events(current_tests) @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': args[POLYMORPHIC_ON] = cls.type return args def last_event_of(self, *types): """Gets the last event of the given types. :raise LookupError: Device has not an event of the given type. """ try: # noinspection PyTypeHints return next(e for e in reversed(self.events) if isinstance(e, types)) except StopIteration: raise LookupError( '{!r} does not contain events of types {}.'.format( self, types)) def _warning_events(self, events): return sorted((ev for ev in events if ev.severity >= Severity.Warning), key=self.EVENT_SORT_KEY) def __lt__(self, other): return self.id < other.id 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})'.format(self) if self.serial_number: v += ' S/N ' + self.serial_number.upper() return v