mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-06 04:19:26 +00:00
Allow the specification to be specified as a URL. (#1871)
Changes proposed in this pull request: - Allow a specification to be specified as a URL that is downloaded when the App runs. In combination with the existing mock features, this makes it a single command to run a mock server for any published API. --------- Co-authored-by: Robbe Sneyders <robbe.sneyders@gmail.com>
This commit is contained in:
@@ -143,8 +143,8 @@ class AbstractApp:
|
|||||||
Register an API represented by a single OpenAPI specification on this application.
|
Register an API represented by a single OpenAPI specification on this application.
|
||||||
Multiple APIs can be registered on a single application.
|
Multiple APIs can be registered on a single application.
|
||||||
|
|
||||||
:param specification: OpenAPI specification. Can be provided either as dict, or as path
|
:param specification: OpenAPI specification. Can be provided either as dict, a path
|
||||||
to file.
|
to file, or a URL.
|
||||||
:param base_path: Base path to host the API. This overrides the basePath / servers in the
|
:param base_path: Base path to host the API. This overrides the basePath / servers in the
|
||||||
specification.
|
specification.
|
||||||
:param name: Name to register the API with. If no name is passed, the base_path is used
|
:param name: Name to register the API with. If no name is passed, the base_path is used
|
||||||
|
|||||||
@@ -117,10 +117,13 @@ def create_app(args: t.Optional[argparse.Namespace] = None) -> AbstractApp:
|
|||||||
|
|
||||||
logging.basicConfig(level=logging_level)
|
logging.basicConfig(level=logging_level)
|
||||||
|
|
||||||
spec_file_full_path = os.path.abspath(args.spec_file)
|
if args.spec_file.startswith("http") or args.spec_file.startswith("https"):
|
||||||
py_module_path = args.base_module_path or os.path.dirname(spec_file_full_path)
|
spec_file_full_path = args.spec_file
|
||||||
sys.path.insert(1, os.path.abspath(py_module_path))
|
else:
|
||||||
logger.debug(f"Added {py_module_path} to system path.")
|
spec_file_full_path = os.path.abspath(args.spec_file)
|
||||||
|
py_module_path = args.base_module_path or os.path.dirname(spec_file_full_path)
|
||||||
|
sys.path.insert(1, os.path.abspath(py_module_path))
|
||||||
|
logger.debug(f"Added {py_module_path} to system path.")
|
||||||
|
|
||||||
resolver_error = None
|
resolver_error = None
|
||||||
if args.stub:
|
if args.stub:
|
||||||
|
|||||||
@@ -371,8 +371,8 @@ class ConnexionMiddleware:
|
|||||||
Register een API represented by a single OpenAPI specification on this middleware.
|
Register een API represented by a single OpenAPI specification on this middleware.
|
||||||
Multiple APIs can be registered on a single middleware.
|
Multiple APIs can be registered on a single middleware.
|
||||||
|
|
||||||
:param specification: OpenAPI specification. Can be provided either as dict, or as path
|
:param specification: OpenAPI specification. Can be provided either as dict, a path
|
||||||
to file.
|
to file, or a URL.
|
||||||
:param base_path: Base path to host the API. This overrides the basePath / servers in the
|
:param base_path: Base path to host the API. This overrides the basePath / servers in the
|
||||||
specification.
|
specification.
|
||||||
:param name: Name to register the API with. If no name is passed, the base_path is used
|
:param name: Name to register the API with. If no name is passed, the base_path is used
|
||||||
@@ -408,7 +408,11 @@ class ConnexionMiddleware:
|
|||||||
if self.middleware_stack is not None:
|
if self.middleware_stack is not None:
|
||||||
raise RuntimeError("Cannot add api after an application has started")
|
raise RuntimeError("Cannot add api after an application has started")
|
||||||
|
|
||||||
if isinstance(specification, (pathlib.Path, str)):
|
if isinstance(specification, str) and (
|
||||||
|
specification.startswith("http://") or specification.startswith("https://")
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
elif isinstance(specification, (pathlib.Path, str)):
|
||||||
specification = t.cast(pathlib.Path, self.specification_dir / specification)
|
specification = t.cast(pathlib.Path, self.specification_dir / specification)
|
||||||
|
|
||||||
# Add specification as file to watch for reloading
|
# Add specification as file to watch for reloading
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from jsonschema import Draft4Validator
|
|||||||
from jsonschema.validators import extend as extend_validator
|
from jsonschema.validators import extend as extend_validator
|
||||||
|
|
||||||
from .exceptions import InvalidSpecification
|
from .exceptions import InvalidSpecification
|
||||||
from .json_schema import NullableTypeValidator, resolve_refs
|
from .json_schema import NullableTypeValidator, URLHandler, resolve_refs
|
||||||
from .operations import AbstractOperation, OpenAPIOperation, Swagger2Operation
|
from .operations import AbstractOperation, OpenAPIOperation, Swagger2Operation
|
||||||
from .utils import deep_get
|
from .utils import deep_get
|
||||||
|
|
||||||
@@ -158,6 +158,14 @@ class Specification(Mapping):
|
|||||||
spec = cls._load_spec_from_file(arguments, specification_path)
|
spec = cls._load_spec_from_file(arguments, specification_path)
|
||||||
return cls.from_dict(spec, base_uri=base_uri)
|
return cls.from_dict(spec, base_uri=base_uri)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_url(cls, spec, *, base_uri=""):
|
||||||
|
"""
|
||||||
|
Takes in a path to a YAML file, and returns a Specification
|
||||||
|
"""
|
||||||
|
spec = URLHandler()(spec)
|
||||||
|
return cls.from_dict(spec, base_uri=base_uri)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_spec_version(spec):
|
def _get_spec_version(spec):
|
||||||
try:
|
try:
|
||||||
@@ -200,6 +208,10 @@ class Specification(Mapping):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, spec, *, arguments=None):
|
def load(cls, spec, *, arguments=None):
|
||||||
|
if isinstance(spec, str) and (
|
||||||
|
spec.startswith("http://") or spec.startswith("https://")
|
||||||
|
):
|
||||||
|
return cls.from_url(spec)
|
||||||
if not isinstance(spec, dict):
|
if not isinstance(spec, dict):
|
||||||
base_uri = f"{pathlib.Path(spec).parent}{os.sep}"
|
base_uri = f"{pathlib.Path(spec).parent}{os.sep}"
|
||||||
return cls.from_file(spec, arguments=arguments, base_uri=base_uri)
|
return cls.from_file(spec, arguments=arguments, base_uri=base_uri)
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ The basic usage of this command is:
|
|||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
- SPEC_FILE: Your OpenAPI specification file in YAML format.
|
- SPEC_FILE: Your OpenAPI specification file in YAML format. Can also be given
|
||||||
|
as a URL, which will be automatically downloaded.
|
||||||
- BASE_MODULE_PATH (optional): filesystem path where the API endpoints
|
- BASE_MODULE_PATH (optional): filesystem path where the API endpoints
|
||||||
handlers are going to be imported from. In short, where your Python
|
handlers are going to be imported from. In short, where your Python
|
||||||
code is saved.
|
code is saved.
|
||||||
@@ -52,3 +53,5 @@ Your API specification file is not required to have any ``operationId``.
|
|||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ connexion run your_api.yaml --mock=all
|
$ connexion run your_api.yaml --mock=all
|
||||||
|
|
||||||
|
$ connexion run https://raw.githubusercontent.com/spec-first/connexion/main/examples/helloworld_async/spec/openapi.yaml --mock=all
|
||||||
|
|||||||
@@ -51,6 +51,42 @@ def test_api_base_path_slash():
|
|||||||
assert api.blueprint.url_prefix == ""
|
assert api.blueprint.url_prefix == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_api():
|
||||||
|
api = FlaskApi(
|
||||||
|
Specification.load(
|
||||||
|
"https://raw.githubusercontent.com/spec-first/connexion/165a915/tests/fixtures/simple/swagger.yaml"
|
||||||
|
),
|
||||||
|
base_path="/api/v1.0",
|
||||||
|
)
|
||||||
|
assert api.blueprint.name == "/api/v1_0"
|
||||||
|
assert api.blueprint.url_prefix == "/api/v1.0"
|
||||||
|
|
||||||
|
api2 = FlaskApi(
|
||||||
|
Specification.load(
|
||||||
|
"https://raw.githubusercontent.com/spec-first/connexion/165a915/tests/fixtures/simple/swagger.yaml"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert api2.blueprint.name == "/v1_0"
|
||||||
|
assert api2.blueprint.url_prefix == "/v1.0"
|
||||||
|
|
||||||
|
api3 = FlaskApi(
|
||||||
|
Specification.load(
|
||||||
|
"https://raw.githubusercontent.com/spec-first/connexion/165a915/tests/fixtures/simple/openapi.yaml"
|
||||||
|
),
|
||||||
|
base_path="/api/v1.0",
|
||||||
|
)
|
||||||
|
assert api3.blueprint.name == "/api/v1_0"
|
||||||
|
assert api3.blueprint.url_prefix == "/api/v1.0"
|
||||||
|
|
||||||
|
api4 = FlaskApi(
|
||||||
|
Specification.load(
|
||||||
|
"https://raw.githubusercontent.com/spec-first/connexion/165a915/tests/fixtures/simple/openapi.yaml"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert api4.blueprint.name == "/v1_0"
|
||||||
|
assert api4.blueprint.url_prefix == "/v1.0"
|
||||||
|
|
||||||
|
|
||||||
def test_template():
|
def test_template():
|
||||||
api1 = FlaskApi(
|
api1 = FlaskApi(
|
||||||
Specification.load(
|
Specification.load(
|
||||||
|
|||||||
Reference in New Issue
Block a user