mirror of
https://github.com/correl/openapi-core.git
synced 2025-01-01 11:03:19 +00:00
servers with request validation
This commit is contained in:
parent
84546fee8e
commit
d60bde446d
6 changed files with 219 additions and 22 deletions
|
@ -15,3 +15,7 @@ class MissingParameterError(OpenAPIMappingError):
|
||||||
|
|
||||||
class InvalidContentTypeError(OpenAPIMappingError):
|
class InvalidContentTypeError(OpenAPIMappingError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidServerError(OpenAPIMappingError):
|
||||||
|
pass
|
||||||
|
|
76
openapi_core/servers.py
Normal file
76
openapi_core/servers.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
|
||||||
|
class Server(object):
|
||||||
|
|
||||||
|
def __init__(self, url, variables=None):
|
||||||
|
self.url = url
|
||||||
|
self.variables = variables and dict(variables) or {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_url(self):
|
||||||
|
return self.get_url()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_variables(self):
|
||||||
|
defaults = {}
|
||||||
|
for name, variable in iteritems(self.variables):
|
||||||
|
defaults[name] = variable.default
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
def get_url(self, **variables):
|
||||||
|
if not variables:
|
||||||
|
variables = self.default_variables
|
||||||
|
return self.url.format(**variables)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerVariable(object):
|
||||||
|
|
||||||
|
def __init__(self, name, default, enum=None):
|
||||||
|
self.name = name
|
||||||
|
self.default = default
|
||||||
|
self.enum = enum and list(enum) or []
|
||||||
|
|
||||||
|
|
||||||
|
class ServersGenerator(object):
|
||||||
|
|
||||||
|
def __init__(self, dereferencer):
|
||||||
|
self.dereferencer = dereferencer
|
||||||
|
|
||||||
|
def generate(self, servers_spec):
|
||||||
|
servers_deref = self.dereferencer.dereference(servers_spec)
|
||||||
|
for server_spec in servers_deref:
|
||||||
|
url = server_spec['url']
|
||||||
|
variables_spec = server_spec.get('variables', {})
|
||||||
|
|
||||||
|
variables = None
|
||||||
|
if variables_spec:
|
||||||
|
variables = self.variables_generator.generate(variables_spec)
|
||||||
|
|
||||||
|
yield Server(url, variables=variables)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lru_cache()
|
||||||
|
def variables_generator(self):
|
||||||
|
return ServerVariablesGenerator(self.dereferencer)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerVariablesGenerator(object):
|
||||||
|
|
||||||
|
def __init__(self, dereferencer):
|
||||||
|
self.dereferencer = dereferencer
|
||||||
|
|
||||||
|
def generate(self, variables_spec):
|
||||||
|
variables_deref = self.dereferencer.dereference(variables_spec)
|
||||||
|
|
||||||
|
if not variables_deref:
|
||||||
|
return [Server('/'), ]
|
||||||
|
|
||||||
|
for variable_name, variable_spec in iteritems(variables_deref):
|
||||||
|
default = variable_spec['default']
|
||||||
|
enum = variable_spec.get('enum')
|
||||||
|
|
||||||
|
variable = ServerVariable(variable_name, default, enum=enum)
|
||||||
|
yield variable_name, variable
|
|
@ -9,6 +9,7 @@ from openapi_core.components import ComponentsFactory
|
||||||
from openapi_core.infos import InfoFactory
|
from openapi_core.infos import InfoFactory
|
||||||
from openapi_core.paths import PathsGenerator
|
from openapi_core.paths import PathsGenerator
|
||||||
from openapi_core.schemas import SchemaRegistry
|
from openapi_core.schemas import SchemaRegistry
|
||||||
|
from openapi_core.servers import ServersGenerator
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -26,8 +27,12 @@ class Spec(object):
|
||||||
def __getitem__(self, path_name):
|
def __getitem__(self, path_name):
|
||||||
return self.paths[path_name]
|
return self.paths[path_name]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_url(self):
|
||||||
|
return self.servers[0].default_url
|
||||||
|
|
||||||
def get_server_url(self, index=0):
|
def get_server_url(self, index=0):
|
||||||
return self.servers[index]['url']
|
return self.servers[index].default_url
|
||||||
|
|
||||||
def get_operation(self, path_pattern, http_method):
|
def get_operation(self, path_pattern, http_method):
|
||||||
return self.paths[path_pattern].operations[http_method]
|
return self.paths[path_pattern].operations[http_method]
|
||||||
|
@ -59,14 +64,16 @@ class SpecFactory(object):
|
||||||
spec_dict_deref = self.dereferencer.dereference(spec_dict)
|
spec_dict_deref = self.dereferencer.dereference(spec_dict)
|
||||||
|
|
||||||
info_spec = spec_dict_deref.get('info', [])
|
info_spec = spec_dict_deref.get('info', [])
|
||||||
servers = spec_dict_deref.get('servers', [])
|
servers_spec = spec_dict_deref.get('servers', [])
|
||||||
paths = spec_dict_deref.get('paths', [])
|
paths = spec_dict_deref.get('paths', [])
|
||||||
components_spec = spec_dict_deref.get('components', [])
|
components_spec = spec_dict_deref.get('components', [])
|
||||||
|
|
||||||
info = self.info_factory.create(info_spec)
|
info = self.info_factory.create(info_spec)
|
||||||
|
servers = self.servers_generator.generate(servers_spec)
|
||||||
paths = self.paths_generator.generate(paths)
|
paths = self.paths_generator.generate(paths)
|
||||||
components = self.components_factory.create(components_spec)
|
components = self.components_factory.create(components_spec)
|
||||||
spec = Spec(info, list(paths), servers=servers, components=components)
|
spec = Spec(
|
||||||
|
info, list(paths), servers=list(servers), components=components)
|
||||||
return spec
|
return spec
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -79,6 +86,11 @@ class SpecFactory(object):
|
||||||
def info_factory(self):
|
def info_factory(self):
|
||||||
return InfoFactory(self.dereferencer)
|
return InfoFactory(self.dereferencer)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lru_cache()
|
||||||
|
def servers_generator(self):
|
||||||
|
return ServersGenerator(self.dereferencer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
def paths_generator(self):
|
def paths_generator(self):
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
"""OpenAPI core wrappers module"""
|
"""OpenAPI core wrappers module"""
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
from six.moves.urllib.parse import urljoin
|
||||||
|
|
||||||
from openapi_core.exceptions import (
|
from openapi_core.exceptions import (
|
||||||
OpenAPIMappingError, MissingParameterError, InvalidContentTypeError,
|
OpenAPIMappingError, MissingParameterError, InvalidContentTypeError,
|
||||||
|
InvalidServerError,
|
||||||
)
|
)
|
||||||
|
|
||||||
SPEC_LOCATION_TO_REQUEST_LOCATION_MAPPING = {
|
SPEC_LOCATION_TO_REQUEST_LOCATION_MAPPING = {
|
||||||
|
@ -32,13 +34,32 @@ class RequestParameters(dict):
|
||||||
"Unknown parameter location: {0}".format(str(location)))
|
"Unknown parameter location: {0}".format(str(location)))
|
||||||
|
|
||||||
|
|
||||||
class RequestParametersFactory(object):
|
class BaseRequestFactory(object):
|
||||||
|
|
||||||
|
def get_operation(self, request, spec):
|
||||||
|
server = self._get_server(request, spec)
|
||||||
|
|
||||||
|
operation_pattern = request.full_url_pattern.replace(
|
||||||
|
server.default_url, '')
|
||||||
|
|
||||||
|
return spec.get_operation(operation_pattern, request.method)
|
||||||
|
|
||||||
|
def _get_server(self, request, spec):
|
||||||
|
for server in spec.servers:
|
||||||
|
if server.default_url in request.full_url_pattern:
|
||||||
|
return server
|
||||||
|
|
||||||
|
raise InvalidServerError(
|
||||||
|
"Invalid request server {0}".format(request.full_url_pattern))
|
||||||
|
|
||||||
|
|
||||||
|
class RequestParametersFactory(BaseRequestFactory):
|
||||||
|
|
||||||
def __init__(self, attr_mapping=SPEC_LOCATION_TO_REQUEST_LOCATION_MAPPING):
|
def __init__(self, attr_mapping=SPEC_LOCATION_TO_REQUEST_LOCATION_MAPPING):
|
||||||
self.attr_mapping = attr_mapping
|
self.attr_mapping = attr_mapping
|
||||||
|
|
||||||
def create(self, request, spec):
|
def create(self, request, spec):
|
||||||
operation = spec.get_operation(request.path_pattern, request.method)
|
operation = self.get_operation(request, spec)
|
||||||
|
|
||||||
params = RequestParameters()
|
params = RequestParameters()
|
||||||
for param_name, param in iteritems(operation.parameters):
|
for param_name, param in iteritems(operation.parameters):
|
||||||
|
@ -65,10 +86,10 @@ class RequestParametersFactory(object):
|
||||||
return param.unmarshal(raw_value)
|
return param.unmarshal(raw_value)
|
||||||
|
|
||||||
|
|
||||||
class RequestBodyFactory(object):
|
class RequestBodyFactory(BaseRequestFactory):
|
||||||
|
|
||||||
def create(self, request, spec):
|
def create(self, request, spec):
|
||||||
operation = spec.get_operation(request.path_pattern, request.method)
|
operation = self.get_operation(request, spec)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
media_type = operation.request_body[request.content_type]
|
media_type = operation.request_body[request.content_type]
|
||||||
|
@ -78,9 +99,13 @@ class RequestBodyFactory(object):
|
||||||
|
|
||||||
return media_type.unmarshal(request.data)
|
return media_type.unmarshal(request.data)
|
||||||
|
|
||||||
|
def _get_operation(self, request, spec):
|
||||||
|
return spec.get_operation(request.path_pattern, request.method)
|
||||||
|
|
||||||
|
|
||||||
class BaseOpenAPIRequest(object):
|
class BaseOpenAPIRequest(object):
|
||||||
|
|
||||||
|
host_url = NotImplemented
|
||||||
path = NotImplemented
|
path = NotImplemented
|
||||||
path_pattern = NotImplemented
|
path_pattern = NotImplemented
|
||||||
method = NotImplemented
|
method = NotImplemented
|
||||||
|
@ -94,6 +119,10 @@ class BaseOpenAPIRequest(object):
|
||||||
|
|
||||||
content_type = NotImplemented
|
content_type = NotImplemented
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_url_pattern(self):
|
||||||
|
return urljoin(self.host_url, self.path_pattern)
|
||||||
|
|
||||||
def get_parameters(self, spec):
|
def get_parameters(self, spec):
|
||||||
return RequestParametersFactory().create(self, spec)
|
return RequestParametersFactory().create(self, spec)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,13 @@ info:
|
||||||
license:
|
license:
|
||||||
name: MIT
|
name: MIT
|
||||||
servers:
|
servers:
|
||||||
- url: http://petstore.swagger.io/v1
|
- url: http://petstore.swagger.io/{version}
|
||||||
|
variables:
|
||||||
|
version:
|
||||||
|
enum:
|
||||||
|
- v1
|
||||||
|
- v2
|
||||||
|
default: v1
|
||||||
paths:
|
paths:
|
||||||
/pets:
|
/pets:
|
||||||
get:
|
get:
|
||||||
|
|
|
@ -3,13 +3,14 @@ import pytest
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
|
||||||
from openapi_core.exceptions import (
|
from openapi_core.exceptions import (
|
||||||
MissingParameterError, InvalidContentTypeError,
|
MissingParameterError, InvalidContentTypeError, InvalidServerError,
|
||||||
)
|
)
|
||||||
from openapi_core.media_types import MediaType
|
from openapi_core.media_types import MediaType
|
||||||
from openapi_core.operations import Operation
|
from openapi_core.operations import Operation
|
||||||
from openapi_core.paths import Path
|
from openapi_core.paths import Path
|
||||||
from openapi_core.request_bodies import RequestBody
|
from openapi_core.request_bodies import RequestBody
|
||||||
from openapi_core.schemas import Schema
|
from openapi_core.schemas import Schema
|
||||||
|
from openapi_core.servers import Server, ServerVariable
|
||||||
from openapi_core.shortcuts import create_spec
|
from openapi_core.shortcuts import create_spec
|
||||||
from openapi_core.wrappers import BaseOpenAPIRequest
|
from openapi_core.wrappers import BaseOpenAPIRequest
|
||||||
|
|
||||||
|
@ -17,9 +18,10 @@ from openapi_core.wrappers import BaseOpenAPIRequest
|
||||||
class RequestMock(BaseOpenAPIRequest):
|
class RequestMock(BaseOpenAPIRequest):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, method, path, path_pattern=None, args=None, view_args=None,
|
self, host_url, method, path, path_pattern=None, args=None,
|
||||||
headers=None, cookies=None, data=None,
|
view_args=None, headers=None, cookies=None, data=None,
|
||||||
content_type='application/json'):
|
content_type='application/json'):
|
||||||
|
self.host_url = host_url
|
||||||
self.path = path
|
self.path = path
|
||||||
self.path_pattern = path_pattern or path
|
self.path_pattern = path_pattern or path
|
||||||
self.method = method
|
self.method = method
|
||||||
|
@ -44,11 +46,26 @@ class TestPetstore(object):
|
||||||
return create_spec(spec_dict)
|
return create_spec(spec_dict)
|
||||||
|
|
||||||
def test_spec(self, spec, spec_dict):
|
def test_spec(self, spec, spec_dict):
|
||||||
|
url = 'http://petstore.swagger.io/v1'
|
||||||
assert spec.info.title == spec_dict['info']['title']
|
assert spec.info.title == spec_dict['info']['title']
|
||||||
assert spec.info.version == spec_dict['info']['version']
|
assert spec.info.version == spec_dict['info']['version']
|
||||||
|
|
||||||
assert spec.servers == spec_dict['servers']
|
assert spec.get_server_url() == url
|
||||||
assert spec.get_server_url() == spec_dict['servers'][0]['url']
|
|
||||||
|
for idx, server in enumerate(spec.servers):
|
||||||
|
assert type(server) == Server
|
||||||
|
|
||||||
|
server_spec = spec_dict['servers'][idx]
|
||||||
|
assert server.url == server_spec['url']
|
||||||
|
assert server.default_url == url
|
||||||
|
|
||||||
|
for variable_name, variable in iteritems(server.variables):
|
||||||
|
assert type(variable) == ServerVariable
|
||||||
|
assert variable.name == variable_name
|
||||||
|
|
||||||
|
variable_spec = server_spec['variables'][variable_name]
|
||||||
|
assert variable.default == variable_spec['default']
|
||||||
|
assert variable.enum == variable_spec.get('enum')
|
||||||
|
|
||||||
for path_name, path in iteritems(spec.paths):
|
for path_name, path in iteritems(spec.paths):
|
||||||
assert type(path) == Path
|
assert type(path) == Path
|
||||||
|
@ -99,12 +116,17 @@ class TestPetstore(object):
|
||||||
assert type(schema) == Schema
|
assert type(schema) == Schema
|
||||||
|
|
||||||
def test_get_pets(self, spec):
|
def test_get_pets(self, spec):
|
||||||
|
host_url = 'http://petstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets'
|
||||||
query_params = {
|
query_params = {
|
||||||
'limit': '20',
|
'limit': '20',
|
||||||
'ids': ['12', '13'],
|
'ids': ['12', '13'],
|
||||||
}
|
}
|
||||||
|
|
||||||
request = RequestMock('get', '/pets', args=query_params)
|
request = RequestMock(
|
||||||
|
host_url, 'get', '/pets',
|
||||||
|
path_pattern=path_pattern, args=query_params,
|
||||||
|
)
|
||||||
|
|
||||||
parameters = request.get_parameters(spec)
|
parameters = request.get_parameters(spec)
|
||||||
|
|
||||||
|
@ -116,17 +138,27 @@ class TestPetstore(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_get_pets_raises_missing_required_param(self, spec):
|
def test_get_pets_raises_missing_required_param(self, spec):
|
||||||
request = RequestMock('get', '/pets')
|
host_url = 'http://petstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets'
|
||||||
|
request = RequestMock(
|
||||||
|
host_url, 'get', '/pets',
|
||||||
|
path_pattern=path_pattern,
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(MissingParameterError):
|
with pytest.raises(MissingParameterError):
|
||||||
request.get_parameters(spec)
|
request.get_parameters(spec)
|
||||||
|
|
||||||
def test_get_pets_failed_to_cast(self, spec):
|
def test_get_pets_failed_to_cast(self, spec):
|
||||||
|
host_url = 'http://petstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets'
|
||||||
query_params = {
|
query_params = {
|
||||||
'limit': 'non_integer_value',
|
'limit': 'non_integer_value',
|
||||||
}
|
}
|
||||||
|
|
||||||
request = RequestMock('get', '/pets', args=query_params)
|
request = RequestMock(
|
||||||
|
host_url, 'get', '/pets',
|
||||||
|
path_pattern=path_pattern, args=query_params,
|
||||||
|
)
|
||||||
|
|
||||||
parameters = request.get_parameters(spec)
|
parameters = request.get_parameters(spec)
|
||||||
|
|
||||||
|
@ -137,11 +169,16 @@ class TestPetstore(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_get_pets_empty_value(self, spec):
|
def test_get_pets_empty_value(self, spec):
|
||||||
|
host_url = 'http://petstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets'
|
||||||
query_params = {
|
query_params = {
|
||||||
'limit': '',
|
'limit': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
request = RequestMock('get', '/pets', args=query_params)
|
request = RequestMock(
|
||||||
|
host_url, 'get', '/pets',
|
||||||
|
path_pattern=path_pattern, args=query_params,
|
||||||
|
)
|
||||||
|
|
||||||
parameters = request.get_parameters(spec)
|
parameters = request.get_parameters(spec)
|
||||||
|
|
||||||
|
@ -152,11 +189,16 @@ class TestPetstore(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_get_pets_none_value(self, spec):
|
def test_get_pets_none_value(self, spec):
|
||||||
|
host_url = 'http://petstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets'
|
||||||
query_params = {
|
query_params = {
|
||||||
'limit': None,
|
'limit': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
request = RequestMock('get', '/pets', args=query_params)
|
request = RequestMock(
|
||||||
|
host_url, 'get', '/pets',
|
||||||
|
path_pattern=path_pattern, args=query_params,
|
||||||
|
)
|
||||||
|
|
||||||
parameters = request.get_parameters(spec)
|
parameters = request.get_parameters(spec)
|
||||||
|
|
||||||
|
@ -167,6 +209,8 @@ class TestPetstore(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_post_pets(self, spec, spec_dict):
|
def test_post_pets(self, spec, spec_dict):
|
||||||
|
host_url = 'http://petstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets'
|
||||||
pet_name = 'Cat'
|
pet_name = 'Cat'
|
||||||
pet_tag = 'cats'
|
pet_tag = 'cats'
|
||||||
pet_street = 'Piekna'
|
pet_street = 'Piekna'
|
||||||
|
@ -181,7 +225,10 @@ class TestPetstore(object):
|
||||||
}
|
}
|
||||||
data = json.dumps(data_json)
|
data = json.dumps(data_json)
|
||||||
|
|
||||||
request = RequestMock('post', '/pets', data=data)
|
request = RequestMock(
|
||||||
|
host_url, 'post', '/pets',
|
||||||
|
path_pattern=path_pattern, data=data,
|
||||||
|
)
|
||||||
|
|
||||||
pet = request.get_body(spec)
|
pet = request.get_body(spec)
|
||||||
|
|
||||||
|
@ -196,6 +243,8 @@ class TestPetstore(object):
|
||||||
assert pet.address.city == pet_city
|
assert pet.address.city == pet_city
|
||||||
|
|
||||||
def test_post_pets_raises_invalid_content_type(self, spec):
|
def test_post_pets_raises_invalid_content_type(self, spec):
|
||||||
|
host_url = 'http://petstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets'
|
||||||
data_json = {
|
data_json = {
|
||||||
'name': 'Cat',
|
'name': 'Cat',
|
||||||
'tag': 'cats',
|
'tag': 'cats',
|
||||||
|
@ -203,18 +252,39 @@ class TestPetstore(object):
|
||||||
data = json.dumps(data_json)
|
data = json.dumps(data_json)
|
||||||
|
|
||||||
request = RequestMock(
|
request = RequestMock(
|
||||||
'post', '/pets', data=data, content_type='text/html')
|
host_url, 'post', '/pets',
|
||||||
|
path_pattern=path_pattern, data=data, content_type='text/html',
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(InvalidContentTypeError):
|
with pytest.raises(InvalidContentTypeError):
|
||||||
request.get_body(spec)
|
request.get_body(spec)
|
||||||
|
|
||||||
|
def test_post_pets_raises_invalid_server_error(self, spec):
|
||||||
|
host_url = 'http://flowerstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets'
|
||||||
|
data_json = {
|
||||||
|
'name': 'Cat',
|
||||||
|
'tag': 'cats',
|
||||||
|
}
|
||||||
|
data = json.dumps(data_json)
|
||||||
|
|
||||||
|
request = RequestMock(
|
||||||
|
host_url, 'post', '/pets',
|
||||||
|
path_pattern=path_pattern, data=data, content_type='text/html',
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(InvalidServerError):
|
||||||
|
request.get_body(spec)
|
||||||
|
|
||||||
def test_get_pet(self, spec):
|
def test_get_pet(self, spec):
|
||||||
|
host_url = 'http://petstore.swagger.io/v1'
|
||||||
|
path_pattern = '/v1/pets/{petId}'
|
||||||
view_args = {
|
view_args = {
|
||||||
'petId': '1',
|
'petId': '1',
|
||||||
}
|
}
|
||||||
request = RequestMock(
|
request = RequestMock(
|
||||||
'get', '/pets/1', path_pattern='/pets/{petId}',
|
host_url, 'get', '/pets/1',
|
||||||
view_args=view_args,
|
path_pattern=path_pattern, view_args=view_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
parameters = request.get_parameters(spec)
|
parameters = request.get_parameters(spec)
|
||||||
|
|
Loading…
Reference in a new issue