Compare commits

...

3 Commits

Author SHA1 Message Date
Gardient
a0dbdc4ddf webhook endpoint 2021-09-30 23:01:16 +03:00
Gardient
04d70ba424 apidoc updates 2021-09-30 23:00:46 +03:00
Gardient
71c1e322aa add proper rabbitMQ extension 2021-09-30 22:57:13 +03:00
16 changed files with 138 additions and 33 deletions

View File

@@ -1,8 +1,8 @@
from flask import Flask, Blueprint from flask import Flask, Blueprint
from . import commands, login, target_exchange, target, registration from . import commands, login, target_exchange, target, registration, webhook
from .exceptions import ApiException from .exceptions import ApiException
from .extensions import db, migrate, jwt, apispec from .extensions import db, migrate, jwt, apispec, rabbit
from .settings import ProdConfig, Config from .settings import ProdConfig, Config
@@ -31,6 +31,7 @@ def register_extensions(app: Flask):
migrate.init_app(app, db) migrate.init_app(app, db)
jwt.init_app(app) jwt.init_app(app)
apispec.init_app(app) apispec.init_app(app)
rabbit.init_app(app)
def register_blueprints(app: Flask): def register_blueprints(app: Flask):
@@ -43,6 +44,7 @@ def register_blueprints(app: Flask):
api_blueprint.register_blueprint(registration.views.blueprint, url_prefix='/registration') api_blueprint.register_blueprint(registration.views.blueprint, url_prefix='/registration')
app.register_blueprint(api_blueprint) app.register_blueprint(api_blueprint)
app.register_blueprint(webhook.views.blueprint)
def register_apispecs(app: Flask): def register_apispecs(app: Flask):
@@ -52,6 +54,9 @@ def register_apispecs(app: Flask):
that one doesn't handle the static routes or blueprints in blueprints well that one doesn't handle the static routes or blueprints in blueprints well
""" """
apispec.spec.components.security_scheme('api_key', {"type": "apiKey", "in": "query", "name": "apikey"})
apispec.spec.components.security_scheme('jwt', {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"})
for name, rule in app.view_functions.items(): for name, rule in app.view_functions.items():
if name == 'static': if name == 'static':
continue continue
@@ -82,6 +87,7 @@ def register_shellcontext(app: Flask):
"""Shell context objects.""" """Shell context objects."""
return { return {
'db': db, 'db': db,
'rabbit': rabbit,
'TargetExchange': target_exchange.models.TargetExchange, 'TargetExchange': target_exchange.models.TargetExchange,
'Target': target.models.Target, 'Target': target.models.Target,
'Registration': registration.models.Registration, 'Registration': registration.models.Registration,

View File

@@ -5,7 +5,7 @@ from flask import current_app
from flask.cli import with_appcontext from flask.cli import with_appcontext
from werkzeug.exceptions import MethodNotAllowed, NotFound from werkzeug.exceptions import MethodNotAllowed, NotFound
import rmq_helper from .extensions import rabbit
from .target.models import Target from .target.models import Target
from .target_exchange.models import TargetExchange from .target_exchange.models import TargetExchange
@@ -100,9 +100,9 @@ def seed():
def setup_rabmq(): def setup_rabmq():
"""Set up rabbitMQ""" """Set up rabbitMQ"""
for exchange in TargetExchange.query.filter(TargetExchange.name != "").all(): for exchange in TargetExchange.query.filter(TargetExchange.name != "").all():
rmq_helper.ensure_exchange_exists(exchange.name) rabbit.ensure_exchange_exists(exchange.name)
for target in Target.query.join(Target.exchange).filter(TargetExchange.name == "").all(): for target in Target.query.join(Target.exchange).filter(TargetExchange.name == "").all():
rmq_helper.ensure_queue_exists(target.routing_key) rabbit.ensure_queue_exists(target.routing_key)
pass pass

View File

@@ -2,6 +2,7 @@ from flask_apispec import FlaskApiSpec
from flask_jwt_extended import JWTManager from flask_jwt_extended import JWTManager
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy, Model from flask_sqlalchemy import SQLAlchemy, Model
from rmq_helper import RabbitMQ
class CRUDMixin(Model): class CRUDMixin(Model):
@@ -36,3 +37,4 @@ db = SQLAlchemy(model_class=CRUDMixin)
migrate = Migrate() migrate = Migrate()
jwt = JWTManager() jwt = JWTManager()
apispec = FlaskApiSpec() apispec = FlaskApiSpec()
rabbit = RabbitMQ()

View File

@@ -1,11 +1,14 @@
from marshmallow import Schema, fields from marshmallow import Schema, fields
class LoginSchema(Schema): class LoginSchema(Schema):
username = fields.Str() username = fields.Str(required=True)
password = fields.Str(load_only=True) password = fields.Str(required=True, load_only=True)
class TokenResponseSchema(Schema): class TokenResponseSchema(Schema):
token = fields.Str() token = fields.Str()
login_schema = LoginSchema() login_schema = LoginSchema()
token_response_schema = TokenResponseSchema() token_response_schema = TokenResponseSchema()

View File

@@ -4,21 +4,19 @@ from flask_apispec import use_kwargs, marshal_with, doc
from marshmallow import fields from marshmallow import fields
from api.exceptions import BadRequestException from api.exceptions import BadRequestException
from api.utils import docwrap
import api.constants as constants import api.constants as constants
from .serializers import token_response_schema from .serializers import token_response_schema, login_schema
blueprint = Blueprint('login', __name__) blueprint = Blueprint('login', __name__)
@doc(tags=['login']) @docwrap('Login', None)
@blueprint.route('', methods=['POST']) @blueprint.route('', methods=['POST'])
@jwt_required(optional=True) @jwt_required(optional=True)
@use_kwargs({ @use_kwargs(login_schema)
'username': fields.Str(required=True),
'password': fields.Str(required=True)
})
@marshal_with(token_response_schema) @marshal_with(token_response_schema)
def login_user(username, password, **kwargs): def login_user(username, password):
if username == constants.API_USER and password == current_app.config[constants.API_PASS]: if username == constants.API_USER and password == current_app.config[constants.API_PASS]:
return {'token': create_access_token(identity=username, fresh=True, expires_delta=False)} return {'token': create_access_token(identity=username, fresh=True, expires_delta=False)}
else: else:

View File

@@ -19,3 +19,6 @@ class Registration(SurrogatePK, Model):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Registration, self).__init__(**kwargs) super(Registration, self).__init__(**kwargs)
def __repr__(self):
return f'<{Registration.__name__}({self.id}):{self.name!r}->({",".join([repr(x) for x in self.targets])})>'

View File

@@ -5,18 +5,21 @@ from marshmallow import fields
from sqlalchemy import or_ from sqlalchemy import or_
from api.exceptions import NotFoundException, BadRequestException from api.exceptions import NotFoundException, BadRequestException
from api.utils import docwrap
from api.target.models import Target from api.target.models import Target
from api.target_exchange.models import TargetExchange from api.target_exchange.models import TargetExchange
from .models import Registration from .models import Registration
from .serializers import registration_schema, registrations_schema from .serializers import registration_schema, registrations_schema
blueprint = Blueprint('Registration', __name__) blueprint = Blueprint('Registration', __name__)
doc = docwrap('Registration')
@doc(tags=['Registration']) @doc
@blueprint.route('', methods=['GET']) @blueprint.route('', methods=['GET'])
@jwt_required() @jwt_required()
@use_kwargs({'exchange': fields.Str(), 'target': fields.Str()}) @use_kwargs({'exchange': fields.Str(), 'target': fields.Str()}, location='query')
@marshal_with(registrations_schema) @marshal_with(registrations_schema)
def get_list(exchange=None, target=None): def get_list(exchange=None, target=None):
res = Registration.query res = Registration.query
@@ -26,7 +29,7 @@ def get_list(exchange=None, target=None):
return res.all() return res.all()
@doc(tags=['Registration']) @doc
@blueprint.route('', methods=['POST']) @blueprint.route('', methods=['POST'])
@jwt_required() @jwt_required()
@use_kwargs(registration_schema) @use_kwargs(registration_schema)
@@ -43,7 +46,7 @@ def create(name, routing_key, targets):
return registration return registration
@doc(tags=['Registration']) @doc
@blueprint.route('/<registration_id>', methods=['GET']) @blueprint.route('/<registration_id>', methods=['GET'])
@jwt_required() @jwt_required()
@marshal_with(registration_schema) @marshal_with(registration_schema)
@@ -55,7 +58,7 @@ def get_by_id(registration_id: int):
return NotFoundException(Registration.__name__) return NotFoundException(Registration.__name__)
@doc(tags=['Registration']) @doc
@blueprint.route('/<registration_id>', methods=['PUT']) @blueprint.route('/<registration_id>', methods=['PUT'])
@jwt_required() @jwt_required()
@use_kwargs(registration_schema) @use_kwargs(registration_schema)

View File

@@ -18,6 +18,7 @@ class Config(object):
JWT_AUTH_HEADER_PREFIX = 'Token' JWT_AUTH_HEADER_PREFIX = 'Token'
APISPEC_TITLE = 'MahssageBus API' APISPEC_TITLE = 'MahssageBus API'
APISPEC_VERSION = 'v0.1' APISPEC_VERSION = 'v0.1'
APISPEC_OAS_VERSION = '3.0.0'
RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST", "localhost") RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST", "localhost")

View File

@@ -16,4 +16,4 @@ class Target(SurrogatePK, Model):
super(Target, self).__init__(**kwargs) super(Target, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
return '<%s(%d):%r->%r>' % (Target.__name__, self.id, self.routing_key, self.exchange.name) return f'<{Target.__name__}({self.id}):{self.routing_key!r}->{self.exchange.name!r}>'

View File

@@ -1,20 +1,23 @@
from flask import Blueprint from flask import Blueprint
from flask_apispec import use_kwargs, marshal_with, doc from flask_apispec import use_kwargs, marshal_with
from flask_jwt_extended import jwt_required from flask_jwt_extended import jwt_required
from marshmallow import fields from marshmallow import fields
from api.exceptions import NotFoundException, BadRequestException from api.exceptions import NotFoundException, BadRequestException
from api.target_exchange.models import TargetExchange from api.target_exchange.models import TargetExchange
from api.utils import docwrap
from .models import Target from .models import Target
from .serializers import target_schema, targets_schema from .serializers import target_schema, targets_schema
blueprint = Blueprint('target', __name__) blueprint = Blueprint('target', __name__)
doc = docwrap('Target')
@doc(tags=['Target']) @doc
@blueprint.route('', methods=['GET']) @blueprint.route('', methods=['GET'])
@jwt_required() @jwt_required()
@use_kwargs({'exchange': fields.Str()}) @use_kwargs({'exchange': fields.Str()}, location='query')
@marshal_with(targets_schema) @marshal_with(targets_schema)
def get_list(exchange=None): def get_list(exchange=None):
res = Target.query res = Target.query
@@ -23,7 +26,7 @@ def get_list(exchange=None):
return res.all() return res.all()
@doc(tags=['Target']) @doc
@blueprint.route('', methods=['POST']) @blueprint.route('', methods=['POST'])
@jwt_required() @jwt_required()
@use_kwargs(target_schema) @use_kwargs(target_schema)
@@ -37,7 +40,7 @@ def create(name, routing_key, exchange):
return target return target
@doc(tags=['Target']) @doc
@blueprint.route('/<target_id>', methods=['GET']) @blueprint.route('/<target_id>', methods=['GET'])
@jwt_required() @jwt_required()
@marshal_with(target_schema) @marshal_with(target_schema)
@@ -49,7 +52,7 @@ def get_by_id(target_id: int):
return NotFoundException(Target.__name__) return NotFoundException(Target.__name__)
@doc(tags=['Target']) @doc
@blueprint.route('/<target_id>', methods=['PUT']) @blueprint.route('/<target_id>', methods=['PUT'])
@jwt_required() @jwt_required()
@use_kwargs(target_schema) @use_kwargs(target_schema)

View File

@@ -9,6 +9,9 @@ class TargetExchange(SurrogatePK, Model):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(TargetExchange, self).__init__(**kwargs) super(TargetExchange, self).__init__(**kwargs)
def __repr__(self):
return f'<{TargetExchange.__name__}({self.id}):{self.name!r}>'
@staticmethod @staticmethod
def ensure_created(name): def ensure_created(name):
if TargetExchange.query.filter_by(name=name).first() is None: if TargetExchange.query.filter_by(name=name).first() is None:

View File

@@ -1,16 +1,18 @@
from flask import Blueprint from flask import Blueprint
from flask_jwt_extended import jwt_required from flask_jwt_extended import jwt_required
from flask_apispec import use_kwargs, marshal_with, doc from flask_apispec import use_kwargs, marshal_with
from api.exceptions import NotFoundException from api.exceptions import NotFoundException
from api.utils import docwrap
from .models import TargetExchange from .models import TargetExchange
from .serializers import target_exchange_schema, target_exchanges_schema from .serializers import target_exchange_schema, target_exchanges_schema
blueprint = Blueprint('target_exchange', __name__) blueprint = Blueprint('target_exchange', __name__)
doc = docwrap('TargetExchange')
@doc(tags=['TargetExchange']) @doc
@blueprint.route('', methods=['GET']) @blueprint.route('', methods=['GET'])
@jwt_required() @jwt_required()
@marshal_with(target_exchanges_schema) @marshal_with(target_exchanges_schema)
@@ -18,7 +20,7 @@ def get_list():
return TargetExchange.query.all() return TargetExchange.query.all()
@doc(tags=['TargetExchange']) @doc
@blueprint.route('', methods=['POST']) @blueprint.route('', methods=['POST'])
@jwt_required() @jwt_required()
@use_kwargs(target_exchange_schema) @use_kwargs(target_exchange_schema)
@@ -29,7 +31,7 @@ def create(name):
return target_exchange return target_exchange
@doc(tags=['TargetExchange']) @doc
@blueprint.route('/<exchange_id>', methods=['GET']) @blueprint.route('/<exchange_id>', methods=['GET'])
@jwt_required() @jwt_required()
@marshal_with(target_exchange_schema) @marshal_with(target_exchange_schema)
@@ -41,7 +43,7 @@ def get_by_id(exchange_id):
return NotFoundException(__name__) return NotFoundException(__name__)
@doc(tags=['TargetExchange']) @doc
@blueprint.route('/<exchange_id>', methods=['PUT']) @blueprint.route('/<exchange_id>', methods=['PUT'])
@jwt_required() @jwt_required()
@use_kwargs(target_exchange_schema) @use_kwargs(target_exchange_schema)

5
api/utils.py Normal file
View File

@@ -0,0 +1,5 @@
from flask_apispec import doc
def docwrap(tag, security='jwt', **kwargs):
return doc(tags=[tag], security=([{security: []}] if security is not None else None), **kwargs)

3
api/webhook/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""the webhook endpoint"""
from . import views

22
api/webhook/views.py Normal file
View File

@@ -0,0 +1,22 @@
from flask import Blueprint, jsonify
from flask_apispec import use_kwargs
from marshmallow import fields
from api.exceptions import NotFoundException
from api.registration.models import Registration
from api.utils import docwrap
blueprint = Blueprint('webhook', __name__)
@docwrap('webhook', 'api_key')
@blueprint.route('/webhook', methods=['GET'])
@use_kwargs({'apikey': fields.String(required=True)}, location='query')
def webhook(apikey):
reg = Registration.query.filter_by(token=apikey).first()
if reg is None:
raise NotFoundException(Registration.__name__)
return jsonify({'response': repr(reg)})
pass

View File

@@ -1,3 +1,54 @@
"""RabbitMQ helpers""" """RabbitMQ helpers"""
from .exchanges import ensure_exchange_exists, exchange_publish import pika
from .queues import ensure_queue_exists, queue_publish from flask import Flask, json
from pika.adapters.blocking_connection import BlockingChannel
class RabbitMQ:
host: str
connection: pika.BlockingConnection
channel: BlockingChannel
def __init__(self, app: Flask = None):
if app is not None:
self.init_app(app)
def init_app(self, app: Flask):
self.host = app.config.get('RABBITMQ_HOST', 'localhost')
self.connection = pika.BlockingConnection(
pika.ConnectionParameters(host=self.host))
self.channel = self.connection.channel()
def ensure_queue_exists(self, queue_name: str):
"""Ensures the queue exists on the default exchange
:param queue_name: the name of the queue
"""
self.channel.queue_declare(queue=queue_name, durable=True)
def queue_publish(self, queue_name: str, message):
self.ensure_queue_exists(queue_name)
body = json.dumps(message)
body_bytes = body.encode('utf-8')
self.channel.basic_publish(exchange='', routing_key=queue_name, body=body_bytes)
def ensure_exchange_exists(self, name: str) -> None:
"""Ensures exchange exists
:param name: the name of the exchange
"""
self.channel.exchange_declare(exchange=name, exchange_type='topic', durable=True)
def exchange_publish(self, exchange_name: str, topic: str, message):
"""Publishes a message to a
:param exchange_name: the name of the exchange
:param topic: the topic to publish to
:param message: the message to publish (will be turned into a json)
"""
self.ensure_exchange_exists(exchange_name)
body = json.dumps(message)
body_bytes = body.encode('utf-8')
self.channel.basic_publish(exchange=exchange_name, routing_key=topic, body=body_bytes)