class FooResource(Resource): @Route.POST() def bar(self, value): pass bar.request_schema = FieldSet({"value": fields.Boolean(nullable=True)}) @bar.GET() def bar(self): pass bar.response_schema = FieldSet({"value": fields.Boolean(nullable=True)}) class Meta: name = 'foo'
def __init__(self, method=None, view_func=None, rule=None, attribute=None, rel=None, title=None, description=None, schema=None, response_schema=None, format_response=True): self.rel = rel self.rule = rule self.method = method self.attribute = attribute self.title = title self.description = description self.view_func = view_func self.format_response = format_response annotations = getattr(view_func, '__annotations__', None) if isinstance(annotations, dict) and len(annotations): self.request_schema = FieldSet({name: field for name, field in annotations.items() if name != 'return'}) self.response_schema = annotations.get('return', response_schema) else: self.request_schema = schema self.response_schema = response_schema self._related_routes = () for method in HTTP_METHODS: setattr(self, method, MethodType(_method_decorator(method), self))
class Box(ModelResource): class Schema: description = fields.String() is_open = fields.Boolean(default=False) class Meta: model = 'box' manager = MemoryManager include_id = True include_type = True @ItemRoute.GET() def verbose_description(self, box): return box["description"] + " box" verbose_description.response_schema = fields.String() @ItemRoute.POST() def open(self, box): return self.manager.update(box, {"is_open": True}) @Route.GET(schema=FieldSet({'greeting': fields.String()})) def greet(self, greeting): return greeting + " box!" open.response_schema = fields.Inline('self', attribute='Test')
def routes(self): io = self.io rule = '/{}'.format(attribute_to_route_uri(self.attribute)) relation_route = ItemRoute(rule='{}/<{}:target_id>'.format( rule, self.target.meta.id_converter)) relations_route = ItemRoute(rule=rule) if "r" in io: def relation_instances(resource, item, page, per_page): return resource.manager.relation_instances( item, self.attribute, self.target, page, per_page) yield relations_route.for_method( 'GET', relation_instances, rel=self.attribute, response_schema=RelationInstances(self.target), schema=FieldSet({ "page": Integer(minimum=1, default=1), "per_page": Integer( minimum=1, default=20, maximum=50 # FIXME use API reference ), }), ) if "w" in io or "u" in io: def relation_add(resource, item, target_item): resource.manager.relation_add(item, self.attribute, self.target, target_item) resource.manager.commit() return target_item yield relations_route.for_method( 'POST', relation_add, rel=to_camel_case('add_{}'.format(self.attribute)), response_schema=ToOne(self.target), schema=ToOne(self.target), ) def relation_remove(resource, item, target_id): target_item = self.target.manager.read(target_id) resource.manager.relation_remove(item, self.attribute, self.target, target_item) resource.manager.commit() return None, 204 yield relation_route.for_method( 'DELETE', relation_remove, rel=to_camel_case('remove_{}'.format(self.attribute)), )
def test_fieldset_rebind(self): class FooResource(Resource): pass class BarResource(Resource): pass FieldSet({"foo": fields.String()}).bind(FooResource).bind(BarResource)
class ShareAnalysisResource(Resource): class Meta: name = 'share' title = 'Share Analysis API' description = 'The API for sharing analysis' # pylint: disable=no-member # pylint: disable=R0201 @Route.POST( '/email', title='Share analysis by email', description='Shares an analysis by email', schema=FieldSet(SHARE_ANALYSIS_EMAIL_SCHEMA), response_schema=fields.Any(), ) def share_by_email(self, **kwargs): shared_image_name = 'share_analysis.png' logger = g.request_logger if hasattr(g, 'request_logger') else LOG attachments = [("attachment", (attachment['filename'], attachment['content'])) for attachment in kwargs['attachments']] base64_image_str = kwargs.get('image_url') if base64_image_str: base64_image_str = base64_image_str.replace( 'data:image/png;base64,', '') attachments.append(( "inline", ( shared_image_name, base64.decodebytes(base64_image_str.encode('utf-8')), ), )) else: shared_image_name = '' for recipient in kwargs['recipients']: msg = current_app.email_renderer.create_share_analysis_email( subject=kwargs['subject'], recipient=recipient, reply_to=kwargs['sender'], body=kwargs['message'], attachments=attachments, query_url=kwargs['query_url'], image=shared_image_name, ) try: current_app.notification_service.send_email(msg) except NotificationError: error = 'Failed to send share analysis email to: \'%s\'' % recipient logger.error(error) raise BadGateway(error) message = (SHARE_ANALYSIS_PREVIEW_MESSAGE.format( recipient=','.join(kwargs['recipients'])) if kwargs.get( 'is_preview', False) else SHARE_ANALYSIS_MESSAGE) return {'message': message}
def test_fieldset_parse_request(self): app = Flask(__name__) env = {} with app.test_request_context(): env = request.environ # Ensure allow empty POST fs = FieldSet(None) env['REQUEST_METHOD'] = 'POST' fs.parse_request(Request(env)) # Ensure failure when there are fields fs = FieldSet({'field': fields.String()}) with self.assertRaises(RequestMustBeJSON): fs.parse_request(Request(env)) # Successful POST with data env['wsgi.input'] = StringIO('{"field": "data"}') env['CONTENT_TYPE'] = 'application/json' env['CONTENT_LENGTH'] = '17' fs.parse_request(Request(env))
def test_fieldset_schema_io(self): fs = FieldSet({ "id": fields.Number(io='r'), "name": fields.String(), "secret": fields.String(io='c'), "updateOnly": fields.String(io='u'), }) self.assertEqual( { "type": "object", "properties": { "id": { "type": "number", "readOnly": True }, "name": { "type": "string" }, }, }, fs.response, ) self.assertEqual( { "type": "object", "additionalProperties": False, "properties": { "name": { "type": "string" }, "updateOnly": { "type": "string" }, }, }, fs.update, ) self.assertEqual( { "name": { "type": "string" }, "secret": { "type": "string" } }, fs.create['properties'], ) self.assertEqual({"name", "secret"}, set(fs.create['required']))
def test_fieldset_format(self): self.assertEqual({ "number": 42, "constant": "constant" }, FieldSet({ "number": fields.Number(), "constant": fields.String(io='r'), "secret": fields.String(io='w'), }).format({ "number": 42, "constant": "constant", "secret": "secret" }))
def augment_standard_schema(additional_fields): '''Augments the schema of the `StandardResponse` class with additional fields that are to be included in the response to an API request. Example ---------- ``` # The `StandardResponse` has 3 attributes: `success`, `description` and `code`. I wish to # return a field named `foo_bar` as well in the response object. from flask_potion import fields # I create the following dictionary that defines the username new_fields = { 'foo_bar': fields.String( title='Foo's bar', description='Some random text I want to include.')} # I augment the standard response schema NEW_SCHEMA = augment_standard_schema(new_fields) # In some API, I have the following Route (`baz`) under the `qux` API: @Route.POST('/baz', response_schema=NEW_SCHEMA) def do_something(): return StandardResponse(message, OK, True, {'foo_bar': 'lorem ipsum dolor'}) # If I view the schema for that API (http://localhost:5000/api2/qux/schema), # I will see that the `foo_bar` field is included as a response type in the schema for # the `POST` method of the `http://localhost:5000/api2/qux/baz` route. ``` Parameters ---------- additional_fields : dict The dictionary containing the new/updated fields in the response object. Returns ------- `flask_potion.schema.FieldSet` The new schema. ''' merged_fields = dict(STANDARD_RESPONSE_FIELDS) merged_fields.update(additional_fields) return FieldSet(merged_fields)
def add_view(self, video) -> FieldSet({'count': fields.Integer(), 'view': fields.Inline(ViewResource)}): '''Create view and return view and count''' view = ViewResource().manager.create({ 'video_id': video.id }) return {'count': video.count, 'view': view}
self['code'] = code self['success'] = success for key, value in list(additional_fields.items()): self[key] = value STANDARD_RESPONSE_FIELDS = { 'success': fields.Boolean( description='Indicates whether the requested operation was successful or not.', nullable=True, ), 'message': fields.String(min_length=1, description='The response from the server.'), 'code': fields.Integer(description='The HTTP response code from the server.'), } STANDARD_RESPONSE_SCHEMA = FieldSet(STANDARD_RESPONSE_FIELDS) def augment_standard_schema(additional_fields): '''Augments the schema of the `StandardResponse` class with additional fields that are to be included in the response to an API request. Example ---------- ``` # The `StandardResponse` has 3 attributes: `success`, `description` and `code`. I wish to # return a field named `foo_bar` as well in the response object. from flask_potion import fields # I create the following dictionary that defines the username
class RoleResource(PrincipalResource): '''The potion class for performing CRUD operations on the `Role` class. ''' resourceType = Relation('resource-type', attribute='resource_type', io='r') class Meta(object): model = Role natural_key = 'name' # Read Permissions are the defaults as defined in # `web.server.security.permissions.PERMISSION_DEFAULTS` # # Create, Update and Delete Permissions are enforced by the # Signal Handlers installed when the API for this Resource # are initialized in `web.server.security.signal_handlers.py` filters = { 'name': True, 'resourceType': { None: ResourceTypeFilter, 'eq': ResourceTypeFilter }, } class Schema(object): permissions = fields.List( PERMISSION_SCHEMA, title='Role Permissions', description='The permissions the role has.', ) resourceType = fields.Custom( fields.String(), attribute='resource_type', converter=None, formatter=lambda rsc_type: rsc_type.name.name, title='resourceType', description='The resource type this role is associated with.', io='r', ) # pylint: disable=E1101 @ItemRoute.PATCH( '/permissions', title='Update Role Permissions', description= 'Updates the role\'s permissions with the values specified.', rel='updatePermissions', schema=fields.List( PERMISSION_SCHEMA, title='Updated Permissions', description='The updated role permissions.', ), ) def update_role_permissions(self, role, new_permissions): with AuthorizedOperation('update_permissions', 'role', role.id): self.manager.update(role, {'permissions': new_permissions}) return None, NO_CONTENT @ItemRoute.POST( '/permission', title='Add Role Permission', description='Adds a single permission to a Role.', rel='addPermission', schema=FieldSet({ 'permissionId': fields.Integer( description= 'The id of the new permission to be added to this role.') }), response_schema=UPDATE_ROLE_PERMISSIONS_RESPONSE_SCHEMA, ) def add_single_permision(self, role, permissionId): with AuthorizedOperation('update_permissions', 'role', role.id): new_permission = find_by_id(Permission, permissionId) if not new_permission: raise ItemNotFound('permission', id=permissionId) if new_permission.resource_type_id != role.resource_type_id: error_message = ( 'The resource type associated with the permission ' 'must match the resource type of the role.') return ( StandardResponse( error_message, BAD_REQUEST, False, roleResourceType=new_permission.resource_type, permissionResourceType=role.resource_type, ), BAD_REQUEST, ) exists = True if new_permission not in role.permissions: exists = False role.permissions.append(new_permission) self.manager.update(role, {'permissions': role.permissions}) added_message = 'already exists for' if exists else 'has been added' success_message = 'Permission \'%s\' %s to Role \'%s\'.' % ( new_permission.permission, added_message, role.name, ) return StandardResponse(success_message, OK, True) @ItemRoute.DELETE( '/permission', title='Delete Role Permission', description='Deletes a single permission from a role.', schema=FieldSet({ 'permissionId': fields.Integer( description= 'The id of the new permission to be deleted from this role.') }), response_schema=UPDATE_ROLE_PERMISSIONS_RESPONSE_SCHEMA, rel='deletePermission', ) def delete_individual_permission(self, role, permissionId): with AuthorizedOperation('update_permissions', 'role', role.id): old_permission = find_by_id(Permission, permissionId) if not old_permission: raise ItemNotFound('permission', id=permissionId) if old_permission.resource_type_id != role.resource_type_id: error_message = ( 'The resource type associated with the permission ' 'must match the resource type of the role.') return ( StandardResponse( error_message, BAD_REQUEST, False, roleResourceType=old_permission.resource_type, permissionResourceType=role.resource_type, ), BAD_REQUEST, ) exists = True try: role.permissions.remove(old_permission) self.manager.update(role, {'permissions': role.permissions}) except ValueError: exists = False deleted_message = ('has been removed from' if exists else 'does not exist for') success_message = 'Permission \'%s\' %s Role \'%s\'.' % ( old_permission.permission, deleted_message, role.name, ) return StandardResponse(success_message, OK, True)
class UserResource(PrincipalResource): '''The potion class for performing CRUD operations on the `User` class. ''' class Meta(object): title = 'Users API' description = ( 'The API through which CRUD operations can be performed on User(s).' ) model = User natural_key = 'username' permissions = {'read': 'yes'} # Read Permissions are the defaults as defined in # `web.server.security.permissions.PERMISSION_DEFAULTS` # # Create, Update and Delete Permissions are enforced by the # Signal Handlers installed when the API for this Resource # are initialized in `web.server.security.signal_handlers.py` # Allow users to filter resources by name, username, status, and phone filters = { 'username': True, 'status': True, 'firstName': True, 'lastName': True, 'phoneNumber': True, } exclude_fields = ('password', 'reset_password_token') class Schema(object): username = USERNAME_SCHEMA roles = fields.Custom( ROLE_MAP_SCHEMA, description='The role(s) that the user currently possesses.', attribute='roles', formatter=role_list_as_map, default=[], ) firstName = fields.String( description='The first name of the user.', attribute='first_name', pattern=INTERNATIONALIZED_ALPHANUMERIC_AND_DELIMITER, ) lastName = fields.String( description='The last name of the user.', attribute='last_name', pattern=INTERNATIONALIZED_ALPHANUMERIC_AND_DELIMITER_OR_EMPTY, ) phoneNumber = fields.String( description='The phone number of the user.', attribute='phone_number') # Disabling this warning because Hyper-Schema is enforcing # that the value of this field MUST match one of the values # of the Enum. # pylint:disable=E1136 # pylint: disable=E1101 status = fields.Custom( fields.String(enum=USER_STATUSES), attribute='status_id', formatter=lambda status_id: UserStatusEnum(status_id).name.lower(), converter=lambda status_value: UserStatusEnum[status_value.upper()] .value, default=UserStatusEnum.ACTIVE.value, nullable=False, ) # The parameter is coming directly from the API which uses camelCase instead of # snake_case # pylint: disable=C0103 # pylint: disable=E1101 @ItemRoute.POST( '/password', title='Update User Password', description= 'Updates the user\'s password with the new value specified.', rel='updatePassword', schema=FieldSet({ 'newPassword': fields.String( min_length=10, max_length=255, description='The new password for the user.', ) }), ) def update_password(self, user, newPassword): # TODO(vedant) Refactor this into a separate module like # we do for the Groups API with AuthorizedOperation('change_password', 'user', user.id): hashed_password = current_app.user_manager.hash_password( newPassword) self.manager.update(user, {'password': hashed_password}) return None, NO_CONTENT @ItemRoute.POST( '/reset_password', title='Reset User Password', description= 'Resets the user\'s password and sends them a password reset email.', rel='resetPassword', schema=None, response_schema=None, ) def reset_password(self, user): with AuthorizedOperation('reset_password', 'user', user.id): # TODO(vedant) - Change the return value of `reset_password` to convey a value # with more resolution than True/False username = user.username # TODO(vedant) - This is for legacy users who signed up with usernames that do # not represent an e-mail address if not EMAIL_REGEX.match(username): message = 'User {username} does not have a valid e-mail address.'.format( username=username) return StandardResponse(message, BAD_REQUEST, False), BAD_REQUEST send_reset_password(username) message = ('User password has been reset and instructions ' 'e-mailed to {username}.'.format(username=username)) g.request_logger.info(message) return None, NO_CONTENT @ItemRoute.DELETE( '/force', rel='forceDestroy', title='Force Delete User', description= 'Force delete a User and ALL their created artifacts (Dashboards, etc.)', ) def force_delete(self, user): with AuthorizedOperation('delete_resource', 'user', user.id): before_delete.send(user) force_delete_user(user) after_delete.send(user) return None, NO_CONTENT @Route.POST( '/invite', title='Invite New User', description='', rel='inviteUser', schema=fields.List(INVITE_OBJECT_SCHEMA), response_schema=fields.List( fields.Inline('self'), description='A listing of all the invited users'), ) def invite_user(self, invitees): with AuthorizedOperation('invite_user', 'site', ROOT_SITE_RESOURCE_ID): invited_users = [invitee.email for invitee in invitees] try: pending_users = invite_users(invitees) emails = [user.username for user in pending_users] g.request_logger.info( 'Successfully invited the following users: \'%s\'.', emails) return pending_users except UserAlreadyInvited as e: g.request_logger.error( 'Attempt to invite the following users has failed: \'%s\'. ' 'The following users already have platform accounts: \'%s\'', invited_users, e.already_registered_users, ) raise # Flask-Potion requires that these be methods and NOT functions # pylint: disable=R0201 # pylint: disable=E1101 @ItemRoute.POST( '/roles', title='Add User Role', description='Adds a single role to a user.', schema=USER_ROLES_SCHEMA, response_schema=STANDARD_RESPONSE_SCHEMA, rel='addRole', ) def add_role_by_name(self, user, request): with AuthorizedOperation('edit_resource', 'user', user.id): # TODO(vedant) Refactor this into a separate module like # we do for the Groups API role_name = request['roleName'] resource_type = request['resourceType'] resource_name = request.get('resourceName') result = add_user_role_api(user, role_name, resource_type, resource_name, commit=True) success = False if isinstance(result, Success): success = True response_code = OK if success else BAD_REQUEST return ( StandardResponse(result['data']['message'], response_code, success), response_code, ) # Flask-Potion requires that these be methods and NOT functions # pylint: disable=R0201 @ItemRoute.DELETE( '/roles', title='Delete User Role', description='Deletes a single role from a user.', schema=USER_ROLES_SCHEMA, response_schema=STANDARD_RESPONSE_SCHEMA, rel='deleteRole', ) def delete_role_by_name(self, user, request): with AuthorizedOperation('edit_resource', 'user', user.id): # TODO(vedant) Refactor this into a separate module like # we do for the Groups API role_name = request['roleName'] resource_type = request['resourceType'] resource_name = request.get('resourceName') result = delete_user_role_api(user, role_name, resource_type, resource_name, commit=True) return StandardResponse(result['data']['message'], OK, True) @ItemRoute.PATCH( '/roles', title='Update User Roles', description= 'Updates all the roles for a user with the values specified.', schema=ROLE_MAP_SCHEMA, response_schema=fields.Inline('self'), rel='updateRoles', ) def update_roles(self, user, request): with AuthorizedOperation('edit_resource', 'user', user.id): roles = update_user_roles_from_map(user, request) message = ( 'Successfully updated the roles attached to user \'%s\'. ' 'New roles are now: \'%s\'' % (user.username, [user_role_as_dictionary(role) for role in roles])) g.request_logger.info(message) return self.manager.read(user.id) @Route.GET( '/default/roles', title='Get Default User Role(s)', description= 'Gets all role(s) that are possessed by all registered users and optionally, ' 'unregistered users.', rel='getDefaultRoles', ) @authorization_required('view_resource', 'user', is_api_request=True) def list_default_roles(self): return StandardResponse( 'Successfully retrieved a listing of all public roles. ', OK, True, roles=role_list_as_map(list_default_roles()), ) @Route.POST( '/default/roles', title='Add Default User Role', description= 'Adds a single role to a user that will apply to all registered users and ' 'optionally, unregistered users.', schema=DEFAULT_ROLES_SCHEMA, rel='addDefaultRole', ) @authorization_required('edit_resource', 'user', is_api_request=True) def add_default_role_by_name(self, request): role_name = request['roleName'] resource_type = request['resourceType'] resource_name = request.get('resourceName') (_, exists) = add_default_role(role_name, resource_type, resource_name) action = 'has been added' if exists else 'already exists' message = 'Role \'%s\' %s for %s' % ( role_name, action, get_resource_string(resource_name, resource_type), ) g.request_logger.info(message) response_code = OK if exists else CREATED return (StandardResponse(message, response_code, True), response_code) @Route.DELETE( '/default/roles', title='Delete Default User Role', description= 'Deletes a single role from a user that will apply to both anonymous ' '(unregistered) as well as registered users.', schema=DEFAULT_ROLES_SCHEMA, response_schema=STANDARD_RESPONSE_SCHEMA, rel='deletePublicRole', ) @authorization_required('delete_resource', 'user', is_api_request=True) def delete_default_role_by_name(self, request): role_name = request['roleName'] resource_type = request['resourceType'] resource_name = request.get('resourceName') (_, exists) = delete_default_role(role_name, resource_type, resource_name) action = 'has been deleted' if exists else 'does not exist' message = 'Role \'%s\' %s for %s' % ( role_name, action, get_resource_string(resource_name, resource_type), ) g.request_logger.info(message) return StandardResponse(message, OK, True)
from flask_potion import fields from flask_potion.routes import ItemRoute, Route from flask_potion.schema import FieldSet from web.server.routes.views.user_query_session import store_query_and_generate_link from web.server.api.api_models import PrincipalResource from models.alchemy.user_query_session.model import UserQuerySession GET_BY_QUERY_UUID_RESPONSE_SCHEMA = FieldSet({ 'queryBlob': fields.Any(), 'queryUuid': fields.String(), 'username': fields.String(), }) class UserQuerySessionResource(PrincipalResource): '''Potion class for interacting with saved queries. ''' class Meta: model = UserQuerySession filters = {'queryUuid': True} id_attribute = 'query_uuid' id_field_class = fields.String() permissions = {'read': 'view_resource'} class Schema: queryUuid = fields.String(attribute='query_uuid', nullable=True) userId = fields.Integer(attribute='user_id') queryBlob = fields.Any(attribute='query_blob')