@@ -1,9 +1,17 @@ from django.urls import path -from .apis import AchievementAPIView, AvailableAchievementsAPIView, IssuedAchievementsAPIView +from .apis import ( + AchievementAPIView, + AvailableAchievementsAPIView, + IssuedAchievementsAPIView, +) urlpatterns = [ - path('available/', AvailableAchievementsAPIView.as_view(), name='available-achievements'), + path( + 'available/', + AvailableAchievementsAPIView.as_view(), + name='available-achievements', + ), path('/', AchievementAPIView.as_view(), name='achievement'), path('', IssuedAchievementsAPIView.as_view(), name='issued-achievements'), ] @@ -1,6 +1,12 @@ -from authentication.exceptions.business_host_exceptions.already_account import AlreadyAccount -from authentication.exceptions.business_host_exceptions.already_has_plan import AlreadyHasPlan -from authentication.exceptions.business_host_exceptions.already_host import AlreadyHost +from authentication.exceptions.business_host_exceptions.already_account import ( + AlreadyAccount, +) +from authentication.exceptions.business_host_exceptions.already_has_plan import ( + AlreadyHasPlan, +) +from authentication.exceptions.business_host_exceptions.already_host import ( + AlreadyHost, +) __all__ = ( 'AlreadyHasPlan', @@ -1,4 +1,6 @@ -from authentication.exceptions.business_host_exceptions.base_already import BaseAlready +from authentication.exceptions.business_host_exceptions.base_already import ( + BaseAlready, +) class AlreadyAccount(BaseAlready): @@ -1,8 +1,12 @@ -from authentication.exceptions.business_host_exceptions.base_already import BaseAlready +from authentication.exceptions.business_host_exceptions.base_already import ( + BaseAlready, +) class AlreadyHasPlan(BaseAlready): def msg(self): return dict( - message='user has a paid plan', user_id=self.user.uid, plan_id=self.user.payment_plan.plan.uid + message='user has a paid plan', + user_id=self.user.uid, + plan_id=self.user.payment_plan.plan.uid, ) @@ -1,4 +1,6 @@ -from authentication.exceptions.business_host_exceptions.base_already import BaseAlready +from authentication.exceptions.business_host_exceptions.base_already import ( + BaseAlready, +) class AlreadyHost(BaseAlready): @@ -1,4 +1,6 @@ -from authentication.exceptions.business_host_exceptions.base_already import BaseAlready +from authentication.exceptions.business_host_exceptions.base_already import ( + BaseAlready, +) __all__ = ('BaseAlready',) @@ -6,4 +6,8 @@ from authentication.models.email_token import EmailToken from authentication.models.user import UTM from authentication.models.user_telegram import TelegramUser from authentication.models.user_vk import VKUser -from authentication.models.whitelist import CompanyIP, CompanyIPWhitelist, PolicyWhitelist +from authentication.models.whitelist import ( + CompanyIP, + CompanyIPWhitelist, + PolicyWhitelist, +) @@ -58,9 +58,7 @@ class BusinessAccount(BaseModel): def clean(self) -> None: if self.group and not self.parent_company == self.group.parent_company: raise ValidationError( - _( - 'Impossible to add this employee to this group which does not belong to this company' - ) + _('Impossible to add this employee to this group which does not belong to this company') ) return super().clean() @@ -41,9 +41,7 @@ class BusinessUserHost(BaseModel): default=BusinessSector.IT, verbose_name=_('Sector'), ) - planned_amount_of_workers = models.IntegerField( - default=1, verbose_name=_('Planned amount of workers') - ) + planned_amount_of_workers = models.IntegerField(default=1, verbose_name=_('Planned amount of workers')) usage_intensity = models.CharField( max_length=50, choices=UsageIntensity.choices, @@ -62,30 +60,20 @@ class BusinessUserHost(BaseModel): default=list, verbose_name=_('Emails token low balance cap'), ) - token_cap_enabled = models.BooleanField( - default=False, verbose_name=_('Token low balance cap enabled') - ) + token_cap_enabled = models.BooleanField(default=False, verbose_name=_('Token low balance cap enabled')) # Judicial information ITN = models.BigIntegerField(null=True, blank=True, verbose_name=_('ITN')) PSRN = models.BigIntegerField(null=True, blank=True, verbose_name=_('PSRN')) - company_name = models.CharField( - max_length=255, null=True, blank=True, verbose_name=_('Name') - ) + company_name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Name')) # Contact information - preferred_name = models.CharField( - max_length=50, null=True, blank=True, verbose_name=_('Preffered name') - ) - corporate_email = models.EmailField( - null=True, blank=True, verbose_name=_('Corporate email') - ) + preferred_name = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Preffered name')) + corporate_email = models.EmailField(null=True, blank=True, verbose_name=_('Corporate email')) corporate_phone = models.CharField( max_length=20, null=True, blank=True, verbose_name=_('Corporate phone') ) - job_title = models.CharField( - max_length=64, null=True, blank=True, verbose_name=_('Job title') - ) + job_title = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Job title')) allowed_models = ArrayField( models.CharField(max_length=64, blank=True), @@ -93,9 +81,7 @@ class BusinessUserHost(BaseModel): verbose_name=_('Allowed models'), ) - is_log_history_enabled = models.BooleanField( - default=False, verbose_name=_('Log history enabled') - ) + is_log_history_enabled = models.BooleanField(default=False, verbose_name=_('Log history enabled')) @property def accounts(self) -> QuerySet[BusinessAccount]: @@ -120,7 +106,10 @@ class BusinessUserHost(BaseModel): @receiver(post_save, sender=BusinessUserHost) def init_ip_whitelist( - sender: type[BusinessUserHost], instance: BusinessUserHost, created: bool, **kwargs + sender: type[BusinessUserHost], + instance: BusinessUserHost, + created: bool, + **kwargs, ): if created: CompanyIPWhitelist.objects.create(company=instance) @@ -2,7 +2,11 @@ import random from typing import TYPE_CHECKING, Optional from uuid import uuid4 -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.contrib.auth.models import ( + AbstractBaseUser, + BaseUserManager, + PermissionsMixin, +) from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models.signals import post_save @@ -13,12 +17,19 @@ from authentication.models.utm import UTM from core.models import BaseModel if TYPE_CHECKING: - from payments.models.referral_account import ReferralAccount, ReferralInvite + from payments.models.referral_account import ( + ReferralAccount, + ReferralInvite, + ) class CustomUserModelManager(BaseUserManager): def create_user( - self, username: str, email: str, password: Optional[str] = None, **kwargs + self, + username: str, + email: str, + password: Optional[str] = None, + **kwargs, ): """ Creates a custom user with the given fields @@ -116,7 +127,11 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): verbose_name=_('Last name'), ) username = models.CharField( - max_length=100, unique=True, null=True, blank=True, verbose_name=_('Username') + max_length=100, + unique=True, + null=True, + blank=True, + verbose_name=_('Username'), ) email = models.EmailField( max_length=100, @@ -134,9 +149,7 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): is_staff = models.BooleanField(default=False, verbose_name=_('Is staff')) is_superuser = models.BooleanField(default=False, verbose_name=_('Is superuser')) is_confirmed = models.BooleanField(default=True, verbose_name=_('Is email confirmed')) - is_subscribed_to_emails = models.BooleanField( - default=True, verbose_name=_('Is subscribed') - ) + is_subscribed_to_emails = models.BooleanField(default=True, verbose_name=_('Is subscribed')) profile_picture_name = models.CharField( max_length=255, default=None, @@ -162,7 +175,9 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): @property def account_type(self): - from authentication.selectors.account_status_selector import AccountStatusSelector + from authentication.selectors.account_status_selector import ( + AccountStatusSelector, + ) from authentication.selectors.business_account_selector import ( BusinessAccountSelector, ) @@ -185,10 +200,7 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): if not self.profile_picture_name: return None - elif any( - prefix in self.profile_picture_name - for prefix in ('googleusercontent', 'yandex') - ): + elif any(prefix in self.profile_picture_name for prefix in ('googleusercontent', 'yandex')): return self.profile_picture_name return MinIOService().get_object_link('air-profiles', self.profile_picture_name) @@ -239,9 +251,7 @@ def on_user_creation_signal(sender, instance, created, **kwargs): free_plan = PaymentPlanSelector(instance).get_free_plan() PaymentPlanService(instance).subscribe_user_to_plan(free_plan) if not instance.profile_picture_name: - instance.profile_picture_name = instance.LAST_NAME_AVATARS.get( - instance.last_name, None - ) + instance.profile_picture_name = instance.LAST_NAME_AVATARS.get(instance.last_name, None) instance.save() @@ -253,9 +263,7 @@ class UserSetting(models.Model): class TypeChoices(models.TextChoices): SIDEBAR = 'sidebar', 'Сайдбар' - id = models.UUIDField( - primary_key=True, editable=False, default=uuid4, verbose_name='ID' - ) + id = models.UUIDField(primary_key=True, editable=False, default=uuid4, verbose_name='ID') user = models.ForeignKey( CustomUserModel, @@ -6,18 +6,10 @@ from django.utils.translation import gettext_lazy as _ class TelegramUser(models.Model): id = models.BigAutoField(primary_key=True, verbose_name=_('Telegram ID')) is_bot = models.BooleanField(default=False, verbose_name=_('Is bot')) - first_name = models.CharField( - max_length=255, null=False, blank=False, verbose_name=_('First name') - ) - last_name = models.CharField( - max_length=255, null=True, blank=True, verbose_name=_('Last name') - ) - username = models.CharField( - max_length=255, null=True, blank=True, verbose_name=_('Username') - ) - language_code = models.CharField( - max_length=4, null=True, blank=True, verbose_name=_('Language') - ) + first_name = models.CharField(max_length=255, null=False, blank=False, verbose_name=_('First name')) + last_name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Last name')) + username = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Username')) + language_code = models.CharField(max_length=4, null=True, blank=True, verbose_name=_('Language')) is_premium = models.BooleanField(null=True, blank=True, verbose_name=_('Is premium')) is_subscribed_to_air_channel = models.BooleanField( default=False, verbose_name=_('Is subscribed to channel') @@ -30,9 +22,7 @@ class TelegramUser(models.Model): verbose_name=_('User'), ) - phone_number = models.CharField( - max_length=20, null=True, blank=True, verbose_name=_('Phonenumber') - ) + phone_number = models.CharField(max_length=20, null=True, blank=True, verbose_name=_('Phonenumber')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at')) @@ -48,7 +48,7 @@ class PolicyWhitelist(BaseModel): emails = ArrayField(models.EmailField(), verbose_name=_('Emails')) def __str__(self): - return f"{','.join(self.emails[:5])}..." + return f'{",".join(self.emails[:5])}...' class Meta: verbose_name = _('Whitelist to cancel policies') @@ -1,4 +1,8 @@ -from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel +from authentication.models import ( + BusinessAccount, + BusinessUserHost, + CustomUserModel, +) from authentication.models.choices import AccountPrivileges @@ -1,6 +1,8 @@ from authentication.models import BusinessAccount, CustomUserModel from authentication.models.business_host import BusinessUserHost -from authentication.services.business_account_service import BusinessAccountService +from authentication.services.business_account_service import ( + BusinessAccountService, +) class BusinessAccountSelector: @@ -5,7 +5,11 @@ from uuid import UUID from django.db.models import BooleanField, Case, Q, Value, When from django.utils.translation import gettext_lazy as _ -from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel +from authentication.models import ( + BusinessAccount, + BusinessUserHost, + CustomUserModel, +) from authentication.selectors.user_selector import UserSelector from authentication.serializers import ( AllowedModelsStatus, @@ -44,7 +48,9 @@ class BusinessHostSelector: accounts = accounts.filter(group__isnull=not have_group) if serialize: return BusinessAccountDataSerializer( - accounts, many=True, context={'account_type': self.user.account_type} + accounts, + many=True, + context={'account_type': self.user.account_type}, ) return accounts @@ -55,17 +61,13 @@ class BusinessHostSelector: return host.first() - def get_by_uid( - self, uid: UUID, serialize: bool = False - ) -> BusinessUserHost | BusinessHostSerializer: + def get_by_uid(self, uid: UUID, serialize: bool = False) -> BusinessUserHost | BusinessHostSerializer: host = BusinessUserHost.objects.get(user=self.user, uid=uid) if serialize: return BusinessHostSerializer(host) return host - def get_by_itn( - self, itn: int, serialize: bool = False - ) -> BusinessUserHost | BusinessHostSerializer: + def get_by_itn(self, itn: int, serialize: bool = False) -> BusinessUserHost | BusinessHostSerializer: host = BusinessUserHost.objects.get( user=self.user, ITN=itn, @@ -81,20 +83,18 @@ class BusinessHostSelector: host = self.user.business_account.parent_company else: raise Exception(_("You haven't rights to access host account information")) - return BusinessHostSerializer( - host, context={'worker_amount': host.accounts.count()} - ) + return BusinessHostSerializer(host, context={'worker_amount': host.accounts.count()}) - def get_all_account_statistics(self) -> BusinessAccountStatisticsSerializer: + def get_all_account_statistics( + self, + ) -> BusinessAccountStatisticsSerializer: accounts = self.list_business_accounts() context = dict() for account in accounts: spending_amount = ModelPaymentSelector(account.user).calculate_self_spending() context[account] = spending_amount - return BusinessAccountStatisticsSerializer( - accounts, many=True, context={'amounts': context} - ) + return BusinessAccountStatisticsSerializer(accounts, many=True, context={'amounts': context}) def get_per_model_statistics(self): accounts = self.list_business_accounts() @@ -102,19 +102,13 @@ class BusinessHostSelector: for model_name in self.user.host_account.allowed_models: model_spending = Decimal('0') for account in accounts: - model_spending += ModelPaymentSelector( - account.user - ).calculate_model_spendings(model_name) + model_spending += ModelPaymentSelector(account.user).calculate_model_spendings(model_name) context[model_name] = model_spending - models = NeuronModel.objects.filter( - title__in=self.user.host_account.allowed_models - ) + models = NeuronModel.objects.filter(title__in=self.user.host_account.allowed_models) - return NeuronModelStatisticsSerialiser( - models, many=True, context={'amounts': context} - ) + return NeuronModelStatisticsSerialiser(models, many=True, context={'amounts': context}) def get_allowed_models(self): acc_type = UserSelector(self.user).check_account_type() @@ -125,7 +119,10 @@ class BusinessHostSelector: ) allowed_models = NeuronModel.objects.annotate( is_allowed=Case( - When(condition=Q(title__in=company.allowed_models), then=Value(True)), + When( + condition=Q(title__in=company.allowed_models), + then=Value(True), + ), default=Value(False), output_field=BooleanField(), ) @@ -9,9 +9,16 @@ from authentication.models import CustomUserModel from authentication.models.choices import InvitationStatus from authentication.models.user_telegram import TelegramUser from authentication.models.user_vk import VKUser -from authentication.selectors.account_status_selector import AccountStatusSelector -from authentication.selectors.business_account_selector import BusinessAccountSelector -from authentication.serializers import SocialAccountSerializer, UserDetailSerializer +from authentication.selectors.account_status_selector import ( + AccountStatusSelector, +) +from authentication.selectors.business_account_selector import ( + BusinessAccountSelector, +) +from authentication.serializers import ( + SocialAccountSerializer, + UserDetailSerializer, +) from payments.models.payment_plan import PaymentPlanUserInfo @@ -20,9 +27,7 @@ class UserSelector: self.user = user @classmethod - def list( - cls, serialize: bool = False - ) -> list[CustomUserModel] | list[UserDetailSerializer]: + def list(cls, serialize: bool = False) -> list[CustomUserModel] | list[UserDetailSerializer]: users = CustomUserModel.objects.prefetch_related( Prefetch( 'payment_plan', @@ -34,9 +39,7 @@ class UserSelector: return users @classmethod - def detail( - cls, serialize: bool = True, **kwargs - ) -> UserDetailSerializer | CustomUserModel: + def detail(cls, serialize: bool = True, **kwargs) -> UserDetailSerializer | CustomUserModel: user_id = kwargs.get('id') user = ( CustomUserModel.objects.filter(uid=user_id) @@ -52,9 +55,7 @@ class UserSelector: user.show_balance = user.business_account.show_balance if not user.show_balance: user.payment_plan.current_token_balance = 0 - user.payment_plan.plan = ( - user.business_account.parent_company.user.payment_plan.plan - ) + user.payment_plan.plan = user.business_account.parent_company.user.payment_plan.plan if serialize: return UserDetailSerializer(user) return user @@ -122,8 +123,6 @@ class UserSelector: """Get newly registered users with further offset if needed""" if date_offset: end_date = register_date + timedelta(days=date_offset + 1) - return CustomUserModel.objects.filter( - created_at__date__range=(register_date, end_date) - ) + return CustomUserModel.objects.filter(created_at__date__range=(register_date, end_date)) return CustomUserModel.objects.filter(created_at__date=register_date) @@ -3,7 +3,11 @@ from typing import Any, OrderedDict, Tuple from django.utils.translation import gettext_lazy as _ -from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel +from authentication.models import ( + BusinessAccount, + BusinessUserHost, + CustomUserModel, +) from authentication.models.choices import InvitationStatus @@ -61,9 +65,7 @@ class BusinessAccountService: elif data['status'] == InvitationStatus.REJECTED: self.reject() else: - raise Exception( - _('Invited account can either accept or reject an invitation') - ) + raise Exception(_('Invited account can either accept or reject an invitation')) return self.account def accept(self): @@ -9,12 +9,24 @@ from authentication.exceptions.business_host_exceptions import ( AlreadyAccount, AlreadyHasPlan, ) -from authentication.exceptions.business_host_exceptions.already_host import AlreadyHost -from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel +from authentication.exceptions.business_host_exceptions.already_host import ( + AlreadyHost, +) +from authentication.models import ( + BusinessAccount, + BusinessUserHost, + CustomUserModel, +) from authentication.models.choices import InvitationStatus -from authentication.selectors.account_status_selector import AccountStatusSelector -from authentication.selectors.business_account_selector import BusinessAccountSelector -from authentication.selectors.business_host_selector import BusinessHostSelector +from authentication.selectors.account_status_selector import ( + AccountStatusSelector, +) +from authentication.selectors.business_account_selector import ( + BusinessAccountSelector, +) +from authentication.selectors.business_host_selector import ( + BusinessHostSelector, +) from authentication.selectors.user_selector import UserSelector from authentication.serializers import ( AccountDataUpdateSerializer, @@ -29,7 +41,9 @@ from authentication.serializers import ( NewBusinessHostSerializer, UserDataSerializer, ) -from authentication.services.business_account_service import BusinessAccountService +from authentication.services.business_account_service import ( + BusinessAccountService, +) from authentication.services.email_service import EmailService from authentication.utils import generate_token from ml_model.models import NeuronModel @@ -51,15 +65,11 @@ class BusinessHostService: ) -> BusinessAccountService: username = f'{self.user.host_account.company_name}_{random_with_N_digits(6)}' password = generate_token(15) - user = CustomUserModel.objects.create_user( - username=username, email=email, password=password - ) + user = CustomUserModel.objects.create_user(username=username, email=email, password=password) user.is_confirmed = True user.save() - PaymentPlanService(user).subscribe_user_to_plan( - PaymentPlan.objects.get(price=0, is_corporate=False) - ) + PaymentPlanService(user).subscribe_user_to_plan(PaymentPlan.objects.get(price=0, is_corporate=False)) account_service = BusinessAccountService.create( user, @@ -69,9 +79,7 @@ class BusinessHostService: ) if token_limit is not None: account_service.update_limit(token_limit) - EmailService(self.user).send_corporate_greeting_email( - account_service.account, password - ) + EmailService(self.user).send_corporate_greeting_email(account_service.account, password) return account_service def create_existing( @@ -114,25 +122,21 @@ class BusinessHostService: serializer.is_valid(raise_exception=True) account = self.create_existing(**serializer.validated_data).account - return BusinessAccountDataSerializer( - account, context={'account_type': 'business_account'} - ) + return BusinessAccountDataSerializer(account, context={'account_type': 'business_account'}) def update_token_limit( self, user: CustomUserModel, amount: Decimal, ): - BusinessAccountSelector.from_user( - user, company=self.user.host_account - ).to_service().update_limit(amount) + BusinessAccountSelector.from_user(user, company=self.user.host_account).to_service().update_limit( + amount + ) - def update_user_invitation_status( - self, user: CustomUserModel, new_status: Tuple[str, Any] - ): - BusinessAccountSelector.from_user( - user, company=self.user.host_account - ).to_service().update_status(new_status) + def update_user_invitation_status(self, user: CustomUserModel, new_status: Tuple[str, Any]): + BusinessAccountSelector.from_user(user, company=self.user.host_account).to_service().update_status( + new_status + ) def update_privileges(self, user: CustomUserModel, new_privileges: str): BusinessAccountSelector.from_user( @@ -164,13 +168,9 @@ class BusinessHostService: if status == InvitationStatus.CANCELLED: return DeletedAccountDataSerializer(user, context={'account_type': 'regular'}) - return BusinessAccountDataSerializer( - user.business_account, context={'account_type': account_type} - ) + return BusinessAccountDataSerializer(user.business_account, context={'account_type': account_type}) - def update_self( - self, request: Request, serialize: bool = True - ) -> BusinessHostSerializer: + def update_self(self, request: Request, serialize: bool = True) -> BusinessHostSerializer: serializer = BusinessHostUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) acc_type = self.user.account_type @@ -179,13 +179,9 @@ class BusinessHostService: if acc_type == 'business_host' else self.user.business_account.parent_company ) - if ( - token_cap_emails := serializer.validated_data.get('token_cap_emails', None) - ) is not None: + if (token_cap_emails := serializer.validated_data.get('token_cap_emails', None)) is not None: company.token_cap_emails = token_cap_emails - if ( - token_cap_enabled := serializer.validated_data.get('token_cap_enabled', None) - ) is not None: + if (token_cap_enabled := serializer.validated_data.get('token_cap_enabled', None)) is not None: company.token_cap_enabled = token_cap_enabled company.save() if serialize: @@ -217,9 +213,7 @@ class BusinessHostService: if AccountStatusSelector(self.user).is_business_account(): raise business_host_exceptions.AlreadyAccount(self.user) - host = BusinessUserHost.objects.create( - user=self.user, **serializer.validated_data - ) + host = BusinessUserHost.objects.create(user=self.user, **serializer.validated_data) host.save() PaymentPlanService(self.user).subscribe_user_to_plan( PaymentPlan.objects.get(price=0, is_corporate=True) @@ -233,9 +227,7 @@ class BusinessHostService: serializer = AddModelsSerializer(data=request.data) serializer.is_valid(raise_exception=True) - models = NeuronModel.objects.filter( - title__in=serializer.validated_data['models'] - ).only('title') + models = NeuronModel.objects.filter(title__in=serializer.validated_data['models']).only('title') for model in models: if model.title not in self.user.host_account.allowed_models: @@ -35,9 +35,7 @@ class EmailService: ) except Exception as exc: logger.exception(exc) - raise Exception( - 'Возникла проблема при регистрации, пожалуйста свяжитесь с администрацией' - ) + raise Exception('Возникла проблема при регистрации, пожалуйста свяжитесь с администрацией') def send_reg_conf_email(self): token = EmailTokenService(self.user).generate_user_token() @@ -112,9 +110,7 @@ class EmailService: ) mail.send(fail_silently=True) - def send_corporate_greeting_email( - self, account: BusinessAccount, password: str | None = None - ): + def send_corporate_greeting_email(self, account: BusinessAccount, password: str | None = None): if self.user.host_account is None: raise Exception(_('Regular users cannot send introductory letters')) message = f""" @@ -6,7 +6,11 @@ import jwt from django.conf import settings from django.contrib.auth.hashers import check_password -from authentication.exceptions import InvalidPassword, InvalidToken, InvalidUsername +from authentication.exceptions import ( + InvalidPassword, + InvalidToken, + InvalidUsername, +) from authentication.models.user import CustomUserModel @@ -78,7 +82,10 @@ class TokenService: @classmethod def _encode( - cls, *, payload: dict[str, Any], token_type: Literal['access', 'refresh'] + cls, + *, + payload: dict[str, Any], + token_type: Literal['access', 'refresh'], ) -> str: issued_at = datetime.now(tz=timezone.utc) jwt_signature = { @@ -7,7 +7,9 @@ from django.db.transaction import atomic from django.utils.translation import gettext_lazy as _ from rest_framework.request import Request -from authentication.exceptions.business_host_exceptions.not_allowed_ip import NotAllowedIP +from authentication.exceptions.business_host_exceptions.not_allowed_ip import ( + NotAllowedIP, +) from authentication.models import ( CompanyIPWhitelist, CustomUserModel, @@ -74,9 +76,7 @@ class UserService: .prefetch_related('user_referral_account') .get() ) - ReferralAccountService.create_invite( - referer_account=referer.referral_account, invitee=user - ) + ReferralAccountService.create_invite(referer_account=referer.referral_account, invitee=user) except CustomUserModel.DoesNotExist: ... return user @@ -84,9 +84,7 @@ class UserService: def create_user_telegram(self, request: Request) -> TelegramUser: serializer = NewUserTelegramSerializer(data=request.data) serializer.is_valid(raise_exception=True) - telegram_user = TelegramUser.objects.create( - **serializer.validated_data, user=self.user - ) + telegram_user = TelegramUser.objects.create(**serializer.validated_data, user=self.user) return telegram_user @classmethod @@ -210,10 +208,7 @@ class UserService: serializer = ChangePasswordSerializer(data=request.data) serializer.is_valid(raise_exception=True) - if ( - serializer.validated_data['password_1'] - != serializer.validated_data['password_2'] - ): + if serializer.validated_data['password_1'] != serializer.validated_data['password_2']: raise Exception(_('Passwords do not match')) user = token.user @@ -235,7 +230,9 @@ class UserService: new_profile_picture = serializer.validated_data.get('profile_picture') if new_profile_picture is not None: new_picture_name = MinIOService().put_object( - new_profile_picture, f'{self.user.username}.png', 'air-profiles' + new_profile_picture, + f'{self.user.username}.png', + 'air-profiles', ) self.user.profile_picture_name = new_picture_name @@ -252,10 +249,7 @@ class UserService: if user is None: raise Exception(_('Current password is wrong')) - if ( - serializer.validated_data['password_1'] - != serializer.validated_data['password_2'] - ): + if serializer.validated_data['password_1'] != serializer.validated_data['password_2']: raise Exception("Passwords don't match") self.user.set_password(serializer.validated_data['password_1']) @@ -266,9 +260,7 @@ class UserService: serializer.is_valid(raise_exception=True) img = serializer.validated_data['new_picture'] - img_name = MinIOService().put_object( - img, f'{self.user.username}.png', 'air-profiles' - ) + img_name = MinIOService().put_object(img, f'{self.user.username}.png', 'air-profiles') self.user.profile_picture_name = img_name self.user.save() @@ -279,21 +271,15 @@ class UserService: @classmethod def exists_in_whitelist(cls, request: Request) -> bool: - return PolicyWhitelist.objects.filter( - emails__contains=[request.query_params['email']] - ).exists() + return PolicyWhitelist.objects.filter(emails__contains=[request.query_params['email']]).exists() @classmethod def list_settings(cls, filters: Q = Q()) -> QuerySet[UserSetting]: return UserSetting.objects.filter(filters) @classmethod - def add_setting( - cls, user_id: UUID, device: str, type: str, value: Any - ) -> UserSetting: - return UserSetting.objects.create( - user_id=user_id, device=device, type=type, value=value - ) + def add_setting(cls, user_id: UUID, device: str, type: str, value: Any) -> UserSetting: + return UserSetting.objects.create(user_id=user_id, device=device, type=type, value=value) @classmethod def update_setting(cls, setting_id: UUID, value: Any) -> None: @@ -312,7 +298,6 @@ def update_profile_picture_social(*args, **kwargs): # Yandex Picture elif response.get('default_avatar_id', None): air_user.profile_picture_name = ( - f"https://avatars.yandex.net/get-yapic/" - f"{response['default_avatar_id']}/islands-retina-50" + f'https://avatars.yandex.net/get-yapic/{response["default_avatar_id"]}/islands-retina-50' ) air_user.save() @@ -165,7 +165,12 @@ class CustomUserModelAdmin(UserAdmin, ExportActionModelAdmin): sheet.append(['Email', 'Дата регистрации', 'Текущий баланс', 'Тип аккаунта']) for user in qs: sheet.append( - [user.email, f'{user.created_at}', user.balance, user.account_type] + [ + user.email, + f'{user.created_at}', + user.balance, + user.account_type, + ] ) response = HttpResponse(content_type='application/ms-excel') response['Content-Disposition'] = 'attachment; filename=users_month_info.xlsx' @@ -178,9 +183,7 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): actions = ['download_statistics_month', 'download_statistics_week'] @admin.action(description='Скачать месячный отчет по выбранным Вайт-листам') - def download_statistics_month( - self, request, qs: QuerySet[PolicyWhitelist], *args, **kwargs - ): + def download_statistics_month(self, request, qs: QuerySet[PolicyWhitelist], *args, **kwargs): end = timezone.now() start = end - timedelta(days=30) wb = Workbook() @@ -199,15 +202,11 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): obj['uid'] for obj in list( chain( - Image.objects.filter( - user__email__in=qs.values('emails')[0]['emails'] - ).values('uid'), - Chat.objects.filter( - user__email__in=qs.values('emails')[0]['emails'] - ).values('uid'), - Copywrite.objects.filter( - user__email__in=qs.values('emails')[0]['emails'] - ).values('uid'), + Image.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values('uid'), + Chat.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values('uid'), + Copywrite.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values( + 'uid' + ), ) ) ], @@ -230,16 +229,12 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): ] ) response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = ( - 'attachment; filename=whitelists_month_info.xlsx' - ) + response['Content-Disposition'] = 'attachment; filename=whitelists_month_info.xlsx' wb.save(response) return response @admin.action(description='Скачать недельный отчет по выбранным Вайт-листам') - def download_statistics_week( - self, request, qs: list[PolicyWhitelist], *args, **kwargs - ): + def download_statistics_week(self, request, qs: list[PolicyWhitelist], *args, **kwargs): end = timezone.now() start = end - timedelta(days=7) wb = Workbook() @@ -258,15 +253,11 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): obj['uid'] for obj in list( chain( - Image.objects.filter( - user__email__in=qs.values('emails')[0]['emails'] - ).values('uid'), - Chat.objects.filter( - user__email__in=qs.values('emails')[0]['emails'] - ).values('uid'), - Copywrite.objects.filter( - user__email__in=qs.values('emails')[0]['emails'] - ).values('uid'), + Image.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values('uid'), + Chat.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values('uid'), + Copywrite.objects.filter(user__email__in=qs.values('emails')[0]['emails']).values( + 'uid' + ), ) ) ], @@ -289,9 +280,7 @@ class PolicyWhitelistAdmin(admin.ModelAdmin): ] ) response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = ( - 'attachment; filename=PolicyWhitelists_month_info.xlsx' - ) + response['Content-Disposition'] = 'attachment; filename=PolicyWhitelists_month_info.xlsx' wb.save(response) return response @@ -351,8 +340,8 @@ class UTMAdmin(admin.ModelAdmin): for utm in qs: sheet.append( [ - f"{utm.utm_source if utm.utm_source else 'Источник отсутствует'}: " - f"{utm.utm_campaign if utm.utm_campaign else 'Компания отсутствует'}: ", + f'{utm.utm_source if utm.utm_source else "Источник отсутствует"}: ' + f'{utm.utm_campaign if utm.utm_campaign else "Компания отсутствует"}: ', PaymentSelector.count_by_utm(utm=utm, from_date=start, to_date=end), PaymentSelector.sum_by_utm(utm=utm, from_date=start, to_date=end), utm.users.filter(created_at__date__range=[start, end]).count(), @@ -411,13 +400,16 @@ class CompanyIPInline(admin.TabularInline): class CompanyIPWhitelistAdmin(admin.ModelAdmin): inlines = [CompanyIPInline] - def log_change( - self, request: HttpRequest, object: Any, message: list[dict[str, any]] - ) -> LogEntry: + def log_change(self, request: HttpRequest, object: Any, message: list[dict[str, any]]) -> LogEntry: ct = ContentType.objects.get_for_model(object, for_concrete_model=False) return [ LogEntry.objects.log_action( - request.user.pk, ct.pk, object.pk, str(object), 1, [message_entry] + request.user.pk, + ct.pk, + object.pk, + str(object), + 1, + [message_entry], ) for message_entry in message if not message_entry.get('changed', None) @@ -3,7 +3,9 @@ from rest_framework.request import Request from rest_framework.views import APIView from authentication.models.choices import AccountPrivileges -from authentication.selectors.account_status_selector import AccountStatusSelector +from authentication.selectors.account_status_selector import ( + AccountStatusSelector, +) from backend import settings @@ -37,4 +37,10 @@ class ReferralUserResource(ModelResource): class Meta: model = CustomUserModel - fields = ('email', 'balance', 'created_at', 'referer', 'payments_count') + fields = ( + 'email', + 'balance', + 'created_at', + 'referer', + 'payments_count', + ) @@ -16,10 +16,7 @@ class SyncAuthBearer(HttpBearer): try: user_payload = async_to_sync(TokenService.decode)(token=token) return CustomUserModel.objects.get( - **{ - key: user_payload[f'{key}'] - for key in settings.JWT_SETTINGS['encode_attributes'] - } + **{key: user_payload[f'{key}'] for key in settings.JWT_SETTINGS['encode_attributes']} ) except InvalidToken: access = AccessToken.objects.prefetch_related('user').get(token=token) @@ -188,9 +188,7 @@ class BusinessAccountDataSerializer(serializers.Serializer): class BusinessHostUpdateSerializer(serializers.Serializer): - token_cap_emails = serializers.ListField( - child=serializers.EmailField(), required=False - ) + token_cap_emails = serializers.ListField(child=serializers.EmailField(), required=False) token_cap_enabled = serializers.BooleanField(required=False) @@ -255,9 +253,7 @@ class BusinessHostSerializer(serializers.Serializer): corporate_phone = serializers.CharField(required=False) job_title = serializers.CharField(required=False) worker_amount = serializers.SerializerMethodField() - is_ip_whitelist_enabled = serializers.BooleanField( - read_only=True, source='ip_whitelist.is_enabled' - ) + is_ip_whitelist_enabled = serializers.BooleanField(read_only=True, source='ip_whitelist.is_enabled') is_log_history_enabled = serializers.BooleanField(read_only=True) token_cap_emails = serializers.ListField(child=serializers.EmailField()) token_cap_enabled = serializers.BooleanField() @@ -335,8 +331,4 @@ class LogSerializer(serializers.ModelSerializer): @extend_schema_field(OpenApiTypes.STR) def _user(self, obj: LogEntry): - return ( - 'Сотрудник AIR' - if obj.content_type.model == 'companyipwhitelist' - else obj.user.email - ) + return 'Сотрудник AIR' if obj.content_type.model == 'companyipwhitelist' else obj.user.email @@ -6,40 +6,74 @@ from authentication import views urlpatterns = [ path('register', views.UserAPIView.as_view(), name='new-user'), path( - 'mail/resend', view=views.EmailRegisterResendView.as_view(), name='email/resend' + 'mail/resend', + view=views.EmailRegisterResendView.as_view(), + name='email/resend', + ), + path( + 'mail/whitelist', + view=views.MailWhitelist.as_view(), + name='mail-whitelist', ), - path('mail/whitelist', view=views.MailWhitelist.as_view(), name='mail-whitelist'), path('me', views.UserAPIView.as_view(), name='me'), path( - 'register-telegram', views.UserTelegramAPIView.as_view(), name='new-user-telegram' + 'register-telegram', + views.UserTelegramAPIView.as_view(), + name='new-user-telegram', ), path('register/vk', views.UserVKAPIView.as_view(), name='new-user-vk'), path('login', views.UserLoginAPIView.as_view(), name='ml_model-login'), path('login-social/', include('drf_social_oauth2.urls', namespace='drf')), path( - 'login-from-token', views.LoginFromTokenAPIView.as_view(), name='login-from-token' + 'login-from-token', + views.LoginFromTokenAPIView.as_view(), + name='login-from-token', + ), + path( + 'login-telegram', + views.UserLoginTelegramView.as_view(), + name='login-telegram', ), - path('login-telegram', views.UserLoginTelegramView.as_view(), name='login-telegram'), path('login/vk', views.UserLoginVKView.as_view(), name='login-vk'), path('logout', views.UserLogoutAPIView.as_view(), name='logout'), path('remove', views.DeleteUserAPIView.as_view(), name='remove'), path('confirm', views.ConfirmUserAPIView.as_view(), name='confirm-user'), - path('change-pass', views.ChangePasswordAPIView.as_view(), name='change-password'), + path( + 'change-pass', + views.ChangePasswordAPIView.as_view(), + name='change-password', + ), path( 'update-pass', views.RequestPasswordChangeAPIView.as_view(), name='request-password-update', ), - path('user-data', views.UpdateUserDataAPIView.as_view(), name='update-user-data'), - path('email-sub', views.UpdateEmailSubscriptionAPIView.as_view(), name='email-sub'), - path('reset-pass', views.UpdatePasswordAPIView.as_view(), name='update-password'), + path( + 'user-data', + views.UpdateUserDataAPIView.as_view(), + name='update-user-data', + ), + path( + 'email-sub', + views.UpdateEmailSubscriptionAPIView.as_view(), + name='email-sub', + ), + path( + 'reset-pass', + views.UpdatePasswordAPIView.as_view(), + name='update-password', + ), path( 'reset-profile-pic', views.UpdateProfilePictureAPIView.as_view(), name='update-profile-pic', ), path('token/refresh', TokenRefreshView.as_view(), name='refresh-jwt'), - path('business-host', views.BusinessHostAPIView.as_view(), name='business-host'), + path( + 'business-host', + views.BusinessHostAPIView.as_view(), + name='business-host', + ), path( 'business-host/create', views.StartHostRegistrationAPIView.as_view(), @@ -52,7 +86,9 @@ urlpatterns = [ ), path('business-host/logs/', views.LogsAPIView.as_view()), path( - 'business-host/accounts', views.HostWorkersAPIView.as_view(), name='hosts-workers' + 'business-host/accounts', + views.HostWorkersAPIView.as_view(), + name='hosts-workers', ), path('business-host/download-expenses', views.ExtractHostExpenses.as_view()), path( @@ -83,7 +119,10 @@ urlpatterns = [ name='download-business-report', ), path('business-groups/', views.BusinessGroupsAPIView.as_view()), - path('business-groups//', views.BusinessGroupAPIView.as_view()), + path( + 'business-groups//', + views.BusinessGroupAPIView.as_view(), + ), path( 'business-groups//accounts//', views.BusinessGroupAccountsAPIView.as_view(), @@ -19,7 +19,9 @@ from rest_framework.response import Response from rest_framework.views import APIView from authentication.exceptions import BaseAlready -from authentication.exceptions.business_host_exceptions.not_allowed_ip import NotAllowedIP +from authentication.exceptions.business_host_exceptions.not_allowed_ip import ( + NotAllowedIP, +) from authentication.models.business_group import BusinessGroup from authentication.models.business_host import BusinessUserHost from authentication.models.choices import AccountPrivileges, InvitationStatus @@ -31,7 +33,9 @@ from authentication.permissions import ( IsTelegramAirBot, IsVKMiniApp, ) -from authentication.selectors.business_host_selector import BusinessHostSelector +from authentication.selectors.business_host_selector import ( + BusinessHostSelector, +) from authentication.selectors.user_selector import UserSelector from authentication.serializers import ( AccountDataUpdateSerializer, @@ -64,7 +68,9 @@ from authentication.serializers import ( UpdateUserDataSerializer, UserDataSerializer, ) -from authentication.services.business_account_service import BusinessAccountService +from authentication.services.business_account_service import ( + BusinessAccountService, +) from authentication.services.business_host_service import BusinessHostService from authentication.services.email_token_service import EmailTokenService from authentication.services.user_services import EmailService, UserService @@ -91,9 +97,7 @@ class UserLoginAPIView(APIView): logger.exception(err) return Response( {'detail': f'{err}'}, - status=status.HTTP_400_BAD_REQUEST - if err != NotAllowedIP - else status.HTTP_403_FORBIDDEN, + status=status.HTTP_400_BAD_REQUEST if err != NotAllowedIP else status.HTTP_403_FORBIDDEN, ) @@ -128,7 +132,8 @@ class UserLoginTelegramView(APIView): permission_classes = (IsTelegramAirBot,) @extend_schema( - request=LoginTelegramUserSerializer, responses={200: UserDataSerializer} + request=LoginTelegramUserSerializer, + responses={200: UserDataSerializer}, ) def post(self, request, *args, **kwargs): """Login user from Telegram Bot by Telegram ID. Bot rights required.""" @@ -160,7 +165,8 @@ class UserLogoutAPIView(APIView): try: UserService(self.request.user).logout_user(request) return Response( - {'detail': 'User logout successfully'}, status=status.HTTP_200_OK + {'detail': 'User logout successfully'}, + status=status.HTTP_200_OK, ) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -237,9 +243,7 @@ class UserStatusAPIView(APIView): """Get user status: regular, business host, business admin or business account""" try: user_status = UserSelector(self.request.user).check_account_type() - response = AccountStatusSerializer( - self.request.user, context={'status': user_status} - ) + response = AccountStatusSerializer(self.request.user, context={'status': user_status}) return Response(response.data, status=status.HTTP_200_OK) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -272,13 +276,12 @@ class BusinessHostAPIView(APIView): return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @extend_schema( - request=BusinessHostUpdateSerializer, responses={200: BusinessHostSerializer} + request=BusinessHostUpdateSerializer, + responses={200: BusinessHostSerializer}, ) def put(self, request, *args, **kwargs): try: - result = BusinessHostService(self.request.user).update_self( - request, serialize=True - ) + result = BusinessHostService(self.request.user).update_self(request, serialize=True) return Response(result.data, status=status.HTTP_200_OK) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -299,7 +302,9 @@ class HostWorkersAPIView(APIView): @extend_schema( parameters=[ OpenApiParameter( - 'type', enum=[value for value, _ in AccountPrivileges.choices], many=True + 'type', + enum=[value for value, _ in AccountPrivileges.choices], + many=True, ), OpenApiParameter('have_group', bool), ], @@ -379,7 +384,8 @@ class ChangePasswordAPIView(APIView): permission_classes = (IsAnonymous,) @extend_schema( - parameters=[OpenApiParameter('token', str)], request=UpdatePasswordSerializer + parameters=[OpenApiParameter('token', str)], + request=UpdatePasswordSerializer, ) def post(self, request, *args, **kwargs): """Edit user password from email.""" @@ -438,7 +444,8 @@ class UpdateProfilePictureAPIView(APIView): try: UserService(self.request.user).update_profile_picture(request) return Response( - {'detail': 'profile picture updated'}, status=status.HTTP_200_OK + {'detail': 'profile picture updated'}, + status=status.HTTP_200_OK, ) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -516,9 +523,7 @@ class AllowedHostModelsAPIView(APIView): def delete(self, request, *args, **kwargs): """Delete all allowed models for company user.""" try: - result = BusinessHostService(self.request.user).remove_available_models( - request - ) + result = BusinessHostService(self.request.user).remove_available_models(request) return Response(result.data, status=status.HTTP_200_OK) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -545,9 +550,7 @@ class AccountInvitationAPIView(APIView): """Change invitation to company from email - Accept.""" try: token = EmailTokenService.get_token(request.query_params.get('token')) - BusinessAccountService.from_user(token.user).update( - data=dict(status=InvitationStatus.ACCEPTED) - ) + BusinessAccountService.from_user(token.user).update(data=dict(status=InvitationStatus.ACCEPTED)) return redirect(settings.MAIN_SITE_URL) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -569,7 +572,8 @@ class ExtractBusinessAccountQueriesAPIView(APIView): start = datetime.fromisoformat( request.query_params.get( - 'start_date', (timezone.now() - timedelta(days=30)).strftime('%Y-%m-%d') + 'start_date', + (timezone.now() - timedelta(days=30)).strftime('%Y-%m-%d'), ) ) end = datetime.fromisoformat( @@ -621,9 +625,7 @@ class ExtractBusinessAccountQueriesAPIView(APIView): ] ) response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = ( - 'attachment; filename=business_accounts_info.xlsx' - ) + response['Content-Disposition'] = 'attachment; filename=business_accounts_info.xlsx' wb.save(response) return response @@ -656,7 +658,8 @@ class BusinessGroupsAPIView(APIView): return Response(BusinessGroupsSerializer(groups, many=True).data, status=200) @extend_schema( - request=BusinessGroupSerializer, responses={201: BusinessGroupSerializer} + request=BusinessGroupSerializer, + responses={201: BusinessGroupSerializer}, ) def post(self, request, *args, **kwargs): serializer = BusinessGroupSerializer(data=request.data) @@ -693,17 +696,13 @@ class BusinessGroupAPIView(APIView): class BusinessGroupAccountsAPIView(APIView): def post(self, request, group_id: UUID, acc_email: UUID, *args, **kwargs): group = BusinessGroup.objects.get(uid=group_id) - business_account = BusinessAccountService.from_user( - UserSelector.get_by_email(acc_email) - ).account + business_account = BusinessAccountService.from_user(UserSelector.get_by_email(acc_email)).account business_account.group = group business_account.save() return Response(status=status.HTTP_200_OK) def delete(self, request, group_id: UUID, acc_email: UUID, *args, **kwargs): - business_account = BusinessAccountService.from_user( - UserSelector.get_by_email(acc_email) - ).account + business_account = BusinessAccountService.from_user(UserSelector.get_by_email(acc_email)).account business_account.group = None business_account.save() return Response(status=status.HTTP_204_NO_CONTENT) @@ -723,9 +722,7 @@ class LogsAPIView(APIView): def get(self, request, *args, **kwargs): logs = LogEntry.objects.filter( action_time__range=[ - request.query_params.get( - 'from-date', timezone.now() - timedelta(days=365) - ), + request.query_params.get('from-date', timezone.now() - timedelta(days=365)), request.query_params.get('to-date', timezone.now()), ] ) @@ -739,9 +736,7 @@ class LogsAPIView(APIView): company.user, *[acc.user for acc in company.accounts.filter(account_privileges='admin')], ] - keys = [ - order['uid'] for order in APIKey.objects.filter(user__in=users).values('uid') - ] + keys = [order['uid'] for order in APIKey.objects.filter(user__in=users).values('uid')] whitelist = company.ip_whitelist match request.query_params.get('log-identity'): case 'public-api': @@ -759,7 +754,10 @@ class LogsAPIView(APIView): case _: logs = logs.filter( object_id__in=[whitelist.pk, *keys], - content_type__app_label__in=['public_api', 'authentication'], + content_type__app_label__in=[ + 'public_api', + 'authentication', + ], content_type__model__in=['apikey', 'companyipwhitelist'], ) return Response(LogSerializer(logs, many=True).data) @@ -783,9 +781,7 @@ class ExtractHostExpenses(APIView): *[acc['user'] for acc in company.accounts.values('user')], ], created_at__range=[ - request.query_params.get( - 'from_date', timezone.now() - timedelta(days=30) - ), + request.query_params.get('from_date', timezone.now() - timedelta(days=30)), request.query_params.get('to_date', timezone.now()), ], ) @@ -796,9 +792,7 @@ class ExtractHostExpenses(APIView): *[acc['user'] for acc in company.accounts.values('user')], ], created_at__range=[ - request.query_params.get( - 'from_date', timezone.now() - timedelta(days=30) - ), + request.query_params.get('from_date', timezone.now() - timedelta(days=30)), request.query_params.get('to_date', timezone.now()), ], ) @@ -819,8 +813,6 @@ class ExtractHostExpenses(APIView): ) response = HttpResponse(content_type='application/ms-excel') - response['Content-Disposition'] = ( - 'attachment; filename=business_accounts_info.xlsx' - ) + response['Content-Disposition'] = 'attachment; filename=business_accounts_info.xlsx' wb.save(response) return response @@ -140,8 +140,7 @@ SOCIALACCOUNT_EMAIL_VERIFICATION = 'none' SOCIALACCOUNT_EMAIL_REQUIRED = False AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.' - 'password_validation.UserAttributeSimilarityValidator', + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', @@ -241,9 +240,7 @@ MINIO_PORT = env.str('MINIO_PORT', default='9000') MINIO_ENDPOINT = env.str('MINIO_ENDPOINT') MINIO_USE_HTTPS = env.bool('MINIO_USE_HTTPS', default=False) MINIO_EXTERNAL_ENDPOINT = env.str('MINIO_EXTERNAL_ENDPOINT', default='localhost:9000') -MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = env.bool( - 'MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=False -) +MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = env.bool('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=False) MINIO_ACCESS_KEY = env.str('MINIO_ACCESS_KEY') MINIO_SECRET_KEY = env.str('MINIO_SECRET_KEY') @@ -334,9 +331,7 @@ UPSCALE_MULTIPLIER_HOST = env.str('UPSCALE_MULTIPLIER_HOST', 'packet:8080') # Payments YOOKASSA_ACCOUNT_ID = env.str('YOOKASSA_ACCOUNT_ID', default='defaultapikey') YOOKASSA_SECRET_KEY = env.str('YOOKASSA_SECRET_KEY', default='defaultapikey') -YOOKASSA_RESULT_PAYMENT_URL = env.str( - 'YOOKASSA_RESULT_PAYMENT_URL', default='defaultapikey' -) +YOOKASSA_RESULT_PAYMENT_URL = env.str('YOOKASSA_RESULT_PAYMENT_URL', default='defaultapikey') RECURRENT_RATE = env.str('RECURRENT_RATE', 'days') # Email EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' @@ -349,12 +344,8 @@ EMAIL_HOST_PASSWORD = env.str('EMAIL_HOST_PASSWORD', default='defaultpass') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER USER_CONFIRMATION_URL = env.str('USER_CONFIRMATION_URL', default='http://localhost:3000') -USER_PASSWORD_RESET_URL = env.str( - 'USER_PASSWORD_RESET_URL', default='http://localhost:3000' -) -INVITATION_RESPONSE_URL = env.str( - 'INVITATION_RESPONSE_URL', default='http://localhost:3000' -) +USER_PASSWORD_RESET_URL = env.str('USER_PASSWORD_RESET_URL', default='http://localhost:3000') +INVITATION_RESPONSE_URL = env.str('INVITATION_RESPONSE_URL', default='http://localhost:3000') MAIN_SITE_URL = env.str('MAIN_SITE_URL', default='http://localhost:3000') @@ -381,7 +372,12 @@ ADMIN_SETTINGS = { 'media', 'copywrite', ], - 'excludes': ['django_celery_beat', 'social_django', 'authtoken', 'auth'], + 'excludes': [ + 'django_celery_beat', + 'social_django', + 'authtoken', + 'auth', + ], }, } @@ -9,7 +9,11 @@ from django.utils.translation import gettext_lazy as _ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from ninja import NinjaAPI -from authentication.exceptions import InvalidPassword, InvalidToken, InvalidUsername +from authentication.exceptions import ( + InvalidPassword, + InvalidToken, + InvalidUsername, +) from backend.public import urlpatterns as public_urlpatterns api = NinjaAPI(title='AIR API', version='1.0.0') @@ -24,9 +28,7 @@ logger = logging.getLogger(__name__) @api.exception_handler(ObjectDoesNotExist) def object_does_not_exists_error_handler(request, exc: ObjectDoesNotExist): logger.exception(exc) - return api.create_response( - request, {'message': _('Requested object does not exists')}, status=404 - ) + return api.create_response(request, {'message': _('Requested object does not exists')}, status=404) @api.exception_handler(InvalidToken) @@ -0,0 +1,10 @@ +from django.db import models +from django.forms import TextInput + + +class ColorField(models.CharField): + max_length = 6 + + def formfield(self, **kwargs): + kwargs['widget'] = TextInput(attrs={'type': 'color'}) + return super().formfield(**kwargs) @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-24 17:41+0300\n" +"POT-Creation-Date: 2025-03-30 03:00+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,7 +18,8 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: achievements/admin.py:11 achievements/models.py:19 stories/models.py:18 +#: achievements/admin.py:11 achievements/models.py:19 ml_model/models.py:47 +#: stories/models.py:18 msgid "Icon" msgstr "" @@ -30,21 +31,21 @@ msgstr "" msgid "Achievements" msgstr "" -#: achievements/models.py:14 ml_model/models.py:16 ml_model/models.py:38 -#: ml_model/models.py:145 +#: achievements/models.py:14 ml_model/models.py:19 ml_model/models.py:39 +#: ml_model/models.py:72 ml_model/models.py:180 msgid "Slug" msgstr "" -#: achievements/models.py:16 ml_model/models.py:36 ml_model/models.py:143 -#: ml_model/models.py:232 payments/models/payment.py:53 +#: achievements/models.py:16 ml_model/models.py:70 ml_model/models.py:179 +#: ml_model/models.py:268 payments/models/payment.py:52 msgid "Description" msgstr "" #: achievements/models.py:43 authentication/models/business_host.py:21 -#: authentication/models/email_token.py:12 authentication/models/user.py:229 -#: authentication/models/user.py:230 authentication/models/user_telegram.py:30 +#: authentication/models/email_token.py:12 authentication/models/user.py:241 +#: authentication/models/user.py:242 authentication/models/user_telegram.py:22 #: authentication/models/user_vk.py:12 payments/models/invoice.py:15 -#: payments/models/payment.py:26 payments/models/payment_plan.py:63 +#: payments/models/payment.py:26 payments/models/payment_plan.py:61 msgid "User" msgstr "" @@ -61,8 +62,8 @@ msgstr "" msgid "Issued achievement" msgstr "" -#: authentication/models/business_account.py:16 payments/models/promocode.py:69 -#: tools/public_api/models.py:35 +#: authentication/models/business_account.py:16 payments/models/promocode.py:72 +#: tools/public_api/models.py:33 msgid "Owner" msgstr "" @@ -85,7 +86,7 @@ msgid "Acceptance" msgstr "" #: authentication/models/business_account.py:44 -#: authentication/models/business_group.py:21 tools/public_api/models.py:45 +#: authentication/models/business_group.py:21 tools/public_api/models.py:43 msgid "Token limit" msgstr "" @@ -93,23 +94,23 @@ msgstr "" msgid "Group" msgstr "" -#: authentication/models/business_account.py:62 +#: authentication/models/business_account.py:61 msgid "" "Impossible to add this employee to this group which does not belong to this " "company" msgstr "" -#: authentication/models/business_account.py:72 +#: authentication/models/business_account.py:70 msgid "Child Business Account" msgstr "" -#: authentication/models/business_account.py:73 +#: authentication/models/business_account.py:71 msgid "Child Business Accounts" msgstr "" -#: authentication/models/business_group.py:8 ml_model/models.py:15 -#: ml_model/models.py:35 ml_model/models.py:230 -#: payments/models/payment_plan.py:28 stories/models.py:12 stories/models.py:35 +#: authentication/models/business_group.py:8 ml_model/models.py:18 +#: ml_model/models.py:38 ml_model/models.py:63 ml_model/models.py:267 +#: payments/models/payment_plan.py:27 stories/models.py:12 stories/models.py:35 #: tools/chats/models.py:9 msgid "Title" msgstr "" @@ -126,9 +127,9 @@ msgstr "" msgid "Affiliated by" msgstr "" -#: authentication/models/business_host.py:35 authentication/models/user.py:132 -#: authentication/models/whitelist.py:16 ml_model/models.py:125 -#: payments/models/promocode.py:83 +#: authentication/models/business_host.py:35 authentication/models/user.py:147 +#: authentication/models/whitelist.py:16 ml_model/models.py:165 +#: payments/models/promocode.py:85 msgid "Is active" msgstr "" @@ -136,68 +137,68 @@ msgstr "" msgid "Sector" msgstr "" -#: authentication/models/business_host.py:45 +#: authentication/models/business_host.py:44 msgid "Planned amount of workers" msgstr "" -#: authentication/models/business_host.py:51 +#: authentication/models/business_host.py:49 msgid "Usage intensity" msgstr "" -#: authentication/models/business_host.py:58 +#: authentication/models/business_host.py:56 msgid "Token low balance cap" msgstr "" -#: authentication/models/business_host.py:63 +#: authentication/models/business_host.py:61 msgid "Emails token low balance cap" msgstr "" -#: authentication/models/business_host.py:66 +#: authentication/models/business_host.py:63 msgid "Token low balance cap enabled" msgstr "" -#: authentication/models/business_host.py:70 +#: authentication/models/business_host.py:66 msgid "ITN" msgstr "" -#: authentication/models/business_host.py:71 +#: authentication/models/business_host.py:67 msgid "PSRN" msgstr "" -#: authentication/models/business_host.py:73 ml_model/models.py:141 -#: tools/public_api/models.py:31 +#: authentication/models/business_host.py:68 ml_model/models.py:178 +#: tools/public_api/models.py:30 msgid "Name" msgstr "" -#: authentication/models/business_host.py:78 +#: authentication/models/business_host.py:71 msgid "Preffered name" msgstr "" -#: authentication/models/business_host.py:81 +#: authentication/models/business_host.py:72 msgid "Corporate email" msgstr "" -#: authentication/models/business_host.py:84 +#: authentication/models/business_host.py:74 msgid "Corporate phone" msgstr "" -#: authentication/models/business_host.py:87 +#: authentication/models/business_host.py:76 msgid "Job title" msgstr "" -#: authentication/models/business_host.py:93 +#: authentication/models/business_host.py:81 msgid "Allowed models" msgstr "" -#: authentication/models/business_host.py:97 +#: authentication/models/business_host.py:84 msgid "Log history enabled" msgstr "" -#: authentication/models/business_host.py:117 +#: authentication/models/business_host.py:103 msgid "Business Account" msgstr "" -#: authentication/models/business_host.py:118 +#: authentication/models/business_host.py:104 msgid "Business Accounts" msgstr "" @@ -265,7 +266,7 @@ msgstr "" msgid "Security" msgstr "" -#: authentication/models/email_token.py:15 ml_model/models.py:234 +#: authentication/models/email_token.py:15 ml_model/models.py:269 msgid "Key" msgstr "" @@ -277,43 +278,43 @@ msgstr "" msgid "Email Tokens" msgstr "" -#: authentication/models/user.py:109 authentication/models/user_telegram.py:10 +#: authentication/models/user.py:120 authentication/models/user_telegram.py:9 msgid "First name" msgstr "" -#: authentication/models/user.py:116 authentication/models/user_telegram.py:13 +#: authentication/models/user.py:127 authentication/models/user_telegram.py:10 msgid "Last name" msgstr "" -#: authentication/models/user.py:119 authentication/models/user_telegram.py:16 +#: authentication/models/user.py:134 authentication/models/user_telegram.py:11 msgid "Username" msgstr "" -#: authentication/models/user.py:126 +#: authentication/models/user.py:141 msgid "Email" msgstr "" -#: authentication/models/user.py:134 +#: authentication/models/user.py:149 msgid "Is staff" msgstr "" -#: authentication/models/user.py:135 +#: authentication/models/user.py:150 msgid "Is superuser" msgstr "" -#: authentication/models/user.py:136 +#: authentication/models/user.py:151 msgid "Is email confirmed" msgstr "" -#: authentication/models/user.py:138 +#: authentication/models/user.py:152 msgid "Is subscribed" msgstr "" -#: authentication/models/user.py:145 +#: authentication/models/user.py:158 msgid "Picture name" msgstr "" -#: authentication/models/user.py:154 authentication/models/utm.py:21 +#: authentication/models/user.py:167 authentication/models/utm.py:21 msgid "UTM" msgstr "" @@ -325,38 +326,38 @@ msgstr "" msgid "Is bot" msgstr "" -#: authentication/models/user_telegram.py:19 +#: authentication/models/user_telegram.py:12 msgid "Language" msgstr "" -#: authentication/models/user_telegram.py:21 +#: authentication/models/user_telegram.py:13 msgid "Is premium" msgstr "" -#: authentication/models/user_telegram.py:23 +#: authentication/models/user_telegram.py:15 msgid "Is subscribed to channel" msgstr "" -#: authentication/models/user_telegram.py:34 +#: authentication/models/user_telegram.py:25 msgid "Phonenumber" msgstr "" -#: authentication/models/user_telegram.py:37 +#: authentication/models/user_telegram.py:27 #: authentication/models/user_vk.py:14 payments/models/invoice.py:11 -#: stories/models.py:15 tools/chats/models.py:11 +#: stories/models.py:15 tools/chats/models.py:10 msgid "Created at" msgstr "" -#: authentication/models/user_telegram.py:38 +#: authentication/models/user_telegram.py:28 #: authentication/models/user_vk.py:15 msgid "Updated at" msgstr "" -#: authentication/models/user_telegram.py:44 +#: authentication/models/user_telegram.py:34 msgid "Telegram User" msgstr "" -#: authentication/models/user_telegram.py:45 +#: authentication/models/user_telegram.py:35 msgid "Telegram Users" msgstr "" @@ -400,360 +401,376 @@ msgstr "" msgid "Whitelists to cancel policies" msgstr "" -#: authentication/selectors/business_host_selector.py:38 -#: authentication/selectors/business_host_selector.py:83 +#: authentication/selectors/business_host_selector.py:42 +#: authentication/selectors/business_host_selector.py:85 msgid "You haven't rights to access host account information" msgstr "" -#: authentication/selectors/business_host_selector.py:54 +#: authentication/selectors/business_host_selector.py:60 msgid "Host user is not registered for this account" msgstr "" -#: authentication/selectors/user_selector.py:79 +#: authentication/selectors/user_selector.py:80 msgid "No user with this uid found" msgstr "" -#: authentication/services/business_account_service.py:54 +#: authentication/services/business_account_service.py:58 msgid "BusinessAccount for this user doesn't exist" msgstr "" -#: authentication/services/business_account_service.py:65 +#: authentication/services/business_account_service.py:68 msgid "Invited account can either accept or reject an invitation" msgstr "" -#: authentication/services/business_account_service.py:71 +#: authentication/services/business_account_service.py:73 msgid "Account is already confirmed" msgstr "" -#: authentication/services/business_host_service.py:148 +#: authentication/services/business_host_service.py:152 msgid "No user_email is provided" msgstr "" -#: authentication/services/business_host_service.py:203 +#: authentication/services/business_host_service.py:199 msgid "No business account by this uid at your company" msgstr "" -#: authentication/services/email_service.py:119 +#: authentication/services/email_service.py:115 msgid "Regular users cannot send introductory letters" msgstr "" -#: authentication/services/email_service.py:141 +#: authentication/services/email_service.py:137 msgid "Regular users cannot send invitation letters" msgstr "" -#: authentication/services/user_services.py:49 +#: authentication/services/user_services.py:51 msgid "New user data is invalid" msgstr "" -#: authentication/services/user_services.py:113 +#: authentication/services/user_services.py:111 msgid "Wrong email" msgstr "" -#: authentication/services/user_services.py:121 backend/urls.py:41 +#: authentication/services/user_services.py:119 backend/urls.py:43 msgid "Wrong password" msgstr "" -#: authentication/services/user_services.py:124 +#: authentication/services/user_services.py:122 msgid "User has not confirmed his email yet" msgstr "" -#: authentication/services/user_services.py:163 +#: authentication/services/user_services.py:161 msgid "No user like this in a database" msgstr "" -#: authentication/services/user_services.py:180 +#: authentication/services/user_services.py:178 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:184 +#: authentication/services/user_services.py:182 msgid "No user token like this in a database" msgstr "" -#: authentication/services/user_services.py:204 +#: authentication/services/user_services.py:202 msgid "No email token provided" msgstr "" -#: authentication/services/user_services.py:208 +#: authentication/services/user_services.py:206 msgid "No token like this in a database" msgstr "" -#: authentication/services/user_services.py:217 +#: authentication/services/user_services.py:212 msgid "Passwords do not match" msgstr "" -#: authentication/services/user_services.py:253 +#: authentication/services/user_services.py:250 msgid "Current password is wrong" msgstr "" -#: backend/urls.py:28 +#: backend/urls.py:31 msgid "Requested object does not exists" msgstr "" -#: backend/urls.py:35 +#: backend/urls.py:37 msgid "Token is invalid" msgstr "" -#: backend/urls.py:47 +#: backend/urls.py:49 msgid "Wrong username" msgstr "" -#: messages/serializers.py:39 +#: messages/serializers.py:42 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" msgstr "" -#: ml_model/apps.py:8 ml_model/models.py:104 +#: ml_model/apps.py:9 ml_model/models.py:146 msgid "Neuron Models" msgstr "" -#: ml_model/models.py:26 ml_model/models.py:46 +#: ml_model/models.py:29 ml_model/models.py:82 msgid "Category" msgstr "" -#: ml_model/models.py:27 +#: ml_model/models.py:30 msgid "Categories" msgstr "" #: ml_model/models.py:40 -msgid "Fill automatically, don't touch" +msgid "Color" +msgstr "" + +#: ml_model/models.py:44 +msgid "Not SVG-pictures not allowed" msgstr "" #: ml_model/models.py:54 +msgid "Model Tag" +msgstr "" + +#: ml_model/models.py:55 +msgid "Model Tags" +msgstr "" + +#: ml_model/models.py:68 +msgid "Alternative Titles" +msgstr "" + +#: ml_model/models.py:74 +msgid "Fill automatically, don't touch" +msgstr "" + +#: ml_model/models.py:90 msgid "Avatar" msgstr "" -#: ml_model/models.py:103 -msgid "Neuron Model" +#: ml_model/models.py:93 +msgid "Tags" msgstr "" -#: ml_model/models.py:112 -msgid "Model" +#: ml_model/models.py:145 +msgid "Neuron Model" msgstr "" -#: ml_model/models.py:129 -msgid "Authorization token" +#: ml_model/models.py:154 +msgid "Model" msgstr "" -#: ml_model/models.py:133 ml_model/models.py:134 +#: ml_model/models.py:170 ml_model/models.py:171 msgid "Settings" msgstr "" -#: ml_model/models.py:137 +#: ml_model/models.py:174 #, python-format msgid "Settings of %(model_title)s" msgstr "" -#: ml_model/models.py:146 -msgid "Default" -msgstr "" - -#: ml_model/models.py:159 +#: ml_model/models.py:193 #, python-format msgid "%(model_title)s | %(version_name)s" msgstr "" -#: ml_model/models.py:165 +#: ml_model/models.py:199 msgid "Model Version" msgstr "" -#: ml_model/models.py:166 +#: ml_model/models.py:200 msgid "Model Versions" msgstr "" -#: ml_model/models.py:175 +#: ml_model/models.py:209 msgid "Versions" msgstr "" -#: ml_model/models.py:176 +#: ml_model/models.py:210 msgid "Link to versions" msgstr "" -#: ml_model/models.py:185 reports/models/error_report.py:12 +#: ml_model/models.py:219 reports/models/error_report.py:10 msgid "Text" msgstr "" -#: ml_model/models.py:186 stories/models.py:36 +#: ml_model/models.py:220 stories/models.py:36 msgid "Image" msgstr "" -#: ml_model/models.py:187 +#: ml_model/models.py:221 msgid "PDF" msgstr "" -#: ml_model/models.py:188 +#: ml_model/models.py:222 msgid "DOCX" msgstr "" -#: ml_model/models.py:189 +#: ml_model/models.py:223 msgid "DOC" msgstr "" -#: ml_model/models.py:190 +#: ml_model/models.py:224 msgid "Text File (Notebook)" msgstr "" -#: ml_model/models.py:191 +#: ml_model/models.py:225 msgid "ZIP Archive" msgstr "" -#: ml_model/models.py:192 +#: ml_model/models.py:226 msgid "Audio" msgstr "" -#: ml_model/models.py:198 ml_model/models.py:236 +#: ml_model/models.py:232 ml_model/models.py:271 #: payments/models/promocode.py:41 msgid "Type" msgstr "" -#: ml_model/models.py:200 ml_model/models.py:249 +#: ml_model/models.py:234 ml_model/models.py:282 msgid "Required" msgstr "" -#: ml_model/models.py:203 +#: ml_model/models.py:237 #, python-format msgid "%(model_title)s | %(input_type)s" msgstr "" -#: ml_model/models.py:209 +#: ml_model/models.py:243 msgid "Model Input" msgstr "" -#: ml_model/models.py:210 +#: ml_model/models.py:244 msgid "Model Inputs" msgstr "" -#: ml_model/models.py:216 +#: ml_model/models.py:250 msgid "Integer" msgstr "" -#: ml_model/models.py:217 +#: ml_model/models.py:251 msgid "Float" msgstr "" -#: ml_model/models.py:218 +#: ml_model/models.py:252 msgid "String" msgstr "" -#: ml_model/models.py:219 +#: ml_model/models.py:255 msgid "List" msgstr "" -#: ml_model/models.py:222 +#: ml_model/models.py:259 msgid "Float range" msgstr "" -#: ml_model/models.py:226 +#: ml_model/models.py:263 msgid "Integer range" msgstr "" -#: ml_model/models.py:228 +#: ml_model/models.py:265 msgid "Logical" msgstr "" -#: ml_model/models.py:243 +#: ml_model/models.py:278 msgid "Values" msgstr "" -#: ml_model/models.py:245 +#: ml_model/models.py:279 msgid "" "These values can contain different interfaces and default value optional" msgstr "" -#: ml_model/models.py:248 +#: ml_model/models.py:281 msgid "Hidden" msgstr "" -#: ml_model/models.py:254 +#: ml_model/models.py:287 #, python-format msgid "Parameter of %(model_title)s" msgstr "" -#: ml_model/models.py:257 +#: ml_model/models.py:290 msgid "Parameter" msgstr "" -#: ml_model/models.py:258 +#: ml_model/models.py:291 msgid "Parameters" msgstr "" -#: ml_model/models.py:263 +#: ml_model/models.py:296 msgid "Fixed" msgstr "" -#: ml_model/models.py:264 +#: ml_model/models.py:297 msgid "Per generation second" msgstr "" -#: ml_model/models.py:265 +#: ml_model/models.py:298 msgid "Per one text token" msgstr "" -#: ml_model/models.py:266 +#: ml_model/models.py:299 msgid "Per image pixel" msgstr "" -#: ml_model/models.py:269 +#: ml_model/models.py:302 msgid "By input data" msgstr "" -#: ml_model/models.py:270 +#: ml_model/models.py:303 msgid "By output data" msgstr "" -#: ml_model/models.py:271 +#: ml_model/models.py:304 msgid "By all data" msgstr "" -#: ml_model/models.py:274 +#: ml_model/models.py:309 msgid "Strategy" msgstr "" -#: ml_model/models.py:279 +#: ml_model/models.py:314 msgid "Interaction Type" msgstr "" -#: ml_model/models.py:284 payments/models/invoice.py:19 +#: ml_model/models.py:319 payments/models/invoice.py:19 msgid "Cost" msgstr "" -#: ml_model/models.py:285 +#: ml_model/models.py:320 msgid "In RUB, per specified strategy" msgstr "" -#: ml_model/models.py:290 +#: ml_model/models.py:325 msgid "Coefficient" msgstr "" -#: ml_model/models.py:291 +#: ml_model/models.py:326 msgid "Cost multiplier" msgstr "" -#: ml_model/models.py:298 +#: ml_model/models.py:333 msgid "Rate" msgstr "" -#: ml_model/models.py:302 +#: ml_model/models.py:337 msgid "Payment Rule" msgstr "" -#: ml_model/models.py:303 +#: ml_model/models.py:338 msgid "Payment Rules" msgstr "" -#: ml_model/selectors/ml_models_selector.py:79 +#: ml_model/selectors/ml_models_selector.py:75 msgid "no model by this id" msgstr "" #: ml_model/services/minio_service.py:35 ml_model/services/minio_service.py:53 -#: ml_model/services/minio_service.py:65 ml_model/services/minio_service.py:74 +#: ml_model/services/minio_service.py:61 ml_model/services/minio_service.py:70 msgid "Unknown bucket destination" msgstr "" -#: ml_model/services/upscaleai.py:123 +#: ml_model/services/upscaleai.py:124 msgid "No image given for improving" msgstr "" -#: payments/apps.py:9 payments/models/payment.py:62 +#: payments/apps.py:9 payments/models/payment.py:60 msgid "Payments" msgstr "" @@ -789,7 +806,7 @@ msgstr "" msgid "Status" msgstr "" -#: payments/models/payment.py:61 +#: payments/models/payment.py:59 msgid "Payment" msgstr "" @@ -809,87 +826,87 @@ msgstr "" msgid "Is recurrent" msgstr "" -#: payments/models/payment_plan.py:31 +#: payments/models/payment_plan.py:29 msgid "Duration" msgstr "" -#: payments/models/payment_plan.py:36 +#: payments/models/payment_plan.py:34 msgid "Is visible" msgstr "" -#: payments/models/payment_plan.py:54 payments/models/payment_plan.py:69 +#: payments/models/payment_plan.py:52 payments/models/payment_plan.py:67 msgid "Payment Plan" msgstr "" -#: payments/models/payment_plan.py:55 +#: payments/models/payment_plan.py:53 msgid "Payment Plans" msgstr "" -#: payments/models/payment_plan.py:71 +#: payments/models/payment_plan.py:69 msgid "Last payment at" msgstr "" -#: payments/models/payment_plan.py:72 +#: payments/models/payment_plan.py:70 msgid "Next payment at" msgstr "" -#: payments/models/payment_plan.py:74 +#: payments/models/payment_plan.py:72 msgid "Current balance" msgstr "" -#: payments/models/payment_plan.py:80 +#: payments/models/payment_plan.py:78 msgid "Recurrent billing task" msgstr "" -#: payments/models/payment_plan.py:97 payments/models/payment_plan.py:98 +#: payments/models/payment_plan.py:99 payments/models/payment_plan.py:100 msgid "User Balance" msgstr "" -#: payments/models/promocode.py:45 +#: payments/models/promocode.py:48 msgid "Action Function" msgstr "" -#: payments/models/promocode.py:70 +#: payments/models/promocode.py:73 msgid "Can be only for referral promos" msgstr "" -#: payments/models/promocode.py:78 +#: payments/models/promocode.py:81 msgid "Is personal" msgstr "" -#: payments/models/promocode.py:79 +#: payments/models/promocode.py:82 msgid "Can be activated only one time" msgstr "" -#: payments/models/promocode.py:88 +#: payments/models/promocode.py:89 msgid "Owner can be only for refferal promos" msgstr "" -#: payments/models/promocode.py:96 payments/models/promocode.py:107 +#: payments/models/promocode.py:97 payments/models/promocode.py:107 msgid "Promocode" msgstr "" -#: payments/models/promocode.py:97 +#: payments/models/promocode.py:98 msgid "Promocodes" msgstr "" -#: payments/models/promocode.py:104 +#: payments/models/promocode.py:105 msgid "Activated by" msgstr "" -#: payments/models/promocode.py:138 +#: payments/models/promocode.py:137 msgid "Promocode Activation" msgstr "" -#: payments/models/promocode.py:139 +#: payments/models/promocode.py:138 msgid "Promocode Activations" msgstr "" -#: payments/models/user_payment_method.py:28 +#: payments/models/user_payment_method.py:24 msgid "Payment Method" msgstr "" -#: payments/models/user_payment_method.py:29 +#: payments/models/user_payment_method.py:25 msgid "Payment Methods" msgstr "" @@ -897,15 +914,15 @@ msgstr "" msgid "Messages for this model are not registered in a selector" msgstr "" -#: payments/selectors/payment_plan_selector.py:29 +#: payments/selectors/payment_plan_selector.py:32 msgid "Business accounts are not allowed to make purchases" msgstr "" -#: payments/selectors/payment_plan_selector.py:54 +#: payments/selectors/payment_plan_selector.py:57 msgid "No plan by this uid" msgstr "" -#: payments/services/model_billing_service.py:27 +#: payments/services/model_billing_service.py:31 msgid "Unknown account type" msgstr "" @@ -933,19 +950,19 @@ msgstr "" msgid "Proxies" msgstr "" -#: reports/models/error_report.py:10 +#: reports/models/error_report.py:9 msgid "Author" msgstr "" -#: reports/models/error_report.py:13 +#: reports/models/error_report.py:11 msgid "Attachments" msgstr "" -#: reports/models/error_report.py:16 +#: reports/models/error_report.py:14 msgid "User Report" msgstr "" -#: reports/models/error_report.py:17 +#: reports/models/error_report.py:15 msgid "User Reports" msgstr "" @@ -989,7 +1006,7 @@ msgstr "" msgid "Tools" msgstr "" -#: tools/apps.py:15 tools/chats/models.py:23 +#: tools/apps.py:15 tools/chats/models.py:21 msgid "Chats" msgstr "" @@ -1009,16 +1026,16 @@ msgstr "" msgid "Feed" msgstr "" -#: tools/chats/models.py:15 tools/public_api/models.py:47 +#: tools/chats/models.py:13 tools/public_api/models.py:45 msgid "Is deleted" msgstr "" -#: tools/chats/models.py:19 +#: tools/chats/models.py:17 #, python-format msgid "Chat %(id)s" msgstr "" -#: tools/chats/models.py:22 +#: tools/chats/models.py:20 msgid "Chat" msgstr "" @@ -1030,14 +1047,18 @@ msgstr "" msgid "API Key not found" msgstr "" -#: tools/public_api/models.py:48 +#: tools/public_api/models.py:46 msgid "Expires at" msgstr "" -#: tools/public_api/models.py:52 +#: tools/public_api/models.py:50 msgid "API Key" msgstr "" -#: tools/public_api/models.py:53 +#: tools/public_api/models.py:51 msgid "API Keys" msgstr "" + +#: tools/public_api/views/base.py:54 +msgid "Model is blocked by outdating or temporary block, please retry later" +msgstr "" @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-24 18:01+0300\n" +"POT-Creation-Date: 2025-03-30 03:00+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -20,7 +20,8 @@ msgstr "" "n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || " "(n%100>=11 && n%100<=14)? 2 : 3);\n" -#: achievements/admin.py:11 achievements/models.py:19 stories/models.py:18 +#: achievements/admin.py:11 achievements/models.py:19 ml_model/models.py:47 +#: stories/models.py:18 msgid "Icon" msgstr "Миниатюра" @@ -32,21 +33,21 @@ msgstr "Достижение" msgid "Achievements" msgstr "Достижения" -#: achievements/models.py:14 ml_model/models.py:16 ml_model/models.py:38 -#: ml_model/models.py:145 +#: achievements/models.py:14 ml_model/models.py:19 ml_model/models.py:39 +#: ml_model/models.py:72 ml_model/models.py:180 msgid "Slug" msgstr "Ярлык" -#: achievements/models.py:16 ml_model/models.py:36 ml_model/models.py:143 -#: ml_model/models.py:232 payments/models/payment.py:53 +#: achievements/models.py:16 ml_model/models.py:70 ml_model/models.py:179 +#: ml_model/models.py:268 payments/models/payment.py:52 msgid "Description" msgstr "Описание" #: achievements/models.py:43 authentication/models/business_host.py:21 -#: authentication/models/email_token.py:12 authentication/models/user.py:229 -#: authentication/models/user.py:230 authentication/models/user_telegram.py:30 +#: authentication/models/email_token.py:12 authentication/models/user.py:241 +#: authentication/models/user.py:242 authentication/models/user_telegram.py:22 #: authentication/models/user_vk.py:12 payments/models/invoice.py:15 -#: payments/models/payment.py:26 payments/models/payment_plan.py:63 +#: payments/models/payment.py:26 payments/models/payment_plan.py:61 msgid "User" msgstr "Пользователь" @@ -64,8 +65,8 @@ msgstr "Достижение %(achievement_title)s пользователя %(us msgid "Issued achievement" msgstr "Выданное достижение" -#: authentication/models/business_account.py:16 payments/models/promocode.py:69 -#: tools/public_api/models.py:35 +#: authentication/models/business_account.py:16 payments/models/promocode.py:72 +#: tools/public_api/models.py:33 msgid "Owner" msgstr "Владелец" @@ -88,7 +89,7 @@ msgid "Acceptance" msgstr "Подтверждение" #: authentication/models/business_account.py:44 -#: authentication/models/business_group.py:21 tools/public_api/models.py:45 +#: authentication/models/business_group.py:21 tools/public_api/models.py:43 msgid "Token limit" msgstr "Лимит токенов" @@ -96,7 +97,7 @@ msgstr "Лимит токенов" msgid "Group" msgstr "Группа" -#: authentication/models/business_account.py:62 +#: authentication/models/business_account.py:61 msgid "" "Impossible to add this employee to this group which does not belong to this " "company" @@ -104,17 +105,17 @@ msgstr "" "Невозможно добавить сотрудника к группе, когда он не принадлежит данной " "компании" -#: authentication/models/business_account.py:72 +#: authentication/models/business_account.py:70 msgid "Child Business Account" msgstr "Дочерний Бизнес Аккаунт" -#: authentication/models/business_account.py:73 +#: authentication/models/business_account.py:71 msgid "Child Business Accounts" msgstr "Дочерние Бизнес Аккаунты" -#: authentication/models/business_group.py:8 ml_model/models.py:15 -#: ml_model/models.py:35 ml_model/models.py:230 -#: payments/models/payment_plan.py:28 stories/models.py:12 stories/models.py:35 +#: authentication/models/business_group.py:8 ml_model/models.py:18 +#: ml_model/models.py:38 ml_model/models.py:63 ml_model/models.py:267 +#: payments/models/payment_plan.py:27 stories/models.py:12 stories/models.py:35 #: tools/chats/models.py:9 msgid "Title" msgstr "Название" @@ -131,9 +132,9 @@ msgstr "Бизнес Группы" msgid "Affiliated by" msgstr "Кем привлечена" -#: authentication/models/business_host.py:35 authentication/models/user.py:132 -#: authentication/models/whitelist.py:16 ml_model/models.py:125 -#: payments/models/promocode.py:83 +#: authentication/models/business_host.py:35 authentication/models/user.py:147 +#: authentication/models/whitelist.py:16 ml_model/models.py:165 +#: payments/models/promocode.py:85 msgid "Is active" msgstr "Является активной" @@ -141,68 +142,68 @@ msgstr "Является активной" msgid "Sector" msgstr "Сектор" -#: authentication/models/business_host.py:45 +#: authentication/models/business_host.py:44 msgid "Planned amount of workers" msgstr "Планируемое число сотрудников" -#: authentication/models/business_host.py:51 +#: authentication/models/business_host.py:49 msgid "Usage intensity" msgstr "Частота использования" -#: authentication/models/business_host.py:58 +#: authentication/models/business_host.py:56 msgid "Token low balance cap" msgstr "Предел низкого баланса" -#: authentication/models/business_host.py:63 +#: authentication/models/business_host.py:61 msgid "Emails token low balance cap" msgstr "Email'ы для рассылки по низкому балансу" -#: authentication/models/business_host.py:66 +#: authentication/models/business_host.py:63 msgid "Token low balance cap enabled" msgstr "Рассылка по низкому балансу включена" -#: authentication/models/business_host.py:70 +#: authentication/models/business_host.py:66 msgid "ITN" msgstr "ИНН" -#: authentication/models/business_host.py:71 +#: authentication/models/business_host.py:67 msgid "PSRN" msgstr "ОГРН" -#: authentication/models/business_host.py:73 ml_model/models.py:141 -#: tools/public_api/models.py:31 +#: authentication/models/business_host.py:68 ml_model/models.py:178 +#: tools/public_api/models.py:30 msgid "Name" msgstr "Наименование" -#: authentication/models/business_host.py:78 +#: authentication/models/business_host.py:71 msgid "Preffered name" msgstr "" -#: authentication/models/business_host.py:81 +#: authentication/models/business_host.py:72 msgid "Corporate email" msgstr "Корпоративная почта" -#: authentication/models/business_host.py:84 +#: authentication/models/business_host.py:74 msgid "Corporate phone" msgstr "Корпоративный телефон" -#: authentication/models/business_host.py:87 +#: authentication/models/business_host.py:76 msgid "Job title" msgstr "Наименование работ" -#: authentication/models/business_host.py:93 +#: authentication/models/business_host.py:81 msgid "Allowed models" msgstr "Разрешенные модели" -#: authentication/models/business_host.py:97 +#: authentication/models/business_host.py:84 msgid "Log history enabled" msgstr "История логов включена" -#: authentication/models/business_host.py:117 +#: authentication/models/business_host.py:103 msgid "Business Account" msgstr "Бизнес Аккаунт" -#: authentication/models/business_host.py:118 +#: authentication/models/business_host.py:104 msgid "Business Accounts" msgstr "Бизнес Аккаунты" @@ -270,7 +271,7 @@ msgstr "Админ" msgid "Security" msgstr "Безопасность" -#: authentication/models/email_token.py:15 ml_model/models.py:234 +#: authentication/models/email_token.py:15 ml_model/models.py:269 msgid "Key" msgstr "Ключ" @@ -282,43 +283,43 @@ msgstr "Email Токен" msgid "Email Tokens" msgstr "Email Токены" -#: authentication/models/user.py:109 authentication/models/user_telegram.py:10 +#: authentication/models/user.py:120 authentication/models/user_telegram.py:9 msgid "First name" msgstr "Имя" -#: authentication/models/user.py:116 authentication/models/user_telegram.py:13 +#: authentication/models/user.py:127 authentication/models/user_telegram.py:10 msgid "Last name" msgstr "Фамилия" -#: authentication/models/user.py:119 authentication/models/user_telegram.py:16 +#: authentication/models/user.py:134 authentication/models/user_telegram.py:11 msgid "Username" msgstr "Имя пользователя" -#: authentication/models/user.py:126 +#: authentication/models/user.py:141 msgid "Email" msgstr "Email" -#: authentication/models/user.py:134 +#: authentication/models/user.py:149 msgid "Is staff" msgstr "Административный" -#: authentication/models/user.py:135 +#: authentication/models/user.py:150 msgid "Is superuser" msgstr "Суперюзер" -#: authentication/models/user.py:136 +#: authentication/models/user.py:151 msgid "Is email confirmed" msgstr "Email подтвержден" -#: authentication/models/user.py:138 +#: authentication/models/user.py:152 msgid "Is subscribed" msgstr "Подписан на уведомления" -#: authentication/models/user.py:145 +#: authentication/models/user.py:158 msgid "Picture name" msgstr "Имя аватара" -#: authentication/models/user.py:154 authentication/models/utm.py:21 +#: authentication/models/user.py:167 authentication/models/utm.py:21 msgid "UTM" msgstr "UTM" @@ -330,38 +331,38 @@ msgstr "Телеграм ID" msgid "Is bot" msgstr "Является ботом" -#: authentication/models/user_telegram.py:19 +#: authentication/models/user_telegram.py:12 msgid "Language" msgstr "Язык" -#: authentication/models/user_telegram.py:21 +#: authentication/models/user_telegram.py:13 msgid "Is premium" msgstr "Премиум" -#: authentication/models/user_telegram.py:23 +#: authentication/models/user_telegram.py:15 msgid "Is subscribed to channel" msgstr "Подписан на канал" -#: authentication/models/user_telegram.py:34 +#: authentication/models/user_telegram.py:25 msgid "Phonenumber" msgstr "Номер телефона" -#: authentication/models/user_telegram.py:37 +#: authentication/models/user_telegram.py:27 #: authentication/models/user_vk.py:14 payments/models/invoice.py:11 -#: stories/models.py:15 tools/chats/models.py:11 +#: stories/models.py:15 tools/chats/models.py:10 msgid "Created at" msgstr "Когда создан" -#: authentication/models/user_telegram.py:38 +#: authentication/models/user_telegram.py:28 #: authentication/models/user_vk.py:15 msgid "Updated at" msgstr "Когда обновлен" -#: authentication/models/user_telegram.py:44 +#: authentication/models/user_telegram.py:34 msgid "Telegram User" msgstr "Пользователь телеграм" -#: authentication/models/user_telegram.py:45 +#: authentication/models/user_telegram.py:35 msgid "Telegram Users" msgstr "Пользователи Телеграм" @@ -405,374 +406,388 @@ msgstr "Вайтлист для отмены политик" msgid "Whitelists to cancel policies" msgstr "Вайтлисты для отмены политик" -#: authentication/selectors/business_host_selector.py:38 -#: authentication/selectors/business_host_selector.py:83 +#: authentication/selectors/business_host_selector.py:42 +#: authentication/selectors/business_host_selector.py:85 msgid "You haven't rights to access host account information" msgstr "" -#: authentication/selectors/business_host_selector.py:54 +#: authentication/selectors/business_host_selector.py:60 msgid "Host user is not registered for this account" msgstr "Пользователь бизнес-аккаунта не зарегистрирован для этого аккаунта" -#: authentication/selectors/user_selector.py:79 +#: authentication/selectors/user_selector.py:80 msgid "No user with this uid found" msgstr "Не найден пользователь с данным ID" -#: authentication/services/business_account_service.py:54 +#: authentication/services/business_account_service.py:58 msgid "BusinessAccount for this user doesn't exist" msgstr "Бизнес-аккаунт для данного юзера не найден" -#: authentication/services/business_account_service.py:65 +#: authentication/services/business_account_service.py:68 msgid "Invited account can either accept or reject an invitation" msgstr "Приглашенный аккаунт может принять или отклонить приглашение" -#: authentication/services/business_account_service.py:71 +#: authentication/services/business_account_service.py:73 msgid "Account is already confirmed" msgstr "Аккаунт уже подтвержден" -#: authentication/services/business_host_service.py:148 +#: authentication/services/business_host_service.py:152 msgid "No user_email is provided" msgstr "" -#: authentication/services/business_host_service.py:203 +#: authentication/services/business_host_service.py:199 msgid "No business account by this uid at your company" msgstr "Такого аккаунта нет в вашей компании" -#: authentication/services/email_service.py:119 +#: authentication/services/email_service.py:115 msgid "Regular users cannot send introductory letters" msgstr "Обычные пользователи не могут отсылать письма" -#: authentication/services/email_service.py:141 +#: authentication/services/email_service.py:137 msgid "Regular users cannot send invitation letters" msgstr "Обычные пользователи не могут отправлять письма для приглашений" -#: authentication/services/user_services.py:49 +#: authentication/services/user_services.py:51 msgid "New user data is invalid" msgstr "" -#: authentication/services/user_services.py:113 +#: authentication/services/user_services.py:111 msgid "Wrong email" msgstr "Неверный email" -#: authentication/services/user_services.py:121 backend/urls.py:41 +#: authentication/services/user_services.py:119 backend/urls.py:43 msgid "Wrong password" msgstr "Неверный пароль" -#: authentication/services/user_services.py:124 +#: authentication/services/user_services.py:122 msgid "User has not confirmed his email yet" msgstr "Пользователь пока не подтвердил свой email" -#: authentication/services/user_services.py:163 +#: authentication/services/user_services.py:161 msgid "No user like this in a database" msgstr "Такой пользователь отсутствует" -#: authentication/services/user_services.py:180 +#: authentication/services/user_services.py:178 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:184 +#: authentication/services/user_services.py:182 msgid "No user token like this in a database" msgstr "" -#: authentication/services/user_services.py:204 +#: authentication/services/user_services.py:202 msgid "No email token provided" msgstr "Токен не получен" -#: authentication/services/user_services.py:208 +#: authentication/services/user_services.py:206 msgid "No token like this in a database" msgstr "Не найдено такого токена" -#: authentication/services/user_services.py:217 +#: authentication/services/user_services.py:212 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: authentication/services/user_services.py:253 +#: authentication/services/user_services.py:250 msgid "Current password is wrong" msgstr "Текущий пароль неверен" -#: backend/urls.py:28 +#: backend/urls.py:31 msgid "Requested object does not exists" msgstr "" -#: backend/urls.py:35 +#: backend/urls.py:37 msgid "Token is invalid" msgstr "" -#: backend/urls.py:47 +#: backend/urls.py:49 #, fuzzy #| msgid "Wrong email" msgid "Wrong username" msgstr "Неверный email" -#: messages/serializers.py:39 +#: messages/serializers.py:42 #, python-format msgid "The file size cannot exceed %(max_mb_size)d MB" -msgstr "" +msgstr "Файл не может быть размером больше %(max_mb_size)d мегабайт" -#: ml_model/apps.py:8 ml_model/models.py:104 +#: ml_model/apps.py:9 ml_model/models.py:146 msgid "Neuron Models" msgstr "Нейронные Модели" -#: ml_model/models.py:26 ml_model/models.py:46 +#: ml_model/models.py:29 ml_model/models.py:82 msgid "Category" msgstr "Категория" -#: ml_model/models.py:27 +#: ml_model/models.py:30 msgid "Categories" msgstr "Категории" #: ml_model/models.py:40 +msgid "Color" +msgstr "Цвет" + +#: ml_model/models.py:44 +msgid "Not SVG-pictures not allowed" +msgstr "Нельзя использовать не SVG-картинки" + +#: ml_model/models.py:54 +msgid "Model Tag" +msgstr "Тег модели" + +#: ml_model/models.py:55 +msgid "Model Tags" +msgstr "Теги модели" + +#: ml_model/models.py:68 +msgid "Alternative Titles" +msgstr "Альтернативные названия" + +#: ml_model/models.py:74 msgid "Fill automatically, don't touch" msgstr "Заполняется автоматически, не трогать" -#: ml_model/models.py:54 +#: ml_model/models.py:90 msgid "Avatar" msgstr "Аватар" -#: ml_model/models.py:103 +#: ml_model/models.py:93 +msgid "Tags" +msgstr "Теги" + +#: ml_model/models.py:145 msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:112 +#: ml_model/models.py:154 msgid "Model" msgstr "Модель" -#: ml_model/models.py:129 -msgid "Authorization token" -msgstr "Авторизационный токен" - -#: ml_model/models.py:133 ml_model/models.py:134 +#: ml_model/models.py:170 ml_model/models.py:171 msgid "Settings" msgstr "Настройки" -#: ml_model/models.py:137 +#: ml_model/models.py:174 #, fuzzy, python-format #| msgid "Settings of %(model_title)" msgid "Settings of %(model_title)s" msgstr "Настройки %(model_title)s" -#: ml_model/models.py:146 -#, fuzzy -#| msgid "Default value" -msgid "Default" -msgstr "Стандартное значение" - -#: ml_model/models.py:159 +#: ml_model/models.py:193 #, python-format msgid "%(model_title)s | %(version_name)s" msgstr "%(model_title)s | %(version_name)s" -#: ml_model/models.py:165 +#: ml_model/models.py:199 msgid "Model Version" msgstr "Версия Модели" -#: ml_model/models.py:166 +#: ml_model/models.py:200 msgid "Model Versions" msgstr "Версии Модели" -#: ml_model/models.py:175 +#: ml_model/models.py:209 msgid "Versions" msgstr "Версии" -#: ml_model/models.py:176 +#: ml_model/models.py:210 msgid "Link to versions" msgstr "Привязка к версиям" -#: ml_model/models.py:185 reports/models/error_report.py:12 +#: ml_model/models.py:219 reports/models/error_report.py:10 msgid "Text" msgstr "Текст" -#: ml_model/models.py:186 stories/models.py:36 +#: ml_model/models.py:220 stories/models.py:36 msgid "Image" msgstr "Картинка" -#: ml_model/models.py:187 +#: ml_model/models.py:221 msgid "PDF" msgstr "PDF" -#: ml_model/models.py:188 +#: ml_model/models.py:222 msgid "DOCX" -msgstr "" +msgstr "DOCX" -#: ml_model/models.py:189 +#: ml_model/models.py:223 msgid "DOC" -msgstr "" +msgstr "DOC" -#: ml_model/models.py:190 +#: ml_model/models.py:224 msgid "Text File (Notebook)" msgstr "Текстовый файл (Блокнот)" -#: ml_model/models.py:191 +#: ml_model/models.py:225 msgid "ZIP Archive" msgstr "ZIP архив" -#: ml_model/models.py:192 +#: ml_model/models.py:226 msgid "Audio" msgstr "Аудио" -#: ml_model/models.py:198 ml_model/models.py:236 +#: ml_model/models.py:232 ml_model/models.py:271 #: payments/models/promocode.py:41 msgid "Type" msgstr "Тип" -#: ml_model/models.py:200 ml_model/models.py:249 +#: ml_model/models.py:234 ml_model/models.py:282 msgid "Required" msgstr "Обязательный" -#: ml_model/models.py:203 +#: ml_model/models.py:237 #, python-format msgid "%(model_title)s | %(input_type)s" msgstr "%(model_title)s | %(input_type)s" -#: ml_model/models.py:209 +#: ml_model/models.py:243 #, fuzzy #| msgid "Model" msgid "Model Input" msgstr "Модель" -#: ml_model/models.py:210 +#: ml_model/models.py:244 msgid "Model Inputs" msgstr "Входящий поток модели" -#: ml_model/models.py:216 +#: ml_model/models.py:250 msgid "Integer" msgstr "Целое число" -#: ml_model/models.py:217 +#: ml_model/models.py:251 msgid "Float" msgstr "Вещественное число" -#: ml_model/models.py:218 +#: ml_model/models.py:252 msgid "String" msgstr "Строка" -#: ml_model/models.py:219 +#: ml_model/models.py:255 msgid "List" msgstr "Список" -#: ml_model/models.py:222 +#: ml_model/models.py:259 msgid "Float range" msgstr "Вещественный диапазон" -#: ml_model/models.py:226 +#: ml_model/models.py:263 msgid "Integer range" msgstr "Целочисленный диапазон" -#: ml_model/models.py:228 +#: ml_model/models.py:265 msgid "Logical" msgstr "Логический" -#: ml_model/models.py:243 +#: ml_model/models.py:278 msgid "Values" msgstr "Значения" -#: ml_model/models.py:245 +#: ml_model/models.py:279 msgid "" "These values can contain different interfaces and default value optional" msgstr "" "Значения могут содержать различные интерфейс и, опционально, значение по " "умолчанию" -#: ml_model/models.py:248 +#: ml_model/models.py:281 msgid "Hidden" msgstr "Скрытый" -#: ml_model/models.py:254 +#: ml_model/models.py:287 #, fuzzy, python-format #| msgid "Parameter of %(model_title)" msgid "Parameter of %(model_title)s" msgstr "Параметр %(model_title)s" -#: ml_model/models.py:257 +#: ml_model/models.py:290 msgid "Parameter" msgstr "Параметр" -#: ml_model/models.py:258 +#: ml_model/models.py:291 msgid "Parameters" msgstr "Параметры" -#: ml_model/models.py:263 +#: ml_model/models.py:296 msgid "Fixed" msgstr "Фикса" -#: ml_model/models.py:264 +#: ml_model/models.py:297 msgid "Per generation second" msgstr "За секунду генерации" -#: ml_model/models.py:265 +#: ml_model/models.py:298 msgid "Per one text token" msgstr "За один текстовый токен" -#: ml_model/models.py:266 +#: ml_model/models.py:299 msgid "Per image pixel" msgstr "За один пиксель" -#: ml_model/models.py:269 +#: ml_model/models.py:302 msgid "By input data" msgstr "По входящим данным" -#: ml_model/models.py:270 +#: ml_model/models.py:303 msgid "By output data" msgstr "По исходящим данным" -#: ml_model/models.py:271 +#: ml_model/models.py:304 msgid "By all data" msgstr "По всем данным" -#: ml_model/models.py:274 +#: ml_model/models.py:309 #, fuzzy #| msgid "Category" msgid "Strategy" msgstr "Стратегия" -#: ml_model/models.py:279 +#: ml_model/models.py:314 msgid "Interaction Type" msgstr "Тип взаимодействия" -#: ml_model/models.py:284 payments/models/invoice.py:19 +#: ml_model/models.py:319 payments/models/invoice.py:19 msgid "Cost" msgstr "Цена" -#: ml_model/models.py:285 +#: ml_model/models.py:320 msgid "In RUB, per specified strategy" msgstr "В рублях, за указанную стратегию" -#: ml_model/models.py:290 +#: ml_model/models.py:325 msgid "Coefficient" msgstr "Коэффициент" -#: ml_model/models.py:291 +#: ml_model/models.py:326 msgid "Cost multiplier" msgstr "Цена" -#: ml_model/models.py:298 +#: ml_model/models.py:333 msgid "Rate" msgstr "Ставка" -#: ml_model/models.py:302 +#: ml_model/models.py:337 #, fuzzy #| msgid "Payment Plan" msgid "Payment Rule" msgstr "Платежное правило" -#: ml_model/models.py:303 +#: ml_model/models.py:338 msgid "Payment Rules" msgstr "Платежные правила" -#: ml_model/selectors/ml_models_selector.py:79 +#: ml_model/selectors/ml_models_selector.py:75 msgid "no model by this id" msgstr "Не найдено моделей по этому ID" #: ml_model/services/minio_service.py:35 ml_model/services/minio_service.py:53 -#: ml_model/services/minio_service.py:65 ml_model/services/minio_service.py:74 +#: ml_model/services/minio_service.py:61 ml_model/services/minio_service.py:70 msgid "Unknown bucket destination" msgstr "Неизвестный бакет для загрузки" -#: ml_model/services/upscaleai.py:123 +#: ml_model/services/upscaleai.py:124 msgid "No image given for improving" msgstr "Нет изображения для улучшения" -#: payments/apps.py:9 payments/models/payment.py:62 +#: payments/apps.py:9 payments/models/payment.py:60 msgid "Payments" msgstr "Платежи" @@ -811,7 +826,7 @@ msgstr "План" msgid "Status" msgstr "Статус" -#: payments/models/payment.py:61 +#: payments/models/payment.py:59 msgid "Payment" msgstr "Платеж" @@ -831,89 +846,89 @@ msgstr "Корпоративный" msgid "Is recurrent" msgstr "Рекуррентный" -#: payments/models/payment_plan.py:31 +#: payments/models/payment_plan.py:29 msgid "Duration" msgstr "Длительность" -#: payments/models/payment_plan.py:36 +#: payments/models/payment_plan.py:34 msgid "Is visible" msgstr "Видимый" -#: payments/models/payment_plan.py:54 payments/models/payment_plan.py:69 +#: payments/models/payment_plan.py:52 payments/models/payment_plan.py:67 msgid "Payment Plan" msgstr "Платежный План" -#: payments/models/payment_plan.py:55 +#: payments/models/payment_plan.py:53 msgid "Payment Plans" msgstr "Платежные Планы" -#: payments/models/payment_plan.py:71 +#: payments/models/payment_plan.py:69 msgid "Last payment at" msgstr "Последнее время платежа" -#: payments/models/payment_plan.py:72 +#: payments/models/payment_plan.py:70 msgid "Next payment at" msgstr "Следующее время платежа" -#: payments/models/payment_plan.py:74 +#: payments/models/payment_plan.py:72 msgid "Current balance" msgstr "Текущий баланс" -#: payments/models/payment_plan.py:80 +#: payments/models/payment_plan.py:78 msgid "Recurrent billing task" msgstr "Рекуррентная задача на платеж" -#: payments/models/payment_plan.py:97 payments/models/payment_plan.py:98 +#: payments/models/payment_plan.py:99 payments/models/payment_plan.py:100 msgid "User Balance" msgstr "Баланс пользователя" -#: payments/models/promocode.py:45 +#: payments/models/promocode.py:48 msgid "Action Function" msgstr "Активирующаяся функция" -#: payments/models/promocode.py:70 +#: payments/models/promocode.py:73 msgid "Can be only for referral promos" msgstr "Может быть только у реферальных промокодов" -#: payments/models/promocode.py:78 +#: payments/models/promocode.py:81 msgid "Is personal" msgstr "Персональный" -#: payments/models/promocode.py:79 +#: payments/models/promocode.py:82 msgid "Can be activated only one time" msgstr "Может быть активирован только один раз" -#: payments/models/promocode.py:88 +#: payments/models/promocode.py:89 #, fuzzy #| msgid "Owner can be only for referral promos" msgid "Owner can be only for refferal promos" msgstr "Владелец может быть только у реферальных промокодов" -#: payments/models/promocode.py:96 payments/models/promocode.py:107 +#: payments/models/promocode.py:97 payments/models/promocode.py:107 msgid "Promocode" msgstr "Промокод" -#: payments/models/promocode.py:97 +#: payments/models/promocode.py:98 msgid "Promocodes" msgstr "Промокоды" -#: payments/models/promocode.py:104 +#: payments/models/promocode.py:105 msgid "Activated by" msgstr "Кем активирован" -#: payments/models/promocode.py:138 +#: payments/models/promocode.py:137 msgid "Promocode Activation" msgstr "Активация Промокода" -#: payments/models/promocode.py:139 +#: payments/models/promocode.py:138 msgid "Promocode Activations" msgstr "Активации Промокодов" -#: payments/models/user_payment_method.py:28 +#: payments/models/user_payment_method.py:24 msgid "Payment Method" msgstr "Платежный метод" -#: payments/models/user_payment_method.py:29 +#: payments/models/user_payment_method.py:25 msgid "Payment Methods" msgstr "Платежные методы" @@ -921,15 +936,15 @@ msgstr "Платежные методы" msgid "Messages for this model are not registered in a selector" msgstr "" -#: payments/selectors/payment_plan_selector.py:29 +#: payments/selectors/payment_plan_selector.py:32 msgid "Business accounts are not allowed to make purchases" msgstr "Сотрудники не могут производить покупки" -#: payments/selectors/payment_plan_selector.py:54 +#: payments/selectors/payment_plan_selector.py:57 msgid "No plan by this uid" msgstr "Не найдено подписки по этому ID" -#: payments/services/model_billing_service.py:27 +#: payments/services/model_billing_service.py:31 msgid "Unknown account type" msgstr "Неизвестный тип аккаунта" @@ -957,19 +972,19 @@ msgstr "Прокси" msgid "Proxies" msgstr "Прокси" -#: reports/models/error_report.py:10 +#: reports/models/error_report.py:9 msgid "Author" msgstr "Автор" -#: reports/models/error_report.py:13 +#: reports/models/error_report.py:11 msgid "Attachments" msgstr "Вложения" -#: reports/models/error_report.py:16 +#: reports/models/error_report.py:14 msgid "User Report" msgstr "Пользовательский репорт" -#: reports/models/error_report.py:17 +#: reports/models/error_report.py:15 msgid "User Reports" msgstr "Пользовательские репорты" @@ -1013,7 +1028,7 @@ msgstr "Виджеты" msgid "Tools" msgstr "Инструменты" -#: tools/apps.py:15 tools/chats/models.py:23 +#: tools/apps.py:15 tools/chats/models.py:21 msgid "Chats" msgstr "Чаты" @@ -1033,17 +1048,17 @@ msgstr "Медиа" msgid "Feed" msgstr "Шейр пользователей" -#: tools/chats/models.py:15 tools/public_api/models.py:47 +#: tools/chats/models.py:13 tools/public_api/models.py:45 msgid "Is deleted" msgstr "Удален" -#: tools/chats/models.py:19 +#: tools/chats/models.py:17 #, fuzzy, python-format #| msgid "Chat %(id)" msgid "Chat %(id)s" msgstr "Чат %(id)s" -#: tools/chats/models.py:22 +#: tools/chats/models.py:20 msgid "Chat" msgstr "Чат" @@ -1055,18 +1070,30 @@ msgstr "Необходимо повысить лимит токенов у API- msgid "API Key not found" msgstr "API-ключ не найден" -#: tools/public_api/models.py:48 +#: tools/public_api/models.py:46 msgid "Expires at" msgstr "Когда заканчивается" -#: tools/public_api/models.py:52 +#: tools/public_api/models.py:50 msgid "API Key" msgstr "API Ключ" -#: tools/public_api/models.py:53 +#: tools/public_api/models.py:51 msgid "API Keys" msgstr "API Ключи" +#: tools/public_api/views/base.py:54 +msgid "Model is blocked by outdating or temporary block, please retry later" +msgstr "Модель заблокирована, т.к перестала обновляться или временно, попробуйте позже" + +#~ msgid "Authorization token" +#~ msgstr "Авторизационный токен" + +#, fuzzy +#~| msgid "Default value" +#~ msgid "Default" +#~ msgstr "Стандартное значение" + #~ msgid "Can search by: model title" #~ msgstr "Можно искать по: Названию модели" @@ -10,7 +10,7 @@ from django_minio_backend import MinioBackend def message_file_upload(instance: 'Message', filename: str): - return f"{instance.content_object.model.slug}_{instance.content_object.user.pk}_{time.time():.0f}.{filename.split('.')[-1]}" + return f'{instance.content_object.model.slug}_{instance.content_object.user.pk}_{time.time():.0f}.{filename.split(".")[-1]}' class Message(models.Model): @@ -36,9 +36,7 @@ class Message(models.Model): null=True, blank=True, ) - created_at = models.DateTimeField( - default=timezone.now, verbose_name='Когда создано', editable=False - ) + created_at = models.DateTimeField(default=timezone.now, verbose_name='Когда создано', editable=False) elapsed_time = models.DurationField( default=timedelta(seconds=0), verbose_name='Затраченное время', @@ -55,9 +53,7 @@ class Message(models.Model): verbose_name='Избранное', ) is_shared = models.BooleanField(default=False, verbose_name='Сообщение в фиде') - content_type = models.ForeignKey( - ContentType, blank=True, null=True, on_delete=models.DO_NOTHING - ) + content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=models.DO_NOTHING) object_id = models.UUIDField(blank=True, null=True) content_object = fields.GenericForeignKey('content_type', 'object_id') info = models.JSONField( @@ -1,10 +1,10 @@ from uuid import uuid4 +from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.db.models import QuerySet -from backend.settings import AUTH_USER_MODEL from ml_model.models import NeuronModel from .message import Message @@ -48,7 +48,7 @@ class MultipleStore(BaseStore): """ user = models.ForeignKey( - AUTH_USER_MODEL, + get_user_model(), null=True, on_delete=models.SET_NULL, verbose_name='Пользователь', @@ -74,7 +74,7 @@ class SingleStore(BaseStore): """ user = models.OneToOneField( - AUTH_USER_MODEL, + get_user_model(), on_delete=models.SET_NULL, verbose_name='Пользователь', null=True, @@ -53,15 +53,11 @@ class MessagesAPIView(APIView): case 'str': missing_info.update({p.key: p.default if p.default else ''}) case 'float': - missing_info.update( - {p.key: float(p.default) if p.default else 1.0} - ) + missing_info.update({p.key: float(p.default) if p.default else 1.0}) case 'oneof': missing_info.update({p.key: p.default.split(',')[0]}) case 'list': - missing_info.update( - {p.key: p.default.split(',') if p.default else []} - ) + missing_info.update({p.key: p.default.split(',') if p.default else []}) merged_info = info | missing_info i = Message.objects.create( **serializer.validated_data, @@ -95,9 +91,7 @@ class MessageAPIView(APIView): def put(self, request, chat_uid, message_uid, *args, **kwargs): """Put message to favourites""" chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get( - uid=message_uid, chats_chats_messages=chat, is_deleted=False - ) + message = Message.objects.get(uid=message_uid, chats_chats_messages=chat, is_deleted=False) message.is_favourite = True message.save() return Response(status=204) @@ -108,9 +102,7 @@ class MessageAPIView(APIView): def delete(self, request, chat_uid, message_uid, *args, **kwargs): """Delete (Hide to deleted) message""" chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get( - pk=message_uid, chats_chats_messages=chat, is_deleted=False - ) + message = Message.objects.get(pk=message_uid, chats_chats_messages=chat, is_deleted=False) message.is_deleted = True message.save() return Response(status=204) @@ -11,7 +11,10 @@ class MessageSerializer(serializers.ModelSerializer): elapsed_time = serializers.DurationField(read_only=True) from_model = serializers.BooleanField(read_only=True) model = serializers.SlugField( - source='content_object.model', default=None, required=False, read_only=True + source='content_object.model', + default=None, + required=False, + read_only=True, ) is_favourite = serializers.BooleanField(read_only=True) is_sent = serializers.BooleanField(read_only=True) @@ -36,7 +39,6 @@ class MessageSerializer(serializers.ModelSerializer): max_mb_size = 8 if file and file.size > (max_mb_size << 10 << 10): raise ValidationError( - _('The file size cannot exceed %(max_mb_size)d MB') - % {'max_mb_size': max_mb_size} + _('The file size cannot exceed %(max_mb_size)d MB') % {'max_mb_size': max_mb_size} ) return file @@ -31,16 +31,13 @@ class Command(BaseCommand): if isinstance(klass.category, ModelCategory): defaults['category'], _ = ModelCategory.objects.get_or_create( - slug=klass.category.slug, defaults={'title': klass.category.title} + slug=klass.category.slug, + defaults={'title': klass.category.title}, ) - model, _ = NeuronModel.objects.get_or_create( - slug=klass.__name__.lower(), defaults=defaults - ) + model, _ = NeuronModel.objects.get_or_create(slug=klass.__name__.lower(), defaults=defaults) - ModelSettings.objects.get_or_create( - model=model, defaults={'is_active': False} - ) + ModelSettings.objects.get_or_create(model=model, defaults={'is_active': False}) for version in klass.versions: try: @@ -58,9 +55,7 @@ class Command(BaseCommand): for input in klass.inputs: try: - sinput, icreated = ModelInput.objects.get_or_create( - model=model, type=input.type - ) + sinput, icreated = ModelInput.objects.get_or_create(model=model, type=input.type) if not icreated: sinput.required = input.required sinput.save() @@ -0,0 +1,42 @@ +# Generated by Django 5.0.11 on 2025-03-29 13:55 + +import core.fields +import django.contrib.postgres.fields +import django.core.validators +import django_minio_backend.models +import ml_model.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0044_alter_modelstat_options'), + ] + + operations = [ + migrations.CreateModel( + name='ModelTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100, verbose_name='Title')), + ('slug', models.SlugField(max_length=120, unique=True, verbose_name='Slug')), + ('color', core.fields.ColorField(verbose_name='Color')), + ('icon', models.FileField(blank=True, null=True, storage=django_minio_backend.models.MinioBackend('air-models'), upload_to=ml_model.models.model_tag_icon_uploader, validators=[django.core.validators.FileExtensionValidator(['svg'], 'Not SVG-pictures not allowed')], verbose_name='Icon')), + ], + options={ + 'verbose_name': 'Model Tag', + 'verbose_name_plural': 'Model Tags', + }, + ), + migrations.AddField( + model_name='neuronmodel', + name='alternative_titles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), blank=True, default=list, size=None, verbose_name='Alternative Titles'), + ), + migrations.AddField( + model_name='neuronmodel', + name='tags', + field=models.ManyToManyField(related_name='model_tags', to='ml_model.modeltag', verbose_name='Tags'), + ), + ] @@ -0,0 +1,27 @@ +# Generated by Django 5.0.11 on 2025-03-29 21:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0045_modeltag_neuronmodel_alternative_titles_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='modelsettings', + name='authorization_token', + ), + migrations.AlterField( + model_name='modelsettings', + name='is_active', + field=models.BooleanField(default=False, help_text='Модель активна для всех пользователей', verbose_name='Is active'), + ), + migrations.AlterField( + model_name='neuronmodel', + name='tags', + field=models.ManyToManyField(blank=True, related_name='models_tags', to='ml_model.modeltag', verbose_name='Tags'), + ), + ] @@ -0,0 +1,17 @@ +# Generated by Django 5.0.11 on 2025-03-29 21:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0046_remove_modelsettings_authorization_token_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='modelversion', + name='default', + ), + ] @@ -0,0 +1,25 @@ +# Generated by Django 5.0.11 on 2025-03-29 23:08 + +import core.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0047_remove_modelversion_default'), + ] + + operations = [ + migrations.AlterField( + model_name='modeltag', + name='color', + field=core.fields.ColorField(blank=True, null=True, verbose_name='Color'), + ), + migrations.AlterField( + model_name='neuronmodel', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='category_models', to='ml_model.modelcategory', verbose_name='Category'), + ), + ] @@ -14,9 +14,7 @@ class NeuronModelSelector: def __init__(self, user: CustomUserModel): self.user = user - def get_models_by_input_content_type( - self, serialize: bool = False, hidden: bool = False - ): + def get_models_by_input_content_type(self, serialize: bool = False, hidden: bool = False): models = NeuronModel.objects.prefetch_related( Prefetch( 'model_modelparameters', @@ -27,9 +25,7 @@ class NeuronModelSelector: return NeuronModelSerializer(models, many=True) return models - def get_models_by_output_content_type( - self, serialize: bool = False, hidden: bool = False - ): + def get_models_by_output_content_type(self, serialize: bool = False, hidden: bool = False): models = NeuronModel.objects.prefetch_related( Prefetch( 'model_modelparameters', @@ -20,7 +20,12 @@ from langchain.agents import AgentExecutor, create_structured_chat_agent from langchain.chains import ConversationChain from langchain_community.tools.google_serper import GoogleSerperResults from langchain_core.chat_history import InMemoryChatMessageHistory -from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage +from langchain_core.messages import ( + AIMessage, + BaseMessage, + HumanMessage, + SystemMessage, +) from langchain_core.prompts.prompt import PromptTemplate from langchain_core.runnables import RunnableWithMessageHistory from langchain_openai.chat_models import ChatOpenAI @@ -28,15 +33,13 @@ from langchain_text_splitters import RecursiveCharacterTextSplitter from PIL import Image from PyPDF2 import PdfReader +from backend import settings from messages.models import BaseStore, Message from ml_model.constants import TEMPORARY_TEST_TEXT from ml_model.exceptions import GenerationException from ml_model.models import ( - ModelCategory, ModelConfiguration, - ModelInput, - ModelParameter, - ModelVersion, + NeuronModel, ) from ml_model.services.base import SimpleService from payments.exceptions.insufficient_balance import InsufficientBalance @@ -47,72 +50,43 @@ from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore -from backend import settings - class Chatgpt(SimpleService): """ ChatGPT Service contains abstract method make, which makes a generation """ - title = 'ChatGPT' - description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' - - versions = [ - ModelVersion(name='GPT-4o1', default=True, slug='o1-preview'), - ModelVersion(name='GPT-4o1 Mini', slug='o1-mini'), - ModelVersion(name='GPT-4omni Mini', slug='gpt-4o-mini'), - ModelVersion(name='GPT-4omni', slug='gpt-4o'), - ] - - inputs = [ - ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), - ModelInput(type=ModelInput.TypeChoices.PDF), - ModelInput(type=ModelInput.TypeChoices.DOCX), - ModelInput(type=ModelInput.TypeChoices.DOC), - ] - - parameters = [ - ModelParameter( - name='Температура', - key='temperature', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 0.1, 'end': 2.0, 'step': 0.1, 'default': 0.5}, - ), - ModelParameter( - name='Лучший процент', - key='top_p', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 0.1, 'end': 2.0, 'step': 0.1, 'default': 0.5}, - ), - ModelParameter( - name='Штраф за присутствие', - key='presence', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 0.1, 'end': 2.0, 'step': 0.1, 'default': 0.5}, - ), - ModelParameter( - name='Использовать интернет', - key='use_web', - type=ModelParameter.TypeChoices.BOOL, - values={'default': True}, - ), - ] - category = ModelCategory(title='Чат-боты', slug='chat-bots') - TOKENS_COST = { - 'o3-mini': {'input': Decimal('0.000605'), 'output': Decimal('0.002420')}, - 'o1-preview': {'input': Decimal('0.008250'), 'output': Decimal('0.033000')}, - 'o1-mini': {'input': Decimal('0.001650'), 'output': Decimal('0.006600')}, - 'gpt-4o-mini': {'input': Decimal('0.000083'), 'output': Decimal('0.000330')}, - 'gpt-4o': {'input': Decimal('0.001375'), 'output': Decimal('0.005500')}, + 'o3-mini': { + 'input': Decimal('0.000605'), + 'output': Decimal('0.002420'), + }, + 'o1-preview': { + 'input': Decimal('0.008250'), + 'output': Decimal('0.033000'), + }, + 'o1-mini': { + 'input': Decimal('0.001650'), + 'output': Decimal('0.006600'), + }, + 'gpt-4o-mini': { + 'input': Decimal('0.000083'), + 'output': Decimal('0.000330'), + }, + 'gpt-4o': { + 'input': Decimal('0.001375'), + 'output': Decimal('0.005500'), + }, } def __init__(self, store: BaseStore) -> None: super().__init__(store) self.logger = logging.getLogger(self.__class__.__name__) + @property + def neuron_model(self): + return NeuronModel.objects.get(title='ChatGPT') + def make( self, input_message: Message, @@ -131,9 +105,7 @@ class Chatgpt(SimpleService): if file_extension == '.pdf': chunks = self.split_text_to_chunks(self.get_pdf_data(file)) elif file_extension in ('.doc', '.docx'): - chunks = self.split_text_to_chunks( - self.get_word_data(file_extension, file) - ) + chunks = self.split_text_to_chunks(self.get_word_data(file_extension, file)) else: image = file if image: @@ -143,9 +115,7 @@ class Chatgpt(SimpleService): format = 'jpeg' if kind.extension == 'jpg' else kind.extension buf = BytesIO() normalized_image.save(buf, format=format) - image_url = ( - f'data:{mime},base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' - ) + image_url = f'data:{mime},base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' buf.close() image_size = normalized_image.size input_content.append({'type': 'image_url', 'image_url': {'url': image_url}}) @@ -187,18 +157,12 @@ class Chatgpt(SimpleService): ) llm_input = HumanMessage(content=input_content) if file and not image: - input_tokens = self.count_text_tokens( - [*chat_history.messages, llm_input, *chunks] - ) + input_tokens = self.count_text_tokens([*chat_history.messages, llm_input, *chunks]) elif image: input_tokens = self.count_text_tokens([llm_input]) else: - input_tokens = self.count_text_tokens( - [*chat_history.messages, llm_input] - ) - self.assert_enough_balance( - input_tokens, image_size, model=self.llm.model_name - ) + input_tokens = self.count_text_tokens([*chat_history.messages, llm_input]) + self.assert_enough_balance(input_tokens, image_size, model=self.llm.model_name) if image: response = self.llm.invoke([llm_input]) chat_history.add_ai_message(response) @@ -221,11 +185,7 @@ class Chatgpt(SimpleService): ) chunk_responses.append(response.content) combined_summary = ' '.join(chunk_responses) - user_prompt = ( - input_message.content - if input_message.content.split() - else 'Суммируй текст' - ) + user_prompt = input_message.content if input_message.content.split() else 'Суммируй текст' question_content = ( f'Вот краткое содержание каждого чанка:\n' f'{combined_summary}\nОтветьте на вопрос по содержанию файла: {user_prompt}' @@ -253,9 +213,7 @@ class Chatgpt(SimpleService): 'input': [llm_input], 'chat_history': chat_history.messages + [ - SystemMessage( - content='Учитывай язык диалога перед выдачей ответа' - ), + SystemMessage(content='Учитывай язык диалога перед выдачей ответа'), SystemMessage( content='Никому не говори, что ты бот и не можешь найти информацию в интернете' ), @@ -275,7 +233,10 @@ class Chatgpt(SimpleService): json={ 'model': model_name, 'messages': [ - {'role': 'user', 'content': input_message.content}, + { + 'role': 'user', + 'content': input_message.content, + }, ], }, ) @@ -284,10 +245,7 @@ class Chatgpt(SimpleService): and data.get('choices') and ( content := ','.join( - [ - choice['message']['content'] - for choice in data.get('choices') - ] + [choice['message']['content'] for choice in data.get('choices')] ) ) ): @@ -309,20 +267,12 @@ class Chatgpt(SimpleService): ) if image and normalized_image: - self.logger.info( - f'Input количество токенов БЕЗ картинки {model_name} - {input_tokens}' - ) + self.logger.info(f'Input количество токенов БЕЗ картинки {model_name} - {input_tokens}') input_tokens += self.count_image_tokens(normalized_image.size, model_name) - self.logger.info( - f'Input количество токенов для {model_name} - {input_tokens}' - ) - self.logger.info( - f'Output количество токенов для {model_name} - {output_tokens}' - ) - self.logger.info( - f'Общее количество токенов для {model_name} - {input_tokens + output_tokens}' - ) + self.logger.info(f'Input количество токенов для {model_name} - {input_tokens}') + self.logger.info(f'Output количество токенов для {model_name} - {output_tokens}') + self.logger.info(f'Общее количество токенов для {model_name} - {input_tokens + output_tokens}') process_time = timedelta(seconds=time.time() - start_time) self.handle_invoice( @@ -336,15 +286,17 @@ class Chatgpt(SimpleService): raise GenerationException def get_chat_history( - self, - message_limit: int = 10, - token_limit: int = 580, # ~ 1 AIR Token with GPT 3.5 + self, + message_limit: int = 10, + token_limit: int = 580, # ~ 1 AIR Token with GPT 3.5 ) -> InMemoryChatMessageHistory: if isinstance(self.store, Chat): air_messages = list( reversed( Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True + chats_chats_messages=self.store, + is_deleted=False, + is_sent=True, ).order_by('-created_at')[:message_limit] ) ) @@ -378,7 +330,10 @@ class Chatgpt(SimpleService): return memory def assert_enough_balance( - self, input_tokens: int, image_size: tuple, model: str = 'gpt-3.5-turbo' + self, + input_tokens: int, + image_size: tuple, + model: str = 'gpt-3.5-turbo', ): balance = PaymentPlanSelector(self.store.user).get_current_balance() total_tokens = input_tokens @@ -391,7 +346,12 @@ class Chatgpt(SimpleService): raise InsufficientBalance(balance, input_cost) def calculate_price( - self, input_tokens: int, output_tokens: int, model: str, *args, **kwargs + self, + input_tokens: int, + output_tokens: int, + model: str, + *args, + **kwargs, ) -> Decimal: price = ( input_tokens * self.TOKENS_COST[model]['input'] @@ -414,11 +374,7 @@ class Chatgpt(SimpleService): if max(width, height) > 2048: a_ratio = width / height - width, height = ( - (2048, int(2048 / a_ratio)) - if a_ratio > 1 - else (int(2048 * a_ratio), 2048) - ) + width, height = (2048, int(2048 / a_ratio)) if a_ratio > 1 else (int(2048 * a_ratio), 2048) if width >= height and height > 768: width, height = int((768 / height) * width), 768 elif height > width and width > 768: @@ -437,8 +393,8 @@ class Chatgpt(SimpleService): if isinstance(message.content, str): total_tokens += len(encoding.encode(message.content)) elif any(isinstance(item, dict) for item in message.content): - total_tokens += len(encoding.encode( - ''.join([input_data.get('text', '') for input_data in message.content])) + total_tokens += len( + encoding.encode(''.join([input_data.get('text', '') for input_data in message.content])) ) else: total_tokens += len(encoding.encode(''.join(message.content))) @@ -464,9 +420,7 @@ class Chatgpt(SimpleService): if raw_text.strip(): return f'Содержимое файла: f{raw_text}' else: - return ( - 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' - ) + return 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' def get_word_data(self, extension: str, word_file: UploadedFile) -> str: """ @@ -495,9 +449,7 @@ class Chatgpt(SimpleService): if text.strip(): return f'Это текст, извлечённый из загруженного WORD-файла:\n{text}' else: - return ( - 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' - ) + return 'Файл пуст или содержит изображения, из которых невозможно извлечь текст.' def split_text_to_chunks( self, raw_text: str, chunk_size: int = 100_000, overlap: int = 300 @@ -516,7 +468,10 @@ class Chatgpt(SimpleService): return [HumanMessage(chunk) for chunk in chunks] def save_results( - self, results: list[BaseMessage], elapsed_time: timedelta, save: bool = True + self, + results: list[BaseMessage], + elapsed_time: timedelta, + save: bool = True, ) -> list[Message]: messages = [ Message( @@ -563,9 +518,7 @@ class Chatgpt(SimpleService): self.llm.get_num_tokens_from_messages(chat_history.messages), self.llm.model_name, ) - msgs = self.save_results( - [chat_history.messages[-1]], process_time, save - ) + msgs = self.save_results([chat_history.messages[-1]], process_time, save) return msgs @classmethod @@ -7,6 +7,7 @@ from typing import Iterator, Any import filetype from PIL import Image +from django.db.models.fields.files import FieldFile from messages.models import Message from ml_model.models import ModelCategory, ModelVersion, ModelInput, ModelParameter @@ -25,18 +26,17 @@ class Claude(SimpleService): TOKENS_COST = { - 'claude-3.7-sonnet:thinking': {'input': Decimal('800'), 'output': Decimal('3000'), 'input_imgs': Decimal('960')}, # 1M tokens + 'claude-3.7-sonnet:thinking': {'input': Decimal('3000'), 'output': Decimal('3000'), 'input_imgs': Decimal('960')}, # 1M tokens } - def calculate_price( - self, version: str, input_tokens: int, output_tokens: int - ) -> Decimal: + def calculate_price(self, version: str, input_tokens: int, output_tokens: int, image: FieldFile) -> Decimal: price_map = self.TOKENS_COST[version.split('/')[1]] price = ( input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 - + price_map['input_imgs'] / 1_000 ) + if image: + price += price_map['input_imgs'] / 1_000 return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( @@ -94,7 +94,8 @@ class Claude(SimpleService): input_message.content_object.model, version=version, input_tokens=result[1], - output_tokens=result[2] + output_tokens=result[2], + image=image ) msgs = self.save_results(result[0], process_time) return msgs @@ -4,7 +4,6 @@ from datetime import timedelta from typing import Any, Iterator from messages.models import Message -from ml_model.models import ModelCategory from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -15,28 +14,13 @@ class Codellama(SimpleService): contains abstract method make, which makes a generation """ - muted = True - - title = 'Code LLaMA-34B' - description = 'Нейросеть, способная генерировать код из вашего контекста' - price = Decimal('0.558') - category = ModelCategory(title='Код', slug='code') - versions = [] - inputs = [] - parameters = [] - _CALLBACK = 'meta/codellama-34b:ffccbaa0d78e4dea7a9d46f29debaf390c2087c357e0632381f127382d3bf2fd' def calculate_price(self, messages: list[Message]) -> Decimal: - price = ( - sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) - * self.price - ) + price = sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) * self.price return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, r: Iterator[Any], t: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, r: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( content=''.join(word for word in r), @@ -7,7 +7,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -18,69 +17,6 @@ class Dalle(SimpleService): contains abstract method make, which makes a generation """ - title = 'Dalle' - description = 'Нейросеть, способная генерировать фотографии из вашего текста' - category = ModelCategory(title='Изображения', slug='images') - versions = [ - ModelVersion(name='Dalle 3', slug='sdxl-lightning-4step', default=True), - ] - inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] - parameters = [ - ModelParameter( - name='Негативный промпт', - key='negative_prompt', - type=ModelParameter.TypeChoices.STR, - ), - ModelParameter( - name='Ширина', - key='width', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1024, 'end': 1280, 'step': 256, 'default': 1024}, - ), - ModelParameter( - name='Высота', - key='height', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1024, 'end': 1280, 'step': 256, 'default': 1024}, - ), - ModelParameter( - name='Планировщик', - key='scheduler', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': [ - "DDIM", - "DPMSolverMultistep", - "HeunDiscrete", - "KarrasDPM", - "K_EULER_ANCESTRAL", - "K_EULER", - "PNDM", - "DPM++2MSDE" - ], - 'default': 'K_EULER', - }, - ), - ModelParameter( - name='Количество изображений', - key='num_outputs', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, - ), - ModelParameter( - name='Точность запроса', - key='guidance_scale', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 0, 'end': 50, 'step': 1, 'default': 0}, - ), - ModelParameter( - name='Шаги предобработки', - key='num_inference_steps', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 10, 'step': 1, 'default': 4}, - ), - ] - PRICE = Decimal('1') _CALLBACK = ( @@ -91,9 +27,7 @@ class Dalle(SimpleService): price = input_message.info.get('num_outputs', 1) * self.PRICE return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -77,9 +77,7 @@ class Deepl(SimpleService): ) ) else: - messages.append( - Message(content=content, content_object=self.store, elapsed_time=t) - ) + messages.append(Message(content=content, content_object=self.store, elapsed_time=t)) if save: return Message.objects.bulk_create(messages) return messages @@ -101,7 +99,8 @@ class Deepl(SimpleService): is_file = False start_time = time.time() languages = self.convert_languages( - input_message.info.get('source_lang'), input_message.info.get('target_lang') + input_message.info.get('source_lang'), + input_message.info.get('target_lang'), ) callback_data = dict( { @@ -135,7 +134,10 @@ class Deepl(SimpleService): translation = result.get() process_time = timedelta(seconds=(time.time() - start_time)) msgs = self.save_results( - content=str(translation), is_file=is_file, t=process_time, save=save + content=str(translation), + is_file=is_file, + t=process_time, + save=save, ) self.handle_invoice(self.neuron_model, messages=msgs) return msgs @@ -16,7 +16,10 @@ class Deepseek(SimpleService): 'input': Decimal('107.800') / 1_000_000, 'output': Decimal('195.800') / 1_000_000, }, - 'deepseek/deepseek-r1:free': {'input': Decimal('0'), 'output': Decimal('0')}, + 'deepseek/deepseek-r1:free': { + 'input': Decimal('0'), + 'output': Decimal('0'), + }, 'deepseek/deepseek-r1': { 'input': Decimal('176.0') / 1_000_000, 'output': Decimal('528.0') / 1_000_000, @@ -24,20 +27,12 @@ class Deepseek(SimpleService): } PRICE_BIAS = Decimal('0.05') - def calculate_price( - self, version: str, input_tokens: int, output_tokens: int - ) -> Decimal: + def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: price_map = self.TOKENS_COST[version] - price = ( - input_tokens * price_map['input'] - + output_tokens * price_map['output'] - + self.PRICE_BIAS - ) + price = input_tokens * price_map['input'] + output_tokens * price_map['output'] + self.PRICE_BIAS return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, content: Iterator[Any], t: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( content=content, @@ -67,11 +62,7 @@ class Deepseek(SimpleService): if ( (data := resp.json()) and data.get('choices') - and ( - content := ','.join( - [choice['message']['content'] for choice in data.get('choices')] - ) - ) + and (content := ','.join([choice['message']['content'] for choice in data.get('choices')])) ): result = content process_time = timedelta(seconds=(time.time() - start_time)) @@ -25,7 +25,7 @@ class Djourney(SimpleService): versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE) + ModelInput(type=ModelInput.TypeChoices.IMAGE), ] parameters = [ ModelParameter( @@ -63,7 +63,7 @@ class Djourney(SimpleService): 'KarrasDPM', 'K_EULER_ANCESTRAL', 'K_EULER', - 'PNDM' + 'PNDM', ], 'default': 'K_EULER', }, @@ -84,17 +84,13 @@ class Djourney(SimpleService): PRICE = Decimal('0.558') - _CALLBACK = ( - 'lorenzomarines/d-journey' - ':2d84f3049a0b3ed1a20fc657f39c4b1bdef1f7a8ea0ea8b6258e7c37296e039a' - ) + _CALLBACK = 'lorenzomarines/d-journey:2d84f3049a0b3ed1a20fc657f39c4b1bdef1f7a8ea0ea8b6258e7c37296e039a' def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True - ) -> list[Message]: + + def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -102,7 +98,7 @@ class Djourney(SimpleService): content_object=self.store, elapsed_time=time, content=prompt, - file=File(BytesIO(requests.get(image).content), '.png') + file=File(BytesIO(requests.get(image).content), '.png'), ) ) if save: @@ -75,8 +75,7 @@ class Epicphotogasm(SimpleService): translated_prompt = self.translate_prompt(input_message.content) activation_prompt = f'{translated_prompt}, cinematic' negative_prompt = self.translate_prompt( - input_message.info.pop('negative_prompt', '') - + ', UnrealisticDream, BadDream, EasyNegative' + input_message.info.pop('negative_prompt', '') + ', UnrealisticDream, BadDream, EasyNegative' ) callback_data = dict( prompt=activation_prompt, @@ -9,13 +9,14 @@ import requests from django.core.files import File from backend import settings -from messages.models import BaseStore, Message +from messages.models import Message from ml_model.models import ( ModelCategory, ModelInput, ModelParameter, ModelPaymentRule, ModelVersion, + NeuronModel, ) from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -31,7 +32,7 @@ class Flux(SimpleService): description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [ - ModelVersion(name='Flux-Schnell', slug='flux-schnell', default=True), + ModelVersion(name='Flux-Schnell', slug='flux-schnell'), ModelVersion(name='Flux-Pro1.1', slug='flux-pro-1.1'), ModelVersion(name='Flux-Dev', slug='flux-dev'), ModelVersion(name='Ultra', slug='flux-1.1-pro-ultra'), @@ -164,6 +165,10 @@ class Flux(SimpleService): _CALLBACK_BASE = 'black-forest-labs/' + @property + def neuron_model(self): + return NeuronModel.objects.get(title='Flux') + def _call_bfl_api(self, payload: dict) -> list: bfl_headers = { 'Content-Type': 'application/json', @@ -174,15 +179,21 @@ class Flux(SimpleService): 'get': 'https://api.bfl.ml/v1/get_result?id=', } response = requests.post( - url=f'{bfl_urls['generate']}{payload['version']}', + url=f'{bfl_urls["generate"]}{payload["version"]}', headers=bfl_headers, json=payload, ) if response.status_code != 200: raise Exception(response.json()) - result = requests.get(url=f'{bfl_urls['get']}{response.json().get('id')}', headers=bfl_headers) + result = requests.get( + url=f'{bfl_urls["get"]}{response.json().get("id")}', + headers=bfl_headers, + ) while result.json()['status'] not in ('Ready', 'Error'): - result = requests.get(url=f'{bfl_urls['get']}{response.json().get('id')}', headers=bfl_headers) + result = requests.get( + url=f'{bfl_urls["get"]}{response.json().get("id")}', + headers=bfl_headers, + ) return result.json()['result']['sample'] def calculate_price(self, input_message: Message) -> Decimal: @@ -197,11 +208,11 @@ class Flux(SimpleService): return payment_rule.rate def save_results( - self, - prompt: str, - images: list, - time: timedelta, - save: bool = True, + self, + prompt: str, + images: list, + time: timedelta, + save: bool = True, ) -> list[Message]: messages: list[Message] = [] for image in images: @@ -229,16 +240,16 @@ class Flux(SimpleService): kind = filetype.guess(input_message.file.read(20)) mime = kind.mime if kind else 'application/octet-stream' input_message.file.seek(0) - image = ( - f"data:{mime};base64," - f"{base64.b64encode(input_message.file.read()).decode('utf-8')}" - ) + image = f'data:{mime};base64,{base64.b64encode(input_message.file.read()).decode("utf-8")}' input_message.file.close() callback_data.update({'image': image}) if callback_data['version'] == 'flux-pro-1.1': images = [self._call_bfl_api(payload=callback_data)] else: - runner = replicate_run(f'{self._CALLBACK_BASE}{callback_data['version']}', callback_data) + runner = replicate_run( + f'{self._CALLBACK_BASE}{callback_data["version"]}', + callback_data, + ) images = runner if isinstance(runner, list) else [runner] process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message) @@ -50,8 +50,8 @@ class Granite(SimpleService): def __init__(self, store: BaseStore) -> None: super().__init__(store) self.urls = { - 'generate': 'https://api.replicate.com/v1/models/ibm-granite/granite-3.0-8b-instruct/', - 'get': 'https://api.replicate.com/v1/predictions/', + 'generate': 'https://api.replicate.com/v1/models/ibm-granite/granite-3.0-8b-instruct/predictions', + 'get': 'https://api.replicate.com/v1/predictions', } def _call_api(self, payload: dict) -> list: @@ -61,29 +61,31 @@ class Granite(SimpleService): } data = {'input': payload} response = requests.post( - url=f'{self.urls["generate"]}predictions', + url=self.urls['generate'], headers=headers, json=data, ) if response.status_code != 201: raise Exception(response.json()) result = requests.get( - url=f'{self.urls["get"]}{response.json().get("id")}', headers=headers + url=f'{self.urls["get"]}{response.json().get("id")}', + headers=headers, ) - while result.json()['status'] not in ('succeeded', 'failed', 'canceled'): + while result.json()['status'] not in ( + 'succeeded', + 'failed', + 'canceled', + ): result = requests.get( - url=f'{self.urls["get"]}{response.json().get("id")}', headers=headers + url=f'{self.urls["get"]}/{response.json().get("id")}', + headers=headers, ) return result.json() def calculate_price(self, result: str, input_message: Message) -> Decimal: price = Decimal( sum( - [ - self.TOKEN_PAYMENT_RULES['granite-output'] - / 1_000_000 - * len(result.split(' ')) - ] + [self.TOKEN_PAYMENT_RULES['granite-output'] / 1_000_000 * len(result.split(' '))] + [ self.TOKEN_PAYMENT_RULES['granite-input'] / 1_000_000 @@ -93,9 +95,7 @@ class Granite(SimpleService): ) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, result: str, time: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, result: str, time: timedelta, save: bool = True) -> list[Message]: msgs: list[Message] = [ Message( content_object=self.store, @@ -3,59 +3,45 @@ import time from datetime import timedelta from decimal import Decimal from io import BytesIO -from typing import Iterator, Any +from typing import Any, Iterator, Dict import filetype +from PIL import Image +from django.db.models.fields.files import FieldFile from messages.models import Message -from ml_model.models import ModelCategory, ModelVersion, ModelInput from ml_model.services.base import SimpleService from ml_model.tasks import openrouter_run from tools.chats.models import Chat from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore -from PIL import Image - class Grok(SimpleService): """ Grok Service contains abstract method make, which makes a generation """ - title = 'Grok' - description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' - category = ModelCategory(title='Чат-боты', slug='chat-bots') - versions = [ - ModelVersion(name='Grok 2 Vision', default=True, slug='grok-2-vision-1212'), - ] - inputs = [ - ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE) - ] - parameters = [] + TOKENS_COST = { 'grok-2-vision-1212': { - 'input': Decimal('400'), + 'input': Decimal('2000'), 'output': Decimal('2000'), - 'input_imgs': Decimal('720') + 'input_imgs': Decimal('720'), }, # 1M tokens and 1K imgs } - def calculate_price( - self, version: str, input_tokens: int, output_tokens: int - ) -> Decimal: + def calculate_price(self, version: str, input_tokens: int, output_tokens: int, image: FieldFile) -> Decimal: price_map = self.TOKENS_COST[version.split('/')[1]] price = ( - input_tokens * price_map['input'] / 1_000_000 - + output_tokens * price_map['output'] / 1_000_000 - + price_map['input_imgs'] / 1_000 + input_tokens * price_map['input'] / 1_000_000 + + output_tokens * price_map['output'] / 1_000_000 ) + if image: + price += price_map['input_imgs'] / 1_000 return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, content: Iterator[Any], t: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( content=content, @@ -69,10 +55,8 @@ class Grok(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: start_time = time.time() - version = f'x-ai/{input_message.info.pop('version', 'grok-2-vision-1212')}' - callback_data = { - **input_message.info - } + version = f'x-ai/{input_message.info.pop("version", "grok-2-vision-1212")}' + callback_data = {**input_message.info} messages = self.get_chat_history() image = input_message.file if image: @@ -82,21 +66,11 @@ class Grok(SimpleService): format = 'jpeg' if kind.extension == 'jpg' else kind.extension buf = BytesIO() normalized_image.save(buf, format=format) - image_url = ( - f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' - ) + image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' buf.close() messages[-1]['content'] = [ - { - 'type': 'text', - 'text': input_message.content - }, - { - 'type': 'image_url', - 'image_url': { - 'url': image_url - } - } + {'type': 'text', 'text': input_message.content}, + {'type': 'image_url', 'image_url': {'url': image_url}}, ] result = openrouter_run(version, messages, callback_data, self.title) process_time = timedelta(seconds=(time.time() - start_time)) @@ -104,12 +78,13 @@ class Grok(SimpleService): input_message.content_object.model, version=version, input_tokens=result[1], - output_tokens=result[2] + output_tokens=result[2], + image=image ) msgs = self.save_results(result[0], process_time) return msgs - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): + def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[Dict]: if isinstance(self.store, Chat): air_messages = list( reversed( @@ -134,9 +109,9 @@ class Grok(SimpleService): for msg in air_messages: content = msg.content or '' if msg.from_model: - memory.append({"role": "assistant", "content": content}) + memory.append({'role': 'assistant', 'content': content}) else: - memory.append({"role": "user", "content": content}) + memory.append({'role': 'user', 'content': content}) character_length = sum(len(content['content']) for content in memory) while character_length > max_character_limit: memory.pop(0) @@ -101,18 +101,13 @@ class Iconic(SimpleService): PRICE = Decimal('1.078') - _CALLBACK = ( - 'miike-ai/flux-ico' - ':478cae37f1aec0fde7977fdd54b272aaeabede7d8060801841920c16306369a9' - ) + _CALLBACK = 'miike-ai/flux-ico:478cae37f1aec0fde7977fdd54b272aaeabede7d8060801841920c16306369a9' def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -64,7 +64,7 @@ class Kandinsky(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, input_prompt: str, r: list[str], t: timedelta, save: bool = True + self, input_prompt: str, r: list[str], t: timedelta, save: bool = True ) -> list[Message]: out = [] for link in r: @@ -88,16 +88,16 @@ class Kandinsky(SimpleService): callback_data = dict( prompt=activation_prompt, negative_prompt=( - "any form of nudity, sexual content, explicit or suggestive themes, " - "graphic violence, disturbing imagery, offensive symbols, hate speech, abusive language, " - "discriminatory content, illegal activities, or any form of inappropriate or harmful material. " - "This includes, but is not limited to, full or partial nudity, suggestive body imagery, sexual innuendos, " - "pornographic content, sexual acts, and anything that could be perceived as sexual or inappropriate. " - "Also, avoid any form of graphic violence, torture, gore, blood, or injury depiction. " - "Do not include offensive symbols, hate speech, racial or ethnic slurs, or any material promoting hatred or discrimination. " - "Any content promoting illegal activities, substance abuse, self-harm, or violence is strictly prohibited. " - "Furthermore, avoid any content that is offensive, harmful, inappropriate for minors, or unsuitable for a general audience. " - f"Additionally, {negative_prompt} should be strictly avoided in any generated material." + 'any form of nudity, sexual content, explicit or suggestive themes, ' + 'graphic violence, disturbing imagery, offensive symbols, hate speech, abusive language, ' + 'discriminatory content, illegal activities, or any form of inappropriate or harmful material. ' + 'This includes, but is not limited to, full or partial nudity, suggestive body imagery, sexual innuendos, ' + 'pornographic content, sexual acts, and anything that could be perceived as sexual or inappropriate. ' + 'Also, avoid any form of graphic violence, torture, gore, blood, or injury depiction. ' + 'Do not include offensive symbols, hate speech, racial or ethnic slurs, or any material promoting hatred or discrimination. ' + 'Any content promoting illegal activities, substance abuse, self-harm, or violence is strictly prohibited. ' + 'Furthermore, avoid any content that is offensive, harmful, inappropriate for minors, or unsuitable for a general audience. ' + f'Additionally, {negative_prompt} should be strictly avoided in any generated material.' ), **input_message.info, ) @@ -86,17 +86,14 @@ class Lightning(SimpleService): PRICE = Decimal('0.825') _CALLBACK = ( - 'bytedance/sdxl-lightning-4step' - ':5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637' + 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637' ) def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -4,7 +4,6 @@ from datetime import timedelta from typing import Any, Iterator from messages.models import Message -from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -15,42 +14,13 @@ class Llama(SimpleService): contains abstract method make, which makes a generation """ - title = 'LLaMA' - description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' - category = ModelCategory(title='Чат-боты', slug='chat-bots') - versions = [ModelVersion(name='LLaMA2 70B', slug='llama2-70b', default=True)] - inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT)] - parameters = [ - ModelParameter( - name='Лучший процент', - key='top_p', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, - ), - ModelParameter( - name='Лучший коэффициент', - key='top_k', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, - ), - ModelParameter( - name='Температура', - key='temperature', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, - ), - ] - price = Decimal('1.078') - model_version = '' def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, r: Iterator[Any], t: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, r: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( content=''.join(word for word in r), @@ -66,17 +36,17 @@ class Llama(SimpleService): info = input_message.info context_ids: list[str] = info.pop('context_messages', []) content = ( - ''.join( - msg.content + ' ' - for msg in Message.objects.filter( - uid__in=context_ids, chats_chats_messages=self.store - ) - ) - + input_message.content + ''.join( + msg.content + ' ' + for msg in Message.objects.filter(uid__in=context_ids, chats_chats_messages=self.store) + ) + + input_message.content ) version = info.pop('version', 'llama2-70b') if version == 'llama2-70b': - self.model_version = 'meta/llama-2-70b-chat:35042c9a33ac8fd5e29e27fb3197f33aa483f72c2ce3b0b9d201155c7fd2a287' + self.model_version = ( + 'meta/llama-2-70b-chat:35042c9a33ac8fd5e29e27fb3197f33aa483f72c2ce3b0b9d201155c7fd2a287' + ) callback_data = dict( { 'prompt': content, @@ -7,7 +7,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -18,83 +17,15 @@ class Logoai(SimpleService): contains abstract method make, which makes a generation """ - title = 'Logo AI' - description = 'Нейросеть, способная генерировать фотографии из вашего текста' - category = ModelCategory(title='Изображения', slug='images') - versions = [] - inputs = [ - ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), - ] - parameters = [ - ModelParameter( - name='Негативный промпт', - key='negative_prompt', - type=ModelParameter.TypeChoices.STR, - ), - ModelParameter( - name='Ширина', - key='width', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1024, 'end': 2048, 'step': 128, 'default': 1024}, - ), - ModelParameter( - name='Высота', - key='height', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1024, 'end': 2048, 'step': 128, 'default': 1024}, - ), - ModelParameter( - name='Планировщик', - key='scheduler', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': [ - 'DDIM', - 'DPMSolverMultistep', - 'HeunDiscrete', - 'KarrasDPM', - 'K_EULER_ANCESTRAL', - 'K_EULER', - 'PNDM', - ], - 'default': 'K_EULER', - }, - ), - ModelParameter( - name='Количество изображений', - key='num_outputs', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, - ), - ModelParameter( - name='Точность запроса', - key='guidance_scale', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 50.0, 'step': 1.0, 'default': 7.5}, - ), - ModelParameter( - name='Шаги предобработки', - key='num_inference_steps', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 500, 'step': 1, 'default': 50}, - ), - ] - PRICE = Decimal('0.544') - _CALLBACK = ( - 'mejiabrayan/logoai' - ':67ed00e8999fecd32035074fa0f2e9a31ee03b57a8415e6a5e2f93a242ddd8d2' - ) + _CALLBACK = 'mejiabrayan/logoai:67ed00e8999fecd32035074fa0f2e9a31ee03b57a8415e6a5e2f93a242ddd8d2' def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -7,7 +7,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -18,36 +17,8 @@ class Midjourney(SimpleService): contains abstract method make, which makes a generation """ - title = 'Midjourney' - description = 'Нейросеть, способная генерировать фотографии из вашего текста' - price = Decimal('2') - category = ModelCategory(title='Изображения', slug='images') - inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] - parameters = [ - ModelParameter( - name='Соотношение сторон', - key='aspect_ratio', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': ['1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9'], - 'default': '1:1', - }, - ), - ModelParameter( - name='Количество изображений', - key='number_of_images', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 9, 'step': 1, 'default': 1}, - ), - ModelParameter( - name='Оптимизация промпта', - key='prompt_optimizer', - type=ModelParameter.TypeChoices.BOOL, - values={'default': True}, - ), - ] - _CALLBACK = 'minimax/image-01' + price = Decimal('2') def __init__(self, store): super().__init__(store) @@ -80,8 +51,6 @@ class Midjourney(SimpleService): callback_data = dict(prompt=activation_prompt, **input_message.info) results = replicate_run(self._CALLBACK, callback_data) process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice( - input_message.content_object.model, input_message=input_message - ) + self.handle_invoice(input_message.content_object.model, input_message=input_message) msgs = self.save_results(input_message.content, results, process_time, save) return msgs @@ -52,15 +52,11 @@ class MinIOService: if bucket not in self.buckets: raise Exception(_('Unknown bucket destination')) - result = self.minio.get_presigned_url( - 'GET', bucket, obj, expires=timedelta(days=7) - ) + result = self.minio.get_presigned_url('GET', bucket, obj, expires=timedelta(days=7)) return result - def get_object( - self, bucket: str, obj: str, as_BytesIO: bool = False - ) -> bytes | BytesIO: + def get_object(self, bucket: str, obj: str, as_BytesIO: bool = False) -> bytes | BytesIO: if bucket not in self.buckets: raise Exception(_('Unknown bucket destination')) @@ -4,7 +4,7 @@ from datetime import timedelta from typing import Any, Iterator from messages.models import Message -from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion +from ml_model.models import NeuronModel from ml_model.services.base import SimpleService from ml_model.tasks import mistral_run @@ -15,53 +15,29 @@ class Mistral(SimpleService): contains abstract method make, which makes a generation """ - title = 'Mistral' - description = 'Нейросеть, способная генерировать качественный текст из вашего промпта' - category = ModelCategory(title='Чат-боты', slug='chat-bots') - versions = [ - ModelVersion(name='Tiny', slug='mistral-tiny'), - ModelVersion(name='Small', slug='mistral-small'), - ModelVersion(name='Medium', slug='mistral-medium', default=True), - ] - inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] - parameters = [ - ModelParameter( - name='Лучший процент', - key='top_p', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 0.0, 'end': 1.0, 'step': 0.1, 'default': 0.5}, - ), - ModelParameter( - name='Температура', - key='temperature', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 0.0, 'end': 1.0, 'step': 0.1, 'default': 0.5}, - ), - ] - TOKEN_PAYMENT_RULES = { - 'mistral-tiny-input': Decimal(0.165), - 'mistral-tiny-output': Decimal(0.494), 'mistral-small-input': Decimal(0.706), 'mistral-small-output': Decimal(2.123), - 'mistral-medium-input': Decimal(2.937), - 'mistral-medium-output': Decimal(8.822), } def __init__(self, store): super().__init__(store) + @property + def neuron_model(self): + return NeuronModel.objects.get(title='Mistral') + def calculate_price(self, messages: list[Message], input_message: Message) -> Decimal: price = Decimal( sum( [ - self.TOKEN_PAYMENT_RULES[f"{input_message.info['version']}-output"] + self.TOKEN_PAYMENT_RULES[f'{input_message.info["version"]}-output'] / 1000 * len(msg.content.split(' ')) for msg in messages ] + [ - self.TOKEN_PAYMENT_RULES[f"{input_message.info['version']}-input"] + self.TOKEN_PAYMENT_RULES[f'{input_message.info["version"]}-input'] / 1000 * len(input_message.content.split(' ')) ] @@ -7,7 +7,6 @@ import requests from django.core.files.base import File from messages.models import BaseStore, Message -from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -18,46 +17,14 @@ class Musicgen(SimpleService): contains abstract method make, which makes a generation """ - title = 'MusicGen' - description = 'Нейросеть, способная генерировать музыку из ваших слов' - price = Decimal('0.633') - category = ModelCategory(title='Чат-боты', slug='chat-bots') - versions = [ - ModelVersion(name='Melody', slug='melody', default=True), - ModelVersion(name='Large', slug='large'), - ] - inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT)] - parameters = [ - ModelParameter( - name='Лучший процент', - key='top_p', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, - ), - ModelParameter( - name='Температура', - key='temperature', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, - ), - ModelParameter( - name='Длительность', - key='duration', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 10, 'step': 1, 'default': 5}, - ), - ] - - _CALLBACK = ( - 'meta/musicgen:7a76a8258b23fae65c5a22debb8841d1d7e816b75c2f24218cd2bd8573787906' - ) + _CALLBACK = 'meta/musicgen:7a76a8258b23fae65c5a22debb8841d1d7e816b75c2f24218cd2bd8573787906' def __init__(self, store: BaseStore): super().__init__(store) def calculate_price(self, process_time: timedelta) -> Decimal: price = Decimal(process_time.total_seconds()) * self.price - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results(self, r: str, t: timedelta, save: bool = True) -> list[Message]: out: list[Message] = [ @@ -90,17 +90,13 @@ class Pulid(SimpleService): PRICE = Decimal('0.374') - _CALLBACK = ( - 'zsxkib/pulid:43d309c37ab4e62361e5e29b8e9e867fb2dcbcec77ae91206a8d95ac5dd451a0' - ) + _CALLBACK = 'zsxkib/pulid:43d309c37ab4e62361e5e29b8e9e867fb2dcbcec77ae91206a8d95ac5dd451a0' def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -1,10 +1,9 @@ import time from datetime import timedelta from decimal import Decimal -from typing import Iterator, Any +from typing import Any, Iterator, Dict from messages.models import Message -from ml_model.models import ModelCategory, ModelVersion, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import openrouter_run from tools.chats.models import Chat @@ -18,33 +17,19 @@ class Qwen(SimpleService): contains abstract method make, which makes a generation """ - title = 'Qwen' - description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' - category = ModelCategory(title='Чат-боты', slug='chat-bots') - versions = [ - ModelVersion(name='QwQ 32B', default=True, slug='qwq-32b'), - # ModelVersion(name='QwQ 32B', slug='qwq-32b:free'), - ] - inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), ] - parameters = [] TOKENS_COST = { - 'qwq-32b': {'input': Decimal('24'), 'output': Decimal('36')}, # 1M tokens + 'qwq-32b': {'input': Decimal('36'), 'output': Decimal('36')}, # 1M tokens 'qwq-32b:free': {'input': Decimal('0'), 'output': Decimal('0')}, # 1M tokens } - def calculate_price( - self, version: str, input_tokens: int, output_tokens: int - ) -> Decimal: + def calculate_price(self, version: str, input_tokens: int, output_tokens: int) -> Decimal: price_map = self.TOKENS_COST[version.split('/')[1]] price = ( - input_tokens * price_map['input'] / 1_000_000 - + output_tokens * price_map['output'] / 1_000_000 + input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 ) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, content: Iterator[Any], t: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( content=content, @@ -58,13 +43,8 @@ class Qwen(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: start_time = time.time() - version = f'qwen/{input_message.info.pop('version', 'qwq-32b')}' - callback_data = { - 'provider': { - 'order': ['DeepInfra'] - }, - **input_message.info - } + version = f'qwen/{input_message.info.pop("version", "qwq-32b")}' + callback_data = {'provider': {'order': ['DeepInfra']}, **input_message.info} messages = self.get_chat_history() result = openrouter_run(version, messages, callback_data, self.title) process_time = timedelta(seconds=(time.time() - start_time)) @@ -72,12 +52,12 @@ class Qwen(SimpleService): input_message.content_object.model, version=version, input_tokens=result[1], - output_tokens=result[2] + output_tokens=result[2], ) msgs = self.save_results(result[0], process_time) return msgs - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): + def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[Dict]: if isinstance(self.store, Chat): air_messages = list( reversed( @@ -102,9 +82,9 @@ class Qwen(SimpleService): for msg in air_messages: content = msg.content or '' if msg.from_model: - memory.append({"role": "assistant", "content": content}) + memory.append({'role': 'assistant', 'content': content}) else: - memory.append({"role": "user", "content": content}) + memory.append({'role': 'user', 'content': content}) character_length = sum(len(content['content']) for content in memory) while character_length > max_character_limit: memory.pop(0) @@ -6,8 +6,7 @@ from io import BytesIO import requests from django.core.files import File -from backend import settings -from messages.models import BaseStore, Message +from messages.models import Message from ml_model.models import ( ModelCategory, ModelInput, @@ -28,7 +27,7 @@ class Recraft(SimpleService): description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [ - ModelVersion(name='Recraft V3', slug='recraft-v3', default=True), + ModelVersion(name='Recraft V3', slug='recraft-v3'), ModelVersion(name='Recraft V3 SVG', slug='recraft-v3-svg'), ] inputs = [ @@ -71,7 +70,7 @@ class Recraft(SimpleService): 'естественное освещение', 'студийный портрет', 'предпринимательство', - 'размытие движения' + 'размытие движения', ], 'default': 'любой', }, @@ -100,9 +99,21 @@ class Recraft(SimpleService): def _get_size(self, width: int, height: int) -> str: available_sizes = ( - (1024, 1024), (1365, 1024), (1024, 1365), (1536, 1024), (1024, 1536), - (1820, 1024), (1024, 1820), (1024, 2048), (2048, 1024), (1434, 1024), - (1024, 1434), (1024, 1280), (1280, 1024), (1024, 1707), (1707, 1024) + (1024, 1024), + (1365, 1024), + (1024, 1365), + (1536, 1024), + (1024, 1536), + (1820, 1024), + (1024, 1820), + (1024, 2048), + (2048, 1024), + (1434, 1024), + (1024, 1434), + (1024, 1280), + (1280, 1024), + (1024, 1707), + (1707, 1024), ) if width >= height: size = min(available_sizes, key=lambda size: abs(width - size[0])) @@ -114,12 +125,12 @@ class Recraft(SimpleService): return self.payment_rules[input_message.info.get('version', 'recraft-v3')] def save_results( - self, - prompt: str, - image: str, - extension: str, - time: timedelta, - save: bool = True, + self, + prompt: str, + image: str, + extension: str, + time: timedelta, + save: bool = True, ) -> list[Message]: messages: list[Message] = [] messages.append( @@ -158,12 +169,17 @@ class Recraft(SimpleService): 'гравировка': 'engraving', 'контурный рисунок': 'line_art', 'схема': 'line_circuit', - 'линогравюра': 'linocut' + 'линогравюра': 'linocut', } start_time = time.time() - extension = '.svg' if input_message.info.get('version', 'recraft-v3') == self.versions[1].slug else '.png' - size = self._get_size(input_message.info.pop('width', 1024), input_message.info.pop('height', 1024)) - callback_url = f'recraft-ai/{input_message.info.get('version', 'recraft-v3')}' + extension = ( + '.svg' if input_message.info.get('version', 'recraft-v3') == self.versions[1].slug else '.png' + ) + size = self._get_size( + input_message.info.pop('width', 1024), + input_message.info.pop('height', 1024), + ) + callback_url = f'recraft-ai/{input_message.info.get("version", "recraft-v3")}' callback_data = dict( { 'prompt': ( @@ -172,7 +188,7 @@ class Recraft(SimpleService): ), 'size': size, 'style': styles.get(input_message.info.pop('style'), 'любой'), - **input_message.info + **input_message.info, } ) image = replicate_run(callback_url, callback_data) @@ -83,17 +83,13 @@ class Sdxlemoji(SimpleService): PRICE = Decimal('0.529') - _CALLBACK = ( - 'fofr/sdxl-emoji:dee76b5afde21b0f01ed7925f0665b7e879c50ee718c5f78a9d38e04d523cc5e' - ) + _CALLBACK = 'fofr/sdxl-emoji:dee76b5afde21b0f01ed7925f0665b7e879c50ee718c5f78a9d38e04d523cc5e' def calculate_price(self, process_time: timedelta) -> Decimal: price = self.PRICE * Decimal(process_time.total_seconds()) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: messages: list[Message] = [] for image in images: messages.append( @@ -66,10 +66,7 @@ class Upscaleai(SimpleService): ), ] - _CALLBACK = ( - 'mcai/babes-v2.0-img2img' - ':2bca10ed539cf2196f18b4ec85128a80355d94934db8620884ecca552cdc4def' - ) + _CALLBACK = 'mcai/babes-v2.0-img2img:2bca10ed539cf2196f18b4ec85128a80355d94934db8620884ecca552cdc4def' def __init__(self, store): super().__init__(store) @@ -79,7 +76,11 @@ class Upscaleai(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, input_prompt: str, results: list[str], t: timedelta, save: bool = True + self, + input_prompt: str, + results: list[str], + t: timedelta, + save: bool = True, ) -> list[Message]: out: list[Message] = [] if len(results) > 1: @@ -90,7 +91,7 @@ class Upscaleai(SimpleService): content = requests.get(link).content except BaseException: continue - zipped.writestr(f"{uuid.uuid4()}_{link.split('/')[-1]}", content) + zipped.writestr(f'{uuid.uuid4()}_{link.split("/")[-1]}', content) zipped.close() out.append( Message( @@ -124,7 +125,10 @@ class Upscaleai(SimpleService): if input_message.file.name.split('.')[-1] == 'zip': results = upscale_run( dict( - archive=(input_message.file.name, BytesIO(input_message.file.read())), + archive=( + input_message.file.name, + BytesIO(input_message.file.read()), + ), prompt=(None, activation_prompt), upscale=(None, input_message.info.get('upscale', '0')), ) @@ -4,7 +4,6 @@ from datetime import timedelta from typing import Any, Iterator from messages.models import Message -from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run @@ -15,48 +14,16 @@ class Vicuna(SimpleService): contains abstract method make, which makes a generation """ - title = 'Vicuna' - description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' - price = Decimal('0.886') - category = ModelCategory(title='Чат-боты', slug='chat-bots') - versions = [ModelVersion(name='13Billion', slug='13B', default=True)] - inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] - parameters = [ - ModelParameter( - name='Температура', - key='temperature', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, - ), - ModelParameter( - name='Штраф за присутствие', - key='repetition_penalty', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, - ), - ModelParameter( - name='Лучший процент', - key='top_p', - type=ModelParameter.TypeChoices.FLOATRANGE, - values={'start': 1.0, 'end': 2.0, 'step': 0.1, 'default': 1.0}, - ), - ] - _CALLBACK = 'replicate/vicuna-13b:6282abe6a492de4145d7bb601023762212f9ddbbe78278bd6771c8b3b2f2a13b' def __init__(self, store): super().__init__(store) def calculate_price(self, messages: list[Message]) -> Decimal: - price = ( - sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) - * self.price - ) + price = sum([Decimal(msg.elapsed_time.total_seconds()) for msg in messages]) * self.price return price.quantize(Decimal('.01')) - def save_results( - self, r: Iterator[Any], t: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, r: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( content=''.join(word for word in r), @@ -8,7 +8,6 @@ from mutagen.mp3 import MP3 from mutagen.wave import WAVE from messages.models import Message -from ml_model.models import ModelCategory, ModelInput, ModelVersion from ml_model.services.base import SimpleService from ml_model.tasks import transcript_audio @@ -19,14 +18,6 @@ class Whisper(SimpleService): contains abstract method make, which makes a generation """ - title = 'Whisper' - description = 'Система автоматического распознавания голоса, обученная на 680000 часах аудио разных языков' - price = Decimal('0.088') # Per second > 4.8/min - category = ModelCategory(title='Аудио', slug='audio') - versions = [ModelVersion(name='Standart', slug='whisper-1', default=True)] - inputs = [ModelInput(type=ModelInput.TypeChoices.AUDIO)] - parameters = [] - def make(self, input_message: Message, save: bool = True) -> list[Message]: audio = BytesIO(input_message.file.read()) start_time = datetime.now() @@ -53,9 +44,7 @@ class Whisper(SimpleService): price = Decimal(length) * self.price return price.quantize(Decimal('.01')) - def save_results( - self, r: str, file: File, t: timedelta, save: bool = True - ) -> list[Message]: + def save_results(self, r: str, file: File, t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( content=r, @@ -1,6 +1,12 @@ +import xml.etree.ElementTree as ET +from io import BytesIO +from tempfile import NamedTemporaryFile + from django.contrib import admin +from django.db.models.fields.files import FieldFile +from django.forms import ModelForm from django.http.response import HttpResponse as HttpResponse -from import_export.admin import ImportExportMixin +from import_export.admin import ExportActionModelAdmin, ImportExportMixin from ordered_model.admin import ( OrderedInlineModelAdminMixin, OrderedModelAdmin, @@ -16,10 +22,18 @@ from ml_model.models import ( ModelPaymentRule, ModelSettings, ModelStat, + ModelTag, ModelVersion, NeuronModel, ) -from ml_model.resources import NeuronModelResource +from ml_model.resources import ( + ModelCategoryResource, + ModelInputResource, + ModelParameterResource, + ModelTagResource, + ModelVersionResource, + NeuronModelResource, +) class ModelSettingsInline(admin.TabularInline): @@ -53,7 +67,13 @@ class ModelVersionsInline(OrderedTabularInline): model = ModelVersion extra = 0 classes = ['collapse'] - fields = ('name', 'description', 'slug', 'default', 'order', 'move_up_down_links') + fields = ( + 'name', + 'description', + 'slug', + 'order', + 'move_up_down_links', + ) readonly_fields = ('order', 'move_up_down_links') ordering = ('order',) @@ -66,17 +86,17 @@ class ModelStatInline(admin.TabularInline): @admin.register(ModelCategory) -class ModelCategoryAdmin(admin.ModelAdmin): +class ModelCategoryAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): list_display = ['title', 'slug'] - list_display_links = ('title', 'slug') prepopulated_fields = {'slug': ('title',)} + resource_classes = [ModelCategoryResource] @admin.register(NeuronModel) -class NeuronModelModelAdmin( - OrderedInlineModelAdminMixin, ImportExportMixin, OrderedModelAdmin +class NeuronModelAdmin( + OrderedInlineModelAdminMixin, ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin ): - list_display = ['title', 'is_active', 'category', 'move_up_down_links'] + list_display = ['title', '_active', '_category', 'move_up_down_links'] resource_classes = (NeuronModelResource,) prepopulated_fields = {'slug': ('title',)} inlines = [ @@ -87,16 +107,75 @@ class NeuronModelModelAdmin( ModelInputsInline, ModelParametersInline, ] - list_filter = ['model_settings__is_active', 'category__title'] + list_filter = ['model_settings__is_active', 'category', 'tags'] + + filter_horizontal = ['tags'] @admin.display(description='Активна?', boolean=True) - def is_active(self, obj: NeuronModel): - if obj.settings: - return obj.settings.is_active + def _active(self, obj: NeuronModel): + return obj.active - @admin.display(description='Категория', boolean=True) - def category(self, obj: NeuronModel): - return obj.category.title + @admin.display(description='Категория') + def _category(self, obj: NeuronModel): + if obj.category: + return obj.category.title + return 'Не присвоена' + + +@admin.register(ModelTag) +class ModelTagAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): + list_display = ['title', 'slug'] + prepopulated_fields = {'slug': ['title']} + resource_classes = [ModelTagResource] + + def save_model(self, request, obj: ModelTag, form: ModelForm, change): + if not isinstance(form.cleaned_data['icon'], FieldFile) and obj.icon.readable(): + with NamedTemporaryFile() as f: + obj.icon.seek(0) + content = obj.icon.read() + f.write(content) + f.seek(0) + root = ET.parse(f.name).getroot() + f.seek(0) + for element in root.iter(): + for attr in ['width', 'height']: + if attr in element.attrib: + del element.attrib[attr] + for attr, val in { + 'fill': 'currentColor', + 'stroke': 'currentColor', + }.items(): + element.attrib[attr] = val + buf = BytesIO() + ET.ElementTree(root).write(buf) + buf.seek(0) + from django.core.files import File + + obj.icon.save( + form.cleaned_data['icon'].name, + File(buf, form.cleaned_data['icon'].name), + ) + return super().save_model(request, obj, form, change) + + +@admin.register(ModelVersion) +class ModelVersionAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): + resource_classes = [ModelVersionResource] + list_filter = ['model'] + + +@admin.register(ModelInput) +class ModelInputAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): + resource_classes = [ModelInputResource] + filter_horizontal = ['versions'] + list_filter = ('model', 'versions') + + +@admin.register(ModelParameter) +class ModelParameterAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): + resource_classes = [ModelParameterResource] + filter_horizontal = ['versions'] + list_filter = ('model', 'versions') class ConfigurationParameterInline(admin.TabularInline): @@ -104,7 +183,7 @@ class ConfigurationParameterInline(admin.TabularInline): extra = 1 can_delete = False - def has_change_permission(self, request, obj=...): + def has_change_permission(self, *args, **kwargs): return False @@ -1,4 +1,5 @@ from django.apps import AppConfig +from django.core.signals import setting_changed from django.utils.translation import gettext_lazy as _ @@ -6,3 +7,10 @@ class MLModelConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'ml_model' verbose_name = _('Neuron Models') + + def ready(self): + from .signals import create_settings + + setting_changed.connect(create_settings) + + return super().ready() @@ -2,12 +2,15 @@ from uuid import uuid4 from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.fields import ArrayField +from django.core.validators import FileExtensionValidator from django.db import models from django.db.models import F, QuerySet from django.utils.translation import gettext_lazy as _ from django_minio_backend.models import MinioBackend from ordered_model.models import OrderedModel, OrderedModelManager +from core.fields import ColorField from core.models import BaseModel @@ -27,13 +30,44 @@ class ModelCategory(models.Model): verbose_name_plural = _('Categories') -def upload_model_avatar(instance, filename): +def model_tag_icon_uploader(instance: 'ModelTag', filename: str): + return f'tags/{instance.slug}/{filename}' + + +class ModelTag(models.Model): + title = models.CharField(max_length=100, verbose_name=_('Title')) + slug = models.SlugField(max_length=120, verbose_name=_('Slug'), unique=True) + color = ColorField(verbose_name=_('Color'), null=True, blank=True) + icon = models.FileField( + storage=MinioBackend('air-models'), + upload_to=model_tag_icon_uploader, + validators=[FileExtensionValidator(['svg'], _('Not SVG-pictures not allowed'))], + blank=True, + null=True, + verbose_name=_('Icon'), + ) + + def __str__(self): + return self.title + + class Meta: + verbose_name = _('Model Tag') + verbose_name_plural = _('Model Tags') + + +def upload_model_avatar(instance: 'NeuronModel', filename: str): return f'avatars/{instance.slug}/{filename}' class NeuronModel(BaseModel, OrderedModel): title = models.CharField(max_length=300, verbose_name=_('Title')) - description = models.TextField(max_length=4000, verbose_name=_('Description')) + alternative_titles = ArrayField( + models.CharField(max_length=30), + default=list, + blank=True, + verbose_name=_('Alternative Titles'), + ) + description = models.TextField(verbose_name=_('Description'), null=True, blank=True) slug = models.SlugField( verbose_name=_('Slug'), unique=True, @@ -43,6 +77,8 @@ class NeuronModel(BaseModel, OrderedModel): category = models.ForeignKey( 'ModelCategory', on_delete=models.PROTECT, + null=True, + blank=True, verbose_name=_('Category'), related_name='category_models', ) @@ -54,6 +90,8 @@ class NeuronModel(BaseModel, OrderedModel): verbose_name=_('Avatar'), ) + tags = models.ManyToManyField(ModelTag, blank=True, verbose_name=_('Tags'), related_name='models_tags') + objects = OrderedModelManager() order_with_respect_to = 'category' @@ -92,9 +130,13 @@ class NeuronModel(BaseModel, OrderedModel): except ModelSettings.DoesNotExist: return None + @property + def active(self) -> bool: + return bool(self.settings) and self.settings.is_active + @property def blocked(self) -> bool: - return self.settings and not self.settings.is_active + return not self.active def __str__(self): return self.title @@ -117,17 +159,12 @@ class ModelDepends(models.Model): class ModelSettings(models.Model): - model = models.OneToOneField( - NeuronModel, on_delete=models.CASCADE, related_name='model_settings' - ) + model = models.OneToOneField(NeuronModel, on_delete=models.CASCADE, related_name='model_settings') is_active = models.BooleanField( - default=True, + default=False, verbose_name=_('Is active'), help_text='Модель активна для всех пользователей', ) - authorization_token = models.CharField( - null=True, blank=True, max_length=255, verbose_name=_('Authorization token') - ) class Meta: verbose_name = _('Settings') @@ -139,11 +176,8 @@ class ModelSettings(models.Model): class ModelVersion(ModelDepends, OrderedModel): name = models.CharField(max_length=16, verbose_name=_('Name')) - description = models.CharField( - max_length=128, null=True, blank=True, verbose_name=_('Description') - ) + description = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Description')) slug = models.CharField(max_length=32, unique=True, verbose_name=_('Slug')) - default = models.BooleanField(default=False, verbose_name=_('Default')) order_with_respect_to = 'model' @@ -216,7 +250,10 @@ class ModelParameter(ModelDepends, ModelVersionsDepends, OrderedModel): INT = 'int', _('Integer') # Integer FLOAT = 'float', _('Float') # Float STR = 'str', _('String') # String - LIST = 'list', _('List') # That accepts a lot of values, that may changed + LIST = ( + 'list', + _('List'), + ) # That accepts a lot of values, that may changed FLOATRANGE = ( 'floatrange', _('Float range'), @@ -228,9 +265,7 @@ class ModelParameter(ModelDepends, ModelVersionsDepends, OrderedModel): BOOL = 'bool', _('Logical') # True/False name = models.CharField(verbose_name=_('Title'), max_length=50) - description = models.CharField( - verbose_name=_('Description'), null=True, blank=True, max_length=512 - ) + description = models.CharField(verbose_name=_('Description'), null=True, blank=True, max_length=512) key = models.CharField(verbose_name=_('Key'), max_length=40) type = models.CharField( verbose_name=_('Type'), @@ -241,9 +276,7 @@ class ModelParameter(ModelDepends, ModelVersionsDepends, OrderedModel): blank=True, default=dict, verbose_name=_('Values'), - help_text=_( - 'These values can contain different interfaces and default value optional' - ), + help_text=_('These values can contain different interfaces and default value optional'), ) hidden = models.BooleanField(_('Hidden'), default=False) required = models.BooleanField(_('Required'), default=False) @@ -271,7 +304,9 @@ class ModelPaymentRule(ModelDepends, ModelVersionsDepends): ALL = 'all', _('By all data') strategy = models.CharField( - max_length=32, choices=StrategyChoices.choices, verbose_name=_('Strategy') + max_length=32, + choices=StrategyChoices.choices, + verbose_name=_('Strategy'), ) interaction_type = models.CharField( max_length=32, @@ -305,9 +340,7 @@ class ModelPaymentRule(ModelDepends, ModelVersionsDepends): class ModelStat(ModelDepends): generation_time = models.DurationField(verbose_name='Время генерации') - tokens_cost = models.DecimalField( - max_digits=50, decimal_places=10, verbose_name='Цена в токенах' - ) + tokens_cost = models.DecimalField(max_digits=50, decimal_places=10, verbose_name='Цена в токенах') created_at = models.DateTimeField(auto_now_add=True, verbose_name='Когда создано') class Meta: @@ -317,9 +350,7 @@ class ModelStat(ModelDepends): class ModelConfiguration(models.Model): - id = models.UUIDField( - primary_key=True, default=uuid4, editable=False, verbose_name='ID' - ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') model = models.ForeignKey( NeuronModel, on_delete=models.PROTECT, @@ -1,76 +1,112 @@ from import_export import fields as ie_fields from import_export.resources import ModelResource -from import_export.widgets import ForeignKeyWidget +from import_export.widgets import ForeignKeyWidget, ManyToManyWidget from ml_model.models import ( ModelCategory, ModelInput, ModelParameter, + ModelPaymentRule, + ModelTag, ModelVersion, NeuronModel, ) -from ml_model.serializers import ( - ModelInputSerializer, - ModelParameterSerializer, - ModelVersionSerializer, -) + + +class ModelTagResource(ModelResource): + class Meta: + model = ModelTag + exclude = ('id', 'color', 'icon') + import_id_fields = ('slug',) + + +class ModelCategoryResource(ModelResource): + class Meta: + model = ModelCategory + exclude = ('id',) + import_id_fields = ('slug',) class NeuronModelResource(ModelResource): category = ie_fields.Field( - column_name='Категория', + column_name='category', attribute='category', - readonly=True, widget=ForeignKeyWidget(ModelCategory, 'slug'), ) + tags = ie_fields.Field( + column_name='tags', + attribute='tags', + widget=ManyToManyWidget(ModelTag, ',', 'slug'), + ) + + class Meta: + model = NeuronModel + exclude = ('uid', 'order', 'created_at', 'updated_at', 'image', 'description') + import_id_fields = ('slug',) + + +class ModelVersionResource(ModelResource): + model = ie_fields.Field( + column_name='model', + attribute='model', + widget=ForeignKeyWidget(NeuronModel, 'slug'), + ) + + class Meta: + model = ModelVersion + exclude = ('id', 'description') + import_id_fields = ('model', 'slug') + + +class ModelInputResource(ModelResource): + model = ie_fields.Field( + column_name='model', + attribute='model', + widget=ForeignKeyWidget(NeuronModel, 'slug'), + ) versions = ie_fields.Field( - column_name='Версии', + column_name='versions', + attribute='versions', + widget=ManyToManyWidget(ModelVersion, ',', 'slug'), ) - inputs = ie_fields.Field( - column_name='Входящие потоки', + + class Meta: + model = ModelInput + exclude = ('id',) + import_id_fields = ('model', 'versions', 'type') + + +class ModelParameterResource(ModelResource): + model = ie_fields.Field( + column_name='model', + attribute='model', + widget=ForeignKeyWidget(NeuronModel, 'slug'), ) - parameters = ie_fields.Field( - column_name='Параметры', + versions = ie_fields.Field( + column_name='versions', + attribute='versions', + widget=ManyToManyWidget(ModelVersion, ',', 'slug'), ) - def dehydrate_versions(self, obj: NeuronModel): - return ModelVersionSerializer(obj.versions, many=True).data - - def dehydrate_inputs(self, obj: NeuronModel): - return ModelInputSerializer(obj.inputs, many=True).data - - def dehydrate_parameters(self, obj: NeuronModel): - return ModelParameterSerializer(obj.parameters, many=True).data - - def after_init_instance(self, instance, new, row, **kwargs): - raw_versions = row.pop('versions', []) - for raw_version in raw_versions: - ModelVersion.objects.get_or_create( - model=instance, slug=raw_version.pop('slug'), defaults=raw_version - ) - raw_inputs = row.pop('inputs', []) - for raw_input in raw_inputs: - depends_on_versions = raw_input.pop('versions', []) - input, _ = ModelInput.objects.get_or_create( - model=instance, type=raw_input.pop('type'), defaults=raw_input - ) - input.versions.set(ModelVersion.objects.filter(slug__in=depends_on_versions)) - raw_parameters = row.pop('parameters', []) - for raw_parameter in raw_parameters: - depends_on_versions = raw_parameter.pop('versions', []) - parameter, _ = ModelParameter.objects.get_or_create( - model=instance, - key=raw_parameter.pop('key'), - values=raw_parameter.pop('values'), - defaults=raw_parameter, - ) - parameter.versions.set( - ModelVersion.objects.filter(slug__in=depends_on_versions) - ) - return super().after_init_instance(instance, new, row, **kwargs) + class Meta: + model = ModelParameter + exclude = ('id',) + import_id_fields = ('model', 'versions', 'key') + + +class ModelPaymentRuleResource(ModelResource): + model = ie_fields.Field( + column_name='model', + attribute='model', + widget=ForeignKeyWidget(NeuronModel, 'slug'), + ) + versions = ie_fields.Field( + column_name='versions', + attribute='versions', + widget=ManyToManyWidget(ModelVersion, ',', 'slug'), + ) class Meta: - model = NeuronModel - use_transactions = True - exclude = ('uid', 'order', 'created_at', 'updated_at', 'description') - import_id_fields = ('slug',) + model = ModelPaymentRule + exclude = ('id',) + import_id_fields = ('model', 'versions', 'strategy', 'interaction_type') @@ -31,4 +31,4 @@ class ModelConfigurationSchema(ModelSchema): class NeuronModelLink(ModelSchema): class Meta: model = NeuronModel - fields = ('title', 'slug') + fields = ('title', 'slug', 'alternative_titles') @@ -5,6 +5,7 @@ from ml_model.models import ( ModelInput, ModelParameter, ModelSettings, + ModelTag, ModelVersion, NeuronModel, ) @@ -13,7 +14,7 @@ from ml_model.models import ( class ModelSettingsSerializer(serializers.ModelSerializer): class Meta: model = ModelSettings - exclude = ('id', 'model', 'authorization_token') + exclude = ('id', 'model') class ModelVersionSerializer(serializers.ModelSerializer): @@ -38,23 +39,31 @@ class ModelInputSerializer(serializers.ModelSerializer): exclude = ('id', 'model') +class ModelTagSerializer(serializers.ModelSerializer): + class Meta: + model = ModelTag + exclude = ('id', 'slug') + + class NeuronModelSerializer(serializers.ModelSerializer): - settings = ModelSettingsSerializer() parameters = ModelParameterSerializer(many=True) versions = ModelVersionSerializer(many=True) inputs = ModelInputSerializer(many=True) + tags = ModelTagSerializer(many=True) + blocked = serializers.BooleanField() class Meta: model = NeuronModel - exclude = ('created_at', 'updated_at', 'order', 'category') + exclude = ('created_at', 'updated_at', 'order', 'category', 'alternative_titles') class NeuronModelsSerializer(serializers.ModelSerializer): blocked = serializers.BooleanField() + tags = ModelTagSerializer(many=True) class Meta: model = NeuronModel - exclude = ('created_at', 'updated_at', 'order', 'category', 'uid') + exclude = ('created_at', 'updated_at', 'order', 'category', 'alternative_titles', 'uid') class ModelCategorySerializer(serializers.ModelSerializer): @@ -0,0 +1,12 @@ +from typing import Type + +from django.db.models.signals import post_save +from django.dispatch import receiver + +from ml_model.models import ModelSettings, NeuronModel + + +@receiver(post_save, sender=NeuronModel) +def create_settings(sender: Type[NeuronModel], instance: NeuronModel, created: bool, **kwargs): + if created: + ModelSettings.objects.create(model=instance) @@ -22,7 +22,7 @@ def create_d_image(payload: dict): if payload.get('image'): return json.loads( requests.post( - f"http://{settings.OPENAI_PROXY_HOST}?{'&'.join([f'proxies={proxy.protocol}://{proxy.address}' for proxy in Proxy.objects.all()])}&uri=images/variations&token={settings.OPENAI_API_KEY}", + f'http://{settings.OPENAI_PROXY_HOST}?{"&".join([f"proxies={proxy.protocol}://{proxy.address}" for proxy in Proxy.objects.all()])}&uri=images/variations&token={settings.OPENAI_API_KEY}', json=payload, timeout=(600, 600), headers={ @@ -32,7 +32,7 @@ def create_d_image(payload: dict): ) return json.loads( requests.post( - f"http://{settings.OPENAI_PROXY_HOST}?{'&'.join([f'proxies={proxy.protocol}://{proxy.address}' for proxy in Proxy.objects.all()])}&uri=images/generations&token={settings.OPENAI_API_KEY}", + f'http://{settings.OPENAI_PROXY_HOST}?{"&".join([f"proxies={proxy.protocol}://{proxy.address}" for proxy in Proxy.objects.all()])}&uri=images/generations&token={settings.OPENAI_API_KEY}', json=payload, timeout=(600, 600), headers={ @@ -45,7 +45,7 @@ def create_d_image(payload: dict): @shared_task(serializer='pickle') def create_sd_image(api_key: str, payload: dict, **kwargs) -> list[tuple[BytesIO, str]]: response = requests.post( - f"https://api.stability.ai/v1/generation/{payload.pop('engine')}/text-to-image", + f'https://api.stability.ai/v1/generation/{payload.pop("engine")}/text-to-image', headers={ 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -54,10 +54,7 @@ def create_sd_image(api_key: str, payload: dict, **kwargs) -> list[tuple[BytesIO json=dict(text_prompts=[{'text': payload.pop('prompt')}], **payload), ) if response.status_code == 200: - return [ - BytesIO(base64.b64decode(gen['base64'])) - for gen in response.json()['artifacts'] - ] + return [BytesIO(base64.b64decode(gen['base64'])) for gen in response.json()['artifacts']] else: raise Exception(response.json()) @@ -94,7 +91,7 @@ def translate(payload: dict[str, Any]) -> Response | TextResult: def transcript_audio(payload: dict[str, Any]): return json.loads( requests.post( - f"http://{settings.OPENAI_PROXY_HOST}?{'&'.join([f'proxies={proxy.protocol}://{proxy.address}' for proxy in Proxy.objects.all()])}&uri=audio/transcript&token={settings.OPENAI_API_KEY}", + f'http://{settings.OPENAI_PROXY_HOST}?{"&".join([f"proxies={proxy.protocol}://{proxy.address}" for proxy in Proxy.objects.all()])}&uri=audio/transcript&token={settings.OPENAI_API_KEY}', json=payload, timeout=(600, 600), headers={ @@ -120,6 +117,7 @@ def openrouter_run(version: str, messages: list, callback_data: dict, model_name base_url='https://openrouter.ai/api/v1', headers={'Authorization': f'Bearer {settings.OPENROUTER_API_KEY}'}, proxy=f'{proxy.protocol}://{proxy.address}', + timeout=600, ) as client: resp = client.post( 'chat/completions', @@ -179,7 +177,9 @@ def claude_run(payload: dict[str, Any]): } return json.loads( requests.post( - 'https://api.anthropic.com/v1/messages', json.dumps(payload), headers=headers + 'https://api.anthropic.com/v1/messages', + json.dumps(payload), + headers=headers, ).content ) @@ -1,6 +1,10 @@ from django.urls import path -from ml_model.views import CategoriesAPIView, NeuronModelAPIView, NeuronModelsAPIView +from ml_model.views import ( + CategoriesAPIView, + NeuronModelAPIView, + NeuronModelsAPIView, +) app_name = 'ml_model' @@ -2,8 +2,12 @@ from random import randint from typing import Literal from authentication.models import CustomUserModel -from authentication.selectors.account_status_selector import AccountStatusSelector -from authentication.selectors.business_account_selector import BusinessAccountSelector +from authentication.selectors.account_status_selector import ( + AccountStatusSelector, +) +from authentication.selectors.business_account_selector import ( + BusinessAccountSelector, +) def random_with_N_digits(n): @@ -56,8 +56,6 @@ class NeuronModelAPIView(APIView): """Retrieve model by slug""" return Response( NeuronModelSerializer( - NeuronModelSelector(request.user).get_model_by_slug( - slug=slug, hidden=False - ) + NeuronModelSelector(request.user).get_model_by_slug(slug=slug, hidden=False) ).data ) @@ -16,7 +16,11 @@ yookassa_payments_total.extend(yookassa_payments.items) while yookassa_payments.next_cursor: yookassa_payments = YookassaPayment.list( - params={'status': 'succeeded', 'limit': 100, 'cursor': yookassa_payments.next_cursor} + params={ + 'status': 'succeeded', + 'limit': 100, + 'cursor': yookassa_payments.next_cursor, + } ) yookassa_payments_total.extend(yookassa_payments.items) @@ -11,7 +11,10 @@ class Command(BaseCommand): user.referral_code = PromoCode.objects.create( promocode_type=PromoCode.REFERRAL, owner=user, - function_call={'name': 'add_tokens_referral', 'arguments': {'amount': 5}}, + function_call={ + 'name': 'add_tokens_referral', + 'arguments': {'amount': 5}, + }, ) user.save() return 'Done!' @@ -49,9 +49,7 @@ class Payment(BaseModel): null=False, blank=False, ) - description = models.CharField( - verbose_name=_('Description'), max_length=128, blank=True, null=True - ) + description = models.CharField(verbose_name=_('Description'), max_length=128, blank=True, null=True) def __str__(self): return f'{self.uid}' @@ -24,9 +24,7 @@ class PaymentPlan(BaseModel): ) is_corporate = models.BooleanField(default=False, verbose_name=_('Is corporate')) is_recurrent = models.BooleanField(default=False, verbose_name=_('Is recurrent')) - title = models.CharField( - verbose_name=_('Title'), max_length=120, null=True, blank=True - ) + title = models.CharField(verbose_name=_('Title'), max_length=120, null=True, blank=True) duration = models.CharField( verbose_name=_('Duration'), max_length=100, @@ -84,7 +82,11 @@ class PaymentPlanUserInfo(BaseModel): ) def save( - self, force_insert=False, force_update=False, using=None, update_fields=None + self, + force_insert=False, + force_update=False, + using=None, + update_fields=None, ): self.last_payment_at = datetime.now().date() self.next_payment_at = self.last_payment_at + relativedelta(months=1) @@ -38,7 +38,10 @@ class PromoCode(BaseModel): unique=True, ) promocode_type = models.CharField( - verbose_name=_('Type'), choices=PROMOCODE_TYPES, null=False, blank=False + verbose_name=_('Type'), + choices=PROMOCODE_TYPES, + null=False, + blank=False, ) function_call = models.JSONField( @@ -79,9 +82,7 @@ class PromoCode(BaseModel): help_text=_('Can be activated only one time'), ) - is_active = models.BooleanField( - verbose_name=_('Is active'), blank=True, null=False, default=True - ) + is_active = models.BooleanField(verbose_name=_('Is active'), blank=True, null=False, default=True) def clean(self): if self.promocode_type != PromoCode.REFERRAL and self.owner: @@ -103,9 +104,7 @@ class PromoCodeActivation(BaseModel): on_delete=models.CASCADE, verbose_name=_('Activated by'), ) - promocode = models.ForeignKey( - to=PromoCode, on_delete=models.CASCADE, verbose_name=_('Promocode') - ) + promocode = models.ForeignKey(to=PromoCode, on_delete=models.CASCADE, verbose_name=_('Promocode')) def save(self, *args, **kwargs) -> None: if self._state.adding: @@ -12,12 +12,8 @@ class UserPaymentMethod(BaseModel): related_name='payment_methods', verbose_name='Пользователь', ) - currently_active = models.BooleanField( - default=False, verbose_name='Способ платежа активен' - ) - payment_method_id = models.UUIDField( - unique=True, verbose_name='UID платёжного метода' - ) + currently_active = models.BooleanField(default=False, verbose_name='Способ платежа активен') + payment_method_id = models.UUIDField(unique=True, verbose_name='UID платёжного метода') card_type = models.CharField(max_length=15, verbose_name='Тип карты') last_four = models.CharField(max_length=4, verbose_name='Последние 4 цифры карты') @@ -10,7 +10,10 @@ from authentication.models.choices import InvitationStatus from authentication.models.user import CustomUserModel from authentication.selectors.user_selector import UserSelector from payments.models.payment_plan import PaymentPlan -from payments.serializers import PaymentPlanSerializer, UserPaymentPlanSerializer +from payments.serializers import ( + PaymentPlanSerializer, + UserPaymentPlanSerializer, +) logger = logging.getLogger(__name__) @@ -87,12 +90,8 @@ class PaymentPlanSelector: return UserPaymentPlanSerializer(plan) - def get_free_plan( - self, corporate: bool = False, recurrent: bool = False - ) -> PaymentPlan: - return PaymentPlan.objects.get_or_create( - price=0, is_corporate=corporate, is_recurrent=recurrent - )[0] + def get_free_plan(self, corporate: bool = False, recurrent: bool = False) -> PaymentPlan: + return PaymentPlan.objects.get_or_create(price=0, is_corporate=corporate, is_recurrent=recurrent)[0] def is_plan_paid(self) -> bool: return self.user.payment_plan.plan.price != Decimal('0') @@ -23,10 +23,16 @@ class PaymentSelector(BaseSelector): return payments @classmethod - def count_by_utm(cls, utm: UTM, from_date: date | None = None, to_date: date | None = None): + def count_by_utm( + cls, + utm: UTM, + from_date: date | None = None, + to_date: date | None = None, + ): if from_date and to_date: return Payment.objects.filter( - created_at__date__range=[from_date, to_date], user__in=utm.users.all() + created_at__date__range=[from_date, to_date], + user__in=utm.users.all(), ).count() elif from_date: return Payment.objects.filter(created_at__date__gte=from_date, user__in=utm.users.all()).count() @@ -35,11 +41,17 @@ class PaymentSelector(BaseSelector): return Payment.objects.filter(user__in=utm.users.all()).count() @classmethod - def sum_by_utm(cls, utm: UTM, from_date: date | None = None, to_date: date | None = None): + def sum_by_utm( + cls, + utm: UTM, + from_date: date | None = None, + to_date: date | None = None, + ): if from_date and to_date: float( Payment.objects.filter( - created_at__date__range=[from_date, to_date], user__in=utm.users.all() + created_at__date__range=[from_date, to_date], + user__in=utm.users.all(), ).aggregate(Sum('amount', default=0))['amount__sum'] ) elif from_date: @@ -16,7 +16,11 @@ class ModelBillingService: if user_type in ['regular', 'business_host']: plan = self.user.payment_plan allowance = None - elif user_type in ['business_account', 'business_admin', 'business_security']: + elif user_type in [ + 'business_account', + 'business_admin', + 'business_security', + ]: plan = self.user.business_account.parent_company.user.payment_plan allowance = ( self.user.business_account.token_limit @@ -6,7 +6,9 @@ from rest_framework.request import Request from authentication.models import CustomUserModel from payments.models import Invoice, PaymentPlan, PaymentPlanUserInfo from payments.selectors.payment_plan_selector import PaymentPlanSelector -from payments.selectors.recurrent_payment_selector import RecurrentPaymentSelector +from payments.selectors.recurrent_payment_selector import ( + RecurrentPaymentSelector, +) from payments.serializers import PaymentLinkSerializer, SuccessPaymentResult from payments.services.model_billing_service import ModelBillingService from payments.services.payment_service import PaymentService @@ -37,9 +39,7 @@ class PaymentPlanService: def create_payment_plan_invoice(self, payment_plan_uid: str): """""" - payment_plan = PaymentPlanSelector(self.user).get_payment_plan_by_id( - payment_plan_uid - ) + payment_plan = PaymentPlanSelector(self.user).get_payment_plan_by_id(payment_plan_uid) resulting_link = self.payment_service.create_payment_link(plan=payment_plan) result = PaymentLinkSerializer(data={'payment_url': resulting_link}) result.is_valid(raise_exception=True) @@ -52,14 +52,15 @@ class PaymentPlanService: plan.plan_schedule = task.task plan.save() else: - current.to_service(RecurrentPaymentService).switch_plan( - self.user, plan.plan.uid - ) + current.to_service(RecurrentPaymentService).switch_plan(self.user, plan.plan.uid) def subscribe_user_to_plan(self, plan: PaymentPlan): plan_info, created = PaymentPlanUserInfo.objects.get_or_create( user=self.user, - defaults={'plan': plan, 'current_token_balance': plan.tokens_per_plan}, + defaults={ + 'plan': plan, + 'current_token_balance': plan.tokens_per_plan, + }, ) if not created: plan_info.plan = plan @@ -71,17 +72,13 @@ class PaymentPlanService: task = plan_info.plan_schedule if task is not None: RecurrentPaymentService(task).delete() - plan_info.plan = PaymentPlanSelector(self.user).get_free_plan( - corporate=self.user.is_corporate() - ) + plan_info.plan = PaymentPlanSelector(self.user).get_free_plan(corporate=self.user.is_corporate()) plan_info.save() def update_per_token_plan_details(self, payment_amount: Decimal, model=None): ModelBillingService(self.user).charge(payment_amount) if model: - return Invoice.objects.create( - model=model, user=self.user, cost=payment_amount - ) + return Invoice.objects.create(model=model, user=self.user, cost=payment_amount) def refill_user_plan_details(self): payment_plan = self.user.payment_plan @@ -79,16 +79,15 @@ class PaymentService: try: if user.referer_account: ReferralAccountService.apply_accrual( - referer_account=user.referer_account, payment=payment_instance + referer_account=user.referer_account, + payment=payment_instance, ) except Exception as exc: logger.exception(exc) return payment_instance @classmethod - def save_payment( - cls, user: CustomUserModel, payment: YookassaPayment - ) -> PaymentModel: + def save_payment(cls, user: CustomUserModel, payment: YookassaPayment) -> PaymentModel: payment_instance, _ = PaymentModel.objects.update_or_create( uid=payment.id, defaults=dict( @@ -33,5 +33,8 @@ class PromoCodeService(BaseService): return PromoCode.objects.create( promocode_type=PromoCode.REFERRAL, owner=self.user, - function_call={'name': 'add_tokens_referral', 'arguments': {'amount': 5}}, + function_call={ + 'name': 'add_tokens_referral', + 'arguments': {'amount': 5}, + }, ) @@ -16,15 +16,11 @@ class ReferralAccountService: @classmethod def create_invite(cls, referer_account: CustomUserModel, invitee: CustomUserModel): - return ReferralInvite.objects.create( - referer_account=referer_account, invitee=invitee - ) + return ReferralInvite.objects.create(referer_account=referer_account, invitee=invitee) @classmethod def apply_accrual(cls, referer_account: ReferralAccount, payment: Payment): - accrual_amount = (payment.plan.tokens_per_plan * Decimal(0.2)).quantize( - Decimal('1') - ) + accrual_amount = (payment.plan.tokens_per_plan * Decimal(0.2)).quantize(Decimal('1')) accrual = ReferralAccrual.objects.create( referer_account=referer_account, payment=payment, @@ -157,18 +157,14 @@ class InvoiceAdmin(admin.ModelAdmin): @admin.register(PromoCode) class PromoCodeAdmin(admin.ModelAdmin): list_display = ['code', 'promocode_type', 'function_call'] - search_fields = [ - *(f'activated_by__{field}' for field in CustomUserModelAdmin.search_fields) - ] + search_fields = [*(f'activated_by__{field}' for field in CustomUserModelAdmin.search_fields)] raw_id_fields = ['activated_by', 'owner'] @admin.register(PromoCodeActivation) class PromoCodeActivationAdmin(admin.ModelAdmin): list_display = ['_code', 'activated_by', 'created_at'] - search_fields = [ - *(f'activated_by__{field}' for field in CustomUserModelAdmin.search_fields) - ] + search_fields = [*(f'activated_by__{field}' for field in CustomUserModelAdmin.search_fields)] raw_id_fields = ['activated_by'] @admin.display(description='Промокод') @@ -236,9 +232,7 @@ class AccruedTokensFilter(admin.SimpleListFilter): ] def queryset(self, request, queryset): - queryset = queryset.annotate( - total_bonuses=Sum('account_referral_accruals__amount') - ) + queryset = queryset.annotate(total_bonuses=Sum('account_referral_accruals__amount')) match self.value(): case 'more-zero': return queryset.filter(total_bonuses__gt=0) @@ -257,7 +251,11 @@ class ReferralAccountAdmin(admin.ModelAdmin): '_accrued_bonuses', ) inlines = (ReferralInviteInline,) - list_filter = (RegistrationsCountFilter, PaymentsCountFilter, AccruedTokensFilter) + list_filter = ( + RegistrationsCountFilter, + PaymentsCountFilter, + AccruedTokensFilter, + ) @admin.display(description='Приглашающий') def _owner(self, obj: ReferralAccount): @@ -9,7 +9,10 @@ from payments.services.referral_account import ReferralAccountService @receiver(post_save, sender=CustomUserModel) def init_referral_account( - sender: Type[CustomUserModel], instance: CustomUserModel, created: bool, **kwargs + sender: Type[CustomUserModel], + instance: CustomUserModel, + created: bool, + **kwargs, ): if created: ReferralAccountService.create_account(user=instance) @@ -12,7 +12,11 @@ urlpatterns = [ ), path('invoices', views.InvoicesAPIView.as_view(), name='invoices'), path('plans', views.PaymentPlanAPIView.as_view(), name='payment-plans'), - path('methods', views.PaymentMethodsAPIView.as_view(), name='payment-methods'), + path( + 'methods', + views.PaymentMethodsAPIView.as_view(), + name='payment-methods', + ), path('user-balance', views.UserPlanAPIView.as_view(), name='user-balance'), path( 'payment-result', @@ -6,7 +6,11 @@ from django.db.models import F, Sum, functions from django.db.models.query import QuerySet from django.db.transaction import atomic from django.utils import timezone -from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema +from drf_spectacular.utils import ( + OpenApiParameter, + OpenApiResponse, + extend_schema, +) from rest_framework import status from rest_framework.generics import ListAPIView from rest_framework.permissions import IsAuthenticated @@ -57,25 +61,22 @@ class PaymentPlanAPIView(APIView): permission_classes = (IsAuthenticated, IsAllowedToPay) @extend_schema( - parameters=[ - OpenApiParameter('duration', str, enum=[PaymentPlan.MONTH, PaymentPlan.YEAR]) - ], + parameters=[OpenApiParameter('duration', str, enum=[PaymentPlan.MONTH, PaymentPlan.YEAR])], responses={200: PaymentPlanSerializer}, ) def get(self, request: Request, *args, **kwargs): """List available payment plans""" try: plan_duration = request.query_params.get('duration', None) - result = PaymentPlanSelector(self.request.user).get_payment_plans( - plan_duration=plan_duration - ) + result = PaymentPlanSelector(self.request.user).get_payment_plans(plan_duration=plan_duration) return Response(result.data, status=status.HTTP_200_OK) except Exception as err: logger.exception(err) return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @extend_schema( - request=NewSubsriptionSerializer, responses={200: PaymentLinkSerializer} + request=NewSubsriptionSerializer, + responses={200: PaymentLinkSerializer}, ) def post(self, request, *args, **kwargs): """Create new payment link for chosen Payment Plan""" @@ -132,9 +133,7 @@ class PaymentMethodsAPIView(APIView): try: serializer = DeletePaymentMethodSerializer(data=request.data) serializer.is_valid(raise_exception=True) - PaymentMethodService(self.request.user).delete_payment_method( - **serializer.validated_data - ) + PaymentMethodService(self.request.user).delete_payment_method(**serializer.validated_data) return Response({'ok': True}, status=status.HTTP_200_OK) except Exception as err: logger.exception(err) @@ -244,17 +243,13 @@ class ExpensesAPIView(APIView): qs = qs.filter( created_at__range=[ timezone.now() - timedelta(days=timezone.now().weekday()), - timezone.now() - - timedelta(days=timezone.now().weekday()) - + timedelta(days=6), + timezone.now() - timedelta(days=timezone.now().weekday()) + timedelta(days=6), ] ) case 'previous_month': qs = qs.filter( created_at__range=[ - (timezone.now().replace(day=1) - timedelta(days=1)).replace( - day=1 - ), + (timezone.now().replace(day=1) - timedelta(days=1)).replace(day=1), timezone.now().replace(day=1) - timedelta(days=1), ] ) @@ -262,10 +257,12 @@ class ExpensesAPIView(APIView): qs = qs.filter( created_at__range=[ datetime.strptime( - request.query_params.get('from', '01.01.00'), '%d.%m.%y' + request.query_params.get('from', '01.01.00'), + '%d.%m.%y', ), datetime.strptime( - request.query_params.get('to', '01.01.50'), '%d.%m.%y' + request.query_params.get('to', '01.01.50'), + '%d.%m.%y', ), ] ) @@ -277,7 +274,8 @@ class ExpensesAPIView(APIView): ) case 'models': qs = qs.values('model__title').annotate( - source=F('model__title'), amount=functions.Round((Sum('cost'))) + source=F('model__title'), + amount=functions.Round((Sum('cost'))), ) case 'days': qs = qs.values(day=functions.TruncDay('created_at')).annotate( @@ -312,9 +310,7 @@ class PromoCodeAPIView(APIView): permission_classes = (IsAuthenticated,) @extend_schema( - parameters=[ - OpenApiParameter(name='code', type=str, required=True, description='Промокод') - ], + parameters=[OpenApiParameter(name='code', type=str, required=True, description='Промокод')], responses={ 200: OpenApiResponse(description='Promocode sucessfully activated'), 403: OpenApiResponse(description='Promocode found, but inactive'), @@ -324,9 +320,7 @@ class PromoCodeAPIView(APIView): def post(self, request: Request, *args, **kwargs): """Activate promocde as authenticated user.""" try: - PromoCodeService(request.user).activate_by_code( - code=request.data.get('code', '') - ) + PromoCodeService(request.user).activate_by_code(code=request.data.get('code', '')) return Response(status=status.HTTP_200_OK) except PromoCodeService.PromoCodeInactive as e: return Response(status=status.HTTP_403_FORBIDDEN, data={'detail': str(e)}) @@ -342,7 +336,10 @@ class TelegramChannelSubscriptionBonusAPIView(APIView): @extend_schema( parameters=[ OpenApiParameter( - name='id', type=str, required=True, description='Telegram ID of user.' + name='id', + type=str, + required=True, + description='Telegram ID of user.', ) ], responses={ @@ -9,7 +9,11 @@ class Command(BaseCommand): help = 'Collect reports and create an Excel file' def add_arguments(self, parser): - parser.add_argument('from_datetime', type=str, help='Start date and time (YYYY-MM-DD HH:MM:SS)') + parser.add_argument( + 'from_datetime', + type=str, + help='Start date and time (YYYY-MM-DD HH:MM:SS)', + ) parser.add_argument( 'to_datetime', type=str, @@ -6,9 +6,7 @@ from core.models import BaseModel class ErrorReport(BaseModel): - author = models.ForeignKey( - get_user_model(), on_delete=models.CASCADE, verbose_name=_('Author') - ) + author = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_('Author')) report_text = models.TextField(verbose_name=_('Text')) additional_images = models.JSONField(null=True, verbose_name=_('Attachments')) @@ -20,7 +20,12 @@ class RequestReponseLogAdmin(admin.ModelAdmin): search_fields = ['created_at', 'response_body'] verbose_name = 'Запрос и ответ' verbose_name_plural = 'Запросы и ответы' - list_display = ['request_body', 'request_user', 'response_body', 'status_code'] + list_display = [ + 'request_body', + 'request_user', + 'response_body', + 'status_code', + ] raw_id_fields = ['request_user'] actions = ['download_xlsx_logs'] @@ -16,8 +16,6 @@ class SendErrorReportAPIView(APIView): """Send new error report from form data.""" try: ErrorReportService(self.request.user).create(request) - return Response( - {'detail': 'error report sent'}, status=status.HTTP_201_CREATED - ) + return Response({'detail': 'error report sent'}, status=status.HTTP_201_CREATED) except Exception as err: return Response({'detail': f'{err}'}, status=status.HTTP_400_BAD_REQUEST) @@ -1,6 +1,5 @@ from typing import List -from django.db.models import Count from ninja import Router from authentication.security import SyncAuthBearer @@ -14,6 +13,6 @@ router = Router(auth=SyncAuthBearer(), tags=['chats']) def get_links(request): return ( NeuronModel.objects.filter(category__slug='chat-bots') - .annotate(inputs_count=Count('model_modelinputs')) - .filter(inputs_count__gt=0) + .filter(model_settings__isnull=False, model_settings__is_active=True) + .order_by('order') ) @@ -2,7 +2,10 @@ import logging import sys from drf_spectacular.utils import OpenApiParameter, extend_schema -from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView +from rest_framework.generics import ( + ListCreateAPIView, + RetrieveUpdateDestroyAPIView, +) from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -166,9 +169,7 @@ class MessageAPIView(APIView): Put message into favourite """ chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get( - uid=message_uid, chats_chats_messages=chat, is_deleted=False - ) + message = Message.objects.get(uid=message_uid, chats_chats_messages=chat, is_deleted=False) message.is_favourite = True message.save() return Response(status=204) @@ -178,9 +179,7 @@ class MessageAPIView(APIView): Put message into shared """ chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get( - uid=message_uid, chats_chats_messages=chat, is_deleted=False - ) + message = Message.objects.get(uid=message_uid, chats_chats_messages=chat, is_deleted=False) message.is_shared = True message.save() return Response(status=204) @@ -193,9 +192,7 @@ class MessageAPIView(APIView): Hide message """ chat = Chat.objects.get(pk=chat_uid) - message = Message.objects.get( - pk=message_uid, chats_chats_messages=chat, is_deleted=False - ) + message = Message.objects.get(pk=message_uid, chats_chats_messages=chat, is_deleted=False) message.is_deleted = True message.save() return Response(status=204) @@ -7,9 +7,7 @@ from messages.models import MultipleStore class Chat(MultipleStore): title = models.CharField(max_length=50, verbose_name=_('Title')) - created_at = models.DateTimeField( - default=timezone.now, verbose_name=_('Created at'), editable=False - ) + created_at = models.DateTimeField(default=timezone.now, verbose_name=_('Created at'), editable=False) is_deleted = models.BooleanField( default=False, verbose_name=_('Is deleted'), @@ -6,9 +6,7 @@ from tools.chats.models import Chat class ChatCreateSerializer(serializers.ModelSerializer): user = serializers.HiddenField(default=serializers.CurrentUserDefault()) - model = serializers.SlugRelatedField( - slug_field='slug', queryset=NeuronModel.objects.all() - ) + model = serializers.SlugRelatedField(slug_field='slug', queryset=NeuronModel.objects.all()) class Meta: model = Chat @@ -85,14 +85,18 @@ def generate(request, id: UUID): @router.put( - 'copywrites/{id}/favourite', tags=['copywrite/copywrites'], response={204: None} + 'copywrites/{id}/favourite', + tags=['copywrite/copywrites'], + response={204: None}, ) def mark_as_favourite(request, id: UUID): return CopywriteService.mark_as_favourite(copywrite_id=id) @router.delete( - 'copywrites/{id}/favourite', tags=['copywrite/copywrites'], response={204: None} + 'copywrites/{id}/favourite', + tags=['copywrite/copywrites'], + response={204: None}, ) def remove_from_favourite(request, id: UUID): return CopywriteService.remove_from_favourite(copywrite_id=id) @@ -114,9 +118,7 @@ def delete_copywrite(request, id: UUID): response=OverridenVariableSchema, ) def override_variable(request, copywrite_id: UUID, data: CreateOverridenVariableSchema): - return CopywriteService.override_variable( - copywrite_id=copywrite_id, **data.model_dump() - ) + return CopywriteService.override_variable(copywrite_id=copywrite_id, **data.model_dump()) @router.put( @@ -124,9 +126,7 @@ def override_variable(request, copywrite_id: UUID, data: CreateOverridenVariable tags=['copywrite/variables'], response={204: None}, ) -def update_overriden_variable( - request, copywrite_id: UUID, id: UUID, data: UpdateOverridenVariableSchema -): +def update_overriden_variable(request, copywrite_id: UUID, id: UUID, data: UpdateOverridenVariableSchema): return CopywriteService.update_overriden_variable( copywrite_id=copywrite_id, variable_id=id, **data.model_dump() ) @@ -151,7 +151,9 @@ def list_template_categories(request): @router.get( - 'templates/', tags=['copywrite/templates'], response=List[TemplateShortSchema] + 'templates/', + tags=['copywrite/templates'], + response=List[TemplateShortSchema], ) def list_templates(request, filters: TemplateFilterSchema = Query(...)): return TemplateService.list_templates(filters=filters.get_filter_expression()) @@ -42,16 +42,12 @@ class CopywriteService: type: Literal['template', 'self'], initial: dict[str, Any], ) -> Copywrite: - klass = ContentType.objects.get_by_natural_key( - 'copywrite', f'{type}copywrite' - ).model_class() + klass = ContentType.objects.get_by_natural_key('copywrite', f'{type}copywrite').model_class() return klass.objects.create(user=user, **initial) @classmethod def get_copywrite_by_id(cls, copywrite_id: UUID) -> TemplateCopywrite | SelfCopywrite: - return Copywrite.objects.prefetch_related('polymorphic_ctype').get( - id=copywrite_id - ) + return Copywrite.objects.prefetch_related('polymorphic_ctype').get(id=copywrite_id) @classmethod def mark_as_favourite(cls, copywrite_id: UUID) -> None: @@ -81,30 +77,25 @@ class CopywriteService: def update_overriden_variable( cls, copywrite_id: UUID, variable_id: UUID, value: PrimitiveType ) -> OverridenVariable: - ov = OverridenVariable.objects.get( - copywrite__id=copywrite_id, variable__id=variable_id - ) + ov = OverridenVariable.objects.get(copywrite__id=copywrite_id, variable__id=variable_id) ov.value = value ov.save() return ov @classmethod def remove_overriden_variable(cls, copywrite_id: UUID, variable_id: UUID) -> None: - OverridenVariable.objects.get( - copywrite__id=copywrite_id, variable__id=variable_id - ).delete() + OverridenVariable.objects.get(copywrite__id=copywrite_id, variable__id=variable_id).delete() @classmethod def generate(cls, copywrite_id: UUID) -> Generator[str, None, None]: - cp: SelfCopywrite | TemplateCopywrite = Copywrite.objects.prefetch_related( - 'polymorphic_ctype' - ).get(id=copywrite_id) + cp: SelfCopywrite | TemplateCopywrite = Copywrite.objects.prefetch_related('polymorphic_ctype').get( + id=copywrite_id + ) if isinstance(cp, SelfCopywrite): content = str(cp.input_content) or '' elif isinstance(cp, TemplateCopywrite): overriden_vars = { - overriden_var.variable.id: overriden_var.value - for overriden_var in cp.overriden_variables + overriden_var.variable.id: overriden_var.value for overriden_var in cp.overriden_variables } blueprint = Template(cp.template.content) ctx = { @@ -13,7 +13,7 @@ class CopywriteConsumer(AsyncJsonWebsocketConsumer): channel_layer: RedisChannelLayer async def connect(self): - copywrite_id = f"{self.scope['url_route']['kwargs']['copywrite_id']}" + copywrite_id = f'{self.scope["url_route"]["kwargs"]["copywrite_id"]}' await self.channel_layer.group_add( COPYWRITE_WS_KEY % copywrite_id, self.channel_name, @@ -24,7 +24,7 @@ class CopywriteConsumer(AsyncJsonWebsocketConsumer): async def close(self, code=None, reason=None): await self.channel_layer.group_discard( - COPYWRITE_WS_KEY % f"{self.scope['url_route']['kwargs']['copywrite_id']}", + COPYWRITE_WS_KEY % f'{self.scope["url_route"]["kwargs"]["copywrite_id"]}', self.channel_name, ) return await super().close(code, reason) @@ -33,15 +33,11 @@ class TemplateCategory(OrderedModel): class Template(OrderedModel): - id = models.UUIDField( - primary_key=True, default=uuid4, editable=False, verbose_name='ID' - ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') title = models.CharField(max_length=50, unique=True, verbose_name='Название') slug = models.SlugField(unique=True, verbose_name='Ярлык') - description = models.CharField( - null=True, blank=True, max_length=200, verbose_name='Описание' - ) + description = models.CharField(null=True, blank=True, max_length=200, verbose_name='Описание') picture = models.FileField( verbose_name='Картинка', storage=MinioBackend(bucket_name='air-templates-pictures'), @@ -87,9 +83,7 @@ class Template(OrderedModel): class TemplateVariable(OrderedModel): - id = models.UUIDField( - primary_key=True, default=uuid4, editable=False, verbose_name='ID' - ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') template = models.ForeignKey( Template, @@ -102,9 +96,7 @@ class TemplateVariable(OrderedModel): name = models.CharField(max_length=100, verbose_name='Название') sysname = models.CharField(max_length=100, verbose_name='Системное название') required = models.BooleanField(default=False, verbose_name='Обязательный') - default_value = models.JSONField( - null=True, blank=True, verbose_name='Стандартное значение' - ) + default_value = models.JSONField(null=True, blank=True, verbose_name='Стандартное значение') order_with_respect_to = 'template' @@ -114,9 +106,7 @@ class TemplateVariable(OrderedModel): class Copywrite(PolymorphicModel): - id = models.UUIDField( - primary_key=True, default=uuid4, editable=False, verbose_name='ID' - ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') user = models.ForeignKey( get_user_model(), on_delete=models.SET_NULL, @@ -125,9 +115,7 @@ class Copywrite(PolymorphicModel): verbose_name='Пользователь', related_name='user_copywrites', ) - output_content = models.TextField( - null=True, blank=True, verbose_name='Исходящий промпт' - ) + output_content = models.TextField(null=True, blank=True, verbose_name='Исходящий промпт') favourite = models.BooleanField(default=False, verbose_name='В избранном') created_at = models.DateTimeField(auto_now_add=True, verbose_name='Когда создано') deleted = models.BooleanField(default=False, verbose_name='Удален') @@ -143,9 +131,7 @@ class Copywrite(PolymorphicModel): @property def type(self) -> str: - return self.polymorphic_ctype.model[ - : self.polymorphic_ctype.model.index('copywrite') - ] + return self.polymorphic_ctype.model[: self.polymorphic_ctype.model.index('copywrite')] @property def generated(self) -> bool: @@ -167,9 +153,7 @@ class Copywrite(PolymorphicModel): class SelfCopywrite(Copywrite): - input_content: str = models.TextField( - null=True, blank=True, verbose_name='Входящий промпт' - ) + input_content: str = models.TextField(null=True, blank=True, verbose_name='Входящий промпт') class Meta: verbose_name = 'Самописный копирайт' @@ -194,9 +178,7 @@ class TemplateCopywrite(Copywrite): class OverridenVariable(models.Model): - id = models.UUIDField( - primary_key=True, default=uuid4, editable=False, verbose_name='ID' - ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False, verbose_name='ID') variable = models.ForeignKey( TemplateVariable, on_delete=models.PROTECT, @@ -98,7 +98,13 @@ class SelfCopywriteSchema(BaseCopywriteSchema, ModelSchema): class Meta: model = SelfCopywrite - exclude = ('copywrite_ptr', 'user', 'polymorphic_ctype', 'created_at', 'deleted') + exclude = ( + 'copywrite_ptr', + 'user', + 'polymorphic_ctype', + 'created_at', + 'deleted', + ) class SelfCopywriteShortSchema(BaseCopywriteSchema, ModelSchema): @@ -138,7 +144,13 @@ class TemplateCopywriteSchema(BaseCopywriteSchema, ModelSchema): class Meta: model = TemplateCopywrite - exclude = ('copywrite_ptr', 'user', 'polymorphic_ctype', 'created_at', 'deleted') + exclude = ( + 'copywrite_ptr', + 'user', + 'polymorphic_ctype', + 'created_at', + 'deleted', + ) class TemplateCopywriteShortSchema(BaseCopywriteSchema, ModelSchema): @@ -162,9 +174,7 @@ class CopywriteFilterSchema(FilterSchema): def filter_types(self, value: List[Literal['template', 'self']]): if value: - return Q( - polymorphic_ctype__model__in=list(map(lambda x: f'{x}copywrite', value)) - ) + return Q(polymorphic_ctype__model__in=list(map(lambda x: f'{x}copywrite', value))) return Q() @@ -7,7 +7,5 @@ from tools.copywrite.models import TemplateCopywrite @receiver(pre_save, sender=TemplateCopywrite) -def copy_model_configuration( - sender: Type[TemplateCopywrite], instance: TemplateCopywrite, **kwargs -): +def copy_model_configuration(sender: Type[TemplateCopywrite], instance: TemplateCopywrite, **kwargs): print(kwargs) @@ -17,7 +17,10 @@ def generate(copywrite_id: UUID): for chunk in CopywriteService.generate(copywrite_id=copywrite_id): async_to_sync(layer.group_send)( COPYWRITE_WS_KEY % copywrite_id, - {'type': 'generate.chunk', 'event_data': {'chunk': chunk, 'end': False}}, + { + 'type': 'generate.chunk', + 'event_data': {'chunk': chunk, 'end': False}, + }, ) content += chunk cache.set(f'copywrite:{copywrite_id}', content) @@ -37,7 +37,7 @@ class Command(BaseCommand): file.write('from django.urls import path') with open(TOOL_DIR / 'apps.py', 'a') as apps: apps.write( - f"\n\nclass {options['toolname'][0].title()}Config(AppConfig):" + f'\n\nclass {options["toolname"][0].title()}Config(AppConfig):' "\n\tdefault_auto_field = 'django.db.models.BigAutoField'" f"\n\tname = 'tools.{options['toolname'][0]}'" ) @@ -1,6 +1,5 @@ from typing import List -from django.db.models import Count from ninja import Router from authentication.security import SyncAuthBearer @@ -10,10 +9,14 @@ from ml_model.schemas import NeuronModelLink router = Router(auth=SyncAuthBearer(), tags=['media']) -@router.get('images/links/', tags=['media/images/links'], response=List[NeuronModelLink]) +@router.get( + 'images/links/', + tags=['media/images/links'], + response=List[NeuronModelLink], +) def get_links(request): return ( NeuronModel.objects.filter(category__slug='images') - .annotate(inputs_count=Count('model_modelinputs')) - .filter(inputs_count__gt=0) + .filter(model_settings__isnull=False, model_settings__is_active=True) + .order_by('order') ) @@ -129,7 +129,7 @@ class MediaAPIView(APIView): info = serializer.validated_data.pop('info', {}) service: type[SimpleService] = getattr( sys.modules['ml_model.services'], - f"{gallery.model.slug.replace('-', '').title()}", + f'{gallery.model.slug.replace("-", "").title()}', ) input_message = Message.objects.create( **serializer.validated_data, @@ -1,6 +1,11 @@ from django.urls import path -from .apis import GalleryAPIView, ModelAudiosAPIVIew, ModelImagesAPIView, ModelVideosAPIView +from .apis import ( + GalleryAPIView, + ModelAudiosAPIVIew, + ModelImagesAPIView, + ModelVideosAPIView, +) urlpatterns = [ path('gallery/', GalleryAPIView.as_view(), name='gallery'), @@ -4,7 +4,9 @@ from decimal import Decimal from django.contrib.admin.models import ADDITION, DELETION, LogEntry from django.contrib.contenttypes.models import ContentType -from authentication.selectors.account_status_selector import AccountStatusSelector +from authentication.selectors.account_status_selector import ( + AccountStatusSelector, +) from core.service import BaseService from tools.public_api.models import APIKey from tools.public_api.selectors.api_key import APIKeySelector @@ -12,9 +14,7 @@ from tools.public_api.serializers import APIKeyResultSerializer class APIKeyService(BaseService): - def create( - self, payload: dict, serialize: bool = False - ) -> APIKey | APIKeyResultSerializer: + def create(self, payload: dict, serialize: bool = False) -> APIKey | APIKeyResultSerializer: if not ( AccountStatusSelector(self.user).is_business_host() or AccountStatusSelector(self.user).is_admin() @@ -31,7 +31,7 @@ class APIKeyService(BaseService): { 'added': { 'name': 'API-ключ', - 'object': f'{api_key.key[:3]}{(len(api_key.key)-4)*"*"}{api_key.key[len(api_key.key)-4:]}', + 'object': f'{api_key.key[:3]}{(len(api_key.key) - 4) * "*"}{api_key.key[len(api_key.key) - 4 :]}', } } ], @@ -73,7 +73,7 @@ class APIKeyService(BaseService): { 'deleted': { 'name': 'API-ключ', - 'object': f'{api_key.key[:3]}{(len(api_key.key)-4)*"*"}{api_key.key[len(api_key.key)-4:]}', + 'object': f'{api_key.key[:3]}{(len(api_key.key) - 4) * "*"}{api_key.key[len(api_key.key) - 4 :]}', } } ], @@ -1,3 +1,10 @@ from .api_key import APIKeyView -from .ml_service import TextView, ImageView, AudioView, VideoView, CodeView, ParamView +from .ml_service import ( + TextView, + ImageView, + AudioView, + VideoView, + CodeView, + ParamView, +) from .user import UserInfoAPIView @@ -1,5 +1,6 @@ import sys +from rest_framework.exceptions import APIException from rest_framework.response import Response from rest_framework.views import APIView @@ -41,24 +42,20 @@ class BaseGenerationView(APIView): def post(self, request, model_slug, *args, **kwargs): """Create new content. Type of content depends on model output content type: text, image, audio, video, or code.""" - user = APIKeySelector.get_user_by_key( - key_value=request.headers.get('Authorization', '') - ) + user = APIKeySelector.get_user_by_key(key_value=request.headers.get('Authorization', '')) balance = user.balance key = APIKey.objects.get(key=request.headers.get('Authorization', '')) - if key.token_limit is not None and ( - key.token_limit > balance or key.token_limit < 0 - ): + if key.token_limit is not None and (key.token_limit > balance or key.token_limit < 0): return Response({'error': 'Not enough tokens on balance or key limit'}, 403) store, _ = APIStore.objects.get_or_create(user=user) - model: NeuronModel = NeuronModelSelector(store.user).get_model_by_slug( - slug=model_slug - ) + model: NeuronModel = NeuronModelSelector(store.user).get_model_by_slug(slug=model_slug) + if model.blocked: + raise APIException( + detail=_('Model is blocked by outdating or temporary block, please retry later') + ) serializer = MessageSerializer(data=request.data) serializer.is_valid(raise_exception=True) - service: type[SimpleService] = getattr( - sys.modules['ml_model.services'], f'{model.slug.title()}' - ) + service: type[SimpleService] = getattr(sys.modules['ml_model.services'], f'{model.slug.title()}') info = serializer.validated_data.pop('info', {}) input_message = Message( **serializer.validated_data, @@ -74,9 +71,7 @@ class BaseGenerationView(APIView): msg.from_public_api = True msg.save() if key.token_limit is not None: - user_after = APIKeySelector.get_user_by_key( - key_value=request.headers.get('Authorization', '') - ) + user_after = APIKeySelector.get_user_by_key(key_value=request.headers.get('Authorization', '')) key.token_limit -= balance - user_after.balance key.save() return Response(MessageSerializer(output_message, many=True).data, 201) @@ -46,12 +46,8 @@ class ParamView(APIView): @extend_schema(responses={200: ModelParameterSerializer}) def get(self, request, model_slug, *args, **kwargs): """List parameters for model pointed in URL Slug.""" - user = APIKeySelector.get_user_by_key( - key_value=request.headers.get('Authorization', '') - ) + user = APIKeySelector.get_user_by_key(key_value=request.headers.get('Authorization', '')) store, created = APIStore.objects.get_or_create(user=user) - model: NeuronModel = NeuronModelSelector(store.user).get_model_by_slug( - slug=model_slug - ) + model: NeuronModel = NeuronModelSelector(store.user).get_model_by_slug(slug=model_slug) params = ParamSelector.get_params_by_model(model, serialize=True) return Response(data=params.data, status=status.HTTP_200_OK) @@ -27,9 +27,7 @@ class APIKey(BaseModel): blank=False, default=generate_api_key, ) - name = models.CharField( - verbose_name=_('Name'), max_length=255, null=False, blank=False - ) + name = models.CharField(verbose_name=_('Name'), max_length=255, null=False, blank=False) user = models.ForeignKey( to=get_user_model(), verbose_name=_('Owner'), @@ -16,7 +16,14 @@ class APIKeyResultSerializer(serializers.ModelSerializer): class Meta: model = APIKey - fields = ('created_at', 'name', 'key', 'expires_at', 'user', 'token_limit') + fields = ( + 'created_at', + 'name', + 'key', + 'expires_at', + 'user', + 'token_limit', + ) class APIKeyCreateSerializer(serializers.ModelSerializer): @@ -6,11 +6,20 @@ urlpatterns = [ path('me', views.UserInfoAPIView.as_view()), ] -for view in (views.TextView, views.ImageView, views.AudioView, views.VideoView, views.CodeView): +for view in ( + views.TextView, + views.ImageView, + views.AudioView, + views.VideoView, + views.CodeView, +): urlpatterns.extend( [ path(view.output_content_type, view.as_view()), path(f'{view.output_content_type}/', view.as_view()), - path(f'{view.output_content_type}//params', views.ParamView.as_view()), + path( + f'{view.output_content_type}//params', + views.ParamView.as_view(), + ), ] ) @@ -11,16 +11,25 @@ default: before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin -build: +build_staging: stage: Build - image: docker:latest script: - touch .env && export ENV=.env + - docker compose build + - docker compose push + only: + - staging + when: on_success + +build_production: + stage: Build + script: + - touch .env && export ENV=.env + - docker rmi $CI_REGISTRY_IMAGE:latest || true - docker compose -f stack.yml build - docker compose -f stack.yml push only: - main - - staging when: on_success deploy_staging: @@ -14,7 +14,7 @@ services: python manage.py collectstatic --no-input python manage.py compilemessages (python manage.py createsuperuser --no-input || true) - python -m debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn backend.asgi:application --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level debug --reload + python -m debugpy --listen 0.0.0.0:5678 -m uvicorn backend.asgi:application --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level debug --reload volumes: - .:/code ports: @@ -1,11 +1,10 @@ services: - backend: + app: restart: unless-stopped image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA build: context: . dockerfile: Dockerfile - container_name: backend volumes: - static:/code/static command: @@ -15,6 +14,17 @@ services: python manage.py collectstatic --no-input python manage.py compilemessages python -m uvicorn --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level info backend.asgi:application + labels: + - traefik.enable=true + - traefik.docker.network=infrastructure + - traefik.http.routers.backend.rule=Host(`$DOMAIN`) + - traefik.http.routers.backend.entrypoints=web,websecure + - traefik.http.routers.backend.tls=true + - traefik.http.routers.backend.tls.certresolver=defaultresolver + - traefik.http.routers.backend.service=backend + - traefik.http.services.backend.loadbalancer.server.port=8000 + - traefik.http.middlewares.backend.redirectscheme.scheme=https + - traefik.http.middlewares.backend.redirectscheme.permanent=true env_file: - $ENV depends_on: @@ -25,10 +35,7 @@ services: migrator: restart: on-failure:1 - container_name: migrator - build: - context: . - dockerfile: Dockerfile + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA command: - /bin/sh - -c @@ -39,10 +46,6 @@ services: celery: restart: unless-stopped image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - build: - context: . - dockerfile: Dockerfile - container_name: celery command: celery -A backend worker -l INFO --concurrency 8 env_file: - $ENV @@ -54,30 +57,47 @@ services: celery_beat: restart: unless-stopped image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - build: - context: . - dockerfile: Dockerfile - container_name: beat command: celery -A backend beat -l INFO env_file: - $ENV depends_on: - celery-mdb + static-server: + image: $CI_REGISTRY_IMAGE/static-server:$CI_COMMIT_SHA + build: + context: nginx + dockerfile: Dockerfile + labels: + - traefik.enable=true + - traefik.docker.network=infrastructure + - traefik.http.routers.backend-static.rule=Host(`$DOMAIN`) && PathPrefix(`/static`) + - traefik.http.routers.backend-static.entrypoints=web,websecure + - traefik.http.routers.backend-static.tls=true + - traefik.http.routers.backend-static.tls.certresolver=defaultresolver + - traefik.http.routers.backend-static.service=backend-static + - traefik.http.services.backend-static.loadbalancer.server.port=80 + - traefik.http.middlewares.backend-static.redirectscheme.scheme=https + - traefik.http.middlewares.backend-static.redirectscheme.permanent=true + networks: + - infrastructure + volumes: + - static:/var/www/static + env_file: + - $ENV + + cache-mdb: image: redis:alpine restart: unless-stopped - container_name: cache-mdb celery-mdb: image: redis:alpine restart: unless-stopped - container_name: celery-mdb channels-mdb: image: redis:alpine restart: unless-stopped - container_name: channels-mdb networks: default: @@ -124,7 +124,7 @@ exclude = [ "migrations" ] -line-length = 90 +line-length = 109 indent-width = 4 target-version = "py312" @@ -3,13 +3,13 @@ services: app: - image: $CI_REGISTRY_IMAGE:latest + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA build: context: . dockerfile: Dockerfile tags: - - $CI_REGISTRY_IMAGE:latest - - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA cache_from: - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true cache_to: @@ -43,7 +43,7 @@ services: labels: - traefik.enable=true - traefik.docker.network=infrastructure - - traefik.http.routers.backend.rule=Host(`backend.air.fail`) + - traefik.http.routers.backend.rule=Host(`$DOMAIN`) || Host(`backend.air.fail`) - traefik.http.routers.backend.entrypoints=web,websecure - traefik.http.routers.backend.tls=true - traefik.http.routers.backend.tls.certresolver=defaultresolver @@ -56,16 +56,6 @@ services: migrator: image: $CI_REGISTRY_IMAGE:latest - build: - context: . - dockerfile: Dockerfile - tags: - - $CI_REGISTRY_IMAGE:latest - - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - cache_from: - - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true - cache_to: - - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true deploy: replicas: 1 restart_policy: @@ -81,17 +71,7 @@ services: - $ENV celery: - image: $CI_REGISTRY_IMAGE:latest - build: - context: . - dockerfile: Dockerfile - tags: - - $CI_REGISTRY_IMAGE:latest - - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - cache_from: - - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true - cache_to: - - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA command: celery -A backend worker -l INFO --concurrency 8 networks: - default @@ -115,17 +95,7 @@ services: - C_FORCE_ROOT=true celery_beat: - image: $CI_REGISTRY_IMAGE:latest - build: - context: . - dockerfile: Dockerfile - tags: - - $CI_REGISTRY_IMAGE:latest - - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - cache_from: - - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true - cache_to: - - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA command: celery -A backend beat -l INFO networks: - default @@ -159,7 +129,7 @@ services: labels: - traefik.enable=true - traefik.docker.network=infrastructure - - traefik.http.routers.backend-static.rule=Host(`backend.air.fail`) && PathPrefix(`/static`) + - traefik.http.routers.backend-static.rule=Host(`$DOMAIN`) && PathPrefix(`/static`) - traefik.http.routers.backend-static.entrypoints=web,websecure - traefik.http.routers.backend-static.tls=true - traefik.http.routers.backend-static.tls.certresolver=defaultresolver