First attempt at using flask-restful and swagger api documentation.

This commit is contained in:
jakedt 2014-03-10 18:30:41 -04:00
parent 52d2229482
commit de1a44f853
5 changed files with 283 additions and 4 deletions

40
endpoints/api/__init__.py Normal file
View file

@ -0,0 +1,40 @@
from flask import Blueprint
from calendar import timegm
from email.utils import formatdate
from functools import partial
api = Blueprint('api', __name__)
def truthy_bool(param):
return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'}
def format_date(date):
""" Output an RFC822 date format. """
return formatdate(timegm(date.utctimetuple()))
def add_method_metadata(name, value):
def modifier(func):
if '__api_metadata' not in dir(func):
func.__metadata = {}
func.__metadata[name] = value
return func
return modifier
def method_metadata(func, name):
if '__api_metadata' in dir(func):
return func.__metadata.get(name, None)
return None
nickname = partial(add_method_metadata, 'nickname')
import endpoints.api.legacy
import endpoints.api.repository
import endpoints.api.discovery

View file

@ -0,0 +1,66 @@
import re
from flask.ext.restful import Api, Resource
from endpoints.api import api, method_metadata, nickname
from endpoints.common import get_route_data
from app import app
discovery_api = Api(api)
param_regex = re.compile(r'<([\w]+:)?([\w]+)>')
def swagger_route_data():
apis = []
for rule in app.url_map.iter_rules():
endpoint_method = app.view_functions[rule.endpoint]
if 'view_class' in dir(endpoint_method):
operations = []
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
for method_name in method_names:
method = getattr(endpoint_method.view_class, method_name.lower(), None)
parameters = []
for param in rule.arguments:
parameters.append({
'paramType': 'path',
'name': param,
'dataType': 'string',
'description': 'Param description.',
'required': True
})
if method is not None:
operations.append({
'method': method_name,
'nickname': method_metadata(method, 'nickname'),
'type': 'void',
'parameters': parameters,
})
swagger_path = param_regex.sub(r'{\2}', rule.rule)
apis.append({
'path': swagger_path,
'description': 'Resource description.',
'operations': operations,
})
swagger_data = {
'apiVersion': 'v1',
'swaggerVersion': '1.2',
'basePath': 'https://quay.io/',
'apis': apis,
}
return swagger_data
class DiscoveryResource(Resource):
@nickname('discovery')
def get(self):
return swagger_route_data()
discovery_api.add_resource(DiscoveryResource, '/v1/discovery')

View file

