кодовая база:
#/home/babyblinkfeeldark/PycharmProjects/prj_root/locust/locustfile.py
from locust import HttpUser, task, between
class WalletUser(HttpUser):
wait_time = between(1, 2)
host = "http://localhost:8000"
def on_start(self):
response = self.client.post("api/v1/wallets/", json={"balance": 1000.0})
if response.status_code == 201:
self.wallet_id = response.json()['id']
else:
print("Ошибка при создании кошелька:", response.text)
@task(2)
def deposit(self):
if hasattr(self, 'wallet_id'):
self.client.post(f"/api/v1/wallets/{self.wallet_id}/operation/", json={
"operation_type": "DEPOSIT",
"amount": 100.0
})
@task(1)
def withdraw(self):
if hasattr(self, 'wallet_id'):
self.client.post(f"/api/v1/wallets/{self.wallet_id}/operation/", json={
"operation_type": "WITHDRAW",
"amount": 50.0
})
@task(1)
def get_balance(self):
if hasattr(self, 'wallet_id'):
self.client.get(f"/api/v1/wallets/{self.wallet_id}/balance/")
#/home/babyblinkfeeldark/PycharmProjects/prj_root/project_root/settings.py
"""
Django settings for project_root project.
Generated by 'django-admin startproject' using Django 5.1.5.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-_skjesb)_v0u6j4%p(2qc))k&nk^@q%cn^j=efl*+a_u3_gxo8'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'wallets',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'project_root.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'project_root.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
load_dotenv()
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#/home/babyblinkfeeldark/PycharmProjects/prj_root/project_root/urls.py
"""
URL configuration for project_root project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.http import HttpResponse
from django.urls import path, include
def home(request):
return HttpResponse("Test page")
urlpatterns = [
path('', home),
path('', include('wallets.urls')), # Убедитесь, что здесь пустой префикс, так как 'api/v1/' уже включён в wallets.urls
path('admin/', admin.site.urls),
]
#/home/babyblinkfeeldark/PycharmProjects/prj_root/wallets/models.py
from email.policy import default
from django.db import models
import uuid
class Wallet(models.Model):
"""
Модель для хранения информации о кошельке пользователя.
Атрибуты:
id (UUIDField): Уникальный идентификатор кошелька (primary key).
balance (DecimalField): Баланс кошелька, с двумя знаками после запятой.
"""
id = models.UUIDField(primary_key=True, default= uuid.uuid4, editable=False)
balance = models.DecimalField(max_digits=20, decimal_places=2,default=0)
def __str__(self):
"""
Возвращает строковое представление объекта Wallet.
Возвращает:
str: Идентификатор кошелька.
"""
return str(self.id)
class WalletOperation(models.Model):
"""
Модель для учета операций с кошельком.
Атрибуты:
DEPOSIT (str): Тип операции для пополнения.
WITHDRAW (str): Тип операции для вывода средств.
OPERATION_TYPE_CHOICES (list): Список возможных типов операций.
id (AutoField): Уникальный идентификатор операции.
wallet (ForeignKey): Ссылка на кошелек, к которому привязана операция.
operation_type (CharField): Тип операции (DEPOSIT или WITHDRAW).
amount (DecimalField): Сумма операции.
timestamp (DateTimeField): Время проведения операции.
"""
DEPOSIT = 'DEPOSIT'
WITHDRAW = 'WITHDRAW'
OPERATION_TYPE_CHOICES = [(DEPOSIT,'DEPOSIT'), (WITHDRAW, "WITHDRAW")]
id = models.AutoField(primary_key = True)
wallet = models.ForeignKey(Wallet, related_name="operations", on_delete=models.CASCADE)
operation_type = models.CharField(max_length=10, choices=OPERATION_TYPE_CHOICES)
amount = models.DecimalField(max_digits=20, decimal_places=2)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""
Возвращает строковое представление операции.
Возвращает:
str: Описание операции с указанием типа, суммы и идентификатора кошелька.
"""
return f"{self.operation_type} of {self.amount} to wallet {self.wallet.id}"
#/home/babyblinkfeeldark/PycharmProjects/prj_root/wallets/serializers.py
from rest_framework import serializers
from .models import Wallet, WalletOperation
class WalletSerializer(serializers.ModelSerializer):
"""
Сериализатор для модели Wallet.
Этот сериализатор используется для преобразования данных о кошельке в формат JSON
и обратно.
Атрибуты:
model (class): Модель, к которой применяется сериализатор.
fields (str or list): Список полей, которые будут включены в сериализацию.
"""
class Meta:
model = Wallet
fields = '__all__'
class WalletOperationSerializer(serializers.Serializer):
"""
Сериализатор для создания операций на кошельке.
Этот сериализатор используется для валидации данных о типе операции и сумме.
"""
operation_type = serializers.ChoiceField(choices=[('DEPOSIT', 'DEPOSIT'), ('WITHDRAW', 'WITHDRAW')])
amount = serializers.DecimalField(max_digits=20, decimal_places=2)
def validate_amount(self, value):
"""
Валидация суммы операции.
"""
if value <= 0:
raise serializers.ValidationError("Amount must be positive.")
return value
def validate_operation_type(self, value):
"""
Валидация типа операции.
"""
if value not in ['DEPOSIT', 'WITHDRAW']:
raise serializers.ValidationError("Invalid operation type.")
return value
class WalletBalanceSerializer(serializers.ModelSerializer):
"""
Сериализатор для вывода баланса кошелька.
Этот сериализатор используется для отображения текущего баланса кошелька.
"""
class Meta:
model = Wallet
fields = ['id', 'balance']
#/home/babyblinkfeeldark/PycharmProjects/prj_root/wallets/urls.py
from django.urls import path
from .views import WalletList, WalletDetail, WalletBalanceDetail, create_wallet_operation
urlpatterns = [
path('api/v1/wallets/', WalletList.as_view(), name='wallet-list'),
path('api/v1/wallets/<uuid:pk>/', WalletDetail.as_view(), name='wallet-detail'),
path('api/v1/wallets/<uuid:wallet_uuid>/balance/', WalletBalanceDetail.as_view(), name='wallet-balance'),
path('api/v1/wallets/<uuid:wallet_uuid>/operation/', create_wallet_operation, name='wallet-operation'),
]
#/home/babyblinkfeeldark/PycharmProjects/prj_root/wallets/views.py
from django.http import Http404
from rest_framework import generics
from .serializers import WalletSerializer, WalletOperationSerializer, WalletBalanceSerializer
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
from .models import Wallet, WalletOperation
from django.db import transaction
import uuid
class WalletList(generics.ListCreateAPIView):
queryset = Wallet.objects.all()
serializer_class = WalletSerializer
class WalletDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Wallet.objects.all()
serializer_class = WalletSerializer
@api_view(['POST'])
def create_wallet_operation(request, wallet_uuid):
try:
with transaction.atomic():
wallet = Wallet.objects.select_for_update().get(id=wallet_uuid)
serializer = WalletOperationSerializer(data=request.data)
if serializer.is_valid():
operation_type = serializer.validated_data['operation_type']
amount = serializer.validated_data['amount']
if operation_type == 'WITHDRAW' and wallet.balance < amount:
return Response({'error': 'Insufficient funds'}, status=status.HTTP_400_BAD_REQUEST)
if operation_type == 'DEPOSIT':
wallet.balance += amount
elif operation_type == 'WITHDRAW':
wallet.balance -= amount
wallet.save()
WalletOperation.objects.create(
wallet=wallet,
operation_type=operation_type,
amount=amount
)
return Response({'balance': wallet.balance}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Wallet.DoesNotExist:
return Response({'error': 'Wallet not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class WalletBalanceDetail(generics.RetrieveAPIView):
queryset = Wallet.objects.all()
serializer_class = WalletBalanceSerializer
def get_object(self):
wallet_uuid = self.kwargs['wallet_uuid']
try:
return Wallet.objects.get(id=wallet_uuid)
except Wallet.DoesNotExist:
raise Http404
В ручную запросы вида
❯ curl -X POST http://localhost:8000/api/v1/wallets/
{"id":"e4b88c44-8066-49f7-b737-b841d1c0a834","balance":"0.00"}%
пробразываются без проблем, но нагрузузочное тестирование не работает
# Failures Method Name Message
1000 POST /api/v1/wallets/
ConnectionRefusedError(111, 'Connection refused')