Skip to content

Commit 9f20460

Browse files
committed
Changed to class-based apis
1 parent dc07ae0 commit 9f20460

File tree

15 files changed

+460
-136
lines changed

15 files changed

+460
-136
lines changed

.idea/runConfigurations/mrmat_python_api_flask.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/setup_py__sdist_.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mrmat_python_api_flask/__init__.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@
3333
from flask_migrate import Migrate
3434
from flask_marshmallow import Marshmallow
3535
from flask_oidc import OpenIDConnect
36+
from flask_smorest import Api
3637

3738
__version__ = pkg_resources.get_distribution('mrmat-python-api-flask').version
3839
db = SQLAlchemy()
3940
ma = Marshmallow()
4041
migrate = Migrate()
4142
oidc = OpenIDConnect()
43+
api = Api()
4244

4345
dictConfig({
4446
'version': 1,
@@ -77,7 +79,11 @@ def create_app(config_override=None, instance_path=None):
7779
Returns: an initialised Flask app object
7880
7981
"""
80-
app = Flask(__name__, instance_relative_config=True, instance_path=instance_path)
82+
app = Flask(__name__,
83+
static_url_path='',
84+
static_folder='static',
85+
instance_relative_config=True,
86+
instance_path=instance_path)
8187

8288
#
8389
# Set configuration defaults. If a config file is present then load it. If we have overrides, apply them
@@ -88,6 +94,20 @@ def create_app(config_override=None, instance_path=None):
8894
app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False)
8995
app.config.setdefault('OIDC_USER_INFO_ENABLED', True)
9096
app.config.setdefault('OIDC_RESOURCE_SERVER_ONLY', True)
97+
app.config.setdefault('API_TITLE', 'MrMat :: Python :: API :: Flask')
98+
app.config.setdefault('API_VERSION', __version__)
99+
app.config.setdefault('OPENAPI_VERSION', '3.0.2')
100+
app.config.setdefault('OPENAPI_URL_PREFIX', '/apidoc')
101+
app.config.setdefault('OPENAPI_JSON_PATH', 'openapi.json')
102+
app.config.setdefault('OPENAPI_SWAGGER_UI_PATH', 'swagger')
103+
app.config.setdefault('OPENAPI_SWAGGER_UI_URL', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.24.2/')
104+
app.config.setdefault('OPENAPI_REDOC_PATH', 'redoc')
105+
app.config.setdefault('OPENAPI_REDOC_URL', 'https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js')
106+
app.config.setdefault('OPENAPI_RAPIDOC_PATH', 'rapidoc')
107+
app.config.setdefault('OPENAPI_RAPIDOC_URL', 'https://unpkg.com/rapidoc/dist/rapidoc-min.js')
108+
#app.config.setdefault('OPENAPI_SWAGGER_UI_CONFIG', {
109+
# 'oauth2RedirectUrl': 'http://localhost:5000/apidoc/swagger/oauth2-redirect'
110+
#})
91111
if 'FLASK_CONFIG' in os.environ and os.path.exists(os.path.expanduser(os.environ['FLASK_CONFIG'])):
92112
app.config.from_json(os.path.expanduser(os.environ['FLASK_CONFIG']))
93113
if config_override is not None:
@@ -113,6 +133,7 @@ def create_app(config_override=None, instance_path=None):
113133
db.init_app(app)
114134
migrate.init_app(app, db)
115135
ma.init_app(app)
136+
api.init_app(app)
116137
if 'OIDC_CLIENT_SECRETS' in app.config.keys():
117138
oidc.init_app(app)
118139
else:
@@ -121,15 +142,45 @@ def create_app(config_override=None, instance_path=None):
121142
#
122143
# Import and register our APIs here
123144

124-
from mrmat_python_api_flask.apis.healthz import bp as api_healthz # pylint: disable=import-outside-toplevel
145+
from mrmat_python_api_flask.apis.healthz import api_healthz # pylint: disable=import-outside-toplevel
125146
from mrmat_python_api_flask.apis.greeting.v1 import api_greeting_v1 # pylint: disable=import-outside-toplevel
126147
from mrmat_python_api_flask.apis.greeting.v2 import api_greeting_v2 # pylint: disable=import-outside-toplevel
127148
from mrmat_python_api_flask.apis.greeting.v3 import api_greeting_v3 # pylint: disable=import-outside-toplevel
128149
from mrmat_python_api_flask.apis.resource.v1 import api_resource_v1 # pylint: disable=import-outside-toplevel
129-
app.register_blueprint(api_healthz, url_prefix='/healthz')
130-
app.register_blueprint(api_greeting_v1, url_prefix='/api/greeting/v1')
131-
app.register_blueprint(api_greeting_v2, url_prefix='/api/greeting/v2')
132-
app.register_blueprint(api_greeting_v3, url_prefix='/api/greeting/v3')
133-
app.register_blueprint(api_resource_v1, url_prefix='/api/resource/v1')
150+
api.register_blueprint(api_healthz, url_prefix='/healthz')
151+
api.register_blueprint(api_greeting_v1, url_prefix='/api/greeting/v1')
152+
api.register_blueprint(api_greeting_v2, url_prefix='/api/greeting/v2')
153+
api.register_blueprint(api_greeting_v3, url_prefix='/api/greeting/v3')
154+
api.register_blueprint(api_resource_v1, url_prefix='/api/resource/v1')
155+
156+
#
157+
# If OAuth2 is in use, register our usage
158+
159+
api.spec.components.security_scheme('mrmat_keycloak',
160+
{'type': 'oauth2',
161+
'description': 'This API uses OAuth 2',
162+
'flows': {
163+
'clientCredentials': {
164+
'tokenUrl': 'https://keycloak.mrmat.org/auth/realms/master/protocol/openid-connect/token',
165+
'scopes': {
166+
'mrmat-python-api-flask-resource-read': 'Allows reading objects '
167+
'in the Resource API',
168+
'mrmat-python-api-flask-resource-write': 'Allows creating/modifying'
169+
' and deleting objects '
170+
'in the Resource API'
171+
}
172+
},
173+
'authorizationCode': {
174+
'authorizationUrl': 'https://keycloak.mrmat.org/auth/realms/master/protocol/openid-connect/auth',
175+
'tokenUrl': 'https://keycloak.mrmat.org/auth/realms/master/protocol/openid-connect/token',
176+
'scopes': {
177+
'mrmat-python-api-flask-resource-read': 'Allows reading objects '
178+
'in the Resource API',
179+
'mrmat-python-api-flask-resource-write': 'Allows creating/modifying'
180+
' and deleting objects '
181+
'in the Resource API'
182+
}
183+
}
184+
}})
134185

135186
return app

mrmat_python_api_flask/apis/greeting/v1/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@
2323
"""Pluggable blueprint of the Greeting API v1
2424
"""
2525

26+
from .model import GreetingV1Output # noqa: F401
2627
from .api import bp as api_greeting_v1 # noqa: F401

mrmat_python_api_flask/apis/greeting/v1/api.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,20 @@
2222

2323
"""Blueprint for the Greeting API in V1
2424
"""
25+
from flask.views import MethodView
26+
from flask_smorest import Blueprint
27+
from .model import greeting_v1_output, GreetingV1Output
2528

26-
from flask import Blueprint
29+
bp = Blueprint('greeting_v1',
30+
__name__,
31+
description='Greeting V1 API')
2732

28-
bp = Blueprint('greeting_v1', __name__)
2933

34+
@bp.route('/')
35+
class GreetingV1(MethodView):
3036

31-
@bp.route('/', methods=['GET'])
32-
def get():
33-
return {'message': 'Hello World'}, 200
37+
@bp.response(200, GreetingV1Output)
38+
def get(self):
39+
"""Get a Hello World message
40+
"""
41+
return greeting_v1_output.dump({'message': 'Hello World'}), 200
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# MIT License
2+
#
3+
# Copyright (c) 2021 MrMat
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Greeting API v1 Model"""
24+
25+
from marshmallow import fields
26+
27+
from mrmat_python_api_flask import ma
28+
29+
30+
class GreetingV1Output(ma.Schema):
31+
class Meta:
32+
fields = ('message',)
33+
34+
message = fields.Str(
35+
required=True,
36+
dump_only=True,
37+
metadata={
38+
'description': 'The message returned'
39+
}
40+
)
41+
42+
43+
greeting_v1_output = GreetingV1Output()

mrmat_python_api_flask/apis/greeting/v2/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@
2323
"""Pluggable blueprint of the Greeting API v2
2424
"""
2525

26-
from .api import bp as api_greeting_v2 # noqa: F401
26+
from .model import GreetingV2Output, GreetingV2Input # noqa: F401
27+
from .api import bp as api_greeting_v2 # noqa: F401

mrmat_python_api_flask/apis/greeting/v2/api.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,31 @@
2323
"""Blueprint for the Greeting API in V2
2424
"""
2525

26-
from flask import Blueprint, request
26+
from flask.views import MethodView
27+
from flask_smorest import Blueprint
28+
from .model import greeting_v2_output, GreetingV2Output, GreetingV2Input
2729

28-
bp = Blueprint('greeting_v2', __name__)
30+
bp = Blueprint('greeting_v2',
31+
__name__,
32+
description='Greeting V2 API')
2933

3034

31-
@bp.route('/', methods=['GET'])
32-
def get():
33-
name: str = request.args.get('name') or 'World'
34-
return {'message': f'Hello {name}'}, 200
35+
@bp.route('/')
36+
class GreetingV2(MethodView):
37+
"""GreetingV2 API Implementation
38+
"""
39+
40+
@bp.arguments(GreetingV2Input,
41+
description='The name to greet',
42+
location='query',
43+
required=False,
44+
as_kwargs=True)
45+
@bp.response(200, GreetingV2Output)
46+
def get(self, **kwargs):
47+
"""Get a named greeting
48+
---
49+
It is possible to place logic here like we do for safe_name, but if we parse
50+
the GreetingV2Input via MarshMallow then we can also set a 'default' or 'missing' there.
51+
"""
52+
safe_name: str = kwargs['name'] or 'World'
53+
return greeting_v2_output.dump({'message': f'Hello {safe_name}'}), 200
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# MIT License
2+
#
3+
# Copyright (c) 2021 MrMat
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Greeting API v2 Model"""
24+
25+
from marshmallow import fields
26+
27+
from mrmat_python_api_flask import ma
28+
29+
30+
class GreetingV2Input(ma.Schema):
31+
class Meta:
32+
fields: ('name',)
33+
34+
name = fields.Str(
35+
required=False,
36+
load_only=True,
37+
missing='Stranger',
38+
metadata={
39+
'description': 'The name to greet'
40+
}
41+
)
42+
43+
44+
class GreetingV2Output(ma.Schema):
45+
class Meta:
46+
fields = ('message',)
47+
48+
message = fields.Str(
49+
required=True,
50+
dump_only=True,
51+
metadata={
52+
'description': 'The message returned'
53+
}
54+
)
55+
56+
57+
greeting_v2_output = GreetingV2Output()

mrmat_python_api_flask/apis/greeting/v3/api.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,23 @@
2323
"""Blueprint for the Greeting API in V3
2424
"""
2525

26-
from flask import Blueprint, g
27-
26+
from flask import g
27+
from flask.views import MethodView
28+
from flask_smorest import Blueprint
2829
from mrmat_python_api_flask import oidc
30+
from .model import GreetingV3Output, greeting_v3_output
31+
32+
bp = Blueprint('greeting_v3',
33+
__name__,
34+
description='The Greeting V3 API')
2935

30-
bp = Blueprint('greeting_v3', __name__)
3136

37+
@bp.route('/')
38+
class GreetingV3(MethodView):
3239

33-
@bp.route('/', methods=['GET'])
34-
@oidc.accept_token(require_token=True)
35-
def get():
36-
return {'message': f'Hello {g.oidc_token_info["preferred_username"]}'}, 200
40+
@oidc.accept_token(require_token=True)
41+
@bp.response(200, GreetingV3Output)
42+
def get(self):
43+
"""Get a greeting for your asserted name from a JWT token
44+
"""
45+
return greeting_v3_output.dump({'message': f'Hello {g.oidc_token_info["preferred_username"]}'}), 200

0 commit comments

Comments
 (0)