Compare commits

...
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.

22 Commits

Author SHA1 Message Date
Santiago L c94b5a648b Autoformat black & isort 2022-04-06 13:05:51 +02:00
Santiago L 186f6398c0 Fix issue calling `create_table_from_selectable()` on LotDeviceDescendants
Replace `None` by `Column()`

sqlalchemy_utils on a4154bd0809bc6bbf0c27d5f7c0f3f2872edd779
breaks using `None` as padding parameter needed to follow restriction:
"All selectables passed to CompoundSelect must have identical numbers of columns"
2022-04-06 13:05:31 +02:00
Santiago L ecafc9ea39 Fix requirements part 2 2022-04-06 13:03:15 +02:00
Santiago L d1f27cc8e7 Fix incompatible dependecies with teal requirements 2022-04-06 11:31:49 +02:00
Santiago L 47a167b947 Merge branch 'upgrade-dependencies' into teal3 2022-04-05 14:03:02 +02:00
Santiago L e77bdfdad7 Bump Werkzeug to version >= 2.0 2022-04-05 13:48:12 +02:00
Santiago L 7f83b670ca Bump to click 8.0 2022-04-05 13:48:12 +02:00
Santiago L 6e5f6e2879
Merge branch 'testing' into dependencies/teal 2022-04-05 13:43:10 +02:00
Santiago L 305ddec2b8 Bump Flask to version 2.x 2022-04-05 13:37:27 +02:00
Santiago L e5dada4c17 Bump teal to devel version
Test if upgraded requirements works (Flask 2.x ...)
2022-04-05 13:32:37 +02:00
Santiago L e185ba297b Move testing requirements to separate file
Created requirements-testing.txt
2022-02-11 12:53:19 +01:00
Santiago L 5cf6adb1c3 Drop Werkzeug because it's a Flask dependency 2022-02-11 12:49:10 +01:00
Santiago L d66c335118 Drop python-dotenv from requirements.txt (unused) 2022-02-11 12:28:18 +01:00
Santiago L 0b41b526fd Drop tqdm as requirement (added but never used) 2022-02-11 12:25:48 +01:00
Santiago L cb231a35f8 Merge branch 'testing' into upgrade-dependencies 2022-02-11 12:09:21 +01:00
Santiago L 09649fee29 Sort requirements alphabetically 2022-02-10 09:51:42 +01:00
Santiago L 74432031e4 Revert "Add pytest-xdist to run test in parallel"
This reverts commit 3a395bed80.
2022-02-09 09:54:57 +01:00
Santiago L 3a395bed80 Add pytest-xdist to run test in parallel
Disable coverage because it's not compatible with parallel execution
2022-02-09 09:42:44 +01:00
Santiago L 01b85661b9 Fix "message" parameter of pytest.raises (part 2) 2022-02-09 09:29:15 +01:00
Santiago L 4256a8ba81 Add missing dependency `more-itertools` 2022-02-08 14:53:44 +01:00
Santiago L 311369691f Fix "message" parameter of pytest.raises
Removed in version 5.0 of pytest
https://docs.pytest.org/en/7.0.x/deprecations.html#message-parameter-of-pytest-raises
2022-02-08 14:48:54 +01:00
Santiago L 8c8323308b Bump pytest to 7.0.0 and drop deprecated pytest-runner 2022-02-08 14:38:13 +01:00
9 changed files with 219 additions and 157 deletions

View File

@ -47,7 +47,7 @@ jobs:
sudo apt-get update -qy
sudo apt-get -y install postgresql-client --no-install-recommends
python -m pip install --upgrade pip
pip install flake8 pytest coverage
pip install -r requirements-testing.txt
pip install -r requirements.txt
- name: Prepare database

View File

