Add inventories with dispatcher

This commit is contained in:
Xavier Bustamante Talavera 2019-01-23 16:55:04 +01:00
parent 79feb33aa3
commit f570e9d3d0
27 changed files with 414 additions and 99 deletions

28
ereuse_devicehub/cli.py Normal file
View File

@ -0,0 +1,28 @@
import os
import click.testing
import flask.cli
from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.devicehub import Devicehub
class DevicehubGroup(flask.cli.FlaskGroup):
CONFIG = DevicehubConfig
def main(self, *args, **kwargs):
# todo this should be taken as an argument for the cli
inventory = os.environ.get('dhi')
if not inventory:
raise ValueError('Please do "export dhi={inventory}"')
self.create_app = self.create_app_factory(inventory)
return super().main(*args, **kwargs)
@staticmethod
def create_app_factory(inventory):
return lambda: Devicehub(inventory)
@click.group(cls=DevicehubGroup)
def cli():
pass

View File

@ -2,13 +2,12 @@ from distutils.version import StrictVersion
from itertools import chain
from typing import Set
import boltons.urlutils
from teal.auth import TokenAuth
from teal.config import Config
from teal.enums import Currency
from teal.utils import import_resource
from ereuse_devicehub.resources import agent, event, lot, tag, user
from ereuse_devicehub.resources import agent, event, inventory, lot, tag, user
from ereuse_devicehub.resources.device import definitions
from ereuse_devicehub.resources.documents import documents
from ereuse_devicehub.resources.enums import PriceSoftware, RatingSoftware
@ -21,23 +20,16 @@ class DevicehubConfig(Config):
import_resource(tag),
import_resource(agent),
import_resource(lot),
import_resource(documents))
import_resource(documents),
import_resource(inventory)),
)
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/devicehub' # type: str
SCHEMA = 'dhub'
MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion
"""
the minimum version of ereuse.org workbench that this devicehub
accepts. we recommend not changing this value.
"""
ORGANIZATION_NAME = None # type: str
ORGANIZATION_TAX_ID = None # type: str
"""
The organization using this Devicehub.
It is used by default, for example, when creating tags.
"""
API_DOC_CONFIG_TITLE = 'Devicehub'
API_DOC_CONFIG_VERSION = '0.2'
API_DOC_CONFIG_COMPONENTS = {
@ -60,19 +52,3 @@ class DevicehubConfig(Config):
"""
Official versions
"""
TAG_BASE_URL = None
TAG_TOKEN = None
"""Access to the tag provider."""
def __init__(self, schema: str = None, token=None) -> None:
if not self.ORGANIZATION_NAME or not self.ORGANIZATION_TAX_ID:
raise ValueError('You need to set the main organization parameters.')
if not self.TAG_BASE_URL:
raise ValueError('You need a tag service.')
self.TAG_TOKEN = token or self.TAG_TOKEN
if not self.TAG_TOKEN:
raise ValueError('You need a tag token')
self.TAG_BASE_URL = boltons.urlutils.URL(self.TAG_BASE_URL)
if schema:
self.SCHEMA = schema
super().__init__()

View File

@ -1,16 +1,23 @@
import uuid
from typing import Type
import boltons.urlutils
import click
import click_spinner
import ereuse_utils.cli
from ereuse_utils.session import DevicehubClient
from flask.globals import _app_ctx_stack, g
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import event
from teal.config import Config as ConfigClass
from teal.teal import Teal
from ereuse_devicehub.auth import Auth
from ereuse_devicehub.client import Client
from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.db import db
from ereuse_devicehub.dummy.dummy import Dummy
from ereuse_devicehub.resources.device.search import DeviceSearch
from ereuse_devicehub.resources.inventory import Inventory, InventoryDef
class Devicehub(Teal):
@ -18,7 +25,8 @@ class Devicehub(Teal):
Dummy = Dummy
def __init__(self,
config: ConfigClass,
inventory: str,
config: DevicehubConfig = DevicehubConfig(),
db: SQLAlchemy = db,
import_name=__name__.split('.')[0],
static_url_path=None,
@ -31,27 +39,76 @@ class Devicehub(Teal):
instance_relative_config=False,
root_path=None,
Auth: Type[Auth] = Auth):
super().__init__(config, db, import_name, static_url_path, static_folder, static_host,
assert inventory
super().__init__(config, db, inventory, import_name, static_url_path, static_folder,
static_host,
host_matching, subdomain_matching, template_folder, instance_path,
instance_relative_config, root_path, Auth)
self.tag_provider = DevicehubClient(**self.config.get_namespace('TAG_'))
self.id = inventory
"""The Inventory ID of this instance. In Teal is the app.schema."""
self.dummy = Dummy(self)
self.before_request(self.register_db_events_listeners)
self.cli.command('regenerate-search')(self.regenerate_search)
self.cli.command('init-db')(self.init_db)
self.before_request(self._prepare_request)
def register_db_events_listeners(self):
"""Registers the SQLAlchemy event listeners."""
# todo can I make it with a global Session only?
event.listen(db.session, 'before_commit', DeviceSearch.update_modified_devices)
def _init_db(self, exclude_schema=None, check=False):
created = super()._init_db(exclude_schema, check)
if created:
# noinspection PyMethodOverriding
@click.option('--name', '-n',
default='Test 1',
help='The human name of the inventory.')
@click.option('--org-name', '-on',
default='My Organization',
help='The name of the default organization that owns this inventory.')
@click.option('--org-id', '-oi',
default='foo-bar',
help='The Tax ID of the organization.')
@click.option('--tag-url', '-tu',
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
default='http://example.com',
help='The base url (scheme and host) of the tag provider.')
@click.option('--tag-token', '-tt',
type=click.UUID,
default='899c794e-1737-4cea-9232-fdc507ab7106',
help='The token provided by the tag provider. It is an UUID.')
@click.option('--erase/--no-erase',
default=False,
help='Delete the full database before? Including all schemas and users.')
@click.option('--common',
default=False,
help='Creates common databases. Only execute if the database is empty.')
def init_db(self, name: str,
org_name: str,
org_id: str,
tag_url: boltons.urlutils.URL,
tag_token: uuid.UUID,
erase: bool,
common: bool):
"""Initializes this inventory with the provided configurations."""
assert _app_ctx_stack.top, 'Use an app context.'
print('Initializing database...'.ljust(30), end='')
with click_spinner.spinner():
if erase:
self.db.drop_all()
exclude_schema = 'common' if not common else None
self._init_db(exclude_schema=exclude_schema)
InventoryDef.set_inventory_config(name, org_name, org_id, tag_url, tag_token)
DeviceSearch.set_all_devices_tokens_if_empty(self.db.session)
return created
self._init_resources(exclude_schema=exclude_schema)
self.db.session.commit()
print('done.')
def regenerate_search(self):
"""Re-creates from 0 all the search tables."""
DeviceSearch.regenerate_search_table(self.db.session)
db.session.commit()
print('Done.')
def _prepare_request(self):
"""Prepares request stuff."""
inv = g.inventory = Inventory.current # type: Inventory
g.tag_provider = DevicehubClient(base_url=inv.tag_provider, token=inv.tag_token)

View File

@ -0,0 +1,46 @@
from threading import Lock
import sqlalchemy as sa
import werkzeug.exceptions
from werkzeug import wsgi
import ereuse_devicehub.config
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.inventory import Inventory
class PathDispatcher:
NOT_FOUND = werkzeug.exceptions.NotFound()
INV = Inventory
def __init__(self, config_cls=ereuse_devicehub.config.DevicehubConfig) -> None:
self.lock = Lock()
self.instances = {}
self.CONFIG = config_cls
self.engine = sa.create_engine(self.CONFIG.SQLALCHEMY_DATABASE_URI)
with self.lock:
self.instantiate()
if not self.instances:
raise ValueError('There are no Devicehub instances! Please, execute `dh init-db`.')
self.one_app = next(iter(self.instances.values()))
def __call__(self, environ, start_response):
if wsgi.get_path_info(environ).startswith('/users'):
# Not nice solution but it works well for now
# Return any app, as all apps can handle login
return self.call(self.one_app, environ, start_response)
inventory = wsgi.pop_path_info(environ)
with self.lock:
if inventory not in self.instances:
self.instantiate()
app = self.instances.get(inventory, self.NOT_FOUND)
return self.call(app, environ, start_response)
@staticmethod
def call(app, environ, start_response):
return app(environ, start_response)
def instantiate(self):
sel = sa.select([self.INV.id]).where(self.INV.id.notin_(self.instances.keys()))
for row in self.engine.execute(sel):
self.instances[row.id] = Devicehub(inventory=row.id)

View File

@ -5,6 +5,7 @@ from typing import Set
import click
import click_spinner
import ereuse_utils.cli
import yaml
from ereuse_utils.test import ANY
@ -41,11 +42,25 @@ class Dummy:
self.app = app
self.app.cli.command('dummy', short_help='Creates dummy devices and users.')(self.run)
@click.option('--tag-url', '-tu',
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
default='http://localhost:8081',
help='The base url (scheme and host) of the tag provider.')
@click.option('--tag-token', '-tt',
type=click.UUID,
default='899c794e-1737-4cea-9232-fdc507ab7106',
help='The token provided by the tag provider. It is an UUID.')
@click.confirmation_option(prompt='This command (re)creates the DB from scratch.'
'Do you want to continue?')
def run(self):
def run(self, tag_url, tag_token):
runner = self.app.test_cli_runner()
self.app.init_db(erase=True)
self.app.init_db('Dummy',
'ACME',
'acme-id',
tag_url,
tag_token,
erase=True,
common=True)
print('Creating stuff...'.ljust(30), end='')
with click_spinner.spinner():
out = runner.invoke(args=['create-org', *self.ORG], catch_exceptions=False).output

View File

@ -1,8 +1,6 @@
import json
import click
from flask import current_app as app
from teal.db import SQLAlchemy
from teal.resource import Converters, Resource
from ereuse_devicehub.db import db
@ -46,11 +44,6 @@ class OrganizationDef(AgentDef):
print(json.dumps(o, indent=2))
return o
def init_db(self, db: SQLAlchemy, exclude_schema=None):
"""Creates the default organization."""
org = models.Organization(**app.config.get_namespace('ORGANIZATION_'))
db.session.add(org)
class Membership(Resource):
SCHEMA = schemas.Membership

View File

@ -3,17 +3,17 @@ from operator import attrgetter
from uuid import uuid4
from citext import CIText
from flask import current_app as app, g
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import backref, relationship, validates
from sqlalchemy_utils import EmailType, PhoneNumberType
from teal import enums
from teal.db import DBError, INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, check_lower
from teal.db import INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, check_lower
from teal.marshmallow import ValidationError
from werkzeug.exceptions import NotImplemented, UnprocessableEntity
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.inventory import Inventory
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User
@ -46,6 +46,7 @@ class Agent(Thing):
__table_args__ = (
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
UniqueConstraint(tax_id, name, name='One tax ID with one name.')
)
@declared_attr
@ -80,21 +81,18 @@ class Agent(Thing):
class Organization(JoinedTableMixin, Agent):
default_of = db.relationship(Inventory,
single_parent=True,
uselist=False,
primaryjoin=lambda: Organization.id == Inventory.org_id)
def __init__(self, name: str, **kwargs) -> None:
super().__init__(**kwargs, name=name)
@classmethod
def get_default_org_id(cls) -> UUID:
"""Retrieves the default organization."""
try:
return g.setdefault('org_id',
Organization.query.filter_by(
**app.config.get_namespace('ORGANIZATION_')
).one().id)
except (DBError, UnprocessableEntity):
# todo test how well this works
raise NotImplemented('Error in getting the default organization. '
'Is the DB initialized?')
return cls.query.filter_by(default_of=Inventory.current).one().id
class Individual(JoinedTableMixin, Agent):

View File

@ -21,6 +21,7 @@ class Agent(Thing):
class Organization(Agent):
members = NestedOn('Membership')
default_of = NestedOn('Inventory')
class Membership(Thing):

View File

@ -0,0 +1,79 @@
import uuid
import boltons.urlutils
import click
import ereuse_utils.cli
from flask import current_app
from teal.db import ResourceNotFound
from teal.resource import Resource
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.inventory import schema
from ereuse_devicehub.resources.inventory.model import Inventory
class InventoryDef(Resource):
SCHEMA = schema.Inventory
VIEW = None
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None):
cli_commands = (
(self.set_inventory_config_cli, 'set-inventory-config'),
)
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands)
@click.option('--name', '-n',
default='Test 1',
help='The human name of the inventory.')
@click.option('--org-name', '-on',
default=None,
help='The name of the default organization that owns this inventory.')
@click.option('--org-id', '-oi',
default=None,
help='The Tax ID of the organization.')
@click.option('--tag-url', '-tu',
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
default=None,
help='The base url (scheme and host) of the tag provider.')
@click.option('--tag-token', '-tt',
type=click.UUID,
default=None,
help='The token provided by the tag provider. It is an UUID.')
def set_inventory_config_cli(self, **kwargs):
"""Sets the inventory configuration. Only updates passed-in
values.
"""
self.set_inventory_config(**kwargs)
db.session.commit()
@classmethod
def set_inventory_config(cls,
name: str = None,
org_name: str = None,
org_id: str = None,
tag_url: boltons.urlutils.URL = None,
tag_token: uuid.UUID = None):
try:
inventory = Inventory.current
except ResourceNotFound: # No inventory defined in db yet
inventory = Inventory(id=current_app.id,
name=name,
tag_provider=tag_url,
tag_token=tag_token)
db.session.add(inventory)
if org_name or org_id:
from ereuse_devicehub.resources.agent.models import Organization
try:
org = Organization.query.filter_by(tax_id=org_id, name=org_name).one()
except ResourceNotFound:
org = Organization(tax_id=org_id, name=org_name)
org.default_of = inventory
db.session.add(org)
if tag_url:
inventory.tag_provider = tag_url
if tag_token:
inventory.tag_token = tag_token

