This repository has been archived by the owner on Jun 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
[AS-216] swagger using flask-restx #21
Merged
Merged
Changes from 7 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
fbe8f48
swagger using flask-restx
e3a7282
shh mypy
c59847f
remove lie
6dcef4d
rest of swagger
65de828
remove from ctmpl
77c9d61
swagger health check too
d84392c
added test for health checking
e9ed0c4
model fixes
8ec7136
fix nested model
6c6eb22
disable x-fields swagger thing
afcfd35
snafu
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,8 @@ | ||
import flask | ||
from app.server import routes, json_response | ||
from app.server import routes | ||
|
||
|
||
def create_app() -> flask.Flask: | ||
app = flask.Flask(__name__) | ||
app.response_class = json_response.JsonResponse | ||
app.register_blueprint(routes.routes) | ||
return app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,41 @@ | ||
import flask | ||
import json | ||
import logging | ||
from sqlalchemy.orm.exc import NoResultFound | ||
from typing import Dict | ||
from flask_restx import fields | ||
|
||
from app.auth import user_auth | ||
from app.db import db, model | ||
from app.db.model import ImportStatus | ||
from app.external import sam, rawls | ||
from app.util import exceptions | ||
|
||
|
||
def handle_health_check() -> flask.Response: | ||
|
||
class HealthResponse: | ||
def __init__(self, db_health: bool, rawls_health: bool, sam_health: bool): | ||
self.ok = all([db_health, rawls_health, sam]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch! |
||
self.subsystems = { | ||
"db": db_health, | ||
"rawls": rawls_health, | ||
"sam": sam_health | ||
} | ||
|
||
@classmethod | ||
def get_model(cls) -> model.ModelDefinition: | ||
subsystem_fields = { | ||
"db": fields.Boolean, | ||
"rawls": fields.Boolean, | ||
"sam": fields.Boolean | ||
} | ||
return { | ||
"ok": fields.Boolean, | ||
"subsystems": fields.Nested(subsystem_fields) | ||
} | ||
|
||
|
||
def handle_health_check() -> HealthResponse: | ||
sam_health = sam.check_health() | ||
rawls_health = rawls.check_health() | ||
db_health = check_health() | ||
|
||
isvc_health = all([sam_health, rawls_health, db_health]) | ||
|
||
return flask.make_response((json.dumps({"ok": isvc_health, "subsystems": {"db": db_health, "rawls": rawls_health, "sam": sam_health}}), 200)) | ||
return HealthResponse(db_health, rawls_health, sam_health) | ||
|
||
|
||
def check_health() -> bool: | ||
with db.session_ctx() as sess: | ||
|
||
res = sess.execute("select true").rowcount | ||
return bool(res) | ||
return bool(res) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,12 @@ | ||
import flask | ||
import jsonschema | ||
import logging | ||
|
||
from app import translate | ||
from app.util import exceptions | ||
from app.db import db, model | ||
from app.external import sam, pubsub | ||
from app.auth import user_auth | ||
|
||
NEW_IMPORT_SCHEMA = { | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"type": "object", | ||
"properties": { | ||
"path": { | ||
"type": "string" | ||
}, | ||
"filetype": { | ||
"type": "string", | ||
"enum": list(translate.FILETYPE_TRANSLATORS.keys()) | ||
} | ||
}, | ||
"required": ["path", "filetype"] | ||
} | ||
|
||
|
||
schema_validator = jsonschema.Draft7Validator(NEW_IMPORT_SCHEMA) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. validation of incoming json is now handled by restx |
||
|
||
|
||
def handle(request: flask.Request, ws_ns: str, ws_name: str) -> flask.Response: | ||
def handle(request: flask.Request, ws_ns: str, ws_name: str) -> model.ImportStatusResponse: | ||
access_token = user_auth.extract_auth_token(request) | ||
user_info = sam.validate_user(access_token) | ||
|
||
|
@@ -37,12 +16,6 @@ def handle(request: flask.Request, ws_ns: str, ws_name: str) -> flask.Response: | |
# make sure the user is allowed to import to this workspace | ||
workspace_uuid = user_auth.workspace_uuid_with_auth(ws_ns, ws_name, access_token, "write") | ||
|
||
try: # now validate that the input is correctly shaped | ||
schema_validator.validate(request_json) | ||
except jsonschema.ValidationError as ve: | ||
logging.info("Got malformed JSON.") | ||
raise exceptions.BadJsonException(ve.message) | ||
|
||
import_url = request_json["path"] | ||
|
||
# and validate the input's path | ||
|
@@ -62,4 +35,4 @@ def handle(request: flask.Request, ws_ns: str, ws_name: str) -> flask.Response: | |
|
||
pubsub.publish_self({"action": "translate", "import_id": new_import_id}) | ||
|
||
return flask.make_response((str(new_import_id), 201)) | ||
return new_import.to_status_response() |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,58 +1,97 @@ | ||
import flask | ||
from flask_restx import Api, Resource, fields | ||
import json | ||
import humps | ||
from typing import Dict, Callable | ||
from typing import Dict, Callable, Any | ||
|
||
from app import new_import, translate, status, health | ||
from app.db import model | ||
import app.auth.service_auth | ||
from app.server.requestutils import httpify_excs, pubsubify_excs | ||
|
||
routes = flask.Blueprint('import-service', __name__, '/') | ||
|
||
authorizations = { | ||
'Bearer': { | ||
"type": "apiKey", | ||
"name": "Authorization", | ||
"in": "header", | ||
"description": "Use your GCP auth token, i.e. `gcloud auth print-access-token`. Required scopes are [openid, email, profile]. Write `Bearer <yourtoken>` in the box." | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. weird hack as described in the description |
||
|
||
api = Api(routes, version='1.0', title='Import Service', | ||
description='import service', | ||
authorizations=authorizations, | ||
security=[{"Bearer": "[]"}]) | ||
|
||
ns = api.namespace('/', description='import handling') | ||
|
||
|
||
@routes.route('/<ws_ns>/<ws_name>/imports', methods=["POST"]) | ||
@httpify_excs | ||
def create_import(ws_ns, ws_name) -> flask.Response: | ||
"""Accept an import request""" | ||
return new_import.handle(flask.request, ws_ns, ws_name) | ||
new_import_model = ns.model("NewImport", | ||
{"path": fields.String(required=True), | ||
"filetype": fields.String(enum=list(translate.FILETYPE_TRANSLATORS.keys()), required=True)}) | ||
import_status_response_model = ns.model("ImportStatusResponse", model.ImportStatusResponse.get_model()) | ||
health_response_model = ns.model("HealthResponse", health.HealthResponse.get_model()) | ||
|
||
|
||
@routes.route('/<ws_ns>/<ws_name>/imports/<import_id>', methods=["GET"]) | ||
@httpify_excs | ||
def import_status(ws_ns, ws_name, import_id) -> flask.Response: | ||
"""Return the status of an import job""" | ||
return status.handle_get_import_status(flask.request, ws_ns, ws_name, import_id) | ||
@ns.route('/<workspace_project>/<workspace_name>/imports/<import_id>') | ||
@ns.param('workspace_project', 'Workspace project') | ||
@ns.param('workspace_name', 'Workspace name') | ||
@ns.param('import_id', 'Import id') | ||
class SpecificImport(Resource): | ||
@httpify_excs | ||
@ns.marshal_with(import_status_response_model) | ||
def get(self, workspace_project, workspace_name, import_id): | ||
"""Return status for this import.""" | ||
return status.handle_get_import_status(flask.request, workspace_project, workspace_name, import_id) | ||
|
||
|
||
@routes.route('/<ws_ns>/<ws_name>/imports', methods=["GET"]) | ||
@httpify_excs | ||
def import_status_workspace(ws_ns, ws_name) -> flask.Response: | ||
"""Return the status of import jobs in a workspace""" | ||
return status.handle_list_import_status(flask.request, ws_ns, ws_name) | ||
@ns.route('/<workspace_project>/<workspace_name>/imports') | ||
@ns.param('workspace_project', 'Workspace project') | ||
@ns.param('workspace_name', 'Workspace name') | ||
class Imports(Resource): | ||
@httpify_excs | ||
@ns.expect(new_import_model, validate=True) | ||
@ns.marshal_with(import_status_response_model, code=201) | ||
def post(self, workspace_project, workspace_name): | ||
"""Accept an import request.""" | ||
return new_import.handle(flask.request, workspace_project, workspace_name), 201 | ||
|
||
@httpify_excs | ||
@ns.marshal_with(import_status_response_model, code=200, as_list=True) | ||
def get(self, workspace_project, workspace_name): | ||
"""Return all imports in the workspace.""" | ||
return status.handle_list_import_status(flask.request, workspace_project, workspace_name) | ||
|
||
@routes.route('/health', methods=["GET"]) | ||
@httpify_excs | ||
def health_check() -> flask.Response: | ||
return health.handle_health_check() | ||
|
||
@ns.route('/health') | ||
class Health(Resource): | ||
@httpify_excs | ||
@ns.marshal_with(health_response_model, code=200) | ||
def get(self): | ||
"""Return whether we and all dependent subsystems are healthy.""" | ||
return health.handle_health_check(), 200 | ||
|
||
|
||
# Dispatcher for pubsub messages. | ||
pubsub_dispatch: Dict[str, Callable[[Dict[str, str]], flask.Response]] = { | ||
pubsub_dispatch: Dict[str, Callable[[Dict[str, str]], Any]] = { | ||
"translate": translate.handle, | ||
"status": status.external_update_status | ||
} | ||
|
||
|
||
# This particular URL, though weird, can be secured using GCP magic. | ||
# See https://cloud.google.com/pubsub/docs/push#authenticating_standard_and_urls | ||
@routes.route('/_ah/push-handlers/receive_messages', methods=['POST']) | ||
@pubsubify_excs | ||
def pubsub_receive() -> flask.Response: | ||
app.auth.service_auth.verify_pubsub_jwt(flask.request) | ||
|
||
envelope = json.loads(flask.request.data.decode('utf-8')) | ||
attributes = envelope['message']['attributes'] | ||
|
||
# humps.decamelize turns camelCase to snake_case in dict keys | ||
return pubsub_dispatch[attributes["action"]](humps.decamelize(attributes)) | ||
@ns.route('/_ah/push-handlers/receive_messages', doc=False) | ||
class PubSub(Resource): | ||
@pubsubify_excs | ||
@ns.marshal_with(import_status_response_model, code=200) | ||
def post(self) -> flask.Response: | ||
app.auth.service_auth.verify_pubsub_jwt(flask.request) | ||
|
||
envelope = json.loads(flask.request.data.decode('utf-8')) | ||
attributes = envelope['message']['attributes'] | ||
|
||
# humps.decamelize turns camelCase to snake_case in dict keys | ||
return pubsub_dispatch[attributes["action"]](humps.decamelize(attributes)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is handled magically for us by restx now