@ -10,7 +10,7 @@ from sqlalchemy import TEXT
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy_utils import LtreeType
from sqlalchemy_utils.types.ltree import LQUERY
from teal.db import CASCADE_OWN, UUIDLtree, check_range, IntEnum
from teal.db import CASCADE_OWN, IntEnum, UUIDLtree, check_range
from teal.resource import url_for_resource
from ereuse_devicehub.db import create_view, db, exp, f
@ -21,24 +21,29 @@ from ereuse_devicehub.resources.user.models import User
class Lot(Thing):
id = db.Column(UUID(as_uuid=True), primary_key=True) # uuid is generated on init by default
id = db.Column(
UUID(as_uuid=True), primary_key=True
) # uuid is generated on init by default
name = db.Column(CIText(), nullable=False)
description = db.Column(CIText())
description.comment = """A comment about the lot."""
closed = db.Column(db.Boolean, default=False, nullable=False)
closed.comment = """A closed lot cannot be modified anymore."""
devices = db.relationship(Device,
devices = db.relationship(
Device,
backref=db.backref('lots', lazy=True, collection_class=set),
secondary=lambda: LotDevice.__table__,
lazy=True,
collection_class=set)
collection_class=set,
)
"""The **children** devices that the lot has.
Note that the lot can have more devices, if they are inside
descendant lots.
"""
parents = db.relationship(lambda: Lot,
parents = db.relationship(
lambda: Lot,
viewonly=True,
lazy=True,
collection_class=set,
@ -46,45 +51,58 @@ class Lot(Thing):
primaryjoin=lambda: Lot.id == LotParent.child_id,
secondaryjoin=lambda: LotParent.parent_id == Lot.id,
cascade='refresh-expire', # propagate changes outside ORM
backref=db.backref('children',
backref=db.backref(
'children',
viewonly=True,
lazy=True,
cascade='refresh-expire',
collection_class=set)
collection_class=set,
),
)
"""The parent lots."""
all_devices = db.relationship(Device,
all_devices = db.relationship(
Device,
viewonly=True,
lazy=True,
collection_class=set,
secondary=lambda: LotDeviceDescendants.__table__,
primaryjoin=lambda: Lot.id == LotDeviceDescendants.ancestor_lot_id,
secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id)
secondaryjoin=lambda: LotDeviceDescendants.device_id == Device.id,
)
"""All devices, including components, inside this lot and its
descendants.
"""
amount = db.Column(db.Integer, check_range('amount', min=0, max=100), default=0)
owner_id = db.Column(UUID(as_uuid=True),
owner_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
default=lambda: g.user.id,
)
owner = db.relationship(User, primaryjoin=owner_id == User.id)
transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False)
transfer_state = db.Column(
IntEnum(TransferState), default=TransferState.Initial, nullable=False
)
transfer_state.comment = TransferState.__doc__
receiver_address = db.Column(CIText(),
receiver_address = db.Column(
CIText(),
db.ForeignKey(User.email),
nullable=False,
default=lambda: g.user.email)
default=lambda: g.user.email,
)
receiver = db.relationship(User, primaryjoin=receiver_address == User.email)
def __init__(self, name: str, closed: bool = closed.default.arg,
description: str = None) -> None:
def __init__(
self, name: str, closed: bool = closed.default.arg, description: str = None
) -> None:
"""Initializes a lot
:param name:
:param closed:
"""
super().__init__(id=uuid.uuid4(), name=name, closed=closed, description=description)
super().__init__(
id=uuid.uuid4(), name=name, closed=closed, description=description
)
Path(self) # Lots have always one edge per default.
@property
@ -115,7 +133,9 @@ class Lot(Thing):
@classmethod
def descendantsq(cls, id):
_id = UUIDLtree.convert(id)
return (cls.id == Path.lot_id) & Path.path.lquery(exp.cast('*.{}.*'.format(_id), LQUERY))
return (cls.id == Path.lot_id) & Path.path.lquery(
exp.cast('*.{}.*'.format(_id), LQUERY)
)
@classmethod
def roots(cls):
@ -176,13 +196,17 @@ class Lot(Thing):
if isinstance(child, Lot):
return Path.has_lot(self.id, child.id)
elif isinstance(child, Device):
device = db.session.query(LotDeviceDescendants) \
.filter(LotDeviceDescendants.device_id == child.id) \
.filter(LotDeviceDescendants.ancestor_lot_id == self.id) \
device = (
db.session.query(LotDeviceDescendants)
.filter(LotDeviceDescendants.device_id == child.id)
.filter(LotDeviceDescendants.ancestor_lot_id == self.id)
.one_or_none()
)
return device
else:
raise TypeError('Lot only contains devices and lots, not {}'.format(child.__class__))
raise TypeError(
'Lot only contains devices and lots, not {}'.format(child.__class__)
)
def __repr__(self) -> str:
return '<Lot {0.name} devices={0.devices!r}>'.format(self)
@ -192,35 +216,44 @@ class LotDevice(db.Model):
device_id = db.Column(db.BigInteger, db.ForeignKey(Device.id), primary_key=True)
lot_id = db.Column(UUID(as_uuid=True), db.ForeignKey(Lot.id), primary_key=True)
created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
author_id = db.Column(UUID(as_uuid=True),
author_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
default=lambda: g.user.id,
)
author = db.relationship(User, primaryjoin=author_id == User.id)
author_id.comment = """The user that put the device in the lot."""
class Path(db.Model):
id = db.Column(db.UUID(as_uuid=True),
id = db.Column(
db.UUID(as_uuid=True),
primary_key=True,
server_default=db.text('gen_random_uuid()'))
server_default=db.text('gen_random_uuid()'),
)
lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False)
lot = db.relationship(Lot,
backref=db.backref('paths',
lazy=True,
collection_class=set,
cascade=CASCADE_OWN),
primaryjoin=Lot.id == lot_id)
lot = db.relationship(
Lot,
backref=db.backref(
'paths', lazy=True, collection_class=set, cascade=CASCADE_OWN
),
primaryjoin=Lot.id == lot_id,
)
path = db.Column(LtreeType, nullable=False)
created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP'))
created = db.Column(
db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP')
)
created.comment = """When Devicehub created this."""
__table_args__ = (
# dag.delete_edge needs to disable internally/temporarily the unique constraint
db.UniqueConstraint(path, name='path_unique', deferrable=True, initially='immediate'),
db.UniqueConstraint(
path, name='path_unique', deferrable=True, initially='immediate'
),
db.Index('path_gist', path, postgresql_using='gist'),
db.Index('path_btree', path, postgresql_using='btree'),
db.Index('lot_id_index', lot_id, postgresql_using='hash')
db.Index('lot_id_index', lot_id, postgresql_using='hash'),
)
def __init__(self, lot: Lot) -> None:
@ -243,7 +276,9 @@ class Path(db.Model):
child_id = UUIDLtree.convert(child_id)
return bool(
db.session.execute(
"SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(parent_id, child_id)
"SELECT 1 from path where path ~ '*.{}.*.{}.*'".format(
parent_id, child_id
)
).first()
)
@ -263,47 +298,75 @@ class LotDeviceDescendants(db.Model):
"""Ancestor lot table."""
_desc = Lot.__table__.alias()
"""Descendant lot table."""
lot_device = _desc \
.join(LotDevice, _desc.c.id == LotDevice.lot_id) \
.join(Path, _desc.c.id == Path.lot_id)
lot_device = _desc.join(LotDevice, _desc.c.id == LotDevice.lot_id).join(
Path, _desc.c.id == Path.lot_id
)
"""Join: Path -- Lot -- LotDevice"""
descendants = "path.path ~ (CAST('*.'|| replace(CAST({}.id as text), '-', '_') " \
descendants = (
"path.path ~ (CAST('*.'|| replace(CAST({}.id as text), '-', '_') "
"|| '.*' AS LQUERY))".format(_ancestor.name)
)
"""Query that gets the descendants of the ancestor lot."""
devices = db.select([
devices = (
db.select(
[
LotDevice.device_id,
_desc.c.id.label('parent_lot_id'),
_ancestor.c.id.label('ancestor_lot_id'),
None
]).select_from(_ancestor).select_from(lot_device).where(db.text(descendants))
db.column(
'padding'
), # foo column to have same nunber of columns on joined selects (union)
]
)
.select_from(_ancestor)
.select_from(lot_device)
.where(db.text(descendants))
)
# Components
_parent_device = Device.__table__.alias(name='parent_device')
"""The device that has the access to the lot."""
lot_device_component = lot_device \
.join(_parent_device, _parent_device.c.id == LotDevice.device_id) \
.join(Component, _parent_device.c.id == Component.parent_id)
lot_device_component = lot_device.join(
_parent_device, _parent_device.c.id == LotDevice.device_id
).join(Component, _parent_device.c.id == Component.parent_id)
"""Join: Path -- Lot -- LotDevice -- ParentDevice (Device) -- Component"""
components = db.select([
components = (
db.select(
[
Component.id.label('device_id'),
_desc.c.id.label('parent_lot_id'),
_ancestor.c.id.label('ancestor_lot_id'),
LotDevice.device_id.label('device_parent_id'),
]).select_from(_ancestor).select_from(lot_device_component).where(db.text(descendants))
]
)
.select_from(_ancestor)
.select_from(lot_device_component)
.where(db.text(descendants))
)
__table__ = create_view('lot_device_descendants', devices.union(components))
class LotParent(db.Model):
i = f.index(Path.path, db.func.text2ltree(f.replace(exp.cast(Path.lot_id, TEXT), '-', '_')))
i = f.index(
Path.path, db.func.text2ltree(f.replace(exp.cast(Path.lot_id, TEXT), '-', '_'))
)
__table__ = create_view(
'lot_parent',
db.select([
db.select(
[
Path.lot_id.label('child_id'),
exp.cast(f.replace(exp.cast(f.subltree(Path.path, i - 1, i), TEXT), '_', '-'),
UUID).label('parent_id')
]).select_from(Path).where(i > 0),
exp.cast(
f.replace(
exp.cast(f.subltree(Path.path, i - 1, i), TEXT), '_', '-'
),
UUID,
).label('parent_id'),
]
)
.select_from(Path)
.where(i > 0),
)