View File

@ -1,3 +1,6 @@
from boltons.typeutils import classproperty
from flask import current_app
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.models import Thing
@ -8,5 +11,12 @@ class Inventory(Thing):
id.comment = """The name of the inventory as in the URL and schema."""
name = db.Column(db.CIText(), nullable=False, unique=True)
name.comment = """The human name of the inventory."""
tag_token = db.Column(db.UUID(as_uuid=True), unique=True)
tag_provider = db.Column(db.URL(), nullable=False)
tag_token = db.Column(db.UUID(as_uuid=True), unique=True, nullable=False)
tag_token.comment = """The token to access a Tag service."""
org_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey('organization.id'), nullable=False)
@classproperty
def current(cls) -> 'Inventory':
"""The inventory of the current_app."""
return Inventory.query.filter_by(id=current_app.id).one()

View File

@ -0,0 +1,11 @@
import teal.marshmallow
from marshmallow import fields as mf
from ereuse_devicehub.resources.schemas import Thing
class Inventory(Thing):
id = mf.String(dump_only=True)
name = mf.String(dump_only=True)
tag_provider = teal.marshmallow.URL(dump_only=True, data_key='tagProvider')
tag_token = mf.UUID(dump_only=True, data_key='tagToken')

View File

@ -1,5 +1,4 @@
from ereuse_utils.session import DevicehubClient
from flask import Response, current_app, current_app as app, jsonify, redirect, request
from flask import Response, current_app as app, g, jsonify, redirect, request
from teal.marshmallow import ValidationError
from teal.resource import View, url_for_resource
@ -19,9 +18,8 @@ class TagView(View):
return res
def _create_many_regular_tags(self, num: int):
tag_provider = current_app.tag_provider # type: DevicehubClient
tags_id, _ = tag_provider.post('/', {}, query=[('num', num)])
tags = [Tag(id=tag_id, provider=current_app.config['TAG_BASE_URL']) for tag_id in tags_id]
tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)])
tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id]
db.session.add_all(tags)
db.session.commit()
response = jsonify(items=self.schema.dump(tags, many=True, nested=1)) # type: Response

