diff --git a/.gitignore b/.gitignore index fb1907d..a7c80e9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ venv tmp *.sqlite3 - # Test&converage htmlcov/ coverage.xml @@ -18,9 +17,6 @@ py_ms.egg-info/* pylintReport.txt .scannerwork/ - -# Docker - # Deploy build/ dist/ diff --git a/docs/command_line.md b/docs/command_line.md index 933099f..2de7501 100644 --- a/docs/command_line.md +++ b/docs/command_line.md @@ -8,7 +8,8 @@ pyms -h Show you a list of options and help instructions to use this command like: ```bash -usage: main.py [-h] [-v VERBOSE] {encrypt,create-key,startproject} ... +usage: main.py [-h] [-v VERBOSE] + {encrypt,create-key,startproject,merge-swagger} ... Python Microservices @@ -20,11 +21,12 @@ optional arguments: Commands: Available commands - {encrypt,create-key,startproject} + {encrypt,create-key,startproject,merge-swagger} encrypt Encrypt a string create-key Generate a Key to encrypt strings in config startproject Generate a project from https://github.com/python- microservices/microservices-template + merge-swagger Merge swagger into a single file ``` @@ -67,3 +69,25 @@ pyms encrypt 'mysql+mysqlconnector://important_user:****@localhost/my_schema' ``` See [Encrypt/Decrypt Configuration](encrypt_decryt_configuration.md) for more information + +## Merge swagger into a single file + +Command: + +```bash +pyms merge-swagger [-h] [-f FILE] +``` + +```bash +optional arguments: + -h, --help show this help message and exit + -f FILE, --file FILE Swagger file path +``` + +This command uses [prance](https://github.com/jfinkhaeuser/prance) to validate the API specification and generate a single YAML file. It has an optional argument to indicate the main file path of the API specification. + +```bash +pyms merge-swagger --file 'app/swagger/swagger.yaml' +>> Swagger file generated [swagger-complete.yaml] +>> OK +``` \ No newline at end of file diff --git a/pyms/cmd/main.py b/pyms/cmd/main.py index cc174e0..841c60b 100755 --- a/pyms/cmd/main.py +++ b/pyms/cmd/main.py @@ -3,10 +3,12 @@ from __future__ import unicode_literals, print_function import argparse +import os import sys -from pyms.utils import check_package_exists, import_from from pyms.crypt.fernet import Crypt +from pyms.flask.services.swagger import merge_swagger_file +from pyms.utils import check_package_exists, import_from class Command: @@ -33,13 +35,23 @@ def __init__(self, *args, **kwargs): parser_create_key.add_argument("create_key", action='store_true', help='Generate a Key to encrypt strings in config') - parser_startproject = commands.add_parser('startproject', - help='Generate a project from https://github.com/python-microservices/microservices-template') - parser_startproject.add_argument("startproject", action='store_true', - help='Generate a project from https://github.com/python-microservices/microservices-template') + parser_startproject = commands.add_parser( + 'startproject', + help='Generate a project from https://github.com/python-microservices/microservices-template') + parser_startproject.add_argument( + "startproject", action='store_true', + help='Generate a project from https://github.com/python-microservices/microservices-template') + + parser_startproject.add_argument( + "-b", "--branch", + help='Select a branch from https://github.com/python-microservices/microservices-template') - parser_startproject.add_argument("-b", "--branch", - help='Select a branch from https://github.com/python-microservices/microservices-template') + parser_merge_swagger = commands.add_parser('merge-swagger', help='Merge swagger into a single file') + parser_merge_swagger.add_argument("merge_swagger", action='store_true', + help='Merge swagger into a single file') + parser_merge_swagger.add_argument( + "-f", "--file", default=os.path.join('project', 'swagger', 'swagger.yaml'), + help='Swagger file path') parser.add_argument("-v", "--verbose", default="", type=str, help="Verbose ") @@ -57,6 +69,11 @@ def __init__(self, *args, **kwargs): self.branch = args.branch except AttributeError: self.startproject = False + try: + self.merge_swagger = args.merge_swagger + self.file = args.file + except AttributeError: + self.merge_swagger = False self.verbose = len(args.verbose) if autorun: # pragma: no cover result = self.run() @@ -89,6 +106,13 @@ def run(self): cookiecutter = import_from("cookiecutter.main", "cookiecutter") cookiecutter('gh:python-microservices/cookiecutter-pyms', checkout=self.branch) self.print_ok("Created project OK") + if self.merge_swagger: + try: + merge_swagger_file(main_file=self.file) + self.print_ok("Swagger file generated [swagger-complete.yaml]") + except FileNotFoundError as ex: + self.print_error(ex.__str__()) + return False return True @staticmethod diff --git a/pyms/flask/services/swagger.py b/pyms/flask/services/swagger.py index 65db243..524e2d9 100644 --- a/pyms/flask/services/swagger.py +++ b/pyms/flask/services/swagger.py @@ -7,6 +7,7 @@ try: import prance + from prance.util import formats, fs except ModuleNotFoundError: # pragma: no cover prance = None @@ -20,6 +21,36 @@ PROJECT_DIR = "project" +def get_bundled_specs(main_file: Path) -> Dict[str, Any]: + """ + Get bundled specs + :param main_file: Swagger file path + :return: + """ + parser = prance.ResolvingParser(str(main_file.absolute()), + lazy=True, backend='openapi-spec-validator') + parser.parse() + return parser.specification + + +def merge_swagger_file(main_file: str): + """ + Generate swagger into a single file + :param main_file: Swagger file path + :return: + """ + input_file = Path(main_file) + output_file = Path(input_file.parent, 'swagger-complete.yaml') + + contents = formats.serialize_spec( + specs=get_bundled_specs(input_file), + filename=output_file, + ) + fs.write_file(filename=output_file, + contents=contents, + encoding='utf-8') + + class Service(DriverService): """The parameters you can add to your config are: * **path:** The relative or absolute route to your swagger yaml file. The default value is the current directory @@ -47,13 +78,6 @@ def _get_application_root(config): application_root = "/" return application_root - @staticmethod - def get_bundled_specs(main_file: Path) -> Dict[str, Any]: - parser = prance.ResolvingParser(str(main_file.absolute()), - lazy=True, backend='openapi-spec-validator') - parser.parse() - return parser.specification - def init_app(self, config, path): """ Initialize Connexion App. See more info in [Connexion Github](https://github.com/zalando/connexion) @@ -89,7 +113,7 @@ def init_app(self, config, path): resolver=RestyResolver(self.project_dir)) params = { - "specification": self.get_bundled_specs( + "specification": get_bundled_specs( Path(os.path.join(specification_dir, self.file))) if prance else self.file, "arguments": {'title': config.APP_NAME}, "base_path": application_root, diff --git a/tests/swagger_for_tests/info.yaml b/tests/swagger_for_tests/info.yaml new file mode 100644 index 0000000..34551b3 --- /dev/null +++ b/tests/swagger_for_tests/info.yaml @@ -0,0 +1,6 @@ +--- +version: "1.0.0" +author: "API Team" +email: "apiteam@swagger.io" +url: "http://swagger.io" +... diff --git a/tests/swagger_for_tests/swagger.yaml b/tests/swagger_for_tests/swagger.yaml index fbff310..c4be461 100644 --- a/tests/swagger_for_tests/swagger.yaml +++ b/tests/swagger_for_tests/swagger.yaml @@ -2,11 +2,17 @@ swagger: "2.0" info: description: "This is a sample server Test server" - version: "1.0.0" + version: + $ref: 'info.yaml#/version' title: "Swagger Test list" termsOfService: "http://swagger.io/terms/" contact: - email: "apiteam@swagger.io" + name: + $ref: 'info.yaml#/author' + url: + $ref: 'info.yaml#/url' + email: + $ref: 'info.yaml#/email' license: name: "Apache 2.0" url: "http://www.apache.org/licenses/LICENSE-2.0.html" @@ -45,4 +51,5 @@ paths: x-swagger-router-controller: "tests.test_flask" externalDocs: description: "Find out more about Swagger" - url: "http://swagger.io" \ No newline at end of file + url: "http://swagger.io" +... \ No newline at end of file diff --git a/tests/test_cmd.py b/tests/test_cmd.py index c238788..2890f57 100644 --- a/tests/test_cmd.py +++ b/tests/test_cmd.py @@ -1,6 +1,7 @@ """Test common rest operations wrapper. """ import os +from pathlib import Path import unittest from unittest.mock import patch @@ -9,6 +10,7 @@ from pyms.cmd import Command from pyms.exceptions import FileDoesNotExistException, PackageNotExists from pyms.crypt.fernet import Crypt +from pyms.flask.services.swagger import get_bundled_specs class TestCmd(unittest.TestCase): @@ -55,3 +57,20 @@ def test_startproject_error(self): with pytest.raises(PackageNotExists) as excinfo: cmd.run() assert "cookiecutter is not installed. try with pip install -U cookiecutter" in str(excinfo.value) + + def test_get_bundled_specs(self): + specs = get_bundled_specs(Path("tests/swagger_for_tests/swagger.yaml")) + self.assertEqual(specs.get('swagger'), "2.0") + self.assertEqual(specs.get('info').get('version'), "1.0.0") + self.assertEqual(specs.get('info').get('contact').get('email'), "apiteam@swagger.io") + + def test_merge_swagger_ok(self): + arguments = ["merge-swagger", "--file", "tests/swagger_for_tests/swagger.yaml", ] + cmd = Command(arguments=arguments, autorun=False) + assert cmd.run() + os.remove("tests/swagger_for_tests/swagger-complete.yaml") + + def test_merge_swagger_error(self): + arguments = ["merge-swagger", ] + cmd = Command(arguments=arguments, autorun=False) + assert not cmd.run()