From ad52bad3f6e0d9055ec7673fcca98e3680d4089c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 17 Mar 2023 11:35:35 +0100 Subject: [PATCH 01/42] up version to 2.5.1 --- CHANGELOG.md | 18 ++++++++++++++++++ ereuse_devicehub/__init__.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec1dff26..5362c338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,24 @@ ml). ## testing - [added] #414 add new vars in the settings file for wb. +## [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. - [added] #411 add new generic device as Other. diff --git a/ereuse_devicehub/__init__.py b/ereuse_devicehub/__init__.py index 50062f87..7a2056f5 100644 --- a/ereuse_devicehub/__init__.py +++ b/ereuse_devicehub/__init__.py @@ -1 +1 @@ -__version__ = "2.5.0" +__version__ = "2.5.1" From a7aae591aa9dfb46e4b516bc4edae5b893e87723 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 20 Mar 2023 17:32:58 +0100 Subject: [PATCH 02/42] fix names of titles --- ereuse_devicehub/inventory/views.py | 2 +- ereuse_devicehub/resources/documents/documents.py | 2 +- ereuse_devicehub/templates/ereuse_devicehub/base_site.html | 2 +- .../templates/ereuse_devicehub/user_profile.html | 6 +++--- ereuse_devicehub/templates/inventory/device_list.html | 2 +- ereuse_devicehub/templates/inventory/erasure_list.html | 4 ++-- ereuse_devicehub/templates/inventory/search.html | 2 +- ereuse_devicehub/templates/workbench/settings.html | 3 ++- ereuse_devicehub/workbench/views.py | 2 +- 9 files changed, 13 insertions(+), 12 deletions(-) diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 2526a850..79145f3c 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -1149,7 +1149,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()), diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index d33fd238..b41f3cbd 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -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(), } diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index f6dfb1b3..de544d75 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
  • diff --git a/ereuse_devicehub/templates/ereuse_devicehub/user_profile.html b/ereuse_devicehub/templates/ereuse_devicehub/user_profile.html index 462fc6ec..e01e6f74 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/user_profile.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/user_profile.html @@ -37,7 +37,7 @@
    @@ -71,7 +71,7 @@
    - +
    {% for f in sanitization_form %} {% if f == sanitization_form.csrf_token %} @@ -95,7 +95,7 @@
    -
    +
    diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index 7f5ac904..2e2bc7b6 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -256,7 +256,7 @@
  • - Erasure Certificate + Device Sanitization
  • diff --git a/ereuse_devicehub/templates/inventory/erasure_list.html b/ereuse_devicehub/templates/inventory/erasure_list.html index e2154c3e..e2ca389e 100644 --- a/ereuse_devicehub/templates/inventory/erasure_list.html +++ b/ereuse_devicehub/templates/inventory/erasure_list.html @@ -6,7 +6,7 @@ @@ -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..435f99cc 100644 --- a/ereuse_devicehub/templates/inventory/search.html +++ b/ereuse_devicehub/templates/inventory/search.html @@ -172,7 +172,7 @@
  • - Erasure Certificate + Device Sanitization
  • 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/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() From 6a14727f31790b65935f96153e1b422e5c80362c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 20 Mar 2023 17:34:20 +0100 Subject: [PATCH 03/42] fix names of titles --- ereuse_devicehub/resources/documents/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index b41f3cbd..363fda83 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -280,7 +280,7 @@ class LotRow(OrderedDict): self['Registered in'] = format(lot.created, '%c') try: self['Description'] = lot.description - except: + except Exception: self['Description'] = '' From 2ff6f402287a51386bb2d2f4c951b7ee79d2c665 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 20 Mar 2023 18:08:37 +0100 Subject: [PATCH 04/42] fix links --- ereuse_devicehub/templates/inventory/erasure_list.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ereuse_devicehub/templates/inventory/erasure_list.html b/ereuse_devicehub/templates/inventory/erasure_list.html index e2154c3e..9ba4d5a7 100644 --- a/ereuse_devicehub/templates/inventory/erasure_list.html +++ b/ereuse_devicehub/templates/inventory/erasure_list.html @@ -22,13 +22,13 @@ From 01ef359bd43e9f7395c6b03ea1f5bbb621c765a6 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 21 Mar 2023 12:08:13 +0100 Subject: [PATCH 05/42] add teal as module --- ereuse_devicehub/auth.py | 6 +- ereuse_devicehub/client.py | 4 +- ereuse_devicehub/config.py | 8 +- ereuse_devicehub/db.py | 9 +- ereuse_devicehub/devicehub.py | 4 +- ereuse_devicehub/inventory/models.py | 2 +- ereuse_devicehub/marshmallow.py | 31 +- ereuse_devicehub/query.py | 50 +- ereuse_devicehub/resources/action/__init__.py | 42 +- ereuse_devicehub/resources/action/models.py | 38 +- ereuse_devicehub/resources/action/schemas.py | 6 +- .../resources/action/views/trade.py | 2 +- .../resources/action/views/views.py | 6 +- ereuse_devicehub/resources/agent/__init__.py | 42 +- ereuse_devicehub/resources/agent/models.py | 11 +- ereuse_devicehub/resources/agent/schemas.py | 13 +- .../resources/deliverynote/__init__.py | 40 +- .../resources/deliverynote/models.py | 96 +- .../resources/deliverynote/schemas.py | 25 +- .../resources/deliverynote/views.py | 7 +- .../resources/device/definitions.py | 3 +- .../resources/device/exceptions.py | 25 +- ereuse_devicehub/resources/device/models.py | 26 +- ereuse_devicehub/resources/device/schemas.py | 11 +- ereuse_devicehub/resources/device/sync.py | 4 +- ereuse_devicehub/resources/device/views.py | 10 +- .../resources/documents/documents.py | 14 +- .../resources/documents/models.py | 27 +- .../resources/documents/schemas.py | 49 +- ereuse_devicehub/resources/image/models.py | 40 +- .../resources/inventory/__init__.py | 61 +- .../resources/licences/licences.py | 41 +- ereuse_devicehub/resources/lot/__init__.py | 62 +- ereuse_devicehub/resources/lot/models.py | 4 +- ereuse_devicehub/resources/lot/schemas.py | 23 +- ereuse_devicehub/resources/lot/views.py | 6 +- .../resources/metric/definitions.py | 2 +- ereuse_devicehub/resources/metric/schema.py | 17 +- ereuse_devicehub/resources/metric/views.py | 22 +- ereuse_devicehub/resources/schemas.py | 12 +- ereuse_devicehub/resources/tag/__init__.py | 115 +- ereuse_devicehub/resources/tag/model.py | 79 +- ereuse_devicehub/resources/tag/schema.py | 12 +- ereuse_devicehub/resources/tag/view.py | 30 +- .../resources/tradedocument/definitions.py | 4 +- .../resources/tradedocument/models.py | 2 +- .../resources/tradedocument/schemas.py | 45 +- .../resources/tradedocument/views.py | 17 +- ereuse_devicehub/resources/user/__init__.py | 87 +- ereuse_devicehub/resources/user/models.py | 2 +- ereuse_devicehub/resources/user/schemas.py | 36 +- ereuse_devicehub/resources/user/views.py | 6 +- .../resources/versions/versions.py | 54 +- ereuse_devicehub/teal/__init__.py | 0 ereuse_devicehub/teal/auth.py | 93 + ereuse_devicehub/teal/cache.py | 28 + ereuse_devicehub/teal/cli.py | 13 + ereuse_devicehub/teal/client.py | 181 + ereuse_devicehub/teal/config.py | 72 + ereuse_devicehub/teal/db.py | 382 ++ ereuse_devicehub/teal/enums.py | 4421 +++++++++++++++++ ereuse_devicehub/teal/json_util.py | 11 + ereuse_devicehub/teal/marshmallow.py | 346 ++ ereuse_devicehub/teal/query.py | 294 ++ ereuse_devicehub/teal/request.py | 28 + ereuse_devicehub/teal/resource.py | 429 ++ ereuse_devicehub/teal/teal.py | 308 ++ ereuse_devicehub/teal/utils.py | 33 + tests/test_action.py | 2 +- tests/test_agent.py | 33 +- tests/test_db.py | 8 +- tests/test_device.py | 4 +- tests/test_device_find.py | 190 +- tests/test_snapshot.py | 4 +- tests/test_tag.py | 9 +- tests/test_user.py | 40 +- 76 files changed, 7688 insertions(+), 631 deletions(-) create mode 100644 ereuse_devicehub/teal/__init__.py create mode 100644 ereuse_devicehub/teal/auth.py create mode 100644 ereuse_devicehub/teal/cache.py create mode 100644 ereuse_devicehub/teal/cli.py create mode 100644 ereuse_devicehub/teal/client.py create mode 100644 ereuse_devicehub/teal/config.py create mode 100644 ereuse_devicehub/teal/db.py create mode 100644 ereuse_devicehub/teal/enums.py create mode 100644 ereuse_devicehub/teal/json_util.py create mode 100644 ereuse_devicehub/teal/marshmallow.py create mode 100644 ereuse_devicehub/teal/query.py create mode 100644 ereuse_devicehub/teal/request.py create mode 100644 ereuse_devicehub/teal/resource.py create mode 100644 ereuse_devicehub/teal/teal.py create mode 100644 ereuse_devicehub/teal/utils.py 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/client.py b/ereuse_devicehub/client.py index 92aa67d2..25d79198 100644 --- a/ereuse_devicehub/client.py +++ b/ereuse_devicehub/client.py @@ -4,11 +4,11 @@ from typing import Dict, Iterable, Type, Union from 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 a41c9640..1b89006a 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, @@ -23,6 +19,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..086895a3 100644 --- a/ereuse_devicehub/devicehub.py +++ b/ereuse_devicehub/devicehub.py @@ -10,8 +10,6 @@ from 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 diff --git a/ereuse_devicehub/inventory/models.py b/ereuse_devicehub/inventory/models.py index f8b4f977..83fd6619 100644 --- a/ereuse_devicehub/inventory/models.py +++ b/ereuse_devicehub/inventory/models.py @@ -5,11 +5,11 @@ from flask import g from sqlalchemy import 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): 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/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 b723bab8..41a65265 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -19,7 +19,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 @@ -45,19 +44,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 @@ -88,6 +76,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: @@ -119,7 +119,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. @@ -548,7 +552,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..a9c1d664 100644 --- a/ereuse_devicehub/resources/action/views/views.py +++ b/ereuse_devicehub/resources/action/views/views.py @@ -8,9 +8,6 @@ import 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') 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 05af6f9b..87ef6fd6 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -35,19 +35,6 @@ 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.resources.device.metrics import Metrics @@ -70,6 +57,19 @@ 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 def create_code(context): 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 59beffd9..a8d0e88d 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -14,11 +14,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.db import ResourceNotFound -from teal.marshmallow import ValidationError -from teal.resource import View from ereuse_devicehub import auth from ereuse_devicehub.db import db @@ -32,6 +27,11 @@ from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.enums import SnapshotSoftware 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/documents.py b/ereuse_devicehub/resources/documents/documents.py index 363fda83..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: 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/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 051cf08c..23a7ef0c 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): 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/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..72b11706 100644 --- a/ereuse_devicehub/resources/tag/__init__.py +++ b/ereuse_devicehub/resources/tag/__init__.py @@ -3,14 +3,18 @@ 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.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 351907a8..4e51e92e 100644 --- a/ereuse_devicehub/resources/user/models.py +++ b/ereuse_devicehub/resources/user/models.py @@ -6,12 +6,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/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..b6b59566 --- /dev/null +++ b/ereuse_devicehub/teal/cache.py @@ -0,0 +1,28 @@ +import datetime +from functools import wraps + +from flask import Response, 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)) # type: Response + 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..f18a1d53 --- /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_utils.test import JSON +from ereuse_utils.test import Client as EreuseUtilsClient +from 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..60500d1b --- /dev/null +++ b/ereuse_devicehub/teal/config.py @@ -0,0 +1,72 @@ +from typing import Dict, Set, Type + +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() # type: Set[Type[Resource]] + """ + A list of resource definitions to load. + """ + + SQLALCHEMY_DATABASE_URI = None # type: str + """ + The access to the main Database. + """ + SQLALCHEMY_BINDS = {} # type: Dict[str, str] + """ + 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..08c02252 --- /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_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..0219fb99 --- /dev/null +++ b/ereuse_devicehub/teal/json_util.py @@ -0,0 +1,11 @@ +import ereuse_utils +from flask.json import JSONEncoder as FlaskJSONEncoder +from sqlalchemy.ext.baked import Result +from sqlalchemy.orm import Query + + +class TealJSONEncoder(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..af5f0eca --- /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_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..f1e3f370 --- /dev/null +++ b/ereuse_devicehub/teal/query.py @@ -0,0 +1,294 @@ +import json +from json import JSONDecodeError + +from 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..31d33599 --- /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_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..7b341b49 --- /dev/null +++ b/ereuse_devicehub/teal/teal.py @@ -0,0 +1,308 @@ +import inspect +from typing import Dict, Type + +import click_spinner +import ereuse_utils +import flask_cors +from anytree import Node +from apispec import APISpec +from click import option +from ereuse_utils import ensure_utf8 +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 + +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 = {} # type: Dict[str, Resource] + """ + The resources definitions loaded on this App, referenced by their + type name. + """ + self.tree = {} # type: Dict[str, Node] + """ + 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_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/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_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..7d82ce55 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -9,8 +9,6 @@ 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 @@ -34,6 +32,8 @@ 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.db import ResourceNotFound +from ereuse_devicehub.teal.enums import Layouts from tests import conftest from tests.conftest import file, json_encode, yaml2json 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_snapshot.py b/tests/test_snapshot.py index 6501a01c..ca4f323b 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -12,8 +12,6 @@ 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 @@ -42,6 +40,8 @@ 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.db import DBError, UniqueViolation +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..14807d3c 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -6,8 +6,6 @@ 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 @@ -23,6 +21,13 @@ 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 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') From 83f1e4c18fad790add4e18577d04d3e1ad82a2b8 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 21 Mar 2023 17:31:43 +0100 Subject: [PATCH 06/42] add ereuse_utils as module --- ereuse_devicehub/cli.py | 46 +- ereuse_devicehub/client.py | 2 +- ereuse_devicehub/devicehub.py | 6 +- ereuse_devicehub/dummy/dummy.py | 4 +- ereuse_devicehub/ereuse_utils/__init__.py | 173 + ereuse_devicehub/ereuse_utils/cli.py | 301 + ereuse_devicehub/ereuse_utils/cmd.py | 150 + ereuse_devicehub/ereuse_utils/getter.py | 171 + ereuse_devicehub/ereuse_utils/naming.py | 143 + .../ereuse_utils/nested_lookup.py | 88 + ereuse_devicehub/ereuse_utils/session.py | 285 + ereuse_devicehub/ereuse_utils/test.py | 165 + ereuse_devicehub/ereuse_utils/text.py | 72 + .../ereuse_utils/usb_flash_drive.py | 80 + ereuse_devicehub/migrations/script.py.mako | 2 +- ...ef_change_testdatastorage_smallint_for_.py | 31 +- .../versions/21afd375a654_session_table.py | 2 +- .../versions/378b6b147b46_nullable.py | 8 +- .../versions/3a3601ac8224_tradedocuments.py | 226 +- .../versions/4f33137586dd_sanitization.py | 2 +- .../versions/7ecb8ff7abad_documents.py | 131 +- .../968b79fa7756_upgrade_confirmrevoke.py | 10 +- .../versions/b4bd1538bad5_update_live.py | 2 +- .../versions/bf600ca861a4_adding_hid.py | 4 +- .../e93aec8fc41f_added_assigned_action.py | 71 +- .../versions/fbb7e2a0cde0_initial.py | 8116 ++++++++++++++--- ereuse_devicehub/parser/computer.py | 4 +- ereuse_devicehub/parser/utils.py | 2 +- .../resources/action/views/views.py | 4 +- ereuse_devicehub/resources/device/models.py | 2 +- .../resources/inventory/schema.py | 6 +- ereuse_devicehub/resources/tag/__init__.py | 2 +- ereuse_devicehub/teal/client.py | 6 +- ereuse_devicehub/teal/db.py | 2 +- ereuse_devicehub/teal/json_util.py | 4 +- ereuse_devicehub/teal/marshmallow.py | 2 +- ereuse_devicehub/teal/query.py | 2 +- ereuse_devicehub/teal/resource.py | 2 +- ereuse_devicehub/teal/teal.py | 6 +- tests/conftest.py | 2 +- 40 files changed, 8683 insertions(+), 1654 deletions(-) create mode 100644 ereuse_devicehub/ereuse_utils/__init__.py create mode 100644 ereuse_devicehub/ereuse_utils/cli.py create mode 100644 ereuse_devicehub/ereuse_utils/cmd.py create mode 100644 ereuse_devicehub/ereuse_utils/getter.py create mode 100644 ereuse_devicehub/ereuse_utils/naming.py create mode 100644 ereuse_devicehub/ereuse_utils/nested_lookup.py create mode 100644 ereuse_devicehub/ereuse_utils/session.py create mode 100644 ereuse_devicehub/ereuse_utils/test.py create mode 100644 ereuse_devicehub/ereuse_utils/text.py create mode 100644 ereuse_devicehub/ereuse_utils/usb_flash_drive.py 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 25d79198..6358aa9e 100644 --- a/ereuse_devicehub/client.py +++ b/ereuse_devicehub/client.py @@ -1,7 +1,7 @@ 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 werkzeug.exceptions import HTTPException diff --git a/ereuse_devicehub/devicehub.py b/ereuse_devicehub/devicehub.py index 086895a3..e5e76145 100644 --- a/ereuse_devicehub/devicehub.py +++ b/ereuse_devicehub/devicehub.py @@ -5,8 +5,8 @@ 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 @@ -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..d4df20bf --- /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): + """ + 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..2f03807f --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/cmd.py @@ -0,0 +1,150 @@ +import subprocess +from contextlib import suppress +from typing import Any, Set, TextIO + +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 + ) # type: TextIO + 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..f2a99bc9 --- /dev/null +++ b/ereuse_devicehub/ereuse_utils/nested_lookup.py @@ -0,0 +1,88 @@ +from typing import Generator + + +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): + """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..6d878b0b --- /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: + """ + 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/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/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 51fe4d61..7d3cb453 100644 --- a/ereuse_devicehub/migrations/versions/4f33137586dd_sanitization.py +++ b/ereuse_devicehub/migrations/versions/4f33137586dd_sanitization.py @@ -7,7 +7,7 @@ Create Date: 2023-02-13 18:01:00.092527 """ import citext 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/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/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/resources/action/views/views.py b/ereuse_devicehub/resources/action/views/views.py index a9c1d664..71ec908e 100644 --- a/ereuse_devicehub/resources/action/views/views.py +++ b/ereuse_devicehub/resources/action/views/views.py @@ -4,7 +4,7 @@ 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 @@ -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/device/models.py b/ereuse_devicehub/resources/device/models.py index 87ef6fd6..0867e921 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -12,7 +12,7 @@ from typing import Dict, List, Set from boltons import urlutils from citext import CIText -from ereuse_utils.naming import HID_CONVERSION_DOC +from ereuse_devicehub.ereuse_utils.naming import HID_CONVERSION_DOC from flask import current_app as app from flask import g, request from more_itertools import unique_everseen 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/tag/__init__.py b/ereuse_devicehub/resources/tag/__init__.py index 72b11706..85ec4084 100644 --- a/ereuse_devicehub/resources/tag/__init__.py +++ b/ereuse_devicehub/resources/tag/__init__.py @@ -2,7 +2,7 @@ import csv import pathlib from click import argument, option -from ereuse_utils import cli +from ereuse_devicehub.ereuse_utils import cli from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.definitions import DeviceDef diff --git a/ereuse_devicehub/teal/client.py b/ereuse_devicehub/teal/client.py index f18a1d53..d2ab1210 100644 --- a/ereuse_devicehub/teal/client.py +++ b/ereuse_devicehub/teal/client.py @@ -1,9 +1,9 @@ from typing import Any, Iterable, Tuple, Type, Union from boltons.urlutils import URL -from ereuse_utils.test import JSON -from ereuse_utils.test import Client as EreuseUtilsClient -from ereuse_utils.test import Res +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 diff --git a/ereuse_devicehub/teal/db.py b/ereuse_devicehub/teal/db.py index 08c02252..7f0b8c7f 100644 --- a/ereuse_devicehub/teal/db.py +++ b/ereuse_devicehub/teal/db.py @@ -7,7 +7,7 @@ from typing import Any, Type, Union from boltons.typeutils import classproperty from boltons.urlutils import URL as BoltonsUrl -from ereuse_utils import if_none_return_none +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 diff --git a/ereuse_devicehub/teal/json_util.py b/ereuse_devicehub/teal/json_util.py index 0219fb99..fd1df357 100644 --- a/ereuse_devicehub/teal/json_util.py +++ b/ereuse_devicehub/teal/json_util.py @@ -1,10 +1,10 @@ -import ereuse_utils +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_utils.JSONEncoder, FlaskJSONEncoder): +class TealJSONEncoder(ereuse_devicehub.ereuse_utils.JSONEncoder, FlaskJSONEncoder): def default(self, obj): if isinstance(obj, (Result, Query)): return tuple(obj) diff --git a/ereuse_devicehub/teal/marshmallow.py b/ereuse_devicehub/teal/marshmallow.py index af5f0eca..72189930 100644 --- a/ereuse_devicehub/teal/marshmallow.py +++ b/ereuse_devicehub/teal/marshmallow.py @@ -4,7 +4,7 @@ from typing import Type, Union import colour from boltons import strutils, urlutils -from ereuse_utils import if_none_return_none +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 diff --git a/ereuse_devicehub/teal/query.py b/ereuse_devicehub/teal/query.py index f1e3f370..e6103719 100644 --- a/ereuse_devicehub/teal/query.py +++ b/ereuse_devicehub/teal/query.py @@ -1,7 +1,7 @@ import json from json import JSONDecodeError -from ereuse_utils import flatten_mixed +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_ diff --git a/ereuse_devicehub/teal/resource.py b/ereuse_devicehub/teal/resource.py index 31d33599..6ba998cb 100644 --- a/ereuse_devicehub/teal/resource.py +++ b/ereuse_devicehub/teal/resource.py @@ -4,7 +4,7 @@ from typing import Callable, Iterable, Iterator, Tuple, Type, Union import inflection from anytree import PreOrderIter from boltons.typeutils import classproperty, issubclass -from ereuse_utils.naming import Naming +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 diff --git a/ereuse_devicehub/teal/teal.py b/ereuse_devicehub/teal/teal.py index 7b341b49..6acf33c1 100644 --- a/ereuse_devicehub/teal/teal.py +++ b/ereuse_devicehub/teal/teal.py @@ -2,12 +2,12 @@ import inspect from typing import Dict, Type import click_spinner -import ereuse_utils +import ereuse_devicehub.ereuse_utils import flask_cors from anytree import Node from apispec import APISpec from click import option -from ereuse_utils import ensure_utf8 +from ereuse_devicehub.ereuse_utils import ensure_utf8 from flask import Flask, jsonify from flask.globals import _app_ctx_stack from flask_sqlalchemy import SQLAlchemy @@ -284,7 +284,7 @@ class Teal(Flask): return jsonify(self._apidocs) -class DumpeableHTTPException(ereuse_utils.Dumpeable): +class DumpeableHTTPException(ereuse_devicehub.ereuse_utils.Dumpeable): """Exceptions that inherit this class will be able to dump to dicts and JSONs. """ diff --git a/tests/conftest.py b/tests/conftest.py index 8e5c52d1..ac75ecc8 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 From e6f91db4e472a0de8600a9867cdbbef687e407ae Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 27 Mar 2023 10:22:08 +0200 Subject: [PATCH 07/42] correct versions of dependencies --- requirements.in | 19 +++++++++---- requirements.txt | 74 +++++++++++++++--------------------------------- setup.py | 36 +++++++++-------------- 3 files changed, 50 insertions(+), 79 deletions(-) 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/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', - ] + ], ) From e6c07851d4f68d9d88ffe05b77c56ad1870478ad Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 27 Mar 2023 10:55:26 +0200 Subject: [PATCH 08/42] fix lint checks --- ereuse_devicehub/ereuse_utils/cli.py | 2 +- ereuse_devicehub/ereuse_utils/cmd.py | 6 ++---- ereuse_devicehub/ereuse_utils/nested_lookup.py | 5 +---- .../ereuse_utils/usb_flash_drive.py | 2 +- ereuse_devicehub/teal/cache.py | 4 ++-- ereuse_devicehub/teal/config.py | 18 ++++++++---------- ereuse_devicehub/teal/teal.py | 14 +++++++------- 7 files changed, 22 insertions(+), 29 deletions(-) diff --git a/ereuse_devicehub/ereuse_utils/cli.py b/ereuse_devicehub/ereuse_utils/cli.py index d4df20bf..a86eb4be 100644 --- a/ereuse_devicehub/ereuse_utils/cli.py +++ b/ereuse_devicehub/ereuse_utils/cli.py @@ -192,7 +192,7 @@ class Line(tqdm): def error_message(self, *args): self._error_message = args - def close(self): + def close(self): # noqa: C901 """ Cleanup and (if leave=False) close the progressbar. """ diff --git a/ereuse_devicehub/ereuse_utils/cmd.py b/ereuse_devicehub/ereuse_utils/cmd.py index 2f03807f..1f1262f3 100644 --- a/ereuse_devicehub/ereuse_utils/cmd.py +++ b/ereuse_devicehub/ereuse_utils/cmd.py @@ -1,6 +1,6 @@ import subprocess from contextlib import suppress -from typing import Any, Set, TextIO +from typing import Any, Set from ereuse_devicehub.ereuse_utils import text @@ -96,9 +96,7 @@ class ProgressiveCmd: 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 - ) # type: TextIO + self.out = conn.stdout if stdout == subprocess.PIPE else conn.stderr self._callback = callback self.last_update_percentage = 0 self.percentage = 0 diff --git a/ereuse_devicehub/ereuse_utils/nested_lookup.py b/ereuse_devicehub/ereuse_utils/nested_lookup.py index f2a99bc9..805543aa 100644 --- a/ereuse_devicehub/ereuse_utils/nested_lookup.py +++ b/ereuse_devicehub/ereuse_utils/nested_lookup.py @@ -1,6 +1,3 @@ -from typing import Generator - - class NestedLookup: @staticmethod def __new__(cls, document, references, operation): @@ -38,7 +35,7 @@ class NestedLookup: return key_value_containing_value @staticmethod - def _nested_lookup(document, references, operation): + 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: diff --git a/ereuse_devicehub/ereuse_utils/usb_flash_drive.py b/ereuse_devicehub/ereuse_utils/usb_flash_drive.py index 6d878b0b..444de53d 100644 --- a/ereuse_devicehub/ereuse_utils/usb_flash_drive.py +++ b/ereuse_devicehub/ereuse_utils/usb_flash_drive.py @@ -5,7 +5,7 @@ from usb import CLASS_MASS_STORAGE from ereuse_devicehub.ereuse_utils.naming import Naming -def plugged_usbs(multiple=True) -> map or dict: +def plugged_usbs(multiple=True) -> map or dict: # noqa: C901 """ Gets the plugged-in USB Flash drives (pen-drives). diff --git a/ereuse_devicehub/teal/cache.py b/ereuse_devicehub/teal/cache.py index b6b59566..e8683ee3 100644 --- a/ereuse_devicehub/teal/cache.py +++ b/ereuse_devicehub/teal/cache.py @@ -1,7 +1,7 @@ import datetime from functools import wraps -from flask import Response, make_response +from flask import make_response def cache(expires: datetime.timedelta = None): @@ -18,7 +18,7 @@ def cache(expires: datetime.timedelta = None): def cache_decorator(view): @wraps(view) def cache_func(*args, **kwargs): - r = make_response(view(*args, **kwargs)) # type: Response + r = make_response(view(*args, **kwargs)) r.expires = datetime.datetime.now(datetime.timezone.utc) + expires r.cache_control.public = True return r diff --git a/ereuse_devicehub/teal/config.py b/ereuse_devicehub/teal/config.py index 60500d1b..46cac145 100644 --- a/ereuse_devicehub/teal/config.py +++ b/ereuse_devicehub/teal/config.py @@ -1,5 +1,3 @@ -from typing import Dict, Set, Type - from boltons.typeutils import issubclass from ereuse_devicehub.teal.resource import Resource @@ -12,16 +10,16 @@ class Config: Subclass and set here your config values. """ - RESOURCE_DEFINITIONS = set() # type: Set[Type[Resource]] + RESOURCE_DEFINITIONS = set() """ A list of resource definitions to load. """ - SQLALCHEMY_DATABASE_URI = None # type: str + SQLALCHEMY_DATABASE_URI = None """ The access to the main Database. """ - SQLALCHEMY_BINDS = {} # type: Dict[str, str] + SQLALCHEMY_BINDS = {} """ Optional extra databases. See `here `_ how bind your models to different @@ -29,11 +27,11 @@ class Config: """ SQLALCHEMY_TRACK_MODIFICATIONS = False """ - Disables flask-sqlalchemy notification system. + 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' @@ -47,7 +45,7 @@ class 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. @@ -57,7 +55,7 @@ class Config: CORS_EXPOSE_HEADERS = 'Authorization' CORS_ALLOW_HEADERS = 'Content-Type', 'Authorization' """ - Configuration for CORS. See the options you can pass by in `Flask-Cors + Configuration for CORS. See the options you can pass by in `Flask-Cors `_, exactly in **Parameters**, like the ones above. """ diff --git a/ereuse_devicehub/teal/teal.py b/ereuse_devicehub/teal/teal.py index 6acf33c1..4cffab9f 100644 --- a/ereuse_devicehub/teal/teal.py +++ b/ereuse_devicehub/teal/teal.py @@ -1,19 +1,19 @@ import inspect -from typing import Dict, Type +from typing import Type import click_spinner -import ereuse_devicehub.ereuse_utils import flask_cors from anytree import Node from apispec import APISpec from click import option -from ereuse_devicehub.ereuse_utils import ensure_utf8 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 @@ -122,17 +122,17 @@ class Teal(Flask): # noinspection PyAttributeOutsideInit def load_resources(self): - self.resources = {} # type: Dict[str, Resource] + self.resources = {} """ The resources definitions loaded on this App, referenced by their type name. """ - self.tree = {} # type: Dict[str, Node] + self.tree = {} """ - A tree representing the hierarchy of the instances of + 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 From 309b266fe97b4d3cf5f2f91e3f5fa43efe2541f3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 27 Mar 2023 11:07:30 +0200 Subject: [PATCH 09/42] fix import in tests --- tests/test_device.py | 14 +++----------- tests/test_documents.py | 6 +----- tests/test_snapshot.py | 10 +--------- tests/test_tag.py | 4 +--- 4 files changed, 6 insertions(+), 28 deletions(-) diff --git a/tests/test_device.py b/tests/test_device.py index 7d82ce55..edf83bdf 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,28 +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 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, @@ -32,10 +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.db import ResourceNotFound 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_documents.py b/tests/test_documents.py index 743577b5..18888ae3 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 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 diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index ca4f323b..6557565d 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -10,19 +10,17 @@ from uuid import uuid4 import pytest from boltons import urlutils -from ereuse_utils.test import ANY -from requests.exceptions import HTTPError 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, @@ -30,17 +28,11 @@ 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.db import DBError, UniqueViolation 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 14807d3c..2cc789f5 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -3,14 +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 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 @@ -29,7 +28,6 @@ from ereuse_devicehub.teal.db import ( ) from ereuse_devicehub.teal.marshmallow import ValidationError from tests import conftest -from tests.conftest import json_encode, yaml2json @pytest.mark.mvp From f3926e3b92bfac24efe198615b4c810563a96559 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 27 Mar 2023 11:13:58 +0200 Subject: [PATCH 10/42] fix import in tests --- tests/test_documents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_documents.py b/tests/test_documents.py index 18888ae3..2996a58d 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -4,7 +4,7 @@ from io import BytesIO, StringIO from pathlib import Path import pytest -import teal.marshmallow +from ereuse_devicehub.teal.marshmallow import ValidationError from ereuse_devicehub import auth from ereuse_devicehub.client import Client, UserClient @@ -90,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, ) From 745b9966df6ca6da329ea4821453bab48fc1f38a Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 27 Mar 2023 16:34:25 +0200 Subject: [PATCH 11/42] add lots in export devices --- ereuse_devicehub/resources/device/models.py | 22 ++++++++++++ .../resources/documents/device_row.py | 6 ++++ tests/files/basic.csv | 4 +-- tests/files/proposal_extended_csv_report.csv | 6 ++-- tests/test_documents.py | 34 +++++++++---------- 5 files changed, 50 insertions(+), 22 deletions(-) diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 05af6f9b..b0a70c81 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -748,6 +748,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 diff --git a/ereuse_devicehub/resources/documents/device_row.py b/ereuse_devicehub/resources/documents/device_row.py index d4efda46..68690cf0 100644 --- a/ereuse_devicehub/resources/documents/device_row.py +++ b/ereuse_devicehub/resources/documents/device_row.py @@ -37,6 +37,9 @@ class BaseDeviceRow(OrderedDict): self['PHID'] = '' self['DHID'] = '' self['Type'] = '' + self['Temporary Lots'] = '' + self['Incoming Lots'] = '' + self['Outgoing Lots'] = '' self['Placeholder Palet'] = '' self['Placeholder Id Supplier'] = '' self['Placeholder Info'] = '' @@ -504,6 +507,9 @@ class DeviceRow(BaseDeviceRow): # Placeholder self['PHID'] = none2str(self.placeholder.phid) self['Type'] = none2str(self.device.is_abstract()) + 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 Palet'] = none2str(self.placeholder.pallet) self['Placeholder Id Supplier'] = none2str(self.placeholder.id_device_supplier) self['Placeholder Info'] = none2str(self.placeholder.info) diff --git a/tests/files/basic.csv b/tests/files/basic.csv index 3ceda076..c9ad235e 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 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";"Mon Mar 27 15:37:54 2023";"Workbench 11.0";"2023-03-27 15:37:55.003749+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/proposal_extended_csv_report.csv b/tests/files/proposal_extended_csv_report.csv index 73608689..6718d091 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 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.";"Mon Mar 27 15:37:34 2023";"Workbench 11.0a2";"2023-03-27 15:37:34.478208+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 15:37:34.440184+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.";"Mon Mar 27 15:37:34 2023";"Workbench 11.0b11";"2023-03-27 15:37:34.836618+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-03-27 15:37:34.791027+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-03-27 15:37:34.792988+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-03-27 15:37:34.796512+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_documents.py b/tests/test_documents.py index 743577b5..c1608858 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -269,11 +269,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][:32] == export_csv[1][:32] ), 'Computer information are not equal' - assert fixture_csv[1][30] == export_csv[1][30], 'Computer information are not equal' + assert fixture_csv[1][33] == export_csv[1][33], 'Computer information are not equal' assert ( - fixture_csv[1][32:] == export_csv[1][32:] + fixture_csv[1][35:] == export_csv[1][35:] ), 'Computer information are not equal' @@ -339,38 +339,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][:32] == export_csv[1][:32] ), 'Computer information are not equal' - assert fixture_csv[1][30] == export_csv[1][30], 'Computer information are not equal' + assert fixture_csv[1][33] == export_csv[1][33], 'Computer information are not equal' assert ( - fixture_csv[1][32:93] == export_csv[1][32:93] + fixture_csv[1][35:93] == export_csv[1][35:93] ), 'Computer information are not equal' - assert fixture_csv[1][94] == export_csv[1][94], 'Computer information are not equal' + assert fixture_csv[1][97] == export_csv[1][97], 'Computer information are not equal' assert ( - fixture_csv[1][97:] == export_csv[1][97:] + fixture_csv[1][100:] == export_csv[1][100:] ), 'Computer information are not equal' assert ( - fixture_csv[2][:29] == export_csv[2][:29] + fixture_csv[2][:32] == export_csv[2][:32] ), 'Computer information are not equal' - assert fixture_csv[2][30] == export_csv[2][30], 'Computer information are not equal' + assert fixture_csv[2][33] == export_csv[2][33], 'Computer information are not equal' assert ( - fixture_csv[2][32:93] == export_csv[2][32:93] + fixture_csv[2][35:96] == export_csv[2][35:96] ), 'Computer information are not equal' - assert fixture_csv[2][94] == export_csv[2][94], 'Computer information are not equal' + assert fixture_csv[2][97] == export_csv[2][97], 'Computer information are not equal' assert ( - fixture_csv[2][97:107] == export_csv[2][97:107] + fixture_csv[2][100:110] == export_csv[2][100:110] ), 'Computer information are not equal' assert ( - fixture_csv[2][120] == export_csv[2][120] + fixture_csv[2][123] == export_csv[2][123] ), 'Computer information are not equal' assert ( - fixture_csv[2][123:144] == export_csv[2][123:144] + fixture_csv[2][126:147] == export_csv[2][126:147] ), 'Computer information are not equal' assert ( - fixture_csv[2][146] == export_csv[2][146] + fixture_csv[2][149] == export_csv[2][149] ), 'Computer information are not equal' assert ( - fixture_csv[2][149:] == export_csv[2][149:] + fixture_csv[2][152:] == export_csv[2][152:] ), 'Computer information are not equal' From 4b9f1c02b985e0aea176f2565116440ce560b603 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 27 Mar 2023 17:17:13 +0200 Subject: [PATCH 12/42] fix render test --- tests/files/export_devices.csv | 4 ++-- tests/test_render_2_0.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/files/export_devices.csv b/tests/files/export_devices.csv index 5754ab48..a87378f5 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 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.";"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/test_render_2_0.py b/tests/test_render_2_0.py index c84ad143..6cb43f31 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][:32] == export_csv[1][:32] ), 'Computer information are not equal' - assert fixture_csv[1][30] == export_csv[1][30], 'Computer information are not equal' + assert fixture_csv[1][33] == export_csv[1][33], 'Computer information are not equal' assert ( - fixture_csv[1][32:93] == export_csv[1][32:93] + fixture_csv[1][35:96] == export_csv[1][35:96] ), 'Computer information are not equal' - assert fixture_csv[1][94] == export_csv[1][94], 'Computer information are not equal' + assert fixture_csv[1][97] == export_csv[1][97], 'Computer information are not equal' assert ( - fixture_csv[1][98:] == export_csv[1][98:] + fixture_csv[1][101:] == export_csv[1][101:] ), 'Computer information are not equal' From 1515302d9839f96dd1374f4f803f77814512b0cd Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Mar 2023 17:09:47 +0200 Subject: [PATCH 13/42] allow remove a document --- ereuse_devicehub/inventory/forms.py | 12 +++++++ ereuse_devicehub/inventory/views.py | 25 +++++++++++++ ereuse_devicehub/resources/models.py | 32 +++++++++++------ .../templates/inventory/device_list.html | 36 +++++++++++++++++++ 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index ffb8d13c..fc5fb5c6 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -1274,8 +1274,14 @@ class TradeDocumentForm(FlaskForm): def __init__(self, *args, **kwargs): lot_id = kwargs.pop('lot') + doc_id = kwargs.pop('document', None) super().__init__(*args, **kwargs) self._lot = Lot.query.filter(Lot.id == lot_id).one() + self.object = None + if doc_id: + self.object = TradeDocument.query.filter_by( + id=doc_id, lot=self._lot, owner=g.user + ).one() if not self._lot.transfer: self.form_errors = ['Error, this lot is not a transfer lot.'] @@ -1307,6 +1313,12 @@ class TradeDocumentForm(FlaskForm): return self._obj + def remove(self): + if self.object: + self.object.delete() + db.session.commit() + return self.object + class TransferForm(FlaskForm): lot_name = StringField( diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 79145f3c..73a55cc1 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -547,6 +547,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] @@ -1515,6 +1536,10 @@ devices.add_url_rule( '/lot//trade-document/add/', view_func=NewTradeDocumentView.as_view('trade_document_add'), ) +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( '/all/device/', view_func=AllDeviceListView.as_view('alldevicelist') 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/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index 2e2bc7b6..bcc0f22f 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -530,6 +530,7 @@ + @@ -545,6 +546,38 @@ + {% endfor %} {% for doc in lot.trade.documents %} @@ -559,6 +592,9 @@ + {% endfor %} From d1c332e89196ff3b84491184e1517b66a72f6a25 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Mar 2023 10:43:30 +0200 Subject: [PATCH 14/42] add edit document in template --- ereuse_devicehub/templates/inventory/device_list.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index bcc0f22f..58a7fc4f 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -547,7 +547,12 @@ {{ doc.created.strftime('%Y-%m-%d %H:%M')}} + diff --git a/ereuse_devicehub/templates/inventory/search.html b/ereuse_devicehub/templates/inventory/search.html index 435f99cc..9b1068fc 100644 --- a/ereuse_devicehub/templates/inventory/search.html +++ b/ereuse_devicehub/templates/inventory/search.html @@ -254,7 +254,7 @@
    PHIDErasure HostSanitization Host
    File Uploaded on
    {{ doc.created.strftime('%Y-%m-%d %H:%M')}} + + + + +
    {{ doc.created.strftime('%Y-%m-%d %H:%M')}} + +
    - + + + + + - +
    + + + + + + + + + {% for doc in placeholder.documents %} + + + + + + + {% endfor %} + +
    FileUploaded on
    + {% if doc.get_url() %} + {{ doc.file_name}} + {% else %} + {{ doc.file_name}} + {% endif %} + + {{ doc.created.strftime('%Y-%m-%d %H:%M')}} + + + + + + + + + +
    +
    +
    Status Details
    diff --git a/ereuse_devicehub/templates/inventory/device_document.html b/ereuse_devicehub/templates/inventory/device_document.html new file mode 100644 index 00000000..1991444f --- /dev/null +++ b/ereuse_devicehub/templates/inventory/device_document.html @@ -0,0 +1,70 @@ +{% extends "ereuse_devicehub/base_site.html" %} +{% block main %} + +
    +

    {{ title }}

    + +
    + +
    +
    +
    + +
    +
    + +
    +
    {{ title }}
    + {% if form.form_errors %} +

    + {% for error in form.form_errors %} + {{ error }}
    + {% endfor %} +

    + {% endif %} +
    + + {% if form._obj or 1==2 %} + + {% else %} + + {% endif %} + {{ form.csrf_token }} + {% for field in form %} + {% if field != form.csrf_token %} +
    + {{ field.label(class_="form-label") }} + {{ field }} + {{ field.description }} + {% if field.errors %} +

    + {% for error in field.errors %} + {{ error }}
    + {% endfor %} +

    + {% endif %} +
    + {% endif %} + {% endfor %} + +
    + Cancel + +
    + + +
    +
    + +
    +
    +
    +{% endblock main %} From 2f27095c8442f810acf6a67d3279ba50f39490d9 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 4 Apr 2023 16:56:27 +0200 Subject: [PATCH 19/42] documents in devices --- ereuse_devicehub/inventory/forms.py | 4 +- ereuse_devicehub/inventory/models.py | 11 +++ ereuse_devicehub/inventory/views.py | 72 +++++++++++------ .../templates/inventory/device_detail.html | 5 +- tests/test_basic.py | 3 + tests/test_render_2_0.py | 79 +++++++++++++++++++ 6 files changed, 148 insertions(+), 26 deletions(-) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index cc1fd447..47b07f91 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -1392,7 +1392,7 @@ class DeviceDocumentForm(FlaskForm): def validate(self, extra_validators=None): is_valid = super().validate(extra_validators) - if g.user == self._device.owner: + if g.user != self._device.owner: is_valid = False return is_valid @@ -1415,7 +1415,7 @@ class DeviceDocumentForm(FlaskForm): if not self._obj.id: db.session.add(self._obj) - self._device.documents.add(self._obj) + # self._device.documents.add(self._obj) if commit: db.session.commit() diff --git a/ereuse_devicehub/inventory/models.py b/ereuse_devicehub/inventory/models.py index 7073078e..1e51113e 100644 --- a/ereuse_devicehub/inventory/models.py +++ b/ereuse_devicehub/inventory/models.py @@ -1,6 +1,7 @@ from uuid import uuid4 from citext import CIText +from dateutil.tz import tzutc from flask import g from sortedcontainers import SortedSet from sqlalchemy import BigInteger, Column, Integer @@ -148,3 +149,13 @@ class DeviceDocument(Thing): # 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 e6906564..bb1ac9a9 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -569,27 +569,6 @@ class DocumentDeleteView(View): return flask.redirect(next_url) -class DeviceDocumentDeleteView(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] @@ -853,6 +832,48 @@ class NewDeviceDocumentView(GenericMixin): 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] @@ -875,7 +896,6 @@ class NewTradeDocumentView(GenericMixin): class EditTransferDocumentView(GenericMixin): - decorators = [login_required] methods = ['POST', 'GET'] template_name = 'inventory/trade_document.html' @@ -1601,6 +1621,14 @@ devices.add_url_rule( '/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'), diff --git a/ereuse_devicehub/templates/inventory/device_detail.html b/ereuse_devicehub/templates/inventory/device_detail.html index a7025549..c5e65e37 100644 --- a/ereuse_devicehub/templates/inventory/device_detail.html +++ b/ereuse_devicehub/templates/inventory/device_detail.html @@ -216,6 +216,7 @@ File Uploaded on + @@ -232,7 +233,7 @@ {{ doc.created.strftime('%Y-%m-%d %H:%M')}} - + @@ -259,7 +260,7 @@ diff --git a/tests/test_basic.py b/tests/test_basic.py index e834cba2..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/', diff --git a/tests/test_render_2_0.py b/tests/test_render_2_0.py index bf127c14..d2842429 100644 --- a/tests/test_render_2_0.py +++ b/tests/test_render_2_0.py @@ -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 From cd4d1bb095bd763324fe9b8ee199ecc416fdab02 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 4 Apr 2023 17:23:50 +0200 Subject: [PATCH 20/42] add select field --- ereuse_devicehub/inventory/forms.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index 47b07f91..bbc0ea1b 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -111,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"] @@ -1352,11 +1361,12 @@ class DeviceDocumentForm(FlaskForm): render_kw={'class': "form-control"}, description="Identification number of document", ) - type = StringField( + type = SelectField( 'Type', [validators.Optional()], - render_kw={'class': "form-control"}, - description="Type of document", + choices=TYPES_DOCUMENTS, + default="", + render_kw={'class': "form-select"}, ) date = DateField( 'Date', From b6b8c6a1f92cf5bec86bda4b76d68f5ead5a806d Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 18 Apr 2023 10:10:43 +0200 Subject: [PATCH 21/42] add new columns --- .../templates/inventory/device_detail.html | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ereuse_devicehub/templates/inventory/device_detail.html b/ereuse_devicehub/templates/inventory/device_detail.html index c5e65e37..5aa45f06 100644 --- a/ereuse_devicehub/templates/inventory/device_detail.html +++ b/ereuse_devicehub/templates/inventory/device_detail.html @@ -214,6 +214,8 @@ File + Type + Description Uploaded on @@ -230,8 +232,14 @@ {% endif %} - {{ doc.created.strftime('%Y-%m-%d %H:%M')}} + {{ doc.type }} + + + {{ doc.description|truncate(30, True) }} + + {{ doc.created.strftime('%Y-%m-%d %H:%M') }} + From 6692233a22ee10d4b774d1b9c1335fa53168a725 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 18 Apr 2023 11:08:43 +0200 Subject: [PATCH 22/42] fix --- ereuse_devicehub/templates/inventory/device_detail.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ereuse_devicehub/templates/inventory/device_detail.html b/ereuse_devicehub/templates/inventory/device_detail.html index 5aa45f06..6b55a799 100644 --- a/ereuse_devicehub/templates/inventory/device_detail.html +++ b/ereuse_devicehub/templates/inventory/device_detail.html @@ -235,7 +235,9 @@ {{ doc.type }} + {% if doc.description %} {{ doc.description|truncate(30, True) }} + {% endif %} {{ doc.created.strftime('%Y-%m-%d %H:%M') }} From 0ce12bbfbad6855fb19d7979bb2f2f767d0ae651 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 18 Apr 2023 18:15:44 +0200 Subject: [PATCH 23/42] fix it --- ereuse_devicehub/inventory/forms.py | 12 ++++++++++++ .../templates/inventory/device_list.html | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index bbc0ea1b..9995ace9 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -1293,6 +1293,8 @@ class TradeDocumentForm(FlaskForm): ).one() kwargs['obj'] = self._obj + if self._obj: + self.file_name.args = () super().__init__(*args, **kwargs) if self._obj: @@ -1313,6 +1315,10 @@ 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) @@ -1393,6 +1399,8 @@ class DeviceDocumentForm(FlaskForm): ).one() kwargs['obj'] = self._obj + if self._obj: + self.file_name.args = () super().__init__(*args, **kwargs) if self._obj: @@ -1410,6 +1418,10 @@ class DeviceDocumentForm(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) diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index cc304687..c89ff6d5 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -529,6 +529,7 @@ File + Description Uploaded on @@ -543,6 +544,11 @@ {{ doc.file_name}} {% endif %} + + {% if doc.description %} + {{ doc.description|truncate(30, True) }} + {% endif %} + {{ doc.created.strftime('%Y-%m-%d %H:%M')}} From ce7693dd9befdfa203e64ee7e64c6bb20a3b3cc2 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 19 Apr 2023 11:30:12 +0200 Subject: [PATCH 24/42] fix import teal in migration --- .../migrations/versions/ac476b60d952_add_document_device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py b/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py index 359f0b57..f2685d94 100644 --- a/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py +++ b/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py @@ -7,10 +7,11 @@ Create Date: 2023-03-31 10:46:02.463007 """ 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 = 'ac476b60d952' down_revision = '4f33137586dd' From 1a7c2f3a0132cfb1a16bedd4e7e778c8a6e87a05 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 19 Apr 2023 16:17:29 +0200 Subject: [PATCH 25/42] fix required in form --- ereuse_devicehub/inventory/forms.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index 9995ace9..ff083908 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -1293,7 +1293,9 @@ class TradeDocumentForm(FlaskForm): ).one() kwargs['obj'] = self._obj - if 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) @@ -1399,7 +1401,9 @@ class DeviceDocumentForm(FlaskForm): ).one() kwargs['obj'] = self._obj - if 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) From 119b4938c04566beb56690b433258d37ce0b29ee Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 20 Apr 2023 10:52:19 +0200 Subject: [PATCH 26/42] upgrade version --- CHANGELOG.md | 9 +++++++++ ereuse_devicehub/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5362c338..771b1d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,16 @@ 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. diff --git a/ereuse_devicehub/__init__.py b/ereuse_devicehub/__init__.py index 7a2056f5..667b52f9 100644 --- a/ereuse_devicehub/__init__.py +++ b/ereuse_devicehub/__init__.py @@ -1 +1 @@ -__version__ = "2.5.1" +__version__ = "2.5.2" From 8f5835fa4fbfa6fe61698be5cbf04dad6714bbe2 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 20 Apr 2023 18:07:12 +0200 Subject: [PATCH 27/42] fix test selenium --- tests/test_selenium.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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() From 750d3e7db6f1eb45f6e9cc7fa2cb26b1d9a943cd Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 21 Apr 2023 12:47:53 +0200 Subject: [PATCH 28/42] fix id supplier in export devices --- ereuse_devicehub/inventory/forms.py | 10 ++++++++-- ereuse_devicehub/resources/documents/device_row.py | 8 +++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index ff083908..26c3a700 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -468,8 +468,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 @@ -683,6 +681,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 ) diff --git a/ereuse_devicehub/resources/documents/device_row.py b/ereuse_devicehub/resources/documents/device_row.py index 68690cf0..ba165e0b 100644 --- a/ereuse_devicehub/resources/documents/device_row.py +++ b/ereuse_devicehub/resources/documents/device_row.py @@ -40,8 +40,9 @@ class BaseDeviceRow(OrderedDict): self['Temporary Lots'] = '' self['Incoming Lots'] = '' self['Outgoing Lots'] = '' - self['Placeholder Palet'] = '' + self['Placeholder Pallet'] = '' self['Placeholder Id Supplier'] = '' + self['Placeholder Id Internal'] = '' self['Placeholder Info'] = '' self['Placeholder Components'] = '' self['Placeholder Type'] = '' @@ -266,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 @@ -510,8 +511,9 @@ class DeviceRow(BaseDeviceRow): 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 Palet'] = none2str(self.placeholder.pallet) + 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) From 075dc8d5b035b4ac923f25342c8be9c2106e7230 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 21 Apr 2023 13:03:12 +0200 Subject: [PATCH 29/42] fix tests --- tests/test_render_2_0.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_render_2_0.py b/tests/test_render_2_0.py index d2842429..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][:32] == export_csv[1][:32] + fixture_csv[1][:33] == export_csv[1][:33] ), 'Computer information are not equal' - assert fixture_csv[1][33] == export_csv[1][33], 'Computer information are not equal' + assert fixture_csv[1][34] == export_csv[1][34], 'Computer information are not equal' assert ( - fixture_csv[1][35:96] == export_csv[1][35:96] + fixture_csv[1][36:97] == export_csv[1][36:97] ), 'Computer information are not equal' - assert fixture_csv[1][97] == export_csv[1][97], 'Computer information are not equal' + assert fixture_csv[1][98] == export_csv[1][98], 'Computer information are not equal' assert ( - fixture_csv[1][101:] == export_csv[1][101:] + fixture_csv[1][102:] == export_csv[1][102:] ), 'Computer information are not equal' From 0e3aa1ce043373e09cd93a711e8d0d15e58e037c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 21 Apr 2023 16:20:30 +0200 Subject: [PATCH 30/42] fix tests --- tests/files/basic.csv | 4 +-- tests/files/export_devices.csv | 4 +-- tests/files/proposal_extended_csv_report.csv | 6 ++-- tests/test_documents.py | 34 ++++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/files/basic.csv b/tests/files/basic.csv index c9ad235e..87597616 100644 --- a/tests/files/basic.csv +++ b/tests/files/basic.csv @@ -1,2 +1,2 @@ -"PHID";"DHID";"Type";"Temporary Lots";"Incoming Lots";"Outgoing Lots";"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";"Mon Mar 27 15:37:54 2023";"Workbench 11.0";"2023-03-27 15:37:55.003749+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 a87378f5..76483aa5 100644 --- a/tests/files/export_devices.csv +++ b/tests/files/export_devices.csv @@ -1,2 +1,2 @@ -"PHID";"DHID";"Type";"Temporary Lots";"Incoming Lots";"Outgoing Lots";"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.";"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" +"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/proposal_extended_csv_report.csv b/tests/files/proposal_extended_csv_report.csv index 6718d091..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";"Temporary Lots";"Incoming Lots";"Outgoing Lots";"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.";"Mon Mar 27 15:37:34 2023";"Workbench 11.0a2";"2023-03-27 15:37:34.478208+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 15:37:34.440184+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.";"Mon Mar 27 15:37:34 2023";"Workbench 11.0b11";"2023-03-27 15:37:34.836618+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-03-27 15:37:34.791027+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-03-27 15:37:34.792988+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-03-27 15:37:34.796512+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_documents.py b/tests/test_documents.py index c1608858..534dddcc 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -269,11 +269,11 @@ def test_export_basic_snapshot(user: UserClient): assert fixture_csv[0] == export_csv[0], 'Headers are not equal' assert ( - fixture_csv[1][:32] == export_csv[1][:32] + fixture_csv[1][:33] == export_csv[1][:33] ), 'Computer information are not equal' - assert fixture_csv[1][33] == export_csv[1][33], 'Computer information are not equal' + assert fixture_csv[1][34] == export_csv[1][34], 'Computer information are not equal' assert ( - fixture_csv[1][35:] == export_csv[1][35:] + fixture_csv[1][36:] == export_csv[1][36:] ), 'Computer information are not equal' @@ -339,38 +339,38 @@ def test_export_extended(app: Devicehub, user: UserClient): assert fixture_csv[0] == export_csv[0], 'Headers are not equal' assert ( - fixture_csv[1][:32] == export_csv[1][:32] + fixture_csv[1][:33] == export_csv[1][:33] ), 'Computer information are not equal' - assert fixture_csv[1][33] == export_csv[1][33], 'Computer information are not equal' + assert fixture_csv[1][34] == export_csv[1][34], 'Computer information are not equal' assert ( - fixture_csv[1][35:93] == export_csv[1][35:93] + fixture_csv[1][36:94] == export_csv[1][36:94] ), 'Computer information are not equal' - assert fixture_csv[1][97] == export_csv[1][97], 'Computer information are not equal' + assert fixture_csv[1][98] == export_csv[1][98], 'Computer information are not equal' assert ( - fixture_csv[1][100:] == export_csv[1][100:] + fixture_csv[1][101:] == export_csv[1][101:] ), 'Computer information are not equal' assert ( - fixture_csv[2][:32] == export_csv[2][:32] + fixture_csv[2][:33] == export_csv[2][:33] ), 'Computer information are not equal' - assert fixture_csv[2][33] == export_csv[2][33], 'Computer information are not equal' + assert fixture_csv[2][34] == export_csv[2][34], 'Computer information are not equal' assert ( - fixture_csv[2][35:96] == export_csv[2][35:96] + fixture_csv[2][36:97] == export_csv[2][36:97] ), 'Computer information are not equal' - assert fixture_csv[2][97] == export_csv[2][97], 'Computer information are not equal' + assert fixture_csv[2][98] == export_csv[2][98], 'Computer information are not equal' assert ( - fixture_csv[2][100:110] == export_csv[2][100:110] + fixture_csv[2][101:111] == export_csv[2][101:111] ), 'Computer information are not equal' assert ( - fixture_csv[2][123] == export_csv[2][123] + fixture_csv[2][124] == export_csv[2][124] ), 'Computer information are not equal' assert ( - fixture_csv[2][126:147] == export_csv[2][126:147] + fixture_csv[2][127:148] == export_csv[2][127:148] ), 'Computer information are not equal' assert ( - fixture_csv[2][149] == export_csv[2][149] + fixture_csv[2][150] == export_csv[2][150] ), 'Computer information are not equal' assert ( - fixture_csv[2][152:] == export_csv[2][152:] + fixture_csv[2][153:] == export_csv[2][153:] ), 'Computer information are not equal' From e365c366f45ccf9a33ef70ca2dac01ba8cd0b206 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 26 Apr 2023 18:25:03 +0200 Subject: [PATCH 31/42] share lot structure --- .../versions/2f2ef041483a_share_lot.py | 52 +++++++++++++++++++ ereuse_devicehub/resources/lot/models.py | 12 +++++ scripts/sharelot.py | 30 +++++++++++ 3 files changed, 94 insertions(+) create mode 100644 ereuse_devicehub/migrations/versions/2f2ef041483a_share_lot.py create mode 100644 scripts/sharelot.py 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/resources/lot/models.py b/ereuse_devicehub/resources/lot/models.py index 23a7ef0c..774689bb 100644 --- a/ereuse_devicehub/resources/lot/models.py +++ b/ereuse_devicehub/resources/lot/models.py @@ -396,3 +396,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/scripts/sharelot.py b/scripts/sharelot.py new file mode 100644 index 00000000..28f4325b --- /dev/null +++ b/scripts/sharelot.py @@ -0,0 +1,30 @@ +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(): + # import pdb; pdb.set_trace() + 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() From 0547e4cf3298f1440b82b921f3993cb801ec4262 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 28 Apr 2023 12:10:25 +0200 Subject: [PATCH 32/42] allow get device for export when is a share lot --- ereuse_devicehub/inventory/forms.py | 13 ++++--- ereuse_devicehub/inventory/views.py | 35 +++++++++++++++---- ereuse_devicehub/resources/lot/models.py | 18 +++++++++- .../templates/ereuse_devicehub/base_site.html | 21 +++++++++++ .../templates/inventory/device_list.html | 32 +++++++++++++++-- ereuse_devicehub/views.py | 3 +- 6 files changed, 105 insertions(+), 17 deletions(-) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index ff083908..bdaccd15 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -70,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 @@ -160,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): @@ -175,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 @@ -256,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 diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index bb1ac9a9..40c6217b 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 @@ -51,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 @@ -73,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) @@ -111,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, } ) @@ -537,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) @@ -1005,9 +1014,21 @@ 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 + join {schema}.lot_device as lot_device on + share.lot_id=lot_device.lot_id + where share.user_to_id='{user_id}' + """.format( + schema='dbtest', 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): diff --git a/ereuse_devicehub/resources/lot/models.py b/ereuse_devicehub/resources/lot/models.py index 774689bb..39ee9f73 100644 --- a/ereuse_devicehub/resources/lot/models.py +++ b/ereuse_devicehub/resources/lot/models.py @@ -124,7 +124,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): @@ -144,6 +147,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) diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index de544d75..179dcfd4 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -276,7 +276,28 @@ {% endif %} {% endfor %} + + {% if share_lots.all() %} + + {% elif lot.is_shared %} + + {% endif %} @@ -39,7 +42,9 @@
    @@ -54,10 +59,12 @@ Create Incoming Lot {% endif %} + {% if not lot.is_shared %} Delete Lot + {% endif %} {% endif %}
    @@ -72,7 +79,7 @@ - {% if lot and not lot.is_temporary %} + {% if lot and not lot.is_temporary and not lot.is_shared %} @@ -106,6 +113,7 @@
    + {% if not lot or not lot.is_shared %} + {% endif %} + {% if lot and lot.is_shared %} + + {% endif %}
    - {% if lot and not lot.is_temporary %} + {% if lot and not lot.is_temporary and not lot.is_shared %}
    {% endif %} From 7b9c33ca4f342fbedd20243fddb8f7f287b040a2 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 4 May 2023 10:27:40 +0200 Subject: [PATCH 37/42] fix base template --- ereuse_devicehub/templates/ereuse_devicehub/base_site.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index aaec8d2e..fad5376f 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -283,7 +283,7 @@ Shared with me - {% if lot.is_shared %} + {% if lot and lot.is_shared %}
    From acc5f6ed784eecce2b90207300aff7a3fc035c22 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 4 May 2023 16:14:43 +0200 Subject: [PATCH 40/42] fix devices lot shared --- ereuse_devicehub/templates/inventory/device_list.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index 426b7b02..82976a38 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -352,12 +352,6 @@ Devices Spreadsheet -
  • - - - Devices Lots Spreadsheet - -
  • {% endif %} From 906dceed56c836a20c60235fae7cbbf1bd471f2c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 4 May 2023 17:04:08 +0200 Subject: [PATCH 41/42] fix --- ereuse_devicehub/inventory/views.py | 1 - .../static/js/main_inventory.build.js | 17 ++++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 8ccb24f8..7e5a7999 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -1014,7 +1014,6 @@ class ExportsView(View): return export_ids[export_id]() def find_devices(self): - # import pdb; pdb.set_trace() sql = """ select lot_device.device_id as id from {schema}.share_lot as share inner join {schema}.lot_device as lot_device diff --git a/ereuse_devicehub/static/js/main_inventory.build.js b/ereuse_devicehub/static/js/main_inventory.build.js index c4bff9c4..389f4377 100644 --- a/ereuse_devicehub/static/js/main_inventory.build.js +++ b/ereuse_devicehub/static/js/main_inventory.build.js @@ -30,6 +30,7 @@ $(document).ready(() => { ; select_shift(); // $('#selectLot').selectpicker(); + $("#filter").on("change", () => { $("#submit_filter").click(); }); @@ -214,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; @@ -327,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; @@ -666,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)
  • "); @@ -692,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)); } From 8e54f34519d9bd3954677cdcba7e94ecc1f753a3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Sun, 7 May 2023 19:43:30 +0200 Subject: [PATCH 42/42] fix set_hid --- ereuse_devicehub/resources/device/models.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 0c1bd082..4858706b 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 uuid @@ -12,7 +13,6 @@ from typing import Dict, List, Set from boltons import urlutils from citext import CIText -from ereuse_devicehub.ereuse_utils.naming import HID_CONVERSION_DOC from flask import current_app as app from flask import g, request from more_itertools import unique_everseen @@ -37,6 +37,7 @@ from sqlalchemy_utils import ColorType from stdnum import imei, meid 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, @@ -71,6 +72,8 @@ 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): _id = Device.query.order_by(Device.id.desc()).first() or 3 @@ -805,7 +808,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: @@ -824,13 +827,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),