Skip to content

Commit

Permalink
Allow the specification to be specified as a URL. (#1871)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
mjp4 and RobbeSneyders authored Feb 18, 2024
1 parent 211bdb0 commit 994f53f
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 11 deletions.
4 changes: 2 additions & 2 deletions connexion/apps/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions connexion/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 7 additions & 3 deletions connexion/middleware/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion connexion/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
36 changes: 36 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 994f53f

Please sign in to comment.