3
pytest.ini Normal file
View File

@ -0,0 +1,3 @@
[pytest]
markers =
mvp: mark tests as required by MVP

4
requirements-testing.txt Normal file
View File

@ -0,0 +1,4 @@
coverage
flake8
pytest==7.0.0
requests-mock==1.5.2

View File

@ -1,45 +1,43 @@
alembic==1.4.2
anytree==2.4.3
apispec==0.39.0
apispec==5.1.1
boltons==18.0.1
click==6.7
click==8.0
click-spinner==0.1.8
colorama==0.3.9
colour==0.1.5
ereuse-utils[naming,test,session,cli]==0.4.0b50
Flask==1.0.2
Flask>=2.0
Flask-Cors==3.0.10
Flask-Login==0.5.0
Flask-SQLAlchemy==2.3.2
Flask-Login==0.6.0
Flask-SQLAlchemy==2.5.1
Flask-WTF==1.0.0
flask-WeasyPrint==0.5
hashids==1.2.0
inflection==0.3.1
# lock itsdangerous version until upgrade to Flask 2.x
itsdangerous==2.0.1
# lock Jinja2 version because it's the latest compatible with Flask 1.0.X
# see related info on https://github.com/pallets/jinja/issues/1628
Jinja2==3.0.3
marshmallow==3.0.0b11
marshmallow-enum==1.4.1
more-itertools==8.12.0
passlib==1.7.1
phonenumbers==8.9.11
pytest==3.7.2
pytest-runner==4.2
psycopg2-binary==2.8.3
PyJWT==2.0.0a1
python-dateutil==2.7.3
python-decouple==3.3
python-stdnum==1.9
PyYAML==5.4
requests[security]==2.27.1
requests-mock==1.5.2
SQLAlchemy==1.3.24
SQLAlchemy-Utils==0.33.11
teal==0.2.0a38
webargs==5.5.3
Werkzeug==0.15.3
sqlalchemy-citext==1.3.post0
flask-weasyprint==0.5
weasyprint==44
psycopg2-binary==2.8.3
sortedcontainers==2.1.0
tqdm==4.32.2
python-decouple==3.3
python-dotenv==0.14.0
pyjwt==2.0.0a1
SQLAlchemy==1.4.34
sqlalchemy-citext==1.3.post0
SQLAlchemy-Utils==0.38.2
# teal under development version
-e git+https://github.com/eReuse/teal/@upgrade-dependencies#egg=teal
WeasyPrint==44
webargs==5.5.3
Werkzeug>=2.0