View File

@ -1,8 +1,11 @@
from typing import Iterable
from click import argument, option
from flask import current_app
from teal.resource import Converters, Resource
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.inventory import Inventory
from ereuse_devicehub.resources.user import schemas
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.user.views import UserView, login
@ -23,13 +26,21 @@ class UserDef(Resource):
self.add_url_rule('/login/', view_func=login, methods={'POST'})
@argument('email')
@option('-i', '--inventory',
multiple=True,
help='Inventories user has access to. By default this one.')
@option('-a', '--agent', help='The name of an agent to create with the user.')
@option('-c', '--country', help='The country of the agent (if --agent is set).')
@option('-t', '--telephone', help='The telephone of the agent (if --agent is set).')
@option('-t', '--tax-id', help='The tax id of the agent (if --agent is set).')
@option('-p', '--password', prompt=True, hide_input=True, confirmation_prompt=True)
def create_user(self, email: str, password: str, agent: str = None, country: str = None,
telephone: str = None, tax_id: str = None) -> dict:
def create_user(self, email: str,
password: str,
inventory: Iterable[str] = tuple(),
agent: str = None,
country: str = None,
telephone: str = None,
tax_id: str = None) -> dict:
"""Creates an user.
If ``--agent`` is passed, it creates an ``Individual`` agent
@ -38,7 +49,9 @@ class UserDef(Resource):
from ereuse_devicehub.resources.agent.models import Individual
u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
.load({'email': email, 'password': password})
user = User(**u)
if inventory:
inventory = Inventory.query.filter(Inventory.id.in_(inventory))
user = User(**u, inventories=inventory)
agent = Individual(**current_app.resources[Individual.t].schema.load(
dict(name=agent, email=email, country=country, telephone=telephone, taxId=tax_id)
))

View File

@ -19,16 +19,24 @@ class User(Thing):
schemes=app.config['PASSWORD_SCHEMES'],
**kwargs
)))
"""
Password field.
From `here <https://sqlalchemy-utils.readthedocs.io/en/latest/
data_types.html#module-sqlalchemy_utils.types.password>`_
"""
token = Column(UUID(as_uuid=True), default=uuid4, unique=True)
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
inventories = db.relationship(Inventory,
backref=db.backref('users', lazy=True, collection_class=set),
secondary=lambda: UserInventory.__table__,
collection_class=set)
# todo set restriction that user has, at least, one active db
def __init__(self, email, password=None, inventories=None) -> None:
"""
Creates an user.
:param email:
:param password:
:param inventories: A set of Inventory where the user has
access to. If none, the user is granted access to the current
inventory.
"""
inventories = inventories or {Inventory.current}
super().__init__(email=email, password=password, inventories=inventories)
def __repr__(self) -> str:
return '<User {0.email}>'.format(self)

View File

@ -2,9 +2,11 @@ from typing import Set, Union
from uuid import UUID
from sqlalchemy import Column
from sqlalchemy.orm import relationship
from sqlalchemy_utils import Password
from ereuse_devicehub.resources.agent.models import Individual
from ereuse_devicehub.resources.inventory import Inventory
from ereuse_devicehub.resources.models import Thing
@ -13,14 +15,17 @@ class User(Thing):
email = ... # type: Column
password = ... # type: Column
token = ... # type: Column
inventories = ... # type: relationship
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
def __init__(self, email: str, password: str = None,
inventories: Set[Inventory] = None) -> None:
super().__init__()
self.id = ... # type: UUID
self.email = ... # type: str
self.password = ... # type: Password
self.individuals = ... # type: Set[Individual]
self.token = ... # type: UUID
self.inventories = ... # type: Set[Inventory]
@property
def individual(self) -> Union[Individual, None]:

View File

@ -5,6 +5,7 @@ from teal.marshmallow import SanitizedStr
from ereuse_devicehub import auth
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.agent.schemas import Individual
from ereuse_devicehub.resources.inventory.schema import Inventory
from ereuse_devicehub.resources.schemas import Thing
@ -17,6 +18,7 @@ class User(Thing):
token = String(dump_only=True,
description='Use this token in an Authorization header to access the app.'
'The token can change overtime.')
inventories = NestedOn(Inventory, many=True, dump_only=True)
def __init__(self,
only=None,

View File

@ -8,7 +8,7 @@ Define appdir /home/devicetag/sites/${servername}/source/
# The path where the app directory is. Apache must have access to this folder.
Define wsgipath ${appdir}/wsgi.wsgi
# The location of the .wsgi file
Define pyvenv ${appdir}/venv/
Define pyvenv ${appdir}../venv/
# The path where the virtual environment is (the folder containing bin/activate)
<VirtualHost *:80>

View File

@ -1,4 +1,3 @@
from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.devicehub import Devicehub
"""
@ -7,10 +6,4 @@ Example app with minimal configuration.
Use this as a starting point.
"""
class MyConfig(DevicehubConfig):
ORGANIZATION_NAME = 'My org'
ORGANIZATION_TAX_ID = 'foo-bar'
app = Devicehub(MyConfig())
app = Devicehub(inventory='db1')

View File

@ -5,7 +5,7 @@ click==6.7
click-spinner==0.1.8
colorama==0.3.9
colour==0.1.5
ereuse-utils==0.4.0b14
ereuse-utils[naming, test, session, cli]==0.4.0b14
Flask==1.0.2
Flask-Cors==3.0.6
Flask-SQLAlchemy==2.3.2
@ -15,7 +15,6 @@ marshmallow==3.0.0b11
marshmallow-enum==1.4.1
passlib==1.7.1
phonenumbers==8.9.11
pySMART.smartx==0.3.9
pytest==3.7.2
pytest-runner==4.2
python-dateutil==2.7.3
@ -25,9 +24,10 @@ requests==2.19.1
requests-mock==1.5.2
SQLAlchemy==1.2.14
SQLAlchemy-Utils==0.33.6
teal==0.2.0a34
teal==0.2.0a35
webargs==4.0.0
Werkzeug==0.14.1
sqlalchemy-citext==1.3.post0
flask-weasyprint==0.5
weasyprint==44
psycopg2-binary==2.7.5

View File

@ -29,10 +29,10 @@ setup(
long_description=long_description,
long_description_content_type='text/markdown',
install_requires=[
'teal>=0.2.0a34', # teal always first
'teal>=0.2.0a35', # teal always first
'click',
'click-spinner',
'ereuse-utils[Naming]>=0.4b14',
'ereuse-utils[naming, test, session, cli]>=0.4b14',
'hashids',
'marshmallow_enum',
'psycopg2-binary',
@ -57,6 +57,11 @@ setup(
'test': test_requires
},
tests_require=test_requires,
entry_points={
'console_scripts': [
'dh = ereuse_devicehub.cli:cli'
]
},
setup_requires=[
'pytest-runner'
],

View File

@ -1,8 +1,10 @@
import io
import uuid
from contextlib import redirect_stdout
from datetime import datetime
from pathlib import Path
import boltons.urlutils
import pytest
import yaml
from psycopg2 import IntegrityError
@ -26,13 +28,8 @@ T = {'start_time': STARTT, 'end_time': ENDT}
class TestConfig(DevicehubConfig):
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/dh_test'
SCHEMA = 'test'
TESTING = True
ORGANIZATION_NAME = 'FooOrg'
ORGANIZATION_TAX_ID = 'foo-org-id'
SERVER_NAME = 'localhost'
TAG_BASE_URL = 'https://example.com'
TAG_TOKEN = 'tagToken'
@pytest.fixture(scope='session')
@ -42,7 +39,7 @@ def config():
@pytest.fixture(scope='session')
def _app(config: TestConfig) -> Devicehub:
return Devicehub(config=config, db=db)
return Devicehub(inventory='test', config=config, db=db)
@pytest.fixture()
@ -52,14 +49,23 @@ def app(request, _app: Devicehub) -> Devicehub:
with _app.app_context():
db.drop_all()
def _init():
_app.init_db(name='Test Inventory',
org_name='FooOrg',
org_id='foo-org-id',
tag_url=boltons.urlutils.URL('https://example.com'),
tag_token=uuid.UUID('52dacef0-6bcb-4919-bfed-f10d2c96ecee'),
erase=False,
common=True)
with _app.app_context():
try:
with redirect_stdout(io.StringIO()):
_app.init_db()
_init()
except (ProgrammingError, IntegrityError):
print('Database was not correctly emptied. Re-empty and re-installing...')
_drop()
_app.init_db()
_init()
request.addfinalizer(_drop)
return _app

View File

@ -107,8 +107,7 @@ def test_default_org_exists(config: DevicehubConfig):
initialization and that is accessible for the method
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
"""
assert models.Organization.query.filter_by(name=config.ORGANIZATION_NAME,
tax_id=config.ORGANIZATION_TAX_ID).one()
assert models.Organization.query.filter_by(name='FooOrg', tax_id='foo-org-id').one()
assert isinstance(models.Organization.get_default_org_id(), UUID)

