From c4b6553c8c0b5d8aea3e757de82bad4407ba9e71 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Tue, 10 Apr 2018 17:06:39 +0200 Subject: [PATCH] Prototyping --- ereuse_devicehub/__init__.py | 0 ereuse_devicehub/auth.py | 6 + ereuse_devicehub/client.py | 5 + ereuse_devicehub/config.py | 6 + ereuse_devicehub/db.py | 5 + ereuse_devicehub/devicehub.py | 6 + ereuse_devicehub/resources/__init__.py | 0 ereuse_devicehub/resources/device/__init__.py | 9 + ereuse_devicehub/resources/device/models.py | 88 +++++++ ereuse_devicehub/resources/device/schemas.py | 78 +++++++ ereuse_devicehub/resources/device/views.py | 8 + ereuse_devicehub/resources/event/__init__.py | 7 + ereuse_devicehub/resources/event/models.py | 219 ++++++++++++++++++ .../resources/event/remove/__init__.py | 0 .../resources/event/remove/views.py | 7 + .../resources/event/snapshot/__init__.py | 7 + .../resources/event/snapshot/views.py | 17 ++ ereuse_devicehub/resources/event/views.py | 7 + ereuse_devicehub/resources/model.py | 20 ++ ereuse_devicehub/resources/schema.py | 10 + ereuse_devicehub/resources/user/__init__.py | 0 ereuse_devicehub/resources/user/model.py | 9 + setup.py | 34 +++ tests/__init__.py | 0 tests/conftest.py | 31 +++ tests/test_basic.py | 15 ++ tests/test_device.py | 40 ++++ tests/test_event.py | 48 ++++ tests/test_models.py | 0 tests/test_schema.py | 6 + 30 files changed, 688 insertions(+) create mode 100644 ereuse_devicehub/__init__.py create mode 100644 ereuse_devicehub/auth.py create mode 100644 ereuse_devicehub/client.py create mode 100644 ereuse_devicehub/config.py create mode 100644 ereuse_devicehub/db.py create mode 100644 ereuse_devicehub/devicehub.py create mode 100644 ereuse_devicehub/resources/__init__.py create mode 100644 ereuse_devicehub/resources/device/__init__.py create mode 100644 ereuse_devicehub/resources/device/models.py create mode 100644 ereuse_devicehub/resources/device/schemas.py create mode 100644 ereuse_devicehub/resources/device/views.py create mode 100644 ereuse_devicehub/resources/event/__init__.py create mode 100644 ereuse_devicehub/resources/event/models.py create mode 100644 ereuse_devicehub/resources/event/remove/__init__.py create mode 100644 ereuse_devicehub/resources/event/remove/views.py create mode 100644 ereuse_devicehub/resources/event/snapshot/__init__.py create mode 100644 ereuse_devicehub/resources/event/snapshot/views.py create mode 100644 ereuse_devicehub/resources/event/views.py create mode 100644 ereuse_devicehub/resources/model.py create mode 100644 ereuse_devicehub/resources/schema.py create mode 100644 ereuse_devicehub/resources/user/__init__.py create mode 100644 ereuse_devicehub/resources/user/model.py create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_basic.py create mode 100644 tests/test_device.py create mode 100644 tests/test_event.py create mode 100644 tests/test_models.py create mode 100644 tests/test_schema.py diff --git a/ereuse_devicehub/__init__.py b/ereuse_devicehub/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/auth.py b/ereuse_devicehub/auth.py new file mode 100644 index 00000000..18880d9b --- /dev/null +++ b/ereuse_devicehub/auth.py @@ -0,0 +1,6 @@ +from teal.auth import TokenAuth + + +class Auth(TokenAuth): + pass + diff --git a/ereuse_devicehub/client.py b/ereuse_devicehub/client.py new file mode 100644 index 00000000..c9e0dbc4 --- /dev/null +++ b/ereuse_devicehub/client.py @@ -0,0 +1,5 @@ +from teal.client import Client as TealClient + + +class Client(TealClient): + pass diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py new file mode 100644 index 00000000..8b66d519 --- /dev/null +++ b/ereuse_devicehub/config.py @@ -0,0 +1,6 @@ +from ereuse_devicehub.resources.device import DeviceDef +from teal.config import Config + + +class DevicehubConfig(Config): + RESOURCE_DEFINITIONS = (DeviceDef,) diff --git a/ereuse_devicehub/db.py b/ereuse_devicehub/db.py new file mode 100644 index 00000000..cffaf06d --- /dev/null +++ b/ereuse_devicehub/db.py @@ -0,0 +1,5 @@ +from flask_sqlalchemy import SQLAlchemy + +from teal.db import Model + +db = SQLAlchemy(model_class=Model) diff --git a/ereuse_devicehub/devicehub.py b/ereuse_devicehub/devicehub.py new file mode 100644 index 00000000..d56716de --- /dev/null +++ b/ereuse_devicehub/devicehub.py @@ -0,0 +1,6 @@ +from ereuse_devicehub.client import Client +from teal.teal import Teal + + +class Devicehub(Teal): + test_client_class = Client diff --git a/ereuse_devicehub/resources/__init__.py b/ereuse_devicehub/resources/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/resources/device/__init__.py b/ereuse_devicehub/resources/device/__init__.py new file mode 100644 index 00000000..793bd0c9 --- /dev/null +++ b/ereuse_devicehub/resources/device/__init__.py @@ -0,0 +1,9 @@ +from ereuse_devicehub.resources.device.schemas import Device +from ereuse_devicehub.resources.device.views import DeviceView +from teal.resource import Resource, Converters + + +class DeviceDef(Resource): + SCHEMA = Device + VIEW = DeviceView + ID_CONVERTER = Converters.int diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py new file mode 100644 index 00000000..8350e132 --- /dev/null +++ b/ereuse_devicehub/resources/device/models.py @@ -0,0 +1,88 @@ +from sqlalchemy import BigInteger, Column, Float, ForeignKey, Integer, Sequence, SmallInteger, \ + Unicode +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import backref, relationship + +from ereuse_devicehub.resources.model import STR_BIG_SIZE, STR_SIZE, Thing, check_range +from teal.db import POLYMORPHIC_ID, POLYMORPHIC_ON, CASCADE + + +class Device(Thing): + id = Column(BigInteger, Sequence('device_seq'), primary_key=True) + type = Column(Unicode) + pid = Column(Unicode(STR_SIZE), unique=True) + gid = Column(Unicode(STR_SIZE), unique=True) + hid = Column(Unicode(STR_BIG_SIZE), unique=True) + model = Column(Unicode(STR_BIG_SIZE)) + manufacturer = Column(Unicode(STR_SIZE)) + serial_number = Column(Unicode(STR_SIZE)) + weight = Column(Float(precision=3), check_range('weight', min=0.1)) + width = Column(Float(precision=3), check_range('width', min=0.1)) + height = Column(Float(precision=3), check_range('height', min=0.1)) + + @declared_attr + def __mapper_args__(cls): + """ + Defines inheritance. + + From `the guide `_ + """ + args = {POLYMORPHIC_ID: cls.__name__} + if cls.__name__ == 'Device': + args[POLYMORPHIC_ON] = cls.type + return args + + +class Computer(Device): + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + + +class Desktop(Computer): + pass + + +class Laptop(Computer): + pass + + +class Netbook(Computer): + pass + + +class Server(Computer): + pass + + +class Microtower(Computer): + pass + + +class Component(Device): + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + + parent_id = Column(BigInteger, ForeignKey('computer.id')) + parent = relationship(Computer, + backref=backref('components', lazy=True, cascade=CASCADE), + primaryjoin='Component.parent_id == Computer.id') + + +class GraphicCard(Component): + memory = Column(SmallInteger, check_range('memory', min=0.1)) + + +class HardDrive(Component): + size = Column(Integer, check_range('size', min=0.1)) + + +class Motherboard(Component): + slots = Column(SmallInteger, check_range('slots')) + usb = Column(SmallInteger, check_range('usb')) + firewire = Column(SmallInteger, check_range('firewire')) + serial = Column(SmallInteger, check_range('serial')) + pcmcia = Column(SmallInteger, check_range('pcmcia')) + + +class NetworkAdapter(Component): + speed = Column(SmallInteger, check_range('speed')) diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py new file mode 100644 index 00000000..45d2989a --- /dev/null +++ b/ereuse_devicehub/resources/device/schemas.py @@ -0,0 +1,78 @@ +from marshmallow.fields import Float, Integer, Nested, Str +from marshmallow.validate import Length, Range + +from ereuse_devicehub.resources.model import STR_BIG_SIZE, STR_SIZE +from ereuse_devicehub.resources.schema import Thing + + +class Device(Thing): + id = Str(dump_only=True) + hid = Str(dump_only=True, + description='The Hardware ID is the unique ID traceability systems ' + 'use to ID a device globally.') + pid = Str(description='The PID identifies a device under a circuit or platform.', + validate=Length(max=STR_SIZE)) + gid = Str(description='The Giver ID links the device to the giver\'s (donor, seller)' + 'internal inventory.', + validate=Length(max=STR_SIZE)) + model = Str(validate=Length(max=STR_BIG_SIZE)) + manufacturer = Str(validate=Length(max=STR_SIZE)) + serial_number = Str(load_from='serialNumber', dump_to='serialNumber') + product_id = Str(load_from='productId', dump_to='productId') + weight = Float(validate=Range(0.1, 3)) + width = Float(validate=Range(0.1, 3)) + height = Float(validate=Range(0.1, 3)) + events = Nested('Event', many=True, dump_only=True, only='id') + + +class Computer(Device): + components = Nested('Component', many=True, dump_only=True, only='id') + pass + + +class Desktop(Computer): + pass + + +class Laptop(Computer): + pass + + +class Netbook(Computer): + pass + + +class Server(Computer): + pass + + +class Microtower(Computer): + pass + + +class Component(Device): + parent = Nested(Device, dump_only=True) + + +class GraphicCard(Component): + memory = Integer(validate=Range(0, 10000)) + + +class HardDrive(Component): + size = Integer(validate=Range(0, 10 ** 8)) + erasure = Nested('EraseBasic', load_only=True) + erasures = Nested('EraseBasic', dump_only=True, many=True) + tests = Nested('TestHardDrive', many=True) + benchmarks = Nested('BenchmarkHardDrive', many=True) + + +class Motherboard(Component): + slots = Integer(validate=Range(1, 20), description='PCI slots the motherboard has.') + usb = Integer(validate=Range(0, 20)) + firewire = Integer(validate=Range(0, 20)) + serial = Integer(validate=Range(0, 20)) + pcmcia = Integer(validate=Range(0, 20)) + + +class NetworkAdapter(Component): + speed = Integer(validate=Range(min=10, max=10000)) diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py new file mode 100644 index 00000000..63a27df1 --- /dev/null +++ b/ereuse_devicehub/resources/device/views.py @@ -0,0 +1,8 @@ +from teal.resource import View + + +class DeviceView(View): + + def one(self, id): + """Gets one device.""" + raise NotImplementedError diff --git a/ereuse_devicehub/resources/event/__init__.py b/ereuse_devicehub/resources/event/__init__.py new file mode 100644 index 00000000..2038f159 --- /dev/null +++ b/ereuse_devicehub/resources/event/__init__.py @@ -0,0 +1,7 @@ +from ereuse_devicehub.resources.event.views import EventView +from teal.resource import Resource + + +class EventDef(Resource): + SCHEMA = None + VIEW = EventView diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py new file mode 100644 index 00000000..c9493939 --- /dev/null +++ b/ereuse_devicehub/resources/event/models.py @@ -0,0 +1,219 @@ +from enum import Enum + +from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \ + ForeignKey, Integer, Interval, JSON, Sequence, SmallInteger, Unicode +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import backref, relationship + +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.model import STR_SIZE, Thing, check_range +from ereuse_devicehub.resources.user.model import User +from teal.db import CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON + + +class JoinedTableMixin: + @declared_attr + def id(cls): + return Column(BigInteger, ForeignKey(Event.id), primary_key=True) + + +class Event(Thing): + id = Column(BigInteger, Sequence('event_seq'), primary_key=True) + date = Column(DateTime) + secured = Column(Boolean, default=False, nullable=False) + type = Column(Unicode) + incidence = Column(Boolean, default=False, nullable=False) + + snapshot_id = Column(BigInteger, ForeignKey('snapshot.id', + use_alter=True, + name='snapshot_events')) + snapshot = relationship('Snapshot', + backref=backref('events', lazy=True, cascade=CASCADE), + primaryjoin='Event.snapshot_id == Snapshot.id') + + author_id = Column(BigInteger, ForeignKey(User.id), nullable=False) + author = relationship(User, + backref=backref('events', lazy=True), + primaryjoin=author_id == User.id) + + components = relationship(Device, + backref=backref('events_components', lazy=True), + secondary=lambda: EventComponent.__table__) + + @declared_attr + def __mapper_args__(cls): + """ + Defines inheritance. + + From `the guide `_ + """ + args = {POLYMORPHIC_ID: cls.__name__} + if cls.__name__ == 'Event': + args[POLYMORPHIC_ON] = cls.type + if JoinedTableMixin in cls.mro(): + args[INHERIT_COND] = cls.id == Event.id + return args + + +class EventComponent(db.Model): + device_id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + event_id = Column(BigInteger, ForeignKey(Event.id), primary_key=True) + + +class EventWithOneDevice(Event): + device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False) + device = relationship(Device, + backref=backref('events_one', lazy=True, cascade=CASCADE), + primaryjoin=Device.id == device_id) + + +class EventWithMultipleDevices(Event): + """ + Note that these events are not deleted when a device is deleted. + """ + devices = relationship(Device, + backref=backref('events', lazy=True), + secondary=lambda: EventDevice.__table__) + + +class EventDevice(db.Model): + device_id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + event_id = Column(BigInteger, ForeignKey(EventWithMultipleDevices.id), primary_key=True) + + +class Add(EventWithOneDevice): + pass + + +class Remove(EventWithOneDevice): + pass + + +class Allocate(JoinedTableMixin, EventWithMultipleDevices): + to_id = Column(BigInteger, ForeignKey(User.id)) + to = relationship(User, primaryjoin=User.id == to_id) + + +class Deallocate(JoinedTableMixin, EventWithMultipleDevices): + from_id = Column(BigInteger, ForeignKey(User.id)) + from_rel = relationship(User, primaryjoin=User.id == from_id) + + +class EraseBasic(JoinedTableMixin, EventWithOneDevice): + starting_time = Column(DateTime, nullable=False) + ending_time = Column(DateTime, nullable=False) + secure_random_steps = Column(SmallInteger, check_range('secure_random_steps', min=0), + nullable=False) + success = Column(Boolean, nullable=False) + clean_with_zeros = Column(Boolean, nullable=False) + + +class EraseSectors(EraseBasic): + pass + + +class StepTypes(Enum): + Zeros = 1 + Random = 2 + + +class Step(db.Model): + id = Column(BigInteger, Sequence('step_seq'), primary_key=True) + num = Column(SmallInteger, primary_key=True) + type = Column(DBEnum(StepTypes), nullable=False) + success = Column(Boolean, nullable=False) + starting_time = Column(DateTime, nullable=False) + ending_time = Column(DateTime, CheckConstraint('ending_time > starting_time'), nullable=False) + secure_random_steps = Column(SmallInteger, check_range('secure_random_steps', min=0), + nullable=False) + clean_with_zeros = Column(Boolean, nullable=False) + + erasure_id = Column(BigInteger, ForeignKey(EraseBasic.id)) + erasure = relationship(EraseBasic, backref=backref('steps', cascade=CASCADE_OWN)) + + +class SoftwareType(Enum): + Workbench = 'Workbench' + AndroidApp = 'AndroidApp' + Web = 'Web' + DesktopApp = 'DesktopApp' + + +class Appearance(Enum): + """Grades the imperfections that aesthetically affect the device, but not its usage.""" + Z = '0. The device is new.' + A = 'A. Is like new (without visual damage)' + B = 'B. Is in really good condition (small visual damage in difficult places to spot)' + C = 'C. Is in good condition (small visual damage in parts that are easy to spot, not screens)' + D = 'D. Is acceptable (visual damage in visible parts, not ¬screens)' + E = 'E. Is unacceptable (considerable visual damage that can affect usage)' + + +class Functionality(Enum): + A = 'A. Everything works perfectly (buttons, and in case of screens there are no scratches)' + B = 'B. There is a button difficult to press or a small scratch in an edge of a screen' + C = 'C. A non-important button (or similar) doesn\'t work; screen has multiple scratches in edges' + D = 'D. Multiple buttons don\'t work; screen has visual damage resulting in uncomfortable usage' + + +class Bios(Enum): + A = 'A. If by pressing a key you could access a boot menu with the network boot' + B = 'B. You had to get into the BIOS, and in less than 5 steps you could set the network boot' + C = 'C. Like B, but with more than 5 steps' + D = 'D. Like B or C, but you had to unlock the BIOS (i.e. by removing the battery)' + E = 'E. The device could not be booted through the network.' + + +class Snapshot(JoinedTableMixin, EventWithOneDevice): + uuid = Column(UUID(as_uuid=True), nullable=False, unique=True) + version = Column(Unicode, nullable=False) + snapshot_software = Column(DBEnum(SoftwareType), nullable=False) + appearance = Column(DBEnum(Appearance), nullable=False) + appearance_score = Column(SmallInteger, nullable=False) + functionality = Column(DBEnum(Functionality), nullable=False) + functionality_score = Column(SmallInteger, check_range('functionality_score', min=0, max=5), + nullable=False) + labelling = Column(Boolean, nullable=False) + bios = Column(DBEnum(Bios), nullable=False) + condition = Column(SmallInteger, check_range('condition', min=0, max=5), nullable=False) + elapsed = Column(Interval, nullable=False) + install_name = Column(Unicode) + install_elapsed = Column(Interval) + install_success = Column(Boolean) + inventory_elapsed = Column(Interval) + + +class SnapshotRequest(db.Model): + id = Column(BigInteger, ForeignKey(Snapshot.id), primary_key=True) + request = Column(JSON, nullable=False) + + snapshot = relationship(Snapshot, backref=backref('request', lazy=True, uselist=False, + cascade=CASCADE_OWN)) + + +class Test(JoinedTableMixin, EventWithOneDevice): + elapsed = Column(Interval, nullable=False) + success = Column(Boolean, nullable=False) + + snapshot = relationship(Snapshot, backref=backref('tests', lazy=True, cascade=CASCADE_OWN)) + + +class TestHardDriveLength(Enum): + Short = 'Short' + Extended = 'Extended' + + +class TestHardDrive(Test): + length = Column(DBEnum(TestHardDriveLength), nullable=False) # todo from type + status = Column(Unicode(STR_SIZE), nullable=False) + lifetime = Column(Interval, nullable=False) + first_error = Column(Integer) + # todo error becomes Test.success + + +class StressTest(Test): + pass diff --git a/ereuse_devicehub/resources/event/remove/__init__.py b/ereuse_devicehub/resources/event/remove/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/resources/event/remove/views.py b/ereuse_devicehub/resources/event/remove/views.py new file mode 100644 index 00000000..9d780596 --- /dev/null +++ b/ereuse_devicehub/resources/event/remove/views.py @@ -0,0 +1,7 @@ +from teal.resource import View + + +class Remove(View): + def post(self): + """Removes a component from a computer.""" + pass diff --git a/ereuse_devicehub/resources/event/snapshot/__init__.py b/ereuse_devicehub/resources/event/snapshot/__init__.py new file mode 100644 index 00000000..fe99151a --- /dev/null +++ b/ereuse_devicehub/resources/event/snapshot/__init__.py @@ -0,0 +1,7 @@ +from ereuse_devicehub.resources.event import EventDef +from ereuse_devicehub.resources.event.snapshot.views import SnapshotView + + +class SnapshotDef(EventDef): + VIEW = SnapshotView + SCHEMA = None diff --git a/ereuse_devicehub/resources/event/snapshot/views.py b/ereuse_devicehub/resources/event/snapshot/views.py new file mode 100644 index 00000000..a4ae0dae --- /dev/null +++ b/ereuse_devicehub/resources/event/snapshot/views.py @@ -0,0 +1,17 @@ +from teal.resource import View + + +class SnapshotView(View): + def post(self): + """Creates a Snapshot.""" + return super().post() + + def delete(self, id): + """Deletes a Snapshot""" + return super().delete(id) + + def patch(self, id): + """Modifies a Snapshot""" + return super().patch(id) + + diff --git a/ereuse_devicehub/resources/event/views.py b/ereuse_devicehub/resources/event/views.py new file mode 100644 index 00000000..8c3ea716 --- /dev/null +++ b/ereuse_devicehub/resources/event/views.py @@ -0,0 +1,7 @@ +from teal.resource import View + + +class EventView(View): + def one(self, id): + """Gets one event.""" + return super().one(id) diff --git a/ereuse_devicehub/resources/model.py b/ereuse_devicehub/resources/model.py new file mode 100644 index 00000000..1a62a362 --- /dev/null +++ b/ereuse_devicehub/resources/model.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from sqlalchemy import CheckConstraint + +from ereuse_devicehub.db import db + +STR_SIZE = 64 +STR_BIG_SIZE = 128 +STR_SM_SIZE = 32 + + +def check_range(column: str, min=1, max=None) -> CheckConstraint: + constraint = '>= {}'.format(min) if max is None else 'BETWEEN {} AND {}'.format(min, max) + return CheckConstraint('{} {}'.format(column, constraint)) + + +class Thing(db.Model): + __abstract__ = True + updated = db.Column(db.DateTime, onupdate=datetime.utcnow) + created = db.Column(db.DateTime, default=datetime.utcnow) diff --git a/ereuse_devicehub/resources/schema.py b/ereuse_devicehub/resources/schema.py new file mode 100644 index 00000000..99d69001 --- /dev/null +++ b/ereuse_devicehub/resources/schema.py @@ -0,0 +1,10 @@ +from marshmallow.fields import DateTime, List, Str, URL, Nested +from teal.resource import Schema + + +class Thing(Schema): + url = URL(dump_only=True, description='The URL of the resource.') + same_as = List(URL(dump_only=True), dump_only=True) + updated = DateTime('iso', dump_only=True) + created = DateTime('iso', dump_only=True) + author = Nested('User', only='id', dump_only=True) diff --git a/ereuse_devicehub/resources/user/__init__.py b/ereuse_devicehub/resources/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/resources/user/model.py b/ereuse_devicehub/resources/user/model.py new file mode 100644 index 00000000..7fe089bf --- /dev/null +++ b/ereuse_devicehub/resources/user/model.py @@ -0,0 +1,9 @@ +from sqlalchemy import BigInteger, Column, Sequence +from sqlalchemy_utils import EmailType + +from ereuse_devicehub.resources.model import Thing + + +class User(Thing): + id = Column(BigInteger, Sequence('user_seq'), primary_key=True) + email = Column(EmailType, nullable=False) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..d94d390c --- /dev/null +++ b/setup.py @@ -0,0 +1,34 @@ +from setuptools import find_packages, setup + +setup( + name="eReuse_Devicehub", + version='0.0.1', + packages=find_packages(), + url='https://github.com/ereuse/devicehub-teal', + license='Affero', + author='eReuse.org team', + author_email='x.bustamante@ereuse.org', + description='A system to manage devices focused in reusing them.', + install_requires=[ + 'teal', + 'marshmallow_enum', + 'ereuse-utils [Naming]', + 'psycopg2-binary', + 'sqlalchemy-utils' + ], + tests_requires=[ + 'pytest', + 'pytest-datadir' + ], + classifiers={ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.5', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' + }, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..4fa851dd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,31 @@ +import pytest + +from ereuse_devicehub.client import Client +from ereuse_devicehub.config import DevicehubConfig +from ereuse_devicehub.db import db +from ereuse_devicehub.devicehub import Devicehub + + +class TestConfig(DevicehubConfig): + SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh_test' + SQLALCHEMY_BINDS = { + 'common': 'postgresql://localhost/dh_test_common' + } + + +@pytest.fixture() +def config(): + return TestConfig() + + +@pytest.fixture() +def app(config: TestConfig) -> Devicehub: + app = Devicehub(config=config, db=db) + db.create_all(app=app) + yield app + db.drop_all(app=app) + + +@pytest.fixture() +def client(app: Devicehub) -> Client: + return app.test_client() diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 00000000..b405ce02 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,15 @@ +from datetime import datetime, timedelta +from uuid import uuid4 + +from ereuse_devicehub.db import db +from ereuse_devicehub.devicehub import Devicehub +from ereuse_devicehub.resources.device.models import Desktop, NetworkAdapter +from ereuse_devicehub.resources.event.models import Snapshot, SoftwareType, Appearance, \ + Functionality, Bios +from ereuse_devicehub.resources.user.model import User + + +# noinspection PyArgumentList +def test_init(app: Devicehub): + """Tests app initialization.""" + pass diff --git a/tests/test_device.py b/tests/test_device.py new file mode 100644 index 00000000..0d2aa82b --- /dev/null +++ b/tests/test_device.py @@ -0,0 +1,40 @@ +from ereuse_devicehub.db import db +from ereuse_devicehub.devicehub import Devicehub +from ereuse_devicehub.resources.device.models import Desktop, GraphicCard, NetworkAdapter, Device + + +def test_device_model(app: Devicehub): + with app.test_request_context(): + pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s') + pc.components = components = [ + NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), + GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) + ] + db.session.add(pc) + db.session.commit() + pc = Desktop.query.one() + assert pc.serial_number == 'p1s' + assert pc.components == components + network_adapter = NetworkAdapter.query.one() + assert network_adapter.parent == pc + + # Removing a component from pc doesn't delete the component + del pc.components[0] + db.session.commit() + pc = Device.query.first() # this is the same as querying for Desktop directly + assert pc.components[0].type == GraphicCard.__name__ + network_adapter = NetworkAdapter.query.one() + assert network_adapter not in pc.components + assert network_adapter.parent is None + + # Deleting the pc deletes everything + gcard = GraphicCard.query.one() + db.session.delete(pc) + assert pc.id == 1 + assert Desktop.query.first() is None + db.session.commit() + assert Desktop.query.first() is None + assert network_adapter.id == 2 + assert NetworkAdapter.query.first() is not None, 'We removed the network adaptor' + assert gcard.id == 3, 'We should still hold a reference to a zombie graphic card' + assert GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc' diff --git a/tests/test_event.py b/tests/test_event.py new file mode 100644 index 00000000..39dc5a4b --- /dev/null +++ b/tests/test_event.py @@ -0,0 +1,48 @@ +from datetime import datetime, timedelta +from uuid import uuid4 + +from ereuse_devicehub.db import db +from ereuse_devicehub.devicehub import Devicehub +from ereuse_devicehub.resources.device.models import Microtower, Device +from ereuse_devicehub.resources.event.models import Snapshot, SoftwareType, Appearance, \ + Functionality, Bios, SnapshotRequest, TestHardDrive, StressTest +from ereuse_devicehub.resources.user.model import User + + +# noinspection PyArgumentList +def test_event_model(app: Devicehub): + """ + Tests creating a Snapshot with its relationships ensuring correct + DB mapping. + """ + with app.test_request_context(): + user = User(email='foo@bar.com') + device = Microtower(serial_number='a1') + snapshot = Snapshot(uuid=uuid4(), + date=datetime.now(), + version='1.0', + snapshot_software=SoftwareType.DesktopApp, + appearance=Appearance.A, + appearance_score=5, + functionality=Functionality.A, + functionality_score=5, + labelling=False, + bios=Bios.C, + condition=5, + elapsed=timedelta(seconds=25)) + snapshot.device = device + snapshot.author = user + snapshot.request = SnapshotRequest(request={'foo': 'bar'}) + + db.session.add(snapshot) + db.session.commit() + device = Microtower.query.one() # type: Microtower + assert device.events_one[0].type == Snapshot.__name__ + db.session.delete(device) + db.session.commit() + assert Snapshot.query.one_or_none() is None + assert SnapshotRequest.query.one_or_none() is None + assert User.query.one() is not None + assert Microtower.query.one_or_none() is None + assert Device.query.one_or_none() is None + diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 00000000..5e7b8944 --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,6 @@ +from flask_sqlalchemy import SQLAlchemy + + +def test_device_schema(): + """Tests device schema.""" + pass \ No newline at end of file