diff --git a/requirements/base.txt b/requirements/base.txt index 121d96029..81b08d174 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -14,9 +14,11 @@ django-extensions==1.7.9 django-model-utils==3.0.0 #tastypie 0.13.3 has breaking changes django-tastypie==0.13.1 +formencode==1.3.1 futures==3.0.5 # used by gunicorn's async workers gevent==1.2.1 # used by gunicorn's async workers gunicorn==19.7.1 +inflect==0.2.1 jsonfield==2.0.1 logutils==0.3.4.1 lxml==3.7.3 diff --git a/requirements/test.txt b/requirements/test.txt index 22165c50f..8647f43ea 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,6 +1,7 @@ # Test dependencies go here. -r base.txt +django_mock_queries==2.0.0 pytest pytest-cov==2.4.0 coverage==4.2 diff --git a/storage_service/locations/api/urls.py b/storage_service/locations/api/urls.py index 9b956f440..90445ae22 100644 --- a/storage_service/locations/api/urls.py +++ b/storage_service/locations/api/urls.py @@ -1,6 +1,7 @@ from django.conf.urls import include, url from tastypie.api import Api from locations.api import v1, v2 +import locations.api.v3 as v3_api from locations.api.sword import views @@ -19,6 +20,7 @@ v2_api.register(v2.AsyncResource()) urlpatterns = [ + url(r'v3/', include(v3_api.urls)), url(r'', include(v1_api.urls)), url(r'v1/sword/$', views.service_document, name='sword_service_document'), url(r'', include(v2_api.urls)), diff --git a/storage_service/locations/api/v3/__init__.py b/storage_service/locations/api/v3/__init__.py new file mode 100644 index 000000000..1a7c983e7 --- /dev/null +++ b/storage_service/locations/api/v3/__init__.py @@ -0,0 +1,68 @@ +"""Version 3 of the Storage Service API. + +The Storage Service exposes the following resources via a consistent HTTP JSON +interface under the path namespace /api/v3/: + +- /locations/ --- purpose-specific paths within a /spaces/ resource +- /packages/ --- Information Package (SIP, DIP or AIP) +- /spaces/ --- storage space with behaviour specific to backing system +- /pipelines/ --- an Archivematica instance that is the source of a package + +The following resources may be exposed later: + +- /file/ --- a file on disk (which is in a package), represented as db row. +- /fsobjects/ --- directories and files on disk, read-only, no database models + +All resources have endpoints that follow this pattern:: + + +-----------------+-------------+----------------------------+------------+ + | Purpose | HTTP Method | Path | Method | + +-----------------+-------------+----------------------------+------------+ + | Create new | POST | // | create | + | Create data | GET | //new/ | new | + | Read all | GET | // | index | + | Read specific | GET | /// | show | + | Update specific | PUT | /// | update | + | Update data | GET | ///edit/ | edit | + | Delete specific | DELETE | /// | delete | + | Search | SEARCH | // | search | + | Search | POST | //search/ | search | + | Search data | GET | //new_search/ | new_search | + +-----------------+-------------+----------------------------+------------+ + +.. note:: To remove the search-related routes for a given resource, create a + ``'searchable'`` key with value ``False`` in the configuration for the + resource in the ``RESOURCES`` dict. E.g., ``'location': {'searchable': + False}`` will make the /locations/ resource non-searchable. + +.. note:: All resources expose the same endpoints. If a resource needs special + treatment, it should be done at the corresponding class level. E.g., if + ``POST /packages/`` (creating a package) is special, then do special stuff + in ``resources.py::Packages.create``. Similarly, if packages are indelible, + then ``resources.py::Packages.delete`` should return 404. + +""" + +from locations.api.v3.remple import API +from locations.api.v3.resources import ( + Locations, + Packages, + Spaces, + Pipelines, +) + +API_VERSION = '3.0.0' +SERVICE_NAME = 'Archivematica Storage Service' + +resources = { + 'location': {'resource_cls': Locations}, + 'package': {'resource_cls': Packages}, # Readonly because of super-class of ``Packages`` + 'space': {'resource_cls': Spaces}, + 'pipeline': {'resource_cls': Pipelines}, +} + +api = API(api_version=API_VERSION, service_name=SERVICE_NAME) +api.register_resources(resources) +urls = api.get_urlpatterns() + +__all__ = ('urls', 'api') diff --git a/storage_service/locations/api/v3/remple/README.rst b/storage_service/locations/api/v3/remple/README.rst new file mode 100644 index 000000000..3ff749f24 --- /dev/null +++ b/storage_service/locations/api/v3/remple/README.rst @@ -0,0 +1,230 @@ +================================================================================ + Remple: REST Simple +================================================================================ + + +OpenAPI Generate Django Command +================================================================================ + +Generate the OpenAPI YAML file for V3 of the Storage Service REST API:: + + $ docker-compose exec archivematica-storage-service /src/storage_service/manage.py apiv3openapi + + +Documentation of the API +================================================================================ + +Note the following should be superseded by the OpenAPI (Swagger) auto-generated +interface/documentation. I am keeping it for now. + + +Get all resources of a given type +-------------------------------------------------------------------------------- + +Example: GET /pipelines/:: + + $ curl -H "Authorization: ApiKey test:test" \ + http://127.0.0.1:62081/api/v3/pipelines/ + [ + { + "api_key": "test", + "uuid": "3bf15d1c-4c7e-4002-b7b0-668983869d49", + "resource_uri": "/api/v3/pipelines/3bf15d1c-4c7e-4002-b7b0-668983869d49/", + "enabled": true, + "api_username": "test", + "remote_name": "172.20.0.13", + "id": 1, + "description": "Archivematica on f5c59e3ed603" + } + ] + +Pagination works by passing query parameters ``page`` and ``items_per_page``. +Example: GET /locations/ with pagination:: + + $ curl -H "Authorization: ApiKey test:test" \ + http://127.0.0.1:62081/api/v3/locations/?page=2&items_per_page=2 + { + "paginator": { + "count": 7, + "items_per_page": 2, + "page": 2 + }, + "items": [ + { + "pipeline": ["3bf15d1c-4c7e-4002-b7b0-668983869d49"], + "used": 0, + "uuid": "5dfd0998-35a6-4724-b428-e538a8f2cdd5", + "space": "c7463e9b-88d2-4674-a85b-5fc6905fd233", + "description": "", + "enabled": true, + "quota": null, + "relative_path": "home", + "purpose": "TS", + "replicators": [], + "id": 1, + "resource_uri": "/api/v3/locations/5dfd0998-35a6-4724-b428-e538a8f2cdd5/" + }, + { + "pipeline": ["3bf15d1c-4c7e-4002-b7b0-668983869d49"], + "used": 0, + "uuid": "7b1784b1-8887-453e-9be3-087ab2e0bb63", + "space": "c7463e9b-88d2-4674-a85b-5fc6905fd233", + "description": "Default transfer backlog", + "enabled": true, + "quota": null, + "relative_path": "var/archivematica/sharedDirectory/www/AIPsStore/transferBacklog", + "purpose": "BL", + "replicators": [], + "id": 4, + "resource_uri": "/api/v3/locations/7b1784b1-8887-453e-9be3-087ab2e0bb63/" + } + ] + } + + +Get a single resource by its UUID +-------------------------------------------------------------------------------- + +Example: GET /pipelines//:: + + $ curl -H "Authorization: ApiKey test:test" \ + http://127.0.0.1:62081/api/v3/pipelines/3bf15d1c-4c7e-4002-b7b0-668983869d49/ + { + "api_key": "test", + "uuid": "3bf15d1c-4c7e-4002-b7b0-668983869d49", + "resource_uri": "/api/v3/pipelines/3bf15d1c-4c7e-4002-b7b0-668983869d49/", + "enabled": true, + "api_username": "test", + "remote_name": "172.20.0.13", + "id": 1, + "description": "Archivematica on f5c59e3ed603" + } + + +Search across resources +-------------------------------------------------------------------------------- + +Search works by making a ``SEARCH`` request to the standard collection URI of +the resource (``/resources/``) or a ``POST`` request to ``/resources/search/``, +e.g., ``/locations/search/``. + +The request body should contain a object (dict) that has a ``query`` key and an +optional ``paginator`` key. The values of both of these keys are objects. The +``query`` dict has a ``filter`` key and an optional ``order_by`` key. Example:: + + { + "query": { + "filter": ["Location", "purpose", "regex", "[AT]S"] + "order_by": [ ... ] + }, + "paginator": { ... } + } + +Regex search for transfer source and archival storage locations. SEARCH +/locations/:: + + $ curl -H "Authorization: ApiKey test:test" \ + -H "Content-Type: application/json" \ + -X SEARCH \ + -d '{"query": {"filter": ["Location", "purpose", "regex", "[AT]S"]}}' \ + http://127.0.0.1:62081/api/v3/locations/ + [ + { + "pipeline": ["3bf15d1c-4c7e-4002-b7b0-668983869d49"], + "used": 0, + "uuid": "5dfd0998-35a6-4724-b428-e538a8f2cdd5", + "space": "c7463e9b-88d2-4674-a85b-5fc6905fd233", + "description": "", + "enabled": true, + "quota": null, + "relative_path": "home", + "purpose": "TS", + "replicators": [], + "id": 1, + "resource_uri": "/api/v3/locations/5dfd0998-35a6-4724-b428-e538a8f2cdd5/" + }, + { + "pipeline": ["3bf15d1c-4c7e-4002-b7b0-668983869d49"], + "used": 0, + "uuid": "a933c327-f081-4faa-b5dc-a0c81f4f494f", + "space": "c7463e9b-88d2-4674-a85b-5fc6905fd233", + "description": "Store AIP in standard Archivematica Directory", + "enabled": true, + "quota": null, + "relative_path": "var/archivematica/sharedDirectory/www/AIPsStore", + "purpose": "AS", + "replicators": [], + "id": 2, + "resource_uri": "/api/v3/locations/a933c327-f081-4faa-b5dc-a0c81f4f494f/" + } + ] + +The same search as above, but with reverse ordering and using ``POST +/locations/search/``:: + + $ curl -H "Authorization: ApiKey test:test" \ + -H "Content-Type: application/json" \ + -X POST \ + -d '{"query": {"filter": ["Location", "purpose", "regex", "[AT]S"], "order_by": [["purpose"]]}}' \ + http://127.0.0.1:62081/api/v3/locations/search/ + [ + { + "pipeline": ["3bf15d1c-4c7e-4002-b7b0-668983869d49" ], + "used": 0, + "uuid": "a933c327-f081-4faa-b5dc-a0c81f4f494f", + "space": "c7463e9b-88d2-4674-a85b-5fc6905fd233", + "description": "Store AIP in standard Archivematica Directory", + "enabled": true, + "quota": null, + "relative_path": "var/archivematica/sharedDirectory/www/AIPsStore", + "purpose": "AS", + "replicators": [], + "id": 2, + "resource_uri": "/api/v3/locations/a933c327-f081-4faa-b5dc-a0c81f4f494f/" + }, + { + "pipeline": ["3bf15d1c-4c7e-4002-b7b0-668983869d49"], + "used": 0, + "uuid": "5dfd0998-35a6-4724-b428-e538a8f2cdd5", + "space": "c7463e9b-88d2-4674-a85b-5fc6905fd233", + "description": "", + "enabled": true, + "quota": null, + "relative_path": "home", + "purpose": "TS", + "replicators": [], + "id": 1, + "resource_uri": "/api/v3/locations/5dfd0998-35a6-4724-b428-e538a8f2cdd5/" + } + ] + +The same search as above, this time adding pagination:: + + $ curl -H "Authorization: ApiKey test:test" \ + -H "Content-Type: application/json" \ + -X POST \ + -d '{"paginator": {"page": 2, "items_per_page": 1}, "query": {"filter": ["Location", "purpose", "regex", "[AT]S"], "order_by": [["purpose"]]}}' \ + http://127.0.0.1:62081/api/v3/locations/search/ + { + "paginator": { + "count": 2, + "items_per_page": 1, + "page": 2 + }, + "items": [ + { + "pipeline": ["3bf15d1c-4c7e-4002-b7b0-668983869d49"], + "used": 0, + "uuid": "5dfd0998-35a6-4724-b428-e538a8f2cdd5", + "space": "c7463e9b-88d2-4674-a85b-5fc6905fd233", + "description": "", + "enabled": true, + "quota": null, + "relative_path": "home", + "purpose": "TS", + "replicators": [], + "id": 1, + "resource_uri": "/api/v3/locations/5dfd0998-35a6-4724-b428-e538a8f2cdd5/" + } + ] + } diff --git a/storage_service/locations/api/v3/remple/__init__.py b/storage_service/locations/api/v3/remple/__init__.py new file mode 100644 index 000000000..bcc5d5b39 --- /dev/null +++ b/storage_service/locations/api/v3/remple/__init__.py @@ -0,0 +1,31 @@ +"""Remple: REST Simple + +Usage:: + + >>> from remple import API, Resources + >>> class Users(Resources): + ... model_cls = User # A Django model class + ... schema_cls = UserSchema # A Formencode class + >>> resources = {'user': {'resource_cls': Users}} + >>> api = API(api_version='0.1.0', service_name='User City!') + >>> api.register_resources(resources) + >>> urls = api.get_urlpatterns() # Include thes in Django urlpatterns +""" + +from __future__ import absolute_import + +from .resources import Resources, ReadonlyResources +from .querybuilder import QueryBuilder +from .routebuilder import RouteBuilder as API +from .routebuilder import ( + UUID_PATT, + ID_PATT, + get_collection_targeting_regex, + get_member_targeting_regex, +) +from . import utils +from .schemata import ValidModelObject + +__all__ = ('API', 'UUID_PATT', 'ID_PATT', 'utils', 'ReadonlyResources', + 'QueryBuilder', 'Resources', 'ValidModelObject', + 'get_collection_targeting_regex', 'get_member_targeting_regex',) diff --git a/storage_service/locations/api/v3/remple/clientbuilder.py b/storage_service/locations/api/v3/remple/clientbuilder.py new file mode 100644 index 000000000..810f2429f --- /dev/null +++ b/storage_service/locations/api/v3/remple/clientbuilder.py @@ -0,0 +1,585 @@ +"""Remple OpenAPI Client + +This module is not a code generator. It does not generate source code. It takes +an OpenAPI 3.0 definition of an API as a dict and returns a module with a +class (dynamically named according to the title of the API defined in the +OpenAPI spec) that provides a Pythonic interface to the OpenAPI-described API. + +Imagine an API entitled "Archivematica Storage Service Api" and which exposes +CRUDS endpoints on two resources: locations and spaces. Example usage might be:: + + >>> from clientbuilder import ArchivematicaStorageServiceApiClient + >>> c = ArchivematicaStorageServiceApiClient( + ... username='test', + ... api_key='test', + ... url='http://127.0.0.1:62081/') + >>> first_2_spaces = c.space.get_many( + ... items_per_page=2, order_by_attribute='id', page=1)['items'] + >>> first_space = first_2_spaces[0] + >>> locations_of_first_space = c.location.search({ + ... 'filter': ['space', 'uuid', '=', first_space['uuid']]})[ + ... 'items'] + >>> first_location = c.location.get(locations_of_first_space[0]['uuid']) + >>> updated_first_location = c.location.update( + ... pk=first_location['uuid'], purpose='AS') + >>> new_location = c.location.create( + ... purpose='AS', + ... relative_path='some/path', + ... space=first_space['uuid']) + >>> pprint.pprint(new_location) + ... {u'pipeline': [u'bb603958-c7f6-46c0-8677-7ce1a4a45497'], + ... u'used': 0, + ... u'description': None, + ... u'space': u'ad48b0df-295e-4f97-810e-b8de14b92c4b', + ... u'enabled': True, + ... u'quota': None, + ... u'relative_path': u'some/path', + ... u'purpose': u'AS', + ... u'id': 6, + ... u'resource_uri': u'/api/v3/locations/ec9c2e51-8883-472a-986a-48c7dc44e3a9/', + ... u'replicators': [], + ... u'uuid': u'ec9c2e51-8883-472a-986a-48c7dc44e3a9'} + +TODOs: + +- Use the OpenAPI response descriptions to handle HTTP responses gracefully. +- handle circular request bodies, e.g., search + +""" + + +from collections import OrderedDict +import logging +import pprint +import sys +import urllib3 + +import requests + + +logger = logging.getLogger(__name__) +log_lvl = logging.DEBUG +out_hdlr = logging.StreamHandler(sys.stdout) +logger.addHandler(out_hdlr) +logger.setLevel(log_lvl) + + +# OPENAPI_SPEC goes here + +HTTP_METHODS = ('get', 'delete', 'post', 'put') +METHOD_GET = "GET" +METHOD_POST = "POST" +METHOD_DELETE = "DELETE" + + +class OpenAPIClientError(Exception): + pass + + +def _call_url_json(url, params=None, method=METHOD_GET, headers=None, + assume_json=True): + """Helper to GET a URL where the expected response is 200 with JSON. + + :param str url: URL to call + :param dict params: Params to pass as HTTP query string or JSON body + :param str method: HTTP method (e.g., 'GET') + :param dict headers: HTTP headers + :param bool assume_json: set to False if the response body should not be + decoded as JSON + :returns: Dict of the returned JSON or an integer error + code to be looked up + """ + method = method.upper() + logger.debug('URL: %s; params: %s; method: %s', url, params, method) + try: + if method == METHOD_GET or method == METHOD_DELETE: + response = requests.request(method, + url=url, + params=params, + headers=headers) + else: + response = requests.request(method, + url=url, + data=params, + headers=headers) + logger.debug('Response: %s', response) + logger.debug('type(response.text): %s ', type(response.text)) + logger.debug('Response content-type: %s', + response.headers['content-type']) + except (urllib3.exceptions.NewConnectionError, + requests.exceptions.ConnectionError) as err: + msg = 'Connection error {}'.format(err) + logger.error(msg) + raise OpenAPIClientError(msg[:30]) + if not response.ok: + logger.warning('%s Request to %s returned %s %s', method, url, + response.status_code, response.reason) + msg = 'Response: {}'.format(response.text) + logger.debug(msg) + raise OpenAPIClientError(msg[:30]) + if assume_json: + try: + return response.json() + except ValueError: # JSON could not be decoded + msg = 'Could not parse JSON from response: {}'.format(response.text) + logger.warning(msg) + raise OpenAPIClientError(msg[:30]) + return response.text + + +def get_openapi_spec(): + return OPENAPI_SPEC + + +def get_client_class_name(openapi_spec): + title = openapi_spec['info']['title'] + return '{}Client'.format( + ''.join(w.capitalize() for w in title.strip().lower().split())) + + +def get_client_class_docstring(openapi_spec, resource_classes): + docstring = [] + title = openapi_spec['info']['title'] + version = openapi_spec['info']['version'] + docstring.append('{} version {} client'.format(title, version)) + description = openapi_spec['info'].get('description') + if description: + docstring.append('\n\n') + docstring.append('The targeted API is described as follows. ') + docstring.append(description) + if resource_classes: + docstring.append('\n\n') + docstring.append( + 'The following instance attributes allow interaction with the' + ' resources that the API exposes. See their documentation:\n\n') + for attr_name in sorted(resource_classes): + docstring.append('- ``self.{}``\n'.format(attr_name)) + if description or resource_classes: + docstring.append('\n') + return ''.join(docstring) + + +def deref(openapi_spec, ref_path): + """Given an OpenAPI $ref path like '#/components/schemas/ErrorSchema', + dereference it, i.e., return its corresponding object (typically a dict). + """ + ref_path_parts = ref_path.strip('#/').split('/') + dict_ = openapi_spec + for key in ref_path_parts: + dict_ = dict_[key] + return dict_ + + +def recursive_deref(openapi_spec, ref_path, seen_paths=None): + """Recursively dereference OpenAPI $ref path ``ref_path``, returning a + 2-tuple containing the corresponding object as well as a boolean indicating + whether the object is circularly referential. + """ + circular = False + seen_paths = seen_paths or [] + if ref_path in seen_paths: + return ref_path, True + seen_paths.append(ref_path) + derefed = deref(openapi_spec, ref_path) + properties = derefed.get('properties') + if properties: + for key, cfg in properties.items(): + ref = cfg.get('$ref') + if ref: + ret, circ = recursive_deref( + openapi_spec, ref, seen_paths=seen_paths) + derefed['properties'][key] = ret + if circ: + circular = True + one_of = derefed.get('oneOf') + if one_of: + new_one_of = [] + for these in one_of: + ret, circ = recursive_deref( + openapi_spec, these['$ref'], seen_paths=seen_paths) + new_one_of.append(ret) + if circ: + circular = True + derefed['oneOf'] = new_one_of + return derefed, circular + + +def ref_path2param_name(ref_path): + return ref_path.strip('#/').split('/')[-1] + + +def process_param(parameter, openapi_spec): + ref_path = parameter['$ref'] + param_name = ref_path2param_name(ref_path) + param_cfg = deref(openapi_spec, ref_path) + return param_name, param_cfg + + +def _reconstruct_params(locals_, args_, kwargs_): + ret = {} + for arg_name, arg_cfg in args_.items(): + ret[arg_name] = locals_[arg_name] + for arg_name, arg_cfg in kwargs_.items(): + try: + ret[arg_name] = locals_[arg_name] + except KeyError: + try: + ret[arg_name] = arg_cfg['default'] + except KeyError: + pass + return ret + + +openapitype2pythontype = { + 'string': ((str,), 'str'), + 'integer': ((int,), 'int'), + 'number': ((int, float), 'int or float'), + 'array': ((list,), 'list'), + 'boolean': ((bool,), 'bool'), +} + + +def get_param_docstring_line(arg, arg_cfg): + arg_line = [' ', arg] + arg_type = arg_cfg.get('type', arg_cfg.get('schema', {}).get('type')) + if arg_type: + _, arg_type = openapitype2pythontype.get(arg_type, (None, arg_type)) + arg_format = arg_cfg.get('format', arg_cfg.get('schema', {}).get('format')) + if arg_format: + arg_line.append(' ({}; {}):'.format(arg_type, arg_format)) + else: + arg_line.append(' ({}):'.format(arg_type)) + arg_description = arg_cfg.get('description') + if arg_description: + if not arg_description.endswith('.'): + arg_description = '{}.'.format(arg_description) + arg_line.append(' {}'.format(arg_description)) + return '\n' + ''.join(arg_line) + + +def get_method_docstring(op_cfg, args, kwargs): + summary = op_cfg.get('summary') + description = op_cfg.get('description') + if not summary: + return None + docstring = [summary] + if description and description != summary: + docstring.append('\n\n') + docstring.append(description) + if args or kwargs: + docstring.append('\n\n') + docstring.append('Args:') + for arg, arg_cfg in args.items(): + docstring.append(get_param_docstring_line(arg, arg_cfg)) + for kwarg, kwarg_cfg in kwargs.items(): + docstring.append(get_param_docstring_line(kwarg.replace('_param', ''), kwarg_cfg)) + return ''.join(docstring) + + +def _validate_min(param_name, param_val, param_schema): + param_min = param_schema.get('minimum') + if param_min: + if param_val < param_min: + raise ValueError( + 'Value {} for argument "{}" must be {} or greater.'.format( + param_val, param_name, param_min)) + + +def _validate_max(param_name, param_val, param_schema): + param_max = param_schema.get('maximum') + if param_max: + if param_val > param_max: + raise ValueError( + 'Value {} for argument "{}" is greater than the maximum' + ' allowed value {}.'.format( + param_val, param_name, param_max)) + + +def _validate_enum(param_name, param_val, param_schema): + param_enum = param_schema.get('enum') + if param_enum: + if param_val not in param_enum: + raise ValueError( + 'Value {} for argument "{}" must be one of {}'.format( + repr(param_val), param_name, + ', '.join(repr(e) for e in param_enum))) + + +def _is_uuid(inp): + err = ValueError('"{}" is not a valid UUID'.format(inp)) + try: + recomposed = [] + parts = inp.split('-') + if [len(p) for p in parts] != [8, 4, 4, 4, 12]: + raise err + for part in parts: + new_part = ''.join(c for c in part if c in '0123456789abcdef') + recomposed.append(new_part) + recomposed = '-'.join(recomposed) + except Exception: + raise err + if recomposed != inp: + raise err + + +format_validators = { + 'uuid': _is_uuid, +} + + +def _validate_format(param_name, param_val, param_schema): + print('validate format') + param_format = param_schema.get('format') + if not param_format: + print('validate format no format') + return + validator = format_validators.get(param_format) + if not validator: + print('validate format no validator') + return + validator(param_val) + + +def _validate(params, args_, kwargs_): + """Validate user-supplied ``params`` using the parameter configurations + described in the ``args_`` and ``kwargs_`` dicts. Raise a ``ValueError`` if + a value is invalid. Also remove unneeded values from ``params``, which is + what gets sent (as request body or query params) in the request. + """ + to_delete = [] + for param_name, param_val in params.items(): + param_cfg = args_.get(param_name, kwargs_.get(param_name, {})) + param_required = param_cfg.get('required', False) + param_schema = param_cfg.get('schema', {}) + param_type = param_schema.get('type') + if not param_type: + continue + param_type, _ = openapitype2pythontype.get(param_type, (None, None)) + if not param_type: + continue + if ((param_val is None) and + (not param_required) and + (not isinstance(None, param_type))): + to_delete.append(param_name) + continue + if not isinstance(param_val, param_type): + raise ValueError( + 'Value "{}" for argument "{}" is of type {}; it must be of type(s)' + ' {}'.format(param_val, param_name, type(param_val), + ', '.join(str(t) for t in param_type))) + _validate_min(param_name, param_val, param_schema) + _validate_max(param_name, param_val, param_schema) + _validate_format(param_name, param_val, param_schema) + _validate_enum(param_name, param_val, param_schema) + + param_in = param_cfg.get('in') + if param_in == 'path': + to_delete.append(param_name) + + for td in to_delete: + del params[td] + + +def _get_kwarg_names(kwargs_): + kwarg_names='' + if kwargs_: + kwarg_names=', ' + ', '.join( + '{}={}'.format( + key, + repr(cfg.get('schema', {}).get('default', None))) + for key, cfg in sorted(kwargs_.items())) + return kwarg_names + + +def generate_method_code(method_name, docstring, args_, kwargs_): + arg_names = '' + if args_: + arg_names = ', ' + ', '.join(args_) + kwarg_names = _get_kwarg_names(kwargs_) + return ''' +def {method_name}(self{arg_names}{kwarg_names}): + """{docstring} + """ + locals_copy = locals().copy() + format_kwargs = {{key: locals_copy[key] for key in args_}} + path_ = globals()['path_'].format(**format_kwargs) + url = self.url + path_ + params = _reconstruct_params(locals(), args_, kwargs_) + _validate(params, args_, kwargs_) + pprint.pprint(params) + return _call_url_json( + url, params=params, method=http_method.upper(), + headers=self.get_auth_headers()) +'''.format(method_name=method_name, + docstring=docstring, + arg_names=arg_names, + kwarg_names=kwarg_names) + + +def _get_request_body_schema(openapi_spec, operation_config): + request_body = operation_config.get('requestBody') + if not request_body: + return None, False + try: + rb_ref_path = request_body[ + 'content']['application/json']['schema']['$ref'] + except KeyError: + return None, False + return recursive_deref(openapi_spec, rb_ref_path) + + +def _get_request_body_args_kwargs(openapi_spec, op_cfg): + args = OrderedDict() + kwargs = {} + rb_schema, rb_circular = _get_request_body_schema(openapi_spec, op_cfg) + if rb_circular: # TODO + return args, kwargs + if rb_schema: + for arg_name in sorted(rb_schema.get('required', [])): + args[arg_name] = rb_schema['properties'][arg_name] + for kwarg, cfg in rb_schema['properties'].items(): + if kwarg in args: + continue + kwargs[kwarg] = cfg + return args, kwargs + + +def get_method(openapi_spec, path, path_params, http_method, op_cfg): + """Return a Python method, its name and its namespace, given the operation + defined by the unique combination of ``path`` and ``http_method``. E.g., + path /locations/{pk}/ and http_method GET will return a method named + ``get`` with namespace ``location``. The ``get`` method will be assigned to + a ``LocationClient`` instance of the ``Client`` instance, thus allowing the + caller to call ``myclient.location.get(pk_of_a_resource)``. + """ + # pylint: disable=exec-used,too-many-locals + method_name, namespace = op_cfg['operationId'].split('.') + rb_args, rb_kwargs = _get_request_body_args_kwargs(openapi_spec, op_cfg) + parameters = op_cfg.get('parameters', []) + args_ = OrderedDict() + kwargs_ = {} + for parameter in path_params: + args_[parameter['name']] = parameter + for param_name, param_cfg in rb_args.items(): + args_[param_name] = param_cfg + for param_name, param_cfg in rb_kwargs.items(): + kwargs_[param_name] = param_cfg + for parameter in parameters: + param_name, param_cfg = process_param(parameter, openapi_spec) + if param_cfg.get('required', True): + args_[param_name] = param_cfg + else: + kwargs_[param_name] = param_cfg + docstring = get_method_docstring(op_cfg, args_, kwargs_) + method_ = generate_method_code(method_name, docstring, args_, kwargs_) + temp_globals = globals().copy() + temp_globals.update({'path_': path, 'args_': args_, 'kwargs_': kwargs_, + 'http_method': http_method,}) + exec method_ in temp_globals, locals() + return locals()[method_name], method_name, namespace + + +def get_namespaces_methods(openapi_spec): + """Return a dict from namespaces to method names to methods, e.g.,:: + + >>> {'location': {'get': , + ... 'get_many': , + ... ...}, + ... 'package': {'get': , + ... ...}, + ... ...} + """ + methods = {} + for path, cfg in openapi_spec['paths'].items(): + path_params = cfg.get('parameters', []) + for http_method, op_cfg in cfg.items(): + if http_method in HTTP_METHODS: + method, method_name, namespace = get_method( + openapi_spec, path, path_params, http_method, op_cfg) + methods.setdefault(namespace, {})[method_name] = method + return methods + + +def get_get_auth_headers_meth(openapi_spec): + """Return a method for the client class that returns the authentication + headers needed to make requests. Just hard-coding this for now to be + specific to the AM SS API, but it should be generalized to parse the + OpenAPI spec. + """ + # pylint: disable=unused-argument + def get_auth_headers(self): + return {'Authorization': 'ApiKey {}:{}'.format( + self.username, + self.api_key)} + return get_auth_headers + + +def get_base_client_class(openapi_spec): + + def __init__(self, username, api_key, url): + """Args: + username (str): The username to use when authenticating to the API. + api_key (str): The API key to use when authenticating to the API. + url (str): The URL of the API. + """ + self.username = username + self.api_key = api_key + self.url = url.rstrip('/') + self.openapi_spec['servers'][0]['url'] + + def get_auth_headers(self): + """Return the authorization header(s) as a dict.""" + return {'Authorization': 'ApiKey {}:{}'.format( + self.username, + self.api_key)} + + return type( + 'BaseClient', + (object,), + {'__init__': __init__, + 'get_auth_headers': get_auth_headers, + 'openapi_spec': openapi_spec}) + + +def get_init_meth(): + def __init__(self, username, api_key, url): + super(self.__class__, self).__init__(username, api_key, url) + for resource_name, resource_class in self.resource_classes.items(): + setattr(self, resource_name, resource_class(username, api_key, url)) + return __init__ + + +def get_rsrc_cls_docstring(resource_name): + return 'Provides access to the {} resource'.format(resource_name) + + +def metaprog_client_class(): + """Define and return the client class and its name. + """ + openapi_spec = get_openapi_spec() + client_class_name_ = get_client_class_name(openapi_spec) + BaseClient = get_base_client_class(openapi_spec) + namespaces_methods = get_namespaces_methods(openapi_spec) + resource_classes = {} + for namespace, rsrc_methods in namespaces_methods.items(): + cls_name = namespace.capitalize() + 'Client' + rsrc_methods['__doc__'] = get_rsrc_cls_docstring(namespace) + rsrc_cls = type(cls_name, (BaseClient,), rsrc_methods) + resource_classes[namespace] = rsrc_cls + client_class_docstring = get_client_class_docstring( + openapi_spec, resource_classes) + attributes = {'__init__': get_init_meth(), + 'resource_classes': resource_classes, + '__doc__': client_class_docstring} + client_class_ = type( + client_class_name_, # e.g., ArchivematicaStorageServiceAPIClass + (BaseClient,), # superclass(es) + attributes # class attributes and methods (descriptors) + ) + return client_class_, client_class_name_ + + +client_class, client_class_name = metaprog_client_class() +globals()[client_class_name] = client_class + +# pylint: disable=undefined-all-variable +__all__ = ('client_class', client_class_name) diff --git a/storage_service/locations/api/v3/remple/constants.py b/storage_service/locations/api/v3/remple/constants.py new file mode 100644 index 000000000..2c8b353aa --- /dev/null +++ b/storage_service/locations/api/v3/remple/constants.py @@ -0,0 +1,54 @@ +JSONDecodeErrorResponse = { + 'error': 'JSON decode error: the parameters provided were not valid' + ' JSON.' +} + +UNAUTHORIZED_MSG = { + 'error': 'You are not authorized to access this resource.' +} + +READONLY_RSLT = {'error': 'This resource is read-only.'} + +OK_STATUS = 200 +BAD_REQUEST_STATUS = 400 +FORBIDDEN_STATUS = 403 +NOT_FOUND_STATUS = 404 +METHOD_NOT_ALLOWED_STATUS = 405 + + +django_field_class2openapi_type = { + 'AutoField': 'integer', + 'BigIntegerField': 'integer', + 'IntegerField': 'integer', + 'BooleanField': 'boolean', + 'CharField': 'string', + 'TextField': 'string', + 'UUIDField': 'string', + 'DateTimeField': 'string', + 'JSONField': 'object', +} + +django_field_class2openapi_format = { + 'UUIDField': 'uuid', + 'DateTimeField': 'date-time', +} + +python_type2openapi_type = { + str: 'string', + int: 'integer', + float: 'integer', +} + +formencode_field_class2openapi_type = { + 'UnicodeString': 'string', + 'OneOf': 'string', # note: not universally accurate + 'IPAddress': 'string', + 'URL': 'string', + 'Int': 'integer', + 'Bool': 'boolean', +} + +formencode_field_class2openapi_format = { + 'IPAddress': 'ipv4', + 'URL': 'uri', +} diff --git a/storage_service/locations/api/v3/remple/openapi.py b/storage_service/locations/api/v3/remple/openapi.py new file mode 100644 index 000000000..1cc6bc768 --- /dev/null +++ b/storage_service/locations/api/v3/remple/openapi.py @@ -0,0 +1,1489 @@ +from collections import OrderedDict +import json +import pprint +import re +import yaml +try: + from yaml import CDumper as Dumper +except ImportError: + from yaml import Dumper +from yaml.representer import SafeRepresenter + +from django.db.models.fields.related import ( + ForeignKey, + ManyToManyRel, + ManyToManyField, + ManyToOneRel, + ManyToOneRel, +) +from django.db.models.fields import ( + AutoField, + BigIntegerField, + BooleanField, + CharField, + NOT_PROVIDED, + TextField, +) +from django_extensions.db.fields import UUIDField + +from .schemata import schemata +from .resources import Resources +from .constants import ( + django_field_class2openapi_type, + django_field_class2openapi_format, + python_type2openapi_type, + formencode_field_class2openapi_type, + formencode_field_class2openapi_format, +) +from .querybuilder import QueryBuilder + +def dict_representer(dumper, data): + return dumper.represent_dict(data.iteritems()) + + +def yaml_dump(obj): + """Allows us to create YAML from Python OrderedDict instances and also have + Python strings and unicode objects correctly represented. From + https://gist.github.com/oglops/c70fb69eef42d40bed06 + """ + Dumper.add_representer(OrderedDict, dict_representer) + Dumper.add_representer(str, SafeRepresenter.represent_str) + Dumper.add_representer(unicode, SafeRepresenter.represent_unicode) + # Cf. http://signal0.com/2013/02/06/disabling_aliases_in_pyyaml.html + Dumper.ignore_aliases = lambda self, data: True + return yaml.dump(obj, Dumper=Dumper, default_flow_style=False) + + +OPENAPI_VERSION = '3.0.0' +SCHEMAS_ABS_PATH = '#/components/schemas/' +ERROR_SCHEMA_NAME = 'ErrorSchema' +PAGINATOR_SCHEMA_NAME = 'PaginatorSchema' + + +class OpenAPI(object): + + def __init__(self, api_version='0.1.0', service_name='My Service', + path_prefix='/api/'): + self.api_version = api_version + self.service_name = service_name + self.path_prefix = path_prefix + + def generate_open_api_spec(self): + """Generate and OpenAPI specification for this API. + + Returns a Python OrderedDict that can be converted to a YAML file which + describes this ``OpenAPI`` instance. + """ + return OrderedDict([ + ('openapi', OPENAPI_VERSION), + ('info', self._get_api_info()), + ('servers', self._get_api_servers()), + ('security', self._get_api_security()), + ('components', OrderedDict([ + ('securitySchemes', self._get_api_security_schemes()), + ('parameters', self._get_api_remple_parameters()), + ('schemas', self._get_schemas()),]), + ), + ('paths', self._get_paths()), + ('tags', self._get_tags()), + + ]) + + def _get_dflt_server_description(self): + return 'The default server for the {}.'.format(self.service_name) + + def _get_api_description(self): + return 'An API for the {}.'.format(self.service_name) + + def _get_api_title(self): + return '{} API'.format(self.service_name) + + def _get_tags(self): + tags = [] + for resource_name, resource_cfg in sorted(self.resources.items()): + tags.append(OrderedDict([ + ('name', self.inflp.plural(resource_name)), + ('description', 'Access to the {} resource'.format( + resource_name.capitalize())), + ])) + return tags + + def _get_paths(self): + paths = {} + for resource_name, resource_cfg in sorted(self.resources.items()): + pk_patt = resource_cfg.get('pk_patt', 'pk') + resource_cls = resource_cfg['resource_cls'] + rsrc_collection_name = self.inflp.plural(resource_name) + self._set_crud_paths(paths, resource_name, resource_cls, pk_patt, + rsrc_collection_name) + if resource_cfg.get('searchable', True): + self._set_search_paths(paths, resource_name, resource_cls, + pk_patt, rsrc_collection_name) + return paths + + + def _set_crud_paths(self, paths, resource_name, resource_cls, pk_patt, + rsrc_collection_name): + for action in self.RESOURCE_ACTIONS: + # Read-only resources need no mutating paths + if (not issubclass(resource_cls, Resources) and + action in self.MUTATING_ACTIONS): + continue + http_method = self.ACTIONS2METHODS.get( + action, self.DEFAULT_METHOD).lower() + operation_id = _get_operation_id(action, resource_name) + # operation_id = '{}_{}'.format(action, resource_name) + path_params = None + if action in self.COLLECTION_TARGETING: + path = get_collection_targeting_openapi_path( + rsrc_collection_name) + # if action == 'index': + # operation_id = '{}_{}'.format(action, rsrc_collection_name) + elif action in self.MEMBER_TARGETING: + path, path_params = get_member_targeting_openapi_path( + resource_name, rsrc_collection_name, pk_patt) + elif action == 'new': + path = get_collection_targeting_openapi_path( + rsrc_collection_name, modifiers=['new']) + else: # edit is default case + path, path_params = get_member_targeting_openapi_path( + resource_name, rsrc_collection_name, pk_patt, + modifiers=['edit']) + if path_params: + path_dict = paths.setdefault(path, {'parameters': path_params}) + else: + path_dict = paths.setdefault(path, {}) + responses_meth = '_get_{}_responses'.format(action) + parameters_meth = '_get_{}_parameters'.format(action) + request_body_meth = '_get_{}_request_body'.format(action) + parameters = getattr(self, parameters_meth, lambda x: None)( + resource_name) + request_body = getattr(self, request_body_meth, lambda x: None)( + resource_name) + summary, description = _summarize( + action, resource_name, rsrc_collection_name) + path_dict[http_method] = OrderedDict([ + ('summary', summary), + ('description', description), + ('operationId', operation_id), + ]) + if parameters: + path_dict[http_method]['parameters'] = parameters + if request_body: + path_dict[http_method]['requestBody'] = request_body + path_dict[http_method]['responses'] = getattr( + self, responses_meth)(resource_name) + path_dict[http_method]['tags'] = [rsrc_collection_name] + + def _get_search_request_body_examples(self, resource_name): + """Note: unfortunately, these examples will not be displayed in the + Swagger UI since that functionality is not implemented yet. See + https://github.com/swagger-api/swagger-ui/issues/3771. + """ + if resource_name == 'package': + array_search_example = { + 'paginator': {'page': 1, 'items_per_page': 10}, + 'query': { + 'filter': [ + 'and', [['description', 'like', '%a%'], + ['not', ['description', 'like', 'T%']], + ['or', [['size', '<', 1000], + ['size', '>', 512]]]]]}} + object_search_example = { + 'paginator': {'page': 1, 'items_per_page': 10}, + 'query': { + 'filter': { + 'conjunction': 'and', + 'complement': [{'attribute': 'description', + 'relation': 'like', + 'value': '%a%'}, + {'negation': 'not', + 'complement': {'attribute': 'description', + 'relation': 'like', + 'value': 'T%'}}, + {'conjunction': 'or', + 'complement': [{'attribute': 'size', + 'relation': '<', + 'value': 1000}, + {'attribute': 'size', + 'relation': '>', + 'value': 512}]}]}}} + return {'ArraySearchOverPackagesExample': array_search_example, + 'ObjectSearchOverPackagesExample': object_search_example} + + def _set_search_paths(self, paths, resource_name, resource_cls, pk_patt, + rsrc_collection_name): + for action in ('search', 'search_post', 'new_search'): + http_method = {'search_post': 'post', 'new_search': 'get'}.get( + action, 'search') + operation_id = _get_operation_id(action, resource_name) + # operation_id = '{}_{}'.format(action, rsrc_collection_name) + modifiers = {'search_post': ['search'], + 'new_search': ['new_search']}.get( + action, []) + path = get_collection_targeting_openapi_path( + rsrc_collection_name, modifiers=modifiers) + path_dict = paths.setdefault(path, {}) + responses_meth = '_get_{}_responses'.format(action) + parameters_meth = '_get_{}_parameters'.format(action) + request_body_meth = '_get_{}_request_body'.format(action) + request_body_examples_meth = '_get_{}_request_body_examples'.format( + action) + parameters = getattr(self, parameters_meth, lambda x: None)( + resource_name) + request_body_examples = getattr( + self, request_body_examples_meth, lambda x: None)(resource_name) + request_body_meth = getattr(self, request_body_meth, None) + request_body = None + if request_body_meth: + request_body = request_body_meth( + resource_name, examples=request_body_examples) + summary, description = _summarize( + action, resource_name, rsrc_collection_name) + path_dict[http_method] = OrderedDict([ + ('summary', summary), + ('description', description), + ('operationId', operation_id), + ]) + if parameters: + path_dict[http_method]['parameters'] = parameters + if request_body: + path_dict[http_method]['requestBody'] = request_body + path_dict[http_method]['responses'] = getattr( + self, responses_meth)(resource_name) + path_dict[http_method]['tags'] = [rsrc_collection_name] + + def _get_ref_response(self, description, ref, examples=None): + """Given a description string and a ref(erence) path to an existing + schema, return the dict describing that response. + """ + application_json = OrderedDict([ + ('schema', OrderedDict([ + ('$ref', ref), + ])), + ]) + if examples: + application_json['examples'] = examples + return OrderedDict([ + ('description', description), + ('content', OrderedDict([ + ('application/json', application_json), + ])), + ]) + + # ========================================================================= + # Schema name getters + # ========================================================================= + + def _get_read_schema_name(self, resource_name): + return '{}View'.format(resource_name.capitalize()) + + def _get_create_schema_name(self, resource_name): + return '{}Create'.format(resource_name.capitalize()) + + def _get_update_schema_name(self, resource_name): + return '{}Update'.format(resource_name.capitalize()) + + def _get_edit_schema_name(self, resource_name): + return 'EditA{}'.format(resource_name.capitalize()) + + def _get_new_schema_name(self, resource_name): + return 'New{}'.format(resource_name.capitalize()) + + def _get_paginated_schema_name(self, resource_name): + return 'PaginatedSubsetOf{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_search_schema_name(self, resource_name): + return 'SearchOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_query_schema_name(self, resource_name): + return 'SearchQueryOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_filter_schema_name(self, resource_name): + return 'FilterOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_object_filter_schema_name(self, resource_name): + return 'ObjectFilterOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_array_filter_schema_name(self, resource_name): + return 'ArrayFilterOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_new_search_schema_name(self, resource_name): + return 'DataForNewSearchOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_related_filter_schema_name(self, resource_name, attribute): + return 'FilterOver{}{}'.format( + self.inflp.plural(resource_name).capitalize(), + attribute.lower().capitalize()) + + def _get_coordinative_filter_schema_name(self, resource_name): + return 'CoordinativeFilterOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_negative_filter_schema_name(self, resource_name): + return 'NegativeFilterOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + def _get_simple_filter_schema_name(self, resource_name): + return 'SimpleFilterOver{}'.format( + self.inflp.plural(resource_name).capitalize()) + + # ========================================================================= + # Schema path getters + # ========================================================================= + + def _get_read_schema_path(self, resource_name): + return _schema_name2path(self._get_read_schema_name(resource_name)) + + def _get_create_schema_path(self, resource_name): + return _schema_name2path(self._get_create_schema_name(resource_name)) + + def _get_update_schema_path(self, resource_name): + return _schema_name2path(self._get_update_schema_name(resource_name)) + + def _get_edit_schema_path(self, resource_name): + return _schema_name2path(self._get_edit_schema_name(resource_name)) + + def _get_paginated_schema_path(self, resource_name): + return _schema_name2path(self._get_paginated_schema_name(resource_name)) + + def _get_new_schema_path(self, resource_name): + return _schema_name2path(self._get_new_schema_name(resource_name)) + + def _get_search_schema_path(self, resource_name): + return _schema_name2path(self._get_search_schema_name(resource_name)) + + def _get_query_schema_path(self, resource_name): + return _schema_name2path(self._get_query_schema_name(resource_name)) + + def _get_filter_schema_path(self, resource_name): + return _schema_name2path(self._get_filter_schema_name(resource_name)) + + def _get_object_filter_schema_path(self, resource_name): + return _schema_name2path(self._get_object_filter_schema_name(resource_name)) + + def _get_array_filter_schema_path(self, resource_name): + return _schema_name2path(self._get_array_filter_schema_name(resource_name)) + + def _get_coordinative_filter_schema_path(self, resource_name): + return _schema_name2path(self._get_coordinative_filter_schema_name( + resource_name)) + + def _get_negative_filter_schema_path(self, resource_name): + return _schema_name2path(self._get_negative_filter_schema_name( + resource_name)) + + def _get_simple_filter_schema_path(self, resource_name): + return _schema_name2path(self._get_simple_filter_schema_name( + resource_name)) + + def _get_related_filter_schema_path(self, resource_name, attribute): + return _schema_name2path(self._get_related_filter_schema_name( + resource_name, attribute)) + + def _get_related_filter_schema_paths_refs(self, resource_name, + resource_cfg): + return [ + {'$ref': + self._get_related_filter_schema_path(resource_name, attribute)} + for attribute, _ + in self._get_relational_attributes(resource_name, resource_cfg)] + + def _get_new_search_schema_path(self, resource_name): + return _schema_name2path(self._get_new_search_schema_name(resource_name)) + + def _get_error_schema_path(self): + return _schema_name2path(ERROR_SCHEMA_NAME) + + def _get_paginator_schema_path(self): + return _schema_name2path(PAGINATOR_SCHEMA_NAME) + + # ========================================================================= + # Response getters + # ========================================================================= + + def _get_create_responses(self, resource_name): + """Return an OpenAPI ``responses`` object for the "create" action on + resource ``resource_name``. + """ + return OrderedDict([ + ('200', self._get_ref_response( + description='Succeeded in creating a new {}.'.format( + resource_name), + ref=self._get_read_schema_path(resource_name))), + ('400', self._get_ref_response( + description='Bad request to create a new {}.'.format( + resource_name), + ref=self._get_error_schema_path())), + ]) + + def _get_delete_responses(self, resource_name): + return OrderedDict([ + ('200', self._get_ref_response( + description='Successful deletion of a {} resource.'.format( + resource_name), + ref=self._get_read_schema_path(resource_name))), + ('404', self._get_ref_response( + description='There is no {} resource with the specified key' + ' so it cannot be deleted.'.format(resource_name), + ref=self._get_error_schema_path())), + ('403', self._get_ref_response( + description='The user is forbidden from deleting this' + ' {} resource.'.format(resource_name), + ref=self._get_error_schema_path())), + ]) + + def _get_edit_responses(self, resource_name): + """Return an OpenAPI ``responses`` object for the "edit" action on + resource ``resource_name``. + """ + return OrderedDict([ + ('200', self._get_ref_response( + description='Succeeded in retrieving the data needed to' + ' edit a(n) {} resource.'.format(resource_name), + ref=self._get_edit_schema_path(resource_name))), + ('404', self._get_ref_response( + description='There is no {} resource with the specified key' + ' so it cannot be edited.'.format(resource_name), + ref=self._get_error_schema_path())), + ('403', self._get_ref_response( + description='The user is forbidden from editing this' + ' {} resource.'.format(resource_name), + ref=self._get_error_schema_path())), + ]) + + def _get_index_responses(self, resource_name): + """Return an OpenAPI ``responses`` object for the "index" action on + resource ``resource_name``. + """ + rsrc_collection_name = self.inflp.plural(resource_name) + return OrderedDict([ + ('200', self._get_ref_response( + description='Successful request to view all' + ' {}.'.format(rsrc_collection_name), + ref=self._get_paginated_schema_path(resource_name))), + ('400', self._get_ref_response( + description='Failed request to view all' + ' {}.'.format(rsrc_collection_name), + ref=self._get_error_schema_path())), + ]) + + def _get_new_responses(self, resource_name): + """Return an OpenAPI ``responses`` object for the "new" action on + resource ``resource_name``. + """ + return OrderedDict([ + ('200', self._get_ref_response( + description='Successful requested data needed to create a' + ' new {} resource.'.format(resource_name), + ref=self._get_new_schema_path(resource_name))), + ]) + + def _get_show_responses(self, resource_name): + """Return an OpenAPI ``responses`` object for the "show" action on + resource ``resource_name``. + """ + return OrderedDict([ + ('200', self._get_ref_response( + description='Successfully requested a(n)' + ' {} resource.'.format(resource_name), + ref=self._get_read_schema_path(resource_name))), + ('404', self._get_ref_response( + description='There is no {} resource with the specified' + ' key.'.format(resource_name), + ref=self._get_error_schema_path())), + ('403', self._get_ref_response( + description='The user is forbidden from viewing this' + ' {} resource.'.format(resource_name), + ref=self._get_error_schema_path())), + ]) + + def _get_update_responses(self, resource_name): + """Return an OpenAPI ``responses`` object for the "update" action on + resource ``resource_name``. + """ + return OrderedDict([ + ('200', self._get_ref_response( + description='Succeeded in updating an existing {}' + ' resource.'.format(resource_name), + ref=self._get_edit_schema_path(resource_name))), + ('404', self._get_ref_response( + description='There is no {} resource with the specified key' + ' so it cannot be updated.'.format(resource_name), + ref=self._get_error_schema_path())), + ('403', self._get_ref_response( + description='The user is forbidden from updating this' + ' {} resource.'.format(resource_name), + ref=self._get_error_schema_path())), + ('400', self._get_ref_response( + description='Bad request to update an existing {}.'.format( + resource_name), + ref=self._get_error_schema_path())), + ]) + + def _get_search_responses(self, resource_name): + """Return an OpenAPI ``responses`` object for the "search" action on + resource ``resource_name``. + """ + rsrc_collection_name = self.inflp.plural(resource_name) + return OrderedDict([ + ('200', self._get_ref_response( + description='Successful request to search across all' + ' {}.'.format(rsrc_collection_name), + ref=self._get_paginated_schema_path(resource_name))), + ('400', self._get_ref_response( + description='Failed request to search across all' + ' {}.'.format(rsrc_collection_name), + ref=self._get_error_schema_path())), + ]) + + def _get_search_post_responses(self, resource_name): + return self._get_search_responses(resource_name) + + def _get_new_search_responses(self, resource_name): + rsrc_collection_name = self.inflp.plural(resource_name) + return OrderedDict([ + ('200', self._get_ref_response( + description='Successful request to get the data needed to' + ' search across all {}.'.format( + rsrc_collection_name), + ref=self._get_new_search_schema_path(resource_name))), + ]) + + # ========================================================================= + # Request body getters + # ========================================================================= + + def _get_create_request_body(self, resource_name): + return OrderedDict([ + ('description', 'JSON object required to create a new {}'.format( + resource_name)), + ('required', True), + ('content', { + 'application/json': { + 'schema': { + '$ref': + self._get_create_schema_path(resource_name) + } + } + }), + ]) + + def _get_update_request_body(self, resource_name): + return OrderedDict([ + ('description', 'JSON object required to update an existing' + ' {}'.format(resource_name)), + ('required', True), + ('content', { + 'application/json': { + 'schema': { + '$ref': + self._get_update_schema_path(resource_name) + } + } + }), + ]) + + def _get_search_request_body(self, resource_name, examples=None): + rsrc_collection_name = self.inflp.plural(resource_name) + application_json = OrderedDict([ + ('schema', {'$ref': + self._get_search_schema_path(resource_name)}), + ]) + if examples: + application_json['example'] = examples + return OrderedDict([ + ('description', 'JSON object required to search over all {}'.format( + rsrc_collection_name)), + ('required', True), + ('content', {'application/json': application_json}), + ]) + + def _get_search_post_request_body(self, resource_name, examples=None): + return self._get_search_request_body(resource_name, examples=examples) + + def _get_index_parameters(self, *args): + """Return an OpenAPI ``parameters`` object for the "index" action.""" + return [ + {'$ref': '#/components/parameters/items_per_page'}, + {'$ref': '#/components/parameters/page'}, + {'$ref': '#/components/parameters/order_by_attribute'}, + {'$ref': '#/components/parameters/order_by_subattribute'}, + {'$ref': '#/components/parameters/order_by_direction'}, + ] + + def to_yaml(self, open_api_spec): + """Return the input OrderedDict as a YAML string.""" + return yaml_dump(open_api_spec) + + def to_json(self, open_api_spec): + """Return the input OrderedDict as a JSON string.""" + return json.dumps(open_api_spec) + + def _get_api_info(self): + """Return an OrderedDict for the top-level ``info`` attribute.""" + return OrderedDict([ + ('version', self.api_version), + ('title', self._get_api_title()), + ('description', self._get_api_description()), + ]) + + def _get_dflt_server_path(self): + return '{}{}'.format( + self.path_prefix, self.get_api_version_slug()) + + def _get_api_servers(self): + """Return a list of OrderedDicts for the top-level ``servers`` + attribute. + """ + return [ + OrderedDict([ + ('url', self._get_dflt_server_path()), + ('description', self._get_dflt_server_description()), + ]), + ] + + def _get_api_security(self): + """Return a list of OrderedDicts for the top-level ``security`` + attribute. + """ + return [ + OrderedDict([('ApiKeyAuth', [])]), + ] + + def _get_api_security_schemes(self): + """Return an OrderedDict for the ``components.securitySchemes`` + attribute. + """ + return OrderedDict([ + ('ApiKeyAuth', OrderedDict([ + ('type', 'apiKey'), + ('in', 'header'), + ('name', 'Authorization'), + # Note: the value of this header must be of the form + # ``ApiKey :``. + ('pattern', r'ApiKey (?\w+):(?\w+)') + ])), + ]) + + def _get_api_remple_parameters(self): + """Return an OrderedDict of OrderedDicts for OpenAPI + ``components.parameters``; these are for the Remple-internal + internal schemata like the paginator. + """ + parameters = OrderedDict() + for schema in schemata: + for parameter in schema.extract_parameters(): + parameter_name = parameter['name'] + parameters[parameter_name] = parameter + for parameter_name, parameter in self._get_order_by_parameters().items(): + parameters[parameter_name] = parameter + return parameters + + def _get_order_by_parameters(self): + """Return a dict of OpenAPI query ``parameters`` for ordering the + results of an idnex request. + """ + return { + 'order_by_attribute': OrderedDict([ + ('in', 'query'), + ('name', 'order_by_attribute'), + ('schema', {'type': 'string'}), + ('description', 'Attribute of the resource that' + ' view results should be ordered by.'), + ('required', False), + ]), + 'order_by_subattribute': OrderedDict([ + ('in', 'query'), + ('name', 'order_by_subattribute'), + ('schema', {'type': 'string'}), + ('required', False), + ('description', 'Attribute of the related attribute' + ' order_by_attribute of the resource' + ' that view results should be' + ' ordered by.'), + ]), + 'order_by_direction': OrderedDict([ + ('in', 'query'), + ('name', 'order_by_direction'), + ('schema', OrderedDict([ + ('type', 'string'), + ('enum', [obd for obd in QueryBuilder.order_by_directions + if obd]), + ])), + ('required', False), + ('description', 'The direction of the ordering; omitting this' + ' parameter means ascending direction.'), + ]) + } + + def _get_error_schema(self): + return OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('error', OrderedDict([ + ('type', 'string'), + ])), + ])), + ('required', ['error']), + ]) + + def _get_schemas(self): + """Return an OpenAPI ``schemas`` OrderedDict. + + It contains a "View", a "Create", and an "Update" schema for each + resource in ``self.resources``, e.g., ``LocationView``, + ``LocationCreate``, and ``LocationUpdate``. The create and update + schemata are only included if the resources is not read-only. The view + schema is constructed by introspecting the Django model, while the + create and update schemata are constructed by introspecting the relevant + Formencode schemata attached as class attributes on the resource class. + """ + schemas = OrderedDict([ + ('ErrorSchema', self._get_error_schema()), + ('PaginatorSchema', self._get_paginator_schema()), + ]) + for resource_name, resource_cfg in sorted(self.resources.items()): + read_schema_name, read_schema = self._get_read_schema( + resource_name, resource_cfg) + schemas[read_schema_name] = read_schema + paginated_schema_name, paginated_schema = ( + self._get_paginated_subset_schema( + resource_name, resource_cfg, read_schema)) + schemas[paginated_schema_name] = paginated_schema + # Only mutable resources need the following schemata + if issubclass(resource_cfg['resource_cls'], Resources): + create_schema_name, create_schema = self._get_create_schema( + resource_name, resource_cfg, read_schema) + schemas[create_schema_name] = create_schema + update_schema_name, update_schema = self._get_update_schema( + resource_name, resource_cfg, read_schema) + schemas[update_schema_name] = update_schema + new_schema_name, new_schema = ( + self._get_new_schema(resource_name, resource_cfg, + read_schema)) + schemas[new_schema_name] = new_schema + edit_schema_name, edit_schema = ( + self._get_edit_schema(resource_name, resource_cfg, + read_schema)) + schemas[edit_schema_name] = edit_schema + if resource_cfg.get('searchable', True): + filter_schemas = ( + self._get_filter_schemas(resource_name, resource_cfg, + read_schema)) + for filter_schema_name, filter_schema in filter_schemas: + schemas[filter_schema_name] = filter_schema + query_schema_name, query_schema = ( + self._get_query_schema(resource_name, resource_cfg, + read_schema)) + schemas[query_schema_name] = query_schema + + search_schema_name, search_schema = ( + self._get_search_schema(resource_name, resource_cfg, + read_schema)) + schemas[search_schema_name] = search_schema + new_search_schema_name, new_search_schema = ( + self._get_new_search_schema(resource_name, resource_cfg, + read_schema)) + schemas[new_search_schema_name] = new_search_schema + return schemas + + def _get_paginator_schema(self): + return OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('count', {'type': 'integer'}), + ('page', {'type': 'integer', 'default': 1, 'minimum': 1}), + ('items_per_page', {'type': 'integer', 'default': 10, 'minimum': 1}), + ])), + ('required', ['page', 'items_per_page']), + ]) + + def _get_paginated_subset_schema(self, resource_name, resource_cfg, + read_schema): + paginated_schema_name = self._get_paginated_schema_name(resource_name) + paginated_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('paginator', {'$ref': self._get_paginator_schema_path()}), + ('items', OrderedDict([ + ('type', 'array'), + ('items', + {'$ref': + self._get_read_schema_path(resource_name)}), + ])), + ])), + ('required', ['paginator', 'items']), + ]) + return paginated_schema_name, paginated_schema + + def _get_edit_schema(self, resource_name, resource_cfg, read_schema): + edit_schema_name = self._get_edit_schema_name(resource_name) + edit_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('data', { + '$ref': + self._get_new_schema_path(resource_name)}), + ('resource', { + '$ref': + self._get_read_schema_path(resource_name)}), + ])), + ('required', ['data', 'resource']), + ]) + return edit_schema_name, edit_schema + + def _get_filter_schemas(self, resource_name, resource_cfg, read_schema): + """Each resource will generate multiple filter schemas: a coordinative + one, a negative one, a simple one and zero or more related (relational) + ones, depending on how many other resources (models) it is related to. + This method returns these schemas as a list. + + Note, there is a shorthand filter schema, which is based on arrays and + which is exemplified via the following (and which canNOT be described + using the OpenAPI spec):: + + ["and", [["Location", "purpose", "=", "AS"], + ["Location", "description", "regex", "2018"]]] + ["not", ["Location", "purpose", "=", "AS"]] + ["Location", "purpose", "=", "AS"] + ["Location", "space", "path", "like", "/usr/data/%"] + + Then there is the long-hand filter schema, which is based on objects + and which is exemplified via the following (which CAN be described + using the OpenAPI spec):: + + {"conjunction": "and", + "complement": [ + {"attribute": "purpose", + "relation": "=", + "value": "AS"}, + {"attribute": "description", + "relation": "regex", + "value": "2018"}]} + + {"negation": "not", + "complement": {"attribute": "purpose", + "relation": "=", + "value": "AS"}} + + {"attribute": "purpose", "relation": "=", "value": "AS"} + + {"attribute": "space", + "subattribute": "path", + "relation": "like", + "value": "/usr/data/%"} + + Note that the filter schema is inherently recursive and the Swagger-ui + web app cannot currently fully display a recursive schema. See + https://github.com/swagger-api/swagger-ui/issues/1679. + """ + return ( + [self._get_simple_filter_schema(resource_name, resource_cfg)] + + self._get_related_filter_schemas(resource_name, resource_cfg) + + [self._get_coordinative_filter_schema(resource_name, resource_cfg), + self._get_negative_filter_schema(resource_name, resource_cfg), + self._get_array_filter_schema(resource_name, resource_cfg), + self._get_object_filter_schema(resource_name, resource_cfg), + self._get_filter_schema(resource_name, resource_cfg)]) + + def _get_simple_filter_schema(self, resource_name, resource_cfg): + model_name = resource_cfg['resource_cls'].model_cls.__name__ + simple_schema_name = self._get_simple_filter_schema_name(resource_name) + simple_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('attribute', OrderedDict([ + ('type', 'string'), + ('enum', self._get_simple_attributes( + model_name, resource_cfg)), + ])), + ('relation', OrderedDict([ + ('type', 'string'), + ('enum', self._get_relations(resource_name, resource_cfg)), + ])), + ('value', {'anyOf': [{'type': 'string'}, + {'type': 'number'}, + {'type': 'boolean'},]}), + ])), + ]) + return simple_schema_name, simple_schema + + def _get_query_schemata(self, resource_cfg): + resource_cls = resource_cfg['resource_cls'] + query_builder = resource_cls._get_query_builder() + return query_builder.schemata + + def _get_relational_attributes(self, resource_name, resource_cfg): + resource_cls = resource_cfg['resource_cls'] + model_name = resource_cfg['resource_cls'].model_cls.__name__ + resource_cls_name = resource_cls.__name__ + query_schemata = self._get_query_schemata(resource_cfg) + return [(attr, cfg.get('foreign_model')) + for attr, cfg in + query_schemata[model_name].items() + if cfg.get('foreign_model')] + + def _get_simple_attributes(self, model_cls_name, resource_cfg): + resource_cls = resource_cfg['resource_cls'] + resource_cls_name = resource_cls.__name__ + query_schemata = self._get_query_schemata(resource_cfg) + return [attr for attr, cfg in + query_schemata[model_cls_name].items() + if not cfg.get('foreign_model')] + + def _get_related_attributes(self, resource_cfg, related_model_name): + query_schemata = self._get_query_schemata(resource_cfg) + return [attr for attr, cfg in query_schemata[related_model_name].items() + if not cfg.get('foreign_model')] + + def _get_relations(self, resource_name, resource_cfg): + resource_cls = resource_cfg['resource_cls'] + query_builder = resource_cls._get_query_builder() + return list(query_builder.relations) + + def _get_related_filter_schemas(self, resource_name, resource_cfg): + schemas = [] + for attribute, related_model_name in self._get_relational_attributes( + resource_name, resource_cfg): + related_schema_name = self._get_related_filter_schema_name( + resource_name, attribute) + related_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('attribute', OrderedDict([ + ('type', 'string'), + ('enum', [attribute]), + ])), + ('subattribute', OrderedDict([ + ('type', 'string'), + ('enum', self._get_related_attributes( + resource_cfg, related_model_name)), + ])), + ('relation', OrderedDict([ + ('type', 'string'), + ('enum', self._get_relations(resource_name, resource_cfg)), + ])), + ('value', {'anyOf': [{'type': 'string'}, + {'type': 'number'}, + {'type': 'boolean'},]}), + ])), + ]) + schemas.append((related_schema_name, related_schema)) + return schemas + + def _get_coordinative_filter_schema(self, resource_name, resource_cfg): + coord_schema_name = self._get_coordinative_filter_schema_name(resource_name) + coord_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('conjunction', OrderedDict([ + ('type', 'string'), + ('enum', ['and', 'or']), + ])), + ('complement', OrderedDict([ + ('type', 'array'), + ('items', {'$ref': + self._get_filter_schema_path(resource_name)}), + ])), + ])), + ]) + return coord_schema_name, coord_schema + + def _get_negative_filter_schema(self, resource_name, resource_cfg): + neg_schema_name = self._get_negative_filter_schema_name(resource_name) + neg_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('negation', OrderedDict([ + ('type', 'string'), + ('enum', ['not']), + ])), + ('complement', + {'$ref': self._get_filter_schema_path(resource_name)}), + ])), + ]) + return neg_schema_name, neg_schema + + def _get_filter_schema(self, resource_name, resource_cfg): + filter_schema_name = self._get_filter_schema_name(resource_name) + filter_schema = { + 'oneOf': [ + {'$ref': self._get_object_filter_schema_path(resource_name)}, + {'$ref': self._get_array_filter_schema_path(resource_name)}, + ] + } + return filter_schema_name, filter_schema + + def _get_array_filter_schema(self, resource_name, resource_cfg): + array_filter_schema_name = self._get_array_filter_schema_name(resource_name) + array_filter_schema = OrderedDict([ + ('type', 'array'), + ('items', {'type': {}}) + ]) + return array_filter_schema_name, array_filter_schema + + def _get_object_filter_schema(self, resource_name, resource_cfg): + object_filter_schema_name = self._get_object_filter_schema_name( + resource_name) + object_filter_schema = { + 'oneOf': [ + {'$ref': self._get_coordinative_filter_schema_path( + resource_name)}, + {'$ref': self._get_negative_filter_schema_path(resource_name)}, + {'$ref': self._get_simple_filter_schema_path(resource_name)}, + ] + self._get_related_filter_schema_paths_refs(resource_name, + resource_cfg) + } + return object_filter_schema_name, object_filter_schema + + def _get_query_schema(self, resource_name, resource_cfg, read_schema): + query_schema_name = self._get_query_schema_name(resource_name) + query_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('filter', {'$ref': + self._get_filter_schema_path(resource_name)}), + ('order_by', OrderedDict([ + ('type', 'array'), + ('items', OrderedDict([ + ('type', 'array'), + ('items', OrderedDict([ + ('type', 'string'), + ])), + ])), + ])), + ])), + ('required', ['filter']), + ]) + return query_schema_name, query_schema + + def _get_search_schema(self, resource_name, resource_cfg, read_schema): + search_schema_name = self._get_search_schema_name(resource_name) + search_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('query', {'$ref': self._get_query_schema_path(resource_name)}), + ('paginator', {'$ref': self._get_paginator_schema_path()}), + ])), + ('required', ['query']), + ]) + return search_schema_name, search_schema + + def _get_new_search_schema(self, resource_name, resource_cfg, read_schema): + new_search_schema_name = self._get_new_search_schema_name(resource_name) + new_search_schema = OrderedDict([ + ('type', 'object'), + ('properties', OrderedDict([ + ('search_parameters', OrderedDict([ + # TODO: this is not a string, but an object with + # "attributes" and "relations". + ('type', 'string'), + ])), + ])), + ('required', ['search_parameters']), + ]) + return new_search_schema_name, new_search_schema + + def _get_new_schema(self, resource_name, resource_cfg, read_schema): + new_schema_name = self._get_new_schema_name(resource_name) + new_schema = OrderedDict([('type', 'object'),]) + properties = OrderedDict() + resource_cls = resource_cfg['resource_cls'] + required_fields = [] + for field_name in resource_cls._get_new_edit_collections(): + properties[field_name] = OrderedDict([ + ('type', 'array'), + ('items', OrderedDict([ + ('type', 'string'), + ('format', 'uuid of an instance of the {} resource'.format( + field_name)), + ])), + ]) + required_fields.append(field_name) + new_schema['properties'] = properties + new_schema['required'] = required_fields + return new_schema_name, new_schema + + def _get_create_schema(self, resource_name, resource_cfg, read_schema): + """Return a create schema for the resource named ``resource_name``. + + The create schema describes what is needed to create an instance of the + input-named resource. + """ + create_schema_name = self._get_create_schema_name(resource_name) + resource_cls = resource_cfg['resource_cls'] + schema_cls = resource_cls.get_create_schema_cls() + create_schema = self._get_create_update_schema( + resource_name, resource_cfg, read_schema, schema_cls) + return create_schema_name, create_schema + + def _get_update_schema(self, resource_name, resource_cfg, read_schema): + """Return an update schema for the resource named ``resource_name``. + + The update schema describes what is needed to update an instance of the + input-named resource. + + TODO: should every parameter in an update request be optional, given + that the resource being updated is presumably valid? + + """ + update_schema_name = self._get_update_schema_name(resource_name) + resource_cls = resource_cfg['resource_cls'] + schema_cls = resource_cls.get_update_schema_cls() + update_schema = self._get_create_update_schema( + resource_name, resource_cfg, read_schema, schema_cls) + return update_schema_name, update_schema + + @staticmethod + def _get_create_update_schema(resource_name, resource_cfg, + read_schema, schema_cls): + schema_properties = {} + schema = {'type': 'object'} + resource_cls = resource_cfg['resource_cls'] + model_cls = resource_cls.model_cls + fields = schema_cls.fields + required_fields = [] + for field_name, field in fields.items(): + field_cls_name = field.__class__.__name__ + field_dict = { + 'ValidModelObject': single_reference_mut_field_dict, + 'ForEach': multi_reference_mut_field_dict, + 'OneOf': enum_field_dict, + 'Any': disjunctive_field_dict, + }.get(field_cls_name, scalar_mut_field_dict)( + **{'field_cls_name': field_cls_name, 'field': field}) + if not field_dict.get('type') and not field_dict.get('anyOf'): + print('WARNING: {}.{} is of unknown type (class {})'.format( + resource_name, field_name, field_cls_name)) + continue + default = read_schema['properties'].get(field_name, {}).get( + 'default', NOT_PROVIDED) + if default != NOT_PROVIDED: + field_dict['default'] = default + if field_name in read_schema.get('required', []): + required_fields.append(field_name) + description = read_schema['properties'].get(field_name, {}).get( + 'description') + if description: + field_dict['description'] = description + schema_properties[field_name] = field_dict + schema['properties'] = schema_properties + if required_fields: + schema['required'] = required_fields + return schema + + def _get_read_schema(self, resource_name, resource_cfg): + """Return a read schema for the resource named ``resource_name``. + + The read schema describes what is returned by the server as a + representation of an instance of the input-named resource. + """ + read_schema_name = self._get_read_schema_name(resource_name) + read_schema_properties = {} + read_schema = {'type': 'object'} + resource_cls = resource_cfg['resource_cls'] + model_cls = resource_cls.model_cls + fields = model_cls._meta.get_fields() + required_fields = [] + for field in fields: + field_name = field.name + field_cls_name = field.__class__.__name__ + field_name, field_dict = { + 'ForeignKey': single_reference_field_dict, + 'OneToOneRel': single_reference_field_dict, + 'ManyToManyField': multi_reference_field_dict, + 'ManyToManyRel': multi_reference_field_dict, + 'ManyToOneRel': multi_reference_field_dict, + }.get(field_cls_name, scalar_field_dict)( + **{'field_name': field_name, 'field_cls_name': field_cls_name, + 'field': field}) + choices = get_choices(field) + if choices: + field_dict['enum'] = choices + if getattr(field, 'null', False) is True: + field_dict['nullable'] = True + default = get_default(field) + if default != NOT_PROVIDED: + field_dict['default'] = default + required = get_required(field, default, field_cls_name) + if required: + required_fields.append(field_name) + if not field_dict.get('type'): + print('WARNING: {}.{} is of unknown type (class {})'.format( + resource_name, field.name, field_cls_name)) + continue + description = getattr(field, 'help_text', None) + if description: + if callable(description): + description = description() + field_dict['description'] = str(description) + read_schema_properties[field_name] = field_dict + read_schema['properties'] = read_schema_properties + if required_fields: + read_schema['required'] = required_fields + return read_schema_name, read_schema + + def get_api_version_slug(self): + return self._get_api_version_slug(self.api_version) + + @staticmethod + def _get_api_version_slug(version): + """Given a version number like 'X.Y.Z', return a slug representation of + it. E.g., + + >>> get_api_version_slug('3.0.0') + ... 'v3' + >>> get_api_version_slug('3.0.1') + ... 'v3_0_1' + >>> get_api_version_slug('3.0') + ... 'v3' + >>> get_api_version_slug('3.9') + ... 'v3_9' + """ + parts = version.strip().split('.') + new_parts = [] + for index, part in enumerate(parts): + part_int = int(part) + if part_int: + new_parts.append(part) + else: + parts_to_right = parts[index + 1:] + non_empty_ptr = [p for p in parts_to_right if int(p)] + if non_empty_ptr: + new_parts.append(part) + return 'v{}'.format('_'.join(new_parts)) + + +def single_reference_field_dict(**kwargs): + """Return an OpenAPI OrderedDict for a Django ForeingKey or OneToOneRel. + """ + if kwargs.get('field_cls_name') == 'OneToOneRel': + return (kwargs['field'].get_accessor_name(), + OrderedDict([('type', 'string'), ('format', 'uri')])) + return (kwargs['field_name'], + OrderedDict([('type', 'string'), ('format', 'uri')])) + + +def multi_reference_field_dict(**kwargs): + """Return an OpenAPI OrderedDict for a Django ManyToManyField, + ManyToManyRel, or ManyToOneRel. + """ + field_name = kwargs.get('field_name') + if kwargs.get('field_cls_name') == 'ManyToOneRel': + field_name = kwargs['field'].get_accessor_name() + return field_name, OrderedDict([ + ('type', 'array'), + ('items', OrderedDict([ + ('type', 'string'), ('format', 'uri')]))]) + + +def scalar_field_dict(**kwargs): + """Return an OpenAPI OrderedDict for a Django scalar field, e.g., a string + or an int. + """ + field_cls_name = kwargs.get('field_cls_name') + openapi_type = django_field_class2openapi_type.get( + field_cls_name) + field_dict = OrderedDict([('type', openapi_type)]) + openapi_format = django_field_class2openapi_format.get( + field_cls_name) + if openapi_format: + field_dict['format'] = openapi_format + return kwargs['field_name'], field_dict + + +def get_choices(field): + choices = getattr(field, 'choices', None) + if choices: + return [c[0] for c in choices] + return None + + +def get_default(field): + return getattr(field, 'default', NOT_PROVIDED) + + +def get_required(field, default, field_cls_name): + if field_cls_name.endswith('Rel'): + return False + field_blank = getattr(field, 'blank', False) + if (not field_blank) and (default == NOT_PROVIDED): + return True + return False + + +def _get_format_from_valid_model_validator(valid_model_validator): + resource_name = valid_model_validator.model_cls.__name__.lower() + pk_attr = getattr(valid_model_validator, 'pk', 'uuid') + return '{} of a {} resource'.format(pk_attr, resource_name) + + +def single_reference_mut_field_dict(**kwargs): + format_ = _get_format_from_valid_model_validator(kwargs['field']) + return OrderedDict([('type', 'string'), + ('format', format_)]) + + +def multi_reference_mut_field_dict(**kwargs): + format_ = _get_format_from_valid_model_validator( + kwargs['field'].validators[0]) + return OrderedDict([ + ('type', 'array'), + ('items', OrderedDict([ + ('type', 'string'), + ('format', format_), + ])), + ]) + + +def enum_field_dict(**kwargs): + enum = list(kwargs['field'].list) + enum_type = type(enum[0]) + return OrderedDict([ + ('type', python_type2openapi_type.get(enum_type, 'string')), + ('enum', enum)]) + + +def disjunctive_field_dict(**kwargs): + anyOf = [] + field_dict = OrderedDict([('anyOf', anyOf)]) + for validator in kwargs['field'].validators: + validator_dict = OrderedDict() + validator_cls = validator.__class__.__name__ + validator_dict['type'] = formencode_field_class2openapi_type.get( + validator_cls, 'string') + validator_format = ( + formencode_field_class2openapi_format.get( + validator_cls)) + if validator_format: + validator_dict['format'] = validator_format + anyOf.append(validator_dict) + return field_dict + + +def scalar_mut_field_dict(**kwargs): + openapi_type = formencode_field_class2openapi_type.get( + kwargs['field_cls_name']) + field_dict = OrderedDict([('type', openapi_type)]) + openapi_format = formencode_field_class2openapi_format.get( + kwargs['field_cls_name']) + if openapi_format: + field_dict['format'] = openapi_format + field = kwargs['field'] + field_min = getattr(field, 'min', None) + field_max = getattr(field, 'max', None) + if field_min: + field_dict['minLength'] = field_min + if field_max: + field_dict['maxLength'] = field_max + return field_dict + + +def get_collection_targeting_openapi_path(rsrc_collection_name, + modifiers=None): + """Return an OpenAPI path of the form '//' + with optional trailing modifiers, e.g., '//new/'. + """ + if modifiers: + return r'/{rsrc_collection_name}/{modifiers}/'.format( + rsrc_collection_name=rsrc_collection_name, + modifiers='/'.join(modifiers)) + return r'/{rsrc_collection_name}/'.format( + rsrc_collection_name=rsrc_collection_name) + + +def get_member_targeting_openapi_path(resource_name, rsrc_collection_name, + pk_patt, modifiers=None): + """Return a regex of the form '^//$' + with optional modifiers after the pk, e.g., + '^//edit/$'. + """ + path_params = [OrderedDict([ + ('in', 'path'), + ('name', 'pk'), + ('required', True), + ('schema', OrderedDict([ + ('type', 'string'), + ('format', 'uuid'), + ])), + ('description', 'The primary key of the {}.'.format(resource_name)), + ])] + if modifiers: + path = (r'/{rsrc_collection_name}/{{pk}}/' + r'{modifiers}/'.format( + rsrc_collection_name=rsrc_collection_name, + pk_patt=pk_patt, + modifiers='/'.join(modifiers))) + else: + path = r'/{rsrc_collection_name}/{{pk}}/'.format( + rsrc_collection_name=rsrc_collection_name, pk_patt=pk_patt) + return path, path_params + +def _summarize(action, resource_name, rsrc_collection_name): + return { + 'create': ( + 'Create a new {}.'.format(resource_name), + 'Create a new {}.'.format(resource_name), + ), + 'delete': ( + 'Delete an existing {}.'.format(resource_name), + 'Delete an existing {}.'.format(resource_name), + ), + 'edit': ( + 'Get the data needed to update an existing {}.'.format(resource_name), + 'Get the data needed to update an existing {}.'.format(resource_name), + ), + 'index': ( + 'View all {}.'.format(rsrc_collection_name), + 'View all {}.'.format(rsrc_collection_name), + ), + 'new': ( + 'Get the data needed to create a new {}.'.format(resource_name), + 'Get the data needed to create a new {}.'.format(resource_name), + ), + 'show': ( + 'View an existing {}.'.format(resource_name), + 'View an existing {}.'.format(resource_name), + ), + 'update': ( + 'Update an existing {}.'.format(resource_name), + 'Update an existing {}.'.format(resource_name), + ), + 'search': ( + 'Search over all {}.'.format(rsrc_collection_name), + 'Search over all {}.'.format(rsrc_collection_name), + ), + 'search_post': ( + 'Search over all {}.'.format(rsrc_collection_name), + 'Search over all {}.'.format(rsrc_collection_name), + ), + 'new_search': ( + 'Get the data needed to search over all {}.'.format( + rsrc_collection_name), + 'Get the data needed to search over all {}.'.format( + rsrc_collection_name), + ), + }[action] + + +def _schema_name2path(schema_name): + return '{}{}'.format(SCHEMAS_ABS_PATH, schema_name) + + +def _get_operation_id(action, resource_name): + operation_name = { + 'index': 'get_many', + 'show': 'get', + 'new': 'data_for_new', + 'edit': 'data_for_edit', + 'search_post': 'search', + }.get(action, action) + return '{}.{}'.format(operation_name, resource_name) diff --git a/storage_service/locations/api/v3/remple/querybuilder.py b/storage_service/locations/api/v3/remple/querybuilder.py new file mode 100644 index 000000000..4e1503ef3 --- /dev/null +++ b/storage_service/locations/api/v3/remple/querybuilder.py @@ -0,0 +1,672 @@ +"""This module defines :class:`QueryBuilder`. A ``QueryBuilder`` instance is +used to build a query object from simple data structure, viz. nested lists. + +The primary public method is ``get_query_set``. It takes a dict with +``'filter'`` and ``'order_by'`` keys. The filter key evaluates to a filter +expression represented as a Python list (or JSON array). The ``get_query_set`` +method returns a Django QuerySet instance. Errors in the Python filter +expression will cause custom ``SearchParseError``s to be raised. + +The searchable models and their attributes (scalars & collections) are defined +in QueryBuilder.schema. + +Simple filter expressions are lists with four or five items. Complex filter +expressions are constructed via lists whose first element is one of the boolean +keywords 'and', 'or', 'not' and whose second element is a filter expression or +a list thereof (in the case of 'and' and 'or'). The examples below show a +filter expression accepted by ``QueryBuilder('Package').get_query_expression`` +on the first line followed by the equivalent Django Q expression. Note that +the target model of the QueryBuilder defaults to 'Package' so all queries will +target the Package model by default. + +1. Queries on scalar attributes:: + + >>> ['Package', 'description', 'like', '%a%'] + >>> Q(description__contains='%a%') + +2. Queries on scalar attributes of related resources:: + + >>> ['Package', 'origin_pipeline', 'description', 'regex', '^[JS]'] + >>> Q(origin_pipeline__description__regex='^[JS]') + +3. Queries based on the presence/absence of a related resource:: + + >>> ['Package', 'origin_pipeline', '=', None] + >>> Q(origin_pipeline__isnull==True) + >>> ['Package', 'origin_pipeline', '!=', None] + >>> Q(origin_pipeline__isnull==False) + +4. Queries over scalar attributes of related collections of resources:: + + >>> ['Package', 'replicas', 'uuid', 'in', [uuid1, uuid2]] + >>> Q(replicas__uuid__in=[uuid1, uuid2]))) + +5. Negation:: + + >>> ['not', ['Package', 'description', 'like', '%a%']] + >>> ~Q(description__contains='%a%') + +6. Conjunction:: + + >>> ['and', [['Package', 'description', 'like', '%a%'], + >>> ['Package', 'origin_pipeline', 'description', '=', + >>> 'Well described.']]] + >>> (Q(description__contains='%a%') & + >>> Q(origin_pipeline__description='Well described.')) + +7. Disjunction:: + + >>> ['or', [['Package', 'description', 'like', '%a%'], + >>> ['Package', 'origin_pipeline', 'description', '=', + >>> 'Well described.']]] + >>> (Q(description__contains='%a%') | + >>> Q(origin_pipeline__description='Well described.')) + +8. Complex hierarchy of filters:: + + >>> ['and', [['Package', 'description', 'like', '%a%'], + >>> ['not', ['Package', 'description', 'like', 'T%']], + >>> ['or', [['Package', 'size', '<', 1000], + >>> ['Package', 'size', '>', 512]]]]] + >>> (Q(description__contains='%a%') & + >>> ~Q(description__contains='T%') & + >>> (Q(size__lt=1000) | Q(size__gt=512))) +""" + +from __future__ import absolute_import + +import functools +import logging +import operator +import pprint + +from django.db.models import Q + +from . import utils + + +logger = logging.getLogger(__name__) + + +class SearchParseError(Exception): + + def __init__(self, errors): + self.errors = errors + super(SearchParseError, self).__init__() + + def __repr__(self): + return '; '.join(['%s: %s' % (k, self.errors[k]) for k in self.errors]) + + def __str__(self): + return self.__repr__() + + def unpack_errors(self): + return self.errors + + +class QueryBuilder(object): + """Generate a query object from a Python dictionary. + + Builds Django ORM queries from Python data structures representing + arbitrarily complex filter expressions. Example usage:: + + query_builder = QueryBuilder(model_name='Package') + python_query = {'filter': [ + 'and', [ + ['Package', 'description', 'like', '%a%'], + ['not', ['Package', 'description', 'regex', '^[Tt]he']], + ['or', [ + ['Package', 'size', '<', 1000], + ['Package', 'size', '>', 512]]]]]} + query_set = query_builder.get_query_set(python_query) + """ + + def __init__(self, model_cls, primary_key='uuid'): + self.errors = {} + self.model_cls = model_cls + self.model_name = self.model_cls.__name__ + self.primary_key = primary_key + self._schemata = None + + def get_query_set(self, query_as_dict): + """Given a dict, return a Django ORM query set.""" + self.clear_errors() + query_expression = self._get_query_expression(query_as_dict.get('filter')) + order_bys = self._get_order_bys( + query_as_dict.get('order_by'), self.primary_key) + self._raise_search_parse_error_if_necessary() + query_set = self._get_model_manager() + query_set = query_set.filter(query_expression) + query_set = query_set.order_by(*order_bys) + return query_set + + def get_query_expression(self, query_data_structure): + """The public method clears the errors and then calls the corresponding + private method. This prevents interference from errors generated by + previous order_by calls. + """ + self.clear_errors() + return self._get_query_expression(query_data_structure) + + def _get_query_expression(self, query_data_structure): + """Return the filter expression generable by the input Python + data structure or raise an SearchParseError if the data structure is + invalid. + """ + if isinstance(query_data_structure, list): + return self._pythonlist2queryexpr(query_data_structure) + return self._pythondict2queryexpr(query_data_structure) + + def get_order_bys(self, inp_order_bys, primary_key='uuid'): + """The public method clears the errors and then calls the private method. + This prevents interference from errors generated by previous order_by calls. + """ + self.clear_errors() + return self._get_order_bys(inp_order_bys, primary_key) + + def _get_order_bys(self, inp_order_bys, primary_key='uuid'): + """Input is a list of lists of the form [], [, + ] or [, , ]; + output is a list of strings that is an acceptable argument to the + Django ORM's ``order_by`` method. + """ + default_order_bys = [primary_key] + if inp_order_bys is None: + return default_order_bys + order_bys = [] + related_attribute_name = None + for inp_order_by in inp_order_bys: + if not isinstance(inp_order_by, list): + self._add_to_errors( + str(inp_order_by), 'Order by elements must be lists') + continue + if len(inp_order_by) == 1: + related_attribute_name, attribute_name, direction = ( + None, inp_order_by[0], '') + if attribute_name and ( + attribute_name in self.order_by_directions): + attribute_name = primary_key + direction = direction + elif len(inp_order_by) == 2: + related_attribute_name, attribute_name, direction = ( + None, inp_order_by[0], inp_order_by[1]) + elif len(inp_order_by) == 3: + related_attribute_name, attribute_name, direction = inp_order_by + else: + self._add_to_errors( + str(inp_order_by), + 'Order by elements must be lists of 1, 2 or 3 elements') + continue + if related_attribute_name: + related_model_name = self._get_attribute_model_name( + related_attribute_name, self.model_name) + attribute_name = self._get_attribute_name( + attribute_name, related_model_name) + related_attribute_name = self._get_attribute_name( + related_attribute_name, self.model_name) + order_by = '{}__{}'.format( + related_attribute_name, attribute_name) + else: + order_by = self._get_attribute_name( + attribute_name, self.model_name) + try: + direction = self.order_by_directions[direction.lower()].get( + 'alias', direction) + except KeyError: + self._add_to_errors( + direction, 'Unrecognized order by direction') + continue + order_bys.append(direction + order_by) + return order_bys + + def clear_errors(self): + self.errors = {} + + def _raise_search_parse_error_if_necessary(self): + if self.errors: + errors = self.errors.copy() + # Clear the errors so the instance can be reused to build further + # queries + self.clear_errors() + raise SearchParseError(errors) + + def _get_model_manager(self): + return self.model_cls.objects + + def _pythonlist2queryexpr(self, query_as_list): + """This is the function that is called recursively (if necessary) to + build the filter expression. + """ + try: + if query_as_list[0] in ('and', 'or'): + op = {'and': operator.and_, 'or': operator.or_}[query_as_list[0]] + return functools.reduce( + op, [self._pythonlist2queryexpr(x) for x in query_as_list[1]]) + if query_as_list[0] == 'not': + return ~(self._pythonlist2queryexpr(query_as_list[1])) + return self._get_simple_Q_expression(*query_as_list) + except (TypeError, IndexError, AttributeError) as exc: + self.errors['Malformed query error'] = 'The submitted query was malformed' + self.errors[str(type(exc))] = str(exc) + + def _pythondict2queryexpr(self, query_as_dict): + try: + conjunction = query_as_dict.get('conjunction') + if conjunction in ('and', 'or'): + op = {'and': operator.and_, 'or': operator.or_}[conjunction] + return functools.reduce( + op, [self._pythondict2queryexpr(x) for x in + query_as_dict['complement']]) + negation = query_as_dict.get('negation') + if negation == 'not': + return ~(self._pythondict2queryexpr(query_as_dict['complement'])) + return self._get_simple_Q_expression_from_dict(query_as_dict) + except (TypeError, IndexError, AttributeError) as exc: + self.errors['Malformed query error'] = 'The submitted query was malformed' + self.errors[str(type(exc))] = str(exc) + + def _add_to_errors(self, key, msg): + self.errors[str(key)] = msg + + ############################################################################ + # Value converters + ############################################################################ + + def _get_date_value(self, date_string): + """Converts ISO 8601 date strings to Python datetime.date objects.""" + if date_string is None: + # None can be used on date comparisons so assume this is what was + # intended + return date_string + date = utils.date_string2date(date_string) + if date is None: + self._add_to_errors( + 'date %s' % str(date_string), + 'Date search parameters must be valid ISO 8601 date strings.') + return date + + def _get_datetime_value(self, datetime_string): + """Converts ISO 8601 datetime strings to Python datetime.datetime objects.""" + if datetime_string is None: + # None can be used on datetime comparisons so assume this is what + # was intended + return datetime_string + datetime_ = utils.datetime_string2datetime(datetime_string) + if datetime_ is None: + self._add_to_errors( + 'datetime %s' % str(datetime_string), + 'Datetime search parameters must be valid ISO 8601 datetime' + ' strings.') + return datetime_ + + ############################################################################ + # Data structures + ############################################################################ + + # Alter the relations and schema dicts in order to change what types of + # input the query builder accepts. + + # The default set of available relations. Relations with aliases are + # treated as their aliases. E.g., a search like + # ['Package', 'source_id' '!=', ...] + # will generate the Q expression Q(source_id=...) + relations = { + 'exact': {}, + '=': {'alias': 'exact'}, + 'ne': {}, + '!=': {'alias': 'ne'}, + 'contains': {}, + 'like': {'alias': 'contains'}, + 'regex': {}, + 'regexp': {'alias': 'regex'}, + 'lt': {}, + '<': {'alias': 'lt'}, + 'gt': {}, + '>': {'alias': 'gt'}, + 'lte': {}, + '<=': {'alias': 'lte'}, + 'gte': {}, + '>=': {'alias': 'gte'}, + 'in': {} + } + + foreign_model_relations = { + 'isnull': {}, + '=': {'alias': 'isnull'}, + '!=': {'alias': 'isnull'} + } + + order_by_directions = { + '': {}, + '-': {}, + 'desc': {'alias': '-'}, + 'descending': {'alias': '-'}, + 'asc': {'alias': ''}, + 'ascending': {'alias': ''} + } + + ############################################################################ + # Model getters + ############################################################################ + + def _get_model_name(self, model_name): + """Always return model_name; store an error if model_name is invalid.""" + if model_name not in self.schemata: + self._add_to_errors( + model_name, + 'Searching on the %s model is not permitted' % model_name) + return model_name + + def _get_attribute_model_name(self, attribute_name, model_name): + """Returns the name of the model X that stores the data for the attribute + A of model M, e.g., the attribute_model_name for model_name='Package' and + attribute_name='origin_pipeline' is 'Pipeline'. + """ + attribute_dict = self._get_attribute_dict(attribute_name, model_name) + try: + return attribute_dict['foreign_model'] + except KeyError: + self._add_to_errors( + '%s.%s' % (model_name, attribute_name), + 'The %s attribute of the %s model does not represent a' + ' many-to-one relation.' % ( + attribute_name, model_name)) + + ############################################################################ + # Attribute getters + ############################################################################ + + def _get_attribute_name(self, attribute_name, model_name): + """Return attribute_name or cache an error if attribute_name is not in + self.schemata[model_name]. + """ + self._get_attribute_dict(attribute_name, model_name, report_error=True) + return attribute_name + + def _get_attribute_dict(self, attribute_name, model_name, report_error=False): + """Return the dict needed to validate a given attribute of a given model, + or return None. Propagate an error (optionally) if the attribute_name is + invalid. + """ + attribute_dict = self.schemata.get(model_name, {}).get( + attribute_name, None) + if attribute_dict is None and report_error: + self._add_to_errors( + '%s.%s' % (model_name, attribute_name), + 'Searching on %s.%s is not permitted' % ( + model_name, attribute_name)) + return attribute_dict + + ############################################################################ + # Relation getters + ############################################################################ + + def _get_relation_name(self, relation_name, model_name, attribute_name): + """Return relation_name or its alias; propagate an error if + relation_name is invalid. + """ + relation_dict = self._get_relation_dict( + relation_name, model_name, attribute_name, report_error=True) + try: + return relation_dict.get('alias', relation_name) + except AttributeError: # relation_dict can be None + return None + + def _get_relation_dict(self, relation_name, model_name, attribute_name, + report_error=False): + attribute_relations = self._get_attribute_relations( + attribute_name, model_name) + try: + relation_dict = attribute_relations.get(relation_name, None) + except AttributeError: + relation_dict = None + if relation_dict is None and report_error: + self._add_to_errors( + '%s.%s.%s' % (model_name, attribute_name, relation_name), + 'The relation %s is not permitted for %s.%s' % ( + relation_name, model_name, attribute_name)) + return relation_dict + + def _get_attribute_relations(self, attribute_name, model_name): + """Return the data structure encoding what relations are valid for the + input attribute name. + """ + attribute_dict = self._get_attribute_dict(attribute_name, model_name) + try: + if attribute_dict.get('foreign_model'): + return self.foreign_model_relations + return self.relations + except AttributeError: # attribute_dict can be None + return None + + ############################################################################ + # Value getters + ############################################################################ + + @staticmethod + def _normalize(value): + def normalize_if_string(value): + if isinstance(value, str): + return utils.normalize(value) + return value + value = normalize_if_string(value) + if isinstance(value, list): + value = [normalize_if_string(i) for i in value] + return value + + def _get_value_converter(self, attribute_name, model_name): + attribute_dict = self._get_attribute_dict(attribute_name, model_name) + try: + value_converter_name = attribute_dict.get('value_converter', '') + return getattr(self, value_converter_name, None) + except AttributeError: # attribute_dict can be None + return None + + def _get_value(self, value, model_name, attribute_name, relation_name=None): + """Unicode normalize & modify the value using a value_converter (if + necessary). + """ + # unicode normalize (NFD) search patterns; we might want to parameterize this + value = self._normalize(value) + value_converter = self._get_value_converter(attribute_name, model_name) + if value_converter is not None: + if isinstance(value, list): + value = [value_converter(li) for li in value] + else: + value = value_converter(value) + attribute_dict = self._get_attribute_dict(attribute_name, model_name) + if (attribute_dict.get('foreign_model') and + relation_name and value is None): + if relation_name == '=': + value = True + if relation_name == '!=': + value = False + return value + + ############################################################################ + # Filter expression getters + ############################################################################ + + @staticmethod + def _get_invalid_filter_expression_message( + model_name, attribute_name, relation_name, value): + return 'Invalid filter expression: %s.%s.%s(%s)' % ( + model_name, attribute_name, relation_name, repr(value)) + + def _get_invalid_model_attribute_errors(self, *args): + """Avoid catching a (costly) RuntimeError by preventing _get_Q_expression + from attempting to build relation(value) or attribute.has(relation(value)). + We do this by returning a non-empty list of error tuples if Model.attribute + errors are present in self.errors. + """ + try: + (value, model_name, attribute_name, relation_name, + attribute_model_name, attribute_model_attribute_name) = args + except ValueError: + raise TypeError( + '_get_invalid_model_attribute_errors() missing 6 required' + ' positional arguments: \'value\', \'model_name\',' + ' \'attribute_name\', \'relation_name\',' + ' \'attribute_model_name\', and' + ' \'attribute_model_attribute_name\'') + e = [] + if attribute_model_name: + error_key = '%s.%s' % ( + attribute_model_name, attribute_model_attribute_name) + if (self.errors.get(error_key) == + 'Searching on the %s is not permitted' % error_key): + e.append( + ('%s.%s.%s' % (attribute_model_name, + attribute_model_attribute_name, + relation_name), + self._get_invalid_filter_expression_message( + attribute_model_name, + attribute_model_attribute_name, + relation_name, + value))) + error_key = '%s.%s' % (model_name, attribute_name) + if (self.errors.get(error_key) == + 'Searching on %s is not permitted' % error_key): + e.append(('%s.%s.%s' % (model_name, attribute_name, relation_name), + self._get_invalid_filter_expression_message( + model_name, attribute_name, relation_name, value))) + return e + + def _get_meta_relation(self, attribute, model_name, attribute_name): + """Return the has() or the any() method of the input attribute, depending + on the value of schema[model_name][attribute_name]['type']. + """ + return getattr(attribute, {'scalar': 'has', 'collection': 'any'}[ + self.schemata[model_name][attribute_name]['type']]) + + @staticmethod + def _get_Q_expression(attribute_name, relation_name, value): + return Q(**{'{}__{}'.format(attribute_name, relation_name): value}) + + def _get_simple_Q_expression(self, *args): + """Build a Q expression. Examples:: + + >>> ['description', '=', 'abc'] + >>> Q(description__exact='abc') + + >>> ['description', '!=', 'abc'] + >>> Q(description__ne='abc') + + >>> ['origin_pipeline', 'description', 'like', 'J%'] + >>> Q(origin_pipeline__description__contains='J%') + + >>> ['replicas', 'uuid', 'contains', 'a%'] + >>> Q(replicas__uuid__contains='a%') + """ + attribute_name = self._get_attribute_name(args[0], self.model_name) + if len(args) == 3: + relation_name = self._get_relation_name( + args[1], self.model_name, attribute_name) + value = self._get_value( + args[2], self.model_name, attribute_name, relation_name=args[1]) + return self._get_Q_expression( + attribute_name, relation_name, value) + attribute_model_name = self._get_attribute_model_name( + attribute_name, self.model_name) + attribute_model_attribute_name = self._get_attribute_name( + args[1], attribute_model_name) + relation_name = self._get_relation_name( + args[2], attribute_model_name, attribute_model_attribute_name) + value = self._get_value( + args[3], attribute_model_name, attribute_model_attribute_name, + relation_name=args[2]) + attribute_name = '{}__{}'.format( + attribute_name, attribute_model_attribute_name) + return self._get_Q_expression( + attribute_name, relation_name, value) + + def _get_simple_Q_expression_from_dict(self, query_as_dict): + subattribute = query_as_dict.get('subattribute') + attribute = query_as_dict['attribute'] + relation = query_as_dict['relation'] + value = query_as_dict['value'] + attribute_name = self._get_attribute_name(attribute, self.model_name) + if subattribute: + attribute_model_name = self._get_attribute_model_name( + attribute_name, self.model_name) + attribute_model_attribute_name = self._get_attribute_name( + subattribute, attribute_model_name) + relation_name = self._get_relation_name( + relation, attribute_model_name, attribute_model_attribute_name) + value = self._get_value( + value, attribute_model_name, attribute_model_attribute_name, + relation_name=relation) + attribute_name = '{}__{}'.format( + attribute_name, attribute_model_attribute_name) + return self._get_Q_expression( + attribute_name, relation_name, value) + relation_name = self._get_relation_name( + relation, self.model_name, attribute_name) + value = self._get_value(value, self.model_name, attribute_name, + relation_name=relation) + return self._get_Q_expression(attribute_name, relation_name, value) + + def get_search_parameters(self): + """Given the view's resource-configured QueryBuilder instance, + return the list of attributes and their aliases and licit relations + relevant to searching. + """ + return { + 'attributes': + self.schemata[self.model_name], + 'relations': self.relations + } + + @property + def schemata(self): + """The schemata attribute describes the database structure in a way + that allows the query builder to properly interpret the list-formatted + queries and generate errors where necessary. It maps model names to + attribute names. Attribute names whose values contain an 'alias' key + are treated as the value of that key, e.g., ['Package', 'enterer' ...] + will be treated as Package.enterer_id... The relations listed in + self.relations above are the default for all attributes. This can be + overridden by specifying a 'relation' key (cf. + schema['Package']['translations'] below). Certain attributes require + value converters -- functions that change the value in some + attribute-specific way, e.g., conversion of ISO 8601 datetimes to + Python datetime objects. + TODO: reconcile above text with actual behaviour + TODO: add value_converters when/if necessary, e.g., 'last_verified': + {'value_converter': '_get_datetime_value'}. + """ + if self._schemata: + return self._schemata + _schemata = {} + model_cls = self.model_cls + model_clses = set([model_cls]) + fields = model_cls._meta.get_fields() + for field in fields: + field_cls_name = field.__class__.__name__ + if field_cls_name in ('ForeignKey', 'OneToOneRel', + 'ManyToManyField', 'ManyToManyRel', + 'ManyToOneRel',): + model_clses.add(field.related_model) + for model_cls in model_clses: + model_schema = {} + model_cls_name = model_cls.__name__ + fields = model_cls._meta.get_fields() + for field in fields: + field_cls_name = field.__class__.__name__ + field_name = field.name + field_val = {} + if field_cls_name in ('ForeignKey', 'OneToOneRel'): + if field_cls_name == 'OneToOneRel': + field_name = field.get_accessor_name() + field_val = {'foreign_model': field.related_model.__name__, + 'type': 'scalar'} + elif field_cls_name in ('ManyToManyField', 'ManyToManyRel', + 'ManyToOneRel',): + if field_cls_name == 'ManyToOneRel': + field_name = field.get_accessor_name() + field_val = {'foreign_model': field.related_model.__name__, + 'type': 'collection'} + model_schema[field_name] = field_val + _schemata[model_cls_name] = model_schema + self._schemata = _schemata + return self._schemata diff --git a/storage_service/locations/api/v3/remple/resources.py b/storage_service/locations/api/v3/remple/resources.py new file mode 100644 index 000000000..b56b06d9b --- /dev/null +++ b/storage_service/locations/api/v3/remple/resources.py @@ -0,0 +1,795 @@ +"""Remple Resources + +Defines the following classes for easily creating controller sub-classes that +handle requests to REST resources: + +- ``Resources`` +- ``ReadonlyResources`` +""" + +from __future__ import absolute_import + +from collections import namedtuple +from functools import partial +import json +import logging + +from django.db import OperationalError +from django.db.models.fields.related import ManyToManyField +from formencode.validators import Invalid, UnicodeString +import inflect + +from .constants import ( + BAD_REQUEST_STATUS, + FORBIDDEN_STATUS, + JSONDecodeErrorResponse, + NOT_FOUND_STATUS, + OK_STATUS, + READONLY_RSLT, + UNAUTHORIZED_MSG, +) +from .querybuilder import QueryBuilder, SearchParseError +from .schemata import PaginatorSchema, ValidModelObject +from .utils import normalize + + +logger = logging.getLogger(__name__) + + +class ReadonlyResources(object): + """Super-class of ``Resources`` and all read-only resource views. + + +-----------------+-------------+--------------------------+--------+ + | Purpose | HTTP Method | Path | Method | + +=================+=============+==========================+========+ + | Create new | POST | / | create | + +-----------------+-------------+--------------------------+--------+ + | Create data | GET | //new | new | + +-----------------+-------------+--------------------------+--------+ + | Read all | GET | / | index | + +-----------------+-------------+--------------------------+--------+ + | Read specific | GET | // | show | + +-----------------+-------------+--------------------------+--------+ + | Update specific | PUT | // | update | + +-----------------+-------------+--------------------------+--------+ + | Update data | GET | ///edit | edit | + +-----------------+-------------+--------------------------+--------+ + | Delete specific | DELETE | // | delete | + +-----------------+-------------+--------------------------+--------+ + | Search | SEARCH | / | search | + +-----------------+-------------+--------------------------+--------+ + + Note: the create, new, update, edit, and delete actions are all exposed via + the REST API; however, they invariably return 404 responses. + """ + + primary_key = 'uuid' + _query_builder = None + + inflect_p = inflect.engine() + inflect_p.classical() + + def __init__(self, request, server_path='/api/0_1_0/'): + self.request = request + self.server_path = server_path + self._logged_in_user = None + self._query_builder = None + # Names + if not getattr(self, 'collection_name', None): + self.collection_name = self.__class__.__name__.lower() + if not getattr(self, 'hmn_collection_name', None): + self.hmn_collection_name = self.collection_name + if not getattr(self, 'member_name', None): + self.member_name = self.inflect_p.singular_noun( + self.collection_name) + if not getattr(self, 'hmn_member_name', None): + self.hmn_member_name = self.member_name + + # RsrcColl is a resource collection object factory. Each instance holds the + # relevant model name and instance getter for a given resource collection + # name. + RsrcColl = namedtuple('RsrcColl', ['model_cls', 'getter']) + + @staticmethod + def get_mini_dicts_getter(model_cls): + def func(): + return [mi.get_mini_dict() for mi in model_cls.objects.all()] + return func + + @property + def query_builder(self): + return self._get_query_builder() + + @classmethod + def _get_query_builder(cls): + if not cls._query_builder: + cls._query_builder = QueryBuilder( + cls.model_cls, + primary_key=cls.primary_key) + return cls._query_builder + + @property + def logged_in_user(self): + """Property to access the logged in user. QUESTION: Is this a db + model? + """ + if not self._logged_in_user: + self._logged_in_user = self.request.user + return self._logged_in_user + + ########################################################################### + # Public CRUD(S) Methods + ########################################################################### + + def create(self): + logger.warning('Failed attempt to create a read-only %s', + self.hmn_member_name) + return READONLY_RSLT, NOT_FOUND_STATUS + + def new(self): + logger.warning('Failed attempt to get data for creating a read-only %s', + self.hmn_member_name) + return READONLY_RSLT, NOT_FOUND_STATUS + + def index(self): + """Get all resources. + + - URL: ``GET /`` with optional query string + parameters for ordering and pagination. + + :returns: a JSON-serialized array of resources objects. + """ + logger.info('Attempting to read all %s', self.hmn_collection_name) + query_set = self.model_cls.objects + get_params = dict(self.request.GET) + try: + query_set = self.add_order_by(query_set, get_params) + query_set = self._filter_query(query_set) + result = self.add_pagination(query_set, get_params) + except Invalid as error: + errors = error.unpack_errors() + logger.warning('Attempt to read all %s resulted in an error(s): %s', + self.hmn_collection_name, errors) + return {'error': errors}, BAD_REQUEST_STATUS + headers_ctl = self._headers_control(result) + if headers_ctl is not False: + return headers_ctl + logger.info('Reading all %s', self.hmn_collection_name) + logger.info(result) + return result, OK_STATUS + + def show(self, pk): + """Return a resource, given its pk. + :URL: ``GET //`` + :param str pk: the ``pk`` value of the resource to be returned. + :returns: a resource model object. + """ + logger.info('Attempting to read a single %s', self.hmn_member_name) + resource_model = self._model_from_pk(pk) + if not resource_model: + msg = self._rsrc_not_exist(pk) + logger.warning(msg) + return {'error': msg}, NOT_FOUND_STATUS + if self._model_access_unauth(resource_model) is not False: + logger.warning(UNAUTHORIZED_MSG) + return UNAUTHORIZED_MSG, FORBIDDEN_STATUS + logger.info('Reading a single %s', self.hmn_member_name) + return self._get_show_dict(resource_model), OK_STATUS + + def update(self, pk): + logger.warning('Failed attempt to update a read-only %s', + self.hmn_member_name) + return READONLY_RSLT, NOT_FOUND_STATUS + + def edit(self, pk): + logger.warning('Failed attempt to get data for updating a read-only %s', + self.hmn_member_name) + return READONLY_RSLT, NOT_FOUND_STATUS + + def delete(self, pk): + logger.warning('Failed attempt to delete a read-only %s', + self.hmn_member_name) + return READONLY_RSLT, NOT_FOUND_STATUS + + def search(self): + """Return the list of resources matching the input JSON query. + + - URL: ``SEARCH /`` (or + ``POST //search``) + - request body: A JSON object of the form:: + + {"query": {"filter": [ ... ], "order_by": [ ... ]}, + "paginator": { ... }} + + where the ``order_by`` and ``paginator`` attributes are optional. + """ + logger.info('Attempting to search over %s', self.hmn_collection_name) + try: + python_search_params = json.loads( + self.request.body.decode('utf8')) + except ValueError: + logger.warning('Request body was not valid JSON') + logger.info(self.request.body.decode('utf8')) + return JSONDecodeErrorResponse, BAD_REQUEST_STATUS + try: + query_set = self.query_builder.get_query_set( + python_search_params.get('query')) + except (SearchParseError, Invalid) as error: + errors = error.unpack_errors() + logger.warning( + 'Attempt to search over all %s resulted in an error(s): %s', + self.hmn_collection_name, errors) + return {'error': errors}, BAD_REQUEST_STATUS + # Might be better to catch (OperationalError, AttributeError, + # InvalidRequestError, RuntimeError): + except Exception as error: # FIX: too general exception + logger.warning('Filter expression (%s) raised an unexpected' + ' exception: %s.', self.request.body, error) + return {'error': 'The specified search parameters generated an' + ' invalid database query'}, BAD_REQUEST_STATUS + query_set = self._eagerload_model(query_set) + query_set = self._filter_query(query_set) + try: + ret = self.add_pagination( + query_set, python_search_params.get('paginator')) + except OperationalError: + msg = ('The specified search parameters generated an invalid' + ' database query') + logger.warning(msg) + return {'error': msg}, BAD_REQUEST_STATUS + except Invalid as error: # For paginator schema errors. + errors = error.unpack_errors() + logger.warning( + 'Attempt to search over all %s resulted in an error(s): %s', + self.hmn_collection_name, errors) + return {'error': errors}, BAD_REQUEST_STATUS + else: + logger.info('Successful search over %s', self.hmn_collection_name) + return ret, OK_STATUS + + def new_search(self): + """Return the data necessary to search over this type of resource. + + - URL: ``GET //new_search`` + + :returns: a JSON object with a ``search_parameters`` attribute which + resolves to an object with attributes ``attributes`` and ``relations``. + """ + logger.info('Returning search parameters for %s', self.hmn_member_name) + return {'search_parameters': + self.query_builder.get_search_parameters()}, OK_STATUS + + def get_paginated_query_results(self, query_set, paginator): + if 'count' not in paginator: + paginator['count'] = query_set.count() + start, end = _get_start_and_end_from_paginator(paginator) + return { + 'paginator': paginator, + 'items': [self._get_show_dict(rsrc_mdl) for rsrc_mdl in + query_set[start:end]] + } + + def add_pagination_OLD(self, query_set, paginator): + if (paginator and paginator.get('page') is not None and + paginator.get('items_per_page') is not None): + # raises formencode.Invalid if paginator is invalid + paginator = PaginatorSchema.to_python(paginator) + return self.get_paginated_query_results(query_set, paginator) + return [self._get_show_dict(rsrc_mdl) for rsrc_mdl in query_set] + + def add_pagination(self, query_set, paginator): + paginator = paginator or {} + try: + page = int(paginator.get('page', [1])[0]) + except TypeError: + page = int(paginator.get('page', [1])) + try: + items_per_page = int(paginator.get('items_per_page', [50])[0]) + except TypeError: + items_per_page = int(paginator.get('items_per_page', [50])) + new_paginator = { + 'page': page, + 'items_per_page': items_per_page, + } + paginator = PaginatorSchema.to_python(new_paginator) + return self.get_paginated_query_results(query_set, paginator) + + ########################################################################### + # Private Methods for Override: redefine in views for custom behaviour + ########################################################################### + + def _get_show_dict(self, resource_model): + """Return the model as a dict for the return value of a successful show + request. This is indirected so that resources like collections can + override and do special things. + """ + try: + return resource_model.get_dict() + except AttributeError: + return self.to_dict(resource_model) + + def _get_create_dict(self, resource_model): + return self._get_show_dict(resource_model) + + def _get_edit_dict(self, resource_model): + return self._get_show_dict(resource_model) + + def _get_update_dict(self, resource_model): + return self._get_create_dict(resource_model) + + def _eagerload_model(self, query_set): + """Override this in a subclass with model-specific eager loading.""" + return get_eagerloader(self.model_cls)(query_set) + + def _filter_query(self, query_set): + """Override this in a subclass with model-specific query filtering. + E.g.,:: + + >>> return filter_restricted_models(self.model_cls, query_set) + """ + return query_set + + def _headers_control(self, result): + """Take actions based on header values and/or modify headers. If + something other than ``False`` is returned, that will be the response. + Useful for Last-Modified/If-Modified-Since caching, e.g., in ``index`` + method of Forms view. + """ + return False + + def _update_unauth(self, resource_model): + """Return ``True`` if update of the resource model cannot proceed.""" + return self._model_access_unauth(resource_model) + + def _update_unauth_msg_obj(self): + """Return the dict that will be returned when ``self._update_unauth()`` + returns ``True``. + """ + return UNAUTHORIZED_MSG + + def _model_access_unauth(self, resource_model): + """Implement resource/model-specific access controls based on + (un-)restricted(-ness) of the current logged in user and the resource + in question. Return something other than ``False`` to trigger a 403 + response. + """ + return False + + def _model_from_pk(self, pk): + """Return a particular model instance, given the model pk.""" + try: + return self.model_cls.objects.get(**{self.primary_key: pk}) + except (self.model_cls.DoesNotExist, + self.model_cls.MultipleObjectsReturned): + return None + + ########################################################################### + # Utilities + ########################################################################### + + def _rsrc_not_exist(self, pk): + return 'There is no %s with %s %s' % (self.hmn_member_name, + self.primary_key, pk) + + def add_order_by(self, query_set, order_by_params, query_builder=None): + """Add an ORDER BY clause to the query_set using the ``get_order_bys`` + method of the instance's query_builder (if possible) or using a default + ORDER BY ASC. + """ + if not query_builder: + query_builder = self.query_builder + inp_order_bys = None + inp_order_by = list(filter( + None, [order_by_params.get('order_by_attribute', [None])[0], + order_by_params.get('order_by_subattribute', [None])[0], + order_by_params.get('order_by_direction', [None])[0]])) + if inp_order_by: + inp_order_bys = [inp_order_by] + order_by = query_builder.get_order_bys( + inp_order_bys, primary_key=self.primary_key) + return query_set.order_by(*order_by) + + +class Resources(ReadonlyResources): + """Abstract base class for all (modifiable) resource views. RESTful + CRUD(S) interface: + + +-----------------+-------------+--------------------------+--------+ + | Purpose | HTTP Method | Path | Method | + +-----------------+-------------+--------------------------+--------+ + | Create new | POST | / | create | + | Create data | GET | //new | new | + | Read all | GET | / | index | + | Read specific | GET | // | show | + | Update specific | PUT | // | update | + | Update data | GET | ///edit | edit | + | Delete specific | DELETE | // | delete | + | Search | SEARCH | / | search | + +-----------------+-------------+--------------------------+--------+ + """ + + @classmethod + def get_create_schema_cls(cls): + return getattr(cls, 'schema_create_cls', getattr(cls, 'schema_cls', None)) + + @classmethod + def get_update_schema_cls(cls): + return getattr(cls, 'schema_update_cls', getattr(cls, 'schema_cls', None)) + + def preprocess_user_data(self, validated_user_data, schema): + """Process the user-provided and validated ``validated_user_data`` + dict, crucially returning a *new* dict created from it which is ready + for construction of a model instance. + """ + processed_data = {} + schema_cls = schema.__class__ + for field_name, field in schema_cls.fields.items(): + value = validated_user_data[field_name] + field_cls_name = field.__class__.__name__ + if field_cls_name == 'ForEach' and isinstance( + field.validators[0], ValidModelObject): + if value: + processed_data[field_name] = value + elif isinstance(field, UnicodeString): + processed_data[field_name] = normalize(value) + else: + processed_data[field_name] = value + return processed_data + + ########################################################################### + # Public CRUD(S) Methods + ########################################################################### + + def create(self): + """Create a new resource and return it. + :URL: ``POST /`` + :request body: JSON object representing the resource to create. + :returns: the newly created resource. + """ + logger.info('Attempting to create a new %s.', self.hmn_member_name) + schema_cls = self.get_create_schema_cls() + schema = schema_cls() + try: + user_data = json.loads(self.request.body.decode('utf8')) + except ValueError: + logger.warning('Request body was not valid JSON') + return JSONDecodeErrorResponse, BAD_REQUEST_STATUS + state = self._get_create_state(user_data) + try: + validated_user_data = schema.to_python(user_data, state) + except Invalid as error: + errors = error.unpack_errors() + logger.warning( + 'Attempt to create a(n) %s resulted in an error(s): %s', + self.hmn_member_name, errors) + return {'error': errors}, BAD_REQUEST_STATUS + resource = self._create_new_resource(validated_user_data, schema) + resource.save() + self._post_create(resource) + logger.info('Created a new %s.', self.hmn_member_name) + return self._get_create_dict(resource), OK_STATUS + + def new(self): + """Return the data necessary to create a new resource. + + - URL: ``GET //new/``. + + :returns: a dict containing the related resources necessary to create a + new resource of this type. + + .. note:: See :func:`_get_new_edit_data` to understand how the query + string parameters can affect the contents of the lists in the + returned dictionary. + """ + logger.info('Returning the data needed to create a new %s.', + self.hmn_member_name) + return self._get_new_edit_data(self.request.GET), OK_STATUS + + def update(self, pk): + """Update a resource and return it. + + - URL: ``PUT //`` + - Request body: JSON object representing the resource with updated + attribute values. + + :param str pk: the ``pk`` value of the resource to be updated. + :returns: the updated resource model. + """ + resource_model = self._model_from_pk(pk) + logger.info('Attempting to update %s %s.', self.hmn_member_name, pk) + if not resource_model: + msg = self._rsrc_not_exist(pk) + logger.warning(msg) + return {'error': msg}, NOT_FOUND_STATUS + if self._update_unauth(resource_model) is not False: + msg = self._update_unauth_msg_obj() + logger.warning(msg) + return msg, FORBIDDEN_STATUS + schema_cls = self.get_update_schema_cls() + schema = schema_cls() + try: + values = json.loads(self.request.body.decode('utf8')) + except ValueError: + logger.warning(JSONDecodeErrorResponse) + return JSONDecodeErrorResponse, BAD_REQUEST_STATUS + state = self._get_update_state(values, pk, resource_model) + try: + validated_user_data = schema.to_python(values, state) + except Invalid as error: + errors = error.unpack_errors() + logger.warning(errors) + return {'error': errors}, BAD_REQUEST_STATUS + resource_model = self._update_resource_model( + resource_model, validated_user_data, schema) + # resource_model will be False if there are no changes + if not resource_model: + msg = ('The update request failed because the submitted data were' + ' not new.') + logger.warning(msg) + return {'error': msg}, BAD_REQUEST_STATUS + resource_model.save() + self._post_update(resource_model) + logger.info('Updated %s %s.', self.hmn_member_name, pk) + return self._get_update_dict(resource_model), OK_STATUS + + def edit(self, pk): + """Return a resource and the data needed to update it. + :URL: ``GET //edit`` + :param str pk: the ``pk`` value of the resource that will be updated. + :returns: a dictionary of the form:: + + {"": {...}, "data": {...}} + + where the value of the ```` key is a + dictionary representation of the resource and the value of the + ``data`` key is a dictionary containing the data needed to edit an + existing resource of this type. + """ + resource_model = self._model_from_pk(pk) + logger.info('Attempting to return the data needed to update %s %s.', + self.hmn_member_name, pk) + if not resource_model: + msg = self._rsrc_not_exist(pk) + logger.warning(msg) + return {'error': msg}, NOT_FOUND_STATUS + if self._model_access_unauth(resource_model) is not False: + logger.warning('User not authorized to access edit action on model') + return UNAUTHORIZED_MSG, FORBIDDEN_STATUS + logger.info('Returned the data needed to update %s %s.', + self.hmn_member_name, pk) + return { + 'data': self._get_new_edit_data(self.request.GET, mode='edit'), + 'resource': self._get_edit_dict(resource_model) + }, OK_STATUS + + def delete(self, pk): + """Delete an existing resource and return it. + :URL: ``DELETE //`` + :param str pk: the ``pk`` value of the resource to be deleted. + :returns: the deleted resource model. + """ + resource_model = self._model_from_pk(pk) + logger.info('Attempting to delete %s %s.', self.hmn_member_name, pk) + if not resource_model: + msg = self._rsrc_not_exist(pk) + logger.warning(msg) + return {'error': msg}, NOT_FOUND_STATUS + if self._delete_unauth(resource_model) is not False: + msg = self._delete_unauth_msg_obj() + logger.warning(msg) + return msg, FORBIDDEN_STATUS + error_msg = self._delete_impossible(resource_model) + if error_msg: + logger.warning(error_msg) + return {'error': error_msg}, FORBIDDEN_STATUS + resource_dict = self._get_delete_dict(resource_model) + self._pre_delete(resource_model) + resource_model.delete() + self._post_delete(resource_model) + logger.info('Deleted %s %s.', self.hmn_member_name, pk) + return resource_dict, OK_STATUS + + ########################################################################### + # Private methods for write-able resources + ########################################################################### + + def _delete_unauth(self, resource_model): + """Implement resource/model-specific controls over delete requests. + Return something other than ``False`` to trigger a 403 response. + """ + return False + + def _delete_unauth_msg_obj(self): + """Return the dict that will be returned when ``self._delete_unauth()`` + returns ``True``. + """ + return UNAUTHORIZED_MSG + + def _get_delete_dict(self, resource_model): + """Override this in sub-classes for special resource dict creation.""" + return self._get_show_dict(resource_model) + + def _pre_delete(self, resource_model): + """Perform actions prior to deleting ``resource_model`` from the + database. + """ + pass + + def _post_delete(self, resource_model): + """Perform actions after deleting ``resource_model`` from the + database. + """ + pass + + def _delete_impossible(self, resource_model): + """Return something other than false in a sub-class if this particular + resource model cannot be deleted. + """ + return False + + def _create_new_resource(self, validated_user_data, schema): + """Create a new resource. + :param dict validated_user_data: the data for the resource to be + created. + :param formencode.Schema schema: schema object used to validate + user-supplied data. + :returns: an SQLAlchemy model object representing the resource. + """ + return self.model_cls( + **self.preprocess_user_data(validated_user_data, schema)) + + def _post_create(self, resource_model): + """Perform some action after creating a new resource model in the + database. E.g., with forms we have to update all of the forms that + contain the newly entered form as a morpheme. + """ + pass + + def _get_create_state(self, values): + """Return a SchemaState instance for validation of the resource during + a create request. + """ + return SchemaState( + full_dict=values, + logged_in_user=self.logged_in_user) + + def _get_update_state(self, values, pk, resource_model): + update_state = self._get_create_state(values) + update_state.pk = pk + return update_state + + def _update_resource_model(self, resource_model, validated_user_data, + schema): + """Update the Django model instance ``resource_model`` with the dict + ``validated_user_data`` and return something other than ``False`` if + ``resource_model`` has changed as a result. + :param resource_model: the resource model to be updated. + :param dict validated_user_data: user-supplied representation of the + updated resource. + :param formencode.Schema schema: schema object used to validate + user-supplied data. + :returns: the updated resource model instance or ``False`` if the data + did not result in an update of the model. + """ + changed = False + for attr, val in validated_user_data.items(): + if self._distinct(attr, val, getattr(resource_model, attr)): + changed = True + break + if changed: + for attr, val in self.preprocess_user_data( + validated_user_data, schema).items(): + setattr(resource_model, attr, val) + return resource_model + return changed + + def _distinct(self, attr, new_val, existing_val): + """Return true if ``new_val`` is distinct from ``existing_val``. The + ``attr`` value is provided so that certain attributes (e.g., m2m) can + have a special definition of "distinct". + """ + return new_val != existing_val + + def _post_update(self, resource_model): + """Perform some action after updating an existing resource model in the + database. + """ + pass + + def get_most_recent_modification_datetime(self, model_cls): + """Return the most recent datetime_modified attribute for the model + + .. note:: This method is intended to be called from + ``_get_new_edit_data`` but the relevant functionality is not yet + implemented. + """ + return None + + def _get_new_edit_data(self, get_params, mode='new'): + """Return the data to create/edit this resource. + + .. note:: the request GET params (``get_params``) should be used here + to allow the user to only request fresh data. However, this makes + assumptions about the Django models that cannot be guaranteed right now + so this functionality is not currently used. + """ + result = {} + for collection, getter in self._get_new_edit_collections( + mode=mode).items(): + result[collection] = getter() + return result + + @staticmethod + def _get_related_model_getter(field): + related_model_cls = field.model_cls + def getter(model_cls): + return [mi.uuid for mi in model_cls.objects.all()] + return partial(getter, related_model_cls) + + def _get_enum_getter(self, field): + def getter(field): + return [c[0] for c in field.list] + return partial(getter, field) + + @classmethod + def _django_model_class_to_plural(cls, model_cls): + return cls.inflect_p.plural(model_cls.__name__.lower()) + + @classmethod + def _get_new_edit_collections(cls, mode='new'): + """Return a dict from collection names (e.g., "users" or "purpose") to + getter functions that will return all instances of that collection, be + they Django models or simple strings. This dict is constructed by + introspecting both ``cls.model_cls`` and ``cls.schema_cls``. + """ + collections = {} + if mode == 'new': + schema_cls = cls.get_create_schema_cls() + else: + schema_cls = cls.get_update_schema_cls() + for field_name, field in schema_cls.fields.items(): + field_cls_name = field.__class__.__name__ + if field_cls_name == 'ValidModelObject': + key = cls._django_model_class_to_plural(field.model_cls) + collections[key] = cls._get_related_model_getter(field) + elif field_cls_name == 'ForEach': + first_validator = field.validators[0] + field_cls_name = first_validator.__class__.__name__ + if field_cls_name == 'ValidModelObject': + key = cls._django_model_class_to_plural( + first_validator.model_cls) + collections[key] = cls._get_related_model_getter(first_validator) + return collections + + def to_dict(self, instance): + opts = instance._meta + data = {'resource_uri': '{}/{}/{}/'.format( + self.server_path, self.collection_name, instance.uuid)} + for f in opts.concrete_fields + opts.many_to_many: + if isinstance(f, ManyToManyField): + if instance.pk is None: + data[f.name] = [] + else: + data[f.name] = list( + f.value_from_object(instance).values_list('uuid', flat=True)) + else: + data[f.name] = f.value_from_object(instance) + return data + + +class SchemaState(object): + + def __init__(self, full_dict=None, logged_in_user=None, **kwargs): + self.full_dict = full_dict + self.user = logged_in_user + for key, val in kwargs.items(): + setattr(self, key, val) + + +def get_eagerloader(model_cls): + return lambda query_set: query_set + + +def _get_start_and_end_from_paginator(paginator): + start = (paginator['page'] - 1) * paginator['items_per_page'] + return (start, start + paginator['items_per_page']) diff --git a/storage_service/locations/api/v3/remple/routebuilder.py b/storage_service/locations/api/v3/remple/routebuilder.py new file mode 100644 index 000000000..22994ed03 --- /dev/null +++ b/storage_service/locations/api/v3/remple/routebuilder.py @@ -0,0 +1,289 @@ +"""Remple Route Builder + +Defines the ``RouteBuilder`` class whose ``register_resources`` method takes a +dict representing a resource configuration. Once resources are registered, the +``get_urlpatterns`` method can be called to return a list of URL patterns that +can be passed to Django's ``django.conf.urls.include``. Example usage:: + + >>> resources = {...} + >>> route_builder = RouteBuilder() + >>> route_builder.register_resources(resources) + >>> urls = route_builder.get_urlpatterns() + >>> urlpatterns = [url(r'v3/', include(urls))] + +All resources registered with the ``RouteBuilder`` are given endpoints that +follow this pattern:: + + +-----------------+-------------+----------------------------+--------+ + | Purpose | HTTP Method | Path | Method | + +-----------------+-------------+----------------------------+--------+ + | Create new | POST | // | create | + | Create data | GET | //new/ | new | + | Read all | GET | // | index | + | Read specific | GET | /// | show | + | Update specific | PUT | /// | update | + | Update data | GET | ///edit/ | edit | + | Delete specific | DELETE | /// | delete | + | Search | SEARCH | // | search | + | Search | POST | //search/ | search | + | Search data | GET | //new_search/ | search | + +-----------------+-------------+----------------------------+--------+ + +Example resource dict:: + + >>> resources = { + ... 'dog': {'resource_cls': Dogs}, + ... 'cat': {'resource_cls': Cats}} + +.. note:: To remove the search-related routes for a given resource, create a + ``'searchable'`` key with value ``False`` in the configuration for the + resource in the ``RESOURCES`` dict. E.g., ``'location': {'searchable': + False}`` will make the /locations/ resource non-searchable. + +.. note:: All resources expose the same endpoints. If a resource needs special + treatment, it should be done at the corresponding class level. E.g., if + ``POST /packages/`` (creating a package) is special, then do special stuff + in ``resources.py::Packages.create``. Similarly, if packages are indelible, + then ``resources.py::Packages.delete`` should return 404. +""" + +from __future__ import absolute_import +from collections import namedtuple +from functools import partial +from itertools import chain +import logging +import string + +from django.conf.urls import url +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +import inflect +from tastypie.authentication import ( + BasicAuthentication, + ApiKeyAuthentication, + MultiAuthentication, + SessionAuthentication +) + +from .constants import ( + OK_STATUS, + METHOD_NOT_ALLOWED_STATUS, + UNAUTHORIZED_MSG, + FORBIDDEN_STATUS, +) +from .openapi import OpenAPI + + +logger = logging.getLogger(__name__) + +UUID_PATT = r'\w{8}-\w{4}-\w{4}-\w{4}-\w{12}' +ID_PATT = r'\d+' +ROUTE_NAME_CHARS = string.letters + '_' + +inflp = inflect.engine() +inflp.classical() + + +class RouteBuilder(OpenAPI): + """A builder of routes: register a bunch of ``Route`` instances with it and + later ask it for a list of corresponding Django ``url`` instances. A + "route" is a path regex, a route name, an HTTP method, and a class/method + to call when the path and HTTP method match a request. + """ + + inflp = inflp + + RESOURCE_ACTIONS = ('create', + 'delete', + 'edit', + 'index', + 'new', + 'show', + 'update') + MUTATING_ACTIONS = ('create', 'delete', 'edit', 'new', 'update') + COLLECTION_TARGETING = ('create', 'index') + MEMBER_TARGETING = ('delete', 'show', 'update') + DEFAULT_METHOD = 'GET' + ACTIONS2METHODS = {'create': 'POST', + 'delete': 'DELETE', + 'update': 'PUT'} + + def __init__(self, *args, **kwargs): + self.routes = {} + self.resources = None + super(RouteBuilder, self).__init__(*args, **kwargs) + + def register_route(self, route): + """Register a ``Route()`` instance by breaking it apart and storing it + in ``self.routes``, keyed by its regex and then by its HTTP method. + """ + config = self.routes.get(route.regex, {}) + config['route_name'] = route.name + http_methods_config = config.get('http_methods', {}) + http_methods_config[route.http_method] = ( + route.resource_cls, route.method_name) + config['http_methods'] = http_methods_config + self.routes[route.regex] = config + + def get_urlpatterns(self): + """Return ``urlpatterns_``, a list of Django ``url`` instances that + cause the appropriate instance method to be called for a given request. + Because Django does not allow the HTTP method to determine what is + called, we must supply a view to ``url`` as an anonymous function with + a closure over the route "config", which the anonymous function can use + to route the request to the appropriate method call. For example, + ``GET /pipelines/`` and ``POST /pipelines/`` are handled by the same + function, but ultimately the former is routed to ``Pipelines().index`` + and the latter to ``Pipelines().create``. + """ + urlpatterns_ = [] + for regex, config in self.routes.items(): + route_name = config['route_name'] + def resource_callable(config, request, **kwargs): + http_methods_config = config['http_methods'] + try: + resource_cls, method_name = http_methods_config[ + request.method] + except KeyError: + return JsonResponse( + method_not_allowed( + request.method, list(http_methods_config.keys())), + status=METHOD_NOT_ALLOWED_STATUS) + # HERE resource class needs to know about other resources and + # API path prefix... + instance = resource_cls( + request, server_path=self._get_dflt_server_path()) + authentication = MultiAuthentication( + BasicAuthentication(), ApiKeyAuthentication(), + SessionAuthentication()) + auth_result = authentication.is_authenticated(request) + if auth_result is True: + method = getattr(instance, method_name) + response, status = method(**kwargs) + else: + logger.warning(UNAUTHORIZED_MSG) + response, status = UNAUTHORIZED_MSG, FORBIDDEN_STATUS + return JsonResponse(response, status=status, safe=False) + urlpatterns_.append(url( + regex, + # Sidestep Python's late binding: + view=csrf_exempt(partial(resource_callable, config)), + name=route_name)) + return urlpatterns_ + + def yield_standard_routes(self, rsrc_member_name, resource_cls): + """Yield the ``Route()``s needed to configure standard CRUD actions on the + resource with member name ``rsrc_member_name``. + """ + pk_patt = {'uuid': UUID_PATT, 'id': ID_PATT}.get( + resource_cls.primary_key, UUID_PATT) + rsrc_collection_name = inflp.plural(rsrc_member_name) + for action in self.RESOURCE_ACTIONS: + method_name = action + http_method = self.ACTIONS2METHODS.get(action, self.DEFAULT_METHOD) + if action in self.COLLECTION_TARGETING: + route_name = rsrc_collection_name + regex = get_collection_targeting_regex(rsrc_collection_name) + elif action in self.MEMBER_TARGETING: + route_name = rsrc_member_name + regex = get_member_targeting_regex(rsrc_collection_name, pk_patt) + elif action == 'new': + route_name = '{}_new'.format(rsrc_collection_name) + regex = get_collection_targeting_regex( + rsrc_collection_name, modifiers=['new']) + else: # edit is default case + route_name = '{}_edit'.format(rsrc_member_name) + regex = get_member_targeting_regex( + rsrc_collection_name, pk_patt, modifiers=['edit']) + yield Route(name=route_name, + regex=regex, + http_method=http_method, + resource_cls=resource_cls, + method_name=method_name) + + def register_routes_for_resource(self, rsrc_member_name, rsrc_config): + """Register all of the routes generable for the resource with member + name ``rsrc_member_name`` and with configuration ``rsrc_config``. The + ``rsrc_config`` can control whether the resource is searchable. + """ + routes = [] + if rsrc_config.get('searchable', True): + routes.append(self.yield_search_routes( + rsrc_member_name, rsrc_config['resource_cls'])) + routes.append(self.yield_standard_routes( + rsrc_member_name, rsrc_config['resource_cls'])) + for route in chain(*routes): + self.register_route(route) + + def register_resources(self, resources_): + """Register all of the routes generable for each resource configured in + the ``resources_`` dict. + """ + self.resources = resources_ + for rsrc_member_name, rsrc_config in resources_.items(): + self.register_routes_for_resource(rsrc_member_name, rsrc_config) + + @staticmethod + def yield_search_routes(rsrc_member_name, resource_cls): + """Yield the ``Route()``s needed to configure search across the resource + with member name ``rsrc_member_name``. + """ + rsrc_collection_name = inflp.plural(rsrc_member_name) + yield Route(name=rsrc_collection_name, + regex=get_collection_targeting_regex(rsrc_collection_name), + http_method='SEARCH', + resource_cls=resource_cls, + method_name='search') + yield Route(name='{}_search'.format(rsrc_collection_name), + regex=get_collection_targeting_regex( + rsrc_collection_name, modifiers=['search']), + http_method='POST', + resource_cls=resource_cls, + method_name='search') + yield Route(name='{}_new_search'.format(rsrc_collection_name), + regex=get_collection_targeting_regex( + rsrc_collection_name, modifiers=['new_search']), + http_method='GET', + resource_cls=resource_cls, + method_name='new_search') + + +# A "route" is a unique combination of path regex, route name, HTTP method, and +# class/method to call when the path regex and HTTP method match a request. +# Note that because of how Django's ``url`` works, multiple distinct routes can +# have the same ``url`` instance with the same name; e.g., POST /pipelines/ and +# GET /pipelines/ are both handled by the "pipelines" ``url``. +Route = namedtuple('Route', 'name regex http_method resource_cls method_name') + + +def get_collection_targeting_regex(rsrc_collection_name, modifiers=None): + """Return a regex of the form '^/$' + with optional trailing modifiers, e.g., '^/new/$'. + """ + if modifiers: + return r'^{rsrc_collection_name}/{modifiers}/$'.format( + rsrc_collection_name=rsrc_collection_name, + modifiers='/'.join(modifiers)) + return r'^{rsrc_collection_name}/$'.format( + rsrc_collection_name=rsrc_collection_name) + + +def get_member_targeting_regex(rsrc_collection_name, pk_patt, modifiers=None): + """Return a regex of the form '^//$' + with optional modifiers after the pk, e.g., + '^//edit/$'. + """ + if modifiers: + return (r'^{rsrc_collection_name}/(?P{pk_patt})/' + r'{modifiers}/$'.format( + rsrc_collection_name=rsrc_collection_name, + pk_patt=pk_patt, + modifiers='/'.join(modifiers))) + return r'^{rsrc_collection_name}/(?P{pk_patt})/$'.format( + rsrc_collection_name=rsrc_collection_name, pk_patt=pk_patt) + + +def method_not_allowed(tried_method, accepted_methods): + return {'error': 'The {} method is not allowed for this resources. The' + ' accepted methods are: {}'.format( + tried_method, ', '.join(accepted_methods))} diff --git a/storage_service/locations/api/v3/remple/schemata.py b/storage_service/locations/api/v3/remple/schemata.py new file mode 100644 index 000000000..a902a28c1 --- /dev/null +++ b/storage_service/locations/api/v3/remple/schemata.py @@ -0,0 +1,119 @@ +from collections import OrderedDict +import re + +from formencode.schema import Schema +from formencode.validators import Int, FancyValidator, Regex + +from .constants import formencode_field_class2openapi_type + + +class NotUsed(Exception): + pass + + +class OpenAPISchema(object): + + def __init__(self, formencode_schema, config): + self.formencode_schema = formencode_schema + self.config = config + + def extract_parameters(self): + """Return a list of OrderedDicts describing this schema as an OpenAPI + parameter. + """ + parameters = [] + for parameter_name, formencode_cls in ( + self.formencode_schema.fields.items()): + config = self.config.get(parameter_name, {}) + parameter = OrderedDict() + schema = OrderedDict() + parameter['in'] = config.get('in', 'query') + parameter['name'] = parameter_name + parameter['required'] = config.get( + 'required', formencode_cls.not_empty) + schema['type'] = formencode_field_class2openapi_type.get( + formencode_cls.__class__.__name__, 'string') + minimum = formencode_cls.min + if minimum is not None: + schema['minimum'] = minimum + parameter['schema'] = schema + description = config.get('description', None) + if description is not None: + parameter['description'] = description + default = config.get('default', NotUsed) + if default != NotUsed: + schema['default'] = default + parameters.append(parameter) + return parameters + + +class PaginatorSchema(Schema): + allow_extra_fields = True + filter_extra_fields = False + items_per_page = Int(not_empty=True, min=1) + page = Int(not_empty=True, min=1) + + +PaginatorOpenAPISchema = OpenAPISchema( + PaginatorSchema, + { + 'page': { + 'description': 'The page number to return.', + 'required': False, + 'default': 1, + }, + 'items_per_page': { + 'description': 'The maximum number of items to return.', + 'required': False, + 'default': 10, + } + }) + +schemata = (PaginatorOpenAPISchema,) + + +UUID = Regex( + r'^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-' + r'[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$') + + +def camel_case2lower_space(name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1 \2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1 \2', s1).lower() + + +class ValidModelObject(FancyValidator): + """Validator for input values that are primary keys of model objects. Value + must be the pk of an existing model of the type specified in the + ``model_cls`` kwarg. If valid, the model object is returned. Example + usage: ValidModelObject(model_cls=models.Package). + """ + + messages = { + 'invalid_model': + 'There is no %(model_name_eng)s with pk %(pk)s.' + } + + def _convert_to_python(self, value, state): + if value in ['', None]: + return None + else: + pk_validator = getattr(self, 'pk_validator', UUID) + pk = pk_validator().to_python(value, state) + pk_attr = getattr(self, 'pk', 'uuid') + model_cls = self.model_cls + try: + model_object = model_cls.objects.get( + **{pk_attr: value}) + except model_cls.DoesNotExist: + model_name_eng=camel_case2lower_space( + self.model_cls.__class__.__name__) + raise Invalid( + self.message('invalid_model', state, id=pk, + model_name_eng=model_name_eng), + value, state) + else: + return model_object + + +__all__ = ('schemata', 'PaginatorSchema', 'ValidModelObject') diff --git a/storage_service/locations/api/v3/remple/utils.py b/storage_service/locations/api/v3/remple/utils.py new file mode 100644 index 000000000..b5afc593a --- /dev/null +++ b/storage_service/locations/api/v3/remple/utils.py @@ -0,0 +1,48 @@ +import unicodedata +import datetime + + +def normalize(unistr): + """Return a unistr using canonical decompositional normalization (NFD).""" + try: + return unicodedata.normalize('NFD', unistr) + except TypeError: + return unicodedata.normalize('NFD', unistr.decode('utf8')) + except UnicodeDecodeError: + return unistr + + +def round_datetime(dt): + """Round a datetime to the nearest second.""" + discard = datetime.timedelta(microseconds=dt.microsecond) + dt -= discard + if discard >= datetime.timedelta(microseconds=500000): + dt += datetime.timedelta(seconds=1) + return dt + + +def datetime_string2datetime(datetime_string): + """Parse an ISO 8601-formatted datetime into a Python datetime object. + Cf. http://stackoverflow.com/questions/531157/\ + parsing-datetime-strings-with-microseconds + """ + try: + parts = datetime_string.split('.') + years_to_seconds_string = parts[0] + datetime_object = datetime.datetime.strptime( + years_to_seconds_string, "%Y-%m-%dT%H:%M:%S") + except ValueError: + return None + try: + microseconds = int(parts[1]) + datetime_object = datetime_object.replace(microsecond=microseconds) + except (IndexError, ValueError, OverflowError): + pass + return datetime_object + + +def date_string2date(date_string): + try: + return datetime.datetime.strptime(date_string, "%Y-%m-%d").date() + except ValueError: + return None diff --git a/storage_service/locations/api/v3/resources.py b/storage_service/locations/api/v3/resources.py new file mode 100644 index 000000000..23241fa1d --- /dev/null +++ b/storage_service/locations/api/v3/resources.py @@ -0,0 +1,55 @@ +"""Resources for Version 3 of the Storage Service API. + +Defines the following sub-classes of ``remple.Resources``: + +- ``Packages`` +- ``Locations`` +- ``Pipelines`` +- ``Spaces`` +""" + +import logging + +from formencode.validators import UnicodeString + +from locations.api.v3.remple import utils, Resources, ReadonlyResources +from locations.api.v3.schemata import ( + LocationSchema, + PackageSchema, + SpaceCreateSchema, + SpaceUpdateSchema, + PipelineSchema, +) +from locations.models import ( + Location, + Package, + Space, + Pipeline, +) + +logger = logging.getLogger(__name__) + + +class Packages(ReadonlyResources): + """TODO: Packages should not be creatable or editable via the REST API. + However, the user should be able to delete them via the API or at least + request their deletion. + """ + model_cls = Package + # schema_cls = PackageSchema + + +class Locations(Resources): + model_cls = Location + schema_cls = LocationSchema + + +class Pipelines(Resources): + model_cls = Pipeline + schema_cls = PipelineSchema + + +class Spaces(Resources): + model_cls = Space + schema_create_cls = SpaceCreateSchema + schema_update_cls = SpaceUpdateSchema diff --git a/storage_service/locations/api/v3/schemata.py b/storage_service/locations/api/v3/schemata.py new file mode 100644 index 000000000..6154f9c0c --- /dev/null +++ b/storage_service/locations/api/v3/schemata.py @@ -0,0 +1,90 @@ +from __future__ import absolute_import +import logging + +from formencode.compound import Any +from formencode.foreach import ForEach +from formencode.schema import Schema +from formencode.validators import ( + Int, + Invalid, + IPAddress, + OneOf, + Bool, + UnicodeString, + URL, +) + +from locations import models +from locations.api.v3.remple import ValidModelObject + + +logger = logging.getLogger(__name__) + + +def _flatten(choices): + return [ch[0] for ch in choices] + + + +class PipelineSchema(Schema): + allow_extra_fields = True + filter_extra_fields = True + + api_key = UnicodeString(max=256) + api_username = UnicodeString(max=256) + description = UnicodeString(max=256) + enabled = Bool() + remote_name = Any(validators=[IPAddress(), URL()]) + + +class SpaceUpdateSchema(Schema): + allow_extra_fields = True + filter_extra_fields = True + + size = Int(min=0) + path = UnicodeString(max=256) + staging_path = UnicodeString(max=256) + + +class SpaceCreateSchema(SpaceUpdateSchema): + allow_extra_fields = True + filter_extra_fields = True + + access_protocol = OneOf( + _flatten(models.Space.ACCESS_PROTOCOL_CHOICES)) + + +class LocationSchema(Schema): + allow_extra_fields = True + filter_extra_fields = True + + description = UnicodeString(max=256) + purpose = OneOf(_flatten(models.Location.PURPOSE_CHOICES)) + relative_path = UnicodeString() + quota = Int(min=0) + enabled = Bool() + space = ValidModelObject(model_cls=models.Space) + pipeline = ForEach(ValidModelObject(model_cls=models.Pipeline)) + replicators = ForEach(ValidModelObject(model_cls=models.Location)) + + +# Note: it does not make sense to have a schema for the package resource since +# it is not truly mutable via an external API. I am leaving this for now in +# case it contains useful information in the future. +class PackageSchema(Schema): + allow_extra_fields = True + filter_extra_fields = True + current_location = ValidModelObject(model_cls=models.Location) + current_path = UnicodeString() + description = UnicodeString(max=256) + encryption_key_fingerprint = UnicodeString(max=512) + misc_attributes = UnicodeString() + origin_pipeline = ValidModelObject(model_cls=models.Pipeline) + package_type = OneOf( + _flatten(models.Package.PACKAGE_TYPE_CHOICES)) + pointer_file_location = ValidModelObject(model_cls=models.Location) + pointer_file_path = UnicodeString() + related_packages = ForEach(ValidModelObject(model_cls=models.Package)) + replicated_package = ValidModelObject(model_cls=models.Package) + size = Int(min=0) + status = OneOf(_flatten(models.Package.STATUS_CHOICES)) diff --git a/storage_service/locations/management/__init__.py b/storage_service/locations/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/storage_service/locations/management/commands/__init__.py b/storage_service/locations/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/storage_service/locations/management/commands/buildapi.py b/storage_service/locations/management/commands/buildapi.py new file mode 100644 index 000000000..aac6f2639 --- /dev/null +++ b/storage_service/locations/management/commands/buildapi.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +"""Management command that builds the OpenAPI YAML for the Storage Service's +REST API. + +It also builds a Python client module that contains the YAML and which +dynamically defines a REST API client. + +To run from a Docker Compose deploy:: + + $ docker-compose exec archivematica-storage-service /src/storage_service/manage.py buildapi + OpenAPI specification written. See: + + - spec file: /src/storage_service/static/openapi/openapispecs/storage-service-0.11.0-openapi-3.0.0.yml + - client Python script: /src/storage_service/static/openapi/openapispecs/client.py + +After the above is run, it should be possible to view the Swagger-UI at +static/openapi/index.html. + +The YAML spec file should be downloadable at the path +static/openapi/openapispecs/storage-service-0.11.0-openapi-3.0.0.yml. + +Finally, the Python client script for interacting with the API should be +downloadable at the path static/openapi/openapispecs/client.py. See the +docstring of the generated clien.py file for usage or do the following:: + + >>> from client import client_class + >>> help(client_class) + +TODOs: + +- Add CI script (similar to Django checkformigrations) that checks whether the + OpenAPI spec needs to be rebuilt. + + - Relatedly, make sure all dicts are ordered dicts so that we don't get + spurious diffs between semantically equivalent specs. +""" + +import os +import pprint +import yaml + +import django +from django.conf import settings as django_settings +from django.core.management.base import BaseCommand + +from locations.api.v3 import api +from storage_service import __version__ as ss_version + + +def _get_client_builder_path(): + return os.path.join( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + 'locations', + 'api', + 'v3', + 'remple', + 'clientbuilder.py', + ) + + +def _get_spec_dir_path(): + return os.path.join( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + 'static', + 'openapi', + 'openapispecs', + ) + + +def build_client_script(open_api_dict): + client_builder_path = _get_client_builder_path() + spec_dir_path = _get_spec_dir_path() + client_path = os.path.join(spec_dir_path, 'client.py') + with open(client_path, 'w') as fileo: + with open(client_builder_path) as filei: + for line in filei: + if line.startswith('# OPENAPI_SPEC goes here'): + fileo.write('\n\nOPENAPI_SPEC = (\n{}\n)\n\n'.format( + pprint.pformat(open_api_dict))) + else: + fileo.write(line) + return client_path + + +def main(output_type='yaml'): + open_api_spec = api.generate_open_api_spec() + spec_dir_path = _get_spec_dir_path() + if output_type == 'yaml': + open_api = api.to_yaml(open_api_spec) + else: + open_api = api.to_json(open_api_spec) + write_name = 'storage-service-{}-openapi-{}.{}'.format( + ss_version, open_api_spec['info']['version'], + {'yaml': 'yml'}.get(output_type, output_type)) + write_path = os.path.join(spec_dir_path, write_name) + with open(write_path, 'w') as fi: + fi.write(open_api) + client_path = build_client_script(open_api_spec) + print('OpenAPI specification written. See:\n\n' + '- spec file: {}\n' + '- client Python script: {}'.format(write_path, client_path)) + return 0 + + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( + '-o', + '--output-type', + default='yaml', + type=str, + dest='output_type', + help='Output type for the API Spec: yaml or json', + ) + + def handle(self, *args, **options): + output_type = options['output_type'].strip().lower() + if output_type not in ('json', 'yaml'): + output_type = 'yaml' + main(output_type=output_type) diff --git a/storage_service/locations/tests/test_api_v3_urls.py b/storage_service/locations/tests/test_api_v3_urls.py new file mode 100644 index 000000000..c7ad781d9 --- /dev/null +++ b/storage_service/locations/tests/test_api_v3_urls.py @@ -0,0 +1,233 @@ +# -*- coding: utf8 -*- +"""Tests for the api.v3.urls module.""" + +from __future__ import print_function + +import pytest + +from locations.api.v3.remple import ( + API, + UUID_PATT, + ID_PATT, + get_collection_targeting_regex, + get_member_targeting_regex, + Resources, +) +from locations.api.v3.resources import ( + Locations, + Packages, + Spaces, + Pipelines, +) + + +class MyPackages(Packages): + # Pretend pk for /packages/ is int id + primary_key = 'id' + + +def test_urls_construction(): + """Tests that we can configure URL routing for RESTful resources using a + simple dict of resource member names. + """ + # Configure routing and get a list of corresponding Django ``url`` + # instances as ``urlpatterns`` + resources = { + 'location': {'resource_cls': Locations}, + 'package': {'resource_cls': MyPackages}, + # Make /spaces/ non-searchable + 'space': {'resource_cls': Spaces, 'searchable': False}, + 'pipeline': {'resource_cls': Pipelines}, + } + + api = API(api_version='0.1.0', service_name='Monkey') + api.register_resources(resources) + urlpatterns = api.get_urlpatterns() + + # Make assertions about ``urlpatterns`` + urlpatterns_names_regexes = sorted( + [(up.name, up.regex.pattern) for up in urlpatterns]) + expected = [ + ('location', '^locations/(?P{})/$'.format(UUID_PATT)), + ('location_edit', + '^locations/(?P{})/edit/$'.format(UUID_PATT)), + ('locations', '^locations/$'), + ('locations_new', '^locations/new/$'), + ('locations_new_search', '^locations/new_search/$'), + ('locations_search', '^locations/search/$'), + # Note the ID_PATT for /packages/ because of pk_patt above + ('package', '^packages/(?P{})/$'.format(ID_PATT)), + ('package_edit', + '^packages/(?P{})/edit/$'.format(ID_PATT)), + ('packages', '^packages/$'), + ('packages_new', '^packages/new/$'), + ('packages_new_search', '^packages/new_search/$'), + ('packages_search', '^packages/search/$'), + ('pipeline', '^pipelines/(?P{})/$'.format(UUID_PATT)), + ('pipeline_edit', + '^pipelines/(?P{})/edit/$'.format(UUID_PATT)), + ('pipelines', '^pipelines/$'), + ('pipelines_new', '^pipelines/new/$'), + ('pipelines_new_search', '^pipelines/new_search/$'), + ('pipelines_search', '^pipelines/search/$'), + # Note that the /spaces/ resource has no search-related routes. + ('space', '^spaces/(?P{})/$'.format(UUID_PATT)), + ('space_edit', '^spaces/(?P{})/edit/$'.format(UUID_PATT)), + ('spaces', '^spaces/$'), + ('spaces_new', '^spaces/new/$') + ] + assert urlpatterns_names_regexes == expected + + # Make assertions about ``api.routes`` + assert api.routes[r'^locations/$'] == { + 'http_methods': {'GET': (Locations, 'index'), + 'POST': (Locations, 'create'), + 'SEARCH': (Locations, 'search')}, + 'route_name': 'locations'} + assert api.routes[ + r'^locations/(?P{})/$'.format(UUID_PATT)] == { + 'http_methods': {'DELETE': (Locations, 'delete'), + 'GET': (Locations, 'show'), + 'PUT': (Locations, 'update')}, + 'route_name': 'location'} + assert api.routes[ + r'^locations/(?P{})/edit/$'.format(UUID_PATT)] == { + 'http_methods': {'GET': (Locations, 'edit')}, + 'route_name': 'location_edit'} + assert api.routes['^locations/new/$'] == { + 'http_methods': {'GET': (Locations, 'new')}, + 'route_name': 'locations_new'} + assert api.routes['^locations/new_search/$'] == { + 'http_methods': {'GET': (Locations, 'new_search')}, + 'route_name': 'locations_new_search'} + assert api.routes['^locations/search/$'] == { + 'http_methods': {'POST': (Locations, 'search')}, + 'route_name': 'locations_search'} + assert '^spaces/search/$' not in api.routes + assert '^pipelines/search/$' in api.routes + assert '^packages/search/$' in api.routes + assert r'^packages/(?P{})/$'.format(ID_PATT) in api.routes + assert r'^packages/(?P{})/$'.format( + UUID_PATT) not in api.routes + + +def test_regex_builders(): + """Test that the regex-building functions can build the correct regexes + given resource names as input. + """ + # Collection-targeting regex builder + assert r'^frogs/$' == get_collection_targeting_regex('frogs') + assert r'^frogs/legs/$' == get_collection_targeting_regex( + 'frogs', modifiers=['legs']) + assert r'^frogs/legs/toes/$' == get_collection_targeting_regex( + 'frogs', modifiers=['legs', 'toes']) + assert r'^frogs/l/e/g/s/$' == get_collection_targeting_regex( + 'frogs', modifiers='legs') + with pytest.raises(TypeError): + get_collection_targeting_regex('frogs', modifiers=1) + + # Member-targeting regex builder + assert r'^frogs/(?P{})/$'.format( + UUID_PATT) == get_member_targeting_regex( + 'frogs', UUID_PATT) + assert r'^frogs/(?P{})/legs/$'.format( + ID_PATT) == get_member_targeting_regex( + 'frogs', ID_PATT, modifiers=['legs']) + assert r'^frogs/(?P{})/legs/toes/$'.format( + UUID_PATT) == get_member_targeting_regex( + 'frogs', UUID_PATT, modifiers=['legs', 'toes']) + assert r'^frogs/(?P{})/l/e/g/s/$'.format( + UUID_PATT) == get_member_targeting_regex( + 'frogs', UUID_PATT, modifiers='legs') + with pytest.raises(TypeError): + get_member_targeting_regex('frogs', UUID_PATT, modifiers=1) + + +def test_standard_routes(): + """Test that standard REST ``Route()``s are yielded from the aptly-named + func. + """ + api = API(api_version='0.1.0', service_name='Elements') + class Skies(Resources): + pass + cr, dr, er, ir, nr, sr, ur = api.yield_standard_routes('sky', Skies) + + # POST /skies/ + assert cr.regex == '^skies/$' + assert cr.name == 'skies' + assert cr.http_method == 'POST' + assert cr.resource_cls == Skies + assert cr.method_name == 'create' + + # DELETE /skies// + assert dr.regex == r'^skies/(?P{})/$'.format(UUID_PATT) + assert dr.name == 'sky' + assert dr.http_method == 'DELETE' + assert dr.resource_cls == Skies + assert dr.method_name == 'delete' + + # GET /skies//edit/ + assert er.regex == r'^skies/(?P{})/edit/$'.format(UUID_PATT) + assert er.name == 'sky_edit' + assert er.http_method == 'GET' + assert er.resource_cls == Skies + assert er.method_name == 'edit' + + # GET /skies/ + assert ir.regex == '^skies/$' + assert ir.name == 'skies' + assert ir.http_method == 'GET' + assert ir.resource_cls == Skies + assert ir.method_name == 'index' + + # GET /skies/new + assert nr.regex == '^skies/new/$' + assert nr.name == 'skies_new' + assert nr.http_method == 'GET' + assert nr.resource_cls == Skies + assert nr.method_name == 'new' + + # GET /skies// + assert sr.regex == r'^skies/(?P{})/$'.format(UUID_PATT) + assert sr.name == 'sky' + assert sr.http_method == 'GET' + assert sr.resource_cls == Skies + assert sr.method_name == 'show' + + # PUT /skies// + assert ur.regex == r'^skies/(?P{})/$'.format(UUID_PATT) + assert ur.name == 'sky' + assert ur.http_method == 'PUT' + assert ur.resource_cls == Skies + assert ur.method_name == 'update' + + +def test_search_routes(): + """Test that search-related ``Route()``s are yielded from the aptly-named + func. + """ + api = API(api_version='0.1.0', service_name='Animals') + class Octopodes(Resources): + pass + r1, r2, r3 = api.yield_search_routes('octopus', Octopodes) + + # SEARCH /octopodes/ + assert r1.regex == '^octopodes/$' + assert r1.name == 'octopodes' + assert r1.http_method == 'SEARCH' + assert r1.resource_cls == Octopodes + assert r1.method_name == 'search' + + # POST /octopodes/search/ + assert r2.regex == '^octopodes/search/$' + assert r2.name == 'octopodes_search' + assert r2.http_method == 'POST' + assert r2.resource_cls == Octopodes + assert r2.method_name == 'search' + + # GET /octopodes/new_search/ + assert r3.regex == '^octopodes/new_search/$' + assert r3.name == 'octopodes_new_search' + assert r3.http_method == 'GET' + assert r3.resource_cls == Octopodes + assert r3.method_name == 'new_search' diff --git a/storage_service/locations/tests/test_querybuilder.py b/storage_service/locations/tests/test_querybuilder.py new file mode 100644 index 000000000..2936e245f --- /dev/null +++ b/storage_service/locations/tests/test_querybuilder.py @@ -0,0 +1,366 @@ +# -*- coding: utf8 -*- +"""Tests for the querybuilder module.""" + +from __future__ import print_function + +from django.db.models import Q + +from locations.models import Package +from locations.api.v3.remple import QueryBuilder + + +def test_query_expression_construction(): + """Test that the ``get_query_expression`` method can convert Python lists + to the corresponding Django Q expression. + """ + qb = QueryBuilder(Package, primary_key='uuid') + assert qb.model_name == 'Package' + + FILTER_1 = ['description', 'like', '%a%'] + qe = qb.get_query_expression(FILTER_1) + filter_obj = qe.children[0] + assert qb.errors == {} + assert isinstance(qe, Q) + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'description__contains' + assert filter_obj[1] == '%a%' + + FILTER_2 = ['origin_pipeline', 'description', 'regex', '^[JS]'] + qe = qb.get_query_expression(FILTER_2) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'origin_pipeline__description__regex' + assert filter_obj[1] == '^[JS]' + + FILTER_3A = ['origin_pipeline', '=', None] + qe = qb.get_query_expression(FILTER_3A) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'origin_pipeline__isnull' + assert filter_obj[1] is True + + FILTER_3B = ['origin_pipeline', '!=', None] + qe = qb.get_query_expression(FILTER_3B) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'origin_pipeline__isnull' + assert filter_obj[1] is False + + UUID_1 = '75a481ea-6e56-4800-81b2-6679d1e8f5ea' + UUID_2 = '90bd9d01-22f5-447c-8b4b-15578c6b8f37' + FILTER_4 = ['replicas', 'uuid', 'in', [UUID_1, UUID_2]] + qe = qb.get_query_expression(FILTER_4) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'replicas__uuid__in' + assert sorted(filter_obj[1]) == sorted([UUID_1, UUID_2]) + + FILTER_5 = ['not', ['description', 'like', '%a%']] + qe = qb.get_query_expression(FILTER_5) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.negated is True + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'description__contains' + assert filter_obj[1] == '%a%' + + FILTER_6 = ['and', [['description', 'like', '%a%'], + ['origin_pipeline', 'description', '=', + 'Well described.']]] + qe = qb.get_query_expression(FILTER_6) + assert qb.errors == {} + filter_obj_1 = qe.children[0] + filter_obj_2 = qe.children[1] + assert len(qe.children) == 2 + assert qe.negated is False + assert qe.connector == 'AND' + assert filter_obj_1[0] == 'description__contains' + assert filter_obj_1[1] == '%a%' + assert filter_obj_2[0] == 'origin_pipeline__description__exact' + assert filter_obj_2[1] == 'Well described.' + + FILTER_7 = ['or', [['description', 'like', '%a%'], + ['origin_pipeline', 'description', '=', + 'Well described.']]] + qe = qb.get_query_expression(FILTER_7) + assert qb.errors == {} + filter_obj_1 = qe.children[0] + filter_obj_2 = qe.children[1] + assert len(qe.children) == 2 + assert qe.negated is False + assert qe.connector == 'OR' + assert filter_obj_1[0] == 'description__contains' + assert filter_obj_1[1] == '%a%' + assert filter_obj_2[0] == 'origin_pipeline__description__exact' + assert filter_obj_2[1] == 'Well described.' + + FILTER_8 = ['and', [['description', 'like', '%a%'], + ['not', ['description', 'like', 'T%']], + ['or', [['size', '<', 1000], + ['size', '>', 512]]]]] + qe = qb.get_query_expression(FILTER_8) + assert qb.errors == {} + filter_obj_1 = qe.children[0] + filter_obj_2 = qe.children[1] + filter_obj_3 = qe.children[2] + assert len(qe.children) == 3 + assert qe.negated is False + assert qe.connector == 'AND' + assert filter_obj_1[0] == 'description__contains' + assert filter_obj_1[1] == '%a%' + assert filter_obj_2.negated is True + assert filter_obj_2.children[0][0] == 'description__contains' + assert filter_obj_2.children[0][1] == 'T%' + assert filter_obj_3.negated is False + assert filter_obj_3.connector == 'OR' + assert len(filter_obj_3.children) == 2 + subchild_1 = filter_obj_3.children[0] + subchild_2 = filter_obj_3.children[1] + assert subchild_1[0] == 'size__lt' + assert subchild_1[1] == 1000 + assert subchild_2[0] == 'size__gt' + assert subchild_2[1] == 512 + + qb.clear_errors() + + BAD_FILTER_1 = ['gonzo', 'like', '%a%'] + qe = qb.get_query_expression(BAD_FILTER_1) + assert qe is None + assert qb.errors['Package.gonzo'] == ( + 'Searching on Package.gonzo is not permitted') + assert qb.errors['Malformed query error'] == ( + 'The submitted query was malformed') + + qb.clear_errors() + + BAD_FILTER_2 = ['origin_pipeline', '<', 2] + qe = qb.get_query_expression(BAD_FILTER_2) + # Note: ``qe`` will be the nonsensical + # ``(AND: ('origin_pipeline__None', 2))`` here. This is ok, since the + # public method ``get_query_set`` will raise an error before executing this + # query against the db. + assert qb.errors['Package.origin_pipeline.<'] == ( + 'The relation < is not permitted for Package.origin_pipeline') + + +def test_order_by_expression_construction(): + """Test that the ``get_order_bys`` method can convert Python lists of lists + to a list of strings that the Django ORM's ``order_by`` method can use to + creat an SQL ``ORDER BY`` clause. + """ + + qb = QueryBuilder(Package) + assert qb.model_name == 'Package' + assert qb.primary_key == 'uuid' + + ORDER_BYS_1 = [['description']] + order_bys = qb.get_order_bys(ORDER_BYS_1) + assert order_bys == ['description'] + + ORDER_BYS_2 = [['description', 'desc']] + order_bys = qb.get_order_bys(ORDER_BYS_2) + assert order_bys == ['-description'] + + ORDER_BYS_3 = [['origin_pipeline', 'uuid', 'desc']] + order_bys = qb.get_order_bys(ORDER_BYS_3) + assert order_bys == ['-origin_pipeline__uuid'] + + ORDER_BYS_4 = [['origin_pipeline', 'uuid', 'asc']] + order_bys = qb.get_order_bys(ORDER_BYS_4) + assert order_bys == ['origin_pipeline__uuid'] + + ORDER_BYS_5 = [['origin_pipeline', 'monkey', 'asc']] + order_bys = qb.get_order_bys(ORDER_BYS_5) + assert qb.errors['Pipeline.monkey'] == ( + 'Searching on Pipeline.monkey is not permitted') + + ORDER_BYS_6 = [['origin_pipeline', 'uuid', 'asc'], ['description', 'desc']] + order_bys = qb.get_order_bys(ORDER_BYS_6) + assert order_bys == ['origin_pipeline__uuid', '-description'] + + + + +def test_query_dict_expression_construction(): + """Test that the ``get_query_expression`` method can convert Python dicts + to the corresponding Django Q expression. + """ + qb = QueryBuilder(Package, primary_key='uuid') + assert qb.model_name == 'Package' + + FILTER_1 = {'attribute': 'description', 'relation': 'like', 'value': '%a%'} + qe = qb.get_query_expression(FILTER_1) + filter_obj = qe.children[0] + assert qb.errors == {} + assert isinstance(qe, Q) + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'description__contains' + assert filter_obj[1] == '%a%' + + FILTER_2 = {'attribute': 'origin_pipeline', + 'subattribute': 'description', + 'relation': 'regex', + 'value': '^[JS]'} + qe = qb.get_query_expression(FILTER_2) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'origin_pipeline__description__regex' + assert filter_obj[1] == '^[JS]' + + FILTER_3A = {'attribute': 'origin_pipeline', 'relation': '=', 'value': None} + qe = qb.get_query_expression(FILTER_3A) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'origin_pipeline__isnull' + assert filter_obj[1] is True + + FILTER_3B = {'attribute': 'origin_pipeline', 'relation': '!=', + 'value': None} + qe = qb.get_query_expression(FILTER_3B) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'origin_pipeline__isnull' + assert filter_obj[1] is False + + UUID_1 = '75a481ea-6e56-4800-81b2-6679d1e8f5ea' + UUID_2 = '90bd9d01-22f5-447c-8b4b-15578c6b8f37' + FILTER_4 = {'attribute': 'replicas', + 'subattribute': 'uuid', + 'relation': 'in', + 'value': [UUID_1, UUID_2]} + qe = qb.get_query_expression(FILTER_4) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'replicas__uuid__in' + assert sorted(filter_obj[1]) == sorted([UUID_1, UUID_2]) + + FILTER_5 = {'negation': 'not', + 'complement': {'attribute': 'description', + 'relation': 'like', + 'value': '%a%'}} + qe = qb.get_query_expression(FILTER_5) + filter_obj = qe.children[0] + assert qb.errors == {} + assert qe.negated is True + assert qe.connector == 'AND' + assert len(qe.children) == 1 + assert filter_obj[0] == 'description__contains' + assert filter_obj[1] == '%a%' + + FILTER_6 = {'conjunction': 'and', + 'complement': [{'attribute': 'description', + 'relation': 'like', + 'value': '%a%'}, + {'attribute': 'origin_pipeline', + 'subattribute': 'description', + 'relation': '=', + 'value': 'Well described.'}]} + qe = qb.get_query_expression(FILTER_6) + assert qb.errors == {} + filter_obj_1 = qe.children[0] + filter_obj_2 = qe.children[1] + assert len(qe.children) == 2 + assert qe.negated is False + assert qe.connector == 'AND' + assert filter_obj_1[0] == 'description__contains' + assert filter_obj_1[1] == '%a%' + assert filter_obj_2[0] == 'origin_pipeline__description__exact' + assert filter_obj_2[1] == 'Well described.' + + FILTER_7 = {'conjunction': 'or', + 'complement':[{'attribute': 'description', + 'relation': 'like', + 'value': '%a%'}, + {'attribute': 'origin_pipeline', + 'subattribute': 'description', + 'relation': '=', + 'value': 'Well described.'}]} + qe = qb.get_query_expression(FILTER_7) + assert qb.errors == {} + filter_obj_1 = qe.children[0] + filter_obj_2 = qe.children[1] + assert len(qe.children) == 2 + assert qe.negated is False + assert qe.connector == 'OR' + assert filter_obj_1[0] == 'description__contains' + assert filter_obj_1[1] == '%a%' + assert filter_obj_2[0] == 'origin_pipeline__description__exact' + assert filter_obj_2[1] == 'Well described.' + + FILTER_8 = {'conjunction': 'and', + 'complement': [{'attribute': 'description', + 'relation': 'like', + 'value': '%a%'}, + {'negation': 'not', + 'complement': {'attribute': 'description', + 'relation': 'like', + 'value': 'T%'}}, + {'conjunction': 'or', + 'complement': [{'attribute': 'size', + 'relation': '<', + 'value': 1000}, + {'attribute': 'size', + 'relation': '>', + 'value': 512}]}]} + qe = qb.get_query_expression(FILTER_8) + assert qb.errors == {} + filter_obj_1 = qe.children[0] + filter_obj_2 = qe.children[1] + filter_obj_3 = qe.children[2] + assert len(qe.children) == 3 + assert qe.negated is False + assert qe.connector == 'AND' + assert filter_obj_1[0] == 'description__contains' + assert filter_obj_1[1] == '%a%' + assert filter_obj_2.negated is True + assert filter_obj_2.children[0][0] == 'description__contains' + assert filter_obj_2.children[0][1] == 'T%' + assert filter_obj_3.negated is False + assert filter_obj_3.connector == 'OR' + assert len(filter_obj_3.children) == 2 + subchild_1 = filter_obj_3.children[0] + subchild_2 = filter_obj_3.children[1] + assert subchild_1[0] == 'size__lt' + assert subchild_1[1] == 1000 + assert subchild_2[0] == 'size__gt' + assert subchild_2[1] == 512 + + qb.clear_errors() + + BAD_FILTER_1 = {'attribute': 'gonzo', 'relation': 'like', 'value': '%a%'} + qe = qb.get_query_expression(BAD_FILTER_1) + assert qe is None + assert qb.errors['Package.gonzo'] == ( + 'Searching on Package.gonzo is not permitted') + assert qb.errors['Malformed query error'] == ( + 'The submitted query was malformed') + + qb.clear_errors() + + BAD_FILTER_2 = {'attribute': 'origin_pipeline', 'relation': '<', 'value': 2} + qe = qb.get_query_expression(BAD_FILTER_2) + # Note: ``qe`` will be the nonsensical + # ``(AND: ('origin_pipeline__None', 2))`` here. This is ok, since the + # public method ``get_query_set`` will raise an error before executing this + # query against the db. + assert qb.errors['Package.origin_pipeline.<'] == ( + 'The relation < is not permitted for Package.origin_pipeline') diff --git a/storage_service/static/openapi/favicon-16x16.png b/storage_service/static/openapi/favicon-16x16.png new file mode 100644 index 000000000..0f7e13b0d Binary files /dev/null and b/storage_service/static/openapi/favicon-16x16.png differ diff --git a/storage_service/static/openapi/favicon-32x32.png b/storage_service/static/openapi/favicon-32x32.png new file mode 100644 index 000000000..b0a3352ff Binary files /dev/null and b/storage_service/static/openapi/favicon-32x32.png differ diff --git a/storage_service/static/openapi/index.html b/storage_service/static/openapi/index.html new file mode 100644 index 000000000..ab7ea2908 --- /dev/null +++ b/storage_service/static/openapi/index.html @@ -0,0 +1,69 @@ + + + + + + Swagger UI + + + + + + + + +
+ + + + + + + diff --git a/storage_service/static/openapi/oauth2-redirect.html b/storage_service/static/openapi/oauth2-redirect.html new file mode 100644 index 000000000..fb68399d2 --- /dev/null +++ b/storage_service/static/openapi/oauth2-redirect.html @@ -0,0 +1,67 @@ + + + + + + diff --git a/storage_service/static/openapi/swagger-ui-bundle.js b/storage_service/static/openapi/swagger-ui-bundle.js new file mode 100644 index 000000000..8904027f5 --- /dev/null +++ b/storage_service/static/openapi/swagger-ui-bundle.js @@ -0,0 +1,104 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUIBundle=t():e.SwaggerUIBundle=t()}(this,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=1167)}([function(e,t,n){"use strict";e.exports=n(92)},function(e,t,n){e.exports=n(951)()},function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}},function(e,t,n){"use strict";t.__esModule=!0;var r,i=n(325),o=(r=i)&&r.__esModule?r:{default:r};t.default=function(){function e(e,t){for(var n=0;n>>0;if(""+n!==t||4294967295===n)return NaN;t=n}return t<0?S(e)+t:t}function A(){return!0}function D(e,t,n){return(0===e||void 0!==n&&e<=-n)&&(void 0===t||void 0!==n&&t>=n)}function M(e,t){return T(e,t,0)}function O(e,t){return T(e,t,t)}function T(e,t,n){return void 0===e?n:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var P=0,I=1,R=2,N="function"==typeof Symbol&&Symbol.iterator,F="@@iterator",j=N||F;function B(e){this.next=e}function L(e,t,n,r){var i=0===e?t:1===e?n:[t,n];return r?r.value=i:r={value:i,done:!1},r}function q(){return{value:void 0,done:!0}}function z(e){return!!V(e)}function U(e){return e&&"function"==typeof e.next}function W(e){var t=V(e);return t&&t.call(e)}function V(e){var t=e&&(N&&e[N]||e[F]);if("function"==typeof t)return t}function H(e){return e&&"number"==typeof e.length}function J(e){return null===e||void 0===e?oe():a(e)?e.toSeq():function(e){var t=ue(e)||"object"==typeof e&&new te(e);if(!t)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+e);return t}(e)}function G(e){return null===e||void 0===e?oe().toKeyedSeq():a(e)?s(e)?e.toSeq():e.fromEntrySeq():ae(e)}function K(e){return null===e||void 0===e?oe():a(e)?s(e)?e.entrySeq():e.toIndexedSeq():se(e)}function X(e){return(null===e||void 0===e?oe():a(e)?s(e)?e.entrySeq():e:se(e)).toSetSeq()}B.prototype.toString=function(){return"[Iterator]"},B.KEYS=P,B.VALUES=I,B.ENTRIES=R,B.prototype.inspect=B.prototype.toSource=function(){return this.toString()},B.prototype[j]=function(){return this},t(J,n),J.of=function(){return J(arguments)},J.prototype.toSeq=function(){return this},J.prototype.toString=function(){return this.__toString("Seq {","}")},J.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},J.prototype.__iterate=function(e,t){return le(this,e,t,!0)},J.prototype.__iterator=function(e,t){return ce(this,e,t,!0)},t(G,J),G.prototype.toKeyedSeq=function(){return this},t(K,J),K.of=function(){return K(arguments)},K.prototype.toIndexedSeq=function(){return this},K.prototype.toString=function(){return this.__toString("Seq [","]")},K.prototype.__iterate=function(e,t){return le(this,e,t,!1)},K.prototype.__iterator=function(e,t){return ce(this,e,t,!1)},t(X,J),X.of=function(){return X(arguments)},X.prototype.toSetSeq=function(){return this},J.isSeq=ie,J.Keyed=G,J.Set=X,J.Indexed=K;var Y,$,Z,Q="@@__IMMUTABLE_SEQ__@@";function ee(e){this._array=e,this.size=e.length}function te(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function ne(e){this._iterable=e,this.size=e.length||e.size}function re(e){this._iterator=e,this._iteratorCache=[]}function ie(e){return!(!e||!e[Q])}function oe(){return Y||(Y=new ee([]))}function ae(e){var t=Array.isArray(e)?new ee(e).fromEntrySeq():U(e)?new re(e).fromEntrySeq():z(e)?new ne(e).fromEntrySeq():"object"==typeof e?new te(e):void 0;if(!t)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+e);return t}function se(e){var t=ue(e);if(!t)throw new TypeError("Expected Array or iterable object of values: "+e);return t}function ue(e){return H(e)?new ee(e):U(e)?new re(e):z(e)?new ne(e):void 0}function le(e,t,n,r){var i=e._cache;if(i){for(var o=i.length-1,a=0;a<=o;a++){var s=i[n?o-a:a];if(!1===t(s[1],r?s[0]:a,e))return a+1}return a}return e.__iterateUncached(t,n)}function ce(e,t,n,r){var i=e._cache;if(i){var o=i.length-1,a=0;return new B(function(){var e=i[n?o-a:a];return a++>o?{value:void 0,done:!0}:L(t,r?e[0]:a-1,e[1])})}return e.__iteratorUncached(t,n)}function pe(e,t){return t?function e(t,n,r,i){if(Array.isArray(n))return t.call(i,r,K(n).map(function(r,i){return e(t,r,i,n)}));if(he(n))return t.call(i,r,G(n).map(function(r,i){return e(t,r,i,n)}));return n}(t,e,"",{"":e}):fe(e)}function fe(e){return Array.isArray(e)?K(e).map(fe).toList():he(e)?G(e).map(fe).toMap():e}function he(e){return e&&(e.constructor===Object||void 0===e.constructor)}function de(e,t){if(e===t||e!=e&&t!=t)return!0;if(!e||!t)return!1;if("function"==typeof e.valueOf&&"function"==typeof t.valueOf){if((e=e.valueOf())===(t=t.valueOf())||e!=e&&t!=t)return!0;if(!e||!t)return!1}return!("function"!=typeof e.equals||"function"!=typeof t.equals||!e.equals(t))}function me(e,t){if(e===t)return!0;if(!a(t)||void 0!==e.size&&void 0!==t.size&&e.size!==t.size||void 0!==e.__hash&&void 0!==t.__hash&&e.__hash!==t.__hash||s(e)!==s(t)||u(e)!==u(t)||c(e)!==c(t))return!1;if(0===e.size&&0===t.size)return!0;var n=!l(e);if(c(e)){var r=e.entries();return t.every(function(e,t){var i=r.next().value;return i&&de(i[1],e)&&(n||de(i[0],t))})&&r.next().done}var i=!1;if(void 0===e.size)if(void 0===t.size)"function"==typeof e.cacheResult&&e.cacheResult();else{i=!0;var o=e;e=t,t=o}var p=!0,f=t.__iterate(function(t,r){if(n?!e.has(t):i?!de(t,e.get(r,y)):!de(e.get(r,y),t))return p=!1,!1});return p&&e.size===f}function ve(e,t){if(!(this instanceof ve))return new ve(e,t);if(this._value=e,this.size=void 0===t?1/0:Math.max(0,t),0===this.size){if($)return $;$=this}}function ge(e,t){if(!e)throw new Error(t)}function ye(e,t,n){if(!(this instanceof ye))return new ye(e,t,n);if(ge(0!==n,"Cannot step a Range by 0"),e=e||0,void 0===t&&(t=1/0),n=void 0===n?1:Math.abs(n),tr?{value:void 0,done:!0}:L(e,i,n[t?r-i++:i++])})},t(te,G),te.prototype.get=function(e,t){return void 0===t||this.has(e)?this._object[e]:t},te.prototype.has=function(e){return this._object.hasOwnProperty(e)},te.prototype.__iterate=function(e,t){for(var n=this._object,r=this._keys,i=r.length-1,o=0;o<=i;o++){var a=r[t?i-o:o];if(!1===e(n[a],a,this))return o+1}return o},te.prototype.__iterator=function(e,t){var n=this._object,r=this._keys,i=r.length-1,o=0;return new B(function(){var a=r[t?i-o:o];return o++>i?{value:void 0,done:!0}:L(e,a,n[a])})},te.prototype[d]=!0,t(ne,K),ne.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);var n=W(this._iterable),r=0;if(U(n))for(var i;!(i=n.next()).done&&!1!==e(i.value,r++,this););return r},ne.prototype.__iteratorUncached=function(e,t){if(t)return this.cacheResult().__iterator(e,t);var n=W(this._iterable);if(!U(n))return new B(q);var r=0;return new B(function(){var t=n.next();return t.done?t:L(e,r++,t.value)})},t(re,K),re.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);for(var n,r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var t=n.next();if(t.done)return t;r[i]=t.value}return L(e,i,r[i++])})},t(ve,K),ve.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},ve.prototype.get=function(e,t){return this.has(e)?this._value:t},ve.prototype.includes=function(e){return de(this._value,e)},ve.prototype.slice=function(e,t){var n=this.size;return D(e,t,n)?this:new ve(this._value,O(t,n)-M(e,n))},ve.prototype.reverse=function(){return this},ve.prototype.indexOf=function(e){return de(this._value,e)?0:-1},ve.prototype.lastIndexOf=function(e){return de(this._value,e)?this.size:-1},ve.prototype.__iterate=function(e,t){for(var n=0;n=0&&t=0&&nn?{value:void 0,done:!0}:L(e,o++,a)})},ye.prototype.equals=function(e){return e instanceof ye?this._start===e._start&&this._end===e._end&&this._step===e._step:me(this,e)},t(_e,n),t(be,_e),t(xe,_e),t(ke,_e),_e.Keyed=be,_e.Indexed=xe,_e.Set=ke;var we="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(e,t){var n=65535&(e|=0),r=65535&(t|=0);return n*r+((e>>>16)*r+n*(t>>>16)<<16>>>0)|0};function Ee(e){return e>>>1&1073741824|3221225471&e}function Se(e){if(!1===e||null===e||void 0===e)return 0;if("function"==typeof e.valueOf&&(!1===(e=e.valueOf())||null===e||void 0===e))return 0;if(!0===e)return 1;var t=typeof e;if("number"===t){if(e!=e||e===1/0)return 0;var n=0|e;for(n!==e&&(n^=4294967295*e);e>4294967295;)n^=e/=4294967295;return Ee(n)}if("string"===t)return e.length>Ie?function(e){var t=Fe[e];void 0===t&&(t=Ce(e),Ne===Re&&(Ne=0,Fe={}),Ne++,Fe[e]=t);return t}(e):Ce(e);if("function"==typeof e.hashCode)return e.hashCode();if("object"===t)return function(e){var t;if(Oe&&void 0!==(t=Me.get(e)))return t;if(void 0!==(t=e[Pe]))return t;if(!De){if(void 0!==(t=e.propertyIsEnumerable&&e.propertyIsEnumerable[Pe]))return t;if(void 0!==(t=function(e){if(e&&e.nodeType>0)switch(e.nodeType){case 1:return e.uniqueID;case 9:return e.documentElement&&e.documentElement.uniqueID}}(e)))return t}t=++Te,1073741824&Te&&(Te=0);if(Oe)Me.set(e,t);else{if(void 0!==Ae&&!1===Ae(e))throw new Error("Non-extensible objects are not allowed as keys.");if(De)Object.defineProperty(e,Pe,{enumerable:!1,configurable:!1,writable:!1,value:t});else if(void 0!==e.propertyIsEnumerable&&e.propertyIsEnumerable===e.constructor.prototype.propertyIsEnumerable)e.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},e.propertyIsEnumerable[Pe]=t;else{if(void 0===e.nodeType)throw new Error("Unable to set a non-enumerable property on object.");e[Pe]=t}}return t}(e);if("function"==typeof e.toString)return Ce(e.toString());throw new Error("Value type "+t+" cannot be hashed.")}function Ce(e){for(var t=0,n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}})},Be.prototype.toString=function(){return this.__toString("Map {","}")},Be.prototype.get=function(e,t){return this._root?this._root.get(0,void 0,e,t):t},Be.prototype.set=function(e,t){return Qe(this,e,t)},Be.prototype.setIn=function(e,t){return this.updateIn(e,y,function(){return t})},Be.prototype.remove=function(e){return Qe(this,e,y)},Be.prototype.deleteIn=function(e){return this.updateIn(e,function(){return y})},Be.prototype.update=function(e,t,n){return 1===arguments.length?e(this):this.updateIn([e],t,n)},Be.prototype.updateIn=function(e,t,n){n||(n=t,t=void 0);var r=function e(t,n,r,i){var o=t===y;var a=n.next();if(a.done){var s=o?r:t,u=i(s);return u===s?t:u}ge(o||t&&t.set,"invalid keyPath");var l=a.value;var c=o?y:t.get(l,y);var p=e(c,n,r,i);return p===c?t:p===y?t.remove(l):(o?Ze():t).set(l,p)}(this,nn(e),t,n);return r===y?void 0:r},Be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Ze()},Be.prototype.merge=function(){return rt(this,void 0,arguments)},Be.prototype.mergeWith=function(t){return rt(this,t,e.call(arguments,1))},Be.prototype.mergeIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,Ze(),function(e){return"function"==typeof e.merge?e.merge.apply(e,n):n[n.length-1]})},Be.prototype.mergeDeep=function(){return rt(this,it,arguments)},Be.prototype.mergeDeepWith=function(t){var n=e.call(arguments,1);return rt(this,ot(t),n)},Be.prototype.mergeDeepIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,Ze(),function(e){return"function"==typeof e.mergeDeep?e.mergeDeep.apply(e,n):n[n.length-1]})},Be.prototype.sort=function(e){return Mt(Ht(this,e))},Be.prototype.sortBy=function(e,t){return Mt(Ht(this,t,e))},Be.prototype.withMutations=function(e){var t=this.asMutable();return e(t),t.wasAltered()?t.__ensureOwner(this.__ownerID):this},Be.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new w)},Be.prototype.asImmutable=function(){return this.__ensureOwner()},Be.prototype.wasAltered=function(){return this.__altered},Be.prototype.__iterator=function(e,t){return new Ke(this,e,t)},Be.prototype.__iterate=function(e,t){var n=this,r=0;return this._root&&this._root.iterate(function(t){return r++,e(t[1],t[0],n)},t),r},Be.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?$e(this.size,this._root,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},Be.isMap=Le;var qe,ze="@@__IMMUTABLE_MAP__@@",Ue=Be.prototype;function We(e,t){this.ownerID=e,this.entries=t}function Ve(e,t,n){this.ownerID=e,this.bitmap=t,this.nodes=n}function He(e,t,n){this.ownerID=e,this.count=t,this.nodes=n}function Je(e,t,n){this.ownerID=e,this.keyHash=t,this.entries=n}function Ge(e,t,n){this.ownerID=e,this.keyHash=t,this.entry=n}function Ke(e,t,n){this._type=t,this._reverse=n,this._stack=e._root&&Ye(e._root)}function Xe(e,t){return L(e,t[0],t[1])}function Ye(e,t){return{node:e,index:0,__prev:t}}function $e(e,t,n,r){var i=Object.create(Ue);return i.size=e,i._root=t,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Ze(){return qe||(qe=$e(0))}function Qe(e,t,n){var r,i;if(e._root){var o=x(_),a=x(b);if(r=et(e._root,e.__ownerID,0,void 0,t,n,o,a),!a.value)return e;i=e.size+(o.value?n===y?-1:1:0)}else{if(n===y)return e;i=1,r=new We(e.__ownerID,[[t,n]])}return e.__ownerID?(e.size=i,e._root=r,e.__hash=void 0,e.__altered=!0,e):r?$e(i,r):Ze()}function et(e,t,n,r,i,o,a,s){return e?e.update(t,n,r,i,o,a,s):o===y?e:(k(s),k(a),new Ge(t,r,[i,o]))}function tt(e){return e.constructor===Ge||e.constructor===Je}function nt(e,t,n,r,i){if(e.keyHash===r)return new Je(t,r,[e.entry,i]);var o,a=(0===n?e.keyHash:e.keyHash>>>n)&g,s=(0===n?r:r>>>n)&g;return new Ve(t,1<>1&1431655765))+(e>>2&858993459))+(e>>4)&252645135,e+=e>>8,127&(e+=e>>16)}function ut(e,t,n,r){var i=r?e:E(e);return i[t]=n,i}Ue[ze]=!0,Ue.delete=Ue.remove,Ue.removeIn=Ue.deleteIn,We.prototype.get=function(e,t,n,r){for(var i=this.entries,o=0,a=i.length;o=lt)return function(e,t,n,r){e||(e=new w);for(var i=new Ge(e,Se(n),[n,r]),o=0;o>>e)&g),o=this.bitmap;return 0==(o&i)?r:this.nodes[st(o&i-1)].get(e+m,t,n,r)},Ve.prototype.update=function(e,t,n,r,i,o,a){void 0===n&&(n=Se(r));var s=(0===t?n:n>>>t)&g,u=1<=ct)return function(e,t,n,r,i){for(var o=0,a=new Array(v),s=0;0!==n;s++,n>>>=1)a[s]=1&n?t[o++]:void 0;return a[r]=i,new He(e,o+1,a)}(e,f,l,s,d);if(c&&!d&&2===f.length&&tt(f[1^p]))return f[1^p];if(c&&d&&1===f.length&&tt(d))return d;var _=e&&e===this.ownerID,b=c?d?l:l^u:l|u,x=c?d?ut(f,p,d,_):function(e,t,n){var r=e.length-1;if(n&&t===r)return e.pop(),e;for(var i=new Array(r),o=0,a=0;a>>e)&g,o=this.nodes[i];return o?o.get(e+m,t,n,r):r},He.prototype.update=function(e,t,n,r,i,o,a){void 0===n&&(n=Se(r));var s=(0===t?n:n>>>t)&g,u=i===y,l=this.nodes,c=l[s];if(u&&!c)return this;var p=et(c,e,t+m,n,r,i,o,a);if(p===c)return this;var f=this.count;if(c){if(!p&&--f0&&r=0&&e=e.size||t<0)return e.withMutations(function(e){t<0?Ct(e,t).set(0,n):Ct(e,0,t+1).set(t,n)});t+=e._origin;var r=e._tail,i=e._root,o=x(b);t>=Dt(e._capacity)?r=wt(r,e.__ownerID,0,t,n,o):i=wt(i,e.__ownerID,e._level,t,n,o);if(!o.value)return e;if(e.__ownerID)return e._root=i,e._tail=r,e.__hash=void 0,e.__altered=!0,e;return xt(e._origin,e._capacity,e._level,i,r)}(this,e,t)},ft.prototype.remove=function(e){return this.has(e)?0===e?this.shift():e===this.size-1?this.pop():this.splice(e,1):this},ft.prototype.insert=function(e,t){return this.splice(e,0,t)},ft.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=m,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):kt()},ft.prototype.push=function(){var e=arguments,t=this.size;return this.withMutations(function(n){Ct(n,0,t+e.length);for(var r=0;r>>t&g;if(r>=this.array.length)return new vt([],e);var i,o=0===r;if(t>0){var a=this.array[r];if((i=a&&a.removeBefore(e,t-m,n))===a&&o)return this}if(o&&!i)return this;var s=Et(this,e);if(!o)for(var u=0;u>>t&g;if(i>=this.array.length)return this;if(t>0){var o=this.array[i];if((r=o&&o.removeAfter(e,t-m,n))===o&&i===this.array.length-1)return this}var a=Et(this,e);return a.array.splice(i+1),r&&(a.array[i]=r),a};var gt,yt,_t={};function bt(e,t){var n=e._origin,r=e._capacity,i=Dt(r),o=e._tail;return a(e._root,e._level,0);function a(e,s,u){return 0===s?function(e,a){var s=a===i?o&&o.array:e&&e.array,u=a>n?0:n-a,l=r-a;l>v&&(l=v);return function(){if(u===l)return _t;var e=t?--l:u++;return s&&s[e]}}(e,u):function(e,i,o){var s,u=e&&e.array,l=o>n?0:n-o>>i,c=1+(r-o>>i);c>v&&(c=v);return function(){for(;;){if(s){var e=s();if(e!==_t)return e;s=null}if(l===c)return _t;var n=t?--c:l++;s=a(u&&u[n],i-m,o+(n<>>n&g,u=e&&s0){var l=e&&e.array[s],c=wt(l,t,n-m,r,i,o);return c===l?e:((a=Et(e,t)).array[s]=c,a)}return u&&e.array[s]===i?e:(k(o),a=Et(e,t),void 0===i&&s===a.array.length-1?a.array.pop():a.array[s]=i,a)}function Et(e,t){return t&&e&&t===e.ownerID?e:new vt(e?e.array.slice():[],t)}function St(e,t){if(t>=Dt(e._capacity))return e._tail;if(t<1<0;)n=n.array[t>>>r&g],r-=m;return n}}function Ct(e,t,n){void 0!==t&&(t|=0),void 0!==n&&(n|=0);var r=e.__ownerID||new w,i=e._origin,o=e._capacity,a=i+t,s=void 0===n?o:n<0?o+n:i+n;if(a===i&&s===o)return e;if(a>=s)return e.clear();for(var u=e._level,l=e._root,c=0;a+c<0;)l=new vt(l&&l.array.length?[void 0,l]:[],r),c+=1<<(u+=m);c&&(a+=c,i+=c,s+=c,o+=c);for(var p=Dt(o),f=Dt(s);f>=1<p?new vt([],r):h;if(h&&f>p&&am;y-=m){var _=p>>>y&g;v=v.array[_]=Et(v.array[_],r)}v.array[p>>>m&g]=h}if(s=f)a-=f,s-=f,u=m,l=null,d=d&&d.removeBefore(r,0,a);else if(a>i||f>>u&g;if(b!==f>>>u&g)break;b&&(c+=(1<i&&(l=l.removeBefore(r,u,a-c)),l&&fo&&(o=l.size),a(u)||(l=l.map(function(e){return pe(e)})),r.push(l)}return o>e.size&&(e=e.setSize(o)),at(e,t,r)}function Dt(e){return e>>m<=v&&a.size>=2*o.size?(r=(i=a.filter(function(e,t){return void 0!==e&&s!==t})).toKeyedSeq().map(function(e){return e[0]}).flip().toMap(),e.__ownerID&&(r.__ownerID=i.__ownerID=e.__ownerID)):(r=o.remove(t),i=s===a.size-1?a.pop():a.set(s,void 0))}else if(u){if(n===a.get(s)[1])return e;r=o,i=a.set(s,[t,n])}else r=o.set(t,a.size),i=a.set(a.size,[t,n]);return e.__ownerID?(e.size=r.size,e._map=r,e._list=i,e.__hash=void 0,e):Tt(r,i)}function Rt(e,t){this._iter=e,this._useKeys=t,this.size=e.size}function Nt(e){this._iter=e,this.size=e.size}function Ft(e){this._iter=e,this.size=e.size}function jt(e){this._iter=e,this.size=e.size}function Bt(e){var t=Qt(e);return t._iter=e,t.size=e.size,t.flip=function(){return e},t.reverse=function(){var t=e.reverse.apply(this);return t.flip=function(){return e.reverse()},t},t.has=function(t){return e.includes(t)},t.includes=function(t){return e.has(t)},t.cacheResult=en,t.__iterateUncached=function(t,n){var r=this;return e.__iterate(function(e,n){return!1!==t(n,e,r)},n)},t.__iteratorUncached=function(t,n){if(t===R){var r=e.__iterator(t,n);return new B(function(){var e=r.next();if(!e.done){var t=e.value[0];e.value[0]=e.value[1],e.value[1]=t}return e})}return e.__iterator(t===I?P:I,n)},t}function Lt(e,t,n){var r=Qt(e);return r.size=e.size,r.has=function(t){return e.has(t)},r.get=function(r,i){var o=e.get(r,y);return o===y?i:t.call(n,o,r,e)},r.__iterateUncached=function(r,i){var o=this;return e.__iterate(function(e,i,a){return!1!==r(t.call(n,e,i,a),i,o)},i)},r.__iteratorUncached=function(r,i){var o=e.__iterator(R,i);return new B(function(){var i=o.next();if(i.done)return i;var a=i.value,s=a[0];return L(r,s,t.call(n,a[1],s,e),i)})},r}function qt(e,t){var n=Qt(e);return n._iter=e,n.size=e.size,n.reverse=function(){return e},e.flip&&(n.flip=function(){var t=Bt(e);return t.reverse=function(){return e.flip()},t}),n.get=function(n,r){return e.get(t?n:-1-n,r)},n.has=function(n){return e.has(t?n:-1-n)},n.includes=function(t){return e.includes(t)},n.cacheResult=en,n.__iterate=function(t,n){var r=this;return e.__iterate(function(e,n){return t(e,n,r)},!n)},n.__iterator=function(t,n){return e.__iterator(t,!n)},n}function zt(e,t,n,r){var i=Qt(e);return r&&(i.has=function(r){var i=e.get(r,y);return i!==y&&!!t.call(n,i,r,e)},i.get=function(r,i){var o=e.get(r,y);return o!==y&&t.call(n,o,r,e)?o:i}),i.__iterateUncached=function(i,o){var a=this,s=0;return e.__iterate(function(e,o,u){if(t.call(n,e,o,u))return s++,i(e,r?o:s-1,a)},o),s},i.__iteratorUncached=function(i,o){var a=e.__iterator(R,o),s=0;return new B(function(){for(;;){var o=a.next();if(o.done)return o;var u=o.value,l=u[0],c=u[1];if(t.call(n,c,l,e))return L(i,r?l:s++,c,o)}})},i}function Ut(e,t,n,r){var i=e.size;if(void 0!==t&&(t|=0),void 0!==n&&(n===1/0?n=i:n|=0),D(t,n,i))return e;var o=M(t,i),a=O(n,i);if(o!=o||a!=a)return Ut(e.toSeq().cacheResult(),t,n,r);var s,u=a-o;u==u&&(s=u<0?0:u);var l=Qt(e);return l.size=0===s?s:e.size&&s||void 0,!r&&ie(e)&&s>=0&&(l.get=function(t,n){return(t=C(this,t))>=0&&ts)return{value:void 0,done:!0};var e=i.next();return r||t===I?e:L(t,u-1,t===P?void 0:e.value[1],e)})},l}function Wt(e,t,n,r){var i=Qt(e);return i.__iterateUncached=function(i,o){var a=this;if(o)return this.cacheResult().__iterate(i,o);var s=!0,u=0;return e.__iterate(function(e,o,l){if(!s||!(s=t.call(n,e,o,l)))return u++,i(e,r?o:u-1,a)}),u},i.__iteratorUncached=function(i,o){var a=this;if(o)return this.cacheResult().__iterator(i,o);var s=e.__iterator(R,o),u=!0,l=0;return new B(function(){var e,o,c;do{if((e=s.next()).done)return r||i===I?e:L(i,l++,i===P?void 0:e.value[1],e);var p=e.value;o=p[0],c=p[1],u&&(u=t.call(n,c,o,a))}while(u);return i===R?e:L(i,o,c,e)})},i}function Vt(e,t,n){var r=Qt(e);return r.__iterateUncached=function(r,i){var o=0,s=!1;return function e(u,l){var c=this;u.__iterate(function(i,u){return(!t||l0}function Kt(e,t,r){var i=Qt(e);return i.size=new ee(r).map(function(e){return e.size}).min(),i.__iterate=function(e,t){for(var n,r=this.__iterator(I,t),i=0;!(n=r.next()).done&&!1!==e(n.value,i++,this););return i},i.__iteratorUncached=function(e,i){var o=r.map(function(e){return e=n(e),W(i?e.reverse():e)}),a=0,s=!1;return new B(function(){var n;return s||(n=o.map(function(e){return e.next()}),s=n.some(function(e){return e.done})),s?{value:void 0,done:!0}:L(e,a++,t.apply(null,n.map(function(e){return e.value})))})},i}function Xt(e,t){return ie(e)?t:e.constructor(t)}function Yt(e){if(e!==Object(e))throw new TypeError("Expected [K, V] tuple: "+e)}function $t(e){return je(e.size),S(e)}function Zt(e){return s(e)?r:u(e)?i:o}function Qt(e){return Object.create((s(e)?G:u(e)?K:X).prototype)}function en(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):J.prototype.cacheResult.call(this)}function tn(e,t){return e>t?1:e=0;n--)t={value:arguments[n],next:t};return this.__ownerID?(this.size=e,this._head=t,this.__hash=void 0,this.__altered=!0,this):An(e,t)},kn.prototype.pushAll=function(e){if(0===(e=i(e)).size)return this;je(e.size);var t=this.size,n=this._head;return e.reverse().forEach(function(e){t++,n={value:e,next:n}}),this.__ownerID?(this.size=t,this._head=n,this.__hash=void 0,this.__altered=!0,this):An(t,n)},kn.prototype.pop=function(){return this.slice(1)},kn.prototype.unshift=function(){return this.push.apply(this,arguments)},kn.prototype.unshiftAll=function(e){return this.pushAll(e)},kn.prototype.shift=function(){return this.pop.apply(this,arguments)},kn.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Dn()},kn.prototype.slice=function(e,t){if(D(e,t,this.size))return this;var n=M(e,this.size);if(O(t,this.size)!==this.size)return xe.prototype.slice.call(this,e,t);for(var r=this.size-n,i=this._head;n--;)i=i.next;return this.__ownerID?(this.size=r,this._head=i,this.__hash=void 0,this.__altered=!0,this):An(r,i)},kn.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?An(this.size,this._head,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},kn.prototype.__iterate=function(e,t){if(t)return this.reverse().__iterate(e);for(var n=0,r=this._head;r&&!1!==e(r.value,n++,this);)r=r.next;return n},kn.prototype.__iterator=function(e,t){if(t)return this.reverse().__iterator(e);var n=0,r=this._head;return new B(function(){if(r){var t=r.value;return r=r.next,L(e,n++,t)}return{value:void 0,done:!0}})},kn.isStack=wn;var En,Sn="@@__IMMUTABLE_STACK__@@",Cn=kn.prototype;function An(e,t,n,r){var i=Object.create(Cn);return i.size=e,i._head=t,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Dn(){return En||(En=An(0))}function Mn(e,t){var n=function(n){e.prototype[n]=t[n]};return Object.keys(t).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(t).forEach(n),e}Cn[Sn]=!0,Cn.withMutations=Ue.withMutations,Cn.asMutable=Ue.asMutable,Cn.asImmutable=Ue.asImmutable,Cn.wasAltered=Ue.wasAltered,n.Iterator=B,Mn(n,{toArray:function(){je(this.size);var e=new Array(this.size||0);return this.valueSeq().__iterate(function(t,n){e[n]=t}),e},toIndexedSeq:function(){return new Nt(this)},toJS:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJS?e.toJS():e}).__toJS()},toJSON:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJSON?e.toJSON():e}).__toJS()},toKeyedSeq:function(){return new Rt(this,!0)},toMap:function(){return Be(this.toKeyedSeq())},toObject:function(){je(this.size);var e={};return this.__iterate(function(t,n){e[n]=t}),e},toOrderedMap:function(){return Mt(this.toKeyedSeq())},toOrderedSet:function(){return vn(s(this)?this.valueSeq():this)},toSet:function(){return un(s(this)?this.valueSeq():this)},toSetSeq:function(){return new Ft(this)},toSeq:function(){return u(this)?this.toIndexedSeq():s(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return kn(s(this)?this.valueSeq():this)},toList:function(){return ft(s(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(e,t){return 0===this.size?e+t:e+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+t},concat:function(){return Xt(this,function(e,t){var n=s(e),i=[e].concat(t).map(function(e){return a(e)?n&&(e=r(e)):e=n?ae(e):se(Array.isArray(e)?e:[e]),e}).filter(function(e){return 0!==e.size});if(0===i.length)return e;if(1===i.length){var o=i[0];if(o===e||n&&s(o)||u(e)&&u(o))return o}var l=new ee(i);return n?l=l.toKeyedSeq():u(e)||(l=l.toSetSeq()),(l=l.flatten(!0)).size=i.reduce(function(e,t){if(void 0!==e){var n=t.size;if(void 0!==n)return e+n}},0),l}(this,e.call(arguments,0)))},includes:function(e){return this.some(function(t){return de(t,e)})},entries:function(){return this.__iterator(R)},every:function(e,t){je(this.size);var n=!0;return this.__iterate(function(r,i,o){if(!e.call(t,r,i,o))return n=!1,!1}),n},filter:function(e,t){return Xt(this,zt(this,e,t,!0))},find:function(e,t,n){var r=this.findEntry(e,t);return r?r[1]:n},forEach:function(e,t){return je(this.size),this.__iterate(t?e.bind(t):e)},join:function(e){je(this.size),e=void 0!==e?""+e:",";var t="",n=!0;return this.__iterate(function(r){n?n=!1:t+=e,t+=null!==r&&void 0!==r?r.toString():""}),t},keys:function(){return this.__iterator(P)},map:function(e,t){return Xt(this,Lt(this,e,t))},reduce:function(e,t,n){var r,i;return je(this.size),arguments.length<2?i=!0:r=t,this.__iterate(function(t,o,a){i?(i=!1,r=t):r=e.call(n,r,t,o,a)}),r},reduceRight:function(e,t,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Xt(this,qt(this,!0))},slice:function(e,t){return Xt(this,Ut(this,e,t,!0))},some:function(e,t){return!this.every(Rn(e),t)},sort:function(e){return Xt(this,Ht(this,e))},values:function(){return this.__iterator(I)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(e,t){return S(e?this.toSeq().filter(e,t):this)},countBy:function(e,t){return function(e,t,n){var r=Be().asMutable();return e.__iterate(function(i,o){r.update(t.call(n,i,o,e),0,function(e){return e+1})}),r.asImmutable()}(this,e,t)},equals:function(e){return me(this,e)},entrySeq:function(){var e=this;if(e._cache)return new ee(e._cache);var t=e.toSeq().map(In).toIndexedSeq();return t.fromEntrySeq=function(){return e.toSeq()},t},filterNot:function(e,t){return this.filter(Rn(e),t)},findEntry:function(e,t,n){var r=n;return this.__iterate(function(n,i,o){if(e.call(t,n,i,o))return r=[i,n],!1}),r},findKey:function(e,t){var n=this.findEntry(e,t);return n&&n[0]},findLast:function(e,t,n){return this.toKeyedSeq().reverse().find(e,t,n)},findLastEntry:function(e,t,n){return this.toKeyedSeq().reverse().findEntry(e,t,n)},findLastKey:function(e,t){return this.toKeyedSeq().reverse().findKey(e,t)},first:function(){return this.find(A)},flatMap:function(e,t){return Xt(this,function(e,t,n){var r=Zt(e);return e.toSeq().map(function(i,o){return r(t.call(n,i,o,e))}).flatten(!0)}(this,e,t))},flatten:function(e){return Xt(this,Vt(this,e,!0))},fromEntrySeq:function(){return new jt(this)},get:function(e,t){return this.find(function(t,n){return de(n,e)},void 0,t)},getIn:function(e,t){for(var n,r=this,i=nn(e);!(n=i.next()).done;){var o=n.value;if((r=r&&r.get?r.get(o,y):y)===y)return t}return r},groupBy:function(e,t){return function(e,t,n){var r=s(e),i=(c(e)?Mt():Be()).asMutable();e.__iterate(function(o,a){i.update(t.call(n,o,a,e),function(e){return(e=e||[]).push(r?[a,o]:o),e})});var o=Zt(e);return i.map(function(t){return Xt(e,o(t))})}(this,e,t)},has:function(e){return this.get(e,y)!==y},hasIn:function(e){return this.getIn(e,y)!==y},isSubset:function(e){return e="function"==typeof e.includes?e:n(e),this.every(function(t){return e.includes(t)})},isSuperset:function(e){return(e="function"==typeof e.isSubset?e:n(e)).isSubset(this)},keyOf:function(e){return this.findKey(function(t){return de(t,e)})},keySeq:function(){return this.toSeq().map(Pn).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(e){return this.toKeyedSeq().reverse().keyOf(e)},max:function(e){return Jt(this,e)},maxBy:function(e,t){return Jt(this,t,e)},min:function(e){return Jt(this,e?Nn(e):Bn)},minBy:function(e,t){return Jt(this,t?Nn(t):Bn,e)},rest:function(){return this.slice(1)},skip:function(e){return this.slice(Math.max(0,e))},skipLast:function(e){return Xt(this,this.toSeq().reverse().skip(e).reverse())},skipWhile:function(e,t){return Xt(this,Wt(this,e,t,!0))},skipUntil:function(e,t){return this.skipWhile(Rn(e),t)},sortBy:function(e,t){return Xt(this,Ht(this,t,e))},take:function(e){return this.slice(0,Math.max(0,e))},takeLast:function(e){return Xt(this,this.toSeq().reverse().take(e).reverse())},takeWhile:function(e,t){return Xt(this,function(e,t,n){var r=Qt(e);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var a=0;return e.__iterate(function(e,i,s){return t.call(n,e,i,s)&&++a&&r(e,i,o)}),a},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var a=e.__iterator(R,i),s=!0;return new B(function(){if(!s)return{value:void 0,done:!0};var e=a.next();if(e.done)return e;var i=e.value,u=i[0],l=i[1];return t.call(n,l,u,o)?r===R?e:L(r,u,l,e):(s=!1,{value:void 0,done:!0})})},r}(this,e,t))},takeUntil:function(e,t){return this.takeWhile(Rn(e),t)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=function(e){if(e.size===1/0)return 0;var t=c(e),n=s(e),r=t?1:0;return function(e,t){return t=we(t,3432918353),t=we(t<<15|t>>>-15,461845907),t=we(t<<13|t>>>-13,5),t=we((t=(t+3864292196|0)^e)^t>>>16,2246822507),t=Ee((t=we(t^t>>>13,3266489909))^t>>>16)}(e.__iterate(n?t?function(e,t){r=31*r+Ln(Se(e),Se(t))|0}:function(e,t){r=r+Ln(Se(e),Se(t))|0}:t?function(e){r=31*r+Se(e)|0}:function(e){r=r+Se(e)|0}),r)}(this))}});var On=n.prototype;On[p]=!0,On[j]=On.values,On.__toJS=On.toArray,On.__toStringMapper=Fn,On.inspect=On.toSource=function(){return this.toString()},On.chain=On.flatMap,On.contains=On.includes,Mn(r,{flip:function(){return Xt(this,Bt(this))},mapEntries:function(e,t){var n=this,r=0;return Xt(this,this.toSeq().map(function(i,o){return e.call(t,[o,i],r++,n)}).fromEntrySeq())},mapKeys:function(e,t){var n=this;return Xt(this,this.toSeq().flip().map(function(r,i){return e.call(t,r,i,n)}).flip())}});var Tn=r.prototype;function Pn(e,t){return t}function In(e,t){return[t,e]}function Rn(e){return function(){return!e.apply(this,arguments)}}function Nn(e){return function(){return-e.apply(this,arguments)}}function Fn(e){return"string"==typeof e?JSON.stringify(e):String(e)}function jn(){return E(arguments)}function Bn(e,t){return et?-1:0}function Ln(e,t){return e^t+2654435769+(e<<6)+(e>>2)|0}return Tn[f]=!0,Tn[j]=On.entries,Tn.__toJS=On.toObject,Tn.__toStringMapper=function(e,t){return JSON.stringify(t)+": "+Fn(e)},Mn(i,{toKeyedSeq:function(){return new Rt(this,!1)},filter:function(e,t){return Xt(this,zt(this,e,t,!1))},findIndex:function(e,t){var n=this.findEntry(e,t);return n?n[0]:-1},indexOf:function(e){var t=this.keyOf(e);return void 0===t?-1:t},lastIndexOf:function(e){var t=this.lastKeyOf(e);return void 0===t?-1:t},reverse:function(){return Xt(this,qt(this,!1))},slice:function(e,t){return Xt(this,Ut(this,e,t,!1))},splice:function(e,t){var n=arguments.length;if(t=Math.max(0|t,0),0===n||2===n&&!t)return this;e=M(e,e<0?this.count():this.size);var r=this.slice(0,e);return Xt(this,1===n?r:r.concat(E(arguments,2),this.slice(e+t)))},findLastIndex:function(e,t){var n=this.findLastEntry(e,t);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(e){return Xt(this,Vt(this,e,!1))},get:function(e,t){return(e=C(this,e))<0||this.size===1/0||void 0!==this.size&&e>this.size?t:this.find(function(t,n){return n===e},void 0,t)},has:function(e){return(e=C(this,e))>=0&&(void 0!==this.size?this.size===1/0||e5e3)return e.textContent;return function(e){for(var n,r,i,o,a,s=e.textContent,u=0,l=s[0],c=1,p=e.innerHTML="",f=0;r=n,n=f<7&&"\\"==n?1:c;){if(c=l,l=s[++u],o=p.length>1,!c||f>8&&"\n"==c||[/\S/.test(c),1,1,!/[$\w]/.test(c),("/"==n||"\n"==n)&&o,'"'==n&&o,"'"==n&&o,s[u-4]+r+n=="--\x3e",r+n=="*/"][f])for(p&&(e.appendChild(a=t.createElement("span")).setAttribute("style",["color: #555; font-weight: bold;","","","color: #555;",""][f?f<3?2:f>6?4:f>3?3:+/^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/.test(p):0]),a.appendChild(t.createTextNode(p))),i=f&&f<7?f:i,p="",f=11;![1,/[\/{}[(\-+*=<>:;|\\.,?!&@~]/.test(c),/[\])]/.test(c),/[$\w]/.test(c),"/"==c&&i<2&&"<"!=n,'"'==c,"'"==c,c+l+s[u+1]+s[u+2]=="\x3c!--",c+l=="/*",c+l=="//","#"==c][--f];);p+=c}}(e)},t.mapToList=function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"key";var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:l.default.Map();if(!l.default.Map.isMap(t)||!t.size)return l.default.List();Array.isArray(n)||(n=[n]);if(n.length<1)return t.merge(r);var a=l.default.List();var s=n[0];var u=!0;var c=!1;var p=void 0;try{for(var f,h=(0,o.default)(t.entries());!(u=(f=h.next()).done);u=!0){var d=f.value,m=(0,i.default)(d,2),v=m[0],g=m[1],y=e(g,n.slice(1),r.set(s,v));a=l.default.List.isList(y)?a.concat(y):a.push(y)}}catch(e){c=!0,p=e}finally{try{!u&&h.return&&h.return()}finally{if(c)throw p}}return a},t.extractFileNameFromContentDispositionHeader=function(e){var t=/filename="([^;]*);?"/i.exec(e);null===t&&(t=/filename=([^;]*);?/i.exec(e));if(null!==t&&t.length>1)return t[1];return null},t.pascalCase=S,t.pascalCaseFilename=function(e){return S(e.replace(/\.[^./]*$/,""))},t.sanitizeUrl=function(e){if("string"!=typeof e||""===e)return"";return(0,c.sanitizeUrl)(e)},t.getAcceptControllingResponse=function(e){if(!l.default.OrderedMap.isOrderedMap(e))return null;if(!e.size)return null;var t=e.find(function(e,t){return t.startsWith("2")&&(0,s.default)(e.get("content")||{}).length>0}),n=e.get("default")||l.default.OrderedMap(),r=(n.get("content")||l.default.OrderedMap()).keySeq().toJS().length?n:null;return t||r},t.deeplyStripKey=function e(t,n){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){return!0};if("object"!==(void 0===t?"undefined":(0,u.default)(t))||Array.isArray(t)||!n)return t;var i=(0,a.default)({},t);(0,s.default)(i).forEach(function(t){t===n&&r(i[t],t)?delete i[t]:i[t]=e(i[t],n,r)});return i};var l=b(n(7)),c=n(489),p=b(n(909)),f=b(n(422)),h=b(n(418)),d=b(n(223)),m=b(n(927)),v=b(n(115)),g=n(170),y=b(n(35)),_=b(n(681));function b(e){return e&&e.__esModule?e:{default:e}}var x="default",k=t.isImmutable=function(e){return l.default.Iterable.isIterable(e)};function w(e){return Array.isArray(e)?e:[e]}function E(e){return!!e&&"object"===(void 0===e?"undefined":(0,u.default)(e))}t.memoize=h.default;function S(e){return(0,f.default)((0,p.default)(e))}t.propChecker=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:[];return(0,s.default)(e).length!==(0,s.default)(t).length||((0,m.default)(e,function(e,n){if(r.includes(n))return!1;var i=t[n];return l.default.Iterable.isIterable(e)?!l.default.is(e,i):("object"!==(void 0===e?"undefined":(0,u.default)(e))||"object"!==(void 0===i?"undefined":(0,u.default)(i)))&&e!==i})||n.some(function(n){return!(0,v.default)(e[n],t[n])}))};var C=t.validateMaximum=function(e,t){if(e>t)return"Value must be less than Maximum"},A=t.validateMinimum=function(e,t){if(et)return"Value must be less than MaxLength"},F=t.validateMinLength=function(e,t){if(e.length2&&void 0!==arguments[2]&&arguments[2],r=[],i=t&&"body"===e.get("in")?e.get("value_xml"):e.get("value"),o=e.get("required"),a=n?e.get("schema"):e;if(!a)return r;var s=a.get("maximum"),c=a.get("minimum"),p=a.get("type"),f=a.get("format"),h=a.get("maxLength"),d=a.get("minLength"),m=a.get("pattern");if(p&&(o||i)){var v="string"===p&&i,g="array"===p&&Array.isArray(i)&&i.length,_="array"===p&&l.default.List.isList(i)&&i.count(),b="file"===p&&i instanceof y.default.File,x="boolean"===p&&(i||!1===i),k="number"===p&&(i||0===i),w="integer"===p&&(i||0===i),E=!1;if(n&&"object"===p)if("object"===(void 0===i?"undefined":(0,u.default)(i)))E=!0;else if("string"==typeof i)try{JSON.parse(i),E=!0}catch(e){return r.push("Parameter string value must be valid JSON"),r}var S=[v,g,_,b,x,k,w,E].some(function(e){return!!e});if(o&&!S)return r.push("Required field is not provided"),r;if(m){var B=j(i,m);B&&r.push(B)}if(h||0===h){var L=N(i,h);L&&r.push(L)}if(d){var q=F(i,d);q&&r.push(q)}if(s||0===s){var z=C(i,s);z&&r.push(z)}if(c||0===c){var U=A(i,c);U&&r.push(U)}if("string"===p){var W=void 0;if(!(W="date-time"===f?I(i):"uuid"===f?R(i):P(i)))return r;r.push(W)}else if("boolean"===p){var V=T(i);if(!V)return r;r.push(V)}else if("number"===p){var H=D(i);if(!H)return r;r.push(H)}else if("integer"===p){var J=M(i);if(!J)return r;r.push(J)}else if("array"===p){var G;if(!_||!i.count())return r;G=a.getIn(["items","type"]),i.forEach(function(e,t){var n=void 0;"number"===G?n=D(e):"integer"===G?n=M(e):"string"===G&&(n=P(e)),n&&r.push({index:t,error:n})})}else if("file"===p){var K=O(i);if(!K)return r;r.push(K)}}return r},t.getSampleSchema=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(/xml/.test(t)){if(!e.xml||!e.xml.name){if(e.xml=e.xml||{},!e.$$ref)return e.type||e.items||e.properties||e.additionalProperties?'\n\x3c!-- XML example cannot be generated --\x3e':null;var i=e.$$ref.match(/\S*\/(\S+)$/);e.xml.name=i[1]}return(0,g.memoizedCreateXMLExample)(e,n)}return(0,r.default)((0,g.memoizedSampleFromSchema)(e,n),null,2)},t.parseSearch=function(){var e={},t=y.default.location.search;if(!t)return{};if(""!=t){var n=t.substr(1).split("&");for(var r in n)n.hasOwnProperty(r)&&(r=n[r].split("="),e[decodeURIComponent(r[0])]=r[1]&&decodeURIComponent(r[1])||"")}return e},t.serializeSearch=function(e){return(0,s.default)(e).map(function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])}).join("&")},t.btoa=function(t){return(t instanceof e?t:new e(t.toString(),"utf-8")).toString("base64")},t.sorters={operationsSorter:{alpha:function(e,t){return e.get("path").localeCompare(t.get("path"))},method:function(e,t){return e.get("method").localeCompare(t.get("method"))}},tagsSorter:{alpha:function(e,t){return e.localeCompare(t)}}},t.buildFormData=function(e){var t=[];for(var n in e){var r=e[n];void 0!==r&&""!==r&&t.push([n,"=",encodeURIComponent(r).replace(/%20/g,"+")].join(""))}return t.join("&")},t.shallowEqualKeys=function(e,t,n){return!!(0,d.default)(n,function(n){return(0,v.default)(e[n],t[n])})};var B=t.createDeepLinkPath=function(e){return"string"==typeof e||e instanceof String?e.trim().replace(/\s/g,"_"):""};t.escapeDeepLinkPath=function(e){return(0,_.default)(B(e))},t.getExtensions=function(e){return e.filter(function(e,t){return/^x-/.test(t)})},t.getCommonExtensions=function(e){return e.filter(function(e,t){return/^pattern|maxLength|minLength|maximum|minimum/.test(t)})}}).call(t,n(52).Buffer)},function(e,t,n){"use strict";var r=n(33);e.exports=r},function(e,t,n){"use strict";e.exports=function(e){for(var t=arguments.length-1,n="Minified React error #"+e+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant="+e,r=0;r>",o={listOf:function(e){return l(e,"List",r.List.isList)},mapOf:function(e,t){return c(e,t,"Map",r.Map.isMap)},orderedMapOf:function(e,t){return c(e,t,"OrderedMap",r.OrderedMap.isOrderedMap)},setOf:function(e){return l(e,"Set",r.Set.isSet)},orderedSetOf:function(e){return l(e,"OrderedSet",r.OrderedSet.isOrderedSet)},stackOf:function(e){return l(e,"Stack",r.Stack.isStack)},iterableOf:function(e){return l(e,"Iterable",r.Iterable.isIterable)},recordOf:function(e){return s(function(t,n,i,o,s){for(var u=arguments.length,l=Array(u>5?u-5:0),c=5;c6?u-6:0),c=6;c5?l-5:0),p=5;p5?o-5:0),s=5;s key("+c[p]+")"].concat(a));if(h instanceof Error)return h}})).apply(void 0,o);var u})}function p(e){var t=void 0===arguments[1]?"Iterable":arguments[1],n=void 0===arguments[2]?r.Iterable.isIterable:arguments[2];return s(function(r,i,o,s,u){for(var l=arguments.length,c=Array(l>5?l-5:0),p=5;p?@[\]^_`{|}~-])/g;function a(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(65535!=(65535&e)&&65534!=(65535&e)&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function s(e){if(e>65535){var t=55296+((e-=65536)>>10),n=56320+(1023&e);return String.fromCharCode(t,n)}return String.fromCharCode(e)}var u=/&([a-z#][a-z0-9]{1,31});/gi,l=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i,c=n(471);function p(e,t){var n=0;return i(c,t)?c[t]:35===t.charCodeAt(0)&&l.test(t)&&a(n="x"===t[1].toLowerCase()?parseInt(t.slice(2),16):parseInt(t.slice(1),10))?s(n):e}var f=/[&<>"]/,h=/[&<>"]/g,d={"&":"&","<":"<",">":">",'"':"""};function m(e){return d[e]}t.assign=function(e){return[].slice.call(arguments,1).forEach(function(t){if(t){if("object"!=typeof t)throw new TypeError(t+"must be object");Object.keys(t).forEach(function(n){e[n]=t[n]})}}),e},t.isString=function(e){return"[object String]"===function(e){return Object.prototype.toString.call(e)}(e)},t.has=i,t.unescapeMd=function(e){return e.indexOf("\\")<0?e:e.replace(o,"$1")},t.isValidEntityCode=a,t.fromCodePoint=s,t.replaceEntities=function(e){return e.indexOf("&")<0?e:e.replace(u,p)},t.escapeHtml=function(e){return f.test(e)?e.replace(h,m):e}},function(e,t,n){"use strict";t.__esModule=!0;var r,i=n(325),o=(r=i)&&r.__esModule?r:{default:r};t.default=function(e,t,n){return t in e?(0,o.default)(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){var r=n(31),i=n(57),o=n(63),a=n(77),s=n(131),u=function(e,t,n){var l,c,p,f,h=e&u.F,d=e&u.G,m=e&u.S,v=e&u.P,g=e&u.B,y=d?r:m?r[t]||(r[t]={}):(r[t]||{}).prototype,_=d?i:i[t]||(i[t]={}),b=_.prototype||(_.prototype={});for(l in d&&(n=t),n)p=((c=!h&&y&&void 0!==y[l])?y:n)[l],f=g&&c?s(p,r):v&&"function"==typeof p?s(Function.call,p):p,y&&a(y,l,p,e&u.U),_[l]!=p&&o(_,l,f),v&&b[l]!=p&&(b[l]=p)};r.core=i,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,e.exports=u},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(30),i=n(107),o=n(58),a=/"/g,s=function(e,t,n,r){var i=String(o(e)),s="<"+t;return""!==n&&(s+=" "+n+'="'+String(r).replace(a,""")+'"'),s+">"+i+""};e.exports=function(e,t){var n={};n[e]=t(s),r(r.P+r.F*i(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",n)}},function(e,t,n){"use strict";function r(e){return function(){return e}}var i=function(){};i.thatReturns=r,i.thatReturnsFalse=r(!1),i.thatReturnsTrue=r(!0),i.thatReturnsNull=r(null),i.thatReturnsThis=function(){return this},i.thatReturnsArgument=function(e){return e},e.exports=i},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=o(n(24));t.isOAS3=a,t.isSwagger2=function(e){var t=e.get("swagger");if(!t)return!1;return t.startsWith("2")},t.OAS3ComponentWrapFactory=function(e){return function(t,n){return function(o){if(n&&n.specSelectors&&n.specSelectors.specJson){var s=n.specSelectors.specJson();return a(s)?i.default.createElement(e,(0,r.default)({},o,n,{Ori:t})):i.default.createElement(t,o)}return console.warn("OAS3 wrapper: couldn't get spec"),null}}};var i=o(n(0));function o(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=e.get("openapi");return!!t&&t.startsWith("3")}},function(e,t,n){"use strict";var r,i=n(95),o=(r=i)&&r.__esModule?r:{default:r};e.exports=function(){var e={location:{},history:{},open:function(){},close:function(){},File:function(){}};if("undefined"==typeof window)return e;try{e=window;var t=!0,n=!1,r=void 0;try{for(var i,a=(0,o.default)(["File","Blob","FormData"]);!(t=(i=a.next()).done);t=!0){var s=i.value;s in window&&(e[s]=window[s])}}catch(e){n=!0,r=e}finally{try{!t&&a.return&&a.return()}finally{if(n)throw r}}}catch(e){console.error(e)}return e}()},function(e,t,n){e.exports={default:n(570),__esModule:!0}},function(e,t,n){var r=n(29);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t,n){var r=n(401),i="object"==typeof self&&self&&self.Object===Object&&self,o=r||i||Function("return this")();e.exports=o},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t){var n,r,i=e.exports={};function o(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===o||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:o}catch(e){n=o}try{r="function"==typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var u,l=[],c=!1,p=-1;function f(){c&&u&&(c=!1,u.length?l=u.concat(l):p=-1,l.length&&h())}function h(){if(!c){var e=s(f);c=!0;for(var t=l.length;t;){for(u=l,l=[];++p1)for(var n=1;n0&&(o=this.buffer[s-1],e.call("\0\r\n…\u2028\u2029",o)<0);)if(s--,this.pointer-s>n/2-1){i=" ... ",s+=5;break}for(u="",r=this.pointer;rn/2-1){u=" ... ",r-=5;break}return""+new Array(t).join(" ")+i+this.buffer.slice(s,r)+u+"\n"+new Array(t+this.pointer-s+i.length).join(" ")+"^"},t.prototype.toString=function(){var e,t;return e=this.get_snippet(),t=" on line "+(this.line+1)+", column "+(this.column+1),e?t:t+":\n"+e},t}(),this.YAMLError=function(e){function n(e){this.message=e,n.__super__.constructor.call(this),this.stack=this.toString()+"\n"+(new Error).stack.split("\n").slice(1).join("\n")}return t(n,e),n.prototype.toString=function(){return this.message},n}(Error),this.MarkedYAMLError=function(e){function n(e,t,r,i,o){this.context=e,this.context_mark=t,this.problem=r,this.problem_mark=i,this.note=o,n.__super__.constructor.call(this)}return t(n,e),n.prototype.toString=function(){var e;return e=[],null!=this.context&&e.push(this.context),null==this.context_mark||null!=this.problem&&null!=this.problem_mark&&this.context_mark.line===this.problem_mark.line&&this.context_mark.column===this.problem_mark.column||e.push(this.context_mark.toString()),null!=this.problem&&e.push(this.problem),null!=this.problem_mark&&e.push(this.problem_mark.toString()),null!=this.note&&e.push(this.note),e.join("\n")},n}(this.YAMLError)}).call(this)},function(e,t,n){e.exports=!n(54)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,n){if(n)return[e,t];return e},e.exports=t.default},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t,n){"use strict";var r=n(13),i=n(69),o=n(33),a=(n(10),["dispatchConfig","_targetInst","nativeEvent","isDefaultPrevented","isPropagationStopped","_dispatchListeners","_dispatchInstances"]),s={type:null,target:null,currentTarget:o.thatReturnsNull,eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null};function u(e,t,n,r){this.dispatchConfig=e,this._targetInst=t,this.nativeEvent=n;var i=this.constructor.Interface;for(var a in i)if(i.hasOwnProperty(a)){0;var s=i[a];s?this[a]=s(n):"target"===a?this.target=r:this[a]=n[a]}var u=null!=n.defaultPrevented?n.defaultPrevented:!1===n.returnValue;return this.isDefaultPrevented=u?o.thatReturnsTrue:o.thatReturnsFalse,this.isPropagationStopped=o.thatReturnsFalse,this}r(u.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!=typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=o.thatReturnsTrue)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!=typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=o.thatReturnsTrue)},persist:function(){this.isPersistent=o.thatReturnsTrue},isPersistent:o.thatReturnsFalse,destructor:function(){var e=this.constructor.Interface;for(var t in e)this[t]=null;for(var n=0;n + * @license MIT + */ +var r=n(553),i=n(738),o=n(379);function a(){return u.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function d(e,t){if(u.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return q(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return z(e).length;default:if(r)return q(e).length;t=(""+t).toLowerCase(),r=!0}}function m(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function v(e,t,n,r,i){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=i?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(i)return-1;n=e.length-1}else if(n<0){if(!i)return-1;n=0}if("string"==typeof t&&(t=u.from(t,r)),u.isBuffer(t))return 0===t.length?-1:g(e,t,n,r,i);if("number"==typeof t)return t&=255,u.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):g(e,[t],n,r,i);throw new TypeError("val must be string, number or Buffer")}function g(e,t,n,r,i){var o,a=1,s=e.length,u=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,u/=2,n/=2}function l(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(i){var c=-1;for(o=n;os&&(n=s-u),o=n;o>=0;o--){for(var p=!0,f=0;fi&&(r=i):r=i;var o=t.length;if(o%2!=0)throw new TypeError("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;a>8,i=n%256,o.push(i),o.push(r);return o}(t,e.length-n),e,n,r)}function E(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function S(e,t,n){n=Math.min(e.length,n);for(var r=[],i=t;i239?4:l>223?3:l>191?2:1;if(i+p<=n)switch(p){case 1:l<128&&(c=l);break;case 2:128==(192&(o=e[i+1]))&&(u=(31&l)<<6|63&o)>127&&(c=u);break;case 3:o=e[i+1],a=e[i+2],128==(192&o)&&128==(192&a)&&(u=(15&l)<<12|(63&o)<<6|63&a)>2047&&(u<55296||u>57343)&&(c=u);break;case 4:o=e[i+1],a=e[i+2],s=e[i+3],128==(192&o)&&128==(192&a)&&128==(192&s)&&(u=(15&l)<<18|(63&o)<<12|(63&a)<<6|63&s)>65535&&u<1114112&&(c=u)}null===c?(c=65533,p=1):c>65535&&(c-=65536,r.push(c>>>10&1023|55296),c=56320|1023&c),r.push(c),i+=p}return function(e){var t=e.length;if(t<=C)return String.fromCharCode.apply(String,e);var n="",r=0;for(;rthis.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return M(this,t,n);case"utf8":case"utf-8":return S(this,t,n);case"ascii":return A(this,t,n);case"latin1":case"binary":return D(this,t,n);case"base64":return E(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return O(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}.apply(this,arguments)},u.prototype.equals=function(e){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===u.compare(this,e)},u.prototype.inspect=function(){var e="",n=t.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},u.prototype.compare=function(e,t,n,r,i){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),t<0||n>e.length||r<0||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(t>>>=0,n>>>=0,r>>>=0,i>>>=0,this===e)return 0;for(var o=i-r,a=n-t,s=Math.min(o,a),l=this.slice(r,i),c=e.slice(t,n),p=0;pi)&&(n=i),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return y(this,e,t,n);case"utf8":case"utf-8":return _(this,e,t,n);case"ascii":return b(this,e,t,n);case"latin1":case"binary":return x(this,e,t,n);case"base64":return k(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return w(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},u.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var C=4096;function A(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;ir)&&(n=r);for(var i="",o=t;on)throw new RangeError("Trying to access beyond buffer length")}function P(e,t,n,r,i,o){if(!u.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||te.length)throw new RangeError("Index out of range")}function I(e,t,n,r){t<0&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);i>>8*(r?i:1-i)}function R(e,t,n,r){t<0&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);i>>8*(r?i:3-i)&255}function N(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function F(e,t,n,r,o){return o||N(e,0,n,4),i.write(e,t,n,r,23,4),n+4}function j(e,t,n,r,o){return o||N(e,0,n,8),i.write(e,t,n,r,52,8),n+8}u.prototype.slice=function(e,t){var n,r=this.length;if(e=~~e,t=void 0===t?r:~~t,e<0?(e+=r)<0&&(e=0):e>r&&(e=r),t<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(i*=256);)r+=this[e+--t]*i;return r},u.prototype.readUInt8=function(e,t){return t||T(e,1,this.length),this[e]},u.prototype.readUInt16LE=function(e,t){return t||T(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function(e,t){return t||T(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function(e,t){return t||T(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function(e,t){return t||T(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||T(e,t,this.length);for(var r=this[e],i=1,o=0;++o=(i*=128)&&(r-=Math.pow(2,8*t)),r},u.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||T(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*t)),o},u.prototype.readInt8=function(e,t){return t||T(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function(e,t){t||T(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function(e,t){t||T(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function(e,t){return t||T(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function(e,t){return t||T(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function(e,t){return t||T(e,4,this.length),i.read(this,e,!0,23,4)},u.prototype.readFloatBE=function(e,t){return t||T(e,4,this.length),i.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function(e,t){return t||T(e,8,this.length),i.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function(e,t){return t||T(e,8,this.length),i.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||P(this,e,t,n,Math.pow(2,8*n)-1,0);var i=1,o=0;for(this[t]=255&e;++o=0&&(o*=256);)this[t+i]=e/o&255;return t+n},u.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},u.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):I(this,e,t,!0),t+2},u.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):I(this,e,t,!1),t+2},u.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):R(this,e,t,!0),t+4},u.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);P(this,e,t,n,i-1,-i)}var o=0,a=1,s=0;for(this[t]=255&e;++o>0)-s&255;return t+n},u.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);P(this,e,t,n,i-1,-i)}var o=n-1,a=1,s=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)e<0&&0===s&&0!==this[t+o+1]&&(s=1),this[t+o]=(e/a>>0)-s&255;return t+n},u.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},u.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):I(this,e,t,!0),t+2},u.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):I(this,e,t,!1),t+2},u.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):R(this,e,t,!0),t+4},u.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||P(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeFloatLE=function(e,t,n){return F(this,e,t,!0,n)},u.prototype.writeFloatBE=function(e,t,n){return F(this,e,t,!1,n)},u.prototype.writeDoubleLE=function(e,t,n){return j(this,e,t,!0,n)},u.prototype.writeDoubleBE=function(e,t,n){return j(this,e,t,!1,n)},u.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--i)e[i+t]=this[i+n];else if(o<1e3||!u.TYPED_ARRAY_SUPPORT)for(i=0;i>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(o=t;o55295&&n<57344){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(n<56320){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=65536+(i-55296<<10|n-56320)}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,n<128){if((t-=1)<0)break;o.push(n)}else if(n<2048){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function z(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(B,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function U(e,t,n,r){for(var i=0;i=t.length||i>=e.length);++i)t[i+n]=e[i];return i}}).call(t,n(19))},function(e,t,n){var r=n(97);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(44),i=n(101);e.exports=n(47)?function(e,t,n){return r.f(e,t,i(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t){var n=e.exports={version:"2.5.6"};"number"==typeof __e&&(__e=n)},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){"use strict";e.exports=function(e){if("function"!=typeof e)throw new TypeError(e+" is not a function");return e}},function(e,t,n){"use strict";function r(e,t){return e===t}function i(e){var t=arguments.length<=1||void 0===arguments[1]?r:arguments[1],n=null,i=null;return function(){for(var r=arguments.length,o=Array(r),a=0;a1?t-1:0),r=1;r2?n-2:0),i=2;i=n?e:e.length+1===n?""+t+e:""+new Array(n-e.length+1).join(t)+e},this.to_hex=function(e){return"string"==typeof e&&(e=e.charCodeAt(0)),e.toString(16)}}).call(this)}).call(t,n(19))},function(e,t,n){var r=n(76);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t,n){var r=n(134),i=n(354);e.exports=n(106)?function(e,t,n){return r.f(e,t,i(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){"use strict";var r=n(703),i=Math.max;e.exports=function(e){return i(0,r(e))}},function(e,t,n){var r=n(82),i=n(864),o=n(893),a="[object Null]",s="[object Undefined]",u=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?s:a:u&&u in Object(e)?i(e):o(e)}},function(e,t,n){var r=n(825),i=n(865);e.exports=function(e,t){var n=i(e,t);return r(n)?n:void 0}},function(e,t,n){var r=n(385),i=n(828),o=n(86);e.exports=function(e){return o(e)?r(e):i(e)}},function(e,t,n){"use strict"},function(e,t,n){"use strict";var r=n(11),i=(n(8),function(e){if(this.instancePool.length){var t=this.instancePool.pop();return this.call(t,e),t}return new this(e)}),o=function(e){e instanceof this||r("25"),e.destructor(),this.instancePool.length`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>",u="]",l=new RegExp("^(?:<[A-Za-z][A-Za-z0-9-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>|]|\x3c!----\x3e|\x3c!--(?:-?[^>-])(?:-?[^-])*--\x3e|[<][?].*?[?][>]|]*>|)","i"),c=/[\\&]/,p="[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]",f=new RegExp("\\\\"+p+"|"+a,"gi"),h=new RegExp('[&<>"]',"g"),d=new RegExp(a+'|[&<>"]',"gi"),m=function(e){return 92===e.charCodeAt(0)?e.charAt(1):o(e)},v=function(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case'"':return""";default:return e}};e.exports={unescapeString:function(e){return c.test(e)?e.replace(f,m):e},normalizeURI:function(e){try{return r(i(e))}catch(t){return e}},escapeXml:function(e,t){return h.test(e)?t?e.replace(d,v):e.replace(h,v):e},reHtmlTag:l,OPENTAG:s,CLOSETAG:u,ENTITY:a,ESCAPABLE:p}},function(e,t){e.exports={}},function(e,t,n){var r=n(180),i=n(177);e.exports=function(e){return r(i(e))}},function(e,t,n){var r=n(177);e.exports=function(e){return Object(r(e))}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){var r=n(31),i=n(63),o=n(133),a=n(202)("src"),s=Function.toString,u=(""+s).split("toString");n(57).inspectSource=function(e){return s.call(e)},(e.exports=function(e,t,n,s){var l="function"==typeof n;l&&(o(n,"name")||i(n,"name",t)),e[t]!==n&&(l&&(o(n,a)||i(n,a,e[t]?""+e[t]:u.join(String(t)))),e===r?e[t]=n:s?e[t]?e[t]=n:i(e,t,n):(delete e[t],i(e,t,n)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[a]||s.call(this)})},function(e,t,n){"use strict";var r=n(366)();e.exports=function(e){return e!==r&&null!==e}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){"use strict";function r(e){return void 0===e||null===e}e.exports.isNothing=r,e.exports.isObject=function(e){return"object"==typeof e&&null!==e},e.exports.toArray=function(e){return Array.isArray(e)?e:r(e)?[]:[e]},e.exports.repeat=function(e,t){var n,r="";for(n=0;n`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>",u="]",l=new RegExp("^(?:<[A-Za-z][A-Za-z0-9-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*/?>|]|\x3c!----\x3e|\x3c!--(?:-?[^>-])(?:-?[^-])*--\x3e|[<][?].*?[?][>]|]*>|)","i"),c=/[\\&]/,p="[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]",f=new RegExp("\\\\"+p+"|"+a,"gi"),h=new RegExp('[&<>"]',"g"),d=new RegExp(a+'|[&<>"]',"gi"),m=function(e){return 92===e.charCodeAt(0)?e.charAt(1):o(e)},v=function(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case'"':return""";default:return e}};e.exports={unescapeString:function(e){return c.test(e)?e.replace(f,m):e},normalizeURI:function(e){try{return r(i(e))}catch(t){return e}},escapeXml:function(e,t){return h.test(e)?t?e.replace(d,v):e.replace(h,v):e},reHtmlTag:l,OPENTAG:s,CLOSETAG:u,ENTITY:a,ESCAPABLE:p}},function(e,t,n){"use strict";var r=n(13),i=n(457),o=n(1058),a=n(1059),s=n(93),u=n(1060),l=n(1061),c=n(1062),p=n(1066),f=s.createElement,h=s.createFactory,d=s.cloneElement,m=r,v=function(e){return e},g={Children:{map:o.map,forEach:o.forEach,count:o.count,toArray:o.toArray,only:p},Component:i.Component,PureComponent:i.PureComponent,createElement:f,cloneElement:d,isValidElement:s.isValidElement,PropTypes:u,createClass:c,createFactory:h,createMixin:v,DOM:a,version:l,__spread:m};e.exports=g},function(e,t,n){"use strict";var r=n(13),i=n(51),o=(n(10),n(461),Object.prototype.hasOwnProperty),a=n(459),s={key:!0,ref:!0,__self:!0,__source:!0};function u(e){return void 0!==e.ref}function l(e){return void 0!==e.key}var c=function(e,t,n,r,i,o,s){var u={$$typeof:a,type:e,key:t,ref:n,props:s,_owner:o};return u};c.createElement=function(e,t,n){var r,a={},p=null,f=null;if(null!=t)for(r in u(t)&&(f=t.ref),l(t)&&(p=""+t.key),void 0===t.__self?null:t.__self,void 0===t.__source?null:t.__source,t)o.call(t,r)&&!s.hasOwnProperty(r)&&(a[r]=t[r]);var h=arguments.length-2;if(1===h)a.children=n;else if(h>1){for(var d=Array(h),m=0;m1){for(var g=Array(v),y=0;y=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){e.exports=!0},function(e,t,n){var r=n(340),i=n(179);e.exports=Object.keys||function(e){return r(e,i)}},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t,n){var r=n(44).f,i=n(55),o=n(21)("toStringTag");e.exports=function(e,t,n){e&&!i(e=n?e:e.prototype,o)&&r(e,o,{configurable:!0,value:t})}},function(e,t,n){"use strict";var r=n(599)(!0);n(334)(String,"String",function(e){this._t=String(e),this._i=0},function(){var e,t=this._t,n=this._i;return n>=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t,n){n(605);for(var r=n(20),i=n(56),o=n(73),a=n(21)("toStringTag"),s="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),u=0;u0?i(r(e),9007199254740991):0}},function(e,t,n){(function(e){function n(e){return Object.prototype.toString.call(e)}t.isArray=function(e){return Array.isArray?Array.isArray(e):"[object Array]"===n(e)},t.isBoolean=function(e){return"boolean"==typeof e},t.isNull=function(e){return null===e},t.isNullOrUndefined=function(e){return null==e},t.isNumber=function(e){return"number"==typeof e},t.isString=function(e){return"string"==typeof e},t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=function(e){return void 0===e},t.isRegExp=function(e){return"[object RegExp]"===n(e)},t.isObject=function(e){return"object"==typeof e&&null!==e},t.isDate=function(e){return"[object Date]"===n(e)},t.isError=function(e){return"[object Error]"===n(e)||e instanceof Error},t.isFunction=function(e){return"function"==typeof e},t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=e.isBuffer}).call(t,n(52).Buffer)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return"string"==typeof e&&r.test(e)};var r=/-webkit-|-moz-|-ms-/;e.exports=t.default},function(e,t,n){"use strict";var r=n(78);e.exports=function(e){if(!r(e))throw new TypeError("Cannot use null or undefined");return e}},function(e,t,n){"use strict";function r(e,t){Error.call(this),this.name="YAMLException",this.reason=e,this.mark=t,this.message=(this.reason||"(unknown reason)")+(this.mark?" "+this.mark.toString():""),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||""}r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r.prototype.toString=function(e){var t=this.name+": ";return t+=this.reason||"(unknown reason)",!e&&this.mark&&(t+=" "+this.mark.toString()),t},e.exports=r},function(e,t,n){"use strict";var r=n(81);e.exports=new r({include:[n(380)],implicit:[n(789),n(782)],explicit:[n(774),n(784),n(785),n(787)]})},function(e,t){e.exports=function(e,t){return e===t||e!=e&&t!=t}},function(e,t,n){"use strict";var r=n(11),i=n(234),o=n(235),a=n(239),s=n(445),u=n(446),l=(n(8),{}),c=null,p=function(e,t){e&&(o.executeDispatchesInOrder(e,t),e.isPersistent()||e.constructor.release(e))},f=function(e){return p(e,!0)},h=function(e){return p(e,!1)},d=function(e){return"."+e._rootNodeID};var m={injection:{injectEventPluginOrder:i.injectEventPluginOrder,injectEventPluginsByName:i.injectEventPluginsByName},putListener:function(e,t,n){"function"!=typeof n&&r("94",t,typeof n);var o=d(e);(l[t]||(l[t]={}))[o]=n;var a=i.registrationNameModules[t];a&&a.didPutListener&&a.didPutListener(e,t,n)},getListener:function(e,t){var n=l[t];if(function(e,t,n){switch(e){case"onClick":case"onClickCapture":case"onDoubleClick":case"onDoubleClickCapture":case"onMouseDown":case"onMouseDownCapture":case"onMouseMove":case"onMouseMoveCapture":case"onMouseUp":case"onMouseUpCapture":return!(!n.disabled||(r=t,"button"!==r&&"input"!==r&&"select"!==r&&"textarea"!==r));default:return!1}var r}(t,e._currentElement.type,e._currentElement.props))return null;var r=d(e);return n&&n[r]},deleteListener:function(e,t){var n=i.registrationNameModules[t];n&&n.willDeleteListener&&n.willDeleteListener(e,t);var r=l[t];r&&delete r[d(e)]},deleteAllListeners:function(e){var t=d(e);for(var n in l)if(l.hasOwnProperty(n)&&l[n][t]){var r=i.registrationNameModules[n];r&&r.willDeleteListener&&r.willDeleteListener(e,n),delete l[n][t]}},extractEvents:function(e,t,n,r){for(var o,a=i.plugins,u=0;u0&&void 0!==arguments[0]?arguments[0]:{};return{type:p,payload:e}},t.clearBy=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!0};return{type:f,payload:e}};var r,i=n(256),o=(r=i)&&r.__esModule?r:{default:r};var a=t.NEW_THROWN_ERR="err_new_thrown_err",s=t.NEW_THROWN_ERR_BATCH="err_new_thrown_err_batch",u=t.NEW_SPEC_ERR="err_new_spec_err",l=t.NEW_SPEC_ERR_BATCH="err_new_spec_err_batch",c=t.NEW_AUTH_ERR="err_new_auth_err",p=t.CLEAR="err_clear",f=t.CLEAR_BY="err_clear_by"},function(e,t,n){e.exports={default:n(577),__esModule:!0}},function(e,t,n){var r; +/*! + Copyright (c) 2016 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames +*/ +/*! + Copyright (c) 2016 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames +*/ +!function(){"use strict";var n={}.hasOwnProperty;function i(){for(var e=[],t=0;t_;_++)if((v=t?y(a(d=e[_])[0],d[1]):y(e[_]))===l||v===c)return v}else for(m=g.call(e);!(d=m.next()).done;)if((v=i(m,y,d.value,t))===l||v===c)return v}).BREAK=l,t.RETURN=c},function(e,t,n){var r=n(129)("meta"),i=n(29),o=n(55),a=n(44).f,s=0,u=Object.isExtensible||function(){return!0},l=!n(54)(function(){return u(Object.preventExtensions({}))}),c=function(e){a(e,r,{value:{i:"O"+ ++s,w:{}}})},p=e.exports={KEY:r,NEED:!1,fastKey:function(e,t){if(!i(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!o(e,r)){if(!u(e))return"F";if(!t)return"E";c(e)}return e[r].i},getWeak:function(e,t){if(!o(e,r)){if(!u(e))return!0;if(!t)return!1;c(e)}return e[r].w},onFreeze:function(e){return l&&p.NEED&&u(e)&&!o(e,r)&&c(e),e}}},function(e,t){t.f={}.propertyIsEnumerable},function(e,t,n){var r=n(188),i=Math.min;e.exports=function(e){return e>0?i(r(e),9007199254740991):0}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,n){var r=n(130);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},function(e,t,n){"use strict";var r=n(63),i=n(77),o=n(107),a=n(58),s=n(18);e.exports=function(e,t,n){var u=s(e),l=n(a,u,""[e]),c=l[0],p=l[1];o(function(){var t={};return t[u]=function(){return 7},7!=""[e](t)})&&(i(String.prototype,e,c),r(RegExp.prototype,u,2==t?function(e,t){return p.call(e,this,t)}:function(e){return p.call(e,this)}))}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(62),i=n(625),o=n(644),a=Object.defineProperty;t.f=n(106)?Object.defineProperty:function(e,t,n){if(r(e),t=o(t,!0),r(n),i)try{return a(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t){var n=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:n)(e)}},function(e,t,n){var r=n(627),i=n(58);e.exports=function(e){return r(i(e))}},function(e,t,n){"use strict";var r=n(367),i=n(370),o=n(707),a=n(712);(e.exports=function(e,t){var n,o,s,u,l;return arguments.length<2||"string"!=typeof e?(u=t,t=e,e=null):u=arguments[2],null==e?(n=s=!0,o=!1):(n=a.call(e,"c"),o=a.call(e,"e"),s=a.call(e,"w")),l={value:t,configurable:n,enumerable:o,writable:s},u?r(i(u),l):l}).gs=function(e,t,n){var s,u,l,c;return"string"!=typeof e?(l=n,n=t,t=e,e=null):l=arguments[3],null==t?t=void 0:o(t)?null==n?n=void 0:o(n)||(l=n,n=void 0):(l=t,t=n=void 0),null==e?(s=!0,u=!1):(s=a.call(e,"c"),u=a.call(e,"e")),c={get:t,set:n,configurable:s,enumerable:u},l?r(i(l),c):c}},function(e,t,n){var r=n(688),i=n(686);t.decode=function(e,t){return(!t||t<=0?i.XML:i.HTML)(e)},t.decodeStrict=function(e,t){return(!t||t<=0?i.XML:i.HTMLStrict)(e)},t.encode=function(e,t){return(!t||t<=0?r.XML:r.HTML)(e)},t.encodeXML=r.XML,t.encodeHTML4=t.encodeHTML5=t.encodeHTML=r.HTML,t.decodeXML=t.decodeXMLStrict=i.XML,t.decodeHTML4=t.decodeHTML5=t.decodeHTML=i.HTML,t.decodeHTML4Strict=t.decodeHTML5Strict=t.decodeHTMLStrict=i.HTMLStrict,t.escape=r.escape},function(e,t,n){"use strict";e.exports=n(704)("forEach")},function(e,t,n){"use strict";var r={};e.exports=r},function(e,t,n){"use strict";var r=n(81);e.exports=r.DEFAULT=new r({include:[n(114)],explicit:[n(780),n(779),n(778)]})},function(e,t,n){var r=n(879),i=n(880),o=n(881),a=n(882),s=n(883);function u(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t-1&&e%1==0&&e]/;e.exports=function(e){return"boolean"==typeof e||"number"==typeof e?""+e:function(e){var t,n=""+e,i=r.exec(n);if(!i)return n;var o="",a=0,s=0;for(a=i.index;a]/,u=n(241)(function(e,t){if(e.namespaceURI!==o.svg||"innerHTML"in e)e.innerHTML=t;else{(r=r||document.createElement("div")).innerHTML=""+t+"";for(var n=r.firstChild;n.firstChild;)e.appendChild(n.firstChild)}});if(i.canUseDOM){var l=document.createElement("div");l.innerHTML=" ",""===l.innerHTML&&(u=function(e,t){if(e.parentNode&&e.parentNode.replaceChild(e,e),a.test(t)||"<"===t[0]&&s.test(t)){e.innerHTML=String.fromCharCode(65279)+t;var n=e.firstChild;1===n.data.length?e.removeChild(n):n.deleteData(0,1)}else e.innerHTML=t}),l=null}e.exports=u},function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e){var t={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]="number"==typeof e[n]?e[n]:e[n].val);return t},e.exports=t.default},function(e,t,n){"use strict";e.exports=function(e,t){var n,r,i,o=-1,a=e.posMax,s=e.pos,u=e.isInLabel;if(e.isInLabel)return-1;if(e.labelUnmatchedScopes)return e.labelUnmatchedScopes--,-1;for(e.pos=t+1,e.isInLabel=!0,n=1;e.pos1&&void 0!==arguments[1])||arguments[1];return e=(0,r.normalizeArray)(e),{type:s,payload:{thing:e,shown:t}}},t.changeMode=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=(0,r.normalizeArray)(e),{type:a,payload:{thing:e,mode:t}}};var r=n(9),i=t.UPDATE_LAYOUT="layout_update_layout",o=t.UPDATE_FILTER="layout_update_filter",a=t.UPDATE_MODE="layout_update_mode",s=t.SHOW="layout_show"},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.setSelectedServer=function(e,t){return{type:r,payload:{selectedServerUrl:e,namespace:t}}},t.setRequestBodyValue=function(e){var t=e.value,n=e.pathMethod;return{type:i,payload:{value:t,pathMethod:n}}},t.setRequestContentType=function(e){var t=e.value,n=e.pathMethod;return{type:o,payload:{value:t,pathMethod:n}}},t.setResponseContentType=function(e){var t=e.value,n=e.path,r=e.method;return{type:a,payload:{value:t,path:n,method:r}}},t.setServerVariableValue=function(e){var t=e.server,n=e.namespace,r=e.key,i=e.val;return{type:s,payload:{server:t,namespace:n,key:r,val:i}}};var r=t.UPDATE_SELECTED_SERVER="oas3_set_servers",i=t.UPDATE_REQUEST_BODY_VALUE="oas3_set_request_body_value",o=t.UPDATE_REQUEST_CONTENT_TYPE="oas3_set_request_content_type",a=t.UPDATE_RESPONSE_CONTENT_TYPE="oas3_set_response_content_type",s=t.UPDATE_SERVER_VARIABLE_VALUE="oas3_set_server_variable_value"},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.memoizedSampleFromSchema=t.memoizedCreateXMLExample=t.sampleXmlFromSchema=t.inferSchema=t.sampleFromSchema=void 0,t.createXMLExample=p;var r=n(9),i=a(n(1155)),o=a(n(939));function a(e){return e&&e.__esModule?e:{default:e}}var s={string:function(){return"string"},string_email:function(){return"user@example.com"},"string_date-time":function(){return(new Date).toISOString()},number:function(){return 0},number_float:function(){return 0},integer:function(){return 0},boolean:function(e){return"boolean"!=typeof e.default||e.default}},u=function(e){var t=e=(0,r.objectify)(e),n=t.type,i=t.format,o=s[n+"_"+i]||s[n];return(0,r.isFunc)(o)?o(e):"Unknown Type: "+e.type},l=t.sampleFromSchema=function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=(0,r.objectify)(t),o=i.type,a=i.example,s=i.properties,l=i.additionalProperties,c=i.items,p=n.includeReadOnly,f=n.includeWriteOnly;if(void 0!==a)return(0,r.deeplyStripKey)(a,"$$ref",function(e){return"string"==typeof e&&e.indexOf("#")>-1});if(!o)if(s)o="object";else{if(!c)return;o="array"}if("object"===o){var h=(0,r.objectify)(s),d={};for(var m in h)h[m].readOnly&&!p||h[m].writeOnly&&!f||(d[m]=e(h[m],n));if(!0===l)d.additionalProp1={};else if(l)for(var v=(0,r.objectify)(l),g=e(v,n),y=1;y<4;y++)d["additionalProp"+y]=g;return d}return"array"===o?Array.isArray(c.anyOf)?c.anyOf.map(function(t){return e(t,n)}):Array.isArray(c.oneOf)?c.oneOf.map(function(t){return e(t,n)}):[e(c,n)]:t.enum?t.default?t.default:(0,r.normalizeArray)(t.enum)[0]:"file"!==o?u(t):void 0},c=(t.inferSchema=function(e){return e.schema&&(e=e.schema),e.properties&&(e.type="object"),e},t.sampleXmlFromSchema=function e(t){var n,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=(0,r.objectify)(t),a=o.type,s=o.properties,l=o.additionalProperties,c=o.items,p=o.example,f=i.includeReadOnly,h=i.includeWriteOnly,d=o.default,m={},v={},g=t.xml,y=g.name,_=g.prefix,b=g.namespace,x=o.enum,k=void 0;if(!a)if(s||l)a="object";else{if(!c)return;a="array"}(y=y||"notagname",n=(_?_+":":"")+y,b)&&(v[_?"xmlns:"+_:"xmlns"]=b);if("array"===a&&c){if(c.xml=c.xml||g||{},c.xml.name=c.xml.name||g.name,g.wrapped)return m[n]=[],Array.isArray(p)?p.forEach(function(t){c.example=t,m[n].push(e(c,i))}):Array.isArray(d)?d.forEach(function(t){c.default=t,m[n].push(e(c,i))}):m[n]=[e(c,i)],v&&m[n].push({_attr:v}),m;var w=[];return Array.isArray(p)?(p.forEach(function(t){c.example=t,w.push(e(c,i))}),w):Array.isArray(d)?(d.forEach(function(t){c.default=t,w.push(e(c,i))}),w):e(c,i)}if("object"===a){var E=(0,r.objectify)(s);for(var S in m[n]=[],p=p||{},E)if(E.hasOwnProperty(S)&&(!E[S].readOnly||f)&&(!E[S].writeOnly||h))if(E[S].xml=E[S].xml||{},E[S].xml.attribute){var C=Array.isArray(E[S].enum)&&E[S].enum[0],A=E[S].example,D=E[S].default;v[E[S].xml.name||S]=void 0!==A&&A||void 0!==p[S]&&p[S]||void 0!==D&&D||C||u(E[S])}else{E[S].xml.name=E[S].xml.name||S,void 0===E[S].example&&void 0!==p[S]&&(E[S].example=p[S]);var M=e(E[S]);Array.isArray(M)?m[n]=m[n].concat(M):m[n].push(M)}return!0===l?m[n].push({additionalProp:"Anything can be here"}):l&&m[n].push({additionalProp:u(l)}),v&&m[n].push({_attr:v}),m}return k=void 0!==p?p:void 0!==d?d:Array.isArray(x)?x[0]:u(t),m[n]=v?[{_attr:v},k]:k,m});function p(e,t){var n=c(e,t);if(n)return(0,i.default)(n,{declaration:!0,indent:"\t"})}t.memoizedCreateXMLExample=(0,o.default)(p),t.memoizedSampleFromSchema=(0,o.default)(l)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.execute=t.executeRequest=t.logRequest=t.setMutatedRequest=t.setRequest=t.setResponse=t.validateParams=t.invalidateResolvedSubtreeCache=t.updateResolvedSubtree=t.requestResolvedSubtree=t.resolveSpec=t.parseToJson=t.SET_SCHEME=t.UPDATE_RESOLVED_SUBTREE=t.UPDATE_RESOLVED=t.UPDATE_OPERATION_META_VALUE=t.CLEAR_VALIDATE_PARAMS=t.CLEAR_REQUEST=t.CLEAR_RESPONSE=t.LOG_REQUEST=t.SET_MUTATED_REQUEST=t.SET_REQUEST=t.SET_RESPONSE=t.VALIDATE_PARAMS=t.UPDATE_PARAM=t.UPDATE_JSON=t.UPDATE_URL=t.UPDATE_SPEC=void 0;var r=_(n(24)),i=_(n(96)),o=_(n(23)),a=_(n(42)),s=_(n(123)),u=_(n(327)),l=_(n(326)),c=_(n(43));t.updateSpec=function(e){var t=F(e).replace(/\t/g," ");if("string"==typeof e)return{type:b,payload:t}},t.updateResolved=function(e){return{type:I,payload:e}},t.updateUrl=function(e){return{type:x,payload:e}},t.updateJsonSpec=function(e){return{type:k,payload:e}},t.changeParam=function(e,t,n,r,i){return{type:w,payload:{path:e,value:r,paramName:t,paramIn:n,isXml:i}}},t.clearValidateParams=function(e){return{type:T,payload:{pathMethod:e}}},t.changeConsumesValue=function(e,t){return{type:P,payload:{path:e,value:t,key:"consumes_value"}}},t.changeProducesValue=function(e,t){return{type:P,payload:{path:e,value:t,key:"produces_value"}}},t.clearResponse=function(e,t){return{type:M,payload:{path:e,method:t}}},t.clearRequest=function(e,t){return{type:O,payload:{path:e,method:t}}},t.setScheme=function(e,t,n){return{type:N,payload:{scheme:e,path:t,method:n}}};var p=_(n(211)),f=n(7),h=_(n(481)),d=_(n(256)),m=_(n(415)),v=_(n(913)),g=_(n(926)),y=n(9);function _(e){return e&&e.__esModule?e:{default:e}}var b=t.UPDATE_SPEC="spec_update_spec",x=t.UPDATE_URL="spec_update_url",k=t.UPDATE_JSON="spec_update_json",w=t.UPDATE_PARAM="spec_update_param",E=t.VALIDATE_PARAMS="spec_validate_param",S=t.SET_RESPONSE="spec_set_response",C=t.SET_REQUEST="spec_set_request",A=t.SET_MUTATED_REQUEST="spec_set_mutated_request",D=t.LOG_REQUEST="spec_log_request",M=t.CLEAR_RESPONSE="spec_clear_response",O=t.CLEAR_REQUEST="spec_clear_request",T=t.CLEAR_VALIDATE_PARAMS="spec_clear_validate_param",P=t.UPDATE_OPERATION_META_VALUE="spec_update_operation_meta_value",I=t.UPDATE_RESOLVED="spec_update_resolved",R=t.UPDATE_RESOLVED_SUBTREE="spec_update_resolved_subtree",N=t.SET_SCHEME="set_scheme",F=function(e){return(0,m.default)(e)?e:""};t.parseToJson=function(e){return function(t){var n=t.specActions,r=t.specSelectors,i=t.errActions,o=r.specStr,a=null;try{e=e||o(),i.clear({source:"parser"}),a=p.default.safeLoad(e)}catch(e){return console.error(e),i.newSpecErr({source:"parser",level:"error",message:e.reason,line:e.mark&&e.mark.line?e.mark.line+1:void 0})}return a&&"object"===(void 0===a?"undefined":(0,c.default)(a))?n.updateJsonSpec(a):{}}};var j=!1,B=(t.resolveSpec=function(e,t){return function(n){var r=n.specActions,i=n.specSelectors,o=n.errActions,a=n.fn,s=a.fetch,u=a.resolve,l=a.AST,c=n.getConfigs;j||(console.warn("specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use requestResolvedSubtree instead!"),j=!0);var p=c(),f=p.modelPropertyMacro,h=p.parameterMacro,d=p.requestInterceptor,m=p.responseInterceptor;void 0===e&&(e=i.specJson()),void 0===t&&(t=i.url());var v=l.getLineNumberForPath,g=i.specStr();return u({fetch:s,spec:e,baseDoc:t,modelPropertyMacro:f,parameterMacro:h,requestInterceptor:d,responseInterceptor:m}).then(function(e){var t=e.spec,n=e.errors;if(o.clear({type:"thrown"}),Array.isArray(n)&&n.length>0){var i=n.map(function(e){return console.error(e),e.line=e.fullPath?v(g,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",Object.defineProperty(e,"message",{enumerable:!0,value:e.message}),e});o.newThrownErrBatch(i)}return r.updateResolved(t)})}},[]),L=(0,v.default)((0,l.default)(u.default.mark(function e(){var t,n,r,i,o,a,c,p,h,d,m,v,y,_,b;return u.default.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(t=B.system){e.next=4;break}return console.error("debResolveSubtrees: don't have a system to operate on, aborting."),e.abrupt("return");case 4:if(n=t.errActions,r=t.errSelectors,i=t.fn,o=i.resolveSubtree,a=i.AST.getLineNumberForPath,c=t.specSelectors,p=t.specActions,o){e.next=8;break}return console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing."),e.abrupt("return");case 8:return h=c.specStr(),d=t.getConfigs(),m=d.modelPropertyMacro,v=d.parameterMacro,y=d.requestInterceptor,_=d.responseInterceptor,e.prev=10,e.next=13,B.reduce(function(){var e=(0,l.default)(u.default.mark(function e(t,i){var s,l,p,f,d,b,x;return u.default.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t;case 2:return s=e.sent,l=s.resultMap,p=s.specWithCurrentSubtrees,e.next=7,o(p,i,{baseDoc:c.url(),modelPropertyMacro:m,parameterMacro:v,requestInterceptor:y,responseInterceptor:_});case 7:return f=e.sent,d=f.errors,b=f.spec,r.allErrors().size&&n.clear({type:"thrown"}),Array.isArray(d)&&d.length>0&&(x=d.map(function(e){return e.line=e.fullPath?a(h,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",Object.defineProperty(e,"message",{enumerable:!0,value:e.message}),e}),n.newThrownErrBatch(x)),(0,g.default)(l,i,b),(0,g.default)(p,i,b),e.abrupt("return",{resultMap:l,specWithCurrentSubtrees:p});case 15:case"end":return e.stop()}},e,void 0)}));return function(t,n){return e.apply(this,arguments)}}(),s.default.resolve({resultMap:(c.specResolvedSubtree([])||(0,f.Map)()).toJS(),specWithCurrentSubtrees:c.specJson().toJS()}));case 13:b=e.sent,delete B.system,B=[],e.next=21;break;case 18:e.prev=18,e.t0=e.catch(10),console.error(e.t0);case 21:p.updateResolvedSubtree([],b.resultMap);case 22:case"end":return e.stop()}},e,void 0,[[10,18]])})),35);t.requestResolvedSubtree=function(e){return function(t){B.push(e),B.system=t,L()}};t.updateResolvedSubtree=function(e,t){return{type:R,payload:{path:e,value:t}}},t.invalidateResolvedSubtreeCache=function(){return{type:R,payload:{path:[],value:(0,f.Map)()}}},t.validateParams=function(e,t){return{type:E,payload:{pathMethod:e,isOAS3:t}}};t.setResponse=function(e,t,n){return{payload:{path:e,method:t,res:n},type:S}},t.setRequest=function(e,t,n){return{payload:{path:e,method:t,req:n},type:C}},t.setMutatedRequest=function(e,t,n){return{payload:{path:e,method:t,req:n},type:A}},t.logRequest=function(e){return{payload:e,type:D}},t.executeRequest=function(e){return function(t){var n=t.fn,r=t.specActions,i=t.specSelectors,s=t.getConfigs,u=t.oas3Selectors,l=e.pathName,c=e.method,p=e.operation,f=s(),m=f.requestInterceptor,v=f.responseInterceptor,g=p.toJS();if(e.contextUrl=(0,h.default)(i.url()).toString(),g&&g.operationId?e.operationId=g.operationId:g&&l&&c&&(e.operationId=n.opId(g,l,c)),i.isOAS3()){var _=l+":"+c;e.server=u.selectedServer(_)||u.selectedServer();var b=u.serverVariables({server:e.server,namespace:_}).toJS(),x=u.serverVariables({server:e.server}).toJS();e.serverVariables=(0,a.default)(b).length?b:x,e.requestContentType=u.requestContentType(l,c),e.responseContentType=u.responseContentType(l,c)||"*/*";var k=u.requestBodyValue(l,c);(0,y.isJSONObject)(k)?e.requestBody=JSON.parse(k):e.requestBody=k}var w=(0,o.default)({},e);w=n.buildRequest(w),r.setRequest(e.pathName,e.method,w);e.requestInterceptor=function(t){var n=m.apply(this,[t]),i=(0,o.default)({},n);return r.setMutatedRequest(e.pathName,e.method,i),n},e.responseInterceptor=v;var E=Date.now();return n.execute(e).then(function(t){t.duration=Date.now()-E,r.setResponse(e.pathName,e.method,t)}).catch(function(t){return r.setResponse(e.pathName,e.method,{error:!0,err:(0,d.default)(t)})})}};t.execute=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.path,n=e.method,o=(0,i.default)(e,["path","method"]);return function(e){var i=e.fn.fetch,a=e.specSelectors,s=e.specActions,u=a.specJsonWithResolvedSubtrees().toJS(),l=a.operationScheme(t,n),c=a.contentTypeValues([t,n]).toJS(),p=c.requestContentType,f=c.responseContentType,h=/xml/i.test(p),d=a.parameterValues([t,n],h).toJS();return s.executeRequest((0,r.default)({},o,{fetch:i,spec:u,pathName:t,method:n,parameters:d,requestContentType:p,scheme:l,responseContentType:f}))}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.validateBeforeExecute=t.canExecuteScheme=t.operationScheme=t.hasHost=t.parameterWithMeta=t.operationWithMeta=t.allowTryItOutFor=t.mutatedRequestFor=t.requestFor=t.responseFor=t.mutatedRequests=t.requests=t.responses=t.taggedOperations=t.operationsWithTags=t.tagDetails=t.tags=t.operationsWithRootInherited=t.schemes=t.host=t.basePath=t.definitions=t.findDefinition=t.securityDefinitions=t.security=t.produces=t.consumes=t.operations=t.paths=t.semver=t.version=t.externalDocs=t.info=t.isOAS3=t.spec=t.specJsonWithResolvedSubtrees=t.specResolvedSubtree=t.specResolved=t.specJson=t.specSource=t.specStr=t.url=t.lastError=void 0;var r,i=n(71),o=(r=i)&&r.__esModule?r:{default:r};t.getParameter=function(e,t,n,r){return t=t||[],e.getIn(["meta","paths"].concat((0,o.default)(t),["parameters"]),(0,u.fromJS)([])).find(function(e){return u.Map.isMap(e)&&e.get("name")===n&&e.get("in")===r})||(0,u.Map)()},t.parameterValues=function(e,t,n){return t=t||[],D.apply(void 0,[e].concat((0,o.default)(t))).get("parameters",(0,u.List)()).reduce(function(e,t){var r=n&&"body"===t.get("in")?t.get("value_xml"):t.get("value");return e.set(t.get("in")+"."+t.get("name"),r)},(0,u.fromJS)({}))},t.parametersIncludeIn=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(u.List.isList(e))return e.some(function(e){return u.Map.isMap(e)&&e.get("in")===t})},t.parametersIncludeType=M,t.contentTypeValues=function(e,t){t=t||[];var n=h(e).getIn(["paths"].concat((0,o.default)(t)),(0,u.fromJS)({})),r=e.getIn(["meta","paths"].concat((0,o.default)(t)),(0,u.fromJS)({})),i=O(e,t),a=n.get("parameters")||new u.List,s=r.get("consumes_value")?r.get("consumes_value"):M(a,"file")?"multipart/form-data":M(a,"formData")?"application/x-www-form-urlencoded":void 0;return(0,u.fromJS)({requestContentType:s,responseContentType:i})},t.operationConsumes=function(e,t){return t=t||[],h(e).getIn(["paths"].concat((0,o.default)(t),["consumes"]),(0,u.fromJS)({}))},t.currentProducesFor=O;var a=n(60),s=n(9),u=n(7);var l=["get","put","post","delete","options","head","patch","trace"],c=function(e){return e||(0,u.Map)()},p=(t.lastError=(0,a.createSelector)(c,function(e){return e.get("lastError")}),t.url=(0,a.createSelector)(c,function(e){return e.get("url")}),t.specStr=(0,a.createSelector)(c,function(e){return e.get("spec")||""}),t.specSource=(0,a.createSelector)(c,function(e){return e.get("specSource")||"not-editor"}),t.specJson=(0,a.createSelector)(c,function(e){return e.get("json",(0,u.Map)())})),f=(t.specResolved=(0,a.createSelector)(c,function(e){return e.get("resolved",(0,u.Map)())}),t.specResolvedSubtree=function(e,t){return e.getIn(["resolvedSubtrees"].concat((0,o.default)(t)),void 0)},function e(t,n){return u.Map.isMap(t)&&u.Map.isMap(n)?n.get("$$ref")?n:(0,u.OrderedMap)().mergeWith(e,t,n):n}),h=t.specJsonWithResolvedSubtrees=(0,a.createSelector)(c,function(e){return(0,u.OrderedMap)().mergeWith(f,e.get("json"),e.get("resolvedSubtrees"))}),d=t.spec=function(e){return p(e)},m=(t.isOAS3=(0,a.createSelector)(d,function(){return!1}),t.info=(0,a.createSelector)(d,function(e){return P(e&&e.get("info"))})),v=(t.externalDocs=(0,a.createSelector)(d,function(e){return P(e&&e.get("externalDocs"))}),t.version=(0,a.createSelector)(m,function(e){return e&&e.get("version")})),g=(t.semver=(0,a.createSelector)(v,function(e){return/v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(e).slice(1)}),t.paths=(0,a.createSelector)(h,function(e){return e.get("paths")})),y=t.operations=(0,a.createSelector)(g,function(e){if(!e||e.size<1)return(0,u.List)();var t=(0,u.List)();return e&&e.forEach?(e.forEach(function(e,n){if(!e||!e.forEach)return{};e.forEach(function(e,r){l.indexOf(r)<0||(t=t.push((0,u.fromJS)({path:n,method:r,operation:e,id:r+"-"+n})))})}),t):(0,u.List)()}),_=t.consumes=(0,a.createSelector)(d,function(e){return(0,u.Set)(e.get("consumes"))}),b=t.produces=(0,a.createSelector)(d,function(e){return(0,u.Set)(e.get("produces"))}),x=(t.security=(0,a.createSelector)(d,function(e){return e.get("security",(0,u.List)())}),t.securityDefinitions=(0,a.createSelector)(d,function(e){return e.get("securityDefinitions")}),t.findDefinition=function(e,t){var n=e.getIn(["resolvedSubtrees","definitions",t],null),r=e.getIn(["json","definitions",t],null);return n||r||null},t.definitions=(0,a.createSelector)(d,function(e){return e.get("definitions")||(0,u.Map)()}),t.basePath=(0,a.createSelector)(d,function(e){return e.get("basePath")}),t.host=(0,a.createSelector)(d,function(e){return e.get("host")}),t.schemes=(0,a.createSelector)(d,function(e){return e.get("schemes",(0,u.Map)())}),t.operationsWithRootInherited=(0,a.createSelector)(y,_,b,function(e,t,n){return e.map(function(e){return e.update("operation",function(e){if(e){if(!u.Map.isMap(e))return;return e.withMutations(function(e){return e.get("consumes")||e.update("consumes",function(e){return(0,u.Set)(e).merge(t)}),e.get("produces")||e.update("produces",function(e){return(0,u.Set)(e).merge(n)}),e})}return(0,u.Map)()})})})),k=t.tags=(0,a.createSelector)(d,function(e){return e.get("tags",(0,u.List)())}),w=t.tagDetails=function(e,t){return(k(e)||(0,u.List)()).filter(u.Map.isMap).find(function(e){return e.get("name")===t},(0,u.Map)())},E=t.operationsWithTags=(0,a.createSelector)(x,k,function(e,t){return e.reduce(function(e,t){var n=(0,u.Set)(t.getIn(["operation","tags"]));return n.count()<1?e.update("default",(0,u.List)(),function(e){return e.push(t)}):n.reduce(function(e,n){return e.update(n,(0,u.List)(),function(e){return e.push(t)})},e)},t.reduce(function(e,t){return e.set(t.get("name"),(0,u.List)())},(0,u.OrderedMap)()))}),S=(t.taggedOperations=function(e){return function(t){var n=(0,t.getConfigs)(),r=n.tagsSorter,i=n.operationsSorter;return E(e).sortBy(function(e,t){return t},function(e,t){var n="function"==typeof r?r:s.sorters.tagsSorter[r];return n?n(e,t):null}).map(function(t,n){var r="function"==typeof i?i:s.sorters.operationsSorter[i],o=r?t.sort(r):t;return(0,u.Map)({tagDetails:w(e,n),operations:o})})}},t.responses=(0,a.createSelector)(c,function(e){return e.get("responses",(0,u.Map)())})),C=t.requests=(0,a.createSelector)(c,function(e){return e.get("requests",(0,u.Map)())}),A=t.mutatedRequests=(0,a.createSelector)(c,function(e){return e.get("mutatedRequests",(0,u.Map)())}),D=(t.responseFor=function(e,t,n){return S(e).getIn([t,n],null)},t.requestFor=function(e,t,n){return C(e).getIn([t,n],null)},t.mutatedRequestFor=function(e,t,n){return A(e).getIn([t,n],null)},t.allowTryItOutFor=function(){return!0},t.operationWithMeta=function(e,t,n){var r=h(e).getIn(["paths",t,n],(0,u.Map)()),i=e.getIn(["meta","paths",t,n],(0,u.Map)()),o=r.get("parameters",(0,u.List)()).map(function(e){return(0,u.Map)().merge(e,i.getIn(["parameters",e.get("name")+"."+e.get("in")]))});return(0,u.Map)().merge(r,i).set("parameters",o)});t.parameterWithMeta=function(e,t,n,r){var i=h(e).getIn(["paths"].concat((0,o.default)(t),["parameters"]),(0,u.Map)()),a=e.getIn(["meta","paths"].concat((0,o.default)(t),["parameters"]),(0,u.Map)());return i.map(function(e){return(0,u.Map)().merge(e,a.get(e.get("name")+"."+e.get("in")))}).find(function(e){return e.get("in")===r&&e.get("name")===n},(0,u.Map)())};t.hasHost=(0,a.createSelector)(d,function(e){var t=e.get("host");return"string"==typeof t&&t.length>0&&"/"!==t[0]});function M(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(u.List.isList(e))return e.some(function(e){return u.Map.isMap(e)&&e.get("type")===t})}function O(e,t){t=t||[];var n=h(e).getIn(["paths"].concat((0,o.default)(t)),null);if(null!==n){var r=e.getIn(["meta","paths"].concat((0,o.default)(t),["produces_value"]),null),i=n.getIn(["produces",0],null);return r||i||"application/json"}}var T=t.operationScheme=function(e,t,n){var r=e.get("url").match(/^([a-z][a-z0-9+\-.]*):/),i=Array.isArray(r)?r[1]:null;return e.getIn(["scheme",t,n])||e.getIn(["scheme","_defaultScheme"])||i||""};t.canExecuteScheme=function(e,t,n){return["http","https"].indexOf(T(e,t,n))>-1},t.validateBeforeExecute=function(e,t){t=t||[];var n=!0;return e.getIn(["meta","paths"].concat((0,o.default)(t),["parameters"]),(0,u.fromJS)([])).forEach(function(e){var t=e.get("errors");t&&t.count()&&(n=!1)}),n};function P(e){return u.Map.isMap(e)?e:new u.Map}},function(e,t,n){"use strict";function r(e){switch(e._type){case"document":case"block_quote":case"list":case"item":case"paragraph":case"heading":case"emph":case"strong":case"link":case"image":case"custom_inline":case"custom_block":return!0;default:return!1}}var i=function(e,t){this.current=e,this.entering=!0===t},o=function(){var e=this.current,t=this.entering;if(null===e)return null;var n=r(e);return t&&n?e._firstChild?(this.current=e._firstChild,this.entering=!0):this.entering=!1:e===this.root?this.current=null:null===e._next?(this.current=e._parent,this.entering=!1):(this.current=e._next,this.entering=!0),{entering:t,node:e}},a=function(e,t){this._type=e,this._parent=null,this._firstChild=null,this._lastChild=null,this._prev=null,this._next=null,this._sourcepos=t,this._lastLineBlank=!1,this._open=!0,this._string_content=null,this._literal=null,this._listData={},this._info=null,this._destination=null,this._title=null,this._isFenced=!1,this._fenceChar=null,this._fenceLength=0,this._fenceOffset=null,this._level=null,this._onEnter=null,this._onExit=null},s=a.prototype;Object.defineProperty(s,"isContainer",{get:function(){return r(this)}}),Object.defineProperty(s,"type",{get:function(){return this._type}}),Object.defineProperty(s,"firstChild",{get:function(){return this._firstChild}}),Object.defineProperty(s,"lastChild",{get:function(){return this._lastChild}}),Object.defineProperty(s,"next",{get:function(){return this._next}}),Object.defineProperty(s,"prev",{get:function(){return this._prev}}),Object.defineProperty(s,"parent",{get:function(){return this._parent}}),Object.defineProperty(s,"sourcepos",{get:function(){return this._sourcepos}}),Object.defineProperty(s,"literal",{get:function(){return this._literal},set:function(e){this._literal=e}}),Object.defineProperty(s,"destination",{get:function(){return this._destination},set:function(e){this._destination=e}}),Object.defineProperty(s,"title",{get:function(){return this._title},set:function(e){this._title=e}}),Object.defineProperty(s,"info",{get:function(){return this._info},set:function(e){this._info=e}}),Object.defineProperty(s,"level",{get:function(){return this._level},set:function(e){this._level=e}}),Object.defineProperty(s,"listType",{get:function(){return this._listData.type},set:function(e){this._listData.type=e}}),Object.defineProperty(s,"listTight",{get:function(){return this._listData.tight},set:function(e){this._listData.tight=e}}),Object.defineProperty(s,"listStart",{get:function(){return this._listData.start},set:function(e){this._listData.start=e}}),Object.defineProperty(s,"listDelimiter",{get:function(){return this._listData.delimiter},set:function(e){this._listData.delimiter=e}}),Object.defineProperty(s,"onEnter",{get:function(){return this._onEnter},set:function(e){this._onEnter=e}}),Object.defineProperty(s,"onExit",{get:function(){return this._onExit},set:function(e){this._onExit=e}}),a.prototype.appendChild=function(e){e.unlink(),e._parent=this,this._lastChild?(this._lastChild._next=e,e._prev=this._lastChild,this._lastChild=e):(this._firstChild=e,this._lastChild=e)},a.prototype.prependChild=function(e){e.unlink(),e._parent=this,this._firstChild?(this._firstChild._prev=e,e._next=this._firstChild,this._firstChild=e):(this._firstChild=e,this._lastChild=e)},a.prototype.unlink=function(){this._prev?this._prev._next=this._next:this._parent&&(this._parent._firstChild=this._next),this._next?this._next._prev=this._prev:this._parent&&(this._parent._lastChild=this._prev),this._parent=null,this._next=null,this._prev=null},a.prototype.insertAfter=function(e){e.unlink(),e._next=this._next,e._next&&(e._next._prev=e),e._prev=this,this._next=e,e._parent=this._parent,e._next||(e._parent._lastChild=e)},a.prototype.insertBefore=function(e){e.unlink(),e._prev=this._prev,e._prev&&(e._prev._next=e),e._next=this,this._prev=e,e._parent=this._parent,e._prev||(e._parent._firstChild=e)},a.prototype.walker=function(){return new function(e){return{current:e,root:e,entering:!0,next:o,resumeAt:i}}(this)},e.exports=a},function(e,t){e.exports=function(e,t,n,r){if(!(e instanceof t)||void 0!==r&&r in e)throw TypeError(n+": incorrect invocation!");return e}},function(e,t,n){var r=n(53),i=n(180),o=n(75),a=n(128),s=n(584);e.exports=function(e,t){var n=1==e,u=2==e,l=3==e,c=4==e,p=6==e,f=5==e||p,h=t||s;return function(t,s,d){for(var m,v,g=o(t),y=i(g),_=r(s,d,3),b=a(y.length),x=0,k=n?h(t,b):u?h(t,0):void 0;b>x;x++)if((f||x in y)&&(v=_(m=y[x],x,g),e))if(n)k[x]=v;else if(v)switch(e){case 3:return!0;case 5:return m;case 6:return x;case 2:k.push(m)}else if(c)return!1;return p?-1:l||c?c:k}}},function(e,t,n){var r=n(98),i=n(21)("toStringTag"),o="Arguments"==r(function(){return arguments}());e.exports=function(e){var t,n,a;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),i))?n:o?r(t):"Object"==(a=r(t))&&"function"==typeof t.callee?"Arguments":a}},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(29),i=n(20).document,o=r(i)&&r(i.createElement);e.exports=function(e){return o?i.createElement(e):{}}},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t,n){var r=n(98);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==r(e)?e.split(""):Object(e)}},function(e,t,n){"use strict";var r=n(97);e.exports.f=function(e){return new function(e){var t,n;this.promise=new e(function(e,r){if(void 0!==t||void 0!==n)throw TypeError("Bad Promise constructor");t=e,n=r}),this.resolve=r(t),this.reject=r(n)}(e)}},function(e,t,n){var r=n(37),i=n(593),o=n(179),a=n(186)("IE_PROTO"),s=function(){},u=function(){var e,t=n(178)("iframe"),r=o.length;for(t.style.display="none",n(329).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("