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 . import commands, login, target_exchange, target, registration
from . import commands, login, target_exchange, target, registration, webhook
from .exceptions import ApiException
from .extensions import db, migrate, jwt, apispec
from .extensions import db, migrate, jwt, apispec, rabbit
from .settings import ProdConfig, Config
@@ -31,6 +31,7 @@ def register_extensions(app: Flask):
migrate.init_app(app, db)
jwt.init_app(app)
apispec.init_app(app)
rabbit.init_app(app)
def register_blueprints(app: Flask):
@@ -43,6 +44,7 @@ def register_blueprints(app: Flask):
api_blueprint.register_blueprint(registration.views.blueprint, url_prefix='/registration')
app.register_blueprint(api_blueprint)
app.register_blueprint(webhook.views.blueprint)
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
"""
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():
if name == 'static':
continue
@@ -82,6 +87,7 @@ def register_shellcontext(app: Flask):
"""Shell context objects."""
return {
'db': db,
'rabbit': rabbit,
'TargetExchange': target_exchange.models.TargetExchange,
'Target': target.models.Target,
'Registration': registration.models.Registration,

View File

@@ -5,7 +5,7 @@ from flask import current_app
from flask.cli import with_appcontext
from werkzeug.exceptions import MethodNotAllowed, NotFound
import rmq_helper
from .extensions import rabbit
from .target.models import Target
from .target_exchange.models import TargetExchange
@@ -100,9 +100,9 @@ def seed():
def setup_rabmq():
"""Set up rabbitMQ"""
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():
rmq_helper.ensure_queue_exists(target.routing_key)
rabbit.ensure_queue_exists(target.routing_key)
pass

View File

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

View File

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

View File

@@ -4,21 +4,19 @@ from flask_apispec import use_kwargs, marshal_with, doc
from marshmallow import fields
from api.exceptions import BadRequestException
from api.utils import docwrap
import api.constants as constants
from .serializers import token_response_schema
from .serializers import token_response_schema, login_schema
blueprint = Blueprint('login', __name__)
@doc(tags=['login'])
@docwrap('Login', None)
@blueprint.route('', methods=['POST'])
@jwt_required(optional=True)
@use_kwargs({
'username': fields.Str(required=True),
'password': fields.Str(required=True)
})
@use_kwargs(login_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]:
return {'token': create_access_token(identity=username, fresh=True, expires_delta=False)}
else:

View File

@@ -19,3 +19,6 @@ class Registration(SurrogatePK, Model):
def __init__(self, **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 api.exceptions import NotFoundException, BadRequestException
from api.utils import docwrap
from api.target.models import Target
from api.target_exchange.models import TargetExchange
from .models import Registration
from .serializers import registration_schema, registrations_schema
blueprint = Blueprint('Registration', __name__)
doc = docwrap('Registration')
@doc(tags=['Registration'])
@doc
@blueprint.route('', methods=['GET'])
@jwt_required()
@use_kwargs({'exchange': fields.Str(), 'target': fields.Str()})
@use_kwargs({'exchange': fields.Str(), 'target': fields.Str()}, location='query')
@marshal_with(registrations_schema)
def get_list(exchange=None, target=None):
res = Registration.query
@@ -26,7 +29,7 @@ def get_list(exchange=None, target=None):
return res.all()
@doc(tags=['Registration'])
@doc
@blueprint.route('', methods=['POST'])
@jwt_required()
@use_kwargs(registration_schema)
@@ -43,7 +46,7 @@ def create(name, routing_key, targets):
return registration
@doc(tags=['Registration'])
@doc
@blueprint.route('/<registration_id>', methods=['GET'])
@jwt_required()
@marshal_with(registration_schema)
@@ -55,7 +58,7 @@ def get_by_id(registration_id: int):
return NotFoundException(Registration.__name__)
@doc(tags=['Registration'])
@doc
@blueprint.route('/<registration_id>', methods=['PUT'])
@jwt_required()
@use_kwargs(registration_schema)

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,18 @@
from flask import Blueprint
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.utils import docwrap
from .models import TargetExchange
from .serializers import target_exchange_schema, target_exchanges_schema
blueprint = Blueprint('target_exchange', __name__)
doc = docwrap('TargetExchange')
@doc(tags=['TargetExchange'])
@doc
@blueprint.route('', methods=['GET'])
@jwt_required()
@marshal_with(target_exchanges_schema)
@@ -18,7 +20,7 @@ def get_list():
return TargetExchange.query.all()
@doc(tags=['TargetExchange'])
@doc
@blueprint.route('', methods=['POST'])
@jwt_required()
@use_kwargs(target_exchange_schema)
@@ -29,7 +31,7 @@ def create(name):
return target_exchange
@doc(tags=['TargetExchange'])
@doc
@blueprint.route('/<exchange_id>', methods=['GET'])
@jwt_required()
@marshal_with(target_exchange_schema)
@@ -41,7 +43,7 @@ def get_by_id(exchange_id):
return NotFoundException(__name__)
@doc(tags=['TargetExchange'])
@doc
@blueprint.route('/<exchange_id>', methods=['PUT'])
@jwt_required()
@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"""
from .exchanges import ensure_exchange_exists, exchange_publish
from .queues import ensure_queue_exists, queue_publish
import pika
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)