View File

@ -3,11 +3,6 @@ from setuptools import find_packages, setup
from ereuse_devicehub import __version__
test_requires = [
'pytest',
'requests_mock'
]
setup(
name='ereuse-devicehub',
version=__version__,
@ -52,17 +47,12 @@ setup(
'docs-auto': [
'sphinx-autobuild'
],
'test': test_requires
},
tests_require=test_requires,
entry_points={
'console_scripts': [
'dh = ereuse_devicehub.cli:cli'
]
},
setup_requires=[
'pytest-runner'
],
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment',
@ -71,8 +61,7 @@ setup(
'License :: OSI Approved :: GNU Affero General Public License v3',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
'Topic :: Software Development :: Libraries :: Python Modules',

View File

@ -75,14 +75,14 @@ def test_erase_basic():
def test_validate_device_data_storage():
"""Checks the validation for data-storage-only actions works."""
# We can't set a GraphicCard
with pytest.raises(TypeError,
message='EraseBasic.device must be a DataStorage '
'but you passed <GraphicCard None model=\'foo-bar\' S/N=\'foo\'>'):
with pytest.raises(TypeError):
models.EraseBasic(
device=GraphicCard(serial_number='foo', manufacturer='bar', model='foo-bar'),
clean_with_zeros=True,
**conftest.T
)
pytest.fail('EraseBasic.device must be a DataStorage '
'but you passed <GraphicCard None model=\'foo-bar\' S/N=\'foo\'>')
@pytest.mark.mvp

View File

@ -24,11 +24,14 @@ def test_authenticate_error(app: Devicehub):
MESSAGE = 'Provide a suitable token.'
create_user()
# Token doesn't exist
with pytest.raises(Unauthorized, message=MESSAGE):
with pytest.raises(Unauthorized):
app.auth.authenticate(token=str(uuid4()))
pytest.fail(MESSAGE)
# Wrong token format
with pytest.raises(Unauthorized, message=MESSAGE):
with pytest.raises(Unauthorized):
app.auth.authenticate(token='this is a wrong uuid')
pytest.fail(MESSAGE)
@pytest.mark.mvp

View File

@ -144,5 +144,7 @@ def test_create_existing_inventory(cli, tdb1):
cli.invoke('inv', 'add', '--common')
with tdb1.app_context():
assert db.has_schema('tdb1')
with pytest.raises(AssertionError, message='Schema tdb1 already exists.'):
with pytest.raises(AssertionError):
cli.invoke('inv', 'add', '--common')
pytest.fail('Schema tdb1 already exists.')