Add inventories with dispatcher
This commit is contained in:
parent
79feb33aa3
commit
f570e9d3d0
28
ereuse_devicehub/cli.py
Normal file
28
ereuse_devicehub/cli.py
Normal 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
|
|
@ -2,13 +2,12 @@ from distutils.version import StrictVersion
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
import boltons.urlutils
|
|
||||||
from teal.auth import TokenAuth
|
from teal.auth import TokenAuth
|
||||||
from teal.config import Config
|
from teal.config import Config
|
||||||
from teal.enums import Currency
|
from teal.enums import Currency
|
||||||
from teal.utils import import_resource
|
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.device import definitions
|
||||||
from ereuse_devicehub.resources.documents import documents
|
from ereuse_devicehub.resources.documents import documents
|
||||||
from ereuse_devicehub.resources.enums import PriceSoftware, RatingSoftware
|
from ereuse_devicehub.resources.enums import PriceSoftware, RatingSoftware
|
||||||
|
@ -21,23 +20,16 @@ class DevicehubConfig(Config):
|
||||||
import_resource(tag),
|
import_resource(tag),
|
||||||
import_resource(agent),
|
import_resource(agent),
|
||||||
import_resource(lot),
|
import_resource(lot),
|
||||||
import_resource(documents))
|
import_resource(documents),
|
||||||
|
import_resource(inventory)),
|
||||||
)
|
)
|
||||||
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
|
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
|
||||||
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/devicehub' # type: str
|
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/devicehub' # type: str
|
||||||
SCHEMA = 'dhub'
|
|
||||||
MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion
|
MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion
|
||||||
"""
|
"""
|
||||||
the minimum version of ereuse.org workbench that this devicehub
|
the minimum version of ereuse.org workbench that this devicehub
|
||||||
accepts. we recommend not changing this value.
|
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_TITLE = 'Devicehub'
|
||||||
API_DOC_CONFIG_VERSION = '0.2'
|
API_DOC_CONFIG_VERSION = '0.2'
|
||||||
API_DOC_CONFIG_COMPONENTS = {
|
API_DOC_CONFIG_COMPONENTS = {
|
||||||
|
@ -60,19 +52,3 @@ class DevicehubConfig(Config):
|
||||||
"""
|
"""
|
||||||
Official versions
|
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__()
|
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
|
import uuid
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
import boltons.urlutils
|
||||||
|
import click
|
||||||
|
import click_spinner
|
||||||
|
import ereuse_utils.cli
|
||||||
from ereuse_utils.session import DevicehubClient
|
from ereuse_utils.session import DevicehubClient
|
||||||
|
from flask.globals import _app_ctx_stack, g
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
from teal.config import Config as ConfigClass
|
|
||||||
from teal.teal import Teal
|
from teal.teal import Teal
|
||||||
|
|
||||||
from ereuse_devicehub.auth import Auth
|
from ereuse_devicehub.auth import Auth
|
||||||
from ereuse_devicehub.client import Client
|
from ereuse_devicehub.client import Client
|
||||||
|
from ereuse_devicehub.config import DevicehubConfig
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.dummy.dummy import Dummy
|
from ereuse_devicehub.dummy.dummy import Dummy
|
||||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||||
|
from ereuse_devicehub.resources.inventory import Inventory, InventoryDef
|
||||||
|
|
||||||
|
|
||||||
class Devicehub(Teal):
|
class Devicehub(Teal):
|
||||||
|
@ -18,7 +25,8 @@ class Devicehub(Teal):
|
||||||
Dummy = Dummy
|
Dummy = Dummy
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
config: ConfigClass,
|
inventory: str,
|
||||||
|
config: DevicehubConfig = DevicehubConfig(),
|
||||||
db: SQLAlchemy = db,
|
db: SQLAlchemy = db,
|
||||||
import_name=__name__.split('.')[0],
|
import_name=__name__.split('.')[0],
|
||||||
static_url_path=None,
|
static_url_path=None,
|
||||||
|
@ -31,27 +39,76 @@ class Devicehub(Teal):
|
||||||
instance_relative_config=False,
|
instance_relative_config=False,
|
||||||
root_path=None,
|
root_path=None,
|
||||||
Auth: Type[Auth] = Auth):
|
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,
|
host_matching, subdomain_matching, template_folder, instance_path,
|
||||||
instance_relative_config, root_path, Auth)
|
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.dummy = Dummy(self)
|
||||||
self.before_request(self.register_db_events_listeners)
|
self.before_request(self.register_db_events_listeners)
|
||||||
self.cli.command('regenerate-search')(self.regenerate_search)
|
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):
|
def register_db_events_listeners(self):
|
||||||
"""Registers the SQLAlchemy event listeners."""
|
"""Registers the SQLAlchemy event listeners."""
|
||||||
# todo can I make it with a global Session only?
|
# todo can I make it with a global Session only?
|
||||||
event.listen(db.session, 'before_commit', DeviceSearch.update_modified_devices)
|
event.listen(db.session, 'before_commit', DeviceSearch.update_modified_devices)
|
||||||
|
|
||||||
def _init_db(self, exclude_schema=None, check=False):
|
# noinspection PyMethodOverriding
|
||||||
created = super()._init_db(exclude_schema, check)
|
@click.option('--name', '-n',
|
||||||
if created:
|
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)
|
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):
|
def regenerate_search(self):
|
||||||
"""Re-creates from 0 all the search tables."""
|
"""Re-creates from 0 all the search tables."""
|
||||||
DeviceSearch.regenerate_search_table(self.db.session)
|
DeviceSearch.regenerate_search_table(self.db.session)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print('Done.')
|
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)
|
||||||
|
|
46
ereuse_devicehub/dispatchers.py
Normal file
46
ereuse_devicehub/dispatchers.py
Normal 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)
|
|
@ -5,6 +5,7 @@ from typing import Set
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import click_spinner
|
import click_spinner
|
||||||
|
import ereuse_utils.cli
|
||||||
import yaml
|
import yaml
|
||||||
from ereuse_utils.test import ANY
|
from ereuse_utils.test import ANY
|
||||||
|
|
||||||
|
@ -41,11 +42,25 @@ class Dummy:
|
||||||
self.app = app
|
self.app = app
|
||||||
self.app.cli.command('dummy', short_help='Creates dummy devices and users.')(self.run)
|
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.'
|
@click.confirmation_option(prompt='This command (re)creates the DB from scratch.'
|
||||||
'Do you want to continue?')
|
'Do you want to continue?')
|
||||||
def run(self):
|
def run(self, tag_url, tag_token):
|
||||||
runner = self.app.test_cli_runner()
|
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='')
|
print('Creating stuff...'.ljust(30), end='')
|
||||||
with click_spinner.spinner():
|
with click_spinner.spinner():
|
||||||
out = runner.invoke(args=['create-org', *self.ORG], catch_exceptions=False).output
|
out = runner.invoke(args=['create-org', *self.ORG], catch_exceptions=False).output
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from flask import current_app as app
|
|
||||||
from teal.db import SQLAlchemy
|
|
||||||
from teal.resource import Converters, Resource
|
from teal.resource import Converters, Resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
@ -46,11 +44,6 @@ class OrganizationDef(AgentDef):
|
||||||
print(json.dumps(o, indent=2))
|
print(json.dumps(o, indent=2))
|
||||||
return o
|
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):
|
class Membership(Resource):
|
||||||
SCHEMA = schemas.Membership
|
SCHEMA = schemas.Membership
|
||||||
|
|
|
@ -3,17 +3,17 @@ from operator import attrgetter
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from flask import current_app as app, g
|
|
||||||
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
|
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.orm import backref, relationship, validates
|
from sqlalchemy.orm import backref, relationship, validates
|
||||||
from sqlalchemy_utils import EmailType, PhoneNumberType
|
from sqlalchemy_utils import EmailType, PhoneNumberType
|
||||||
from teal import enums
|
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 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.models import STR_SM_SIZE, Thing
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ class Agent(Thing):
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
||||||
|
UniqueConstraint(tax_id, name, name='One tax ID with one name.')
|
||||||
)
|
)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
|
@ -80,21 +81,18 @@ class Agent(Thing):
|
||||||
|
|
||||||
|
|
||||||
class Organization(JoinedTableMixin, Agent):
|
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:
|
def __init__(self, name: str, **kwargs) -> None:
|
||||||
super().__init__(**kwargs, name=name)
|
super().__init__(**kwargs, name=name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_default_org_id(cls) -> UUID:
|
def get_default_org_id(cls) -> UUID:
|
||||||
"""Retrieves the default organization."""
|
"""Retrieves the default organization."""
|
||||||
try:
|
return cls.query.filter_by(default_of=Inventory.current).one().id
|
||||||
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?')
|
|
||||||
|
|
||||||
|
|
||||||
class Individual(JoinedTableMixin, Agent):
|
class Individual(JoinedTableMixin, Agent):
|
||||||
|
|
|
@ -21,6 +21,7 @@ class Agent(Thing):
|
||||||
|
|
||||||
class Organization(Agent):
|
class Organization(Agent):
|
||||||
members = NestedOn('Membership')
|
members = NestedOn('Membership')
|
||||||
|
default_of = NestedOn('Inventory')
|
||||||
|
|
||||||
|
|
||||||
class Membership(Thing):
|
class Membership(Thing):
|
||||||
|
|
|
@ -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
|
|
@ -1,3 +1,6 @@
|
||||||
|
from boltons.typeutils import classproperty
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.models import Thing
|
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."""
|
id.comment = """The name of the inventory as in the URL and schema."""
|
||||||
name = db.Column(db.CIText(), nullable=False, unique=True)
|
name = db.Column(db.CIText(), nullable=False, unique=True)
|
||||||
name.comment = """The human name of the inventory."""
|
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."""
|
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()
|
||||||
|
|
11
ereuse_devicehub/resources/inventory/schema.py
Normal file
11
ereuse_devicehub/resources/inventory/schema.py
Normal 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')
|
|
@ -1,5 +1,4 @@
|
||||||
from ereuse_utils.session import DevicehubClient
|
from flask import Response, current_app as app, g, jsonify, redirect, request
|
||||||
from flask import Response, current_app, current_app as app, jsonify, redirect, request
|
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
from teal.resource import View, url_for_resource
|
from teal.resource import View, url_for_resource
|
||||||
|
|
||||||
|
@ -19,9 +18,8 @@ class TagView(View):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _create_many_regular_tags(self, num: int):
|
def _create_many_regular_tags(self, num: int):
|
||||||
tag_provider = current_app.tag_provider # type: DevicehubClient
|
tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)])
|
||||||
tags_id, _ = tag_provider.post('/', {}, query=[('num', num)])
|
tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id]
|
||||||
tags = [Tag(id=tag_id, provider=current_app.config['TAG_BASE_URL']) for tag_id in tags_id]
|
|
||||||
db.session.add_all(tags)
|
db.session.add_all(tags)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
response = jsonify(items=self.schema.dump(tags, many=True, nested=1)) # type: Response
|
response = jsonify(items=self.schema.dump(tags, many=True, nested=1)) # type: Response
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
from click import argument, option
|
from click import argument, option
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from teal.resource import Converters, Resource
|
from teal.resource import Converters, Resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
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 import schemas
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
from ereuse_devicehub.resources.user.views import UserView, login
|
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'})
|
self.add_url_rule('/login/', view_func=login, methods={'POST'})
|
||||||
|
|
||||||
@argument('email')
|
@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('-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('-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', '--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('-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)
|
@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,
|
def create_user(self, email: str,
|
||||||
telephone: str = None, tax_id: str = None) -> dict:
|
password: str,
|
||||||
|
inventory: Iterable[str] = tuple(),
|
||||||
|
agent: str = None,
|
||||||
|
country: str = None,
|
||||||
|
telephone: str = None,
|
||||||
|
tax_id: str = None) -> dict:
|
||||||
"""Creates an user.
|
"""Creates an user.
|
||||||
|
|
||||||
If ``--agent`` is passed, it creates an ``Individual`` agent
|
If ``--agent`` is passed, it creates an ``Individual`` agent
|
||||||
|
@ -38,7 +49,9 @@ class UserDef(Resource):
|
||||||
from ereuse_devicehub.resources.agent.models import Individual
|
from ereuse_devicehub.resources.agent.models import Individual
|
||||||
u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
|
u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
|
||||||
.load({'email': email, 'password': password})
|
.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(
|
agent = Individual(**current_app.resources[Individual.t].schema.load(
|
||||||
dict(name=agent, email=email, country=country, telephone=telephone, taxId=tax_id)
|
dict(name=agent, email=email, country=country, telephone=telephone, taxId=tax_id)
|
||||||
))
|
))
|
||||||
|
|
|
@ -19,16 +19,24 @@ class User(Thing):
|
||||||
schemes=app.config['PASSWORD_SCHEMES'],
|
schemes=app.config['PASSWORD_SCHEMES'],
|
||||||
**kwargs
|
**kwargs
|
||||||
)))
|
)))
|
||||||
"""
|
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
||||||
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)
|
|
||||||
inventories = db.relationship(Inventory,
|
inventories = db.relationship(Inventory,
|
||||||
backref=db.backref('users', lazy=True, collection_class=set),
|
backref=db.backref('users', lazy=True, collection_class=set),
|
||||||
secondary=lambda: UserInventory.__table__,
|
secondary=lambda: UserInventory.__table__,
|
||||||
collection_class=set)
|
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:
|
def __repr__(self) -> str:
|
||||||
return '<User {0.email}>'.format(self)
|
return '<User {0.email}>'.format(self)
|
||||||
|
|
|
@ -2,9 +2,11 @@ from typing import Set, Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import Column
|
from sqlalchemy import Column
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy_utils import Password
|
from sqlalchemy_utils import Password
|
||||||
|
|
||||||
from ereuse_devicehub.resources.agent.models import Individual
|
from ereuse_devicehub.resources.agent.models import Individual
|
||||||
|
from ereuse_devicehub.resources.inventory import Inventory
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,14 +15,17 @@ class User(Thing):
|
||||||
email = ... # type: Column
|
email = ... # type: Column
|
||||||
password = ... # type: Column
|
password = ... # type: Column
|
||||||
token = ... # type: Column
|
token = ... # type: Column
|
||||||
|
inventories = ... # type: relationship
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, email: str, password: str = None,
|
||||||
super().__init__(**kwargs)
|
inventories: Set[Inventory] = None) -> None:
|
||||||
|
super().__init__()
|
||||||
self.id = ... # type: UUID
|
self.id = ... # type: UUID
|
||||||
self.email = ... # type: str
|
self.email = ... # type: str
|
||||||
self.password = ... # type: Password
|
self.password = ... # type: Password
|
||||||
self.individuals = ... # type: Set[Individual]
|
self.individuals = ... # type: Set[Individual]
|
||||||
self.token = ... # type: UUID
|
self.token = ... # type: UUID
|
||||||
|
self.inventories = ... # type: Set[Inventory]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def individual(self) -> Union[Individual, None]:
|
def individual(self) -> Union[Individual, None]:
|
||||||
|
|
|
@ -5,6 +5,7 @@ from teal.marshmallow import SanitizedStr
|
||||||
from ereuse_devicehub import auth
|
from ereuse_devicehub import auth
|
||||||
from ereuse_devicehub.marshmallow import NestedOn
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
from ereuse_devicehub.resources.agent.schemas import Individual
|
from ereuse_devicehub.resources.agent.schemas import Individual
|
||||||
|
from ereuse_devicehub.resources.inventory.schema import Inventory
|
||||||
from ereuse_devicehub.resources.schemas import Thing
|
from ereuse_devicehub.resources.schemas import Thing
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ class User(Thing):
|
||||||
token = String(dump_only=True,
|
token = String(dump_only=True,
|
||||||
description='Use this token in an Authorization header to access the app.'
|
description='Use this token in an Authorization header to access the app.'
|
||||||
'The token can change overtime.')
|
'The token can change overtime.')
|
||||||
|
inventories = NestedOn(Inventory, many=True, dump_only=True)
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
only=None,
|
only=None,
|
||||||
|
|
|
@ -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.
|
# The path where the app directory is. Apache must have access to this folder.
|
||||||
Define wsgipath ${appdir}/wsgi.wsgi
|
Define wsgipath ${appdir}/wsgi.wsgi
|
||||||
# The location of the .wsgi file
|
# 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)
|
# The path where the virtual environment is (the folder containing bin/activate)
|
||||||
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from ereuse_devicehub.config import DevicehubConfig
|
|
||||||
from ereuse_devicehub.devicehub import Devicehub
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -7,10 +6,4 @@ Example app with minimal configuration.
|
||||||
Use this as a starting point.
|
Use this as a starting point.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
app = Devicehub(inventory='db1')
|
||||||
class MyConfig(DevicehubConfig):
|
|
||||||
ORGANIZATION_NAME = 'My org'
|
|
||||||
ORGANIZATION_TAX_ID = 'foo-bar'
|
|
||||||
|
|
||||||
|
|
||||||
app = Devicehub(MyConfig())
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ click==6.7
|
||||||
click-spinner==0.1.8
|
click-spinner==0.1.8
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
colour==0.1.5
|
colour==0.1.5
|
||||||
ereuse-utils==0.4.0b14
|
ereuse-utils[naming, test, session, cli]==0.4.0b14
|
||||||
Flask==1.0.2
|
Flask==1.0.2
|
||||||
Flask-Cors==3.0.6
|
Flask-Cors==3.0.6
|
||||||
Flask-SQLAlchemy==2.3.2
|
Flask-SQLAlchemy==2.3.2
|
||||||
|
@ -15,7 +15,6 @@ marshmallow==3.0.0b11
|
||||||
marshmallow-enum==1.4.1
|
marshmallow-enum==1.4.1
|
||||||
passlib==1.7.1
|
passlib==1.7.1
|
||||||
phonenumbers==8.9.11
|
phonenumbers==8.9.11
|
||||||
pySMART.smartx==0.3.9
|
|
||||||
pytest==3.7.2
|
pytest==3.7.2
|
||||||
pytest-runner==4.2
|
pytest-runner==4.2
|
||||||
python-dateutil==2.7.3
|
python-dateutil==2.7.3
|
||||||
|
@ -25,9 +24,10 @@ requests==2.19.1
|
||||||
requests-mock==1.5.2
|
requests-mock==1.5.2
|
||||||
SQLAlchemy==1.2.14
|
SQLAlchemy==1.2.14
|
||||||
SQLAlchemy-Utils==0.33.6
|
SQLAlchemy-Utils==0.33.6
|
||||||
teal==0.2.0a34
|
teal==0.2.0a35
|
||||||
webargs==4.0.0
|
webargs==4.0.0
|
||||||
Werkzeug==0.14.1
|
Werkzeug==0.14.1
|
||||||
sqlalchemy-citext==1.3.post0
|
sqlalchemy-citext==1.3.post0
|
||||||
flask-weasyprint==0.5
|
flask-weasyprint==0.5
|
||||||
weasyprint==44
|
weasyprint==44
|
||||||
|
psycopg2-binary==2.7.5
|
||||||
|
|
9
setup.py
9
setup.py
|
@ -29,10 +29,10 @@ setup(
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'teal>=0.2.0a34', # teal always first
|
'teal>=0.2.0a35', # teal always first
|
||||||
'click',
|
'click',
|
||||||
'click-spinner',
|
'click-spinner',
|
||||||
'ereuse-utils[Naming]>=0.4b14',
|
'ereuse-utils[naming, test, session, cli]>=0.4b14',
|
||||||
'hashids',
|
'hashids',
|
||||||
'marshmallow_enum',
|
'marshmallow_enum',
|
||||||
'psycopg2-binary',
|
'psycopg2-binary',
|
||||||
|
@ -57,6 +57,11 @@ setup(
|
||||||
'test': test_requires
|
'test': test_requires
|
||||||
},
|
},
|
||||||
tests_require=test_requires,
|
tests_require=test_requires,
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'dh = ereuse_devicehub.cli:cli'
|
||||||
|
]
|
||||||
|
},
|
||||||
setup_requires=[
|
setup_requires=[
|
||||||
'pytest-runner'
|
'pytest-runner'
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import io
|
import io
|
||||||
|
import uuid
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import boltons.urlutils
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
from psycopg2 import IntegrityError
|
from psycopg2 import IntegrityError
|
||||||
|
@ -26,13 +28,8 @@ T = {'start_time': STARTT, 'end_time': ENDT}
|
||||||
|
|
||||||
class TestConfig(DevicehubConfig):
|
class TestConfig(DevicehubConfig):
|
||||||
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/dh_test'
|
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/dh_test'
|
||||||
SCHEMA = 'test'
|
|
||||||
TESTING = True
|
TESTING = True
|
||||||
ORGANIZATION_NAME = 'FooOrg'
|
|
||||||
ORGANIZATION_TAX_ID = 'foo-org-id'
|
|
||||||
SERVER_NAME = 'localhost'
|
SERVER_NAME = 'localhost'
|
||||||
TAG_BASE_URL = 'https://example.com'
|
|
||||||
TAG_TOKEN = 'tagToken'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
|
@ -42,7 +39,7 @@ def config():
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def _app(config: TestConfig) -> Devicehub:
|
def _app(config: TestConfig) -> Devicehub:
|
||||||
return Devicehub(config=config, db=db)
|
return Devicehub(inventory='test', config=config, db=db)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
@ -52,14 +49,23 @@ def app(request, _app: Devicehub) -> Devicehub:
|
||||||
with _app.app_context():
|
with _app.app_context():
|
||||||
db.drop_all()
|
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():
|
with _app.app_context():
|
||||||
try:
|
try:
|
||||||
with redirect_stdout(io.StringIO()):
|
with redirect_stdout(io.StringIO()):
|
||||||
_app.init_db()
|
_init()
|
||||||
except (ProgrammingError, IntegrityError):
|
except (ProgrammingError, IntegrityError):
|
||||||
print('Database was not correctly emptied. Re-empty and re-installing...')
|
print('Database was not correctly emptied. Re-empty and re-installing...')
|
||||||
_drop()
|
_drop()
|
||||||
_app.init_db()
|
_init()
|
||||||
|
|
||||||
request.addfinalizer(_drop)
|
request.addfinalizer(_drop)
|
||||||
return _app
|
return _app
|
||||||
|
|
|
@ -107,8 +107,7 @@ def test_default_org_exists(config: DevicehubConfig):
|
||||||
initialization and that is accessible for the method
|
initialization and that is accessible for the method
|
||||||
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
|
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
|
||||||
"""
|
"""
|
||||||
assert models.Organization.query.filter_by(name=config.ORGANIZATION_NAME,
|
assert models.Organization.query.filter_by(name='FooOrg', tax_id='foo-org-id').one()
|
||||||
tax_id=config.ORGANIZATION_TAX_ID).one()
|
|
||||||
assert isinstance(models.Organization.get_default_org_id(), UUID)
|
assert isinstance(models.Organization.get_default_org_id(), UUID)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -42,4 +42,4 @@ def test_api_docs(client: Client):
|
||||||
'scheme': 'basic',
|
'scheme': 'basic',
|
||||||
'name': 'Authorization'
|
'name': 'Authorization'
|
||||||
}
|
}
|
||||||
assert 95 == len(docs['definitions'])
|
assert len(docs['definitions']) == 96
|
||||||
|
|
48
tests/test_dispatcher.py
Normal file
48
tests/test_dispatcher.py
Normal 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
16
tests/test_inventory.py
Normal 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
|
|
@ -241,7 +241,9 @@ def test_crate_num_regular_tags(user: UserClient, requests_mock: requests_mock.m
|
||||||
"""
|
"""
|
||||||
requests_mock.post('https://example.com/',
|
requests_mock.post('https://example.com/',
|
||||||
# request
|
# request
|
||||||
request_headers={'Authorization': 'Basic tagToken'},
|
request_headers={
|
||||||
|
'Authorization': 'Basic 52dacef0-6bcb-4919-bfed-f10d2c96ecee'
|
||||||
|
},
|
||||||
# response
|
# response
|
||||||
json=['tag1id', 'tag2id'],
|
json=['tag1id', 'tag2id'],
|
||||||
status_code=201)
|
status_code=201)
|
||||||
|
|
|
@ -82,6 +82,7 @@ def test_login_success(client: Client, app: Devicehub):
|
||||||
assert user['individuals'][0]['name'] == 'Timmy'
|
assert user['individuals'][0]['name'] == 'Timmy'
|
||||||
assert user['individuals'][0]['type'] == 'Person'
|
assert user['individuals'][0]['type'] == 'Person'
|
||||||
assert len(user['individuals']) == 1
|
assert len(user['individuals']) == 1
|
||||||
|
assert user['inventories'][0]['id'] == 'test'
|
||||||
|
|
||||||
|
|
||||||
def test_login_failure(client: Client, app: Devicehub):
|
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'},
|
client.post({'email': 'this is not an email', 'password': 'nope'},
|
||||||
uri='/users/login/',
|
uri='/users/login/',
|
||||||
status=ValidationError)
|
status=ValidationError)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason='Test not developed')
|
||||||
|
def test_user_at_least_one_inventory():
|
||||||
|
pass
|
||||||
|
|
Reference in a new issue