diff --git a/CHANGELOG.md b/CHANGELOG.md index ec1dff26..771b1d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,34 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ml). ## testing + +## [2.5.2] - 2023-04-20 - [added] #414 add new vars in the settings file for wb. +- [added] #440 add lots in export devices. +- [added] #441 allow remove documents. +- [added] #442 allow edit documents. +- [added] #443 add documents to devices. +- [added] #444 add new columns in list of documents. +- [changed] #439 move teal as internal module. +- [fixed] #437 replace names erasure by sanitization in templates. + +## [2.5.1] - 2023-03-17 +- [changed] #423 new hid. +- [changed] #426 new version of public page of device. +- [changed] #427 update links of terms and condotions. +- [changed] #428 only the data storage allow syncrinize, the rest are duplicate. +- [changed] #430 new version of erasure certificate. +- [fixed] #416 fix dhid in snapshot logs. +- [fixed] #419 fix settings version and template. +- [fixed] #420 not appear all lots in the dropdown menu for select the a lot. +- [fixed] #421 fix remove a placeholder from one old trade lot. +- [fixed] #422 fix simple datatables. +- [fixed] #424 fix new hid. +- [fixed] #431 fix forms for customer details. +- [fixed] #432 fix erasure certificate for a servers. +- [fixed] #433 fix get the last incoming for show customer datas in certificate. +- [fixed] #434 fix reopen transfer. +- [fixed] #436 fix hid in erasure certificate. ## [2.5.0] - 2022-11-30 - [added] #407 erasure section with tabs in top. diff --git a/ereuse_devicehub/__init__.py b/ereuse_devicehub/__init__.py index 50062f87..667b52f9 100644 --- a/ereuse_devicehub/__init__.py +++ b/ereuse_devicehub/__init__.py @@ -1 +1 @@ -__version__ = "2.5.0" +__version__ = "2.5.2" diff --git a/ereuse_devicehub/auth.py b/ereuse_devicehub/auth.py index 19f6e5fa..f6f5477e 100644 --- a/ereuse_devicehub/auth.py +++ b/ereuse_devicehub/auth.py @@ -1,9 +1,9 @@ from sqlalchemy.exc import DataError -from teal.auth import TokenAuth -from teal.db import ResourceNotFound from werkzeug.exceptions import Unauthorized -from ereuse_devicehub.resources.user.models import User, Session +from ereuse_devicehub.resources.user.models import Session, User +from ereuse_devicehub.teal.auth import TokenAuth +from ereuse_devicehub.teal.db import ResourceNotFound class Auth(TokenAuth): diff --git a/ereuse_devicehub/cli.py b/ereuse_devicehub/cli.py index 67054095..e2cec81c 100644 --- a/ereuse_devicehub/cli.py +++ b/ereuse_devicehub/cli.py @@ -2,21 +2,23 @@ import os import click.testing import flask.cli -import ereuse_utils +import ereuse_devicehub.ereuse_utils from ereuse_devicehub.config import DevicehubConfig from ereuse_devicehub.devicehub import Devicehub import sys + sys.ps1 = '\001\033[92m\002>>> \001\033[0m\002' -sys.ps2= '\001\033[94m\002... \001\033[0m\002' +sys.ps2 = '\001\033[94m\002... \001\033[0m\002' import os, readline, atexit + history_file = os.path.join(os.environ['HOME'], '.python_history') try: - readline.read_history_file(history_file) + readline.read_history_file(history_file) except IOError: - pass + pass readline.parse_and_bind("tab: complete") readline.parse_and_bind('"\e[5~": history-search-backward') readline.parse_and_bind('"\e[6~": history-search-forward') @@ -29,6 +31,7 @@ readline.parse_and_bind('"\e[1;5D": backward-word') readline.set_history_length(100000) atexit.register(readline.write_history_file, history_file) + class DevicehubGroup(flask.cli.FlaskGroup): # todo users cannot make cli to use a custom db this way! CONFIG = DevicehubConfig @@ -49,26 +52,37 @@ class DevicehubGroup(flask.cli.FlaskGroup): def get_version(ctx, param, value): if not value or ctx.resilient_parsing: return - click.echo('Devicehub {}'.format(ereuse_utils.version('ereuse-devicehub')), color=ctx.color) + click.echo( + 'Devicehub {}'.format( + ereuse_devicehub.ereuse_utils.version('ereuse-devicehub') + ), + color=ctx.color, + ) flask.cli.get_version(ctx, param, value) -@click.option('--version', - help='Devicehub version.', - expose_value=False, - callback=get_version, - is_flag=True, - is_eager=True) -@click.group(cls=DevicehubGroup, - context_settings=Devicehub.cli_context_settings, - add_version_option=False, - help="""Manages the Devicehub of the inventory {}. +@click.option( + '--version', + help='Devicehub version.', + expose_value=False, + callback=get_version, + is_flag=True, + is_eager=True, +) +@click.group( + cls=DevicehubGroup, + context_settings=Devicehub.cli_context_settings, + add_version_option=False, + help="""Manages the Devicehub of the inventory {}. Use 'export dhi=xx' to set the inventory that this CLI manages. For example 'export dhi=db1' and then executing 'dh tag add' adds a tag in the db1 database. Operations that affect the common database (like creating an user) are not affected by this. - """.format(os.environ.get('dhi'))) + """.format( + os.environ.get('dhi') + ), +) def cli(): pass diff --git a/ereuse_devicehub/client.py b/ereuse_devicehub/client.py index 92aa67d2..6358aa9e 100644 --- a/ereuse_devicehub/client.py +++ b/ereuse_devicehub/client.py @@ -1,14 +1,14 @@ from inspect import isclass from typing import Dict, Iterable, Type, Union -from ereuse_utils.test import JSON, Res +from ereuse_devicehub.ereuse_utils.test import JSON, Res from flask.testing import FlaskClient from flask_wtf.csrf import generate_csrf -from teal.client import Client as TealClient -from teal.client import Query, Status from werkzeug.exceptions import HTTPException from ereuse_devicehub.resources import models, schemas +from ereuse_devicehub.teal.client import Client as TealClient +from ereuse_devicehub.teal.client import Query, Status ResourceLike = Union[Type[Union[models.Thing, schemas.Thing]], str] diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index 16d2005a..b8dd9a82 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -2,10 +2,6 @@ from distutils.version import StrictVersion from itertools import chain from decouple import config -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 ( action, @@ -24,6 +20,10 @@ from ereuse_devicehub.resources.licences import licences from ereuse_devicehub.resources.metric import definitions as metric_def from ereuse_devicehub.resources.tradedocument import definitions as tradedocument from ereuse_devicehub.resources.versions import versions +from ereuse_devicehub.teal.auth import TokenAuth +from ereuse_devicehub.teal.config import Config +from ereuse_devicehub.teal.enums import Currency +from ereuse_devicehub.teal.utils import import_resource class DevicehubConfig(Config): diff --git a/ereuse_devicehub/db.py b/ereuse_devicehub/db.py index 964de614..16ede4fc 100644 --- a/ereuse_devicehub/db.py +++ b/ereuse_devicehub/db.py @@ -4,7 +4,8 @@ from sqlalchemy.dialects import postgresql from sqlalchemy.orm import sessionmaker from sqlalchemy.sql import expression from sqlalchemy_utils import view -from teal.db import SchemaSQLAlchemy, SchemaSession + +from ereuse_devicehub.teal.db import SchemaSession, SchemaSQLAlchemy class DhSession(SchemaSession): @@ -23,6 +24,7 @@ class DhSession(SchemaSession): # flush, all the new / dirty interesting things in a variable # until DeviceSearch is executed from ereuse_devicehub.resources.device.search import DeviceSearch + DeviceSearch.update_modified_devices(session=self) @@ -31,6 +33,7 @@ class SQLAlchemy(SchemaSQLAlchemy): schema of the database, as it is in the `search_path` defined in teal. """ + # todo add here all types of columns used so we don't have to # manually import them all the time UUID = postgresql.UUID @@ -60,7 +63,9 @@ def create_view(name, selectable): # We need to ensure views are created / destroyed before / after # SchemaSQLAlchemy's listeners execute # That is why insert=True in 'after_create' - event.listen(db.metadata, 'after_create', view.CreateView(name, selectable), insert=True) + event.listen( + db.metadata, 'after_create', view.CreateView(name, selectable), insert=True + ) event.listen(db.metadata, 'before_drop', view.DropView(name)) return table diff --git a/ereuse_devicehub/devicehub.py b/ereuse_devicehub/devicehub.py index 82f5dd4a..e5e76145 100644 --- a/ereuse_devicehub/devicehub.py +++ b/ereuse_devicehub/devicehub.py @@ -5,13 +5,11 @@ from typing import Type import boltons.urlutils import click import click_spinner -import ereuse_utils.cli -from ereuse_utils.session import DevicehubClient +import ereuse_devicehub.ereuse_utils.cli +from ereuse_devicehub.ereuse_utils.session import DevicehubClient from flask import _app_ctx_stack, g from flask_login import LoginManager, current_user from flask_sqlalchemy import SQLAlchemy -from teal.db import ResourceNotFound, SchemaSQLAlchemy -from teal.teal import Teal from ereuse_devicehub.auth import Auth from ereuse_devicehub.client import Client, UserClient @@ -24,6 +22,8 @@ from ereuse_devicehub.dummy.dummy import Dummy from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.inventory import Inventory, InventoryDef from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.db import ResourceNotFound, SchemaSQLAlchemy +from ereuse_devicehub.teal.teal import Teal from ereuse_devicehub.templating import Environment @@ -122,7 +122,7 @@ class Devicehub(Teal): @click.option( '--tag-url', '-tu', - type=ereuse_utils.cli.URL(scheme=True, host=True, path=False), + type=ereuse_devicehub.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.', ) diff --git a/ereuse_devicehub/dummy/dummy.py b/ereuse_devicehub/dummy/dummy.py index f3533d48..3769bea1 100644 --- a/ereuse_devicehub/dummy/dummy.py +++ b/ereuse_devicehub/dummy/dummy.py @@ -5,10 +5,10 @@ from pathlib import Path import click import click_spinner -import ereuse_utils.cli import jwt import yaml -from ereuse_utils.test import ANY +from ereuse_devicehub.ereuse_utils.test import ANY +from ereuse_devicehub import ereuse_utils from ereuse_devicehub.client import UserClient from ereuse_devicehub.db import db diff --git a/ereuse_devicehub/ereuse_utils/__init__.py b/ereuse_devicehub/ereuse_utils/__init__.py new file mode 100644 index 00000000..0155567f --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/__init__.py @@ -0,0 +1,173 @@ +import enum +import ipaddress +import json +import locale +from collections import Iterable +from datetime import datetime, timedelta +from decimal import Decimal +from distutils.version import StrictVersion +from functools import wraps +from typing import Generator, Union +from uuid import UUID + + +class JSONEncoder(json.JSONEncoder): + """An overloaded JSON Encoder with extra type support.""" + + def default(self, obj): + if isinstance(obj, enum.Enum): + return obj.name + elif isinstance(obj, datetime): + return obj.isoformat() + elif isinstance(obj, timedelta): + return round(obj.total_seconds()) + elif isinstance(obj, UUID): + return str(obj) + elif isinstance(obj, StrictVersion): + return str(obj) + elif isinstance(obj, set): + return list(obj) + elif isinstance(obj, Decimal): + return float(obj) + elif isinstance(obj, Dumpeable): + return obj.dump() + elif isinstance(obj, ipaddress._BaseAddress): + return str(obj) + # Instead of failing, return the string representation by default + return str(obj) + + +class Dumpeable: + """Dumps dictionaries and jsons for Devicehub. + + A base class to allow subclasses to generate dictionaries + and json suitable for sending to a Devicehub, i.e. preventing + private and constants to be in the JSON and camelCases field names. + """ + + ENCODER = JSONEncoder + + def dump(self): + """ + Creates a dictionary consisting of the + non-private fields of this instance with camelCase field names. + """ + import inflection + + return { + inflection.camelize(name, uppercase_first_letter=False): getattr(self, name) + for name in self._field_names() + if not name.startswith('_') and not name[0].isupper() + } + + def _field_names(self): + """An iterable of the names to dump.""" + # Feel free to override this + return vars(self).keys() + + def to_json(self): + """ + Creates a JSON representation of the non-private fields of + this class. + """ + return json.dumps(self, cls=self.ENCODER, indent=2) + + +class DumpeableModel(Dumpeable): + """A dumpeable for SQLAlchemy models. + + Note that this does not avoid recursive relations. + """ + + def _field_names(self): + from sqlalchemy import inspect + + return (a.key for a in inspect(self).attrs) + + +def ensure_utf8(app_name_to_show_on_error: str): + """ + Python3 uses by default the system set, but it expects it to be + ‘utf-8’ to work correctly. + This can generate problems in reading and writing files and in + ``.decode()`` method. + + An example how to 'fix' it:: + + echo 'export LC_CTYPE=en_US.UTF-8' > .bash_profile + echo 'export LC_ALL=en_US.UTF-8' > .bash_profile + """ + encoding = locale.getpreferredencoding() + if encoding.lower() != 'utf-8': + raise OSError( + '{} works only in UTF-8, but yours is set at {}' + ''.format(app_name_to_show_on_error, encoding) + ) + + +def now() -> datetime: + """ + Returns a compatible 'now' with DeviceHub's API, + this is as UTC and without microseconds. + """ + return datetime.utcnow().replace(microsecond=0) + + +def flatten_mixed(values: Iterable) -> Generator: + """ + Flatten a list containing lists and other elements. This is not deep. + + >>> list(flatten_mixed([1, 2, [3, 4]])) + [1, 2, 3, 4] + """ + for x in values: + if isinstance(x, list): + for y in x: + yield y + else: + yield x + + +def if_none_return_none(f): + """If the first value is None return None, otherwise execute f.""" + + @wraps(f) + def wrapper(self, value, *args, **kwargs): + if value is None: + return None + return f(self, value, *args, **kwargs) + + return wrapper + + +def local_ip( + dest='109.69.8.152', +) -> Union[ipaddress.IPv4Address, ipaddress.IPv6Address]: + """Gets the local IP of the interface that has access to the + Internet. + + This is a reliable way to test if a device has an active + connection to the Internet. + + This method works by connecting, by default, + to the IP of ereuse01.ereuse.org. + + >>> local_ip() + + :raise OSError: The device cannot connect to the Internet. + """ + import socket, ipaddress + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect((dest, 80)) + ip = s.getsockname()[0] + s.close() + return ipaddress.ip_address(ip) + + +def version(package_name: str) -> StrictVersion: + """Returns the version of a package name installed with pip.""" + # From https://stackoverflow.com/a/2073599 + import pkg_resources + + return StrictVersion(pkg_resources.require(package_name)[0].version) diff --git a/ereuse_devicehub/ereuse_utils/cli.py b/ereuse_devicehub/ereuse_utils/cli.py new file mode 100644 index 00000000..a86eb4be --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/cli.py @@ -0,0 +1,301 @@ +import enum as _enum +import getpass +import itertools +import os +import pathlib +import threading +from contextlib import contextmanager +from time import sleep +from typing import Any, Iterable, Type + +from boltons import urlutils +from click import types as click_types +from colorama import Fore +from tqdm import tqdm + +from ereuse_devicehub.ereuse_utils import if_none_return_none + +COMMON_CONTEXT_S = {'help_option_names': ('-h', '--help')} +"""Common Context settings used for our implementations of the +Click cli. +""" + +# Py2/3 compat. Empty conditional to avoid coverage +try: + _unicode = unicode +except NameError: + _unicode = str + + +class Enum(click_types.Choice): + """ + Enum support for click. + + Use it as a collection: @click.option(..., type=cli.Enum(MyEnum)). + Then, this expects you to pass the *name* of a member of the enum. + + From `this github issue `_. + """ + + def __init__(self, enum: Type[_enum.Enum]): + self.__enum = enum + super().__init__(enum.__members__) + + def convert(self, value, param, ctx): + return self.__enum[super().convert(value, param, ctx)] + + +class Path(click_types.Path): + """Like click.Path but returning ``pathlib.Path`` objects.""" + + def convert(self, value, param, ctx): + return pathlib.Path(super().convert(value, param, ctx)) + + +class URL(click_types.StringParamType): + """Returns a bolton's URL.""" + + name = 'url' + + def __init__( + self, + scheme=None, + username=None, + password=None, + host=None, + port=None, + path=None, + query_params=None, + fragment=None, + ) -> None: + super().__init__() + """Creates the type URL. You can require or enforce parts + of the URL by setting parameters of this constructor. + + If the param is... + + - None, no check is performed (default). + - True, it is then required as part of the URL. + - False, it is then required NOT to be part of the URL. + - Any other value, then such value is required to be in + the URL. + """ + self.attrs = ( + ('scheme', scheme), + ('username', username), + ('password', password), + ('host', host), + ('port', port), + ('path', path), + ('query_params', query_params), + ('fragment', fragment), + ) + + @if_none_return_none + def convert(self, value, param, ctx): + url = urlutils.URL(super().convert(value, param, ctx)) + for name, attr in self.attrs: + if attr is True: + if not getattr(url, name): + self.fail( + 'URL {} must contain {} but it does not.'.format(url, name) + ) + elif attr is False: + if getattr(url, name): + self.fail('URL {} cannot contain {} but it does.'.format(url, name)) + elif attr: + if getattr(url, name) != attr: + self.fail('{} form {} can only be {}'.format(name, url, attr)) + return url + + +def password(service: str, username: str, prompt: str = 'Password:') -> str: + """Gets a password from the keyring or the terminal.""" + import keyring + + return keyring.get_password(service, username) or getpass.getpass(prompt) + + +class Line(tqdm): + spinner_cycle = itertools.cycle(['-', '/', '|', '\\']) + + def __init__( + self, + total=None, + desc=None, + leave=True, + file=None, + ncols=None, + mininterval=0.2, + maxinterval=10.0, + miniters=None, + ascii=None, + disable=False, + unit='it', + unit_scale=False, + dynamic_ncols=True, + smoothing=0.3, + bar_format=None, + initial=0, + position=None, + postfix=None, + unit_divisor=1000, + write_bytes=None, + gui=False, + close_message: Iterable = None, + error_message: Iterable = None, + **kwargs, + ): + """This cannot work with iterables. Iterable use is considered + backward-compatibility in tqdm and inconsistent in Line. + Manually call ``update``. + """ + self._close_message = close_message + self._error_message = error_message + if total: + bar_format = '{desc}{percentage:.1f}% |{bar}| {n:1g}/{total:1g} {elapsed}<{remaining}' + super().__init__( + None, + desc, + total, + leave, + file, + ncols, + mininterval, + maxinterval, + miniters, + ascii, + disable, + unit, + unit_scale, + dynamic_ncols, + smoothing, + bar_format, + initial, + position, + postfix, + unit_divisor, + write_bytes, + gui, + **kwargs, + ) + + def write_at_line(self, *args): + self.clear() + with self._lock: + self.display(''.join(str(arg) for arg in args)) + + def close_message(self, *args): + self._close_message = args + + def error_message(self, *args): + self._error_message = args + + def close(self): # noqa: C901 + """ + Cleanup and (if leave=False) close the progressbar. + """ + if self.disable: + return + + # Prevent multiple closures + self.disable = True + + # decrement instance pos and remove from internal set + pos = abs(self.pos) + self._decr_instances(self) + + # GUI mode + if not hasattr(self, "sp"): + return + + # annoyingly, _supports_unicode isn't good enough + def fp_write(s): + self.fp.write(_unicode(s)) + + try: + fp_write('') + except ValueError as e: + if 'closed' in str(e): + return + raise # pragma: no cover + + with self._lock: + if self.leave: + if self._close_message: + self.display( + ''.join(str(arg) for arg in self._close_message), pos=pos + ) + elif self.last_print_n < self.n: + # stats for overall rate (no weighted average) + self.avg_time = None + self.display(pos=pos) + if not max( + [abs(getattr(i, "pos", 0)) for i in self._instances] + [pos] + ): + # only if not nested (#477) + fp_write('\n') + else: + if self._close_message: + self.display( + ''.join(str(arg) for arg in self._close_message), pos=pos + ) + else: + self.display(msg='', pos=pos) + if not pos: + fp_write('\r') + + @contextmanager + def spin(self, prefix: str): + self._stop_running = threading.Event() + spin_thread = threading.Thread(target=self._spin, args=[prefix]) + spin_thread.start() + try: + yield + finally: + self._stop_running.set() + spin_thread.join() + + def _spin(self, prefix: str): + while not self._stop_running.is_set(): + self.write_at_line(prefix, next(self.spinner_cycle)) + sleep(0.50) + + @classmethod + @contextmanager + def reserve_lines(self, n): + try: + yield + finally: + self.move_down(n - 1) + + @classmethod + def move_down(cls, n: int): + print('\n' * n) + + def __exit__(self, *exc): + if exc[0]: + self._close_message = self._error_message + return super().__exit__(*exc) + + +def clear(): + os.system('clear') + + +def title(text: Any, ljust=32) -> str: + # Note that is 38 px + 1 extra space = 39 min + return str(text).ljust(ljust) + ' ' + + +def danger(text: Any) -> str: + return '{}{}{}'.format(Fore.RED, text, Fore.RESET) + + +def warning(text: Any) -> str: + return '{}{}{}'.format(Fore.YELLOW, text, Fore.RESET) + + +def done(text: Any = 'done.') -> str: + return '{}{}{}'.format(Fore.GREEN, text, Fore.RESET) diff --git a/ereuse_devicehub/ereuse_utils/cmd.py b/ereuse_devicehub/ereuse_utils/cmd.py new file mode 100644 index 00000000..1f1262f3 --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/cmd.py @@ -0,0 +1,148 @@ +import subprocess +from contextlib import suppress +from typing import Any, Set + +from ereuse_devicehub.ereuse_utils import text + + +def run( + *cmd: Any, + out=subprocess.PIPE, + err=subprocess.DEVNULL, + to_string=True, + check=True, + shell=False, + **kwargs, +) -> subprocess.CompletedProcess: + """subprocess.run with a better API. + + :param cmd: A list of commands to execute as parameters. + Parameters will be passed-in to ``str()`` so they + can be any object that can handle str(). + :param out: As ``subprocess.run.stdout``. + :param err: As ``subprocess.run.stderr``. + :param to_string: As ``subprocess.run.universal_newlines``. + :param check: As ``subprocess.run.check``. + :param shell: + :param kwargs: Any other parameters that ``subprocess.run`` + accepts. + :return: The result of executing ``subprocess.run``. + """ + cmds = tuple(str(c) for c in cmd) + return subprocess.run( + ' '.join(cmds) if shell else cmds, + stdout=out, + stderr=err, + universal_newlines=to_string, + check=check, + shell=shell, + **kwargs, + ) + + +class ProgressiveCmd: + """Executes a cmd while interpreting its completion percentage. + + The completion percentage of the cmd is stored in + :attr:`.percentage` and the user can obtain percentage + increments by executing :meth:`.increment`. + + This class is useful to use within a child thread, so a main + thread can request from time to time the percentage / increment + status of the running command. + """ + + READ_LINE = None + DECIMALS = {4, 5, 6} + DECIMAL_NUMBERS = 2 + INT = {1, 2, 3} + + def __init__( + self, + *cmd: Any, + stdout=subprocess.DEVNULL, + number_chars: Set[int] = INT, + decimal_numbers: int = None, + read: int = READ_LINE, + callback=None, + check=True, + ): + """ + :param cmd: The command to execute. + :param stderr: the stderr passed-in to Popen. + :param stdout: the stdout passed-in to Popen + :param number_chars: The number of chars used to represent + the percentage. Normalized cases are + :attr:`.DECIMALS` and :attr:`.INT`. + :param read: For commands that do not print lines, how many + characters we should read between updates. + The percentage should be between those + characters. + :param callback: If passed in, this method is executed every time + run gets an update from the command, passing + in the increment from the last execution. + If not passed-in, you can get such increment + by executing manually the ``increment`` method. + :param check: Raise error if subprocess return code is non-zero. + """ + self.cmd = tuple(str(c) for c in cmd) + self.read = read + self.step = 0 + self.check = check + self.number_chars = number_chars + self.decimal_numbers = decimal_numbers + # We call subprocess in the main thread so the main thread + # can react on ``CalledProcessError`` exceptions + self.conn = conn = subprocess.Popen( + self.cmd, universal_newlines=True, stderr=subprocess.PIPE, stdout=stdout + ) + self.out = conn.stdout if stdout == subprocess.PIPE else conn.stderr + self._callback = callback + self.last_update_percentage = 0 + self.percentage = 0 + + @property + def percentage(self): + return self._percentage + + @percentage.setter + def percentage(self, v): + self._percentage = v + if self._callback and self._percentage > 0: + increment = self.increment() + if ( + increment > 0 + ): # Do not bother calling if there has not been any increment + self._callback(increment, self._percentage) + + def run(self) -> None: + """Processes the output.""" + while True: + out = self.out.read(self.read) if self.read else self.out.readline() + if out: + with suppress(StopIteration): + self.percentage = next( + text.positive_percentages( + out, self.number_chars, self.decimal_numbers + ) + ) + else: # No more output + break + return_code = self.conn.wait() # wait until cmd ends + if self.check and return_code != 0: + raise subprocess.CalledProcessError( + self.conn.returncode, self.conn.args, stderr=self.conn.stderr.read() + ) + + def increment(self): + """Returns the increment of progression from + the last time this method is executed. + """ + # for cmd badblocks the increment can be negative at the + # beginning of the second step where last_percentage + # is 100 and percentage is 0. By using max we + # kind-of reset the increment and start counting for + # the second step + increment = max(self.percentage - self.last_update_percentage, 0) + self.last_update_percentage = self.percentage + return increment diff --git a/ereuse_devicehub/ereuse_utils/getter.py b/ereuse_devicehub/ereuse_utils/getter.py new file mode 100644 index 00000000..b9ffd71d --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/getter.py @@ -0,0 +1,171 @@ +"""Functions to get values from dictionaries and list encoded key-value +strings with meaningful indentations. + +Values obtained from these functions are sanitized and automatically +(or explicitly set) casted. Sanitization includes removing unnecessary +whitespaces and removing useless keywords (in the context of +computer hardware) from the texts. +""" + +import re +from itertools import chain +from typing import Any, Iterable, Set, Type, Union +from unittest.mock import DEFAULT + +import boltons.iterutils +import yaml + +from ereuse_devicehub.ereuse_utils.text import clean + + +def dict( + d: dict, + path: Union[str, tuple], + remove: Set[str] = set(), + default: Any = DEFAULT, + type: Type = None, +): + """Gets a value from the dictionary and sanitizes it. + + Values are patterned and compared against sets + of meaningless characters for device hardware. + + :param d: A dictionary potentially containing the value. + :param path: The key or a tuple-path where the value should be. + :param remove: Remove these words if found. + :param default: A default value to return if not found. If not set, + an exception is raised. + :param type: Enforce a type on the value (like ``int``). By default + dict tries to guess the correct type. + """ + try: + v = boltons.iterutils.get_path(d, (path,) if isinstance(path, str) else path) + except KeyError: + return _default(path, default) + else: + return sanitize(v, remove, type=type) + + +def kv( + iterable: Iterable[str], + key: str, + default: Any = DEFAULT, + sep=':', + type: Type = None, +) -> Any: + """Key-value. Gets a value from an iterable representing key values in the + form of a list of strings lines, for example an ``.ini`` or yaml file, + if they are opened with ``.splitlines()``. + + :param iterable: An iterable of strings. + :param key: The key where the value should be. + :param default: A default value to return if not found. If not set, + an exception is raised. + :param sep: What separates the key from the value in the line. + Usually ``:`` or ``=``. + :param type: Enforce a type on the value (like ``int``). By default + dict tries to guess the correct type. + """ + for line in iterable: + try: + k, value, *_ = line.strip().split(sep) + except ValueError: + continue + else: + if key == k: + return sanitize(value, type=type) + return _default(key, default) + + +def indents(iterable: Iterable[str], keyword: str, indent=' '): + """For a given iterable of strings, returns blocks of the same + left indentation. + + For example: + foo1 + bar1 + bar2 + foo2 + foo2 + + For that text, this method would return ``[bar1, bar2]`` for passed-in + keyword ``foo1``. + + :param iterable: A list of strings representing lines. + :param keyword: The title preceding the indentation. + :param indent: Which characters makes the indentation. + """ + section_pos = None + for i, line in enumerate(iterable): + if not line.startswith(indent): + if keyword in line: + section_pos = i + elif section_pos is not None: + yield iterable[section_pos:i] + section_pos = None + return + + +def _default(key, default): + if default is DEFAULT: + raise IndexError('Value {} not found.'.format(key)) + else: + return default + + +"""Gets""" +TO_REMOVE = {'none', 'prod', 'o.e.m', 'oem', r'n/a', 'atapi', 'pc', 'unknown'} +"""Delete those *words* from the value""" +assert all(v.lower() == v for v in TO_REMOVE), 'All words need to be lower-case' + +REMOVE_CHARS_BETWEEN = '(){}[]' +""" +Remove those *characters* from the value. +All chars inside those are removed. Ex: foo (bar) => foo +""" +CHARS_TO_REMOVE = '*' +"""Remove the characters. + +'*' Needs to be removed or otherwise it is interpreted +as a glob expression by regexes. +""" + +MEANINGLESS = { + 'to be filled', + 'system manufacturer', + 'system product', + 'sernum', + 'xxxxx', + 'system name', + 'not specified', + 'modulepartnumber', + 'system serial', + '0001-067a-0000', + 'partnum', + 'manufacturer', + '0000000', + 'fffff', + 'jedec id:ad 00 00 00 00 00 00 00', + '012000', + 'x.x', + 'sku', +} +"""Discard a value if any of these values are inside it. """ +assert all(v.lower() == v for v in MEANINGLESS), 'All values need to be lower-case' + + +def sanitize(value, remove=set(), type=None): + if value is None: + return None + remove = remove | TO_REMOVE + regex = r'({})\W'.format('|'.join(s for s in remove)) + val = re.sub(regex, '', value, flags=re.IGNORECASE) + val = '' if val.lower() in remove else val # regex's `\W` != whole string + val = re.sub(r'\([^)]*\)', '', val) # Remove everything between + for char_to_remove in chain(REMOVE_CHARS_BETWEEN, CHARS_TO_REMOVE): + val = val.replace(char_to_remove, '') + val = clean(val) + if val and not any(meaningless in val.lower() for meaningless in MEANINGLESS): + return type(val) if type else yaml.load(val, Loader=yaml.SafeLoader) + else: + return None diff --git a/ereuse_devicehub/ereuse_utils/naming.py b/ereuse_devicehub/ereuse_utils/naming.py new file mode 100644 index 00000000..32371f25 --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/naming.py @@ -0,0 +1,143 @@ +from inflection import ( + camelize, + dasherize, + parameterize, + pluralize, + singularize, + underscore, +) + +HID_CONVERSION_DOC = """ + The HID is the result of concatenating, + in the following order: the type of device (ex. Computer), + the manufacturer name, the model name, and the S/N. It is joined + with hyphens, and adapted to comply with the URI specification, so + it can be used in the URI identifying the device on the Internet. + The conversion is done as follows: + + 1. non-ASCII characters are converted to their ASCII equivalent or + removed. + 2. Characterst that are not letters or numbers are converted to + underscores, in a way that there are no trailing underscores + and no underscores together, and they are set to lowercase. + + Ex. ``laptop-acer-aod270-lusga_0d0242201212c7614`` + """ + + +class Naming: + """ + In DeviceHub there are many ways to name the same resource (yay!), this is because of all the different + types of schemas we work with. But no worries, we offer easy ways to change between naming conventions. + + - TypeCase (or resource-type) is the one represented with '@type' and follow PascalCase and always singular. + This is the standard preferred one. + - resource-case is the eve naming, using the standard URI conventions. This one is tricky, as although the types + are represented in singular, the URI convention is to be plural (Event vs events), however just few of them + follow this rule (Snapshot [type] to snapshot [resource]). You can set which ones you want to change their + number. + - python_case is the one used by python for its folders and modules. It is underscored and always singular. + """ + + TYPE_PREFIX = ':' + RESOURCE_PREFIX = '_' + + @staticmethod + def resource(string: str): + """ + :param string: String can be type, resource or python case + """ + try: + prefix, resulting_type = Naming.pop_prefix(string) + prefix += Naming.RESOURCE_PREFIX + except IndexError: + prefix = '' + resulting_type = string + resulting_type = dasherize(underscore(resulting_type)) + return prefix + pluralize(resulting_type) + + @staticmethod + def python(string: str): + """ + :param string: String can be type, resource or python case + """ + return underscore(singularize(string)) + + @staticmethod + def type(string: str): + try: + prefix, resulting_type = Naming.pop_prefix(string) + prefix += Naming.TYPE_PREFIX + except IndexError: + prefix = '' + resulting_type = string + resulting_type = singularize(resulting_type) + resulting_type = resulting_type.replace( + '-', '_' + ) # camelize does not convert '-' but '_' + return prefix + camelize(resulting_type) + + @staticmethod + def url_word(word: str): + """ + Normalizes a full word to be inserted to an url. If the word has spaces, etc, is used '_' and not '-' + """ + return parameterize(word, '_') + + @staticmethod + def pop_prefix(string: str): + """Erases the prefix and returns it. + :throws IndexError: There is no prefix. + :return A set with two elements: 1- the prefix, 2- the type without it. + """ + result = string.split(Naming.TYPE_PREFIX) + if len(result) == 1: + result = string.split(Naming.RESOURCE_PREFIX) + if len(result) == 1: + raise IndexError() + return result + + @staticmethod + def new_type(type_name: str, prefix: str or None = None) -> str: + """ + Creates a resource type with optionally a prefix. + + Using the rules of JSON-LD, we use prefixes to disambiguate between different types with the same name: + one can Accept a device or a project. In eReuse.org there are different events with the same names, in + linked-data terms they have different URI. In eReuse.org, we solve this with the following: + + "@type": "devices:Accept" // the URI for these events is 'devices/events/accept' + "@type": "projects:Accept" // the URI for these events is 'projects/events/accept + ... + + Type is only used in events, when there are ambiguities. The rest of + + "@type": "devices:Accept" + "@type": "Accept" + + But these not: + + "@type": "projects:Accept" // it is an event from a project + "@type": "Accept" // it is an event from a device + """ + if Naming.TYPE_PREFIX in type_name: + raise TypeError( + 'Cannot create new type: type {} is already prefixed.'.format(type_name) + ) + prefix = (prefix + Naming.TYPE_PREFIX) if prefix is not None else '' + return prefix + type_name + + @staticmethod + def hid(type: str, manufacturer: str, model: str, serial_number: str) -> str: + ( + """Computes the HID for the given properties of a device. + The HID is suitable to use to an URI. + """ + + HID_CONVERSION_DOC + ) + return '{type}-{mn}-{ml}-{sn}'.format( + type=Naming.url_word(type), + mn=Naming.url_word(manufacturer), + ml=Naming.url_word(model), + sn=Naming.url_word(serial_number), + ) diff --git a/ereuse_devicehub/ereuse_utils/nested_lookup.py b/ereuse_devicehub/ereuse_utils/nested_lookup.py new file mode 100644 index 00000000..805543aa --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/nested_lookup.py @@ -0,0 +1,85 @@ +class NestedLookup: + @staticmethod + def __new__(cls, document, references, operation): + """Lookup a key in a nested document, return a list of values + From https://github.com/russellballestrini/nested-lookup/ but in python 3 + """ + return list(NestedLookup._nested_lookup(document, references, operation)) + + @staticmethod + def key_equality_factory(key_to_find): + def key_equality(key, _): + return key == key_to_find + + return key_equality + + @staticmethod + def is_sub_type_factory(type): + def _is_sub_type(_, value): + return is_sub_type(value, type) + + return _is_sub_type + + @staticmethod + def key_value_equality_factory(key_to_find, value_to_find): + def key_value_equality(key, value): + return key == key_to_find and value == value_to_find + + return key_value_equality + + @staticmethod + def key_value_containing_value_factory(key_to_find, value_to_find): + def key_value_containing_value(key, value): + return key == key_to_find and value_to_find in value + + return key_value_containing_value + + @staticmethod + def _nested_lookup(document, references, operation): # noqa: C901 + """Lookup a key in a nested document, yield a value""" + if isinstance(document, list): + for d in document: + for result in NestedLookup._nested_lookup(d, references, operation): + yield result + + if isinstance(document, dict): + for k, v in document.items(): + if operation(k, v): + references.append((document, k)) + yield v + elif isinstance(v, dict): + for result in NestedLookup._nested_lookup(v, references, operation): + yield result + elif isinstance(v, list): + for d in v: + for result in NestedLookup._nested_lookup( + d, references, operation + ): + yield result + + +def is_sub_type(value, resource_type): + try: + return issubclass(value, resource_type) + except TypeError: + return issubclass(value.__class__, resource_type) + + +def get_nested_dicts_with_key_value(parent_dict: dict, key, value): + """Return all nested dictionaries that contain a key with a specific value. A sub-case of NestedLookup.""" + references = [] + NestedLookup( + parent_dict, references, NestedLookup.key_value_equality_factory(key, value) + ) + return (document for document, _ in references) + + +def get_nested_dicts_with_key_containing_value(parent_dict: dict, key, value): + """Return all nested dictionaries that contain a key with a specific value. A sub-case of NestedLookup.""" + references = [] + NestedLookup( + parent_dict, + references, + NestedLookup.key_value_containing_value_factory(key, value), + ) + return (document for document, _ in references) diff --git a/ereuse_devicehub/ereuse_utils/session.py b/ereuse_devicehub/ereuse_utils/session.py new file mode 100644 index 00000000..647c7634 --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/session.py @@ -0,0 +1,285 @@ +import base64 +import json +from typing import Any, Dict, Iterable, Tuple, TypeVar, Union + +import boltons.urlutils +from requests import Response +from requests_toolbelt.sessions import BaseUrlSession +from urllib3 import Retry + +from ereuse_devicehub import ereuse_utils + +# mypy +Query = Iterable[Tuple[str, Any]] + +Status = Union[int] + +try: + from typing import Protocol # Only py 3.6+ +except ImportError: + pass +else: + class HasStatusProperty(Protocol): + def __init__(self, *args, **kwargs) -> None: + self.status = ... # type: int + + + Status = Union[int, HasStatusProperty] + +JSON = 'application/json' +ANY = '*/*' +AUTH = 'Authorization' +BASIC = 'Basic {}' +URL = Union[str, boltons.urlutils.URL] +Data = Union[str, dict, ereuse_utils.Dumpeable] +Res = Tuple[Union[Dict[str, Any], str], Response] + + +# actual code + + +class Session(BaseUrlSession): + """A BaseUrlSession that always raises for status and sets a + timeout for all requests by default. + """ + + def __init__(self, base_url=None, timeout=15): + """ + :param base_url: + :param timeout: Time requests will wait to receive the first + response bytes (not the whole) from the server. In seconds. + """ + super().__init__(base_url) + self.timeout = timeout + self.hooks['response'] = lambda r, *args, **kwargs: r.raise_for_status() + + def request(self, method, url, *args, **kwargs): + kwargs.setdefault('timeout', self.timeout) + return super().request(method, url, *args, **kwargs) + + def __repr__(self): + return '<{} base={}>.'.format(self.__class__.__name__, self.base_url) + + +class DevicehubClient(Session): + """A Session pre-configured to connect to Devicehub-like APIs.""" + + def __init__(self, base_url: URL = None, + token: str = None, + inventory: Union[str, bool] = False, + **kwargs): + """Initializes a session pointing to a Devicehub endpoint. + + Authentication can be passed-in as a token for endpoints + that require them, now at ini, after when executing the method, + or in between with ``set_auth``. + + :param base_url: An url pointing to a endpoint. + :param token: A Base64 encoded token, as given by a devicehub. + You can encode tokens by executing `encode_token`. + :param inventory: If True, use the default inventory of the user. + If False, do not use inventories (single-inventory + database, this is the option by default). + If a string, always use the set inventory. + """ + if isinstance(base_url, boltons.urlutils.URL): + base_url = base_url.to_text() + else: + base_url = str(base_url) + super().__init__(base_url, **kwargs) + assert base_url[-1] != '/', 'Do not provide a final slash to the URL' + if token: + self.set_auth(token) + self.inventory = inventory + self.user = None # type: Dict[str, object] + + def set_auth(self, token): + self.headers['Authorization'] = 'Basic {}'.format(token) + + @classmethod + def encode_token(cls, token: str): + """Encodes a token suitable for a Devicehub endpoint.""" + return base64.b64encode(str.encode(str(token) + ':')).decode() + + def login(self, email: str, password: str) -> Dict[str, Any]: + """Performs login, authenticating future requests. + + :return: The logged-in user. + """ + user, _ = self.post('/users/login/', {'email': email, 'password': password}, status=200) + self.set_auth(user['token']) + self.user = user + self.inventory = user['inventories'][0]['id'] + return user + + def get(self, + base_url: URL, + uri=None, + status: Status = 200, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + headers: dict = None, + token=None, + **kwargs) -> Res: + return super().get(base_url, + uri=uri, + status=status, + query=query, + accept=accept, + content_type=content_type, + headers=headers, + token=token, **kwargs) + + def post(self, base_url: URL, + data: Data, + uri=None, + status: Status = 201, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + headers: dict = None, + token=None, + **kwargs) -> Res: + return super().post(base_url, + data=data, + uri=uri, + status=status, + query=query, + accept=accept, + content_type=content_type, + headers=headers, + token=token, **kwargs) + + def delete(self, + base_url: URL, + uri=None, + status: Status = 204, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + headers: dict = None, + token=None, + **kwargs) -> Res: + return super().delete(base_url, + uri=uri, + status=status, + query=query, + accept=accept, + content_type=content_type, + headers=headers, + token=token, **kwargs) + + def patch(self, base_url: URL, + data: Data, + uri=None, + status: Status = 201, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + headers: dict = None, + token=None, + **kwargs) -> Res: + return super().patch(base_url, + data=data, + uri=uri, + status=status, + query=query, + accept=accept, + content_type=content_type, + headers=headers, + token=token, **kwargs) + + def request(self, + method, + base_url: URL, + uri=None, + status: Status = 200, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + data=None, + headers: dict = None, + token=None, + **kw) -> Res: + assert not kw.get('json', None), 'Do not use json; use data.' + # We allow uris without slashes for item endpoints + uri = str(uri) if uri else None + headers = headers or {} + headers['Accept'] = accept + headers['Content-Type'] = content_type + if token: + headers['Authorization'] = 'Basic {}'.format(token) + if data and content_type == JSON: + data = json.dumps(data, cls=ereuse_utils.JSONEncoder, sort_keys=True) + url = base_url if not isinstance(base_url, boltons.urlutils.URL) else base_url.to_text() + assert url[-1] == '/', 'base_url should end with a slash' + if self.inventory and not isinstance(self.inventory, bool): + url = '{}/{}'.format(self.inventory, base_url) + assert url[-1] == '/', 'base_url should end with a slash' + if uri: + url = self.parse_uri(url, uri) + if query: + url = self.parse_query(url, query) + response = super().request(method, url, data=data, headers=headers, **kw) + if status: + _status = getattr(status, 'code', status) + if _status != response.status_code: + raise WrongStatus('Req to {} failed bc the status is {} but it should have been {}' + .format(url, response.status_code, _status)) + data = response.content if not accept == JSON or not response.content else response.json() + return data, response + + @staticmethod + def parse_uri(base_url, uri): + return boltons.urlutils.URL(base_url).navigate(uri).to_text() + + @staticmethod + def parse_query(uri, query): + url = boltons.urlutils.URL(uri) + url.query_params = boltons.urlutils.QueryParamDict([ + (k, json.dumps(v, cls=ereuse_utils.JSONEncoder) if isinstance(v, (list, dict)) else v) + for k, v in query + ]) + return url.to_text() + + def __repr__(self): + return '<{} base={} inv={} user={}>.'.format(self.__class__.__name__, self.base_url, + self.inventory, self.user) + + +class WrongStatus(Exception): + pass + + +import requests +from requests.adapters import HTTPAdapter + +T = TypeVar('T', bound=requests.Session) + + +def retry(session: T, + retries=3, + backoff_factor=1, + status_to_retry=(500, 502, 504)) -> T: + """Configures requests from the given session to retry in + failed requests due to connection errors, HTTP response codes + with ``status_to_retry`` and 30X redirections. + + Remember that you still need + """ + # From https://www.peterbe.com/plog/best-practice-with-retries-with-requests + # Doc in https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html#module-urllib3.util.retry + session = session or requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_to_retry, + method_whitelist=False # Retry too in non-idempotent methods like POST + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session diff --git a/ereuse_devicehub/ereuse_utils/test.py b/ereuse_devicehub/ereuse_utils/test.py new file mode 100644 index 00000000..b78631b2 --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/test.py @@ -0,0 +1,165 @@ +from contextlib import suppress +from typing import Dict, Tuple, Union + +from flask import json +from flask.testing import FlaskClient +from werkzeug.wrappers import Response + +from ereuse_devicehub.ereuse_utils.session import ANY, AUTH, BASIC, DevicehubClient, JSON, Query, Status + +ANY = ANY +AUTH = AUTH +BASIC = BASIC + +Res = Tuple[Union[Dict[str, object], str], Response] + + +class Client(FlaskClient): + """ + A client for the REST servers of DeviceHub and WorkbenchServer. + + - JSON first. By default it sends and expects receiving JSON files. + - Assert regular status responses, like 200 for GET. + - Auto-parses a nested dictionary of URL query params to the + URL version with nested properties to JSON. + - Meaningful headers format: a dictionary of name-values. + """ + + def open(self, + uri: str, + status: Status = 200, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + item=None, + headers: dict = None, + **kw) -> Res: + """ + + :param uri: The URI without basename and query. + :param status: Assert the response for specified status. Set + None to avoid. + :param query: The query of the URL in the form of + [(key1, value1), (key2, value2), (key1, value3)]. + If value is a list or a dict, they will be + converted to JSON. + Please, see :class:`boltons.urlutils`. + QueryParamDict` for more info. + :param accept: The Accept header. If 'application/json' + (default) then it will parse incoming JSON. + :param item: The last part of the path. Useful to do something + like ``get('db/accounts', item='24')``. If you + use ``item``, you can't set a final backslash into + ``uri`` (or the parse will fail). + :param headers: A dictionary of headers, where keys are header + names and values their values. + Ex: {'Accept', 'application/json'}. + :param kw: Kwargs passed into parent ``open``. + :return: A tuple with: 1. response data, as a string or JSON + depending of Accept, and 2. the Response object. + """ + j_encoder = self.application.json_encoder + headers = headers or {} + headers['Accept'] = accept + headers['Content-Type'] = content_type + headers = [(k, v) for k, v in headers.items()] + if 'data' in kw and content_type == JSON: + kw['data'] = json.dumps(kw['data'], cls=j_encoder) + if item: + uri = DevicehubClient.parse_uri(uri, item) + if query: + uri = DevicehubClient.parse_query(uri, query) + response = super().open(uri, headers=headers, **kw) + if status: + _status = getattr(status, 'code', status) + assert response.status_code == _status, \ + 'Expected status code {} but got {}. Returned data is:\n' \ + '{}'.format(_status, response.status_code, response.get_data().decode()) + + data = response.get_data() + with suppress(UnicodeDecodeError): + data = data.decode() + if accept == JSON: + data = json.loads(data) if data else {} + return data, response + + def get(self, + uri: str, + query: Query = tuple(), + item: str = None, + status: Status = 200, + accept: str = JSON, + headers: dict = None, + **kw) -> Res: + """ + Performs a GET. + + See the parameters in :meth:`ereuse_utils.test.Client.open`. + Moreover: + + :param query: A dictionary of query params. If a parameter is a + dict or a list, it will be parsed to JSON, then + all params are encoded with ``urlencode``. + :param kw: Kwargs passed into parent ``open``. + """ + return super().get(uri, item=item, status=status, accept=accept, headers=headers, + query=query, **kw) + + def post(self, + uri: str, + data: str or dict, + query: Query = tuple(), + status: Status = 201, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + **kw) -> Res: + """ + Performs a POST. + + See the parameters in :meth:`ereuse_utils.test.Client.open`. + """ + return super().post(uri, data=data, status=status, content_type=content_type, + accept=accept, headers=headers, query=query, **kw) + + def patch(self, + uri: str, + data: str or dict, + query: Query = tuple(), + status: Status = 200, + content_type: str = JSON, + item: str = None, + accept: str = JSON, + headers: dict = None, + **kw) -> Res: + """ + Performs a PATCH. + + See the parameters in :meth:`ereuse_utils.test.Client.open`. + """ + return super().patch(uri, item=item, data=data, status=status, content_type=content_type, + accept=accept, headers=headers, query=query, **kw) + + def put(self, + uri: str, + data: str or dict, + query: Query = tuple(), + status: Status = 201, + content_type: str = JSON, + item: str = None, + accept: str = JSON, + headers: dict = None, + **kw) -> Res: + return super().put(uri, item=item, data=data, status=status, content_type=content_type, + accept=accept, headers=headers, query=query, **kw) + + def delete(self, + uri: str, + query: Query = tuple(), + item: str = None, + status: Status = 204, + accept: str = JSON, + headers: dict = None, + **kw) -> Res: + return super().delete(uri, query=query, item=item, status=status, accept=accept, + headers=headers, **kw) diff --git a/ereuse_devicehub/ereuse_utils/text.py b/ereuse_devicehub/ereuse_utils/text.py new file mode 100644 index 00000000..747aea52 --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/text.py @@ -0,0 +1,72 @@ +import ast +import re +from typing import Iterator, Set, Union + + +def grep(text: str, value: str): + """An easy 'grep -i' that yields lines where value is found.""" + for line in text.splitlines(): + if value in line: + yield line + + +def between(text: str, begin='(', end=')'): + """Dead easy text between two characters. + Not recursive or repetitions. + """ + return text.split(begin)[-1].split(end)[0] + + +def numbers(text: str) -> Iterator[Union[int, float]]: + """Gets numbers in strings with other characters. + + Integer Numbers: 1 2 3 987 +4 -8 + Decimal Numbers: 0.1 2. .3 .987 +4.0 -0.8 + Scientific Notation: 1e2 0.2e2 3.e2 .987e2 +4e-1 -8.e+2 + Numbers with percentages: 49% 32.39% + + This returns int or float. + """ + # From https://regexr.com/33jqd + for x in re.finditer(r'[+-]?(?=\.\d|\d)(?:\d+)?(?:\.?\d*)(?:[eE][+-]?\d+)?', text): + yield ast.literal_eval(x.group()) + + +def positive_percentages( + text: str, lengths: Set[int] = None, decimal_numbers: int = None +) -> Iterator[Union[int, float]]: + """Gets numbers postfixed with a '%' in strings with other characters. + + 1)100% 2)56.78% 3)56 78.90% 4)34.6789% some text + + :param text: The text to search for. + :param lengths: A set of lengths that the percentage + number should have to be considered valid. + Ex. {5,6} would validate '90.32' and '100.00' + """ + # From https://regexr.com/3aumh + for x in re.finditer(r'[\d|\.]+%', text): + num = x.group()[:-1] + if lengths: + if not len(num) in lengths: + continue + if decimal_numbers: + try: + pos = num.rindex('.') + except ValueError: + continue + else: + if len(num) - pos - 1 != decimal_numbers: + continue + yield float(num) + + +def macs(text: str) -> Iterator[str]: + """Find MACs in strings with other characters.""" + for x in re.finditer('{0}:{0}:{0}:{0}:{0}:{0}'.format(r'[a-fA-F0-9.+_-]+'), text): + yield x.group() + + +def clean(text: str) -> str: + """Trims the text and replaces multiple spaces with a single space.""" + return ' '.join(text.split()) diff --git a/ereuse_devicehub/ereuse_utils/usb_flash_drive.py b/ereuse_devicehub/ereuse_utils/usb_flash_drive.py new file mode 100644 index 00000000..444de53d --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/usb_flash_drive.py @@ -0,0 +1,80 @@ +import usb.core +import usb.util +from usb import CLASS_MASS_STORAGE + +from ereuse_devicehub.ereuse_utils.naming import Naming + + +def plugged_usbs(multiple=True) -> map or dict: # noqa: C901 + """ + Gets the plugged-in USB Flash drives (pen-drives). + + If multiple is true, it returns a map, and a dict otherwise. + + If multiple is false, this method will raise a :class:`.NoUSBFound` if no USB is found. + """ + + class FindPenDrives(object): + # From https://github.com/pyusb/pyusb/blob/master/docs/tutorial.rst + def __init__(self, class_): + self._class = class_ + + def __call__(self, device): + # first, let's check the device + if device.bDeviceClass == self._class: + return True + # ok, transverse all devices to find an + # interface that matches our class + for cfg in device: + # find_descriptor: what's it? + intf = usb.util.find_descriptor(cfg, bInterfaceClass=self._class) + # We don't want Card readers + if intf is not None: + try: + product = intf.device.product.lower() + except ValueError as e: + if 'langid' in str(e): + raise OSError( + 'Cannot get "langid". Do you have permissions?' + ) + else: + raise e + if 'crw' not in product and 'reader' not in product: + return True + return False + + def get_pendrive(pen: usb.Device) -> dict: + if not pen.manufacturer or not pen.product or not pen.serial_number: + raise UsbDoesNotHaveHid() + manufacturer = pen.manufacturer.strip() or str(pen.idVendor) + model = pen.product.strip() or str(pen.idProduct) + serial_number = pen.serial_number.strip() + hid = Naming.hid('USBFlashDrive', manufacturer, model, serial_number) + return { + 'id': hid, # Make live easier to DeviceHubClient by using _id + 'hid': hid, + 'type': 'USBFlashDrive', + 'serialNumber': serial_number, + 'model': model, + 'manufacturer': manufacturer, + 'vendorId': pen.idVendor, + 'productId': pen.idProduct, + } + + result = usb.core.find( + find_all=multiple, custom_match=FindPenDrives(CLASS_MASS_STORAGE) + ) + if multiple: + return map(get_pendrive, result) + else: + if not result: + raise NoUSBFound() + return get_pendrive(result) + + +class NoUSBFound(Exception): + pass + + +class UsbDoesNotHaveHid(Exception): + pass diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index 4f75c759..a56d41ce 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -32,6 +32,7 @@ from wtforms.fields import FormField from ereuse_devicehub.db import db from ereuse_devicehub.inventory.models import ( DeliveryNote, + DeviceDocument, ReceiverNote, Transfer, TransferCustomerDetails, @@ -69,7 +70,7 @@ from ereuse_devicehub.resources.device.models import ( from ereuse_devicehub.resources.documents.models import DataWipeDocument from ereuse_devicehub.resources.enums import Severity from ereuse_devicehub.resources.hash_reports import insert_hash -from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.resources.lot.models import Lot, ShareLot from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.user.models import User @@ -110,6 +111,15 @@ DEVICES = { "Other Devices": ["Other"], } +TYPES_DOCUMENTS = [ + ("", ""), + ("image", "Image"), + ("main_image", "Main Image"), + ("functionality_report", "Functionality Report"), + ("data_sanitization_report", "Data Sanitization Report"), + ("disposition_report", "Disposition Report"), +] + COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer'] MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"] @@ -150,11 +160,14 @@ class FilterForm(FlaskForm): '', choices=DEVICES, default="All Computers", render_kw={'class': "form-select"} ) - def __init__(self, lots, lot_id, *args, **kwargs): + def __init__(self, lots, lot, lot_id, *args, **kwargs): self.all_devices = kwargs.pop('all_devices', False) super().__init__(*args, **kwargs) self.lots = lots + self.lot = lot self.lot_id = lot_id + if self.lot_id and not self.lot: + self.lot = self.lots.filter(Lot.id == self.lot_id).one() self._get_types() def _get_types(self): @@ -165,8 +178,7 @@ class FilterForm(FlaskForm): self.filter.data = self.device_type def filter_from_lots(self): - if self.lot_id: - self.lot = self.lots.filter(Lot.id == self.lot_id).one() + if self.lot: device_ids = (d.id for d in self.lot.devices) self.devices = Device.query.filter(Device.id.in_(device_ids)).filter( Device.binding == None # noqa: E711 @@ -246,7 +258,8 @@ class LotForm(FlaskForm): return self.id def remove(self): - if self.instance and not self.instance.trade: + shared = ShareLot.query.filter_by(lot=self.instance).first() + if self.instance and not self.instance.trade and not shared: self.instance.delete() db.session.commit() return self.instance @@ -459,8 +472,6 @@ class NewDeviceForm(FlaskForm): if self._obj.placeholder.is_abstract: self.type.render_kw = disabled self.amount.render_kw = disabled - # self.id_device_supplier.render_kw = disabled - self.pallet.render_kw = disabled self.info.render_kw = disabled self.components.render_kw = disabled self.serial_number.render_kw = disabled @@ -674,6 +685,14 @@ class NewDeviceForm(FlaskForm): ): self._obj.set_functionality(self.functionality.data) + else: + self._obj.placeholder.id_device_supplier = ( + self.id_device_supplier.data or None + ) + self._obj.placeholder.id_device_internal = ( + self.id_device_internal.data or None + ) + self._obj.placeholder.pallet = self.pallet.data or None placeholder_log = PlaceholdersLog( type="Update", source='Web form', placeholder=self._obj.placeholder ) @@ -1275,8 +1294,24 @@ class TradeDocumentForm(FlaskForm): def __init__(self, *args, **kwargs): lot_id = kwargs.pop('lot') - super().__init__(*args, **kwargs) + doc_id = kwargs.pop('document', None) self._lot = Lot.query.filter(Lot.id == lot_id).one() + self._obj = None + if doc_id: + self._obj = TradeDocument.query.filter_by( + id=doc_id, lot=self._lot, owner=g.user + ).one() + kwargs['obj'] = self._obj + + if not self.file_name.args: + self.file_name.args = ("File", [validators.DataRequired()]) + if doc_id: + self.file_name.args = () + super().__init__(*args, **kwargs) + + if self._obj: + if isinstance(self.url.data, URL): + self.url.data = self.url.data.to_text() if not self._lot.transfer: self.form_errors = ['Error, this lot is not a transfer lot.'] @@ -1292,22 +1327,143 @@ class TradeDocumentForm(FlaskForm): def save(self, commit=True): file_name = '' file_hash = '' + if self._obj: + file_name = self._obj.file_name + file_hash = self._obj.file_hash + if self.file_name.data: file_name = self.file_name.data.filename file_hash = insert_hash(self.file_name.data.read(), commit=False) self.url.data = URL(self.url.data) - self._obj = TradeDocument(lot_id=self._lot.id) + if not self._obj: + self._obj = TradeDocument(lot_id=self._lot.id) + self.populate_obj(self._obj) + self._obj.file_name = file_name self._obj.file_hash = file_hash - db.session.add(self._obj) - self._lot.documents.add(self._obj) + + if not self._obj.id: + db.session.add(self._obj) + self._lot.documents.add(self._obj) + if commit: db.session.commit() return self._obj + def remove(self): + if self._obj: + self._obj.delete() + db.session.commit() + return self._obj + + +class DeviceDocumentForm(FlaskForm): + url = URLField( + 'Url', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="Url where the document resides", + ) + description = StringField( + 'Description', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="", + ) + id_document = StringField( + 'Document Id', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="Identification number of document", + ) + type = SelectField( + 'Type', + [validators.Optional()], + choices=TYPES_DOCUMENTS, + default="", + render_kw={'class': "form-select"}, + ) + date = DateField( + 'Date', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="", + ) + file_name = FileField( + 'File', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + description="""This file is not stored on our servers, it is only used to + generate a digital signature and obtain the name of the file.""", + ) + + def __init__(self, *args, **kwargs): + id = kwargs.pop('dhid') + doc_id = kwargs.pop('document', None) + self._device = Device.query.filter(Device.devicehub_id == id).first() + self._obj = None + if doc_id: + self._obj = DeviceDocument.query.filter_by( + id=doc_id, device=self._device, owner=g.user + ).one() + kwargs['obj'] = self._obj + + if not self.file_name.args: + self.file_name.args = ("File", [validators.DataRequired()]) + if doc_id: + self.file_name.args = () + super().__init__(*args, **kwargs) + + if self._obj: + if isinstance(self.url.data, URL): + self.url.data = self.url.data.to_text() + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if g.user != self._device.owner: + is_valid = False + + return is_valid + + def save(self, commit=True): + file_name = '' + file_hash = '' + if self._obj: + file_name = self._obj.file_name + file_hash = self._obj.file_hash + + if self.file_name.data: + file_name = self.file_name.data.filename + file_hash = insert_hash(self.file_name.data.read(), commit=False) + + self.url.data = URL(self.url.data) + if not self._obj: + self._obj = DeviceDocument(device_id=self._device.id) + + self.populate_obj(self._obj) + + self._obj.file_name = file_name + self._obj.file_hash = file_hash + + if not self._obj.id: + db.session.add(self._obj) + # self._device.documents.add(self._obj) + + if commit: + db.session.commit() + + return self._obj + + def remove(self): + if self._obj: + self._obj.delete() + db.session.commit() + return self._obj + class TransferForm(FlaskForm): lot_name = StringField( diff --git a/ereuse_devicehub/inventory/models.py b/ereuse_devicehub/inventory/models.py index f8b4f977..ace7da19 100644 --- a/ereuse_devicehub/inventory/models.py +++ b/ereuse_devicehub/inventory/models.py @@ -1,15 +1,17 @@ from uuid import uuid4 from citext import CIText +from dateutil.tz import tzutc from flask import g -from sqlalchemy import Column, Integer +from sortedcontainers import SortedSet +from sqlalchemy import BigInteger, Column, Integer from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import backref, relationship -from teal.db import CASCADE_OWN, URL from ereuse_devicehub.db import db from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.db import CASCADE_OWN, URL class Transfer(Thing): @@ -110,3 +112,50 @@ class TransferCustomerDetails(Thing): ), primaryjoin='TransferCustomerDetails.transfer_id == Transfer.id', ) + + +_sorted_documents = { + 'order_by': lambda: DeviceDocument.created, + 'collection_class': SortedSet, +} + + +class DeviceDocument(Thing): + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) + type = Column(db.CIText(), nullable=True) + date = Column(db.DateTime, nullable=True) + id_document = Column(db.CIText(), nullable=True) + description = Column(db.CIText(), nullable=True) + owner_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id, + ) + owner = db.relationship(User, primaryjoin=owner_id == User.id) + device_id = db.Column(BigInteger, db.ForeignKey('device.id'), nullable=False) + device = db.relationship( + 'Device', + primaryjoin='DeviceDocument.device_id == Device.id', + backref=backref( + 'documents', lazy=True, cascade=CASCADE_OWN, **_sorted_documents + ), + ) + file_name = Column(db.CIText(), nullable=True) + file_hash = Column(db.CIText(), nullable=True) + url = db.Column(URL(), nullable=True) + + # __table_args__ = ( + # db.Index('document_id', id, postgresql_using='hash'), + # db.Index('type_doc', type, postgresql_using='hash') + # ) + + def get_url(self) -> str: + if self.url: + return self.url.to_text() + return '' + + def __lt__(self, other): + return self.created.replace(tzinfo=tzutc()) < other.created.replace( + tzinfo=tzutc() + ) diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 2526a850..7e5a7999 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -14,6 +14,7 @@ from flask import current_app as app from flask import g, make_response, request, url_for from flask.views import View from flask_login import current_user, login_required +from sqlalchemy import or_ from werkzeug.exceptions import NotFound from ereuse_devicehub import messages @@ -24,6 +25,7 @@ from ereuse_devicehub.inventory.forms import ( BindingForm, CustomerDetailsForm, DataWipeForm, + DeviceDocumentForm, EditTransferForm, FilterForm, LotForm, @@ -50,7 +52,7 @@ from ereuse_devicehub.resources.device.models import ( from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow from ereuse_devicehub.resources.enums import SnapshotSoftware from ereuse_devicehub.resources.hash_reports import insert_hash -from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.resources.lot.models import Lot, ShareLot from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.views import GenericMixin @@ -72,19 +74,25 @@ class DeviceListMixin(GenericMixin): per_page = int(request.args.get('per_page', PER_PAGE)) filter = request.args.get('filter', "All+Computers") + lot = None + + share_lots = self.context['share_lots'] + share_lot = share_lots.filter_by(lot_id=lot_id).first() + if share_lot: + lot = share_lot.lot + lots = self.context['lots'] - form_filter = FilterForm(lots, lot_id, all_devices=all_devices) + form_filter = FilterForm(lots, lot, lot_id, all_devices=all_devices) devices = form_filter.search().paginate(page=page, per_page=per_page) devices.first = per_page * devices.page - per_page + 1 devices.last = len(devices.items) + devices.first - 1 - lot = None form_transfer = '' form_delivery = '' form_receiver = '' form_customer_details = '' - if lot_id: + if lot_id and not lot: lot = lots.filter(Lot.id == lot_id).one() if not lot.is_temporary and lot.transfer: form_transfer = EditTransferForm(lot_id=lot.id) @@ -110,6 +118,7 @@ class DeviceListMixin(GenericMixin): 'list_devices': self.get_selected_devices(form_new_action), 'all_devices': all_devices, 'filter': filter, + 'share_lots': share_lots, } ) @@ -536,8 +545,9 @@ class LotDeleteView(View): def dispatch_request(self, id): form = LotForm(id=id) - if form.instance.trade: - msg = "Sorry, the lot cannot be deleted because have a trade action " + shared = ShareLot.query.filter_by(lot=form.instance).first() + if form.instance.trade or shared: + msg = "Sorry, the lot cannot be deleted because this lot is share" messages.error(msg) next_url = url_for('inventory.lotdevicelist', lot_id=id) return flask.redirect(next_url) @@ -547,6 +557,27 @@ class LotDeleteView(View): return flask.redirect(next_url) +class DocumentDeleteView(View): + methods = ['GET'] + decorators = [login_required] + template_name = 'inventory/device_list.html' + form_class = TradeDocumentForm + + def dispatch_request(self, lot_id, doc_id): + next_url = url_for('inventory.lotdevicelist', lot_id=lot_id) + form = self.form_class(lot=lot_id, document=doc_id) + try: + form.remove() + except Exception as err: + msg = "{}".format(err) + messages.error(msg) + return flask.redirect(next_url) + + msg = "Document removed successfully." + messages.success(msg) + return flask.redirect(next_url) + + class UploadSnapshotView(GenericMixin): methods = ['GET', 'POST'] decorators = [login_required] @@ -789,6 +820,69 @@ class NewTradeView(DeviceListMixin, NewActionView): return flask.redirect(next_url) +class NewDeviceDocumentView(GenericMixin): + methods = ['POST', 'GET'] + decorators = [login_required] + template_name = 'inventory/device_document.html' + form_class = DeviceDocumentForm + title = "Add new document" + + def dispatch_request(self, dhid): + self.form = self.form_class(dhid=dhid) + self.get_context() + + if self.form.validate_on_submit(): + self.form.save() + messages.success('Document created successfully!') + next_url = url_for('inventory.device_details', id=dhid) + return flask.redirect(next_url) + + self.context.update({'form': self.form, 'title': self.title}) + return flask.render_template(self.template_name, **self.context) + + +class EditDeviceDocumentView(GenericMixin): + decorators = [login_required] + methods = ['POST', 'GET'] + template_name = 'inventory/device_document.html' + form_class = DeviceDocumentForm + title = "Edit document" + + def dispatch_request(self, dhid, doc_id): + self.form = self.form_class(dhid=dhid, document=doc_id) + self.get_context() + + if self.form.validate_on_submit(): + self.form.save() + messages.success('Edit document successfully!') + next_url = url_for('inventory.device_details', id=dhid) + return flask.redirect(next_url) + + self.context.update({'form': self.form, 'title': self.title}) + return flask.render_template(self.template_name, **self.context) + + +class DeviceDocumentDeleteView(View): + methods = ['GET'] + decorators = [login_required] + template_name = 'inventory/device_detail.html' + form_class = DeviceDocumentForm + + def dispatch_request(self, dhid, doc_id): + self.form = self.form_class(dhid=dhid, document=doc_id) + next_url = url_for('inventory.device_details', id=dhid) + try: + self.form.remove() + except Exception as err: + msg = "{}".format(err) + messages.error(msg) + return flask.redirect(next_url) + + msg = "Document removed successfully." + messages.success(msg) + return flask.redirect(next_url) + + class NewTradeDocumentView(GenericMixin): methods = ['POST', 'GET'] decorators = [login_required] @@ -810,6 +904,27 @@ class NewTradeDocumentView(GenericMixin): return flask.render_template(self.template_name, **self.context) +class EditTransferDocumentView(GenericMixin): + decorators = [login_required] + methods = ['POST', 'GET'] + template_name = 'inventory/trade_document.html' + form_class = TradeDocumentForm + title = "Edit document" + + def dispatch_request(self, lot_id, doc_id): + self.form = self.form_class(lot=lot_id, document=doc_id) + self.get_context() + + if self.form.validate_on_submit(): + self.form.save() + messages.success('Edit document successfully!') + next_url = url_for('inventory.lotdevicelist', lot_id=lot_id) + return flask.redirect(next_url) + + self.context.update({'form': self.form, 'title': self.title}) + return flask.render_template(self.template_name, **self.context) + + class NewTransferView(GenericMixin): methods = ['POST', 'GET'] template_name = 'inventory/new_transfer.html' @@ -899,9 +1014,20 @@ class ExportsView(View): return export_ids[export_id]() def find_devices(self): + sql = """ + select lot_device.device_id as id from {schema}.share_lot as share + inner join {schema}.lot_device as lot_device + on share.lot_id=lot_device.lot_id + where share.user_to_id='{user_id}' + """.format( + schema=app.config.get('SCHEMA'), user_id=g.user.id + ) + + shared = (x[0] for x in db.session.execute(sql)) + args = request.args.get('ids') ids = args.split(',') if args else [] - query = Device.query.filter(Device.owner == g.user) + query = Device.query.filter(or_(Device.owner == g.user, Device.id.in_(shared))) return query.filter(Device.devicehub_id.in_(ids)) def response_csv(self, data, name): @@ -1149,7 +1275,7 @@ class ExportsView(View): n_computers = len({x.parent for x in erasures} - erasures_host) params = { - 'title': 'Erasure Certificate', + 'title': 'Device Sanitization', 'erasures': tuple(erasures), 'url_pdf': '', 'date_report': '{:%c}'.format(datetime.datetime.now()), @@ -1196,12 +1322,18 @@ class ExportsView(View): 'Receiver Note Date', 'Receiver Note Units', 'Receiver Note Weight', + 'Customer Company Name', + 'Customer Location', ] ) - for lot in Lot.query.filter_by(owner=g.user): + all_lots = set(Lot.query.filter_by(owner=g.user).all()) + share_lots = [s.lot for s in ShareLot.query.filter_by(user_to=g.user)] + all_lots = all_lots.union(share_lots) + for lot in all_lots: delivery_note = lot.transfer and lot.transfer.delivery_note or '' receiver_note = lot.transfer and lot.transfer.receiver_note or '' + customer = lot.transfer and lot.transfer.customer_details or '' wb_devs = 0 placeholders = 0 @@ -1214,10 +1346,13 @@ class ExportsView(View): elif snapshots[-1].software in [SnapshotSoftware.Workbench]: wb_devs += 1 + type_lot = lot.type_transfer() + if lot in share_lots: + type_lot = "Shared" row = [ lot.id, lot.name, - lot.type_transfer(), + type_lot, lot.transfer and (lot.transfer.closed and 'Closed' or 'Open') or '', lot.transfer and lot.transfer.code or '', lot.transfer and lot.transfer.date or '', @@ -1235,6 +1370,8 @@ class ExportsView(View): receiver_note and receiver_note.date or '', receiver_note and receiver_note.units or '', receiver_note and receiver_note.weight or '', + customer and customer.company_name or '', + customer and customer.location or '', ] cw.writerow(row) @@ -1264,11 +1401,14 @@ class ExportsView(View): for dev in self.find_devices(): for lot in dev.lots: + type_lot = lot.type_transfer() + if lot.is_shared: + type_lot = "Shared" row = [ dev.devicehub_id, lot.id, lot.name, - lot.type_transfer(), + type_lot, lot.transfer and (lot.transfer.closed and 'Closed' or 'Open') or '', lot.transfer and lot.transfer.code or '', lot.transfer and lot.transfer.date or '', @@ -1512,8 +1652,28 @@ devices.add_url_rule( '/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add') ) devices.add_url_rule( - '/lot//trade-document/add/', - view_func=NewTradeDocumentView.as_view('trade_document_add'), + '/device//document/add/', + view_func=NewDeviceDocumentView.as_view('device_document_add'), +) +devices.add_url_rule( + '/device//document/edit/', + view_func=EditDeviceDocumentView.as_view('device_document_edit'), +) +devices.add_url_rule( + '/device//document/del/', + view_func=DeviceDocumentDeleteView.as_view('device_document_del'), +) +devices.add_url_rule( + '/lot//transfer-document/add/', + view_func=NewTradeDocumentView.as_view('transfer_document_add'), +) +devices.add_url_rule( + '/lot//document/edit/', + view_func=EditTransferDocumentView.as_view('transfer_document_edit'), +) +devices.add_url_rule( + '/lot//document/del/', + view_func=DocumentDeleteView.as_view('document_del'), ) devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist')) devices.add_url_rule( diff --git a/ereuse_devicehub/labels/views.py b/ereuse_devicehub/labels/views.py index 13766bfe..89697271 100644 --- a/ereuse_devicehub/labels/views.py +++ b/ereuse_devicehub/labels/views.py @@ -8,7 +8,7 @@ from requests.exceptions import ConnectionError from ereuse_devicehub import __version__, messages from ereuse_devicehub.labels.forms import PrintLabelsForm, TagForm, TagUnnamedForm -from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.resources.lot.models import Lot, ShareLot from ereuse_devicehub.resources.tag.model import Tag labels = Blueprint('labels', __name__, url_prefix='/labels') @@ -23,6 +23,7 @@ class TagListView(View): def dispatch_request(self): lots = Lot.query.filter(Lot.owner_id == current_user.id) + share_lots = ShareLot.query.filter_by(user_to_id=current_user.id) tags = Tag.query.filter(Tag.owner_id == current_user.id).order_by( Tag.created.desc() ) @@ -31,6 +32,7 @@ class TagListView(View): 'tags': tags, 'page_title': 'Unique Identifiers Management', 'version': __version__, + 'share_lots': share_lots, } return flask.render_template(self.template_name, **context) @@ -42,7 +44,13 @@ class TagAddView(View): def dispatch_request(self): lots = Lot.query.filter(Lot.owner_id == current_user.id) - context = {'page_title': 'New Tag', 'lots': lots, 'version': __version__} + share_lots = ShareLot.query.filter_by(user_to_id=current_user.id) + context = { + 'page_title': 'New Tag', + 'lots': lots, + 'version': __version__, + 'share_lots': share_lots, + } form = TagForm() if form.validate_on_submit(): form.save() @@ -59,10 +67,12 @@ class TagAddUnnamedView(View): def dispatch_request(self): lots = Lot.query.filter(Lot.owner_id == current_user.id) + share_lots = ShareLot.query.filter_by(user_to_id=current_user.id) context = { 'page_title': 'New Unnamed Tag', 'lots': lots, 'version': __version__, + 'share_lots': share_lots, } form = TagUnnamedForm() if form.validate_on_submit(): @@ -94,11 +104,13 @@ class PrintLabelsView(View): def dispatch_request(self): lots = Lot.query.filter(Lot.owner_id == current_user.id) + share_lots = ShareLot.query.filter_by(user_to_id=current_user.id) context = { 'lots': lots, 'page_title': self.title, 'version': __version__, 'referrer': request.referrer, + 'share_lots': share_lots, } form = PrintLabelsForm() @@ -123,6 +135,7 @@ class LabelDetailView(View): def dispatch_request(self, id): lots = Lot.query.filter(Lot.owner_id == current_user.id) + share_lots = ShareLot.query.filter_by(user_to_id=current_user.id) tag = ( Tag.query.filter(Tag.owner_id == current_user.id).filter(Tag.id == id).one() ) @@ -131,6 +144,7 @@ class LabelDetailView(View): 'page_title': self.title, 'version': __version__, 'referrer': request.referrer, + 'share_lots': share_lots, } devices = [] diff --git a/ereuse_devicehub/marshmallow.py b/ereuse_devicehub/marshmallow.py index 2ff78d99..784d6100 100644 --- a/ereuse_devicehub/marshmallow.py +++ b/ereuse_devicehub/marshmallow.py @@ -1,14 +1,33 @@ from marshmallow.fields import missing_ -from teal.db import SQLAlchemy -from teal.marshmallow import NestedOn as TealNestedOn from ereuse_devicehub.db import db +from ereuse_devicehub.teal.db import SQLAlchemy +from ereuse_devicehub.teal.marshmallow import NestedOn as TealNestedOn class NestedOn(TealNestedOn): __doc__ = TealNestedOn.__doc__ - def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, collection_class=list, - default=missing_, exclude=tuple(), only_query: str = None, only=None, **kwargs): - super().__init__(nested, polymorphic_on, db, collection_class, default, exclude, - only_query, only, **kwargs) + def __init__( + self, + nested, + polymorphic_on='type', + db: SQLAlchemy = db, + collection_class=list, + default=missing_, + exclude=tuple(), + only_query: str = None, + only=None, + **kwargs, + ): + super().__init__( + nested, + polymorphic_on, + db, + collection_class, + default, + exclude, + only_query, + only, + **kwargs, + ) diff --git a/ereuse_devicehub/migrations/script.py.mako b/ereuse_devicehub/migrations/script.py.mako index 3fbbfa7f..cabe65db 100644 --- a/ereuse_devicehub/migrations/script.py.mako +++ b/ereuse_devicehub/migrations/script.py.mako @@ -9,7 +9,7 @@ from alembic import op import sqlalchemy as sa import sqlalchemy_utils import citext -import teal +from ereuse_devicehub import teal ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/ereuse_devicehub/migrations/versions/0cbd839b09ef_change_testdatastorage_smallint_for_.py b/ereuse_devicehub/migrations/versions/0cbd839b09ef_change_testdatastorage_smallint_for_.py index 12346873..3207c4a1 100644 --- a/ereuse_devicehub/migrations/versions/0cbd839b09ef_change_testdatastorage_smallint_for_.py +++ b/ereuse_devicehub/migrations/versions/0cbd839b09ef_change_testdatastorage_smallint_for_.py @@ -10,7 +10,7 @@ from alembic import op import sqlalchemy as sa import sqlalchemy_utils import citext -import teal +from ereuse_devicehub import teal # revision identifiers, used by Alembic. @@ -26,11 +26,32 @@ def get_inv(): raise ValueError("Inventory value is not specified") return INV + def upgrade(): - op.alter_column('test_data_storage', 'current_pending_sector_count', type_=sa.Integer(), schema=f'{get_inv()}') - op.alter_column('test_data_storage', 'offline_uncorrectable', type_=sa.Integer(), schema=f'{get_inv()}') + op.alter_column( + 'test_data_storage', + 'current_pending_sector_count', + type_=sa.Integer(), + schema=f'{get_inv()}', + ) + op.alter_column( + 'test_data_storage', + 'offline_uncorrectable', + type_=sa.Integer(), + schema=f'{get_inv()}', + ) def downgrade(): - op.alter_column('test_data_storage', 'current_pending_sector_count', type_=sa.SmallInteger(), schema=f'{get_inv()}') - op.alter_column('test_data_storage', 'offline_uncorrectable', type_=sa.SmallInteger(), schema=f'{get_inv()}') + op.alter_column( + 'test_data_storage', + 'current_pending_sector_count', + type_=sa.SmallInteger(), + schema=f'{get_inv()}', + ) + op.alter_column( + 'test_data_storage', + 'offline_uncorrectable', + type_=sa.SmallInteger(), + schema=f'{get_inv()}', + ) diff --git a/ereuse_devicehub/migrations/versions/21afd375a654_session_table.py b/ereuse_devicehub/migrations/versions/21afd375a654_session_table.py index 97a50435..644dfb1d 100644 --- a/ereuse_devicehub/migrations/versions/21afd375a654_session_table.py +++ b/ereuse_devicehub/migrations/versions/21afd375a654_session_table.py @@ -11,7 +11,7 @@ from sqlalchemy.dialects import postgresql import sqlalchemy as sa import sqlalchemy_utils import citext -import teal +from ereuse_devicehub import teal from ereuse_devicehub.resources.enums import SessionType diff --git a/ereuse_devicehub/migrations/versions/2f2ef041483a_share_lot.py b/ereuse_devicehub/migrations/versions/2f2ef041483a_share_lot.py new file mode 100644 index 00000000..9284475e --- /dev/null +++ b/ereuse_devicehub/migrations/versions/2f2ef041483a_share_lot.py @@ -0,0 +1,52 @@ +"""share lot + +Revision ID: 2f2ef041483a +Revises: ac476b60d952 +Create Date: 2023-04-26 16:04:21.560888 + +""" +import sqlalchemy as sa +from alembic import context, op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '2f2ef041483a' +down_revision = 'ac476b60d952' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + + +def upgrade(): + op.create_table( + 'share_lot', + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_to_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint(['user_to_id'], ['common.user.id']), + sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id']), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + + +def downgrade(): + op.drop_table('share_lot', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/migrations/versions/378b6b147b46_nullable.py b/ereuse_devicehub/migrations/versions/378b6b147b46_nullable.py index 4bc48443..a941f6c0 100644 --- a/ereuse_devicehub/migrations/versions/378b6b147b46_nullable.py +++ b/ereuse_devicehub/migrations/versions/378b6b147b46_nullable.py @@ -5,12 +5,12 @@ Revises: bf600ca861a4 Create Date: 2020-12-16 11:45:13.339624 """ -from alembic import context -from alembic import op +import citext import sqlalchemy as sa import sqlalchemy_utils -import citext -import teal +from alembic import context +from alembic import op +from ereuse_devicehub import teal # revision identifiers, used by Alembic. diff --git a/ereuse_devicehub/migrations/versions/3a3601ac8224_tradedocuments.py b/ereuse_devicehub/migrations/versions/3a3601ac8224_tradedocuments.py index 19e65087..8a2e8cf9 100644 --- a/ereuse_devicehub/migrations/versions/3a3601ac8224_tradedocuments.py +++ b/ereuse_devicehub/migrations/versions/3a3601ac8224_tradedocuments.py @@ -5,15 +5,14 @@ Revises: 51439cf24be8 Create Date: 2021-06-15 14:38:59.931818 """ -import teal import citext import sqlalchemy as sa +from ereuse_devicehub import teal from alembic import op from alembic import context from sqlalchemy.dialects import postgresql - # revision identifiers, used by Alembic. revision = '3a3601ac8224' down_revision = '51439cf24be8' @@ -27,108 +26,143 @@ def get_inv(): raise ValueError("Inventory value is not specified") return INV + def upgrade(): - op.create_table('trade_document', - sa.Column( - 'updated', - sa.TIMESTAMP(timezone=True), - server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n ' - ), - sa.Column( - 'created', - sa.TIMESTAMP(timezone=True), - server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='When Devicehub created this.' - ), - sa.Column( - 'id', - sa.BigInteger(), - nullable=False, - comment='The identifier of the device for this database. Used only\n internally for software; users should not use this.\n ' - ), - sa.Column( - 'date', - sa.DateTime(), - nullable=True, - comment='The date of document, some documents need to have one date\n ' - ), - sa.Column( - 'id_document', - citext.CIText(), - nullable=True, - comment='The id of one document like invoice so they can be linked.' - ), - sa.Column( - 'description', - citext.CIText(), - nullable=True, - comment='A description of document.' - ), - sa.Column( - 'owner_id', - postgresql.UUID(as_uuid=True), - nullable=False - ), - sa.Column( - 'lot_id', - postgresql.UUID(as_uuid=True), - nullable=False - ), - sa.Column( - 'file_name', - citext.CIText(), - nullable=True, - comment='This is the name of the file when user up the document.' - ), - sa.Column( - 'file_hash', - citext.CIText(), - nullable=True, - comment='This is the hash of the file produced from frontend.' - ), - sa.Column( - 'url', - citext.CIText(), - teal.db.URL(), - nullable=True, - comment='This is the url where resides the document.' - ), - sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id'],), - sa.ForeignKeyConstraint(['owner_id'], ['common.user.id'],), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' + op.create_table( + 'trade_document', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column( + 'id', + sa.BigInteger(), + nullable=False, + comment='The identifier of the device for this database. Used only\n internally for software; users should not use this.\n ', + ), + sa.Column( + 'date', + sa.DateTime(), + nullable=True, + comment='The date of document, some documents need to have one date\n ', + ), + sa.Column( + 'id_document', + citext.CIText(), + nullable=True, + comment='The id of one document like invoice so they can be linked.', + ), + sa.Column( + 'description', + citext.CIText(), + nullable=True, + comment='A description of document.', + ), + sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column( + 'file_name', + citext.CIText(), + nullable=True, + comment='This is the name of the file when user up the document.', + ), + sa.Column( + 'file_hash', + citext.CIText(), + nullable=True, + comment='This is the hash of the file produced from frontend.', + ), + sa.Column( + 'url', + citext.CIText(), + teal.db.URL(), + nullable=True, + comment='This is the url where resides the document.', + ), + sa.ForeignKeyConstraint( + ['lot_id'], + [f'{get_inv()}.lot.id'], + ), + sa.ForeignKeyConstraint( + ['owner_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', ) # Action document table - op.create_table('action_trade_document', - sa.Column('document_id', sa.BigInteger(), nullable=False), - sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['action_id'], [f'{get_inv()}.action.id'], ), - sa.ForeignKeyConstraint(['document_id'], [f'{get_inv()}.trade_document.id'], ), - sa.PrimaryKeyConstraint('document_id', 'action_id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'action_trade_document', + sa.Column('document_id', sa.BigInteger(), nullable=False), + sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['action_id'], + [f'{get_inv()}.action.id'], + ), + sa.ForeignKeyConstraint( + ['document_id'], + [f'{get_inv()}.trade_document.id'], + ), + sa.PrimaryKeyConstraint('document_id', 'action_id'), + schema=f'{get_inv()}', + ) - op.create_index('document_id', 'trade_document', ['id'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') - op.create_index(op.f('ix_trade_document_created'), 'trade_document', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_trade_document_updated'), 'trade_document', ['updated'], unique=False, schema=f'{get_inv()}') + op.create_index( + 'document_id', + 'trade_document', + ['id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_trade_document_created'), + 'trade_document', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_trade_document_updated'), + 'trade_document', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) - op.create_table('confirm_document', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), + op.create_table( + 'confirm_document', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.ForeignKeyConstraint( + ['action_id'], + [f'{get_inv()}.action.id'], + ), + sa.ForeignKeyConstraint( + ['user_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.ForeignKeyConstraint(['action_id'], [f'{get_inv()}.action.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) def downgrade(): op.drop_table('action_trade_document', schema=f'{get_inv()}') op.drop_table('confirm_document', schema=f'{get_inv()}') op.drop_table('trade_document', schema=f'{get_inv()}') - diff --git a/ereuse_devicehub/migrations/versions/4f33137586dd_sanitization.py b/ereuse_devicehub/migrations/versions/4f33137586dd_sanitization.py index c546483d..3643bef7 100644 --- a/ereuse_devicehub/migrations/versions/4f33137586dd_sanitization.py +++ b/ereuse_devicehub/migrations/versions/4f33137586dd_sanitization.py @@ -7,10 +7,11 @@ Create Date: 2023-02-13 18:01:00.092527 """ import citext import sqlalchemy as sa -import teal from alembic import context, op from sqlalchemy.dialects import postgresql +from ereuse_devicehub import teal + # revision identifiers, used by Alembic. revision = '4f33137586dd' down_revision = '8334535d56fa' diff --git a/ereuse_devicehub/migrations/versions/7ecb8ff7abad_documents.py b/ereuse_devicehub/migrations/versions/7ecb8ff7abad_documents.py index 83daaf61..5e2c00cf 100644 --- a/ereuse_devicehub/migrations/versions/7ecb8ff7abad_documents.py +++ b/ereuse_devicehub/migrations/versions/7ecb8ff7abad_documents.py @@ -9,7 +9,7 @@ from alembic import op import sqlalchemy as sa import sqlalchemy_utils import citext -import teal +from ereuse_devicehub import teal from alembic import op from alembic import context @@ -32,51 +32,98 @@ def get_inv(): def upgrade(): # Document table - op.create_table('document', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Document recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Document created this.'), - sa.Column('document_type', sa.Unicode(), nullable=False), - sa.Column('date', sa.TIMESTAMP(timezone=True), nullable=True), - sa.Column('id_document', sa.Unicode(), nullable=True), - sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('file_name', sa.Unicode(), nullable=False), - sa.Column('file_hash', sa.Unicode(), nullable=False), - sa.Column('url', sa.Unicode(), nullable=True), - - sa.ForeignKeyConstraint(['owner_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) - op.create_index('generic_document_id', 'document', ['id'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') - op.create_index(op.f('ix_document_created'), 'document', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_document_updated'), 'document', ['updated'], unique=False, schema=f'{get_inv()}') - op.create_index('document_type_index', 'document', ['document_type'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') - + op.create_table( + 'document', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Document recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Document created this.', + ), + sa.Column('document_type', sa.Unicode(), nullable=False), + sa.Column('date', sa.TIMESTAMP(timezone=True), nullable=True), + sa.Column('id_document', sa.Unicode(), nullable=True), + sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('file_name', sa.Unicode(), nullable=False), + sa.Column('file_hash', sa.Unicode(), nullable=False), + sa.Column('url', sa.Unicode(), nullable=True), + sa.ForeignKeyConstraint( + ['owner_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.create_index( + 'generic_document_id', + 'document', + ['id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_document_created'), + 'document', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_document_updated'), + 'document', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + 'document_type_index', + 'document', + ['document_type'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) # DataWipeDocument table - op.create_table('data_wipe_document', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.Column('software', sa.Unicode(), nullable=True), - sa.Column('success', sa.Boolean(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.document.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) - + op.create_table( + 'data_wipe_document', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('software', sa.Unicode(), nullable=True), + sa.Column('success', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.document.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # DataWipe table - op.create_table('data_wipe', - sa.Column('document_id', sa.BigInteger(), nullable=False), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['document_id'], [f'{get_inv()}.document.id'], ), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'data_wipe', + sa.Column('document_id', sa.BigInteger(), nullable=False), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['document_id'], + [f'{get_inv()}.document.id'], + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) def downgrade(): diff --git a/ereuse_devicehub/migrations/versions/968b79fa7756_upgrade_confirmrevoke.py b/ereuse_devicehub/migrations/versions/968b79fa7756_upgrade_confirmrevoke.py index bd3a995d..cb066b1a 100644 --- a/ereuse_devicehub/migrations/versions/968b79fa7756_upgrade_confirmrevoke.py +++ b/ereuse_devicehub/migrations/versions/968b79fa7756_upgrade_confirmrevoke.py @@ -10,7 +10,7 @@ from alembic import context import sqlalchemy as sa import sqlalchemy_utils import citext -import teal +from ereuse_devicehub import teal # revision identifiers, used by Alembic. @@ -26,10 +26,10 @@ def get_inv(): raise ValueError("Inventory value is not specified") return INV + def upgrade(): con = op.get_bind() - confirmsRevokes_sql = f"select * from {get_inv()}.action as action join {get_inv()}.confirm as confirm on action.id=confirm.id where action.type='ConfirmRevoke'" revokes_sql = f"select confirm.id, confirm.action_id from {get_inv()}.action as action join {get_inv()}.confirm as confirm on action.id=confirm.id where action.type='Revoke'" confirmsRevokes = [a for a in con.execute(confirmsRevokes_sql)] @@ -40,12 +40,12 @@ def upgrade(): revoke_id = ac.action_id trade_id = revokes[revoke_id] sql_action = f"update {get_inv()}.action set type='Revoke' where id='{ac_id}'" - sql_confirm = f"update {get_inv()}.confirm set action_id='{trade_id}' where id='{ac_id}'" + sql_confirm = ( + f"update {get_inv()}.confirm set action_id='{trade_id}' where id='{ac_id}'" + ) con.execute(sql_action) con.execute(sql_confirm) - - def downgrade(): pass diff --git a/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py b/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py new file mode 100644 index 00000000..f2685d94 --- /dev/null +++ b/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py @@ -0,0 +1,101 @@ +"""add document device + +Revision ID: ac476b60d952 +Revises: 4f33137586dd +Create Date: 2023-03-31 10:46:02.463007 + +""" +import citext +import sqlalchemy as sa +from alembic import context, op +from sqlalchemy.dialects import postgresql + +from ereuse_devicehub import teal + +# revision identifiers, used by Alembic. +revision = 'ac476b60d952' +down_revision = '4f33137586dd' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + + +def upgrade(): + op.create_table( + 'device_document', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column( + 'id', + postgresql.UUID(as_uuid=True), + nullable=False, + ), + sa.Column( + 'type', + citext.CIText(), + nullable=True, + ), + sa.Column( + 'date', + sa.DateTime(), + nullable=True, + ), + sa.Column( + 'id_document', + citext.CIText(), + nullable=True, + ), + sa.Column( + 'description', + citext.CIText(), + nullable=True, + ), + sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('device_id', sa.BigInteger(), nullable=False), + sa.Column( + 'file_name', + citext.CIText(), + nullable=True, + ), + sa.Column( + 'file_hash', + citext.CIText(), + nullable=True, + ), + sa.Column( + 'url', + citext.CIText(), + teal.db.URL(), + nullable=True, + ), + sa.ForeignKeyConstraint( + ['device_id'], + [f'{get_inv()}.device.id'], + ), + sa.ForeignKeyConstraint( + ['owner_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + + +def downgrade(): + op.drop_table('device_document', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/migrations/versions/b4bd1538bad5_update_live.py b/ereuse_devicehub/migrations/versions/b4bd1538bad5_update_live.py index f340df6a..d54cb16f 100644 --- a/ereuse_devicehub/migrations/versions/b4bd1538bad5_update_live.py +++ b/ereuse_devicehub/migrations/versions/b4bd1538bad5_update_live.py @@ -6,7 +6,7 @@ Create Date: 2020-12-29 20:19:46.981207 """ import sqlalchemy as sa -import teal +from ereuse_devicehub import teal from alembic import context, op from sqlalchemy.dialects import postgresql diff --git a/ereuse_devicehub/migrations/versions/bf600ca861a4_adding_hid.py b/ereuse_devicehub/migrations/versions/bf600ca861a4_adding_hid.py index ae68e3ed..76f2c0f2 100644 --- a/ereuse_devicehub/migrations/versions/bf600ca861a4_adding_hid.py +++ b/ereuse_devicehub/migrations/versions/bf600ca861a4_adding_hid.py @@ -10,7 +10,7 @@ from alembic import op import sqlalchemy as sa import sqlalchemy_utils import citext -import teal +from ereuse_devicehub import teal # revision identifiers, used by Alembic. @@ -26,6 +26,7 @@ def get_inv(): raise ValueError("Inventory value is not specified") return INV + def upgrade(): con = op.get_bind() sql = f""" @@ -60,6 +61,5 @@ def upgrade(): con.execute(sql) - def downgrade(): pass diff --git a/ereuse_devicehub/migrations/versions/e93aec8fc41f_added_assigned_action.py b/ereuse_devicehub/migrations/versions/e93aec8fc41f_added_assigned_action.py index b335256d..f6b97f88 100644 --- a/ereuse_devicehub/migrations/versions/e93aec8fc41f_added_assigned_action.py +++ b/ereuse_devicehub/migrations/versions/e93aec8fc41f_added_assigned_action.py @@ -10,7 +10,7 @@ import sqlalchemy as sa from alembic import context import sqlalchemy_utils import citext -import teal +from ereuse_devicehub import teal from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. @@ -26,48 +26,85 @@ def get_inv(): raise ValueError("Inventory value is not specified") return INV + def upgrade(): # Allocate action op.drop_table('allocate', schema=f'{get_inv()}') - op.create_table('allocate', - sa.Column('final_user_code', citext.CIText(), default='', nullable=True, - comment = "This is a internal code for mainteing the secrets of the personal datas of the new holder"), - sa.Column('transaction', citext.CIText(), nullable=True, comment='The code used from the owner for relation with external tool.'), + op.create_table( + 'allocate', + sa.Column( + 'final_user_code', + citext.CIText(), + default='', + nullable=True, + comment="This is a internal code for mainteing the secrets of the personal datas of the new holder", + ), + sa.Column( + 'transaction', + citext.CIText(), + nullable=True, + comment='The code used from the owner for relation with external tool.', + ), sa.Column('end_users', sa.Numeric(precision=4), nullable=True), sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' + schema=f'{get_inv()}', ) # Deallocate action op.drop_table('deallocate', schema=f'{get_inv()}') - op.create_table('deallocate', - sa.Column('transaction', citext.CIText(), nullable=True, comment='The code used from the owner for relation with external tool.'), + op.create_table( + 'deallocate', + sa.Column( + 'transaction', + citext.CIText(), + nullable=True, + comment='The code used from the owner for relation with external tool.', + ), sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' + schema=f'{get_inv()}', ) # Add allocate as a column in device - op.add_column('device', sa.Column('allocated', sa.Boolean(), nullable=True), schema=f'{get_inv()}') + op.add_column( + 'device', + sa.Column('allocated', sa.Boolean(), nullable=True), + schema=f'{get_inv()}', + ) # Receive action op.drop_table('receive', schema=f'{get_inv()}') # Live action op.drop_table('live', schema=f'{get_inv()}') - op.create_table('live', + op.create_table( + 'live', sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('serial_number', sa.Unicode(), nullable=True, - comment='The serial number of the Hard Disk in lower case.'), + sa.Column( + 'serial_number', + sa.Unicode(), + nullable=True, + comment='The serial number of the Hard Disk in lower case.', + ), sa.Column('usage_time_hdd', sa.Interval(), nullable=True), sa.Column('snapshot_uuid', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' + schema=f'{get_inv()}', ) + def downgrade(): op.drop_table('allocate', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/migrations/versions/fbb7e2a0cde0_initial.py b/ereuse_devicehub/migrations/versions/fbb7e2a0cde0_initial.py index b6ad7490..08758ec3 100644 --- a/ereuse_devicehub/migrations/versions/fbb7e2a0cde0_initial.py +++ b/ereuse_devicehub/migrations/versions/fbb7e2a0cde0_initial.py @@ -8,7 +8,7 @@ Create Date: 2020-05-07 10:04:40.269511 import citext import sqlalchemy as sa import sqlalchemy_utils -import teal +from ereuse_devicehub import teal from alembic import context from alembic import op from sqlalchemy.dialects import postgresql @@ -35,1565 +35,6789 @@ def upgrade(): op.execute(f"create schema {get_inv()}") # Inventory table - op.create_table('inventory', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', sa.Unicode(), nullable=False, - comment='The name of the inventory as in the URL and schema.'), - sa.Column('name', citext.CIText(), nullable=False, comment='The human name of the inventory.'), - sa.Column('tag_provider', teal.db.URL(), nullable=False), - sa.Column('tag_token', postgresql.UUID(as_uuid=True), nullable=False, - comment='The token to access a Tag service.'), - sa.Column('org_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name'), - sa.UniqueConstraint('tag_token'), - schema='common' - ) - op.create_index('id_hash', 'inventory', ['id'], unique=False, schema='common', postgresql_using='hash') - op.create_index(op.f('ix_common_inventory_created'), 'inventory', ['created'], unique=False, schema='common') - op.create_index(op.f('ix_common_inventory_updated'), 'inventory', ['updated'], unique=False, schema='common') + op.create_table( + 'inventory', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column( + 'id', + sa.Unicode(), + nullable=False, + comment='The name of the inventory as in the URL and schema.', + ), + sa.Column( + 'name', + citext.CIText(), + nullable=False, + comment='The human name of the inventory.', + ), + sa.Column('tag_provider', teal.db.URL(), nullable=False), + sa.Column( + 'tag_token', + postgresql.UUID(as_uuid=True), + nullable=False, + comment='The token to access a Tag service.', + ), + sa.Column('org_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name'), + sa.UniqueConstraint('tag_token'), + schema='common', + ) + op.create_index( + 'id_hash', + 'inventory', + ['id'], + unique=False, + schema='common', + postgresql_using='hash', + ) + op.create_index( + op.f('ix_common_inventory_created'), + 'inventory', + ['created'], + unique=False, + schema='common', + ) + op.create_index( + op.f('ix_common_inventory_updated'), + 'inventory', + ['updated'], + unique=False, + schema='common', + ) # Manufacturer table - op.create_table('manufacturer', - sa.Column('name', citext.CIText(), nullable=False, - comment='The normalized name of the manufacturer.'), - sa.Column('url', teal.db.URL(), nullable=True, - comment='An URL to a page describing the manufacturer.'), - sa.Column('logo', teal.db.URL(), nullable=True, - comment='An URL pointing to the logo of the manufacturer.'), - sa.PrimaryKeyConstraint('name'), - sa.UniqueConstraint('url'), - schema='common' - ) + op.create_table( + 'manufacturer', + sa.Column( + 'name', + citext.CIText(), + nullable=False, + comment='The normalized name of the manufacturer.', + ), + sa.Column( + 'url', + teal.db.URL(), + nullable=True, + comment='An URL to a page describing the manufacturer.', + ), + sa.Column( + 'logo', + teal.db.URL(), + nullable=True, + comment='An URL pointing to the logo of the manufacturer.', + ), + sa.PrimaryKeyConstraint('name'), + sa.UniqueConstraint('url'), + schema='common', + ) # User table - op.create_table('user', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('email', sqlalchemy_utils.types.email.EmailType(length=255), nullable=False), - sa.Column('password', sqlalchemy_utils.types.password.PasswordType(max_length=64), nullable=True), - sa.Column('token', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('ethereum_address', citext.CIText(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('ethereum_address'), - sa.UniqueConstraint('token'), - schema='common' - ) - op.create_index(op.f('ix_common_user_created'), 'user', ['created'], unique=False, schema='common') - op.create_index(op.f('ix_common_user_updated'), 'user', ['updated'], unique=False, schema='common') + op.create_table( + 'user', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column( + 'email', sqlalchemy_utils.types.email.EmailType(length=255), nullable=False + ), + sa.Column( + 'password', + sqlalchemy_utils.types.password.PasswordType(max_length=64), + nullable=True, + ), + sa.Column('token', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ethereum_address', citext.CIText(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('ethereum_address'), + sa.UniqueConstraint('token'), + schema='common', + ) + op.create_index( + op.f('ix_common_user_created'), + 'user', + ['created'], + unique=False, + schema='common', + ) + op.create_index( + op.f('ix_common_user_updated'), + 'user', + ['updated'], + unique=False, + schema='common', + ) # User Inventory table - op.create_table('user_inventory', - sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('inventory_id', sa.Unicode(), nullable=False), - sa.ForeignKeyConstraint(['inventory_id'], ['common.inventory.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('user_id', 'inventory_id'), - schema='common' - ) + op.create_table( + 'user_inventory', + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('inventory_id', sa.Unicode(), nullable=False), + sa.ForeignKeyConstraint( + ['inventory_id'], + ['common.inventory.id'], + ), + sa.ForeignKeyConstraint( + ['user_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('user_id', 'inventory_id'), + schema='common', + ) # Device table - op.create_table('device', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', sa.BigInteger(), nullable=False, - comment='The identifier of the device for this database. Used only\n internally for software; users should not use this.\n '), - sa.Column('type', sa.Unicode(length=32), nullable=False), - sa.Column('hid', sa.Unicode(), nullable=True, - comment='The Hardware ID (HID) is the unique ID traceability\n systems use to ID a device globally. This field is auto-generated\n from Devicehub using literal identifiers from the device,\n so it can re-generated *offline*.\n \n The HID is the result of concatenating,\n in the following order: the type of device (ex. Computer),\n the manufacturer name, the model name, and the S/N. It is joined\n with hyphens, and adapted to comply with the URI specification, so\n it can be used in the URI identifying the device on the Internet.\n The conversion is done as follows:\n \n 1. non-ASCII characters are converted to their ASCII equivalent or\n removed.\n 2. Characterst that are not letters or numbers are converted to \n underscores, in a way that there are no trailing underscores\n and no underscores together, and they are set to lowercase.\n \n Ex. ``laptop-acer-aod270-lusga_0d0242201212c7614``\n '), - sa.Column('model', sa.Unicode(), nullable=True, - comment='The model of the device in lower case.\n\n\n The model is the unambiguous, as technical as possible, denomination\n for the product. This field, among others, is used to identify\n the product.\n '), - sa.Column('manufacturer', sa.Unicode(), nullable=True, - comment='The normalized name of the manufacturer,\n in lower case.\n\n Although as of now Devicehub does not enforce normalization,\n users can choose a list of normalized manufacturer names\n from the own ``/manufacturers`` REST endpoint.\n '), - sa.Column('serial_number', sa.Unicode(), nullable=True, - comment='The serial number of the device in lower case.'), - sa.Column('brand', citext.CIText(), nullable=True, - comment='A naming for consumers. This field can represent\n several models, so it can be ambiguous, and it is not used to\n identify the product.\n '), - sa.Column('generation', sa.SmallInteger(), nullable=True, comment='The generation of the device.'), - sa.Column('version', citext.CIText(), nullable=True, - comment='The version code of this device, like v1 or A001.'), - sa.Column('weight', sa.Float(decimal_return_scale=4), nullable=True, - comment='The weight of the device in Kg.'), - sa.Column('width', sa.Float(decimal_return_scale=4), nullable=True, - comment='The width of the device in meters.'), - sa.Column('height', sa.Float(decimal_return_scale=4), nullable=True, - comment='The height of the device in meters.'), - sa.Column('depth', sa.Float(decimal_return_scale=4), nullable=True, - comment='The depth of the device in meters.'), - sa.Column('color', sqlalchemy_utils.types.color.ColorType(length=20), nullable=True, - comment='The predominant color of the device.'), - sa.Column('production_date', sa.DateTime(), nullable=True, - comment='The date of production of the device.\n This is timezone naive, as Workbench cannot report this data with timezone information.\n '), - sa.Column('variant', citext.CIText(), nullable=True, - comment='A variant or sub-model of the device.'), - sa.Column('sku', citext.CIText(), nullable=True, - comment='The Stock Keeping Unit (SKU), i.e. a\n merchant-specific identifier for a product or service.\n '), - sa.Column('image', teal.db.URL(), nullable=True, comment='An image of the device.'), - sa.Column('max_drill_bit_size', sa.SmallInteger(), nullable=True), - sa.Column('size', sa.SmallInteger(), nullable=True, comment='The capacity in Liters.'), - sa.Column('max_allowed_weight', sa.Integer(), nullable=True), - sa.Column('wheel_size', sa.SmallInteger(), nullable=True), - sa.Column('gears', sa.SmallInteger(), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) - op.create_index('device_id', 'device', ['id'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') - op.create_index(op.f('ix_device_created'), 'device', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_device_updated'), 'device', ['updated'], unique=False, schema=f'{get_inv()}') - op.create_index('type_index', 'device', ['type'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') - op.create_table('agent', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('type', sa.Unicode(), nullable=False), - sa.Column('name', citext.CIText(), nullable=True, - comment='The name of the organization or person.'), - sa.Column('tax_id', sa.Unicode(length=32), nullable=True, - comment='The Tax / Fiscal ID of the organization, \n e.g. the TIN in the US or the CIF/NIF in Spain.\n '), - sa.Column('country', - sa.Enum('AF', 'AX', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', 'AR', 'AM', 'AW', - 'AU', 'AT', 'AZ', 'BS', 'BH', 'BD', 'BB', 'BY', 'BE', 'BZ', 'BJ', 'BM', 'BT', - 'BO', 'BQ', 'BA', 'BW', 'BV', 'BR', 'IO', 'BN', 'BG', 'BF', 'BI', 'KH', 'CM', - 'CA', 'CV', 'KY', 'CF', 'TD', 'CL', 'CN', 'CX', 'CC', 'CO', 'KM', 'CG', 'CD', - 'CK', 'CR', 'CI', 'HR', 'CU', 'CW', 'CY', 'CZ', 'DK', 'DJ', 'DM', 'DO', 'EC', - 'EG', 'SV', 'GQ', 'ER', 'EE', 'ET', 'FK', 'FO', 'FJ', 'FI', 'FR', 'GF', 'PF', - 'TF', 'GA', 'GM', 'GE', 'DE', 'GH', 'GI', 'GR', 'GL', 'GD', 'GP', 'GU', 'GT', - 'GG', 'GN', 'GW', 'GY', 'HT', 'HM', 'VA', 'HN', 'HK', 'HU', 'IS', 'IN', 'ID', - 'IR', 'IQ', 'IE', 'IM', 'IL', 'IT', 'JM', 'JP', 'JE', 'JO', 'KZ', 'KE', 'KI', - 'KP', 'KR', 'KW', 'KG', 'LA', 'LV', 'LB', 'LS', 'LR', 'LY', 'LI', 'LT', 'LU', - 'MO', 'MK', 'MG', 'MW', 'MY', 'MV', 'ML', 'MT', 'MH', 'MQ', 'MR', 'MU', 'YT', - 'MX', 'FM', 'MD', 'MC', 'MN', 'ME', 'MS', 'MA', 'MZ', 'MM', 'NA', 'NR', 'NP', - 'NL', 'NC', 'NZ', 'NI', 'NE', 'NG', 'NU', 'NF', 'MP', 'NO', 'OM', 'PK', 'PW', - 'PS', 'PA', 'PG', 'PY', 'PE', 'PH', 'PN', 'PL', 'PT', 'PR', 'QA', 'RE', 'RO', - 'RU', 'RW', 'BL', 'SH', 'KN', 'LC', 'MF', 'PM', 'VC', 'WS', 'SM', 'ST', 'SA', - 'SN', 'RS', 'SC', 'SL', 'SG', 'SX', 'SK', 'SI', 'SB', 'SO', 'ZA', 'GS', 'SS', - 'ES', 'LK', 'SD', 'SR', 'SJ', 'SZ', 'SE', 'CH', 'SY', 'TW', 'TJ', 'TZ', 'TH', - 'TL', 'TG', 'TK', 'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', - 'GB', 'US', 'UM', 'UY', 'UZ', 'VU', 'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', - 'ZM', 'ZW', name='country'), nullable=True, - comment='Country issuing the tax_id number.'), - sa.Column('telephone', sqlalchemy_utils.types.phone_number.PhoneNumberType(length=20), - nullable=True), - sa.Column('email', sqlalchemy_utils.types.email.EmailType(length=255), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email'), - sa.UniqueConstraint('tax_id', 'country', name='Registration Number per country.'), - sa.UniqueConstraint('tax_id', 'name', name='One tax ID with one name.'), - schema=f'{get_inv()}' - ) - op.create_index('agent_type', 'agent', ['type'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') - op.create_index(op.f('ix_agent_created'), 'agent', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_agent_updated'), 'agent', ['updated'], unique=False, schema=f'{get_inv()}') + op.create_table( + 'device', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column( + 'id', + sa.BigInteger(), + nullable=False, + comment='The identifier of the device for this database. Used only\n internally for software; users should not use this.\n ', + ), + sa.Column('type', sa.Unicode(length=32), nullable=False), + sa.Column( + 'hid', + sa.Unicode(), + nullable=True, + comment='The Hardware ID (HID) is the unique ID traceability\n systems use to ID a device globally. This field is auto-generated\n from Devicehub using literal identifiers from the device,\n so it can re-generated *offline*.\n \n The HID is the result of concatenating,\n in the following order: the type of device (ex. Computer),\n the manufacturer name, the model name, and the S/N. It is joined\n with hyphens, and adapted to comply with the URI specification, so\n it can be used in the URI identifying the device on the Internet.\n The conversion is done as follows:\n \n 1. non-ASCII characters are converted to their ASCII equivalent or\n removed.\n 2. Characterst that are not letters or numbers are converted to \n underscores, in a way that there are no trailing underscores\n and no underscores together, and they are set to lowercase.\n \n Ex. ``laptop-acer-aod270-lusga_0d0242201212c7614``\n ', + ), + sa.Column( + 'model', + sa.Unicode(), + nullable=True, + comment='The model of the device in lower case.\n\n\n The model is the unambiguous, as technical as possible, denomination\n for the product. This field, among others, is used to identify\n the product.\n ', + ), + sa.Column( + 'manufacturer', + sa.Unicode(), + nullable=True, + comment='The normalized name of the manufacturer,\n in lower case.\n\n Although as of now Devicehub does not enforce normalization,\n users can choose a list of normalized manufacturer names\n from the own ``/manufacturers`` REST endpoint.\n ', + ), + sa.Column( + 'serial_number', + sa.Unicode(), + nullable=True, + comment='The serial number of the device in lower case.', + ), + sa.Column( + 'brand', + citext.CIText(), + nullable=True, + comment='A naming for consumers. This field can represent\n several models, so it can be ambiguous, and it is not used to\n identify the product.\n ', + ), + sa.Column( + 'generation', + sa.SmallInteger(), + nullable=True, + comment='The generation of the device.', + ), + sa.Column( + 'version', + citext.CIText(), + nullable=True, + comment='The version code of this device, like v1 or A001.', + ), + sa.Column( + 'weight', + sa.Float(decimal_return_scale=4), + nullable=True, + comment='The weight of the device in Kg.', + ), + sa.Column( + 'width', + sa.Float(decimal_return_scale=4), + nullable=True, + comment='The width of the device in meters.', + ), + sa.Column( + 'height', + sa.Float(decimal_return_scale=4), + nullable=True, + comment='The height of the device in meters.', + ), + sa.Column( + 'depth', + sa.Float(decimal_return_scale=4), + nullable=True, + comment='The depth of the device in meters.', + ), + sa.Column( + 'color', + sqlalchemy_utils.types.color.ColorType(length=20), + nullable=True, + comment='The predominant color of the device.', + ), + sa.Column( + 'production_date', + sa.DateTime(), + nullable=True, + comment='The date of production of the device.\n This is timezone naive, as Workbench cannot report this data with timezone information.\n ', + ), + sa.Column( + 'variant', + citext.CIText(), + nullable=True, + comment='A variant or sub-model of the device.', + ), + sa.Column( + 'sku', + citext.CIText(), + nullable=True, + comment='The Stock Keeping Unit (SKU), i.e. a\n merchant-specific identifier for a product or service.\n ', + ), + sa.Column( + 'image', teal.db.URL(), nullable=True, comment='An image of the device.' + ), + sa.Column('max_drill_bit_size', sa.SmallInteger(), nullable=True), + sa.Column( + 'size', sa.SmallInteger(), nullable=True, comment='The capacity in Liters.' + ), + sa.Column('max_allowed_weight', sa.Integer(), nullable=True), + sa.Column('wheel_size', sa.SmallInteger(), nullable=True), + sa.Column('gears', sa.SmallInteger(), nullable=True), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.create_index( + 'device_id', + 'device', + ['id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_device_created'), + 'device', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_device_updated'), + 'device', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + 'type_index', + 'device', + ['type'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_table( + 'agent', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('type', sa.Unicode(), nullable=False), + sa.Column( + 'name', + citext.CIText(), + nullable=True, + comment='The name of the organization or person.', + ), + sa.Column( + 'tax_id', + sa.Unicode(length=32), + nullable=True, + comment='The Tax / Fiscal ID of the organization, \n e.g. the TIN in the US or the CIF/NIF in Spain.\n ', + ), + sa.Column( + 'country', + sa.Enum( + 'AF', + 'AX', + 'AL', + 'DZ', + 'AS', + 'AD', + 'AO', + 'AI', + 'AQ', + 'AG', + 'AR', + 'AM', + 'AW', + 'AU', + 'AT', + 'AZ', + 'BS', + 'BH', + 'BD', + 'BB', + 'BY', + 'BE', + 'BZ', + 'BJ', + 'BM', + 'BT', + 'BO', + 'BQ', + 'BA', + 'BW', + 'BV', + 'BR', + 'IO', + 'BN', + 'BG', + 'BF', + 'BI', + 'KH', + 'CM', + 'CA', + 'CV', + 'KY', + 'CF', + 'TD', + 'CL', + 'CN', + 'CX', + 'CC', + 'CO', + 'KM', + 'CG', + 'CD', + 'CK', + 'CR', + 'CI', + 'HR', + 'CU', + 'CW', + 'CY', + 'CZ', + 'DK', + 'DJ', + 'DM', + 'DO', + 'EC', + 'EG', + 'SV', + 'GQ', + 'ER', + 'EE', + 'ET', + 'FK', + 'FO', + 'FJ', + 'FI', + 'FR', + 'GF', + 'PF', + 'TF', + 'GA', + 'GM', + 'GE', + 'DE', + 'GH', + 'GI', + 'GR', + 'GL', + 'GD', + 'GP', + 'GU', + 'GT', + 'GG', + 'GN', + 'GW', + 'GY', + 'HT', + 'HM', + 'VA', + 'HN', + 'HK', + 'HU', + 'IS', + 'IN', + 'ID', + 'IR', + 'IQ', + 'IE', + 'IM', + 'IL', + 'IT', + 'JM', + 'JP', + 'JE', + 'JO', + 'KZ', + 'KE', + 'KI', + 'KP', + 'KR', + 'KW', + 'KG', + 'LA', + 'LV', + 'LB', + 'LS', + 'LR', + 'LY', + 'LI', + 'LT', + 'LU', + 'MO', + 'MK', + 'MG', + 'MW', + 'MY', + 'MV', + 'ML', + 'MT', + 'MH', + 'MQ', + 'MR', + 'MU', + 'YT', + 'MX', + 'FM', + 'MD', + 'MC', + 'MN', + 'ME', + 'MS', + 'MA', + 'MZ', + 'MM', + 'NA', + 'NR', + 'NP', + 'NL', + 'NC', + 'NZ', + 'NI', + 'NE', + 'NG', + 'NU', + 'NF', + 'MP', + 'NO', + 'OM', + 'PK', + 'PW', + 'PS', + 'PA', + 'PG', + 'PY', + 'PE', + 'PH', + 'PN', + 'PL', + 'PT', + 'PR', + 'QA', + 'RE', + 'RO', + 'RU', + 'RW', + 'BL', + 'SH', + 'KN', + 'LC', + 'MF', + 'PM', + 'VC', + 'WS', + 'SM', + 'ST', + 'SA', + 'SN', + 'RS', + 'SC', + 'SL', + 'SG', + 'SX', + 'SK', + 'SI', + 'SB', + 'SO', + 'ZA', + 'GS', + 'SS', + 'ES', + 'LK', + 'SD', + 'SR', + 'SJ', + 'SZ', + 'SE', + 'CH', + 'SY', + 'TW', + 'TJ', + 'TZ', + 'TH', + 'TL', + 'TG', + 'TK', + 'TO', + 'TT', + 'TN', + 'TR', + 'TM', + 'TC', + 'TV', + 'UG', + 'UA', + 'AE', + 'GB', + 'US', + 'UM', + 'UY', + 'UZ', + 'VU', + 'VE', + 'VN', + 'VG', + 'VI', + 'WF', + 'EH', + 'YE', + 'ZM', + 'ZW', + name='country', + ), + nullable=True, + comment='Country issuing the tax_id number.', + ), + sa.Column( + 'telephone', + sqlalchemy_utils.types.phone_number.PhoneNumberType(length=20), + nullable=True, + ), + sa.Column( + 'email', sqlalchemy_utils.types.email.EmailType(length=255), nullable=True + ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint( + 'tax_id', 'country', name='Registration Number per country.' + ), + sa.UniqueConstraint('tax_id', 'name', name='One tax ID with one name.'), + schema=f'{get_inv()}', + ) + op.create_index( + 'agent_type', + 'agent', + ['type'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_agent_created'), + 'agent', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_agent_updated'), + 'agent', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) # Computer table - op.create_table('computer', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.Column('chassis', - sa.Enum('Tower', 'Docking', 'AllInOne', 'Microtower', 'PizzaBox', 'Lunchbox', 'Stick', - 'Netbook', 'Handheld', 'Laptop', 'Convertible', 'Detachable', 'Tablet', 'Virtual', - name='computerchassis'), nullable=False, - comment='The physical form of the computer.\n\n It is a subset of the Linux definition of DMI / DMI decode.\n '), - sa.Column('ethereum_address', citext.CIText(), nullable=True), - sa.Column('deposit', sa.Integer(), nullable=True), - sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('transfer_state', teal.db.IntEnum(TransferState), nullable=False, - comment='State of transfer for a given Lot of devices.\n '), - sa.Column('receiver_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('deliverynote_address', citext.CIText(), nullable=True), - sa.Column('layout', - sa.Enum('US', 'AF', 'ARA', 'AL', 'AM', 'AT', 'AU', 'AZ', 'BY', 'BE', 'BD', 'BA', 'BR', - 'BG', 'DZ', 'MA', 'CM', 'MM', 'CA', 'CD', 'CN', 'HR', 'CZ', 'DK', 'NL', 'BT', - 'EE', 'IR', 'IQ', 'FO', 'FI', 'FR', 'GH', 'GN', 'GE', 'DE', 'GR', 'HU', 'IL', - 'IT', 'JP', 'KG', 'KH', 'KZ', 'LA', 'LATAM', 'LT', 'LV', 'MAO', 'ME', 'MK', 'MT', - 'MN', 'NO', 'PL', 'PT', 'RO', 'RU', 'RS', 'SI', 'SK', 'ES', 'SE', 'CH', 'SY', - 'TJ', 'LK', 'TH', 'TR', 'TW', 'UA', 'GB', 'UZ', 'VN', 'KR', 'IE', 'PK', 'MV', - 'ZA', 'EPO', 'NP', 'NG', 'ET', 'SN', 'BRAI', 'TM', 'ML', 'TZ', 'TG', 'KE', 'BW', - 'PH', 'MD', 'ID', 'MY', 'BN', 'IN', 'IS', 'NEC_VNDR_JP', name='layouts'), - nullable=True, - comment='Layout of a built-in keyboard of the computer,\n if any.\n '), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.device.id'], ), - sa.ForeignKeyConstraint(['owner_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['receiver_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('ethereum_address'), - schema=f'{get_inv()}' - ) + op.create_table( + 'computer', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column( + 'chassis', + sa.Enum( + 'Tower', + 'Docking', + 'AllInOne', + 'Microtower', + 'PizzaBox', + 'Lunchbox', + 'Stick', + 'Netbook', + 'Handheld', + 'Laptop', + 'Convertible', + 'Detachable', + 'Tablet', + 'Virtual', + name='computerchassis', + ), + nullable=False, + comment='The physical form of the computer.\n\n It is a subset of the Linux definition of DMI / DMI decode.\n ', + ), + sa.Column('ethereum_address', citext.CIText(), nullable=True), + sa.Column('deposit', sa.Integer(), nullable=True), + sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column( + 'transfer_state', + teal.db.IntEnum(TransferState), + nullable=False, + comment='State of transfer for a given Lot of devices.\n ', + ), + sa.Column('receiver_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('deliverynote_address', citext.CIText(), nullable=True), + sa.Column( + 'layout', + sa.Enum( + 'US', + 'AF', + 'ARA', + 'AL', + 'AM', + 'AT', + 'AU', + 'AZ', + 'BY', + 'BE', + 'BD', + 'BA', + 'BR', + 'BG', + 'DZ', + 'MA', + 'CM', + 'MM', + 'CA', + 'CD', + 'CN', + 'HR', + 'CZ', + 'DK', + 'NL', + 'BT', + 'EE', + 'IR', + 'IQ', + 'FO', + 'FI', + 'FR', + 'GH', + 'GN', + 'GE', + 'DE', + 'GR', + 'HU', + 'IL', + 'IT', + 'JP', + 'KG', + 'KH', + 'KZ', + 'LA', + 'LATAM', + 'LT', + 'LV', + 'MAO', + 'ME', + 'MK', + 'MT', + 'MN', + 'NO', + 'PL', + 'PT', + 'RO', + 'RU', + 'RS', + 'SI', + 'SK', + 'ES', + 'SE', + 'CH', + 'SY', + 'TJ', + 'LK', + 'TH', + 'TR', + 'TW', + 'UA', + 'GB', + 'UZ', + 'VN', + 'KR', + 'IE', + 'PK', + 'MV', + 'ZA', + 'EPO', + 'NP', + 'NG', + 'ET', + 'SN', + 'BRAI', + 'TM', + 'ML', + 'TZ', + 'TG', + 'KE', + 'BW', + 'PH', + 'MD', + 'ID', + 'MY', + 'BN', + 'IN', + 'IS', + 'NEC_VNDR_JP', + name='layouts', + ), + nullable=True, + comment='Layout of a built-in keyboard of the computer,\n if any.\n ', + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.device.id'], + ), + sa.ForeignKeyConstraint( + ['owner_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['receiver_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('ethereum_address'), + schema=f'{get_inv()}', + ) # Computer accessory - op.create_table('computer_accessory', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.Column('layout', - sa.Enum('US', 'AF', 'ARA', 'AL', 'AM', 'AT', 'AU', 'AZ', 'BY', 'BE', 'BD', 'BA', 'BR', - 'BG', 'DZ', 'MA', 'CM', 'MM', 'CA', 'CD', 'CN', 'HR', 'CZ', 'DK', 'NL', 'BT', - 'EE', 'IR', 'IQ', 'FO', 'FI', 'FR', 'GH', 'GN', 'GE', 'DE', 'GR', 'HU', 'IL', - 'IT', 'JP', 'KG', 'KH', 'KZ', 'LA', 'LATAM', 'LT', 'LV', 'MAO', 'ME', 'MK', 'MT', - 'MN', 'NO', 'PL', 'PT', 'RO', 'RU', 'RS', 'SI', 'SK', 'ES', 'SE', 'CH', 'SY', - 'TJ', 'LK', 'TH', 'TR', 'TW', 'UA', 'GB', 'UZ', 'VN', 'KR', 'IE', 'PK', 'MV', - 'ZA', 'EPO', 'NP', 'NG', 'ET', 'SN', 'BRAI', 'TM', 'ML', 'TZ', 'TG', 'KE', 'BW', - 'PH', 'MD', 'ID', 'MY', 'BN', 'IN', 'IS', 'NEC_VNDR_JP', name='layouts'), - nullable=True), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'computer_accessory', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column( + 'layout', + sa.Enum( + 'US', + 'AF', + 'ARA', + 'AL', + 'AM', + 'AT', + 'AU', + 'AZ', + 'BY', + 'BE', + 'BD', + 'BA', + 'BR', + 'BG', + 'DZ', + 'MA', + 'CM', + 'MM', + 'CA', + 'CD', + 'CN', + 'HR', + 'CZ', + 'DK', + 'NL', + 'BT', + 'EE', + 'IR', + 'IQ', + 'FO', + 'FI', + 'FR', + 'GH', + 'GN', + 'GE', + 'DE', + 'GR', + 'HU', + 'IL', + 'IT', + 'JP', + 'KG', + 'KH', + 'KZ', + 'LA', + 'LATAM', + 'LT', + 'LV', + 'MAO', + 'ME', + 'MK', + 'MT', + 'MN', + 'NO', + 'PL', + 'PT', + 'RO', + 'RU', + 'RS', + 'SI', + 'SK', + 'ES', + 'SE', + 'CH', + 'SY', + 'TJ', + 'LK', + 'TH', + 'TR', + 'TW', + 'UA', + 'GB', + 'UZ', + 'VN', + 'KR', + 'IE', + 'PK', + 'MV', + 'ZA', + 'EPO', + 'NP', + 'NG', + 'ET', + 'SN', + 'BRAI', + 'TM', + 'ML', + 'TZ', + 'TG', + 'KE', + 'BW', + 'PH', + 'MD', + 'ID', + 'MY', + 'BN', + 'IN', + 'IS', + 'NEC_VNDR_JP', + name='layouts', + ), + nullable=True, + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Device search table - op.create_table('device_search', - sa.Column('device_id', sa.BigInteger(), nullable=False), - sa.Column('properties', postgresql.TSVECTOR(), nullable=False), - sa.Column('tags', postgresql.TSVECTOR(), nullable=True), - sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.device.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('device_id'), - schema=f'{get_inv()}' - ) - op.create_index('properties gist', 'device_search', ['properties'], unique=False, postgresql_using='gist', - schema=f'{get_inv()}') - op.create_index('tags gist', 'device_search', ['tags'], unique=False, postgresql_using='gist', - schema=f'{get_inv()}') + op.create_table( + 'device_search', + sa.Column('device_id', sa.BigInteger(), nullable=False), + sa.Column('properties', postgresql.TSVECTOR(), nullable=False), + sa.Column('tags', postgresql.TSVECTOR(), nullable=True), + sa.ForeignKeyConstraint( + ['device_id'], [f'{get_inv()}.device.id'], ondelete='CASCADE' + ), + sa.PrimaryKeyConstraint('device_id'), + schema=f'{get_inv()}', + ) + op.create_index( + 'properties gist', + 'device_search', + ['properties'], + unique=False, + postgresql_using='gist', + schema=f'{get_inv()}', + ) + op.create_index( + 'tags gist', + 'device_search', + ['tags'], + unique=False, + postgresql_using='gist', + schema=f'{get_inv()}', + ) # Lot table - op.create_table('lot', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('name', citext.CIText(), nullable=False), - sa.Column('description', citext.CIText(), nullable=True, comment='A comment about the lot.'), - sa.Column('closed', sa.Boolean(), nullable=False, - comment='A closed lot cannot be modified anymore.'), - sa.Column('deposit', sa.Integer(), nullable=True), - sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('transfer_state', teal.db.IntEnum(TransferState), nullable=False, - comment='State of transfer for a given Lot of devices.\n '), - sa.Column('receiver_address', citext.CIText(), nullable=True), - sa.Column('deliverynote_address', citext.CIText(), nullable=True), - sa.ForeignKeyConstraint(['owner_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['receiver_address'], ['common.user.ethereum_address'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) - op.create_index(op.f('ix_lot_created'), 'lot', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_lot_updated'), 'lot', ['updated'], unique=False, schema=f'{get_inv()}') + op.create_table( + 'lot', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', citext.CIText(), nullable=False), + sa.Column( + 'description', + citext.CIText(), + nullable=True, + comment='A comment about the lot.', + ), + sa.Column( + 'closed', + sa.Boolean(), + nullable=False, + comment='A closed lot cannot be modified anymore.', + ), + sa.Column('deposit', sa.Integer(), nullable=True), + sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column( + 'transfer_state', + teal.db.IntEnum(TransferState), + nullable=False, + comment='State of transfer for a given Lot of devices.\n ', + ), + sa.Column('receiver_address', citext.CIText(), nullable=True), + sa.Column('deliverynote_address', citext.CIText(), nullable=True), + sa.ForeignKeyConstraint( + ['owner_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['receiver_address'], + ['common.user.ethereum_address'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_lot_created'), 'lot', ['created'], unique=False, schema=f'{get_inv()}' + ) + op.create_index( + op.f('ix_lot_updated'), 'lot', ['updated'], unique=False, schema=f'{get_inv()}' + ) # Mobile table - op.create_table('mobile', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.Column('imei', sa.BigInteger(), nullable=True, - comment='The International Mobile Equipment Identity of\n the smartphone as an integer.\n '), - sa.Column('meid', sa.Unicode(), nullable=True, - comment='The Mobile Equipment Identifier as a hexadecimal\n string.\n '), - sa.Column('ram_size', sa.Integer(), nullable=True, comment='The total of RAM of the device in MB.'), - sa.Column('data_storage_size', sa.Integer(), nullable=True, - comment='The total of data storage of the device in MB'), - sa.Column('display_size', sa.Float(decimal_return_scale=1), nullable=True, - comment='The total size of the device screen'), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'mobile', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column( + 'imei', + sa.BigInteger(), + nullable=True, + comment='The International Mobile Equipment Identity of\n the smartphone as an integer.\n ', + ), + sa.Column( + 'meid', + sa.Unicode(), + nullable=True, + comment='The Mobile Equipment Identifier as a hexadecimal\n string.\n ', + ), + sa.Column( + 'ram_size', + sa.Integer(), + nullable=True, + comment='The total of RAM of the device in MB.', + ), + sa.Column( + 'data_storage_size', + sa.Integer(), + nullable=True, + comment='The total of data storage of the device in MB', + ), + sa.Column( + 'display_size', + sa.Float(decimal_return_scale=1), + nullable=True, + comment='The total size of the device screen', + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Monitor table - op.create_table('monitor', - sa.Column('size', sa.Float(decimal_return_scale=1), nullable=False, - comment='The size of the monitor in inches.'), - sa.Column('technology', - sa.Enum('CRT', 'TFT', 'LED', 'PDP', 'LCD', 'OLED', 'AMOLED', name='displaytech'), - nullable=True, - comment='The technology the monitor uses to display\n the image.\n '), - sa.Column('resolution_width', sa.SmallInteger(), nullable=False, - comment='The maximum horizontal resolution the\n monitor can natively support in pixels.\n '), - sa.Column('resolution_height', sa.SmallInteger(), nullable=False, - comment='The maximum vertical resolution the\n monitor can natively support in pixels.\n '), - sa.Column('refresh_rate', sa.SmallInteger(), nullable=True), - sa.Column('contrast_ratio', sa.SmallInteger(), nullable=True), - sa.Column('touchable', sa.Boolean(), nullable=True, comment='Whether it is a touchscreen.'), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'monitor', + sa.Column( + 'size', + sa.Float(decimal_return_scale=1), + nullable=False, + comment='The size of the monitor in inches.', + ), + sa.Column( + 'technology', + sa.Enum( + 'CRT', 'TFT', 'LED', 'PDP', 'LCD', 'OLED', 'AMOLED', name='displaytech' + ), + nullable=True, + comment='The technology the monitor uses to display\n the image.\n ', + ), + sa.Column( + 'resolution_width', + sa.SmallInteger(), + nullable=False, + comment='The maximum horizontal resolution the\n monitor can natively support in pixels.\n ', + ), + sa.Column( + 'resolution_height', + sa.SmallInteger(), + nullable=False, + comment='The maximum vertical resolution the\n monitor can natively support in pixels.\n ', + ), + sa.Column('refresh_rate', sa.SmallInteger(), nullable=True), + sa.Column('contrast_ratio', sa.SmallInteger(), nullable=True), + sa.Column( + 'touchable', + sa.Boolean(), + nullable=True, + comment='Whether it is a touchscreen.', + ), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Networtking table - op.create_table('networking', - sa.Column('speed', sa.SmallInteger(), nullable=True, - comment='The maximum speed this network adapter can handle,\n in mbps.\n '), - sa.Column('wireless', sa.Boolean(), nullable=False, comment='Whether it is a wireless interface.'), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'networking', + sa.Column( + 'speed', + sa.SmallInteger(), + nullable=True, + comment='The maximum speed this network adapter can handle,\n in mbps.\n ', + ), + sa.Column( + 'wireless', + sa.Boolean(), + nullable=False, + comment='Whether it is a wireless interface.', + ), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Organization table - op.create_table('organization', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.agent.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'organization', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.agent.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Printer table - op.create_table('printer', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.Column('wireless', sa.Boolean(), nullable=False, comment='Whether it is a wireless printer.'), - sa.Column('scanning', sa.Boolean(), nullable=False, - comment='Whether the printer has scanning capabilities.'), - sa.Column('technology', - sa.Enum('Toner', 'Inkjet', 'SolidInk', 'Dye', 'Thermal', name='printertechnology'), - nullable=True, comment='Technology used to print.'), - sa.Column('monochrome', sa.Boolean(), nullable=False, - comment='Whether the printer is only monochrome.'), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'printer', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column( + 'wireless', + sa.Boolean(), + nullable=False, + comment='Whether it is a wireless printer.', + ), + sa.Column( + 'scanning', + sa.Boolean(), + nullable=False, + comment='Whether the printer has scanning capabilities.', + ), + sa.Column( + 'technology', + sa.Enum( + 'Toner', + 'Inkjet', + 'SolidInk', + 'Dye', + 'Thermal', + name='printertechnology', + ), + nullable=True, + comment='Technology used to print.', + ), + sa.Column( + 'monochrome', + sa.Boolean(), + nullable=False, + comment='Whether the printer is only monochrome.', + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Proof table - op.create_table('proof', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('type', sa.Unicode(), nullable=False), - sa.Column('ethereum_hash', citext.CIText(), nullable=False), - sa.Column('device_id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) - op.create_index(op.f('ix_proof_created'), 'proof', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_proof_updated'), 'proof', ['updated'], unique=False, schema=f'{get_inv()}') + op.create_table( + 'proof', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('type', sa.Unicode(), nullable=False), + sa.Column('ethereum_hash', citext.CIText(), nullable=False), + sa.Column('device_id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['device_id'], + [f'{get_inv()}.device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_proof_created'), + 'proof', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_proof_updated'), + 'proof', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) # Action table - op.create_table('action', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('type', sa.Unicode(), nullable=False), - sa.Column('name', citext.CIText(), nullable=False, - comment='A name or title for the action. Used when searching\n for actions.\n '), - sa.Column('severity', teal.db.IntEnum(Severity), nullable=False, - comment='A flag evaluating the action execution. Ex. failed actions\n have the value `Severity.Error`. Devicehub uses 4 severity levels:\n\n * Info (Pass): default neutral severity. The action succeeded.\n * Notice: The action succeeded but it is raising awareness.\n Notices are not usually that important but something\n (good or bad) worth checking.\n * Warning: The action succeeded but there is something important\n to check negatively affecting the action.\n * Error (Fail): the action failed.\n\n Devicehub specially raises user awareness when an action\n has a Severity of ``Warning`` or greater.\n '), - sa.Column('closed', sa.Boolean(), nullable=False, - comment='Whether the author has finished the action.\n After this is set to True, no modifications are allowed.\n By default actions are closed when performed.\n '), - sa.Column('description', sa.Unicode(), nullable=False, comment='A comment about the action.'), - sa.Column('start_time', sa.TIMESTAMP(timezone=True), nullable=True, - comment='When the action starts. For some actions like\n reservations the time when they are available, for others like renting\n when the renting starts.\n '), - sa.Column('end_time', sa.TIMESTAMP(timezone=True), nullable=True, - comment='When the action ends. For some actions like reservations\n the time when they expire, for others like renting\n the time the end rents. For punctual actions it is the time \n they are performed; it differs with ``created`` in which\n created is the where the system received the action.\n '), - sa.Column('snapshot_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('author_id', postgresql.UUID(as_uuid=True), nullable=False, - comment='The user that recorded this action in the system.\n \n This does not necessarily has to be the person that produced\n the action in the real world. For that purpose see\n ``agent``.\n '), - sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=False, - comment='The direct performer or driver of the action. \n e.g. John wrote a book.\n \n It can differ with the user that registered the action in the\n system, which can be in their behalf.\n '), - sa.Column('parent_id', sa.BigInteger(), nullable=True, - comment='For actions that are performed to components, \n the device parent at that time.\n \n For example: for a ``EraseBasic`` performed on a data storage, this\n would point to the computer that contained this data storage, if any.\n '), - sa.ForeignKeyConstraint(['agent_id'], [f'{get_inv()}.agent.id'], ), - sa.ForeignKeyConstraint(['author_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['parent_id'], [f'{get_inv()}.computer.id'], ), - sa.ForeignKeyConstraint(['snapshot_id'], [f'{get_inv()}.snapshot.id'], name='snapshot_actions', - use_alter=True), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) - op.create_index(op.f('ix_action_created'), 'action', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_action_updated'), 'action', ['updated'], unique=False, schema=f'{get_inv()}') - op.create_index('ix_id', 'action', ['id'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') - op.create_index('ix_parent_id', 'action', ['parent_id'], unique=False, postgresql_using='hash', - schema=f'{get_inv()}') - op.create_index('ix_type', 'action', ['type'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') + op.create_table( + 'action', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('type', sa.Unicode(), nullable=False), + sa.Column( + 'name', + citext.CIText(), + nullable=False, + comment='A name or title for the action. Used when searching\n for actions.\n ', + ), + sa.Column( + 'severity', + teal.db.IntEnum(Severity), + nullable=False, + comment='A flag evaluating the action execution. Ex. failed actions\n have the value `Severity.Error`. Devicehub uses 4 severity levels:\n\n * Info (Pass): default neutral severity. The action succeeded.\n * Notice: The action succeeded but it is raising awareness.\n Notices are not usually that important but something\n (good or bad) worth checking.\n * Warning: The action succeeded but there is something important\n to check negatively affecting the action.\n * Error (Fail): the action failed.\n\n Devicehub specially raises user awareness when an action\n has a Severity of ``Warning`` or greater.\n ', + ), + sa.Column( + 'closed', + sa.Boolean(), + nullable=False, + comment='Whether the author has finished the action.\n After this is set to True, no modifications are allowed.\n By default actions are closed when performed.\n ', + ), + sa.Column( + 'description', + sa.Unicode(), + nullable=False, + comment='A comment about the action.', + ), + sa.Column( + 'start_time', + sa.TIMESTAMP(timezone=True), + nullable=True, + comment='When the action starts. For some actions like\n reservations the time when they are available, for others like renting\n when the renting starts.\n ', + ), + sa.Column( + 'end_time', + sa.TIMESTAMP(timezone=True), + nullable=True, + comment='When the action ends. For some actions like reservations\n the time when they expire, for others like renting\n the time the end rents. For punctual actions it is the time \n they are performed; it differs with ``created`` in which\n created is the where the system received the action.\n ', + ), + sa.Column('snapshot_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column( + 'author_id', + postgresql.UUID(as_uuid=True), + nullable=False, + comment='The user that recorded this action in the system.\n \n This does not necessarily has to be the person that produced\n the action in the real world. For that purpose see\n ``agent``.\n ', + ), + sa.Column( + 'agent_id', + postgresql.UUID(as_uuid=True), + nullable=False, + comment='The direct performer or driver of the action. \n e.g. John wrote a book.\n \n It can differ with the user that registered the action in the\n system, which can be in their behalf.\n ', + ), + sa.Column( + 'parent_id', + sa.BigInteger(), + nullable=True, + comment='For actions that are performed to components, \n the device parent at that time.\n \n For example: for a ``EraseBasic`` performed on a data storage, this\n would point to the computer that contained this data storage, if any.\n ', + ), + sa.ForeignKeyConstraint( + ['agent_id'], + [f'{get_inv()}.agent.id'], + ), + sa.ForeignKeyConstraint( + ['author_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['parent_id'], + [f'{get_inv()}.computer.id'], + ), + sa.ForeignKeyConstraint( + ['snapshot_id'], + [f'{get_inv()}.snapshot.id'], + name='snapshot_actions', + use_alter=True, + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_action_created'), + 'action', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_action_updated'), + 'action', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + 'ix_id', + 'action', + ['id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_index( + 'ix_parent_id', + 'action', + ['parent_id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_index( + 'ix_type', + 'action', + ['type'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) # Component table - op.create_table('component', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.Column('parent_id', sa.BigInteger(), nullable=True), - sa.Column('focal_length', sa.SmallInteger(), nullable=True), - sa.Column('video_height', sa.SmallInteger(), nullable=True), - sa.Column('video_width', sa.Integer(), nullable=True), - sa.Column('horizontal_view_angle', sa.Integer(), nullable=True), - sa.Column('facing', sa.Enum('Front', 'Back', name='camerafacing'), nullable=True), - sa.Column('vertical_view_angle', sa.SmallInteger(), nullable=True), - sa.Column('video_stabilization', sa.Boolean(), nullable=True), - sa.Column('flash', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.device.id'], ), - sa.ForeignKeyConstraint(['parent_id'], [f'{get_inv()}.computer.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) - op.create_index('parent_index', 'component', ['parent_id'], unique=False, postgresql_using='hash', - schema=f'{get_inv()}') + op.create_table( + 'component', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('parent_id', sa.BigInteger(), nullable=True), + sa.Column('focal_length', sa.SmallInteger(), nullable=True), + sa.Column('video_height', sa.SmallInteger(), nullable=True), + sa.Column('video_width', sa.Integer(), nullable=True), + sa.Column('horizontal_view_angle', sa.Integer(), nullable=True), + sa.Column( + 'facing', sa.Enum('Front', 'Back', name='camerafacing'), nullable=True + ), + sa.Column('vertical_view_angle', sa.SmallInteger(), nullable=True), + sa.Column('video_stabilization', sa.Boolean(), nullable=True), + sa.Column('flash', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.device.id'], + ), + sa.ForeignKeyConstraint( + ['parent_id'], + [f'{get_inv()}.computer.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.create_index( + 'parent_index', + 'component', + ['parent_id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) # Deliverynote table - op.create_table('deliverynote', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('document_id', citext.CIText(), nullable=False), - sa.Column('creator_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('supplier_email', citext.CIText(), nullable=False), - sa.Column('receiver_address', citext.CIText(), nullable=False), - sa.Column('date', sa.DateTime(), nullable=False, comment='The date the DeliveryNote initiated'), - sa.Column('deposit', sa.Integer(), nullable=True), - sa.Column('expected_devices', postgresql.JSONB(astext_type=sa.Text()), nullable=False), - sa.Column('transferred_devices', sa.ARRAY(sa.Integer(), dimensions=1), nullable=True), - sa.Column('transfer_state', teal.db.IntEnum(TransferState), nullable=False, - comment='State of transfer for a given Lot of devices.\n '), - sa.Column('ethereum_address', citext.CIText(), nullable=True), - sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['creator_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id'], ), - sa.ForeignKeyConstraint(['receiver_address'], ['common.user.email'], ), - sa.ForeignKeyConstraint(['supplier_email'], ['common.user.email'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('ethereum_address'), - schema=f'{get_inv()}' - ) - op.create_index(op.f('ix_deliverynote_created'), 'deliverynote', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_deliverynote_updated'), 'deliverynote', ['updated'], unique=False, schema=f'{get_inv()}') + op.create_table( + 'deliverynote', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('document_id', citext.CIText(), nullable=False), + sa.Column('creator_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('supplier_email', citext.CIText(), nullable=False), + sa.Column('receiver_address', citext.CIText(), nullable=False), + sa.Column( + 'date', + sa.DateTime(), + nullable=False, + comment='The date the DeliveryNote initiated', + ), + sa.Column('deposit', sa.Integer(), nullable=True), + sa.Column( + 'expected_devices', postgresql.JSONB(astext_type=sa.Text()), nullable=False + ), + sa.Column( + 'transferred_devices', sa.ARRAY(sa.Integer(), dimensions=1), nullable=True + ), + sa.Column( + 'transfer_state', + teal.db.IntEnum(TransferState), + nullable=False, + comment='State of transfer for a given Lot of devices.\n ', + ), + sa.Column('ethereum_address', citext.CIText(), nullable=True), + sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['creator_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['lot_id'], + [f'{get_inv()}.lot.id'], + ), + sa.ForeignKeyConstraint( + ['receiver_address'], + ['common.user.email'], + ), + sa.ForeignKeyConstraint( + ['supplier_email'], + ['common.user.email'], + ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('ethereum_address'), + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_deliverynote_created'), + 'deliverynote', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_deliverynote_updated'), + 'deliverynote', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) # Individual table - op.create_table('individual', - sa.Column('active_org_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['active_org_id'], [f'{get_inv()}.organization.id'], ), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.agent.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'individual', + sa.Column('active_org_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['active_org_id'], + [f'{get_inv()}.organization.id'], + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.agent.id'], + ), + sa.ForeignKeyConstraint( + ['user_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id'), + schema=f'{get_inv()}', + ) # Lot device table - op.create_table('lot_device', - sa.Column('device_id', sa.BigInteger(), nullable=False), - sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('created', sa.DateTime(), nullable=False), - sa.Column('author_id', postgresql.UUID(as_uuid=True), nullable=False, - comment='The user that put the device in the lot.'), - sa.ForeignKeyConstraint(['author_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.device.id'], ), - sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id'], ), - sa.PrimaryKeyConstraint('device_id', 'lot_id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'lot_device', + sa.Column('device_id', sa.BigInteger(), nullable=False), + sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column( + 'author_id', + postgresql.UUID(as_uuid=True), + nullable=False, + comment='The user that put the device in the lot.', + ), + sa.ForeignKeyConstraint( + ['author_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['device_id'], + [f'{get_inv()}.device.id'], + ), + sa.ForeignKeyConstraint( + ['lot_id'], + [f'{get_inv()}.lot.id'], + ), + sa.PrimaryKeyConstraint('device_id', 'lot_id'), + schema=f'{get_inv()}', + ) # Path table - op.create_table('path', - sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()'), - nullable=False), - sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('path', sqlalchemy_utils.types.ltree.LtreeType(), nullable=False), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=True, comment='When Devicehub created this.'), - sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('path', deferrable='True', initially='immediate', name='path_unique'), - schema=f'{get_inv()}' - ) - op.create_index('lot_id_index', 'path', ['lot_id'], unique=False, postgresql_using='hash', schema=f'{get_inv()}') - op.create_index('path_btree', 'path', ['path'], unique=False, postgresql_using='btree', schema=f'{get_inv()}') - op.create_index('path_gist', 'path', ['path'], unique=False, postgresql_using='gist', schema=f'{get_inv()}') + op.create_table( + 'path', + sa.Column( + 'id', + postgresql.UUID(as_uuid=True), + server_default=sa.text('gen_random_uuid()'), + nullable=False, + ), + sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('path', sqlalchemy_utils.types.ltree.LtreeType(), nullable=False), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=True, + comment='When Devicehub created this.', + ), + sa.ForeignKeyConstraint( + ['lot_id'], + [f'{get_inv()}.lot.id'], + ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint( + 'path', deferrable='True', initially='immediate', name='path_unique' + ), + schema=f'{get_inv()}', + ) + op.create_index( + 'lot_id_index', + 'path', + ['lot_id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_index( + 'path_btree', + 'path', + ['path'], + unique=False, + postgresql_using='btree', + schema=f'{get_inv()}', + ) + op.create_index( + 'path_gist', + 'path', + ['path'], + unique=False, + postgresql_using='gist', + schema=f'{get_inv()}', + ) # Proof recycling table - op.create_table('proof_recycling', - sa.Column('collection_point', citext.CIText(), nullable=False), - sa.Column('date', sa.DateTime(), nullable=False), - sa.Column('contact', citext.CIText(), nullable=False), - sa.Column('ticket', citext.CIText(), nullable=False), - sa.Column('gps_location', citext.CIText(), nullable=False), - sa.Column('recycler_code', citext.CIText(), nullable=False), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.proof.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'proof_recycling', + sa.Column('collection_point', citext.CIText(), nullable=False), + sa.Column('date', sa.DateTime(), nullable=False), + sa.Column('contact', citext.CIText(), nullable=False), + sa.Column('ticket', citext.CIText(), nullable=False), + sa.Column('gps_location', citext.CIText(), nullable=False), + sa.Column('recycler_code', citext.CIText(), nullable=False), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.proof.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Proof reuse table - op.create_table('proof_reuse', - sa.Column('receiver_segment', citext.CIText(), nullable=False), - sa.Column('id_receipt', citext.CIText(), nullable=False), - sa.Column('supplier_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('receiver_id', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('price', sa.Integer(), nullable=True), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.proof.id'], ), - sa.ForeignKeyConstraint(['receiver_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['supplier_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'proof_reuse', + sa.Column('receiver_segment', citext.CIText(), nullable=False), + sa.Column('id_receipt', citext.CIText(), nullable=False), + sa.Column('supplier_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('receiver_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('price', sa.Integer(), nullable=True), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.proof.id'], + ), + sa.ForeignKeyConstraint( + ['receiver_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['supplier_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Proof transfer table - op.create_table('proof_transfer', - sa.Column('supplier_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('receiver_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('deposit', sa.Integer(), nullable=True), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.proof.id'], ), - sa.ForeignKeyConstraint(['receiver_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['supplier_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'proof_transfer', + sa.Column('supplier_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('receiver_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('deposit', sa.Integer(), nullable=True), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.proof.id'], + ), + sa.ForeignKeyConstraint( + ['receiver_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['supplier_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Tag table - op.create_table('tag', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', citext.CIText(), nullable=False, comment='The ID of the tag.'), - sa.Column('org_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('provider', teal.db.URL(), nullable=True, - comment='The tag provider URL. If None, the provider is\n this Devicehub.\n '), - sa.Column('device_id', sa.BigInteger(), nullable=True), - sa.Column('secondary', citext.CIText(), nullable=True, - comment='A secondary identifier for this tag. \n It has the same constraints as the main one. Only needed in special cases.\n '), - sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.device.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['org_id'], [f'{get_inv()}.organization.id'], ), - sa.PrimaryKeyConstraint('id', 'org_id'), - sa.UniqueConstraint('id', 'org_id', name='one tag id per organization'), - sa.UniqueConstraint('secondary', 'org_id', name='one secondary tag per organization'), - schema=f'{get_inv()}' - ) - op.create_index('device_id_index', 'tag', ['device_id'], unique=False, postgresql_using='hash', - schema=f'{get_inv()}') - op.create_index(op.f('ix_tag_created'), 'tag', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_tag_secondary'), 'tag', ['secondary'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_tag_updated'), 'tag', ['updated'], unique=False, schema=f'{get_inv()}') + op.create_table( + 'tag', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column('id', citext.CIText(), nullable=False, comment='The ID of the tag.'), + sa.Column('org_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column( + 'provider', + teal.db.URL(), + nullable=True, + comment='The tag provider URL. If None, the provider is\n this Devicehub.\n ', + ), + sa.Column('device_id', sa.BigInteger(), nullable=True), + sa.Column( + 'secondary', + citext.CIText(), + nullable=True, + comment='A secondary identifier for this tag. \n It has the same constraints as the main one. Only needed in special cases.\n ', + ), + sa.ForeignKeyConstraint( + ['device_id'], [f'{get_inv()}.device.id'], ondelete='SET NULL' + ), + sa.ForeignKeyConstraint( + ['org_id'], + [f'{get_inv()}.organization.id'], + ), + sa.PrimaryKeyConstraint('id', 'org_id'), + sa.UniqueConstraint('id', 'org_id', name='one tag id per organization'), + sa.UniqueConstraint( + 'secondary', 'org_id', name='one secondary tag per organization' + ), + schema=f'{get_inv()}', + ) + op.create_index( + 'device_id_index', + 'tag', + ['device_id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_tag_created'), 'tag', ['created'], unique=False, schema=f'{get_inv()}' + ) + op.create_index( + op.f('ix_tag_secondary'), + 'tag', + ['secondary'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_tag_updated'), 'tag', ['updated'], unique=False, schema=f'{get_inv()}' + ) # ActionComponent table - op.create_table('action_component', - sa.Column('device_id', sa.BigInteger(), nullable=False), - sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['action_id'], [f'{get_inv()}.action.id'], ), - sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('device_id', 'action_id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'action_component', + sa.Column('device_id', sa.BigInteger(), nullable=False), + sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['action_id'], + [f'{get_inv()}.action.id'], + ), + sa.ForeignKeyConstraint( + ['device_id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('device_id', 'action_id'), + schema=f'{get_inv()}', + ) # Action device table - op.create_table('action_device', - sa.Column('device_id', sa.BigInteger(), nullable=False), - sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['action_id'], [f'{get_inv()}.action.id'], ), - sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.device.id'], ), - sa.PrimaryKeyConstraint('device_id', 'action_id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'action_device', + sa.Column('device_id', sa.BigInteger(), nullable=False), + sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['action_id'], + [f'{get_inv()}.action.id'], + ), + sa.ForeignKeyConstraint( + ['device_id'], + [f'{get_inv()}.device.id'], + ), + sa.PrimaryKeyConstraint('device_id', 'action_id'), + schema=f'{get_inv()}', + ) # ActionWithOneDevice table - op.create_table('action_with_one_device', - sa.Column('device_id', sa.BigInteger(), nullable=False), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.device.id'], ), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) - op.create_index('action_one_device_id_index', 'action_with_one_device', ['device_id'], unique=False, - postgresql_using='hash', schema=f'{get_inv()}') + op.create_table( + 'action_with_one_device', + sa.Column('device_id', sa.BigInteger(), nullable=False), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['device_id'], + [f'{get_inv()}.device.id'], + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.create_index( + 'action_one_device_id_index', + 'action_with_one_device', + ['device_id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) # Allocate table - op.create_table('allocate', - sa.Column('to_id', postgresql.UUID(), nullable=True), - sa.Column('organization', citext.CIText(), nullable=True), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.ForeignKeyConstraint(['to_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'allocate', + sa.Column('to_id', postgresql.UUID(), nullable=True), + sa.Column('organization', citext.CIText(), nullable=True), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.ForeignKeyConstraint( + ['to_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # BAtter table - op.create_table('battery', - sa.Column('wireless', sa.Boolean(), nullable=True, - comment='If the battery can be charged wirelessly.'), - sa.Column('technology', sa.Enum('LiIon', 'NiCd', 'NiMH', 'LiPoly', 'LiFe', 'LiMn', 'Al', - name='batterytechnology'), nullable=True), - sa.Column('size', sa.Integer(), nullable=False, - comment='Maximum battery capacity by design, in mAh.\n\n Use BatteryTest\'s "size" to get the actual size of the battery.\n '), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'battery', + sa.Column( + 'wireless', + sa.Boolean(), + nullable=True, + comment='If the battery can be charged wirelessly.', + ), + sa.Column( + 'technology', + sa.Enum( + 'LiIon', + 'NiCd', + 'NiMH', + 'LiPoly', + 'LiFe', + 'LiMn', + 'Al', + name='batterytechnology', + ), + nullable=True, + ), + sa.Column( + 'size', + sa.Integer(), + nullable=False, + comment='Maximum battery capacity by design, in mAh.\n\n Use BatteryTest\'s "size" to get the actual size of the battery.\n ', + ), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # DataStorage table - op.create_table('data_storage', - sa.Column('size', sa.Integer(), nullable=True, comment='The size of the data-storage in MB.'), - sa.Column('interface', sa.Enum('ATA', 'USB', 'PCI', name='datastorageinterface'), nullable=True), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'data_storage', + sa.Column( + 'size', + sa.Integer(), + nullable=True, + comment='The size of the data-storage in MB.', + ), + sa.Column( + 'interface', + sa.Enum('ATA', 'USB', 'PCI', name='datastorageinterface'), + nullable=True, + ), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Deallocate table - op.create_table('deallocate', - sa.Column('from_id', postgresql.UUID(), nullable=True), - sa.Column('organization', citext.CIText(), nullable=True), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['from_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'deallocate', + sa.Column('from_id', postgresql.UUID(), nullable=True), + sa.Column('organization', citext.CIText(), nullable=True), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['from_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Display table - op.create_table('display', - sa.Column('size', sa.Float(decimal_return_scale=1), nullable=False, - comment='The size of the monitor in inches.'), - sa.Column('technology', - sa.Enum('CRT', 'TFT', 'LED', 'PDP', 'LCD', 'OLED', 'AMOLED', name='displaytech'), - nullable=True, - comment='The technology the monitor uses to display\n the image.\n '), - sa.Column('resolution_width', sa.SmallInteger(), nullable=False, - comment='The maximum horizontal resolution the\n monitor can natively support in pixels.\n '), - sa.Column('resolution_height', sa.SmallInteger(), nullable=False, - comment='The maximum vertical resolution the\n monitor can natively support in pixels.\n '), - sa.Column('refresh_rate', sa.SmallInteger(), nullable=True), - sa.Column('contrast_ratio', sa.SmallInteger(), nullable=True), - sa.Column('touchable', sa.Boolean(), nullable=True, comment='Whether it is a touchscreen.'), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'display', + sa.Column( + 'size', + sa.Float(decimal_return_scale=1), + nullable=False, + comment='The size of the monitor in inches.', + ), + sa.Column( + 'technology', + sa.Enum( + 'CRT', 'TFT', 'LED', 'PDP', 'LCD', 'OLED', 'AMOLED', name='displaytech' + ), + nullable=True, + comment='The technology the monitor uses to display\n the image.\n ', + ), + sa.Column( + 'resolution_width', + sa.SmallInteger(), + nullable=False, + comment='The maximum horizontal resolution the\n monitor can natively support in pixels.\n ', + ), + sa.Column( + 'resolution_height', + sa.SmallInteger(), + nullable=False, + comment='The maximum vertical resolution the\n monitor can natively support in pixels.\n ', + ), + sa.Column('refresh_rate', sa.SmallInteger(), nullable=True), + sa.Column('contrast_ratio', sa.SmallInteger(), nullable=True), + sa.Column( + 'touchable', + sa.Boolean(), + nullable=True, + comment='Whether it is a touchscreen.', + ), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # GraphiCard table - op.create_table('graphic_card', - sa.Column('memory', sa.SmallInteger(), nullable=True, - comment='The amount of memory of the Graphic Card in MB.'), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'graphic_card', + sa.Column( + 'memory', + sa.SmallInteger(), + nullable=True, + comment='The amount of memory of the Graphic Card in MB.', + ), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Membership table - op.create_table('membership', - sa.Column('updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, - comment='The last time Devicehub recorded a change for \n this thing.\n '), - sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), - nullable=False, comment='When Devicehub created this.'), - sa.Column('id', sa.Unicode(), nullable=True), - sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('individual_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['individual_id'], [f'{get_inv()}.individual.id'], ), - sa.ForeignKeyConstraint(['organization_id'], [f'{get_inv()}.organization.id'], ), - sa.PrimaryKeyConstraint('organization_id', 'individual_id'), - sa.UniqueConstraint('id', 'organization_id', name='One member id per organization.'), - schema=f'{get_inv()}' - ) - op.create_index(op.f('ix_membership_created'), 'membership', ['created'], unique=False, schema=f'{get_inv()}') - op.create_index(op.f('ix_membership_updated'), 'membership', ['updated'], unique=False, schema=f'{get_inv()}') + op.create_table( + 'membership', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='The last time Devicehub recorded a change for \n this thing.\n ', + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + comment='When Devicehub created this.', + ), + sa.Column('id', sa.Unicode(), nullable=True), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('individual_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['individual_id'], + [f'{get_inv()}.individual.id'], + ), + sa.ForeignKeyConstraint( + ['organization_id'], + [f'{get_inv()}.organization.id'], + ), + sa.PrimaryKeyConstraint('organization_id', 'individual_id'), + sa.UniqueConstraint( + 'id', 'organization_id', name='One member id per organization.' + ), + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_membership_created'), + 'membership', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_membership_updated'), + 'membership', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) # Migrate table - op.create_table('migrate', - sa.Column('other', teal.db.URL(), nullable=False, - comment='\n The URL of the Migrate in the other end.\n '), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'migrate', + sa.Column( + 'other', + teal.db.URL(), + nullable=False, + comment='\n The URL of the Migrate in the other end.\n ', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Motherboard table - op.create_table('motherboard', - sa.Column('slots', sa.SmallInteger(), nullable=True, comment='PCI slots the motherboard has.'), - sa.Column('usb', sa.SmallInteger(), nullable=True), - sa.Column('firewire', sa.SmallInteger(), nullable=True), - sa.Column('serial', sa.SmallInteger(), nullable=True), - sa.Column('pcmcia', sa.SmallInteger(), nullable=True), - sa.Column('bios_date', sa.Date(), nullable=True, comment='The date of the BIOS version.'), - sa.Column('ram_slots', sa.SmallInteger(), nullable=True), - sa.Column('ram_max_size', sa.Integer(), nullable=True), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'motherboard', + sa.Column( + 'slots', + sa.SmallInteger(), + nullable=True, + comment='PCI slots the motherboard has.', + ), + sa.Column('usb', sa.SmallInteger(), nullable=True), + sa.Column('firewire', sa.SmallInteger(), nullable=True), + sa.Column('serial', sa.SmallInteger(), nullable=True), + sa.Column('pcmcia', sa.SmallInteger(), nullable=True), + sa.Column( + 'bios_date', + sa.Date(), + nullable=True, + comment='The date of the BIOS version.', + ), + sa.Column('ram_slots', sa.SmallInteger(), nullable=True), + sa.Column('ram_max_size', sa.Integer(), nullable=True), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Network adapter - op.create_table('network_adapter', - sa.Column('speed', sa.SmallInteger(), nullable=True, - comment='The maximum speed this network adapter can handle,\n in mbps.\n '), - sa.Column('wireless', sa.Boolean(), nullable=False, comment='Whether it is a wireless interface.'), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'network_adapter', + sa.Column( + 'speed', + sa.SmallInteger(), + nullable=True, + comment='The maximum speed this network adapter can handle,\n in mbps.\n ', + ), + sa.Column( + 'wireless', + sa.Boolean(), + nullable=False, + comment='Whether it is a wireless interface.', + ), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Organize table - op.create_table('organize', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'organize', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Processor table - op.create_table('processor', - sa.Column('speed', sa.Float(), nullable=True, comment='The regular CPU speed.'), - sa.Column('cores', sa.SmallInteger(), nullable=True, comment='The number of regular cores.'), - sa.Column('threads', sa.SmallInteger(), nullable=True, comment='The number of threads per core.'), - sa.Column('address', sa.SmallInteger(), nullable=True, - comment='The address of the CPU: 8, 16, 32, 64, 128 or 256 bits.'), - sa.Column('abi', sa.Unicode(), nullable=True, - comment='The Application Binary Interface of the processor.'), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'processor', + sa.Column('speed', sa.Float(), nullable=True, comment='The regular CPU speed.'), + sa.Column( + 'cores', + sa.SmallInteger(), + nullable=True, + comment='The number of regular cores.', + ), + sa.Column( + 'threads', + sa.SmallInteger(), + nullable=True, + comment='The number of threads per core.', + ), + sa.Column( + 'address', + sa.SmallInteger(), + nullable=True, + comment='The address of the CPU: 8, 16, 32, 64, 128 or 256 bits.', + ), + sa.Column( + 'abi', + sa.Unicode(), + nullable=True, + comment='The Application Binary Interface of the processor.', + ), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # RamModule table - op.create_table('ram_module', - sa.Column('size', sa.SmallInteger(), nullable=True, comment='The capacity of the RAM stick.'), - sa.Column('speed', sa.SmallInteger(), nullable=True), - sa.Column('interface', - sa.Enum('SDRAM', 'DDR', 'DDR2', 'DDR3', 'DDR4', 'DDR5', 'DDR6', name='raminterface'), - nullable=True), - sa.Column('format', sa.Enum('DIMM', 'SODIMM', name='ramformat'), nullable=True), - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'ram_module', + sa.Column( + 'size', + sa.SmallInteger(), + nullable=True, + comment='The capacity of the RAM stick.', + ), + sa.Column('speed', sa.SmallInteger(), nullable=True), + sa.Column( + 'interface', + sa.Enum( + 'SDRAM', + 'DDR', + 'DDR2', + 'DDR3', + 'DDR4', + 'DDR5', + 'DDR6', + name='raminterface', + ), + nullable=True, + ), + sa.Column('format', sa.Enum('DIMM', 'SODIMM', name='ramformat'), nullable=True), + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Receive table - op.create_table('receive', - sa.Column('role', - sa.Enum('Intermediary', 'FinalUser', 'CollectionPoint', 'RecyclingPoint', 'Transporter', - name='receiverrole'), nullable=False), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'receive', + sa.Column( + 'role', + sa.Enum( + 'Intermediary', + 'FinalUser', + 'CollectionPoint', + 'RecyclingPoint', + 'Transporter', + name='receiverrole', + ), + nullable=False, + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Sound card table - op.create_table('sound_card', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.component.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'sound_card', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.component.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Benchmark table - op.create_table('benchmark', - sa.Column('elapsed', sa.Interval(), nullable=True), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action_with_one_device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'benchmark', + sa.Column('elapsed', sa.Interval(), nullable=True), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action_with_one_device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Erase basic table - op.create_table('erase_basic', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('method', sa.Enum('Shred', 'Disintegration', name='physicalerasuremethod'), - nullable=True), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action_with_one_device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'erase_basic', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column( + 'method', + sa.Enum('Shred', 'Disintegration', name='physicalerasuremethod'), + nullable=True, + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action_with_one_device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) - op.create_table('install', - sa.Column('elapsed', sa.Interval(), nullable=False), - sa.Column('address', sa.SmallInteger(), nullable=True), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action_with_one_device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'install', + sa.Column('elapsed', sa.Interval(), nullable=False), + sa.Column('address', sa.SmallInteger(), nullable=True), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action_with_one_device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Live table - op.create_table('live', - sa.Column('ip', teal.db.IP(), nullable=False, comment='The IP where the live was triggered.'), - sa.Column('subdivision_confidence', sa.SmallInteger(), nullable=False), - sa.Column('subdivision', - sa.Enum('AE-AJ', 'AE-AZ', 'AE-DU', 'AE-FU', 'AE-RK', 'AE-SH', 'AE-UQ', 'AF-BAL', 'AF-BAM', - 'AF-BDG', 'AF-BDS', 'AF-BGL', 'AF-FRAU', 'AF-FYB', 'AF-GHA', 'AF-GHO', 'AF-HEL', - 'AF-HER', 'AF-JOW', 'AF-KAB', 'AF-KANN', 'AF-KAP', 'AF-KDZ', 'AF-KNR', 'AF-LAG', - 'AF-LOW', 'AF-NAN', 'AF-NIM', 'AF-ORU', 'AF-PAR', 'AF-PIA', 'AF-PKA', 'AF-SAM', - 'AF-SAR', 'AF-TAK', 'AF-WAR', 'AF-ZAB', 'AL-BR', 'AL-BU', 'AL-DI', 'AL-DL', - 'AL-DR', 'AL-DV', 'AL-EL', 'AL-ER', 'AL-FR', 'AL-GJ', 'AL-GR', 'AL-HA', 'AL-KA', - 'AL-KB', 'AL-KC', 'AL-KO', 'AL-KR', 'AL-KU', 'AL-LA', 'AL-LB', 'AL-LE', 'AL-LU', - 'AL-MK', 'AL-MM', 'AL-MR', 'AL-MT', 'AL-PG', 'AL-PQ', 'AL-PR', 'AL-PU', 'AL-SH', - 'AL-SK', 'AL-SR', 'AL-TE', 'AL-TP', 'AL-TR', 'AL-VL', 'AM-AG', 'AM-AR', 'AM-AV', - 'AM-ER', 'AM-GR', 'AM-KT', 'AM-LO', 'AM-SH', 'AM-SU', 'AM-TV', 'AM-VD', 'AO-BGO', - 'AO-BGU', 'AO-BIE', 'AO-CAB', 'AO-CCU', 'AO-CNN', 'AO-CNO', 'AO-CUS', 'AO-HUA', - 'AO-HUI', 'AO-LNO', 'AO-LSU', 'AO-LUA', 'AO-MAL', 'AO-MOX', 'AO-NAM', 'AO-UIG', - 'AO-ZAI', 'AR-A', 'AR-B', 'AR-C', 'AR-D', 'AR-E', 'AR-F', 'AR-G', 'AR-H', 'AR-J', - 'AR-K', 'AR-L', 'AR-M', 'AR-N', 'AR-P', 'AR-Q', 'AR-R', 'AR-S', 'AR-T', 'AR-U', - 'AR-V', 'AR-W', 'AR-X', 'AR-Y', 'AR-Z', 'AT-1', 'AT-2', 'AT-3', 'AT-4', 'AT-5', - 'AT-6', 'AT-7', 'AT-8', 'AT-9', 'AU-CT', 'AU-NS', 'AU-NT', 'AU-QL', 'AU-SA', - 'AU-TS', 'AU-VI', 'AU-WA', 'AZ-AB', 'AZ-ABS', 'AZ-AGA', 'AZ-AGC', 'AZ-AGM', - 'AZ-AGS', 'AZ-AGU', 'AZ-AST', 'AZ-BA', 'AZ-BAB', 'AZ-BAL', 'AZ-BAR', 'AZ-BEY', - 'AZ-BIL', 'AZ-CAB', 'AZ-CAL', 'AZ-CUL', 'AZ-DAS', 'AZ-DAV', 'AZ-FUZ', 'AZ-GA', - 'AZ-GAD', 'AZ-GOR', 'AZ-GOY', 'AZ-HAC', 'AZ-IMI', 'AZ-ISM', 'AZ-KAL', 'AZ-KUR', - 'AZ-LA', 'AZ-LAC', 'AZ-LAN', 'AZ-LER', 'AZ-MAS', 'AZ-MI', 'AZ-MM', 'AZ-NA', - 'AZ-NEF', 'AZ-OGU', 'AZ-ORD', 'AZ-QAB', 'AZ-QAX', 'AZ-QAZ', 'AZ-QBA', 'AZ-QBI', - 'AZ-QOB', 'AZ-QUS', 'AZ-SA', 'AZ-SAB', 'AZ-SAD', 'AZ-SAH', 'AZ-SAK', 'AZ-SAL', - 'AZ-SAR', 'AZ-SAT', 'AZ-SIY', 'AZ-SKR', 'AZ-SM', 'AZ-SMI', 'AZ-SMX', 'AZ-SS', - 'AZ-SUS', 'AZ-TAR', 'AZ-TOV', 'AZ-UCA', 'AZ-XA', 'AZ-XAC', 'AZ-XAN', 'AZ-XCI', - 'AZ-XIZ', 'AZ-XVD', 'AZ-YAR', 'AZ-YE', 'AZ-YEV', 'AZ-ZAN', 'AZ-ZAQ', 'AZ-ZAR', - 'BA-BIH', 'BA-SRP', 'BD-01', 'BD-02', 'BD-03', 'BD-04', 'BD-05', 'BD-06', 'BD-07', - 'BD-08', 'BD-09', 'BD-1', 'BD-10', 'BD-11', 'BD-12', 'BD-13', 'BD-14', 'BD-15', - 'BD-16', 'BD-17', 'BD-18', 'BD-19', 'BD-2', 'BD-20', 'BD-21', 'BD-22', 'BD-23', - 'BD-24', 'BD-25', 'BD-26', 'BD-27', 'BD-28', 'BD-29', 'BD-3', 'BD-30', 'BD-31', - 'BD-32', 'BD-33', 'BD-34', 'BD-35', 'BD-36', 'BD-37', 'BD-38', 'BD-39', 'BD-4', - 'BD-40', 'BD-41', 'BD-42', 'BD-43', 'BD-44', 'BD-45', 'BD-46', 'BD-47', 'BD-48', - 'BD-49', 'BD-5', 'BD-50', 'BD-51', 'BD-52', 'BD-53', 'BD-54', 'BD-55', 'BD-56', - 'BD-57', 'BD-58', 'BD-59', 'BD-6', 'BD-60', 'BD-61', 'BD-62', 'BD-63', 'BD-64', - 'BE-BRU', 'BE-VAN', 'BE-VBR', 'BE-VLG', 'BE-VLI', 'BE-VOV', 'BE-VWV', 'BE-WAL', - 'BE-WBR', 'BE-WHT', 'BE-WLG', 'BE-WLX', 'BE-WNA', 'BF-BAL', 'BF-BAM', 'BF-BAN', - 'BF-BAZ', 'BF-BGR', 'BF-BLG', 'BF-BLK', 'BF-COM', 'BF-GAN', 'BF-GNA', 'BF-GOU', - 'BF-HOU', 'BF-IOB', 'BF-KAD', 'BF-KEN', 'BF-KMD', 'BF-KMP', 'BF-KOP', 'BF-KOS', - 'BF-KOT', 'BF-KOW', 'BF-LER', 'BF-LOR', 'BF-MOU', 'BF-NAM', 'BF-NAO', 'BF-NAY', - 'BF-NOU', 'BF-OUB', 'BF-OUD', 'BF-PAS', 'BF-PON', 'BF-SEN', 'BF-SIS', 'BF-SMT', - 'BF-SNG', 'BF-SOM', 'BF-SOR', 'BF-TAP', 'BF-TUI', 'BF-YAG', 'BF-YAT', 'BF-ZIR', - 'BF-ZON', 'BF-ZOU', 'BG-01', 'BG-02', 'BG-03', 'BG-04', 'BG-05', 'BG-06', 'BG-07', - 'BG-08', 'BG-09', 'BG-10', 'BG-11', 'BG-12', 'BG-13', 'BG-14', 'BG-15', 'BG-16', - 'BG-17', 'BG-18', 'BG-19', 'BG-20', 'BG-21', 'BG-22', 'BG-23', 'BG-24', 'BG-25', - 'BG-26', 'BG-27', 'BG-28', 'BH-01', 'BH-02', 'BH-03', 'BH-04', 'BH-05', 'BH-06', - 'BH-07', 'BH-08', 'BH-09', 'BH-10', 'BH-11', 'BH-12', 'BI-BB', 'BI-BJ', 'BI-BR', - 'BI-CA', 'BI-CI', 'BI-GI', 'BI-KI', 'BI-KR', 'BI-KY', 'BI-MA', 'BI-MU', 'BI-MW', - 'BI-MY', 'BI-NG', 'BI-RT', 'BI-RY', 'BJ-AK', 'BJ-AL', 'BJ-AQ', 'BJ-BO', 'BJ-CO', - 'BJ-DO', 'BJ-KO', 'BJ-LI', 'BJ-MO', 'BJ-OU', 'BJ-PL', 'BJ-ZO', 'BN-BE', 'BN-BM', - 'BN-TE', 'BN-TU', 'BO-B', 'BO-C', 'BO-H', 'BO-L', 'BO-N', 'BO-O', 'BO-P', 'BO-S', - 'BO-T', 'BR-AC', 'BR-AL', 'BR-AM', 'BR-AP', 'BR-BA', 'BR-CE', 'BR-DF', 'BR-ES', - 'BR-GO', 'BR-MA', 'BR-MG', 'BR-MS', 'BR-MT', 'BR-PA', 'BR-PB', 'BR-PE', 'BR-PI', - 'BR-PR', 'BR-RJ', 'BR-RN', 'BR-RO', 'BR-RR', 'BR-RS', 'BR-SC', 'BR-SE', 'BR-SP', - 'BR-TO', 'BS-AC', 'BS-BI', 'BS-CI', 'BS-EX', 'BS-FC', 'BS-FP', 'BS-GH', 'BS-GT', - 'BS-HI', 'BS-HR', 'BS-IN', 'BS-KB', 'BS-LI', 'BS-MG', 'BS-MH', 'BS-NB', 'BS-NP', - 'BS-RI', 'BS-RS', 'BS-SP', 'BS-SR', 'BT-11', 'BT-12', 'BT-13', 'BT-14', 'BT-15', - 'BT-21', 'BT-22', 'BT-23', 'BT-24', 'BT-31', 'BT-32', 'BT-33', 'BT-34', 'BT-41', - 'BT-42', 'BT-43', 'BT-44', 'BT-45', 'BT-GA', 'BT-TY', 'BW-CE', 'BW-CH', 'BW-GH', - 'BW-KG', 'BW-KL', 'BW-KW', 'BW-NE', 'BW-NG', 'BW-SE', 'BW-SO', 'BY-BR', 'BY-HO', - 'BY-HR', 'BY-MA', 'BY-MI', 'BY-VI', 'BZ-BZ', 'BZ-CY', 'BZ-CZL', 'BZ-OW', 'BZ-SC', - 'BZ-TOL', 'CA-AB', 'CA-BC', 'CA-MB', 'CA-NB', 'CA-NL', 'CA-NS', 'CA-NT', 'CA-NU', - 'CA-ON', 'CA-PE', 'CA-QC', 'CA-SK', 'CA-YT', 'CD-BC', 'CD-BN', 'CD-EQ', 'CD-KA', - 'CD-KE', 'CD-KN', 'CD-KW', 'CD-MA', 'CD-NK', 'CD-OR', 'CD-SK', 'CF-AC', 'CF-BB', - 'CF-BGF', 'CF-BK', 'CF-HK', 'CF-HM', 'CF-HS', 'CF-KB', 'CF-KG', 'CF-LB', 'CF-MB', - 'CF-MP', 'CF-NM', 'CF-OP', 'CF-SE', 'CF-UK', 'CF-VK', 'CG-11', 'CG-12', 'CG-13', - 'CG-14', 'CG-15', 'CG-2', 'CG-5', 'CG-7', 'CG-8', 'CG-9', 'CG-BZV', 'CH-AG', - 'CH-AI', 'CH-AR', 'CH-BE', 'CH-BL', 'CH-BS', 'CH-FR', 'CH-GE', 'CH-GL', 'CH-GR', - 'CH-JU', 'CH-LU', 'CH-NE', 'CH-NW', 'CH-OW', 'CH-SG', 'CH-SH', 'CH-SO', 'CH-SZ', - 'CH-TG', 'CH-TI', 'CH-UR', 'CH-VD', 'CH-VS', 'CH-ZG', 'CH-ZH', 'CI-01', 'CI-02', - 'CI-03', 'CI-04', 'CI-05', 'CI-06', 'CI-07', 'CI-08', 'CI-09', 'CI-10', 'CI-11', - 'CI-12', 'CI-13', 'CI-14', 'CI-15', 'CI-16', 'CL-AI', 'CL-AN', 'CL-AR', 'CL-AT', - 'CL-BI', 'CL-CO', 'CL-LI', 'CL-LL', 'CL-MA', 'CL-ML', 'CL-RM', 'CL-TA', 'CL-VS', - 'CM-AD', 'CM-CE', 'CM-EN', 'CM-ES', 'CM-LT', 'CM-NO', 'CM-NW', 'CM-OU', 'CM-SU', - 'CM-SW', 'CN-11', 'CN-12', 'CN-13', 'CN-14', 'CN-15', 'CN-21', 'CN-22', 'CN-23', - 'CN-31', 'CN-32', 'CN-33', 'CN-34', 'CN-35', 'CN-36', 'CN-37', 'CN-41', 'CN-42', - 'CN-43', 'CN-44', 'CN-45', 'CN-46', 'CN-50', 'CN-51', 'CN-52', 'CN-53', 'CN-54', - 'CN-61', 'CN-62', 'CN-63', 'CN-64', 'CN-65', 'CN-71', 'CN-91', 'CN-92', 'CO-AMA', - 'CO-ANT', 'CO-ARA', 'CO-ATL', 'CO-BOL', 'CO-BOY', 'CO-CAL', 'CO-CAQ', 'CO-CAS', - 'CO-CAU', 'CO-CES', 'CO-CHO', 'CO-COR', 'CO-CUN', 'CO-DC', 'CO-GUA', 'CO-GUV', - 'CO-HUI', 'CO-LAG', 'CO-MAG', 'CO-MET', 'CO-NAR', 'CO-NSA', 'CO-PUT', 'CO-QUI', - 'CO-RIS', 'CO-SAN', 'CO-SAP', 'CO-SUC', 'CO-TOL', 'CO-VAC', 'CO-VAU', 'CO-VID', - 'CR-A', 'CR-C', 'CR-G', 'CR-H', 'CR-L', 'CR-P', 'CR-SJ', 'CU-01', 'CU-02', - 'CU-03', 'CU-04', 'CU-05', 'CU-06', 'CU-07', 'CU-08', 'CU-09', 'CU-10', 'CU-11', - 'CU-12', 'CU-13', 'CU-14', 'CU-99', 'CV-B', 'CV-BR', 'CV-BV', 'CV-CA', 'CV-CR', - 'CV-CS', 'CV-FO', 'CV-MA', 'CV-MO', 'CV-PA', 'CV-PN', 'CV-PR', 'CV-RG', 'CV-S', - 'CV-SF', 'CV-SL', 'CV-SN', 'CV-SV', 'CV-TA', 'CY-01', 'CY-02', 'CY-03', 'CY-04', - 'CY-05', 'CY-06', 'CZ-JC', 'CZ-JM', 'CZ-KA', 'CZ-KR', 'CZ-LI', 'CZ-MO', 'CZ-OL', - 'CZ-PA', 'CZ-PL', 'CZ-PR', 'CZ-ST', 'CZ-US', 'CZ-VY', 'CZ-ZL', 'DE-BB', 'DE-BE', - 'DE-BW', 'DE-BY', 'DE-HB', 'DE-HE', 'DE-HH', 'DE-MV', 'DE-NI', 'DE-NW', 'DE-RP', - 'DE-SH', 'DE-SL', 'DE-SN', 'DE-ST', 'DE-TH', 'DJ-AS', 'DJ-DI', 'DJ-DJ', 'DJ-OB', - 'DJ-TA', 'DK-015', 'DK-020', 'DK-025', 'DK-030', 'DK-035', 'DK-040', 'DK-042', - 'DK-050', 'DK-055', 'DK-060', 'DK-065', 'DK-070', 'DK-076', 'DK-080', 'DK-101', - 'DK-147', 'DO-01', 'DO-02', 'DO-03', 'DO-04', 'DO-05', 'DO-06', 'DO-07', 'DO-08', - 'DO-09', 'DO-10', 'DO-11', 'DO-12', 'DO-13', 'DO-14', 'DO-15', 'DO-16', 'DO-17', - 'DO-18', 'DO-19', 'DO-20', 'DO-21', 'DO-22', 'DO-23', 'DO-24', 'DO-25', 'DO-26', - 'DO-27', 'DO-28', 'DO-29', 'DO-30', 'DZ-01', 'DZ-02', 'DZ-03', 'DZ-04', 'DZ-05', - 'DZ-06', 'DZ-07', 'DZ-08', 'DZ-09', 'DZ-10', 'DZ-11', 'DZ-12', 'DZ-13', 'DZ-14', - 'DZ-15', 'DZ-16', 'DZ-17', 'DZ-18', 'DZ-19', 'DZ-20', 'DZ-21', 'DZ-22', 'DZ-23', - 'DZ-24', 'DZ-25', 'DZ-26', 'DZ-27', 'DZ-28', 'DZ-29', 'DZ-30', 'DZ-31', 'DZ-32', - 'DZ-33', 'DZ-34', 'DZ-35', 'DZ-36', 'DZ-37', 'DZ-38', 'DZ-39', 'DZ-40', 'DZ-41', - 'DZ-42', 'DZ-43', 'DZ-44', 'DZ-45', 'DZ-46', 'DZ-47', 'DZ-48', 'EC-A', 'EC-B', - 'EC-C', 'EC-D', 'EC-E', 'EC-F', 'EC-G', 'EC-H', 'EC-I', 'EC-L', 'EC-M', 'EC-N', - 'EC-O', 'EC-P', 'EC-R', 'EC-S', 'EC-T', 'EC-U', 'EC-W', 'EC-X', 'EC-Y', 'EC-Z', - 'EE-37', 'EE-39', 'EE-44', 'EE-49', 'EE-51', 'EE-57', 'EE-59', 'EE-65', 'EE-67', - 'EE-70', 'EE-74', 'EE-78', 'EE-82', 'EE-84', 'EE-86', 'EG-ALX', 'EG-ASN', - 'EG-AST', 'EG-BA', 'EG-BH', 'EG-BNS', 'EG-C', 'EG-DK', 'EG-DT', 'EG-FYM', 'EG-GH', - 'EG-GZ', 'EG-IS', 'EG-JS', 'EG-KB', 'EG-KFS', 'EG-KN', 'EG-MN', 'EG-MNF', 'EG-MT', - 'EG-PTS', 'EG-SHG', 'EG-SHR', 'EG-SIN', 'EG-SUZ', 'EG-WAD', 'ER-AN', 'ER-DK', - 'ER-DU', 'ER-GB', 'ER-MA', 'ER-SK', 'ES-A', 'ES-AB', 'ES-AL', 'ES-AN', 'ES-AR', - 'ES-AV', 'ES-B', 'ES-BA', 'ES-BI', 'ES-BU', 'ES-C', 'ES-CA', 'ES-CC', 'ES-CE', - 'ES-CL', 'ES-CM', 'ES-CN', 'ES-CO', 'ES-CR', 'ES-CS', 'ES-CT', 'ES-CU', 'ES-EX', - 'ES-GA', 'ES-GC', 'ES-GI', 'ES-GR', 'ES-GU', 'ES-H', 'ES-HU', 'ES-J', 'ES-L', - 'ES-LE', 'ES-LO', 'ES-LU', 'ES-M', 'ES-MA', 'ES-ML', 'ES-MU', 'ES-NA', 'ES-O', - 'ES-OR', 'ES-P', 'ES-PM', 'ES-PO', 'ES-PV', 'ES-S', 'ES-SA', 'ES-SE', 'ES-SG', - 'ES-SO', 'ES-SS', 'ES-T', 'ES-TE', 'ES-TF', 'ES-TO', 'ES-V', 'ES-VA', 'ES-VC', - 'ES-VI', 'ES-Z', 'ES-ZA', 'ET-AA', 'ET-AF', 'ET-AM', 'ET-BE', 'ET-DD', 'ET-GA', - 'ET-HA', 'ET-OR', 'ET-SN', 'ET-SO', 'ET-TI', 'FI-AL', 'FI-ES', 'FI-IS', 'FI-LL', - 'FI-LS', 'FI-OL', 'FJ-C', 'FJ-E', 'FJ-N', 'FJ-R', 'FJ-W', 'FM-KSA', 'FM-PNI', - 'FM-TRK', 'FM-YAP', 'FR-01', 'FR-02', 'FR-03', 'FR-04', 'FR-05', 'FR-06', 'FR-07', - 'FR-08', 'FR-09', 'FR-10', 'FR-11', 'FR-12', 'FR-13', 'FR-14', 'FR-15', 'FR-16', - 'FR-17', 'FR-18', 'FR-19', 'FR-21', 'FR-22', 'FR-23', 'FR-24', 'FR-25', 'FR-26', - 'FR-27', 'FR-28', 'FR-29', 'FR-2A', 'FR-2B', 'FR-30', 'FR-31', 'FR-32', 'FR-33', - 'FR-34', 'FR-35', 'FR-36', 'FR-37', 'FR-38', 'FR-39', 'FR-40', 'FR-41', 'FR-42', - 'FR-43', 'FR-44', 'FR-45', 'FR-46', 'FR-47', 'FR-48', 'FR-49', 'FR-50', 'FR-51', - 'FR-52', 'FR-53', 'FR-54', 'FR-55', 'FR-56', 'FR-57', 'FR-58', 'FR-59', 'FR-60', - 'FR-61', 'FR-62', 'FR-63', 'FR-64', 'FR-65', 'FR-66', 'FR-67', 'FR-68', 'FR-69', - 'FR-70', 'FR-71', 'FR-72', 'FR-73', 'FR-74', 'FR-75', 'FR-76', 'FR-77', 'FR-78', - 'FR-79', 'FR-80', 'FR-81', 'FR-82', 'FR-83', 'FR-84', 'FR-85', 'FR-86', 'FR-87', - 'FR-88', 'FR-89', 'FR-90', 'FR-91', 'FR-92', 'FR-93', 'FR-94', 'FR-95', 'FR-A', - 'FR-B', 'FR-C', 'FR-D', 'FR-E', 'FR-F', 'FR-G', 'FR-GF', 'FR-GP', 'FR-H', 'FR-I', - 'FR-J', 'FR-K', 'FR-L', 'FR-M', 'FR-MQ', 'FR-N', 'FR-NC', 'FR-O', 'FR-P', 'FR-PF', - 'FR-PM', 'FR-Q', 'FR-R', 'FR-RE', 'FR-S', 'FR-T', 'FR-TF', 'FR-U', 'FR-V', - 'FR-WF', 'FR-YT', 'GA-1', 'GA-2', 'GA-3', 'GA-4', 'GA-5', 'GA-6', 'GA-7', 'GA-8', - 'GA-9', 'GB-ABD', 'GB-ABE', 'GB-AGB', 'GB-AGY', 'GB-ANS', 'GB-ANT', 'GB-ARD', - 'GB-ARM', 'GB-BAS', 'GB-BBD', 'GB-BDF', 'GB-BDG', 'GB-BEN', 'GB-BEX', 'GB-BFS', - 'GB-BGE', 'GB-BGW', 'GB-BIR', 'GB-BKM', 'GB-BLA', 'GB-BLY', 'GB-BMH', 'GB-BNB', - 'GB-BNE', 'GB-BNH', 'GB-BNS', 'GB-BOL', 'GB-BPL', 'GB-BRC', 'GB-BRD', 'GB-BRY', - 'GB-BST', 'GB-BUR', 'GB-CAM', 'GB-CAY', 'GB-CGN', 'GB-CGV', 'GB-CHA', 'GB-CHS', - 'GB-CKF', 'GB-CKT', 'GB-CLD', 'GB-CLK', 'GB-CLR', 'GB-CMA', 'GB-CMD', 'GB-CMN', - 'GB-CON', 'GB-COV', 'GB-CRF', 'GB-CRY', 'GB-CSR', 'GB-CWY', 'GB-DAL', 'GB-DBY', - 'GB-DEN', 'GB-DER', 'GB-DEV', 'GB-DGN', 'GB-DGY', 'GB-DNC', 'GB-DND', 'GB-DOR', - 'GB-DOW', 'GB-DRY', 'GB-DUD', 'GB-DUR', 'GB-EAL', 'GB-EAW', 'GB-EAY', 'GB-EDH', - 'GB-EDU', 'GB-ELN', 'GB-ELS', 'GB-ENF', 'GB-ENG', 'GB-ERW', 'GB-ERY', 'GB-ESS', - 'GB-ESX', 'GB-FAL', 'GB-FER', 'GB-FIF', 'GB-FLN', 'GB-GAT', 'GB-GBN', 'GB-GLG', - 'GB-GLS', 'GB-GRE', 'GB-GSY', 'GB-GWN', 'GB-HAL', 'GB-HAM', 'GB-HAV', 'GB-HCK', - 'GB-HEF', 'GB-HIL', 'GB-HLD', 'GB-HMF', 'GB-HNS', 'GB-HPL', 'GB-HRT', 'GB-HRW', - 'GB-HRY', 'GB-IOM', 'GB-IOS', 'GB-IOW', 'GB-ISL', 'GB-IVC', 'GB-JSY', 'GB-KEC', - 'GB-KEN', 'GB-KHL', 'GB-KIR', 'GB-KTT', 'GB-KWL', 'GB-LAN', 'GB-LBH', 'GB-LCE', - 'GB-LDS', 'GB-LEC', 'GB-LEW', 'GB-LIN', 'GB-LIV', 'GB-LMV', 'GB-LND', 'GB-LRN', - 'GB-LSB', 'GB-LUT', 'GB-MAN', 'GB-MDB', 'GB-MDW', 'GB-MFT', 'GB-MIK', 'GB-MLN', - 'GB-MON', 'GB-MRT', 'GB-MRY', 'GB-MTY', 'GB-MYL', 'GB-NAY', 'GB-NBL', 'GB-NDN', - 'GB-NEL', 'GB-NET', 'GB-NFK', 'GB-NGM', 'GB-NIR', 'GB-NLK', 'GB-NLN', 'GB-NSM', - 'GB-NTA', 'GB-NTH', 'GB-NTL', 'GB-NTT', 'GB-NTY', 'GB-NWM', 'GB-NWP', 'GB-NYK', - 'GB-NYM', 'GB-OLD', 'GB-OMH', 'GB-ORK', 'GB-OXF', 'GB-PEM', 'GB-PKN', 'GB-PLY', - 'GB-POL', 'GB-POR', 'GB-POW', 'GB-PTE', 'GB-RCC', 'GB-RCH', 'GB-RCT', 'GB-RDB', - 'GB-RDG', 'GB-RFW', 'GB-RIC', 'GB-ROT', 'GB-RUT', 'GB-SAW', 'GB-SAY', 'GB-SCB', - 'GB-SCT', 'GB-SFK', 'GB-SFT', 'GB-SGC', 'GB-SHF', 'GB-SHN', 'GB-SHR', 'GB-SKP', - 'GB-SLF', 'GB-SLG', 'GB-SLK', 'GB-SND', 'GB-SOL', 'GB-SOM', 'GB-SOS', 'GB-SRY', - 'GB-STB', 'GB-STE', 'GB-STG', 'GB-STH', 'GB-STN', 'GB-STS', 'GB-STT', 'GB-STY', - 'GB-SWA', 'GB-SWD', 'GB-SWK', 'GB-TAM', 'GB-TFW', 'GB-THR', 'GB-TOB', 'GB-TOF', - 'GB-TRF', 'GB-TWH', 'GB-UKM', 'GB-VGL', 'GB-WAR', 'GB-WBK', 'GB-WDU', 'GB-WFT', - 'GB-WGN', 'GB-WILL', 'GB-WKF', 'GB-WLL', 'GB-WLN', 'GB-WLS', 'GB-WLV', 'GB-WND', - 'GB-WNM', 'GB-WOK', 'GB-WOR', 'GB-WRL', 'GB-WRT', 'GB-WRX', 'GB-WSM', 'GB-WSX', - 'GB-YOR', 'GB-ZET', 'GE-AB', 'GE-AJ', 'GE-GU', 'GE-IM', 'GE-KA', 'GE-KK', 'GE-MM', - 'GE-RL', 'GE-SJ', 'GE-SK', 'GE-SZ', 'GE-TB', 'GH-AA', 'GH-AH', 'GH-BA', 'GH-CP', - 'GH-EP', 'GH-NP', 'GH-TV', 'GH-UE', 'GH-UW', 'GH-WP', 'GM-B', 'GM-L', 'GM-M', - 'GM-N', 'GM-U', 'GM-W', 'GN-B', 'GN-BE', 'GN-BF', 'GN-BK', 'GN-C', 'GN-CO', - 'GN-D', 'GN-DB', 'GN-DI', 'GN-DL', 'GN-DU', 'GN-F', 'GN-FA', 'GN-FO', 'GN-FR', - 'GN-GA', 'GN-GU', 'GN-K', 'GN-KA', 'GN-KB', 'GN-KD; 2', 'GN-KE', 'GN-KN', 'GN-KO', - 'GN-KS', 'GN-L', 'GN-LA', 'GN-LE', 'GN-LO', 'GN-M', 'GN-MC', 'GN-MD', 'GN-ML', - 'GN-MM', 'GN-N', 'GN-NZ', 'GN-PI', 'GN-SI', 'GN-TE', 'GN-TO', 'GN-YO', 'GQ-AN', - 'GQ-BN', 'GQ-BS', 'GQ-C', 'GQ-CS', 'GQ-I', 'GQ-KN', 'GQ-LI', 'GQ-WN', 'GR-01', - 'GR-03', 'GR-04', 'GR-05', 'GR-06', 'GR-07', 'GR-11', 'GR-12', 'GR-13', 'GR-14', - 'GR-15', 'GR-16', 'GR-17', 'GR-21', 'GR-22', 'GR-23', 'GR-24', 'GR-31', 'GR-32', - 'GR-33', 'GR-34', 'GR-41', 'GR-42', 'GR-43', 'GR-44', 'GR-51', 'GR-52', 'GR-53', - 'GR-54', 'GR-55', 'GR-56', 'GR-57', 'GR-58', 'GR-59', 'GR-61', 'GR-62', 'GR-63', - 'GR-64', 'GR-69', 'GR-71', 'GR-72', 'GR-73', 'GR-81', 'GR-82', 'GR-83', 'GR-84', - 'GR-85', 'GR-91', 'GR-92', 'GR-93', 'GR-94', 'GR-A1', 'GR-I', 'GR-II', 'GR-III', - 'GR-IV', 'GR-IX', 'GR-V', 'GR-VI', 'GR-VII', 'GR-VIII', 'GR-X', 'GR-XI', 'GR-XII', - 'GR-XIII', 'GT-AV', 'GT-BV', 'GT-CM', 'GT-CQ', 'GT-ES', 'GT-GU', 'GT-HU', 'GT-IZ', - 'GT-JA', 'GT-JU', 'GT-PE', 'GT-PR', 'GT-QC', 'GT-QZ', 'GT-RE', 'GT-SA', 'GT-SM', - 'GT-SO', 'GT-SR', 'GT-SU', 'GT-TO', 'GT-ZA', 'GW-BA', 'GW-BL', 'GW-BM', 'GW-BS', - 'GW-CA', 'GW-GA', 'GW-L', 'GW-N', 'GW-OI', 'GW-QU', 'GW-S', 'GW-TO', 'GY-BA', - 'GY-CU', 'GY-DE', 'GY-EB', 'GY-ES', 'GY-MA', 'GY-PM', 'GY-PT', 'GY-UD', 'GY-UT', - 'HN-AT', 'HN-CH', 'HN-CL', 'HN-CM', 'HN-CP', 'HN-CR', 'HN-EP', 'HN-FM', 'HN-GD', - 'HN-IB', 'HN-IN', 'HN-LE', 'HN-LP', 'HN-OC', 'HN-OL', 'HN-SB', 'HN-VA', 'HN-YO', - 'HR-01', 'HR-02', 'HR-03', 'HR-04', 'HR-05', 'HR-06', 'HR-07', 'HR-08', 'HR-09', - 'HR-10', 'HR-11', 'HR-12', 'HR-13', 'HR-14', 'HR-15', 'HR-16', 'HR-17', 'HR-18', - 'HR-19', 'HR-20', 'HR-21', 'HT-AR', 'HT-CE', 'HT-GA', 'HT-ND', 'HT-NE', 'HT-NO', - 'HT-OU', 'HT-SD', 'HT-SE', 'HU-BA', 'HU-BC', 'HU-BE', 'HU-BK', 'HU-BU', 'HU-BZ', - 'HU-CS', 'HU-DE', 'HU-DU', 'HU-EG', 'HU-FE', 'HU-GS', 'HU-GY', 'HU-HB', 'HU-HE', - 'HU-HV', 'HU-JN', 'HU-KE', 'HU-KM', 'HU-KV', 'HU-MI', 'HU-NK', 'HU-NO', 'HU-NY', - 'HU-PE', 'HU-PS', 'HU-SD', 'HU-SF', 'HU-SH', 'HU-SK', 'HU-SN', 'HU-SO', 'HU-SS', - 'HU-ST', 'HU-SZ', 'HU-TB', 'HU-TO', 'HU-VA', 'HU-VE', 'HU-VM', 'HU-ZA', 'HU-ZE', - 'ID-AC', 'ID-BA', 'ID-BB', 'ID-BE', 'ID-BT', 'ID-GO', 'ID-IJ', 'ID-JA', 'ID-JB', - 'ID-JI', 'ID-JK', 'ID-JT', 'ID-JW', 'ID-KA', 'ID-KB', 'ID-KI', 'ID-KS', 'ID-KT', - 'ID-LA', 'ID-MA', 'ID-MU', 'ID-NB', 'ID-NT', 'ID-NU', 'ID-PA', 'ID-RI', 'ID-SA', - 'ID-SB', 'ID-SG', 'ID-SL', 'ID-SM', 'ID-SN', 'ID-SS', 'ID-ST', 'ID-SU', 'ID-YO', - 'IE-C', 'IE-C; 2', 'IE-CE', 'IE-CN', 'IE-CW', 'IE-D', 'IE-DL', 'IE-G', 'IE-KE', - 'IE-KK', 'IE-KY', 'IE-L', 'IE-LD', 'IE-LH', 'IE-LK', 'IE-LM', 'IE-LS', 'IE-M', - 'IE-MH', 'IE-MN', 'IE-MO', 'IE-OY', 'IE-RN', 'IE-SO', 'IE-TA', 'IE-U', 'IE-WD', - 'IE-WH', 'IE-WW', 'IE-WX', 'IL-D', 'IL-HA', 'IL-JM', 'IL-M', 'IL-TA', 'IL-Z', - 'IN-AN', 'IN-AP', 'IN-AR', 'IN-AS', 'IN-BR', 'IN-CH', 'IN-CT', 'IN-DD', 'IN-DL', - 'IN-DN', 'IN-GA', 'IN-GJ', 'IN-HP', 'IN-HR', 'IN-JH', 'IN-JK', 'IN-KA', 'IN-KL', - 'IN-LD', 'IN-MH', 'IN-ML', 'IN-MN', 'IN-MP', 'IN-MZ', 'IN-NL', 'IN-OR', 'IN-PB', - 'IN-PY', 'IN-RJ', 'IN-SK', 'IN-TN', 'IN-TR', 'IN-UL', 'IN-UP', 'IN-WB', 'IQ-AN', - 'IQ-AR', 'IQ-BA', 'IQ-BB', 'IQ-BG', 'IQ-DA', 'IQ-DI', 'IQ-DQ', 'IQ-KA', 'IQ-MA', - 'IQ-MU', 'IQ-NA', 'IQ-NI', 'IQ-QA', 'IQ-SD', 'IQ-SU', 'IQ-TS', 'IQ-WA', 'IR-01', - 'IR-02', 'IR-03', 'IR-04', 'IR-05', 'IR-06', 'IR-07', 'IR-08', 'IR-09', 'IR-10', - 'IR-11', 'IR-12', 'IR-13', 'IR-14', 'IR-15', 'IR-16', 'IR-17', 'IR-18', 'IR-19', - 'IR-20', 'IR-21', 'IR-22', 'IR-23', 'IR-24', 'IR-25', 'IR-26', 'IR-27', 'IR-28', - 'IS-0', 'IS-1', 'IS-2', 'IS-3', 'IS-4', 'IS-5', 'IS-6', 'IS-7', 'IS-8', 'IT-21', - 'IT-23', 'IT-25', 'IT-32', 'IT-34', 'IT-36', 'IT-42', 'IT-45', 'IT-52', 'IT-55', - 'IT-57', 'IT-62', 'IT-65', 'IT-67', 'IT-72', 'IT-75', 'IT-77', 'IT-78', 'IT-82', - 'IT-88', 'IT-AG', 'IT-AL', 'IT-AN', 'IT-AO', 'IT-AP', 'IT-AQ', 'IT-AR', 'IT-AT', - 'IT-AV', 'IT-BA', 'IT-BG', 'IT-BI', 'IT-BL', 'IT-BN', 'IT-BO', 'IT-BR', 'IT-BS', - 'IT-BZ', 'IT-CA', 'IT-CB', 'IT-CE', 'IT-CH', 'IT-CL', 'IT-CN', 'IT-CO', 'IT-CR', - 'IT-CS', 'IT-CT', 'IT-CZ', 'IT-DU', 'IT-EN', 'IT-FE', 'IT-FG', 'IT-FI', 'IT-FO', - 'IT-FR', 'IT-GE', 'IT-GO', 'IT-GR', 'IT-IM', 'IT-IS', 'IT-KR', 'IT-LC', 'IT-LE', - 'IT-LI', 'IT-LO', 'IT-LT', 'IT-LU', 'IT-MC', 'IT-ME', 'IT-MI', 'IT-MN', 'IT-MO', - 'IT-MS', 'IT-MT', 'IT-NA', 'IT-NO', 'IT-NU', 'IT-OR', 'IT-PA', 'IT-PC', 'IT-PD', - 'IT-PE', 'IT-PG', 'IT-PI', 'IT-PN', 'IT-PO', 'IT-PR', 'IT-PS', 'IT-PT', 'IT-PV', - 'IT-PZ', 'IT-RA', 'IT-RC', 'IT-RE', 'IT-RG', 'IT-RI', 'IT-RM', 'IT-RN', 'IT-RO', - 'IT-SA', 'IT-SI', 'IT-SO', 'IT-SP', 'IT-SR', 'IT-SS', 'IT-SV', 'IT-TA', 'IT-TE', - 'IT-TN', 'IT-TO', 'IT-TP', 'IT-TR', 'IT-TS', 'IT-TV', 'IT-VA', 'IT-VB', 'IT-VC', - 'IT-VE', 'IT-VI', 'IT-VR', 'IT-VT', 'IT-VV', 'JM-01', 'JM-02', 'JM-03', 'JM-04', - 'JM-05', 'JM-06', 'JM-07', 'JM-08', 'JM-09', 'JM-10', 'JM-11', 'JM-12', 'JM-13', - 'JM-14', 'JO-AJ', 'JO-AM', 'JO-AQ', 'JO-AT', 'JO-AZ', 'JO-BA', 'JO-IR', 'JO-JA', - 'JO-KA', 'JO-MA', 'JO-MD', 'JO-MN', 'JP-01', 'JP-02', 'JP-03', 'JP-04', 'JP-05', - 'JP-06', 'JP-07', 'JP-08', 'JP-09', 'JP-10', 'JP-11', 'JP-12', 'JP-13', 'JP-14', - 'JP-15', 'JP-16', 'JP-17', 'JP-18', 'JP-19', 'JP-20', 'JP-21', 'JP-22', 'JP-23', - 'JP-24', 'JP-25', 'JP-26', 'JP-27', 'JP-28', 'JP-29', 'JP-30', 'JP-31', 'JP-32', - 'JP-33', 'JP-34', 'JP-35', 'JP-36', 'JP-37', 'JP-38', 'JP-39', 'JP-40', 'JP-41', - 'JP-42', 'JP-43', 'JP-44', 'JP-45', 'JP-46', 'JP-47', 'KE-110', 'KE-200', - 'KE-300', 'KE-400', 'KE-500', 'KE-600', 'KE-700', 'KE-900', 'KG-B', 'KG-C', - 'KG-GB', 'KG-J', 'KG-N', 'KG-O', 'KG-T', 'KG-Y', 'KH-1', 'KH-10', 'KH-11', - 'KH-12', 'KH-13', 'KH-14', 'KH-15', 'KH-16', 'KH-17', 'KH-18', 'KH-19', 'KH-2', - 'KH-20', 'KH-21', 'KH-22', 'KH-23', 'KH-24', 'KH-3', 'KH-4', 'KH-5', 'KH-6', - 'KH-7', 'KH-8', 'KH-9', 'KI-G', 'KI-L', 'KI-P', 'KM-A', 'KM-G', 'KM-M', 'KP-CHA', - 'KP-HAB', 'KP-HAN', 'KP-HWB', 'KP-HWN', 'KP-KAE', 'KP-KAN', 'KP-NAJ', 'KP-NAM', - 'KP-PYB', 'KP-PYN', 'KP-PYO', 'KP-YAN', 'KR-11', 'KR-26', 'KR-27', 'KR-28', - 'KR-29', 'KR-30', 'KR-31', 'KR-41', 'KR-42', 'KR-43', 'KR-44', 'KR-45', 'KR-46', - 'KR-47', 'KR-48', 'KR-49', 'KW-AH', 'KW-FA', 'KW-HA', 'KW-JA', 'KW-KU', 'KZ-AKM', - 'KZ-AKT', 'KZ-ALA', 'KZ-ALM', 'KZ-AST', 'KZ-ATY', 'KZ-KAR', 'KZ-KUS', 'KZ-KZY', - 'KZ-MAN', 'KZ-PAV', 'KZ-SEV', 'KZ-VOS', 'KZ-YUZ', 'KZ-ZAP', 'KZ-ZHA', 'LA-AT', - 'LA-BK', 'LA-BL', 'LA-CH', 'LA-HO', 'LA-KH', 'LA-LM', 'LA-LP', 'LA-OU', 'LA-PH', - 'LA-SL', 'LA-SV', 'LA-VI', 'LA-VT', 'LA-XA', 'LA-XE', 'LA-XI', 'LA-XN', 'LB-AS', - 'LB-BA', 'LB-BI', 'LB-JA', 'LB-JL', 'LB-NA', 'LK-1', 'LK-11', 'LK-12', 'LK-13', - 'LK-2', 'LK-21', 'LK-22', 'LK-23', 'LK-3', 'LK-31', 'LK-32', 'LK-33', 'LK-4', - 'LK-41', 'LK-42', 'LK-43', 'LK-44', 'LK-45', 'LK-5', 'LK-51', 'LK-52', 'LK-53', - 'LK-6', 'LK-61', 'LK-62', 'LK-7', 'LK-71', 'LK-72', 'LK-8', 'LK-81', 'LK-82', - 'LK-9', 'LK-91', 'LK-92', 'LR-BG', 'LR-BM', 'LR-CM', 'LR-GB', 'LR-GG', 'LR-GK', - 'LR-LO', 'LR-MG', 'LR-MO', 'LR-MY', 'LR-NI', 'LR-RI', 'LR-SI', 'LS-A', 'LS-B', - 'LS-C', 'LS-D', 'LS-E', 'LS-F', 'LS-G', 'LS-H', 'LS-J', 'LS-K', 'LT-AL', 'LT-KL', - 'LT-KU', 'LT-MR', 'LT-PN', 'LT-SA', 'LT-TA', 'LT-TE', 'LT-UT', 'LT-VL', 'LU-D', - 'LU-G', 'LU-L', 'LV-AI', 'LV-AL', 'LV-BL', 'LV-BU', 'LV-CE', 'LV-DA', 'LV-DGV', - 'LV-DO', 'LV-GU', 'LV-JEL', 'LV-JK', 'LV-JL', 'LV-JUR', 'LV-KR', 'LV-KU', 'LV-LE', - 'LV-LM', 'LV-LPX', 'LV-LU', 'LV-MA', 'LV-OG', 'LV-PR', 'LV-RE', 'LV-REZ', 'LV-RI', - 'LV-RIX', 'LV-SA', 'LV-TA', 'LV-TU', 'LV-VE', 'LV-VEN', 'LV-VK', 'LV-VM', 'LY-BA', - 'LY-BU', 'LY-FA', 'LY-JA', 'LY-JG', 'LY-JU', 'LY-MI', 'LY-NA', 'LY-SF', 'LY-TB', - 'LY-WA', 'LY-WU', 'LY-ZA', 'MA-01', 'MA-02', 'MA-03', 'MA-04', 'MA-05', 'MA-06', - 'MA-07', 'MA-08', 'MA-09', 'MA-10', 'MA-11', 'MA-12', 'MA-13', 'MA-14', 'MA-15', - 'MA-16', 'MA-AGD', 'MA-ASZ', 'MA-AZI', 'MA-BAH', 'MA-BEM', 'MA-BER', 'MA-BES', - 'MA-BOD', 'MA-BOM', 'MA-CAS', 'MA-CHE', 'MA-CHI', 'MA-ERR', 'MA-ESI', 'MA-ESM', - 'MA-FES', 'MA-FIG', 'MA-GUE', 'MA-HAJ', 'MA-HAO', 'MA-HOC', 'MA-IFR', 'MA-JDI', - 'MA-JRA', 'MA-KEN', 'MA-KES', 'MA-KHE', 'MA-KHN', 'MA-KHO', 'MA-LAA', 'MA-LAR', - 'MA-MAR', 'MA-MEK', 'MA-MEL', 'MA-NAD', 'MA-OUA', 'MA-OUD', 'MA-OUJ', 'MA-RBA', - 'MA-SAF', 'MA-SEF', 'MA-SET', 'MA-SIK', 'MA-TAO', 'MA-TAR', 'MA-TAT', 'MA-TAZ', - 'MA-TET', 'MA-TIZ', 'MA-TNG', 'MA-TNT', 'MD-BA', 'MD-CA', 'MD-CH', 'MD-CU', - 'MD-ED', 'MD-GA', 'MD-LA', 'MD-OR', 'MD-SN', 'MD-SO', 'MD-TA', 'MD-TI', 'MD-UN', - 'MG-A', 'MG-D', 'MG-F', 'MG-M', 'MG-T', 'MG-U', 'MH-ALK', 'MH-ALL', 'MH-ARN', - 'MH-AUR', 'MH-EBO', 'MH-ENI', 'MH-JAL', 'MH-KIL', 'MH-KWA', 'MH-L', 'MH-LAE', - 'MH-LIB', 'MH-LIK', 'MH-MAJ', 'MH-MAL', 'MH-MEJ', 'MH-MIL', 'MH-NMK', 'MH-NMU', - 'MH-RON', 'MH-T', 'MH-UJA', 'MH-UJL', 'MH-UTI', 'MH-WTH', 'MH-WTJ', 'ML-1', - 'ML-2', 'ML-3', 'ML-4', 'ML-5', 'ML-6', 'ML-7', 'ML-8', 'ML-BKO', 'MM-01', - 'MM-02', 'MM-03', 'MM-04', 'MM-05', 'MM-06', 'MM-07', 'MM-11', 'MM-12', 'MM-13', - 'MM-14', 'MM-15', 'MM-16', 'MM-17', 'MN-035', 'MN-037', 'MN-039', 'MN-041', - 'MN-043', 'MN-046', 'MN-047', 'MN-049', 'MN-051', 'MN-053', 'MN-055', 'MN-057', - 'MN-059', 'MN-061', 'MN-063', 'MN-064', 'MN-065', 'MN-067', 'MN-069', 'MN-071', - 'MN-073', 'MN-1', 'MR-01', 'MR-02', 'MR-03', 'MR-04', 'MR-05', 'MR-06', 'MR-07', - 'MR-08', 'MR-09', 'MR-10', 'MR-11', 'MR-12', 'MR-NKC', 'MU-AG', 'MU-BL', 'MU-BR', - 'MU-CC', 'MU-CU', 'MU-FL', 'MU-GP', 'MU-MO', 'MU-PA', 'MU-PL', 'MU-PU', 'MU-PW', - 'MU-QB', 'MU-RO', 'MU-RR', 'MU-SA', 'MU-VP', 'MV-01', 'MV-02', 'MV-03', 'MV-04', - 'MV-05', 'MV-07', 'MV-08', 'MV-12', 'MV-13', 'MV-14', 'MV-17', 'MV-20', 'MV-23', - 'MV-24', 'MV-25', 'MV-26', 'MV-27', 'MV-28', 'MV-29', 'MV-MLE', 'MW-BA', 'MW-BL', - 'MW-C', 'MW-CK', 'MW-CR', 'MW-CT', 'MW-DE', 'MW-DO', 'MW-KR', 'MW-KS', 'MW-LI', - 'MW-LK', 'MW-MC', 'MW-MG', 'MW-MH', 'MW-MU', 'MW-MW', 'MW-MZ', 'MW-N', 'MW-NB', - 'MW-NI', 'MW-NK', 'MW-NS', 'MW-NU', 'MW-PH', 'MW-RU', 'MW-S', 'MW-SA', 'MW-TH', - 'MW-ZO', 'MX-AGU', 'MX-BCN', 'MX-BCS', 'MX-CAM', 'MX-CHH', 'MX-CHP', 'MX-COA', - 'MX-COL', 'MX-DIF', 'MX-DUR', 'MX-GRO', 'MX-GUA', 'MX-HID', 'MX-JAL', 'MX-MEX', - 'MX-MIC', 'MX-MOR', 'MX-NAY', 'MX-NLE', 'MX-OAX', 'MX-PUE', 'MX-QUE', 'MX-ROO', - 'MX-SIN', 'MX-SLP', 'MX-SON', 'MX-TAB', 'MX-TAM', 'MX-TLA', 'MX-VER', 'MX-YUC', - 'MX-ZAC', 'MY-A', 'MY-B', 'MY-C', 'MY-D', 'MY-J', 'MY-K', 'MY-L', 'MY-M', 'MY-N', - 'MY-P', 'MY-R', 'MY-SA', 'MY-SK', 'MY-T', 'MY-W', 'MZ-A', 'MZ-B', 'MZ-G', 'MZ-I', - 'MZ-L', 'MZ-MPM', 'MZ-N', 'MZ-P', 'MZ-Q', 'MZ-S', 'MZ-T', 'NA-CA', 'NA-ER', - 'NA-HA', 'NA-KA', 'NA-KH', 'NA-KU', 'NA-OD', 'NA-OH', 'NA-OK', 'NA-ON', 'NA-OS', - 'NA-OT', 'NA-OW', 'NE-1', 'NE-2', 'NE-3', 'NE-4', 'NE-5', 'NE-6', 'NE-7', 'NE-8', - 'NG-AB', 'NG-AD', 'NG-AK', 'NG-AN', 'NG-BA', 'NG-BE', 'NG-BO', 'NG-BY', 'NG-CR', - 'NG-DE', 'NG-EB', 'NG-ED', 'NG-EK', 'NG-EN', 'NG-FC', 'NG-GO', 'NG-IM', 'NG-JI', - 'NG-KD', 'NG-KE', 'NG-KN', 'NG-KO', 'NG-KT', 'NG-KW', 'NG-LA', 'NG-NA', 'NG-NI', - 'NG-OG', 'NG-ON', 'NG-OS', 'NG-OY', 'NG-PL', 'NG-RI', 'NG-SO', 'NG-TA', 'NG-YO', - 'NG-ZA', 'NI-AN', 'NI-AS', 'NI-BO', 'NI-CA', 'NI-CI', 'NI-CO', 'NI-ES', 'NI-GR', - 'NI-JI', 'NI-LE', 'NI-MD', 'NI-MN', 'NI-MS', 'NI-MT', 'NI-NS', 'NI-RI', 'NI-SJ', - 'NL-DR', 'NL-FL', 'NL-FR', 'NL-GE', 'NL-GR', 'NL-LI', 'NL-NB', 'NL-NH', 'NL-OV', - 'NL-UT', 'NL-ZE', 'NL-ZH', 'NO-01', 'NO-02', 'NO-03', 'NO-04', 'NO-05', 'NO-06', - 'NO-07', 'NO-08', 'NO-09', 'NO-10', 'NO-11', 'NO-12', 'NO-14', 'NO-15', 'NO-16', - 'NO-17', 'NO-18', 'NO-19', 'NO-20', 'NO-21', 'NO-22', 'NP-1', 'NP-2', 'NP-3', - 'NP-4', 'NP-5', 'NP-BA', 'NP-BH', 'NP-DH', 'NP-GA', 'NP-JA', 'NP-KA', 'NP-KO', - 'NP-LU', 'NP-MA', 'NP-ME', 'NP-NA', 'NP-RA', 'NP-SA', 'NP-SE', 'NZ-AUK', 'NZ-BOP', - 'NZ-CAN', 'NZ-GIS', 'NZ-HKB', 'NZ-MBH', 'NZ-MWT', 'NZ-N', 'NZ-NSN', 'NZ-NTL', - 'NZ-OTA', 'NZ-S', 'NZ-STL', 'NZ-TAS', 'NZ-TKI', 'NZ-WGN', 'NZ-WKO', 'NZ-WTC', - 'OM-BA', 'OM-DA', 'OM-JA', 'OM-MA', 'OM-MU', 'OM-SH', 'OM-WU', 'OM-ZA', 'PA-0', - 'PA-1', 'PA-2', 'PA-3', 'PA-4', 'PA-5', 'PA-6', 'PA-7', 'PA-8', 'PA-9', 'PE-AMA', - 'PE-ANC', 'PE-APU', 'PE-ARE', 'PE-AYA', 'PE-CAJ', 'PE-CAL', 'PE-CUS', 'PE-HUC', - 'PE-HUV', 'PE-ICA', 'PE-JUN', 'PE-LAL', 'PE-LAM', 'PE-LIM', 'PE-LOR', 'PE-MDD', - 'PE-MOQ', 'PE-PAS', 'PE-PIU', 'PE-PUN', 'PE-SAM', 'PE-TAC', 'PE-TUM', 'PE-UCA', - 'PG-CPK', 'PG-CPM', 'PG-EBR', 'PG-EHG', 'PG-EPW', 'PG-ESW', 'PG-GPK', 'PG-MBA', - 'PG-MPL', 'PG-MPM', 'PG-MRL', 'PG-NCD', 'PG-NIK', 'PG-NPP', 'PG-NSA', 'PG-SAN', - 'PG-SHM', 'PG-WBK', 'PG-WHM', 'PG-WPD', 'PH-00', 'PH-01', 'PH-02', 'PH-03', - 'PH-04', 'PH-05', 'PH-06', 'PH-07', 'PH-08', 'PH-09', 'PH-10', 'PH-11', 'PH-12', - 'PH-13', 'PH-14', 'PH-15', 'PH-ABR', 'PH-AGN', 'PH-AGS', 'PH-AKL', 'PH-ALB', - 'PH-ANT', 'PH-APA', 'PH-AUR', 'PH-BAN', 'PH-BAS', 'PH-BEN', 'PH-BIL', 'PH-BOH', - 'PH-BTG', 'PH-BTN', 'PH-BUK', 'PH-BUL', 'PH-CAG', 'PH-CAM', 'PH-CAN', 'PH-CAP', - 'PH-CAS', 'PH-CAT', 'PH-CAV', 'PH-CEB', 'PH-COM', 'PH-DAO', 'PH-DAS', 'PH-DAV', - 'PH-EAS', 'PH-GUI', 'PH-IFU', 'PH-ILI', 'PH-ILN', 'PH-ILS', 'PH-ISA', 'PH-KAL', - 'PH-LAG', 'PH-LAN', 'PH-LAS', 'PH-LEY', 'PH-LUN', 'PH-MAD', 'PH-MAG', 'PH-MAS', - 'PH-MDC', 'PH-MDR', 'PH-MOU', 'PH-MSC', 'PH-MSR', 'PH-NCO', 'PH-NEC', 'PH-NER', - 'PH-NSA', 'PH-NUE', 'PH-NUV', 'PH-PAM', 'PH-PAN', 'PH-PLW', 'PH-QUE', 'PH-QUI', - 'PH-RIZ', 'PH-ROM', 'PH-SAR', 'PH-SCO', 'PH-SIG', 'PH-SLE', 'PH-SLU', 'PH-SOR', - 'PH-SUK', 'PH-SUN', 'PH-SUR', 'PH-TAR', 'PH-TAW', 'PH-WSA', 'PH-ZAN', 'PH-ZAS', - 'PH-ZMB', 'PH-ZSI', 'PK-BA', 'PK-IS', 'PK-JK', 'PK-NA', 'PK-NW', 'PK-PB', 'PK-SD', - 'PK-TA', 'PL-DS', 'PL-KP', 'PL-LB', 'PL-LD', 'PL-LU', 'PL-MA', 'PL-MZ', 'PL-OP', - 'PL-PD', 'PL-PK', 'PL-PM', 'PL-SK', 'PL-SL', 'PL-WN', 'PL-WP', 'PL-ZP', 'PT-01', - 'PT-02', 'PT-03', 'PT-04', 'PT-05', 'PT-06', 'PT-07', 'PT-08', 'PT-09', 'PT-10', - 'PT-11', 'PT-12', 'PT-13', 'PT-14', 'PT-15', 'PT-16', 'PT-17', 'PT-18', 'PT-20', - 'PT-30', 'PY-1', 'PY-10', 'PY-11', 'PY-12', 'PY-13', 'PY-14', 'PY-15', 'PY-16', - 'PY-19', 'PY-2', 'PY-3', 'PY-4', 'PY-5', 'PY-6', 'PY-7', 'PY-8', 'PY-9', 'PY-ASU', - 'QA-DA', 'QA-GH', 'QA-JB', 'QA-JU', 'QA-KH', 'QA-MS', 'QA-RA', 'QA-US', 'QA-WA', - 'RO-AB', 'RO-AG', 'RO-AR', 'RO-B', 'RO-BC', 'RO-BH', 'RO-BN', 'RO-BR', 'RO-BT', - 'RO-BV', 'RO-BZ', 'RO-CJ', 'RO-CL', 'RO-CS', 'RO-CT', 'RO-CV', 'RO-DB', 'RO-DJ', - 'RO-GJ', 'RO-GL', 'RO-GR', 'RO-HD', 'RO-HR', 'RO-IF', 'RO-IL', 'RO-IS', 'RO-MH', - 'RO-MM', 'RO-MS', 'RO-NT', 'RO-OT', 'RO-PH', 'RO-SB', 'RO-SJ', 'RO-SM', 'RO-SV', - 'RO-TL', 'RO-TM', 'RO-TR', 'RO-VL', 'RO-VN', 'RO-VS', 'RU-AD', 'RU-AGB', 'RU-AL', - 'RU-ALT', 'RU-AMU', 'RU-ARK', 'RU-AST', 'RU-BA', 'RU-BEL', 'RU-BRY', 'RU-BU', - 'RU-CE', 'RU-CHE', 'RU-CHI', 'RU-CHU', 'RU-CU', 'RU-DA', 'RU-DU', 'RU-EVE', - 'RU-IN', 'RU-IRK', 'RU-IVA', 'RU-KAM', 'RU-KB', 'RU-KC', 'RU-KDA', 'RU-KEM', - 'RU-KGD', 'RU-KGN', 'RU-KHA', 'RU-KHM', 'RU-KIR', 'RU-KK', 'RU-KL', 'RU-KLU', - 'RU-KO', 'RU-KOP', 'RU-KOR', 'RU-KOS', 'RU-KR', 'RU-KRS', 'RU-KYA', 'RU-LEN', - 'RU-LIP', 'RU-MAG', 'RU-ME', 'RU-MO', 'RU-MOS', 'RU-MOW', 'RU-MUR', 'RU-NEN', - 'RU-NGR', 'RU-NIZ', 'RU-NVS', 'RU-OMS', 'RU-ORE', 'RU-ORL', 'RU-PER', 'RU-PNZ', - 'RU-PRI', 'RU-PSK', 'RU-ROS', 'RU-RYA', 'RU-SA', 'RU-SAK', 'RU-SAM', 'RU-SAR', - 'RU-SE', 'RU-SMO', 'RU-SPE', 'RU-STA', 'RU-SVE', 'RU-TA', 'RU-TAM', 'RU-TAY', - 'RU-TOM', 'RU-TUL', 'RU-TVE', 'RU-TY', 'RU-TYU', 'RU-ULY', 'RU-UOB', 'RU-VGG', - 'RU-VLA', 'RU-VLG', 'RU-VOR', 'RU-YAN', 'RU-YAR', 'RU-YEV', 'RW-B', 'RW-C', - 'RW-D', 'RW-E', 'RW-F', 'RW-G', 'RW-H', 'RW-I', 'RW-J', 'RW-K', 'RW-L', 'RW-M', - 'SA-01', 'SA-02', 'SA-03', 'SA-04', 'SA-05', 'SA-06', 'SA-07', 'SA-08', 'SA-09', - 'SA-10', 'SA-11', 'SA-12', 'SA-14', 'SB-CE', 'SB-CT', 'SB-GU', 'SB-IS', 'SB-MK', - 'SB-ML', 'SB-TE', 'SB-WE', 'SD-01', 'SD-02', 'SD-03', 'SD-04', 'SD-05', 'SD-06', - 'SD-07', 'SD-08', 'SD-09', 'SD-10', 'SD-11', 'SD-12', 'SD-13', 'SD-14', 'SD-15', - 'SD-16', 'SD-17', 'SD-18', 'SD-19', 'SD-20', 'SD-21', 'SD-22', 'SD-23', 'SD-24', - 'SD-25', 'SD-26', 'SE-AB', 'SE-AC', 'SE-BD', 'SE-C', 'SE-D', 'SE-E', 'SE-F', - 'SE-G', 'SE-H', 'SE-I', 'SE-K', 'SE-M', 'SE-N', 'SE-O', 'SE-S', 'SE-T', 'SE-U', - 'SE-W', 'SE-X', 'SE-Y', 'SE-Z', 'SH-AC', 'SH-SH', 'SH-TA', 'SI-01', 'SI-02', - 'SI-03', 'SI-04', 'SI-05', 'SI-06', 'SI-07', 'SI-08', 'SI-09', 'SI-10', 'SI-11', - 'SI-12', 'SK-BC', 'SK-BL', 'SK-KI', 'SK-NI', 'SK-PV', 'SK-TA', 'SK-TC', 'SK-ZI', - 'SL-E', 'SL-N', 'SL-S', 'SL-W', 'SN-DB', 'SN-DK', 'SN-FK', 'SN-KD', 'SN-KL', - 'SN-LG', 'SN-SL', 'SN-TC', 'SN-TH', 'SN-ZG', 'SO-AW', 'SO-BK', 'SO-BN', 'SO-BR', - 'SO-BY', 'SO-GA', 'SO-GE', 'SO-HI', 'SO-JD', 'SO-JH', 'SO-MU', 'SO-NU', 'SO-SA', - 'SO-SD', 'SO-SH', 'SO-SO', 'SO-TO', 'SO-WO', 'SR-BR', 'SR-CM', 'SR-CR', 'SR-MA', - 'SR-NI', 'SR-PM', 'SR-PR', 'SR-SA', 'SR-SI', 'SR-WA', 'ST-P', 'ST-S', 'SV-AH', - 'SV-CA', 'SV-CH', 'SV-CU', 'SV-LI', 'SV-MO', 'SV-PA', 'SV-SA', 'SV-SM', 'SV-SO', - 'SV-SS', 'SV-SV', 'SV-UN', 'SV-US', 'SY-DI', 'SY-DR', 'SY-DY', 'SY-HA', 'SY-HI', - 'SY-HL', 'SY-HM', 'SY-ID', 'SY-LA', 'SY-QU', 'SY-RA', 'SY-RD', 'SY-SU', 'SY-TA', - 'SZ-HH', 'SZ-LU', 'SZ-MA', 'SZ-SH', 'TD-BA', 'TD-BET', 'TD-BI', 'TD-CB', 'TD-GR', - 'TD-KA', 'TD-LC', 'TD-LO', 'TD-LR', 'TD-MC', 'TD-MK', 'TD-OD', 'TD-SA', 'TD-TA', - 'TG-C', 'TG-K', 'TG-M', 'TG-P', 'TG-S', 'TH-10', 'TH-11', 'TH-12', 'TH-13', - 'TH-14', 'TH-15', 'TH-16', 'TH-17', 'TH-18', 'TH-19', 'TH-20', 'TH-21', 'TH-22', - 'TH-23', 'TH-24', 'TH-25', 'TH-26', 'TH-27', 'TH-30', 'TH-31', 'TH-32', 'TH-33', - 'TH-34', 'TH-35', 'TH-36', 'TH-37', 'TH-39', 'TH-40', 'TH-41', 'TH-42', 'TH-43', - 'TH-44', 'TH-45', 'TH-46', 'TH-47', 'TH-48', 'TH-49', 'TH-50', 'TH-51', 'TH-52', - 'TH-53', 'TH-54', 'TH-55', 'TH-56', 'TH-57', 'TH-58', 'TH-60', 'TH-61', 'TH-62', - 'TH-63', 'TH-64', 'TH-65', 'TH-66', 'TH-67', 'TH-70', 'TH-71', 'TH-72', 'TH-73', - 'TH-74', 'TH-75', 'TH-76', 'TH-77', 'TH-80', 'TH-81', 'TH-82', 'TH-83', 'TH-84', - 'TH-85', 'TH-86', 'TH-90', 'TH-91', 'TH-92', 'TH-93', 'TH-94', 'TH-95', 'TH-96', - 'TH-S', 'TJ-GB', 'TJ-KT', 'TJ-SU', 'TL-AL', 'TL-AN', 'TL-BA', 'TL-BO', 'TL-CO', - 'TL-DI', 'TL-ER', 'TL-LA', 'TL-LI', 'TL-MF', 'TL-MT', 'TL-OE', 'TL-VI', 'TM-A', - 'TM-B', 'TM-D', 'TM-L', 'TM-M', 'TN-11', 'TN-12', 'TN-13', 'TN-21', 'TN-22', - 'TN-23', 'TN-31', 'TN-32', 'TN-33', 'TN-34', 'TN-41', 'TN-42', 'TN-43', 'TN-51', - 'TN-52', 'TN-53', 'TN-61', 'TN-71', 'TN-72', 'TN-73', 'TN-81', 'TN-82', 'TN-83', - 'TR-01', 'TR-02', 'TR-03', 'TR-04', 'TR-05', 'TR-06', 'TR-07', 'TR-08', 'TR-09', - 'TR-10', 'TR-11', 'TR-12', 'TR-13', 'TR-14', 'TR-15', 'TR-16', 'TR-17', 'TR-18', - 'TR-19', 'TR-20', 'TR-21', 'TR-22', 'TR-23', 'TR-24', 'TR-25', 'TR-26', 'TR-27', - 'TR-28', 'TR-29', 'TR-30', 'TR-31', 'TR-32', 'TR-33', 'TR-34', 'TR-35', 'TR-36', - 'TR-37', 'TR-38', 'TR-39', 'TR-40', 'TR-41', 'TR-42', 'TR-43', 'TR-44', 'TR-45', - 'TR-46', 'TR-47', 'TR-48', 'TR-49', 'TR-50', 'TR-51', 'TR-52', 'TR-53', 'TR-54', - 'TR-55', 'TR-56', 'TR-57', 'TR-58', 'TR-59', 'TR-60', 'TR-61', 'TR-62', 'TR-63', - 'TR-64', 'TR-65', 'TR-66', 'TR-67', 'TR-68', 'TR-69', 'TR-70', 'TR-71', 'TR-72', - 'TR-73', 'TR-74', 'TR-75', 'TR-76', 'TR-77', 'TR-78', 'TR-79', 'TR-80', 'TR-81', - 'TT-ARI', 'TT-CHA', 'TT-CTT', 'TT-DMN', 'TT-ETO', 'TT-PED', 'TT-POS', 'TT-PRT', - 'TT-PTF', 'TT-RCM', 'TT-SFO', 'TT-SGE', 'TT-SIP', 'TT-SJL', 'TT-TUP', 'TT-WTO', - 'TW-CHA', 'TW-CYQ', 'TW-HSQ', 'TW-HUA', 'TW-ILA', 'TW-KEE', 'TW-KHQ', 'TW-MIA', - 'TW-NAN', 'TW-PEN', 'TW-PIF', 'TW-TAO', 'TW-TNQ', 'TW-TPQ', 'TW-TTT', 'TW-TXQ', - 'TW-YUN', 'TZ-01', 'TZ-02', 'TZ-03', 'TZ-04', 'TZ-05', 'TZ-06', 'TZ-07', 'TZ-08', - 'TZ-09', 'TZ-10', 'TZ-11', 'TZ-12', 'TZ-13', 'TZ-14', 'TZ-15', 'TZ-16', 'TZ-17', - 'TZ-18', 'TZ-19', 'TZ-20', 'TZ-21', 'TZ-22', 'TZ-23', 'TZ-24', 'TZ-25', 'UA-05', - 'UA-07', 'UA-09', 'UA-12', 'UA-14', 'UA-18', 'UA-21', 'UA-23', 'UA-26', 'UA-30', - 'UA-32', 'UA-35', 'UA-40', 'UA-43', 'UA-46', 'UA-48', 'UA-51', 'UA-53', 'UA-56', - 'UA-59', 'UA-61', 'UA-63', 'UA-65', 'UA-68', 'UA-71', 'UA-74', 'UA-77', 'UG-AJM', - 'UG-APA', 'UG-ARU', 'UG-BUA', 'UG-BUG', 'UG-BUN', 'UG-BUS', 'UG-C', 'UG-E', - 'UG-GUL', 'UG-HOI', 'UG-IGA', 'UG-JIN', 'UG-KAP', 'UG-KAS', 'UG-KAT', 'UG-KBL', - 'UG-KBR', 'UG-KIB', 'UG-KIS', 'UG-KIT', 'UG-KLA', 'UG-KLE', 'UG-KLG', 'UG-KLI', - 'UG-KOT', 'UG-KUM', 'UG-LIR', 'UG-LUW', 'UG-MBL', 'UG-MBR', 'UG-MOR', 'UG-MOY', - 'UG-MPI', 'UG-MSI', 'UG-MSK', 'UG-MUB', 'UG-MUK', 'UG-N', 'UG-NAK', 'UG-NEB', - 'UG-NTU', 'UG-PAL', 'UG-RAK', 'UG-RUK', 'UG-SEM', 'UG-SOR', 'UG-TOR', 'UG-W', - 'UM-67', 'UM-71', 'UM-76', 'UM-79', 'UM-81', 'UM-84', 'UM-86', 'UM-89', 'UM-95', - 'US-AK', 'US-AL', 'US-AR', 'US-AS', 'US-AZ', 'US-CA', 'US-CO', 'US-CT', 'US-DC', - 'US-DE', 'US-FL', 'US-GA', 'US-GU', 'US-HI', 'US-IA', 'US-ID', 'US-IL', 'US-IN', - 'US-KS', 'US-KY', 'US-LA', 'US-MA', 'US-MD', 'US-ME', 'US-MI', 'US-MN', 'US-MO', - 'US-MP', 'US-MS', 'US-MT', 'US-NC', 'US-ND', 'US-NE', 'US-NH', 'US-NJ', 'US-NM', - 'US-NV', 'US-NY', 'US-OH', 'US-OK', 'US-OR', 'US-PA', 'US-PR', 'US-RI', 'US-SC', - 'US-SD', 'US-TN', 'US-TX', 'US-UM', 'US-UT', 'US-VA', 'US-VI', 'US-VT', 'US-WA', - 'US-WI', 'US-WV', 'US-WY', 'UY-AR', 'UY-CA', 'UY-CL', 'UY-CO', 'UY-DU', 'UY-FD', - 'UY-FS', 'UY-LA', 'UY-MA', 'UY-MO', 'UY-PA', 'UY-RN', 'UY-RO', 'UY-RV', 'UY-SA', - 'UY-SJ', 'UY-SO', 'UY-TA', 'UY-TT', 'UZ-AN', 'UZ-BU', 'UZ-FA', 'UZ-JI', 'UZ-NG', - 'UZ-NW', 'UZ-QA', 'UZ-QR', 'UZ-SA', 'UZ-SI', 'UZ-SU', 'UZ-TK', 'UZ-TO', 'UZ-XO', - 'VE-A', 'VE-B', 'VE-C', 'VE-D', 'VE-E', 'VE-F', 'VE-G', 'VE-H', 'VE-I', 'VE-J', - 'VE-K', 'VE-L', 'VE-M', 'VE-N', 'VE-O', 'VE-P', 'VE-R', 'VE-S', 'VE-T', 'VE-U', - 'VE-V', 'VE-W', 'VE-X', 'VE-Y', 'VE-Z', 'VN-01', 'VN-02', 'VN-03', 'VN-04', - 'VN-05', 'VN-06', 'VN-07', 'VN-09', 'VN-13', 'VN-14', 'VN-15', 'VN-18', 'VN-20', - 'VN-21', 'VN-22', 'VN-23', 'VN-24', 'VN-25', 'VN-26', 'VN-27', 'VN-28', 'VN-29', - 'VN-30', 'VN-31', 'VN-32', 'VN-33', 'VN-34', 'VN-35', 'VN-36', 'VN-37', 'VN-39', - 'VN-40', 'VN-41', 'VN-43', 'VN-44', 'VN-45', 'VN-46', 'VN-47', 'VN-48', 'VN-49', - 'VN-50', 'VN-51', 'VN-52', 'VN-53', 'VN-54', 'VN-55', 'VN-56', 'VN-57', 'VN-58', - 'VN-59', 'VN-60', 'VN-61', 'VN-62', 'VN-63', 'VN-64', 'VN-65', 'VN-66', 'VN-67', - 'VN-68', 'VN-69', 'VN-70', 'VU-MAP', 'VU-PAM', 'VU-SAM', 'VU-SEE', 'VU-TAE', - 'VU-TOB', 'WS-AA', 'WS-AL', 'WS-AT', 'WS-FA', 'WS-GE', 'WS-GI', 'WS-PA', 'WS-SA', - 'WS-TU', 'WS-VF', 'WS-VS', 'YE-AB', 'YE-AD', 'YE-AM', 'YE-BA', 'YE-DA', 'YE-DH', - 'YE-HD', 'YE-HJ', 'YE-HU', 'YE-IB', 'YE-JA', 'YE-LA', 'YE-MA', 'YE-MR', 'YE-MW', - 'YE-SD', 'YE-SH', 'YE-SN', 'YE-TA', 'YU-CG', 'YU-KM', 'YU-SR', 'YU-VO', 'ZA-EC', - 'ZA-FS', 'ZA-GT', 'ZA-MP', 'ZA-NC', 'ZA-NL', 'ZA-NP', 'ZA-NW', 'ZA-WC', 'ZM-01', - 'ZM-02', 'ZM-03', 'ZM-04', 'ZM-05', 'ZM-06', 'ZM-07', 'ZM-08', 'ZM-09', 'ZW-BU', - 'ZW-HA', 'ZW-MA', 'ZW-MC', 'ZW-ME', 'ZW-MI', 'ZW-MN', 'ZW-MS', 'ZW-MV', 'ZW-MW', - name='subdivision'), nullable=False), - sa.Column('city', sa.Unicode(length=32), nullable=False), - sa.Column('city_confidence', sa.SmallInteger(), nullable=False), - sa.Column('isp', sa.Unicode(length=32), nullable=False), - sa.Column('organization', sa.Unicode(length=32), nullable=True), - sa.Column('organization_type', sa.Unicode(length=32), nullable=True), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action_with_one_device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'live', + sa.Column( + 'ip', + teal.db.IP(), + nullable=False, + comment='The IP where the live was triggered.', + ), + sa.Column('subdivision_confidence', sa.SmallInteger(), nullable=False), + sa.Column( + 'subdivision', + sa.Enum( + 'AE-AJ', + 'AE-AZ', + 'AE-DU', + 'AE-FU', + 'AE-RK', + 'AE-SH', + 'AE-UQ', + 'AF-BAL', + 'AF-BAM', + 'AF-BDG', + 'AF-BDS', + 'AF-BGL', + 'AF-FRAU', + 'AF-FYB', + 'AF-GHA', + 'AF-GHO', + 'AF-HEL', + 'AF-HER', + 'AF-JOW', + 'AF-KAB', + 'AF-KANN', + 'AF-KAP', + 'AF-KDZ', + 'AF-KNR', + 'AF-LAG', + 'AF-LOW', + 'AF-NAN', + 'AF-NIM', + 'AF-ORU', + 'AF-PAR', + 'AF-PIA', + 'AF-PKA', + 'AF-SAM', + 'AF-SAR', + 'AF-TAK', + 'AF-WAR', + 'AF-ZAB', + 'AL-BR', + 'AL-BU', + 'AL-DI', + 'AL-DL', + 'AL-DR', + 'AL-DV', + 'AL-EL', + 'AL-ER', + 'AL-FR', + 'AL-GJ', + 'AL-GR', + 'AL-HA', + 'AL-KA', + 'AL-KB', + 'AL-KC', + 'AL-KO', + 'AL-KR', + 'AL-KU', + 'AL-LA', + 'AL-LB', + 'AL-LE', + 'AL-LU', + 'AL-MK', + 'AL-MM', + 'AL-MR', + 'AL-MT', + 'AL-PG', + 'AL-PQ', + 'AL-PR', + 'AL-PU', + 'AL-SH', + 'AL-SK', + 'AL-SR', + 'AL-TE', + 'AL-TP', + 'AL-TR', + 'AL-VL', + 'AM-AG', + 'AM-AR', + 'AM-AV', + 'AM-ER', + 'AM-GR', + 'AM-KT', + 'AM-LO', + 'AM-SH', + 'AM-SU', + 'AM-TV', + 'AM-VD', + 'AO-BGO', + 'AO-BGU', + 'AO-BIE', + 'AO-CAB', + 'AO-CCU', + 'AO-CNN', + 'AO-CNO', + 'AO-CUS', + 'AO-HUA', + 'AO-HUI', + 'AO-LNO', + 'AO-LSU', + 'AO-LUA', + 'AO-MAL', + 'AO-MOX', + 'AO-NAM', + 'AO-UIG', + 'AO-ZAI', + 'AR-A', + 'AR-B', + 'AR-C', + 'AR-D', + 'AR-E', + 'AR-F', + 'AR-G', + 'AR-H', + 'AR-J', + 'AR-K', + 'AR-L', + 'AR-M', + 'AR-N', + 'AR-P', + 'AR-Q', + 'AR-R', + 'AR-S', + 'AR-T', + 'AR-U', + 'AR-V', + 'AR-W', + 'AR-X', + 'AR-Y', + 'AR-Z', + 'AT-1', + 'AT-2', + 'AT-3', + 'AT-4', + 'AT-5', + 'AT-6', + 'AT-7', + 'AT-8', + 'AT-9', + 'AU-CT', + 'AU-NS', + 'AU-NT', + 'AU-QL', + 'AU-SA', + 'AU-TS', + 'AU-VI', + 'AU-WA', + 'AZ-AB', + 'AZ-ABS', + 'AZ-AGA', + 'AZ-AGC', + 'AZ-AGM', + 'AZ-AGS', + 'AZ-AGU', + 'AZ-AST', + 'AZ-BA', + 'AZ-BAB', + 'AZ-BAL', + 'AZ-BAR', + 'AZ-BEY', + 'AZ-BIL', + 'AZ-CAB', + 'AZ-CAL', + 'AZ-CUL', + 'AZ-DAS', + 'AZ-DAV', + 'AZ-FUZ', + 'AZ-GA', + 'AZ-GAD', + 'AZ-GOR', + 'AZ-GOY', + 'AZ-HAC', + 'AZ-IMI', + 'AZ-ISM', + 'AZ-KAL', + 'AZ-KUR', + 'AZ-LA', + 'AZ-LAC', + 'AZ-LAN', + 'AZ-LER', + 'AZ-MAS', + 'AZ-MI', + 'AZ-MM', + 'AZ-NA', + 'AZ-NEF', + 'AZ-OGU', + 'AZ-ORD', + 'AZ-QAB', + 'AZ-QAX', + 'AZ-QAZ', + 'AZ-QBA', + 'AZ-QBI', + 'AZ-QOB', + 'AZ-QUS', + 'AZ-SA', + 'AZ-SAB', + 'AZ-SAD', + 'AZ-SAH', + 'AZ-SAK', + 'AZ-SAL', + 'AZ-SAR', + 'AZ-SAT', + 'AZ-SIY', + 'AZ-SKR', + 'AZ-SM', + 'AZ-SMI', + 'AZ-SMX', + 'AZ-SS', + 'AZ-SUS', + 'AZ-TAR', + 'AZ-TOV', + 'AZ-UCA', + 'AZ-XA', + 'AZ-XAC', + 'AZ-XAN', + 'AZ-XCI', + 'AZ-XIZ', + 'AZ-XVD', + 'AZ-YAR', + 'AZ-YE', + 'AZ-YEV', + 'AZ-ZAN', + 'AZ-ZAQ', + 'AZ-ZAR', + 'BA-BIH', + 'BA-SRP', + 'BD-01', + 'BD-02', + 'BD-03', + 'BD-04', + 'BD-05', + 'BD-06', + 'BD-07', + 'BD-08', + 'BD-09', + 'BD-1', + 'BD-10', + 'BD-11', + 'BD-12', + 'BD-13', + 'BD-14', + 'BD-15', + 'BD-16', + 'BD-17', + 'BD-18', + 'BD-19', + 'BD-2', + 'BD-20', + 'BD-21', + 'BD-22', + 'BD-23', + 'BD-24', + 'BD-25', + 'BD-26', + 'BD-27', + 'BD-28', + 'BD-29', + 'BD-3', + 'BD-30', + 'BD-31', + 'BD-32', + 'BD-33', + 'BD-34', + 'BD-35', + 'BD-36', + 'BD-37', + 'BD-38', + 'BD-39', + 'BD-4', + 'BD-40', + 'BD-41', + 'BD-42', + 'BD-43', + 'BD-44', + 'BD-45', + 'BD-46', + 'BD-47', + 'BD-48', + 'BD-49', + 'BD-5', + 'BD-50', + 'BD-51', + 'BD-52', + 'BD-53', + 'BD-54', + 'BD-55', + 'BD-56', + 'BD-57', + 'BD-58', + 'BD-59', + 'BD-6', + 'BD-60', + 'BD-61', + 'BD-62', + 'BD-63', + 'BD-64', + 'BE-BRU', + 'BE-VAN', + 'BE-VBR', + 'BE-VLG', + 'BE-VLI', + 'BE-VOV', + 'BE-VWV', + 'BE-WAL', + 'BE-WBR', + 'BE-WHT', + 'BE-WLG', + 'BE-WLX', + 'BE-WNA', + 'BF-BAL', + 'BF-BAM', + 'BF-BAN', + 'BF-BAZ', + 'BF-BGR', + 'BF-BLG', + 'BF-BLK', + 'BF-COM', + 'BF-GAN', + 'BF-GNA', + 'BF-GOU', + 'BF-HOU', + 'BF-IOB', + 'BF-KAD', + 'BF-KEN', + 'BF-KMD', + 'BF-KMP', + 'BF-KOP', + 'BF-KOS', + 'BF-KOT', + 'BF-KOW', + 'BF-LER', + 'BF-LOR', + 'BF-MOU', + 'BF-NAM', + 'BF-NAO', + 'BF-NAY', + 'BF-NOU', + 'BF-OUB', + 'BF-OUD', + 'BF-PAS', + 'BF-PON', + 'BF-SEN', + 'BF-SIS', + 'BF-SMT', + 'BF-SNG', + 'BF-SOM', + 'BF-SOR', + 'BF-TAP', + 'BF-TUI', + 'BF-YAG', + 'BF-YAT', + 'BF-ZIR', + 'BF-ZON', + 'BF-ZOU', + 'BG-01', + 'BG-02', + 'BG-03', + 'BG-04', + 'BG-05', + 'BG-06', + 'BG-07', + 'BG-08', + 'BG-09', + 'BG-10', + 'BG-11', + 'BG-12', + 'BG-13', + 'BG-14', + 'BG-15', + 'BG-16', + 'BG-17', + 'BG-18', + 'BG-19', + 'BG-20', + 'BG-21', + 'BG-22', + 'BG-23', + 'BG-24', + 'BG-25', + 'BG-26', + 'BG-27', + 'BG-28', + 'BH-01', + 'BH-02', + 'BH-03', + 'BH-04', + 'BH-05', + 'BH-06', + 'BH-07', + 'BH-08', + 'BH-09', + 'BH-10', + 'BH-11', + 'BH-12', + 'BI-BB', + 'BI-BJ', + 'BI-BR', + 'BI-CA', + 'BI-CI', + 'BI-GI', + 'BI-KI', + 'BI-KR', + 'BI-KY', + 'BI-MA', + 'BI-MU', + 'BI-MW', + 'BI-MY', + 'BI-NG', + 'BI-RT', + 'BI-RY', + 'BJ-AK', + 'BJ-AL', + 'BJ-AQ', + 'BJ-BO', + 'BJ-CO', + 'BJ-DO', + 'BJ-KO', + 'BJ-LI', + 'BJ-MO', + 'BJ-OU', + 'BJ-PL', + 'BJ-ZO', + 'BN-BE', + 'BN-BM', + 'BN-TE', + 'BN-TU', + 'BO-B', + 'BO-C', + 'BO-H', + 'BO-L', + 'BO-N', + 'BO-O', + 'BO-P', + 'BO-S', + 'BO-T', + 'BR-AC', + 'BR-AL', + 'BR-AM', + 'BR-AP', + 'BR-BA', + 'BR-CE', + 'BR-DF', + 'BR-ES', + 'BR-GO', + 'BR-MA', + 'BR-MG', + 'BR-MS', + 'BR-MT', + 'BR-PA', + 'BR-PB', + 'BR-PE', + 'BR-PI', + 'BR-PR', + 'BR-RJ', + 'BR-RN', + 'BR-RO', + 'BR-RR', + 'BR-RS', + 'BR-SC', + 'BR-SE', + 'BR-SP', + 'BR-TO', + 'BS-AC', + 'BS-BI', + 'BS-CI', + 'BS-EX', + 'BS-FC', + 'BS-FP', + 'BS-GH', + 'BS-GT', + 'BS-HI', + 'BS-HR', + 'BS-IN', + 'BS-KB', + 'BS-LI', + 'BS-MG', + 'BS-MH', + 'BS-NB', + 'BS-NP', + 'BS-RI', + 'BS-RS', + 'BS-SP', + 'BS-SR', + 'BT-11', + 'BT-12', + 'BT-13', + 'BT-14', + 'BT-15', + 'BT-21', + 'BT-22', + 'BT-23', + 'BT-24', + 'BT-31', + 'BT-32', + 'BT-33', + 'BT-34', + 'BT-41', + 'BT-42', + 'BT-43', + 'BT-44', + 'BT-45', + 'BT-GA', + 'BT-TY', + 'BW-CE', + 'BW-CH', + 'BW-GH', + 'BW-KG', + 'BW-KL', + 'BW-KW', + 'BW-NE', + 'BW-NG', + 'BW-SE', + 'BW-SO', + 'BY-BR', + 'BY-HO', + 'BY-HR', + 'BY-MA', + 'BY-MI', + 'BY-VI', + 'BZ-BZ', + 'BZ-CY', + 'BZ-CZL', + 'BZ-OW', + 'BZ-SC', + 'BZ-TOL', + 'CA-AB', + 'CA-BC', + 'CA-MB', + 'CA-NB', + 'CA-NL', + 'CA-NS', + 'CA-NT', + 'CA-NU', + 'CA-ON', + 'CA-PE', + 'CA-QC', + 'CA-SK', + 'CA-YT', + 'CD-BC', + 'CD-BN', + 'CD-EQ', + 'CD-KA', + 'CD-KE', + 'CD-KN', + 'CD-KW', + 'CD-MA', + 'CD-NK', + 'CD-OR', + 'CD-SK', + 'CF-AC', + 'CF-BB', + 'CF-BGF', + 'CF-BK', + 'CF-HK', + 'CF-HM', + 'CF-HS', + 'CF-KB', + 'CF-KG', + 'CF-LB', + 'CF-MB', + 'CF-MP', + 'CF-NM', + 'CF-OP', + 'CF-SE', + 'CF-UK', + 'CF-VK', + 'CG-11', + 'CG-12', + 'CG-13', + 'CG-14', + 'CG-15', + 'CG-2', + 'CG-5', + 'CG-7', + 'CG-8', + 'CG-9', + 'CG-BZV', + 'CH-AG', + 'CH-AI', + 'CH-AR', + 'CH-BE', + 'CH-BL', + 'CH-BS', + 'CH-FR', + 'CH-GE', + 'CH-GL', + 'CH-GR', + 'CH-JU', + 'CH-LU', + 'CH-NE', + 'CH-NW', + 'CH-OW', + 'CH-SG', + 'CH-SH', + 'CH-SO', + 'CH-SZ', + 'CH-TG', + 'CH-TI', + 'CH-UR', + 'CH-VD', + 'CH-VS', + 'CH-ZG', + 'CH-ZH', + 'CI-01', + 'CI-02', + 'CI-03', + 'CI-04', + 'CI-05', + 'CI-06', + 'CI-07', + 'CI-08', + 'CI-09', + 'CI-10', + 'CI-11', + 'CI-12', + 'CI-13', + 'CI-14', + 'CI-15', + 'CI-16', + 'CL-AI', + 'CL-AN', + 'CL-AR', + 'CL-AT', + 'CL-BI', + 'CL-CO', + 'CL-LI', + 'CL-LL', + 'CL-MA', + 'CL-ML', + 'CL-RM', + 'CL-TA', + 'CL-VS', + 'CM-AD', + 'CM-CE', + 'CM-EN', + 'CM-ES', + 'CM-LT', + 'CM-NO', + 'CM-NW', + 'CM-OU', + 'CM-SU', + 'CM-SW', + 'CN-11', + 'CN-12', + 'CN-13', + 'CN-14', + 'CN-15', + 'CN-21', + 'CN-22', + 'CN-23', + 'CN-31', + 'CN-32', + 'CN-33', + 'CN-34', + 'CN-35', + 'CN-36', + 'CN-37', + 'CN-41', + 'CN-42', + 'CN-43', + 'CN-44', + 'CN-45', + 'CN-46', + 'CN-50', + 'CN-51', + 'CN-52', + 'CN-53', + 'CN-54', + 'CN-61', + 'CN-62', + 'CN-63', + 'CN-64', + 'CN-65', + 'CN-71', + 'CN-91', + 'CN-92', + 'CO-AMA', + 'CO-ANT', + 'CO-ARA', + 'CO-ATL', + 'CO-BOL', + 'CO-BOY', + 'CO-CAL', + 'CO-CAQ', + 'CO-CAS', + 'CO-CAU', + 'CO-CES', + 'CO-CHO', + 'CO-COR', + 'CO-CUN', + 'CO-DC', + 'CO-GUA', + 'CO-GUV', + 'CO-HUI', + 'CO-LAG', + 'CO-MAG', + 'CO-MET', + 'CO-NAR', + 'CO-NSA', + 'CO-PUT', + 'CO-QUI', + 'CO-RIS', + 'CO-SAN', + 'CO-SAP', + 'CO-SUC', + 'CO-TOL', + 'CO-VAC', + 'CO-VAU', + 'CO-VID', + 'CR-A', + 'CR-C', + 'CR-G', + 'CR-H', + 'CR-L', + 'CR-P', + 'CR-SJ', + 'CU-01', + 'CU-02', + 'CU-03', + 'CU-04', + 'CU-05', + 'CU-06', + 'CU-07', + 'CU-08', + 'CU-09', + 'CU-10', + 'CU-11', + 'CU-12', + 'CU-13', + 'CU-14', + 'CU-99', + 'CV-B', + 'CV-BR', + 'CV-BV', + 'CV-CA', + 'CV-CR', + 'CV-CS', + 'CV-FO', + 'CV-MA', + 'CV-MO', + 'CV-PA', + 'CV-PN', + 'CV-PR', + 'CV-RG', + 'CV-S', + 'CV-SF', + 'CV-SL', + 'CV-SN', + 'CV-SV', + 'CV-TA', + 'CY-01', + 'CY-02', + 'CY-03', + 'CY-04', + 'CY-05', + 'CY-06', + 'CZ-JC', + 'CZ-JM', + 'CZ-KA', + 'CZ-KR', + 'CZ-LI', + 'CZ-MO', + 'CZ-OL', + 'CZ-PA', + 'CZ-PL', + 'CZ-PR', + 'CZ-ST', + 'CZ-US', + 'CZ-VY', + 'CZ-ZL', + 'DE-BB', + 'DE-BE', + 'DE-BW', + 'DE-BY', + 'DE-HB', + 'DE-HE', + 'DE-HH', + 'DE-MV', + 'DE-NI', + 'DE-NW', + 'DE-RP', + 'DE-SH', + 'DE-SL', + 'DE-SN', + 'DE-ST', + 'DE-TH', + 'DJ-AS', + 'DJ-DI', + 'DJ-DJ', + 'DJ-OB', + 'DJ-TA', + 'DK-015', + 'DK-020', + 'DK-025', + 'DK-030', + 'DK-035', + 'DK-040', + 'DK-042', + 'DK-050', + 'DK-055', + 'DK-060', + 'DK-065', + 'DK-070', + 'DK-076', + 'DK-080', + 'DK-101', + 'DK-147', + 'DO-01', + 'DO-02', + 'DO-03', + 'DO-04', + 'DO-05', + 'DO-06', + 'DO-07', + 'DO-08', + 'DO-09', + 'DO-10', + 'DO-11', + 'DO-12', + 'DO-13', + 'DO-14', + 'DO-15', + 'DO-16', + 'DO-17', + 'DO-18', + 'DO-19', + 'DO-20', + 'DO-21', + 'DO-22', + 'DO-23', + 'DO-24', + 'DO-25', + 'DO-26', + 'DO-27', + 'DO-28', + 'DO-29', + 'DO-30', + 'DZ-01', + 'DZ-02', + 'DZ-03', + 'DZ-04', + 'DZ-05', + 'DZ-06', + 'DZ-07', + 'DZ-08', + 'DZ-09', + 'DZ-10', + 'DZ-11', + 'DZ-12', + 'DZ-13', + 'DZ-14', + 'DZ-15', + 'DZ-16', + 'DZ-17', + 'DZ-18', + 'DZ-19', + 'DZ-20', + 'DZ-21', + 'DZ-22', + 'DZ-23', + 'DZ-24', + 'DZ-25', + 'DZ-26', + 'DZ-27', + 'DZ-28', + 'DZ-29', + 'DZ-30', + 'DZ-31', + 'DZ-32', + 'DZ-33', + 'DZ-34', + 'DZ-35', + 'DZ-36', + 'DZ-37', + 'DZ-38', + 'DZ-39', + 'DZ-40', + 'DZ-41', + 'DZ-42', + 'DZ-43', + 'DZ-44', + 'DZ-45', + 'DZ-46', + 'DZ-47', + 'DZ-48', + 'EC-A', + 'EC-B', + 'EC-C', + 'EC-D', + 'EC-E', + 'EC-F', + 'EC-G', + 'EC-H', + 'EC-I', + 'EC-L', + 'EC-M', + 'EC-N', + 'EC-O', + 'EC-P', + 'EC-R', + 'EC-S', + 'EC-T', + 'EC-U', + 'EC-W', + 'EC-X', + 'EC-Y', + 'EC-Z', + 'EE-37', + 'EE-39', + 'EE-44', + 'EE-49', + 'EE-51', + 'EE-57', + 'EE-59', + 'EE-65', + 'EE-67', + 'EE-70', + 'EE-74', + 'EE-78', + 'EE-82', + 'EE-84', + 'EE-86', + 'EG-ALX', + 'EG-ASN', + 'EG-AST', + 'EG-BA', + 'EG-BH', + 'EG-BNS', + 'EG-C', + 'EG-DK', + 'EG-DT', + 'EG-FYM', + 'EG-GH', + 'EG-GZ', + 'EG-IS', + 'EG-JS', + 'EG-KB', + 'EG-KFS', + 'EG-KN', + 'EG-MN', + 'EG-MNF', + 'EG-MT', + 'EG-PTS', + 'EG-SHG', + 'EG-SHR', + 'EG-SIN', + 'EG-SUZ', + 'EG-WAD', + 'ER-AN', + 'ER-DK', + 'ER-DU', + 'ER-GB', + 'ER-MA', + 'ER-SK', + 'ES-A', + 'ES-AB', + 'ES-AL', + 'ES-AN', + 'ES-AR', + 'ES-AV', + 'ES-B', + 'ES-BA', + 'ES-BI', + 'ES-BU', + 'ES-C', + 'ES-CA', + 'ES-CC', + 'ES-CE', + 'ES-CL', + 'ES-CM', + 'ES-CN', + 'ES-CO', + 'ES-CR', + 'ES-CS', + 'ES-CT', + 'ES-CU', + 'ES-EX', + 'ES-GA', + 'ES-GC', + 'ES-GI', + 'ES-GR', + 'ES-GU', + 'ES-H', + 'ES-HU', + 'ES-J', + 'ES-L', + 'ES-LE', + 'ES-LO', + 'ES-LU', + 'ES-M', + 'ES-MA', + 'ES-ML', + 'ES-MU', + 'ES-NA', + 'ES-O', + 'ES-OR', + 'ES-P', + 'ES-PM', + 'ES-PO', + 'ES-PV', + 'ES-S', + 'ES-SA', + 'ES-SE', + 'ES-SG', + 'ES-SO', + 'ES-SS', + 'ES-T', + 'ES-TE', + 'ES-TF', + 'ES-TO', + 'ES-V', + 'ES-VA', + 'ES-VC', + 'ES-VI', + 'ES-Z', + 'ES-ZA', + 'ET-AA', + 'ET-AF', + 'ET-AM', + 'ET-BE', + 'ET-DD', + 'ET-GA', + 'ET-HA', + 'ET-OR', + 'ET-SN', + 'ET-SO', + 'ET-TI', + 'FI-AL', + 'FI-ES', + 'FI-IS', + 'FI-LL', + 'FI-LS', + 'FI-OL', + 'FJ-C', + 'FJ-E', + 'FJ-N', + 'FJ-R', + 'FJ-W', + 'FM-KSA', + 'FM-PNI', + 'FM-TRK', + 'FM-YAP', + 'FR-01', + 'FR-02', + 'FR-03', + 'FR-04', + 'FR-05', + 'FR-06', + 'FR-07', + 'FR-08', + 'FR-09', + 'FR-10', + 'FR-11', + 'FR-12', + 'FR-13', + 'FR-14', + 'FR-15', + 'FR-16', + 'FR-17', + 'FR-18', + 'FR-19', + 'FR-21', + 'FR-22', + 'FR-23', + 'FR-24', + 'FR-25', + 'FR-26', + 'FR-27', + 'FR-28', + 'FR-29', + 'FR-2A', + 'FR-2B', + 'FR-30', + 'FR-31', + 'FR-32', + 'FR-33', + 'FR-34', + 'FR-35', + 'FR-36', + 'FR-37', + 'FR-38', + 'FR-39', + 'FR-40', + 'FR-41', + 'FR-42', + 'FR-43', + 'FR-44', + 'FR-45', + 'FR-46', + 'FR-47', + 'FR-48', + 'FR-49', + 'FR-50', + 'FR-51', + 'FR-52', + 'FR-53', + 'FR-54', + 'FR-55', + 'FR-56', + 'FR-57', + 'FR-58', + 'FR-59', + 'FR-60', + 'FR-61', + 'FR-62', + 'FR-63', + 'FR-64', + 'FR-65', + 'FR-66', + 'FR-67', + 'FR-68', + 'FR-69', + 'FR-70', + 'FR-71', + 'FR-72', + 'FR-73', + 'FR-74', + 'FR-75', + 'FR-76', + 'FR-77', + 'FR-78', + 'FR-79', + 'FR-80', + 'FR-81', + 'FR-82', + 'FR-83', + 'FR-84', + 'FR-85', + 'FR-86', + 'FR-87', + 'FR-88', + 'FR-89', + 'FR-90', + 'FR-91', + 'FR-92', + 'FR-93', + 'FR-94', + 'FR-95', + 'FR-A', + 'FR-B', + 'FR-C', + 'FR-D', + 'FR-E', + 'FR-F', + 'FR-G', + 'FR-GF', + 'FR-GP', + 'FR-H', + 'FR-I', + 'FR-J', + 'FR-K', + 'FR-L', + 'FR-M', + 'FR-MQ', + 'FR-N', + 'FR-NC', + 'FR-O', + 'FR-P', + 'FR-PF', + 'FR-PM', + 'FR-Q', + 'FR-R', + 'FR-RE', + 'FR-S', + 'FR-T', + 'FR-TF', + 'FR-U', + 'FR-V', + 'FR-WF', + 'FR-YT', + 'GA-1', + 'GA-2', + 'GA-3', + 'GA-4', + 'GA-5', + 'GA-6', + 'GA-7', + 'GA-8', + 'GA-9', + 'GB-ABD', + 'GB-ABE', + 'GB-AGB', + 'GB-AGY', + 'GB-ANS', + 'GB-ANT', + 'GB-ARD', + 'GB-ARM', + 'GB-BAS', + 'GB-BBD', + 'GB-BDF', + 'GB-BDG', + 'GB-BEN', + 'GB-BEX', + 'GB-BFS', + 'GB-BGE', + 'GB-BGW', + 'GB-BIR', + 'GB-BKM', + 'GB-BLA', + 'GB-BLY', + 'GB-BMH', + 'GB-BNB', + 'GB-BNE', + 'GB-BNH', + 'GB-BNS', + 'GB-BOL', + 'GB-BPL', + 'GB-BRC', + 'GB-BRD', + 'GB-BRY', + 'GB-BST', + 'GB-BUR', + 'GB-CAM', + 'GB-CAY', + 'GB-CGN', + 'GB-CGV', + 'GB-CHA', + 'GB-CHS', + 'GB-CKF', + 'GB-CKT', + 'GB-CLD', + 'GB-CLK', + 'GB-CLR', + 'GB-CMA', + 'GB-CMD', + 'GB-CMN', + 'GB-CON', + 'GB-COV', + 'GB-CRF', + 'GB-CRY', + 'GB-CSR', + 'GB-CWY', + 'GB-DAL', + 'GB-DBY', + 'GB-DEN', + 'GB-DER', + 'GB-DEV', + 'GB-DGN', + 'GB-DGY', + 'GB-DNC', + 'GB-DND', + 'GB-DOR', + 'GB-DOW', + 'GB-DRY', + 'GB-DUD', + 'GB-DUR', + 'GB-EAL', + 'GB-EAW', + 'GB-EAY', + 'GB-EDH', + 'GB-EDU', + 'GB-ELN', + 'GB-ELS', + 'GB-ENF', + 'GB-ENG', + 'GB-ERW', + 'GB-ERY', + 'GB-ESS', + 'GB-ESX', + 'GB-FAL', + 'GB-FER', + 'GB-FIF', + 'GB-FLN', + 'GB-GAT', + 'GB-GBN', + 'GB-GLG', + 'GB-GLS', + 'GB-GRE', + 'GB-GSY', + 'GB-GWN', + 'GB-HAL', + 'GB-HAM', + 'GB-HAV', + 'GB-HCK', + 'GB-HEF', + 'GB-HIL', + 'GB-HLD', + 'GB-HMF', + 'GB-HNS', + 'GB-HPL', + 'GB-HRT', + 'GB-HRW', + 'GB-HRY', + 'GB-IOM', + 'GB-IOS', + 'GB-IOW', + 'GB-ISL', + 'GB-IVC', + 'GB-JSY', + 'GB-KEC', + 'GB-KEN', + 'GB-KHL', + 'GB-KIR', + 'GB-KTT', + 'GB-KWL', + 'GB-LAN', + 'GB-LBH', + 'GB-LCE', + 'GB-LDS', + 'GB-LEC', + 'GB-LEW', + 'GB-LIN', + 'GB-LIV', + 'GB-LMV', + 'GB-LND', + 'GB-LRN', + 'GB-LSB', + 'GB-LUT', + 'GB-MAN', + 'GB-MDB', + 'GB-MDW', + 'GB-MFT', + 'GB-MIK', + 'GB-MLN', + 'GB-MON', + 'GB-MRT', + 'GB-MRY', + 'GB-MTY', + 'GB-MYL', + 'GB-NAY', + 'GB-NBL', + 'GB-NDN', + 'GB-NEL', + 'GB-NET', + 'GB-NFK', + 'GB-NGM', + 'GB-NIR', + 'GB-NLK', + 'GB-NLN', + 'GB-NSM', + 'GB-NTA', + 'GB-NTH', + 'GB-NTL', + 'GB-NTT', + 'GB-NTY', + 'GB-NWM', + 'GB-NWP', + 'GB-NYK', + 'GB-NYM', + 'GB-OLD', + 'GB-OMH', + 'GB-ORK', + 'GB-OXF', + 'GB-PEM', + 'GB-PKN', + 'GB-PLY', + 'GB-POL', + 'GB-POR', + 'GB-POW', + 'GB-PTE', + 'GB-RCC', + 'GB-RCH', + 'GB-RCT', + 'GB-RDB', + 'GB-RDG', + 'GB-RFW', + 'GB-RIC', + 'GB-ROT', + 'GB-RUT', + 'GB-SAW', + 'GB-SAY', + 'GB-SCB', + 'GB-SCT', + 'GB-SFK', + 'GB-SFT', + 'GB-SGC', + 'GB-SHF', + 'GB-SHN', + 'GB-SHR', + 'GB-SKP', + 'GB-SLF', + 'GB-SLG', + 'GB-SLK', + 'GB-SND', + 'GB-SOL', + 'GB-SOM', + 'GB-SOS', + 'GB-SRY', + 'GB-STB', + 'GB-STE', + 'GB-STG', + 'GB-STH', + 'GB-STN', + 'GB-STS', + 'GB-STT', + 'GB-STY', + 'GB-SWA', + 'GB-SWD', + 'GB-SWK', + 'GB-TAM', + 'GB-TFW', + 'GB-THR', + 'GB-TOB', + 'GB-TOF', + 'GB-TRF', + 'GB-TWH', + 'GB-UKM', + 'GB-VGL', + 'GB-WAR', + 'GB-WBK', + 'GB-WDU', + 'GB-WFT', + 'GB-WGN', + 'GB-WILL', + 'GB-WKF', + 'GB-WLL', + 'GB-WLN', + 'GB-WLS', + 'GB-WLV', + 'GB-WND', + 'GB-WNM', + 'GB-WOK', + 'GB-WOR', + 'GB-WRL', + 'GB-WRT', + 'GB-WRX', + 'GB-WSM', + 'GB-WSX', + 'GB-YOR', + 'GB-ZET', + 'GE-AB', + 'GE-AJ', + 'GE-GU', + 'GE-IM', + 'GE-KA', + 'GE-KK', + 'GE-MM', + 'GE-RL', + 'GE-SJ', + 'GE-SK', + 'GE-SZ', + 'GE-TB', + 'GH-AA', + 'GH-AH', + 'GH-BA', + 'GH-CP', + 'GH-EP', + 'GH-NP', + 'GH-TV', + 'GH-UE', + 'GH-UW', + 'GH-WP', + 'GM-B', + 'GM-L', + 'GM-M', + 'GM-N', + 'GM-U', + 'GM-W', + 'GN-B', + 'GN-BE', + 'GN-BF', + 'GN-BK', + 'GN-C', + 'GN-CO', + 'GN-D', + 'GN-DB', + 'GN-DI', + 'GN-DL', + 'GN-DU', + 'GN-F', + 'GN-FA', + 'GN-FO', + 'GN-FR', + 'GN-GA', + 'GN-GU', + 'GN-K', + 'GN-KA', + 'GN-KB', + 'GN-KD; 2', + 'GN-KE', + 'GN-KN', + 'GN-KO', + 'GN-KS', + 'GN-L', + 'GN-LA', + 'GN-LE', + 'GN-LO', + 'GN-M', + 'GN-MC', + 'GN-MD', + 'GN-ML', + 'GN-MM', + 'GN-N', + 'GN-NZ', + 'GN-PI', + 'GN-SI', + 'GN-TE', + 'GN-TO', + 'GN-YO', + 'GQ-AN', + 'GQ-BN', + 'GQ-BS', + 'GQ-C', + 'GQ-CS', + 'GQ-I', + 'GQ-KN', + 'GQ-LI', + 'GQ-WN', + 'GR-01', + 'GR-03', + 'GR-04', + 'GR-05', + 'GR-06', + 'GR-07', + 'GR-11', + 'GR-12', + 'GR-13', + 'GR-14', + 'GR-15', + 'GR-16', + 'GR-17', + 'GR-21', + 'GR-22', + 'GR-23', + 'GR-24', + 'GR-31', + 'GR-32', + 'GR-33', + 'GR-34', + 'GR-41', + 'GR-42', + 'GR-43', + 'GR-44', + 'GR-51', + 'GR-52', + 'GR-53', + 'GR-54', + 'GR-55', + 'GR-56', + 'GR-57', + 'GR-58', + 'GR-59', + 'GR-61', + 'GR-62', + 'GR-63', + 'GR-64', + 'GR-69', + 'GR-71', + 'GR-72', + 'GR-73', + 'GR-81', + 'GR-82', + 'GR-83', + 'GR-84', + 'GR-85', + 'GR-91', + 'GR-92', + 'GR-93', + 'GR-94', + 'GR-A1', + 'GR-I', + 'GR-II', + 'GR-III', + 'GR-IV', + 'GR-IX', + 'GR-V', + 'GR-VI', + 'GR-VII', + 'GR-VIII', + 'GR-X', + 'GR-XI', + 'GR-XII', + 'GR-XIII', + 'GT-AV', + 'GT-BV', + 'GT-CM', + 'GT-CQ', + 'GT-ES', + 'GT-GU', + 'GT-HU', + 'GT-IZ', + 'GT-JA', + 'GT-JU', + 'GT-PE', + 'GT-PR', + 'GT-QC', + 'GT-QZ', + 'GT-RE', + 'GT-SA', + 'GT-SM', + 'GT-SO', + 'GT-SR', + 'GT-SU', + 'GT-TO', + 'GT-ZA', + 'GW-BA', + 'GW-BL', + 'GW-BM', + 'GW-BS', + 'GW-CA', + 'GW-GA', + 'GW-L', + 'GW-N', + 'GW-OI', + 'GW-QU', + 'GW-S', + 'GW-TO', + 'GY-BA', + 'GY-CU', + 'GY-DE', + 'GY-EB', + 'GY-ES', + 'GY-MA', + 'GY-PM', + 'GY-PT', + 'GY-UD', + 'GY-UT', + 'HN-AT', + 'HN-CH', + 'HN-CL', + 'HN-CM', + 'HN-CP', + 'HN-CR', + 'HN-EP', + 'HN-FM', + 'HN-GD', + 'HN-IB', + 'HN-IN', + 'HN-LE', + 'HN-LP', + 'HN-OC', + 'HN-OL', + 'HN-SB', + 'HN-VA', + 'HN-YO', + 'HR-01', + 'HR-02', + 'HR-03', + 'HR-04', + 'HR-05', + 'HR-06', + 'HR-07', + 'HR-08', + 'HR-09', + 'HR-10', + 'HR-11', + 'HR-12', + 'HR-13', + 'HR-14', + 'HR-15', + 'HR-16', + 'HR-17', + 'HR-18', + 'HR-19', + 'HR-20', + 'HR-21', + 'HT-AR', + 'HT-CE', + 'HT-GA', + 'HT-ND', + 'HT-NE', + 'HT-NO', + 'HT-OU', + 'HT-SD', + 'HT-SE', + 'HU-BA', + 'HU-BC', + 'HU-BE', + 'HU-BK', + 'HU-BU', + 'HU-BZ', + 'HU-CS', + 'HU-DE', + 'HU-DU', + 'HU-EG', + 'HU-FE', + 'HU-GS', + 'HU-GY', + 'HU-HB', + 'HU-HE', + 'HU-HV', + 'HU-JN', + 'HU-KE', + 'HU-KM', + 'HU-KV', + 'HU-MI', + 'HU-NK', + 'HU-NO', + 'HU-NY', + 'HU-PE', + 'HU-PS', + 'HU-SD', + 'HU-SF', + 'HU-SH', + 'HU-SK', + 'HU-SN', + 'HU-SO', + 'HU-SS', + 'HU-ST', + 'HU-SZ', + 'HU-TB', + 'HU-TO', + 'HU-VA', + 'HU-VE', + 'HU-VM', + 'HU-ZA', + 'HU-ZE', + 'ID-AC', + 'ID-BA', + 'ID-BB', + 'ID-BE', + 'ID-BT', + 'ID-GO', + 'ID-IJ', + 'ID-JA', + 'ID-JB', + 'ID-JI', + 'ID-JK', + 'ID-JT', + 'ID-JW', + 'ID-KA', + 'ID-KB', + 'ID-KI', + 'ID-KS', + 'ID-KT', + 'ID-LA', + 'ID-MA', + 'ID-MU', + 'ID-NB', + 'ID-NT', + 'ID-NU', + 'ID-PA', + 'ID-RI', + 'ID-SA', + 'ID-SB', + 'ID-SG', + 'ID-SL', + 'ID-SM', + 'ID-SN', + 'ID-SS', + 'ID-ST', + 'ID-SU', + 'ID-YO', + 'IE-C', + 'IE-C; 2', + 'IE-CE', + 'IE-CN', + 'IE-CW', + 'IE-D', + 'IE-DL', + 'IE-G', + 'IE-KE', + 'IE-KK', + 'IE-KY', + 'IE-L', + 'IE-LD', + 'IE-LH', + 'IE-LK', + 'IE-LM', + 'IE-LS', + 'IE-M', + 'IE-MH', + 'IE-MN', + 'IE-MO', + 'IE-OY', + 'IE-RN', + 'IE-SO', + 'IE-TA', + 'IE-U', + 'IE-WD', + 'IE-WH', + 'IE-WW', + 'IE-WX', + 'IL-D', + 'IL-HA', + 'IL-JM', + 'IL-M', + 'IL-TA', + 'IL-Z', + 'IN-AN', + 'IN-AP', + 'IN-AR', + 'IN-AS', + 'IN-BR', + 'IN-CH', + 'IN-CT', + 'IN-DD', + 'IN-DL', + 'IN-DN', + 'IN-GA', + 'IN-GJ', + 'IN-HP', + 'IN-HR', + 'IN-JH', + 'IN-JK', + 'IN-KA', + 'IN-KL', + 'IN-LD', + 'IN-MH', + 'IN-ML', + 'IN-MN', + 'IN-MP', + 'IN-MZ', + 'IN-NL', + 'IN-OR', + 'IN-PB', + 'IN-PY', + 'IN-RJ', + 'IN-SK', + 'IN-TN', + 'IN-TR', + 'IN-UL', + 'IN-UP', + 'IN-WB', + 'IQ-AN', + 'IQ-AR', + 'IQ-BA', + 'IQ-BB', + 'IQ-BG', + 'IQ-DA', + 'IQ-DI', + 'IQ-DQ', + 'IQ-KA', + 'IQ-MA', + 'IQ-MU', + 'IQ-NA', + 'IQ-NI', + 'IQ-QA', + 'IQ-SD', + 'IQ-SU', + 'IQ-TS', + 'IQ-WA', + 'IR-01', + 'IR-02', + 'IR-03', + 'IR-04', + 'IR-05', + 'IR-06', + 'IR-07', + 'IR-08', + 'IR-09', + 'IR-10', + 'IR-11', + 'IR-12', + 'IR-13', + 'IR-14', + 'IR-15', + 'IR-16', + 'IR-17', + 'IR-18', + 'IR-19', + 'IR-20', + 'IR-21', + 'IR-22', + 'IR-23', + 'IR-24', + 'IR-25', + 'IR-26', + 'IR-27', + 'IR-28', + 'IS-0', + 'IS-1', + 'IS-2', + 'IS-3', + 'IS-4', + 'IS-5', + 'IS-6', + 'IS-7', + 'IS-8', + 'IT-21', + 'IT-23', + 'IT-25', + 'IT-32', + 'IT-34', + 'IT-36', + 'IT-42', + 'IT-45', + 'IT-52', + 'IT-55', + 'IT-57', + 'IT-62', + 'IT-65', + 'IT-67', + 'IT-72', + 'IT-75', + 'IT-77', + 'IT-78', + 'IT-82', + 'IT-88', + 'IT-AG', + 'IT-AL', + 'IT-AN', + 'IT-AO', + 'IT-AP', + 'IT-AQ', + 'IT-AR', + 'IT-AT', + 'IT-AV', + 'IT-BA', + 'IT-BG', + 'IT-BI', + 'IT-BL', + 'IT-BN', + 'IT-BO', + 'IT-BR', + 'IT-BS', + 'IT-BZ', + 'IT-CA', + 'IT-CB', + 'IT-CE', + 'IT-CH', + 'IT-CL', + 'IT-CN', + 'IT-CO', + 'IT-CR', + 'IT-CS', + 'IT-CT', + 'IT-CZ', + 'IT-DU', + 'IT-EN', + 'IT-FE', + 'IT-FG', + 'IT-FI', + 'IT-FO', + 'IT-FR', + 'IT-GE', + 'IT-GO', + 'IT-GR', + 'IT-IM', + 'IT-IS', + 'IT-KR', + 'IT-LC', + 'IT-LE', + 'IT-LI', + 'IT-LO', + 'IT-LT', + 'IT-LU', + 'IT-MC', + 'IT-ME', + 'IT-MI', + 'IT-MN', + 'IT-MO', + 'IT-MS', + 'IT-MT', + 'IT-NA', + 'IT-NO', + 'IT-NU', + 'IT-OR', + 'IT-PA', + 'IT-PC', + 'IT-PD', + 'IT-PE', + 'IT-PG', + 'IT-PI', + 'IT-PN', + 'IT-PO', + 'IT-PR', + 'IT-PS', + 'IT-PT', + 'IT-PV', + 'IT-PZ', + 'IT-RA', + 'IT-RC', + 'IT-RE', + 'IT-RG', + 'IT-RI', + 'IT-RM', + 'IT-RN', + 'IT-RO', + 'IT-SA', + 'IT-SI', + 'IT-SO', + 'IT-SP', + 'IT-SR', + 'IT-SS', + 'IT-SV', + 'IT-TA', + 'IT-TE', + 'IT-TN', + 'IT-TO', + 'IT-TP', + 'IT-TR', + 'IT-TS', + 'IT-TV', + 'IT-VA', + 'IT-VB', + 'IT-VC', + 'IT-VE', + 'IT-VI', + 'IT-VR', + 'IT-VT', + 'IT-VV', + 'JM-01', + 'JM-02', + 'JM-03', + 'JM-04', + 'JM-05', + 'JM-06', + 'JM-07', + 'JM-08', + 'JM-09', + 'JM-10', + 'JM-11', + 'JM-12', + 'JM-13', + 'JM-14', + 'JO-AJ', + 'JO-AM', + 'JO-AQ', + 'JO-AT', + 'JO-AZ', + 'JO-BA', + 'JO-IR', + 'JO-JA', + 'JO-KA', + 'JO-MA', + 'JO-MD', + 'JO-MN', + 'JP-01', + 'JP-02', + 'JP-03', + 'JP-04', + 'JP-05', + 'JP-06', + 'JP-07', + 'JP-08', + 'JP-09', + 'JP-10', + 'JP-11', + 'JP-12', + 'JP-13', + 'JP-14', + 'JP-15', + 'JP-16', + 'JP-17', + 'JP-18', + 'JP-19', + 'JP-20', + 'JP-21', + 'JP-22', + 'JP-23', + 'JP-24', + 'JP-25', + 'JP-26', + 'JP-27', + 'JP-28', + 'JP-29', + 'JP-30', + 'JP-31', + 'JP-32', + 'JP-33', + 'JP-34', + 'JP-35', + 'JP-36', + 'JP-37', + 'JP-38', + 'JP-39', + 'JP-40', + 'JP-41', + 'JP-42', + 'JP-43', + 'JP-44', + 'JP-45', + 'JP-46', + 'JP-47', + 'KE-110', + 'KE-200', + 'KE-300', + 'KE-400', + 'KE-500', + 'KE-600', + 'KE-700', + 'KE-900', + 'KG-B', + 'KG-C', + 'KG-GB', + 'KG-J', + 'KG-N', + 'KG-O', + 'KG-T', + 'KG-Y', + 'KH-1', + 'KH-10', + 'KH-11', + 'KH-12', + 'KH-13', + 'KH-14', + 'KH-15', + 'KH-16', + 'KH-17', + 'KH-18', + 'KH-19', + 'KH-2', + 'KH-20', + 'KH-21', + 'KH-22', + 'KH-23', + 'KH-24', + 'KH-3', + 'KH-4', + 'KH-5', + 'KH-6', + 'KH-7', + 'KH-8', + 'KH-9', + 'KI-G', + 'KI-L', + 'KI-P', + 'KM-A', + 'KM-G', + 'KM-M', + 'KP-CHA', + 'KP-HAB', + 'KP-HAN', + 'KP-HWB', + 'KP-HWN', + 'KP-KAE', + 'KP-KAN', + 'KP-NAJ', + 'KP-NAM', + 'KP-PYB', + 'KP-PYN', + 'KP-PYO', + 'KP-YAN', + 'KR-11', + 'KR-26', + 'KR-27', + 'KR-28', + 'KR-29', + 'KR-30', + 'KR-31', + 'KR-41', + 'KR-42', + 'KR-43', + 'KR-44', + 'KR-45', + 'KR-46', + 'KR-47', + 'KR-48', + 'KR-49', + 'KW-AH', + 'KW-FA', + 'KW-HA', + 'KW-JA', + 'KW-KU', + 'KZ-AKM', + 'KZ-AKT', + 'KZ-ALA', + 'KZ-ALM', + 'KZ-AST', + 'KZ-ATY', + 'KZ-KAR', + 'KZ-KUS', + 'KZ-KZY', + 'KZ-MAN', + 'KZ-PAV', + 'KZ-SEV', + 'KZ-VOS', + 'KZ-YUZ', + 'KZ-ZAP', + 'KZ-ZHA', + 'LA-AT', + 'LA-BK', + 'LA-BL', + 'LA-CH', + 'LA-HO', + 'LA-KH', + 'LA-LM', + 'LA-LP', + 'LA-OU', + 'LA-PH', + 'LA-SL', + 'LA-SV', + 'LA-VI', + 'LA-VT', + 'LA-XA', + 'LA-XE', + 'LA-XI', + 'LA-XN', + 'LB-AS', + 'LB-BA', + 'LB-BI', + 'LB-JA', + 'LB-JL', + 'LB-NA', + 'LK-1', + 'LK-11', + 'LK-12', + 'LK-13', + 'LK-2', + 'LK-21', + 'LK-22', + 'LK-23', + 'LK-3', + 'LK-31', + 'LK-32', + 'LK-33', + 'LK-4', + 'LK-41', + 'LK-42', + 'LK-43', + 'LK-44', + 'LK-45', + 'LK-5', + 'LK-51', + 'LK-52', + 'LK-53', + 'LK-6', + 'LK-61', + 'LK-62', + 'LK-7', + 'LK-71', + 'LK-72', + 'LK-8', + 'LK-81', + 'LK-82', + 'LK-9', + 'LK-91', + 'LK-92', + 'LR-BG', + 'LR-BM', + 'LR-CM', + 'LR-GB', + 'LR-GG', + 'LR-GK', + 'LR-LO', + 'LR-MG', + 'LR-MO', + 'LR-MY', + 'LR-NI', + 'LR-RI', + 'LR-SI', + 'LS-A', + 'LS-B', + 'LS-C', + 'LS-D', + 'LS-E', + 'LS-F', + 'LS-G', + 'LS-H', + 'LS-J', + 'LS-K', + 'LT-AL', + 'LT-KL', + 'LT-KU', + 'LT-MR', + 'LT-PN', + 'LT-SA', + 'LT-TA', + 'LT-TE', + 'LT-UT', + 'LT-VL', + 'LU-D', + 'LU-G', + 'LU-L', + 'LV-AI', + 'LV-AL', + 'LV-BL', + 'LV-BU', + 'LV-CE', + 'LV-DA', + 'LV-DGV', + 'LV-DO', + 'LV-GU', + 'LV-JEL', + 'LV-JK', + 'LV-JL', + 'LV-JUR', + 'LV-KR', + 'LV-KU', + 'LV-LE', + 'LV-LM', + 'LV-LPX', + 'LV-LU', + 'LV-MA', + 'LV-OG', + 'LV-PR', + 'LV-RE', + 'LV-REZ', + 'LV-RI', + 'LV-RIX', + 'LV-SA', + 'LV-TA', + 'LV-TU', + 'LV-VE', + 'LV-VEN', + 'LV-VK', + 'LV-VM', + 'LY-BA', + 'LY-BU', + 'LY-FA', + 'LY-JA', + 'LY-JG', + 'LY-JU', + 'LY-MI', + 'LY-NA', + 'LY-SF', + 'LY-TB', + 'LY-WA', + 'LY-WU', + 'LY-ZA', + 'MA-01', + 'MA-02', + 'MA-03', + 'MA-04', + 'MA-05', + 'MA-06', + 'MA-07', + 'MA-08', + 'MA-09', + 'MA-10', + 'MA-11', + 'MA-12', + 'MA-13', + 'MA-14', + 'MA-15', + 'MA-16', + 'MA-AGD', + 'MA-ASZ', + 'MA-AZI', + 'MA-BAH', + 'MA-BEM', + 'MA-BER', + 'MA-BES', + 'MA-BOD', + 'MA-BOM', + 'MA-CAS', + 'MA-CHE', + 'MA-CHI', + 'MA-ERR', + 'MA-ESI', + 'MA-ESM', + 'MA-FES', + 'MA-FIG', + 'MA-GUE', + 'MA-HAJ', + 'MA-HAO', + 'MA-HOC', + 'MA-IFR', + 'MA-JDI', + 'MA-JRA', + 'MA-KEN', + 'MA-KES', + 'MA-KHE', + 'MA-KHN', + 'MA-KHO', + 'MA-LAA', + 'MA-LAR', + 'MA-MAR', + 'MA-MEK', + 'MA-MEL', + 'MA-NAD', + 'MA-OUA', + 'MA-OUD', + 'MA-OUJ', + 'MA-RBA', + 'MA-SAF', + 'MA-SEF', + 'MA-SET', + 'MA-SIK', + 'MA-TAO', + 'MA-TAR', + 'MA-TAT', + 'MA-TAZ', + 'MA-TET', + 'MA-TIZ', + 'MA-TNG', + 'MA-TNT', + 'MD-BA', + 'MD-CA', + 'MD-CH', + 'MD-CU', + 'MD-ED', + 'MD-GA', + 'MD-LA', + 'MD-OR', + 'MD-SN', + 'MD-SO', + 'MD-TA', + 'MD-TI', + 'MD-UN', + 'MG-A', + 'MG-D', + 'MG-F', + 'MG-M', + 'MG-T', + 'MG-U', + 'MH-ALK', + 'MH-ALL', + 'MH-ARN', + 'MH-AUR', + 'MH-EBO', + 'MH-ENI', + 'MH-JAL', + 'MH-KIL', + 'MH-KWA', + 'MH-L', + 'MH-LAE', + 'MH-LIB', + 'MH-LIK', + 'MH-MAJ', + 'MH-MAL', + 'MH-MEJ', + 'MH-MIL', + 'MH-NMK', + 'MH-NMU', + 'MH-RON', + 'MH-T', + 'MH-UJA', + 'MH-UJL', + 'MH-UTI', + 'MH-WTH', + 'MH-WTJ', + 'ML-1', + 'ML-2', + 'ML-3', + 'ML-4', + 'ML-5', + 'ML-6', + 'ML-7', + 'ML-8', + 'ML-BKO', + 'MM-01', + 'MM-02', + 'MM-03', + 'MM-04', + 'MM-05', + 'MM-06', + 'MM-07', + 'MM-11', + 'MM-12', + 'MM-13', + 'MM-14', + 'MM-15', + 'MM-16', + 'MM-17', + 'MN-035', + 'MN-037', + 'MN-039', + 'MN-041', + 'MN-043', + 'MN-046', + 'MN-047', + 'MN-049', + 'MN-051', + 'MN-053', + 'MN-055', + 'MN-057', + 'MN-059', + 'MN-061', + 'MN-063', + 'MN-064', + 'MN-065', + 'MN-067', + 'MN-069', + 'MN-071', + 'MN-073', + 'MN-1', + 'MR-01', + 'MR-02', + 'MR-03', + 'MR-04', + 'MR-05', + 'MR-06', + 'MR-07', + 'MR-08', + 'MR-09', + 'MR-10', + 'MR-11', + 'MR-12', + 'MR-NKC', + 'MU-AG', + 'MU-BL', + 'MU-BR', + 'MU-CC', + 'MU-CU', + 'MU-FL', + 'MU-GP', + 'MU-MO', + 'MU-PA', + 'MU-PL', + 'MU-PU', + 'MU-PW', + 'MU-QB', + 'MU-RO', + 'MU-RR', + 'MU-SA', + 'MU-VP', + 'MV-01', + 'MV-02', + 'MV-03', + 'MV-04', + 'MV-05', + 'MV-07', + 'MV-08', + 'MV-12', + 'MV-13', + 'MV-14', + 'MV-17', + 'MV-20', + 'MV-23', + 'MV-24', + 'MV-25', + 'MV-26', + 'MV-27', + 'MV-28', + 'MV-29', + 'MV-MLE', + 'MW-BA', + 'MW-BL', + 'MW-C', + 'MW-CK', + 'MW-CR', + 'MW-CT', + 'MW-DE', + 'MW-DO', + 'MW-KR', + 'MW-KS', + 'MW-LI', + 'MW-LK', + 'MW-MC', + 'MW-MG', + 'MW-MH', + 'MW-MU', + 'MW-MW', + 'MW-MZ', + 'MW-N', + 'MW-NB', + 'MW-NI', + 'MW-NK', + 'MW-NS', + 'MW-NU', + 'MW-PH', + 'MW-RU', + 'MW-S', + 'MW-SA', + 'MW-TH', + 'MW-ZO', + 'MX-AGU', + 'MX-BCN', + 'MX-BCS', + 'MX-CAM', + 'MX-CHH', + 'MX-CHP', + 'MX-COA', + 'MX-COL', + 'MX-DIF', + 'MX-DUR', + 'MX-GRO', + 'MX-GUA', + 'MX-HID', + 'MX-JAL', + 'MX-MEX', + 'MX-MIC', + 'MX-MOR', + 'MX-NAY', + 'MX-NLE', + 'MX-OAX', + 'MX-PUE', + 'MX-QUE', + 'MX-ROO', + 'MX-SIN', + 'MX-SLP', + 'MX-SON', + 'MX-TAB', + 'MX-TAM', + 'MX-TLA', + 'MX-VER', + 'MX-YUC', + 'MX-ZAC', + 'MY-A', + 'MY-B', + 'MY-C', + 'MY-D', + 'MY-J', + 'MY-K', + 'MY-L', + 'MY-M', + 'MY-N', + 'MY-P', + 'MY-R', + 'MY-SA', + 'MY-SK', + 'MY-T', + 'MY-W', + 'MZ-A', + 'MZ-B', + 'MZ-G', + 'MZ-I', + 'MZ-L', + 'MZ-MPM', + 'MZ-N', + 'MZ-P', + 'MZ-Q', + 'MZ-S', + 'MZ-T', + 'NA-CA', + 'NA-ER', + 'NA-HA', + 'NA-KA', + 'NA-KH', + 'NA-KU', + 'NA-OD', + 'NA-OH', + 'NA-OK', + 'NA-ON', + 'NA-OS', + 'NA-OT', + 'NA-OW', + 'NE-1', + 'NE-2', + 'NE-3', + 'NE-4', + 'NE-5', + 'NE-6', + 'NE-7', + 'NE-8', + 'NG-AB', + 'NG-AD', + 'NG-AK', + 'NG-AN', + 'NG-BA', + 'NG-BE', + 'NG-BO', + 'NG-BY', + 'NG-CR', + 'NG-DE', + 'NG-EB', + 'NG-ED', + 'NG-EK', + 'NG-EN', + 'NG-FC', + 'NG-GO', + 'NG-IM', + 'NG-JI', + 'NG-KD', + 'NG-KE', + 'NG-KN', + 'NG-KO', + 'NG-KT', + 'NG-KW', + 'NG-LA', + 'NG-NA', + 'NG-NI', + 'NG-OG', + 'NG-ON', + 'NG-OS', + 'NG-OY', + 'NG-PL', + 'NG-RI', + 'NG-SO', + 'NG-TA', + 'NG-YO', + 'NG-ZA', + 'NI-AN', + 'NI-AS', + 'NI-BO', + 'NI-CA', + 'NI-CI', + 'NI-CO', + 'NI-ES', + 'NI-GR', + 'NI-JI', + 'NI-LE', + 'NI-MD', + 'NI-MN', + 'NI-MS', + 'NI-MT', + 'NI-NS', + 'NI-RI', + 'NI-SJ', + 'NL-DR', + 'NL-FL', + 'NL-FR', + 'NL-GE', + 'NL-GR', + 'NL-LI', + 'NL-NB', + 'NL-NH', + 'NL-OV', + 'NL-UT', + 'NL-ZE', + 'NL-ZH', + 'NO-01', + 'NO-02', + 'NO-03', + 'NO-04', + 'NO-05', + 'NO-06', + 'NO-07', + 'NO-08', + 'NO-09', + 'NO-10', + 'NO-11', + 'NO-12', + 'NO-14', + 'NO-15', + 'NO-16', + 'NO-17', + 'NO-18', + 'NO-19', + 'NO-20', + 'NO-21', + 'NO-22', + 'NP-1', + 'NP-2', + 'NP-3', + 'NP-4', + 'NP-5', + 'NP-BA', + 'NP-BH', + 'NP-DH', + 'NP-GA', + 'NP-JA', + 'NP-KA', + 'NP-KO', + 'NP-LU', + 'NP-MA', + 'NP-ME', + 'NP-NA', + 'NP-RA', + 'NP-SA', + 'NP-SE', + 'NZ-AUK', + 'NZ-BOP', + 'NZ-CAN', + 'NZ-GIS', + 'NZ-HKB', + 'NZ-MBH', + 'NZ-MWT', + 'NZ-N', + 'NZ-NSN', + 'NZ-NTL', + 'NZ-OTA', + 'NZ-S', + 'NZ-STL', + 'NZ-TAS', + 'NZ-TKI', + 'NZ-WGN', + 'NZ-WKO', + 'NZ-WTC', + 'OM-BA', + 'OM-DA', + 'OM-JA', + 'OM-MA', + 'OM-MU', + 'OM-SH', + 'OM-WU', + 'OM-ZA', + 'PA-0', + 'PA-1', + 'PA-2', + 'PA-3', + 'PA-4', + 'PA-5', + 'PA-6', + 'PA-7', + 'PA-8', + 'PA-9', + 'PE-AMA', + 'PE-ANC', + 'PE-APU', + 'PE-ARE', + 'PE-AYA', + 'PE-CAJ', + 'PE-CAL', + 'PE-CUS', + 'PE-HUC', + 'PE-HUV', + 'PE-ICA', + 'PE-JUN', + 'PE-LAL', + 'PE-LAM', + 'PE-LIM', + 'PE-LOR', + 'PE-MDD', + 'PE-MOQ', + 'PE-PAS', + 'PE-PIU', + 'PE-PUN', + 'PE-SAM', + 'PE-TAC', + 'PE-TUM', + 'PE-UCA', + 'PG-CPK', + 'PG-CPM', + 'PG-EBR', + 'PG-EHG', + 'PG-EPW', + 'PG-ESW', + 'PG-GPK', + 'PG-MBA', + 'PG-MPL', + 'PG-MPM', + 'PG-MRL', + 'PG-NCD', + 'PG-NIK', + 'PG-NPP', + 'PG-NSA', + 'PG-SAN', + 'PG-SHM', + 'PG-WBK', + 'PG-WHM', + 'PG-WPD', + 'PH-00', + 'PH-01', + 'PH-02', + 'PH-03', + 'PH-04', + 'PH-05', + 'PH-06', + 'PH-07', + 'PH-08', + 'PH-09', + 'PH-10', + 'PH-11', + 'PH-12', + 'PH-13', + 'PH-14', + 'PH-15', + 'PH-ABR', + 'PH-AGN', + 'PH-AGS', + 'PH-AKL', + 'PH-ALB', + 'PH-ANT', + 'PH-APA', + 'PH-AUR', + 'PH-BAN', + 'PH-BAS', + 'PH-BEN', + 'PH-BIL', + 'PH-BOH', + 'PH-BTG', + 'PH-BTN', + 'PH-BUK', + 'PH-BUL', + 'PH-CAG', + 'PH-CAM', + 'PH-CAN', + 'PH-CAP', + 'PH-CAS', + 'PH-CAT', + 'PH-CAV', + 'PH-CEB', + 'PH-COM', + 'PH-DAO', + 'PH-DAS', + 'PH-DAV', + 'PH-EAS', + 'PH-GUI', + 'PH-IFU', + 'PH-ILI', + 'PH-ILN', + 'PH-ILS', + 'PH-ISA', + 'PH-KAL', + 'PH-LAG', + 'PH-LAN', + 'PH-LAS', + 'PH-LEY', + 'PH-LUN', + 'PH-MAD', + 'PH-MAG', + 'PH-MAS', + 'PH-MDC', + 'PH-MDR', + 'PH-MOU', + 'PH-MSC', + 'PH-MSR', + 'PH-NCO', + 'PH-NEC', + 'PH-NER', + 'PH-NSA', + 'PH-NUE', + 'PH-NUV', + 'PH-PAM', + 'PH-PAN', + 'PH-PLW', + 'PH-QUE', + 'PH-QUI', + 'PH-RIZ', + 'PH-ROM', + 'PH-SAR', + 'PH-SCO', + 'PH-SIG', + 'PH-SLE', + 'PH-SLU', + 'PH-SOR', + 'PH-SUK', + 'PH-SUN', + 'PH-SUR', + 'PH-TAR', + 'PH-TAW', + 'PH-WSA', + 'PH-ZAN', + 'PH-ZAS', + 'PH-ZMB', + 'PH-ZSI', + 'PK-BA', + 'PK-IS', + 'PK-JK', + 'PK-NA', + 'PK-NW', + 'PK-PB', + 'PK-SD', + 'PK-TA', + 'PL-DS', + 'PL-KP', + 'PL-LB', + 'PL-LD', + 'PL-LU', + 'PL-MA', + 'PL-MZ', + 'PL-OP', + 'PL-PD', + 'PL-PK', + 'PL-PM', + 'PL-SK', + 'PL-SL', + 'PL-WN', + 'PL-WP', + 'PL-ZP', + 'PT-01', + 'PT-02', + 'PT-03', + 'PT-04', + 'PT-05', + 'PT-06', + 'PT-07', + 'PT-08', + 'PT-09', + 'PT-10', + 'PT-11', + 'PT-12', + 'PT-13', + 'PT-14', + 'PT-15', + 'PT-16', + 'PT-17', + 'PT-18', + 'PT-20', + 'PT-30', + 'PY-1', + 'PY-10', + 'PY-11', + 'PY-12', + 'PY-13', + 'PY-14', + 'PY-15', + 'PY-16', + 'PY-19', + 'PY-2', + 'PY-3', + 'PY-4', + 'PY-5', + 'PY-6', + 'PY-7', + 'PY-8', + 'PY-9', + 'PY-ASU', + 'QA-DA', + 'QA-GH', + 'QA-JB', + 'QA-JU', + 'QA-KH', + 'QA-MS', + 'QA-RA', + 'QA-US', + 'QA-WA', + 'RO-AB', + 'RO-AG', + 'RO-AR', + 'RO-B', + 'RO-BC', + 'RO-BH', + 'RO-BN', + 'RO-BR', + 'RO-BT', + 'RO-BV', + 'RO-BZ', + 'RO-CJ', + 'RO-CL', + 'RO-CS', + 'RO-CT', + 'RO-CV', + 'RO-DB', + 'RO-DJ', + 'RO-GJ', + 'RO-GL', + 'RO-GR', + 'RO-HD', + 'RO-HR', + 'RO-IF', + 'RO-IL', + 'RO-IS', + 'RO-MH', + 'RO-MM', + 'RO-MS', + 'RO-NT', + 'RO-OT', + 'RO-PH', + 'RO-SB', + 'RO-SJ', + 'RO-SM', + 'RO-SV', + 'RO-TL', + 'RO-TM', + 'RO-TR', + 'RO-VL', + 'RO-VN', + 'RO-VS', + 'RU-AD', + 'RU-AGB', + 'RU-AL', + 'RU-ALT', + 'RU-AMU', + 'RU-ARK', + 'RU-AST', + 'RU-BA', + 'RU-BEL', + 'RU-BRY', + 'RU-BU', + 'RU-CE', + 'RU-CHE', + 'RU-CHI', + 'RU-CHU', + 'RU-CU', + 'RU-DA', + 'RU-DU', + 'RU-EVE', + 'RU-IN', + 'RU-IRK', + 'RU-IVA', + 'RU-KAM', + 'RU-KB', + 'RU-KC', + 'RU-KDA', + 'RU-KEM', + 'RU-KGD', + 'RU-KGN', + 'RU-KHA', + 'RU-KHM', + 'RU-KIR', + 'RU-KK', + 'RU-KL', + 'RU-KLU', + 'RU-KO', + 'RU-KOP', + 'RU-KOR', + 'RU-KOS', + 'RU-KR', + 'RU-KRS', + 'RU-KYA', + 'RU-LEN', + 'RU-LIP', + 'RU-MAG', + 'RU-ME', + 'RU-MO', + 'RU-MOS', + 'RU-MOW', + 'RU-MUR', + 'RU-NEN', + 'RU-NGR', + 'RU-NIZ', + 'RU-NVS', + 'RU-OMS', + 'RU-ORE', + 'RU-ORL', + 'RU-PER', + 'RU-PNZ', + 'RU-PRI', + 'RU-PSK', + 'RU-ROS', + 'RU-RYA', + 'RU-SA', + 'RU-SAK', + 'RU-SAM', + 'RU-SAR', + 'RU-SE', + 'RU-SMO', + 'RU-SPE', + 'RU-STA', + 'RU-SVE', + 'RU-TA', + 'RU-TAM', + 'RU-TAY', + 'RU-TOM', + 'RU-TUL', + 'RU-TVE', + 'RU-TY', + 'RU-TYU', + 'RU-ULY', + 'RU-UOB', + 'RU-VGG', + 'RU-VLA', + 'RU-VLG', + 'RU-VOR', + 'RU-YAN', + 'RU-YAR', + 'RU-YEV', + 'RW-B', + 'RW-C', + 'RW-D', + 'RW-E', + 'RW-F', + 'RW-G', + 'RW-H', + 'RW-I', + 'RW-J', + 'RW-K', + 'RW-L', + 'RW-M', + 'SA-01', + 'SA-02', + 'SA-03', + 'SA-04', + 'SA-05', + 'SA-06', + 'SA-07', + 'SA-08', + 'SA-09', + 'SA-10', + 'SA-11', + 'SA-12', + 'SA-14', + 'SB-CE', + 'SB-CT', + 'SB-GU', + 'SB-IS', + 'SB-MK', + 'SB-ML', + 'SB-TE', + 'SB-WE', + 'SD-01', + 'SD-02', + 'SD-03', + 'SD-04', + 'SD-05', + 'SD-06', + 'SD-07', + 'SD-08', + 'SD-09', + 'SD-10', + 'SD-11', + 'SD-12', + 'SD-13', + 'SD-14', + 'SD-15', + 'SD-16', + 'SD-17', + 'SD-18', + 'SD-19', + 'SD-20', + 'SD-21', + 'SD-22', + 'SD-23', + 'SD-24', + 'SD-25', + 'SD-26', + 'SE-AB', + 'SE-AC', + 'SE-BD', + 'SE-C', + 'SE-D', + 'SE-E', + 'SE-F', + 'SE-G', + 'SE-H', + 'SE-I', + 'SE-K', + 'SE-M', + 'SE-N', + 'SE-O', + 'SE-S', + 'SE-T', + 'SE-U', + 'SE-W', + 'SE-X', + 'SE-Y', + 'SE-Z', + 'SH-AC', + 'SH-SH', + 'SH-TA', + 'SI-01', + 'SI-02', + 'SI-03', + 'SI-04', + 'SI-05', + 'SI-06', + 'SI-07', + 'SI-08', + 'SI-09', + 'SI-10', + 'SI-11', + 'SI-12', + 'SK-BC', + 'SK-BL', + 'SK-KI', + 'SK-NI', + 'SK-PV', + 'SK-TA', + 'SK-TC', + 'SK-ZI', + 'SL-E', + 'SL-N', + 'SL-S', + 'SL-W', + 'SN-DB', + 'SN-DK', + 'SN-FK', + 'SN-KD', + 'SN-KL', + 'SN-LG', + 'SN-SL', + 'SN-TC', + 'SN-TH', + 'SN-ZG', + 'SO-AW', + 'SO-BK', + 'SO-BN', + 'SO-BR', + 'SO-BY', + 'SO-GA', + 'SO-GE', + 'SO-HI', + 'SO-JD', + 'SO-JH', + 'SO-MU', + 'SO-NU', + 'SO-SA', + 'SO-SD', + 'SO-SH', + 'SO-SO', + 'SO-TO', + 'SO-WO', + 'SR-BR', + 'SR-CM', + 'SR-CR', + 'SR-MA', + 'SR-NI', + 'SR-PM', + 'SR-PR', + 'SR-SA', + 'SR-SI', + 'SR-WA', + 'ST-P', + 'ST-S', + 'SV-AH', + 'SV-CA', + 'SV-CH', + 'SV-CU', + 'SV-LI', + 'SV-MO', + 'SV-PA', + 'SV-SA', + 'SV-SM', + 'SV-SO', + 'SV-SS', + 'SV-SV', + 'SV-UN', + 'SV-US', + 'SY-DI', + 'SY-DR', + 'SY-DY', + 'SY-HA', + 'SY-HI', + 'SY-HL', + 'SY-HM', + 'SY-ID', + 'SY-LA', + 'SY-QU', + 'SY-RA', + 'SY-RD', + 'SY-SU', + 'SY-TA', + 'SZ-HH', + 'SZ-LU', + 'SZ-MA', + 'SZ-SH', + 'TD-BA', + 'TD-BET', + 'TD-BI', + 'TD-CB', + 'TD-GR', + 'TD-KA', + 'TD-LC', + 'TD-LO', + 'TD-LR', + 'TD-MC', + 'TD-MK', + 'TD-OD', + 'TD-SA', + 'TD-TA', + 'TG-C', + 'TG-K', + 'TG-M', + 'TG-P', + 'TG-S', + 'TH-10', + 'TH-11', + 'TH-12', + 'TH-13', + 'TH-14', + 'TH-15', + 'TH-16', + 'TH-17', + 'TH-18', + 'TH-19', + 'TH-20', + 'TH-21', + 'TH-22', + 'TH-23', + 'TH-24', + 'TH-25', + 'TH-26', + 'TH-27', + 'TH-30', + 'TH-31', + 'TH-32', + 'TH-33', + 'TH-34', + 'TH-35', + 'TH-36', + 'TH-37', + 'TH-39', + 'TH-40', + 'TH-41', + 'TH-42', + 'TH-43', + 'TH-44', + 'TH-45', + 'TH-46', + 'TH-47', + 'TH-48', + 'TH-49', + 'TH-50', + 'TH-51', + 'TH-52', + 'TH-53', + 'TH-54', + 'TH-55', + 'TH-56', + 'TH-57', + 'TH-58', + 'TH-60', + 'TH-61', + 'TH-62', + 'TH-63', + 'TH-64', + 'TH-65', + 'TH-66', + 'TH-67', + 'TH-70', + 'TH-71', + 'TH-72', + 'TH-73', + 'TH-74', + 'TH-75', + 'TH-76', + 'TH-77', + 'TH-80', + 'TH-81', + 'TH-82', + 'TH-83', + 'TH-84', + 'TH-85', + 'TH-86', + 'TH-90', + 'TH-91', + 'TH-92', + 'TH-93', + 'TH-94', + 'TH-95', + 'TH-96', + 'TH-S', + 'TJ-GB', + 'TJ-KT', + 'TJ-SU', + 'TL-AL', + 'TL-AN', + 'TL-BA', + 'TL-BO', + 'TL-CO', + 'TL-DI', + 'TL-ER', + 'TL-LA', + 'TL-LI', + 'TL-MF', + 'TL-MT', + 'TL-OE', + 'TL-VI', + 'TM-A', + 'TM-B', + 'TM-D', + 'TM-L', + 'TM-M', + 'TN-11', + 'TN-12', + 'TN-13', + 'TN-21', + 'TN-22', + 'TN-23', + 'TN-31', + 'TN-32', + 'TN-33', + 'TN-34', + 'TN-41', + 'TN-42', + 'TN-43', + 'TN-51', + 'TN-52', + 'TN-53', + 'TN-61', + 'TN-71', + 'TN-72', + 'TN-73', + 'TN-81', + 'TN-82', + 'TN-83', + 'TR-01', + 'TR-02', + 'TR-03', + 'TR-04', + 'TR-05', + 'TR-06', + 'TR-07', + 'TR-08', + 'TR-09', + 'TR-10', + 'TR-11', + 'TR-12', + 'TR-13', + 'TR-14', + 'TR-15', + 'TR-16', + 'TR-17', + 'TR-18', + 'TR-19', + 'TR-20', + 'TR-21', + 'TR-22', + 'TR-23', + 'TR-24', + 'TR-25', + 'TR-26', + 'TR-27', + 'TR-28', + 'TR-29', + 'TR-30', + 'TR-31', + 'TR-32', + 'TR-33', + 'TR-34', + 'TR-35', + 'TR-36', + 'TR-37', + 'TR-38', + 'TR-39', + 'TR-40', + 'TR-41', + 'TR-42', + 'TR-43', + 'TR-44', + 'TR-45', + 'TR-46', + 'TR-47', + 'TR-48', + 'TR-49', + 'TR-50', + 'TR-51', + 'TR-52', + 'TR-53', + 'TR-54', + 'TR-55', + 'TR-56', + 'TR-57', + 'TR-58', + 'TR-59', + 'TR-60', + 'TR-61', + 'TR-62', + 'TR-63', + 'TR-64', + 'TR-65', + 'TR-66', + 'TR-67', + 'TR-68', + 'TR-69', + 'TR-70', + 'TR-71', + 'TR-72', + 'TR-73', + 'TR-74', + 'TR-75', + 'TR-76', + 'TR-77', + 'TR-78', + 'TR-79', + 'TR-80', + 'TR-81', + 'TT-ARI', + 'TT-CHA', + 'TT-CTT', + 'TT-DMN', + 'TT-ETO', + 'TT-PED', + 'TT-POS', + 'TT-PRT', + 'TT-PTF', + 'TT-RCM', + 'TT-SFO', + 'TT-SGE', + 'TT-SIP', + 'TT-SJL', + 'TT-TUP', + 'TT-WTO', + 'TW-CHA', + 'TW-CYQ', + 'TW-HSQ', + 'TW-HUA', + 'TW-ILA', + 'TW-KEE', + 'TW-KHQ', + 'TW-MIA', + 'TW-NAN', + 'TW-PEN', + 'TW-PIF', + 'TW-TAO', + 'TW-TNQ', + 'TW-TPQ', + 'TW-TTT', + 'TW-TXQ', + 'TW-YUN', + 'TZ-01', + 'TZ-02', + 'TZ-03', + 'TZ-04', + 'TZ-05', + 'TZ-06', + 'TZ-07', + 'TZ-08', + 'TZ-09', + 'TZ-10', + 'TZ-11', + 'TZ-12', + 'TZ-13', + 'TZ-14', + 'TZ-15', + 'TZ-16', + 'TZ-17', + 'TZ-18', + 'TZ-19', + 'TZ-20', + 'TZ-21', + 'TZ-22', + 'TZ-23', + 'TZ-24', + 'TZ-25', + 'UA-05', + 'UA-07', + 'UA-09', + 'UA-12', + 'UA-14', + 'UA-18', + 'UA-21', + 'UA-23', + 'UA-26', + 'UA-30', + 'UA-32', + 'UA-35', + 'UA-40', + 'UA-43', + 'UA-46', + 'UA-48', + 'UA-51', + 'UA-53', + 'UA-56', + 'UA-59', + 'UA-61', + 'UA-63', + 'UA-65', + 'UA-68', + 'UA-71', + 'UA-74', + 'UA-77', + 'UG-AJM', + 'UG-APA', + 'UG-ARU', + 'UG-BUA', + 'UG-BUG', + 'UG-BUN', + 'UG-BUS', + 'UG-C', + 'UG-E', + 'UG-GUL', + 'UG-HOI', + 'UG-IGA', + 'UG-JIN', + 'UG-KAP', + 'UG-KAS', + 'UG-KAT', + 'UG-KBL', + 'UG-KBR', + 'UG-KIB', + 'UG-KIS', + 'UG-KIT', + 'UG-KLA', + 'UG-KLE', + 'UG-KLG', + 'UG-KLI', + 'UG-KOT', + 'UG-KUM', + 'UG-LIR', + 'UG-LUW', + 'UG-MBL', + 'UG-MBR', + 'UG-MOR', + 'UG-MOY', + 'UG-MPI', + 'UG-MSI', + 'UG-MSK', + 'UG-MUB', + 'UG-MUK', + 'UG-N', + 'UG-NAK', + 'UG-NEB', + 'UG-NTU', + 'UG-PAL', + 'UG-RAK', + 'UG-RUK', + 'UG-SEM', + 'UG-SOR', + 'UG-TOR', + 'UG-W', + 'UM-67', + 'UM-71', + 'UM-76', + 'UM-79', + 'UM-81', + 'UM-84', + 'UM-86', + 'UM-89', + 'UM-95', + 'US-AK', + 'US-AL', + 'US-AR', + 'US-AS', + 'US-AZ', + 'US-CA', + 'US-CO', + 'US-CT', + 'US-DC', + 'US-DE', + 'US-FL', + 'US-GA', + 'US-GU', + 'US-HI', + 'US-IA', + 'US-ID', + 'US-IL', + 'US-IN', + 'US-KS', + 'US-KY', + 'US-LA', + 'US-MA', + 'US-MD', + 'US-ME', + 'US-MI', + 'US-MN', + 'US-MO', + 'US-MP', + 'US-MS', + 'US-MT', + 'US-NC', + 'US-ND', + 'US-NE', + 'US-NH', + 'US-NJ', + 'US-NM', + 'US-NV', + 'US-NY', + 'US-OH', + 'US-OK', + 'US-OR', + 'US-PA', + 'US-PR', + 'US-RI', + 'US-SC', + 'US-SD', + 'US-TN', + 'US-TX', + 'US-UM', + 'US-UT', + 'US-VA', + 'US-VI', + 'US-VT', + 'US-WA', + 'US-WI', + 'US-WV', + 'US-WY', + 'UY-AR', + 'UY-CA', + 'UY-CL', + 'UY-CO', + 'UY-DU', + 'UY-FD', + 'UY-FS', + 'UY-LA', + 'UY-MA', + 'UY-MO', + 'UY-PA', + 'UY-RN', + 'UY-RO', + 'UY-RV', + 'UY-SA', + 'UY-SJ', + 'UY-SO', + 'UY-TA', + 'UY-TT', + 'UZ-AN', + 'UZ-BU', + 'UZ-FA', + 'UZ-JI', + 'UZ-NG', + 'UZ-NW', + 'UZ-QA', + 'UZ-QR', + 'UZ-SA', + 'UZ-SI', + 'UZ-SU', + 'UZ-TK', + 'UZ-TO', + 'UZ-XO', + 'VE-A', + 'VE-B', + 'VE-C', + 'VE-D', + 'VE-E', + 'VE-F', + 'VE-G', + 'VE-H', + 'VE-I', + 'VE-J', + 'VE-K', + 'VE-L', + 'VE-M', + 'VE-N', + 'VE-O', + 'VE-P', + 'VE-R', + 'VE-S', + 'VE-T', + 'VE-U', + 'VE-V', + 'VE-W', + 'VE-X', + 'VE-Y', + 'VE-Z', + 'VN-01', + 'VN-02', + 'VN-03', + 'VN-04', + 'VN-05', + 'VN-06', + 'VN-07', + 'VN-09', + 'VN-13', + 'VN-14', + 'VN-15', + 'VN-18', + 'VN-20', + 'VN-21', + 'VN-22', + 'VN-23', + 'VN-24', + 'VN-25', + 'VN-26', + 'VN-27', + 'VN-28', + 'VN-29', + 'VN-30', + 'VN-31', + 'VN-32', + 'VN-33', + 'VN-34', + 'VN-35', + 'VN-36', + 'VN-37', + 'VN-39', + 'VN-40', + 'VN-41', + 'VN-43', + 'VN-44', + 'VN-45', + 'VN-46', + 'VN-47', + 'VN-48', + 'VN-49', + 'VN-50', + 'VN-51', + 'VN-52', + 'VN-53', + 'VN-54', + 'VN-55', + 'VN-56', + 'VN-57', + 'VN-58', + 'VN-59', + 'VN-60', + 'VN-61', + 'VN-62', + 'VN-63', + 'VN-64', + 'VN-65', + 'VN-66', + 'VN-67', + 'VN-68', + 'VN-69', + 'VN-70', + 'VU-MAP', + 'VU-PAM', + 'VU-SAM', + 'VU-SEE', + 'VU-TAE', + 'VU-TOB', + 'WS-AA', + 'WS-AL', + 'WS-AT', + 'WS-FA', + 'WS-GE', + 'WS-GI', + 'WS-PA', + 'WS-SA', + 'WS-TU', + 'WS-VF', + 'WS-VS', + 'YE-AB', + 'YE-AD', + 'YE-AM', + 'YE-BA', + 'YE-DA', + 'YE-DH', + 'YE-HD', + 'YE-HJ', + 'YE-HU', + 'YE-IB', + 'YE-JA', + 'YE-LA', + 'YE-MA', + 'YE-MR', + 'YE-MW', + 'YE-SD', + 'YE-SH', + 'YE-SN', + 'YE-TA', + 'YU-CG', + 'YU-KM', + 'YU-SR', + 'YU-VO', + 'ZA-EC', + 'ZA-FS', + 'ZA-GT', + 'ZA-MP', + 'ZA-NC', + 'ZA-NL', + 'ZA-NP', + 'ZA-NW', + 'ZA-WC', + 'ZM-01', + 'ZM-02', + 'ZM-03', + 'ZM-04', + 'ZM-05', + 'ZM-06', + 'ZM-07', + 'ZM-08', + 'ZM-09', + 'ZW-BU', + 'ZW-HA', + 'ZW-MA', + 'ZW-MC', + 'ZW-ME', + 'ZW-MI', + 'ZW-MN', + 'ZW-MS', + 'ZW-MV', + 'ZW-MW', + name='subdivision', + ), + nullable=False, + ), + sa.Column('city', sa.Unicode(length=32), nullable=False), + sa.Column('city_confidence', sa.SmallInteger(), nullable=False), + sa.Column('isp', sa.Unicode(length=32), nullable=False), + sa.Column('organization', sa.Unicode(length=32), nullable=True), + sa.Column('organization_type', sa.Unicode(length=32), nullable=True), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action_with_one_device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Rate table - op.create_table('rate', - sa.Column('rating', sa.Float(decimal_return_scale=2), nullable=True, - comment='The rating for the content.'), - sa.Column('version', teal.db.StrictVersionType(), nullable=True, - comment='The version of the software.'), - sa.Column('appearance', sa.Float(decimal_return_scale=2), nullable=True, - comment='Subjective value representing aesthetic aspects.'), - sa.Column('functionality', sa.Float(decimal_return_scale=2), nullable=True, - comment='Subjective value representing usage aspects.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action_with_one_device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'rate', + sa.Column( + 'rating', + sa.Float(decimal_return_scale=2), + nullable=True, + comment='The rating for the content.', + ), + sa.Column( + 'version', + teal.db.StrictVersionType(), + nullable=True, + comment='The version of the software.', + ), + sa.Column( + 'appearance', + sa.Float(decimal_return_scale=2), + nullable=True, + comment='Subjective value representing aesthetic aspects.', + ), + sa.Column( + 'functionality', + sa.Float(decimal_return_scale=2), + nullable=True, + comment='Subjective value representing usage aspects.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action_with_one_device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Snapshot table - op.create_table('snapshot', - sa.Column('uuid', postgresql.UUID(as_uuid=True), nullable=True), - sa.Column('version', teal.db.StrictVersionType(length=32), nullable=False), - sa.Column('software', sa.Enum('Workbench', 'WorkbenchAndroid', 'AndroidApp', 'Web', 'DesktopApp', - name='snapshotsoftware'), nullable=False), - sa.Column('elapsed', sa.Interval(), nullable=True, - comment='For Snapshots made with Workbench, the total amount \n of time it took to complete.\n '), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action_with_one_device.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('uuid'), - schema=f'{get_inv()}' - ) + op.create_table( + 'snapshot', + sa.Column('uuid', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('version', teal.db.StrictVersionType(length=32), nullable=False), + sa.Column( + 'software', + sa.Enum( + 'Workbench', + 'WorkbenchAndroid', + 'AndroidApp', + 'Web', + 'DesktopApp', + name='snapshotsoftware', + ), + nullable=False, + ), + sa.Column( + 'elapsed', + sa.Interval(), + nullable=True, + comment='For Snapshots made with Workbench, the total amount \n of time it took to complete.\n ', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action_with_one_device.id'], + ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('uuid'), + schema=f'{get_inv()}', + ) # Test table - op.create_table('test', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action_with_one_device.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action_with_one_device.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # BenchmarkDataStorage table - op.create_table('benchmark_data_storage', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('read_speed', sa.Float(decimal_return_scale=2), nullable=False), - sa.Column('write_speed', sa.Float(decimal_return_scale=2), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.benchmark.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'benchmark_data_storage', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('read_speed', sa.Float(decimal_return_scale=2), nullable=False), + sa.Column('write_speed', sa.Float(decimal_return_scale=2), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.benchmark.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # BenchmarkWithRate table - op.create_table('benchmark_with_rate', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('rate', sa.Float(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.benchmark.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'benchmark_with_rate', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('rate', sa.Float(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.benchmark.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # MeasureBattery table - op.create_table('measure_battery', - sa.Column('size', sa.Integer(), nullable=False, comment='Maximum battery capacity, in mAh.'), - sa.Column('voltage', sa.Integer(), nullable=False, - comment='The actual voltage of the battery, in mV.'), - sa.Column('cycle_count', sa.Integer(), nullable=True, - comment='The number of full charges – discharges \n cycles.\n '), - sa.Column('health', sa.Enum('Cold', 'Dead', 'Good', 'Overheat', 'OverVoltage', 'UnspecifiedValue', - name='batteryhealth'), nullable=True, - comment='The health of the Battery. \n Only reported in Android.\n '), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'measure_battery', + sa.Column( + 'size', + sa.Integer(), + nullable=False, + comment='Maximum battery capacity, in mAh.', + ), + sa.Column( + 'voltage', + sa.Integer(), + nullable=False, + comment='The actual voltage of the battery, in mV.', + ), + sa.Column( + 'cycle_count', + sa.Integer(), + nullable=True, + comment='The number of full charges – discharges \n cycles.\n ', + ), + sa.Column( + 'health', + sa.Enum( + 'Cold', + 'Dead', + 'Good', + 'Overheat', + 'OverVoltage', + 'UnspecifiedValue', + name='batteryhealth', + ), + nullable=True, + comment='The health of the Battery. \n Only reported in Android.\n ', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Price table - op.create_table('price', - sa.Column('currency', - sa.Enum('AFN', 'ARS', 'AWG', 'AUD', 'AZN', 'BSD', 'BBD', 'BDT', 'BYR', 'BZD', 'BMD', - 'BOB', 'BAM', 'BWP', 'BGN', 'BRL', 'BND', 'KHR', 'CAD', 'KYD', 'CLP', 'CNY', - 'COP', 'CRC', 'HRK', 'CUP', 'CZK', 'DKK', 'DOP', 'XCD', 'EGP', 'SVC', 'EEK', - 'EUR', 'FKP', 'FJD', 'GHC', 'GIP', 'GTQ', 'GGP', 'GYD', 'HNL', 'HKD', 'HUF', - 'ISK', 'INR', 'IDR', 'IRR', 'IMP', 'ILS', 'JMD', 'JPY', 'JEP', 'KZT', 'KPW', - 'KRW', 'KGS', 'LAK', 'LVL', 'LBP', 'LRD', 'LTL', 'MKD', 'MYR', 'MUR', 'MXN', - 'MNT', 'MZN', 'NAD', 'NPR', 'ANG', 'NZD', 'NIO', 'NGN', 'NOK', 'OMR', 'PKR', - 'PAB', 'PYG', 'PEN', 'PHP', 'PLN', 'QAR', 'RON', 'RUB', 'SHP', 'SAR', 'RSD', - 'SCR', 'SGD', 'SBD', 'SOS', 'ZAR', 'LKR', 'SEK', 'CHF', 'SRD', 'SYP', 'TWD', - 'THB', 'TTD', 'TRY', 'TRL', 'TVD', 'UAH', 'GBP', 'USD', 'UYU', 'UZS', 'VEF', - 'VND', 'YER', 'ZWD', name='currency'), nullable=False, - comment='The currency of this price as for ISO 4217.'), - sa.Column('price', sa.Numeric(precision=19, scale=4), nullable=False, comment='The value.'), - sa.Column('software', sa.Enum('Ereuse', name='pricesoftware'), nullable=True, - comment='The software used to compute this price,\n if the price was computed automatically. This field is None\n if the price has been manually set.\n '), - sa.Column('version', teal.db.StrictVersionType(), nullable=True, - comment='The version of the software, or None.'), - sa.Column('rating_id', postgresql.UUID(as_uuid=True), nullable=True, - comment='The Rate used to auto-compute\n this price, if it has not been set manually.\n '), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action_with_one_device.id'], ), - sa.ForeignKeyConstraint(['rating_id'], [f'{get_inv()}.rate.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'price', + sa.Column( + 'currency', + sa.Enum( + 'AFN', + 'ARS', + 'AWG', + 'AUD', + 'AZN', + 'BSD', + 'BBD', + 'BDT', + 'BYR', + 'BZD', + 'BMD', + 'BOB', + 'BAM', + 'BWP', + 'BGN', + 'BRL', + 'BND', + 'KHR', + 'CAD', + 'KYD', + 'CLP', + 'CNY', + 'COP', + 'CRC', + 'HRK', + 'CUP', + 'CZK', + 'DKK', + 'DOP', + 'XCD', + 'EGP', + 'SVC', + 'EEK', + 'EUR', + 'FKP', + 'FJD', + 'GHC', + 'GIP', + 'GTQ', + 'GGP', + 'GYD', + 'HNL', + 'HKD', + 'HUF', + 'ISK', + 'INR', + 'IDR', + 'IRR', + 'IMP', + 'ILS', + 'JMD', + 'JPY', + 'JEP', + 'KZT', + 'KPW', + 'KRW', + 'KGS', + 'LAK', + 'LVL', + 'LBP', + 'LRD', + 'LTL', + 'MKD', + 'MYR', + 'MUR', + 'MXN', + 'MNT', + 'MZN', + 'NAD', + 'NPR', + 'ANG', + 'NZD', + 'NIO', + 'NGN', + 'NOK', + 'OMR', + 'PKR', + 'PAB', + 'PYG', + 'PEN', + 'PHP', + 'PLN', + 'QAR', + 'RON', + 'RUB', + 'SHP', + 'SAR', + 'RSD', + 'SCR', + 'SGD', + 'SBD', + 'SOS', + 'ZAR', + 'LKR', + 'SEK', + 'CHF', + 'SRD', + 'SYP', + 'TWD', + 'THB', + 'TTD', + 'TRY', + 'TRL', + 'TVD', + 'UAH', + 'GBP', + 'USD', + 'UYU', + 'UZS', + 'VEF', + 'VND', + 'YER', + 'ZWD', + name='currency', + ), + nullable=False, + comment='The currency of this price as for ISO 4217.', + ), + sa.Column( + 'price', + sa.Numeric(precision=19, scale=4), + nullable=False, + comment='The value.', + ), + sa.Column( + 'software', + sa.Enum('Ereuse', name='pricesoftware'), + nullable=True, + comment='The software used to compute this price,\n if the price was computed automatically. This field is None\n if the price has been manually set.\n ', + ), + sa.Column( + 'version', + teal.db.StrictVersionType(), + nullable=True, + comment='The version of the software, or None.', + ), + sa.Column( + 'rating_id', + postgresql.UUID(as_uuid=True), + nullable=True, + comment='The Rate used to auto-compute\n this price, if it has not been set manually.\n ', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action_with_one_device.id'], + ), + sa.ForeignKeyConstraint( + ['rating_id'], + [f'{get_inv()}.rate.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # ProofDataWipe table - op.create_table('proof_data_wipe', - sa.Column('date', sa.DateTime(), nullable=False), - sa.Column('result', sa.Boolean(), nullable=False, comment='Identifies proof datawipe as a result.'), - sa.Column('proof_author_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('erasure_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['erasure_id'], [f'{get_inv()}.erase_basic.id'], ), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.proof.id'], ), - sa.ForeignKeyConstraint(['proof_author_id'], ['common.user.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'proof_data_wipe', + sa.Column('date', sa.DateTime(), nullable=False), + sa.Column( + 'result', + sa.Boolean(), + nullable=False, + comment='Identifies proof datawipe as a result.', + ), + sa.Column('proof_author_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('erasure_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['erasure_id'], + [f'{get_inv()}.erase_basic.id'], + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.proof.id'], + ), + sa.ForeignKeyConstraint( + ['proof_author_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # PRoofFuntion - op.create_table('proof_function', - sa.Column('disk_usage', sa.Integer(), nullable=True), - sa.Column('proof_author_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('rate_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.proof.id'], ), - sa.ForeignKeyConstraint(['proof_author_id'], ['common.user.id'], ), - sa.ForeignKeyConstraint(['rate_id'], [f'{get_inv()}.rate.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'proof_function', + sa.Column('disk_usage', sa.Integer(), nullable=True), + sa.Column('proof_author_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('rate_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.proof.id'], + ), + sa.ForeignKeyConstraint( + ['proof_author_id'], + ['common.user.id'], + ), + sa.ForeignKeyConstraint( + ['rate_id'], + [f'{get_inv()}.rate.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # RateComputer table - op.create_table('rate_computer', - sa.Column('processor', sa.Float(decimal_return_scale=2), nullable=True, - comment='The rate of the Processor.'), - sa.Column('ram', sa.Float(decimal_return_scale=2), nullable=True, comment='The rate of the RAM.'), - sa.Column('data_storage', sa.Float(decimal_return_scale=2), nullable=True, - comment="'Data storage rate, like HHD, SSD.'"), - sa.Column('graphic_card', sa.Float(decimal_return_scale=2), nullable=True, - comment='Graphic card rate.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.rate.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'rate_computer', + sa.Column( + 'processor', + sa.Float(decimal_return_scale=2), + nullable=True, + comment='The rate of the Processor.', + ), + sa.Column( + 'ram', + sa.Float(decimal_return_scale=2), + nullable=True, + comment='The rate of the RAM.', + ), + sa.Column( + 'data_storage', + sa.Float(decimal_return_scale=2), + nullable=True, + comment="'Data storage rate, like HHD, SSD.'", + ), + sa.Column( + 'graphic_card', + sa.Float(decimal_return_scale=2), + nullable=True, + comment='Graphic card rate.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.rate.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # SnapshotRequest table - op.create_table('snapshot_request', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('request', sa.JSON(), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.snapshot.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'snapshot_request', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('request', sa.JSON(), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.snapshot.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Step table - op.create_table('step', - sa.Column('erasure_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('type', sa.Unicode(length=32), nullable=False), - sa.Column('num', sa.SmallInteger(), nullable=False), - sa.Column('severity', teal.db.IntEnum(Severity), nullable=False), - sa.Column('start_time', sa.TIMESTAMP(timezone=True), nullable=False, - comment='When the action starts. For some actions like\n reservations the time when they are available, for others like renting\n when the renting starts.\n '), - sa.Column('end_time', sa.TIMESTAMP(timezone=True), nullable=False, - comment='When the action ends. For some actions like reservations\n the time when they expire, for others like renting\n the time the end rents. For punctual actions it is the time \n they are performed; it differs with ``created`` in which\n created is the where the system received the action.\n '), - sa.ForeignKeyConstraint(['erasure_id'], [f'{get_inv()}.erase_basic.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('erasure_id', 'num'), - schema=f'{get_inv()}' - ) + op.create_table( + 'step', + sa.Column('erasure_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('type', sa.Unicode(length=32), nullable=False), + sa.Column('num', sa.SmallInteger(), nullable=False), + sa.Column('severity', teal.db.IntEnum(Severity), nullable=False), + sa.Column( + 'start_time', + sa.TIMESTAMP(timezone=True), + nullable=False, + comment='When the action starts. For some actions like\n reservations the time when they are available, for others like renting\n when the renting starts.\n ', + ), + sa.Column( + 'end_time', + sa.TIMESTAMP(timezone=True), + nullable=False, + comment='When the action ends. For some actions like reservations\n the time when they expire, for others like renting\n the time the end rents. For punctual actions it is the time \n they are performed; it differs with ``created`` in which\n created is the where the system received the action.\n ', + ), + sa.ForeignKeyConstraint( + ['erasure_id'], [f'{get_inv()}.erase_basic.id'], ondelete='CASCADE' + ), + sa.PrimaryKeyConstraint('erasure_id', 'num'), + schema=f'{get_inv()}', + ) - op.create_table('stress_test', - sa.Column('elapsed', sa.Interval(), nullable=False), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'stress_test', + sa.Column('elapsed', sa.Interval(), nullable=False), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) - op.create_table('test_audio', - sa.Column('speaker', sa.Boolean(), nullable=True, comment='Whether the speaker works as expected.'), - sa.Column('microphone', sa.Boolean(), nullable=True, - comment='Whether the microphone works as expected.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_audio', + sa.Column( + 'speaker', + sa.Boolean(), + nullable=True, + comment='Whether the speaker works as expected.', + ), + sa.Column( + 'microphone', + sa.Boolean(), + nullable=True, + comment='Whether the microphone works as expected.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) - op.create_table('test_bios', - sa.Column('beeps_power_on', sa.Boolean(), nullable=True, - comment='Whether there are no beeps or error\n codes when booting up.\n \n Reference: R2 provision 6 page 23.\n '), - sa.Column('access_range', sa.Enum('A', 'B', 'C', 'D', 'E', name='biosaccessrange'), nullable=True, - comment='Difficulty to modify the boot menu.\n \n This is used as an usability measure for accessing and modifying\n a bios, specially as something as important as modifying the boot\n menu.\n '), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_bios', + sa.Column( + 'beeps_power_on', + sa.Boolean(), + nullable=True, + comment='Whether there are no beeps or error\n codes when booting up.\n \n Reference: R2 provision 6 page 23.\n ', + ), + sa.Column( + 'access_range', + sa.Enum('A', 'B', 'C', 'D', 'E', name='biosaccessrange'), + nullable=True, + comment='Difficulty to modify the boot menu.\n \n This is used as an usability measure for accessing and modifying\n a bios, specially as something as important as modifying the boot\n menu.\n ', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) - op.create_table('test_camera', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_camera', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) - op.create_table('test_connectivity', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_connectivity', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) - op.create_table('test_data_storage', - sa.Column('length', sa.Enum('Short', 'Extended', name='testdatastoragelength'), nullable=False), - sa.Column('status', sa.Unicode(), nullable=False), - sa.Column('lifetime', sa.Interval(), nullable=True), - sa.Column('assessment', sa.Boolean(), nullable=True), - sa.Column('reallocated_sector_count', sa.SmallInteger(), nullable=True), - sa.Column('power_cycle_count', sa.SmallInteger(), nullable=True), - sa.Column('reported_uncorrectable_errors', sa.Integer(), nullable=True), - sa.Column('command_timeout', sa.Integer(), nullable=True), - sa.Column('current_pending_sector_count', sa.SmallInteger(), nullable=True), - sa.Column('offline_uncorrectable', sa.SmallInteger(), nullable=True), - sa.Column('remaining_lifetime_percentage', sa.SmallInteger(), nullable=True), - sa.Column('elapsed', sa.Interval(), nullable=False), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_data_storage', + sa.Column( + 'length', + sa.Enum('Short', 'Extended', name='testdatastoragelength'), + nullable=False, + ), + sa.Column('status', sa.Unicode(), nullable=False), + sa.Column('lifetime', sa.Interval(), nullable=True), + sa.Column('assessment', sa.Boolean(), nullable=True), + sa.Column('reallocated_sector_count', sa.SmallInteger(), nullable=True), + sa.Column('power_cycle_count', sa.SmallInteger(), nullable=True), + sa.Column('reported_uncorrectable_errors', sa.Integer(), nullable=True), + sa.Column('command_timeout', sa.Integer(), nullable=True), + sa.Column('current_pending_sector_count', sa.SmallInteger(), nullable=True), + sa.Column('offline_uncorrectable', sa.SmallInteger(), nullable=True), + sa.Column('remaining_lifetime_percentage', sa.SmallInteger(), nullable=True), + sa.Column('elapsed', sa.Interval(), nullable=False), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # TestDisplayHinge table - op.create_table('test_display_hinge', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_display_hinge', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # TestKeyboard table - op.create_table('test_keyboard', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_keyboard', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # TestPowerAdapter table - op.create_table('test_power_adapter', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_power_adapter', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # TestTrackpad table - op.create_table('test_trackpad', - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'test_trackpad', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # VisualTest table - op.create_table('visual_test', - sa.Column('appearance_range', sa.Enum('Z', 'A', 'B', 'C', 'D', 'E', name='appearancerange'), - nullable=True, - comment='Grades the imperfections that aesthetically affect the device, but not its usage.'), - sa.Column('functionality_range', sa.Enum('A', 'B', 'C', 'D', name='functionalityrange'), - nullable=True, comment='Grades the defects of a device that affect its usage.'), - sa.Column('labelling', sa.Boolean(), nullable=True, - comment='Whether there are tags to be removed.'), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.test.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'visual_test', + sa.Column( + 'appearance_range', + sa.Enum('Z', 'A', 'B', 'C', 'D', 'E', name='appearancerange'), + nullable=True, + comment='Grades the imperfections that aesthetically affect the device, but not its usage.', + ), + sa.Column( + 'functionality_range', + sa.Enum('A', 'B', 'C', 'D', name='functionalityrange'), + nullable=True, + comment='Grades the defects of a device that affect its usage.', + ), + sa.Column( + 'labelling', + sa.Boolean(), + nullable=True, + comment='Whether there are tags to be removed.', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.test.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # Trade table - op.create_table('trade', - sa.Column('shipping_date', sa.TIMESTAMP(timezone=True), nullable=True, - comment='When are the devices going to be ready \n for shipping?\n '), - sa.Column('invoice_number', citext.CIText(), nullable=True, - comment='The id of the invoice so they can be linked.'), - sa.Column('price_id', postgresql.UUID(as_uuid=True), nullable=True, - comment='The price set for this trade. \n If no price is set it is supposed that the trade was\n not payed, usual in donations.\n '), - sa.Column('to_id', postgresql.UUID(as_uuid=True), nullable=False), - sa.Column('confirms_id', postgresql.UUID(as_uuid=True), nullable=True, - comment='An organize action that this association confirms. \n \n For example, a ``Sell`` or ``Rent``\n can confirm a ``Reserve`` action.\n '), - sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), - sa.ForeignKeyConstraint(['confirms_id'], [f'{get_inv()}.organize.id'], ), - sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), - sa.ForeignKeyConstraint(['price_id'], [f'{get_inv()}.price.id'], ), - sa.ForeignKeyConstraint(['to_id'], [f'{get_inv()}.agent.id'], ), - sa.PrimaryKeyConstraint('id'), - schema=f'{get_inv()}' - ) + op.create_table( + 'trade', + sa.Column( + 'shipping_date', + sa.TIMESTAMP(timezone=True), + nullable=True, + comment='When are the devices going to be ready \n for shipping?\n ', + ), + sa.Column( + 'invoice_number', + citext.CIText(), + nullable=True, + comment='The id of the invoice so they can be linked.', + ), + sa.Column( + 'price_id', + postgresql.UUID(as_uuid=True), + nullable=True, + comment='The price set for this trade. \n If no price is set it is supposed that the trade was\n not payed, usual in donations.\n ', + ), + sa.Column('to_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column( + 'confirms_id', + postgresql.UUID(as_uuid=True), + nullable=True, + comment='An organize action that this association confirms. \n \n For example, a ``Sell`` or ``Rent``\n can confirm a ``Reserve`` action.\n ', + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint( + ['confirms_id'], + [f'{get_inv()}.organize.id'], + ), + sa.ForeignKeyConstraint( + ['id'], + [f'{get_inv()}.action.id'], + ), + sa.ForeignKeyConstraint( + ['price_id'], + [f'{get_inv()}.price.id'], + ), + sa.ForeignKeyConstraint( + ['to_id'], + [f'{get_inv()}.agent.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) # ### end Alembic commands ### @@ -1643,7 +6867,9 @@ def downgrade(): op.drop_table('test', schema=f'{get_inv()}') - op.drop_constraint("snapshot_actions", "action", type_="foreignkey", schema=f'{get_inv()}') + op.drop_constraint( + "snapshot_actions", "action", type_="foreignkey", schema=f'{get_inv()}' + ) op.drop_table('snapshot', schema=f'{get_inv()}') op.drop_table('rate', schema=f'{get_inv()}') @@ -1672,8 +6898,12 @@ def downgrade(): op.drop_table('migrate', schema=f'{get_inv()}') - op.drop_index(op.f('ix_membership_updated'), table_name='membership', schema=f'{get_inv()}') - op.drop_index(op.f('ix_membership_created'), table_name='membership', schema=f'{get_inv()}') + op.drop_index( + op.f('ix_membership_updated'), table_name='membership', schema=f'{get_inv()}' + ) + op.drop_index( + op.f('ix_membership_created'), table_name='membership', schema=f'{get_inv()}' + ) op.drop_table('membership', schema=f'{get_inv()}') op.drop_table('graphic_card', schema=f'{get_inv()}') @@ -1688,7 +6918,11 @@ def downgrade(): op.drop_table('allocate', schema=f'{get_inv()}') - op.drop_index('action_one_device_id_index', table_name='action_with_one_device', schema=f'{get_inv()}') + op.drop_index( + 'action_one_device_id_index', + table_name='action_with_one_device', + schema=f'{get_inv()}', + ) op.drop_table('action_with_one_device', schema=f'{get_inv()}') op.drop_table('action_device', schema=f'{get_inv()}') @@ -1720,8 +6954,16 @@ def downgrade(): op.drop_table('individual', schema=f'{get_inv()}') - op.drop_index(op.f('ix_deliverynote_updated'), table_name='deliverynote', schema=f'{get_inv()}') - op.drop_index(op.f('ix_deliverynote_created'), table_name='deliverynote', schema=f'{get_inv()}') + op.drop_index( + op.f('ix_deliverynote_updated'), + table_name='deliverynote', + schema=f'{get_inv()}', + ) + op.drop_index( + op.f('ix_deliverynote_created'), + table_name='deliverynote', + schema=f'{get_inv()}', + ) op.drop_table('deliverynote', schema=f'{get_inv()}') op.drop_index('parent_index', table_name='component', schema=f'{get_inv()}') @@ -1780,8 +7022,12 @@ def downgrade(): op.drop_table('manufacturer', schema='common') - op.drop_index(op.f('ix_common_inventory_updated'), table_name='inventory', schema='common') - op.drop_index(op.f('ix_common_inventory_created'), table_name='inventory', schema='common') + op.drop_index( + op.f('ix_common_inventory_updated'), table_name='inventory', schema='common' + ) + op.drop_index( + op.f('ix_common_inventory_created'), table_name='inventory', schema='common' + ) op.drop_index('id_hash', table_name='inventory', schema='common') op.drop_table('inventory', schema='common') diff --git a/ereuse_devicehub/parser/computer.py b/ereuse_devicehub/parser/computer.py index b41ce833..50a8bf61 100644 --- a/ereuse_devicehub/parser/computer.py +++ b/ereuse_devicehub/parser/computer.py @@ -7,8 +7,8 @@ from math import hypot from typing import Iterator, List, Optional, TypeVar import dateutil.parser -from ereuse_utils import getter, text -from ereuse_utils.nested_lookup import ( +from ereuse_devicehub.ereuse_utils import getter, text +from ereuse_devicehub.ereuse_utils.nested_lookup import ( get_nested_dicts_with_key_containing_value, get_nested_dicts_with_key_value, ) diff --git a/ereuse_devicehub/parser/utils.py b/ereuse_devicehub/parser/utils.py index e36990fe..3d912111 100644 --- a/ereuse_devicehub/parser/utils.py +++ b/ereuse_devicehub/parser/utils.py @@ -5,7 +5,7 @@ import struct from contextlib import contextmanager from enum import Enum -from ereuse_utils import Dumpeable +from ereuse_devicehub.ereuse_utils import Dumpeable class Severity(Enum): diff --git a/ereuse_devicehub/query.py b/ereuse_devicehub/query.py index c5bd1528..c7e26569 100644 --- a/ereuse_devicehub/query.py +++ b/ereuse_devicehub/query.py @@ -1,12 +1,12 @@ from typing import Dict, List from flask import Response, jsonify, request -from teal.query import NestedQueryFlaskParser from webargs.flaskparser import FlaskParser +from ereuse_devicehub.teal.query import NestedQueryFlaskParser + class SearchQueryParser(NestedQueryFlaskParser): - def parse_querystring(self, req, name, field): if name == 'search': v = FlaskParser.parse_querystring(self, req, name, field) @@ -15,29 +15,33 @@ class SearchQueryParser(NestedQueryFlaskParser): return v -def things_response(items: List[Dict], - page: int = None, - per_page: int = None, - total: int = None, - previous: int = None, - next: int = None, - url: str = None, - code: int = 200) -> Response: +def things_response( + items: List[Dict], + page: int = None, + per_page: int = None, + total: int = None, + previous: int = None, + next: int = None, + url: str = None, + code: int = 200, +) -> Response: """Generates a Devicehub API list conformant response for multiple things. """ - response = jsonify({ - 'items': items, - # todo pagination should be in Header like github - # https://developer.github.com/v3/guides/traversing-with-pagination/ - 'pagination': { - 'page': page, - 'perPage': per_page, - 'total': total, - 'previous': previous, - 'next': next - }, - 'url': url or request.path - }) + response = jsonify( + { + 'items': items, + # todo pagination should be in Header like github + # https://developer.github.com/v3/guides/traversing-with-pagination/ + 'pagination': { + 'page': page, + 'perPage': per_page, + 'total': total, + 'previous': previous, + 'next': next, + }, + 'url': url or request.path, + } + ) response.status_code = code return response diff --git a/ereuse_devicehub/resources/action/__init__.py b/ereuse_devicehub/resources/action/__init__.py index b405e164..96c966f4 100644 --- a/ereuse_devicehub/resources/action/__init__.py +++ b/ereuse_devicehub/resources/action/__init__.py @@ -1,11 +1,14 @@ from typing import Callable, Iterable, Tuple -from teal.resource import Converters, Resource - from ereuse_devicehub.resources.action import schemas -from ereuse_devicehub.resources.action.views.views import (ActionView, AllocateView, DeallocateView, - LiveView) +from ereuse_devicehub.resources.action.views.views import ( + ActionView, + AllocateView, + DeallocateView, + LiveView, +) from ereuse_devicehub.resources.device.sync import Sync +from ereuse_devicehub.teal.resource import Converters, Resource class ActionDef(Resource): @@ -169,13 +172,32 @@ class SnapshotDef(ActionDef): VIEW = None SCHEMA = schemas.Snapshot - 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: Iterable[Tuple[Callable, str or None]] = tuple()): + 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: Iterable[Tuple[Callable, str or None]] = tuple(), + ): url_prefix = '/{}'.format(ActionDef.resource) - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path, cli_commands) + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_commands, + ) self.sync = Sync() diff --git a/ereuse_devicehub/resources/action/models.py b/ereuse_devicehub/resources/action/models.py index b486e2bd..5a05256d 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -23,7 +23,6 @@ from typing import Optional, Set, Union from uuid import uuid4 import inflection -import teal.db from boltons import urlutils from citext import CIText from dateutil.tz import tzutc @@ -50,19 +49,8 @@ from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.orm import backref, relationship, validates from sqlalchemy.orm.events import AttributeEvents as Events from sqlalchemy.util import OrderedSet -from teal.db import ( - CASCADE_OWN, - INHERIT_COND, - POLYMORPHIC_ID, - POLYMORPHIC_ON, - URL, - StrictVersionType, - check_lower, - check_range, -) -from teal.enums import Currency -from teal.resource import url_for_resource +import ereuse_devicehub.teal.db from ereuse_devicehub.db import db from ereuse_devicehub.resources.agent.models import Agent from ereuse_devicehub.resources.device.metrics import TradeMetrics @@ -94,6 +82,18 @@ from ereuse_devicehub.resources.enums import ( from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.db import ( + CASCADE_OWN, + INHERIT_COND, + POLYMORPHIC_ID, + POLYMORPHIC_ON, + URL, + StrictVersionType, + check_lower, + check_range, +) +from ereuse_devicehub.teal.enums import Currency +from ereuse_devicehub.teal.resource import url_for_resource class JoinedTableMixin: @@ -125,7 +125,11 @@ class Action(Thing): name.comment = """A name or title for the action. Used when searching for actions. """ - severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False) + severity = Column( + ereuse_devicehub.teal.db.IntEnum(Severity), + default=Severity.Info, + nullable=False, + ) severity.comment = Severity.__doc__ closed = Column(Boolean, default=True, nullable=False) closed.comment = """Whether the author has finished the action. @@ -594,7 +598,11 @@ class Step(db.Model): ) type = Column(Unicode(STR_SM_SIZE), nullable=False) num = Column(SmallInteger, primary_key=True) - severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False) + severity = Column( + ereuse_devicehub.teal.db.IntEnum(Severity), + default=Severity.Info, + nullable=False, + ) start_time = Column(db.TIMESTAMP(timezone=True), nullable=False) start_time.comment = Action.start_time.comment end_time = Column( diff --git a/ereuse_devicehub/resources/action/schemas.py b/ereuse_devicehub/resources/action/schemas.py index da70c49a..6b10ca69 100644 --- a/ereuse_devicehub/resources/action/schemas.py +++ b/ereuse_devicehub/resources/action/schemas.py @@ -21,9 +21,6 @@ from marshmallow.fields import ( ) from marshmallow.validate import Length, OneOf, Range from sqlalchemy.util import OrderedSet -from teal.enums import Country, Currency, Subdivision -from teal.marshmallow import IP, URL, EnumField, SanitizedStr, Version -from teal.resource import Schema from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources import enums @@ -48,6 +45,9 @@ from ereuse_devicehub.resources.tradedocument import schemas as s_document from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.user import schemas as s_user from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.enums import Country, Currency, Subdivision +from ereuse_devicehub.teal.marshmallow import IP, URL, EnumField, SanitizedStr, Version +from ereuse_devicehub.teal.resource import Schema class Action(Thing): diff --git a/ereuse_devicehub/resources/action/views/trade.py b/ereuse_devicehub/resources/action/views/trade.py index 4e2a31d5..2624a74c 100644 --- a/ereuse_devicehub/resources/action/views/trade.py +++ b/ereuse_devicehub/resources/action/views/trade.py @@ -1,5 +1,4 @@ from flask import g -from teal.marshmallow import ValidationError from ereuse_devicehub.db import db from ereuse_devicehub.inventory.models import Transfer @@ -13,6 +12,7 @@ from ereuse_devicehub.resources.action.models import ( ) from ereuse_devicehub.resources.lot.views import delete_from_trade from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.marshmallow import ValidationError class TradeView: diff --git a/ereuse_devicehub/resources/action/views/views.py b/ereuse_devicehub/resources/action/views/views.py index 25814b4a..71ec908e 100644 --- a/ereuse_devicehub/resources/action/views/views.py +++ b/ereuse_devicehub/resources/action/views/views.py @@ -4,13 +4,10 @@ from datetime import timedelta from distutils.version import StrictVersion from uuid import UUID -import ereuse_utils +import ereuse_devicehub.ereuse_utils import jwt from flask import current_app as app from flask import g, request -from teal.db import ResourceNotFound -from teal.marshmallow import ValidationError -from teal.resource import View from ereuse_devicehub.db import db from ereuse_devicehub.query import things_response @@ -35,6 +32,9 @@ from ereuse_devicehub.resources.action.views.snapshot import ( ) from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device from ereuse_devicehub.resources.enums import Severity +from ereuse_devicehub.teal.db import ResourceNotFound +from ereuse_devicehub.teal.marshmallow import ValidationError +from ereuse_devicehub.teal.resource import View SUPPORTED_WORKBENCH = StrictVersion('11.0') @@ -203,7 +203,7 @@ def decode_snapshot(data): data['data'], app.config['JWT_PASS'], algorithms="HS256", - json_encoder=ereuse_utils.JSONEncoder, + json_encoder=ereuse_devicehub.ereuse_utils.JSONEncoder, ) except jwt.exceptions.InvalidSignatureError as err: txt = 'Invalid snapshot' diff --git a/ereuse_devicehub/resources/agent/__init__.py b/ereuse_devicehub/resources/agent/__init__.py index 20d4945d..dcbb566b 100644 --- a/ereuse_devicehub/resources/agent/__init__.py +++ b/ereuse_devicehub/resources/agent/__init__.py @@ -2,10 +2,10 @@ import json import click from boltons.typeutils import classproperty -from teal.resource import Converters, Resource from ereuse_devicehub.db import db from ereuse_devicehub.resources.agent import models, schemas +from ereuse_devicehub.teal.resource import Converters, Resource class AgentDef(Resource): @@ -19,26 +19,40 @@ class OrganizationDef(AgentDef): SCHEMA = schemas.Organization 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): + 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.create_org, 'add'),) - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path, cli_commands) + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_commands, + ) @click.argument('name') @click.option('--tax_id', '-t') @click.option('--country', '-c') def create_org(self, name: str, tax_id: str = None, country: str = None) -> dict: """Creates an organization.""" - org = models.Organization(**self.schema.load( - { - 'name': name, - 'taxId': tax_id, - 'country': country - } - )) + org = models.Organization( + **self.schema.load({'name': name, 'taxId': tax_id, 'country': country}) + ) db.session.add(org) db.session.commit() o = self.schema.dump(org) diff --git a/ereuse_devicehub/resources/agent/models.py b/ereuse_devicehub/resources/agent/models.py index 826d0545..7204423e 100644 --- a/ereuse_devicehub/resources/agent/models.py +++ b/ereuse_devicehub/resources/agent/models.py @@ -10,14 +10,19 @@ 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 INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, check_lower -from teal.marshmallow import ValidationError 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 +from ereuse_devicehub.teal import enums +from ereuse_devicehub.teal.db import ( + INHERIT_COND, + POLYMORPHIC_ID, + POLYMORPHIC_ON, + check_lower, +) +from ereuse_devicehub.teal.marshmallow import ValidationError class JoinedTableMixin: diff --git a/ereuse_devicehub/resources/agent/schemas.py b/ereuse_devicehub/resources/agent/schemas.py index 24109c18..8490e1de 100644 --- a/ereuse_devicehub/resources/agent/schemas.py +++ b/ereuse_devicehub/resources/agent/schemas.py @@ -1,19 +1,20 @@ -from marshmallow import fields as ma_fields, validate as ma_validate +from marshmallow import fields as ma_fields +from marshmallow import validate as ma_validate from marshmallow.fields import Email -from teal import enums -from teal.marshmallow import EnumField, Phone, SanitizedStr from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.models import STR_SIZE, STR_SM_SIZE from ereuse_devicehub.resources.schemas import Thing +from ereuse_devicehub.teal import enums +from ereuse_devicehub.teal.marshmallow import EnumField, Phone, SanitizedStr class Agent(Thing): id = ma_fields.UUID(dump_only=True) name = SanitizedStr(validate=ma_validate.Length(max=STR_SM_SIZE)) - tax_id = SanitizedStr(lower=True, - validate=ma_validate.Length(max=STR_SM_SIZE), - data_key='taxId') + tax_id = SanitizedStr( + lower=True, validate=ma_validate.Length(max=STR_SM_SIZE), data_key='taxId' + ) country = EnumField(enums.Country) telephone = Phone() email = Email() diff --git a/ereuse_devicehub/resources/deliverynote/__init__.py b/ereuse_devicehub/resources/deliverynote/__init__.py index cf49bacd..112bb1e9 100644 --- a/ereuse_devicehub/resources/deliverynote/__init__.py +++ b/ereuse_devicehub/resources/deliverynote/__init__.py @@ -1,9 +1,8 @@ from typing import Callable, Iterable, Tuple -from teal.resource import Converters, Resource - from ereuse_devicehub.resources.deliverynote import schemas from ereuse_devicehub.resources.deliverynote.views import DeliverynoteView +from ereuse_devicehub.teal.resource import Converters, Resource class DeliverynoteDef(Resource): @@ -12,15 +11,28 @@ class DeliverynoteDef(Resource): AUTH = True ID_CONVERTER = Converters.uuid - 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: Iterable[Tuple[Callable, str or None]] = tuple()): - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path, cli_commands) + 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: Iterable[Tuple[Callable, str or None]] = tuple(), + ): + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_commands, + ) diff --git a/ereuse_devicehub/resources/deliverynote/models.py b/ereuse_devicehub/resources/deliverynote/models.py index 57eefe08..6407c2ef 100644 --- a/ereuse_devicehub/resources/deliverynote/models.py +++ b/ereuse_devicehub/resources/deliverynote/models.py @@ -5,35 +5,47 @@ from typing import Iterable from boltons import urlutils from citext import CIText from flask import g -from sqlalchemy.dialects.postgresql import UUID, JSONB -from teal.db import check_range, IntEnum -from teal.resource import url_for_resource +from sqlalchemy.dialects.postgresql import JSONB, UUID from ereuse_devicehub.db import db from ereuse_devicehub.resources.enums import TransferState from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.db import IntEnum, check_range +from ereuse_devicehub.teal.resource import url_for_resource class Deliverynote(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 document_id = db.Column(CIText(), nullable=False) - creator_id = db.Column(UUID(as_uuid=True), - db.ForeignKey(User.id), - nullable=False, - default=lambda: g.user.id) + creator_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id, + ) creator = db.relationship(User, primaryjoin=creator_id == User.id) - supplier_email = db.Column(CIText(), - db.ForeignKey(User.email), - nullable=False, - default=lambda: g.user.email) - supplier = db.relationship(User, primaryjoin=lambda: Deliverynote.supplier_email == User.email) - receiver_address = db.Column(CIText(), - db.ForeignKey(User.email), - nullable=False, - default=lambda: g.user.email) - receiver = db.relationship(User, primaryjoin=lambda: Deliverynote.receiver_address == User.email) + supplier_email = db.Column( + CIText(), + db.ForeignKey(User.email), + nullable=False, + default=lambda: g.user.email, + ) + supplier = db.relationship( + User, primaryjoin=lambda: Deliverynote.supplier_email == User.email + ) + receiver_address = db.Column( + CIText(), + db.ForeignKey(User.email), + nullable=False, + default=lambda: g.user.email, + ) + receiver = db.relationship( + User, primaryjoin=lambda: Deliverynote.receiver_address == User.email + ) date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) date.comment = 'The date the DeliveryNote initiated' amount = db.Column(db.Integer, check_range('amount', min=0, max=100), default=0) @@ -44,27 +56,37 @@ class Deliverynote(Thing): expected_devices = db.Column(JSONB, nullable=False) # expected_devices = db.Column(db.ARRAY(JSONB, dimensions=1), nullable=False) transferred_devices = db.Column(db.ARRAY(db.Integer, dimensions=1), nullable=True) - 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__ - lot_id = db.Column(UUID(as_uuid=True), - db.ForeignKey(Lot.id), - nullable=False) - lot = db.relationship(Lot, - backref=db.backref('deliverynote', uselist=False, lazy=True), - lazy=True, - primaryjoin=Lot.id == lot_id) + lot_id = db.Column(UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False) + lot = db.relationship( + Lot, + backref=db.backref('deliverynote', uselist=False, lazy=True), + lazy=True, + primaryjoin=Lot.id == lot_id, + ) - def __init__(self, document_id: str, amount: str, date, - supplier_email: str, - expected_devices: Iterable, - transfer_state: TransferState) -> None: - """Initializes a delivery note - """ - super().__init__(id=uuid.uuid4(), - document_id=document_id, amount=amount, date=date, - supplier_email=supplier_email, - expected_devices=expected_devices, - transfer_state=transfer_state) + def __init__( + self, + document_id: str, + amount: str, + date, + supplier_email: str, + expected_devices: Iterable, + transfer_state: TransferState, + ) -> None: + """Initializes a delivery note""" + super().__init__( + id=uuid.uuid4(), + document_id=document_id, + amount=amount, + date=date, + supplier_email=supplier_email, + expected_devices=expected_devices, + transfer_state=transfer_state, + ) @property def type(self) -> str: diff --git a/ereuse_devicehub/resources/deliverynote/schemas.py b/ereuse_devicehub/resources/deliverynote/schemas.py index f0dbecdc..30955279 100644 --- a/ereuse_devicehub/resources/deliverynote/schemas.py +++ b/ereuse_devicehub/resources/deliverynote/schemas.py @@ -1,5 +1,4 @@ from marshmallow import fields as f -from teal.marshmallow import SanitizedStr, EnumField from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.deliverynote import models as m @@ -7,20 +6,30 @@ from ereuse_devicehub.resources.enums import TransferState from ereuse_devicehub.resources.models import STR_SIZE from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.user import schemas as s_user +from ereuse_devicehub.teal.marshmallow import EnumField, SanitizedStr class Deliverynote(Thing): id = f.UUID(dump_only=True) - document_id = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), - required=True, data_key='documentID') + document_id = SanitizedStr( + validate=f.validate.Length(max=STR_SIZE), required=True, data_key='documentID' + ) creator = NestedOn(s_user.User, dump_only=True) - supplier_email = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), - load_only=True, required=True, data_key='supplierEmail') + supplier_email = SanitizedStr( + validate=f.validate.Length(max=STR_SIZE), + load_only=True, + required=True, + data_key='supplierEmail', + ) supplier = NestedOn(s_user.User, dump_only=True) receiver = NestedOn(s_user.User, dump_only=True) date = f.DateTime('iso', required=True) - amount = f.Integer(validate=f.validate.Range(min=0, max=100), - description=m.Deliverynote.amount.__doc__) + amount = f.Integer( + validate=f.validate.Range(min=0, max=100), + description=m.Deliverynote.amount.__doc__, + ) expected_devices = f.List(f.Dict, required=True, data_key='expectedDevices') - transferred_devices = f.List(f.Integer(), required=False, data_key='transferredDevices') + transferred_devices = f.List( + f.Integer(), required=False, data_key='transferredDevices' + ) transfer_state = EnumField(TransferState, description=m.Lot.transfer_state.comment) diff --git a/ereuse_devicehub/resources/deliverynote/views.py b/ereuse_devicehub/resources/deliverynote/views.py index 1f21a6a1..9d2a27c0 100644 --- a/ereuse_devicehub/resources/deliverynote/views.py +++ b/ereuse_devicehub/resources/deliverynote/views.py @@ -2,21 +2,22 @@ import datetime import uuid from flask import Response, request -from teal.resource import View from ereuse_devicehub.db import db from ereuse_devicehub.resources.deliverynote.models import Deliverynote from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.teal.resource import View class DeliverynoteView(View): - def post(self): # Create delivery note dn = request.get_json() dlvnote = Deliverynote(**dn) # Create a lot - lot_name = dlvnote.document_id + "_" + datetime.datetime.utcnow().strftime("%Y-%m-%d") + lot_name = ( + dlvnote.document_id + "_" + datetime.datetime.utcnow().strftime("%Y-%m-%d") + ) new_lot = Lot(name=lot_name) dlvnote.lot_id = new_lot.id db.session.add(new_lot) diff --git a/ereuse_devicehub/resources/device/definitions.py b/ereuse_devicehub/resources/device/definitions.py index d5678135..febacf38 100644 --- a/ereuse_devicehub/resources/device/definitions.py +++ b/ereuse_devicehub/resources/device/definitions.py @@ -1,7 +1,5 @@ from typing import Callable, Iterable, Tuple -from teal.resource import Converters, Resource - from ereuse_devicehub.resources.device import schemas from ereuse_devicehub.resources.device.models import Manufacturer from ereuse_devicehub.resources.device.views import ( @@ -9,6 +7,7 @@ from ereuse_devicehub.resources.device.views import ( DeviceView, ManufacturerView, ) +from ereuse_devicehub.teal.resource import Converters, Resource class DeviceDef(Resource): diff --git a/ereuse_devicehub/resources/device/exceptions.py b/ereuse_devicehub/resources/device/exceptions.py index 0b98c381..f40c569d 100644 --- a/ereuse_devicehub/resources/device/exceptions.py +++ b/ereuse_devicehub/resources/device/exceptions.py @@ -1,10 +1,11 @@ -from teal.marshmallow import ValidationError +from ereuse_devicehub.teal.marshmallow import ValidationError class MismatchBetweenIds(ValidationError): def __init__(self, other_device_id: int, field: str, value: str): - message = 'The device {} has the same {} than this one ({}).'.format(other_device_id, - field, value) + message = 'The device {} has the same {} than this one ({}).'.format( + other_device_id, field, value + ) super().__init__(message, field_names=[field]) @@ -15,13 +16,15 @@ class NeedsId(ValidationError): class DeviceIsInAnotherDevicehub(ValidationError): - def __init__(self, - tag_id, - message=None, - field_names=None, - fields=None, - data=None, - valid_data=None, - **kwargs): + def __init__( + self, + tag_id, + message=None, + field_names=None, + fields=None, + data=None, + valid_data=None, + **kwargs, + ): message = message or 'Device {} is from another Devicehub.'.format(tag_id) super().__init__(message, field_names, fields, data, valid_data, **kwargs) diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index a59546dc..f1bd47a5 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -1,6 +1,7 @@ import copy import hashlib import json +import logging import os import pathlib import time @@ -13,7 +14,6 @@ from typing import Dict, List, Set from boltons import urlutils from citext import CIText -from ereuse_utils.naming import HID_CONVERSION_DOC from ereuseapi.methods import API from flask import current_app as app from flask import g, request, session @@ -37,21 +37,9 @@ from sqlalchemy.orm import ColumnProperty, backref, relationship, validates from sqlalchemy.util import OrderedSet from sqlalchemy_utils import ColorType from stdnum import imei, meid -from teal.db import ( - CASCADE_DEL, - POLYMORPHIC_ID, - POLYMORPHIC_ON, - URL, - IntEnum, - ResourceNotFound, - check_lower, - check_range, -) -from teal.enums import Layouts -from teal.marshmallow import ValidationError -from teal.resource import url_for_resource from ereuse_devicehub.db import db +from ereuse_devicehub.ereuse_utils.naming import HID_CONVERSION_DOC from ereuse_devicehub.resources.device.metrics import Metrics from ereuse_devicehub.resources.enums import ( BatteryTechnology, @@ -72,6 +60,21 @@ from ereuse_devicehub.resources.models import ( ) from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.utils import hashcode +from ereuse_devicehub.teal.db import ( + CASCADE_DEL, + POLYMORPHIC_ID, + POLYMORPHIC_ON, + URL, + IntEnum, + ResourceNotFound, + check_lower, + check_range, +) +from ereuse_devicehub.teal.enums import Layouts +from ereuse_devicehub.teal.marshmallow import ValidationError +from ereuse_devicehub.teal.resource import url_for_resource + +logger = logging.getLogger(__name__) def create_code(context): @@ -750,6 +753,28 @@ class Device(Thing): return '' + def get_lots_from_type(self, lot_type): + lots_type = { + 'temporary': lambda x: x.is_temporary, + 'incoming': lambda x: x.is_incoming, + 'outgoing': lambda x: x.is_outgoing, + } + + if lot_type not in lots_type: + return '' + + get_lots_type = lots_type[lot_type] + + lots = self.lots + if not lots and self.binding: + lots = self.binding.device.lots + + if lots: + lots = [lot.name for lot in lots if get_lots_type(lot)] + return ", ".join(sorted(lots)) + + return '' + def is_status(self, action): from ereuse_devicehub.resources.device import states @@ -785,7 +810,7 @@ class Device(Thing): def get_from_db(self): if 'property_hid' in app.blueprints.keys(): try: - from modules.device.utils import get_from_db + from ereuse_devicehub.modules.device.utils import get_from_db return get_from_db(self) except Exception: @@ -804,13 +829,13 @@ class Device(Thing): def set_hid(self): if 'property_hid' in app.blueprints.keys(): try: - from modules.device.utils import set_hid + from ereuse_devicehub.modules.device.utils import set_hid self.hid = set_hid(self) self.set_chid() return - except Exception: - pass + except Exception as err: + logger.error(err) self.hid = "{}-{}-{}-{}".format( self._clean_string(self.type), @@ -1251,6 +1276,13 @@ class Placeholder(Thing): return 'Twin' return 'Placeholder' + @property + def documents(self): + docs = self.device.documents + if self.binding: + return docs.union(self.binding.documents) + return docs + class Computer(Device): """A chassis with components inside that can be processed diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 76a1fee2..527c7328 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -17,9 +17,6 @@ from marshmallow.fields import ( from marshmallow.validate import Length, OneOf, Range from sqlalchemy.util import OrderedSet from stdnum import imei, meid -from teal.enums import Layouts -from teal.marshmallow import URL, EnumField, SanitizedStr, ValidationError -from teal.resource import Schema from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources import enums @@ -27,6 +24,14 @@ from ereuse_devicehub.resources.device import models as m from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.schemas import Thing, UnitCodes +from ereuse_devicehub.teal.enums import Layouts +from ereuse_devicehub.teal.marshmallow import ( + URL, + EnumField, + SanitizedStr, + ValidationError, +) +from ereuse_devicehub.teal.resource import Schema class Device(Thing): diff --git a/ereuse_devicehub/resources/device/sync.py b/ereuse_devicehub/resources/device/sync.py index 32c4a2f2..38b5793d 100644 --- a/ereuse_devicehub/resources/device/sync.py +++ b/ereuse_devicehub/resources/device/sync.py @@ -8,8 +8,6 @@ from flask import g from sqlalchemy import inspect from sqlalchemy.exc import IntegrityError from sqlalchemy.util import OrderedSet -from teal.db import ResourceNotFound -from teal.marshmallow import ValidationError from ereuse_devicehub.db import db from ereuse_devicehub.resources.action.models import Remove @@ -21,6 +19,8 @@ from ereuse_devicehub.resources.device.models import ( Placeholder, ) from ereuse_devicehub.resources.tag.model import Tag +from ereuse_devicehub.teal.db import ResourceNotFound +from ereuse_devicehub.teal.marshmallow import ValidationError # DEVICES_ALLOW_DUPLICITY = [ # 'RamModule', diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index adac9b4b..8b467051 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -12,10 +12,6 @@ from marshmallow import fields from marshmallow import fields as f from marshmallow import validate as v from sqlalchemy.util import OrderedSet -from teal import query -from teal.cache import cache -from teal.marshmallow import ValidationError -from teal.resource import View from ereuse_devicehub import auth from ereuse_devicehub.db import db @@ -28,6 +24,11 @@ from ereuse_devicehub.resources.device.models import Computer, Device, Manufactu from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.lot.models import LotDeviceDescendants from ereuse_devicehub.resources.tag.model import Tag +from ereuse_devicehub.teal import query +from ereuse_devicehub.teal.cache import cache +from ereuse_devicehub.teal.db import ResourceNotFound +from ereuse_devicehub.teal.marshmallow import ValidationError +from ereuse_devicehub.teal.resource import View class OfType(f.Str): diff --git a/ereuse_devicehub/resources/documents/device_row.py b/ereuse_devicehub/resources/documents/device_row.py index d4efda46..ba165e0b 100644 --- a/ereuse_devicehub/resources/documents/device_row.py +++ b/ereuse_devicehub/resources/documents/device_row.py @@ -37,8 +37,12 @@ class BaseDeviceRow(OrderedDict): self['PHID'] = '' self['DHID'] = '' self['Type'] = '' - self['Placeholder Palet'] = '' + self['Temporary Lots'] = '' + self['Incoming Lots'] = '' + self['Outgoing Lots'] = '' + self['Placeholder Pallet'] = '' self['Placeholder Id Supplier'] = '' + self['Placeholder Id Internal'] = '' self['Placeholder Info'] = '' self['Placeholder Components'] = '' self['Placeholder Type'] = '' @@ -263,7 +267,7 @@ class BaseDeviceRow(OrderedDict): class DeviceRow(BaseDeviceRow): - def __init__(self, device: d.Device, document_ids: dict) -> None: + def __init__(self, device: d.Device, document_ids: dict) -> None: # noqa: C901 super().__init__() self.placeholder = device.binding or device.placeholder self.device = self.placeholder.binding or self.placeholder.device @@ -504,8 +508,12 @@ class DeviceRow(BaseDeviceRow): # Placeholder self['PHID'] = none2str(self.placeholder.phid) self['Type'] = none2str(self.device.is_abstract()) - self['Placeholder Palet'] = none2str(self.placeholder.pallet) + self['Temporary Lots'] = none2str(self.device.get_lots_from_type('temporary')) + self['Incoming Lots'] = none2str(self.device.get_lots_from_type('incoming')) + self['Outgoing Lots'] = none2str(self.device.get_lots_from_type('outgoing')) + self['Placeholder Pallet'] = none2str(self.placeholder.pallet) self['Placeholder Id Supplier'] = none2str(self.placeholder.id_device_supplier) + self['Placeholder Id Internal'] = none2str(self.placeholder.id_device_internal) self['Placeholder Info'] = none2str(self.placeholder.info) self['Placeholder Components'] = none2str(self.placeholder.components) self['Placeholder Type'] = none2str(self.placeholder.device.type) diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index d33fd238..78a69314 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -11,14 +11,12 @@ from typing import Callable, Iterable, Tuple import boltons import flask import flask_weasyprint -import teal.marshmallow from boltons import urlutils from flask import current_app as app from flask import g, make_response, request from flask.json import jsonify -from teal.cache import cache -from teal.resource import Resource, View +import ereuse_devicehub.teal.marshmallow from ereuse_devicehub import auth from ereuse_devicehub.db import db from ereuse_devicehub.resources.action import models as evs @@ -37,6 +35,8 @@ from ereuse_devicehub.resources.hash_reports import ReportHash, insert_hash, ver from ereuse_devicehub.resources.lot import LotView from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.user.models import Session +from ereuse_devicehub.teal.cache import cache +from ereuse_devicehub.teal.resource import Resource, View class Format(enum.Enum): @@ -46,7 +46,7 @@ class Format(enum.Enum): class DocumentView(DeviceView): class FindArgs(DeviceView.FindArgs): - format = teal.marshmallow.EnumField(Format, missing=None) + format = ereuse_devicehub.teal.marshmallow.EnumField(Format, missing=None) def get(self, id): """Get a collection of resources or a specific one. @@ -71,7 +71,7 @@ class DocumentView(DeviceView): if not ids and not id: msg = 'Document must be an ID or UUID.' - raise teal.marshmallow.ValidationError(msg) + raise ereuse_devicehub.teal.marshmallow.ValidationError(msg) if id: try: @@ -81,7 +81,7 @@ class DocumentView(DeviceView): ids.append(int(id)) except ValueError: msg = 'Document must be an ID or UUID.' - raise teal.marshmallow.ValidationError(msg) + raise ereuse_devicehub.teal.marshmallow.ValidationError(msg) else: query = devs.Device.query.filter(Device.id.in_(ids)) else: @@ -98,7 +98,7 @@ class DocumentView(DeviceView): # try: # id = int(id) # except ValueError: - # raise teal.marshmallow.ValidationError('Document must be an ID or UUID.') + # raise ereuse_devicehub.teal.marshmallow.ValidationError('Document must be an ID or UUID.') # else: # query = devs.Device.query.filter_by(id=id) # else: @@ -138,7 +138,7 @@ class DocumentView(DeviceView): url_pdf = boltons.urlutils.URL(flask.request.url) url_pdf.query_params['format'] = 'PDF' params = { - 'title': 'Erasure Certificate', + 'title': 'Device Sanitization', 'erasures': tuple(erasures()), 'url_pdf': url_pdf.to_text(), } @@ -280,7 +280,7 @@ class LotRow(OrderedDict): self['Registered in'] = format(lot.created, '%c') try: self['Description'] = lot.description - except: + except Exception: self['Description'] = '' diff --git a/ereuse_devicehub/resources/documents/models.py b/ereuse_devicehub/resources/documents/models.py index 9c557b3e..d3ae78d3 100644 --- a/ereuse_devicehub/resources/documents/models.py +++ b/ereuse_devicehub/resources/documents/models.py @@ -1,20 +1,19 @@ +from citext import CIText from flask import g -from citext import CIText from sortedcontainers import SortedSet -from sqlalchemy import BigInteger, Column, Sequence, Unicode, Boolean, ForeignKey -from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy import BigInteger, Boolean, Column, ForeignKey, Sequence, Unicode from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import backref -from teal.db import CASCADE_OWN, URL from ereuse_devicehub.db import db +from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing from ereuse_devicehub.resources.user.models import User -from ereuse_devicehub.resources.models import Thing, STR_SM_SIZE - +from ereuse_devicehub.teal.db import CASCADE_OWN, URL _sorted_documents = { 'order_by': lambda: Document.created, - 'collection_class': SortedSet + 'collection_class': SortedSet, } @@ -30,11 +29,15 @@ class Document(Thing): date.comment = """The date of document, some documents need to have one date """ id_document = Column(CIText(), nullable=True) - id_document.comment = """The id of one document like invoice so they can be linked.""" - owner_id = db.Column(UUID(as_uuid=True), - db.ForeignKey(User.id), - nullable=False, - default=lambda: g.user.id) + id_document.comment = ( + """The id of one document like invoice so they can be linked.""" + ) + owner_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id, + ) owner = db.relationship(User, primaryjoin=owner_id == User.id) file_name = Column(db.CIText(), nullable=False) file_name.comment = """This is the name of the file when user up the document.""" diff --git a/ereuse_devicehub/resources/documents/schemas.py b/ereuse_devicehub/resources/documents/schemas.py index d62cec41..e16811db 100644 --- a/ereuse_devicehub/resources/documents/schemas.py +++ b/ereuse_devicehub/resources/documents/schemas.py @@ -1,34 +1,43 @@ -from marshmallow.fields import DateTime, Integer, validate, Boolean, Float from marshmallow import post_load +from marshmallow.fields import Boolean, DateTime, Float, Integer, validate from marshmallow.validate import Range -from teal.marshmallow import SanitizedStr, URL + from ereuse_devicehub.marshmallow import NestedOn +from ereuse_devicehub.resources.documents import models as m from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.tradedocument.models import TradeDocument -from ereuse_devicehub.resources.documents import models as m +from ereuse_devicehub.teal.marshmallow import URL, SanitizedStr class DataWipeDocument(Thing): __doc__ = m.DataWipeDocument.__doc__ id = Integer(description=m.DataWipeDocument.id.comment, dump_only=True) - url = URL(required= False, description=m.DataWipeDocument.url.comment) - success = Boolean(required=False, default=False, description=m.DataWipeDocument.success.comment) + url = URL(required=False, description=m.DataWipeDocument.url.comment) + success = Boolean( + required=False, default=False, description=m.DataWipeDocument.success.comment + ) software = SanitizedStr(description=m.DataWipeDocument.software.comment) - date = DateTime(data_key='endTime', - required=False, - description=m.DataWipeDocument.date.comment) - id_document = SanitizedStr(data_key='documentId', - required=False, - default='', - description=m.DataWipeDocument.id_document.comment) - file_name = SanitizedStr(data_key='filename', - default='', - description=m.DataWipeDocument.file_name.comment, - validate=validate.Length(max=100)) - file_hash = SanitizedStr(data_key='hash', - default='', - description=m.DataWipeDocument.file_hash.comment, - validate=validate.Length(max=64)) + date = DateTime( + data_key='endTime', required=False, description=m.DataWipeDocument.date.comment + ) + id_document = SanitizedStr( + data_key='documentId', + required=False, + default='', + description=m.DataWipeDocument.id_document.comment, + ) + file_name = SanitizedStr( + data_key='filename', + default='', + description=m.DataWipeDocument.file_name.comment, + validate=validate.Length(max=100), + ) + file_hash = SanitizedStr( + data_key='hash', + default='', + description=m.DataWipeDocument.file_hash.comment, + validate=validate.Length(max=64), + ) @post_load def get_trade_document(self, data): diff --git a/ereuse_devicehub/resources/image/models.py b/ereuse_devicehub/resources/image/models.py index 06f45bad..522ec1b2 100644 --- a/ereuse_devicehub/resources/image/models.py +++ b/ereuse_devicehub/resources/image/models.py @@ -1,28 +1,34 @@ from uuid import uuid4 from citext import CIText -from sqlalchemy import BigInteger, Column, Enum as DBEnum, ForeignKey +from sqlalchemy import BigInteger, Column +from sqlalchemy import Enum as DBEnum +from sqlalchemy import ForeignKey from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import backref, relationship from sqlalchemy.util import OrderedSet -from teal.db import CASCADE_OWN from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.enums import ImageMimeTypes, Orientation from ereuse_devicehub.resources.models import Thing +from ereuse_devicehub.teal.db import CASCADE_OWN class ImageList(Thing): id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid4) device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False) - device = relationship(Device, - primaryjoin=Device.id == device_id, - backref=backref('images', - lazy=True, - cascade=CASCADE_OWN, - order_by=lambda: ImageList.created, - collection_class=OrderedSet)) + device = relationship( + Device, + primaryjoin=Device.id == device_id, + backref=backref( + 'images', + lazy=True, + cascade=CASCADE_OWN, + order_by=lambda: ImageList.created, + collection_class=OrderedSet, + ), + ) class Image(Thing): @@ -32,12 +38,16 @@ class Image(Thing): file_format = db.Column(DBEnum(ImageMimeTypes), nullable=False) orientation = db.Column(DBEnum(Orientation), nullable=False) image_list_id = Column(UUID(as_uuid=True), ForeignKey(ImageList.id), nullable=False) - image_list = relationship(ImageList, - primaryjoin=ImageList.id == image_list_id, - backref=backref('images', - cascade=CASCADE_OWN, - order_by=lambda: Image.created, - collection_class=OrderedSet)) + image_list = relationship( + ImageList, + primaryjoin=ImageList.id == image_list_id, + backref=backref( + 'images', + cascade=CASCADE_OWN, + order_by=lambda: Image.created, + collection_class=OrderedSet, + ), + ) # todo make an image Field that converts to/from image object # todo which metadata we get from Photobox? diff --git a/ereuse_devicehub/resources/inventory/__init__.py b/ereuse_devicehub/resources/inventory/__init__.py index 8f57cada..201c38cb 100644 --- a/ereuse_devicehub/resources/inventory/__init__.py +++ b/ereuse_devicehub/resources/inventory/__init__.py @@ -2,42 +2,61 @@ import uuid import boltons.urlutils 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 +from ereuse_devicehub.teal.db import ResourceNotFound +from ereuse_devicehub.teal.resource import Resource 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): - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path) + 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, + ): + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + ) @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): + 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) + 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: @@ -54,12 +73,14 @@ class InventoryDef(Resource): only access to this inventory. """ from ereuse_devicehub.resources.user.models import User, UserInventory + inv = Inventory.query.filter_by(id=current_app.id).one() db.session.delete(inv) db.session.flush() # Remove users that end-up without any inventory # todo this should be done in a trigger / action - users = User.query \ - .filter(User.id.notin_(db.session.query(UserInventory.user_id).distinct())) + users = User.query.filter( + User.id.notin_(db.session.query(UserInventory.user_id).distinct()) + ) for user in users: db.session.delete(user) diff --git a/ereuse_devicehub/resources/inventory/schema.py b/ereuse_devicehub/resources/inventory/schema.py index 57b157d5..d1579ef0 100644 --- a/ereuse_devicehub/resources/inventory/schema.py +++ b/ereuse_devicehub/resources/inventory/schema.py @@ -1,4 +1,4 @@ -import teal.marshmallow +import ereuse_devicehub.teal.marshmallow from marshmallow import fields as mf from ereuse_devicehub.resources.schemas import Thing @@ -7,4 +7,6 @@ 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_provider = ereuse_devicehub.teal.marshmallow.URL( + dump_only=True, data_key='tagProvider' + ) diff --git a/ereuse_devicehub/resources/licences/licences.py b/ereuse_devicehub/resources/licences/licences.py index f664fe4e..e39ff331 100644 --- a/ereuse_devicehub/resources/licences/licences.py +++ b/ereuse_devicehub/resources/licences/licences.py @@ -1,6 +1,8 @@ from typing import Callable, Iterable, Tuple + from flask.json import jsonify -from teal.resource import Resource, View + +from ereuse_devicehub.teal.resource import Resource, View class LicenceView(View): @@ -23,18 +25,31 @@ class LicencesDef(Resource): VIEW = None # We do not want to create default / documents endpoint AUTH = False - def __init__(self, app, - import_name=__name__, - static_folder=None, - static_url_path=None, - template_folder=None, - url_prefix=None, - subdomain=None, - url_defaults=None, - root_path=None, - cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()): - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path, cli_commands) + def __init__( + self, + app, + import_name=__name__, + static_folder=None, + static_url_path=None, + template_folder=None, + url_prefix=None, + subdomain=None, + url_defaults=None, + root_path=None, + cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(), + ): + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_commands, + ) get = {'GET'} d = {} diff --git a/ereuse_devicehub/resources/lot/__init__.py b/ereuse_devicehub/resources/lot/__init__.py index d76bc54b..b78761cf 100644 --- a/ereuse_devicehub/resources/lot/__init__.py +++ b/ereuse_devicehub/resources/lot/__init__.py @@ -1,12 +1,15 @@ import pathlib from typing import Callable, Iterable, Tuple -from teal.resource import Converters, Resource - from ereuse_devicehub.db import db from ereuse_devicehub.resources.lot import schemas -from ereuse_devicehub.resources.lot.views import LotBaseChildrenView, LotChildrenView, \ - LotDeviceView, LotView +from ereuse_devicehub.resources.lot.views import ( + LotBaseChildrenView, + LotChildrenView, + LotDeviceView, + LotView, +) +from ereuse_devicehub.teal.resource import Converters, Resource class LotDef(Resource): @@ -15,24 +18,49 @@ class LotDef(Resource): AUTH = True ID_CONVERTER = Converters.uuid - 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: Iterable[Tuple[Callable, str or None]] = tuple()): - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path, cli_commands) - lot_children = LotChildrenView.as_view('lot-children', definition=self, auth=app.auth) + 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: Iterable[Tuple[Callable, str or None]] = tuple(), + ): + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_commands, + ) + lot_children = LotChildrenView.as_view( + 'lot-children', definition=self, auth=app.auth + ) if self.AUTH: lot_children = app.auth.requires_auth(lot_children) - self.add_url_rule('/<{}:{}>/children'.format(self.ID_CONVERTER.value, self.ID_NAME), - view_func=lot_children, - methods={'POST', 'DELETE'}) + self.add_url_rule( + '/<{}:{}>/children'.format(self.ID_CONVERTER.value, self.ID_NAME), + view_func=lot_children, + methods={'POST', 'DELETE'}, + ) lot_device = LotDeviceView.as_view('lot-device', definition=self, auth=app.auth) if self.AUTH: lot_device = app.auth.requires_auth(lot_device) - self.add_url_rule('/<{}:{}>/devices'.format(self.ID_CONVERTER.value, self.ID_NAME), - view_func=lot_device, - methods={'POST', 'DELETE'}) + self.add_url_rule( + '/<{}:{}>/devices'.format(self.ID_CONVERTER.value, self.ID_NAME), + view_func=lot_device, + methods={'POST', 'DELETE'}, + ) def init_db(self, db: 'db.SQLAlchemy', exclude_schema=None): # Create functions diff --git a/ereuse_devicehub/resources/lot/models.py b/ereuse_devicehub/resources/lot/models.py index 3cb62adb..bce24336 100644 --- a/ereuse_devicehub/resources/lot/models.py +++ b/ereuse_devicehub/resources/lot/models.py @@ -10,14 +10,14 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship from sqlalchemy_utils import LtreeType from sqlalchemy_utils.types.ltree import LQUERY -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 from ereuse_devicehub.resources.device.models import Component, Device from ereuse_devicehub.resources.enums import TransferState from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.db import CASCADE_OWN, IntEnum, UUIDLtree, check_range +from ereuse_devicehub.teal.resource import url_for_resource class Lot(Thing): @@ -125,7 +125,10 @@ class Lot(Thing): @property def is_temporary(self): - return not bool(self.trade) and not bool(self.transfer) + trade = bool(self.trade) + transfer = bool(self.transfer) + owner = self.owner == g.user + return not trade and not transfer and owner @property def is_incoming(self): @@ -145,6 +148,19 @@ class Lot(Thing): return False + @property + def is_shared(self): + try: + self.shared + except Exception: + self.shared = ShareLot.query.filter_by( + lot_id=self.id, user_to=g.user + ).first() + + if self.shared: + return True + return False + @classmethod def descendantsq(cls, id): _id = UUIDLtree.convert(id) @@ -397,3 +413,15 @@ class LotParent(db.Model): .select_from(Path) .where(i > 0), ) + + +class ShareLot(Thing): + id = db.Column(UUID(as_uuid=True), primary_key=True) + lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False) + lot = db.relationship(Lot, primaryjoin=lot_id == Lot.id) + user_to_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=True, + ) + user_to = db.relationship(User, primaryjoin=user_to_id == User.id) diff --git a/ereuse_devicehub/resources/lot/schemas.py b/ereuse_devicehub/resources/lot/schemas.py index 9a5a58aa..623f4754 100644 --- a/ereuse_devicehub/resources/lot/schemas.py +++ b/ereuse_devicehub/resources/lot/schemas.py @@ -1,15 +1,14 @@ from marshmallow import fields as f -from teal.marshmallow import SanitizedStr, URL, EnumField from ereuse_devicehub.marshmallow import NestedOn +from ereuse_devicehub.resources.action import schemas as s_action from ereuse_devicehub.resources.deliverynote import schemas as s_deliverynote from ereuse_devicehub.resources.device import schemas as s_device -from ereuse_devicehub.resources.action import schemas as s_action from ereuse_devicehub.resources.enums import TransferState from ereuse_devicehub.resources.lot import models as m from ereuse_devicehub.resources.models import STR_SIZE from ereuse_devicehub.resources.schemas import Thing - +from ereuse_devicehub.teal.marshmallow import URL, EnumField, SanitizedStr TRADE_VALUES = ( 'id', @@ -18,16 +17,11 @@ TRADE_VALUES = ( 'user_from.id', 'user_to.id', 'user_to.code', - 'user_from.code' + 'user_from.code', ) -DOCUMENTS_VALUES = ( - 'id', - 'file_name', - 'total_weight', - 'trading' -) +DOCUMENTS_VALUES = ('id', 'file_name', 'total_weight', 'trading') class Old_Lot(Thing): @@ -39,8 +33,9 @@ class Old_Lot(Thing): children = NestedOn('Lot', many=True, dump_only=True) parents = NestedOn('Lot', many=True, dump_only=True) url = URL(dump_only=True, description=m.Lot.url.__doc__) - amount = f.Integer(validate=f.validate.Range(min=0, max=100), - description=m.Lot.amount.__doc__) + amount = f.Integer( + validate=f.validate.Range(min=0, max=100), description=m.Lot.amount.__doc__ + ) # author_id = NestedOn(s_user.User,only_query='author_id') owner_id = f.UUID(data_key='ownerID') transfer_state = EnumField(TransferState, description=m.Lot.transfer_state.comment) @@ -54,4 +49,6 @@ class Lot(Thing): name = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), required=True) description = SanitizedStr(description=m.Lot.description.comment) trade = f.Nested(s_action.Trade, dump_only=True, only=TRADE_VALUES) - documents = f.Nested('TradeDocument', many=True, dump_only=True, only=DOCUMENTS_VALUES) + documents = f.Nested( + 'TradeDocument', many=True, dump_only=True, only=DOCUMENTS_VALUES + ) diff --git a/ereuse_devicehub/resources/lot/views.py b/ereuse_devicehub/resources/lot/views.py index 80922e47..6fd40282 100644 --- a/ereuse_devicehub/resources/lot/views.py +++ b/ereuse_devicehub/resources/lot/views.py @@ -9,8 +9,6 @@ from marshmallow import Schema as MarshmallowSchema from marshmallow import fields as f from sqlalchemy import or_ from sqlalchemy.util import OrderedSet -from teal.marshmallow import EnumField -from teal.resource import View from ereuse_devicehub.db import db from ereuse_devicehub.inventory.models import Transfer @@ -18,6 +16,8 @@ from ereuse_devicehub.query import things_response from ereuse_devicehub.resources.action.models import Confirm, Revoke, Trade from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device from ereuse_devicehub.resources.lot.models import Lot, Path +from ereuse_devicehub.teal.marshmallow import EnumField +from ereuse_devicehub.teal.resource import View class LotFormat(Enum): @@ -79,7 +79,7 @@ class LotView(View): lot = Lot.query.filter_by(id=id).one() # type: Lot return self.schema.jsonify(lot, nested=2) - # @teal.cache.cache(datetime.timedelta(minutes=5)) + # @ereuse_devicehub.teal.cache.cache(datetime.timedelta(minutes=5)) def find(self, args: dict): """Gets lots. diff --git a/ereuse_devicehub/resources/metric/definitions.py b/ereuse_devicehub/resources/metric/definitions.py index 4c90c77f..56f2dbdb 100644 --- a/ereuse_devicehub/resources/metric/definitions.py +++ b/ereuse_devicehub/resources/metric/definitions.py @@ -1,6 +1,6 @@ -from teal.resource import Resource from ereuse_devicehub.resources.metric.schema import Metric from ereuse_devicehub.resources.metric.views import MetricsView +from ereuse_devicehub.teal.resource import Resource class MetricDef(Resource): diff --git a/ereuse_devicehub/resources/metric/schema.py b/ereuse_devicehub/resources/metric/schema.py index 8bee7c81..70875ee9 100644 --- a/ereuse_devicehub/resources/metric/schema.py +++ b/ereuse_devicehub/resources/metric/schema.py @@ -1,11 +1,18 @@ -from teal.resource import Schema from marshmallow.fields import DateTime +from ereuse_devicehub.teal.resource import Schema + + class Metric(Schema): """ This schema filter dates for search the metrics """ - start_time = DateTime(data_key='start_time', required=True, - description="Start date for search metrics") - end_time = DateTime(data_key='end_time', required=True, - description="End date for search metrics") + + start_time = DateTime( + data_key='start_time', + required=True, + description="Start date for search metrics", + ) + end_time = DateTime( + data_key='end_time', required=True, description="End date for search metrics" + ) diff --git a/ereuse_devicehub/resources/metric/views.py b/ereuse_devicehub/resources/metric/views.py index 561150da..9c47964f 100644 --- a/ereuse_devicehub/resources/metric/views.py +++ b/ereuse_devicehub/resources/metric/views.py @@ -1,31 +1,38 @@ -from flask import request, g, jsonify from contextlib import suppress -from teal.resource import View + +from flask import g, jsonify, request from ereuse_devicehub.resources.action import schemas -from ereuse_devicehub.resources.action.models import Allocate, Live, Action, ToRepair, ToPrepare +from ereuse_devicehub.resources.action.models import ( + Action, + Allocate, + Live, + ToPrepare, + ToRepair, +) from ereuse_devicehub.resources.device import models as m from ereuse_devicehub.resources.metric.schema import Metric +from ereuse_devicehub.teal.resource import View class MetricsView(View): def find(self, args: dict): metrics = { - "allocateds": self.allocated(), - "live": self.live(), + "allocateds": self.allocated(), + "live": self.live(), } return jsonify(metrics) def allocated(self): # TODO @cayop we need uncomment when the pr/83 is approved # return m.Device.query.filter(m.Device.allocated==True, owner==g.user).count() - return m.Device.query.filter(m.Device.allocated==True).count() + return m.Device.query.filter(m.Device.allocated == True).count() def live(self): # TODO @cayop we need uncomment when the pr/83 is approved # devices = m.Device.query.filter(m.Device.allocated==True, owner==g.user) - devices = m.Device.query.filter(m.Device.allocated==True) + devices = m.Device.query.filter(m.Device.allocated == True) count = 0 for dev in devices: live = allocate = None @@ -41,4 +48,3 @@ class MetricsView(View): count += 1 return count - diff --git a/ereuse_devicehub/resources/models.py b/ereuse_devicehub/resources/models.py index e079269f..485fc96e 100644 --- a/ereuse_devicehub/resources/models.py +++ b/ereuse_devicehub/resources/models.py @@ -1,4 +1,5 @@ from datetime import datetime, timezone + from flask_sqlalchemy import event from ereuse_devicehub.db import db @@ -16,18 +17,23 @@ class Thing(db.Model): `schema.org's Thing class `_ using only needed fields. """ + __abstract__ = True - updated = db.Column(db.TIMESTAMP(timezone=True), - nullable=False, - index=True, - server_default=db.text('CURRENT_TIMESTAMP')) - updated.comment = """The last time Devicehub recorded a change for + updated = db.Column( + db.TIMESTAMP(timezone=True), + nullable=False, + index=True, + server_default=db.text('CURRENT_TIMESTAMP'), + ) + updated.comment = """The last time Devicehub recorded a change for this thing. """ - created = db.Column(db.TIMESTAMP(timezone=True), - nullable=False, - index=True, - server_default=db.text('CURRENT_TIMESTAMP')) + created = db.Column( + db.TIMESTAMP(timezone=True), + nullable=False, + index=True, + server_default=db.text('CURRENT_TIMESTAMP'), + ) created.comment = """When Devicehub created this.""" def __init__(self, **kwargs) -> None: @@ -36,11 +42,15 @@ class Thing(db.Model): self.created = kwargs.get('created', datetime.now(timezone.utc)) super().__init__(**kwargs) + def delete(self): + db.session.delete(self) + def update_object_timestamp(mapper, connection, thing_obj): - """ This function update the stamptime of field updated """ + """This function update the stamptime of field updated""" thing_obj.updated = datetime.now(timezone.utc) + def listener_reset_field_updated_in_actual_time(thing_obj): - """ This function launch a event than listen like a signal when some object is saved """ + """This function launch a event than listen like a signal when some object is saved""" event.listen(thing_obj, 'before_update', update_object_timestamp, propagate=True) diff --git a/ereuse_devicehub/resources/schemas.py b/ereuse_devicehub/resources/schemas.py index 0c632578..30ef7b24 100644 --- a/ereuse_devicehub/resources/schemas.py +++ b/ereuse_devicehub/resources/schemas.py @@ -4,10 +4,10 @@ from typing import Any from marshmallow import post_load from marshmallow.fields import DateTime, List, String from marshmallow.schema import SchemaMeta -from teal.marshmallow import URL -from teal.resource import Schema from ereuse_devicehub.resources import models as m +from ereuse_devicehub.teal.marshmallow import URL +from ereuse_devicehub.teal.resource import Schema class UnitCodes(Enum): @@ -38,8 +38,8 @@ class UnitCodes(Enum): # Then the directive in our docs/config.py file reads these variables # generating the documentation. -class Meta(type): +class Meta(type): def __new__(cls, *args, **kw) -> Any: base_name = args[1][0].__name__ y = super().__new__(cls, *args, **kw) @@ -47,7 +47,7 @@ class Meta(type): return y -SchemaMeta.__bases__ = Meta, +SchemaMeta.__bases__ = (Meta,) @classmethod @@ -70,9 +70,7 @@ value. class Thing(Schema): type = String(description=_type_description) - same_as = List(URL(dump_only=True), - dump_only=True, - data_key='sameAs') + same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs') updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment) created = DateTime('iso', dump_only=True, description=m.Thing.created.comment) diff --git a/ereuse_devicehub/resources/tag/__init__.py b/ereuse_devicehub/resources/tag/__init__.py index 2ad3eaa6..85ec4084 100644 --- a/ereuse_devicehub/resources/tag/__init__.py +++ b/ereuse_devicehub/resources/tag/__init__.py @@ -2,15 +2,19 @@ import csv import pathlib from click import argument, option -from ereuse_utils import cli -from teal.resource import Converters, Resource -from teal.teal import Teal +from ereuse_devicehub.ereuse_utils import cli from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.definitions import DeviceDef from ereuse_devicehub.resources.tag import schema from ereuse_devicehub.resources.tag.model import Tag -from ereuse_devicehub.resources.tag.view import TagDeviceView, TagView, get_device_from_tag +from ereuse_devicehub.resources.tag.view import ( + TagDeviceView, + TagView, + get_device_from_tag, +) +from ereuse_devicehub.teal.resource import Converters, Resource +from ereuse_devicehub.teal.teal import Teal class TagDef(Resource): @@ -25,48 +29,77 @@ class TagDef(Resource): 'By default set to the actual Devicehub.' CLI_SCHEMA = schema.Tag(only=('id', 'provider', 'org', 'secondary')) - def __init__(self, app: Teal, 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.create_tag, 'add'), - (self.create_tags_csv, 'add-csv') + def __init__( + self, + app: Teal, + 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.create_tag, 'add'), (self.create_tags_csv, 'add-csv')) + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_commands, ) - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path, cli_commands) # DeviceTagView URLs - device_view = TagDeviceView.as_view('tag-device-view', definition=self, auth=app.auth) + device_view = TagDeviceView.as_view( + 'tag-device-view', definition=self, auth=app.auth + ) if self.AUTH: device_view = app.auth.requires_auth(device_view) - self.add_url_rule('/<{0.ID_CONVERTER.value}:{0.ID_NAME}>/device'.format(self), - view_func=device_view, - methods={'GET'}) - self.add_url_rule('/<{0.ID_CONVERTER.value}:tag_id>/'.format(self) + - 'device/<{0.ID_CONVERTER.value}:device_id>'.format(DeviceDef), - view_func=device_view, - methods={'PUT'}) - self.add_url_rule('/<{0.ID_CONVERTER.value}:tag_id>/'.format(self) + - 'device/<{0.ID_CONVERTER.value}:device_id>'.format(DeviceDef), - view_func=device_view, - methods={'DELETE'}) + self.add_url_rule( + '/<{0.ID_CONVERTER.value}:{0.ID_NAME}>/device'.format(self), + view_func=device_view, + methods={'GET'}, + ) + self.add_url_rule( + '/<{0.ID_CONVERTER.value}:tag_id>/'.format(self) + + 'device/<{0.ID_CONVERTER.value}:device_id>'.format(DeviceDef), + view_func=device_view, + methods={'PUT'}, + ) + self.add_url_rule( + '/<{0.ID_CONVERTER.value}:tag_id>/'.format(self) + + 'device/<{0.ID_CONVERTER.value}:device_id>'.format(DeviceDef), + view_func=device_view, + methods={'DELETE'}, + ) @option('-u', '--owner', help=OWNER_H) @option('-o', '--org', help=ORG_H) @option('-p', '--provider', help=PROV_H) @option('-s', '--sec', help=Tag.secondary.comment) @argument('id') - def create_tag(self, - id: str, - org: str = None, - owner: str = None, - sec: str = None, - provider: str = None): + def create_tag( + self, + id: str, + org: str = None, + owner: str = None, + sec: str = None, + provider: str = None, + ): """Create a tag with the given ID.""" - db.session.add(Tag(**self.schema.load( - dict(id=id, owner=owner, org=org, secondary=sec, provider=provider) - ))) + db.session.add( + Tag( + **self.schema.load( + dict(id=id, owner=owner, org=org, secondary=sec, provider=provider) + ) + ) + ) db.session.commit() @option('-u', '--owner', help=OWNER_H) @@ -83,7 +116,17 @@ class TagDef(Resource): """ with path.open() as f: for id, sec in csv.reader(f): - db.session.add(Tag(**self.schema.load( - dict(id=id, owner=owner, org=org, secondary=sec, provider=provider) - ))) + db.session.add( + Tag( + **self.schema.load( + dict( + id=id, + owner=owner, + org=org, + secondary=sec, + provider=provider, + ) + ) + ) + ) db.session.commit() diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 9c672f8c..4e29321c 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -3,12 +3,9 @@ from typing import Set from boltons import urlutils from flask import g -from sqlalchemy import BigInteger, Column, ForeignKey, UniqueConstraint, Sequence +from sqlalchemy import BigInteger, Column, ForeignKey, Sequence, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import backref, relationship, validates -from teal.db import DB_CASCADE_SET_NULL, Query, URL -from teal.marshmallow import ValidationError -from teal.resource import url_for_resource from ereuse_devicehub.db import db from ereuse_devicehub.resources.agent.models import Organization @@ -16,6 +13,9 @@ from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.utils import hashcode +from ereuse_devicehub.teal.db import DB_CASCADE_SET_NULL, URL, Query +from ereuse_devicehub.teal.marshmallow import ValidationError +from ereuse_devicehub.teal.resource import url_for_resource class Tags(Set['Tag']): @@ -26,51 +26,59 @@ class Tags(Set['Tag']): return ', '.join(format(tag, format_spec) for tag in self).strip() - - class Tag(Thing): - internal_id = Column(BigInteger, Sequence('tag_internal_id_seq'), unique=True, nullable=False) + internal_id = Column( + BigInteger, Sequence('tag_internal_id_seq'), unique=True, nullable=False + ) internal_id.comment = """The identifier of the tag for this database. Used only internally for software; users should not use this. """ id = Column(db.CIText(), primary_key=True) id.comment = """The ID of the tag.""" - owner_id = Column(UUID(as_uuid=True), - ForeignKey(User.id), - primary_key=True, - nullable=False, - default=lambda: g.user.id) + owner_id = Column( + UUID(as_uuid=True), + ForeignKey(User.id), + primary_key=True, + nullable=False, + default=lambda: g.user.id, + ) owner = relationship(User, primaryjoin=owner_id == User.id) - org_id = Column(UUID(as_uuid=True), - ForeignKey(Organization.id), - # If we link with the Organization object this instance - # will be set as persistent and added to session - # which is something we don't want to enforce by default - default=lambda: Organization.get_default_org_id()) - org = relationship(Organization, - backref=backref('tags', lazy=True), - primaryjoin=Organization.id == org_id, - collection_class=set) + org_id = Column( + UUID(as_uuid=True), + ForeignKey(Organization.id), + # If we link with the Organization object this instance + # will be set as persistent and added to session + # which is something we don't want to enforce by default + default=lambda: Organization.get_default_org_id(), + ) + org = relationship( + Organization, + backref=backref('tags', lazy=True), + primaryjoin=Organization.id == org_id, + collection_class=set, + ) """The organization that issued the tag.""" provider = Column(URL()) provider.comment = """The tag provider URL. If None, the provider is this Devicehub. """ - device_id = Column(BigInteger, - # We don't want to delete the tag on device deletion, only set to null - ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL)) - device = relationship(Device, - backref=backref('tags', lazy=True, collection_class=Tags), - primaryjoin=Device.id == device_id) + device_id = Column( + BigInteger, + # We don't want to delete the tag on device deletion, only set to null + ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL), + ) + device = relationship( + Device, + backref=backref('tags', lazy=True, collection_class=Tags), + primaryjoin=Device.id == device_id, + ) """The device linked to this tag.""" secondary = Column(db.CIText(), index=True) secondary.comment = """A secondary identifier for this tag. It has the same constraints as the main one. Only needed in special cases. """ - __table_args__ = ( - db.Index('device_id_index', device_id, postgresql_using='hash'), - ) + __table_args__ = (db.Index('device_id_index', device_id, postgresql_using='hash'),) def __init__(self, id: str, **kwargs) -> None: super().__init__(id=id, **kwargs) @@ -99,13 +107,16 @@ class Tag(Thing): @validates('provider') def use_only_domain(self, _, url: URL): if url.path: - raise ValidationError('Provider can only contain scheme and host', - field_names=['provider']) + raise ValidationError( + 'Provider can only contain scheme and host', field_names=['provider'] + ) return url __table_args__ = ( UniqueConstraint(id, owner_id, name='one tag id per owner'), - UniqueConstraint(secondary, owner_id, name='one secondary tag per organization') + UniqueConstraint( + secondary, owner_id, name='one secondary tag per organization' + ), ) @property diff --git a/ereuse_devicehub/resources/tag/schema.py b/ereuse_devicehub/resources/tag/schema.py index e1c8b608..a1d0df06 100644 --- a/ereuse_devicehub/resources/tag/schema.py +++ b/ereuse_devicehub/resources/tag/schema.py @@ -1,6 +1,5 @@ from marshmallow.fields import Boolean from sqlalchemy.util import OrderedSet -from teal.marshmallow import SanitizedStr, URL from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.agent.schemas import Organization @@ -8,6 +7,7 @@ from ereuse_devicehub.resources.device.schemas import Device from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.tag import model as m from ereuse_devicehub.resources.user.schemas import User +from ereuse_devicehub.teal.marshmallow import URL, SanitizedStr def without_slash(x: str) -> bool: @@ -16,12 +16,10 @@ def without_slash(x: str) -> bool: class Tag(Thing): - id = SanitizedStr(lower=True, - description=m.Tag.id.comment, - validator=without_slash, - required=True) - provider = URL(description=m.Tag.provider.comment, - validator=without_slash) + id = SanitizedStr( + lower=True, description=m.Tag.id.comment, validator=without_slash, required=True + ) + provider = URL(description=m.Tag.provider.comment, validator=without_slash) device = NestedOn(Device, dump_only=True) owner = NestedOn(User, only_query='id') org = NestedOn(Organization, collection_class=OrderedSet, only_query='id') diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index 081383ae..f16d056e 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -1,14 +1,16 @@ -from flask import Response, current_app as app, g, redirect, request +from flask import Response +from flask import current_app as app +from flask import g, redirect, request from flask_sqlalchemy import Pagination -from teal.marshmallow import ValidationError -from teal.resource import View, url_for_resource from ereuse_devicehub import auth from ereuse_devicehub.db import db from ereuse_devicehub.query import things_response -from ereuse_devicehub.resources.utils import hashcode from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.tag import Tag +from ereuse_devicehub.resources.utils import hashcode +from ereuse_devicehub.teal.marshmallow import ValidationError +from ereuse_devicehub.teal.resource import View, url_for_resource class TagView(View): @@ -34,13 +36,19 @@ class TagView(View): @auth.Auth.requires_auth def find(self, args: dict): - tags = Tag.query.filter(Tag.is_printable_q()) \ - .filter_by(owner=g.user) \ - .order_by(Tag.created.desc()) \ - .paginate(per_page=200) # type: Pagination + tags = ( + Tag.query.filter(Tag.is_printable_q()) + .filter_by(owner=g.user) + .order_by(Tag.created.desc()) + .paginate(per_page=200) + ) # type: Pagination return things_response( self.schema.dump(tags.items, many=True, nested=0), - tags.page, tags.per_page, tags.total, tags.prev_num, tags.next_num + tags.page, + tags.per_page, + tags.total, + tags.prev_num, + tags.next_num, ) def _create_many_regular_tags(self, num: int): @@ -48,7 +56,9 @@ class TagView(View): tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id] db.session.add_all(tags) db.session().final_flush() - response = things_response(self.schema.dump(tags, many=True, nested=1), code=201) + response = things_response( + self.schema.dump(tags, many=True, nested=1), code=201 + ) db.session.commit() return response diff --git a/ereuse_devicehub/resources/tradedocument/definitions.py b/ereuse_devicehub/resources/tradedocument/definitions.py index e321c7b4..94d0f44e 100644 --- a/ereuse_devicehub/resources/tradedocument/definitions.py +++ b/ereuse_devicehub/resources/tradedocument/definitions.py @@ -1,7 +1,7 @@ -from teal.resource import Converters, Resource - from ereuse_devicehub.resources.tradedocument import schemas from ereuse_devicehub.resources.tradedocument.views import TradeDocumentView +from ereuse_devicehub.teal.resource import Converters, Resource + class TradeDocumentDef(Resource): SCHEMA = schemas.TradeDocument diff --git a/ereuse_devicehub/resources/tradedocument/models.py b/ereuse_devicehub/resources/tradedocument/models.py index 07af62d1..432b8c51 100644 --- a/ereuse_devicehub/resources/tradedocument/models.py +++ b/ereuse_devicehub/resources/tradedocument/models.py @@ -7,12 +7,12 @@ from sortedcontainers import SortedSet from sqlalchemy import BigInteger, Column, Sequence from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import backref -from teal.db import CASCADE_OWN, URL from ereuse_devicehub.db import db from ereuse_devicehub.resources.enums import Severity from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.db import CASCADE_OWN, URL _sorted_documents = { 'order_by': lambda: TradeDocument.created, diff --git a/ereuse_devicehub/resources/tradedocument/schemas.py b/ereuse_devicehub/resources/tradedocument/schemas.py index 99aa9ab6..5de7a55f 100644 --- a/ereuse_devicehub/resources/tradedocument/schemas.py +++ b/ereuse_devicehub/resources/tradedocument/schemas.py @@ -1,10 +1,13 @@ -from marshmallow.fields import DateTime, Integer, Float, validate -from teal.marshmallow import SanitizedStr, URL -# from marshmallow import ValidationError, validates_schema +from marshmallow.fields import DateTime, Float, Integer, validate from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.tradedocument import models as m +from ereuse_devicehub.teal.marshmallow import URL, SanitizedStr + +# from marshmallow import ValidationError, validates_schema + + # from ereuse_devicehub.resources.lot import schemas as s_lot @@ -12,20 +15,28 @@ class TradeDocument(Thing): __doc__ = m.TradeDocument.__doc__ id = Integer(description=m.TradeDocument.id.comment, dump_only=True) date = DateTime(required=False, description=m.TradeDocument.date.comment) - id_document = SanitizedStr(data_key='documentId', - default='', - description=m.TradeDocument.id_document.comment) - description = SanitizedStr(default='', - description=m.TradeDocument.description.comment, - validate=validate.Length(max=500)) - file_name = SanitizedStr(data_key='filename', - default='', - description=m.TradeDocument.file_name.comment, - validate=validate.Length(max=100)) - file_hash = SanitizedStr(data_key='hash', - default='', - description=m.TradeDocument.file_hash.comment, - validate=validate.Length(max=64)) + id_document = SanitizedStr( + data_key='documentId', + default='', + description=m.TradeDocument.id_document.comment, + ) + description = SanitizedStr( + default='', + description=m.TradeDocument.description.comment, + validate=validate.Length(max=500), + ) + file_name = SanitizedStr( + data_key='filename', + default='', + description=m.TradeDocument.file_name.comment, + validate=validate.Length(max=100), + ) + file_hash = SanitizedStr( + data_key='hash', + default='', + description=m.TradeDocument.file_hash.comment, + validate=validate.Length(max=64), + ) url = URL(description=m.TradeDocument.url.comment) lot = NestedOn('Lot', only_query='id', description=m.TradeDocument.lot.__doc__) trading = SanitizedStr(dump_only=True, description='') diff --git a/ereuse_devicehub/resources/tradedocument/views.py b/ereuse_devicehub/resources/tradedocument/views.py index a478da6f..5d71e73a 100644 --- a/ereuse_devicehub/resources/tradedocument/views.py +++ b/ereuse_devicehub/resources/tradedocument/views.py @@ -1,18 +1,20 @@ import os import time from datetime import datetime -from flask import current_app as app, request, g, Response + +from flask import Response +from flask import current_app as app +from flask import g, request from marshmallow import ValidationError -from teal.resource import View from ereuse_devicehub.db import db -from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.action.models import ConfirmDocument from ereuse_devicehub.resources.hash_reports import ReportHash +from ereuse_devicehub.resources.tradedocument.models import TradeDocument +from ereuse_devicehub.teal.resource import View class TradeDocumentView(View): - def one(self, id: str): doc = TradeDocument.query.filter_by(id=id, owner=g.user).one() return self.schema.jsonify(doc) @@ -33,10 +35,9 @@ class TradeDocumentView(View): trade = doc.lot.trade if trade: trade.documents.add(doc) - confirm = ConfirmDocument(action=trade, - user=g.user, - devices=set(), - documents={doc}) + confirm = ConfirmDocument( + action=trade, user=g.user, devices=set(), documents={doc} + ) db.session.add(confirm) db.session.add(doc) db.session().final_flush() diff --git a/ereuse_devicehub/resources/user/__init__.py b/ereuse_devicehub/resources/user/__init__.py index 1bbe508b..13d99bee 100644 --- a/ereuse_devicehub/resources/user/__init__.py +++ b/ereuse_devicehub/resources/user/__init__.py @@ -2,12 +2,12 @@ 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.user import schemas from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.views import UserView, login, logout +from ereuse_devicehub.teal.resource import Converters, Resource class UserDef(Resource): @@ -16,49 +16,88 @@ class UserDef(Resource): ID_CONVERTER = Converters.uuid AUTH = True - 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): + 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.create_user, 'add'),) - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path, cli_commands) + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_commands, + ) self.add_url_rule('/login/', view_func=login, methods={'POST'}) logout_view = app.auth.requires_auth(logout) self.add_url_rule('/logout/', view_func=logout_view, methods={'GET'}) @argument('email') - @option('-i', '--inventory', - multiple=True, - help='Inventories user has access to. By default this one.') - @option('-a', '--agent', - help='Create too an Individual agent representing this user, ' - 'and give a name to this individual.') + @option( + '-i', + '--inventory', + multiple=True, + help='Inventories user has access to. By default this one.', + ) + @option( + '-a', + '--agent', + help='Create too an Individual agent representing this user, ' + 'and give a name to this individual.', + ) @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, - inventory: Iterable[str] = tuple(), - 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: """Create an user. If ``--agent`` is passed, it creates too an ``Individual`` agent that represents the user. """ from ereuse_devicehub.resources.agent.models import Individual - u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \ - .load({'email': email, 'password': password}) + + u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)).load( + {'email': email, 'password': password} + ) if inventory: from ereuse_devicehub.resources.inventory import 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) - )) + agent = Individual( + **current_app.resources[Individual.t].schema.load( + dict( + name=agent, + email=email, + country=country, + telephone=telephone, + taxId=tax_id, + ) + ) + ) user.individuals.add(agent) db.session.add(user) db.session.commit() diff --git a/ereuse_devicehub/resources/user/models.py b/ereuse_devicehub/resources/user/models.py index 75a598b1..56383488 100644 --- a/ereuse_devicehub/resources/user/models.py +++ b/ereuse_devicehub/resources/user/models.py @@ -8,12 +8,12 @@ from flask_login import UserMixin from sqlalchemy import BigInteger, Boolean, Column, Sequence from sqlalchemy.dialects.postgresql import UUID from sqlalchemy_utils import EmailType, PasswordType -from teal.db import CASCADE_OWN, URL, IntEnum from ereuse_devicehub.db import db from ereuse_devicehub.resources.enums import SessionType from ereuse_devicehub.resources.inventory.model import Inventory from ereuse_devicehub.resources.models import STR_SIZE, Thing +from ereuse_devicehub.teal.db import CASCADE_OWN, URL, IntEnum class User(UserMixin, Thing): diff --git a/ereuse_devicehub/resources/user/schemas.py b/ereuse_devicehub/resources/user/schemas.py index a70c0da8..2e2cb725 100644 --- a/ereuse_devicehub/resources/user/schemas.py +++ b/ereuse_devicehub/resources/user/schemas.py @@ -1,12 +1,12 @@ from marshmallow import post_dump -from marshmallow.fields import Email, String, UUID -from teal.marshmallow import SanitizedStr +from marshmallow.fields import UUID, Email, String 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 +from ereuse_devicehub.teal.marshmallow import SanitizedStr class Session(Thing): @@ -19,27 +19,33 @@ class User(Thing): password = SanitizedStr(load_only=True, required=True) individuals = NestedOn(Individual, many=True, dump_only=True) name = SanitizedStr() - token = String(dump_only=True, - description='Use this token in an Authorization header to access the app.' - 'The token can change overtime.') + 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) code = String(dump_only=True, description='Code of inactive accounts') - def __init__(self, - only=None, - exclude=('token',), - prefix='', - many=False, - context=None, - load_only=(), - dump_only=(), - partial=False): + def __init__( + self, + only=None, + exclude=('token',), + prefix='', + many=False, + context=None, + load_only=(), + dump_only=(), + partial=False, + ): """Instantiates the User. By default we exclude token from both load/dump so they are not taken / set in normal usage by mistake. """ - super().__init__(only, exclude, prefix, many, context, load_only, dump_only, partial) + super().__init__( + only, exclude, prefix, many, context, load_only, dump_only, partial + ) @post_dump def base64encode_token(self, data: dict): diff --git a/ereuse_devicehub/resources/user/views.py b/ereuse_devicehub/resources/user/views.py index 2fc8fc31..6aa9ddc0 100644 --- a/ereuse_devicehub/resources/user/views.py +++ b/ereuse_devicehub/resources/user/views.py @@ -2,11 +2,11 @@ from uuid import UUID, uuid4 from flask import g, request from flask.json import jsonify -from teal.resource import View from ereuse_devicehub.db import db from ereuse_devicehub.resources.user.exceptions import WrongCredentials from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.resource import View class UserView(View): @@ -19,7 +19,9 @@ def login(): user_s = g.resource_def.SCHEMA(only=('email', 'password')) # type: UserS # noinspection PyArgumentList u = request.get_json(schema=user_s) - user = User.query.filter_by(email=u['email'], active=True, phantom=False).one_or_none() + user = User.query.filter_by( + email=u['email'], active=True, phantom=False + ).one_or_none() if user and user.password == u['password']: schema_with_token = g.resource_def.SCHEMA(exclude=set()) return schema_with_token.jsonify(user) diff --git a/ereuse_devicehub/resources/versions/versions.py b/ereuse_devicehub/resources/versions/versions.py index 78af7f6a..33e22fd8 100644 --- a/ereuse_devicehub/resources/versions/versions.py +++ b/ereuse_devicehub/resources/versions/versions.py @@ -1,16 +1,16 @@ -import flask import json -import requests -import teal.marshmallow - from typing import Callable, Iterable, Tuple from urllib.parse import urlparse -from flask import make_response, g -from flask.json import jsonify -from teal.resource import Resource, View -from ereuse_devicehub.resources.inventory.model import Inventory +import flask +import requests +from flask import g, make_response +from flask.json import jsonify + +import ereuse_devicehub.teal.marshmallow from ereuse_devicehub import __version__ +from ereuse_devicehub.resources.inventory.model import Inventory +from ereuse_devicehub.teal.resource import Resource, View def get_tag_version(app): @@ -29,6 +29,7 @@ def get_tag_version(app): else: return {} + class VersionView(View): def get(self, *args, **kwargs): """Get version of DeviceHub and ereuse-tag.""" @@ -48,18 +49,31 @@ class VersionDef(Resource): VIEW = None # We do not want to create default / documents endpoint AUTH = False - def __init__(self, app, - import_name=__name__, - static_folder=None, - static_url_path=None, - template_folder=None, - url_prefix=None, - subdomain=None, - url_defaults=None, - root_path=None, - cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()): - super().__init__(app, import_name, static_folder, static_url_path, template_folder, - url_prefix, subdomain, url_defaults, root_path, cli_commands) + def __init__( + self, + app, + import_name=__name__, + static_folder=None, + static_url_path=None, + template_folder=None, + url_prefix=None, + subdomain=None, + url_defaults=None, + root_path=None, + cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(), + ): + super().__init__( + app, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_commands, + ) d = {'devicehub': __version__, "ereuse_tag": "0.0.0"} get = {'GET'} diff --git a/ereuse_devicehub/static/js/main_inventory.build.js b/ereuse_devicehub/static/js/main_inventory.build.js index 0032f649..389f4377 100644 --- a/ereuse_devicehub/static/js/main_inventory.build.js +++ b/ereuse_devicehub/static/js/main_inventory.build.js @@ -30,6 +30,10 @@ $(document).ready(() => { ; select_shift(); // $('#selectLot').selectpicker(); + + $("#filter").on("change", () => { + $("#submit_filter").click(); + }); }); class TableController { @@ -211,8 +215,8 @@ function removeLot() { } function select_shift() { - const chkboxes = $('.deviceSelect'); - var lastChecked = null; + const chkboxes = $(".deviceSelect"); + let lastChecked = null; chkboxes.click(function (e) { if (!lastChecked) { lastChecked = this; @@ -324,17 +328,16 @@ function export_file(type_file) { function export_actions_erasure(type_file) { const actions = TableController.getSelectedDevices(); - const actions_id = $.map(actions, (x) => $(x).attr("data-action-erasure")).join(","); + const actions_id = $.map(actions, x => $(x).attr("data-action-erasure")).join(","); if (actions_id) { - const url = `/inventory/export/${type_file}/?ids=${actions_id}`; + const url = "/inventory/export/".concat(type_file, "/?ids=").concat(actions_id); window.location.href = url; } else { $("#exportAlertModal").click(); } } - class lotsSearcher { static enable() { if (this.lotsSearchElement) this.lotsSearchElement.disabled = false; @@ -663,19 +666,14 @@ async function processSelectedDevices() { return lot; }); - listHTML.html(""); const lot_temporary = lots.filter(lot => !lot.transfer && !lot.trade); appendMenu(lot_temporary, listHTML, templateLot, selectedDevices, actions, "Temporary"); - const lot_incoming = lots.filter(lot => lot.transfer && lot.transfer == "Incoming"); appendMenu(lot_incoming, listHTML, templateLot, selectedDevices, actions, "Incoming"); - const lot_outgoing = lots.filter(lot => lot.transfer && lot.transfer == "Outgoing"); appendMenu(lot_outgoing, listHTML, templateLot, selectedDevices, actions, "Outgoing"); - lotsSearcher.enable(); - } catch (error) { console.log(error); listHTML.html("
  • Error feching devices and lots
    (see console for more details)
  • "); @@ -689,6 +687,6 @@ function appendMenu(lots, listHTML, templateLot, selectedDevices, actions, title lotsList.push(lots.filter(lot => lot.state == "false").sort((a, b) => a.name.localeCompare(b.name))); lotsList = lotsList.flat(); // flat array - listHTML.append(`
  • ${ title }
  • `); + listHTML.append("
  • ".concat(title, "
  • ")); lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions)); } diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js index f5c50241..3088257c 100644 --- a/ereuse_devicehub/static/js/main_inventory.js +++ b/ereuse_devicehub/static/js/main_inventory.js @@ -16,6 +16,9 @@ $(document).ready(() => { }; select_shift(); // $('#selectLot').selectpicker(); + $("#filter").on("change", () => { + $("#submit_filter").click(); + }); }) class TableController { diff --git a/ereuse_devicehub/teal/__init__.py b/ereuse_devicehub/teal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/teal/auth.py b/ereuse_devicehub/teal/auth.py new file mode 100644 index 00000000..14f1b404 --- /dev/null +++ b/ereuse_devicehub/teal/auth.py @@ -0,0 +1,93 @@ +import base64 +from functools import wraps +from typing import Callable + +from flask import current_app, g, request +from werkzeug.datastructures import Authorization +from werkzeug.exceptions import Unauthorized + + +class Auth: + """ + Authentication handler for Teal. + + To authenticate the user (perform login): + 1. Set Resource.AUTH to True, or manually decorate the view with + @auth.requires_auth + 2. Extend any subclass of this one (like TokenAuth). + 3. Implement the authenticate method with the authentication logic. + For example, in TokenAuth here you get the user from the token. + 5. Set in your teal the Auth class you have created so + teal can use it. + """ + + API_DOCS = { + 'type': 'http', + 'description:': 'HTTP Basic scheme', + 'name': 'Authorization', + 'in': 'header', + 'scheme': 'basic', + } + + @classmethod + def requires_auth(cls, f: Callable): + """ + Decorate a view enforcing authentication (logged in user). + """ + + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + if not auth: + raise Unauthorized('Provide proper authorization credentials') + current_app.auth.perform_auth(auth) + return f(*args, **kwargs) + + return decorated + + def perform_auth(self, auth: Authorization): + """ + Authenticate an user. This loads the user. + + An exception (expected Unauthorized) is raised if + authentication failed. + """ + g.user = self.authenticate(auth.username, auth.password) + + def authenticate(self, username: str, password: str) -> object: + """ + The authentication logic. The result of this method is + a user or a raised exception, like Werkzeug's Unauthorized, + if authentication failed. + + :raise: Unauthorized Authentication failed. + :return: The user object. + """ + raise NotImplementedError() + + +class TokenAuth(Auth): + API_DOCS = Auth.API_DOCS.copy() + API_DOCS['description'] = 'Basic scheme with token.' + + def authenticate(self, token: str, *args, **kw) -> object: + """ + The result of this method is + a user or a raised exception if authentication failed. + + :raise: Unauthorized Authentication failed. + :return The user object. + """ + raise NotImplementedError() + + @staticmethod + def encode(value: str): + """Creates a suitable Token that can be sent to a client + and sent back. + """ + return base64.b64encode(str.encode(str(value) + ':')).decode() + + @staticmethod + def decode(value: str): + """Decodes a token generated by ``encode``.""" + return base64.b64decode(value.encode()).decode()[:-1] diff --git a/ereuse_devicehub/teal/cache.py b/ereuse_devicehub/teal/cache.py new file mode 100644 index 00000000..e8683ee3 --- /dev/null +++ b/ereuse_devicehub/teal/cache.py @@ -0,0 +1,28 @@ +import datetime +from functools import wraps + +from flask import make_response + + +def cache(expires: datetime.timedelta = None): + """Sets HTTP cache for now + passed-in time. + + Example usage:: + + @app.route('/map') + @header_cache(expires=datetime.datetime(seconds=50)) + def index(): + return render_template('index.html') + """ + + def cache_decorator(view): + @wraps(view) + def cache_func(*args, **kwargs): + r = make_response(view(*args, **kwargs)) + r.expires = datetime.datetime.now(datetime.timezone.utc) + expires + r.cache_control.public = True + return r + + return cache_func + + return cache_decorator diff --git a/ereuse_devicehub/teal/cli.py b/ereuse_devicehub/teal/cli.py new file mode 100644 index 00000000..21fc4ba8 --- /dev/null +++ b/ereuse_devicehub/teal/cli.py @@ -0,0 +1,13 @@ +from flask.testing import FlaskCliRunner + + +class TealCliRunner(FlaskCliRunner): + """The same as FlaskCliRunner but with invoke's + 'catch_exceptions' as False. + """ + + def invoke(self, *args, cli=None, **kwargs): + kwargs.setdefault('catch_exceptions', False) + r = super().invoke(cli, args, **kwargs) + assert r.exit_code == 0, 'CLI code {}: {}'.format(r.exit_code, r.output) + return r diff --git a/ereuse_devicehub/teal/client.py b/ereuse_devicehub/teal/client.py new file mode 100644 index 00000000..d2ab1210 --- /dev/null +++ b/ereuse_devicehub/teal/client.py @@ -0,0 +1,181 @@ +from typing import Any, Iterable, Tuple, Type, Union + +from boltons.urlutils import URL +from ereuse_devicehub.ereuse_utils.test import JSON +from ereuse_devicehub.ereuse_utils.test import Client as EreuseUtilsClient +from ereuse_devicehub.ereuse_utils.test import Res +from werkzeug.exceptions import HTTPException + +from ereuse_devicehub.teal.marshmallow import ValidationError + +Status = Union[int, Type[HTTPException], Type[ValidationError]] +Query = Iterable[Tuple[str, Any]] + + +class Client(EreuseUtilsClient): + """A REST interface to a Teal app.""" + + def open( + self, + uri: str, + res: str = None, + status: Status = 200, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + item=None, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + headers = headers or {} + if res: + resource_url = self.application.resources[res].url_prefix + '/' + uri = URL(uri).navigate(resource_url).to_text() + if token: + headers['Authorization'] = 'Basic {}'.format(token) + res = super().open( + uri, status, query, accept, content_type, item, headers, **kw + ) + # ereuse-utils checks for status code + # here we check for specific type + # (when response: {'type': 'foobar', 'code': 422}) + _status = getattr(status, 'code', status) + if not isinstance(status, int) and res[1].status_code == _status: + assert ( + status.__name__ == res[0]['type'] + ), 'Expected exception {0} but it was {1}'.format( + status.__name__, res[0]['type'] + ) + return res + + def get( + self, + uri: str = '', + res: str = None, + query: Query = tuple(), + status: Status = 200, + item=None, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + """ + Performs GET. + + :param uri: The uri where to GET from. This is optional, as you + can build the URI too through ``res`` and ``item``. + :param res: The resource where to GET from, if any. + If this is set, the client will try to get the + url from the resource definition. + :param query: The query params in a dict. This method + automatically converts the dict to URL params, + and if the dict had nested dictionaries, those + are converted to JSON. + :param status: A status code or exception to assert. + :param item: The id of a resource to GET from, if any. + :param accept: The accept headers. By default + ``application/json``. + :param headers: A dictionary of header name - header value. + :param token: A token to add to an ``Authentication`` header. + :return: A tuple containing 1. a dict (if content-type is JSON) + or a str with the data, and 2. the ``Response`` object. + """ + kw['res'] = res + kw['token'] = token + return super().get(uri, query, item, status, accept, headers, **kw) + + def post( + self, + data: str or dict, + uri: str = '', + res: str = None, + query: Query = tuple(), + status: Status = 201, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + kw['res'] = res + kw['token'] = token + return super().post( + uri, data, query, status, content_type, accept, headers, **kw + ) + + def patch( + self, + data: str or dict, + uri: str = '', + res: str = None, + query: Query = tuple(), + item=None, + status: Status = 200, + content_type: str = JSON, + accept: str = JSON, + token: str = None, + headers: dict = None, + **kw, + ) -> Res: + kw['res'] = res + kw['token'] = token + return super().patch( + uri, data, query, status, content_type, item, accept, headers, **kw + ) + + def put( + self, + data: str or dict, + uri: str = '', + res: str = None, + query: Query = tuple(), + item=None, + status: Status = 201, + content_type: str = JSON, + accept: str = JSON, + token: str = None, + headers: dict = None, + **kw, + ) -> Res: + kw['res'] = res + kw['token'] = token + return super().put( + uri, data, query, status, content_type, item, accept, headers, **kw + ) + + def delete( + self, + uri: str = '', + res: str = None, + query: Query = tuple(), + status: Status = 204, + item=None, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + kw['res'] = res + kw['token'] = token + return super().delete(uri, query, item, status, accept, headers, **kw) + + def post_get( + self, + res: str, + data: str or dict, + query: Query = tuple(), + status: Status = 200, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + key='id', + token: str = None, + **kw, + ) -> Res: + """Performs post and then gets the resource through its key.""" + r, _ = self.post( + '', data, res, query, status, content_type, accept, token, headers, **kw + ) + return self.get(res=res, item=r[key]) diff --git a/ereuse_devicehub/teal/config.py b/ereuse_devicehub/teal/config.py new file mode 100644 index 00000000..46cac145 --- /dev/null +++ b/ereuse_devicehub/teal/config.py @@ -0,0 +1,70 @@ +from boltons.typeutils import issubclass + +from ereuse_devicehub.teal.resource import Resource + + +class Config: + """ + The configuration class. + + Subclass and set here your config values. + """ + + RESOURCE_DEFINITIONS = set() + """ + A list of resource definitions to load. + """ + + SQLALCHEMY_DATABASE_URI = None + """ + The access to the main Database. + """ + SQLALCHEMY_BINDS = {} + """ + Optional extra databases. See `here `_ how bind your models to different + databases. + """ + SQLALCHEMY_TRACK_MODIFICATIONS = False + """ + Disables flask-sqlalchemy notification system. + Save resources and hides a warning by flask-sqlalchemy itself. + + See `this answer in Stackoverflow for more info + `_. + """ + + API_DOC_CONFIG_TITLE = 'Teal' + API_DOC_CONFIG_VERSION = '0.1' + """ + Configuration options for the api docs. They are the parameters + passed to `apispec `_. Prefix the configuration + names with ``API_DOC_CONFIG_``. + """ + API_DOC_CLASS_DISCRIMINATOR = None + """ + Configuration options for the api docs class definitions. + + You can pass any `schema definition `_ + prefiex by ``API_DOC_CLASS_`` like in the example above. + """ + + CORS_ORIGINS = '*' + CORS_EXPOSE_HEADERS = 'Authorization' + CORS_ALLOW_HEADERS = 'Content-Type', 'Authorization' + """ + Configuration for CORS. See the options you can pass by in `Flask-Cors + `_, + exactly in **Parameters**, like the ones above. + """ + + def __init__(self) -> None: + """ + :param db: Optional. Set the ``SQLALCHEMY_DATABASE_URI`` param. + """ + for r in self.RESOURCE_DEFINITIONS: + assert issubclass( + r, Resource + ), '{0!r} is not a subclass of Resource'.format(r) diff --git a/ereuse_devicehub/teal/db.py b/ereuse_devicehub/teal/db.py new file mode 100644 index 00000000..7f0b8c7f --- /dev/null +++ b/ereuse_devicehub/teal/db.py @@ -0,0 +1,382 @@ +import enum +import ipaddress +import re +import uuid +from distutils.version import StrictVersion +from typing import Any, Type, Union + +from boltons.typeutils import classproperty +from boltons.urlutils import URL as BoltonsUrl +from ereuse_devicehub.ereuse_utils import if_none_return_none +from flask_sqlalchemy import BaseQuery +from flask_sqlalchemy import Model as _Model +from flask_sqlalchemy import SignallingSession +from flask_sqlalchemy import SQLAlchemy as FlaskSQLAlchemy +from sqlalchemy import CheckConstraint, SmallInteger, cast, event, types +from sqlalchemy.dialects.postgresql import ARRAY, INET +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound +from sqlalchemy_utils import Ltree +from werkzeug.exceptions import BadRequest, NotFound, UnprocessableEntity + + +class ResourceNotFound(NotFound): + # todo show id + def __init__(self, resource: str) -> None: + super().__init__('The {} doesn\'t exist.'.format(resource)) + + +class MultipleResourcesFound(UnprocessableEntity): + # todo show id + def __init__(self, resource: str) -> None: + super().__init__( + 'Expected only one {} but multiple where found'.format(resource) + ) + + +POLYMORPHIC_ID = 'polymorphic_identity' +POLYMORPHIC_ON = 'polymorphic_on' +INHERIT_COND = 'inherit_condition' +DEFAULT_CASCADE = 'save-update, merge' +CASCADE_DEL = '{}, delete'.format(DEFAULT_CASCADE) +CASCADE_OWN = '{}, delete-orphan'.format(CASCADE_DEL) +DB_CASCADE_SET_NULL = 'SET NULL' + + +class Query(BaseQuery): + def one(self): + try: + return super().one() + except NoResultFound: + raise ResourceNotFound(self._entities[0]._label_name) + except MultipleResultsFound: + raise MultipleResourcesFound(self._entities[0]._label_name) + + +class Model(_Model): + # Just provide typing + query_class = Query # type: Type[Query] + query = None # type: Query + + @classproperty + def t(cls): + return cls.__name__ + + +class Session(SignallingSession): + """A SQLAlchemy session that raises better exceptions.""" + + def _flush(self, objects=None): + try: + super()._flush(objects) + except IntegrityError as e: + raise DBError(e) # This creates a suitable subclass + + +class SchemaSession(Session): + """Session that is configured to use a PostgreSQL's Schema. + + Idea from `here `_. + """ + + def __init__(self, db, autocommit=False, autoflush=True, **options): + super().__init__(db, autocommit, autoflush, **options) + self.execute('SET search_path TO {}, public'.format(self.app.schema)) + + +class StrictVersionType(types.TypeDecorator): + """StrictVersion support for SQLAlchemy as Unicode. + + Idea `from official documentation `_. + """ + + impl = types.Unicode + + @if_none_return_none + def process_bind_param(self, value, dialect): + return str(value) + + @if_none_return_none + def process_result_value(self, value, dialect): + return StrictVersion(value) + + +class URL(types.TypeDecorator): + """bolton's URL support for SQLAlchemy as Unicode.""" + + impl = types.Unicode + + @if_none_return_none + def process_bind_param(self, value: BoltonsUrl, dialect): + return value.to_text() + + @if_none_return_none + def process_result_value(self, value, dialect): + return BoltonsUrl(value) + + +class IP(types.TypeDecorator): + """ipaddress support for SQLAlchemy as PSQL INET.""" + + impl = INET + + @if_none_return_none + def process_bind_param(self, value, dialect): + return str(value) + + @if_none_return_none + def process_result_value(self, value, dialect): + return ipaddress.ip_address(value) + + +class IntEnum(types.TypeDecorator): + """SmallInteger -- IntEnum""" + + impl = SmallInteger + + def __init__(self, enumeration: Type[enum.IntEnum], *args, **kwargs): + self.enum = enumeration + super().__init__(*args, **kwargs) + + @if_none_return_none + def process_bind_param(self, value, dialect): + assert isinstance(value, self.enum), 'Value should be instance of {}'.format( + self.enum + ) + return value.value + + @if_none_return_none + def process_result_value(self, value, dialect): + return self.enum(value) + + +class UUIDLtree(Ltree): + """This Ltree only wants UUIDs as paths elements.""" + + def __init__(self, path_or_ltree: Union[Ltree, uuid.UUID]): + """ + Creates a new Ltree. If the passed-in value is an UUID, + it automatically generates a suitable string for Ltree. + """ + if not isinstance(path_or_ltree, Ltree): + if isinstance(path_or_ltree, uuid.UUID): + path_or_ltree = self.convert(path_or_ltree) + else: + raise ValueError( + 'Ltree does not accept {}'.format(path_or_ltree.__class__) + ) + super().__init__(path_or_ltree) + + @staticmethod + def convert(id: uuid.UUID) -> str: + """Transforms an uuid to a ready-to-ltree str representation.""" + return str(id).replace('-', '_') + + +def check_range(column: str, min=1, max=None) -> CheckConstraint: + """Database constraint for ranged values.""" + constraint = ( + '>= {}'.format(min) if max is None else 'BETWEEN {} AND {}'.format(min, max) + ) + return CheckConstraint('{} {}'.format(column, constraint)) + + +def check_lower(field_name: str): + """Constraint that checks if the string is lower case.""" + return CheckConstraint( + '{0} = lower({0})'.format(field_name), + name='{} must be lower'.format(field_name), + ) + + +class ArrayOfEnum(ARRAY): + """ + Allows to use Arrays of Enums for psql. + + From `the docs `_ + and `this issue `_. + """ + + def bind_expression(self, bindvalue): + return cast(bindvalue, self) + + def result_processor(self, dialect, coltype): + super_rp = super(ArrayOfEnum, self).result_processor(dialect, coltype) + + def handle_raw_string(value): + inner = re.match(r'^{(.*)}$', value).group(1) + return inner.split(',') if inner else [] + + def process(value): + if value is None: + return None + return super_rp(handle_raw_string(value)) + + return process + + +class SQLAlchemy(FlaskSQLAlchemy): + """ + Enhances :class:`flask_sqlalchemy.SQLAlchemy` by adding our + Session and Model. + """ + + StrictVersionType = StrictVersionType + URL = URL + IP = IP + IntEnum = IntEnum + UUIDLtree = UUIDLtree + ArrayOfEnum = ArrayOfEnum + + def __init__( + self, + app=None, + use_native_unicode=True, + session_options=None, + metadata=None, + query_class=BaseQuery, + model_class=Model, + ): + super().__init__( + app, use_native_unicode, session_options, metadata, query_class, model_class + ) + + def create_session(self, options): + """As parent's create_session but adding our Session.""" + return sessionmaker(class_=Session, db=self, **options) + + +class SchemaSQLAlchemy(SQLAlchemy): + """ + Enhances :class:`flask_sqlalchemy.SQLAlchemy` by using PostgreSQL's + schemas when creating/dropping tables. + + See :attr:`teal.config.SCHEMA` for more info. + """ + + def __init__( + self, + app=None, + use_native_unicode=True, + session_options=None, + metadata=None, + query_class=Query, + model_class=Model, + ): + super().__init__( + app, use_native_unicode, session_options, metadata, query_class, model_class + ) + # The following listeners set psql's search_path to the correct + # schema and create the schemas accordingly + + # Specifically: + # 1. Creates the schemas and set ``search_path`` to app's config SCHEMA + event.listen(self.metadata, 'before_create', self.create_schemas) + # Set ``search_path`` to default (``public``) + event.listen(self.metadata, 'after_create', self.revert_connection) + # Set ``search_path`` to app's config SCHEMA + event.listen(self.metadata, 'before_drop', self.set_search_path) + # Set ``search_path`` to default (``public``) + event.listen(self.metadata, 'after_drop', self.revert_connection) + + def create_all(self, bind='__all__', app=None, exclude_schema=None): + """Create all tables. + + :param exclude_schema: Do not create tables in this schema. + """ + app = self.get_app(app) + # todo how to pass exclude_schema without contaminating self? + self._exclude_schema = exclude_schema + super().create_all(bind, app) + + def _execute_for_all_tables(self, app, bind, operation, skip_tables=False): + # todo how to pass app to our event listeners without contaminating self? + self._app = self.get_app(app) + super()._execute_for_all_tables(app, bind, operation, skip_tables) + + def get_tables_for_bind(self, bind=None): + """As super method, but only getting tales that are not + part of exclude_schema, if set. + """ + tables = super().get_tables_for_bind(bind) + if getattr(self, '_exclude_schema', None): + tables = [t for t in tables if t.schema != self._exclude_schema] + return tables + + def create_schemas(self, target, connection, **kw): + """ + Create the schemas and set the active schema. + + From `here `_. + """ + schemas = set(table.schema for table in target.tables.values() if table.schema) + if self._app.schema: + schemas.add(self._app.schema) + for schema in schemas: + connection.execute('CREATE SCHEMA IF NOT EXISTS {}'.format(schema)) + self.set_search_path(target, connection) + + def set_search_path(self, _, connection, **kw): + app = self.get_app() + if app.schema: + connection.execute('SET search_path TO {}, public'.format(app.schema)) + + def revert_connection(self, _, connection, **kw): + connection.execute('SET search_path TO public') + + def create_session(self, options): + """As parent's create_session but adding our SchemaSession.""" + return sessionmaker(class_=SchemaSession, db=self, **options) + + def drop_schema(self, app=None, schema=None): + """Nukes a schema and everything that depends on it.""" + app = self.get_app(app) + schema = schema or app.schema + with self.engine.begin() as conn: + conn.execute('DROP SCHEMA IF EXISTS {} CASCADE'.format(schema)) + + def has_schema(self, schema: str) -> bool: + """Does the db have the passed-in schema?""" + return self.engine.execute( + "SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname='{}')".format( + schema + ) + ).scalar() + + +class DBError(BadRequest): + """An Error from the database. + + This helper error is used to map SQLAlchemy's IntegrityError + to more precise errors (like UniqueViolation) that are understood + as a client-ready HTTP Error. + + When instantiating the class it auto-selects the best error. + """ + + def __init__(self, origin: IntegrityError): + super().__init__(str(origin)) + self._origin = origin + + def __new__(cls, origin: IntegrityError) -> Any: + msg = str(origin) + if 'unique constraint' in msg.lower(): + return super().__new__(UniqueViolation) + return super().__new__(cls) + + +class UniqueViolation(DBError): + def __init__(self, origin: IntegrityError): + super().__init__(origin) + self.constraint = self.description.split('"')[1] + self.field_name = None + self.field_value = None + if isinstance(origin.params, dict): + self.field_name, self.field_value = next( + (k, v) for k, v in origin.params.items() if k in self.constraint + ) diff --git a/ereuse_devicehub/teal/enums.py b/ereuse_devicehub/teal/enums.py new file mode 100644 index 00000000..175b2227 --- /dev/null +++ b/ereuse_devicehub/teal/enums.py @@ -0,0 +1,4421 @@ +from enum import Enum, unique + + +@unique +class Currency(Enum): + """Currencies as for ISO 4217.""" + + AFN = 1 + ARS = 2 + AWG = 3 + AUD = 4 + AZN = 5 + BSD = 6 + BBD = 7 + BDT = 8 + BYR = 9 + BZD = 10 + BMD = 11 + BOB = 12 + BAM = 13 + BWP = 14 + BGN = 15 + BRL = 16 + BND = 17 + KHR = 18 + CAD = 19 + KYD = 20 + CLP = 21 + CNY = 22 + COP = 23 + CRC = 24 + HRK = 25 + CUP = 26 + CZK = 27 + DKK = 28 + DOP = 29 + XCD = 30 + EGP = 31 + SVC = 32 + EEK = 33 + EUR = 34 + FKP = 35 + FJD = 36 + GHC = 37 + GIP = 38 + GTQ = 39 + GGP = 40 + GYD = 41 + HNL = 42 + HKD = 43 + HUF = 44 + ISK = 45 + INR = 46 + IDR = 47 + IRR = 48 + IMP = 49 + ILS = 50 + JMD = 51 + JPY = 52 + JEP = 53 + KZT = 54 + KPW = 55 + KRW = 56 + KGS = 57 + LAK = 58 + LVL = 59 + LBP = 60 + LRD = 61 + LTL = 62 + MKD = 63 + MYR = 64 + MUR = 65 + MXN = 66 + MNT = 67 + MZN = 68 + NAD = 69 + NPR = 70 + ANG = 71 + NZD = 72 + NIO = 73 + NGN = 74 + NOK = 75 + OMR = 76 + PKR = 77 + PAB = 78 + PYG = 79 + PEN = 80 + PHP = 81 + PLN = 82 + QAR = 83 + RON = 84 + RUB = 85 + SHP = 86 + SAR = 87 + RSD = 88 + SCR = 89 + SGD = 90 + SBD = 91 + SOS = 92 + ZAR = 93 + LKR = 94 + SEK = 95 + CHF = 96 + SRD = 97 + SYP = 98 + TWD = 99 + THB = 100 + TTD = 101 + TRY = 102 + TRL = 103 + TVD = 104 + UAH = 105 + GBP = 106 + USD = 107 + UYU = 108 + UZS = 109 + VEF = 110 + VND = 111 + YER = 112 + ZWD = 113 + + def __str__(self): + if self == Currency.EUR: + return '€' + else: + return self.name + + +@unique +class Continent(Enum): + """ + Continent codes. + + From `Data hub `_. + """ + + AF = 'Africa' + NA = 'North America' + OC = 'Oceania' + AN = 'Antartica' + AS = 'Asia' + EU = 'Europe' + SA = 'South America' + + +@unique +class Country(Enum): + """ + Countries as ISO 3166-1 alpha 2. + + Taken from table from `iso-3616-1 commit + 8e31d749b9ce331cfa50c280a29b04ae2d805b7e `_. + """ + + AF = "Afghanistan" + AX = "Åland Islands" + AL = "Albania" + DZ = "Algeria" + AS = "American Samoa" + AD = "Andorra" + AO = "Angola" + AI = "Anguilla" + AQ = "Antarctica" + AG = "Antigua and Barbuda" + AR = "Argentina" + AM = "Armenia" + AW = "Aruba" + AU = "Australia" + AT = "Austria" + AZ = "Azerbaijan" + BS = "Bahamas" + BH = "Bahrain" + BD = "Bangladesh" + BB = "Barbados" + BY = "Belarus" + BE = "Belgium" + BZ = "Belize" + BJ = "Benin" + BM = "Bermuda" + BT = "Bhutan" + BO = "Bolivia (Plurinational State of)" + BQ = "Bonaire, Sint Eustatius and Saba" + BA = "Bosnia and Herzegovina" + BW = "Botswana" + BV = "Bouvet Island" + BR = "Brazil" + IO = "British Indian Ocean Territory" + BN = "Brunei Darussalam" + BG = "Bulgaria" + BF = "Burkina Faso" + BI = "Burundi" + KH = "Cambodia" + CM = "Cameroon" + CA = "Canada" + CV = "Cabo Verde" + KY = "Cayman Islands" + CF = "Central African Republic" + TD = "Chad" + CL = "Chile" + CN = "China" + CX = "Christmas Island" + CC = "Cocos (Keeling) Islands" + CO = "Colombia" + KM = "Comoros" + CG = "Congo" + CD = "Congo (Democratic Republic of the)" + CK = "Cook Islands" + CR = "Costa Rica" + CI = "Côte d'Ivoire" + HR = "Croatia" + CU = "Cuba" + CW = "Curaçao" + CY = "Cyprus" + CZ = "Czech Republic" + DK = "Denmark" + DJ = "Djibouti" + DM = "Dominica" + DO = "Dominican Republic" + EC = "Ecuador" + EG = "Egypt" + SV = "El Salvador" + GQ = "Equatorial Guinea" + ER = "Eritrea" + EE = "Estonia" + ET = "Ethiopia" + FK = "Falkland Islands (Malvinas)" + FO = "Faroe Islands" + FJ = "Fiji" + FI = "Finland" + FR = "France" + GF = "French Guiana" + PF = "French Polynesia" + TF = "French Southern Territories" + GA = "Gabon" + GM = "Gambia" + GE = "Georgia" + DE = "Germany" + GH = "Ghana" + GI = "Gibraltar" + GR = "Greece" + GL = "Greenland" + GD = "Grenada" + GP = "Guadeloupe" + GU = "Guam" + GT = "Guatemala" + GG = "Guernsey" + GN = "Guinea" + GW = "Guinea-Bissau" + GY = "Guyana" + HT = "Haiti" + HM = "Heard Island and McDonald Islands" + VA = "Holy See" + HN = "Honduras" + HK = "Hong Kong" + HU = "Hungary" + IS = "Iceland" + IN = "India" + ID = "Indonesia" + IR = "Iran (Islamic Republic of)" + IQ = "Iraq" + IE = "Ireland" + IM = "Isle of Man" + IL = "Israel" + IT = "Italy" + JM = "Jamaica" + JP = "Japan" + JE = "Jersey" + JO = "Jordan" + KZ = "Kazakhstan" + KE = "Kenya" + KI = "Kiribati" + KP = "Korea (Democratic People's Republic of)" + KR = "Korea (Republic of)" + KW = "Kuwait" + KG = "Kyrgyzstan" + LA = "Lao People's Democratic Republic" + LV = "Latvia" + LB = "Lebanon" + LS = "Lesotho" + LR = "Liberia" + LY = "Libya" + LI = "Liechtenstein" + LT = "Lithuania" + LU = "Luxembourg" + MO = "Macao" + MK = "Macedonia (the former Yugoslav Republic of)" + MG = "Madagascar" + MW = "Malawi" + MY = "Malaysia" + MV = "Maldives" + ML = "Mali" + MT = "Malta" + MH = "Marshall Islands" + MQ = "Martinique" + MR = "Mauritania" + MU = "Mauritius" + YT = "Mayotte" + MX = "Mexico" + FM = "Micronesia (Federated States of)" + MD = "Moldova (Republic of)" + MC = "Monaco" + MN = "Mongolia" + ME = "Montenegro" + MS = "Montserrat" + MA = "Morocco" + MZ = "Mozambique" + MM = "Myanmar" + NA = "Namibia" + NR = "Nauru" + NP = "Nepal" + NL = "Netherlands" + NC = "New Caledonia" + NZ = "New Zealand" + NI = "Nicaragua" + NE = "Niger" + NG = "Nigeria" + NU = "Niue" + NF = "Norfolk Island" + MP = "Northern Mariana Islands" + NO = "Norway" + OM = "Oman" + PK = "Pakistan" + PW = "Palau" + PS = "Palestine, State of" + PA = "Panama" + PG = "Papua New Guinea" + PY = "Paraguay" + PE = "Peru" + PH = "Philippines" + PN = "Pitcairn" + PL = "Poland" + PT = "Portugal" + PR = "Puerto Rico" + QA = "Qatar" + RE = "Réunion" + RO = "Romania" + RU = "Russian Federation" + RW = "Rwanda" + BL = "Saint Barthélemy" + SH = "Saint Helena, Ascension and Tristan da Cunha" + KN = "Saint Kitts and Nevis" + LC = "Saint Lucia" + MF = "Saint Martin (French part)" + PM = "Saint Pierre and Miquelon" + VC = "Saint Vincent and the Grenadines" + WS = "Samoa" + SM = "San Marino" + ST = "Sao Tome and Principe" + SA = "Saudi Arabia" + SN = "Senegal" + RS = "Serbia" + SC = "Seychelles" + SL = "Sierra Leone" + SG = "Singapore" + SX = "Sint Maarten (Dutch part)" + SK = "Slovakia" + SI = "Slovenia" + SB = "Solomon Islands" + SO = "Somalia" + ZA = "South Africa" + GS = "South Georgia and the South Sandwich Islands" + SS = "South Sudan" + ES = "Spain" + LK = "Sri Lanka" + SD = "Sudan" + SR = "Suriname" + SJ = "Svalbard and Jan Mayen" + SZ = "Swaziland" + SE = "Sweden" + CH = "Switzerland" + SY = "Syrian Arab Republic" + TW = "Taiwan, Province of China" + TJ = "Tajikistan" + TZ = "Tanzania, United Republic of" + TH = "Thailand" + TL = "Timor-Leste" + TG = "Togo" + TK = "Tokelau" + TO = "Tonga" + TT = "Trinidad and Tobago" + TN = "Tunisia" + TR = "Turkey" + TM = "Turkmenistan" + TC = "Turks and Caicos Islands" + TV = "Tuvalu" + UG = "Uganda" + UA = "Ukraine" + AE = "United Arab Emirates" + GB = "United Kingdom of Great Britain and Northern Ireland" + US = "United States of America" + UM = "United States Minor Outlying Islands" + UY = "Uruguay" + UZ = "Uzbekistan" + VU = "Vanuatu" + VE = "Venezuela (Bolivarian Republic of)" + VN = "Viet Nam" + VG = "Virgin Islands (British)" + VI = "Virgin Islands (U.S.)" + WF = "Wallis and Futuna" + EH = "Western Sahara" + YE = "Yemen" + ZM = "Zambia" + ZW = "Zimbabwe" + + def __contains__(self, item: 'Subdivision'): + """Checks if a Subdivision is inside of this Country.""" + if not isinstance(item, Subdivision): + raise TypeError('Only subdivisions can be inside a country.') + return item.country == self + + def __str__(self): + return self.value + + +class SubdivisionMixin: + @property + def country(self: Enum) -> Country: + """Returns the Country of the Subdivision.""" + return Country[self.name[0:2]] + + +# noinspection PyArgumentList +Subdivision = Enum( + 'Subdivision', + type=SubdivisionMixin, + module=__name__, + names=( + 'AE-AJ', + 'AE-AZ', + 'AE-DU', + 'AE-FU', + 'AE-RK', + 'AE-SH', + 'AE-UQ', + 'AF-BAL', + 'AF-BAM', + 'AF-BDG', + 'AF-BDS', + 'AF-BGL', + 'AF-FRAU', + 'AF-FYB', + 'AF-GHA', + 'AF-GHO', + 'AF-HEL', + 'AF-HER', + 'AF-JOW', + 'AF-KAB', + 'AF-KANN', + 'AF-KAP', + 'AF-KDZ', + 'AF-KNR', + 'AF-LAG', + 'AF-LOW', + 'AF-NAN', + 'AF-NIM', + 'AF-ORU', + 'AF-PAR', + 'AF-PIA', + 'AF-PKA', + 'AF-SAM', + 'AF-SAR', + 'AF-TAK', + 'AF-WAR', + 'AF-ZAB', + 'AL-BR', + 'AL-BU', + 'AL-DI', + 'AL-DL', + 'AL-DR', + 'AL-DV', + 'AL-EL', + 'AL-ER', + 'AL-FR', + 'AL-GJ', + 'AL-GR', + 'AL-HA', + 'AL-KA', + 'AL-KB', + 'AL-KC', + 'AL-KO', + 'AL-KR', + 'AL-KU', + 'AL-LA', + 'AL-LB', + 'AL-LE', + 'AL-LU', + 'AL-MK', + 'AL-MM', + 'AL-MR', + 'AL-MT', + 'AL-PG', + 'AL-PQ', + 'AL-PR', + 'AL-PU', + 'AL-SH', + 'AL-SK', + 'AL-SR', + 'AL-TE', + 'AL-TP', + 'AL-TR', + 'AL-VL', + 'AM-AG', + 'AM-AR', + 'AM-AV', + 'AM-ER', + 'AM-GR', + 'AM-KT', + 'AM-LO', + 'AM-SH', + 'AM-SU', + 'AM-TV', + 'AM-VD', + 'AO-BGO', + 'AO-BGU', + 'AO-BIE', + 'AO-CAB', + 'AO-CCU', + 'AO-CNN', + 'AO-CNO', + 'AO-CUS', + 'AO-HUA', + 'AO-HUI', + 'AO-LNO', + 'AO-LSU', + 'AO-LUA', + 'AO-MAL', + 'AO-MOX', + 'AO-NAM', + 'AO-UIG', + 'AO-ZAI', + 'AR-A', + 'AR-B', + 'AR-C', + 'AR-D', + 'AR-E', + 'AR-F', + 'AR-G', + 'AR-H', + 'AR-J', + 'AR-K', + 'AR-L', + 'AR-M', + 'AR-N', + 'AR-P', + 'AR-Q', + 'AR-R', + 'AR-S', + 'AR-T', + 'AR-U', + 'AR-V', + 'AR-W', + 'AR-X', + 'AR-Y', + 'AR-Z', + 'AT-1', + 'AT-2', + 'AT-3', + 'AT-4', + 'AT-5', + 'AT-6', + 'AT-7', + 'AT-8', + 'AT-9', + 'AU-CT', + 'AU-NS', + 'AU-NT', + 'AU-QL', + 'AU-SA', + 'AU-TS', + 'AU-VI', + 'AU-WA', + 'AZ-AB', + 'AZ-ABS', + 'AZ-AGA', + 'AZ-AGC', + 'AZ-AGM', + 'AZ-AGS', + 'AZ-AGU', + 'AZ-AST', + 'AZ-BA', + 'AZ-BAB', + 'AZ-BAL', + 'AZ-BAR', + 'AZ-BEY', + 'AZ-BIL', + 'AZ-CAB', + 'AZ-CAL', + 'AZ-CUL', + 'AZ-DAS', + 'AZ-DAV', + 'AZ-FUZ', + 'AZ-GA', + 'AZ-GAD', + 'AZ-GOR', + 'AZ-GOY', + 'AZ-HAC', + 'AZ-IMI', + 'AZ-ISM', + 'AZ-KAL', + 'AZ-KUR', + 'AZ-LA', + 'AZ-LAC', + 'AZ-LAN', + 'AZ-LER', + 'AZ-MAS', + 'AZ-MI', + 'AZ-MM', + 'AZ-NA', + 'AZ-NEF', + 'AZ-OGU', + 'AZ-ORD', + 'AZ-QAB', + 'AZ-QAX', + 'AZ-QAZ', + 'AZ-QBA', + 'AZ-QBI', + 'AZ-QOB', + 'AZ-QUS', + 'AZ-SA', + 'AZ-SAB', + 'AZ-SAD', + 'AZ-SAH', + 'AZ-SAK', + 'AZ-SAL', + 'AZ-SAR', + 'AZ-SAT', + 'AZ-SIY', + 'AZ-SKR', + 'AZ-SM', + 'AZ-SMI', + 'AZ-SMX', + 'AZ-SS', + 'AZ-SUS', + 'AZ-TAR', + 'AZ-TOV', + 'AZ-UCA', + 'AZ-XA', + 'AZ-XAC', + 'AZ-XAN', + 'AZ-XCI', + 'AZ-XIZ', + 'AZ-XVD', + 'AZ-YAR', + 'AZ-YE', + 'AZ-YEV', + 'AZ-ZAN', + 'AZ-ZAQ', + 'AZ-ZAR', + 'BA-BIH', + 'BA-SRP', + 'BD-01', + 'BD-02', + 'BD-03', + 'BD-04', + 'BD-05', + 'BD-06', + 'BD-07', + 'BD-08', + 'BD-09', + 'BD-1', + 'BD-10', + 'BD-11', + 'BD-12', + 'BD-13', + 'BD-14', + 'BD-15', + 'BD-16', + 'BD-17', + 'BD-18', + 'BD-19', + 'BD-2', + 'BD-20', + 'BD-21', + 'BD-22', + 'BD-23', + 'BD-24', + 'BD-25', + 'BD-26', + 'BD-27', + 'BD-28', + 'BD-29', + 'BD-3', + 'BD-30', + 'BD-31', + 'BD-32', + 'BD-33', + 'BD-34', + 'BD-35', + 'BD-36', + 'BD-37', + 'BD-38', + 'BD-39', + 'BD-4', + 'BD-40', + 'BD-41', + 'BD-42', + 'BD-43', + 'BD-44', + 'BD-45', + 'BD-46', + 'BD-47', + 'BD-48', + 'BD-49', + 'BD-5', + 'BD-50', + 'BD-51', + 'BD-52', + 'BD-53', + 'BD-54', + 'BD-55', + 'BD-56', + 'BD-57', + 'BD-58', + 'BD-59', + 'BD-6', + 'BD-60', + 'BD-61', + 'BD-62', + 'BD-63', + 'BD-64', + 'BE-BRU', + 'BE-VAN', + 'BE-VBR', + 'BE-VLG', + 'BE-VLI', + 'BE-VOV', + 'BE-VWV', + 'BE-WAL', + 'BE-WBR', + 'BE-WHT', + 'BE-WLG', + 'BE-WLX', + 'BE-WNA', + 'BF-BAL', + 'BF-BAM', + 'BF-BAN', + 'BF-BAZ', + 'BF-BGR', + 'BF-BLG', + 'BF-BLK', + 'BF-COM', + 'BF-GAN', + 'BF-GNA', + 'BF-GOU', + 'BF-HOU', + 'BF-IOB', + 'BF-KAD', + 'BF-KEN', + 'BF-KMD', + 'BF-KMP', + 'BF-KOP', + 'BF-KOS', + 'BF-KOT', + 'BF-KOW', + 'BF-LER', + 'BF-LOR', + 'BF-MOU', + 'BF-NAM', + 'BF-NAO', + 'BF-NAY', + 'BF-NOU', + 'BF-OUB', + 'BF-OUD', + 'BF-PAS', + 'BF-PON', + 'BF-SEN', + 'BF-SIS', + 'BF-SMT', + 'BF-SNG', + 'BF-SOM', + 'BF-SOR', + 'BF-TAP', + 'BF-TUI', + 'BF-YAG', + 'BF-YAT', + 'BF-ZIR', + 'BF-ZON', + 'BF-ZOU', + 'BG-01', + 'BG-02', + 'BG-03', + 'BG-04', + 'BG-05', + 'BG-06', + 'BG-07', + 'BG-08', + 'BG-09', + 'BG-10', + 'BG-11', + 'BG-12', + 'BG-13', + 'BG-14', + 'BG-15', + 'BG-16', + 'BG-17', + 'BG-18', + 'BG-19', + 'BG-20', + 'BG-21', + 'BG-22', + 'BG-23', + 'BG-24', + 'BG-25', + 'BG-26', + 'BG-27', + 'BG-28', + 'BH-01', + 'BH-02', + 'BH-03', + 'BH-04', + 'BH-05', + 'BH-06', + 'BH-07', + 'BH-08', + 'BH-09', + 'BH-10', + 'BH-11', + 'BH-12', + 'BI-BB', + 'BI-BJ', + 'BI-BR', + 'BI-CA', + 'BI-CI', + 'BI-GI', + 'BI-KI', + 'BI-KR', + 'BI-KY', + 'BI-MA', + 'BI-MU', + 'BI-MW', + 'BI-MY', + 'BI-NG', + 'BI-RT', + 'BI-RY', + 'BJ-AK', + 'BJ-AL', + 'BJ-AQ', + 'BJ-BO', + 'BJ-CO', + 'BJ-DO', + 'BJ-KO', + 'BJ-LI', + 'BJ-MO', + 'BJ-OU', + 'BJ-PL', + 'BJ-ZO', + 'BN-BE', + 'BN-BM', + 'BN-TE', + 'BN-TU', + 'BO-B', + 'BO-C', + 'BO-H', + 'BO-L', + 'BO-N', + 'BO-O', + 'BO-P', + 'BO-S', + 'BO-T', + 'BR-AC', + 'BR-AL', + 'BR-AM', + 'BR-AP', + 'BR-BA', + 'BR-CE', + 'BR-DF', + 'BR-ES', + 'BR-GO', + 'BR-MA', + 'BR-MG', + 'BR-MS', + 'BR-MT', + 'BR-PA', + 'BR-PB', + 'BR-PE', + 'BR-PI', + 'BR-PR', + 'BR-RJ', + 'BR-RN', + 'BR-RO', + 'BR-RR', + 'BR-RS', + 'BR-SC', + 'BR-SE', + 'BR-SP', + 'BR-TO', + 'BS-AC', + 'BS-BI', + 'BS-CI', + 'BS-EX', + 'BS-FC', + 'BS-FP', + 'BS-GH', + 'BS-GT', + 'BS-HI', + 'BS-HR', + 'BS-IN', + 'BS-KB', + 'BS-LI', + 'BS-MG', + 'BS-MH', + 'BS-NB', + 'BS-NP', + 'BS-RI', + 'BS-RS', + 'BS-SP', + 'BS-SR', + 'BT-11', + 'BT-12', + 'BT-13', + 'BT-14', + 'BT-15', + 'BT-21', + 'BT-22', + 'BT-23', + 'BT-24', + 'BT-31', + 'BT-32', + 'BT-33', + 'BT-34', + 'BT-41', + 'BT-42', + 'BT-43', + 'BT-44', + 'BT-45', + 'BT-GA', + 'BT-TY', + 'BW-CE', + 'BW-CH', + 'BW-GH', + 'BW-KG', + 'BW-KL', + 'BW-KW', + 'BW-NE', + 'BW-NG', + 'BW-SE', + 'BW-SO', + 'BY-BR', + 'BY-HO', + 'BY-HR', + 'BY-MA', + 'BY-MI', + 'BY-VI', + 'BZ-BZ', + 'BZ-CY', + 'BZ-CZL', + 'BZ-OW', + 'BZ-SC', + 'BZ-TOL', + 'CA-AB', + 'CA-BC', + 'CA-MB', + 'CA-NB', + 'CA-NL', + 'CA-NS', + 'CA-NT', + 'CA-NU', + 'CA-ON', + 'CA-PE', + 'CA-QC', + 'CA-SK', + 'CA-YT', + 'CD-BC', + 'CD-BN', + 'CD-EQ', + 'CD-KA', + 'CD-KE', + 'CD-KN', + 'CD-KW', + 'CD-MA', + 'CD-NK', + 'CD-OR', + 'CD-SK', + 'CF-AC', + 'CF-BB', + 'CF-BGF', + 'CF-BK', + 'CF-HK', + 'CF-HM', + 'CF-HS', + 'CF-KB', + 'CF-KG', + 'CF-LB', + 'CF-MB', + 'CF-MP', + 'CF-NM', + 'CF-OP', + 'CF-SE', + 'CF-UK', + 'CF-VK', + 'CG-11', + 'CG-12', + 'CG-13', + 'CG-14', + 'CG-15', + 'CG-2', + 'CG-5', + 'CG-7', + 'CG-8', + 'CG-9', + 'CG-BZV', + 'CH-AG', + 'CH-AI', + 'CH-AR', + 'CH-BE', + 'CH-BL', + 'CH-BS', + 'CH-FR', + 'CH-GE', + 'CH-GL', + 'CH-GR', + 'CH-JU', + 'CH-LU', + 'CH-NE', + 'CH-NW', + 'CH-OW', + 'CH-SG', + 'CH-SH', + 'CH-SO', + 'CH-SZ', + 'CH-TG', + 'CH-TI', + 'CH-UR', + 'CH-VD', + 'CH-VS', + 'CH-ZG', + 'CH-ZH', + 'CI-01', + 'CI-02', + 'CI-03', + 'CI-04', + 'CI-05', + 'CI-06', + 'CI-07', + 'CI-08', + 'CI-09', + 'CI-10', + 'CI-11', + 'CI-12', + 'CI-13', + 'CI-14', + 'CI-15', + 'CI-16', + 'CL-AI', + 'CL-AN', + 'CL-AR', + 'CL-AT', + 'CL-BI', + 'CL-CO', + 'CL-LI', + 'CL-LL', + 'CL-MA', + 'CL-ML', + 'CL-RM', + 'CL-TA', + 'CL-VS', + 'CM-AD', + 'CM-CE', + 'CM-EN', + 'CM-ES', + 'CM-LT', + 'CM-NO', + 'CM-NW', + 'CM-OU', + 'CM-SU', + 'CM-SW', + 'CN-11', + 'CN-12', + 'CN-13', + 'CN-14', + 'CN-15', + 'CN-21', + 'CN-22', + 'CN-23', + 'CN-31', + 'CN-32', + 'CN-33', + 'CN-34', + 'CN-35', + 'CN-36', + 'CN-37', + 'CN-41', + 'CN-42', + 'CN-43', + 'CN-44', + 'CN-45', + 'CN-46', + 'CN-50', + 'CN-51', + 'CN-52', + 'CN-53', + 'CN-54', + 'CN-61', + 'CN-62', + 'CN-63', + 'CN-64', + 'CN-65', + 'CN-71', + 'CN-91', + 'CN-92', + 'CO-AMA', + 'CO-ANT', + 'CO-ARA', + 'CO-ATL', + 'CO-BOL', + 'CO-BOY', + 'CO-CAL', + 'CO-CAQ', + 'CO-CAS', + 'CO-CAU', + 'CO-CES', + 'CO-CHO', + 'CO-COR', + 'CO-CUN', + 'CO-DC', + 'CO-GUA', + 'CO-GUV', + 'CO-HUI', + 'CO-LAG', + 'CO-MAG', + 'CO-MET', + 'CO-NAR', + 'CO-NSA', + 'CO-PUT', + 'CO-QUI', + 'CO-RIS', + 'CO-SAN', + 'CO-SAP', + 'CO-SUC', + 'CO-TOL', + 'CO-VAC', + 'CO-VAU', + 'CO-VID', + 'CR-A', + 'CR-C', + 'CR-G', + 'CR-H', + 'CR-L', + 'CR-P', + 'CR-SJ', + 'CU-01', + 'CU-02', + 'CU-03', + 'CU-04', + 'CU-05', + 'CU-06', + 'CU-07', + 'CU-08', + 'CU-09', + 'CU-10', + 'CU-11', + 'CU-12', + 'CU-13', + 'CU-14', + 'CU-99', + 'CV-B', + 'CV-BR', + 'CV-BV', + 'CV-CA', + 'CV-CR', + 'CV-CS', + 'CV-FO', + 'CV-MA', + 'CV-MO', + 'CV-PA', + 'CV-PN', + 'CV-PR', + 'CV-RG', + 'CV-S', + 'CV-SF', + 'CV-SL', + 'CV-SN', + 'CV-SV', + 'CV-TA', + 'CY-01', + 'CY-02', + 'CY-03', + 'CY-04', + 'CY-05', + 'CY-06', + 'CZ-JC', + 'CZ-JM', + 'CZ-KA', + 'CZ-KR', + 'CZ-LI', + 'CZ-MO', + 'CZ-OL', + 'CZ-PA', + 'CZ-PL', + 'CZ-PR', + 'CZ-ST', + 'CZ-US', + 'CZ-VY', + 'CZ-ZL', + 'DE-BB', + 'DE-BE', + 'DE-BW', + 'DE-BY', + 'DE-HB', + 'DE-HE', + 'DE-HH', + 'DE-MV', + 'DE-NI', + 'DE-NW', + 'DE-RP', + 'DE-SH', + 'DE-SL', + 'DE-SN', + 'DE-ST', + 'DE-TH', + 'DJ-AS', + 'DJ-DI', + 'DJ-DJ', + 'DJ-OB', + 'DJ-TA', + 'DK-015', + 'DK-020', + 'DK-025', + 'DK-030', + 'DK-035', + 'DK-040', + 'DK-042', + 'DK-050', + 'DK-055', + 'DK-060', + 'DK-065', + 'DK-070', + 'DK-076', + 'DK-080', + 'DK-101', + 'DK-147', + 'DO-01', + 'DO-02', + 'DO-03', + 'DO-04', + 'DO-05', + 'DO-06', + 'DO-07', + 'DO-08', + 'DO-09', + 'DO-10', + 'DO-11', + 'DO-12', + 'DO-13', + 'DO-14', + 'DO-15', + 'DO-16', + 'DO-17', + 'DO-18', + 'DO-19', + 'DO-20', + 'DO-21', + 'DO-22', + 'DO-23', + 'DO-24', + 'DO-25', + 'DO-26', + 'DO-27', + 'DO-28', + 'DO-29', + 'DO-30', + 'DZ-01', + 'DZ-02', + 'DZ-03', + 'DZ-04', + 'DZ-05', + 'DZ-06', + 'DZ-07', + 'DZ-08', + 'DZ-09', + 'DZ-10', + 'DZ-11', + 'DZ-12', + 'DZ-13', + 'DZ-14', + 'DZ-15', + 'DZ-16', + 'DZ-17', + 'DZ-18', + 'DZ-19', + 'DZ-20', + 'DZ-21', + 'DZ-22', + 'DZ-23', + 'DZ-24', + 'DZ-25', + 'DZ-26', + 'DZ-27', + 'DZ-28', + 'DZ-29', + 'DZ-30', + 'DZ-31', + 'DZ-32', + 'DZ-33', + 'DZ-34', + 'DZ-35', + 'DZ-36', + 'DZ-37', + 'DZ-38', + 'DZ-39', + 'DZ-40', + 'DZ-41', + 'DZ-42', + 'DZ-43', + 'DZ-44', + 'DZ-45', + 'DZ-46', + 'DZ-47', + 'DZ-48', + 'EC-A', + 'EC-B', + 'EC-C', + 'EC-D', + 'EC-E', + 'EC-F', + 'EC-G', + 'EC-H', + 'EC-I', + 'EC-L', + 'EC-M', + 'EC-N', + 'EC-O', + 'EC-P', + 'EC-R', + 'EC-S', + 'EC-T', + 'EC-U', + 'EC-W', + 'EC-X', + 'EC-Y', + 'EC-Z', + 'EE-37', + 'EE-39', + 'EE-44', + 'EE-49', + 'EE-51', + 'EE-57', + 'EE-59', + 'EE-65', + 'EE-67', + 'EE-70', + 'EE-74', + 'EE-78', + 'EE-82', + 'EE-84', + 'EE-86', + 'EG-ALX', + 'EG-ASN', + 'EG-AST', + 'EG-BA', + 'EG-BH', + 'EG-BNS', + 'EG-C', + 'EG-DK', + 'EG-DT', + 'EG-FYM', + 'EG-GH', + 'EG-GZ', + 'EG-IS', + 'EG-JS', + 'EG-KB', + 'EG-KFS', + 'EG-KN', + 'EG-MN', + 'EG-MNF', + 'EG-MT', + 'EG-PTS', + 'EG-SHG', + 'EG-SHR', + 'EG-SIN', + 'EG-SUZ', + 'EG-WAD', + 'ER-AN', + 'ER-DK', + 'ER-DU', + 'ER-GB', + 'ER-MA', + 'ER-SK', + 'ES-A', + 'ES-AB', + 'ES-AL', + 'ES-AN', + 'ES-AR', + 'ES-AV', + 'ES-B', + 'ES-BA', + 'ES-BI', + 'ES-BU', + 'ES-C', + 'ES-CA', + 'ES-CC', + 'ES-CE', + 'ES-CL', + 'ES-CM', + 'ES-CN', + 'ES-CO', + 'ES-CR', + 'ES-CS', + 'ES-CT', + 'ES-CU', + 'ES-EX', + 'ES-GA', + 'ES-GC', + 'ES-GI', + 'ES-GR', + 'ES-GU', + 'ES-H', + 'ES-HU', + 'ES-J', + 'ES-L', + 'ES-LE', + 'ES-LO', + 'ES-LU', + 'ES-M', + 'ES-MA', + 'ES-ML', + 'ES-MU', + 'ES-NA', + 'ES-O', + 'ES-OR', + 'ES-P', + 'ES-PM', + 'ES-PO', + 'ES-PV', + 'ES-S', + 'ES-SA', + 'ES-SE', + 'ES-SG', + 'ES-SO', + 'ES-SS', + 'ES-T', + 'ES-TE', + 'ES-TF', + 'ES-TO', + 'ES-V', + 'ES-VA', + 'ES-VC', + 'ES-VI', + 'ES-Z', + 'ES-ZA', + 'ET-AA', + 'ET-AF', + 'ET-AM', + 'ET-BE', + 'ET-DD', + 'ET-GA', + 'ET-HA', + 'ET-OR', + 'ET-SN', + 'ET-SO', + 'ET-TI', + 'FI-AL', + 'FI-ES', + 'FI-IS', + 'FI-LL', + 'FI-LS', + 'FI-OL', + 'FJ-C', + 'FJ-E', + 'FJ-N', + 'FJ-R', + 'FJ-W', + 'FM-KSA', + 'FM-PNI', + 'FM-TRK', + 'FM-YAP', + 'FR-01', + 'FR-02', + 'FR-03', + 'FR-04', + 'FR-05', + 'FR-06', + 'FR-07', + 'FR-08', + 'FR-09', + 'FR-10', + 'FR-11', + 'FR-12', + 'FR-13', + 'FR-14', + 'FR-15', + 'FR-16', + 'FR-17', + 'FR-18', + 'FR-19', + 'FR-21', + 'FR-22', + 'FR-23', + 'FR-24', + 'FR-25', + 'FR-26', + 'FR-27', + 'FR-28', + 'FR-29', + 'FR-2A', + 'FR-2B', + 'FR-30', + 'FR-31', + 'FR-32', + 'FR-33', + 'FR-34', + 'FR-35', + 'FR-36', + 'FR-37', + 'FR-38', + 'FR-39', + 'FR-40', + 'FR-41', + 'FR-42', + 'FR-43', + 'FR-44', + 'FR-45', + 'FR-46', + 'FR-47', + 'FR-48', + 'FR-49', + 'FR-50', + 'FR-51', + 'FR-52', + 'FR-53', + 'FR-54', + 'FR-55', + 'FR-56', + 'FR-57', + 'FR-58', + 'FR-59', + 'FR-60', + 'FR-61', + 'FR-62', + 'FR-63', + 'FR-64', + 'FR-65', + 'FR-66', + 'FR-67', + 'FR-68', + 'FR-69', + 'FR-70', + 'FR-71', + 'FR-72', + 'FR-73', + 'FR-74', + 'FR-75', + 'FR-76', + 'FR-77', + 'FR-78', + 'FR-79', + 'FR-80', + 'FR-81', + 'FR-82', + 'FR-83', + 'FR-84', + 'FR-85', + 'FR-86', + 'FR-87', + 'FR-88', + 'FR-89', + 'FR-90', + 'FR-91', + 'FR-92', + 'FR-93', + 'FR-94', + 'FR-95', + 'FR-A', + 'FR-B', + 'FR-C', + 'FR-D', + 'FR-E', + 'FR-F', + 'FR-G', + 'FR-GF', + 'FR-GP', + 'FR-H', + 'FR-I', + 'FR-J', + 'FR-K', + 'FR-L', + 'FR-M', + 'FR-MQ', + 'FR-N', + 'FR-NC', + 'FR-O', + 'FR-P', + 'FR-PF', + 'FR-PM', + 'FR-Q', + 'FR-R', + 'FR-RE', + 'FR-S', + 'FR-T', + 'FR-TF', + 'FR-U', + 'FR-V', + 'FR-WF', + 'FR-YT', + 'GA-1', + 'GA-2', + 'GA-3', + 'GA-4', + 'GA-5', + 'GA-6', + 'GA-7', + 'GA-8', + 'GA-9', + 'GB-ABD', + 'GB-ABE', + 'GB-AGB', + 'GB-AGY', + 'GB-ANS', + 'GB-ANT', + 'GB-ARD', + 'GB-ARM', + 'GB-BAS', + 'GB-BBD', + 'GB-BDF', + 'GB-BDG', + 'GB-BEN', + 'GB-BEX', + 'GB-BFS', + 'GB-BGE', + 'GB-BGW', + 'GB-BIR', + 'GB-BKM', + 'GB-BLA', + 'GB-BLY', + 'GB-BMH', + 'GB-BNB', + 'GB-BNE', + 'GB-BNH', + 'GB-BNS', + 'GB-BOL', + 'GB-BPL', + 'GB-BRC', + 'GB-BRD', + 'GB-BRY', + 'GB-BST', + 'GB-BUR', + 'GB-CAM', + 'GB-CAY', + 'GB-CGN', + 'GB-CGV', + 'GB-CHA', + 'GB-CHS', + 'GB-CKF', + 'GB-CKT', + 'GB-CLD', + 'GB-CLK', + 'GB-CLR', + 'GB-CMA', + 'GB-CMD', + 'GB-CMN', + 'GB-CON', + 'GB-COV', + 'GB-CRF', + 'GB-CRY', + 'GB-CSR', + 'GB-CWY', + 'GB-DAL', + 'GB-DBY', + 'GB-DEN', + 'GB-DER', + 'GB-DEV', + 'GB-DGN', + 'GB-DGY', + 'GB-DNC', + 'GB-DND', + 'GB-DOR', + 'GB-DOW', + 'GB-DRY', + 'GB-DUD', + 'GB-DUR', + 'GB-EAL', + 'GB-EAW', + 'GB-EAY', + 'GB-EDH', + 'GB-EDU', + 'GB-ELN', + 'GB-ELS', + 'GB-ENF', + 'GB-ENG', + 'GB-ERW', + 'GB-ERY', + 'GB-ESS', + 'GB-ESX', + 'GB-FAL', + 'GB-FER', + 'GB-FIF', + 'GB-FLN', + 'GB-GAT', + 'GB-GBN', + 'GB-GLG', + 'GB-GLS', + 'GB-GRE', + 'GB-GSY', + 'GB-GWN', + 'GB-HAL', + 'GB-HAM', + 'GB-HAV', + 'GB-HCK', + 'GB-HEF', + 'GB-HIL', + 'GB-HLD', + 'GB-HMF', + 'GB-HNS', + 'GB-HPL', + 'GB-HRT', + 'GB-HRW', + 'GB-HRY', + 'GB-IOM', + 'GB-IOS', + 'GB-IOW', + 'GB-ISL', + 'GB-IVC', + 'GB-JSY', + 'GB-KEC', + 'GB-KEN', + 'GB-KHL', + 'GB-KIR', + 'GB-KTT', + 'GB-KWL', + 'GB-LAN', + 'GB-LBH', + 'GB-LCE', + 'GB-LDS', + 'GB-LEC', + 'GB-LEW', + 'GB-LIN', + 'GB-LIV', + 'GB-LMV', + 'GB-LND', + 'GB-LRN', + 'GB-LSB', + 'GB-LUT', + 'GB-MAN', + 'GB-MDB', + 'GB-MDW', + 'GB-MFT', + 'GB-MIK', + 'GB-MLN', + 'GB-MON', + 'GB-MRT', + 'GB-MRY', + 'GB-MTY', + 'GB-MYL', + 'GB-NAY', + 'GB-NBL', + 'GB-NDN', + 'GB-NEL', + 'GB-NET', + 'GB-NFK', + 'GB-NGM', + 'GB-NIR', + 'GB-NLK', + 'GB-NLN', + 'GB-NSM', + 'GB-NTA', + 'GB-NTH', + 'GB-NTL', + 'GB-NTT', + 'GB-NTY', + 'GB-NWM', + 'GB-NWP', + 'GB-NYK', + 'GB-NYM', + 'GB-OLD', + 'GB-OMH', + 'GB-ORK', + 'GB-OXF', + 'GB-PEM', + 'GB-PKN', + 'GB-PLY', + 'GB-POL', + 'GB-POR', + 'GB-POW', + 'GB-PTE', + 'GB-RCC', + 'GB-RCH', + 'GB-RCT', + 'GB-RDB', + 'GB-RDG', + 'GB-RFW', + 'GB-RIC', + 'GB-ROT', + 'GB-RUT', + 'GB-SAW', + 'GB-SAY', + 'GB-SCB', + 'GB-SCT', + 'GB-SFK', + 'GB-SFT', + 'GB-SGC', + 'GB-SHF', + 'GB-SHN', + 'GB-SHR', + 'GB-SKP', + 'GB-SLF', + 'GB-SLG', + 'GB-SLK', + 'GB-SND', + 'GB-SOL', + 'GB-SOM', + 'GB-SOS', + 'GB-SRY', + 'GB-STB', + 'GB-STE', + 'GB-STG', + 'GB-STH', + 'GB-STN', + 'GB-STS', + 'GB-STT', + 'GB-STY', + 'GB-SWA', + 'GB-SWD', + 'GB-SWK', + 'GB-TAM', + 'GB-TFW', + 'GB-THR', + 'GB-TOB', + 'GB-TOF', + 'GB-TRF', + 'GB-TWH', + 'GB-UKM', + 'GB-VGL', + 'GB-WAR', + 'GB-WBK', + 'GB-WDU', + 'GB-WFT', + 'GB-WGN', + 'GB-WILL', + 'GB-WKF', + 'GB-WLL', + 'GB-WLN', + 'GB-WLS', + 'GB-WLV', + 'GB-WND', + 'GB-WNM', + 'GB-WOK', + 'GB-WOR', + 'GB-WRL', + 'GB-WRT', + 'GB-WRX', + 'GB-WSM', + 'GB-WSX', + 'GB-YOR', + 'GB-ZET', + 'GE-AB', + 'GE-AJ', + 'GE-GU', + 'GE-IM', + 'GE-KA', + 'GE-KK', + 'GE-MM', + 'GE-RL', + 'GE-SJ', + 'GE-SK', + 'GE-SZ', + 'GE-TB', + 'GH-AA', + 'GH-AH', + 'GH-BA', + 'GH-CP', + 'GH-EP', + 'GH-NP', + 'GH-TV', + 'GH-UE', + 'GH-UW', + 'GH-WP', + 'GM-B', + 'GM-L', + 'GM-M', + 'GM-N', + 'GM-U', + 'GM-W', + 'GN-B', + 'GN-BE', + 'GN-BF', + 'GN-BK', + 'GN-C', + 'GN-CO', + 'GN-D', + 'GN-DB', + 'GN-DI', + 'GN-DL', + 'GN-DU', + 'GN-F', + 'GN-FA', + 'GN-FO', + 'GN-FR', + 'GN-GA', + 'GN-GU', + 'GN-K', + 'GN-KA', + 'GN-KB', + 'GN-KD; 2', + 'GN-KE', + 'GN-KN', + 'GN-KO', + 'GN-KS', + 'GN-L', + 'GN-LA', + 'GN-LE', + 'GN-LO', + 'GN-M', + 'GN-MC', + 'GN-MD', + 'GN-ML', + 'GN-MM', + 'GN-N', + 'GN-NZ', + 'GN-PI', + 'GN-SI', + 'GN-TE', + 'GN-TO', + 'GN-YO', + 'GQ-AN', + 'GQ-BN', + 'GQ-BS', + 'GQ-C', + 'GQ-CS', + 'GQ-I', + 'GQ-KN', + 'GQ-LI', + 'GQ-WN', + 'GR-01', + 'GR-03', + 'GR-04', + 'GR-05', + 'GR-06', + 'GR-07', + 'GR-11', + 'GR-12', + 'GR-13', + 'GR-14', + 'GR-15', + 'GR-16', + 'GR-17', + 'GR-21', + 'GR-22', + 'GR-23', + 'GR-24', + 'GR-31', + 'GR-32', + 'GR-33', + 'GR-34', + 'GR-41', + 'GR-42', + 'GR-43', + 'GR-44', + 'GR-51', + 'GR-52', + 'GR-53', + 'GR-54', + 'GR-55', + 'GR-56', + 'GR-57', + 'GR-58', + 'GR-59', + 'GR-61', + 'GR-62', + 'GR-63', + 'GR-64', + 'GR-69', + 'GR-71', + 'GR-72', + 'GR-73', + 'GR-81', + 'GR-82', + 'GR-83', + 'GR-84', + 'GR-85', + 'GR-91', + 'GR-92', + 'GR-93', + 'GR-94', + 'GR-A1', + 'GR-I', + 'GR-II', + 'GR-III', + 'GR-IV', + 'GR-IX', + 'GR-V', + 'GR-VI', + 'GR-VII', + 'GR-VIII', + 'GR-X', + 'GR-XI', + 'GR-XII', + 'GR-XIII', + 'GT-AV', + 'GT-BV', + 'GT-CM', + 'GT-CQ', + 'GT-ES', + 'GT-GU', + 'GT-HU', + 'GT-IZ', + 'GT-JA', + 'GT-JU', + 'GT-PE', + 'GT-PR', + 'GT-QC', + 'GT-QZ', + 'GT-RE', + 'GT-SA', + 'GT-SM', + 'GT-SO', + 'GT-SR', + 'GT-SU', + 'GT-TO', + 'GT-ZA', + 'GW-BA', + 'GW-BL', + 'GW-BM', + 'GW-BS', + 'GW-CA', + 'GW-GA', + 'GW-L', + 'GW-N', + 'GW-OI', + 'GW-QU', + 'GW-S', + 'GW-TO', + 'GY-BA', + 'GY-CU', + 'GY-DE', + 'GY-EB', + 'GY-ES', + 'GY-MA', + 'GY-PM', + 'GY-PT', + 'GY-UD', + 'GY-UT', + 'HN-AT', + 'HN-CH', + 'HN-CL', + 'HN-CM', + 'HN-CP', + 'HN-CR', + 'HN-EP', + 'HN-FM', + 'HN-GD', + 'HN-IB', + 'HN-IN', + 'HN-LE', + 'HN-LP', + 'HN-OC', + 'HN-OL', + 'HN-SB', + 'HN-VA', + 'HN-YO', + 'HR-01', + 'HR-02', + 'HR-03', + 'HR-04', + 'HR-05', + 'HR-06', + 'HR-07', + 'HR-08', + 'HR-09', + 'HR-10', + 'HR-11', + 'HR-12', + 'HR-13', + 'HR-14', + 'HR-15', + 'HR-16', + 'HR-17', + 'HR-18', + 'HR-19', + 'HR-20', + 'HR-21', + 'HT-AR', + 'HT-CE', + 'HT-GA', + 'HT-ND', + 'HT-NE', + 'HT-NO', + 'HT-OU', + 'HT-SD', + 'HT-SE', + 'HU-BA', + 'HU-BC', + 'HU-BE', + 'HU-BK', + 'HU-BU', + 'HU-BZ', + 'HU-CS', + 'HU-DE', + 'HU-DU', + 'HU-EG', + 'HU-FE', + 'HU-GS', + 'HU-GY', + 'HU-HB', + 'HU-HE', + 'HU-HV', + 'HU-JN', + 'HU-KE', + 'HU-KM', + 'HU-KV', + 'HU-MI', + 'HU-NK', + 'HU-NO', + 'HU-NY', + 'HU-PE', + 'HU-PS', + 'HU-SD', + 'HU-SF', + 'HU-SH', + 'HU-SK', + 'HU-SN', + 'HU-SO', + 'HU-SS', + 'HU-ST', + 'HU-SZ', + 'HU-TB', + 'HU-TO', + 'HU-VA', + 'HU-VE', + 'HU-VM', + 'HU-ZA', + 'HU-ZE', + 'ID-AC', + 'ID-BA', + 'ID-BB', + 'ID-BE', + 'ID-BT', + 'ID-GO', + 'ID-IJ', + 'ID-JA', + 'ID-JB', + 'ID-JI', + 'ID-JK', + 'ID-JT', + 'ID-JW', + 'ID-KA', + 'ID-KB', + 'ID-KI', + 'ID-KS', + 'ID-KT', + 'ID-LA', + 'ID-MA', + 'ID-MU', + 'ID-NB', + 'ID-NT', + 'ID-NU', + 'ID-PA', + 'ID-RI', + 'ID-SA', + 'ID-SB', + 'ID-SG', + 'ID-SL', + 'ID-SM', + 'ID-SN', + 'ID-SS', + 'ID-ST', + 'ID-SU', + 'ID-YO', + 'IE-C', + 'IE-C; 2', + 'IE-CE', + 'IE-CN', + 'IE-CW', + 'IE-D', + 'IE-DL', + 'IE-G', + 'IE-KE', + 'IE-KK', + 'IE-KY', + 'IE-L', + 'IE-LD', + 'IE-LH', + 'IE-LK', + 'IE-LM', + 'IE-LS', + 'IE-M', + 'IE-MH', + 'IE-MN', + 'IE-MO', + 'IE-OY', + 'IE-RN', + 'IE-SO', + 'IE-TA', + 'IE-U', + 'IE-WD', + 'IE-WH', + 'IE-WW', + 'IE-WX', + 'IL-D', + 'IL-HA', + 'IL-JM', + 'IL-M', + 'IL-TA', + 'IL-Z', + 'IN-AN', + 'IN-AP', + 'IN-AR', + 'IN-AS', + 'IN-BR', + 'IN-CH', + 'IN-CT', + 'IN-DD', + 'IN-DL', + 'IN-DN', + 'IN-GA', + 'IN-GJ', + 'IN-HP', + 'IN-HR', + 'IN-JH', + 'IN-JK', + 'IN-KA', + 'IN-KL', + 'IN-LD', + 'IN-MH', + 'IN-ML', + 'IN-MN', + 'IN-MP', + 'IN-MZ', + 'IN-NL', + 'IN-OR', + 'IN-PB', + 'IN-PY', + 'IN-RJ', + 'IN-SK', + 'IN-TN', + 'IN-TR', + 'IN-UL', + 'IN-UP', + 'IN-WB', + 'IQ-AN', + 'IQ-AR', + 'IQ-BA', + 'IQ-BB', + 'IQ-BG', + 'IQ-DA', + 'IQ-DI', + 'IQ-DQ', + 'IQ-KA', + 'IQ-MA', + 'IQ-MU', + 'IQ-NA', + 'IQ-NI', + 'IQ-QA', + 'IQ-SD', + 'IQ-SU', + 'IQ-TS', + 'IQ-WA', + 'IR-01', + 'IR-02', + 'IR-03', + 'IR-04', + 'IR-05', + 'IR-06', + 'IR-07', + 'IR-08', + 'IR-09', + 'IR-10', + 'IR-11', + 'IR-12', + 'IR-13', + 'IR-14', + 'IR-15', + 'IR-16', + 'IR-17', + 'IR-18', + 'IR-19', + 'IR-20', + 'IR-21', + 'IR-22', + 'IR-23', + 'IR-24', + 'IR-25', + 'IR-26', + 'IR-27', + 'IR-28', + 'IS-0', + 'IS-1', + 'IS-2', + 'IS-3', + 'IS-4', + 'IS-5', + 'IS-6', + 'IS-7', + 'IS-8', + 'IT-21', + 'IT-23', + 'IT-25', + 'IT-32', + 'IT-34', + 'IT-36', + 'IT-42', + 'IT-45', + 'IT-52', + 'IT-55', + 'IT-57', + 'IT-62', + 'IT-65', + 'IT-67', + 'IT-72', + 'IT-75', + 'IT-77', + 'IT-78', + 'IT-82', + 'IT-88', + 'IT-AG', + 'IT-AL', + 'IT-AN', + 'IT-AO', + 'IT-AP', + 'IT-AQ', + 'IT-AR', + 'IT-AT', + 'IT-AV', + 'IT-BA', + 'IT-BG', + 'IT-BI', + 'IT-BL', + 'IT-BN', + 'IT-BO', + 'IT-BR', + 'IT-BS', + 'IT-BZ', + 'IT-CA', + 'IT-CB', + 'IT-CE', + 'IT-CH', + 'IT-CL', + 'IT-CN', + 'IT-CO', + 'IT-CR', + 'IT-CS', + 'IT-CT', + 'IT-CZ', + 'IT-DU', + 'IT-EN', + 'IT-FE', + 'IT-FG', + 'IT-FI', + 'IT-FO', + 'IT-FR', + 'IT-GE', + 'IT-GO', + 'IT-GR', + 'IT-IM', + 'IT-IS', + 'IT-KR', + 'IT-LC', + 'IT-LE', + 'IT-LI', + 'IT-LO', + 'IT-LT', + 'IT-LU', + 'IT-MC', + 'IT-ME', + 'IT-MI', + 'IT-MN', + 'IT-MO', + 'IT-MS', + 'IT-MT', + 'IT-NA', + 'IT-NO', + 'IT-NU', + 'IT-OR', + 'IT-PA', + 'IT-PC', + 'IT-PD', + 'IT-PE', + 'IT-PG', + 'IT-PI', + 'IT-PN', + 'IT-PO', + 'IT-PR', + 'IT-PS', + 'IT-PT', + 'IT-PV', + 'IT-PZ', + 'IT-RA', + 'IT-RC', + 'IT-RE', + 'IT-RG', + 'IT-RI', + 'IT-RM', + 'IT-RN', + 'IT-RO', + 'IT-SA', + 'IT-SI', + 'IT-SO', + 'IT-SP', + 'IT-SR', + 'IT-SS', + 'IT-SV', + 'IT-TA', + 'IT-TE', + 'IT-TN', + 'IT-TO', + 'IT-TP', + 'IT-TR', + 'IT-TS', + 'IT-TV', + 'IT-VA', + 'IT-VB', + 'IT-VC', + 'IT-VE', + 'IT-VI', + 'IT-VR', + 'IT-VT', + 'IT-VV', + 'JM-01', + 'JM-02', + 'JM-03', + 'JM-04', + 'JM-05', + 'JM-06', + 'JM-07', + 'JM-08', + 'JM-09', + 'JM-10', + 'JM-11', + 'JM-12', + 'JM-13', + 'JM-14', + 'JO-AJ', + 'JO-AM', + 'JO-AQ', + 'JO-AT', + 'JO-AZ', + 'JO-BA', + 'JO-IR', + 'JO-JA', + 'JO-KA', + 'JO-MA', + 'JO-MD', + 'JO-MN', + 'JP-01', + 'JP-02', + 'JP-03', + 'JP-04', + 'JP-05', + 'JP-06', + 'JP-07', + 'JP-08', + 'JP-09', + 'JP-10', + 'JP-11', + 'JP-12', + 'JP-13', + 'JP-14', + 'JP-15', + 'JP-16', + 'JP-17', + 'JP-18', + 'JP-19', + 'JP-20', + 'JP-21', + 'JP-22', + 'JP-23', + 'JP-24', + 'JP-25', + 'JP-26', + 'JP-27', + 'JP-28', + 'JP-29', + 'JP-30', + 'JP-31', + 'JP-32', + 'JP-33', + 'JP-34', + 'JP-35', + 'JP-36', + 'JP-37', + 'JP-38', + 'JP-39', + 'JP-40', + 'JP-41', + 'JP-42', + 'JP-43', + 'JP-44', + 'JP-45', + 'JP-46', + 'JP-47', + 'KE-110', + 'KE-200', + 'KE-300', + 'KE-400', + 'KE-500', + 'KE-600', + 'KE-700', + 'KE-900', + 'KG-B', + 'KG-C', + 'KG-GB', + 'KG-J', + 'KG-N', + 'KG-O', + 'KG-T', + 'KG-Y', + 'KH-1', + 'KH-10', + 'KH-11', + 'KH-12', + 'KH-13', + 'KH-14', + 'KH-15', + 'KH-16', + 'KH-17', + 'KH-18', + 'KH-19', + 'KH-2', + 'KH-20', + 'KH-21', + 'KH-22', + 'KH-23', + 'KH-24', + 'KH-3', + 'KH-4', + 'KH-5', + 'KH-6', + 'KH-7', + 'KH-8', + 'KH-9', + 'KI-G', + 'KI-L', + 'KI-P', + 'KM-A', + 'KM-G', + 'KM-M', + 'KP-CHA', + 'KP-HAB', + 'KP-HAN', + 'KP-HWB', + 'KP-HWN', + 'KP-KAE', + 'KP-KAN', + 'KP-NAJ', + 'KP-NAM', + 'KP-PYB', + 'KP-PYN', + 'KP-PYO', + 'KP-YAN', + 'KR-11', + 'KR-26', + 'KR-27', + 'KR-28', + 'KR-29', + 'KR-30', + 'KR-31', + 'KR-41', + 'KR-42', + 'KR-43', + 'KR-44', + 'KR-45', + 'KR-46', + 'KR-47', + 'KR-48', + 'KR-49', + 'KW-AH', + 'KW-FA', + 'KW-HA', + 'KW-JA', + 'KW-KU', + 'KZ-AKM', + 'KZ-AKT', + 'KZ-ALA', + 'KZ-ALM', + 'KZ-AST', + 'KZ-ATY', + 'KZ-KAR', + 'KZ-KUS', + 'KZ-KZY', + 'KZ-MAN', + 'KZ-PAV', + 'KZ-SEV', + 'KZ-VOS', + 'KZ-YUZ', + 'KZ-ZAP', + 'KZ-ZHA', + 'LA-AT', + 'LA-BK', + 'LA-BL', + 'LA-CH', + 'LA-HO', + 'LA-KH', + 'LA-LM', + 'LA-LP', + 'LA-OU', + 'LA-PH', + 'LA-SL', + 'LA-SV', + 'LA-VI', + 'LA-VT', + 'LA-XA', + 'LA-XE', + 'LA-XI', + 'LA-XN', + 'LB-AS', + 'LB-BA', + 'LB-BI', + 'LB-JA', + 'LB-JL', + 'LB-NA', + 'LK-1', + 'LK-11', + 'LK-12', + 'LK-13', + 'LK-2', + 'LK-21', + 'LK-22', + 'LK-23', + 'LK-3', + 'LK-31', + 'LK-32', + 'LK-33', + 'LK-4', + 'LK-41', + 'LK-42', + 'LK-43', + 'LK-44', + 'LK-45', + 'LK-5', + 'LK-51', + 'LK-52', + 'LK-53', + 'LK-6', + 'LK-61', + 'LK-62', + 'LK-7', + 'LK-71', + 'LK-72', + 'LK-8', + 'LK-81', + 'LK-82', + 'LK-9', + 'LK-91', + 'LK-92', + 'LR-BG', + 'LR-BM', + 'LR-CM', + 'LR-GB', + 'LR-GG', + 'LR-GK', + 'LR-LO', + 'LR-MG', + 'LR-MO', + 'LR-MY', + 'LR-NI', + 'LR-RI', + 'LR-SI', + 'LS-A', + 'LS-B', + 'LS-C', + 'LS-D', + 'LS-E', + 'LS-F', + 'LS-G', + 'LS-H', + 'LS-J', + 'LS-K', + 'LT-AL', + 'LT-KL', + 'LT-KU', + 'LT-MR', + 'LT-PN', + 'LT-SA', + 'LT-TA', + 'LT-TE', + 'LT-UT', + 'LT-VL', + 'LU-D', + 'LU-G', + 'LU-L', + 'LV-AI', + 'LV-AL', + 'LV-BL', + 'LV-BU', + 'LV-CE', + 'LV-DA', + 'LV-DGV', + 'LV-DO', + 'LV-GU', + 'LV-JEL', + 'LV-JK', + 'LV-JL', + 'LV-JUR', + 'LV-KR', + 'LV-KU', + 'LV-LE', + 'LV-LM', + 'LV-LPX', + 'LV-LU', + 'LV-MA', + 'LV-OG', + 'LV-PR', + 'LV-RE', + 'LV-REZ', + 'LV-RI', + 'LV-RIX', + 'LV-SA', + 'LV-TA', + 'LV-TU', + 'LV-VE', + 'LV-VEN', + 'LV-VK', + 'LV-VM', + 'LY-BA', + 'LY-BU', + 'LY-FA', + 'LY-JA', + 'LY-JG', + 'LY-JU', + 'LY-MI', + 'LY-NA', + 'LY-SF', + 'LY-TB', + 'LY-WA', + 'LY-WU', + 'LY-ZA', + 'MA-01', + 'MA-02', + 'MA-03', + 'MA-04', + 'MA-05', + 'MA-06', + 'MA-07', + 'MA-08', + 'MA-09', + 'MA-10', + 'MA-11', + 'MA-12', + 'MA-13', + 'MA-14', + 'MA-15', + 'MA-16', + 'MA-AGD', + 'MA-ASZ', + 'MA-AZI', + 'MA-BAH', + 'MA-BEM', + 'MA-BER', + 'MA-BES', + 'MA-BOD', + 'MA-BOM', + 'MA-CAS', + 'MA-CHE', + 'MA-CHI', + 'MA-ERR', + 'MA-ESI', + 'MA-ESM', + 'MA-FES', + 'MA-FIG', + 'MA-GUE', + 'MA-HAJ', + 'MA-HAO', + 'MA-HOC', + 'MA-IFR', + 'MA-JDI', + 'MA-JRA', + 'MA-KEN', + 'MA-KES', + 'MA-KHE', + 'MA-KHN', + 'MA-KHO', + 'MA-LAA', + 'MA-LAR', + 'MA-MAR', + 'MA-MEK', + 'MA-MEL', + 'MA-NAD', + 'MA-OUA', + 'MA-OUD', + 'MA-OUJ', + 'MA-RBA', + 'MA-SAF', + 'MA-SEF', + 'MA-SET', + 'MA-SIK', + 'MA-TAO', + 'MA-TAR', + 'MA-TAT', + 'MA-TAZ', + 'MA-TET', + 'MA-TIZ', + 'MA-TNG', + 'MA-TNT', + 'MD-BA', + 'MD-CA', + 'MD-CH', + 'MD-CU', + 'MD-ED', + 'MD-GA', + 'MD-LA', + 'MD-OR', + 'MD-SN', + 'MD-SO', + 'MD-TA', + 'MD-TI', + 'MD-UN', + 'MG-A', + 'MG-D', + 'MG-F', + 'MG-M', + 'MG-T', + 'MG-U', + 'MH-ALK', + 'MH-ALL', + 'MH-ARN', + 'MH-AUR', + 'MH-EBO', + 'MH-ENI', + 'MH-JAL', + 'MH-KIL', + 'MH-KWA', + 'MH-L', + 'MH-LAE', + 'MH-LIB', + 'MH-LIK', + 'MH-MAJ', + 'MH-MAL', + 'MH-MEJ', + 'MH-MIL', + 'MH-NMK', + 'MH-NMU', + 'MH-RON', + 'MH-T', + 'MH-UJA', + 'MH-UJL', + 'MH-UTI', + 'MH-WTH', + 'MH-WTJ', + 'ML-1', + 'ML-2', + 'ML-3', + 'ML-4', + 'ML-5', + 'ML-6', + 'ML-7', + 'ML-8', + 'ML-BKO', + 'MM-01', + 'MM-02', + 'MM-03', + 'MM-04', + 'MM-05', + 'MM-06', + 'MM-07', + 'MM-11', + 'MM-12', + 'MM-13', + 'MM-14', + 'MM-15', + 'MM-16', + 'MM-17', + 'MN-035', + 'MN-037', + 'MN-039', + 'MN-041', + 'MN-043', + 'MN-046', + 'MN-047', + 'MN-049', + 'MN-051', + 'MN-053', + 'MN-055', + 'MN-057', + 'MN-059', + 'MN-061', + 'MN-063', + 'MN-064', + 'MN-065', + 'MN-067', + 'MN-069', + 'MN-071', + 'MN-073', + 'MN-1', + 'MR-01', + 'MR-02', + 'MR-03', + 'MR-04', + 'MR-05', + 'MR-06', + 'MR-07', + 'MR-08', + 'MR-09', + 'MR-10', + 'MR-11', + 'MR-12', + 'MR-NKC', + 'MU-AG', + 'MU-BL', + 'MU-BR', + 'MU-CC', + 'MU-CU', + 'MU-FL', + 'MU-GP', + 'MU-MO', + 'MU-PA', + 'MU-PL', + 'MU-PU', + 'MU-PW', + 'MU-QB', + 'MU-RO', + 'MU-RR', + 'MU-SA', + 'MU-VP', + 'MV-01', + 'MV-02', + 'MV-03', + 'MV-04', + 'MV-05', + 'MV-07', + 'MV-08', + 'MV-12', + 'MV-13', + 'MV-14', + 'MV-17', + 'MV-20', + 'MV-23', + 'MV-24', + 'MV-25', + 'MV-26', + 'MV-27', + 'MV-28', + 'MV-29', + 'MV-MLE', + 'MW-BA', + 'MW-BL', + 'MW-C', + 'MW-CK', + 'MW-CR', + 'MW-CT', + 'MW-DE', + 'MW-DO', + 'MW-KR', + 'MW-KS', + 'MW-LI', + 'MW-LK', + 'MW-MC', + 'MW-MG', + 'MW-MH', + 'MW-MU', + 'MW-MW', + 'MW-MZ', + 'MW-N', + 'MW-NB', + 'MW-NI', + 'MW-NK', + 'MW-NS', + 'MW-NU', + 'MW-PH', + 'MW-RU', + 'MW-S', + 'MW-SA', + 'MW-TH', + 'MW-ZO', + 'MX-AGU', + 'MX-BCN', + 'MX-BCS', + 'MX-CAM', + 'MX-CHH', + 'MX-CHP', + 'MX-COA', + 'MX-COL', + 'MX-DIF', + 'MX-DUR', + 'MX-GRO', + 'MX-GUA', + 'MX-HID', + 'MX-JAL', + 'MX-MEX', + 'MX-MIC', + 'MX-MOR', + 'MX-NAY', + 'MX-NLE', + 'MX-OAX', + 'MX-PUE', + 'MX-QUE', + 'MX-ROO', + 'MX-SIN', + 'MX-SLP', + 'MX-SON', + 'MX-TAB', + 'MX-TAM', + 'MX-TLA', + 'MX-VER', + 'MX-YUC', + 'MX-ZAC', + 'MY-A', + 'MY-B', + 'MY-C', + 'MY-D', + 'MY-J', + 'MY-K', + 'MY-L', + 'MY-M', + 'MY-N', + 'MY-P', + 'MY-R', + 'MY-SA', + 'MY-SK', + 'MY-T', + 'MY-W', + 'MZ-A', + 'MZ-B', + 'MZ-G', + 'MZ-I', + 'MZ-L', + 'MZ-MPM', + 'MZ-N', + 'MZ-P', + 'MZ-Q', + 'MZ-S', + 'MZ-T', + 'NA-CA', + 'NA-ER', + 'NA-HA', + 'NA-KA', + 'NA-KH', + 'NA-KU', + 'NA-OD', + 'NA-OH', + 'NA-OK', + 'NA-ON', + 'NA-OS', + 'NA-OT', + 'NA-OW', + 'NE-1', + 'NE-2', + 'NE-3', + 'NE-4', + 'NE-5', + 'NE-6', + 'NE-7', + 'NE-8', + 'NG-AB', + 'NG-AD', + 'NG-AK', + 'NG-AN', + 'NG-BA', + 'NG-BE', + 'NG-BO', + 'NG-BY', + 'NG-CR', + 'NG-DE', + 'NG-EB', + 'NG-ED', + 'NG-EK', + 'NG-EN', + 'NG-FC', + 'NG-GO', + 'NG-IM', + 'NG-JI', + 'NG-KD', + 'NG-KE', + 'NG-KN', + 'NG-KO', + 'NG-KT', + 'NG-KW', + 'NG-LA', + 'NG-NA', + 'NG-NI', + 'NG-OG', + 'NG-ON', + 'NG-OS', + 'NG-OY', + 'NG-PL', + 'NG-RI', + 'NG-SO', + 'NG-TA', + 'NG-YO', + 'NG-ZA', + 'NI-AN', + 'NI-AS', + 'NI-BO', + 'NI-CA', + 'NI-CI', + 'NI-CO', + 'NI-ES', + 'NI-GR', + 'NI-JI', + 'NI-LE', + 'NI-MD', + 'NI-MN', + 'NI-MS', + 'NI-MT', + 'NI-NS', + 'NI-RI', + 'NI-SJ', + 'NL-DR', + 'NL-FL', + 'NL-FR', + 'NL-GE', + 'NL-GR', + 'NL-LI', + 'NL-NB', + 'NL-NH', + 'NL-OV', + 'NL-UT', + 'NL-ZE', + 'NL-ZH', + 'NO-01', + 'NO-02', + 'NO-03', + 'NO-04', + 'NO-05', + 'NO-06', + 'NO-07', + 'NO-08', + 'NO-09', + 'NO-10', + 'NO-11', + 'NO-12', + 'NO-14', + 'NO-15', + 'NO-16', + 'NO-17', + 'NO-18', + 'NO-19', + 'NO-20', + 'NO-21', + 'NO-22', + 'NP-1', + 'NP-2', + 'NP-3', + 'NP-4', + 'NP-5', + 'NP-BA', + 'NP-BH', + 'NP-DH', + 'NP-GA', + 'NP-JA', + 'NP-KA', + 'NP-KO', + 'NP-LU', + 'NP-MA', + 'NP-ME', + 'NP-NA', + 'NP-RA', + 'NP-SA', + 'NP-SE', + 'NZ-AUK', + 'NZ-BOP', + 'NZ-CAN', + 'NZ-GIS', + 'NZ-HKB', + 'NZ-MBH', + 'NZ-MWT', + 'NZ-N', + 'NZ-NSN', + 'NZ-NTL', + 'NZ-OTA', + 'NZ-S', + 'NZ-STL', + 'NZ-TAS', + 'NZ-TKI', + 'NZ-WGN', + 'NZ-WKO', + 'NZ-WTC', + 'OM-BA', + 'OM-DA', + 'OM-JA', + 'OM-MA', + 'OM-MU', + 'OM-SH', + 'OM-WU', + 'OM-ZA', + 'PA-0', + 'PA-1', + 'PA-2', + 'PA-3', + 'PA-4', + 'PA-5', + 'PA-6', + 'PA-7', + 'PA-8', + 'PA-9', + 'PE-AMA', + 'PE-ANC', + 'PE-APU', + 'PE-ARE', + 'PE-AYA', + 'PE-CAJ', + 'PE-CAL', + 'PE-CUS', + 'PE-HUC', + 'PE-HUV', + 'PE-ICA', + 'PE-JUN', + 'PE-LAL', + 'PE-LAM', + 'PE-LIM', + 'PE-LOR', + 'PE-MDD', + 'PE-MOQ', + 'PE-PAS', + 'PE-PIU', + 'PE-PUN', + 'PE-SAM', + 'PE-TAC', + 'PE-TUM', + 'PE-UCA', + 'PG-CPK', + 'PG-CPM', + 'PG-EBR', + 'PG-EHG', + 'PG-EPW', + 'PG-ESW', + 'PG-GPK', + 'PG-MBA', + 'PG-MPL', + 'PG-MPM', + 'PG-MRL', + 'PG-NCD', + 'PG-NIK', + 'PG-NPP', + 'PG-NSA', + 'PG-SAN', + 'PG-SHM', + 'PG-WBK', + 'PG-WHM', + 'PG-WPD', + 'PH-00', + 'PH-01', + 'PH-02', + 'PH-03', + 'PH-04', + 'PH-05', + 'PH-06', + 'PH-07', + 'PH-08', + 'PH-09', + 'PH-10', + 'PH-11', + 'PH-12', + 'PH-13', + 'PH-14', + 'PH-15', + 'PH-ABR', + 'PH-AGN', + 'PH-AGS', + 'PH-AKL', + 'PH-ALB', + 'PH-ANT', + 'PH-APA', + 'PH-AUR', + 'PH-BAN', + 'PH-BAS', + 'PH-BEN', + 'PH-BIL', + 'PH-BOH', + 'PH-BTG', + 'PH-BTN', + 'PH-BUK', + 'PH-BUL', + 'PH-CAG', + 'PH-CAM', + 'PH-CAN', + 'PH-CAP', + 'PH-CAS', + 'PH-CAT', + 'PH-CAV', + 'PH-CEB', + 'PH-COM', + 'PH-DAO', + 'PH-DAS', + 'PH-DAV', + 'PH-EAS', + 'PH-GUI', + 'PH-IFU', + 'PH-ILI', + 'PH-ILN', + 'PH-ILS', + 'PH-ISA', + 'PH-KAL', + 'PH-LAG', + 'PH-LAN', + 'PH-LAS', + 'PH-LEY', + 'PH-LUN', + 'PH-MAD', + 'PH-MAG', + 'PH-MAS', + 'PH-MDC', + 'PH-MDR', + 'PH-MOU', + 'PH-MSC', + 'PH-MSR', + 'PH-NCO', + 'PH-NEC', + 'PH-NER', + 'PH-NSA', + 'PH-NUE', + 'PH-NUV', + 'PH-PAM', + 'PH-PAN', + 'PH-PLW', + 'PH-QUE', + 'PH-QUI', + 'PH-RIZ', + 'PH-ROM', + 'PH-SAR', + 'PH-SCO', + 'PH-SIG', + 'PH-SLE', + 'PH-SLU', + 'PH-SOR', + 'PH-SUK', + 'PH-SUN', + 'PH-SUR', + 'PH-TAR', + 'PH-TAW', + 'PH-WSA', + 'PH-ZAN', + 'PH-ZAS', + 'PH-ZMB', + 'PH-ZSI', + 'PK-BA', + 'PK-IS', + 'PK-JK', + 'PK-NA', + 'PK-NW', + 'PK-PB', + 'PK-SD', + 'PK-TA', + 'PL-DS', + 'PL-KP', + 'PL-LB', + 'PL-LD', + 'PL-LU', + 'PL-MA', + 'PL-MZ', + 'PL-OP', + 'PL-PD', + 'PL-PK', + 'PL-PM', + 'PL-SK', + 'PL-SL', + 'PL-WN', + 'PL-WP', + 'PL-ZP', + 'PT-01', + 'PT-02', + 'PT-03', + 'PT-04', + 'PT-05', + 'PT-06', + 'PT-07', + 'PT-08', + 'PT-09', + 'PT-10', + 'PT-11', + 'PT-12', + 'PT-13', + 'PT-14', + 'PT-15', + 'PT-16', + 'PT-17', + 'PT-18', + 'PT-20', + 'PT-30', + 'PY-1', + 'PY-10', + 'PY-11', + 'PY-12', + 'PY-13', + 'PY-14', + 'PY-15', + 'PY-16', + 'PY-19', + 'PY-2', + 'PY-3', + 'PY-4', + 'PY-5', + 'PY-6', + 'PY-7', + 'PY-8', + 'PY-9', + 'PY-ASU', + 'QA-DA', + 'QA-GH', + 'QA-JB', + 'QA-JU', + 'QA-KH', + 'QA-MS', + 'QA-RA', + 'QA-US', + 'QA-WA', + 'RO-AB', + 'RO-AG', + 'RO-AR', + 'RO-B', + 'RO-BC', + 'RO-BH', + 'RO-BN', + 'RO-BR', + 'RO-BT', + 'RO-BV', + 'RO-BZ', + 'RO-CJ', + 'RO-CL', + 'RO-CS', + 'RO-CT', + 'RO-CV', + 'RO-DB', + 'RO-DJ', + 'RO-GJ', + 'RO-GL', + 'RO-GR', + 'RO-HD', + 'RO-HR', + 'RO-IF', + 'RO-IL', + 'RO-IS', + 'RO-MH', + 'RO-MM', + 'RO-MS', + 'RO-NT', + 'RO-OT', + 'RO-PH', + 'RO-SB', + 'RO-SJ', + 'RO-SM', + 'RO-SV', + 'RO-TL', + 'RO-TM', + 'RO-TR', + 'RO-VL', + 'RO-VN', + 'RO-VS', + 'RU-AD', + 'RU-AGB', + 'RU-AL', + 'RU-ALT', + 'RU-AMU', + 'RU-ARK', + 'RU-AST', + 'RU-BA', + 'RU-BEL', + 'RU-BRY', + 'RU-BU', + 'RU-CE', + 'RU-CHE', + 'RU-CHI', + 'RU-CHU', + 'RU-CU', + 'RU-DA', + 'RU-DU', + 'RU-EVE', + 'RU-IN', + 'RU-IRK', + 'RU-IVA', + 'RU-KAM', + 'RU-KB', + 'RU-KC', + 'RU-KDA', + 'RU-KEM', + 'RU-KGD', + 'RU-KGN', + 'RU-KHA', + 'RU-KHM', + 'RU-KIR', + 'RU-KK', + 'RU-KL', + 'RU-KLU', + 'RU-KO', + 'RU-KOP', + 'RU-KOR', + 'RU-KOS', + 'RU-KR', + 'RU-KRS', + 'RU-KYA', + 'RU-LEN', + 'RU-LIP', + 'RU-MAG', + 'RU-ME', + 'RU-MO', + 'RU-MOS', + 'RU-MOW', + 'RU-MUR', + 'RU-NEN', + 'RU-NGR', + 'RU-NIZ', + 'RU-NVS', + 'RU-OMS', + 'RU-ORE', + 'RU-ORL', + 'RU-PER', + 'RU-PNZ', + 'RU-PRI', + 'RU-PSK', + 'RU-ROS', + 'RU-RYA', + 'RU-SA', + 'RU-SAK', + 'RU-SAM', + 'RU-SAR', + 'RU-SE', + 'RU-SMO', + 'RU-SPE', + 'RU-STA', + 'RU-SVE', + 'RU-TA', + 'RU-TAM', + 'RU-TAY', + 'RU-TOM', + 'RU-TUL', + 'RU-TVE', + 'RU-TY', + 'RU-TYU', + 'RU-ULY', + 'RU-UOB', + 'RU-VGG', + 'RU-VLA', + 'RU-VLG', + 'RU-VOR', + 'RU-YAN', + 'RU-YAR', + 'RU-YEV', + 'RW-B', + 'RW-C', + 'RW-D', + 'RW-E', + 'RW-F', + 'RW-G', + 'RW-H', + 'RW-I', + 'RW-J', + 'RW-K', + 'RW-L', + 'RW-M', + 'SA-01', + 'SA-02', + 'SA-03', + 'SA-04', + 'SA-05', + 'SA-06', + 'SA-07', + 'SA-08', + 'SA-09', + 'SA-10', + 'SA-11', + 'SA-12', + 'SA-14', + 'SB-CE', + 'SB-CT', + 'SB-GU', + 'SB-IS', + 'SB-MK', + 'SB-ML', + 'SB-TE', + 'SB-WE', + 'SD-01', + 'SD-02', + 'SD-03', + 'SD-04', + 'SD-05', + 'SD-06', + 'SD-07', + 'SD-08', + 'SD-09', + 'SD-10', + 'SD-11', + 'SD-12', + 'SD-13', + 'SD-14', + 'SD-15', + 'SD-16', + 'SD-17', + 'SD-18', + 'SD-19', + 'SD-20', + 'SD-21', + 'SD-22', + 'SD-23', + 'SD-24', + 'SD-25', + 'SD-26', + 'SE-AB', + 'SE-AC', + 'SE-BD', + 'SE-C', + 'SE-D', + 'SE-E', + 'SE-F', + 'SE-G', + 'SE-H', + 'SE-I', + 'SE-K', + 'SE-M', + 'SE-N', + 'SE-O', + 'SE-S', + 'SE-T', + 'SE-U', + 'SE-W', + 'SE-X', + 'SE-Y', + 'SE-Z', + 'SH-AC', + 'SH-SH', + 'SH-TA', + 'SI-01', + 'SI-02', + 'SI-03', + 'SI-04', + 'SI-05', + 'SI-06', + 'SI-07', + 'SI-08', + 'SI-09', + 'SI-10', + 'SI-11', + 'SI-12', + 'SK-BC', + 'SK-BL', + 'SK-KI', + 'SK-NI', + 'SK-PV', + 'SK-TA', + 'SK-TC', + 'SK-ZI', + 'SL-E', + 'SL-N', + 'SL-S', + 'SL-W', + 'SN-DB', + 'SN-DK', + 'SN-FK', + 'SN-KD', + 'SN-KL', + 'SN-LG', + 'SN-SL', + 'SN-TC', + 'SN-TH', + 'SN-ZG', + 'SO-AW', + 'SO-BK', + 'SO-BN', + 'SO-BR', + 'SO-BY', + 'SO-GA', + 'SO-GE', + 'SO-HI', + 'SO-JD', + 'SO-JH', + 'SO-MU', + 'SO-NU', + 'SO-SA', + 'SO-SD', + 'SO-SH', + 'SO-SO', + 'SO-TO', + 'SO-WO', + 'SR-BR', + 'SR-CM', + 'SR-CR', + 'SR-MA', + 'SR-NI', + 'SR-PM', + 'SR-PR', + 'SR-SA', + 'SR-SI', + 'SR-WA', + 'ST-P', + 'ST-S', + 'SV-AH', + 'SV-CA', + 'SV-CH', + 'SV-CU', + 'SV-LI', + 'SV-MO', + 'SV-PA', + 'SV-SA', + 'SV-SM', + 'SV-SO', + 'SV-SS', + 'SV-SV', + 'SV-UN', + 'SV-US', + 'SY-DI', + 'SY-DR', + 'SY-DY', + 'SY-HA', + 'SY-HI', + 'SY-HL', + 'SY-HM', + 'SY-ID', + 'SY-LA', + 'SY-QU', + 'SY-RA', + 'SY-RD', + 'SY-SU', + 'SY-TA', + 'SZ-HH', + 'SZ-LU', + 'SZ-MA', + 'SZ-SH', + 'TD-BA', + 'TD-BET', + 'TD-BI', + 'TD-CB', + 'TD-GR', + 'TD-KA', + 'TD-LC', + 'TD-LO', + 'TD-LR', + 'TD-MC', + 'TD-MK', + 'TD-OD', + 'TD-SA', + 'TD-TA', + 'TG-C', + 'TG-K', + 'TG-M', + 'TG-P', + 'TG-S', + 'TH-10', + 'TH-11', + 'TH-12', + 'TH-13', + 'TH-14', + 'TH-15', + 'TH-16', + 'TH-17', + 'TH-18', + 'TH-19', + 'TH-20', + 'TH-21', + 'TH-22', + 'TH-23', + 'TH-24', + 'TH-25', + 'TH-26', + 'TH-27', + 'TH-30', + 'TH-31', + 'TH-32', + 'TH-33', + 'TH-34', + 'TH-35', + 'TH-36', + 'TH-37', + 'TH-39', + 'TH-40', + 'TH-41', + 'TH-42', + 'TH-43', + 'TH-44', + 'TH-45', + 'TH-46', + 'TH-47', + 'TH-48', + 'TH-49', + 'TH-50', + 'TH-51', + 'TH-52', + 'TH-53', + 'TH-54', + 'TH-55', + 'TH-56', + 'TH-57', + 'TH-58', + 'TH-60', + 'TH-61', + 'TH-62', + 'TH-63', + 'TH-64', + 'TH-65', + 'TH-66', + 'TH-67', + 'TH-70', + 'TH-71', + 'TH-72', + 'TH-73', + 'TH-74', + 'TH-75', + 'TH-76', + 'TH-77', + 'TH-80', + 'TH-81', + 'TH-82', + 'TH-83', + 'TH-84', + 'TH-85', + 'TH-86', + 'TH-90', + 'TH-91', + 'TH-92', + 'TH-93', + 'TH-94', + 'TH-95', + 'TH-96', + 'TH-S', + 'TJ-GB', + 'TJ-KT', + 'TJ-SU', + 'TL-AL', + 'TL-AN', + 'TL-BA', + 'TL-BO', + 'TL-CO', + 'TL-DI', + 'TL-ER', + 'TL-LA', + 'TL-LI', + 'TL-MF', + 'TL-MT', + 'TL-OE', + 'TL-VI', + 'TM-A', + 'TM-B', + 'TM-D', + 'TM-L', + 'TM-M', + 'TN-11', + 'TN-12', + 'TN-13', + 'TN-21', + 'TN-22', + 'TN-23', + 'TN-31', + 'TN-32', + 'TN-33', + 'TN-34', + 'TN-41', + 'TN-42', + 'TN-43', + 'TN-51', + 'TN-52', + 'TN-53', + 'TN-61', + 'TN-71', + 'TN-72', + 'TN-73', + 'TN-81', + 'TN-82', + 'TN-83', + 'TR-01', + 'TR-02', + 'TR-03', + 'TR-04', + 'TR-05', + 'TR-06', + 'TR-07', + 'TR-08', + 'TR-09', + 'TR-10', + 'TR-11', + 'TR-12', + 'TR-13', + 'TR-14', + 'TR-15', + 'TR-16', + 'TR-17', + 'TR-18', + 'TR-19', + 'TR-20', + 'TR-21', + 'TR-22', + 'TR-23', + 'TR-24', + 'TR-25', + 'TR-26', + 'TR-27', + 'TR-28', + 'TR-29', + 'TR-30', + 'TR-31', + 'TR-32', + 'TR-33', + 'TR-34', + 'TR-35', + 'TR-36', + 'TR-37', + 'TR-38', + 'TR-39', + 'TR-40', + 'TR-41', + 'TR-42', + 'TR-43', + 'TR-44', + 'TR-45', + 'TR-46', + 'TR-47', + 'TR-48', + 'TR-49', + 'TR-50', + 'TR-51', + 'TR-52', + 'TR-53', + 'TR-54', + 'TR-55', + 'TR-56', + 'TR-57', + 'TR-58', + 'TR-59', + 'TR-60', + 'TR-61', + 'TR-62', + 'TR-63', + 'TR-64', + 'TR-65', + 'TR-66', + 'TR-67', + 'TR-68', + 'TR-69', + 'TR-70', + 'TR-71', + 'TR-72', + 'TR-73', + 'TR-74', + 'TR-75', + 'TR-76', + 'TR-77', + 'TR-78', + 'TR-79', + 'TR-80', + 'TR-81', + 'TT-ARI', + 'TT-CHA', + 'TT-CTT', + 'TT-DMN', + 'TT-ETO', + 'TT-PED', + 'TT-POS', + 'TT-PRT', + 'TT-PTF', + 'TT-RCM', + 'TT-SFO', + 'TT-SGE', + 'TT-SIP', + 'TT-SJL', + 'TT-TUP', + 'TT-WTO', + 'TW-CHA', + 'TW-CYQ', + 'TW-HSQ', + 'TW-HUA', + 'TW-ILA', + 'TW-KEE', + 'TW-KHQ', + 'TW-MIA', + 'TW-NAN', + 'TW-PEN', + 'TW-PIF', + 'TW-TAO', + 'TW-TNQ', + 'TW-TPQ', + 'TW-TTT', + 'TW-TXQ', + 'TW-YUN', + 'TZ-01', + 'TZ-02', + 'TZ-03', + 'TZ-04', + 'TZ-05', + 'TZ-06', + 'TZ-07', + 'TZ-08', + 'TZ-09', + 'TZ-10', + 'TZ-11', + 'TZ-12', + 'TZ-13', + 'TZ-14', + 'TZ-15', + 'TZ-16', + 'TZ-17', + 'TZ-18', + 'TZ-19', + 'TZ-20', + 'TZ-21', + 'TZ-22', + 'TZ-23', + 'TZ-24', + 'TZ-25', + 'UA-05', + 'UA-07', + 'UA-09', + 'UA-12', + 'UA-14', + 'UA-18', + 'UA-21', + 'UA-23', + 'UA-26', + 'UA-30', + 'UA-32', + 'UA-35', + 'UA-40', + 'UA-43', + 'UA-46', + 'UA-48', + 'UA-51', + 'UA-53', + 'UA-56', + 'UA-59', + 'UA-61', + 'UA-63', + 'UA-65', + 'UA-68', + 'UA-71', + 'UA-74', + 'UA-77', + 'UG-AJM', + 'UG-APA', + 'UG-ARU', + 'UG-BUA', + 'UG-BUG', + 'UG-BUN', + 'UG-BUS', + 'UG-C', + 'UG-E', + 'UG-GUL', + 'UG-HOI', + 'UG-IGA', + 'UG-JIN', + 'UG-KAP', + 'UG-KAS', + 'UG-KAT', + 'UG-KBL', + 'UG-KBR', + 'UG-KIB', + 'UG-KIS', + 'UG-KIT', + 'UG-KLA', + 'UG-KLE', + 'UG-KLG', + 'UG-KLI', + 'UG-KOT', + 'UG-KUM', + 'UG-LIR', + 'UG-LUW', + 'UG-MBL', + 'UG-MBR', + 'UG-MOR', + 'UG-MOY', + 'UG-MPI', + 'UG-MSI', + 'UG-MSK', + 'UG-MUB', + 'UG-MUK', + 'UG-N', + 'UG-NAK', + 'UG-NEB', + 'UG-NTU', + 'UG-PAL', + 'UG-RAK', + 'UG-RUK', + 'UG-SEM', + 'UG-SOR', + 'UG-TOR', + 'UG-W', + 'UM-67', + 'UM-71', + 'UM-76', + 'UM-79', + 'UM-81', + 'UM-84', + 'UM-86', + 'UM-89', + 'UM-95', + 'US-AK', + 'US-AL', + 'US-AR', + 'US-AS', + 'US-AZ', + 'US-CA', + 'US-CO', + 'US-CT', + 'US-DC', + 'US-DE', + 'US-FL', + 'US-GA', + 'US-GU', + 'US-HI', + 'US-IA', + 'US-ID', + 'US-IL', + 'US-IN', + 'US-KS', + 'US-KY', + 'US-LA', + 'US-MA', + 'US-MD', + 'US-ME', + 'US-MI', + 'US-MN', + 'US-MO', + 'US-MP', + 'US-MS', + 'US-MT', + 'US-NC', + 'US-ND', + 'US-NE', + 'US-NH', + 'US-NJ', + 'US-NM', + 'US-NV', + 'US-NY', + 'US-OH', + 'US-OK', + 'US-OR', + 'US-PA', + 'US-PR', + 'US-RI', + 'US-SC', + 'US-SD', + 'US-TN', + 'US-TX', + 'US-UM', + 'US-UT', + 'US-VA', + 'US-VI', + 'US-VT', + 'US-WA', + 'US-WI', + 'US-WV', + 'US-WY', + 'UY-AR', + 'UY-CA', + 'UY-CL', + 'UY-CO', + 'UY-DU', + 'UY-FD', + 'UY-FS', + 'UY-LA', + 'UY-MA', + 'UY-MO', + 'UY-PA', + 'UY-RN', + 'UY-RO', + 'UY-RV', + 'UY-SA', + 'UY-SJ', + 'UY-SO', + 'UY-TA', + 'UY-TT', + 'UZ-AN', + 'UZ-BU', + 'UZ-FA', + 'UZ-JI', + 'UZ-NG', + 'UZ-NW', + 'UZ-QA', + 'UZ-QR', + 'UZ-SA', + 'UZ-SI', + 'UZ-SU', + 'UZ-TK', + 'UZ-TO', + 'UZ-XO', + 'VE-A', + 'VE-B', + 'VE-C', + 'VE-D', + 'VE-E', + 'VE-F', + 'VE-G', + 'VE-H', + 'VE-I', + 'VE-J', + 'VE-K', + 'VE-L', + 'VE-M', + 'VE-N', + 'VE-O', + 'VE-P', + 'VE-R', + 'VE-S', + 'VE-T', + 'VE-U', + 'VE-V', + 'VE-W', + 'VE-X', + 'VE-Y', + 'VE-Z', + 'VN-01', + 'VN-02', + 'VN-03', + 'VN-04', + 'VN-05', + 'VN-06', + 'VN-07', + 'VN-09', + 'VN-13', + 'VN-14', + 'VN-15', + 'VN-18', + 'VN-20', + 'VN-21', + 'VN-22', + 'VN-23', + 'VN-24', + 'VN-25', + 'VN-26', + 'VN-27', + 'VN-28', + 'VN-29', + 'VN-30', + 'VN-31', + 'VN-32', + 'VN-33', + 'VN-34', + 'VN-35', + 'VN-36', + 'VN-37', + 'VN-39', + 'VN-40', + 'VN-41', + 'VN-43', + 'VN-44', + 'VN-45', + 'VN-46', + 'VN-47', + 'VN-48', + 'VN-49', + 'VN-50', + 'VN-51', + 'VN-52', + 'VN-53', + 'VN-54', + 'VN-55', + 'VN-56', + 'VN-57', + 'VN-58', + 'VN-59', + 'VN-60', + 'VN-61', + 'VN-62', + 'VN-63', + 'VN-64', + 'VN-65', + 'VN-66', + 'VN-67', + 'VN-68', + 'VN-69', + 'VN-70', + 'VU-MAP', + 'VU-PAM', + 'VU-SAM', + 'VU-SEE', + 'VU-TAE', + 'VU-TOB', + 'WS-AA', + 'WS-AL', + 'WS-AT', + 'WS-FA', + 'WS-GE', + 'WS-GI', + 'WS-PA', + 'WS-SA', + 'WS-TU', + 'WS-VF', + 'WS-VS', + 'YE-AB', + 'YE-AD', + 'YE-AM', + 'YE-BA', + 'YE-DA', + 'YE-DH', + 'YE-HD', + 'YE-HJ', + 'YE-HU', + 'YE-IB', + 'YE-JA', + 'YE-LA', + 'YE-MA', + 'YE-MR', + 'YE-MW', + 'YE-SD', + 'YE-SH', + 'YE-SN', + 'YE-TA', + 'YU-CG', + 'YU-KM', + 'YU-SR', + 'YU-VO', + 'ZA-EC', + 'ZA-FS', + 'ZA-GT', + 'ZA-MP', + 'ZA-NC', + 'ZA-NL', + 'ZA-NP', + 'ZA-NW', + 'ZA-WC', + 'ZM-01', + 'ZM-02', + 'ZM-03', + 'ZM-04', + 'ZM-05', + 'ZM-06', + 'ZM-07', + 'ZM-08', + 'ZM-09', + 'ZW-BU', + 'ZW-HA', + 'ZW-MA', + 'ZW-MC', + 'ZW-ME', + 'ZW-MI', + 'ZW-MN', + 'ZW-MS', + 'ZW-MV', + 'ZW-MW', + ), +) + +Subdivision.__doc__ = """ +Subvidision country codes from ISO 3166-2. + +Taken from `here `_. +""" + + +class Layouts(Enum): + """Keyboard layouts. Taken from Debian's 9 + /usr/share/X11/xkb/rules/evdev.lst. + """ + + US = 'English (US)' + AF = 'Afghani' + ARA = 'Arabic' + AL = 'Albanian' + AM = 'Armenian' + AT = 'German (Austria)' + AU = 'English (Australian)' + AZ = 'Azerbaijani' + BY = 'Belarusian' + BE = 'Belgian' + BD = 'Bangla' + BA = 'Bosnian' + BR = 'Portuguese (Brazil)' + BG = 'Bulgarian' + DZ = 'Berber (Algeria, Latin characters)' + MA = 'Arabic (Morocco)' + CM = 'English (Cameroon)' + MM = 'Burmese' + CA = 'French (Canada)' + CD = 'French (Democratic Republic of the Congo)' + CN = 'Chinese' + HR = 'Croatian' + CZ = 'Czech' + DK = 'Danish' + NL = 'Dutch' + BT = 'Dzongkha' + EE = 'Estonian' + IR = 'Persian' + IQ = 'Iraqi' + FO = 'Faroese' + FI = 'Finnish' + FR = 'French' + GH = 'English (Ghana)' + GN = 'French (Guinea)' + GE = 'Georgian' + DE = 'German' + GR = 'Greek' + HU = 'Hungarian' + IL = 'Hebrew' + IT = 'Italian' + JP = 'Japanese' + KG = 'Kyrgyz' + KH = 'Khmer (Cambodia)' + KZ = 'Kazakh' + LA = 'Lao' + LATAM = 'Spanish (Latin American)' + LT = 'Lithuanian' + LV = 'Latvian' + MAO = 'Maori' + ME = 'Montenegrin' + MK = 'Macedonian' + MT = 'Maltese' + MN = 'Mongolian' + NO = 'Norwegian' + PL = 'Polish' + PT = 'Portuguese' + RO = 'Romanian' + RU = 'Russian' + RS = 'Serbian' + SI = 'Slovenian' + SK = 'Slovak' + ES = 'Spanish' + SE = 'Swedish' + CH = 'German (Switzerland)' + SY = 'Arabic (Syria)' + TJ = 'Tajik' + LK = 'Sinhala (phonetic)' + TH = 'Thai' + TR = 'Turkish' + TW = 'Taiwanese' + UA = 'Ukrainian' + GB = 'English (UK)' + UZ = 'Uzbek' + VN = 'Vietnamese' + KR = 'Korean' + IE = 'Irish' + PK = 'Urdu (Pakistan)' + MV = 'Dhivehi' + ZA = 'English (South Africa)' + EPO = 'Esperanto' + NP = 'Nepali' + NG = 'English (Nigeria)' + ET = 'Amharic' + SN = 'Wolof' + BRAI = 'Braille' + TM = 'Turkmen' + ML = 'Bambara' + TZ = 'Swahili (Tanzania)' + TG = 'French (Togo)' + KE = 'Swahili (Kenya)' + BW = 'Tswana' + PH = 'Filipino' + MD = 'Moldavian' + ID = 'Indonesian (Jawi)' + MY = 'Malay (Jawi)' + BN = 'Malay (Jawi)' + IN = 'Indian' + IS = 'Icelandic' + NEC_VNDR_JP = 'Japanese (PC-98xx Series)' + + def __str__(self): + return self.value diff --git a/ereuse_devicehub/teal/json_util.py b/ereuse_devicehub/teal/json_util.py new file mode 100644 index 00000000..fd1df357 --- /dev/null +++ b/ereuse_devicehub/teal/json_util.py @@ -0,0 +1,11 @@ +import ereuse_devicehub.ereuse_utils +from flask.json import JSONEncoder as FlaskJSONEncoder +from sqlalchemy.ext.baked import Result +from sqlalchemy.orm import Query + + +class TealJSONEncoder(ereuse_devicehub.ereuse_utils.JSONEncoder, FlaskJSONEncoder): + def default(self, obj): + if isinstance(obj, (Result, Query)): + return tuple(obj) + return super().default(obj) diff --git a/ereuse_devicehub/teal/marshmallow.py b/ereuse_devicehub/teal/marshmallow.py new file mode 100644 index 00000000..72189930 --- /dev/null +++ b/ereuse_devicehub/teal/marshmallow.py @@ -0,0 +1,346 @@ +import ipaddress +from distutils.version import StrictVersion +from typing import Type, Union + +import colour +from boltons import strutils, urlutils +from ereuse_devicehub.ereuse_utils import if_none_return_none +from flask import current_app as app +from flask import g +from marshmallow import utils +from marshmallow.fields import Field +from marshmallow.fields import Nested as MarshmallowNested +from marshmallow.fields import String +from marshmallow.fields import ValidationError as _ValidationError +from marshmallow.fields import missing_ +from marshmallow.validate import Validator +from marshmallow_enum import EnumField as _EnumField +from sqlalchemy_utils import PhoneNumber + +from ereuse_devicehub.teal import db as tealdb +from ereuse_devicehub.teal.resource import Schema + + +class Version(Field): + """A python StrictVersion field, like '1.0.1'.""" + + @if_none_return_none + def _serialize(self, value, attr, obj): + return str(value) + + @if_none_return_none + def _deserialize(self, value, attr, data): + return StrictVersion(value) + + +class Color(Field): + """Any color field that can be accepted by the colour package.""" + + @if_none_return_none + def _serialize(self, value, attr, obj): + return str(value) + + @if_none_return_none + def _deserialize(self, value, attr, data): + return colour.Color(value) + + +class URL(Field): + def __init__( + self, + require_path=False, + default=missing_, + attribute=None, + data_key=None, + error=None, + validate=None, + required=False, + allow_none=None, + load_only=False, + dump_only=False, + missing=missing_, + error_messages=None, + **metadata, + ): + super().__init__( + default, + attribute, + data_key, + error, + validate, + required, + allow_none, + load_only, + dump_only, + missing, + error_messages, + **metadata, + ) + self.require_path = require_path + + @if_none_return_none + def _serialize(self, value, attr, obj): + return value.to_text() + + @if_none_return_none + def _deserialize(self, value, attr, data): + url = urlutils.URL(value) + if url.scheme or url.host: + if self.require_path: + if url.path and url.path != '/': + return url + else: + return url + raise ValueError('Not a valid URL.') + + +class IP(Field): + @if_none_return_none + def _serialize( + self, value: Union[ipaddress.IPv4Address, ipaddress.IPv6Address], attr, obj + ): + return str(value) + + @if_none_return_none + def _deserialize(self, value: str, attr, data): + return ipaddress.ip_address(value) + + +class Phone(Field): + @if_none_return_none + def _serialize(self, value: PhoneNumber, attr, obj): + return value.international + + @if_none_return_none + def _deserialize(self, value: str, attr, data): + phone = PhoneNumber(value) + if not phone.is_valid_number(): + raise ValueError('The phone number is invalid.') + return phone + + +class SanitizedStr(String): + """String field that only has regular user strings. + + A String that removes whitespaces, + optionally makes it lower, and invalidates HTML or ANSI codes. + """ + + def __init__( + self, + lower=False, + default=missing_, + attribute=None, + data_key=None, + error=None, + validate=None, + required=False, + allow_none=None, + load_only=False, + dump_only=False, + missing=missing_, + error_messages=None, + **metadata, + ): + super().__init__( + default, + attribute, + data_key, + error, + validate, + required, + allow_none, + load_only, + dump_only, + missing, + error_messages, + **metadata, + ) + self.lower = lower + + def _deserialize(self, value, attr, data): + out = super()._deserialize(value, attr, data) + out = out.strip() + if self.lower: + out = out.lower() + if strutils.html2text(out) != out: + self.fail('invalid') + elif strutils.strip_ansi(out) != out: + self.fail('invalid') + return out + + +class NestedOn(MarshmallowNested): + """A relationship with a resource schema that emulates the + relationships in SQLAlchemy. + + It allows instantiating SQLA models when deserializing NestedOn + values in two fashions: + + - If the :attr:`.only_query` is set, NestedOn expects a scalar + (str, int...) value when deserializing, and tries to get + an existing model that has such value. Typical case is setting + :attr:`.only_query` to ``id``, and then pass-in the id + of a nested model. In such case NestedOn will change the id + for the model representing the ID. + - If :attr:`.only_query` is not set, NestedOn expects the + value to deserialize to be a dictionary, and instantiates + the model with the values of the dictionary. In this case + NestedOn requires :attr:`.polymorphic_on` to be set as a field, + usually called ``type``, that references a subclass of Model; + ex. {'type': 'SpecificDevice', ...}. + + When serializing from :meth:`teal.resource.Schema.jsonify` it + serializes nested relationships up to a defined limit. + + :param polymorphic_on: The field name that discriminates + the type of object. For example ``type``. + Then ``type`` contains the class name + of a subschema of ``nested``. + """ + + NESTED_LEVEL = '_level' + NESTED_LEVEL_MAX = '_level_max' + + def __init__( + self, + nested, + polymorphic_on: str, + db: tealdb.SQLAlchemy, + collection_class=list, + default=missing_, + exclude=tuple(), + only_query: str = None, + only=None, + **kwargs, + ): + self.polymorphic_on = polymorphic_on + self.collection_class = collection_class + self.only_query = only_query + assert isinstance(polymorphic_on, str) + assert isinstance(only, str) or only is None + super().__init__(nested, default, exclude, only, **kwargs) + self.db = db + + def _deserialize(self, value, attr, data): + if self.many and not utils.is_collection(value): + self.fail('type', input=value, type=value.__class__.__name__) + + if isinstance(self.only, str): # self.only is a field name + if self.many: + value = self.collection_class({self.only: v} for v in value) + else: + value = {self.only: value} + # New code: + parent_schema = app.resources[super().schema.t].SCHEMA + if self.many: + return self.collection_class( + self._deserialize_one(single, parent_schema, attr) for single in value + ) + else: + return self._deserialize_one(value, parent_schema, attr) + + def _deserialize_one(self, value, parent_schema: Type[Schema], attr): + if isinstance(value, dict) and self.polymorphic_on in value: + type = value[self.polymorphic_on] + resource = app.resources[type] + if not issubclass(resource.SCHEMA, parent_schema): + raise ValidationError( + '{} is not a sub-type of {}'.format(type, parent_schema.t), + field_names=[attr], + ) + schema = resource.SCHEMA( + only=self.only, + exclude=self.exclude, + context=getattr(self.parent, 'context', {}), + load_only=self._nested_normalized_option('load_only'), + dump_only=self._nested_normalized_option('dump_only'), + ) + schema.ordered = getattr(self.parent, 'ordered', False) + value = schema.load(value) + model = self._model(type)(**value) + elif self.only_query: # todo test only_query + model = ( + self._model(parent_schema.t) + .query.filter_by(**{self.only_query: value}) + .one() + ) + else: + raise ValidationError( + '\'Type\' field required to disambiguate resources.', field_names=[attr] + ) + assert isinstance(model, tealdb.Model) + return model + + def _model(self, type: str) -> Type[tealdb.Model]: + """Given the type of a model it returns the model class.""" + return self.db.Model._decl_class_registry.data[type]() + + def serialize(self, attr: str, obj, accessor=None) -> dict: + """See class docs.""" + if g.get(NestedOn.NESTED_LEVEL) == g.get(NestedOn.NESTED_LEVEL_MAX): + # Idea from https://marshmallow-sqlalchemy.readthedocs.io + # /en/latest/recipes.html#smart-nested-field + # Gets the FK of the relationship instead of the full object + # This won't work for many-many relationships (as they are lists) + # In such case return None + # todo is this the behaviour we want? + return getattr(obj, attr + '_id', None) + setattr(g, NestedOn.NESTED_LEVEL, g.get(NestedOn.NESTED_LEVEL) + 1) + ret = super().serialize(attr, obj, accessor) + setattr(g, NestedOn.NESTED_LEVEL, g.get(NestedOn.NESTED_LEVEL) - 1) + return ret + + +class IsType(Validator): + """ + Validator which succeeds if the value it is passed is a registered + resource type. + + :param parent: If set, type must be a subtype of such resource. + By default accept any resource. + """ + + # todo remove if not needed + no_type = 'Type does not exist.' + no_subtype = 'Type is not a descendant type of {parent}' + + def __init__(self, parent: str = None) -> None: + self.parent = parent # type: str + + def _repr_args(self): + return 'parent={0!r}'.format(self.parent) + + def __call__(self, type: str): + assert not self.parent or self.parent in app.resources + try: + r = app.resources[type] + if self.parent: + if not issubclass(r.__class__, app.resources[self.parent].__class__): + raise ValidationError(self.no_subtype.format(self.parent)) + except KeyError: + raise ValidationError(self.no_type) + + +class ValidationError(_ValidationError): + code = 422 + + +class EnumField(_EnumField): + """ + An EnumField that allows + generating OpenApi enums through Apispec. + """ + + def __init__( + self, + enum, + by_value=False, + load_by=None, + dump_by=None, + error='', + *args, + **kwargs, + ): + super().__init__(enum, by_value, load_by, dump_by, error, *args, **kwargs) + self.metadata['enum'] = [e.name for e in enum] diff --git a/ereuse_devicehub/teal/query.py b/ereuse_devicehub/teal/query.py new file mode 100644 index 00000000..e6103719 --- /dev/null +++ b/ereuse_devicehub/teal/query.py @@ -0,0 +1,294 @@ +import json +from json import JSONDecodeError + +from ereuse_devicehub.ereuse_utils import flatten_mixed +from marshmallow import Schema as MarshmallowSchema +from marshmallow.fields import Boolean, Field, List, Nested, Str, missing_ +from sqlalchemy import Column, between, or_ +from webargs.flaskparser import FlaskParser + + +class ListQuery(List): + """Base class for list-based queries.""" + + def __init__(self, column: Column, cls_or_instance, **kwargs): + self.column = column + super().__init__(cls_or_instance, **kwargs) + + +class Between(ListQuery): + """ + Generates a `Between` SQL statement. + + This method wants the user to provide exactly two parameters: + min and max:: + + f = Between(Model.foo, Integer()) + ... + Query().loads({'f': [0, 100]} + + """ + + def _deserialize(self, value, attr, data): + l = super()._deserialize(value, attr, data) + return between(self.column, *l) + + +class Equal(Field): + """ + Generates an SQL equal ``==`` clause for a given column and value:: + + class MyArgs(Query): + f = Equal(MyModel.foo, Integer()) + MyArgs().load({'f': 24}) -> SQL: ``MyModel.foo == 24`` + + """ + + def __init__( + self, + column: Column, + field: Field, + default=missing_, + attribute=None, + data_key=None, + error=None, + validate=None, + required=False, + allow_none=None, + load_only=False, + dump_only=False, + missing=missing_, + error_messages=None, + **metadata, + ): + super().__init__( + default, + attribute, + data_key, + error, + validate, + required, + allow_none, + load_only, + dump_only, + missing, + error_messages, + **metadata, + ) + self.column = column + self.field = field + + def _deserialize(self, value, attr, data): + v = super()._deserialize(value, attr, data) + return self.column == self.field.deserialize(v) + + +class Or(List): + """ + Generates an `OR` SQL statement. This is like a Marshmallow List field, + so you can specify the type of value of the OR and validations. + + As an example, you can define with this a list of options:: + + f = Or(Equal(Model.foo, Str(validates=OneOf(['option1', 'option2']))) + + Where the user can select one or more:: + + {'f': ['option1']} + + And with ``Length`` you can enforce the user to only choose one option:: + + f = Or(..., validates=Length(equal=1)) + """ + + def _deserialize(self, value, attr, data): + l = super()._deserialize(value, attr, data) + return or_(v for v in l) + + +class ILike(Str): + """ + Generates a insensitive `LIKE` statement for strings. + """ + + def __init__( + self, + column: Column, + default=missing_, + attribute=None, + data_key=None, + error=None, + validate=None, + required=False, + allow_none=None, + load_only=False, + dump_only=False, + missing=missing_, + error_messages=None, + **metadata, + ): + super().__init__( + default, + attribute, + data_key, + error, + validate, + required, + allow_none, + load_only, + dump_only, + missing, + error_messages, + **metadata, + ) + self.column = column + + def _deserialize(self, value, attr, data): + v = super()._deserialize(value, attr, data) + return self.column.ilike('{}%'.format(v)) + + +class QueryField(Field): + """A field whose first parameter is a function that when + executed by passing only the value returns a SQLAlchemy query + expression. + """ + + def __init__( + self, + query, + field: Field, + default=missing_, + attribute=None, + data_key=None, + error=None, + validate=None, + required=False, + allow_none=None, + load_only=False, + dump_only=False, + missing=missing_, + error_messages=None, + **metadata, + ): + super().__init__( + default, + attribute, + data_key, + error, + validate, + required, + allow_none, + load_only, + dump_only, + missing, + error_messages, + **metadata, + ) + self.query = query + self.field = field + + def _deserialize(self, value, attr, data): + v = super()._deserialize(value, attr, data) + return self.query(v) + + +class Join(Nested): + # todo Joins are manual: they should be able to use ORM's join + def __init__( + self, join, nested, default=missing_, exclude=tuple(), only=None, **kwargs + ): + super().__init__(nested, default, exclude, only, **kwargs) + self.join = join + + def _deserialize(self, value, attr, data): + v = list(super()._deserialize(value, attr, data)) + v.append(self.join) + return v + + +class Query(MarshmallowSchema): + """ + A Marshmallow schema that outputs SQLAlchemy queries when ``loading`` + dictionaries:: + + class MyQuery(Query): + foo = Like(Mymodel.foocolumn) + + Mymodel.query.filter(*MyQuery().load({'foo': 'bar'})).all() + # Executes query SELECT ... WHERE foocolumn IS LIKE 'bar%' + + When used with ``webargs`` library you can pass generate queries + directly from the browser: ``foo.com/foo/?filter={'foo': 'bar'}``. + """ + + def load(self, data, many=None, partial=None): + """ + Flatten ``Nested`` ``Query`` and add the list of results to + a SQL ``AND``. + """ + values = super().load(data, many, partial).values() + return flatten_mixed(values) + + def dump(self, obj, many=None, update_fields=True): + raise NotImplementedError('Why would you want to dump a query?') + + +class Sort(MarshmallowSchema): + """ + A Marshmallow schema that outputs SQLAlchemy order clauses:: + + class MySort(Sort): + foo = SortField(MyModel.foocolumn) + MyModel.query.filter(...).order_by(*MyQuery().load({'foo': 0})).all() + + When used with ``webargs`` library you can pass generate sorts + directly from the browser: ``foo.com/foo/?sort={'foo': 1, 'bar': 0}``. + """ + + ASCENDING = True + """Sort in ascending order.""" + DESCENDING = False + """Sort in descending order.""" + + def load(self, data, many=None, partial=None): + values = super().load(data, many, partial).values() + return flatten_mixed(values) + + +class SortField(Boolean): + """A field that outputs a SQLAlchemy order clause.""" + + def __init__( + self, column: Column, truthy=Boolean.truthy, falsy=Boolean.falsy, **kwargs + ): + super().__init__(truthy, falsy, **kwargs) + self.column = column + + def _deserialize(self, value, attr, data): + v = super()._deserialize(value, attr, data) + return self.column.asc() if v else self.column.desc() + + +class NestedQueryFlaskParser(FlaskParser): + """ + Parses JSON-encoded URL parameters like + ``.../foo?param={"x": "y"}¶m2=["x", "y"]``, and it still allows + normal non-JSON-encoded params ``../foo?param=23¶m2={"a": "b"}``. + + You can keep a value always a string, regardless if it is a valid + JSON, by overriding the following method and setting per-case + actions by checking `name` property. + """ + + def parse_querystring(self, req, name, field): + v = super().parse_querystring(req, name, field) + try: + return json.loads(v) + except (JSONDecodeError, TypeError): + return v + + +class FullTextSearch(Str): + # todo this is dummy for now + pass diff --git a/ereuse_devicehub/teal/request.py b/ereuse_devicehub/teal/request.py new file mode 100644 index 00000000..f6b6c7fe --- /dev/null +++ b/ereuse_devicehub/teal/request.py @@ -0,0 +1,28 @@ +from flask import Request as _Request +from flask import current_app as app + +from ereuse_devicehub.teal.resource import Schema + + +class Request(_Request): + def get_json( + self, + force=False, + silent=False, + cache=True, + validate=True, + schema: Schema = None, + ) -> dict: + """ + As :meth:`flask.Request.get_json` but parsing + the resulting json through passed-in ``schema`` (or by default + ``g.schema``). + """ + json = super().get_json(force, silent, cache) + if validate: + json = ( + schema.load(json) + if schema + else app.resources[self.blueprint].schema.load(json) + ) + return json diff --git a/ereuse_devicehub/teal/resource.py b/ereuse_devicehub/teal/resource.py new file mode 100644 index 00000000..6ba998cb --- /dev/null +++ b/ereuse_devicehub/teal/resource.py @@ -0,0 +1,429 @@ +from enum import Enum +from typing import Callable, Iterable, Iterator, Tuple, Type, Union + +import inflection +from anytree import PreOrderIter +from boltons.typeutils import classproperty, issubclass +from ereuse_devicehub.ereuse_utils.naming import Naming +from flask import Blueprint, current_app, g, request, url_for +from flask.json import jsonify +from flask.views import MethodView +from marshmallow import Schema as MarshmallowSchema +from marshmallow import SchemaOpts as MarshmallowSchemaOpts +from marshmallow import ValidationError, post_dump, pre_load, validates_schema +from werkzeug.exceptions import MethodNotAllowed +from werkzeug.routing import UnicodeConverter + +from ereuse_devicehub.teal import db, query + + +class SchemaOpts(MarshmallowSchemaOpts): + """ + Subclass of Marshmallow's SchemaOpts that provides + options for Teal's schemas. + """ + + def __init__(self, meta, ordered=False): + super().__init__(meta, ordered) + self.PREFIX = meta.PREFIX + + +class Schema(MarshmallowSchema): + """ + The definition of the fields of a resource. + """ + + OPTIONS_CLASS = SchemaOpts + + class Meta: + PREFIX = None + """Optional. A prefix for the type; ex. devices:Computer.""" + + # noinspection PyMethodParameters + @classproperty + def t(cls: Type['Schema']) -> str: + """The type for this schema, auto-computed from its name.""" + name, *_ = cls.__name__.split('Schema') + return Naming.new_type(name, cls.Meta.PREFIX) + + # noinspection PyMethodParameters + @classproperty + def resource(cls: Type['Schema']) -> str: + """The resource name of this schema.""" + return Naming.resource(cls.t) + + @validates_schema(pass_original=True) + def check_unknown_fields(self, _, original_data: dict): + """ + Raises a validationError when user sends extra fields. + + From `Marshmallow docs`_. + """ + unknown_fields = set(original_data) - set( + f.data_key or n for n, f in self.fields.items() + ) + if unknown_fields: + raise ValidationError('Unknown field', unknown_fields) + + @validates_schema(pass_original=True) + def check_dump_only(self, _, orig_data: dict): + """ + Raises a ValidationError if the user is submitting + 'read-only' fields. + """ + # Note that validates_schema does not execute when dumping + dump_only_fields = ( + name for name, field in self.fields.items() if field.dump_only + ) + non_writable = set(orig_data).intersection(dump_only_fields) + if non_writable: + raise ValidationError('Non-writable field', non_writable) + + @pre_load + @post_dump + def remove_none_values(self, data: dict) -> dict: + """ + Skip from dumping and loading values that are None. + + A value that is None will be the same as a value that has not + been set. + + `From here `_. + """ + # Will I always want this? + # maybe this could be a setting in the future? + return {key: value for key, value in data.items() if value is not None} + + def dump( + self, + model: Union['db.Model', Iterable['db.Model']], + many=None, + update_fields=True, + nested=None, + polymorphic_on='t', + ): + """ + Like marshmallow's dump but with nested resource support and + it only works for Models. + + This can load model relationships up to ``nested`` level. For + example, if ``nested`` is ``1`` and we pass in a model of + ``User`` that has a relationship with a table of ``Post``, it + will load ``User`` and ``User.posts`` with all posts objects + populated, but it won't load relationships inside the + ``Post`` object. If, at the same time the ``Post`` has + an ``author`` relationship with ``author_id`` being the FK, + ``user.posts[n].author`` will be the value of ``author_id``. + + Define nested fields with the + :class:`ereuse_devicehub.teal.marshmallow.NestedOn` + + This method requires an active application context as it needs + to store some stuff in ``g``. + + :param nested: How many layers of nested relationships to load? + By default only loads 1 nested relationship. + """ + from ereuse_devicehub.teal.marshmallow import NestedOn + + if nested is not None: + setattr(g, NestedOn.NESTED_LEVEL, 0) + setattr(g, NestedOn.NESTED_LEVEL_MAX, nested) + if many: + # todo this breaks with normal dicts. Maybe this should go + # in NestedOn in the same way it happens when loading + if isinstance(model, dict): + return super().dump(model, update_fields=update_fields) + else: + return [ + self._polymorphic_dump(o, update_fields, polymorphic_on) + for o in model + ] + + else: + if isinstance(model, dict): + return super().dump(model, update_fields=update_fields) + else: + return self._polymorphic_dump(model, update_fields, polymorphic_on) + + def _polymorphic_dump(self, obj: 'db.Model', update_fields, polymorphic_on='t'): + schema = current_app.resources[getattr(obj, polymorphic_on)].schema + if schema.t != self.t: + return super(schema.__class__, schema).dump(obj, False, update_fields) + else: + return super().dump(obj, False, update_fields) + + def jsonify( + self, + model: Union['db.Model', Iterable['db.Model']], + nested=1, + many=False, + update_fields: bool = True, + polymorphic_on='t', + **kw, + ) -> str: + """ + Like flask's jsonify but with model / marshmallow schema + support. + + :param nested: How many layers of nested relationships to load? + By default only loads 1 nested relationship. + """ + return jsonify(self.dump(model, many, update_fields, nested, polymorphic_on)) + + +class View(MethodView): + """ + A REST interface for resources. + """ + + QUERY_PARSER = query.NestedQueryFlaskParser() + + class FindArgs(MarshmallowSchema): + """ + Allowed arguments for the ``find`` + method (GET collection) endpoint + """ + + def __init__(self, definition: 'Resource', **kw) -> None: + self.resource_def = definition + """The ResourceDefinition tied to this view.""" + self.schema = None # type: Schema + """The schema tied to this view.""" + self.find_args = self.FindArgs() + super().__init__() + + def dispatch_request(self, *args, **kwargs): + # This is unique for each view call + self.schema = g.schema + """ + The default schema in this resource. + Added as an attr for commodity; you can always use g.schema. + """ + return super().dispatch_request(*args, **kwargs) + + def get(self, id): + """Get a collection of resources or a specific one. + --- + parameters: + - name: id + in: path + description: The identifier of the resource. + type: string + required: false + responses: + 200: + description: Return the collection or the specific one. + """ + if id: + response = self.one(id) + else: + args = self.QUERY_PARSER.parse( + self.find_args, request, locations=('querystring',) + ) + response = self.find(args) + return response + + def one(self, id): + """GET one specific resource (ex. /cars/1).""" + raise MethodNotAllowed() + + def find(self, args: dict): + """GET a list of resources (ex. /cars).""" + raise MethodNotAllowed() + + def post(self): + raise MethodNotAllowed() + + def delete(self, id): + raise MethodNotAllowed() + + def put(self, id): + raise MethodNotAllowed() + + def patch(self, id): + raise MethodNotAllowed() + + +class Converters(Enum): + """An enumeration of available URL converters.""" + + string = 'string' + int = 'int' + float = 'float' + path = 'path' + any = 'any' + uuid = 'uuid' + lower = 'lower' + + +class LowerStrConverter(UnicodeConverter): + """Like StringConverter but lowering the string.""" + + def to_python(self, value): + return super().to_python(value).lower() + + +class Resource(Blueprint): + """ + Main resource class. Defines the schema, views, + authentication, database and collection of a resource. + + A ``ResourceDefinition`` is a Flask + :class:`flask.blueprints.Blueprint` that provides everything + needed to set a REST endpoint. + """ + + VIEW = None # type: Type[View] + """ + Resource view linked to this definition or None. + If none, this resource does not generate any view. + """ + SCHEMA = Schema # type: Type[Schema] + """The Schema that validates a submitting resource at the entry point.""" + AUTH = False + """ + If true, authentication is required for all the endpoints of this + resource defined in ``VIEW``. + """ + ID_NAME = 'id' + """ + The variable name for GET *one* operations that is used as an id. + """ + ID_CONVERTER = Converters.string + """ + The converter for the id. + + Note that converters do **cast** the value, so the converter + ``uuid`` will return an ``UUID`` object. + """ + __type__ = None # type: str + """ + The type of resource. + If none, it is used the type of the Schema (``Schema.type``) + """ + + def __init__( + self, + app, + import_name=__name__, + static_folder=None, + static_url_path=None, + template_folder=None, + url_prefix=None, + subdomain=None, + url_defaults=None, + root_path=None, + cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(), + ): + assert not self.VIEW or issubclass( + self.VIEW, View + ), 'VIEW should be a subclass of View' + assert not self.SCHEMA or issubclass( + self.SCHEMA, Schema + ), 'SCHEMA should be a subclass of Schema or None.' + # todo test for cases where self.SCHEMA is None + url_prefix = ( + url_prefix if url_prefix is not None else '/{}'.format(self.resource) + ) + super().__init__( + self.type, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + ) + # todo __name__ in import_name forces subclasses to override the constructor + # otherwise import_name equals to teal.resource not project1.myresource + # and it is not very elegant... + + self.app = app + self.schema = self.SCHEMA() if self.SCHEMA else None + # Views + if self.VIEW: + view = self.VIEW.as_view('main', definition=self, auth=app.auth) + if self.AUTH: + view = app.auth.requires_auth(view) + self.add_url_rule( + '/', defaults={'id': None}, view_func=view, methods={'GET'} + ) + self.add_url_rule('/', view_func=view, methods={'POST'}) + self.add_url_rule( + '/<{}:{}>'.format(self.ID_CONVERTER.value, self.ID_NAME), + view_func=view, + methods={'GET', 'PUT', 'DELETE', 'PATCH'}, + ) + self.cli_commands = cli_commands + self.before_request(self.load_resource) + + @classproperty + def type(cls): + t = cls.__type__ or cls.SCHEMA.t + assert t, 'Resource needs a type: either from SCHEMA or manually from __type__.' + return t + + @classproperty + def t(cls): + return cls.type + + @classproperty + def resource(cls): + return Naming.resource(cls.type) + + @classproperty + def cli_name(cls): + """The name used to generate the CLI Click group for this + resource.""" + return inflection.singularize(cls.resource) + + def load_resource(self): + """ + Loads a schema and resource_def into the current request so it + can be used easily by functions outside view. + """ + g.schema = self.schema + g.resource_def = self + + def init_db(self, db: 'db.SQLAlchemy', exclude_schema=None): + """ + Put here code to execute when initializing the database for this + resource. + + We guarantee this to be executed in an app_context. + + No need to commit. + """ + pass + + @property + def subresources_types(self) -> Iterator[str]: + """Gets the types of the subresources.""" + return (node.name for node in PreOrderIter(self.app.tree[self.t])) + + +TYPE = Union[ + Resource, Schema, 'db.Model', str, Type[Resource], Type[Schema], Type['db.Model'] +] + + +def url_for_resource(resource: TYPE, item_id=None, method='GET') -> str: + """ + As Flask's ``url_for``, this generates an URL but specifically for + a View endpoint of the given resource. + :param method: The method whose view URL should be generated. + :param resource: + :param item_id: If given, append the ID of the resource in the URL, + ex. GET /devices/1 + :return: An URL. + """ + type = getattr(resource, 't', resource) + values = {} + if item_id: + values[current_app.resources[type].ID_NAME] = item_id + return url_for('{}.main'.format(type), _method=method, **values) diff --git a/ereuse_devicehub/teal/teal.py b/ereuse_devicehub/teal/teal.py new file mode 100644 index 00000000..4cffab9f --- /dev/null +++ b/ereuse_devicehub/teal/teal.py @@ -0,0 +1,308 @@ +import inspect +from typing import Type + +import click_spinner +import flask_cors +from anytree import Node +from apispec import APISpec +from click import option +from flask import Flask, jsonify +from flask.globals import _app_ctx_stack +from flask_sqlalchemy import SQLAlchemy +from marshmallow import ValidationError +from werkzeug.exceptions import HTTPException, UnprocessableEntity + +import ereuse_devicehub.ereuse_utils +from ereuse_devicehub.ereuse_utils import ensure_utf8 +from ereuse_devicehub.teal.auth import Auth +from ereuse_devicehub.teal.cli import TealCliRunner +from ereuse_devicehub.teal.client import Client +from ereuse_devicehub.teal.config import Config as ConfigClass +from ereuse_devicehub.teal.db import SchemaSQLAlchemy +from ereuse_devicehub.teal.json_util import TealJSONEncoder +from ereuse_devicehub.teal.request import Request +from ereuse_devicehub.teal.resource import Converters, LowerStrConverter, Resource + + +class Teal(Flask): + """ + An opinionated REST and JSON first server built on Flask using + MongoDB and Marshmallow. + """ + + test_client_class = Client + request_class = Request + json_encoder = TealJSONEncoder + cli_context_settings = {'help_option_names': ('-h', '--help')} + test_cli_runner_class = TealCliRunner + + def __init__( + self, + config: ConfigClass, + db: SQLAlchemy, + schema: str = None, + import_name=__name__.split('.')[0], + static_url_path=None, + static_folder='static', + static_host=None, + host_matching=False, + subdomain_matching=False, + template_folder='templates', + instance_path=None, + instance_relative_config=False, + root_path=None, + use_init_db=True, + Auth: Type[Auth] = Auth, + ): + """ + + :param config: + :param db: + :param schema: A string describing the main PostgreSQL's schema. + ``None`` disables this functionality. + If you use a factory of apps (for example by using + :func:`teal.teal.prefixed_database_factory`) and then set this + value differently per each app (as each app has a separate config) + you effectively create a `multi-tenant app `_. + Your models by default will be created in this ``SCHEMA``, + unless you set something like:: + + class User(db.Model): + __table_args__ = {'schema': 'users'} + + In which case this will be created in the ``users`` schema. + Schemas are interesting over having multiple databases (i.e. using + flask-sqlalchemy's data binding) because you can have relationships + between them. + + Note that this only works with PostgreSQL. + :param import_name: + :param static_url_path: + :param static_folder: + :param static_host: + :param host_matching: + :param subdomain_matching: + :param template_folder: + :param instance_path: + :param instance_relative_config: + :param root_path: + :param Auth: + """ + self.schema = schema + ensure_utf8(self.__class__.__name__) + super().__init__( + import_name, + static_url_path, + static_folder, + static_host, + host_matching, + subdomain_matching, + template_folder, + instance_path, + instance_relative_config, + root_path, + ) + self.config.from_object(config) + flask_cors.CORS(self) + # Load databases + self.auth = Auth() + self.url_map.converters[Converters.lower.name] = LowerStrConverter + self.load_resources() + self.register_error_handler(HTTPException, self._handle_standard_error) + self.register_error_handler(ValidationError, self._handle_validation_error) + self.db = db + db.init_app(self) + if use_init_db: + self.cli.command('init-db', context_settings=self.cli_context_settings)( + self.init_db + ) + self.spec = None # type: APISpec + self.apidocs() + + # noinspection PyAttributeOutsideInit + def load_resources(self): + self.resources = {} + """ + The resources definitions loaded on this App, referenced by their + type name. + """ + self.tree = {} + """ + A tree representing the hierarchy of the instances of + ResourceDefinitions. ResourceDefinitions use these nodes to + traverse their hierarchy. + + Do not use the normal python class hierarchy as it is global, + thus unreliable if you run different apps with different + schemas (for example, an extension that is only added on the + third app adds a new type of user). + """ + for ResourceDef in self.config['RESOURCE_DEFINITIONS']: + resource_def = ResourceDef(self) # type: Resource + self.register_blueprint(resource_def) + + if resource_def.cli_commands: + + @self.cli.group( + resource_def.cli_name, + context_settings=self.cli_context_settings, + short_help='{} management.'.format(resource_def.type), + ) + def dummy_group(): + pass + + for ( + cli_command, + *args, + ) in resource_def.cli_commands: # Register CLI commands + # todo cli commands with multiple arguments end-up reversed + # when teal has been executed multiple times (ex. testing) + # see _param_memo func in click package + dummy_group.command(*args)(cli_command) + + # todo should we use resource_def.name instead of type? + # are we going to have collisions? (2 resource_def -> 1 schema) + self.resources[resource_def.type] = resource_def + self.tree[resource_def.type] = Node(resource_def.type) + # Link tree nodes between them + for _type, node in self.tree.items(): + resource_def = self.resources[_type] + _, Parent, *superclasses = inspect.getmro(resource_def.__class__) + if Parent is not Resource: + node.parent = self.tree[Parent.type] + + @staticmethod + def _handle_standard_error(e: HTTPException): + """ + Handles HTTPExceptions by transforming them to JSON. + """ + try: + response = jsonify(e) + response.status_code = e.code + except (AttributeError, TypeError) as e: + code = getattr(e, 'code', 500) + response = jsonify( + {'message': str(e), 'code': code, 'type': e.__class__.__name__} + ) + response.status_code = code + return response + + @staticmethod + def _handle_validation_error(e: ValidationError): + data = { + 'message': e.messages, + 'code': UnprocessableEntity.code, + 'type': e.__class__.__name__, + } + response = jsonify(data) + response.status_code = UnprocessableEntity.code + return response + + @option( + '--erase/--no-erase', + default=False, + help='Delete all contents from the database (including common schemas)?', + ) + @option( + '--exclude-schema', + default=None, + help='Schema to exclude creation (and deletion if --erase is set). ' + 'Required the SchemaSQLAlchemy.', + ) + def init_db(self, erase: bool = False, exclude_schema=None): + """ + Initializes a database from scratch, + creating tables and needed resources. + + Note that this does not create the database per se. + + If executing this directly, remember to use an app_context. + + Resources can hook functions that will be called when this + method executes, by subclassing :meth:`teal.resource. + Resource.load_resource`. + """ + assert _app_ctx_stack.top, 'Use an app context.' + print('Initializing database...'.ljust(30), end='') + with click_spinner.spinner(): + if erase: + if exclude_schema: # Using then a schema teal sqlalchemy + assert isinstance(self.db, SchemaSQLAlchemy) + self.db.drop_schema() + else: # using regular flask sqlalchemy + self.db.drop_all() + self._init_db(exclude_schema) + self._init_resources() + self.db.session.commit() + print('done.') + + def _init_db(self, exclude_schema=None) -> bool: + """Where the database is initialized. You can override this. + + :return: A flag stating if the database has been created (can + be False in case check is True and the schema already + exists). + """ + if exclude_schema: # Using then a schema teal sqlalchemy + assert isinstance(self.db, SchemaSQLAlchemy) + self.db.create_all(exclude_schema=exclude_schema) + else: # using regular flask sqlalchemy + self.db.create_all() + return True + + def _init_resources(self, **kw): + for resource in self.resources.values(): + resource.init_db(self.db, **kw) + + def apidocs(self): + """Apidocs configuration and generation.""" + self.spec = APISpec( + plugins=( + 'apispec.ext.flask', + 'apispec.ext.marshmallow', + ), + **self.config.get_namespace('API_DOC_CONFIG_'), + ) + for name, resource in self.resources.items(): + if resource.SCHEMA: + self.spec.definition( + name, + schema=resource.SCHEMA, + extra_fields=self.config.get_namespace('API_DOC_CLASS_'), + ) + self.add_url_rule('/apidocs', view_func=self.apidocs_endpoint) + + def apidocs_endpoint(self): + """An endpoint that prints a JSON OpenApi 2.0 specification.""" + if not getattr(self, '_apidocs', None): + # We are forced to to this under a request context + for path, view_func in self.view_functions.items(): + if path != 'static': + self.spec.add_path(view=view_func) + self._apidocs = self.spec.to_dict() + return jsonify(self._apidocs) + + +class DumpeableHTTPException(ereuse_devicehub.ereuse_utils.Dumpeable): + """Exceptions that inherit this class will be able to dump + to dicts and JSONs. + """ + + def dump(self): + # todo this is heavily ad-hoc and should be more generic + value = super().dump() + value['type'] = self.__class__.__name__ + value['code'] = self.code + value.pop('exc', None) + value.pop('response', None) + if 'data' in value: + value['fields'] = value['data']['messages'] + del value['data'] + if 'message' not in value: + value['message'] = value.pop('description', str(self)) + return value + + +# Add dump capacity to Werkzeug's HTTPExceptions +HTTPException.__bases__ = HTTPException.__bases__ + (DumpeableHTTPException,) diff --git a/ereuse_devicehub/teal/utils.py b/ereuse_devicehub/teal/utils.py new file mode 100644 index 00000000..ec3668ca --- /dev/null +++ b/ereuse_devicehub/teal/utils.py @@ -0,0 +1,33 @@ +import inspect +from typing import Dict, Iterator, Tuple + +from sqlalchemy.dialects import postgresql + +from ereuse_devicehub.teal import resource + + +def compiled(Model, query) -> Tuple[str, Dict[str, str]]: + """ + Generates a SQL statement. + + :return A tuple with 1. the SQL statement and 2. the params for it. + """ + c = Model.query.filter(*query).statement.compile(dialect=postgresql.dialect()) + return str(c), c.params + + +def import_resource(module) -> Iterator['resource.Resource']: + """ + Gets the resource classes from the passed-in module. + + This method yields subclasses of :class:`teal.resource.Resource` + found in the given module. + """ + + for obj in vars(module).values(): + if ( + inspect.isclass(obj) + and issubclass(obj, resource.Resource) + and obj != resource.Resource + ): + yield obj diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index f6dfb1b3..fad5376f 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -73,7 +73,7 @@
  • - Data Storage Erasures + Drives Sanitization
  • @@ -193,7 +193,7 @@ {% else %} {% if lot and lot.is_incoming %} @@ -329,6 +337,24 @@ + {% endif %} + {% if lot and lot.is_shared %} + + {% endif %} @@ -514,10 +540,10 @@ - {% if lot and not lot.is_temporary %} + {% if lot and not lot.is_temporary and not lot.is_shared %}
    @@ -22,13 +22,13 @@ @@ -82,7 +82,7 @@
  • - Erasure Certificate + Device Sanitization
  • diff --git a/ereuse_devicehub/templates/inventory/search.html b/ereuse_devicehub/templates/inventory/search.html index 57f62975..9b1068fc 100644 --- a/ereuse_devicehub/templates/inventory/search.html +++ b/ereuse_devicehub/templates/inventory/search.html @@ -172,7 +172,7 @@
  • - Erasure Certificate + Device Sanitization
  • @@ -254,7 +254,7 @@
    -
    + {% else %} + + {% endif %} {{ form.csrf_token }} {% for field in form %} {% if field != form.csrf_token %} diff --git a/ereuse_devicehub/templates/workbench/settings.html b/ereuse_devicehub/templates/workbench/settings.html index d4d591f1..ff21dfa9 100644 --- a/ereuse_devicehub/templates/workbench/settings.html +++ b/ereuse_devicehub/templates/workbench/settings.html @@ -20,12 +20,13 @@
    +
    Sanitization servers
    - + diff --git a/ereuse_devicehub/views.py b/ereuse_devicehub/views.py index 1b6f8be0..91676032 100644 --- a/ereuse_devicehub/views.py +++ b/ereuse_devicehub/views.py @@ -11,7 +11,7 @@ from ereuse_devicehub import __version__, messages from ereuse_devicehub.db import db from ereuse_devicehub.forms import LoginForm, PasswordForm, SanitizationEntityForm from ereuse_devicehub.resources.action.models import Trade -from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.resources.lot.models import Lot, ShareLot from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.utils import is_safe_url @@ -91,6 +91,7 @@ class GenericMixin(View): self.context = { 'lots': self.get_lots(), 'version': __version__, + 'share_lots': ShareLot.query.filter_by(user_to=g.user), } return self.context diff --git a/ereuse_devicehub/workbench/views.py b/ereuse_devicehub/workbench/views.py index 1b81c8f2..25805261 100644 --- a/ereuse_devicehub/workbench/views.py +++ b/ereuse_devicehub/workbench/views.py @@ -23,7 +23,7 @@ class SettingsView(GenericMixin): decorators = [login_required] methods = ['GET', 'POST'] template_name = 'workbench/settings.html' - page_title = "Snapshots" + page_title = "Setup" def dispatch_request(self): self.get_context() diff --git a/requirements.in b/requirements.in index 9fbc32da..b536e82a 100644 --- a/requirements.in +++ b/requirements.in @@ -3,7 +3,6 @@ atomicwrites==1.4.0 click-spinner==0.1.8 colorama==0.3.9 colour==0.1.5 -ereuse-utils[naming,test,session,cli]==0.4.0b50 Flask-Cors==3.0.10 Flask-Login==0.5.0 Flask-WTF==1.0.0 @@ -24,7 +23,6 @@ requests-toolbelt==0.9.1 sortedcontainers==2.1.0 sqlalchemy-citext==1.3.post0 sqlalchemy-utils==0.33.11 -teal==0.2.0a38 tqdm==4.32.2 # workbench json parsing dependencies @@ -37,8 +35,19 @@ xlrd==2.0.1 # pandas dependency openpyxl==3.0.10 # pandas dependency et_xmlfile==1.1.0 # pandas dependency -# manual dependency -marshmallow-enum==1.4.1 - # flask_mail dependency blinker==1.5 + +# teal dependency +anytree==2.8.0 +apispec==0.39.0 +# apispec-webframeworks==0.5.2 +boltons==23.0.0 +flask-sqlalchemy>=2.5.1 +marshmallow==3.0.0b11 +marshmallow-enum==1.4.1 +webargs==5.5.3 +Werkzeug>=2.0.3 + +# ereuse-utils dependency +inflection==0.5.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 11ca5758..8d5f9408 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,18 +6,16 @@ # alembic==1.4.2 # via -r requirements.in -anytree==2.4.3 - # via teal +anytree==2.8.0 + # via -r requirements.in apispec==0.39.0 - # via teal + # via -r requirements.in atomicwrites==1.4.0 # via -r requirements.in blinker==1.5 # via -r requirements.in -boltons==18.0.1 - # via - # ereuse-utils - # teal +boltons==23.0.0 + # via -r requirements.in cairocffi==1.4.0 # via # cairosvg @@ -33,21 +31,13 @@ cffi==1.15.1 charset-normalizer==2.0.12 # via requests click==6.7 - # via - # ereuse-utils - # flask + # via flask click-spinner==0.1.8 - # via - # -r requirements.in - # teal + # via -r requirements.in colorama==0.3.9 - # via - # -r requirements.in - # ereuse-utils + # via -r requirements.in colour==0.1.5 - # via - # -r requirements.in - # sqlalchemy-utils + # via -r requirements.in cssselect2==0.7.0 # via # cairosvg @@ -56,31 +46,23 @@ defusedxml==0.7.1 # via # cairosvg # odfpy -ereuse-utils[cli,naming,session,test]==0.4.0b50 - # via - # -r requirements.in - # teal et-xmlfile==1.1.0 # via # -r requirements.in # openpyxl flask==1.0.2 # via - # ereuse-utils # flask-cors # flask-login # flask-sqlalchemy # flask-weasyprint # flask-wtf - # teal flask-cors==3.0.10 - # via - # -r requirements.in - # teal + # via -r requirements.in flask-login==0.5.0 # via -r requirements.in flask-sqlalchemy==2.5.1 - # via teal + # via -r requirements.in flask-weasyprint==0.4 # via -r requirements.in flask-wtf==1.0.0 @@ -91,8 +73,8 @@ html5lib==1.1 # via weasyprint idna==3.4 # via requests -inflection==0.3.1 - # via ereuse-utils +inflection==0.5.1 + # via -r requirements.in itsdangerous==2.0.1 # via # flask @@ -108,8 +90,8 @@ markupsafe==2.1.1 # wtforms marshmallow==3.0.0b11 # via + # -r requirements.in # marshmallow-enum - # teal # webargs marshmallow-enum==1.4.1 # via -r requirements.in @@ -126,13 +108,9 @@ openpyxl==3.0.10 pandas==1.3.5 # via -r requirements.in passlib==1.7.1 - # via - # -r requirements.in - # sqlalchemy-utils + # via -r requirements.in phonenumbers==8.9.11 - # via - # -r requirements.in - # sqlalchemy-utils + # via -r requirements.in pillow==9.2.0 # via cairosvg pint==0.9 @@ -173,9 +151,7 @@ requests==2.27.1 requests-mock==1.5.2 # via -r requirements.in requests-toolbelt==0.9.1 - # via - # -r requirements.in - # ereuse-utils + # via -r requirements.in six==1.16.0 # via # anytree @@ -194,11 +170,7 @@ sqlalchemy==1.3.24 # sqlalchemy-utils sqlalchemy-citext==1.3.post0 # via -r requirements.in -sqlalchemy-utils[color,password,phone]==0.33.11 - # via - # -r requirements.in - # teal -teal==0.2.0a38 +sqlalchemy-utils==0.33.11 # via -r requirements.in tinycss2==1.1.1 # via @@ -206,22 +178,22 @@ tinycss2==1.1.1 # cssselect2 # weasyprint tqdm==4.32.2 - # via - # -r requirements.in - # ereuse-utils + # via -r requirements.in urllib3==1.26.12 # via requests weasyprint==44 # via flask-weasyprint webargs==5.5.3 - # via teal + # via -r requirements.in webencodings==0.5.1 # via # cssselect2 # html5lib # tinycss2 werkzeug==2.0.3 - # via flask + # via + # -r requirements.in + # flask wtforms==3.0.1 # via flask-wtf xlrd==2.0.1 diff --git a/scripts/sharelot.py b/scripts/sharelot.py new file mode 100644 index 00000000..fca71044 --- /dev/null +++ b/scripts/sharelot.py @@ -0,0 +1,29 @@ +import sys +import uuid + +from decouple import config + +from ereuse_devicehub.db import db +from ereuse_devicehub.devicehub import Devicehub +from ereuse_devicehub.resources.lot.models import Lot, ShareLot +from ereuse_devicehub.resources.user.models import User + + +def main(): + schema = config('DB_SCHEMA') + app = Devicehub(inventory=schema) + app.app_context().push() + email = sys.argv[1] + lot_id = sys.argv[2] + id = uuid.uuid4() + user = User.query.filter_by(email=email).first() + lot = Lot.query.filter_by(id=lot_id).first() + + share_lot = ShareLot(id=id, lot=lot, user_to=user) + + db.session.add(share_lot) + db.session.commit() + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 9dbedba9..8ddf6ca6 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,10 @@ from pathlib import Path + from setuptools import find_packages, setup + from ereuse_devicehub import __version__ - -test_requires = [ - 'pytest', - 'requests_mock' -] +test_requires = ['pytest', 'requests_mock'] setup( name='ereuse-devicehub', @@ -15,7 +13,7 @@ setup( project_urls={ 'Documentation': 'http://devicehub.ereuse.org', 'Code': 'http://github.com/ereuse/devicehub-teal', - 'Issue tracker': 'https://tree.taiga.io/project/ereuseorg-devicehub/issues?q=rules' + 'Issue tracker': 'https://tree.taiga.io/project/ereuseorg-devicehub/issues?q=rules', }, license='Affero', author='eReuse.org team', @@ -26,10 +24,10 @@ setup( python_requires='>=3.7.3', long_description=Path('README.md').read_text('utf8'), install_requires=[ - 'teal>=0.2.0a38', # teal always first + # 'teal>=0.2.0a38', # teal always first 'click', 'click-spinner', - 'ereuse-utils[naming,test,session,cli]>=0.4b49', + # 'ereuse-utils[naming,test,session,cli]>=0.4b49', 'hashids', 'marshmallow_enum', 'psycopg2-binary', @@ -40,29 +38,21 @@ setup( 'sqlalchemy-citext', 'sqlalchemy-utils[password, color, phone]', 'Flask-WeasyPrint', - 'sortedcontainers' + 'sortedcontainers', ], extras_require={ 'docs': [ 'sphinx', 'sphinxcontrib-httpdomain >= 1.5.0', 'sphinxcontrib-plantuml >= 0.12', - 'sphinxcontrib-websupport >= 1.0.1' + 'sphinxcontrib-websupport >= 1.0.1', ], - 'docs-auto': [ - 'sphinx-autobuild' - ], - 'test': test_requires + 'docs-auto': ['sphinx-autobuild'], + 'test': test_requires, }, tests_require=test_requires, - entry_points={ - 'console_scripts': [ - 'dh = ereuse_devicehub.cli:cli' - ] - }, - setup_requires=[ - 'pytest-runner' - ], + entry_points={'console_scripts': ['dh = ereuse_devicehub.cli:cli']}, + setup_requires=['pytest-runner'], classifiers=[ 'Development Status :: 2 - Pre-Alpha', 'Environment :: Web Environment', @@ -76,5 +66,5 @@ setup( 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 'Topic :: Software Development :: Libraries :: Python Modules', - ] + ], ) diff --git a/tests/conftest.py b/tests/conftest.py index 8e5c52d1..f99f7717 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,6 @@ from datetime import datetime from pathlib import Path import boltons.urlutils -import ereuse_utils import jwt import pytest import yaml @@ -14,6 +13,7 @@ from decouple import config from psycopg2 import IntegrityError from sqlalchemy.exc import ProgrammingError +from ereuse_devicehub import ereuse_utils from ereuse_devicehub.api.views import api from ereuse_devicehub.client import Client, UserClient, UserClientFlask from ereuse_devicehub.config import DevicehubConfig @@ -67,6 +67,7 @@ def _app(config: TestConfig) -> Devicehub: app.register_blueprint(workbench) app.config["SQLALCHEMY_RECORD_QUERIES"] = True app.config['PROFILE'] = True + app.config['SCHEMA'] = 'test' # app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30]) mail = Mail(app) app.mail = mail diff --git a/tests/files/basic.csv b/tests/files/basic.csv index 3ceda076..87597616 100644 --- a/tests/files/basic.csv +++ b/tests/files/basic.csv @@ -1,2 +1,2 @@ -"PHID";"DHID";"Type";"Placeholder Palet";"Placeholder Id Supplier";"Placeholder Info";"Placeholder Components";"Placeholder Type";"Placeholder Serial Number";"Placeholder Part Number";"Placeholder Model";"Placeholder Manufacturer";"DocumentID";"Public Link";"Tag 1 Type";"Tag 1 ID";"Tag 1 Organization";"Tag 2 Type";"Tag 2 ID";"Tag 2 Organization";"Tag 3 Type";"Tag 3 ID";"Tag 3 Organization";"Device Hardware ID";"Device Type";"Device Chassis";"Device Serial Number";"Device Model";"Device Manufacturer";"Registered in";"Registered (process)";"Updated in (software)";"Updated in (web)";"Physical state";"Allocate state";"Lifecycle state";"Processor";"RAM (MB)";"Data Storage Size (MB)";"Processor 1";"Processor 1 Manufacturer";"Processor 1 Model";"Processor 1 Serial Number";"Processor 1 Number of cores";"Processor 1 Speed (GHz)";"Benchmark Processor 1 (points)";"Benchmark ProcessorSysbench Processor 1 (points)";"Processor 2";"Processor 2 Manufacturer";"Processor 2 Model";"Processor 2 Serial Number";"Processor 2 Number of cores";"Processor 2 Speed (GHz)";"Benchmark Processor 2 (points)";"Benchmark ProcessorSysbench Processor 2 (points)";"RamModule 1";"RamModule 1 Manufacturer";"RamModule 1 Model";"RamModule 1 Serial Number";"RamModule 1 Size (MB)";"RamModule 1 Speed (MHz)";"RamModule 2";"RamModule 2 Manufacturer";"RamModule 2 Model";"RamModule 2 Serial Number";"RamModule 2 Size (MB)";"RamModule 2 Speed (MHz)";"RamModule 3";"RamModule 3 Manufacturer";"RamModule 3 Model";"RamModule 3 Serial Number";"RamModule 3 Size (MB)";"RamModule 3 Speed (MHz)";"RamModule 4";"RamModule 4 Manufacturer";"RamModule 4 Model";"RamModule 4 Serial Number";"RamModule 4 Size (MB)";"RamModule 4 Speed (MHz)";"DataStorage 1";"DataStorage 1 Manufacturer";"DataStorage 1 Model";"DataStorage 1 Serial Number";"DataStorage 1 Size (MB)";"Erasure DataStorage 1";"Erasure DataStorage 1 Serial Number";"Erasure DataStorage 1 Size (MB)";"Erasure DataStorage 1 Software";"Erasure DataStorage 1 Result";"Erasure DataStorage 1 Certificate URL";"Erasure DataStorage 1 Type";"Erasure DataStorage 1 Method";"Erasure DataStorage 1 Elapsed (hours)";"Erasure DataStorage 1 Date";"Erasure DataStorage 1 Steps";"Erasure DataStorage 1 Steps Start Time";"Erasure DataStorage 1 Steps End Time";"Benchmark DataStorage 1 Read Speed (MB/s)";"Benchmark DataStorage 1 Writing speed (MB/s)";"Test DataStorage 1 Software";"Test DataStorage 1 Type";"Test DataStorage 1 Result";"Test DataStorage 1 Power cycle count";"Test DataStorage 1 Lifetime (days)";"Test DataStorage 1 Power on hours";"DataStorage 2";"DataStorage 2 Manufacturer";"DataStorage 2 Model";"DataStorage 2 Serial Number";"DataStorage 2 Size (MB)";"Erasure DataStorage 2";"Erasure DataStorage 2 Serial Number";"Erasure DataStorage 2 Size (MB)";"Erasure DataStorage 2 Software";"Erasure DataStorage 2 Result";"Erasure DataStorage 2 Certificate URL";"Erasure DataStorage 2 Type";"Erasure DataStorage 2 Method";"Erasure DataStorage 2 Elapsed (hours)";"Erasure DataStorage 2 Date";"Erasure DataStorage 2 Steps";"Erasure DataStorage 2 Steps Start Time";"Erasure DataStorage 2 Steps End Time";"Benchmark DataStorage 2 Read Speed (MB/s)";"Benchmark DataStorage 2 Writing speed (MB/s)";"Test DataStorage 2 Software";"Test DataStorage 2 Type";"Test DataStorage 2 Result";"Test DataStorage 2 Power cycle count";"Test DataStorage 2 Lifetime (days)";"Test DataStorage 2 Power on hours";"DataStorage 3";"DataStorage 3 Manufacturer";"DataStorage 3 Model";"DataStorage 3 Serial Number";"DataStorage 3 Size (MB)";"Erasure DataStorage 3";"Erasure DataStorage 3 Serial Number";"Erasure DataStorage 3 Size (MB)";"Erasure DataStorage 3 Software";"Erasure DataStorage 3 Result";"Erasure DataStorage 3 Certificate URL";"Erasure DataStorage 3 Type";"Erasure DataStorage 3 Method";"Erasure DataStorage 3 Elapsed (hours)";"Erasure DataStorage 3 Date";"Erasure DataStorage 3 Steps";"Erasure DataStorage 3 Steps Start Time";"Erasure DataStorage 3 Steps End Time";"Benchmark DataStorage 3 Read Speed (MB/s)";"Benchmark DataStorage 3 Writing speed (MB/s)";"Test DataStorage 3 Software";"Test DataStorage 3 Type";"Test DataStorage 3 Result";"Test DataStorage 3 Power cycle count";"Test DataStorage 3 Lifetime (days)";"Test DataStorage 3 Power on hours";"DataStorage 4";"DataStorage 4 Manufacturer";"DataStorage 4 Model";"DataStorage 4 Serial Number";"DataStorage 4 Size (MB)";"Erasure DataStorage 4";"Erasure DataStorage 4 Serial Number";"Erasure DataStorage 4 Size (MB)";"Erasure DataStorage 4 Software";"Erasure DataStorage 4 Result";"Erasure DataStorage 4 Certificate URL";"Erasure DataStorage 4 Type";"Erasure DataStorage 4 Method";"Erasure DataStorage 4 Elapsed (hours)";"Erasure DataStorage 4 Date";"Erasure DataStorage 4 Steps";"Erasure DataStorage 4 Steps Start Time";"Erasure DataStorage 4 Steps End Time";"Benchmark DataStorage 4 Read Speed (MB/s)";"Benchmark DataStorage 4 Writing speed (MB/s)";"Test DataStorage 4 Software";"Test DataStorage 4 Type";"Test DataStorage 4 Result";"Test DataStorage 4 Power cycle count";"Test DataStorage 4 Lifetime (days)";"Test DataStorage 4 Power on hours";"Motherboard 1";"Motherboard 1 Manufacturer";"Motherboard 1 Model";"Motherboard 1 Serial Number";"Display 1";"Display 1 Manufacturer";"Display 1 Model";"Display 1 Serial Number";"GraphicCard 1";"GraphicCard 1 Manufacturer";"GraphicCard 1 Model";"GraphicCard 1 Serial Number";"GraphicCard 1 Memory (MB)";"GraphicCard 2";"GraphicCard 2 Manufacturer";"GraphicCard 2 Model";"GraphicCard 2 Serial Number";"GraphicCard 2 Memory (MB)";"NetworkAdapter 1";"NetworkAdapter 1 Manufacturer";"NetworkAdapter 1 Model";"NetworkAdapter 1 Serial Number";"NetworkAdapter 2";"NetworkAdapter 2 Manufacturer";"NetworkAdapter 2 Model";"NetworkAdapter 2 Serial Number";"SoundCard 1";"SoundCard 1 Manufacturer";"SoundCard 1 Model";"SoundCard 1 Serial Number";"SoundCard 2";"SoundCard 2 Manufacturer";"SoundCard 2 Model";"SoundCard 2 Serial Number";"Device Rate";"Device Range";"Processor Rate";"Processor Range";"RAM Rate";"RAM Range";"Data Storage Rate";"Data Storage Range";"Benchmark RamSysbench (points)" -"4";"E39W3";"Snapshot";"";"";"";"";"Desktop";"d1s";"";"d1ml";"d1mr";"";"http://localhost/devices/E39W3";"";"";"";"";"";"";"";"";"";"0de0de8ed27a9a67e937a12a65799f6c5c69731c9bcd282054cd21a2faf980db";"Desktop";"Microtower";"d1s";"d1ml";"d1mr";"Wed Sep 21 15:39:24 2022";"Workbench 11.0";"2022-09-21 15:39:24.321860+02:00";"";"";"";"";"p1ml";"0";"0";"Processor 7: model p1ml, S/N p1s";"p1mr";"p1ml";"p1s";"";"1.6";"2410.0";"";"";"";"";"";"";"";"";"";"RamModule 6: model rm1ml, S/N rm1s";"rm1mr";"rm1ml";"rm1s";"";"1333";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"GraphicCard 5: model gc1ml, S/N gc1s";"gc1mr";"gc1ml";"gc1s";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"" +"PHID";"DHID";"Type";"Temporary Lots";"Incoming Lots";"Outgoing Lots";"Placeholder Pallet";"Placeholder Id Supplier";"Placeholder Id Internal";"Placeholder Info";"Placeholder Components";"Placeholder Type";"Placeholder Serial Number";"Placeholder Part Number";"Placeholder Model";"Placeholder Manufacturer";"DocumentID";"Public Link";"Tag 1 Type";"Tag 1 ID";"Tag 1 Organization";"Tag 2 Type";"Tag 2 ID";"Tag 2 Organization";"Tag 3 Type";"Tag 3 ID";"Tag 3 Organization";"Device Hardware ID";"Device Type";"Device Chassis";"Device Serial Number";"Device Model";"Device Manufacturer";"Registered in";"Registered (process)";"Updated in (software)";"Updated in (web)";"Physical state";"Allocate state";"Lifecycle state";"Processor";"RAM (MB)";"Data Storage Size (MB)";"Processor 1";"Processor 1 Manufacturer";"Processor 1 Model";"Processor 1 Serial Number";"Processor 1 Number of cores";"Processor 1 Speed (GHz)";"Benchmark Processor 1 (points)";"Benchmark ProcessorSysbench Processor 1 (points)";"Processor 2";"Processor 2 Manufacturer";"Processor 2 Model";"Processor 2 Serial Number";"Processor 2 Number of cores";"Processor 2 Speed (GHz)";"Benchmark Processor 2 (points)";"Benchmark ProcessorSysbench Processor 2 (points)";"RamModule 1";"RamModule 1 Manufacturer";"RamModule 1 Model";"RamModule 1 Serial Number";"RamModule 1 Size (MB)";"RamModule 1 Speed (MHz)";"RamModule 2";"RamModule 2 Manufacturer";"RamModule 2 Model";"RamModule 2 Serial Number";"RamModule 2 Size (MB)";"RamModule 2 Speed (MHz)";"RamModule 3";"RamModule 3 Manufacturer";"RamModule 3 Model";"RamModule 3 Serial Number";"RamModule 3 Size (MB)";"RamModule 3 Speed (MHz)";"RamModule 4";"RamModule 4 Manufacturer";"RamModule 4 Model";"RamModule 4 Serial Number";"RamModule 4 Size (MB)";"RamModule 4 Speed (MHz)";"DataStorage 1";"DataStorage 1 Manufacturer";"DataStorage 1 Model";"DataStorage 1 Serial Number";"DataStorage 1 Size (MB)";"Erasure DataStorage 1";"Erasure DataStorage 1 Serial Number";"Erasure DataStorage 1 Size (MB)";"Erasure DataStorage 1 Software";"Erasure DataStorage 1 Result";"Erasure DataStorage 1 Certificate URL";"Erasure DataStorage 1 Type";"Erasure DataStorage 1 Method";"Erasure DataStorage 1 Elapsed (hours)";"Erasure DataStorage 1 Date";"Erasure DataStorage 1 Steps";"Erasure DataStorage 1 Steps Start Time";"Erasure DataStorage 1 Steps End Time";"Benchmark DataStorage 1 Read Speed (MB/s)";"Benchmark DataStorage 1 Writing speed (MB/s)";"Test DataStorage 1 Software";"Test DataStorage 1 Type";"Test DataStorage 1 Result";"Test DataStorage 1 Power cycle count";"Test DataStorage 1 Lifetime (days)";"Test DataStorage 1 Power on hours";"DataStorage 2";"DataStorage 2 Manufacturer";"DataStorage 2 Model";"DataStorage 2 Serial Number";"DataStorage 2 Size (MB)";"Erasure DataStorage 2";"Erasure DataStorage 2 Serial Number";"Erasure DataStorage 2 Size (MB)";"Erasure DataStorage 2 Software";"Erasure DataStorage 2 Result";"Erasure DataStorage 2 Certificate URL";"Erasure DataStorage 2 Type";"Erasure DataStorage 2 Method";"Erasure DataStorage 2 Elapsed (hours)";"Erasure DataStorage 2 Date";"Erasure DataStorage 2 Steps";"Erasure DataStorage 2 Steps Start Time";"Erasure DataStorage 2 Steps End Time";"Benchmark DataStorage 2 Read Speed (MB/s)";"Benchmark DataStorage 2 Writing speed (MB/s)";"Test DataStorage 2 Software";"Test DataStorage 2 Type";"Test DataStorage 2 Result";"Test DataStorage 2 Power cycle count";"Test DataStorage 2 Lifetime (days)";"Test DataStorage 2 Power on hours";"DataStorage 3";"DataStorage 3 Manufacturer";"DataStorage 3 Model";"DataStorage 3 Serial Number";"DataStorage 3 Size (MB)";"Erasure DataStorage 3";"Erasure DataStorage 3 Serial Number";"Erasure DataStorage 3 Size (MB)";"Erasure DataStorage 3 Software";"Erasure DataStorage 3 Result";"Erasure DataStorage 3 Certificate URL";"Erasure DataStorage 3 Type";"Erasure DataStorage 3 Method";"Erasure DataStorage 3 Elapsed (hours)";"Erasure DataStorage 3 Date";"Erasure DataStorage 3 Steps";"Erasure DataStorage 3 Steps Start Time";"Erasure DataStorage 3 Steps End Time";"Benchmark DataStorage 3 Read Speed (MB/s)";"Benchmark DataStorage 3 Writing speed (MB/s)";"Test DataStorage 3 Software";"Test DataStorage 3 Type";"Test DataStorage 3 Result";"Test DataStorage 3 Power cycle count";"Test DataStorage 3 Lifetime (days)";"Test DataStorage 3 Power on hours";"DataStorage 4";"DataStorage 4 Manufacturer";"DataStorage 4 Model";"DataStorage 4 Serial Number";"DataStorage 4 Size (MB)";"Erasure DataStorage 4";"Erasure DataStorage 4 Serial Number";"Erasure DataStorage 4 Size (MB)";"Erasure DataStorage 4 Software";"Erasure DataStorage 4 Result";"Erasure DataStorage 4 Certificate URL";"Erasure DataStorage 4 Type";"Erasure DataStorage 4 Method";"Erasure DataStorage 4 Elapsed (hours)";"Erasure DataStorage 4 Date";"Erasure DataStorage 4 Steps";"Erasure DataStorage 4 Steps Start Time";"Erasure DataStorage 4 Steps End Time";"Benchmark DataStorage 4 Read Speed (MB/s)";"Benchmark DataStorage 4 Writing speed (MB/s)";"Test DataStorage 4 Software";"Test DataStorage 4 Type";"Test DataStorage 4 Result";"Test DataStorage 4 Power cycle count";"Test DataStorage 4 Lifetime (days)";"Test DataStorage 4 Power on hours";"Motherboard 1";"Motherboard 1 Manufacturer";"Motherboard 1 Model";"Motherboard 1 Serial Number";"Display 1";"Display 1 Manufacturer";"Display 1 Model";"Display 1 Serial Number";"GraphicCard 1";"GraphicCard 1 Manufacturer";"GraphicCard 1 Model";"GraphicCard 1 Serial Number";"GraphicCard 1 Memory (MB)";"GraphicCard 2";"GraphicCard 2 Manufacturer";"GraphicCard 2 Model";"GraphicCard 2 Serial Number";"GraphicCard 2 Memory (MB)";"NetworkAdapter 1";"NetworkAdapter 1 Manufacturer";"NetworkAdapter 1 Model";"NetworkAdapter 1 Serial Number";"NetworkAdapter 2";"NetworkAdapter 2 Manufacturer";"NetworkAdapter 2 Model";"NetworkAdapter 2 Serial Number";"SoundCard 1";"SoundCard 1 Manufacturer";"SoundCard 1 Model";"SoundCard 1 Serial Number";"SoundCard 2";"SoundCard 2 Manufacturer";"SoundCard 2 Model";"SoundCard 2 Serial Number";"Device Rate";"Device Range";"Processor Rate";"Processor Range";"RAM Rate";"RAM Range";"Data Storage Rate";"Data Storage Range";"Benchmark RamSysbench (points)" +"4";"E39W3";"Snapshot";"";"";"";"";"";"";"";"";"Desktop";"d1s";"";"d1ml";"d1mr";"";"http://localhost/devices/E39W3";"";"";"";"";"";"";"";"";"";"0de0de8ed27a9a67e937a12a65799f6c5c69731c9bcd282054cd21a2faf980db";"Desktop";"Microtower";"d1s";"d1ml";"d1mr";"Fri Apr 21 16:02:03 2023";"Workbench 11.0";"2023-04-21 16:02:03.734280+02:00";"";"";"";"";"p1ml";"0";"0";"Processor 7: model p1ml, S/N p1s";"p1mr";"p1ml";"p1s";"";"1.6";"2410.0";"";"";"";"";"";"";"";"";"";"RamModule 6: model rm1ml, S/N rm1s";"rm1mr";"rm1ml";"rm1s";"";"1333";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"GraphicCard 5: model gc1ml, S/N gc1s";"gc1mr";"gc1ml";"gc1s";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"" diff --git a/tests/files/export_devices.csv b/tests/files/export_devices.csv index 5754ab48..76483aa5 100644 --- a/tests/files/export_devices.csv +++ b/tests/files/export_devices.csv @@ -1,2 +1,2 @@ -"PHID";"DHID";"Type";"Placeholder Palet";"Placeholder Id Supplier";"Placeholder Info";"Placeholder Components";"Placeholder Type";"Placeholder Serial Number";"Placeholder Part Number";"Placeholder Model";"Placeholder Manufacturer";"DocumentID";"Public Link";"Tag 1 Type";"Tag 1 ID";"Tag 1 Organization";"Tag 2 Type";"Tag 2 ID";"Tag 2 Organization";"Tag 3 Type";"Tag 3 ID";"Tag 3 Organization";"Device Hardware ID";"Device Type";"Device Chassis";"Device Serial Number";"Device Model";"Device Manufacturer";"Registered in";"Registered (process)";"Updated in (software)";"Updated in (web)";"Physical state";"Allocate state";"Lifecycle state";"Processor";"RAM (MB)";"Data Storage Size (MB)";"Processor 1";"Processor 1 Manufacturer";"Processor 1 Model";"Processor 1 Serial Number";"Processor 1 Number of cores";"Processor 1 Speed (GHz)";"Benchmark Processor 1 (points)";"Benchmark ProcessorSysbench Processor 1 (points)";"Processor 2";"Processor 2 Manufacturer";"Processor 2 Model";"Processor 2 Serial Number";"Processor 2 Number of cores";"Processor 2 Speed (GHz)";"Benchmark Processor 2 (points)";"Benchmark ProcessorSysbench Processor 2 (points)";"RamModule 1";"RamModule 1 Manufacturer";"RamModule 1 Model";"RamModule 1 Serial Number";"RamModule 1 Size (MB)";"RamModule 1 Speed (MHz)";"RamModule 2";"RamModule 2 Manufacturer";"RamModule 2 Model";"RamModule 2 Serial Number";"RamModule 2 Size (MB)";"RamModule 2 Speed (MHz)";"RamModule 3";"RamModule 3 Manufacturer";"RamModule 3 Model";"RamModule 3 Serial Number";"RamModule 3 Size (MB)";"RamModule 3 Speed (MHz)";"RamModule 4";"RamModule 4 Manufacturer";"RamModule 4 Model";"RamModule 4 Serial Number";"RamModule 4 Size (MB)";"RamModule 4 Speed (MHz)";"DataStorage 1";"DataStorage 1 Manufacturer";"DataStorage 1 Model";"DataStorage 1 Serial Number";"DataStorage 1 Size (MB)";"Erasure DataStorage 1";"Erasure DataStorage 1 Serial Number";"Erasure DataStorage 1 Size (MB)";"Erasure DataStorage 1 Software";"Erasure DataStorage 1 Result";"Erasure DataStorage 1 Certificate URL";"Erasure DataStorage 1 Type";"Erasure DataStorage 1 Method";"Erasure DataStorage 1 Elapsed (hours)";"Erasure DataStorage 1 Date";"Erasure DataStorage 1 Steps";"Erasure DataStorage 1 Steps Start Time";"Erasure DataStorage 1 Steps End Time";"Benchmark DataStorage 1 Read Speed (MB/s)";"Benchmark DataStorage 1 Writing speed (MB/s)";"Test DataStorage 1 Software";"Test DataStorage 1 Type";"Test DataStorage 1 Result";"Test DataStorage 1 Power cycle count";"Test DataStorage 1 Lifetime (days)";"Test DataStorage 1 Power on hours";"DataStorage 2";"DataStorage 2 Manufacturer";"DataStorage 2 Model";"DataStorage 2 Serial Number";"DataStorage 2 Size (MB)";"Erasure DataStorage 2";"Erasure DataStorage 2 Serial Number";"Erasure DataStorage 2 Size (MB)";"Erasure DataStorage 2 Software";"Erasure DataStorage 2 Result";"Erasure DataStorage 2 Certificate URL";"Erasure DataStorage 2 Type";"Erasure DataStorage 2 Method";"Erasure DataStorage 2 Elapsed (hours)";"Erasure DataStorage 2 Date";"Erasure DataStorage 2 Steps";"Erasure DataStorage 2 Steps Start Time";"Erasure DataStorage 2 Steps End Time";"Benchmark DataStorage 2 Read Speed (MB/s)";"Benchmark DataStorage 2 Writing speed (MB/s)";"Test DataStorage 2 Software";"Test DataStorage 2 Type";"Test DataStorage 2 Result";"Test DataStorage 2 Power cycle count";"Test DataStorage 2 Lifetime (days)";"Test DataStorage 2 Power on hours";"DataStorage 3";"DataStorage 3 Manufacturer";"DataStorage 3 Model";"DataStorage 3 Serial Number";"DataStorage 3 Size (MB)";"Erasure DataStorage 3";"Erasure DataStorage 3 Serial Number";"Erasure DataStorage 3 Size (MB)";"Erasure DataStorage 3 Software";"Erasure DataStorage 3 Result";"Erasure DataStorage 3 Certificate URL";"Erasure DataStorage 3 Type";"Erasure DataStorage 3 Method";"Erasure DataStorage 3 Elapsed (hours)";"Erasure DataStorage 3 Date";"Erasure DataStorage 3 Steps";"Erasure DataStorage 3 Steps Start Time";"Erasure DataStorage 3 Steps End Time";"Benchmark DataStorage 3 Read Speed (MB/s)";"Benchmark DataStorage 3 Writing speed (MB/s)";"Test DataStorage 3 Software";"Test DataStorage 3 Type";"Test DataStorage 3 Result";"Test DataStorage 3 Power cycle count";"Test DataStorage 3 Lifetime (days)";"Test DataStorage 3 Power on hours";"DataStorage 4";"DataStorage 4 Manufacturer";"DataStorage 4 Model";"DataStorage 4 Serial Number";"DataStorage 4 Size (MB)";"Erasure DataStorage 4";"Erasure DataStorage 4 Serial Number";"Erasure DataStorage 4 Size (MB)";"Erasure DataStorage 4 Software";"Erasure DataStorage 4 Result";"Erasure DataStorage 4 Certificate URL";"Erasure DataStorage 4 Type";"Erasure DataStorage 4 Method";"Erasure DataStorage 4 Elapsed (hours)";"Erasure DataStorage 4 Date";"Erasure DataStorage 4 Steps";"Erasure DataStorage 4 Steps Start Time";"Erasure DataStorage 4 Steps End Time";"Benchmark DataStorage 4 Read Speed (MB/s)";"Benchmark DataStorage 4 Writing speed (MB/s)";"Test DataStorage 4 Software";"Test DataStorage 4 Type";"Test DataStorage 4 Result";"Test DataStorage 4 Power cycle count";"Test DataStorage 4 Lifetime (days)";"Test DataStorage 4 Power on hours";"Motherboard 1";"Motherboard 1 Manufacturer";"Motherboard 1 Model";"Motherboard 1 Serial Number";"Display 1";"Display 1 Manufacturer";"Display 1 Model";"Display 1 Serial Number";"GraphicCard 1";"GraphicCard 1 Manufacturer";"GraphicCard 1 Model";"GraphicCard 1 Serial Number";"GraphicCard 1 Memory (MB)";"GraphicCard 2";"GraphicCard 2 Manufacturer";"GraphicCard 2 Model";"GraphicCard 2 Serial Number";"GraphicCard 2 Memory (MB)";"NetworkAdapter 1";"NetworkAdapter 1 Manufacturer";"NetworkAdapter 1 Model";"NetworkAdapter 1 Serial Number";"NetworkAdapter 2";"NetworkAdapter 2 Manufacturer";"NetworkAdapter 2 Model";"NetworkAdapter 2 Serial Number";"SoundCard 1";"SoundCard 1 Manufacturer";"SoundCard 1 Model";"SoundCard 1 Serial Number";"SoundCard 2";"SoundCard 2 Manufacturer";"SoundCard 2 Model";"SoundCard 2 Serial Number";"Device Rate";"Device Range";"Processor Rate";"Processor Range";"RAM Rate";"RAM Range";"Data Storage Rate";"Data Storage Range";"Benchmark RamSysbench (points)" -"10";"E39W3";"Snapshot";"";"";"";"";"Laptop";"b8oaas048285";"";"1001pxd";"asustek computer inc.";"";"http://localhost/devices/E39W3";"";"";"";"";"";"";"";"";"";"83cb9066430a8ea7def04af61d521d6517193a486c02ea3bc914c9eaeb2b718b";"Laptop";"Netbook";"b8oaas048285";"1001pxd";"asustek computer inc.";"Wed Dec 14 12:28:44 2022";"Workbench 11.0a2";"2022-12-14 12:28:44.757147+01:00";"";"";"";"";"intel atom cpu n455 @ 2.66ghz";"1024";"238475";"Processor 7: model intel atom cpu n455 @ 2.66ghz, S/N None";"intel corp.";"intel atom cpu n455 @ 2.66ghz";"";"1";"2.667";"6666.24";"164.0803";"";"";"";"";"";"";"";"";"RamModule 11: model None, S/N None";"";"";"";"1024";"667";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"HardDrive 12: model hts54322, S/N e2024242cv86mm";"hitachi";"hts54322";"e2024242cv86mm";"238475";"8558ea99955f34c788cb72174c0ec165e0398306efbc0efe40b280b65d16d0d0";"e2024242cv86mm";"238475";"Workbench 11.0a2";"Success";"";"EraseBasic";"Shred";"1:16:49";"2022-12-14 12:28:44.712329+01:00";"✓ – StepRandom 1:16:49";"2018-07-03 11:15:22.257059+02:00";"2018-07-03 12:32:11.843190+02:00";"66.2";"21.8";"Workbench 11.0a2";"Short";"Failure";"";"";"0";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"Motherboard 13: model 1001pxd, S/N eee0123456720";"asustek computer inc.";"1001pxd";"eee0123456720";"";"";"";"";"GraphicCard 8: model atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller, S/N None";"intel corporation";"atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller";"";"256";"";"";"";"";"";"NetworkAdapter 5: model ar9285 wireless network adapter, S/N 74:2f:68:8b:fd:c9";"qualcomm atheros";"ar9285 wireless network adapter";"74:2f:68:8b:fd:c9";"NetworkAdapter 6: model ar8152 v2.0 fast ethernet, S/N 14:da:e9:42:f6:7b";"qualcomm atheros";"ar8152 v2.0 fast ethernet";"14:da:e9:42:f6:7b";"SoundCard 9: model nm10/ich7 family high definition audio controller, S/N None";"intel corporation";"nm10/ich7 family high definition audio controller";"";"SoundCard 10: model usb 2.0 uvc vga webcam, S/N 0x0001";"azurewave";"usb 2.0 uvc vga webcam";"0x0001";"";"";"";"";"";"";"";"";"15.7188" +"PHID";"DHID";"Type";"Temporary Lots";"Incoming Lots";"Outgoing Lots";"Placeholder Pallet";"Placeholder Id Supplier";"Placeholder Id Internal";"Placeholder Info";"Placeholder Components";"Placeholder Type";"Placeholder Serial Number";"Placeholder Part Number";"Placeholder Model";"Placeholder Manufacturer";"DocumentID";"Public Link";"Tag 1 Type";"Tag 1 ID";"Tag 1 Organization";"Tag 2 Type";"Tag 2 ID";"Tag 2 Organization";"Tag 3 Type";"Tag 3 ID";"Tag 3 Organization";"Device Hardware ID";"Device Type";"Device Chassis";"Device Serial Number";"Device Model";"Device Manufacturer";"Registered in";"Registered (process)";"Updated in (software)";"Updated in (web)";"Physical state";"Allocate state";"Lifecycle state";"Processor";"RAM (MB)";"Data Storage Size (MB)";"Processor 1";"Processor 1 Manufacturer";"Processor 1 Model";"Processor 1 Serial Number";"Processor 1 Number of cores";"Processor 1 Speed (GHz)";"Benchmark Processor 1 (points)";"Benchmark ProcessorSysbench Processor 1 (points)";"Processor 2";"Processor 2 Manufacturer";"Processor 2 Model";"Processor 2 Serial Number";"Processor 2 Number of cores";"Processor 2 Speed (GHz)";"Benchmark Processor 2 (points)";"Benchmark ProcessorSysbench Processor 2 (points)";"RamModule 1";"RamModule 1 Manufacturer";"RamModule 1 Model";"RamModule 1 Serial Number";"RamModule 1 Size (MB)";"RamModule 1 Speed (MHz)";"RamModule 2";"RamModule 2 Manufacturer";"RamModule 2 Model";"RamModule 2 Serial Number";"RamModule 2 Size (MB)";"RamModule 2 Speed (MHz)";"RamModule 3";"RamModule 3 Manufacturer";"RamModule 3 Model";"RamModule 3 Serial Number";"RamModule 3 Size (MB)";"RamModule 3 Speed (MHz)";"RamModule 4";"RamModule 4 Manufacturer";"RamModule 4 Model";"RamModule 4 Serial Number";"RamModule 4 Size (MB)";"RamModule 4 Speed (MHz)";"DataStorage 1";"DataStorage 1 Manufacturer";"DataStorage 1 Model";"DataStorage 1 Serial Number";"DataStorage 1 Size (MB)";"Erasure DataStorage 1";"Erasure DataStorage 1 Serial Number";"Erasure DataStorage 1 Size (MB)";"Erasure DataStorage 1 Software";"Erasure DataStorage 1 Result";"Erasure DataStorage 1 Certificate URL";"Erasure DataStorage 1 Type";"Erasure DataStorage 1 Method";"Erasure DataStorage 1 Elapsed (hours)";"Erasure DataStorage 1 Date";"Erasure DataStorage 1 Steps";"Erasure DataStorage 1 Steps Start Time";"Erasure DataStorage 1 Steps End Time";"Benchmark DataStorage 1 Read Speed (MB/s)";"Benchmark DataStorage 1 Writing speed (MB/s)";"Test DataStorage 1 Software";"Test DataStorage 1 Type";"Test DataStorage 1 Result";"Test DataStorage 1 Power cycle count";"Test DataStorage 1 Lifetime (days)";"Test DataStorage 1 Power on hours";"DataStorage 2";"DataStorage 2 Manufacturer";"DataStorage 2 Model";"DataStorage 2 Serial Number";"DataStorage 2 Size (MB)";"Erasure DataStorage 2";"Erasure DataStorage 2 Serial Number";"Erasure DataStorage 2 Size (MB)";"Erasure DataStorage 2 Software";"Erasure DataStorage 2 Result";"Erasure DataStorage 2 Certificate URL";"Erasure DataStorage 2 Type";"Erasure DataStorage 2 Method";"Erasure DataStorage 2 Elapsed (hours)";"Erasure DataStorage 2 Date";"Erasure DataStorage 2 Steps";"Erasure DataStorage 2 Steps Start Time";"Erasure DataStorage 2 Steps End Time";"Benchmark DataStorage 2 Read Speed (MB/s)";"Benchmark DataStorage 2 Writing speed (MB/s)";"Test DataStorage 2 Software";"Test DataStorage 2 Type";"Test DataStorage 2 Result";"Test DataStorage 2 Power cycle count";"Test DataStorage 2 Lifetime (days)";"Test DataStorage 2 Power on hours";"DataStorage 3";"DataStorage 3 Manufacturer";"DataStorage 3 Model";"DataStorage 3 Serial Number";"DataStorage 3 Size (MB)";"Erasure DataStorage 3";"Erasure DataStorage 3 Serial Number";"Erasure DataStorage 3 Size (MB)";"Erasure DataStorage 3 Software";"Erasure DataStorage 3 Result";"Erasure DataStorage 3 Certificate URL";"Erasure DataStorage 3 Type";"Erasure DataStorage 3 Method";"Erasure DataStorage 3 Elapsed (hours)";"Erasure DataStorage 3 Date";"Erasure DataStorage 3 Steps";"Erasure DataStorage 3 Steps Start Time";"Erasure DataStorage 3 Steps End Time";"Benchmark DataStorage 3 Read Speed (MB/s)";"Benchmark DataStorage 3 Writing speed (MB/s)";"Test DataStorage 3 Software";"Test DataStorage 3 Type";"Test DataStorage 3 Result";"Test DataStorage 3 Power cycle count";"Test DataStorage 3 Lifetime (days)";"Test DataStorage 3 Power on hours";"DataStorage 4";"DataStorage 4 Manufacturer";"DataStorage 4 Model";"DataStorage 4 Serial Number";"DataStorage 4 Size (MB)";"Erasure DataStorage 4";"Erasure DataStorage 4 Serial Number";"Erasure DataStorage 4 Size (MB)";"Erasure DataStorage 4 Software";"Erasure DataStorage 4 Result";"Erasure DataStorage 4 Certificate URL";"Erasure DataStorage 4 Type";"Erasure DataStorage 4 Method";"Erasure DataStorage 4 Elapsed (hours)";"Erasure DataStorage 4 Date";"Erasure DataStorage 4 Steps";"Erasure DataStorage 4 Steps Start Time";"Erasure DataStorage 4 Steps End Time";"Benchmark DataStorage 4 Read Speed (MB/s)";"Benchmark DataStorage 4 Writing speed (MB/s)";"Test DataStorage 4 Software";"Test DataStorage 4 Type";"Test DataStorage 4 Result";"Test DataStorage 4 Power cycle count";"Test DataStorage 4 Lifetime (days)";"Test DataStorage 4 Power on hours";"Motherboard 1";"Motherboard 1 Manufacturer";"Motherboard 1 Model";"Motherboard 1 Serial Number";"Display 1";"Display 1 Manufacturer";"Display 1 Model";"Display 1 Serial Number";"GraphicCard 1";"GraphicCard 1 Manufacturer";"GraphicCard 1 Model";"GraphicCard 1 Serial Number";"GraphicCard 1 Memory (MB)";"GraphicCard 2";"GraphicCard 2 Manufacturer";"GraphicCard 2 Model";"GraphicCard 2 Serial Number";"GraphicCard 2 Memory (MB)";"NetworkAdapter 1";"NetworkAdapter 1 Manufacturer";"NetworkAdapter 1 Model";"NetworkAdapter 1 Serial Number";"NetworkAdapter 2";"NetworkAdapter 2 Manufacturer";"NetworkAdapter 2 Model";"NetworkAdapter 2 Serial Number";"SoundCard 1";"SoundCard 1 Manufacturer";"SoundCard 1 Model";"SoundCard 1 Serial Number";"SoundCard 2";"SoundCard 2 Manufacturer";"SoundCard 2 Model";"SoundCard 2 Serial Number";"Device Rate";"Device Range";"Processor Rate";"Processor Range";"RAM Rate";"RAM Range";"Data Storage Rate";"Data Storage Range";"Benchmark RamSysbench (points)" +"10";"E39W3";"Snapshot";"";"";"";"";"";"";"";"";"Laptop";"b8oaas048285";"";"1001pxd";"asustek computer inc.";"";"http://localhost/devices/E39W3";"";"";"";"";"";"";"";"";"";"83cb9066430a8ea7def04af61d521d6517193a486c02ea3bc914c9eaeb2b718b";"Laptop";"Netbook";"b8oaas048285";"1001pxd";"asustek computer inc.";"Mon Mar 27 17:11:36 2023";"Workbench 11.0a2";"2023-03-27 17:11:36.126304+02:00";"";"";"";"";"intel atom cpu n455 @ 2.66ghz";"1024";"238475";"Processor 7: model intel atom cpu n455 @ 2.66ghz, S/N None";"intel corp.";"intel atom cpu n455 @ 2.66ghz";"";"1";"2.667";"6666.24";"164.0803";"";"";"";"";"";"";"";"";"RamModule 11: model None, S/N None";"";"";"";"1024";"667";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"HardDrive 12: model hts54322, S/N e2024242cv86mm";"hitachi";"hts54322";"e2024242cv86mm";"238475";"8558ea99955f34c788cb72174c0ec165e0398306efbc0efe40b280b65d16d0d0";"e2024242cv86mm";"238475";"Workbench 11.0a2";"Success";"";"EraseBasic";"Shred";"1:16:49";"2023-03-27 17:11:36.085767+02:00";"✓ – StepRandom 1:16:49";"2018-07-03 11:15:22.257059+02:00";"2018-07-03 12:32:11.843190+02:00";"66.2";"21.8";"Workbench 11.0a2";"Short";"Failure";"";"";"0";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"Motherboard 13: model 1001pxd, S/N eee0123456720";"asustek computer inc.";"1001pxd";"eee0123456720";"";"";"";"";"GraphicCard 8: model atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller, S/N None";"intel corporation";"atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller";"";"256";"";"";"";"";"";"NetworkAdapter 5: model ar9285 wireless network adapter, S/N 74:2f:68:8b:fd:c9";"qualcomm atheros";"ar9285 wireless network adapter";"74:2f:68:8b:fd:c9";"NetworkAdapter 6: model ar8152 v2.0 fast ethernet, S/N 14:da:e9:42:f6:7b";"qualcomm atheros";"ar8152 v2.0 fast ethernet";"14:da:e9:42:f6:7b";"SoundCard 9: model nm10/ich7 family high definition audio controller, S/N None";"intel corporation";"nm10/ich7 family high definition audio controller";"";"SoundCard 10: model usb 2.0 uvc vga webcam, S/N 0x0001";"azurewave";"usb 2.0 uvc vga webcam";"0x0001";"";"";"";"";"";"";"";"";"15.7188" diff --git a/tests/files/lots.csv b/tests/files/lots.csv index 35c36a34..9092f036 100644 --- a/tests/files/lots.csv +++ b/tests/files/lots.csv @@ -1,2 +1,2 @@ -"Lot Id";"Lot Name";"Lot Type";"Transfer Status";"Transfer Code";"Transfer Date";"Transfer Creation Date";"Transfer Update Date";"Transfer Description";"Devices Number";"Devices Snapshots";"Devices Placeholders";"Delivery Note Number";"Delivery Note Date";"Delivery Note Units";"Delivery Note Weight";"Receiver Note Number";"Receiver Note Date";"Receiver Note Units";"Receiver Note Weight" -"d992faea-5411-4a47-9388-073d67e9ee2d";"lot1";"Temporary";"";"";"";"";"";"";"1";"1";"0";"";"";"";"";"";"";"";"" +"Lot Id";"Lot Name";"Lot Type";"Transfer Status";"Transfer Code";"Transfer Date";"Transfer Creation Date";"Transfer Update Date";"Transfer Description";"Devices Number";"Devices Snapshots";"Devices Placeholders";"Delivery Note Number";"Delivery Note Date";"Delivery Note Units";"Delivery Note Weight";"Receiver Note Number";"Receiver Note Date";"Receiver Note Units";"Receiver Note Weight";"Customer Company Name";"Customer Location" +"31174349-d3a4-4bea-a8fc-aef6cc099041";"lot1";"Temporary";"";"";"";"";"";"";"1";"1";"0";"";"";"";"";"";"";"";"";"";"" diff --git a/tests/files/proposal_extended_csv_report.csv b/tests/files/proposal_extended_csv_report.csv index 73608689..12f977ce 100644 --- a/tests/files/proposal_extended_csv_report.csv +++ b/tests/files/proposal_extended_csv_report.csv @@ -1,3 +1,3 @@ -"PHID";"DHID";"Type";"Placeholder Palet";"Placeholder Id Supplier";"Placeholder Info";"Placeholder Components";"Placeholder Type";"Placeholder Serial Number";"Placeholder Part Number";"Placeholder Model";"Placeholder Manufacturer";"DocumentID";"Public Link";"Tag 1 Type";"Tag 1 ID";"Tag 1 Organization";"Tag 2 Type";"Tag 2 ID";"Tag 2 Organization";"Tag 3 Type";"Tag 3 ID";"Tag 3 Organization";"Device Hardware ID";"Device Type";"Device Chassis";"Device Serial Number";"Device Model";"Device Manufacturer";"Registered in";"Registered (process)";"Updated in (software)";"Updated in (web)";"Physical state";"Allocate state";"Lifecycle state";"Processor";"RAM (MB)";"Data Storage Size (MB)";"Processor 1";"Processor 1 Manufacturer";"Processor 1 Model";"Processor 1 Serial Number";"Processor 1 Number of cores";"Processor 1 Speed (GHz)";"Benchmark Processor 1 (points)";"Benchmark ProcessorSysbench Processor 1 (points)";"Processor 2";"Processor 2 Manufacturer";"Processor 2 Model";"Processor 2 Serial Number";"Processor 2 Number of cores";"Processor 2 Speed (GHz)";"Benchmark Processor 2 (points)";"Benchmark ProcessorSysbench Processor 2 (points)";"RamModule 1";"RamModule 1 Manufacturer";"RamModule 1 Model";"RamModule 1 Serial Number";"RamModule 1 Size (MB)";"RamModule 1 Speed (MHz)";"RamModule 2";"RamModule 2 Manufacturer";"RamModule 2 Model";"RamModule 2 Serial Number";"RamModule 2 Size (MB)";"RamModule 2 Speed (MHz)";"RamModule 3";"RamModule 3 Manufacturer";"RamModule 3 Model";"RamModule 3 Serial Number";"RamModule 3 Size (MB)";"RamModule 3 Speed (MHz)";"RamModule 4";"RamModule 4 Manufacturer";"RamModule 4 Model";"RamModule 4 Serial Number";"RamModule 4 Size (MB)";"RamModule 4 Speed (MHz)";"DataStorage 1";"DataStorage 1 Manufacturer";"DataStorage 1 Model";"DataStorage 1 Serial Number";"DataStorage 1 Size (MB)";"Erasure DataStorage 1";"Erasure DataStorage 1 Serial Number";"Erasure DataStorage 1 Size (MB)";"Erasure DataStorage 1 Software";"Erasure DataStorage 1 Result";"Erasure DataStorage 1 Certificate URL";"Erasure DataStorage 1 Type";"Erasure DataStorage 1 Method";"Erasure DataStorage 1 Elapsed (hours)";"Erasure DataStorage 1 Date";"Erasure DataStorage 1 Steps";"Erasure DataStorage 1 Steps Start Time";"Erasure DataStorage 1 Steps End Time";"Benchmark DataStorage 1 Read Speed (MB/s)";"Benchmark DataStorage 1 Writing speed (MB/s)";"Test DataStorage 1 Software";"Test DataStorage 1 Type";"Test DataStorage 1 Result";"Test DataStorage 1 Power cycle count";"Test DataStorage 1 Lifetime (days)";"Test DataStorage 1 Power on hours";"DataStorage 2";"DataStorage 2 Manufacturer";"DataStorage 2 Model";"DataStorage 2 Serial Number";"DataStorage 2 Size (MB)";"Erasure DataStorage 2";"Erasure DataStorage 2 Serial Number";"Erasure DataStorage 2 Size (MB)";"Erasure DataStorage 2 Software";"Erasure DataStorage 2 Result";"Erasure DataStorage 2 Certificate URL";"Erasure DataStorage 2 Type";"Erasure DataStorage 2 Method";"Erasure DataStorage 2 Elapsed (hours)";"Erasure DataStorage 2 Date";"Erasure DataStorage 2 Steps";"Erasure DataStorage 2 Steps Start Time";"Erasure DataStorage 2 Steps End Time";"Benchmark DataStorage 2 Read Speed (MB/s)";"Benchmark DataStorage 2 Writing speed (MB/s)";"Test DataStorage 2 Software";"Test DataStorage 2 Type";"Test DataStorage 2 Result";"Test DataStorage 2 Power cycle count";"Test DataStorage 2 Lifetime (days)";"Test DataStorage 2 Power on hours";"DataStorage 3";"DataStorage 3 Manufacturer";"DataStorage 3 Model";"DataStorage 3 Serial Number";"DataStorage 3 Size (MB)";"Erasure DataStorage 3";"Erasure DataStorage 3 Serial Number";"Erasure DataStorage 3 Size (MB)";"Erasure DataStorage 3 Software";"Erasure DataStorage 3 Result";"Erasure DataStorage 3 Certificate URL";"Erasure DataStorage 3 Type";"Erasure DataStorage 3 Method";"Erasure DataStorage 3 Elapsed (hours)";"Erasure DataStorage 3 Date";"Erasure DataStorage 3 Steps";"Erasure DataStorage 3 Steps Start Time";"Erasure DataStorage 3 Steps End Time";"Benchmark DataStorage 3 Read Speed (MB/s)";"Benchmark DataStorage 3 Writing speed (MB/s)";"Test DataStorage 3 Software";"Test DataStorage 3 Type";"Test DataStorage 3 Result";"Test DataStorage 3 Power cycle count";"Test DataStorage 3 Lifetime (days)";"Test DataStorage 3 Power on hours";"DataStorage 4";"DataStorage 4 Manufacturer";"DataStorage 4 Model";"DataStorage 4 Serial Number";"DataStorage 4 Size (MB)";"Erasure DataStorage 4";"Erasure DataStorage 4 Serial Number";"Erasure DataStorage 4 Size (MB)";"Erasure DataStorage 4 Software";"Erasure DataStorage 4 Result";"Erasure DataStorage 4 Certificate URL";"Erasure DataStorage 4 Type";"Erasure DataStorage 4 Method";"Erasure DataStorage 4 Elapsed (hours)";"Erasure DataStorage 4 Date";"Erasure DataStorage 4 Steps";"Erasure DataStorage 4 Steps Start Time";"Erasure DataStorage 4 Steps End Time";"Benchmark DataStorage 4 Read Speed (MB/s)";"Benchmark DataStorage 4 Writing speed (MB/s)";"Test DataStorage 4 Software";"Test DataStorage 4 Type";"Test DataStorage 4 Result";"Test DataStorage 4 Power cycle count";"Test DataStorage 4 Lifetime (days)";"Test DataStorage 4 Power on hours";"Motherboard 1";"Motherboard 1 Manufacturer";"Motherboard 1 Model";"Motherboard 1 Serial Number";"Display 1";"Display 1 Manufacturer";"Display 1 Model";"Display 1 Serial Number";"GraphicCard 1";"GraphicCard 1 Manufacturer";"GraphicCard 1 Model";"GraphicCard 1 Serial Number";"GraphicCard 1 Memory (MB)";"GraphicCard 2";"GraphicCard 2 Manufacturer";"GraphicCard 2 Model";"GraphicCard 2 Serial Number";"GraphicCard 2 Memory (MB)";"NetworkAdapter 1";"NetworkAdapter 1 Manufacturer";"NetworkAdapter 1 Model";"NetworkAdapter 1 Serial Number";"NetworkAdapter 2";"NetworkAdapter 2 Manufacturer";"NetworkAdapter 2 Model";"NetworkAdapter 2 Serial Number";"SoundCard 1";"SoundCard 1 Manufacturer";"SoundCard 1 Model";"SoundCard 1 Serial Number";"SoundCard 2";"SoundCard 2 Manufacturer";"SoundCard 2 Model";"SoundCard 2 Serial Number";"Device Rate";"Device Range";"Processor Rate";"Processor Range";"RAM Rate";"RAM Range";"Data Storage Rate";"Data Storage Range";"Benchmark RamSysbench (points)" -"10";"E39W3";"Snapshot";"";"";"";"";"Laptop";"b8oaas048285";"";"1001pxd";"asustek computer inc.";"";"http://localhost/devices/E39W3";"";"";"";"";"";"";"";"";"";"83cb9066430a8ea7def04af61d521d6517193a486c02ea3bc914c9eaeb2b718b";"Laptop";"Netbook";"b8oaas048285";"1001pxd";"asustek computer inc.";"Wed Sep 21 15:41:31 2022";"Workbench 11.0a2";"2022-09-21 15:41:31.084078+02:00";"";"";"";"";"intel atom cpu n455 @ 2.66ghz";"1024";"238475";"Processor 7: model intel atom cpu n455 @ 2.66ghz, S/N None";"intel corp.";"intel atom cpu n455 @ 2.66ghz";"";"1";"2.667";"6666.24";"164.0803";"";"";"";"";"";"";"";"";"RamModule 11: model None, S/N None";"";"";"";"1024";"667";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"HardDrive 12: model hts54322, S/N e2024242cv86mm";"hitachi";"hts54322";"e2024242cv86mm";"238475";"8558ea99955f34c788cb72174c0ec165e0398306efbc0efe40b280b65d16d0d0";"e2024242cv86mm";"238475";"Workbench 11.0a2";"Success";"";"EraseBasic";"Shred";"1:16:49";"2022-09-21 15:41:31.030798+02:00";"✓ – StepRandom 1:16:49";"2018-07-03 11:15:22.257059+02:00";"2018-07-03 12:32:11.843190+02:00";"66.2";"21.8";"Workbench 11.0a2";"Short";"Failure";"";"";"0";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"Motherboard 13: model 1001pxd, S/N eee0123456720";"asustek computer inc.";"1001pxd";"eee0123456720";"";"";"";"";"GraphicCard 8: model atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller, S/N None";"intel corporation";"atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller";"";"256";"";"";"";"";"";"NetworkAdapter 5: model ar9285 wireless network adapter, S/N 74:2f:68:8b:fd:c9";"qualcomm atheros";"ar9285 wireless network adapter";"74:2f:68:8b:fd:c9";"NetworkAdapter 6: model ar8152 v2.0 fast ethernet, S/N 14:da:e9:42:f6:7b";"qualcomm atheros";"ar8152 v2.0 fast ethernet";"14:da:e9:42:f6:7b";"SoundCard 9: model nm10/ich7 family high definition audio controller, S/N None";"intel corporation";"nm10/ich7 family high definition audio controller";"";"SoundCard 10: model usb 2.0 uvc vga webcam, S/N 0x0001";"azurewave";"usb 2.0 uvc vga webcam";"0x0001";"";"";"";"";"";"";"";"";"15.7188" -"24";"45VG4";"Snapshot";"";"";"";"";"Laptop";"b8oaas048287";"";"1001pxd";"asustek computer inc.";"";"http://localhost/devices/45VG4";"";"";"";"";"";"";"";"";"";"c3c6726385eb7e43a7476512236fe27fa234028c394237344d6b403611c25564";"Laptop";"Netbook";"b8oaas048287";"1001pxd";"asustek computer inc.";"Wed Sep 21 15:41:31 2022";"Workbench 11.0b11";"2022-09-21 15:41:31.398843+02:00";"";"";"";"";"intel atom cpu n455 @ 1.66ghz";"2048";"558558";"Processor 28: model intel atom cpu n455 @ 1.66ghz, S/N None";"intel corp.";"intel atom cpu n455 @ 1.66ghz";"";"1";"1.667";"6666.24";"164.0803";"";"";"";"";"";"";"";"";"RamModule 32: model None, S/N None";"";"";"";"1024";"667";"RamModule 33: model 48594d503131325336344350362d53362020, S/N 4f43487b";"hynix semiconductor";"48594d503131325336344350362d53362020";"4f43487b";"1024";"667";"";"";"";"";"";"";"";"";"";"";"";"";"HardDrive 34: model hts54322, S/N e2024242cv86hj";"hitachi";"hts54322";"e2024242cv86hj";"238475";"092462ec48ccf594fa369eb55c7026de4b56620f3430fb09a840ed3769b99851";"e2024242cv86hj";"238475";"Workbench 11.0b11";"Success";"";"EraseBasic";"Shred";"1:16:49";"2022-09-21 15:41:31.340555+02:00";"✓ – StepRandom 1:16:49";"2018-07-03 11:15:22.257059+02:00";"2018-07-03 12:32:11.843190+02:00";"66.2";"21.8";"Workbench 11.0b11";"Extended";"Failure";"";"";"0";"DataStorage 35: model wdc wd1600bevt-2, S/N wd-wx11a80w7430";"western digital";"wdc wd1600bevt-2";"wd-wx11a80w7430";"160041";"datastorage-western_digital-wdc_wd1600bevt-2-wd-wx11a80w7430";"wd-wx11a80w7430";"160041";"Workbench 11.0b11";"Failure";"";"EraseBasic";"Shred";"0:45:36";"2022-09-21 15:41:31.342722+02:00";"✓ – StepRandom 0:45:36";"2019-10-23 09:49:54.410830+02:00";"2019-10-23 10:35:31.400587+02:00";"41.6";"17.3";"Workbench 11.0b11";"Short";"Success";"5293";"195 days, 12:00:00";"4692";"SolidStateDrive 36: model wdc wd1600bevt-2, S/N wd-wx11a80w7430";"western digital";"wdc wd1600bevt-2";"wd-wx11a80w7430";"160042";"c5856fc1632d695a7eccf5062667d15439ec3c765245ba3fa60272c335d6e83f";"wd-wx11a80w7430";"160042";"Workbench 11.0b11";"Success";"";"EraseSectors";"Badblocks";"1:46:03";"2022-09-21 15:41:31.346565+02:00";"✓ – StepRandom 0:46:03,✓ – StepZero 1:00:00";"2019-08-19 18:48:19.690458+02:00,2019-08-19 19:34:22.690458+02:00";"2019-08-19 19:34:22.930562+02:00,2019-08-19 20:34:22.930562+02:00";"41.1";"17.1";"Workbench 11.0b11";"Short";"Success";"5231";"194 days, 17:00:00";"4673";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"Motherboard 37: model 1001pxd, S/N eee0123456789";"asustek computer inc.";"1001pxd";"eee0123456789";"";"auo ""auo""";"auo lcd monitor";"";"GraphicCard 29: model atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller, S/N None";"intel corporation";"atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller";"";"256";"";"";"";"";"";"NetworkAdapter 26: model ar9285 wireless network adapter, S/N 74:2f:68:8b:fd:c8";"qualcomm atheros";"ar9285 wireless network adapter";"74:2f:68:8b:fd:c8";"NetworkAdapter 27: model ar8152 v2.0 fast ethernet, S/N 14:da:e9:42:f6:7c";"qualcomm atheros";"ar8152 v2.0 fast ethernet";"14:da:e9:42:f6:7c";"SoundCard 30: model nm10/ich7 family high definition audio controller, S/N None";"intel corporation";"nm10/ich7 family high definition audio controller";"";"SoundCard 31: model usb 2.0 uvc vga webcam, S/N 0x0001";"azurewave";"usb 2.0 uvc vga webcam";"0x0001";"";"";"";"";"";"";"";"";"15.7188" +"PHID";"DHID";"Type";"Temporary Lots";"Incoming Lots";"Outgoing Lots";"Placeholder Pallet";"Placeholder Id Supplier";"Placeholder Id Internal";"Placeholder Info";"Placeholder Components";"Placeholder Type";"Placeholder Serial Number";"Placeholder Part Number";"Placeholder Model";"Placeholder Manufacturer";"DocumentID";"Public Link";"Tag 1 Type";"Tag 1 ID";"Tag 1 Organization";"Tag 2 Type";"Tag 2 ID";"Tag 2 Organization";"Tag 3 Type";"Tag 3 ID";"Tag 3 Organization";"Device Hardware ID";"Device Type";"Device Chassis";"Device Serial Number";"Device Model";"Device Manufacturer";"Registered in";"Registered (process)";"Updated in (software)";"Updated in (web)";"Physical state";"Allocate state";"Lifecycle state";"Processor";"RAM (MB)";"Data Storage Size (MB)";"Processor 1";"Processor 1 Manufacturer";"Processor 1 Model";"Processor 1 Serial Number";"Processor 1 Number of cores";"Processor 1 Speed (GHz)";"Benchmark Processor 1 (points)";"Benchmark ProcessorSysbench Processor 1 (points)";"Processor 2";"Processor 2 Manufacturer";"Processor 2 Model";"Processor 2 Serial Number";"Processor 2 Number of cores";"Processor 2 Speed (GHz)";"Benchmark Processor 2 (points)";"Benchmark ProcessorSysbench Processor 2 (points)";"RamModule 1";"RamModule 1 Manufacturer";"RamModule 1 Model";"RamModule 1 Serial Number";"RamModule 1 Size (MB)";"RamModule 1 Speed (MHz)";"RamModule 2";"RamModule 2 Manufacturer";"RamModule 2 Model";"RamModule 2 Serial Number";"RamModule 2 Size (MB)";"RamModule 2 Speed (MHz)";"RamModule 3";"RamModule 3 Manufacturer";"RamModule 3 Model";"RamModule 3 Serial Number";"RamModule 3 Size (MB)";"RamModule 3 Speed (MHz)";"RamModule 4";"RamModule 4 Manufacturer";"RamModule 4 Model";"RamModule 4 Serial Number";"RamModule 4 Size (MB)";"RamModule 4 Speed (MHz)";"DataStorage 1";"DataStorage 1 Manufacturer";"DataStorage 1 Model";"DataStorage 1 Serial Number";"DataStorage 1 Size (MB)";"Erasure DataStorage 1";"Erasure DataStorage 1 Serial Number";"Erasure DataStorage 1 Size (MB)";"Erasure DataStorage 1 Software";"Erasure DataStorage 1 Result";"Erasure DataStorage 1 Certificate URL";"Erasure DataStorage 1 Type";"Erasure DataStorage 1 Method";"Erasure DataStorage 1 Elapsed (hours)";"Erasure DataStorage 1 Date";"Erasure DataStorage 1 Steps";"Erasure DataStorage 1 Steps Start Time";"Erasure DataStorage 1 Steps End Time";"Benchmark DataStorage 1 Read Speed (MB/s)";"Benchmark DataStorage 1 Writing speed (MB/s)";"Test DataStorage 1 Software";"Test DataStorage 1 Type";"Test DataStorage 1 Result";"Test DataStorage 1 Power cycle count";"Test DataStorage 1 Lifetime (days)";"Test DataStorage 1 Power on hours";"DataStorage 2";"DataStorage 2 Manufacturer";"DataStorage 2 Model";"DataStorage 2 Serial Number";"DataStorage 2 Size (MB)";"Erasure DataStorage 2";"Erasure DataStorage 2 Serial Number";"Erasure DataStorage 2 Size (MB)";"Erasure DataStorage 2 Software";"Erasure DataStorage 2 Result";"Erasure DataStorage 2 Certificate URL";"Erasure DataStorage 2 Type";"Erasure DataStorage 2 Method";"Erasure DataStorage 2 Elapsed (hours)";"Erasure DataStorage 2 Date";"Erasure DataStorage 2 Steps";"Erasure DataStorage 2 Steps Start Time";"Erasure DataStorage 2 Steps End Time";"Benchmark DataStorage 2 Read Speed (MB/s)";"Benchmark DataStorage 2 Writing speed (MB/s)";"Test DataStorage 2 Software";"Test DataStorage 2 Type";"Test DataStorage 2 Result";"Test DataStorage 2 Power cycle count";"Test DataStorage 2 Lifetime (days)";"Test DataStorage 2 Power on hours";"DataStorage 3";"DataStorage 3 Manufacturer";"DataStorage 3 Model";"DataStorage 3 Serial Number";"DataStorage 3 Size (MB)";"Erasure DataStorage 3";"Erasure DataStorage 3 Serial Number";"Erasure DataStorage 3 Size (MB)";"Erasure DataStorage 3 Software";"Erasure DataStorage 3 Result";"Erasure DataStorage 3 Certificate URL";"Erasure DataStorage 3 Type";"Erasure DataStorage 3 Method";"Erasure DataStorage 3 Elapsed (hours)";"Erasure DataStorage 3 Date";"Erasure DataStorage 3 Steps";"Erasure DataStorage 3 Steps Start Time";"Erasure DataStorage 3 Steps End Time";"Benchmark DataStorage 3 Read Speed (MB/s)";"Benchmark DataStorage 3 Writing speed (MB/s)";"Test DataStorage 3 Software";"Test DataStorage 3 Type";"Test DataStorage 3 Result";"Test DataStorage 3 Power cycle count";"Test DataStorage 3 Lifetime (days)";"Test DataStorage 3 Power on hours";"DataStorage 4";"DataStorage 4 Manufacturer";"DataStorage 4 Model";"DataStorage 4 Serial Number";"DataStorage 4 Size (MB)";"Erasure DataStorage 4";"Erasure DataStorage 4 Serial Number";"Erasure DataStorage 4 Size (MB)";"Erasure DataStorage 4 Software";"Erasure DataStorage 4 Result";"Erasure DataStorage 4 Certificate URL";"Erasure DataStorage 4 Type";"Erasure DataStorage 4 Method";"Erasure DataStorage 4 Elapsed (hours)";"Erasure DataStorage 4 Date";"Erasure DataStorage 4 Steps";"Erasure DataStorage 4 Steps Start Time";"Erasure DataStorage 4 Steps End Time";"Benchmark DataStorage 4 Read Speed (MB/s)";"Benchmark DataStorage 4 Writing speed (MB/s)";"Test DataStorage 4 Software";"Test DataStorage 4 Type";"Test DataStorage 4 Result";"Test DataStorage 4 Power cycle count";"Test DataStorage 4 Lifetime (days)";"Test DataStorage 4 Power on hours";"Motherboard 1";"Motherboard 1 Manufacturer";"Motherboard 1 Model";"Motherboard 1 Serial Number";"Display 1";"Display 1 Manufacturer";"Display 1 Model";"Display 1 Serial Number";"GraphicCard 1";"GraphicCard 1 Manufacturer";"GraphicCard 1 Model";"GraphicCard 1 Serial Number";"GraphicCard 1 Memory (MB)";"GraphicCard 2";"GraphicCard 2 Manufacturer";"GraphicCard 2 Model";"GraphicCard 2 Serial Number";"GraphicCard 2 Memory (MB)";"NetworkAdapter 1";"NetworkAdapter 1 Manufacturer";"NetworkAdapter 1 Model";"NetworkAdapter 1 Serial Number";"NetworkAdapter 2";"NetworkAdapter 2 Manufacturer";"NetworkAdapter 2 Model";"NetworkAdapter 2 Serial Number";"SoundCard 1";"SoundCard 1 Manufacturer";"SoundCard 1 Model";"SoundCard 1 Serial Number";"SoundCard 2";"SoundCard 2 Manufacturer";"SoundCard 2 Model";"SoundCard 2 Serial Number";"Device Rate";"Device Range";"Processor Rate";"Processor Range";"RAM Rate";"RAM Range";"Data Storage Rate";"Data Storage Range";"Benchmark RamSysbench (points)" +"10";"E39W3";"Snapshot";"";"";"";"";"";"";"";"";"Laptop";"b8oaas048285";"";"1001pxd";"asustek computer inc.";"";"http://localhost/devices/E39W3";"";"";"";"";"";"";"";"";"";"83cb9066430a8ea7def04af61d521d6517193a486c02ea3bc914c9eaeb2b718b";"Laptop";"Netbook";"b8oaas048285";"1001pxd";"asustek computer inc.";"Fri Apr 21 15:58:08 2023";"Workbench 11.0a2";"2023-04-21 15:58:08.127427+02:00";"";"";"";"";"intel atom cpu n455 @ 2.66ghz";"1024";"238475";"Processor 7: model intel atom cpu n455 @ 2.66ghz, S/N None";"intel corp.";"intel atom cpu n455 @ 2.66ghz";"";"1";"2.667";"6666.24";"164.0803";"";"";"";"";"";"";"";"";"RamModule 11: model None, S/N None";"";"";"";"1024";"667";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"HardDrive 12: model hts54322, S/N e2024242cv86mm";"hitachi";"hts54322";"e2024242cv86mm";"238475";"8558ea99955f34c788cb72174c0ec165e0398306efbc0efe40b280b65d16d0d0";"e2024242cv86mm";"238475";"Workbench 11.0a2";"Success";"";"EraseBasic";"Shred";"1:16:49";"2023-04-21 15:58:08.084215+02:00";"✓ – StepRandom 1:16:49";"2018-07-03 11:15:22.257059+02:00";"2018-07-03 12:32:11.843190+02:00";"66.2";"21.8";"Workbench 11.0a2";"Short";"Failure";"";"";"0";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"Motherboard 13: model 1001pxd, S/N eee0123456720";"asustek computer inc.";"1001pxd";"eee0123456720";"";"";"";"";"GraphicCard 8: model atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller, S/N None";"intel corporation";"atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller";"";"256";"";"";"";"";"";"NetworkAdapter 5: model ar9285 wireless network adapter, S/N 74:2f:68:8b:fd:c9";"qualcomm atheros";"ar9285 wireless network adapter";"74:2f:68:8b:fd:c9";"NetworkAdapter 6: model ar8152 v2.0 fast ethernet, S/N 14:da:e9:42:f6:7b";"qualcomm atheros";"ar8152 v2.0 fast ethernet";"14:da:e9:42:f6:7b";"SoundCard 9: model nm10/ich7 family high definition audio controller, S/N None";"intel corporation";"nm10/ich7 family high definition audio controller";"";"SoundCard 10: model usb 2.0 uvc vga webcam, S/N 0x0001";"azurewave";"usb 2.0 uvc vga webcam";"0x0001";"";"";"";"";"";"";"";"";"15.7188" +"24";"45VG4";"Snapshot";"";"";"";"";"";"";"";"";"Laptop";"b8oaas048287";"";"1001pxd";"asustek computer inc.";"";"http://localhost/devices/45VG4";"";"";"";"";"";"";"";"";"";"c3c6726385eb7e43a7476512236fe27fa234028c394237344d6b403611c25564";"Laptop";"Netbook";"b8oaas048287";"1001pxd";"asustek computer inc.";"Fri Apr 21 15:58:08 2023";"Workbench 11.0b11";"2023-04-21 15:58:08.528897+02:00";"";"";"";"";"intel atom cpu n455 @ 1.66ghz";"2048";"558558";"Processor 28: model intel atom cpu n455 @ 1.66ghz, S/N None";"intel corp.";"intel atom cpu n455 @ 1.66ghz";"";"1";"1.667";"6666.24";"164.0803";"";"";"";"";"";"";"";"";"RamModule 32: model None, S/N None";"";"";"";"1024";"667";"RamModule 33: model 48594d503131325336344350362d53362020, S/N 4f43487b";"hynix semiconductor";"48594d503131325336344350362d53362020";"4f43487b";"1024";"667";"";"";"";"";"";"";"";"";"";"";"";"";"HardDrive 34: model hts54322, S/N e2024242cv86hj";"hitachi";"hts54322";"e2024242cv86hj";"238475";"092462ec48ccf594fa369eb55c7026de4b56620f3430fb09a840ed3769b99851";"e2024242cv86hj";"238475";"Workbench 11.0b11";"Success";"";"EraseBasic";"Shred";"1:16:49";"2023-04-21 15:58:08.478442+02:00";"✓ – StepRandom 1:16:49";"2018-07-03 11:15:22.257059+02:00";"2018-07-03 12:32:11.843190+02:00";"66.2";"21.8";"Workbench 11.0b11";"Extended";"Failure";"";"";"0";"DataStorage 35: model wdc wd1600bevt-2, S/N wd-wx11a80w7430";"western digital";"wdc wd1600bevt-2";"wd-wx11a80w7430";"160041";"29a61ec930753904d90764c4dfa2901d6cf40e2f63cf470f204edd7c38e493a6";"wd-wx11a80w7430";"160041";"Workbench 11.0b11";"Failure";"";"EraseBasic";"Shred";"0:45:36";"2023-04-21 15:58:08.480636+02:00";"✓ – StepRandom 0:45:36";"2019-10-23 09:49:54.410830+02:00";"2019-10-23 10:35:31.400587+02:00";"41.6";"17.3";"Workbench 11.0b11";"Short";"Success";"5293";"195 days, 12:00:00";"4692";"SolidStateDrive 36: model wdc wd1600bevt-2, S/N wd-wx11a80w7430";"western digital";"wdc wd1600bevt-2";"wd-wx11a80w7430";"160042";"c5856fc1632d695a7eccf5062667d15439ec3c765245ba3fa60272c335d6e83f";"wd-wx11a80w7430";"160042";"Workbench 11.0b11";"Success";"";"EraseSectors";"Badblocks";"1:46:03";"2023-04-21 15:58:08.485398+02:00";"✓ – StepRandom 0:46:03,✓ – StepZero 1:00:00";"2019-08-19 18:48:19.690458+02:00,2019-08-19 19:34:22.690458+02:00";"2019-08-19 19:34:22.930562+02:00,2019-08-19 20:34:22.930562+02:00";"41.1";"17.1";"Workbench 11.0b11";"Short";"Success";"5231";"194 days, 17:00:00";"4673";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"Motherboard 37: model 1001pxd, S/N eee0123456789";"asustek computer inc.";"1001pxd";"eee0123456789";"";"auo ""auo""";"auo lcd monitor";"";"GraphicCard 29: model atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller, S/N None";"intel corporation";"atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller";"";"256";"";"";"";"";"";"NetworkAdapter 26: model ar9285 wireless network adapter, S/N 74:2f:68:8b:fd:c8";"qualcomm atheros";"ar9285 wireless network adapter";"74:2f:68:8b:fd:c8";"NetworkAdapter 27: model ar8152 v2.0 fast ethernet, S/N 14:da:e9:42:f6:7c";"qualcomm atheros";"ar8152 v2.0 fast ethernet";"14:da:e9:42:f6:7c";"SoundCard 30: model nm10/ich7 family high definition audio controller, S/N None";"intel corporation";"nm10/ich7 family high definition audio controller";"";"SoundCard 31: model usb 2.0 uvc vga webcam, S/N 0x0001";"azurewave";"usb 2.0 uvc vga webcam";"0x0001";"";"";"";"";"";"";"";"";"15.7188" diff --git a/tests/test_action.py b/tests/test_action.py index 01ca8ca6..7992bd21 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -14,7 +14,6 @@ from flask import current_app as app from flask import g from pytest import raises from sqlalchemy.util import OrderedSet -from teal.enums import Currency from ereuse_devicehub.client import Client, UserClient from ereuse_devicehub.db import db @@ -39,6 +38,7 @@ from ereuse_devicehub.resources.enums import ( from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.enums import Currency from tests import conftest from tests.conftest import create_user, file, json_encode, yaml2json diff --git a/tests/test_agent.py b/tests/test_agent.py index 9d5f379e..e65779cc 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -3,25 +3,32 @@ from uuid import UUID import pytest from marshmallow import ValidationError from sqlalchemy_utils import PhoneNumber -from teal.db import UniqueViolation, DBError -from teal.enums import Country from ereuse_devicehub.config import DevicehubConfig from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.agent import OrganizationDef, models, schemas -from ereuse_devicehub.resources.agent.models import Membership, Organization, Person, System +from ereuse_devicehub.resources.agent.models import ( + Membership, + Organization, + Person, + System, +) +from ereuse_devicehub.teal.db import DBError, UniqueViolation +from ereuse_devicehub.teal.enums import Country from tests.conftest import app_context, create_user @pytest.mark.usefixtures(app_context.__name__) def test_agent(): """Tests creating an person.""" - person = Person(name='Timmy', - tax_id='xyz', - country=Country.ES, - telephone=PhoneNumber('+34666666666'), - email='foo@bar.com') + person = Person( + name='Timmy', + tax_id='xyz', + country=Country.ES, + telephone=PhoneNumber('+34666666666'), + email='foo@bar.com', + ) db.session.add(person) db.session.commit() @@ -36,8 +43,7 @@ def test_agent(): @pytest.mark.usefixtures(app_context.__name__) def test_system(): """Tests creating a system.""" - system = System(name='Workbench', - email='hello@ereuse.org') + system = System(name='Workbench', email='hello@ereuse.org') db.session.add(system) db.session.commit() @@ -49,10 +55,9 @@ def test_system(): @pytest.mark.usefixtures(app_context.__name__) def test_organization(): """Tests creating an organization.""" - org = Organization(name='ACME', - tax_id='xyz', - country=Country.ES, - email='contact@acme.com') + org = Organization( + name='ACME', tax_id='xyz', country=Country.ES, email='contact@acme.com' + ) db.session.add(org) db.session.commit() diff --git a/tests/test_basic.py b/tests/test_basic.py index 4b734079..bf78792d 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -55,6 +55,9 @@ def test_api_docs(client: Client): '/inventory/device/add/', '/inventory/device/{id}/', '/inventory/device/{dhid}/binding/', + '/inventory/device/{dhid}/document/del/{doc_id}', + '/inventory/device/{dhid}/document/edit/{doc_id}', + '/inventory/device/{dhid}/document/add/', '/inventory/device/erasure/', '/inventory/device/erasure/{orphans}/', '/inventory/all/device/', @@ -66,13 +69,15 @@ def test_api_docs(client: Client): '/inventory/lot/{lot_id}/device/add/', '/inventory/lot/{lot_id}/deliverynote/', '/inventory/lot/{lot_id}/receivernote/', - '/inventory/lot/{lot_id}/trade-document/add/', + '/inventory/lot/{lot_id}/transfer-document/add/', '/inventory/lot/{lot_id}/transfer/{type_id}/', '/inventory/lot/{lot_id}/opentransfer/', '/inventory/lot/{lot_id}/transfer/', '/inventory/lot/transfer/{type_id}/', '/inventory/lot/{lot_id}/upload-snapshot/', '/inventory/lot/{lot_id}/customerdetails/', + '/inventory/lot/{lot_id}/document/edit/{doc_id}', + '/inventory/lot/{lot_id}/document/del/{doc_id}', '/inventory/snapshots/{snapshot_uuid}/', '/inventory/snapshots/', '/inventory/tag/devices/{dhid}/add/', diff --git a/tests/test_db.py b/tests/test_db.py index 92b345ee..d2085b61 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -2,7 +2,8 @@ import datetime from uuid import UUID import pytest -from teal.db import UniqueViolation + +from ereuse_devicehub.teal.db import UniqueViolation @pytest.mark.mvp @@ -12,9 +13,10 @@ def test_unique_violation(): self.params = { 'uuid': UUID('f5efd26e-8754-46bc-87bf-fbccc39d60d9'), 'version': '11.0', - 'software': 'Workbench', 'elapsed': datetime.timedelta(0, 4), + 'software': 'Workbench', + 'elapsed': datetime.timedelta(0, 4), 'expected_actions': None, - 'id': UUID('dbdef3d8-2cac-48cb-adb8-419bc3e59687') + 'id': UUID('dbdef3d8-2cac-48cb-adb8-419bc3e59687'), } def __str__(self): diff --git a/tests/test_device.py b/tests/test_device.py index 0d67c325..edf83bdf 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,30 +1,21 @@ -import copy import datetime from uuid import UUID import pytest from colour import Color -from ereuse_utils.naming import Naming -from ereuse_utils.test import ANY from flask import g -from pytest import raises from sqlalchemy.util import OrderedSet -from teal.db import ResourceNotFound -from teal.enums import Layouts from ereuse_devicehub.client import Client, UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub +from ereuse_devicehub.ereuse_utils.test import ANY from ereuse_devicehub.resources.action import models as m from ereuse_devicehub.resources.action.models import Remove, TestConnectivity from ereuse_devicehub.resources.agent.models import Person from ereuse_devicehub.resources.device import models as d from ereuse_devicehub.resources.device.schemas import Device as DeviceS -from ereuse_devicehub.resources.device.sync import ( - MismatchBetweenTags, - MismatchBetweenTagsAndHid, - Sync, -) +from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.enums import ( ComputerChassis, DisplayTech, @@ -34,8 +25,9 @@ from ereuse_devicehub.resources.enums import ( ) from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.user import User +from ereuse_devicehub.teal.enums import Layouts from tests import conftest -from tests.conftest import file, json_encode, yaml2json +from tests.conftest import file, yaml2json @pytest.mark.mvp diff --git a/tests/test_device_find.py b/tests/test_device_find.py index ae5eff5c..9b584be8 100644 --- a/tests/test_device_find.py +++ b/tests/test_device_find.py @@ -1,40 +1,46 @@ -import pytest import uuid -from teal.utils import compiled + +import pytest from ereuse_devicehub.client import UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.action.models import Snapshot -from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, Laptop, Server, \ - SolidStateDrive +from ereuse_devicehub.resources.device.models import ( + Desktop, + Device, + GraphicCard, + Laptop, + Server, + SolidStateDrive, +) from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.device.views import Filters, Sorting from ereuse_devicehub.resources.enums import ComputerChassis from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.teal.utils import compiled from tests import conftest -from tests.conftest import file, yaml2json, json_encode +from tests.conftest import file, json_encode, yaml2json @pytest.mark.mvp @pytest.mark.usefixtures(conftest.app_context.__name__) def test_device_filters(): schema = Filters() - q = schema.load({ - 'type': ['Computer', 'Laptop'], - 'manufacturer': 'Dell', - 'rating': { - 'rating': [3, 6], - 'appearance': [2, 4] - }, - 'tag': { - 'id': ['bcn-', 'activa-02'] + q = schema.load( + { + 'type': ['Computer', 'Laptop'], + 'manufacturer': 'Dell', + 'rating': {'rating': [3, 6], 'appearance': [2, 4]}, + 'tag': {'id': ['bcn-', 'activa-02']}, } - }) + ) s, params = compiled(Device, q) # Order between query clauses can change - assert '(device.type IN (%(type_1)s, %(type_2)s, %(type_3)s, %(type_4)s) ' \ - 'OR device.type IN (%(type_5)s))' in s + assert ( + '(device.type IN (%(type_1)s, %(type_2)s, %(type_3)s, %(type_4)s) ' + 'OR device.type IN (%(type_5)s))' in s + ) assert 'device.manufacturer ILIKE %(manufacturer_1)s' in s assert 'rate.rating BETWEEN %(rating_1)s AND %(rating_2)s' in s assert 'rate.appearance BETWEEN %(appearance_1)s AND %(appearance_2)s' in s @@ -42,11 +48,33 @@ def test_device_filters(): # type_x can be assigned at different values # ex: type_1 can be 'Desktop' in one execution but the next one 'Laptop' - assert set(params.keys()) == {'id_2', 'appearance_1', 'type_1', 'type_4', 'rating_2', 'type_5', - 'type_3', 'type_2', 'appearance_2', 'id_1', 'rating_1', - 'manufacturer_1'} - assert set(params.values()) == {2.0, 'Laptop', 4.0, 3.0, 6.0, 'Desktop', 'activa-02%', - 'Server', 'Dell%', 'Computer', 'bcn-%'} + assert set(params.keys()) == { + 'id_2', + 'appearance_1', + 'type_1', + 'type_4', + 'rating_2', + 'type_5', + 'type_3', + 'type_2', + 'appearance_2', + 'id_1', + 'rating_1', + 'manufacturer_1', + } + assert set(params.values()) == { + 2.0, + 'Laptop', + 4.0, + 3.0, + 6.0, + 'Desktop', + 'activa-02%', + 'Server', + 'Dell%', + 'Computer', + 'bcn-%', + } @pytest.mark.usefixtures(conftest.app_context.__name__) @@ -70,22 +98,30 @@ def device_query_dummy(app: Devicehub): """ with app.app_context(): devices = ( # The order matters ;-) - Desktop(serial_number='1', - model='ml1', - manufacturer='mr1', - chassis=ComputerChassis.Tower), - Desktop(serial_number='2', - model='ml2', - manufacturer='mr2', - chassis=ComputerChassis.Microtower), - Laptop(serial_number='3', - model='ml3', - manufacturer='mr3', - chassis=ComputerChassis.Detachable), - Server(serial_number='4', - model='ml4', - manufacturer='mr4', - chassis=ComputerChassis.Tower), + Desktop( + serial_number='1', + model='ml1', + manufacturer='mr1', + chassis=ComputerChassis.Tower, + ), + Desktop( + serial_number='2', + model='ml2', + manufacturer='mr2', + chassis=ComputerChassis.Microtower, + ), + Laptop( + serial_number='3', + model='ml3', + manufacturer='mr3', + chassis=ComputerChassis.Detachable, + ), + Server( + serial_number='4', + model='ml4', + manufacturer='mr4', + chassis=ComputerChassis.Tower, + ), ) devices[0].components.add( GraphicCard(serial_number='1-gc', model='s1ml', manufacturer='s1mr') @@ -116,10 +152,13 @@ def test_device_query_filter_type(user: UserClient): @pytest.mark.usefixtures(device_query_dummy.__name__) def test_device_query_filter_sort(user: UserClient): - i, _ = user.get(res=Device, query=[ - ('sort', {'created': Sorting.DESCENDING}), - ('filter', {'type': ['Computer']}) - ]) + i, _ = user.get( + res=Device, + query=[ + ('sort', {'created': Sorting.DESCENDING}), + ('filter', {'type': ['Computer']}), + ], + ) assert ('4', '3', '2', '1') == tuple(d['serialNumber'] for d in i['items']) @@ -128,46 +167,49 @@ def test_device_query_filter_lots(user: UserClient): parent, _ = user.post({'name': 'Parent'}, res=Lot) child, _ = user.post({'name': 'Child'}, res=Lot) - i, _ = user.get(res=Device, query=[ - ('filter', {'lot': {'id': [parent['id']]}}) - ]) + i, _ = user.get(res=Device, query=[('filter', {'lot': {'id': [parent['id']]}})]) assert not i['items'], 'No devices in lot' - parent, _ = user.post({}, - res=Lot, - item='{}/children'.format(parent['id']), - query=[('id', child['id'])]) - i, _ = user.get(res=Device, query=[ - ('filter', {'type': ['Computer']}) - ]) + parent, _ = user.post( + {}, + res=Lot, + item='{}/children'.format(parent['id']), + query=[('id', child['id'])], + ) + i, _ = user.get(res=Device, query=[('filter', {'type': ['Computer']})]) assert ('1', '2', '3', '4') == tuple(d['serialNumber'] for d in i['items']) - parent, _ = user.post({}, - res=Lot, - item='{}/devices'.format(parent['id']), - query=[('id', d['id']) for d in i['items'][:2]]) - child, _ = user.post({}, - res=Lot, - item='{}/devices'.format(child['id']), - query=[('id', d['id']) for d in i['items'][2:]]) - i, _ = user.get(res=Device, query=[ - ('filter', {'lot': {'id': [parent['id']]}}) - ]) + parent, _ = user.post( + {}, + res=Lot, + item='{}/devices'.format(parent['id']), + query=[('id', d['id']) for d in i['items'][:2]], + ) + child, _ = user.post( + {}, + res=Lot, + item='{}/devices'.format(child['id']), + query=[('id', d['id']) for d in i['items'][2:]], + ) + i, _ = user.get(res=Device, query=[('filter', {'lot': {'id': [parent['id']]}})]) assert ('1', '2', '3', '4', '1-gc', '2-ssd', '4-ssd') == tuple( x['serialNumber'] for x in i['items'] - ), 'The parent lot contains 2 items plus indirectly the other ' \ - '2 from the child lot, with all their 2 components' + ), ( + 'The parent lot contains 2 items plus indirectly the other ' + '2 from the child lot, with all their 2 components' + ) - i, _ = user.get(res=Device, query=[ - ('filter', {'type': ['Computer'], 'lot': {'id': [parent['id']]}}), - ]) + i, _ = user.get( + res=Device, + query=[ + ('filter', {'type': ['Computer'], 'lot': {'id': [parent['id']]}}), + ], + ) assert ('1', '2', '3', '4') == tuple(x['serialNumber'] for x in i['items']) - s, _ = user.get(res=Device, query=[ - ('filter', {'lot': {'id': [child['id']]}}) - ]) + s, _ = user.get(res=Device, query=[('filter', {'lot': {'id': [child['id']]}})]) assert ('3', '4', '4-ssd') == tuple(x['serialNumber'] for x in s['items']) - s, _ = user.get(res=Device, query=[ - ('filter', {'lot': {'id': [child['id'], parent['id']]}}) - ]) + s, _ = user.get( + res=Device, query=[('filter', {'lot': {'id': [child['id'], parent['id']]}})] + ) assert ('1', '2', '3', '4', '1-gc', '2-ssd', '4-ssd') == tuple( x['serialNumber'] for x in s['items'] ), 'Adding both lots is redundant in this case and we have the 4 elements.' diff --git a/tests/test_documents.py b/tests/test_documents.py index 743577b5..aa5889c9 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -1,26 +1,22 @@ import csv import hashlib -from datetime import datetime from io import BytesIO, StringIO from pathlib import Path import pytest -import teal.marshmallow -from ereuse_utils.test import ANY -from flask import url_for -from werkzeug.exceptions import Unauthorized +from ereuse_devicehub.teal.marshmallow import ValidationError from ereuse_devicehub import auth from ereuse_devicehub.client import Client, UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub +from ereuse_devicehub.ereuse_utils.test import ANY from ereuse_devicehub.resources.action.models import Allocate, Live, Snapshot from ereuse_devicehub.resources.device import models as d from ereuse_devicehub.resources.documents import documents from ereuse_devicehub.resources.enums import SessionType from ereuse_devicehub.resources.hash_reports import ReportHash from ereuse_devicehub.resources.lot.models import Lot -from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.user.models import Session from tests import conftest @@ -94,7 +90,7 @@ def test_erasure_certificate_wrong_id(client: Client): client.get( res=documents.DocumentDef.t, item='erasures/this-is-not-an-id', - status=teal.marshmallow.ValidationError, + status=ValidationError, ) @@ -269,11 +265,11 @@ def test_export_basic_snapshot(user: UserClient): assert fixture_csv[0] == export_csv[0], 'Headers are not equal' assert ( - fixture_csv[1][:29] == export_csv[1][:29] + fixture_csv[1][:33] == export_csv[1][:33] ), 'Computer information are not equal' - assert fixture_csv[1][30] == export_csv[1][30], 'Computer information are not equal' + assert fixture_csv[1][34] == export_csv[1][34], 'Computer information are not equal' assert ( - fixture_csv[1][32:] == export_csv[1][32:] + fixture_csv[1][36:] == export_csv[1][36:] ), 'Computer information are not equal' @@ -339,38 +335,38 @@ def test_export_extended(app: Devicehub, user: UserClient): assert fixture_csv[0] == export_csv[0], 'Headers are not equal' assert ( - fixture_csv[1][:29] == export_csv[1][:29] + fixture_csv[1][:33] == export_csv[1][:33] ), 'Computer information are not equal' - assert fixture_csv[1][30] == export_csv[1][30], 'Computer information are not equal' + assert fixture_csv[1][34] == export_csv[1][34], 'Computer information are not equal' assert ( - fixture_csv[1][32:93] == export_csv[1][32:93] + fixture_csv[1][36:94] == export_csv[1][36:94] ), 'Computer information are not equal' - assert fixture_csv[1][94] == export_csv[1][94], 'Computer information are not equal' + assert fixture_csv[1][98] == export_csv[1][98], 'Computer information are not equal' assert ( - fixture_csv[1][97:] == export_csv[1][97:] + fixture_csv[1][101:] == export_csv[1][101:] ), 'Computer information are not equal' assert ( - fixture_csv[2][:29] == export_csv[2][:29] + fixture_csv[2][:33] == export_csv[2][:33] ), 'Computer information are not equal' - assert fixture_csv[2][30] == export_csv[2][30], 'Computer information are not equal' + assert fixture_csv[2][34] == export_csv[2][34], 'Computer information are not equal' assert ( - fixture_csv[2][32:93] == export_csv[2][32:93] + fixture_csv[2][36:97] == export_csv[2][36:97] ), 'Computer information are not equal' - assert fixture_csv[2][94] == export_csv[2][94], 'Computer information are not equal' + assert fixture_csv[2][98] == export_csv[2][98], 'Computer information are not equal' assert ( - fixture_csv[2][97:107] == export_csv[2][97:107] + fixture_csv[2][101:111] == export_csv[2][101:111] ), 'Computer information are not equal' assert ( - fixture_csv[2][120] == export_csv[2][120] + fixture_csv[2][124] == export_csv[2][124] ), 'Computer information are not equal' assert ( - fixture_csv[2][123:144] == export_csv[2][123:144] + fixture_csv[2][127:148] == export_csv[2][127:148] ), 'Computer information are not equal' assert ( - fixture_csv[2][146] == export_csv[2][146] + fixture_csv[2][150] == export_csv[2][150] ), 'Computer information are not equal' assert ( - fixture_csv[2][149:] == export_csv[2][149:] + fixture_csv[2][153:] == export_csv[2][153:] ), 'Computer information are not equal' diff --git a/tests/test_render_2_0.py b/tests/test_render_2_0.py index c84ad143..b5bd45ab 100644 --- a/tests/test_render_2_0.py +++ b/tests/test_render_2_0.py @@ -264,16 +264,16 @@ def test_export_devices(user3: UserClientFlask): assert fixture_csv[0] == export_csv[0], 'Headers are not equal' assert ( - fixture_csv[1][:29] == export_csv[1][:29] + fixture_csv[1][:33] == export_csv[1][:33] ), 'Computer information are not equal' - assert fixture_csv[1][30] == export_csv[1][30], 'Computer information are not equal' + assert fixture_csv[1][34] == export_csv[1][34], 'Computer information are not equal' assert ( - fixture_csv[1][32:93] == export_csv[1][32:93] + fixture_csv[1][36:97] == export_csv[1][36:97] ), 'Computer information are not equal' - assert fixture_csv[1][94] == export_csv[1][94], 'Computer information are not equal' + assert fixture_csv[1][98] == export_csv[1][98], 'Computer information are not equal' assert ( - fixture_csv[1][98:] == export_csv[1][98:] + fixture_csv[1][102:] == export_csv[1][102:] ), 'Computer information are not equal' @@ -2468,7 +2468,7 @@ def test_bug_3831_documents(user3: UserClientFlask): lot = Lot.query.filter_by(name=lot_name).one() lot_id = lot.id - uri = f'/inventory/lot/{lot_id}/trade-document/add/' + uri = f'/inventory/lot/{lot_id}/transfer-document/add/' body, status = user3.get(uri) txt = 'Error, this lot is not a transfer lot.' @@ -2486,7 +2486,7 @@ def test_bug_3831_documents(user3: UserClientFlask): assert 'Incoming Lot' in body lot_id = Lot.query.all()[1].id - uri = f'/inventory/lot/{lot_id}/trade-document/add/' + uri = f'/inventory/lot/{lot_id}/transfer-document/add/' body, status = user3.get(uri) b_file = b'1234567890' @@ -2502,12 +2502,12 @@ def test_bug_3831_documents(user3: UserClientFlask): 'file': file_upload, } - uri = f'/inventory/lot/{lot_id}/trade-document/add/' + uri = f'/inventory/lot/{lot_id}/transfer-document/add/' body, status = user3.post(uri, data=data, content_type="multipart/form-data") assert status == '200 OK' # Second document - uri = f'/inventory/lot/{lot_id}/trade-document/add/' + uri = f'/inventory/lot/{lot_id}/transfer-document/add/' file_upload = (BytesIO(b_file), file_name) data['file'] = file_upload data['csrf_token'] = generate_csrf() @@ -2774,3 +2774,82 @@ def test_reliable_device(user3: UserClientFlask): assert Snapshot.query.first() == snapshot assert len(snapshot.device.components) == 8 assert len(snapshot.device.actions) == 7 + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_add_device_document(user3: UserClientFlask): + snapshot = create_device(user3, 'real-eee-1001pxd.snapshot.12.json') + device = Device.query.filter_by(devicehub_id=snapshot.device.dhid).one() + uri = '/inventory/device/{}/document/add/'.format(device.dhid) + user3.get(uri) + + name = "doc1.pdf" + url = "https://www.usody.com/" + file_name = (BytesIO(b'1234567890'), name) + data = { + 'url': url, + 'file_name': file_name, + 'csrf_token': generate_csrf(), + } + + user3.post(uri, data=data, content_type="multipart/form-data") + assert device.documents[0].file_name == name + assert device.documents[0].url.to_text() == url + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_edit_device_document(user3: UserClientFlask): + snapshot = create_device(user3, 'real-eee-1001pxd.snapshot.12.json') + device = Device.query.filter_by(devicehub_id=snapshot.device.dhid).one() + uri = '/inventory/device/{}/document/add/'.format(device.dhid) + user3.get(uri) + + name = "doc1.pdf" + url = "https://www.usody.com/" + file_name = (BytesIO(b'1234567890'), name) + data = { + 'url': url, + 'file_name': file_name, + 'csrf_token': generate_csrf(), + } + + user3.post(uri, data=data, content_type="multipart/form-data") + + doc_id = str(device.documents[0].id) + uri = '/inventory/device/{}/document/edit/{}'.format(device.dhid, doc_id) + user3.get(uri) + + data['url'] = "https://www.ereuse.org/" + data['csrf_token'] = generate_csrf() + data['file_name'] = (BytesIO(b'1234567890'), name) + + user3.post(uri, data=data, content_type="multipart/form-data") + assert device.documents[0].file_name == name + assert device.documents[0].url.to_text() == data['url'] + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_delete_device_document(user3: UserClientFlask): + snapshot = create_device(user3, 'real-eee-1001pxd.snapshot.12.json') + device = Device.query.filter_by(devicehub_id=snapshot.device.dhid).one() + uri = '/inventory/device/{}/document/add/'.format(device.dhid) + user3.get(uri) + + name = "doc1.pdf" + url = "https://www.usody.com/" + file_name = (BytesIO(b'1234567890'), name) + data = { + 'url': url, + 'file_name': file_name, + 'csrf_token': generate_csrf(), + } + + user3.post(uri, data=data, content_type="multipart/form-data") + + doc_id = str(device.documents[0].id) + uri = '/inventory/device/{}/document/del/{}'.format(device.dhid, doc_id) + user3.get(uri) + assert len(device.documents) == 0 diff --git a/tests/test_selenium.py b/tests/test_selenium.py index 948e947e..3402816f 100644 --- a/tests/test_selenium.py +++ b/tests/test_selenium.py @@ -27,7 +27,7 @@ class TestSelenium: # login self.driver.find_element(By.ID, "yourEmail").click() self.driver.implicitly_wait(3) - self.driver.find_element(By.ID, "yourPassword").send_keys("1234") + self.driver.find_element(By.ID, "id_password").send_keys("1234") self.driver.find_element(By.ID, "yourEmail").send_keys("user@dhub.com") self.driver.find_element(By.CSS_SELECTOR, ".btn").click() self.driver.implicitly_wait(3) @@ -104,7 +104,9 @@ class TestSelenium: # logout # self.driver.find_element(By.CSS_SELECTOR, ".d-md-block:nth-child(2)").click() self.driver.find_element(By.CSS_SELECTOR, ".d-md-block:nth-child(2)").click() - self.driver.find_element(By.CSS_SELECTOR, "li:nth-child(9) > .dropdown-item > span").click() + self.driver.find_element( + By.CSS_SELECTOR, "li:nth-child(9) > .dropdown-item > span" + ).click() # self.driver.find_element(By.CSS_SELECTOR, ".d-md-block").click() # self.driver.implicitly_wait(3) # self.driver.find_element(By.LINK_TEXT, "Sign Out").click() diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 6501a01c..6557565d 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -10,21 +10,17 @@ from uuid import uuid4 import pytest from boltons import urlutils -from ereuse_utils.test import ANY -from requests.exceptions import HTTPError -from teal.db import DBError, UniqueViolation -from teal.marshmallow import ValidationError from ereuse_devicehub.client import Client, UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub +from ereuse_devicehub.ereuse_utils.test import ANY from ereuse_devicehub.parser.models import SnapshotsLog from ereuse_devicehub.resources.action.models import ( Action, BenchmarkDataStorage, BenchmarkProcessor, EraseSectors, - EreusePrice, Ready, Snapshot, SnapshotRequest, @@ -32,16 +28,12 @@ from ereuse_devicehub.resources.action.models import ( ) from ereuse_devicehub.resources.action.views.snapshot import save_json from ereuse_devicehub.resources.device import models as m -from ereuse_devicehub.resources.device.exceptions import NeedsId from ereuse_devicehub.resources.device.models import Device, SolidStateDrive -from ereuse_devicehub.resources.device.sync import ( - MismatchBetweenProperties, - MismatchBetweenTagsAndHid, -) from ereuse_devicehub.resources.documents import documents from ereuse_devicehub.resources.enums import ComputerChassis, SnapshotSoftware from ereuse_devicehub.resources.tag import Tag from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.marshmallow import ValidationError from tests import conftest from tests.conftest import file, file_json, json_encode, yaml2json diff --git a/tests/test_tag.py b/tests/test_tag.py index cbdd7e5f..2cc789f5 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -3,16 +3,13 @@ import pathlib import pytest import requests_mock from boltons.urlutils import URL -from ereuse_utils.session import DevicehubClient from flask import g from pytest import raises -from teal.db import DBError, MultipleResourcesFound, ResourceNotFound, UniqueViolation -from teal.marshmallow import ValidationError from ereuse_devicehub.client import Client, UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub -from ereuse_devicehub.resources.action.models import Snapshot +from ereuse_devicehub.ereuse_utils.session import DevicehubClient from ereuse_devicehub.resources.agent.models import Organization from ereuse_devicehub.resources.device.models import Desktop, Device from ereuse_devicehub.resources.enums import ComputerChassis @@ -23,8 +20,14 @@ from ereuse_devicehub.resources.tag.view import ( TagNotLinked, ) from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.db import ( + DBError, + MultipleResourcesFound, + ResourceNotFound, + UniqueViolation, +) +from ereuse_devicehub.teal.marshmallow import ValidationError from tests import conftest -from tests.conftest import json_encode, yaml2json @pytest.mark.mvp diff --git a/tests/test_user.py b/tests/test_user.py index 9fba986d..5a367611 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -2,8 +2,6 @@ from uuid import UUID import pytest from sqlalchemy_utils import Password -from teal.enums import Country -from teal.marshmallow import ValidationError from werkzeug.exceptions import NotFound from ereuse_devicehub import auth @@ -13,6 +11,8 @@ from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.user import UserDef from ereuse_devicehub.resources.user.exceptions import WrongCredentials from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.teal.enums import Country +from ereuse_devicehub.teal.marshmallow import ValidationError from tests.conftest import app_context, create_user @@ -24,12 +24,14 @@ def test_create_user_method_with_agent(app: Devicehub): This method checks that the token is correct, too. """ user_def = app.resources['User'] # type: UserDef - u = user_def.create_user(email='foo@foo.com', - password='foo', - agent='Nice Person', - country=Country.ES.name, - telephone='+34 666 66 66 66', - tax_id='1234') + u = user_def.create_user( + email='foo@foo.com', + password='foo', + agent='Nice Person', + country=Country.ES.name, + telephone='+34 666 66 66 66', + tax_id='1234', + ) user = User.query.filter_by(id=u['id']).one() # type: User assert user.email == 'foo@foo.com' assert isinstance(user.token, UUID) @@ -75,9 +77,9 @@ def test_login_success(client: Client, app: Devicehub): """ with app.app_context(): create_user() - user, _ = client.post({'email': 'foo@foo.com', 'password': 'foo'}, - uri='/users/login/', - status=200) + user, _ = client.post( + {'email': 'foo@foo.com', 'password': 'foo'}, uri='/users/login/', status=200 + ) assert user['email'] == 'foo@foo.com' assert UUID(auth.Auth.decode(user['token'])) assert 'password' not in user @@ -126,16 +128,20 @@ def test_login_failure(client: Client, app: Devicehub): # Wrong password with app.app_context(): create_user() - client.post({'email': 'foo@foo.com', 'password': 'wrong pass'}, - uri='/users/login/', - status=WrongCredentials) + client.post( + {'email': 'foo@foo.com', 'password': 'wrong pass'}, + uri='/users/login/', + status=WrongCredentials, + ) # Wrong URI client.post({}, uri='/wrong-uri', status=NotFound) # Malformed data client.post({}, uri='/users/login/', status=ValidationError) - client.post({'email': 'this is not an email', 'password': 'nope'}, - uri='/users/login/', - status=ValidationError) + client.post( + {'email': 'this is not an email', 'password': 'nope'}, + uri='/users/login/', + status=ValidationError, + ) @pytest.mark.xfail(reason='Test not developed')
    PHIDErasure HostSanitization Host