View File

@ -42,4 +42,4 @@ def test_api_docs(client: Client):
'scheme': 'basic',
'name': 'Authorization'
}
assert 95 == len(docs['definitions'])
assert len(docs['definitions']) == 96

48
tests/test_dispatcher.py Normal file
View File

@ -0,0 +1,48 @@
from unittest.mock import Mock
import pytest
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.dispatchers import PathDispatcher
from tests.conftest import TestConfig
def noop():
pass
@pytest.fixture()
def dispatcher(app: Devicehub, config: TestConfig) -> PathDispatcher:
print('whoho')
PathDispatcher.call = Mock(side_effect=lambda *args: args[0])
return PathDispatcher(config_cls=config)
def test_dispatcher_default(dispatcher: PathDispatcher):
"""The dispatcher returns not found for an URL that does not
route to an app.
"""
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/'}, noop)
assert app == PathDispatcher.NOT_FOUND
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/foo/foo'}, noop)
assert app == PathDispatcher.NOT_FOUND
def test_dispatcher_return_app(dispatcher: PathDispatcher):
"""The dispatcher returns the correct app for the URL"""
# Note that the dispatcher does not check if the URL points
# to a well-known endpoint for the app.
# Only if can route it to an app. And then the app checks
# if the path exists
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/test/foo/'}, noop)
assert isinstance(app, Devicehub)
assert app.id == 'test'
def test_dispatcher_users(dispatcher: PathDispatcher):
"""Users special endpoint returns an app"""
# For now returns the first app, as all apps
# can answer {}/users/login
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/users/'}, noop)
assert isinstance(app, Devicehub)
assert app.id == 'test'

