tox: add code-style target

This commit is contained in:
Benjamin Dauvergne 2022-03-10 17:01:10 +01:00
parent 283a0eae60
commit 2ce0cf927e
24 changed files with 414 additions and 355 deletions

18
.pre-commit-config.yaml Normal file
View File

@ -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']

View File

@ -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

6
debian/settings.py vendored
View File

@ -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

View File

@ -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)

View File

@ -14,36 +14,41 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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()

View File

@ -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:

View File

@ -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')},
),
]

View File

@ -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',

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations

View File

@ -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',

View File

@ -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'
),
),
]

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -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-_]+$')],
),
),
]

View File

@ -14,53 +14,39 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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')

View File

@ -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

View File

@ -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())

View File

@ -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<partner_name>[\w,-]+)/(?P<cut_uuid>[\w,-]{,255})/$',
url(
r'^api/(?P<partner_name>[\w,-]+)/(?P<cut_uuid>[\w,-]{,255})/$',
PetalAPIKeysView.as_view(),
name='api-keys'),
url(r'^api/(?P<partner_name>[\w,-]+)/(?P<cut_uuid>[\w,-]{,255})/(?P<petal_name>[\w,-]+)/$',
PetalAPIView.as_view(), name='api')
name='api-keys',
),
url(
r'^api/(?P<partner_name>[\w,-]+)/(?P<cut_uuid>[\w,-]{,255})/(?P<petal_name>[\w,-]+)/$',
PetalAPIView.as_view(),
name='api',
),
]

View File

@ -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

View File

@ -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)

View File

@ -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'
),
]

View File

@ -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])

View File

@ -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:

View File

@ -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)

View File

@ -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 =