From 2ce0cf927e7642b43cb927a975214be7cd5b45da Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 10 Mar 2022 17:01:10 +0100 Subject: [PATCH] tox: add code-style target --- .pre-commit-config.yaml | 18 +++ debian/debian_config.py | 2 +- debian/settings.py | 6 +- petale/admin.py | 4 + petale/api_views.py | 91 +++++------ petale/authentication.py | 27 ++-- petale/migrations/0001_initial.py | 92 +++++++++-- petale/migrations/0002_auto_20170329_1823.py | 30 ++-- .../0003_rename_model_and_fields.py | 3 - petale/migrations/0004_auto_20170330_0046.py | 25 ++- petale/migrations/0005_auto_20170721_1416.py | 16 +- petale/migrations/0006_auto_20171017_1625.py | 3 - petale/migrations/0007_auto_20180110_1126.py | 11 +- petale/models.py | 151 ++++++------------ petale/permissions.py | 7 +- petale/settings.py | 21 ++- petale/urls.py | 16 +- petale/utils.py | 20 +-- setup.py | 23 ++- tests/conftest.py | 31 ++-- tests/test_admin.py | 14 +- tests/test_api.py | 128 ++++++++------- tests/utils.py | 21 +-- tox.ini | 9 ++ 24 files changed, 414 insertions(+), 355 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a21e161 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black + args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110'] +- repo: https://github.com/PyCQA/isort + rev: 5.7.0 + hooks: + - id: isort + args: ['--profile', 'black', '--line-length', '110'] +- repo: https://github.com/asottile/pyupgrade + rev: v2.20.0 + hooks: + - id: pyupgrade + args: ['--keep-percent-format', '--py37-plus'] diff --git a/debian/debian_config.py b/debian/debian_config.py index ca86917..115994c 100644 --- a/debian/debian_config.py +++ b/debian/debian_config.py @@ -1,7 +1,7 @@ # This file is sourced by "execfile" from petale.settings -import os import glob +import os from django.core.exceptions import ImproperlyConfigured diff --git a/debian/settings.py b/debian/settings.py index 25c4d9f..da6396d 100644 --- a/debian/settings.py +++ b/debian/settings.py @@ -15,15 +15,15 @@ DEBUG = False TEMPLATE_DEBUG = False -#ADMINS = ( +# ADMINS = ( # # ('User 1', 'watchdog@example.net'), # # ('User 2', 'janitor@example.net'), -#) +# ) # ALLOWED_HOSTS must be correct in production! # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = [ - '*', + '*', ] # Databases diff --git a/petale/admin.py b/petale/admin.py index e08efa7..a1d94dd 100644 --- a/petale/admin.py +++ b/petale/admin.py @@ -24,6 +24,7 @@ class PartnerAdmin(admin.ModelAdmin): list_display = ['name', 'admin_emails'] readonly_fields = ['size'] + admin.site.register(models.Partner, PartnerAdmin) @@ -31,6 +32,7 @@ class CUTAdmin(admin.ModelAdmin): search_fields = ['uuid'] list_display = ['uuid'] + admin.site.register(models.CUT, CUTAdmin) @@ -40,6 +42,7 @@ class PetalAdmin(admin.ModelAdmin): readonly_fields = ['etag', 'size'] raw_id_fields = ['cut'] + admin.site.register(models.Petal, PetalAdmin) @@ -47,4 +50,5 @@ class AccessControlListAdmin(admin.ModelAdmin): search_fields = ['partner__name', 'user__username'] list_display = ['partner', 'user', 'order', 'methods', 'key'] + admin.site.register(models.AccessControlList, AccessControlListAdmin) diff --git a/petale/api_views.py b/petale/api_views.py index 82c9244..e5e21c3 100644 --- a/petale/api_views.py +++ b/petale/api_views.py @@ -14,36 +14,41 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from __future__ import unicode_literals import logging + try: from functools import reduce except ImportError: pass import requests + try: from time import process_time except ImportError: from time import clock as process_time -from django.utils.six.moves.urllib import parse as urlparse - -from django.db.models.query import Q, F -from django.http import StreamingHttpResponse, HttpResponse -from django.conf import settings -from django.db.transaction import atomic - -from rest_framework import status -from rest_framework.views import APIView -from rest_framework.response import Response - from atomicwrites import atomic_write +from django.conf import settings +from django.db.models.query import F, Q +from django.db.transaction import atomic +from django.http import HttpResponse, StreamingHttpResponse +from django.utils.six.moves.urllib import parse as urlparse +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView -from .models import CUT, Petal, Partner, AccessControlList -from .utils import logit, StreamingHash -from .exceptions import (PartnerNotFound, CutNotFound, KeyNotFound, NotFound, MissingContentType, - ConcurrentAccess, PreconditionException) +from .exceptions import ( + ConcurrentAccess, + CutNotFound, + KeyNotFound, + MissingContentType, + NotFound, + PartnerNotFound, + PreconditionException, +) +from .models import CUT, AccessControlList, Partner, Petal +from .utils import StreamingHash, logit def cut_exists(request, cut_uuid): @@ -55,25 +60,26 @@ def cut_exists(request, cut_uuid): authentic_url = getattr(settings, 'PETALE_AUTHENTIC_URL', None) if not authentic_url: - logger.warning(u'PETALE_AUTHENTIC SETTINGS improperly defined') + logger.warning('PETALE_AUTHENTIC SETTINGS improperly defined') return False url = urlparse.urljoin(authentic_url, 'api/users/synchronization/') try: - response = requests.post(url, json={"known_uuids": [cut_uuid]}, - auth=request.user.credentials, verify=False) + response = requests.post( + url, json={"known_uuids": [cut_uuid]}, auth=request.user.credentials, verify=False + ) response.raise_for_status() except requests.RequestException as e: - logger.warning(u'authentic synchro API failed: %s', e) + logger.warning('authentic synchro API failed: %s', e) return False try: data = response.json() except ValueError as e: - logger.warning(u'authentic synchro API failed to decode response: %s', e) + logger.warning('authentic synchro API failed to decode response: %s', e) return False if data.get("unknown_uuids"): - logger.warning(u'unknown uuid : %s %s', request.user.credentials[0], data) + logger.warning('unknown uuid : %s %s', request.user.credentials[0], data) return False CUT.objects.get_or_create(uuid=cut_uuid) @@ -122,9 +128,7 @@ class PetalAPIKeysView(APIView): if key_filter: qs = qs.filter(key_filter) - return Response({ - 'keys': [petal.name for petal in qs] - }) + return Response({'keys': [petal.name for petal in qs]}) @logit def delete(self, request, partner_name, cut_uuid): @@ -146,11 +150,7 @@ class PetalAPIView(APIView): if_none_match = if_none_match and [x.strip() for x in if_none_match.split(',')] try: - qs = Petal.objects.filter( - name=petal_name, - partner_id__name=partner_name, - cut_id__uuid=cut_uuid - ) + qs = Petal.objects.filter(name=petal_name, partner_id__name=partner_name, cut_id__uuid=cut_uuid) qs = qs.select_related('partner', 'cut') petal = qs.get() except Petal.DoesNotExist: @@ -193,18 +193,13 @@ class PetalAPIView(APIView): petal = self.get_petal(partner_name, cut_uuid, petal_name) if if_none_match: if if_none_match == ['*'] or petal.etag in if_none_match: - return Response( - status=status.HTTP_304_NOT_MODIFIED, - headers={ - 'ETag': petal.etag - } - ) + return Response(status=status.HTTP_304_NOT_MODIFIED, headers={'ETag': petal.etag}) # verify file exists before creating a StreamingHttpResponse # as StreamingHttpResponse generate its content after the Django global try/catch try: petal.data.open(mode='rb') response = HttpResponse(petal.data.read(), content_type=petal.content_type) - except IOError: + except OSError: continue finally: petal.data.close() @@ -227,9 +222,9 @@ class PetalAPIView(APIView): raise MissingContentType try: - petal = self.get_petal(partner_name, cut_uuid, petal_name, - if_match=if_match, - if_none_match=if_none_match) + petal = self.get_petal( + partner_name, cut_uuid, petal_name, if_match=if_match, if_none_match=if_none_match + ) created = False except PreconditionException: raise ConcurrentAccess @@ -244,8 +239,8 @@ class PetalAPIView(APIView): raise CutNotFound petal, created = Petal.objects.get_or_create( - name=petal_name, partner=partner, cut=cut, - defaults={'size': 0}) + name=petal_name, partner=partner, cut=cut, defaults={'size': 0} + ) if not created and if_none_match: raise ConcurrentAccess else: @@ -283,12 +278,7 @@ class PetalAPIView(APIView): fd.write(block) update_meta() - return Response( - {}, - status=status_code, - headers={ - 'ETag': petal.etag - }) + return Response({}, status=status_code, headers={'ETag': petal.etag}) @logit def delete(self, request, partner_name, cut_uuid, petal_name): @@ -297,9 +287,8 @@ class PetalAPIView(APIView): try: petal = self.get_petal( - partner_name, cut_uuid, petal_name, - if_match=if_match, - if_none_match=if_none_match) + partner_name, cut_uuid, petal_name, if_match=if_match, if_none_match=if_none_match + ) except PreconditionException: raise ConcurrentAccess petal.delete() diff --git a/petale/authentication.py b/petale/authentication.py index ab7d42f..01a09b2 100644 --- a/petale/authentication.py +++ b/petale/authentication.py @@ -17,52 +17,48 @@ import logging import requests - -from django.utils.six.moves.urllib import parse as urlparse - from django.conf import settings from django.contrib.auth.models import User +from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ - from rest_framework.authentication import BasicAuthentication from rest_framework.exceptions import AuthenticationFailed class PetalAuthentication(BasicAuthentication): - def authentic_proxy(self, userid, password): '''Check userid and password with configured Authentic IdP, and verify it is an OIDC - client. + client. ''' logger = logging.getLogger(__name__) authentic_url = getattr(settings, 'PETALE_AUTHENTIC_URL', None) if not authentic_url: - logger.warning(u'authentic check-password not configured') + logger.warning('authentic check-password not configured') return False, '' authentic_auth = getattr(settings, 'PETALE_AUTHENTIC_AUTH', None) if not authentic_auth: - logger.warning(u'authentic check-password not configured') + logger.warning('authentic check-password not configured') return False, '' url = urlparse.urljoin(authentic_url, 'api/check-password/') try: - response = requests.post(url, json={ - 'username': userid, - 'password': password}, auth=authentic_auth, verify=False) + response = requests.post( + url, json={'username': userid, 'password': password}, auth=authentic_auth, verify=False + ) response.raise_for_status() except requests.RequestException as e: - logger.warning(u'authentic check-password API failed: %s', e) + logger.warning('authentic check-password API failed: %s', e) return False, 'authentic is down' try: response = response.json() except ValueError as e: - logger.warning(u'authentic check-password API failed: %s, %r', e, response.content) + logger.warning('authentic check-password API failed: %s, %r', e, response.content) return False, 'authentic is down' if response.get('result') == 0: - logger.warning(u'authentic check-password API failed') + logger.warning('authentic check-password API failed') return False, response.get('errors', [''])[0] return True, None @@ -70,8 +66,7 @@ class PetalAuthentication(BasicAuthentication): def authenticate_credentials(self, userid, password, request=None): username = userid[:30] try: - user, auth = super(PetalAuthentication, self).authenticate_credentials(username, - password) + user, auth = super().authenticate_credentials(username, password) except AuthenticationFailed: success, error = self.authentic_proxy(userid, password) if not success: diff --git a/petale/migrations/0001_initial.py b/petale/migrations/0001_initial.py index 125cda1..6f6431a 100644 --- a/petale/migrations/0001_initial.py +++ b/petale/migrations/0001_initial.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models import django.core.validators -import petale.models from django.conf import settings +from django.db import migrations, models + +import petale.models class Migration(migrations.Migration): @@ -17,25 +15,73 @@ class Migration(migrations.Migration): migrations.CreateModel( name='AccessControlList', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('order', models.IntegerField(verbose_name='Order')), - ('methods', models.CharField(default='GET,PUT,DELETE', help_text='GET, PUT, DELETE', max_length=128, verbose_name='Allowed methods')), - ('key', models.CharField(default='*', max_length=128, verbose_name='Allowed keys', validators=[])), + ( + 'methods', + models.CharField( + default='GET,PUT,DELETE', + help_text='GET, PUT, DELETE', + max_length=128, + verbose_name='Allowed methods', + ), + ), + ( + 'key', + models.CharField(default='*', max_length=128, verbose_name='Allowed keys', validators=[]), + ), ], ), migrations.CreateModel( name='CUTIdentifier', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('uuid', models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$', message='Invalid uuid format')])), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), + ( + 'uuid', + models.CharField( + max_length=32, + validators=[ + django.core.validators.RegexValidator( + '^[A-Za-z0-9-_]+$', message='Invalid uuid format' + ) + ], + ), + ), ], ), migrations.CreateModel( name='Partner', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=64, verbose_name='Partner', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$', message='Invalid name format')])), - ('admin_emails', models.CharField(help_text='List of admin emails separated by comma', max_length=256, verbose_name='Admin emails')), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), + ( + 'name', + models.CharField( + max_length=64, + verbose_name='Partner', + validators=[ + django.core.validators.RegexValidator( + '^[A-Za-z0-9-_]+$', message='Invalid name format' + ) + ], + ), + ), + ( + 'admin_emails', + models.CharField( + help_text='List of admin emails separated by comma', + max_length=256, + verbose_name='Admin emails', + ), + ), ('hard_global_max_size', models.IntegerField(verbose_name='Hard max size')), ('soft_global_max_size', models.IntegerField(verbose_name='Soft max size')), ('hard_per_key_max_size', models.IntegerField(verbose_name='Hard max size per key')), @@ -45,10 +91,24 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Petal', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ( + 'id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), + ), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=128, verbose_name='Name', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$', message='Invalid name format')])), + ( + 'name', + models.CharField( + max_length=128, + verbose_name='Name', + validators=[ + django.core.validators.RegexValidator( + '^[A-Za-z0-9-_]+$', message='Invalid name format' + ) + ], + ), + ), ('etag', models.CharField(max_length=256, verbose_name='ETag', blank=True)), ('data', models.FileField(upload_to='data', verbose_name='Data Content', blank=True)), ('content_type', models.CharField(max_length=128, verbose_name='Content type', blank=True)), @@ -69,6 +129,6 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='petal', - unique_together=set([('name', 'partner_id', 'cut_id')]), + unique_together={('name', 'partner_id', 'cut_id')}, ), ] diff --git a/petale/migrations/0002_auto_20170329_1823.py b/petale/migrations/0002_auto_20170329_1823.py index 281404f..39b4bda 100644 --- a/petale/migrations/0002_auto_20170329_1823.py +++ b/petale/migrations/0002_auto_20170329_1823.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import petale.models -from django.conf import settings import django.core.validators +from django.conf import settings +from django.db import migrations, models + +import petale.models class Migration(migrations.Migration): @@ -32,17 +30,25 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='accesscontrollist', name='user', - field=models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + field=models.ForeignKey( + verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), ), migrations.AlterField( model_name='cutidentifier', name='uuid', - field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]), + field=models.CharField( + max_length=32, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')] + ), ), migrations.AlterField( model_name='partner', name='name', - field=models.CharField(max_length=64, verbose_name='Partner', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]), + field=models.CharField( + max_length=64, + verbose_name='Partner', + validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')], + ), ), migrations.AlterField( model_name='petal', @@ -67,7 +73,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='petal', name='name', - field=models.CharField(max_length=128, verbose_name='Name', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]), + field=models.CharField( + max_length=128, + verbose_name='Name', + validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')], + ), ), migrations.AlterField( model_name='petal', diff --git a/petale/migrations/0003_rename_model_and_fields.py b/petale/migrations/0003_rename_model_and_fields.py index 91bb490..99ef469 100644 --- a/petale/migrations/0003_rename_model_and_fields.py +++ b/petale/migrations/0003_rename_model_and_fields.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations diff --git a/petale/migrations/0004_auto_20170330_0046.py b/petale/migrations/0004_auto_20170330_0046.py index cdf4993..838d1f5 100644 --- a/petale/migrations/0004_auto_20170330_0046.py +++ b/petale/migrations/0004_auto_20170330_0046.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models import django.core.validators +from django.db import migrations, models class Migration(migrations.Migration): @@ -14,7 +11,11 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='accesscontrollist', - options={'ordering': ['partner__name', 'user__username', 'order', 'key', 'methods'], 'verbose_name': 'Access control list', 'verbose_name_plural': 'Access control lists'}, + options={ + 'ordering': ['partner__name', 'user__username', 'order', 'key', 'methods'], + 'verbose_name': 'Access control list', + 'verbose_name_plural': 'Access control lists', + }, ), migrations.AlterModelOptions( name='cut', @@ -36,7 +37,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='partner', name='admin_emails', - field=models.CharField(help_text='List of admin emails separated by comma', max_length=256, verbose_name='Admin emails', blank=True), + field=models.CharField( + help_text='List of admin emails separated by comma', + max_length=256, + verbose_name='Admin emails', + blank=True, + ), ), migrations.AlterField( model_name='partner', @@ -51,7 +57,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='partner', name='name', - field=models.CharField(unique=True, max_length=64, verbose_name='Partner', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]), + field=models.CharField( + unique=True, + max_length=64, + verbose_name='Partner', + validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')], + ), ), migrations.AlterField( model_name='partner', diff --git a/petale/migrations/0005_auto_20170721_1416.py b/petale/migrations/0005_auto_20170721_1416.py index 8d2e582..57d8df7 100644 --- a/petale/migrations/0005_auto_20170721_1416.py +++ b/petale/migrations/0005_auto_20170721_1416.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import petale.models import django.core.validators +from django.db import migrations, models + +import petale.models class Migration(migrations.Migration): @@ -16,11 +14,15 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='cut', name='uuid', - field=models.CharField(max_length=255, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]), + field=models.CharField( + max_length=255, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')] + ), ), migrations.AlterField( model_name='petal', name='data', - field=models.FileField(upload_to=petale.models.petal_directory, max_length=512, verbose_name='Data Content'), + field=models.FileField( + upload_to=petale.models.petal_directory, max_length=512, verbose_name='Data Content' + ), ), ] diff --git a/petale/migrations/0006_auto_20171017_1625.py b/petale/migrations/0006_auto_20171017_1625.py index abb8e73..9182cb6 100644 --- a/petale/migrations/0006_auto_20171017_1625.py +++ b/petale/migrations/0006_auto_20171017_1625.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/petale/migrations/0007_auto_20180110_1126.py b/petale/migrations/0007_auto_20180110_1126.py index f85fcff..aa440f9 100644 --- a/petale/migrations/0007_auto_20180110_1126.py +++ b/petale/migrations/0007_auto_20180110_1126.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models import django.core.validators +from django.db import migrations, models class Migration(migrations.Migration): @@ -15,6 +12,10 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='cut', name='uuid', - field=models.CharField(unique=True, max_length=255, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]), + field=models.CharField( + unique=True, + max_length=255, + validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')], + ), ), ] diff --git a/petale/models.py b/petale/models.py index 6c4af68..0266b73 100644 --- a/petale/models.py +++ b/petale/models.py @@ -14,53 +14,39 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from __future__ import unicode_literals import hashlib -from django.utils import six -from django.db import models from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy as _ -from django.core.validators import RegexValidator from django.core.mail import send_mail - -from .exceptions import GlobalSpaceExhausted, PetalSizeExhausted +from django.core.validators import RegexValidator +from django.db import models +from django.utils import six +from django.utils.translation import ugettext_lazy as _ from . import utils - +from .exceptions import GlobalSpaceExhausted, PetalSizeExhausted id_validator = RegexValidator('^[A-Za-z0-9-_]+$') -@six.python_2_unicode_compatible class Partner(models.Model): - name = models.CharField( - verbose_name=_('Partner'), - max_length=64, - unique=True, - validators=[id_validator]) + name = models.CharField(verbose_name=_('Partner'), max_length=64, unique=True, validators=[id_validator]) admin_emails = models.CharField( verbose_name=_('Admin emails'), max_length=256, blank=True, - help_text=_('List of admin emails separated by comma')) - hard_global_max_size = models.IntegerField( - verbose_name=_('Hard max size'), - help_text=_('as kilobytes')) - soft_global_max_size = models.IntegerField( - verbose_name=_('Soft max size'), - help_text=_('as kilobytes')) + help_text=_('List of admin emails separated by comma'), + ) + hard_global_max_size = models.IntegerField(verbose_name=_('Hard max size'), help_text=_('as kilobytes')) + soft_global_max_size = models.IntegerField(verbose_name=_('Soft max size'), help_text=_('as kilobytes')) hard_per_key_max_size = models.IntegerField( - verbose_name=_('Hard max size per key'), - help_text=_('as kilobytes')) + verbose_name=_('Hard max size per key'), help_text=_('as kilobytes') + ) soft_per_key_max_size = models.IntegerField( - verbose_name=_('Soft max size per key'), - help_text=_('as kilobytes')) - size = models.BigIntegerField( - verbose_name=_('Size'), - default=0, - help_text=_('as bytes')) + verbose_name=_('Soft max size per key'), help_text=_('as kilobytes') + ) + size = models.BigIntegerField(verbose_name=_('Size'), default=0, help_text=_('as bytes')) def __str__(self): return self.name @@ -71,15 +57,15 @@ class Partner(models.Model): if new_size > self.hard_global_max_size * 1024: raise GlobalSpaceExhausted - if (self.size < self.soft_global_max_size * 1024 - and new_size > self.soft_global_max_size * 1024): + if self.size < self.soft_global_max_size * 1024 and new_size > self.soft_global_max_size * 1024: self.notify_admins( subject=_('Partner %s space almost exhausted') % self.name, # pylint: disable=no-member body=_('Current size: {current_size}, Max size: {max_size}').format( - current_size=new_size, - max_size=self.hard_global_max_size * 1024), - **kwargs) + current_size=new_size, max_size=self.hard_global_max_size * 1024 + ), + **kwargs, + ) def notify_admins(self, subject, body, **kwargs): if kwargs: @@ -94,12 +80,8 @@ class Partner(models.Model): ordering = ['name'] -@six.python_2_unicode_compatible class CUT(models.Model): - uuid = models.CharField( - max_length=255, - validators=[id_validator], - unique=True) + uuid = models.CharField(max_length=255, validators=[id_validator], unique=True) def __str__(self): return self.uuid @@ -116,44 +98,27 @@ def petal_directory(instance, filename): assert instance.cut assert instance.cut.uuid - return 'data/{0}/{1}/{2}/{3}'.format( + return 'data/{}/{}/{}/{}'.format( instance.partner.name, hashlib.md5(instance.cut.uuid.encode('ascii')).hexdigest()[:3], instance.cut.uuid, - instance.name) + instance.name, + ) -@six.python_2_unicode_compatible class Petal(models.Model): - created_at = models.DateTimeField( - _('Created'), - auto_now_add=True) - updated_at = models.DateTimeField( - _('Updated'), - auto_now=True) - name = models.CharField( - _('Name'), - max_length=128, - validators=[id_validator]) - etag = models.CharField( - _('ETag'), - max_length=256) - data = models.FileField( - _('Data Content'), - max_length=512, - upload_to=petal_directory) - content_type = models.CharField( - _('Content type'), - max_length=128) - size = models.IntegerField( - _('Size'), - default=0, - help_text=_('as bytes')) + created_at = models.DateTimeField(_('Created'), auto_now_add=True) + updated_at = models.DateTimeField(_('Updated'), auto_now=True) + name = models.CharField(_('Name'), max_length=128, validators=[id_validator]) + etag = models.CharField(_('ETag'), max_length=256) + data = models.FileField(_('Data Content'), max_length=512, upload_to=petal_directory) + content_type = models.CharField(_('Content type'), max_length=128) + size = models.IntegerField(_('Size'), default=0, help_text=_('as bytes')) cut = models.ForeignKey(CUT, on_delete=models.CASCADE) partner = models.ForeignKey(Partner, on_delete=models.CASCADE) def __str__(self): - return u'%s/%s/%s' % (self.partner.name, self.cut.uuid, self.name) + return '%s/%s/%s' % (self.partner.name, self.cut.uuid, self.name) def clean(self): if self.data: @@ -164,60 +129,46 @@ class Petal(models.Model): '''Delegate global limits check to partner, and check per key size limits''' size_delta = content_length - self.size - self.partner.check_limits(size_delta, - partner=self.partner.name, - cut=self.cut.uuid, - key=self.name) + self.partner.check_limits(size_delta, partner=self.partner.name, cut=self.cut.uuid, key=self.name) if content_length > self.partner.hard_per_key_max_size * 1024: raise PetalSizeExhausted - if (self.size <= self.partner.soft_per_key_max_size * 1024 - and content_length > self.partner.soft_per_key_max_size * 1024): + if ( + self.size <= self.partner.soft_per_key_max_size * 1024 + and content_length > self.partner.soft_per_key_max_size * 1024 + ): self.partner.notify_admins( # pylint: disable=no-member _('Key {key} space of partner {partner} almost exhausted').format( - key=self.name, - partner=self.partner.name), + key=self.name, partner=self.partner.name + ), # pylint: disable=no-member _('Current size: {current_size}, Max size: {max_size}').format( - current_size=content_length, - max_size=self.partner.hard_per_key_max_size * 1024), + current_size=content_length, max_size=self.partner.hard_per_key_max_size * 1024 + ), partner=self.partner.name, cut=self.cut.uuid, - key=self.name) + key=self.name, + ) class Meta: - unique_together = (('name', 'partner', 'cut')) + unique_together = ('name', 'partner', 'cut') verbose_name = _('Petal') verbose_name_plural = _('Petals') -@six.python_2_unicode_compatible class AccessControlList(models.Model): - order = models.IntegerField( - _('Order')) - partner = models.ForeignKey( - Partner, - verbose_name=_('Partner'), - on_delete=models.CASCADE) - user = models.ForeignKey( - User, - verbose_name=_('User'), - on_delete=models.CASCADE) + order = models.IntegerField(_('Order')) + partner = models.ForeignKey(Partner, verbose_name=_('Partner'), on_delete=models.CASCADE) + user = models.ForeignKey(User, verbose_name=_('User'), on_delete=models.CASCADE) methods = models.CharField( - _('Allowed methods'), - max_length=128, - default='GET,PUT,DELETE', - help_text=("GET, PUT, DELETE")) - key = models.CharField( - _('Allowed keys'), - max_length=128, - default='*') + _('Allowed methods'), max_length=128, default='GET,PUT,DELETE', help_text=("GET, PUT, DELETE") + ) + key = models.CharField(_('Allowed keys'), max_length=128, default='*') def __str__(self): - return u'%s %s %s %s' % ( - self.partner.name, self.user.username, self.methods, self.key) + return '%s %s %s %s' % (self.partner.name, self.user.username, self.methods, self.key) class Meta: verbose_name = _('Access control list') diff --git a/petale/permissions.py b/petale/permissions.py index 4e59e61..1ff6628 100644 --- a/petale/permissions.py +++ b/petale/permissions.py @@ -16,8 +16,8 @@ from rest_framework.permissions import BasePermission -from .models import AccessControlList from .exceptions import AccessForbidden +from .models import AccessControlList class PetaleAccessPermission(BasePermission): @@ -26,9 +26,8 @@ class PetaleAccessPermission(BasePermission): petal_name = view.kwargs.get('petal_name') qs = AccessControlList.objects.filter( - partner__name=partner_name, - user=request.user, - methods__contains=request.method) + partner__name=partner_name, user=request.user, methods__contains=request.method + ) if not qs.exists(): raise AccessForbidden diff --git a/petale/settings.py b/petale/settings.py index a0e6875..33a5108 100644 --- a/petale/settings.py +++ b/petale/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os + BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -37,7 +38,7 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', - 'petale' + 'petale', ) MIDDLEWARE = [ @@ -82,8 +83,7 @@ USE_TZ = True TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - ], + 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -108,13 +108,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static') REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'petale.authentication.PetalAuthentication', - ), + 'DEFAULT_AUTHENTICATION_CLASSES': ('petale.authentication.PetalAuthentication',), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', 'petale.permissions.PetaleAccessPermission', - ) + ), } @@ -147,14 +145,13 @@ LOGGING = { 'handlers': ['syslog'], 'level': 'DEBUG', 'propagate': True, - } - } + }, + }, } local_settings_file = os.environ.get( - 'PETALE_SETTINGS_FILE', - os.path.join(os.path.dirname(__file__), 'local_settings.py')) + 'PETALE_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py') +) if os.path.exists(local_settings_file): with open(local_settings_file) as fd: exec(fd.read()) - diff --git a/petale/urls.py b/petale/urls.py index 26af7b3..ba29b50 100644 --- a/petale/urls.py +++ b/petale/urls.py @@ -17,14 +17,18 @@ from django.conf.urls import include, url from django.contrib import admin -from .api_views import PetalAPIView, PetalAPIKeysView - +from .api_views import PetalAPIKeysView, PetalAPIView urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^api/(?P[\w,-]+)/(?P[\w,-]{,255})/$', + url( + r'^api/(?P[\w,-]+)/(?P[\w,-]{,255})/$', PetalAPIKeysView.as_view(), - name='api-keys'), - url(r'^api/(?P[\w,-]+)/(?P[\w,-]{,255})/(?P[\w,-]+)/$', - PetalAPIView.as_view(), name='api') + name='api-keys', + ), + url( + r'^api/(?P[\w,-]+)/(?P[\w,-]{,255})/(?P[\w,-]+)/$', + PetalAPIView.as_view(), + name='api', + ), ] diff --git a/petale/utils.py b/petale/utils.py index d80a75e..bd2bff4 100644 --- a/petale/utils.py +++ b/petale/utils.py @@ -18,7 +18,6 @@ import hashlib import logging from functools import wraps - DEFAULT_HASH_ALGO = 'sha1' @@ -28,19 +27,20 @@ def logit(func): logger = logging.getLogger('petale') req_url = '%s %s %s' % (request.method, request.path, request.GET.urlencode()) logger.info(req_url, extra={'request_url': req_url}) - req_headers = ''.join( - ['%s: %s | ' % (k, v) for k, v in request.META.items() if k.isupper()]) + req_headers = ''.join(['%s: %s | ' % (k, v) for k, v in request.META.items() if k.isupper()]) logger.debug('Request Headers: %s', req_headers, extra={'request_headers': req_headers}) response = func(self, request, *args, **kwargs) resp_headers = ''.join(['%s: %s | ' % (k, v) for k, v in response.items()]) - logger.debug('Response Headers: %s', resp_headers, - extra={'response_headers': resp_headers}) + logger.debug('Response Headers: %s', resp_headers, extra={'response_headers': resp_headers}) if hasattr(response, 'data'): - logger.debug('Response Data: %r', response.data, - extra={'response_body': response.data}) - logger.debug('Response Status Code: %s', response.status_code, - extra={'response_status_code': response.status_code}) + logger.debug('Response Data: %r', response.data, extra={'response_body': response.data}) + logger.debug( + 'Response Status Code: %s', + response.status_code, + extra={'response_status_code': response.status_code}, + ) return response + return wrapper @@ -57,7 +57,7 @@ def etag(stream): return '"%s:%s"' % (DEFAULT_HASH_ALGO, digest.hexdigest()) -class StreamingHash(object): +class StreamingHash: def __init__(self, readable, hash_algo=DEFAULT_HASH_ALGO): self.readable = readable self.hash_algo = hash_algo diff --git a/setup.py b/setup.py index ac7a9fc..3f9bb2c 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,14 @@ #! /usr/bin/env python -# -*- coding: utf-8 -*- import os import subprocess import sys - -from setuptools.command.install_lib import install_lib as _install_lib +from distutils.cmd import Command from distutils.command.build import build as _build from distutils.command.sdist import sdist -from distutils.cmd import Command -from setuptools import setup, find_packages + +from setuptools import find_packages, setup +from setuptools.command.install_lib import install_lib as _install_lib class eo_sdist(sdist): @@ -29,15 +28,17 @@ class eo_sdist(sdist): def get_version(): '''Use the VERSION, if absent generates a version with git describe, if not - tag exists, take 0.0- and add the length of the commit log. + tag exists, take 0.0- and add the length of the commit log. ''' if os.path.exists('VERSION'): - with open('VERSION', 'r') as v: + with open('VERSION') as v: return v.read() if os.path.exists('.git'): p = subprocess.Popen( ['git', 'describe', '--dirty=.dirty', '--match=v*'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) result = p.communicate()[0] if p.returncode == 0: result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v @@ -48,9 +49,7 @@ def get_version(): version = result return version else: - return '0.0.post%s' % len( - subprocess.check_output( - ['git', 'rev-list', 'HEAD']).splitlines()) + return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines()) return '0.0' @@ -76,6 +75,7 @@ class compile_translations(Command): def run(self): try: from django.core.management import call_command + for path, dirs, files in os.walk('petale'): if 'locale' not in dirs: continue @@ -94,7 +94,6 @@ class build(_build): class install_lib(_install_lib): - def run(self): self.run_command('compile_translations') _install_lib.run(self) diff --git a/tests/conftest.py b/tests/conftest.py index eb53fef..ac0bcaf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,17 @@ -from __future__ import unicode_literals - -from tempfile import mkdtemp import shutil +from tempfile import mkdtemp -import pytest import django_webtest - -from utils import (create_partner, create_petal, create_service, create_cut, - get_service, create_acl_record, get_tests_file_content) +import pytest +from utils import ( + create_acl_record, + create_cut, + create_partner, + create_petal, + create_service, + get_service, + get_tests_file_content, +) @pytest.fixture @@ -19,6 +23,7 @@ def app(request, settings): def fin(): shutil.rmtree(settings.MEDIA_ROOT) + request.addfinalizer(fin) return django_webtest.DjangoTestApp() @@ -65,12 +70,18 @@ def acl(db, partner_southpark, partner_gotham): create_acl_record(1, partner_southpark, get_service('family'), 'invoice', methods='GET,HEAD,PUT'), create_acl_record(2, partner_southpark, get_service('library'), 'books', methods='GET,HEAD'), create_acl_record(3, partner_gotham, get_service('arkham'), 'taxes*', methods='GET,HEAD,PUT,DELETE'), - create_acl_record(4, partner_southpark, get_service('library'), 'loans', methods='GET,HEAD,PUT,DELETE'), + create_acl_record( + 4, partner_southpark, get_service('library'), 'loans', methods='GET,HEAD,PUT,DELETE' + ), create_acl_record(5, partner_southpark, get_service('library'), 'favourite*', methods='GET,PUT'), - create_acl_record(6, partner_southpark, get_service('cityhall'), 'favourite*', methods='GET,PUT,DELETE'), + create_acl_record( + 6, partner_southpark, get_service('cityhall'), 'favourite*', methods='GET,PUT,DELETE' + ), create_acl_record(3, partner_southpark, get_service('library'), 'profile', methods='GET,PUT,DELETE'), create_acl_record(1, partner_southpark, get_service('arkham'), 'ma*', methods='GET,HEAD,PUT,DELETE'), - create_acl_record(4, partner_southpark, get_service(('a1b2' * 8)[:30]), 'profile*', methods='GET,PUT') + create_acl_record( + 4, partner_southpark, get_service(('a1b2' * 8)[:30]), 'profile*', methods='GET,PUT' + ), ] diff --git a/tests/test_admin.py b/tests/test_admin.py index 93fb540..aff74f2 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,16 +1,11 @@ -#-*- coding: utf-8 -*- - import pytest @pytest.fixture def admin(db): from django.contrib.auth.models import User - user = User.objects.create( - username='admin', - email='admin@example.com', - is_superuser=True, - is_staff=True) + + user = User.objects.create(username='admin', email='admin@example.com', is_superuser=True, is_staff=True) user.set_password('admin') user.save() return user @@ -18,6 +13,7 @@ def admin(db): def test_create_user(settings, app, db, admin): from django.contrib.auth.models import User + settings.PETALE_CHECK_CUT_UUID = False # Login @@ -50,7 +46,7 @@ def test_create_user(settings, app, db, admin): response = response.click('Accueil') # Création de la règle de contrôle d'accès - response = response.click(u'Règles de cont') + response = response.click('Règles de cont') response = response.click('Ajouter') response.form.set('order', '1') response.form.select('partner', text='partenaire1') @@ -77,7 +73,7 @@ def test_create_user(settings, app, db, admin): response.form.set('password', 'admin') response = response.form.submit().follow() - response = response.click(u'Pétales') + response = response.click('Pétales') row = [e.text() for e in response.pyquery('tbody tr > *').items()] assert row[:1] + row[2:] == ['', 'partenaire1', 'cut1', 'cle1'] response = response.click(row[1]) diff --git a/tests/test_api.py b/tests/test_api.py index 281faf0..cf78f96 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,17 +1,15 @@ -import os import json -from xml.etree import ElementTree as etree +import os from multiprocessing.pool import ThreadPool +from unittest import mock +from xml.etree import ElementTree as etree import pytest -import mock - -from utils import get_tests_file_content, FakedResponse - from django.utils.encoding import force_text +from utils import FakedResponse, get_tests_file_content -from petale.utils import etag from petale.models import CUT, Petal +from petale.utils import etag pytestmark = pytest.mark.django_db @@ -44,8 +42,7 @@ def test_resource_not_found(app, partner_southpark, cut_kevin_uuid, acl): app.head(url, status=404) -def test_access_control_list(app, partner_southpark, cut_kevin_uuid, - petal_books, petal_invoice, acl): +def test_access_control_list(app, partner_southpark, cut_kevin_uuid, petal_books, petal_invoice, acl): app.authorization = ('Basic', ('arkham', 'arkham')) # test permission on requested partner @@ -79,16 +76,18 @@ def test_simple_api(app, partner_southpark, cut_kevin_uuid, acl, petal_invoice): # test create key without content-type resp = app.put( - url, params=json.dumps(payload), + url, + params=json.dumps(payload), headers={ 'If-None-Match': '*', 'Content-Type': '', - }, status=400) + }, + status=400, + ) assert resp.json['error'] == 'missing-content-type' # test create key with cut uuid length over 32 - app.put_json('/api/southaprk/%s12/whatever/' % cut_kevin_uuid, - params=json.dumps(payload), status=404) + app.put_json('/api/southaprk/%s12/whatever/' % cut_kevin_uuid, params=json.dumps(payload), status=404) # test create key resp = app.put_json(url, params=payload, headers={'If-None-Match': '*'}, status=201) @@ -115,7 +114,7 @@ def test_simple_api(app, partner_southpark, cut_kevin_uuid, acl, petal_invoice): resp = app.head(cut_keys_url, status=405) resp = app.get(cut_keys_url, status=200) - assert set(resp.json['keys']) == set(['loans']) + assert set(resp.json['keys']) == {'loans'} # test get all keys and prefix resp = app.get('%s?prefix=invoice' % cut_keys_url, status=200) @@ -154,8 +153,12 @@ def test_binary_data(app, partner_southpark, cut_kevin_uuid, acl): # test create xml data url = '/api/southpark/%s/profile-friends/' % cut_kevin_uuid content_type = 'text/xml' - resp = app.put(url, params=get_tests_file_content('users.xml'), - headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201) + resp = app.put( + url, + params=get_tests_file_content('users.xml'), + headers={'If-None-Match': '*', 'Content-Type': content_type}, + status=201, + ) resp = app.get(url) assert resp.headers.get('Content-Type') == content_type xml_data = etree.fromstring(resp.content) @@ -178,8 +181,9 @@ def test_binary_data(app, partner_southpark, cut_kevin_uuid, acl): url = '/api/southpark/%s/profile-picture/' % cut_kevin_uuid content_type = 'application/octet-stream' content = get_tests_file_content('fg.jpg') * 100 - resp = app.put(url, params=content, - headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201) + resp = app.put( + url, params=content, headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201 + ) resp = app.get(url) assert resp.headers.get('Content-Type') == content_type assert resp.content == content @@ -187,8 +191,12 @@ def test_binary_data(app, partner_southpark, cut_kevin_uuid, acl): # test create binary data url = '/api/southpark/%s/profile-invoice/' % cut_kevin_uuid content_type = 'application/pdf' - resp = app.put(url, params=get_tests_file_content('invoice.pdf'), - headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201) + resp = app.put( + url, + params=get_tests_file_content('invoice.pdf'), + headers={'If-None-Match': '*', 'Content-Type': content_type}, + status=201, + ) resp = app.get(url) assert resp.headers.get('Content-Type') == content_type @@ -224,7 +232,7 @@ def test_caching(app, partner_southpark, cut_kevin_uuid, acl): etags = [ '"sha1:5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9"', '"sha1:c75ac194bf3ed4ef3f3e14343585c2fd7ed9b06bbdfbf0eb9f817b7337b966ea" ', - ' "sha1:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"' + ' "sha1:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"', ] etags.append(cache) resp = app.put_json(url, params=payload, headers={'If-None-Match': ','.join(etags)}, status=412) @@ -235,11 +243,13 @@ def test_caching(app, partner_southpark, cut_kevin_uuid, acl): app.authorization = ('Basic', ('cityhall', 'cityhall')) resp = app.get(url, status=200) data = json.loads(resp.text) - data['favourites'].append({ - "name": "best books", - "url": "https://southpark.com/library", - "items": ["Guide To Life by E. Cartman", "Gingers Have Sools by Kyle"] - }) + data['favourites'].append( + { + "name": "best books", + "url": "https://southpark.com/library", + "items": ["Guide To Life by E. Cartman", "Gingers Have Sools by Kyle"], + } + ) etag = resp.headers['Etag'] resp = app.put_json(url, params=data, headers={'If-Match': etag}, status=200) @@ -249,7 +259,7 @@ def test_caching(app, partner_southpark, cut_kevin_uuid, acl): new_item = { "name": "Green zones", "url": "https://southpark.com/parks", - "items": ["Main Street", "Emo Kids Street"] + "items": ["Main Street", "Emo Kids Street"], } payload['favourites'].append(new_item) resp = app.put_json(url, params=payload, headers={'If-Match': cache}, status=412) @@ -272,19 +282,23 @@ def test_partner_size_limit(app, cut_kevin_uuid, acl, petal_invoice, petal_books # test sending data sized above per key hard max size url = '/api/gotham/%s/taxes-fail/' % cut_kevin_uuid - resp = app.put(url, - params='a' * 3 * 1024, # 3ko - headers={'If-None-Match': '*'}, - content_type='text/plain', - status=500) + resp = app.put( + url, + params='a' * 3 * 1024, # 3ko + headers={'If-None-Match': '*'}, + content_type='text/plain', + status=500, + ) assert resp.json['error'] == 'key-space-exhausted' # test sending data sized within soft and hard key limit - resp = app.put(url, - params='a' * (1024 + 1), # 1ko + 1 - headers={'If-None-Match': '*'}, - content_type='text/plain', - status=201) + resp = app.put( + url, + params='a' * (1024 + 1), # 1ko + 1 + headers={'If-None-Match': '*'}, + content_type='text/plain', + status=201, + ) assert len(mailoutbox) == 1 sent_mail = mailoutbox[0] assert sent_mail.to[0] == 'b.wayne@gotham.gov' @@ -295,11 +309,7 @@ def test_partner_size_limit(app, cut_kevin_uuid, acl, petal_invoice, petal_books for i in range(18): url = '/api/gotham/%s/taxes-%d/' % (cut_kevin_uuid, i) - app.put(url, - params='a' * 1024, - headers={'If-None-Match': '*'}, - content_type='text/plain', - status=201) + app.put(url, params='a' * 1024, headers={'If-None-Match': '*'}, content_type='text/plain', status=201) assert len(mailoutbox) == 2 sent_mail = mailoutbox[1] @@ -309,11 +319,13 @@ def test_partner_size_limit(app, cut_kevin_uuid, acl, petal_invoice, petal_books assert str(1024 * 20) in sent_mail.message().as_string() url = '/api/gotham/%s/taxes-100/' % cut_kevin_uuid - resp = app.put(url, - params='a' * (1024 * 2 - 1), - headers={'If-None-Match': '*'}, - content_type='text/plain', - status=500) + resp = app.put( + url, + params='a' * (1024 * 2 - 1), + headers={'If-None-Match': '*'}, + content_type='text/plain', + status=500, + ) assert resp.json['error'] == 'global-space-exhausted' @@ -349,9 +361,7 @@ def test_idp_based_partner_authentication(mocked_post, app, cut_kevin_uuid, acl) payload = {"friends": [{"name": "Token", "age": 10}, {"name": "Kenny", "age": 10}]} url = '/api/southpark/%s/profile-whatever/' % cut_kevin_uuid # failure - response = { - "result": 0, "errors": ["Invalid username/password."] - } + response = {"result": 0, "errors": ["Invalid username/password."]} mocked_post.return_value = FakedResponse(content=json.dumps(response)) app.put_json(url, params=payload, status=401, headers={'If-None-Match': '*'}) response = {"result": 1, "errors": []} @@ -376,15 +386,12 @@ def test_cut_uuid_idp_checking(mocked_post, settings, app, acl): # failure with AUTHENTIC_PETALE improperly defined settings.PETALE_AUTHENTIC_URL = '' - resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, - headers=headers, status=404) + resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=404) assert resp.json["error"] == "cut-not-found" # failure when cut uuid doesn't exist on idp response = {"unknown_uuids": [cut_uuid], "result": 1} - mocked_post.return_value = FakedResponse(content=json.dumps(response), - status_code=200) - resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, - headers=headers, status=404) + mocked_post.return_value = FakedResponse(content=json.dumps(response), status_code=200) + resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=404) assert resp.json["error"] == "cut-not-found" resp = app.get('/api/southpark/%s/' % cut_uuid, headers=headers, status=404) @@ -396,8 +403,7 @@ def test_cut_uuid_idp_checking(mocked_post, settings, app, acl): # sucess settings.PETALE_AUTHENTIC_URL = 'http://example.net/idp/' response = {"unknown_uuids": [], "result": 1} - mocked_post.return_value = FakedResponse(content=json.dumps(response), - status_code=200) + mocked_post.return_value = FakedResponse(content=json.dumps(response), status_code=200) resp = app.get('/api/southpark/%s/profile/' % cut_uuid, headers=headers, status=404) assert resp.json["error"] == "key-not-found" @@ -405,8 +411,7 @@ def test_cut_uuid_idp_checking(mocked_post, settings, app, acl): resp = app.get('/api/southpark/%s/' % cut_uuid, headers=headers) assert resp.json["keys"] == [] - resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, - headers=headers, status=201) + resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=201) assert CUT.objects.get(uuid=cut_uuid) assert Petal.objects.get(name='profile', cut__uuid=cut_uuid, partner__name='southpark') @@ -426,7 +431,7 @@ def test_storage_error(app, partner_southpark, cut_kevin_uuid, acl, caplog): def test_concurrent_put(app, transactional_db, settings): '''Test concurrent PUT to the same key''' - from utils import create_cut, create_partner, create_service, create_acl_record + from utils import create_acl_record, create_cut, create_partner, create_service uuid = create_cut('a' * 255).uuid southpark = create_partner('southpark', hg=20240, hk=19728) @@ -446,6 +451,7 @@ def test_concurrent_put(app, transactional_db, settings): def f(i): from django.db import connection + if i % 2 == 0: response = app.get(url, status=200) else: diff --git a/tests/utils.py b/tests/utils.py index 89a0b18..e0c26f1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,13 +1,13 @@ -import os import json +import os from io import BytesIO +from unittest import mock import pytest -import mock - from django.contrib.auth.models import User from django.core.files import File -from petale.models import Partner, CUT, AccessControlList, Petal + +from petale.models import CUT, AccessControlList, Partner, Petal from petale.utils import etag pytestmark = pytest.mark.django_db @@ -39,9 +39,13 @@ def create_partner(name, admins=None, hg=2, sg=1, hk=1, sk=1): if not admins: admins = 'e.cartman@southpark.com,t.blakc@southpark.com' return Partner.objects.create( - name=name, admin_emails=admins, - hard_global_max_size=hg, soft_global_max_size=sg, - hard_per_key_max_size=hk, soft_per_key_max_size=sk) + name=name, + admin_emails=admins, + hard_global_max_size=hg, + soft_global_max_size=sg, + hard_per_key_max_size=hk, + soft_per_key_max_size=sk, + ) def create_acl_record(order, partner, user, key, methods='*'): @@ -57,12 +61,11 @@ def create_petal(cut_uuid, partner, name, data, content_type): size=len(data), etag=etag(data), data=File(BytesIO(data), name), - content_type=content_type + content_type=content_type, ) class FakedResponse(mock.Mock): - def json(self): return json.loads(self.content) diff --git a/tox.ini b/tox.ini index b5b2170..7500737 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,12 @@ toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/petale/ envlist = py3-dj22-drf39 + code-style [tox:jenkins] envlist = pylint + code-style py3-dj22-drf39 [testenv] @@ -62,6 +64,13 @@ deps = commands = /bin/bash -c "./pylint.sh petale/" +[testenv:code-style] +skip_install = true +deps = + pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure + [pytest] junit_family=xunit2 filterwarnings =