diff --git a/connexion/apps/abstract.py b/connexion/apps/abstract.py index 75daa1cc2..8929e02a9 100644 --- a/connexion/apps/abstract.py +++ b/connexion/apps/abstract.py @@ -143,8 +143,8 @@ def add_api( Register an API represented by a single OpenAPI specification on this application. Multiple APIs can be registered on a single application. - :param specification: OpenAPI specification. Can be provided either as dict, or as path - to file. + :param specification: OpenAPI specification. Can be provided either as dict, a path + to file, or a URL. :param base_path: Base path to host the API. This overrides the basePath / servers in the specification. :param name: Name to register the API with. If no name is passed, the base_path is used diff --git a/connexion/cli.py b/connexion/cli.py index 73fd2726b..8b7c62505 100644 --- a/connexion/cli.py +++ b/connexion/cli.py @@ -117,10 +117,13 @@ def create_app(args: t.Optional[argparse.Namespace] = None) -> AbstractApp: logging.basicConfig(level=logging_level) - 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.") + if args.spec_file.startswith("http") or args.spec_file.startswith("https"): + spec_file_full_path = args.spec_file + else: + 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 if args.stub: diff --git a/connexion/middleware/main.py b/connexion/middleware/main.py index 641576634..ea1db66fa 100644 --- a/connexion/middleware/main.py +++ b/connexion/middleware/main.py @@ -371,8 +371,8 @@ def add_api( Register een API represented by a single OpenAPI specification on this middleware. Multiple APIs can be registered on a single middleware. - :param specification: OpenAPI specification. Can be provided either as dict, or as path - to file. + :param specification: OpenAPI specification. Can be provided either as dict, a path + to file, or a URL. :param base_path: Base path to host the API. This overrides the basePath / servers in the specification. :param name: Name to register the API with. If no name is passed, the base_path is used @@ -408,7 +408,11 @@ def add_api( if self.middleware_stack is not None: 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) # Add specification as file to watch for reloading diff --git a/connexion/spec.py b/connexion/spec.py index be5d653e5..40fe46d70 100644 --- a/connexion/spec.py +++ b/connexion/spec.py @@ -19,7 +19,7 @@ from jsonschema.validators import extend as extend_validator 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 .utils import deep_get @@ -158,6 +158,14 @@ def from_file(cls, spec, *, arguments=None, base_uri=""): spec = cls._load_spec_from_file(arguments, specification_path) 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 def _get_spec_version(spec): try: @@ -200,6 +208,10 @@ def clone(self): @classmethod 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): base_uri = f"{pathlib.Path(spec).parent}{os.sep}" return cls.from_file(spec, arguments=arguments, base_uri=base_uri) diff --git a/docs/cli.rst b/docs/cli.rst index a61d0efb9..bd77391b4 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -30,7 +30,8 @@ The basic usage of this command is: 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 handlers are going to be imported from. In short, where your Python code is saved. @@ -52,3 +53,5 @@ Your API specification file is not required to have any ``operationId``. .. code-block:: bash $ 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 diff --git a/tests/test_api.py b/tests/test_api.py index 388429792..818be5896 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -51,6 +51,42 @@ def test_api_base_path_slash(): 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(): api1 = FlaskApi( Specification.load(