Skip to content

Commit 1da70fd

Browse files
f-arruzaFernando Arruza
and
Fernando Arruza
authored
Feature bundled specs command (#116)
* use prance library to get bundled OAS Co-authored-by: Fernando Arruza <[email protected]>
1 parent 0e3ab07 commit 1da70fd

File tree

7 files changed

+124
-24
lines changed

7 files changed

+124
-24
lines changed

.gitignore

-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ venv
66
tmp
77
*.sqlite3
88

9-
109
# Test&converage
1110
htmlcov/
1211
coverage.xml
@@ -18,9 +17,6 @@ py_ms.egg-info/*
1817
pylintReport.txt
1918
.scannerwork/
2019

21-
22-
# Docker
23-
2420
# Deploy
2521
build/
2622
dist/

docs/command_line.md

+26-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ pyms -h
88
Show you a list of options and help instructions to use this command like:
99

1010
```bash
11-
usage: main.py [-h] [-v VERBOSE] {encrypt,create-key,startproject} ...
11+
usage: main.py [-h] [-v VERBOSE]
12+
{encrypt,create-key,startproject,merge-swagger} ...
1213

1314
Python Microservices
1415

@@ -20,11 +21,12 @@ optional arguments:
2021
Commands:
2122
Available commands
2223

23-
{encrypt,create-key,startproject}
24+
{encrypt,create-key,startproject,merge-swagger}
2425
encrypt Encrypt a string
2526
create-key Generate a Key to encrypt strings in config
2627
startproject Generate a project from https://github.com/python-
2728
microservices/microservices-template
29+
merge-swagger Merge swagger into a single file
2830

2931
```
3032

@@ -67,3 +69,25 @@ pyms encrypt 'mysql+mysqlconnector://important_user:****@localhost/my_schema'
6769
```
6870

6971
See [Encrypt/Decrypt Configuration](encrypt_decryt_configuration.md) for more information
72+
73+
## Merge swagger into a single file
74+
75+
Command:
76+
77+
```bash
78+
pyms merge-swagger [-h] [-f FILE]
79+
```
80+
81+
```bash
82+
optional arguments:
83+
-h, --help show this help message and exit
84+
-f FILE, --file FILE Swagger file path
85+
```
86+
87+
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.
88+
89+
```bash
90+
pyms merge-swagger --file 'app/swagger/swagger.yaml'
91+
>> Swagger file generated [swagger-complete.yaml]
92+
>> OK
93+
```

pyms/cmd/main.py

+31-7
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from __future__ import unicode_literals, print_function
44

55
import argparse
6+
import os
67
import sys
78

8-
from pyms.utils import check_package_exists, import_from
99
from pyms.crypt.fernet import Crypt
10+
from pyms.flask.services.swagger import merge_swagger_file
11+
from pyms.utils import check_package_exists, import_from
1012

1113

1214
class Command:
@@ -33,13 +35,23 @@ def __init__(self, *args, **kwargs):
3335
parser_create_key.add_argument("create_key", action='store_true',
3436
help='Generate a Key to encrypt strings in config')
3537

36-
parser_startproject = commands.add_parser('startproject',
37-
help='Generate a project from https://github.com/python-microservices/microservices-template')
38-
parser_startproject.add_argument("startproject", action='store_true',
39-
help='Generate a project from https://github.com/python-microservices/microservices-template')
38+
parser_startproject = commands.add_parser(
39+
'startproject',
40+
help='Generate a project from https://github.com/python-microservices/microservices-template')
41+
parser_startproject.add_argument(
42+
"startproject", action='store_true',
43+
help='Generate a project from https://github.com/python-microservices/microservices-template')
44+
45+
parser_startproject.add_argument(
46+
"-b", "--branch",
47+
help='Select a branch from https://github.com/python-microservices/microservices-template')
4048

41-
parser_startproject.add_argument("-b", "--branch",
42-
help='Select a branch from https://github.com/python-microservices/microservices-template')
49+
parser_merge_swagger = commands.add_parser('merge-swagger', help='Merge swagger into a single file')
50+
parser_merge_swagger.add_argument("merge_swagger", action='store_true',
51+
help='Merge swagger into a single file')
52+
parser_merge_swagger.add_argument(
53+
"-f", "--file", default=os.path.join('project', 'swagger', 'swagger.yaml'),
54+
help='Swagger file path')
4355

4456
parser.add_argument("-v", "--verbose", default="", type=str, help="Verbose ")
4557

@@ -57,6 +69,11 @@ def __init__(self, *args, **kwargs):
5769
self.branch = args.branch
5870
except AttributeError:
5971
self.startproject = False
72+
try:
73+
self.merge_swagger = args.merge_swagger
74+
self.file = args.file
75+
except AttributeError:
76+
self.merge_swagger = False
6077
self.verbose = len(args.verbose)
6178
if autorun: # pragma: no cover
6279
result = self.run()
@@ -89,6 +106,13 @@ def run(self):
89106
cookiecutter = import_from("cookiecutter.main", "cookiecutter")
90107
cookiecutter('gh:python-microservices/cookiecutter-pyms', checkout=self.branch)
91108
self.print_ok("Created project OK")
109+
if self.merge_swagger:
110+
try:
111+
merge_swagger_file(main_file=self.file)
112+
self.print_ok("Swagger file generated [swagger-complete.yaml]")
113+
except FileNotFoundError as ex:
114+
self.print_error(ex.__str__())
115+
return False
92116
return True
93117

94118
@staticmethod

pyms/flask/services/swagger.py

+32-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
try:
99
import prance
10+
from prance.util import formats, fs
1011
except ModuleNotFoundError: # pragma: no cover
1112
prance = None
1213

@@ -20,6 +21,36 @@
2021
PROJECT_DIR = "project"
2122

2223

24+
def get_bundled_specs(main_file: Path) -> Dict[str, Any]:
25+
"""
26+
Get bundled specs
27+
:param main_file: Swagger file path
28+
:return:
29+
"""
30+
parser = prance.ResolvingParser(str(main_file.absolute()),
31+
lazy=True, backend='openapi-spec-validator')
32+
parser.parse()
33+
return parser.specification
34+
35+
36+
def merge_swagger_file(main_file: str):
37+
"""
38+
Generate swagger into a single file
39+
:param main_file: Swagger file path
40+
:return:
41+
"""
42+
input_file = Path(main_file)
43+
output_file = Path(input_file.parent, 'swagger-complete.yaml')
44+
45+
contents = formats.serialize_spec(
46+
specs=get_bundled_specs(input_file),
47+
filename=output_file,
48+
)
49+
fs.write_file(filename=output_file,
50+
contents=contents,
51+
encoding='utf-8')
52+
53+
2354
class Service(DriverService):
2455
"""The parameters you can add to your config are:
2556
* **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):
4778
application_root = "/"
4879
return application_root
4980

50-
@staticmethod
51-
def get_bundled_specs(main_file: Path) -> Dict[str, Any]:
52-
parser = prance.ResolvingParser(str(main_file.absolute()),
53-
lazy=True, backend='openapi-spec-validator')
54-
parser.parse()
55-
return parser.specification
56-
5781
def init_app(self, config, path):
5882
"""
5983
Initialize Connexion App. See more info in [Connexion Github](https://github.com/zalando/connexion)
@@ -89,7 +113,7 @@ def init_app(self, config, path):
89113
resolver=RestyResolver(self.project_dir))
90114

91115
params = {
92-
"specification": self.get_bundled_specs(
116+
"specification": get_bundled_specs(
93117
Path(os.path.join(specification_dir, self.file))) if prance else self.file,
94118
"arguments": {'title': config.APP_NAME},
95119
"base_path": application_root,

tests/swagger_for_tests/info.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
version: "1.0.0"
3+
author: "API Team"
4+
5+
url: "http://swagger.io"
6+
...

tests/swagger_for_tests/swagger.yaml

+10-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22
swagger: "2.0"
33
info:
44
description: "This is a sample server Test server"
5-
version: "1.0.0"
5+
version:
6+
$ref: 'info.yaml#/version'
67
title: "Swagger Test list"
78
termsOfService: "http://swagger.io/terms/"
89
contact:
9-
10+
name:
11+
$ref: 'info.yaml#/author'
12+
url:
13+
$ref: 'info.yaml#/url'
14+
email:
15+
$ref: 'info.yaml#/email'
1016
license:
1117
name: "Apache 2.0"
1218
url: "http://www.apache.org/licenses/LICENSE-2.0.html"
@@ -45,4 +51,5 @@ paths:
4551
x-swagger-router-controller: "tests.test_flask"
4652
externalDocs:
4753
description: "Find out more about Swagger"
48-
url: "http://swagger.io"
54+
url: "http://swagger.io"
55+
...

tests/test_cmd.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Test common rest operations wrapper.
22
"""
33
import os
4+
from pathlib import Path
45
import unittest
56
from unittest.mock import patch
67

@@ -9,6 +10,7 @@
910
from pyms.cmd import Command
1011
from pyms.exceptions import FileDoesNotExistException, PackageNotExists
1112
from pyms.crypt.fernet import Crypt
13+
from pyms.flask.services.swagger import get_bundled_specs
1214

1315

1416
class TestCmd(unittest.TestCase):
@@ -55,3 +57,20 @@ def test_startproject_error(self):
5557
with pytest.raises(PackageNotExists) as excinfo:
5658
cmd.run()
5759
assert "cookiecutter is not installed. try with pip install -U cookiecutter" in str(excinfo.value)
60+
61+
def test_get_bundled_specs(self):
62+
specs = get_bundled_specs(Path("tests/swagger_for_tests/swagger.yaml"))
63+
self.assertEqual(specs.get('swagger'), "2.0")
64+
self.assertEqual(specs.get('info').get('version'), "1.0.0")
65+
self.assertEqual(specs.get('info').get('contact').get('email'), "[email protected]")
66+
67+
def test_merge_swagger_ok(self):
68+
arguments = ["merge-swagger", "--file", "tests/swagger_for_tests/swagger.yaml", ]
69+
cmd = Command(arguments=arguments, autorun=False)
70+
assert cmd.run()
71+
os.remove("tests/swagger_for_tests/swagger-complete.yaml")
72+
73+
def test_merge_swagger_error(self):
74+
arguments = ["merge-swagger", ]
75+
cmd = Command(arguments=arguments, autorun=False)
76+
assert not cmd.run()

0 commit comments

Comments
 (0)