@ -11,6 +11,7 @@ from functools import wraps
from collections import defaultdict from collections import defaultdict
from urllib import quote from urllib import quote
from endpoints.api import api
from data import model from data import model
from data.plans import PLANS, get_plan from data.plans import PLANS, get_plan
from app import app from app import app
@ -42,9 +43,6 @@ build_logs = app.config['BUILDLOGS']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api = Blueprint('api', __name__)
@api.before_request @api.before_request
def csrf_protect(): def csrf_protect():
if request.method != "GET" and request.method != "HEAD": if request.method != "GET" and request.method != "HEAD":
@ -1146,6 +1144,8 @@ def trigger_view(trigger):
def build_status_view(build_obj, can_write=False): def build_status_view(build_obj, can_write=False):
status = build_logs.get_status(build_obj.uuid) status = build_logs.get_status(build_obj.uuid)
logger.debug('Can write: %s job_config: %s', can_write, build_obj.job_config)
build_obj.job_config = None
return { return {
'id': build_obj.uuid, 'id': build_obj.uuid,
'phase': build_obj.phase, 'phase': build_obj.phase,

171
endpoints/api/repository.py Normal file
View file

@ -0,0 +1,171 @@
import logging
import json
from functools import wraps
from flask.ext.restful import Resource, Api, reqparse, abort, fields
from flask.ext.login import current_user
from data import model
from endpoints.api import api, truthy_bool, format_date, nickname
from util.names import parse_namespace_repository
from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission,
AdministerRepositoryPermission)
logger = logging.getLogger(__name__)
repo_api = Api(api)
def parse_repository_name(f):
@wraps(f)
def wrapper(repository, *args, **kwargs):
(namespace, repository) = parse_namespace_repository(repository)
return f(namespace, repository, *args, **kwargs)
return wrapper
class RepositoryParamResource(Resource):
method_decorators = [parse_repository_name]
def resource(*urls, **kwargs):
def wrapper(api_resource):
repo_api.add_resource(api_resource, *urls, **kwargs)
return api_resource
return wrapper
def require_repo_permission(permission_class, allow_public=False):
def wrapper(func):
@wraps(func)
def wrapped(self, namespace, repository, *args, **kwargs):
permission = permission_class(namespace, repository)
if (permission.can() or
(allow_public and
model.repository_is_public(namespace, repository))):
return func(self, namespace, repository, *args, **kwargs)
abort(403)
func.__required_permission = 'read'
return wrapped
return wrapper
require_repo_read = require_repo_permission(ReadRepositoryPermission, True)
require_repo_write = require_repo_permission(ModifyRepositoryPermission)
require_repo_admin = require_repo_permission(AdministerRepositoryPermission)
@resource('/v1/repository')
class RepositoryList(Resource):
@nickname('createRepo')
def post(self):
pass
@nickname('listRepos')
def get(self):
parser = reqparse.RequestParser()
parser.add_argument('page', type=int, help='Page number must be an int.')
parser.add_argument('limit', type=int, help='Limit must be an int.')
parser.add_argument('namespace', type=str)
parser.add_argument('public', type=truthy_bool, default=True)
parser.add_argument('private', type=truthy_bool, default=True)
parser.add_argument('sort', type=truthy_bool, default=False)
parser.add_argument('count', type=truthy_bool, default=False)
args = parser.parse_args()
def repo_view(repo_obj):
return {
'namespace': repo_obj.namespace,
'name': repo_obj.name,
'description': repo_obj.description,
'is_public': repo_obj.visibility.name == 'public',
}
username = None
if current_user.is_authenticated() and args['private']:
username = current_user.db_user().username
response = {}
repo_count = None
if args['count']:
repo_count = model.get_visible_repository_count(username,
include_public=args['public'],
namespace=args['namespace'])
response['count'] = repo_count
repo_query = model.get_visible_repositories(username, limit=args['limit'],
page=args['page'],
include_public=args['public'],
sort=args['sort'],
namespace=args['namespace'])
response['repositories'] = [repo_view(repo) for repo in repo_query]
return response
def image_view(image):
extended_props = image
if image.storage and image.storage.id:
extended_props = image.storage
command = extended_props.command
return {
'id': image.docker_image_id,
'created': format_date(extended_props.created),
'comment': extended_props.comment,
'command': json.loads(command) if command else None,
'ancestors': image.ancestors,
'dbid': image.id,
'size': extended_props.image_size,
}
@resource('/v1/repository/<path:repository>')
class Repository(RepositoryParamResource):
@require_repo_read
@nickname('getRepo')
def get(self, namespace, repository):
logger.debug('Get repo: %s/%s' % (namespace, repository))
def tag_view(tag):
image = model.get_tag_image(namespace, repository, tag.name)
if not image:
return {}
return {
'name': tag.name,
'image': image_view(image),
}
organization = None
try:
organization = model.get_organization(namespace)
except model.InvalidOrganizationException:
pass
is_public = model.repository_is_public(namespace, repository)
repo = model.get_repository(namespace, repository)
if repo:
tags = model.list_repository_tags(namespace, repository)
tag_dict = {tag.name: tag_view(tag) for tag in tags}
can_write = ModifyRepositoryPermission(namespace, repository).can()
can_admin = AdministerRepositoryPermission(namespace, repository).can()
active_builds = model.list_repository_builds(namespace, repository, 1,
include_inactive=False)
return {
'namespace': namespace,
'name': repository,
'description': repo.description,
'tags': tag_dict,
'can_write': can_write,
'can_admin': can_admin,
'is_public': is_public,
'is_building': len(list(active_builds)) > 0,
'is_organization': bool(organization),
'status_token': repo.badge_token if not is_public else ''
}
abort(404) # Not found

View file

@ -23,4 +23,6 @@ redis
hiredis hiredis
git+https://github.com/dotcloud/docker-py.git git+https://github.com/dotcloud/docker-py.git
loremipsum loremipsum
pygithub pygithub
flask-restful
jsonschema