Switch the Python side to Swagger v2

This commit is contained in:
Joseph Schorr 2015-05-14 16:47:38 -04:00
parent 86f400fdf5
commit 0bc1c29dff
21 changed files with 217 additions and 138 deletions

View file

@ -1,3 +1,5 @@
""" Billing information, subscriptions, and plan information. """
import stripe
from flask import request
@ -352,7 +354,7 @@ class UserInvoiceList(ApiResource):
@path_param('orgname', 'The name of the organization')
@related_user_resource(UserInvoiceList)
@show_if(features.BILLING)
class OrgnaizationInvoiceList(ApiResource):
class OrganizationInvoiceList(ApiResource):
""" Resource for listing an orgnaization's invoices. """
@require_scope(scopes.ORG_ADMIN)
@nickname('listOrgInvoices')

View file

@ -1,3 +1,5 @@
""" Create, list, cancel and get status/logs of repository builds. """
import logging
import json
import time
@ -165,7 +167,8 @@ class RepositoryBuildList(RepositoryParamResource):
},
'docker_tags': {
'type': 'array',
'description': 'The tags to which the built images will be pushed',
'description': 'The tags to which the built images will be pushed. ' +
'If none specified, "latest" is used.',
'items': {
'type': 'string'
},

View file

@ -1,5 +1,8 @@
""" API discovery information. """
import re
import logging
import sys
from flask.ext.restful import reqparse
@ -7,7 +10,7 @@ from endpoints.api import (ApiResource, resource, method_metadata, nickname, tru
parse_args, query_param)
from app import app
from auth import scopes
from collections import OrderedDict
logger = logging.getLogger(__name__)
@ -32,162 +35,195 @@ def fully_qualified_name(method_view_class):
def swagger_route_data(include_internal=False, compact=False):
apis = []
def swagger_parameter(name, description, kind='path', param_type='string', required=True,
enum=None, schema=None):
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#parameterObject
parameter_info = {
'name': name,
'in': kind,
'required': required
}
if not compact:
parameter_info['description'] = description or ''
if schema:
parameter_info['schema'] = {
'$ref': '#/definitions/%s' % schema
}
else:
parameter_info['type'] = param_type
if enum is not None and len(list(enum)) > 0:
parameter_info['enum'] = list(enum)
return parameter_info
paths = {}
models = {}
tags = []
tags_added = set()
for rule in app.url_map.iter_rules():
endpoint_method = app.view_functions[rule.endpoint]
if 'view_class' in dir(endpoint_method):
view_class = endpoint_method.view_class
# Verify that we have a view class for this API method.
if not 'view_class' in dir(endpoint_method):
continue
param_data_map = {}
if '__api_path_params' in dir(view_class):
param_data_map = view_class.__api_path_params
view_class = endpoint_method.view_class
operations = []
# Hide the class if it is internal.
internal = method_metadata(view_class, 'internal')
if not include_internal and internal:
continue
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
for method_name in method_names:
method = getattr(view_class, method_name.lower(), None)
# Build the tag.
parts = fully_qualified_name(view_class).split('.')
tag_name = parts[-2]
if not tag_name in tags_added:
tags_added.add(tag_name)
tags.append({
'name': tag_name,
'description': (sys.modules[view_class.__module__].__doc__ or '').strip()
})
parameters = []
# Build the Swagger data for the path.
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
path_swagger = {
'name': fully_qualified_name(view_class),
'tag': tag_name
}
for param in rule.arguments:
parameters.append({
'paramType': 'path',
'name': param,
'dataType': 'string',
'description': param_data_map.get(param, {'description': ''})['description'],
'required': True,
})
paths[swagger_path] = path_swagger
if method is None:
logger.debug('Unable to find method for %s in class %s', method_name, view_class)
else:
req_schema_name = method_metadata(method, 'request_schema')
if req_schema_name:
parameters.append({
'paramType': 'body',
'name': 'body',
'description': 'Request body contents.',
'dataType': req_schema_name,
'required': True,
})
# Add any global path parameters.
param_data_map = view_class.__api_path_params if '__api_path_params' in dir(view_class) else {}
if param_data_map:
path_parameters_swagger = []
for path_parameter in param_data_map:
description = param_data_map[path_parameter].get('description')
path_parameters_swagger.append(swagger_parameter(path_parameter, description))
schema = view_class.schemas[req_schema_name]
models[req_schema_name] = schema
path_swagger['parameters'] = path_parameters_swagger
if '__api_query_params' in dir(method):
for param_spec in method.__api_query_params:
new_param = {
'paramType': 'query',
'name': param_spec['name'],
'description': param_spec['help'],
'dataType': TYPE_CONVERTER[param_spec['type']],
'required': param_spec['required'],
}
# Add the individual HTTP operations.
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
for method_name in method_names:
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object
method = getattr(view_class, method_name.lower(), None)
if method is None:
logger.debug('Unable to find method for %s in class %s', method_name, view_class)
continue
if len(param_spec['choices']) > 0:
new_param['enum'] = list(param_spec['choices'])
parameters.append(new_param)
new_operation = {
'method': method_name,
'nickname': method_metadata(method, 'nickname') or '(unnamed)'
}
if not compact:
response_type = 'void'
res_schema_name = method_metadata(method, 'response_schema')
if res_schema_name:
models[res_schema_name] = view_class.schemas[res_schema_name]
response_type = res_schema_name
new_operation.update({
'type': response_type,
'summary': method.__doc__.strip() if method.__doc__ else '',
'parameters': parameters,
})
scope = method_metadata(method, 'oauth2_scope')
if scope and not compact:
new_operation['authorizations'] = {
'oauth2': [
{
'scope': scope.scope
}
],
}
internal = method_metadata(method, 'internal')
if internal is not None:
new_operation['internal'] = True
if include_internal:
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
if requires_fresh_login is not None:
new_operation['requires_fresh_login'] = True
if not internal or (internal and include_internal):
# Swagger requires valid nicknames on all operations.
if new_operation.get('nickname'):
operations.append(new_operation)
else:
logger.debug('Operation missing nickname: %s' % method)
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
new_resource = {
'path': swagger_path,
'description': view_class.__doc__.strip() if view_class.__doc__ else "",
'operations': operations,
'name': fully_qualified_name(view_class),
operation_swagger = {
'operationId': method_metadata(method, 'nickname') or 'unnamed',
'parameters': [],
}
related_user_res = method_metadata(view_class, 'related_user_resource')
if related_user_res is not None:
new_resource['quayUserRelated'] = fully_qualified_name(related_user_res)
if not compact:
operation_swagger.update({
'description': method.__doc__.strip() if method.__doc__ else '',
'tags': [tag_name]
})
internal = method_metadata(view_class, 'internal')
# Mark the method as internal.
internal = method_metadata(method, 'internal')
if internal is not None:
new_resource['internal'] = True
operation_swagger['internal'] = True
if include_internal:
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
if requires_fresh_login is not None:
operation_swagger['requires_fresh_login'] = True
related_user_res = method_metadata(view_class, 'related_user_resource')
if related_user_res is not None:
operation_swagger['quayUserRelated'] = fully_qualified_name(related_user_res)
# Add the path parameters.
if rule.arguments:
for path_parameter in rule.arguments:
description = param_data_map.get(path_parameter, {}).get('description')
operation_swagger['parameters'].append(swagger_parameter(path_parameter, description))
# Add the query parameters.
if '__api_query_params' in dir(method):
for query_parameter_info in method.__api_query_params:
name = query_parameter_info['name']
description = query_parameter_info['help']
param_type = TYPE_CONVERTER[query_parameter_info['type']]
required = query_parameter_info['required']
operation_swagger['parameters'].append(
swagger_parameter(name, description, kind='query',
param_type=param_type,
required=required,
enum=query_parameter_info['choices']))
# Add the OAuth security block.
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#securityRequirementObject
scope = method_metadata(method, 'oauth2_scope')
if scope and not compact:
operation_swagger['security'] = [{'oauth2_implicit': scope.scope}]
# TODO: Add the responses block.
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#responsesObject
response_schema_name = method_metadata(method, 'response_schema')
if response_schema_name and not compact:
models[response_schema_name] = view_class.schemas[response_schema_name]
response_type = response_schema_name
# Add the request block.
request_schema_name = method_metadata(method, 'request_schema')
if request_schema_name and not compact:
models[request_schema_name] = view_class.schemas[request_schema_name]
operation_swagger['parameters'].append(
swagger_parameter('body', 'Request body contents.', kind='body',
schema=request_schema_name))
# Add the operation to the parent path.
if not internal or (internal and include_internal):
apis.append(new_resource)
path_swagger[method_name.lower()] = operation_swagger
tags.sort(key=lambda t: t['name'])
paths = OrderedDict(sorted(paths.items(), key=lambda p: p[1]['tag']))
# If compact form was requested, simply return the APIs.
if compact:
return {'apis': apis}
return {'paths': paths}
swagger_data = {
'apiVersion': 'v1',
'swaggerVersion': '1.2',
'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
'resourcePath': '/',
'swagger': '2.0',
'host': SERVER_HOSTNAME,
'basePath': '/',
'schemes': [
PREFERRED_URL_SCHEME
],
'info': {
'title': 'Quay.io API',
'version': 'v1',
'title': 'Quay.io Frontend',
'description': ('This API allows you to perform many of the operations required to work '
'with Quay.io repositories, users, and organizations. You can find out more '
'at <a href="https://quay.io">Quay.io</a>.'),
'termsOfServiceUrl': 'https://quay.io/tos',
'contact': 'support@quay.io',
'termsOfService': 'https://quay.io/tos',
'contact': {
'email': 'support@quay.io'
}
},
'authorizations': {
'oauth2': {
'scopes': [scope._asdict() for scope in scopes.ALL_SCOPES.values()],
'grantTypes': {
"implicit": {
"tokenName": "access_token",
"loginEndpoint": {
"url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
},
},
},
'securityDefinitions': {
'oauth2_implicit': {
"type": "oauth2",
"flow": "implicit",
"authorizationUrl": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
'scopes': {scope.scope:scope.description for scope in scopes.ALL_SCOPES.values()},
},
},
'apis': apis,
'models': models,
'paths': paths,
'definitions': models,
'tags': tags
}
return swagger_data

View file

@ -1,3 +1,5 @@
""" List and lookup repository images, and download image diffs. """
import json
from collections import defaultdict

View file

@ -1,3 +1,5 @@
""" Acces usage logs for organizations or repositories. """
import json
from datetime import datetime, timedelta

View file

@ -1,3 +1,5 @@
""" Manage organizations, members and OAuth applications. """
import logging
from flask import request
@ -333,7 +335,7 @@ def app_view(application):
@resource('/v1/organization/<orgname>/applications')
@path_param('orgname', 'The name of the organization')
class OrganizationApplications(ApiResource):
""" Resource for managing applications defined by an organizations. """
""" Resource for managing applications defined by an organization. """
schemas = {
'NewApp': {
'id': 'NewApp',

View file

@ -1,3 +1,5 @@
""" Manage repository permissions. """
import logging
from flask import request

View file

@ -1,3 +1,5 @@
""" Manage default permissions added to repositories. """
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,

View file

@ -1,3 +1,5 @@
""" Authorize repository to send e-mail notifications. """
import logging
from flask import request, abort

View file

@ -1,3 +1,5 @@
""" List, create and manage repositories. """
import logging
import json
import datetime

View file

@ -1,3 +1,5 @@
""" List, create and manage repository events/notifications. """
import json
from flask import request, abort

View file

@ -1,3 +1,5 @@
""" Manage repository access tokens (DEPRECATED). """
import logging
from flask import request

View file

@ -1,3 +1,5 @@
""" Manage user and organization robot accounts. """
from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource,
Unauthorized, require_user_admin, internal_only, require_scope,
path_param, parse_args, truthy_bool, query_param)
@ -68,7 +70,6 @@ def robots_list(prefix, include_permissions=False):
return {'robots': robots.values()}
@resource('/v1/user/robots')
@internal_only
class UserRobotList(ApiResource):
""" Resource for listing user robots. """
@require_user_admin
@ -85,7 +86,6 @@ class UserRobotList(ApiResource):
@resource('/v1/user/robots/<robot_shortname>')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
class UserRobot(ApiResource):
""" Resource for managing a user's robots. """
@require_user_admin
@ -181,7 +181,6 @@ class OrgRobot(ApiResource):
@resource('/v1/user/robots/<robot_shortname>/permissions')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
class UserRobotPermissions(ApiResource):
""" Resource for listing the permissions a user's robot has in the system. """
@require_user_admin
@ -222,7 +221,6 @@ class OrgRobotPermissions(ApiResource):
@resource('/v1/user/robots/<robot_shortname>/regenerate')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
class RegenerateUserRobot(ApiResource):
""" Resource for regenerate an organization's robot's token. """
@require_user_admin

View file

@ -1,3 +1,5 @@
""" Conduct searches against all registry context. """
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
require_scope, path_param)
from data import model

View file

@ -1,3 +1,5 @@
""" Subscribe to plans. """
import logging
import stripe

View file

@ -1,3 +1,5 @@
""" Superuser Config API. """
import logging
import os
import json

View file

@ -1,3 +1,5 @@
""" Superuser API. """
import string
import logging
import json

View file

@ -1,3 +1,5 @@
""" Manage the tags of a repository. """
from flask import request, abort
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,

View file

@ -1,3 +1,5 @@
""" Create, list and manage an organization's teams. """
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,

View file

@ -1,3 +1,5 @@
""" Create, list and manage build triggers. """
import json
import logging
@ -397,7 +399,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
'properties': {
'branch_name': {
'type': 'string',
'description': '(GitHub Only) If specified, the name of the GitHub branch to build.'
'description': '(SCM only) If specified, the name of the branch to build.'
},
'commit_sha': {
'type': 'string',

View file

@ -1,3 +1,5 @@
""" Manage the current user. """
import logging
import json
@ -182,11 +184,17 @@ class User(ApiResource):
},
'organizations': {
'type': 'array',
'description': 'Information about the organizations in which the user is a member'
'description': 'Information about the organizations in which the user is a member',
'items': {
'type': 'object'
}
},
'logins': {
'type': 'array',
'description': 'The list of external login providers against which the user has authenticated'
'description': 'The list of external login providers against which the user has authenticated',
'items': {
'type': 'object'
}
},
'can_create_repo': {
'type': 'boolean',
@ -794,10 +802,10 @@ class StarredRepositoryList(ApiResource):
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class StarredRepository(RepositoryParamResource):
""" Operations for managing a specific starred repository. """
@nickname('deleteStar')
@require_user_admin
def delete(self, namespace, repository):
""" Removes a star from a repository. """
user = get_authenticated_user()
repo = model.get_repository(namespace, repository)