16
tests/test_inventory.py Normal file
View File

@ -0,0 +1,16 @@
import pytest
@pytest.mark.xfail(reason='Test not developed')
def test_create_inventory():
"""Tests creating an inventory with an user."""
@pytest.mark.xfail(reason='Test not developed')
def test_create_existing_inventory():
pass
@pytest.mark.xfail(reason='Test not developed')
def test_delete_inventory():
pass

View File

@ -241,7 +241,9 @@ def test_crate_num_regular_tags(user: UserClient, requests_mock: requests_mock.m
"""
requests_mock.post('https://example.com/',
# request
request_headers={'Authorization': 'Basic tagToken'},
request_headers={
'Authorization': 'Basic 52dacef0-6bcb-4919-bfed-f10d2c96ecee'
},
# response
json=['tag1id', 'tag2id'],
status_code=201)

View File

@ -82,6 +82,7 @@ def test_login_success(client: Client, app: Devicehub):
assert user['individuals'][0]['name'] == 'Timmy'
assert user['individuals'][0]['type'] == 'Person'
assert len(user['individuals']) == 1
assert user['inventories'][0]['id'] == 'test'
def test_login_failure(client: Client, app: Devicehub):
@ -99,3 +100,8 @@ def test_login_failure(client: Client, app: Devicehub):
client.post({'email': 'this is not an email', 'password': 'nope'},
uri='/users/login/',
status=ValidationError)
@pytest.mark.xfail(reason='Test not developed')
def test_user_at_least_one_inventory():
pass