@@ -8,7 +8,7 @@ "connect": { "port": 5678 }, - "justMyCode": true, + "justMyCode": false, "pathMappings": [ { "localRoot": "${workspaceFolder}", @@ -1,5 +1,6 @@ { "files.eol": "\n", + "ruff.showNotifications": "onWarning", "[python]": { "editor.formatOnSave": true, "editor.defaultFormatter": "charliermarsh.ruff", @@ -1,17 +1,9 @@ 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,12 +1,6 @@ -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,6 +1,4 @@ -from authentication.exceptions.business_host_exceptions.base_already import ( - BaseAlready, -) +from authentication.exceptions.business_host_exceptions.base_already import BaseAlready class AlreadyAccount(BaseAlready): @@ -1,12 +1,8 @@ -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,6 +1,4 @@ -from authentication.exceptions.business_host_exceptions.base_already import ( - BaseAlready, -) +from authentication.exceptions.business_host_exceptions.base_already import BaseAlready class AlreadyHost(BaseAlready): @@ -1,6 +1,4 @@ -from authentication.exceptions.business_host_exceptions.base_already import ( - BaseAlready, -) +from authentication.exceptions.business_host_exceptions.base_already import BaseAlready __all__ = ('BaseAlready',) @@ -6,8 +6,4 @@ 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,7 +58,9 @@ 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,7 +41,9 @@ 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, @@ -60,20 +62,30 @@ 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), @@ -81,7 +93,9 @@ 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]: @@ -106,10 +120,7 @@ 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,11 +2,7 @@ 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 @@ -17,19 +13,12 @@ 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 @@ -62,48 +51,43 @@ class CustomUserModelManager(BaseUserManager): class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): FIRST_NAME_PLACEHOLDERS = [ - 'Любопытный', - 'Позитивный', - 'Веселый', - 'Искренний', - 'Смелый', - 'Игривый', - 'Добрый', + 'Очаровательный', + 'Смышлёный', + 'Забавный', 'Дружелюбный', 'Шустрый', - 'Милый', - 'Лучезарный', - 'Умный', + 'Талантливый', + 'Кокетливый', + 'Поэтичный', + 'Храбрый', + 'Добродушный', + 'Загадочный', ] LAST_NAME_PLACEHOLDERS = [ - 'Цыпленок', - 'Кот', - 'Щенок', - 'Хомяк', - 'Кролик', - 'Ежик', - 'Лис', + 'Филин', + 'Жираф', + 'Лев', 'Медведь', - 'Бельчонок', 'Пингвин', - 'Осьминог', - 'Бобер', + 'Ягнёнок', + 'Пони', + 'Муравей', + 'Карп', + 'Василиск', ] LAST_NAME_AVATARS = { - 'Цыпленок': 'chicken.png', - 'Кот': 'cat.png', - 'Щенок': 'puppy.png', - 'Хомяк': 'hamster.png', - 'Кролик': 'rabbit.png', - 'Ежик': 'hedgehog.png', - 'Лис': 'fox.png', + 'Филин': 'owl.png', + 'Жираф': 'giraffe.png', + 'Лев': 'lion.png', 'Медведь': 'bear.png', - 'Бельчонок': 'squirrel.png', 'Пингвин': 'penguin.png', - 'Осьминог': 'octopus.png', - 'Бобер': 'beaver.png', + 'Ягнёнок': 'lamb.png', + 'Пони': 'horse.png', + 'Муравей': 'ant.png', + 'Карп': 'fish.png', + 'Василиск': 'lizard.png', } def random_first_name(*args, **kwargs): @@ -127,11 +111,7 @@ 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, @@ -149,7 +129,9 @@ 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, @@ -175,9 +157,7 @@ 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, ) @@ -200,9 +180,11 @@ 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) @property @@ -251,7 +233,9 @@ 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() @@ -263,7 +247,9 @@ 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,10 +6,18 @@ 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') @@ -22,7 +30,9 @@ 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,8 +1,4 @@ -from authentication.models import ( - BusinessAccount, - BusinessUserHost, - CustomUserModel, -) +from authentication.models import BusinessAccount, BusinessUserHost, CustomUserModel from authentication.models.choices import AccountPrivileges @@ -1,8 +1,6 @@ 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,11 +5,7 @@ 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, @@ -48,9 +44,7 @@ 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 @@ -61,13 +55,17 @@ 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, @@ -83,18 +81,20 @@ 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,13 +102,19 @@ 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() @@ -119,10 +125,7 @@ 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,16 +9,9 @@ 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 @@ -27,7 +20,9 @@ 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', @@ -39,7 +34,9 @@ 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) @@ -55,7 +52,9 @@ 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 @@ -123,6 +122,8 @@ 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,11 +3,7 @@ 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 @@ -65,7 +61,9 @@ 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): @@ -86,7 +84,9 @@ class BusinessAccountService: def update_status(self, status: Tuple[str, Any]): if status not in self.STATUS_STATE_MACHINE[self.account.acceptance_status]: - raise Exception('Update to this state is impossible') + raise Exception( + 'Update to this state is impossible' ' from current application status' + ) self.account.acceptance_status = status self.account.save() @@ -9,24 +9,12 @@ 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, @@ -41,9 +29,7 @@ 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 @@ -63,13 +49,17 @@ class BusinessHostService: token_limit: Decimal | None = None, account_privileges: Tuple[str, Any] | None = None, ) -> BusinessAccountService: - username = f'{self.user.host_account.company_name}_{random_with_N_digits(6)}' + username = f'{self.user.host_account.company_name}' f'_{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, @@ -79,7 +69,9 @@ 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( @@ -122,21 +114,25 @@ 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_limit(new_status) def update_privileges(self, user: CustomUserModel, new_privileges: str): BusinessAccountSelector.from_user( @@ -168,9 +164,13 @@ 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,9 +179,13 @@ 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: @@ -213,7 +217,9 @@ 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) @@ -227,7 +233,9 @@ 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: @@ -1,8 +1,6 @@ -import logging - from django.conf import settings from django.core.mail import EmailMessage, send_mail -from django.utils.html import format_html, strip_tags +from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from authentication.models import BusinessAccount, BusinessUserHost @@ -11,8 +9,6 @@ from authentication.services.email_token_service import EmailTokenService from ml_model.services.minio_service import MinIOService from reports.models.error_report import ErrorReport -logger = logging.getLogger(__name__) - class EmailService: def __init__(self, user: CustomUserModel): @@ -24,18 +20,13 @@ class EmailService: return self.user.is_subscribed_to_emails def send_email(self, subject: str, message: str, user_email: str): - try: - send_mail( - subject=subject, - message=strip_tags(message), - html_message=message, - from_email=settings.EMAIL_HOST_USER, - recipient_list=(user_email,), - auth_password=settings.EMAIL_HOST_PASSWORD, - ) - except Exception as exc: - logger.exception(exc) - raise Exception('Возникла проблема при регистрации, пожалуйста свяжитесь с администрацией') + send_mail( + subject=subject, + message=message, + from_email=settings.EMAIL_HOST_USER, + recipient_list=(user_email,), + auth_password=settings.EMAIL_HOST_PASSWORD, + ) def send_reg_conf_email(self): token = EmailTokenService(self.user).generate_user_token() @@ -110,7 +101,9 @@ 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""" @@ -153,4 +146,10 @@ class EmailService: settings.INVITATION_RESPONSE_URL, token.key, ) - self.send_email('', message, account.user.email) + mail = EmailMessage( + subject='', + body=message, + from_email=settings.EMAIL_HOST_USER, + to=(account.user.email,), + ) + mail.send(fail_silently=True) @@ -6,11 +6,7 @@ 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 @@ -82,10 +78,7 @@ 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,9 +7,7 @@ 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, @@ -76,7 +74,9 @@ 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,7 +84,9 @@ 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 @@ -208,7 +210,10 @@ 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 @@ -230,9 +235,7 @@ 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 @@ -249,7 +252,10 @@ 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']) @@ -260,7 +266,9 @@ 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() @@ -271,15 +279,21 @@ 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: @@ -298,6 +312,7 @@ 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/{response["default_avatar_id"]}/islands-retina-50' + f"https://avatars.yandex.net/get-yapic/" + f"{response['default_avatar_id']}/islands-retina-50" ) air_user.save() @@ -165,12 +165,7 @@ 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' @@ -183,7 +178,9 @@ 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() @@ -202,11 +199,15 @@ 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'), ) ) ], @@ -229,12 +230,16 @@ 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() @@ -253,11 +258,15 @@ 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'), ) ) ], @@ -280,7 +289,9 @@ 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 @@ -340,8 +351,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(), @@ -400,16 +411,13 @@ 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,9 +3,7 @@ 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,10 +37,4 @@ class ReferralUserResource(ModelResource): class Meta: model = CustomUserModel - fields = ( - 'email', - 'balance', - 'created_at', - 'referer', - 'payments_count', - ) + fields = ('email', 'balance', 'created_at', 'referer', 'payments_count') @@ -16,7 +16,10 @@ 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,7 +188,9 @@ 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) @@ -253,7 +255,9 @@ 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() @@ -331,4 +335,8 @@ 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,74 +6,40 @@ 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', - ), - path( - 'mail/whitelist', - view=views.MailWhitelist.as_view(), - name='mail-whitelist', + 'mail/resend', view=views.EmailRegisterResendView.as_view(), name='email/resend' ), + 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', - ), - path( - 'login-telegram', - views.UserLoginTelegramView.as_view(), - name='login-telegram', + 'login-from-token', views.LoginFromTokenAPIView.as_view(), name='login-from-token' ), + 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(), @@ -86,9 +52,7 @@ 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( @@ -119,10 +83,7 @@ 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,9 +19,7 @@ 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 @@ -33,9 +31,7 @@ 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, @@ -68,9 +64,7 @@ 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 @@ -97,7 +91,9 @@ 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, ) @@ -132,8 +128,7 @@ 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.""" @@ -165,8 +160,7 @@ 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) @@ -243,7 +237,9 @@ 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) @@ -276,12 +272,13 @@ 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) @@ -302,9 +299,7 @@ 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), ], @@ -384,8 +379,7 @@ 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.""" @@ -444,8 +438,7 @@ 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) @@ -523,7 +516,9 @@ 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) @@ -550,7 +545,9 @@ 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) @@ -572,8 +569,7 @@ 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( @@ -625,7 +621,9 @@ 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 @@ -658,8 +656,7 @@ 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) @@ -696,13 +693,17 @@ 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) @@ -722,7 +723,9 @@ 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()), ] ) @@ -736,7 +739,9 @@ 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': @@ -754,10 +759,7 @@ 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) @@ -781,7 +783,9 @@ 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()), ], ) @@ -792,7 +796,9 @@ 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()), ], ) @@ -813,6 +819,8 @@ 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,7 +140,8 @@ 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', @@ -207,7 +208,6 @@ DATABASES = { 'PASSWORD': env.str('POSTGRES_PASSWORD'), 'HOST': env.str('POSTGRES_HOST'), 'PORT': env.str('POSTGRES_PORT'), - 'CONN_HEALTH_CHECKS': True, } } @@ -240,7 +240,9 @@ 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') @@ -331,7 +333,9 @@ 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' @@ -344,8 +348,12 @@ 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') @@ -372,12 +380,7 @@ ADMIN_SETTINGS = { 'media', 'copywrite', ], - 'excludes': [ - 'django_celery_beat', - 'social_django', - 'authtoken', - 'auth', - ], + 'excludes': ['django_celery_beat', 'social_django', 'authtoken', 'auth'], }, } @@ -1,5 +1,3 @@ -import logging - from django.conf import settings from django.conf.urls.static import static from django.contrib import admin @@ -9,11 +7,7 @@ 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') @@ -22,30 +16,26 @@ api.add_router('users/', 'authentication.routes.v1.router') api.add_router('chats/', 'tools.chats.routes.v1.router') api.add_router('media/', 'tools.media.routes.v1.router') -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) def invalid_token_error_handler(request, exc: InvalidToken): - logger.exception(exc) return api.create_response(request, {'message': _('Token is invalid')}, status=401) @api.exception_handler(InvalidPassword) def invalid_password_error_handler(request, exc: InvalidPassword): - logger.exception(exc) return api.create_response(request, {'message': _('Wrong password')}, status=401) @api.exception_handler(InvalidUsername) def invalid_username_error_handler(request, exc: InvalidUsername): - logger.exception(exc) return api.create_response(request, {'message': _('Wrong username')}, status=401) @@ -1,10 +0,0 @@ -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-30 03:00+0300\n" +"POT-Creation-Date: 2024-10-30 20:55+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,8 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: achievements/admin.py:11 achievements/models.py:19 ml_model/models.py:47 -#: stories/models.py:18 +#: achievements/admin.py:11 achievements/models.py:19 stories/models.py:18 msgid "Icon" msgstr "" @@ -31,21 +30,21 @@ msgstr "" msgid "Achievements" msgstr "" -#: achievements/models.py:14 ml_model/models.py:19 ml_model/models.py:39 -#: ml_model/models.py:72 ml_model/models.py:180 +#: achievements/models.py:14 ml_model/models.py:12 ml_model/models.py:34 +#: ml_model/models.py:123 msgid "Slug" msgstr "" -#: achievements/models.py:16 ml_model/models.py:70 ml_model/models.py:179 -#: ml_model/models.py:268 payments/models/payment.py:52 +#: achievements/models.py:16 ml_model/models.py:32 ml_model/models.py:121 +#: ml_model/models.py:208 payments/models/payment.py:53 msgid "Description" msgstr "" #: achievements/models.py:43 authentication/models/business_host.py:21 -#: authentication/models/email_token.py:12 authentication/models/user.py:241 -#: authentication/models/user.py:242 authentication/models/user_telegram.py:22 +#: authentication/models/email_token.py:12 authentication/models/user.py:203 +#: authentication/models/user.py:204 authentication/models/user_telegram.py:30 #: authentication/models/user_vk.py:12 payments/models/invoice.py:15 -#: payments/models/payment.py:26 payments/models/payment_plan.py:61 +#: payments/models/payment.py:26 payments/models/payment_plan.py:50 msgid "User" msgstr "" @@ -62,8 +61,8 @@ msgstr "" msgid "Issued achievement" msgstr "" -#: authentication/models/business_account.py:16 payments/models/promocode.py:72 -#: tools/public_api/models.py:33 +#: authentication/models/business_account.py:16 payments/models/promocode.py:68 +#: tools/public_api/models.py:35 msgid "Owner" msgstr "" @@ -86,7 +85,7 @@ msgid "Acceptance" msgstr "" #: authentication/models/business_account.py:44 -#: authentication/models/business_group.py:21 tools/public_api/models.py:43 +#: authentication/models/business_group.py:21 tools/public_api/models.py:45 msgid "Token limit" msgstr "" @@ -94,23 +93,23 @@ msgstr "" msgid "Group" msgstr "" -#: authentication/models/business_account.py:61 +#: authentication/models/business_account.py:62 msgid "" "Impossible to add this employee to this group which does not belong to this " "company" msgstr "" -#: authentication/models/business_account.py:70 +#: authentication/models/business_account.py:72 msgid "Child Business Account" msgstr "" -#: authentication/models/business_account.py:71 +#: authentication/models/business_account.py:73 msgid "Child Business Accounts" msgstr "" -#: 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 +#: authentication/models/business_group.py:8 ml_model/models.py:11 +#: ml_model/models.py:31 ml_model/models.py:206 +#: payments/models/payment_plan.py:26 stories/models.py:12 stories/models.py:35 #: tools/chats/models.py:9 msgid "Title" msgstr "" @@ -127,9 +126,9 @@ msgstr "" msgid "Affiliated by" msgstr "" -#: 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 +#: authentication/models/business_host.py:35 authentication/models/user.py:123 +#: authentication/models/whitelist.py:16 ml_model/models.py:103 +#: payments/models/promocode.py:82 msgid "Is active" msgstr "" @@ -137,68 +136,68 @@ msgstr "" msgid "Sector" msgstr "" -#: authentication/models/business_host.py:44 +#: authentication/models/business_host.py:45 msgid "Planned amount of workers" msgstr "" -#: authentication/models/business_host.py:49 +#: authentication/models/business_host.py:51 msgid "Usage intensity" msgstr "" -#: authentication/models/business_host.py:56 +#: authentication/models/business_host.py:58 msgid "Token low balance cap" msgstr "" -#: authentication/models/business_host.py:61 +#: authentication/models/business_host.py:63 msgid "Emails token low balance cap" msgstr "" -#: authentication/models/business_host.py:63 +#: authentication/models/business_host.py:66 msgid "Token low balance cap enabled" msgstr "" -#: authentication/models/business_host.py:66 +#: authentication/models/business_host.py:70 msgid "ITN" msgstr "" -#: authentication/models/business_host.py:67 +#: authentication/models/business_host.py:71 msgid "PSRN" msgstr "" -#: authentication/models/business_host.py:68 ml_model/models.py:178 -#: tools/public_api/models.py:30 +#: authentication/models/business_host.py:73 ml_model/models.py:119 +#: tools/public_api/models.py:31 msgid "Name" msgstr "" -#: authentication/models/business_host.py:71 +#: authentication/models/business_host.py:78 msgid "Preffered name" msgstr "" -#: authentication/models/business_host.py:72 +#: authentication/models/business_host.py:81 msgid "Corporate email" msgstr "" -#: authentication/models/business_host.py:74 +#: authentication/models/business_host.py:84 msgid "Corporate phone" msgstr "" -#: authentication/models/business_host.py:76 +#: authentication/models/business_host.py:87 msgid "Job title" msgstr "" -#: authentication/models/business_host.py:81 +#: authentication/models/business_host.py:93 msgid "Allowed models" msgstr "" -#: authentication/models/business_host.py:84 +#: authentication/models/business_host.py:97 msgid "Log history enabled" msgstr "" -#: authentication/models/business_host.py:103 +#: authentication/models/business_host.py:117 msgid "Business Account" msgstr "" -#: authentication/models/business_host.py:104 +#: authentication/models/business_host.py:118 msgid "Business Accounts" msgstr "" @@ -266,7 +265,7 @@ msgstr "" msgid "Security" msgstr "" -#: authentication/models/email_token.py:15 ml_model/models.py:269 +#: authentication/models/email_token.py:15 ml_model/models.py:210 msgid "Key" msgstr "" @@ -278,43 +277,43 @@ msgstr "" msgid "Email Tokens" msgstr "" -#: authentication/models/user.py:120 authentication/models/user_telegram.py:9 +#: authentication/models/user.py:100 authentication/models/user_telegram.py:10 msgid "First name" msgstr "" -#: authentication/models/user.py:127 authentication/models/user_telegram.py:10 +#: authentication/models/user.py:107 authentication/models/user_telegram.py:13 msgid "Last name" msgstr "" -#: authentication/models/user.py:134 authentication/models/user_telegram.py:11 +#: authentication/models/user.py:110 authentication/models/user_telegram.py:16 msgid "Username" msgstr "" -#: authentication/models/user.py:141 +#: authentication/models/user.py:117 msgid "Email" msgstr "" -#: authentication/models/user.py:149 +#: authentication/models/user.py:125 msgid "Is staff" msgstr "" -#: authentication/models/user.py:150 +#: authentication/models/user.py:126 msgid "Is superuser" msgstr "" -#: authentication/models/user.py:151 +#: authentication/models/user.py:127 msgid "Is email confirmed" msgstr "" -#: authentication/models/user.py:152 +#: authentication/models/user.py:129 msgid "Is subscribed" msgstr "" -#: authentication/models/user.py:158 +#: authentication/models/user.py:136 msgid "Picture name" msgstr "" -#: authentication/models/user.py:167 authentication/models/utm.py:21 +#: authentication/models/user.py:145 authentication/models/utm.py:21 msgid "UTM" msgstr "" @@ -326,38 +325,38 @@ msgstr "" msgid "Is bot" msgstr "" -#: authentication/models/user_telegram.py:12 +#: authentication/models/user_telegram.py:19 msgid "Language" msgstr "" -#: authentication/models/user_telegram.py:13 +#: authentication/models/user_telegram.py:21 msgid "Is premium" msgstr "" -#: authentication/models/user_telegram.py:15 +#: authentication/models/user_telegram.py:23 msgid "Is subscribed to channel" msgstr "" -#: authentication/models/user_telegram.py:25 +#: authentication/models/user_telegram.py:34 msgid "Phonenumber" msgstr "" -#: authentication/models/user_telegram.py:27 +#: authentication/models/user_telegram.py:37 #: authentication/models/user_vk.py:14 payments/models/invoice.py:11 -#: stories/models.py:15 tools/chats/models.py:10 +#: stories/models.py:15 tools/chats/models.py:11 msgid "Created at" msgstr "" -#: authentication/models/user_telegram.py:28 +#: authentication/models/user_telegram.py:38 #: authentication/models/user_vk.py:15 msgid "Updated at" msgstr "" -#: authentication/models/user_telegram.py:34 +#: authentication/models/user_telegram.py:44 msgid "Telegram User" msgstr "" -#: authentication/models/user_telegram.py:35 +#: authentication/models/user_telegram.py:45 msgid "Telegram Users" msgstr "" @@ -401,385 +400,339 @@ msgstr "" msgid "Whitelists to cancel policies" msgstr "" -#: 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:60 +#: authentication/selectors/business_host_selector.py:47 msgid "Host user is not registered for this account" msgstr "" -#: authentication/selectors/user_selector.py:80 +#: authentication/selectors/user_selector.py:73 msgid "No user with this uid found" msgstr "" -#: authentication/services/business_account_service.py:58 +#: authentication/services/business_account_service.py:54 msgid "BusinessAccount for this user doesn't exist" msgstr "" -#: authentication/services/business_account_service.py:68 +#: authentication/services/business_account_service.py:65 msgid "Invited account can either accept or reject an invitation" msgstr "" -#: authentication/services/business_account_service.py:73 +#: authentication/services/business_account_service.py:71 msgid "Account is already confirmed" msgstr "" -#: authentication/services/business_host_service.py:152 +#: authentication/services/business_host_service.py:148 msgid "No user_email is provided" msgstr "" -#: authentication/services/business_host_service.py:199 +#: authentication/services/business_host_service.py:203 msgid "No business account by this uid at your company" msgstr "" -#: authentication/services/email_service.py:115 +#: authentication/services/email_service.py:100 msgid "Regular users cannot send introductory letters" msgstr "" -#: authentication/services/email_service.py:137 +#: authentication/services/email_service.py:122 msgid "Regular users cannot send invitation letters" msgstr "" -#: authentication/services/user_services.py:51 +#: authentication/services/user_services.py:45 msgid "New user data is invalid" msgstr "" -#: authentication/services/user_services.py:111 +#: authentication/services/user_services.py:97 msgid "Wrong email" msgstr "" -#: authentication/services/user_services.py:119 backend/urls.py:43 +#: authentication/services/user_services.py:105 msgid "Wrong password" msgstr "" -#: authentication/services/user_services.py:122 +#: authentication/services/user_services.py:108 msgid "User has not confirmed his email yet" msgstr "" -#: authentication/services/user_services.py:161 +#: authentication/services/user_services.py:147 msgid "No user like this in a database" msgstr "" -#: authentication/services/user_services.py:178 +#: authentication/services/user_services.py:164 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:182 +#: authentication/services/user_services.py:168 msgid "No user token like this in a database" msgstr "" -#: authentication/services/user_services.py:202 +#: authentication/services/user_services.py:188 msgid "No email token provided" msgstr "" -#: authentication/services/user_services.py:206 +#: authentication/services/user_services.py:192 msgid "No token like this in a database" msgstr "" -#: authentication/services/user_services.py:212 +#: authentication/services/user_services.py:201 msgid "Passwords do not match" msgstr "" -#: authentication/services/user_services.py:250 +#: authentication/services/user_services.py:237 msgid "Current password is wrong" msgstr "" -#: backend/urls.py:31 -msgid "Requested object does not exists" -msgstr "" - -#: backend/urls.py:37 -msgid "Token is invalid" -msgstr "" - -#: backend/urls.py:49 -msgid "Wrong username" -msgstr "" - -#: messages/serializers.py:42 -#, python-format -msgid "The file size cannot exceed %(max_mb_size)d MB" -msgstr "" - -#: ml_model/apps.py:9 ml_model/models.py:146 +#: ml_model/apps.py:8 ml_model/models.py:82 msgid "Neuron Models" msgstr "" -#: ml_model/models.py:29 ml_model/models.py:82 +#: ml_model/models.py:22 ml_model/models.py:42 msgid "Category" msgstr "" -#: ml_model/models.py:30 +#: ml_model/models.py:23 msgid "Categories" msgstr "" -#: ml_model/models.py:40 -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 +#: ml_model/models.py:36 msgid "Fill automatically, don't touch" msgstr "" -#: ml_model/models.py:90 +#: ml_model/models.py:50 msgid "Avatar" msgstr "" -#: ml_model/models.py:93 -msgid "Tags" -msgstr "" - -#: ml_model/models.py:145 +#: ml_model/models.py:81 msgid "Neuron Model" msgstr "" -#: ml_model/models.py:154 +#: ml_model/models.py:90 msgid "Model" msgstr "" -#: ml_model/models.py:170 ml_model/models.py:171 +#: ml_model/models.py:107 +msgid "Authorization token" +msgstr "" + +#: ml_model/models.py:111 ml_model/models.py:112 msgid "Settings" msgstr "" -#: ml_model/models.py:174 +#: ml_model/models.py:115 #, python-format msgid "Settings of %(model_title)s" msgstr "" -#: ml_model/models.py:193 +#: ml_model/models.py:124 +msgid "Default" +msgstr "" + +#: ml_model/models.py:137 #, python-format msgid "%(model_title)s | %(version_name)s" msgstr "" -#: ml_model/models.py:199 +#: ml_model/models.py:143 msgid "Model Version" msgstr "" -#: ml_model/models.py:200 +#: ml_model/models.py:144 msgid "Model Versions" msgstr "" -#: ml_model/models.py:209 +#: ml_model/models.py:153 msgid "Versions" msgstr "" -#: ml_model/models.py:210 +#: ml_model/models.py:154 msgid "Link to versions" msgstr "" -#: ml_model/models.py:219 reports/models/error_report.py:10 +#: ml_model/models.py:163 reports/models/error_report.py:12 msgid "Text" msgstr "" -#: ml_model/models.py:220 stories/models.py:36 +#: ml_model/models.py:164 stories/models.py:36 msgid "Image" msgstr "" -#: ml_model/models.py:221 +#: ml_model/models.py:165 msgid "PDF" msgstr "" -#: ml_model/models.py:222 -msgid "DOCX" -msgstr "" - -#: ml_model/models.py:223 -msgid "DOC" -msgstr "" - -#: ml_model/models.py:224 +#: ml_model/models.py:166 msgid "Text File (Notebook)" msgstr "" -#: ml_model/models.py:225 +#: ml_model/models.py:167 msgid "ZIP Archive" msgstr "" -#: ml_model/models.py:226 +#: ml_model/models.py:168 msgid "Audio" msgstr "" -#: ml_model/models.py:232 ml_model/models.py:271 -#: payments/models/promocode.py:41 +#: ml_model/models.py:174 ml_model/models.py:212 +#: payments/models/promocode.py:40 msgid "Type" msgstr "" -#: ml_model/models.py:234 ml_model/models.py:282 +#: ml_model/models.py:176 ml_model/models.py:225 msgid "Required" msgstr "" -#: ml_model/models.py:237 +#: ml_model/models.py:179 #, python-format msgid "%(model_title)s | %(input_type)s" msgstr "" -#: ml_model/models.py:243 +#: ml_model/models.py:185 msgid "Model Input" msgstr "" -#: ml_model/models.py:244 +#: ml_model/models.py:186 msgid "Model Inputs" msgstr "" -#: ml_model/models.py:250 +#: ml_model/models.py:192 msgid "Integer" msgstr "" -#: ml_model/models.py:251 +#: ml_model/models.py:193 msgid "Float" msgstr "" -#: ml_model/models.py:252 +#: ml_model/models.py:194 msgid "String" msgstr "" -#: ml_model/models.py:255 +#: ml_model/models.py:195 msgid "List" msgstr "" -#: ml_model/models.py:259 +#: ml_model/models.py:198 msgid "Float range" msgstr "" -#: ml_model/models.py:263 +#: ml_model/models.py:202 msgid "Integer range" msgstr "" -#: ml_model/models.py:265 +#: ml_model/models.py:204 msgid "Logical" msgstr "" -#: ml_model/models.py:278 +#: ml_model/models.py:219 msgid "Values" msgstr "" -#: ml_model/models.py:279 +#: ml_model/models.py:221 msgid "" "These values can contain different interfaces and default value optional" msgstr "" -#: ml_model/models.py:281 +#: ml_model/models.py:224 msgid "Hidden" msgstr "" -#: ml_model/models.py:287 +#: ml_model/models.py:230 #, python-format msgid "Parameter of %(model_title)s" msgstr "" -#: ml_model/models.py:290 +#: ml_model/models.py:233 msgid "Parameter" msgstr "" -#: ml_model/models.py:291 +#: ml_model/models.py:234 msgid "Parameters" msgstr "" -#: ml_model/models.py:296 +#: ml_model/models.py:239 msgid "Fixed" msgstr "" -#: ml_model/models.py:297 +#: ml_model/models.py:240 msgid "Per generation second" msgstr "" -#: ml_model/models.py:298 +#: ml_model/models.py:241 msgid "Per one text token" msgstr "" -#: ml_model/models.py:299 +#: ml_model/models.py:242 msgid "Per image pixel" msgstr "" -#: ml_model/models.py:302 +#: ml_model/models.py:245 msgid "By input data" msgstr "" -#: ml_model/models.py:303 +#: ml_model/models.py:246 msgid "By output data" msgstr "" -#: ml_model/models.py:304 +#: ml_model/models.py:247 msgid "By all data" msgstr "" -#: ml_model/models.py:309 +#: ml_model/models.py:250 msgid "Strategy" msgstr "" -#: ml_model/models.py:314 +#: ml_model/models.py:255 msgid "Interaction Type" msgstr "" -#: ml_model/models.py:319 payments/models/invoice.py:19 +#: ml_model/models.py:260 payments/models/invoice.py:19 msgid "Cost" msgstr "" -#: ml_model/models.py:320 +#: ml_model/models.py:261 msgid "In RUB, per specified strategy" msgstr "" -#: ml_model/models.py:325 +#: ml_model/models.py:266 msgid "Coefficient" msgstr "" -#: ml_model/models.py:326 +#: ml_model/models.py:267 msgid "Cost multiplier" msgstr "" -#: ml_model/models.py:333 +#: ml_model/models.py:274 msgid "Rate" msgstr "" -#: ml_model/models.py:337 +#: ml_model/models.py:278 msgid "Payment Rule" msgstr "" -#: ml_model/models.py:338 +#: ml_model/models.py:279 msgid "Payment Rules" msgstr "" -#: ml_model/selectors/ml_models_selector.py:75 +#: ml_model/selectors/ml_models_selector.py:56 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:61 ml_model/services/minio_service.py:70 +#: ml_model/services/minio_service.py:65 ml_model/services/minio_service.py:74 msgid "Unknown bucket destination" msgstr "" -#: ml_model/services/upscaleai.py:124 +#: ml_model/services/upscaleai.py:126 msgid "No image given for improving" msgstr "" -#: payments/apps.py:9 payments/models/payment.py:60 +#: payments/apps.py:8 payments/models/payment.py:62 msgid "Payments" msgstr "" -#: payments/exceptions/insufficient_balance.py:18 +#: payments/exceptions/insufficient_balance.py:15 #, python-format msgid "" -"Token balance: %(balance).2f,\n" -"Required amount: %(required)s,\n" -"Needed %(needed)s more" +"Token balance: %(balance)d,\n" +"Required amount: %(required)d,\n" +"Needed %(needed)d more" msgstr "" #: payments/models/invoice.py:23 @@ -806,23 +759,23 @@ msgstr "" msgid "Status" msgstr "" -#: payments/models/payment.py:59 +#: payments/models/payment.py:61 msgid "Payment" msgstr "" -#: payments/models/payment_plan.py:18 +#: payments/models/payment_plan.py:16 msgid "Price" msgstr "" -#: payments/models/payment_plan.py:22 +#: payments/models/payment_plan.py:20 msgid "Tokens per plan" msgstr "" -#: payments/models/payment_plan.py:25 +#: payments/models/payment_plan.py:23 msgid "Is corporate" msgstr "" -#: payments/models/payment_plan.py:26 +#: payments/models/payment_plan.py:24 msgid "Is recurrent" msgstr "" @@ -834,79 +787,79 @@ msgstr "" msgid "Is visible" msgstr "" -#: payments/models/payment_plan.py:52 payments/models/payment_plan.py:67 +#: payments/models/payment_plan.py:41 payments/models/payment_plan.py:56 msgid "Payment Plan" msgstr "" -#: payments/models/payment_plan.py:53 +#: payments/models/payment_plan.py:42 msgid "Payment Plans" msgstr "" -#: payments/models/payment_plan.py:69 +#: payments/models/payment_plan.py:58 msgid "Last payment at" msgstr "" -#: payments/models/payment_plan.py:70 +#: payments/models/payment_plan.py:59 msgid "Next payment at" msgstr "" -#: payments/models/payment_plan.py:72 +#: payments/models/payment_plan.py:61 msgid "Current balance" msgstr "" -#: payments/models/payment_plan.py:78 +#: payments/models/payment_plan.py:67 msgid "Recurrent billing task" msgstr "" -#: payments/models/payment_plan.py:99 payments/models/payment_plan.py:100 +#: payments/models/payment_plan.py:84 payments/models/payment_plan.py:85 msgid "User Balance" msgstr "" -#: payments/models/promocode.py:48 +#: payments/models/promocode.py:44 msgid "Action Function" msgstr "" -#: payments/models/promocode.py:73 +#: payments/models/promocode.py:69 msgid "Can be only for referral promos" msgstr "" -#: payments/models/promocode.py:81 +#: payments/models/promocode.py:77 msgid "Is personal" msgstr "" -#: payments/models/promocode.py:82 +#: payments/models/promocode.py:78 msgid "Can be activated only one time" msgstr "" -#: payments/models/promocode.py:89 -msgid "Owner can be only for refferal promos" +#: payments/models/promocode.py:87 +msgid "Owner can be only for referral promos" msgstr "" -#: payments/models/promocode.py:97 payments/models/promocode.py:107 +#: payments/models/promocode.py:95 payments/models/promocode.py:106 msgid "Promocode" msgstr "" -#: payments/models/promocode.py:98 +#: payments/models/promocode.py:96 msgid "Promocodes" msgstr "" -#: payments/models/promocode.py:105 +#: payments/models/promocode.py:103 msgid "Activated by" msgstr "" -#: payments/models/promocode.py:137 +#: payments/models/promocode.py:134 msgid "Promocode Activation" msgstr "" -#: payments/models/promocode.py:138 +#: payments/models/promocode.py:135 msgid "Promocode Activations" msgstr "" -#: payments/models/user_payment_method.py:24 +#: payments/models/user_payment_method.py:28 msgid "Payment Method" msgstr "" -#: payments/models/user_payment_method.py:25 +#: payments/models/user_payment_method.py:29 msgid "Payment Methods" msgstr "" @@ -914,15 +867,15 @@ msgstr "" msgid "Messages for this model are not registered in a selector" msgstr "" -#: payments/selectors/payment_plan_selector.py:32 +#: payments/selectors/payment_plan_selector.py:29 msgid "Business accounts are not allowed to make purchases" msgstr "" -#: payments/selectors/payment_plan_selector.py:57 +#: payments/selectors/payment_plan_selector.py:54 msgid "No plan by this uid" msgstr "" -#: payments/services/model_billing_service.py:31 +#: payments/services/model_billing_service.py:27 msgid "Unknown account type" msgstr "" @@ -950,19 +903,19 @@ msgstr "" msgid "Proxies" msgstr "" -#: reports/models/error_report.py:9 +#: reports/models/error_report.py:10 msgid "Author" msgstr "" -#: reports/models/error_report.py:11 +#: reports/models/error_report.py:13 msgid "Attachments" msgstr "" -#: reports/models/error_report.py:14 +#: reports/models/error_report.py:16 msgid "User Report" msgstr "" -#: reports/models/error_report.py:15 +#: reports/models/error_report.py:17 msgid "User Reports" msgstr "" @@ -1002,40 +955,40 @@ msgstr "" msgid "Widgets" msgstr "" -#: tools/apps.py:9 +#: tools/apps.py:8 msgid "Tools" msgstr "" -#: tools/apps.py:15 tools/chats/models.py:21 +#: tools/apps.py:14 tools/chats/models.py:23 msgid "Chats" msgstr "" -#: tools/apps.py:21 +#: tools/apps.py:20 msgid "Copywrite" msgstr "" -#: tools/apps.py:32 +#: tools/apps.py:26 msgid "Public API" msgstr "" -#: tools/apps.py:38 +#: tools/apps.py:32 msgid "Media" msgstr "" -#: tools/apps.py:44 +#: tools/apps.py:38 msgid "Feed" msgstr "" -#: tools/chats/models.py:13 tools/public_api/models.py:45 +#: tools/chats/models.py:15 tools/public_api/models.py:47 msgid "Is deleted" msgstr "" -#: tools/chats/models.py:17 +#: tools/chats/models.py:19 #, python-format msgid "Chat %(id)s" msgstr "" -#: tools/chats/models.py:20 +#: tools/chats/models.py:22 msgid "Chat" msgstr "" @@ -1047,18 +1000,14 @@ msgstr "" msgid "API Key not found" msgstr "" -#: tools/public_api/models.py:46 +#: tools/public_api/models.py:48 msgid "Expires at" msgstr "" -#: tools/public_api/models.py:50 +#: tools/public_api/models.py:52 msgid "API Key" msgstr "" -#: tools/public_api/models.py:51 +#: tools/public_api/models.py:53 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-30 03:00+0300\n" +"POT-Creation-Date: 2024-10-30 20:55+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -20,8 +20,7 @@ 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 ml_model/models.py:47 -#: stories/models.py:18 +#: achievements/admin.py:11 achievements/models.py:19 stories/models.py:18 msgid "Icon" msgstr "Миниатюра" @@ -33,21 +32,21 @@ msgstr "Достижение" msgid "Achievements" msgstr "Достижения" -#: achievements/models.py:14 ml_model/models.py:19 ml_model/models.py:39 -#: ml_model/models.py:72 ml_model/models.py:180 +#: achievements/models.py:14 ml_model/models.py:12 ml_model/models.py:34 +#: ml_model/models.py:123 msgid "Slug" msgstr "Ярлык" -#: achievements/models.py:16 ml_model/models.py:70 ml_model/models.py:179 -#: ml_model/models.py:268 payments/models/payment.py:52 +#: achievements/models.py:16 ml_model/models.py:32 ml_model/models.py:121 +#: ml_model/models.py:208 payments/models/payment.py:53 msgid "Description" msgstr "Описание" #: achievements/models.py:43 authentication/models/business_host.py:21 -#: authentication/models/email_token.py:12 authentication/models/user.py:241 -#: authentication/models/user.py:242 authentication/models/user_telegram.py:22 +#: authentication/models/email_token.py:12 authentication/models/user.py:203 +#: authentication/models/user.py:204 authentication/models/user_telegram.py:30 #: authentication/models/user_vk.py:12 payments/models/invoice.py:15 -#: payments/models/payment.py:26 payments/models/payment_plan.py:61 +#: payments/models/payment.py:26 payments/models/payment_plan.py:50 msgid "User" msgstr "Пользователь" @@ -65,8 +64,8 @@ msgstr "Достижение %(achievement_title)s пользователя %(us msgid "Issued achievement" msgstr "Выданное достижение" -#: authentication/models/business_account.py:16 payments/models/promocode.py:72 -#: tools/public_api/models.py:33 +#: authentication/models/business_account.py:16 payments/models/promocode.py:68 +#: tools/public_api/models.py:35 msgid "Owner" msgstr "Владелец" @@ -89,7 +88,7 @@ msgid "Acceptance" msgstr "Подтверждение" #: authentication/models/business_account.py:44 -#: authentication/models/business_group.py:21 tools/public_api/models.py:43 +#: authentication/models/business_group.py:21 tools/public_api/models.py:45 msgid "Token limit" msgstr "Лимит токенов" @@ -97,7 +96,7 @@ msgstr "Лимит токенов" msgid "Group" msgstr "Группа" -#: authentication/models/business_account.py:61 +#: authentication/models/business_account.py:62 msgid "" "Impossible to add this employee to this group which does not belong to this " "company" @@ -105,17 +104,17 @@ msgstr "" "Невозможно добавить сотрудника к группе, когда он не принадлежит данной " "компании" -#: authentication/models/business_account.py:70 +#: authentication/models/business_account.py:72 msgid "Child Business Account" msgstr "Дочерний Бизнес Аккаунт" -#: authentication/models/business_account.py:71 +#: authentication/models/business_account.py:73 msgid "Child Business Accounts" msgstr "Дочерние Бизнес Аккаунты" -#: 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 +#: authentication/models/business_group.py:8 ml_model/models.py:11 +#: ml_model/models.py:31 ml_model/models.py:206 +#: payments/models/payment_plan.py:26 stories/models.py:12 stories/models.py:35 #: tools/chats/models.py:9 msgid "Title" msgstr "Название" @@ -132,9 +131,9 @@ msgstr "Бизнес Группы" msgid "Affiliated by" msgstr "Кем привлечена" -#: 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 +#: authentication/models/business_host.py:35 authentication/models/user.py:123 +#: authentication/models/whitelist.py:16 ml_model/models.py:103 +#: payments/models/promocode.py:82 msgid "Is active" msgstr "Является активной" @@ -142,68 +141,68 @@ msgstr "Является активной" msgid "Sector" msgstr "Сектор" -#: authentication/models/business_host.py:44 +#: authentication/models/business_host.py:45 msgid "Planned amount of workers" msgstr "Планируемое число сотрудников" -#: authentication/models/business_host.py:49 +#: authentication/models/business_host.py:51 msgid "Usage intensity" msgstr "Частота использования" -#: authentication/models/business_host.py:56 +#: authentication/models/business_host.py:58 msgid "Token low balance cap" msgstr "Предел низкого баланса" -#: authentication/models/business_host.py:61 +#: authentication/models/business_host.py:63 msgid "Emails token low balance cap" msgstr "Email'ы для рассылки по низкому балансу" -#: authentication/models/business_host.py:63 +#: authentication/models/business_host.py:66 msgid "Token low balance cap enabled" msgstr "Рассылка по низкому балансу включена" -#: authentication/models/business_host.py:66 +#: authentication/models/business_host.py:70 msgid "ITN" msgstr "ИНН" -#: authentication/models/business_host.py:67 +#: authentication/models/business_host.py:71 msgid "PSRN" msgstr "ОГРН" -#: authentication/models/business_host.py:68 ml_model/models.py:178 -#: tools/public_api/models.py:30 +#: authentication/models/business_host.py:73 ml_model/models.py:119 +#: tools/public_api/models.py:31 msgid "Name" msgstr "Наименование" -#: authentication/models/business_host.py:71 +#: authentication/models/business_host.py:78 msgid "Preffered name" msgstr "" -#: authentication/models/business_host.py:72 +#: authentication/models/business_host.py:81 msgid "Corporate email" msgstr "Корпоративная почта" -#: authentication/models/business_host.py:74 +#: authentication/models/business_host.py:84 msgid "Corporate phone" msgstr "Корпоративный телефон" -#: authentication/models/business_host.py:76 +#: authentication/models/business_host.py:87 msgid "Job title" msgstr "Наименование работ" -#: authentication/models/business_host.py:81 +#: authentication/models/business_host.py:93 msgid "Allowed models" msgstr "Разрешенные модели" -#: authentication/models/business_host.py:84 +#: authentication/models/business_host.py:97 msgid "Log history enabled" msgstr "История логов включена" -#: authentication/models/business_host.py:103 +#: authentication/models/business_host.py:117 msgid "Business Account" msgstr "Бизнес Аккаунт" -#: authentication/models/business_host.py:104 +#: authentication/models/business_host.py:118 msgid "Business Accounts" msgstr "Бизнес Аккаунты" @@ -271,7 +270,7 @@ msgstr "Админ" msgid "Security" msgstr "Безопасность" -#: authentication/models/email_token.py:15 ml_model/models.py:269 +#: authentication/models/email_token.py:15 ml_model/models.py:210 msgid "Key" msgstr "Ключ" @@ -283,43 +282,43 @@ msgstr "Email Токен" msgid "Email Tokens" msgstr "Email Токены" -#: authentication/models/user.py:120 authentication/models/user_telegram.py:9 +#: authentication/models/user.py:100 authentication/models/user_telegram.py:10 msgid "First name" msgstr "Имя" -#: authentication/models/user.py:127 authentication/models/user_telegram.py:10 +#: authentication/models/user.py:107 authentication/models/user_telegram.py:13 msgid "Last name" msgstr "Фамилия" -#: authentication/models/user.py:134 authentication/models/user_telegram.py:11 +#: authentication/models/user.py:110 authentication/models/user_telegram.py:16 msgid "Username" msgstr "Имя пользователя" -#: authentication/models/user.py:141 +#: authentication/models/user.py:117 msgid "Email" msgstr "Email" -#: authentication/models/user.py:149 +#: authentication/models/user.py:125 msgid "Is staff" msgstr "Административный" -#: authentication/models/user.py:150 +#: authentication/models/user.py:126 msgid "Is superuser" msgstr "Суперюзер" -#: authentication/models/user.py:151 +#: authentication/models/user.py:127 msgid "Is email confirmed" msgstr "Email подтвержден" -#: authentication/models/user.py:152 +#: authentication/models/user.py:129 msgid "Is subscribed" msgstr "Подписан на уведомления" -#: authentication/models/user.py:158 +#: authentication/models/user.py:136 msgid "Picture name" msgstr "Имя аватара" -#: authentication/models/user.py:167 authentication/models/utm.py:21 +#: authentication/models/user.py:145 authentication/models/utm.py:21 msgid "UTM" msgstr "UTM" @@ -331,38 +330,38 @@ msgstr "Телеграм ID" msgid "Is bot" msgstr "Является ботом" -#: authentication/models/user_telegram.py:12 +#: authentication/models/user_telegram.py:19 msgid "Language" msgstr "Язык" -#: authentication/models/user_telegram.py:13 +#: authentication/models/user_telegram.py:21 msgid "Is premium" msgstr "Премиум" -#: authentication/models/user_telegram.py:15 +#: authentication/models/user_telegram.py:23 msgid "Is subscribed to channel" msgstr "Подписан на канал" -#: authentication/models/user_telegram.py:25 +#: authentication/models/user_telegram.py:34 msgid "Phonenumber" msgstr "Номер телефона" -#: authentication/models/user_telegram.py:27 +#: authentication/models/user_telegram.py:37 #: authentication/models/user_vk.py:14 payments/models/invoice.py:11 -#: stories/models.py:15 tools/chats/models.py:10 +#: stories/models.py:15 tools/chats/models.py:11 msgid "Created at" msgstr "Когда создан" -#: authentication/models/user_telegram.py:28 +#: authentication/models/user_telegram.py:38 #: authentication/models/user_vk.py:15 msgid "Updated at" msgstr "Когда обновлен" -#: authentication/models/user_telegram.py:34 +#: authentication/models/user_telegram.py:44 msgid "Telegram User" msgstr "Пользователь телеграм" -#: authentication/models/user_telegram.py:35 +#: authentication/models/user_telegram.py:45 msgid "Telegram Users" msgstr "Пользователи Телеграм" @@ -406,401 +405,354 @@ msgstr "Вайтлист для отмены политик" msgid "Whitelists to cancel policies" msgstr "Вайтлисты для отмены политик" -#: 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:60 +#: authentication/selectors/business_host_selector.py:47 msgid "Host user is not registered for this account" msgstr "Пользователь бизнес-аккаунта не зарегистрирован для этого аккаунта" -#: authentication/selectors/user_selector.py:80 +#: authentication/selectors/user_selector.py:73 msgid "No user with this uid found" msgstr "Не найден пользователь с данным ID" -#: authentication/services/business_account_service.py:58 +#: authentication/services/business_account_service.py:54 msgid "BusinessAccount for this user doesn't exist" msgstr "Бизнес-аккаунт для данного юзера не найден" -#: authentication/services/business_account_service.py:68 +#: authentication/services/business_account_service.py:65 msgid "Invited account can either accept or reject an invitation" msgstr "Приглашенный аккаунт может принять или отклонить приглашение" -#: authentication/services/business_account_service.py:73 +#: authentication/services/business_account_service.py:71 msgid "Account is already confirmed" msgstr "Аккаунт уже подтвержден" -#: authentication/services/business_host_service.py:152 +#: authentication/services/business_host_service.py:148 msgid "No user_email is provided" msgstr "" -#: authentication/services/business_host_service.py:199 +#: authentication/services/business_host_service.py:203 msgid "No business account by this uid at your company" msgstr "Такого аккаунта нет в вашей компании" -#: authentication/services/email_service.py:115 +#: authentication/services/email_service.py:100 msgid "Regular users cannot send introductory letters" msgstr "Обычные пользователи не могут отсылать письма" -#: authentication/services/email_service.py:137 +#: authentication/services/email_service.py:122 msgid "Regular users cannot send invitation letters" msgstr "Обычные пользователи не могут отправлять письма для приглашений" -#: authentication/services/user_services.py:51 +#: authentication/services/user_services.py:45 msgid "New user data is invalid" msgstr "" -#: authentication/services/user_services.py:111 +#: authentication/services/user_services.py:97 msgid "Wrong email" msgstr "Неверный email" -#: authentication/services/user_services.py:119 backend/urls.py:43 +#: authentication/services/user_services.py:105 msgid "Wrong password" msgstr "Неверный пароль" -#: authentication/services/user_services.py:122 +#: authentication/services/user_services.py:108 msgid "User has not confirmed his email yet" msgstr "Пользователь пока не подтвердил свой email" -#: authentication/services/user_services.py:161 +#: authentication/services/user_services.py:147 msgid "No user like this in a database" msgstr "Такой пользователь отсутствует" -#: authentication/services/user_services.py:178 +#: authentication/services/user_services.py:164 msgid "token is not provided" msgstr "" -#: authentication/services/user_services.py:182 +#: authentication/services/user_services.py:168 msgid "No user token like this in a database" msgstr "" -#: authentication/services/user_services.py:202 +#: authentication/services/user_services.py:188 msgid "No email token provided" msgstr "Токен не получен" -#: authentication/services/user_services.py:206 +#: authentication/services/user_services.py:192 msgid "No token like this in a database" msgstr "Не найдено такого токена" -#: authentication/services/user_services.py:212 +#: authentication/services/user_services.py:201 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: authentication/services/user_services.py:250 +#: authentication/services/user_services.py:237 msgid "Current password is wrong" msgstr "Текущий пароль неверен" -#: backend/urls.py:31 -msgid "Requested object does not exists" -msgstr "" - -#: backend/urls.py:37 -msgid "Token is invalid" -msgstr "" - -#: backend/urls.py:49 -#, fuzzy -#| msgid "Wrong email" -msgid "Wrong username" -msgstr "Неверный email" - -#: messages/serializers.py:42 -#, python-format -msgid "The file size cannot exceed %(max_mb_size)d MB" -msgstr "Файл не может быть размером больше %(max_mb_size)d мегабайт" - -#: ml_model/apps.py:9 ml_model/models.py:146 +#: ml_model/apps.py:8 ml_model/models.py:82 msgid "Neuron Models" msgstr "Нейронные Модели" -#: ml_model/models.py:29 ml_model/models.py:82 +#: ml_model/models.py:22 ml_model/models.py:42 msgid "Category" msgstr "Категория" -#: ml_model/models.py:30 +#: ml_model/models.py:23 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 +#: ml_model/models.py:36 msgid "Fill automatically, don't touch" msgstr "Заполняется автоматически, не трогать" -#: ml_model/models.py:90 +#: ml_model/models.py:50 msgid "Avatar" msgstr "Аватар" -#: ml_model/models.py:93 -msgid "Tags" -msgstr "Теги" - -#: ml_model/models.py:145 +#: ml_model/models.py:81 msgid "Neuron Model" msgstr "Нейронная Модель" -#: ml_model/models.py:154 +#: ml_model/models.py:90 msgid "Model" msgstr "Модель" -#: ml_model/models.py:170 ml_model/models.py:171 +#: ml_model/models.py:107 +msgid "Authorization token" +msgstr "Авторизационный токен" + +#: ml_model/models.py:111 ml_model/models.py:112 msgid "Settings" msgstr "Настройки" -#: ml_model/models.py:174 +#: ml_model/models.py:115 #, fuzzy, python-format #| msgid "Settings of %(model_title)" msgid "Settings of %(model_title)s" msgstr "Настройки %(model_title)s" -#: ml_model/models.py:193 +#: ml_model/models.py:124 +#, fuzzy +#| msgid "Default value" +msgid "Default" +msgstr "Стандартное значение" + +#: ml_model/models.py:137 #, python-format msgid "%(model_title)s | %(version_name)s" msgstr "%(model_title)s | %(version_name)s" -#: ml_model/models.py:199 +#: ml_model/models.py:143 msgid "Model Version" msgstr "Версия Модели" -#: ml_model/models.py:200 +#: ml_model/models.py:144 msgid "Model Versions" msgstr "Версии Модели" -#: ml_model/models.py:209 +#: ml_model/models.py:153 msgid "Versions" msgstr "Версии" -#: ml_model/models.py:210 +#: ml_model/models.py:154 msgid "Link to versions" msgstr "Привязка к версиям" -#: ml_model/models.py:219 reports/models/error_report.py:10 +#: ml_model/models.py:163 reports/models/error_report.py:12 msgid "Text" msgstr "Текст" -#: ml_model/models.py:220 stories/models.py:36 +#: ml_model/models.py:164 stories/models.py:36 msgid "Image" msgstr "Картинка" -#: ml_model/models.py:221 +#: ml_model/models.py:165 msgid "PDF" msgstr "PDF" -#: ml_model/models.py:222 -msgid "DOCX" -msgstr "DOCX" - -#: ml_model/models.py:223 -msgid "DOC" -msgstr "DOC" - -#: ml_model/models.py:224 +#: ml_model/models.py:166 msgid "Text File (Notebook)" msgstr "Текстовый файл (Блокнот)" -#: ml_model/models.py:225 +#: ml_model/models.py:167 msgid "ZIP Archive" msgstr "ZIP архив" -#: ml_model/models.py:226 +#: ml_model/models.py:168 msgid "Audio" msgstr "Аудио" -#: ml_model/models.py:232 ml_model/models.py:271 -#: payments/models/promocode.py:41 +#: ml_model/models.py:174 ml_model/models.py:212 +#: payments/models/promocode.py:40 msgid "Type" msgstr "Тип" -#: ml_model/models.py:234 ml_model/models.py:282 +#: ml_model/models.py:176 ml_model/models.py:225 msgid "Required" msgstr "Обязательный" -#: ml_model/models.py:237 +#: ml_model/models.py:179 #, python-format msgid "%(model_title)s | %(input_type)s" msgstr "%(model_title)s | %(input_type)s" -#: ml_model/models.py:243 +#: ml_model/models.py:185 #, fuzzy #| msgid "Model" msgid "Model Input" msgstr "Модель" -#: ml_model/models.py:244 +#: ml_model/models.py:186 msgid "Model Inputs" msgstr "Входящий поток модели" -#: ml_model/models.py:250 +#: ml_model/models.py:192 msgid "Integer" msgstr "Целое число" -#: ml_model/models.py:251 +#: ml_model/models.py:193 msgid "Float" msgstr "Вещественное число" -#: ml_model/models.py:252 +#: ml_model/models.py:194 msgid "String" msgstr "Строка" -#: ml_model/models.py:255 +#: ml_model/models.py:195 msgid "List" msgstr "Список" -#: ml_model/models.py:259 +#: ml_model/models.py:198 msgid "Float range" msgstr "Вещественный диапазон" -#: ml_model/models.py:263 +#: ml_model/models.py:202 msgid "Integer range" msgstr "Целочисленный диапазон" -#: ml_model/models.py:265 +#: ml_model/models.py:204 msgid "Logical" msgstr "Логический" -#: ml_model/models.py:278 +#: ml_model/models.py:219 msgid "Values" msgstr "Значения" -#: ml_model/models.py:279 +#: ml_model/models.py:221 msgid "" "These values can contain different interfaces and default value optional" msgstr "" "Значения могут содержать различные интерфейс и, опционально, значение по " "умолчанию" -#: ml_model/models.py:281 +#: ml_model/models.py:224 msgid "Hidden" msgstr "Скрытый" -#: ml_model/models.py:287 +#: ml_model/models.py:230 #, fuzzy, python-format #| msgid "Parameter of %(model_title)" msgid "Parameter of %(model_title)s" msgstr "Параметр %(model_title)s" -#: ml_model/models.py:290 +#: ml_model/models.py:233 msgid "Parameter" msgstr "Параметр" -#: ml_model/models.py:291 +#: ml_model/models.py:234 msgid "Parameters" msgstr "Параметры" -#: ml_model/models.py:296 +#: ml_model/models.py:239 msgid "Fixed" msgstr "Фикса" -#: ml_model/models.py:297 +#: ml_model/models.py:240 msgid "Per generation second" msgstr "За секунду генерации" -#: ml_model/models.py:298 +#: ml_model/models.py:241 msgid "Per one text token" msgstr "За один текстовый токен" -#: ml_model/models.py:299 +#: ml_model/models.py:242 msgid "Per image pixel" msgstr "За один пиксель" -#: ml_model/models.py:302 +#: ml_model/models.py:245 msgid "By input data" msgstr "По входящим данным" -#: ml_model/models.py:303 +#: ml_model/models.py:246 msgid "By output data" msgstr "По исходящим данным" -#: ml_model/models.py:304 +#: ml_model/models.py:247 msgid "By all data" msgstr "По всем данным" -#: ml_model/models.py:309 +#: ml_model/models.py:250 #, fuzzy #| msgid "Category" msgid "Strategy" msgstr "Стратегия" -#: ml_model/models.py:314 +#: ml_model/models.py:255 msgid "Interaction Type" msgstr "Тип взаимодействия" -#: ml_model/models.py:319 payments/models/invoice.py:19 +#: ml_model/models.py:260 payments/models/invoice.py:19 msgid "Cost" msgstr "Цена" -#: ml_model/models.py:320 +#: ml_model/models.py:261 msgid "In RUB, per specified strategy" msgstr "В рублях, за указанную стратегию" -#: ml_model/models.py:325 +#: ml_model/models.py:266 msgid "Coefficient" msgstr "Коэффициент" -#: ml_model/models.py:326 +#: ml_model/models.py:267 msgid "Cost multiplier" msgstr "Цена" -#: ml_model/models.py:333 +#: ml_model/models.py:274 msgid "Rate" msgstr "Ставка" -#: ml_model/models.py:337 +#: ml_model/models.py:278 #, fuzzy #| msgid "Payment Plan" msgid "Payment Rule" msgstr "Платежное правило" -#: ml_model/models.py:338 +#: ml_model/models.py:279 +#, fuzzy +#| msgid "Payment Plans" msgid "Payment Rules" msgstr "Платежные правила" -#: ml_model/selectors/ml_models_selector.py:75 +#: ml_model/selectors/ml_models_selector.py:56 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:61 ml_model/services/minio_service.py:70 +#: ml_model/services/minio_service.py:65 ml_model/services/minio_service.py:74 msgid "Unknown bucket destination" msgstr "Неизвестный бакет для загрузки" -#: ml_model/services/upscaleai.py:124 +#: ml_model/services/upscaleai.py:126 msgid "No image given for improving" msgstr "Нет изображения для улучшения" -#: payments/apps.py:9 payments/models/payment.py:60 +#: payments/apps.py:8 payments/models/payment.py:62 msgid "Payments" msgstr "Платежи" -#: payments/exceptions/insufficient_balance.py:18 +#: payments/exceptions/insufficient_balance.py:15 #, python-format msgid "" -"Token balance: %(balance).2f,\n" -"Required amount: %(required)s,\n" -"Needed %(needed)s more" -msgstr "" -"Баланс: %(balance).2f токенов,\n" -"Нужно: %(required)s токенов,\n" -"Нужно еще: %(needed)s токенов" +"Token balance: %(balance)d,\n" +"Required amount: %(required)d,\n" +"Needed %(needed)d more" +msgstr "Баланс: %(balance)d токенов. Нужно: %(required)d токенов. Нужно еще: %(needed)d токенов" #: payments/models/invoice.py:23 msgid "Generative Model" @@ -826,23 +778,23 @@ msgstr "План" msgid "Status" msgstr "Статус" -#: payments/models/payment.py:59 +#: payments/models/payment.py:61 msgid "Payment" msgstr "Платеж" -#: payments/models/payment_plan.py:18 +#: payments/models/payment_plan.py:16 msgid "Price" msgstr "Цена" -#: payments/models/payment_plan.py:22 +#: payments/models/payment_plan.py:20 msgid "Tokens per plan" msgstr "Токенов за план" -#: payments/models/payment_plan.py:25 +#: payments/models/payment_plan.py:23 msgid "Is corporate" msgstr "Корпоративный" -#: payments/models/payment_plan.py:26 +#: payments/models/payment_plan.py:24 msgid "Is recurrent" msgstr "Рекуррентный" @@ -854,81 +806,79 @@ msgstr "Длительность" msgid "Is visible" msgstr "Видимый" -#: payments/models/payment_plan.py:52 payments/models/payment_plan.py:67 +#: payments/models/payment_plan.py:41 payments/models/payment_plan.py:56 msgid "Payment Plan" msgstr "Платежный План" -#: payments/models/payment_plan.py:53 +#: payments/models/payment_plan.py:42 msgid "Payment Plans" msgstr "Платежные Планы" -#: payments/models/payment_plan.py:69 +#: payments/models/payment_plan.py:58 msgid "Last payment at" msgstr "Последнее время платежа" -#: payments/models/payment_plan.py:70 +#: payments/models/payment_plan.py:59 msgid "Next payment at" msgstr "Следующее время платежа" -#: payments/models/payment_plan.py:72 +#: payments/models/payment_plan.py:61 msgid "Current balance" msgstr "Текущий баланс" -#: payments/models/payment_plan.py:78 +#: payments/models/payment_plan.py:67 msgid "Recurrent billing task" msgstr "Рекуррентная задача на платеж" -#: payments/models/payment_plan.py:99 payments/models/payment_plan.py:100 +#: payments/models/payment_plan.py:84 payments/models/payment_plan.py:85 msgid "User Balance" msgstr "Баланс пользователя" -#: payments/models/promocode.py:48 +#: payments/models/promocode.py:44 msgid "Action Function" msgstr "Активирующаяся функция" -#: payments/models/promocode.py:73 +#: payments/models/promocode.py:69 msgid "Can be only for referral promos" msgstr "Может быть только у реферальных промокодов" -#: payments/models/promocode.py:81 +#: payments/models/promocode.py:77 msgid "Is personal" msgstr "Персональный" -#: payments/models/promocode.py:82 +#: payments/models/promocode.py:78 msgid "Can be activated only one time" msgstr "Может быть активирован только один раз" -#: payments/models/promocode.py:89 -#, fuzzy -#| msgid "Owner can be only for referral promos" -msgid "Owner can be only for refferal promos" +#: payments/models/promocode.py:87 +msgid "Owner can be only for referral promos" msgstr "Владелец может быть только у реферальных промокодов" -#: payments/models/promocode.py:97 payments/models/promocode.py:107 +#: payments/models/promocode.py:95 payments/models/promocode.py:106 msgid "Promocode" msgstr "Промокод" -#: payments/models/promocode.py:98 +#: payments/models/promocode.py:96 msgid "Promocodes" msgstr "Промокоды" -#: payments/models/promocode.py:105 +#: payments/models/promocode.py:103 msgid "Activated by" msgstr "Кем активирован" -#: payments/models/promocode.py:137 +#: payments/models/promocode.py:134 msgid "Promocode Activation" msgstr "Активация Промокода" -#: payments/models/promocode.py:138 +#: payments/models/promocode.py:135 msgid "Promocode Activations" msgstr "Активации Промокодов" -#: payments/models/user_payment_method.py:24 +#: payments/models/user_payment_method.py:28 msgid "Payment Method" msgstr "Платежный метод" -#: payments/models/user_payment_method.py:25 +#: payments/models/user_payment_method.py:29 msgid "Payment Methods" msgstr "Платежные методы" @@ -936,15 +886,15 @@ msgstr "Платежные методы" msgid "Messages for this model are not registered in a selector" msgstr "" -#: payments/selectors/payment_plan_selector.py:32 +#: payments/selectors/payment_plan_selector.py:29 msgid "Business accounts are not allowed to make purchases" msgstr "Сотрудники не могут производить покупки" -#: payments/selectors/payment_plan_selector.py:57 +#: payments/selectors/payment_plan_selector.py:54 msgid "No plan by this uid" msgstr "Не найдено подписки по этому ID" -#: payments/services/model_billing_service.py:31 +#: payments/services/model_billing_service.py:27 msgid "Unknown account type" msgstr "Неизвестный тип аккаунта" @@ -972,19 +922,19 @@ msgstr "Прокси" msgid "Proxies" msgstr "Прокси" -#: reports/models/error_report.py:9 +#: reports/models/error_report.py:10 msgid "Author" msgstr "Автор" -#: reports/models/error_report.py:11 +#: reports/models/error_report.py:13 msgid "Attachments" msgstr "Вложения" -#: reports/models/error_report.py:14 +#: reports/models/error_report.py:16 msgid "User Report" msgstr "Пользовательский репорт" -#: reports/models/error_report.py:15 +#: reports/models/error_report.py:17 msgid "User Reports" msgstr "Пользовательские репорты" @@ -1024,41 +974,41 @@ msgstr "Виджет" msgid "Widgets" msgstr "Виджеты" -#: tools/apps.py:9 +#: tools/apps.py:8 msgid "Tools" msgstr "Инструменты" -#: tools/apps.py:15 tools/chats/models.py:21 +#: tools/apps.py:14 tools/chats/models.py:23 msgid "Chats" msgstr "Чаты" -#: tools/apps.py:21 +#: tools/apps.py:20 msgid "Copywrite" msgstr "Копирайт" -#: tools/apps.py:32 +#: tools/apps.py:26 msgid "Public API" msgstr "Публичный API" -#: tools/apps.py:38 +#: tools/apps.py:32 msgid "Media" msgstr "Медиа" -#: tools/apps.py:44 +#: tools/apps.py:38 msgid "Feed" msgstr "Шейр пользователей" -#: tools/chats/models.py:13 tools/public_api/models.py:45 +#: tools/chats/models.py:15 tools/public_api/models.py:47 msgid "Is deleted" msgstr "Удален" -#: tools/chats/models.py:17 +#: tools/chats/models.py:19 #, fuzzy, python-format #| msgid "Chat %(id)" msgid "Chat %(id)s" msgstr "Чат %(id)s" -#: tools/chats/models.py:20 +#: tools/chats/models.py:22 msgid "Chat" msgstr "Чат" @@ -1070,30 +1020,18 @@ msgstr "Необходимо повысить лимит токенов у API- msgid "API Key not found" msgstr "API-ключ не найден" -#: tools/public_api/models.py:46 +#: tools/public_api/models.py:48 msgid "Expires at" msgstr "Когда заканчивается" -#: tools/public_api/models.py:50 +#: tools/public_api/models.py:52 msgid "API Key" msgstr "API Ключ" -#: tools/public_api/models.py:51 +#: tools/public_api/models.py:53 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 "Можно искать по: Названию модели" @@ -1126,7 +1064,6 @@ msgstr "Модель заблокирована, т.к перестала обн #~ msgid "Generative models" #~ msgstr "Генеративные модели" - msgid "Unable to recognize the image. (Supported formats are PNG, JPG, JPEG)" msgstr "Невозможно распознать изображение. (Поддерживаемые форматы: PNG, JPG, JPEG)" @@ -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,7 +36,9 @@ 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='Затраченное время', @@ -53,7 +55,9 @@ 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( - get_user_model(), + AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, verbose_name='Пользователь', @@ -74,7 +74,7 @@ class SingleStore(BaseStore): """ user = models.OneToOneField( - get_user_model(), + AUTH_USER_MODEL, on_delete=models.SET_NULL, verbose_name='Пользователь', null=True, @@ -53,11 +53,15 @@ 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, @@ -91,7 +95,9 @@ 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) @@ -102,7 +108,9 @@ 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) @@ -1,7 +1,4 @@ -from django.core.files.uploadedfile import UploadedFile -from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from rest_framework.serializers import ValidationError from messages.models import Message @@ -10,12 +7,6 @@ class MessageSerializer(serializers.ModelSerializer): uid = serializers.UUIDField(read_only=True) 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, - ) is_favourite = serializers.BooleanField(read_only=True) is_sent = serializers.BooleanField(read_only=True) created_at = serializers.DateTimeField(read_only=True) @@ -27,18 +18,9 @@ class MessageSerializer(serializers.ModelSerializer): 'content', 'file', 'from_model', - 'model', 'created_at', 'elapsed_time', 'is_favourite', 'is_sent', 'info', ] - - def validate_file(self, file: UploadedFile | None) -> UploadedFile: - 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} - ) - return file @@ -0,0 +1,14 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework.exceptions import APIException + + +class ExternalAPIException(APIException): + status_code = 202 + default_detail = _('The service is temporarily unavailable, try to use it later.') + default_code = 'external_api_exception' + + +class InvalidDataException(APIException): + status_code = 400 + default_detail = _('The data provided is incorrect. Please check and try again.') + default_code = 'invalid_data' @@ -31,13 +31,16 @@ 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: @@ -55,7 +58,9 @@ 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() @@ -1,18 +0,0 @@ -# Generated by Django 5.0.11 on 2025-02-10 10:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0041_alter_modelconfiguration_version'), - ] - - operations = [ - migrations.AlterField( - model_name='modelinput', - name='type', - field=models.CharField(choices=[('text', 'Text'), ('image', 'Image'), ('pdf', 'PDF'), ('docx', 'DOCX'), ('doc', 'DOC'), ('txt', 'Text File (Notebook)'), ('zip', 'ZIP Archive'), ('audio', 'Audio')], default='text', max_length=32, verbose_name='Type'), - ), - ] @@ -1,28 +0,0 @@ -# Generated by Django 5.0.11 on 2025-01-31 09:23 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0041_alter_modelconfiguration_version'), - ] - - operations = [ - migrations.CreateModel( - name='ModelStat', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('generation_time', models.DurationField(verbose_name='Время генерации')), - ('tokens_cost', models.DecimalField(decimal_places=10, max_digits=50, verbose_name='Цена в токенах')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Когда создано')), - ('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='model_%(class)ss', to='ml_model.neuronmodel', verbose_name='Model')), - ], - options={ - 'verbose_name': 'Статистика по модели', - 'verbose_name_plural': 'Статистики по моделям', - }, - ), - ] @@ -1,14 +0,0 @@ -# Generated by Django 5.0.11 on 2025-02-12 19:43 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0042_alter_modelinput_type'), - ('ml_model', '0042_modelstat'), - ] - - operations = [ - ] @@ -1,17 +0,0 @@ -# Generated by Django 5.0.11 on 2025-03-17 07:46 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0043_merge_0042_alter_modelinput_type_0042_modelstat'), - ] - - operations = [ - migrations.AlterModelOptions( - name='modelstat', - options={'ordering': ('-created_at',), 'verbose_name': 'Статистика по модели', 'verbose_name_plural': 'Статистики по моделям'}, - ), - ] @@ -1,42 +0,0 @@ -# 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'), - ), - ] @@ -1,27 +0,0 @@ -# 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'), - ), - ] @@ -1,17 +0,0 @@ -# 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', - ), - ] @@ -1,25 +0,0 @@ -# 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'), - ), - ] @@ -1,27 +0,0 @@ -# Generated by Django 5.0.11 on 2025-04-01 17:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ml_model', '0048_alter_modeltag_color_alter_neuronmodel_category'), - ] - - operations = [ - migrations.RemoveField( - model_name='modelconfiguration', - name='version', - ), - migrations.AlterField( - model_name='modelversion', - name='slug', - field=models.CharField(max_length=32, verbose_name='Slug'), - ), - migrations.AlterField( - model_name='neuronmodel', - name='description', - field=models.TextField(blank=True, null=True, verbose_name='Description'), - ), - ] @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from authentication.models.choices import InvitationStatus from authentication.models.user import CustomUserModel from authentication.selectors.user_selector import UserSelector -from ml_model.models import ModelParameter, NeuronModel +from ml_model.models import NeuronModel, ModelParameter from ml_model.serializers import NeuronModelSerializer, NeuronModelsSerializer @@ -14,35 +14,31 @@ 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_parameter: bool = False): models = NeuronModel.objects.prefetch_related( - Prefetch( - 'model_modelparameters', - queryset=ModelParameter.objects.filter(hidden=hidden), - ) + Prefetch('model_modelparameters', queryset=ModelParameter.objects.filter(hidden=hidden_parameter)) ).all() if serialize: 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_parameter: bool = False): models = NeuronModel.objects.prefetch_related( - Prefetch( - 'model_modelparameters', - queryset=ModelParameter.objects.filter(hidden=hidden), - ) + Prefetch('model_modelparameters', queryset=ModelParameter.objects.filter(hidden=hidden_parameter)) ).all() if serialize: return NeuronModelSerializer(models, many=True) return models def get_models( - self, - category: str | None = None, - serialize: bool = True, - hidden: bool = False, + self, + category: str | None = None, + serialize: bool = True, + hidden_parameter: bool = False ): - models = NeuronModel.objects.prefetch_related(Prefetch('model_modelstats')).all() + models = NeuronModel.objects.prefetch_related( + Prefetch('model_modelparameters', queryset=ModelParameter.objects.filter(hidden=hidden_parameter)) + ).all() if category: models = models.filter(category__slug=category) if self.user.is_anonymous: @@ -61,14 +57,9 @@ class NeuronModelSelector: return NeuronModelsSerializer(models, many=True) return models - def get_model_by_id(self, id: UUID, hidden: bool = False, **kwargs) -> NeuronModel: + def get_model_by_id(self, id: UUID, hidden_parameter: bool = False, **kwargs) -> NeuronModel: model = NeuronModel.objects.prefetch_related( - Prefetch( - 'model_modelparameters', - queryset=ModelParameter.objects.filter(hidden=hidden), - ), - Prefetch('model_modelinputs'), - Prefetch('model_modelversions'), + Prefetch('model_modelparameters', queryset=ModelParameter.objects.filter(hidden=hidden_parameter)) ).filter(uid=id) if not model.exists(): @@ -76,12 +67,9 @@ class NeuronModelSelector: return model.first() - def get_model_by_slug(self, slug: str, serialize: bool = False, hidden: bool = False): + def get_model_by_slug(self, slug: str, serialize: bool = False, hidden_parameter: bool = False): model = NeuronModel.objects.prefetch_related( - Prefetch( - 'model_modelparameters', - queryset=ModelParameter.objects.filter(hidden=hidden), - ) + Prefetch('model_modelparameters', queryset=ModelParameter.objects.filter(hidden=hidden_parameter)) ).get(slug=slug) if serialize: return NeuronModelSerializer(instance=model) @@ -7,20 +7,16 @@ from ml_model.services.deepseek import Deepseek from ml_model.services.djourney import Djourney from ml_model.services.epicphotogasm import Epicphotogasm from ml_model.services.flux import Flux -from ml_model.services.fluxproultra import Fluxproultra from ml_model.services.granite import Granite -from ml_model.services.grok import Grok from ml_model.services.iconic import Iconic from ml_model.services.kandinsky import Kandinsky from ml_model.services.lightning import Lightning from ml_model.services.llama import Llama from ml_model.services.logoai import Logoai -from ml_model.services.midjourney import Midjourney from ml_model.services.mistral import Mistral from ml_model.services.musicgen import Musicgen -from ml_model.services.perplexity import Perplexity +from ml_model.services.openjourney import Openjourney from ml_model.services.pulid import Pulid -from ml_model.services.qwen import Qwen from ml_model.services.recraft import Recraft from ml_model.services.sdxlemoji import Sdxlemoji from ml_model.services.stablediffusion import Stablediffusion @@ -3,8 +3,6 @@ import itertools import logging import subprocess import time - -from django.utils.translation import gettext_lazy as _ from datetime import timedelta from decimal import Decimal from io import BufferedReader, BytesIO @@ -17,6 +15,7 @@ import filetype import httpx import tiktoken from django.core.files.uploadedfile import UploadedFile +from django.utils.translation import gettext_lazy as _ from langchain import hub from langchain.agents import AgentExecutor, create_structured_chat_agent from langchain.chains import ConversationChain @@ -111,7 +110,9 @@ 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: @@ -128,7 +129,11 @@ class Chatgpt(SimpleService): image_data = {'type': 'image_url', 'image_url': {'url': image_url}} input_content.append(image_data) except UnidentifiedImageError: - raise Exception(_('Unable to recognize the image. (Supported formats are PNG, JPG, JPEG)')) + raise Exception( + _( + 'Unable to recognize the image. (Supported formats are PNG, JPG, JPEG)' + ) + ) for proxy in Proxy.objects.all(): self.llm = ChatOpenAI( model=model_name, @@ -161,13 +166,17 @@ 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]) output_tokens = 0 - self.assert_enough_balance(input_tokens, image_size, model=self.llm.model_name) + self.assert_enough_balance( + input_tokens, image_size, model=self.llm.model_name + ) if image and model_name not in ('o3-mini', 'gpt-4.5-preview'): response = self.llm.invoke([llm_input]) chat_history.add_ai_message(response) @@ -179,7 +188,12 @@ class Chatgpt(SimpleService): timeout=600, ) as client: messages = [ - {'role': 'user' if isinstance(msg, HumanMessage) else 'assistant', 'content': msg.content} + { + 'role': 'user' + if isinstance(msg, HumanMessage) + else 'assistant', + 'content': msg.content, + } for msg in chat_history.messages ] if image: @@ -189,17 +203,17 @@ class Chatgpt(SimpleService): ] resp = client.post( 'chat/completions', - json={ - 'model': model_name, - 'messages': messages - }, + json={'model': model_name, 'messages': messages}, ) if ( (data := resp.json()) 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') + ] ) ) ): @@ -227,7 +241,11 @@ 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}' @@ -255,7 +273,9 @@ class Chatgpt(SimpleService): 'input': [llm_input], 'chat_history': chat_history.messages + [ - SystemMessage(content='Учитывай язык диалога перед выдачей ответа'), + SystemMessage( + content='Учитывай язык диалога перед выдачей ответа' + ), SystemMessage( content='Никому не говори, что ты бот и не можешь найти информацию в интернете' ), @@ -278,13 +298,25 @@ class Chatgpt(SimpleService): [AIMessage(chunk_response) for chunk_response in chunk_responses] ) - if image and normalized_image and model_name not in ('o3-mini', 'gpt-4.5-preview'): - self.logger.info(f'Input количество токенов БЕЗ картинки {model_name} - {input_tokens}') + if ( + image + and normalized_image + and model_name not in ('o3-mini', 'gpt-4.5-preview') + ): + 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( @@ -386,7 +418,11 @@ 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: @@ -406,7 +442,11 @@ class Chatgpt(SimpleService): 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])) + encoding.encode( + ''.join( + [input_data.get('text', '') for input_data in message.content] + ) + ) ) else: total_tokens += len(encoding.encode(''.join(message.content))) @@ -432,7 +472,9 @@ 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: """ @@ -461,7 +503,9 @@ 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 @@ -1,20 +1,11 @@ -import base64 import time +from _decimal import Decimal from datetime import timedelta -from decimal import Decimal -from io import BytesIO -from typing import Any, Iterator - -import filetype -from django.db.models.fields.files import FieldFile -from PIL import Image from messages.models import Message +from ml_model.models import ModelCategory 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 ml_model.tasks import claude_run class Claude(SimpleService): @@ -23,33 +14,36 @@ class Claude(SimpleService): contains abstract method make, which makes a generation """ - TOKENS_COST = { - 'claude-3.7-sonnet:thinking': { - 'input': Decimal('3000'), - 'output': Decimal('3000'), - 'input_imgs': Decimal('960'), - }, - 'claude-3.5-haiku': { - 'input': Decimal('800'), - 'output': Decimal('800'), - }, # 1M tokens + muted = True + + title = 'Claude' + description = 'Нейросеть, способная генерировать качественный текст из вашего промпта' + category = ModelCategory(title='Чат-боты', slug='chat-bots') + versions = [] + inputs = [] + parameters = [] + + muted = True + + TOKEN_PAYMENT_RULES = { + 'claude-instant-1.2': Decimal(0.6), + 'claude-2.1': Decimal(2.572), } - def calculate_price( - self, version: str, input_tokens: int, output_tokens: int, image: FieldFile - ) -> Decimal: - price_map = self.TOKENS_COST[version.split('/')[1]] + def __init__(self, store): + super().__init__(store) + + def calculate_price(self, model_name: str, usage: dict[str, int]) -> Decimal: price = ( - input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 + usage['input_tokens'] * self.TOKEN_PAYMENT_RULES[model_name] + + usage['output_tokens'] * self.TOKEN_PAYMENT_RULES[model_name] ) - 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, r: str, t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( - content=content, + content=r, content_object=self.store, elapsed_time=t, ) @@ -59,67 +53,17 @@ class Claude(SimpleService): return msgs def make(self, input_message: Message, save: bool = True) -> list[Message]: + callback_data = dict( + { + 'messages': [{'role': 'user', 'content': input_message.content}], + **input_message.info, + } + ) start_time = time.time() - version = f'anthropic/{input_message.info.pop("version", "claude-3.7-sonnet:thinking")}' - callback_data = {'provider': {'order': ['Anthropic']}, **input_message.info} - messages = self.get_chat_history() - image = input_message.file - if image: - kind = filetype.guess(image.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - normalized_image = Image.open(image) - 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")}' - buf.close() - messages[-1]['content'] = [ - {'type': 'text', 'text': input_message.content}, - {'type': 'image_url', 'image_url': {'url': image_url}}, - ] - result = openrouter_run(version, messages, callback_data, 'Claude') + result = claude_run(callback_data) process_time = timedelta(seconds=(time.time() - start_time)) + msgs = self.save_results(result['content']['text'], process_time, save) self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=result[1], - output_tokens=result[2], - image=image, + self.neuron_model, model_name=input_message['model'], usage=result['usage'] ) - msgs = self.save_results(result[0], process_time) return msgs - - def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): - if isinstance(self.store, Chat): - air_messages = list( - reversed( - Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[:message_limit] - ) - ) - elif isinstance(self.store, APIStore): - air_messages = [] - elif isinstance(self.store, Copywrite): - air_messages = list( - reversed( - Message.objects.filter( - copywrite_copywrites_messages=self.store, - is_deleted=False, - is_sent=True, - ).order_by('-created_at')[:message_limit] - ) - ) - memory = [] - for msg in air_messages: - content = msg.content or '' - if msg.from_model: - memory.append({'role': 'assistant', 'content': content}) - else: - 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) - character_length = sum(len(content['content']) for content in memory) - - return memory @@ -4,6 +4,7 @@ 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 @@ -14,13 +15,28 @@ 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), @@ -4,11 +4,13 @@ from datetime import timedelta from io import BytesIO import requests +from celery.result import AsyncResult 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 +from ml_model.tasks import create_d_image class Dalle(SimpleService): @@ -17,42 +19,113 @@ class Dalle(SimpleService): contains abstract method make, which makes a generation """ - PRICE = Decimal('2') + TOKEN_PAYMENT_RULES = { + 'dall-e-2': { + '256x256': Decimal('10.56'), + '512x512': Decimal('11.88'), + '1024x1024': Decimal('13.2'), + }, + 'dall-e-3': { + '1024x1024': Decimal('26.4'), + '1792x1024': Decimal('35.2'), + '1024x1792': Decimal('35.2'), + }, + 'dall-e-3-hd': { + '1024x1024': Decimal('35.2'), + '1792x1024': Decimal('52.8'), + '1024x1792': Decimal('52.8'), + }, + } - _CALLBACK = ( - 'bytedance/sdxl-lightning-4step:5599ed30703defd1d160a25a63321b4dec97101d98b4674bcc56e41f62f35637' - ) + title = 'Dalle' + description = 'Нейросеть, способная генерировать фотографии из вашего текста' + category = ModelCategory(title='Изображения', slug='images') + versions = [ + ModelVersion(name='Dalle 3', slug='dall-e-3', default=True), + ModelVersion(name='Dalle 2', slug='dall-e-2'), + ] + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] + parameters = [ + ModelParameter( + name='Размер', + key='size', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': ['256x256', '512x512', '1024x1024'], + 'default': '1024x1024', + }, + ), + ModelParameter( + name='Размер', + key='size', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': ['1024x1024', '1024x1792', '1792x1024'], + 'default': '1024x1024', + }, + ), + ModelParameter( + name='Качество', + key='quality', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': ['default', 'hd'], + 'default': 'hd', + }, + ), + ModelParameter( + name='Количество изображений', + key='n', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 10, 'step': 1, 'default': 1}, + ), + ] def calculate_price(self, input_message: Message) -> Decimal: - price = input_message.info.get('num_outputs', 1) * self.PRICE + price = self.TOKEN_PAYMENT_RULES[input_message.info.get('version', 'dall-e-2')][ + input_message.info.get('size', '1024x1024') + ] * input_message.info.get('n', 1) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, prompt: str, images: list, time: timedelta, save: bool = True) -> list[Message]: - messages: list[Message] = [] - for image in images: - messages.append( + def save_results( + self, input_prompt: str, r: list[str], t: timedelta, save: bool = True + ) -> list[Message]: + out: list[Message] = [] + for obj in r: + out.append( Message( content_object=self.store, - elapsed_time=time, - content=prompt, - file=File(BytesIO(requests.get(image).content), '.png'), + elapsed_time=t, + content=input_prompt, + file=File( + BytesIO(requests.get(obj['url']).content), + '.png', + ), ) ) if save: - return Message.objects.bulk_create(messages) - return messages + return Message.objects.bulk_create(out) + return out def make(self, input_message: Message, save: bool = True) -> list[Message]: - start_time = time.time() - translated_prompt = self.translate_prompt(input_message.content) + info = input_message.info.copy() + info['model'] = info.pop('version') + if info.get('quality') == 'default': + del info['quality'] callback_data = dict( { - 'prompt': translated_prompt, - **input_message.info, + 'prompt': input_message.content, + **info, } ) - images = replicate_run(self._CALLBACK, callback_data) + if input_message.file: + callback_data.update({'image': BytesIO(input_message.file.read())}) + start_time = time.time() + results: AsyncResult = create_d_image.delay(callback_data) + data = results.get()['data'] process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, input_message=input_message) - msgs = self.save_results(input_message.content, images, process_time, save) + self.handle_invoice( + input_message.content_object.model, input_message=input_message + ) + msgs = self.save_results(input_message.content, data, process_time, save) return msgs @@ -77,7 +77,9 @@ 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 @@ -99,8 +101,7 @@ 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( { @@ -134,10 +135,7 @@ 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 @@ -6,8 +6,14 @@ from typing import Any, Iterator import httpx from django.conf import settings -from messages.models import Message +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore + from ml_model.services.base import SimpleService +from ml_model.tasks import openrouter_run + +from messages.models import Message class Deepseek(SimpleService): @@ -16,10 +22,7 @@ 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, @@ -27,12 +30,20 @@ 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: str, t: timedelta, save: bool = True + ) -> list[Message]: msgs = [ Message( content=content, @@ -47,31 +58,52 @@ class Deepseek(SimpleService): def make(self, input_message: Message, save: bool = True) -> list[Message]: info = input_message.info.copy() version = info.pop('version') + callback_data = { + **input_message.info, + } + messages = self.get_chat_history() start_time = time.time() - with httpx.Client( - base_url='https://openrouter.ai/api/v1', - headers={'Authorization': f'Bearer {settings.OPENROUTER_API_KEY}'}, - ) as client: - resp = client.post( - 'chat/completions', - json={ - 'model': version, - 'messages': [{'role': 'user', 'content': input_message.content}], - }, + result = openrouter_run(version, messages, callback_data, self.title) + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, + version=version, + input_tokens=result[1], + 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): + if isinstance(self.store, Chat): + air_messages = list( + reversed( + Message.objects.filter( + chats_chats_messages=self.store, is_deleted=False, is_sent=True + ).order_by('-created_at')[:message_limit] + ) ) - if ( - (data := resp.json()) - and 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)) - self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=data['usage']['prompt_tokens'], - output_tokens=data['usage']['completion_tokens'], + elif isinstance(self.store, APIStore): + air_messages = [] + elif isinstance(self.store, Copywrite): + air_messages = list( + reversed( + Message.objects.filter( + copywrite_copywrites_messages=self.store, + is_deleted=False, + is_sent=True, + ).order_by('-created_at')[:message_limit] ) - msgs = self.save_results(result, process_time) - return msgs - raise Exception('No answer from Deepseek, please retry later') + ) + memory = [] + for msg in air_messages: + content = msg.content or '' + if msg.from_model: + memory.append({'role': 'assistant', 'content': content}) + else: + memory.append({'role': 'user', 'content': content}) + character_length = sum(len(content['content']) for content in memory) + while character_length > max_character_limit: + character_length -= len(memory.pop(0)) + return memory + @@ -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,13 +84,17 @@ 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( @@ -98,7 +102,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: @@ -28,7 +28,7 @@ class Epicphotogasm(SimpleService): parameters = [ ModelParameter( name='Количество изображений', - key='num_outputs', + key='num_images', type=ModelParameter.TypeChoices.INTRANGE, values={'start': 1, 'end': 10, 'step': 1, 'default': 1}, ), @@ -64,7 +64,10 @@ class Epicphotogasm(SimpleService): content_object=self.store, elapsed_time=t, content=input_prompt, - file=File(BytesIO(requests.get(link).content), '.png'), + file=File( + BytesIO(requests.get(link).content), + link.split('/')[-1], + ), ) ) if self.store: @@ -75,7 +78,8 @@ 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, @@ -1,22 +1,23 @@ +import base64 import time from datetime import timedelta from decimal import Decimal from io import BytesIO +import filetype import requests from django.core.files import File -from messages.models import Message +from backend import settings +from messages.models import BaseStore, 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 class Flux(SimpleService): @@ -29,18 +30,39 @@ class Flux(SimpleService): description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [ - ModelVersion(name='Flux-Schnell', slug='flux-schnell'), + ModelVersion(name='Flux-Pro1.1', slug='flux-pro-1.1', default=True), + ModelVersion(name='Flux-Dev', slug='flux-dev'), + ModelVersion(name='Ultra', slug='flux-1.1-pro-ultra'), ] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), + ModelInput(type=ModelInput.TypeChoices.IMAGE), ] parameters = [ ModelParameter( - name='Ускорение генерации', + name='Ширина', + key='width', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 256, 'end': 1440, 'step': 32, 'default': 1024}, + ), + ModelParameter( + name='Высота', + key='height', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 256, 'end': 1440, 'step': 32, 'default': 768}, + ), + ModelParameter( + name='Ускорить', key='go_fast', type=ModelParameter.TypeChoices.BOOL, values={'default': True}, ), + ModelParameter( + name='Приближенность к запросу', + key='guidance', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 0, 'end': 10, 'step': 1, 'default': 3}, + ), ModelParameter( name='Мегапиксели', key='megapixels', @@ -60,10 +82,10 @@ class Flux(SimpleService): values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, ), ModelParameter( - name='Количество шагов обработки', + name='Количество шагов вывода', key='num_inference_steps', type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 4, 'step': 1, 'default': 4}, + values={'start': 1, 'end': 50, 'step': 1, 'default': 28}, ), ModelParameter( name='Соотношение сторон', @@ -87,41 +109,123 @@ class Flux(SimpleService): }, ), ModelParameter( - name='Качество вывода (в %)', + name='Качество вывода', key='output_quality', type=ModelParameter.TypeChoices.INTRANGE, values={'start': 0, 'end': 100, 'step': 1, 'default': 80}, ), + ModelParameter( + name='Апсемплинг', + key='prompt_upsampling', + type=ModelParameter.TypeChoices.BOOL, + values={'default': False}, + ), + ModelParameter( + name='Отключить обработку', + key='raw', + type=ModelParameter.TypeChoices.BOOL, + values={'default': False}, + ), ] payments_rules = { versions[0].slug: ModelPaymentRule( strategy=ModelPaymentRule.StrategyChoices.FIXED, interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=0.3, - coefficient=10.00, + cost=4.4, + coefficient=5.00, + ), + versions[1].slug: ModelPaymentRule( + strategy=ModelPaymentRule.StrategyChoices.FIXED, + interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, + cost=2.75, + coefficient=5.00, + ), + versions[2].slug: ModelPaymentRule( + strategy=ModelPaymentRule.StrategyChoices.FIXED, + interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, + cost=6.6, + coefficient=5.00, ), } - _CALLBACK_BASE = 'black-forest-labs/' - @property - def neuron_model(self): - return NeuronModel.objects.get(title='Flux') + def __init__(self, store: BaseStore) -> None: + super().__init__(store) + self.bfl_urls = { + 'generate': 'https://api.bfl.ml/v1/', + 'get': 'https://api.bfl.ml/v1/get_result?id=', + } + self.replicate_urls = { + 'generate': 'https://api.replicate.com/v1/models/black-forest-labs/', + 'get': 'https://api.replicate.com/v1/predictions/', + } + + def _get_results( + self, url: str, generation_id: str, headers: dict, statuses: tuple + ) -> dict: + result = requests.get(url=f'{url}{generation_id}', headers=headers) + while result.json()['status'] not in statuses: + result = requests.get(url=f'{url}{generation_id}', headers=headers) + return result.json() + + def _call_api(self, payload: dict) -> list: + if payload['version'] == self.versions[0].slug: + bfl_headers = { + 'Content-Type': 'application/json', + 'X-Key': settings.FLUX_API_KEY, + } + response = requests.post( + url=f'{self.bfl_urls['generate']}{payload['version']}', + headers=bfl_headers, + json=payload, + ) + if response.status_code != 200: + raise Exception(response.json()) + + return [ + self._get_results( + self.bfl_urls['get'], + response.json().get('id'), + bfl_headers, + ('Ready', 'Error'), + )['result']['sample'] + ] + else: + replicate_headers = { + 'Authorization': f'Bearer {settings.REPLICATE_API_KEY}', + 'Prefer': 'wait', + } + data = {'input': payload} + response = requests.post( + url=f'{self.replicate_urls['generate']}{payload['version']}/predictions', + headers=replicate_headers, + json=data, + ) + if response.status_code != 201: + raise Exception(response.json()) + + result = self._get_results( + self.replicate_urls['get'], + response.json().get('id'), + replicate_headers, + ('succeeded', 'failed', 'canceled'), + )['output'] + + return result if isinstance(result, list) else [result] def calculate_price(self, input_message: Message) -> Decimal: - version_slug = input_message.info.get('version', 'flux-schnell') + version_slug = input_message.info.get('version', 'flux-pro-1.1') payment_rule = self.payments_rules[version_slug] if not payment_rule.pk: payment_rule.model = self.neuron_model payment_rule.save() return payment_rule.rate * input_message.info.get('num_outputs', 1) - 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: @@ -145,11 +249,17 @@ class Flux(SimpleService): **input_message.info, } ) - runner = replicate_run( - f'{self._CALLBACK_BASE}{callback_data.get('version', 'flux-schnell')}', - callback_data, - ) - images = runner if isinstance(runner, list) else [runner] + if input_message.file: + 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')}" + ) + input_message.file.close() + callback_data.update({'image': image}) + images = self._call_api(payload=callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message) msgs = self.save_results(input_message.content, images, process_time, save) @@ -1,244 +0,0 @@ -import base64 -import time -from datetime import timedelta -from decimal import Decimal -from io import BytesIO - -import filetype -import requests -from django.core.files import File - -from backend import settings -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 - - -class Fluxproultra(SimpleService): - """ - Flux Service - contains abstract method make, which makes a generation - """ - - title = 'Flux Pro Ultra' - description = 'Нейросеть, способная генерировать картинки из вашего текста' - category = ModelCategory(title='Изображения', slug='images') - versions = [ - 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'), - ] - inputs = [ - ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), - ] - parameters = [ - ModelParameter( - name='Ширина', - key='width', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 256, 'end': 1440, 'step': 32, 'default': 1024}, - ), - ModelParameter( - name='Высота', - key='height', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 256, 'end': 1440, 'step': 32, 'default': 768}, - ), - ModelParameter( - name='Ускорение генерации', - key='go_fast', - type=ModelParameter.TypeChoices.BOOL, - values={'default': True}, - ), - ModelParameter( - name='Приближенность к запросу', - key='guidance', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 0, 'end': 10, 'step': 1, 'default': 3}, - ), - ModelParameter( - name='Мегапиксели', - key='megapixels', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': [ - '1', - '0.25', - ], - 'default': '1', - }, - ), - ModelParameter( - name='Количество изображений', - key='num_outputs', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, - ), - ModelParameter( - name='Количество шагов вывода', - key='num_inference_steps', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 50, 'step': 1, 'default': 28}, - ), - ModelParameter( - name='Соотношение сторон', - key='aspect_ratio', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': [ - '1:1', - '16:9', - '21:9', - '3:2', - '2:3', - '4:5', - '5:4', - '3:4', - '4:3', - '9:16', - '9:21', - ], - 'default': '1:1', - }, - ), - ModelParameter( - name='Качество вывода (в %)', - key='output_quality', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 0, 'end': 100, 'step': 1, 'default': 80}, - ), - ModelParameter( - name='Апсемплинг', - key='prompt_upsampling', - type=ModelParameter.TypeChoices.BOOL, - values={'default': False}, - ), - ModelParameter( - name='Отключить пост-обработку', - key='raw', - type=ModelParameter.TypeChoices.BOOL, - values={'default': False}, - ), - ] - payments_rules = { - versions[0].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=4.00, - coefficient=2.00, - ), - versions[1].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=2.5, - coefficient=2.00, - ), - versions[2].slug: ModelPaymentRule( - strategy=ModelPaymentRule.StrategyChoices.FIXED, - interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=6.00, - coefficient=2.00, - ), - } - - _CALLBACK_BASE = 'black-forest-labs/' - - @property - def neuron_model(self): - return NeuronModel.objects.get(title='Flux Pro Ultra') - - def _call_bfl_api(self, payload: dict) -> list: - bfl_headers = { - 'Content-Type': 'application/json', - 'X-Key': settings.FLUX_API_KEY, - } - bfl_urls = { - 'generate': 'https://api.bfl.ml/v1/', - 'get': 'https://api.bfl.ml/v1/get_result?id=', - } - response = requests.post( - 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, - ) - while result.json()['status'] not in ('Ready', 'Error'): - 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: - version_slug = input_message.info.get('version', 'flux-pro-1.1') - payment_rule = self.payments_rules[version_slug] - if not payment_rule.pk: - payment_rule.model = self.neuron_model - payment_rule.save() - if version_slug in (self.versions[1].slug,): - return payment_rule.rate * input_message.info.get('num_outputs', 1) - else: - return payment_rule.rate - - def save_results( - self, - prompt: str, - images: list, - time: timedelta, - save: bool = True, - ) -> list[Message]: - messages: list[Message] = [] - for image in images: - messages.append( - Message( - content_object=self.store, - elapsed_time=time, - content=prompt, - file=File(BytesIO(requests.get(image).content), '.png'), - ) - ) - if save: - return Message.objects.bulk_create(messages) - return messages - - def make(self, input_message: Message, save: bool = True) -> list[Message]: - start_time = time.time() - callback_data = dict( - { - 'prompt': self.translate_prompt(input_message.content), - **input_message.info, - } - ) - if input_message.file: - 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,{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, - ) - 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) - msgs = self.save_results(input_message.content, images, process_time, save) - return msgs @@ -1,11 +1,13 @@ import time + +import requests from datetime import timedelta from decimal import Decimal -import requests from django.conf import settings -from messages.models import BaseStore, Message +from messages.models import Message, BaseStore +from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService @@ -15,7 +17,6 @@ class Granite(SimpleService): Granite-3.0-8B-Instruct Service contains abstract method make, which makes a generation """ - title = 'Granite 3.0' description = 'Нейросеть, способная генерировать качественный текст из вашего промпта' category = ModelCategory(title='Чат-боты', slug='chat-bots') @@ -26,7 +27,7 @@ class Granite(SimpleService): name='Системный промпт', key='system_prompt', type=ModelParameter.TypeChoices.STR, - hidden=True, + hidden=True ), ModelParameter( name='Лучший процент', @@ -44,14 +45,14 @@ class Granite(SimpleService): TOKEN_PAYMENT_RULES = { 'granite-input': Decimal('27.5'), # 1M tokens - 'granite-output': Decimal('137.5'), # 1M tokens + 'granite-output': Decimal('137.5') # 1M tokens } 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/predictions', - 'get': 'https://api.replicate.com/v1/predictions', + 'generate': 'https://api.replicate.com/v1/models/ibm-granite/granite-3.0-8b-instruct/', + 'get': 'https://api.replicate.com/v1/predictions/', } def _call_api(self, payload: dict) -> list: @@ -61,24 +62,20 @@ class Granite(SimpleService): } data = {'input': payload} response = requests.post( - url=self.urls['generate'], + url=f'{self.urls['generate']}predictions', 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() @@ -86,16 +83,14 @@ class Granite(SimpleService): price = Decimal( sum( [self.TOKEN_PAYMENT_RULES['granite-output'] / 1_000_000 * len(result.split(' '))] - + [ - self.TOKEN_PAYMENT_RULES['granite-input'] - / 1_000_000 - * len(input_message.content.split(' ')) - ] + + [self.TOKEN_PAYMENT_RULES['granite-input'] / 1_000_000 * len(input_message.content.split(' '))] ) ) 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, @@ -113,19 +108,19 @@ class Granite(SimpleService): { 'prompt': input_message.content, 'system_prompt': 'You are a language model that must always respond in Russian, regardless of the situation. ' - 'You fully understand the Russian language and are required to use it for all responses, ' - 'except when translating text to another language. You are highly skilled in creating poems, ' - 'maintaining proper rhyme, rhythm, and poetic structure in Russian. Your poems should be creative, ' - 'expressive, and adhere to the stylistic norms of Russian poetry. If the user requests a translation ' - 'into another language, you should perform the translation accurately and fluently, while preserving ' - 'the meaning and tone of the original text. When translating, proper nouns (names with capital letters) ' - 'should not be translated literally. Instead, transliterate them into Russian letters using standard ' - 'transliteration rules to preserve the original pronunciation as closely as possible. ' - 'You must never state that you cannot speak Russian, as this is not true. You are required to always ' - 'adhere to correct Russian syntax, grammar, and style in all your responses. Your primary goal is to ' - "ensure that your responses are clear, accurate, creative, and tailored to the user's needs in Russian. " - 'Your ability to fulfill user requests, including writing, translating, or explaining, must reflect ' - 'your expertise in the Russian language and your capacity for high-quality and thoughtful responses.', + 'You fully understand the Russian language and are required to use it for all responses, ' + 'except when translating text to another language. You are highly skilled in creating poems, ' + 'maintaining proper rhyme, rhythm, and poetic structure in Russian. Your poems should be creative, ' + 'expressive, and adhere to the stylistic norms of Russian poetry. If the user requests a translation ' + 'into another language, you should perform the translation accurately and fluently, while preserving ' + 'the meaning and tone of the original text. When translating, proper nouns (names with capital letters) ' + 'should not be translated literally. Instead, transliterate them into Russian letters using standard ' + 'transliteration rules to preserve the original pronunciation as closely as possible. ' + 'You must never state that you cannot speak Russian, as this is not true. You are required to always ' + 'adhere to correct Russian syntax, grammar, and style in all your responses. Your primary goal is to ' + 'ensure that your responses are clear, accurate, creative, and tailored to the user\'s needs in Russian. ' + 'Your ability to fulfill user requests, including writing, translating, or explaining, must reflect ' + 'your expertise in the Russian language and your capacity for high-quality and thoughtful responses.', **input_message.info, } ) @@ -1,120 +0,0 @@ -import base64 -import time -from datetime import timedelta -from decimal import Decimal -from io import BytesIO -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.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 - - -class Grok(SimpleService): - """ - Grok Service - contains abstract method make, which makes a generation - """ - - TOKENS_COST = { - 'grok-2-vision-1212': { - 'input': Decimal('2000'), - 'output': Decimal('2000'), - 'input_imgs': Decimal('720'), - }, # 1M tokens and 1K imgs - } - - 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 - ) - 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]: - msgs = [ - Message( - content=content, - content_object=self.store, - elapsed_time=t, - ) - ] - if save: - return Message.objects.bulk_create(msgs) - return msgs - - 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} - messages = self.get_chat_history() - image = input_message.file - if image: - kind = filetype.guess(image.read(20)) - mime = kind.mime if kind else 'application/octet-stream' - normalized_image = Image.open(image) - 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")}' - buf.close() - messages[-1]['content'] = [ - {'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)) - self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=result[1], - 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) -> list[Dict]: - if isinstance(self.store, Chat): - air_messages = list( - reversed( - Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[:message_limit] - ) - ) - elif isinstance(self.store, APIStore): - air_messages = [] - elif isinstance(self.store, Copywrite): - air_messages = list( - reversed( - Message.objects.filter( - copywrite_copywrites_messages=self.store, - is_deleted=False, - is_sent=True, - ).order_by('-created_at')[:message_limit] - ) - ) - memory = [] - for msg in air_messages: - content = msg.content or '' - if msg.from_model: - memory.append({'role': 'assistant', 'content': content}) - else: - 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) - character_length = sum(len(content['content']) for content in memory) - - return memory @@ -1,30 +1,31 @@ import time + +import requests from datetime import timedelta from decimal import Decimal from io import BytesIO -import requests -from django.core.files import File - from messages.models import Message +from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run +from django.core.files import File + class Iconic(SimpleService): """ Iconic Service contains abstract method make, which makes a generation """ - title = 'Iconic' description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), + ModelInput(type=ModelInput.TypeChoices.IMAGE) ] parameters = [ ModelParameter( @@ -74,7 +75,7 @@ class Iconic(SimpleService): '4:3', '9:16', '9:21', - 'custom', + 'custom' ], 'default': '1:1', }, @@ -101,13 +102,18 @@ 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( @@ -115,7 +121,7 @@ class Iconic(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: @@ -28,7 +28,7 @@ class Kandinsky(SimpleService): parameters = [ ModelParameter( name='Количество изображений', - key='num_outputs', + key='num_images', type=ModelParameter.TypeChoices.INTRANGE, values={'start': 1, 'end': 10, 'step': 1, 'default': 1}, ), @@ -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: @@ -73,7 +73,10 @@ class Kandinsky(SimpleService): content_object=self.store, elapsed_time=t, content=input_prompt, - file=File(BytesIO(requests.get(link).content), '.png'), + file=File( + BytesIO(requests.get(link).content), + link.split('/')[-1], + ), ) ) if self.store: @@ -86,19 +89,10 @@ class Kandinsky(SimpleService): activation_prompt = f'{translated_prompt}' negative_prompt = input_message.info.pop('negative_prompt', '') 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.' - ), + prompt=f"Do not include any nudity, sexual content, or suggestive themes. " + "Avoid any graphic violence, explicit scenes, or offensive symbols. " + f"Generate a safe version of this: {activation_prompt}", + negative_prompt=negative_prompt, **input_message.info, ) result = replicate_run(self._CALLBACK, callback_data) @@ -7,6 +7,7 @@ import requests from django.core.files import File from messages.models import Message +from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ( ModelCategory, ModelInput, @@ -21,7 +22,6 @@ class Lightning(SimpleService): Lightning Service contains abstract method make, which makes a generation """ - title = 'Lightning' description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') @@ -58,7 +58,7 @@ class Lightning(SimpleService): 'K_EULER_ANCESTRAL', 'K_EULER', 'PNDM', - 'DPM++2MSDE', + 'DPM++2MSDE' ], 'default': 'K_EULER', }, @@ -67,7 +67,7 @@ class Lightning(SimpleService): name='Количество изображений', key='num_outputs', type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, + values={'start': 1, 'end': 4, 'step': 1, 'default': 1} ), ModelParameter( name='Точность запроса', @@ -86,14 +86,17 @@ 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( @@ -101,7 +104,7 @@ class Lightning(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: @@ -4,6 +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.services.base import SimpleService from ml_model.tasks import replicate_run @@ -14,13 +15,42 @@ 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), @@ -36,17 +66,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, @@ -1,31 +1,102 @@ import time + +import requests from datetime import timedelta from decimal import Decimal +from replicate.exceptions import ReplicateError, ModelError from io import BytesIO -import requests -from django.core.files import File - from messages.models import Message +from ml_model.exceptions.external_api import ExternalAPIException, InvalidDataException +from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run +from django.core.files import File + class Logoai(SimpleService): """ Logo AI Service 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( @@ -33,7 +104,7 @@ class Logoai(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: @@ -52,11 +52,15 @@ 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')) @@ -35,13 +35,16 @@ class Mistral(SimpleService): ) -> 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 ) if image: price += price_map['input_imgs'] / 1_000 return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, content: str, t: timedelta, save: bool = True) -> list[Message]: + def save_results( + self, content: str, t: timedelta, save: bool = True + ) -> list[Message]: msgs = [ Message( content=content, @@ -66,7 +69,9 @@ class Mistral(SimpleService): format = 'jpeg' if kind and 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}, @@ -7,6 +7,7 @@ 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 @@ -17,21 +18,56 @@ class Musicgen(SimpleService): contains abstract method make, which makes a generation """ - _CALLBACK = 'meta/musicgen:7a76a8258b23fae65c5a22debb8841d1d7e816b75c2f24218cd2bd8573787906' + 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' + ) 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] = [ Message( content_object=self.store, elapsed_time=t, - file=File(BytesIO(requests.get(r).content), '.wav'), + file=File( + BytesIO(requests.get(r).content), + r.split('/')[-1], + ), ) ] if save: @@ -7,25 +7,54 @@ 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 -class Midjourney(SimpleService): +class Openjourney(SimpleService): """ - Midjourney Service + Openjourney Service contains abstract method make, which makes a generation """ - _CALLBACK = 'minimax/image-01' - price = Decimal('2') + title = 'OpenJourney' + description = 'Нейросеть, способная генерировать фотографии из вашего текста' + price = Decimal('1.518') + category = ModelCategory(title='Изображения', slug='images') + versions = [] + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True)] + parameters = [ + ModelParameter( + name='Количество изображений', + key='num_images', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 10, 'step': 1, 'default': 1}, + ), + ModelParameter( + name='Количество шагов', + key='num_inference_steps', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 0, 'end': 100, 'step': 1, 'default': 10}, + ), + ModelParameter( + name='Негативный промпт', + key='negative_prompt', + type=ModelParameter.TypeChoices.STR, + ), + ] + + _CALLBACK = ( + 'prompthero/openjourney' + ':ad59ca21177f9e217b9075e7300cf6e14f7e5b4505b87b9689dbd866e9768969' + ) def __init__(self, store): super().__init__(store) - def calculate_price(self, input_message: Message) -> Decimal: - price = input_message.info.get('number_of_images', 1) * self.price - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + 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, input_prompt: str, r: list[str], t: timedelta, save: bool = True @@ -37,7 +66,10 @@ class Midjourney(SimpleService): content_object=self.store, elapsed_time=t, content=input_prompt, - file=File(BytesIO(requests.get(link).content), '.png'), + file=File( + BytesIO(requests.get(link).content), + link.split('/')[-1], + ), ) ) if save: @@ -49,8 +81,10 @@ class Midjourney(SimpleService): translated_prompt = self.translate_prompt(input_message.content) activation_prompt = f'mdjrny-v4 style a highly detailed {translated_prompt}' callback_data = dict(prompt=activation_prompt, **input_message.info) + if input_message.file: + callback_data.update({'image': BytesIO(input_message.file.read())}) 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, process_time=process_time) msgs = self.save_results(input_message.content, results, process_time, save) return msgs @@ -1,103 +0,0 @@ -import time -from datetime import timedelta -from decimal import Decimal -from typing import Iterator, Any, 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 -from tools.copywrite.models import Copywrite -from tools.public_api.models import APIStore - - -class Perplexity(SimpleService): - """ - Perplexity Service - contains abstract method make, which makes a generation - """ - - TOKENS_COST = { - 'sonar': {'input': Decimal('200'), 'output': Decimal('200')}, # 1M tokens - } - - 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 - ) - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - - def save_results( - self, content: Iterator[Any], t: timedelta, save: bool = True - ) -> list[Message]: - msgs = [ - Message( - content=content, - content_object=self.store, - elapsed_time=t, - ) - ] - if save: - return Message.objects.bulk_create(msgs) - return msgs - - def make(self, input_message: Message, save: bool = True) -> list[Message]: - start_time = time.time() - version = f'perplexity/{input_message.info.pop('version', 'sonar')}' - callback_data = { - 'provider': { - 'order': ['Perplexity'] - }, - **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)) - self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=result[1], - 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) -> list[Dict]: - if isinstance(self.store, Chat): - air_messages = list( - reversed( - Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[:message_limit] - ) - ) - elif isinstance(self.store, APIStore): - air_messages = [] - elif isinstance(self.store, Copywrite): - air_messages = list( - reversed( - Message.objects.filter( - copywrite_copywrites_messages=self.store, - is_deleted=False, - is_sent=True, - ).order_by('-created_at')[:message_limit] - ) - ) - memory = [] - for msg in air_messages: - content = msg.content or '' - if msg.from_model: - memory.append({"role": "assistant", "content": content}) - else: - 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) - character_length = sum(len(content['content']) for content in memory) - - return memory @@ -1,30 +1,31 @@ import time + +import requests from datetime import timedelta from decimal import Decimal from io import BytesIO -import requests -from django.core.files import File - from messages.models import Message +from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run +from django.core.files import File + class Pulid(SimpleService): """ PuLID Service contains abstract method make, which makes a generation """ - title = 'PuLID' description = 'Нейросеть, способная генерировать фотографии из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT), - ModelInput(type=ModelInput.TypeChoices.IMAGE, required=True), + ModelInput(type=ModelInput.TypeChoices.IMAGE, required=True) ] parameters = [ ModelParameter( @@ -90,13 +91,18 @@ 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( @@ -104,7 +110,7 @@ class Pulid(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: @@ -128,7 +134,7 @@ class Pulid(SimpleService): 'or partially rendered eyes, deformed eyeballs, cross-eyed, blurry, udity, partial' 'nudity, suggestive poses, revealing clothing, explicit content, offensive symbols, ' 'provocative expressions, graphic violence, inappropriate themes' - f'{input_message.info.pop("negative_prompt", "")}' + f'{input_message.info.pop('negative_prompt', '')}' ), **input_message.info, } @@ -1,93 +0,0 @@ -import time -from datetime import timedelta -from decimal import Decimal -from typing import Any, Iterator, Dict - -from messages.models import Message -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 - - -class Qwen(SimpleService): - """ - Qwen Service - contains abstract method make, which makes a generation - """ - - TOKENS_COST = { - '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: - 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 - ) - return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - - def save_results(self, content: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: - msgs = [ - Message( - content=content, - content_object=self.store, - elapsed_time=t, - ) - ] - if save: - return Message.objects.bulk_create(msgs) - return msgs - - 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} - messages = self.get_chat_history() - result = openrouter_run(version, messages, callback_data, self.title) - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice( - input_message.content_object.model, - version=version, - input_tokens=result[1], - 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) -> list[Dict]: - if isinstance(self.store, Chat): - air_messages = list( - reversed( - Message.objects.filter( - chats_chats_messages=self.store, is_deleted=False, is_sent=True - ).order_by('-created_at')[:message_limit] - ) - ) - elif isinstance(self.store, APIStore): - air_messages = [] - elif isinstance(self.store, Copywrite): - air_messages = list( - reversed( - Message.objects.filter( - copywrite_copywrites_messages=self.store, - is_deleted=False, - is_sent=True, - ).order_by('-created_at')[:message_limit] - ) - ) - memory = [] - for msg in air_messages: - content = msg.content or '' - if msg.from_model: - memory.append({'role': 'assistant', 'content': content}) - else: - 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) - character_length = sum(len(content['content']) for content in memory) - - return memory @@ -6,7 +6,8 @@ from io import BytesIO import requests from django.core.files import File -from messages.models import Message +from backend import settings +from messages.models import BaseStore, Message from ml_model.models import ( ModelCategory, ModelInput, @@ -14,7 +15,6 @@ from ml_model.models import ( ModelVersion, ) from ml_model.services.base import SimpleService -from ml_model.tasks import replicate_run class Recraft(SimpleService): @@ -27,7 +27,7 @@ class Recraft(SimpleService): description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [ - ModelVersion(name='Recraft V3', slug='recraft-v3'), + ModelVersion(name='Recraft V3', slug='recraft-v3', default=True), ModelVersion(name='Recraft V3 SVG', slug='recraft-v3-svg'), ] inputs = [ @@ -70,7 +70,7 @@ class Recraft(SimpleService): 'естественное освещение', 'студийный портрет', 'предпринимательство', - 'размытие движения', + 'размытие движения' ], 'default': 'любой', }, @@ -97,23 +97,16 @@ class Recraft(SimpleService): versions[1].slug: Decimal('44'), } + def __init__(self, store: BaseStore) -> None: + super().__init__(store) + self.generate_url = 'https://api.replicate.com/v1/models/recraft-ai/' + self.get_url = 'https://api.replicate.com/v1/predictions/' + 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])) @@ -121,16 +114,36 @@ class Recraft(SimpleService): size = min(available_sizes, key=lambda size: abs(height - size[1])) return f'{size[0]}x{size[1]}' + def _call_api(self, payload: dict) -> list: + headers = { + 'Authorization': f'Bearer {settings.REPLICATE_API_KEY}', + 'Content-Type': 'application/json', + 'Prefer': 'wait', + } + data = {'input': payload} + response = requests.post( + url=f'{self.generate_url}{payload.get('version', 'recraft-v3')}/predictions', + headers=headers, + json=data, + ) + if response.status_code != 201: + raise Exception(response.json()) + + result = requests.get(url=f'{self.get_url}{response.json().get('id')}', headers=headers) + while result.json()['status'] not in ('succeeded', 'failed', 'canceled'): + result = requests.get(url=f'{self.get_url}{response.json().get('id')}', headers=headers) + return result.json()['output'] + def calculate_price(self, input_message: Message) -> Decimal: 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( @@ -169,17 +182,11 @@ 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_data = dict( { 'prompt': ( @@ -188,10 +195,10 @@ 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) + image = self._call_api(payload=callback_data) process_time = timedelta(seconds=(time.time() - start_time)) self.handle_invoice(input_message.content_object.model, input_message) message = self.save_results(input_message.content, image, extension, process_time, save) @@ -1,30 +1,31 @@ import time + +import requests from datetime import timedelta from decimal import Decimal from io import BytesIO -import requests -from django.core.files import File - from messages.models import Message +from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run +from django.core.files import File + class Sdxlemoji(SimpleService): """ Sdxl-emoji Service contains abstract method make, which makes a generation """ - title = 'Sdxl-emoji' description = 'Нейросеть, способная генерировать фотографии из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), + ModelInput(type=ModelInput.TypeChoices.IMAGE) ] parameters = [ ModelParameter( @@ -62,7 +63,7 @@ class Sdxlemoji(SimpleService): 'KarrasDPM', 'K_EULER_ANCESTRAL', 'K_EULER', - 'PNDM', + 'PNDM' ], 'default': 'K_EULER', }, @@ -83,13 +84,18 @@ 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( @@ -97,7 +103,7 @@ class Sdxlemoji(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: @@ -1,19 +1,16 @@ -import logging import time import uuid from datetime import timedelta from decimal import Decimal from io import BytesIO -import httpx -from django.conf import settings from django.core.files import File +from backend import settings from messages.models import Message +from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService -from poller.models import Proxy - -logger = logging.getLogger(__name__) +from ml_model.tasks import create_new_sd_image, create_sd_image class Stablediffusion(SimpleService): @@ -21,79 +18,203 @@ class Stablediffusion(SimpleService): Stablediffusion Service contains abstract method make, which makes a generation """ + title = 'StableDiffusion' + description = 'Нейросеть, способная генерировать картинки из вашего текста' + category = ModelCategory(title='Изображения', slug='images') + versions = [ + ModelVersion(name='V3', slug='sd3', default=True), + ModelVersion(name='V3-Turbo', slug='sd3-turbo'), + ModelVersion(name='Standart', slug='stable-diffusion-xl-1024-v1-0'), + ] + inputs = [ + ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), + ModelInput(type=ModelInput.TypeChoices.IMAGE), + ] + parameters = [ + ModelParameter( + name='Sampler', + key='sampler', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': ['DDIM', 'K_EULER', 'K_EULER_ANCESTRAL', 'K_DMP_2'], + 'default': 'DDIM', + }, + ), + ModelParameter( + name='Шаги предобработки', + key='num_inference_steps', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 100, 'step': 1, 'default': 10}, + ), + ModelParameter( + name='Шаги процесса диффузии', + key='steps', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 50, 'step': 1, 'default': 30}, + ), + ModelParameter( + name='Количество изображений', + key='num_images', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 10, 'step': 1, 'default': 1}, + ), + ModelParameter( + name='CFG Scale', + key='cfg_scale', + type=ModelParameter.TypeChoices.INTRANGE, + values={'start': 1, 'end': 10, 'step': 1, 'default': 1}, + ), + ModelParameter( + name='Стиль', + key='style', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': [ + '3d-model', + 'analog-film', + 'anime', + 'cinematic', + 'comic-book', + 'digital-art', + 'enhance', + 'fantasy-art', + 'isometric', + 'line-art', + 'low-poly', + 'modeling-compound', + 'neon-punk', + 'origami', + 'photographic', + 'pixel-art', + 'tile-texture', + ], + 'default': 'photographic', + }, + ), + ModelParameter( + name='Clip Guidance Scale', + key='clip_guidance_scale', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': ['FAST_BLUE', 'FAST_GREEN', 'SIMPLE', 'SLOW', 'SLOWEST'], + 'default': 'SIMPLE', + }, + ), + ModelParameter( + name='Соотношение сторон', + key='aspect_ratio', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': + [ + '1:1', + '16:9', + '9:16', + ], + 'default': '1:1' + }, + ), + ModelParameter( + name='Соотношение сторон', + key='aspect_ratio', + type=ModelParameter.TypeChoices.LIST, + values={ + 'availables': + [ + '1:1', + '4:3', + '3:4', + '3:2', + '16:9', + '9:16', + '24:10', + '10:24' + ], + 'default': '1:1' + }, + ), + ] + + TOKEN_PRICE = Decimal('11') - MODELS = ['sd3', 'sd3-turbo', 'sd3-medium'] - MODELS_LINKS = { - 'sd3': 'stable-diffusion-3.5-large', - 'sd3-turbo': 'stable-diffusion-3.5-large-turbo', - 'sd3-medium': 'stable-diffusion-3.5-medium' - } + _API_KEY = settings.STABLE_DIFFUSION_API_KEY def calculate_price(self, input_message: Message) -> Decimal: - if input_message.info.get('version') == 'sd3': - return Decimal('13') - elif input_message.info.get('version') == 'sd3-turbo': - return Decimal('8') - elif input_message.info.get('version') == 'sd3-medium': - return Decimal('7') + if ( + input_message.info.get('version') or input_message.info.get('engine', 'sd3') + ) == 'sd3': + return Decimal('71.5') + elif ( + input_message.info.get('version') or input_message.info.get('engine', 'sd3') + ) == 'sd3-turbo': + return Decimal('44') + steps = input_message.info.get('steps', 30) + default_price = Decimal('0.9') * self.TOKEN_PRICE + return default_price if steps <= 30 else default_price * Decimal((steps / 30)) def save_results( - self, - input_prompt: str, - link: str, - t: timedelta, - save: bool = True, + self, input_prompt: str, r: list[BytesIO], t: timedelta, save: bool = True ) -> list[Message]: out: list[Message] = [] - out.append( - Message( - content_object=self.store, - elapsed_time=t, - content=input_prompt, - file=File( - BytesIO(httpx.get(link).content), - f'{uuid.uuid4()}.png', - ), + for obj in r: + out.append( + Message( + content_object=self.store, + elapsed_time=t, + content=input_prompt, + file=File( + obj, + f'{uuid.uuid4()}.png', + ), + ) ) - ) if save: return Message.objects.bulk_create(out) return out def make(self, input_message: Message, save: bool = True) -> list[Message]: + aspect_rations = { + '1:1': (1024, 1024), + '4:3': (1152, 896), + '3:4': (896, 1152), + '3:2': (1216, 832), + '16:9': (1344, 768), + '9:16': (768, 1344), + '24:10': (1536, 640), + '10:24': (640, 1536) + } start_time = time.time() info = input_message.info.copy() - model_name = self.MODELS_LINKS[info.get('version', 'sd3')] - translated_prompt = self.translate_prompt(input_message.content) - callback_data = { - 'prompt': translated_prompt, - 'aspect_ratio': input_message.info.get('aspect_ratio', '1:1'), - 'output_quality': 100, - 'output_format': 'png', + model_name = info.get('version') or info.get('engine', 'sd3') + formatter = { + 'width': int( + aspect_rations[input_message.info.get('aspect_ratio', '1:1')][0] + ), + 'height': int( + aspect_rations[input_message.info.get('aspect_ratio', '1:1')][1] + ), } - link = '' - for proxy in Proxy.objects.all(): - with httpx.Client( - headers={ - 'Authorization': f'Bearer {settings.REPLICATE_API_KEY}', - 'Prefer': 'wait', - 'Content-Type': 'application/json', - }, - timeout=600, - proxy=f'{proxy.protocol}://{proxy.address}', - ) as client: - result = client.post( - f'https://api.replicate.com/v1/models/stability-ai/{model_name}/predictions', - json={'input': callback_data}, - ) - while result.json()['status'] not in ('succeeded', 'failed', 'canceled'): - result = client.get(result.json()['urls']['get']) - if result.json()['status'] in ('failed', 'canceled'): - logger.error(result.json()['logs']) - raise Exception('No answer from Stable Diffusion, please retry later') - link = result.json()['output'][0] - - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, input_message) - msgs = self.save_results(input_message.content, link, process_time, save) - return msgs + translated_prompt = self.translate_prompt(input_message.content) + callback_data = dict( + prompt=translated_prompt, + ) + if input_message.file: + callback_data.update({'init_image': BytesIO(input_message.file.read())}) + if style := input_message.info.get('style', None): + callback_data.update({'style_preset': style}) + if model_name in ('sd3', 'sd3-turbo'): + callback_data = dict( + prompt=translated_prompt, + model=model_name, + aspect_ratio=input_message.info.get('aspect_ratio', '1:1'), + output_format='jpeg', + ) + results = create_new_sd_image.delay(callback_data) + else: + callback_data.update(**formatter, engine=model_name) + results = create_sd_image.delay(self._API_KEY, callback_data) + images = results.get() + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice(input_message.content_object.model, input_message) + msgs = self.save_results(input_message.content, images, process_time, save) + return msgs @@ -66,7 +66,10 @@ class Upscaleai(SimpleService): ), ] - _CALLBACK = 'mcai/babes-v2.0-img2img:2bca10ed539cf2196f18b4ec85128a80355d94934db8620884ecca552cdc4def' + _CALLBACK = ( + 'mcai/babes-v2.0-img2img' + ':2bca10ed539cf2196f18b4ec85128a80355d94934db8620884ecca552cdc4def' + ) def __init__(self, store): super().__init__(store) @@ -76,11 +79,7 @@ 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: @@ -91,7 +90,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( @@ -125,10 +124,7 @@ 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,6 +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.services.base import SimpleService from ml_model.tasks import replicate_run @@ -14,16 +15,48 @@ 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,6 +8,7 @@ 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 @@ -18,6 +19,14 @@ 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() @@ -44,7 +53,9 @@ 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,13 +1,6 @@ -import re -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 ExportActionModelAdmin, ImportExportMixin +from import_export.admin import ImportExportMixin from ordered_model.admin import ( OrderedInlineModelAdminMixin, OrderedModelAdmin, @@ -22,19 +15,10 @@ from ml_model.models import ( ModelParameter, ModelPaymentRule, ModelSettings, - ModelStat, - ModelTag, ModelVersion, NeuronModel, ) -from ml_model.resources import ( - ModelCategoryResource, - ModelInputResource, - ModelParameterResource, - ModelTagResource, - ModelVersionResource, - NeuronModelResource, -) +from ml_model.resources import NeuronModelResource class ModelSettingsInline(admin.TabularInline): @@ -66,125 +50,45 @@ class ModelInputsInline(admin.TabularInline): class ModelVersionsInline(OrderedTabularInline): model = ModelVersion + fk_name = 'model' extra = 0 classes = ['collapse'] - fields = ( - 'name', - 'description', - 'slug', - 'order', - 'move_up_down_links', - ) + fields = ('name', 'description', 'slug', 'default', 'order', 'move_up_down_links') readonly_fields = ('order', 'move_up_down_links') ordering = ('order',) -class ModelStatInline(admin.TabularInline): - model = ModelStat - verbose_name_plural = 'Статистика по модели' - extra = 0 - classes = ['collapse'] - - @admin.register(ModelCategory) -class ModelCategoryAdmin(ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin): +class ModelCategoryAdmin(admin.ModelAdmin): list_display = ['title', 'slug'] + list_display_links = ('title', 'slug') prepopulated_fields = {'slug': ('title',)} - resource_classes = [ModelCategoryResource] @admin.register(NeuronModel) -class NeuronModelAdmin( - OrderedInlineModelAdminMixin, ImportExportMixin, ExportActionModelAdmin, OrderedModelAdmin +class NeuronModelModelAdmin( + OrderedInlineModelAdminMixin, ImportExportMixin, OrderedModelAdmin ): - list_display = ['title', '_active', '_category', 'move_up_down_links'] + list_display = ['title', 'is_active', 'category', 'move_up_down_links'] resource_classes = (NeuronModelResource,) prepopulated_fields = {'slug': ('title',)} inlines = [ ModelSettingsInline, ModelVersionsInline, ModelPaymentRulesInline, - ModelStatInline, ModelInputsInline, ModelParametersInline, ] - list_filter = ['model_settings__is_active', 'category', 'tags'] - - filter_horizontal = ['tags'] + list_filter = ['model_settings__is_active', 'category__title'] @admin.display(description='Активна?', boolean=True) - def _active(self, obj: NeuronModel): - return obj.active - - @admin.display(description='Категория') - def _category(self, obj: NeuronModel): - if obj.category: - return obj.category.title - return 'Не присвоена' + def is_active(self, obj: NeuronModel): + if obj.settings: + return obj.settings.is_active - -@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(): - tag_regexp = re.match(r'{.*}(?P.*)', element.tag) - if tag_regexp: - element.tag = tag_regexp.groupdict().get('tagname') - for attr in ['width', 'height']: - if attr in element.attrib: - del element.attrib[attr] - for attr, val in { - 'fill': 'currentColor', - 'stroke': 'currentColor', - }.items(): - if element.tag == 'svg' and attr == 'stroke': - continue - if attr in element.attrib: - element.attrib[attr] = val - buf = BytesIO() - ET.ElementTree(root).write( - buf, encoding='utf-8', xml_declaration=True, default_namespace={}.get(None) - ) - 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') + @admin.display(description='Категория', boolean=True) + def category(self, obj: NeuronModel): + return obj.category.title class ConfigurationParameterInline(admin.TabularInline): @@ -192,7 +96,7 @@ class ConfigurationParameterInline(admin.TabularInline): extra = 1 can_delete = False - def has_change_permission(self, *args, **kwargs): + def has_change_permission(self, request, obj=...): return False @@ -1,5 +1,4 @@ from django.apps import AppConfig -from django.core.signals import setting_changed from django.utils.translation import gettext_lazy as _ @@ -7,10 +6,3 @@ 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() @@ -1,12 +0,0 @@ -# накинуть перевод через gettext_lazy - - -class GenerationException(Exception): - def __str__(self): - return 'Случилась ошибка во время генерации у этой модели, пожалуйста повторите попытку позже' - - -class NSFWDetectedException(Exception): ... - - -class LargeResourceConsumptionException(Exception): ... @@ -2,15 +2,12 @@ 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 @@ -30,44 +27,13 @@ class ModelCategory(models.Model): verbose_name_plural = _('Categories') -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): +def upload_model_avatar(instance, filename): return f'avatars/{instance.slug}/{filename}' class NeuronModel(BaseModel, OrderedModel): title = models.CharField(max_length=300, verbose_name=_('Title')) - 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) + description = models.TextField(max_length=4000, verbose_name=_('Description')) slug = models.SlugField( verbose_name=_('Slug'), unique=True, @@ -77,8 +43,6 @@ class NeuronModel(BaseModel, OrderedModel): category = models.ForeignKey( 'ModelCategory', on_delete=models.PROTECT, - null=True, - blank=True, verbose_name=_('Category'), related_name='category_models', ) @@ -90,8 +54,6 @@ 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' @@ -112,17 +74,6 @@ class NeuronModel(BaseModel, OrderedModel): def payment_rules(self) -> QuerySet['ModelPaymentRule']: return self.model_modelpaymentrules.all() - @property - def stats(self) -> QuerySet['ModelStat']: - return self.model_modelstats.all() - - @property - def first_stat(self) -> 'ModelStat': - try: - return self.stats[0] - except IndexError: - return None - @property def settings(self) -> 'ModelSettings': try: @@ -130,13 +81,9 @@ 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 not self.active + return bool(self.settings) and self.settings.is_active and self.inputs.count() > 0 def __str__(self): return self.title @@ -159,12 +106,17 @@ 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=False, + default=True, 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') @@ -176,8 +128,11 @@ 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')) - slug = models.CharField(max_length=32, verbose_name=_('Slug')) + 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' @@ -219,8 +174,6 @@ class ModelInput(ModelDepends, ModelVersionsDepends): TEXT = 'text', _('Text') IMAGE = 'image', _('Image') PDF = 'pdf', _('PDF') - DOCX = 'docx', _('DOCX') - DOC = 'doc', _('DOC') TXT = 'txt', _('Text File (Notebook)') ZIPARCHIVE = 'zip', _('ZIP Archive') AUDIO = 'audio', _('Audio') @@ -250,10 +203,7 @@ 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'), @@ -265,7 +215,9 @@ 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'), @@ -276,7 +228,9 @@ 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) @@ -304,9 +258,7 @@ 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, @@ -338,19 +290,10 @@ class ModelPaymentRule(ModelDepends, ModelVersionsDepends): verbose_name_plural = _('Payment Rules') -class ModelStat(ModelDepends): - generation_time = models.DurationField(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: - verbose_name = 'Статистика по модели' - verbose_name_plural = 'Статистики по моделям' - ordering = ('-created_at',) - - 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, @@ -358,6 +301,15 @@ class ModelConfiguration(models.Model): related_name='model_configurations', verbose_name='Модель', ) + version = models.ForeignKey( + ModelVersion, + on_delete=models.PROTECT, + to_field='slug', + null=True, + blank=True, + related_name='version_configurations', + verbose_name='Версия модели', + ) ct = models.ForeignKey(ContentType, on_delete=models.CASCADE) oid = models.UUIDField() @@ -1,30 +1,19 @@ from import_export import fields as ie_fields from import_export.resources import ModelResource -from import_export.widgets import ForeignKeyWidget, ManyToManyWidget +from import_export.widgets import ForeignKeyWidget from ml_model.models import ( ModelCategory, ModelInput, ModelParameter, - ModelPaymentRule, - ModelTag, ModelVersion, NeuronModel, ) - - -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',) +from ml_model.serializers import ( + ModelInputSerializer, + ModelParameterSerializer, + ModelVersionSerializer, +) class NeuronModelResource(ModelResource): @@ -33,80 +22,48 @@ class NeuronModelResource(ModelResource): attribute='category', widget=ForeignKeyWidget(ModelCategory, 'slug'), ) - tags = ie_fields.Field( - column_name='tags', - attribute='tags', - widget=ManyToManyWidget(ModelTag, ',', 'slug'), - ) + versions = ie_fields.Field() + inputs = ie_fields.Field() + parameters = ie_fields.Field() + + 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 = NeuronModel - exclude = ('uid', 'order', 'created_at', 'updated_at', 'image', 'description') + use_transactions = True + exclude = ('uid', 'order', 'created_at', 'updated_at', '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='versions', - attribute='versions', - widget=ManyToManyWidget(ModelVersion, ',', 'slug'), - ) - - 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'), - ) - versions = ie_fields.Field( - column_name='versions', - attribute='versions', - widget=ManyToManyWidget(ModelVersion, ',', 'slug'), - ) - - 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 = ModelPaymentRule - exclude = ('id',) - import_id_fields = ('model', 'versions', 'strategy', 'interaction_type') @@ -22,7 +22,7 @@ class ModelConfigurationSchema(ModelSchema): class Meta: model = ModelConfiguration - exclude = ('ct', 'oid', 'obj', 'model') + exclude = ('ct', 'oid', 'obj', 'model', 'version') class Config: protected_namespaces = () @@ -31,4 +31,4 @@ class ModelConfigurationSchema(ModelSchema): class NeuronModelLink(ModelSchema): class Meta: model = NeuronModel - fields = ('title', 'slug', 'alternative_titles') + fields = ('title', 'slug') @@ -5,7 +5,6 @@ from ml_model.models import ( ModelInput, ModelParameter, ModelSettings, - ModelTag, ModelVersion, NeuronModel, ) @@ -14,7 +13,7 @@ from ml_model.models import ( class ModelSettingsSerializer(serializers.ModelSerializer): class Meta: model = ModelSettings - exclude = ('id', 'model') + exclude = ('id', 'model', 'authorization_token') class ModelVersionSerializer(serializers.ModelSerializer): @@ -39,31 +38,23 @@ 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', 'alternative_titles') + exclude = ('created_at', 'updated_at', 'order', 'category') class NeuronModelsSerializer(serializers.ModelSerializer): blocked = serializers.BooleanField() - tags = ModelTagSerializer(many=True) class Meta: model = NeuronModel - exclude = ('created_at', 'updated_at', 'order', 'category', 'alternative_titles', 'uid') + exclude = ('created_at', 'updated_at', 'order', 'category', 'uid') class ModelCategorySerializer(serializers.ModelSerializer): @@ -1,12 +0,0 @@ -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) @@ -7,7 +7,6 @@ from io import BytesIO from typing import IO, Any, Dict import deepl -import httpx import replicate import requests from celery import shared_task @@ -17,15 +16,13 @@ from requests import Response from backend import settings from poller.models import Proxy -logger = logging.getLogger(__name__) - @shared_task 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={ @@ -35,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={ @@ -48,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', @@ -57,7 +54,10 @@ 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 +94,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={ @@ -108,34 +108,11 @@ def transcript_audio(payload: dict[str, Any]): def replicate_run(callback_url: str, payload: dict[str, Any]): replicate_client = replicate.Client(settings.REPLICATE_API_KEY) return replicate_client.run( - ref=callback_url, + model_version=callback_url, input=payload, ) -@shared_task -def openrouter_run(version: str, messages: list, callback_data: dict, model_name: str): - for proxy in Proxy.objects.all(): - with httpx.Client( - 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', - json={'model': version, 'messages': messages, **callback_data}, - ) - if ( - (data := resp.json()) - and data.get('choices') - and (content := ','.join([choice['message']['content'] for choice in data.get('choices')])) - ): - return (content, data['usage']['prompt_tokens'], data['usage']['completion_tokens']) - logger.error(f'Error occured via model {model_name}. Data: {resp.content}') - raise Exception(f'No answer from {model_name}, please retry later') - - @shared_task def upscale_run(payload: dict[str, tuple[str, IO]]) -> list[str]: content = requests.post( @@ -169,9 +146,7 @@ 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,10 +1,6 @@ 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,12 +2,8 @@ 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,6 +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_parameter=False) ).data ) @@ -1,6 +0,0 @@ -FROM nginx:alpine - -COPY root.conf /etc/nginx/nginx.conf - -ENTRYPOINT ["sh", "docker-entrypoint.sh" ] -CMD ["nginx", "-g", "daemon off;"] @@ -1,49 +0,0 @@ -worker_processes 4; - -events { - worker_connections 1024; - use epoll; - multi_accept on; -} - -http { - include mime.types; - default_type application/octet-stream; - client_max_body_size 25m; - - http2 on; - - access_log off; - error_log off; - - keepalive_timeout 30; - keepalive_requests 1000; - - sendfile on; - sendfile_max_chunk 1460; - tcp_nopush on; - tcp_nodelay on; - aio on; - aio_write on; - directio 1m; - output_buffers 1 1m; - - gzip on; - gzip_static on; - gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; - gzip_proxied any; - gzip_vary on; - gzip_comp_level 5; - gzip_buffers 16 8k; - gzip_http_version 1.1; - - server { - listen 80 default_server; - - location /static { - autoindex on; - expires 365d; - alias /var/www/static/; - } - } -} @@ -1,4 +1,3 @@ -import math from decimal import Decimal from django.utils.translation import gettext_lazy as _ @@ -12,17 +11,11 @@ class InsufficientBalance(Exception): ): self.account_balance = account_balance self.requested_amount = requested_amount - self.needed = requested_amount - account_balance - result_message = _( - 'Token balance: %(balance).2f,\nRequired amount: %(required)s,\nNeeded %(needed)s more' + 'Token balance: %(balance)d,\nRequired amount: %(required)d,\nNeeded %(needed)d more' ) % { 'balance': self.account_balance, - 'required': math.ceil(self.requested_amount) - if self.requested_amount % 1 == 0 - else f'~{math.ceil(self.requested_amount)}', - 'needed': round(self.needed, 2) - if self.needed >= 1 - else f'~{math.ceil(self.needed)}', + 'required': self.requested_amount, + 'needed': self.requested_amount - self.account_balance, } super().__init__(result_message) @@ -16,11 +16,7 @@ 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,10 +11,7 @@ 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!' @@ -1,20 +0,0 @@ -# Generated by Django 5.0.11 on 2025-03-17 07:46 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('payments', '0014_auto_20241116_1547'), - ] - - operations = [ - migrations.AddField( - model_name='paymentplan', - name='points', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(), default=[], help_text='Перечислять через запятую', size=None, verbose_name='Поинты'), - preserve_default=False, - ), - ] @@ -1,19 +0,0 @@ -# Generated by Django 5.0.11 on 2025-03-17 08:04 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('payments', '0015_paymentplan_points'), - ] - - operations = [ - migrations.AlterField( - model_name='paymentplan', - name='points', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(), blank=True, help_text='Перечислять через запятую', null=True, size=None, verbose_name='Поинты'), - ), - ] @@ -1,19 +0,0 @@ -# Generated by Django 5.0.11 on 2025-03-17 08:15 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('payments', '0016_alter_paymentplan_points'), - ] - - operations = [ - migrations.AlterField( - model_name='paymentplan', - name='points', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(), blank=True, default=list, help_text='Перечислять через запятую', size=None, verbose_name='Поинты'), - ), - ] @@ -49,7 +49,9 @@ 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}' @@ -2,7 +2,6 @@ from datetime import datetime from dateutil.relativedelta import relativedelta from django.contrib.auth import get_user_model -from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils.translation import gettext_lazy as _ from django_celery_beat.models import PeriodicTask @@ -24,7 +23,9 @@ 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, @@ -32,20 +33,13 @@ class PaymentPlan(BaseModel): default=MONTH, ) is_visible = models.BooleanField(default=True, verbose_name=_('Is visible')) - points = ArrayField( - default=list, - blank=True, - base_field=models.CharField(), - verbose_name='Поинты', - help_text='Перечислять через запятую', - ) accessed_models = models.ManyToManyField( NeuronModel, verbose_name='Доступные модели', ) def __str__(self) -> str: - return f'{self.title or "Ошибка"}' + return f"{self.title or 'Ошибка'}" class Meta: ordering = ['price'] @@ -82,18 +76,14 @@ 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) return super().save(force_insert, force_update, using, update_fields) def __str__(self) -> str: - return f'{self.user.email or "Ошибка"}' + return f"{self.user.email or 'Ошибка'}" class Meta: verbose_name = _('User Balance') @@ -38,10 +38,7 @@ 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( @@ -82,7 +79,9 @@ 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: @@ -104,7 +103,9 @@ 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,8 +12,12 @@ 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,10 +10,7 @@ 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__) @@ -90,8 +87,12 @@ 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,16 +23,10 @@ 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() @@ -41,17 +35,11 @@ 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,11 +16,7 @@ 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,9 +6,7 @@ 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 @@ -39,7 +37,9 @@ 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,15 +52,14 @@ 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 @@ -72,13 +71,17 @@ 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,15 +79,16 @@ 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,8 +33,5 @@ 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,11 +16,15 @@ 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,14 +157,18 @@ 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='Промокод') @@ -232,7 +236,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')) match self.value(): case 'more-zero': return queryset.filter(total_bonuses__gt=0) @@ -251,11 +255,7 @@ 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): @@ -271,4 +271,4 @@ class ReferralAccountAdmin(admin.ModelAdmin): @admin.display(description='Получено бонусов') def _accrued_bonuses(self, obj: ReferralAccount): - return f'{obj.accrued_bonuses.aggregate(total=Coalesce(Sum("amount"), Decimal(0), output_field=models.DecimalField()))["total"]} токенов' + return f'{obj.accrued_bonuses.aggregate(total=Coalesce(Sum('amount'), Decimal(0), output_field=models.DecimalField()))['total']} токенов' @@ -12,7 +12,6 @@ class PaymentPlanSerializer(serializers.Serializer): price = serializers.DecimalField(max_digits=10, decimal_places=2) tokens_per_plan = serializers.DecimalField(max_digits=50, decimal_places=2) duration = serializers.CharField(read_only=True) - points = serializers.ListField(read_only=True) accessed_models = serializers.SlugRelatedField( slug_field='slug', queryset=NeuronModel.objects.all(), many=True ) @@ -9,10 +9,7 @@ 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,11 +12,7 @@ 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,11 +6,7 @@ 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 @@ -61,22 +57,25 @@ 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""" @@ -133,7 +132,9 @@ 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) @@ -243,13 +244,17 @@ 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), ] ) @@ -257,12 +262,10 @@ 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' ), ] ) @@ -274,8 +277,7 @@ 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( @@ -310,7 +312,9 @@ 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'), @@ -320,7 +324,9 @@ 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)}) @@ -336,10 +342,7 @@ 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,11 +9,7 @@ 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,7 +6,9 @@ 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,12 +20,7 @@ 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,6 +16,8 @@ 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,5 +1,6 @@ from typing import List +from django.db.models import Count from ninja import Router from authentication.security import SyncAuthBearer @@ -13,6 +14,6 @@ router = Router(auth=SyncAuthBearer(), tags=['chats']) def get_links(request): return ( NeuronModel.objects.filter(category__slug='chat-bots') - .filter(model_settings__isnull=False, model_settings__is_active=True) - .order_by('order') + .annotate(inputs_count=Count('model_modelinputs')) + .filter(inputs_count__gt=0) ) @@ -2,10 +2,7 @@ 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 @@ -145,8 +142,10 @@ class MessagesAPIView(APIView): except Exception as exc: input_message.is_sent = False input_message.save() - logger.exception(exc) - return Response(f'Error occured: {exc}', status=400) + logger.info(f'Error occured: {exc}') + return Response( + f'Error occured: {exc}', status=400 + ) output_messages.insert(0, input_message) return Response(MessageSerializer(output_messages, many=True).data, 201) else: @@ -169,7 +168,9 @@ 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) @@ -179,7 +180,9 @@ 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) @@ -192,7 +195,9 @@ 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,7 +7,9 @@ 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,7 +6,9 @@ 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,18 +85,14 @@ 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) @@ -118,7 +114,9 @@ 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( @@ -126,7 +124,9 @@ 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,9 +151,7 @@ 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,12 +42,16 @@ 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: @@ -77,25 +81,30 @@ 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,11 +33,15 @@ 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'), @@ -83,7 +87,9 @@ 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, @@ -96,7 +102,9 @@ 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' @@ -106,7 +114,9 @@ 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, @@ -115,7 +125,9 @@ 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='Удален') @@ -131,7 +143,9 @@ 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: @@ -153,7 +167,9 @@ 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 = 'Самописный копирайт' @@ -178,7 +194,9 @@ 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,13 +98,7 @@ 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): @@ -144,13 +138,7 @@ 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): @@ -174,7 +162,9 @@ 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,5 +7,7 @@ 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,10 +17,7 @@ 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,5 +1,6 @@ from typing import List +from django.db.models import Count from ninja import Router from authentication.security import SyncAuthBearer @@ -9,14 +10,10 @@ 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') - .filter(model_settings__isnull=False, model_settings__is_active=True) - .order_by('order') + .annotate(inputs_count=Count('model_modelinputs')) + .filter(inputs_count__gt=0) ) @@ -35,8 +35,6 @@ class GalleryAPIView(APIView): required=True, enum=['images', 'videos', 'audios'], ), - OpenApiParameter('limit', int, required=False), - OpenApiParameter('offset', int, required=False), ], responses={200: MessageSerializer(many=True)}, ) @@ -45,24 +43,11 @@ class GalleryAPIView(APIView): List all messages by strategy: images, videos, or audios """ - messages_ids = ( - self.manager.objects.filter(user=request.user) - .prefetch_related('messages') - .values_list('messages', flat=True) - ) - - return Response( - MessageSerializer( - Message.objects.filter(uid__in=messages_ids, from_model=True)[ - int(request.query_params.get('offset', '0')) : int( - request.query_params.get('offset', '0') - ) - + int(request.query_params.get('limit', '10')) - ], - many=True, - ).data, - 200, - ) + galleries = self.manager.objects.filter(user=request.user) + out = [] + for gallery in galleries: + out += gallery.output_messages + return Response(MessageSerializer(out, many=True).data, 200) class MediaAPIView(APIView): @@ -129,7 +114,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,11 +1,6 @@ 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,9 +4,7 @@ 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 @@ -14,7 +12,9 @@ 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:]}', } } ], @@ -54,7 +54,8 @@ class APIKeyService(BaseService): api_key.name = new_name if expires_at is not None: api_key.expires_at = expires_at - api_key.token_limit = token_limit + if token_limit is not None: + api_key.token_limit = token_limit api_key.save() if serialize: @@ -73,7 +74,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,10 +1,3 @@ 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 @@ -46,20 +46,30 @@ 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, 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 + ) if model.blocked: raise ClientException( - detail=_('Model is blocked by outdating or temporary block, please retry later') + 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, @@ -81,7 +91,9 @@ 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,8 +46,12 @@ 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,7 +27,9 @@ 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,14 +16,7 @@ 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,20 +6,11 @@ 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()), ] ) @@ -3,7 +3,6 @@ SECRET_KEY=testtest DEBUG=true # NEURON MODELS -OPENROUTER_API_KEY=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 OPENAI_API_KEY=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 STABLE_DIFFUSION_API_KEY=sk-fztQxZobaL0SD7PgpmK7XMQlyNivpKFZNJnqAVG2CcbvAP6Z REPLICATE_API_KEY=r8_HBk6Ts5UJU60nDOUl1V6Uej4ihAAxUc3HAZLO @@ -5,31 +5,21 @@ stages: - Deploy default: - image: docker:cli - services: - - docker:dind + image: docker:rc-cli before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin -build_staging: +build: stage: Build 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 + services: + - docker:dind only: - main + - staging when: on_success deploy_staging: @@ -38,6 +28,8 @@ deploy_staging: DOCKER_HOST: tcp://$STAGING_CLUSTER_HOST:2376 DOCKER_TLS_VERIFY: 1 DOCKER_CERT_PATH: "/certs" + services: + - docker:dind environment: name: staging deployment_tier: staging @@ -61,9 +53,11 @@ deploy_production: DOCKER_HOST: tcp://$PRODUCTION_CLUSTER_HOST:2376 DOCKER_TLS_VERIFY: 1 DOCKER_CERT_PATH: "/certs" + services: + - docker:dind environment: name: production - url: https://backend.air.fail + url: https://api.air.fail only: - main when: on_success @@ -74,4 +68,5 @@ deploy_production: - echo "$PRODUCTION_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin script: - - docker stack deploy --prune --with-registry-auth --resolve-image=always --compose-file stack.yml --detach backend \ No newline at end of file + - docker compose pull + - docker compose --project-name air-backend up -d @@ -22,8 +22,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ --mount=target=/var/cache/apt,type=cache,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update \ - && apt-get -y --no-install-recommends install -y gettext \ - && apt-get -y install antiword + && apt-get -y --no-install-recommends install -y gettext RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt @@ -23,8 +23,7 @@ RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ --mount=target=/var/cache/apt,type=cache,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ && apt-get update \ - && apt-get -y --no-install-recommends install gettext \ - && apt-get -y install antiword + && apt-get -y --no-install-recommends install gettext RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt @@ -1,26 +1,26 @@ .DEFAULT_GOAL=start start: - cp -n .env.dist .env + cp --update=none .env.dist .env docker compose -f docker-compose.debug.yml --project-name air up .PHONY=start rebuild: - cp -n .env.dist .env + cp --update=none .env.dist .env docker compose -f docker-compose.debug.yml --project-name air up --build .PHONY=rebuild stop: - cp -n .env.dist .env + cp --update=none .env.dist .env docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans .PHONY=stop cleanup: - cp -n .env.dist .env + cp --update=none .env.dist .env docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans -v .PHONY=cleanup full-cleanup: - cp -n .env.dist .env + cp --update=none .env.dist .env docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans -v --rmi local .PHONY=full-cleanup \ No newline at end of file @@ -1,68 +1,41 @@ services: app: restart: unless-stopped - container_name: app - user: '1000' build: context: . dockerfile: Dockerfile.dev command: - /bin/sh - -c - - | - python manage.py initialize_buckets - python manage.py collectstatic --no-input - python manage.py compilemessages - (python manage.py createsuperuser --no-input || true) - 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 + - python manage.py initialize_buckets && + python manage.py collectstatic --no-input && python manage.py migrate && + (python manage.py createsuperuser --no-input || true) && + python -m uvicorn --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off backend.asgi:application --log-level debug --reload volumes: - .:/code ports: - "8000:8000" - - "5678:5678" env_file: - .env depends_on: - migrator: - condition: service_completed_successfully - cache-mdb: - condition: service_started - s3: - condition: service_started - db: - condition: service_started - - migrator: - restart: on-failure:1 - container_name: migrator - build: - context: . - dockerfile: Dockerfile.dev - command: - - /bin/sh - - -c - - python manage.py migrate - env_file: - - .env + - cache-mdb + - s3 + - db cache-mdb: - container_name: cache-mdb image: redis:alpine restart: unless-stopped celery-mdb: - container_name: celery-mdb image: redis:alpine restart: unless-stopped channels-mdb: - container_name: channels-mdb image: redis:alpine restart: unless-stopped celery: restart: unless-stopped - container_name: celery build: context: . dockerfile: Dockerfile.dev @@ -78,7 +51,6 @@ services: celery_beat: restart: unless-stopped - container_name: celery-beat build: context: . dockerfile: Dockerfile.dev @@ -91,8 +63,8 @@ services: - celery-mdb db: restart: unless-stopped - container_name: db image: postgres:alpine + container_name: db volumes: - pgdata:/var/lib/postgresql/data env_file: @@ -102,7 +74,6 @@ services: s3: image: webcenter/alpine-minio - container_name: s3 restart: unless-stopped volumes: - s3data:/data @@ -5,6 +5,7 @@ services: build: context: . dockerfile: Dockerfile + container_name: backend volumes: - static:/code/static command: @@ -14,38 +15,19 @@ 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 + ports: + - "8000:8000" env_file: - $ENV depends_on: - migrator: - condition: service_completed_successfully - cache-mdb: - condition: service_started - - migrator: - restart: on-failure:1 - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - command: - - /bin/sh - - -c - - python manage.py migrate - env_file: - - $ENV - + - cache-mdb 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 @@ -57,52 +39,34 @@ 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: - name: infrastructure - external: true + name: "air" volumes: static: @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -6,7 +6,6 @@ version = "2.4.4" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, @@ -18,7 +17,6 @@ version = "3.11.11" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, @@ -116,7 +114,6 @@ version = "1.3.2" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, @@ -131,7 +128,6 @@ version = "5.3.1" description = "Low-level AMQP client for Python (fork of amqplib)." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, @@ -146,7 +142,6 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -158,7 +153,6 @@ version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, @@ -180,7 +174,6 @@ version = "23.1.0" description = "Argon2 for Python" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, @@ -201,7 +194,6 @@ version = "21.2.0" description = "Low-level CFFI bindings for Argon2" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, @@ -239,7 +231,6 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" -groups = ["main", "typing"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -254,7 +245,6 @@ version = "24.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, @@ -274,7 +264,6 @@ version = "24.4.2" description = "WebSocket client & server library, WAMP real-time framework" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "autobahn-24.4.2-py2.py3-none-any.whl", hash = "sha256:c56a2abe7ac78abbfb778c02892d673a4de58fd004d088cd7ab297db25918e81"}, {file = "autobahn-24.4.2.tar.gz", hash = "sha256:a2d71ef1b0cf780b6d11f8b205fd2c7749765e65795f2ea7d823796642ee92c9"}, @@ -304,7 +293,6 @@ version = "24.8.1" description = "Self-service finite-state machines for the programmer on the go." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a"}, {file = "automat-24.8.1.tar.gz", hash = "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88"}, @@ -319,7 +307,6 @@ version = "4.2.1" description = "Python multiprocessing fork with improvements and bugfixes" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, @@ -331,7 +318,6 @@ version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, @@ -343,7 +329,6 @@ version = "5.4.0" description = "Distributed Task Queue." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, @@ -401,7 +386,6 @@ version = "0.1.3" description = "celery stubs" optional = false python-versions = "*" -groups = ["typing"] files = [ {file = "celery-stubs-0.1.3.tar.gz", hash = "sha256:0fb5345820f8a2bd14e6ffcbef2d10181e12e40f8369f551d7acc99d8d514919"}, {file = "celery_stubs-0.1.3-py3-none-any.whl", hash = "sha256:dfb9ad27614a8af028b2055bb4a4ae99ca5e9a8d871428a506646d62153218d7"}, @@ -417,7 +401,6 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["main", "typing"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -429,7 +412,6 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -509,7 +491,6 @@ version = "4.2.0" description = "Brings async, event-driven capabilities to Django." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "channels-4.2.0-py3-none-any.whl", hash = "sha256:6b75bc8d6888fb7236e7e7bf1948520b72d296ad08216a242fc56b1db0ffde1a"}, {file = "channels-4.2.0.tar.gz", hash = "sha256:d9e707487431ba5dbce9af982970dab3b0efd786580fadb99e45dca5e39fdd59"}, @@ -530,7 +511,6 @@ version = "4.2.1" description = "Redis-backed ASGI channel layer implementation" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "channels_redis-4.2.1-py3-none-any.whl", hash = "sha256:2ca33105b3a04b5a327a9c47dd762b546f30b76a0cd3f3f593a23d91d346b6f4"}, {file = "channels_redis-4.2.1.tar.gz", hash = "sha256:8375e81493e684792efe6e6eca60ef3d7782ef76c6664057d2e5c31e80d636dd"}, @@ -552,7 +532,6 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main", "typing"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -654,7 +633,6 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -669,7 +647,6 @@ version = "0.3.1" description = "Enables git-like *did-you-mean* feature in click" optional = false python-versions = ">=3.6.2" -groups = ["main"] files = [ {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, @@ -684,7 +661,6 @@ version = "1.1.1" description = "An extension module for click to enable registering CLI commands via setuptools entry-points." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, @@ -702,7 +678,6 @@ version = "0.3.0" description = "REPL plugin for Click" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, @@ -721,12 +696,10 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] name = "constantly" @@ -734,7 +707,6 @@ version = "23.10.4" description = "Symbolic constants in Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, @@ -746,7 +718,6 @@ version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, @@ -821,7 +792,6 @@ version = "1.4.5" description = "A Python library that converts cron expressions into human readable strings." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "cron_descriptor-1.4.5-py3-none-any.whl", hash = "sha256:736b3ae9d1a99bc3dbfc5b55b5e6e7c12031e7ba5de716625772f8b02dcd6013"}, {file = "cron_descriptor-1.4.5.tar.gz", hash = "sha256:f51ce4ffc1d1f2816939add8524f206c376a42c87a5fca3091ce26725b3b1bca"}, @@ -836,7 +806,6 @@ version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" -groups = ["main"] files = [ {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, @@ -886,7 +855,6 @@ version = "4.1.2" description = "Django ASGI (HTTP/WebSocket) server" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "daphne-4.1.2-py3-none-any.whl", hash = "sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a"}, {file = "daphne-4.1.2.tar.gz", hash = "sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761"}, @@ -906,7 +874,6 @@ version = "0.6.7" description = "Easily serialize dataclasses to and from JSON." optional = false python-versions = "<4.0,>=3.7" -groups = ["main"] files = [ {file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"}, {file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"}, @@ -922,7 +889,6 @@ version = "1.8.11" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" -groups = ["debug"] files = [ {file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"}, {file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"}, @@ -958,7 +924,6 @@ version = "1.21.0" description = "Python library for the DeepL API." optional = false python-versions = "<4,>=3.6.2" -groups = ["main"] files = [ {file = "deepl-1.21.0-py3-none-any.whl", hash = "sha256:f9cb882b2cee4b0a28bc648e5af27f357e5e8ad5dad1d4a40cb23c754c2be628"}, {file = "deepl-1.21.0.tar.gz", hash = "sha256:fae768ba0cafbfcc7de3fcec58e2eafd45d0c917df9385f9bde2126968c35a9e"}, @@ -976,7 +941,6 @@ version = "0.7.1" description = "XML bomb protection for Python stdlib modules" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] files = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, @@ -988,7 +952,6 @@ version = "1.2.15" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -groups = ["main"] files = [ {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, @@ -1006,7 +969,6 @@ version = "20241021" description = "Repackaging of Google's Diff Match and Patch libraries." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "diff_match_patch-20241021-py3-none-any.whl", hash = "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782"}, {file = "diff_match_patch-20241021.tar.gz", hash = "sha256:beae57a99fa48084532935ee2968b8661db861862ec82c6f21f4acdd6d835073"}, @@ -1021,7 +983,6 @@ version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -1033,7 +994,6 @@ version = "4.0.1" description = "Authentication and Registration in Django Rest Framework" optional = false python-versions = ">=3.5" -groups = ["main"] files = [ {file = "dj-rest-auth-4.0.1.tar.gz", hash = "sha256:ec87f934c83b520217399f4793506e36cccccc84e899623510ee9c7289f80573"}, ] @@ -1051,7 +1011,6 @@ version = "5.0.11" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" -groups = ["main", "typing"] files = [ {file = "Django-5.0.11-py3-none-any.whl", hash = "sha256:09e8128f717266bf382d82ffa4933f13da05d82579abf008ede86acb15dec88b"}, {file = "Django-5.0.11.tar.gz", hash = "sha256:e7d98fa05ce09cb3e8d5ad6472fb602322acd1740bfdadc29c8404182d664f65"}, @@ -1072,7 +1031,6 @@ version = "7.1" description = "A slick ORM cache with automatic granular event-driven invalidation for Django." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "django_cacheops-7.1-py2.py3-none-any.whl", hash = "sha256:7d5e0f42e41ab4a8052130d33d9f3b26c47bef944ef2df0a64db92d70e51d87e"}, {file = "django_cacheops-7.1.tar.gz", hash = "sha256:ec079abb968557321ee208c6274820231820f98ca6377dda971f04981bc2ab52"}, @@ -1089,7 +1047,6 @@ version = "2.7.0" description = "Database-backed Periodic Tasks." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "django_celery_beat-2.7.0-py3-none-any.whl", hash = "sha256:851c680d8fbf608ca5fecd5836622beea89fa017bc2b3f94a5b8c648c32d84b1"}, {file = "django_celery_beat-2.7.0.tar.gz", hash = "sha256:8482034925e09b698c05ad61c36ed2a8dbc436724a3fe119215193a4ca6dc967"}, @@ -1109,7 +1066,6 @@ version = "4.6.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "django_cors_headers-4.6.0-py3-none-any.whl", hash = "sha256:8edbc0497e611c24d5150e0055d3b178c6534b8ed826fb6f53b21c63f5d48ba3"}, {file = "django_cors_headers-4.6.0.tar.gz", hash = "sha256:14d76b4b4c8d39375baeddd89e4f08899051eeaf177cb02a29bd6eae8cf63aa8"}, @@ -1125,7 +1081,6 @@ version = "23.5" description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "django-filter-23.5.tar.gz", hash = "sha256:67583aa43b91fe8c49f74a832d95f4d8442be628fd4c6d65e9f811f5153a4e5c"}, {file = "django_filter-23.5-py3-none-any.whl", hash = "sha256:99122a201d83860aef4fe77758b69dda913e874cc5e0eaa50a86b0b18d708400"}, @@ -1140,7 +1095,6 @@ version = "4.3.4" description = "Django application and library for importing and exporting data with included admin integration." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "django_import_export-4.3.4-py3-none-any.whl", hash = "sha256:9b56e847ddcb22c0bfbb508bc3668f8b33f95d888002e931c4039d0466270230"}, {file = "django_import_export-4.3.4.tar.gz", hash = "sha256:9ba43ced4fefae614ee7e30da8fdb55d6fcf0450489e7d290299672e0830a436"}, @@ -1168,7 +1122,6 @@ version = "3.8.0" description = "The django-minio-backend provides a wrapper around the MinIO Python Library." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "django_minio_backend-3.8.0-py3-none-any.whl", hash = "sha256:1e5aa883d1df2694843ab79c9646af3a5d79aa1712732128e0d84e441d8d5057"}, {file = "django_minio_backend-3.8.0.tar.gz", hash = "sha256:67425eed262d64425beb25183980dac1abf7285af8f68c444a2f820cf0b120d7"}, @@ -1184,7 +1137,6 @@ version = "1.3.0" description = "Django Ninja - Fast Django REST framework" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "django_ninja-1.3.0-py3-none-any.whl", hash = "sha256:f58096b6c767d1403dfd6c49743f82d780d7b9688d9302ecab316ac1fa6131bb"}, {file = "django_ninja-1.3.0.tar.gz", hash = "sha256:5b320e2dc0f41a6032bfa7e1ebc33559ae1e911a426f0c6be6674a50b20819be"}, @@ -1205,7 +1157,6 @@ version = "2.4.0" description = "OAuth2 Provider for Django" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "django_oauth_toolkit-2.4.0-py3-none-any.whl", hash = "sha256:4931d6bf64b6aee32a42f989f218769d1876f3daa53c6bf883d8ab793fb302ee"}, {file = "django_oauth_toolkit-2.4.0.tar.gz", hash = "sha256:8975eaf697413a8d54208ee068bc5ad6d1ed76f1df84e4882fbb25e7e6966e1b"}, @@ -1224,7 +1175,6 @@ version = "3.7.4" description = "Allows Django models to be ordered and provides a simple admin interface for reordering them." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "django-ordered-model-3.7.4.tar.gz", hash = "sha256:f258b9762525c00a53009e82f8b8bf2a3aa315e8b453e281e8fdbbfe2b8cb3ba"}, {file = "django_ordered_model-3.7.4-py3-none-any.whl", hash = "sha256:dfcd3183fe0749dad1c9971cba1d6240ce7328742a30ddc92feca41107bb241d"}, @@ -1236,7 +1186,6 @@ version = "3.1.0" description = "Seamless polymorphic inheritance for Django models" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "django-polymorphic-3.1.0.tar.gz", hash = "sha256:d6955b5308bf6e41dcb22ba7c96f00b51dfa497a8a5ab1e9c06c7951bf417bf8"}, {file = "django_polymorphic-3.1.0-py3-none-any.whl", hash = "sha256:08bc4f4f4a773a19b2deced5a56deddd1ef56ebd15207bf4052e2901c25ef57e"}, @@ -1251,7 +1200,6 @@ version = "2.3.1" description = "Django middlewares to monitor your application with Prometheus.io." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "django-prometheus-2.3.1.tar.gz", hash = "sha256:f9c8b6c780c9419ea01043c63a437d79db2c33353451347894408184ad9c3e1e"}, {file = "django_prometheus-2.3.1-py2.py3-none-any.whl", hash = "sha256:cf9b26f7ba2e4568f08f8f91480a2882023f5908579681bcf06a4d2465f12168"}, @@ -1266,7 +1214,6 @@ version = "5.4.0" description = "Full featured redis cache backend for Django." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42"}, {file = "django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b"}, @@ -1285,7 +1232,6 @@ version = "4.2.7" description = "Mypy stubs for Django" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"}, {file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"}, @@ -1307,7 +1253,6 @@ version = "5.1.2" description = "Monkey-patching and extensions for django-stubs" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "django_stubs_ext-5.1.2-py3-none-any.whl", hash = "sha256:6c559214538d6a26f631ca638ddc3251a0a891d607de8ce01d23d3201ad8ad6c"}, {file = "django_stubs_ext-5.1.2.tar.gz", hash = "sha256:421c0c3025a68e3ab8e16f065fad9ba93335ecefe2dd92a0cff97a665680266c"}, @@ -1323,7 +1268,6 @@ version = "7.1" description = "A Django app providing DB, form, and REST framework fields for zoneinfo and pytz timezone objects." optional = false python-versions = "<4.0,>=3.8" -groups = ["main"] files = [ {file = "django_timezone_field-7.1-py3-none-any.whl", hash = "sha256:93914713ed882f5bccda080eda388f7006349f25930b6122e9b07bf8db49c4b4"}, {file = "django_timezone_field-7.1.tar.gz", hash = "sha256:b3ef409d88a2718b566fabe10ea996f2838bc72b22d3a2900c0aa905c761380c"}, @@ -1338,7 +1282,6 @@ version = "3.15.2" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, @@ -1353,7 +1296,6 @@ version = "5.4.0" description = "A minimal JSON Web Token authentication plugin for Django REST Framework" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "djangorestframework_simplejwt-5.4.0-py3-none-any.whl", hash = "sha256:7aec953db9ed4163430c16d086eecb0f028f814ce6bba62b06c25919261e9077"}, {file = "djangorestframework_simplejwt-5.4.0.tar.gz", hash = "sha256:cccecce1a0e1a4a240fae80da73e5fc23055bababb8b67de88fa47cd36822320"}, @@ -1378,7 +1320,6 @@ version = "3.14.5" description = "PEP-484 stubs for django-rest-framework" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"}, {file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"}, @@ -1396,24 +1337,12 @@ compatible-mypy = ["django-stubs[compatible-mypy]", "mypy (>=1.7.0,<1.8.0)"] coreapi = ["coreapi (>=2.0.0)"] markdown = ["types-Markdown (>=0.1.5)"] -[[package]] -name = "docx2txt" -version = "0.8" -description = "A pure python-based utility to extract text and images from docx files." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "docx2txt-0.8.tar.gz", hash = "sha256:2c06d98d7cfe2d3947e5760a57d924e3ff07745b379c8737723922e7009236e5"}, -] - [[package]] name = "drf-social-oauth2" version = "2.1.0" description = "drf-social-oauth2 is a frameworks meant to be used with Django and Django Rest Framework." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "drf-social-oauth2-2.1.0.tar.gz", hash = "sha256:6ef656dbca4944ba1ad40299ecaa36532f9dab71ba8a10a2f03b1c7301d4f067"}, {file = "drf_social_oauth2-2.1.0-py3-none-any.whl", hash = "sha256:51293ae18496642fb3abf932db847dd1098dbfc8a606af6f3fcfb8d73940de36"}, @@ -1431,7 +1360,6 @@ version = "0.27.2" description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "drf-spectacular-0.27.2.tar.gz", hash = "sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981"}, {file = "drf_spectacular-0.27.2-py3-none-any.whl", hash = "sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b"}, @@ -1456,7 +1384,6 @@ version = "2024.12.1" description = "Serve self-contained distribution builds of Swagger UI and Redoc with Django" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "drf_spectacular_sidecar-2024.12.1-py3-none-any.whl", hash = "sha256:e30821d150d29294f3be2018aab31b55cd724158e9e690b51a215264751aa8c7"}, {file = "drf_spectacular_sidecar-2024.12.1.tar.gz", hash = "sha256:6be31df38bcf95681224b6550faa9344ee6dd5360dcf2b44afcc3f7460385613"}, @@ -1471,7 +1398,6 @@ version = "0.19.0" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" -groups = ["main"] files = [ {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, @@ -1490,7 +1416,6 @@ version = "9.5.0" description = "simplified environment variable parsing" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, @@ -1512,7 +1437,6 @@ version = "2.0.0" description = "An implementation of lxml.xmlfile for the standard library" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, @@ -1524,7 +1448,6 @@ version = "3.3.1" description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "factory_boy-3.3.1-py2.py3-none-any.whl", hash = "sha256:7b1113c49736e1e9995bc2a18f4dbf2c52cf0f841103517010b1d825712ce3ca"}, {file = "factory_boy-3.3.1.tar.gz", hash = "sha256:8317aa5289cdfc45f9cae570feb07a6177316c82e34d14df3c2e1f22f26abef0"}, @@ -1537,53 +1460,12 @@ Faker = ">=0.7.0" dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "mongomock", "mypy", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] -[[package]] -name = "faiss-cpu" -version = "1.10.0" -description = "A library for efficient similarity search and clustering of dense vectors." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "faiss_cpu-1.10.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6693474be296a7142ade1051ea18e7d85cedbfdee4b7eac9c52f83fed0467855"}, - {file = "faiss_cpu-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70ebe60a560414dc8dd6cfe8fed105c8f002c0d11f765f5adfe8d63d42c0467f"}, - {file = "faiss_cpu-1.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:74c5712d4890f15c661ab7b1b75867812e9596e1469759956fad900999bedbb5"}, - {file = "faiss_cpu-1.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:473d158fbd638d6ad5fb64469ba79a9f09d3494b5f4e8dfb4f40ce2fc335dca4"}, - {file = "faiss_cpu-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:dcd0cb2ec84698cbe3df9ed247d2392f09bda041ad34b92d38fa916cd019ad4b"}, - {file = "faiss_cpu-1.10.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8ff6924b0f00df278afe70940ae86302066466580724c2f3238860039e9946f1"}, - {file = "faiss_cpu-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb80b530a9ded44a7d4031a7355a237aaa0ff1f150c1176df050e0254ea5f6f6"}, - {file = "faiss_cpu-1.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7a9fef4039ed877d40e41d5563417b154c7f8cd57621487dad13c4eb4f32515f"}, - {file = "faiss_cpu-1.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:49b6647aa9e159a2c4603cbff2e1b313becd98ad6e851737ab325c74fe8e0278"}, - {file = "faiss_cpu-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:6f8c0ef8b615c12c7bf612bd1fc51cffa49c1ddaa6207c6981f01ab6782e6b3b"}, - {file = "faiss_cpu-1.10.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:2aca486fe2d680ea64a18d356206c91ff85db99fd34c19a757298c67c23262b1"}, - {file = "faiss_cpu-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1108a4059c66c37c403183e566ca1ed0974a6af7557c92d49207639aab661bc"}, - {file = "faiss_cpu-1.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:449f3eb778d6d937e01a16a3170de4bb8aabfe87c7cb479b458fb790276310c5"}, - {file = "faiss_cpu-1.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9899c340f92bd94071d6faf4bef0ccb5362843daea42144d4ba857a2a1f67511"}, - {file = "faiss_cpu-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:345a52dbfa980d24b93c94410eadf82d1eef359c6a42e5e0768cca96539f1c3c"}, - {file = "faiss_cpu-1.10.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:cb8473d69c3964c1bf3f8eb3e04287bb3275f536e6d9635ef32242b5f506b45d"}, - {file = "faiss_cpu-1.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82ca5098de694e7b8495c1a8770e2c08df6e834922546dad0ae1284ff519ced6"}, - {file = "faiss_cpu-1.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:035e4d797e2db7fc0d0c90531d4a655d089ad5d1382b7a49358c1f2307b3a309"}, - {file = "faiss_cpu-1.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e02af3696a6b9e1f9072e502f48095a305de2163c42ceb1f6f6b1db9e7ffe574"}, - {file = "faiss_cpu-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:e71f7e24d5b02d3a51df47b77bd10f394a1b48a8331d5c817e71e9e27a8a75ac"}, - {file = "faiss_cpu-1.10.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:3118b5d7680b0e0a3cd64b3d29389d8384de4298739504fc661b658109540b4b"}, - {file = "faiss_cpu-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71c5860c860df2320299f9e4f2ca1725beb559c04acb1cf961ed24e6218277a"}, - {file = "faiss_cpu-1.10.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2f15b7957d474391fc63f02bfb8011b95317a580e4d9bd70c276f4bc179a17b3"}, - {file = "faiss_cpu-1.10.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dadbbb834ddc34ca7e21411811833cebaae4c5a86198dd7c2a349dbe4e7e0398"}, - {file = "faiss_cpu-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb77a6a5f304890c23ffb4c566bc819c0e0cf34370b20ddff02477f2bbbaf7a3"}, - {file = "faiss_cpu-1.10.0.tar.gz", hash = "sha256:5bdca555f24bc036f4d67f8a5a4d6cc91b8d2126d4e78de496ca23ccd46e479d"}, -] - -[package.dependencies] -numpy = ">=1.25.0,<3.0" -packaging = "*" - [[package]] name = "faker" version = "33.3.1" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "Faker-33.3.1-py3-none-any.whl", hash = "sha256:ac4cf2f967ce02c898efa50651c43180bd658a7707cfd676fcc5410ad1482c03"}, {file = "faker-33.3.1.tar.gz", hash = "sha256:49dde3b06a5602177bc2ad013149b6f60a290b7154539180d37b6f876ae79b20"}, @@ -1599,7 +1481,6 @@ version = "0.3.0" description = "A fast native implementation of diff algorithm with a pure python fallback" optional = false python-versions = "*" -groups = ["test"] files = [ {file = "fastdiff-0.3.0-py2.py3-none-any.whl", hash = "sha256:ca5f61f6ddf5a1564ddfd98132ad28e7abe4a88a638a8b014a2214f71e5918ec"}, {file = "fastdiff-0.3.0.tar.gz", hash = "sha256:4dfa09c47832a8c040acda3f1f55fc0ab4d666f0e14e6951e6da78d59acd945a"}, @@ -1615,7 +1496,6 @@ version = "1.2.0" description = "Infer file type and MIME type of any file/buffer. No external dependencies." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"}, {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, @@ -1627,7 +1507,6 @@ version = "1.5.1" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, @@ -1642,7 +1521,6 @@ version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, @@ -1744,7 +1622,6 @@ version = "2.0" description = "A fancy and practical functional tools" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0"}, {file = "funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb"}, @@ -1756,7 +1633,6 @@ version = "24.11.1" description = "Coroutine-based network library" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "gevent-24.11.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:92fe5dfee4e671c74ffaa431fd7ffd0ebb4b339363d24d0d944de532409b935e"}, {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7bfcfe08d038e1fa6de458891bca65c1ada6d145474274285822896a858c870"}, @@ -1813,24 +1689,19 @@ test = ["cffi (>=1.17.1)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idn [[package]] name = "google-ai-generativelanguage" -version = "0.6.15" +version = "0.4.0" description = "Google Ai Generativelanguage API client library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ - {file = "google_ai_generativelanguage-0.6.15-py3-none-any.whl", hash = "sha256:5a03ef86377aa184ffef3662ca28f19eeee158733e45d7947982eb953c6ebb6c"}, - {file = "google_ai_generativelanguage-0.6.15.tar.gz", hash = "sha256:8f6d9dc4c12b065fe2d0289026171acea5183ebf2d0b11cefe12f3821e159ec3"}, + {file = "google-ai-generativelanguage-0.4.0.tar.gz", hash = "sha256:c8199066c08f74c4e91290778329bb9f357ba1ea5d6f82de2bc0d10552bf4f8c"}, + {file = "google_ai_generativelanguage-0.4.0-py3-none-any.whl", hash = "sha256:e4c425376c1ee26c78acbc49a24f735f90ebfa81bf1a06495fae509a2433232c"}, ] [package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" -proto-plus = [ - {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, - {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, -] -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" +google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} +proto-plus = ">=1.22.3,<2.0.0dev" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" [[package]] name = "google-api-core" @@ -1838,7 +1709,6 @@ version = "2.24.0" description = "Google API client core library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "google_api_core-2.24.0-py3-none-any.whl", hash = "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9"}, {file = "google_api_core-2.24.0.tar.gz", hash = "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf"}, @@ -1862,32 +1732,12 @@ grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] -[[package]] -name = "google-api-python-client" -version = "2.161.0" -description = "Google API Client Library for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "google_api_python_client-2.161.0-py2.py3-none-any.whl", hash = "sha256:9476a5a4f200bae368140453df40f9cda36be53fa7d0e9a9aac4cdb859a26448"}, - {file = "google_api_python_client-2.161.0.tar.gz", hash = "sha256:324c0cce73e9ea0a0d2afd5937e01b7c2d6a4d7e2579cdb6c384f9699d6c9f37"}, -] - -[package.dependencies] -google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" -google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" -google-auth-httplib2 = ">=0.2.0,<1.0.0" -httplib2 = ">=0.19.0,<1.dev0" -uritemplate = ">=3.0.1,<5" - [[package]] name = "google-auth" version = "2.37.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"}, {file = "google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00"}, @@ -1906,40 +1756,21 @@ pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] requests = ["requests (>=2.20.0,<3.0.0.dev0)"] -[[package]] -name = "google-auth-httplib2" -version = "0.2.0" -description = "Google Authentication Library: httplib2 transport" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, - {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, -] - -[package.dependencies] -google-auth = "*" -httplib2 = ">=0.19.0" - [[package]] name = "google-generativeai" -version = "0.8.4" +version = "0.3.2" description = "Google Generative AI High level API client library and tools." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ - {file = "google_generativeai-0.8.4-py3-none-any.whl", hash = "sha256:e987b33ea6decde1e69191ddcaec6ef974458864d243de7191db50c21a7c5b82"}, + {file = "google_generativeai-0.3.2-py3-none-any.whl", hash = "sha256:8761147e6e167141932dc14a7b7af08f2310dd56668a78d206c19bb8bd85bcd7"}, ] [package.dependencies] -google-ai-generativelanguage = "0.6.15" +google-ai-generativelanguage = "0.4.0" google-api-core = "*" -google-api-python-client = "*" -google-auth = ">=2.15.0" +google-auth = "*" protobuf = "*" -pydantic = "*" tqdm = "*" typing-extensions = "*" @@ -1952,7 +1783,6 @@ version = "1.66.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, @@ -1970,7 +1800,6 @@ version = "4.0.0" description = "Free Google Translate API for Python. Translates totally free of charge." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "googletrans-py-4.0.0.tar.gz", hash = "sha256:487963819ced88f1f81d848786e2d3e02544833d161cc5edb178a1f74bde6d98"}, ] @@ -1985,8 +1814,6 @@ version = "3.1.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" -groups = ["main"] -markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or platform_python_implementation == \"CPython\"" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -2073,7 +1900,6 @@ version = "1.69.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "grpcio-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2060ca95a8db295ae828d0fc1c7f38fb26ccd5edf9aa51a0f44251f5da332e97"}, {file = "grpcio-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2e52e107261fd8fa8fa457fe44bfadb904ae869d87c1280bf60f93ecd3e79278"}, @@ -2141,7 +1967,6 @@ version = "1.62.3" description = "Status proto mapping for gRPC" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485"}, {file = "grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8"}, @@ -2158,7 +1983,6 @@ version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -2181,7 +2005,6 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -2193,7 +2016,6 @@ version = "4.1.0" description = "HTTP/2 State-Machine based protocol implementation" optional = false python-versions = ">=3.6.1" -groups = ["main"] files = [ {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, @@ -2209,7 +2031,6 @@ version = "4.0.0" description = "Pure-Python HPACK header compression" optional = false python-versions = ">=3.6.1" -groups = ["main"] files = [ {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, @@ -2221,7 +2042,6 @@ version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -2237,28 +2057,12 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<1.0)"] -[[package]] -name = "httplib2" -version = "0.22.0" -description = "A comprehensive HTTP client library." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -files = [ - {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, - {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, -] - -[package.dependencies] -pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} - [[package]] name = "httptools" version = "0.6.4" description = "A collection of framework independent HTTP protocol utils." optional = false python-versions = ">=3.8.0" -groups = ["main"] files = [ {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, @@ -2314,7 +2118,6 @@ version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, @@ -2339,7 +2142,6 @@ version = "0.4.0" description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, @@ -2351,7 +2153,6 @@ version = "6.0.1" description = "HTTP/2 framing layer for Python" optional = false python-versions = ">=3.6.1" -groups = ["main"] files = [ {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, @@ -2363,7 +2164,6 @@ version = "21.0.0" description = "A featureful, immutable, and correct URL for Python." optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] files = [ {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, @@ -2378,7 +2178,6 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main", "typing"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -2393,7 +2192,6 @@ version = "24.7.2" description = "A small library that versions your Python projects." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"}, {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"}, @@ -2411,7 +2209,6 @@ version = "0.5.1" description = "A port of Ruby on Rails inflector to Python" optional = false python-versions = ">=3.5" -groups = ["main", "test"] files = [ {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, @@ -2423,7 +2220,6 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -2435,7 +2231,6 @@ version = "0.8.2" description = "Fast iterable JSON parser." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "jiter-0.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ca8577f6a413abe29b079bc30f907894d7eb07a865c4df69475e868d73e71c7b"}, {file = "jiter-0.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b25bd626bde7fb51534190c7e3cb97cee89ee76b76d7585580e22f34f5e3f393"}, @@ -2521,7 +2316,6 @@ version = "1.33" description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -groups = ["main"] files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -2536,7 +2330,6 @@ version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -2548,7 +2341,6 @@ version = "4.23.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, @@ -2570,7 +2362,6 @@ version = "2024.10.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, @@ -2585,7 +2376,6 @@ version = "1.5.6" description = "Implementation of JOSE Web standards" optional = false python-versions = ">= 3.8" -groups = ["main"] files = [ {file = "jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789"}, {file = "jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039"}, @@ -2601,7 +2391,6 @@ version = "5.4.2" description = "Messaging library for Python." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, @@ -2631,144 +2420,142 @@ zookeeper = ["kazoo (>=2.8.0)"] [[package]] name = "langchain" -version = "0.3.19" +version = "0.1.20" description = "Building applications with LLMs through composability" optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] +python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain-0.3.19-py3-none-any.whl", hash = "sha256:1e16d97db9106640b7de4c69f8f5ed22eeda56b45b9241279e83f111640eff16"}, - {file = "langchain-0.3.19.tar.gz", hash = "sha256:b96f8a445f01d15d522129ffe77cc89c8468dbd65830d153a676de8f6b899e7b"}, + {file = "langchain-0.1.20-py3-none-any.whl", hash = "sha256:09991999fbd6c3421a12db3c7d1f52d55601fc41d9b2a3ef51aab2e0e9c38da9"}, + {file = "langchain-0.1.20.tar.gz", hash = "sha256:f35c95eed8c8375e02dce95a34f2fd4856a4c98269d6dc34547a23dba5beab7e"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" -langchain-core = ">=0.3.35,<1.0.0" -langchain-text-splitters = ">=0.3.6,<1.0.0" -langsmith = ">=0.1.17,<0.4" -numpy = {version = ">=1.26.2,<3", markers = "python_version >= \"3.12\""} -pydantic = ">=2.7.4,<3.0.0" +dataclasses-json = ">=0.5.7,<0.7" +langchain-community = ">=0.0.38,<0.1" +langchain-core = ">=0.1.52,<0.2.0" +langchain-text-splitters = ">=0.0.1,<0.1" +langsmith = ">=0.1.17,<0.2.0" +numpy = ">=1,<2" +pydantic = ">=1,<3" PyYAML = ">=5.3" requests = ">=2,<3" SQLAlchemy = ">=1.4,<3" -tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10" +tenacity = ">=8.1.0,<9.0.0" [package.extras] -anthropic = ["langchain-anthropic"] -aws = ["langchain-aws"] -cohere = ["langchain-cohere"] -community = ["langchain-community"] -deepseek = ["langchain-deepseek"] -fireworks = ["langchain-fireworks"] -google-genai = ["langchain-google-genai"] -google-vertexai = ["langchain-google-vertexai"] -groq = ["langchain-groq"] -huggingface = ["langchain-huggingface"] -mistralai = ["langchain-mistralai"] -ollama = ["langchain-ollama"] -openai = ["langchain-openai"] -together = ["langchain-together"] -xai = ["langchain-xai"] +azure = ["azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-textanalytics (>=5.3.0,<6.0.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-core (>=1.26.4,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "azure-search-documents (==11.4.0b8)", "openai (<2)"] +clarifai = ["clarifai (>=9.1.0)"] +cli = ["typer (>=0.9.0,<0.10.0)"] +cohere = ["cohere (>=4,<6)"] +docarray = ["docarray[hnswlib] (>=0.32.0,<0.33.0)"] +embeddings = ["sentence-transformers (>=2,<3)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<6)", "couchbase (>=4.1.9,<5.0.0)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "langchain-openai (>=0.0.2,<0.1)", "lxml (>=4.9.3,<6.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "rdflib (==7.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] +javascript = ["esprima (>=4.0.1,<5.0.0)"] +llms = ["clarifai (>=9.1.0)", "cohere (>=4,<6)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (<2)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)"] +openai = ["openai (<2)", "tiktoken (>=0.3.2,<0.6.0)"] +qdrant = ["qdrant-client (>=1.3.1,<2.0.0)"] +text-helpers = ["chardet (>=5.1.0,<6.0.0)"] [[package]] name = "langchain-community" -version = "0.3.18" +version = "0.0.38" description = "Community contributed LangChain integrations." optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] +python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_community-0.3.18-py3-none-any.whl", hash = "sha256:0d4a70144a1750045c4f726f9a43379ed2484178f76e4b8295bcef3a7fdf41d5"}, - {file = "langchain_community-0.3.18.tar.gz", hash = "sha256:fa2889a8f0b2d22b5c306fd1b070c0970e1f11b604bf55fad2f4a1d0bf68a077"}, + {file = "langchain_community-0.0.38-py3-none-any.whl", hash = "sha256:ecb48660a70a08c90229be46b0cc5f6bc9f38f2833ee44c57dfab9bf3a2c121a"}, + {file = "langchain_community-0.0.38.tar.gz", hash = "sha256:127fc4b75bc67b62fe827c66c02e715a730fef8fe69bd2023d466bab06b5810d"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" dataclasses-json = ">=0.5.7,<0.7" -httpx-sse = ">=0.4.0,<1.0.0" -langchain = ">=0.3.19,<1.0.0" -langchain-core = ">=0.3.37,<1.0.0" -langsmith = ">=0.1.125,<0.4" -numpy = {version = ">=1.26.2,<3", markers = "python_version >= \"3.12\""} -pydantic-settings = ">=2.4.0,<3.0.0" +langchain-core = ">=0.1.52,<0.2.0" +langsmith = ">=0.1.0,<0.2.0" +numpy = ">=1,<2" PyYAML = ">=5.3" requests = ">=2,<3" SQLAlchemy = ">=1.4,<3" -tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10" +tenacity = ">=8.1.0,<9.0.0" + +[package.extras] +cli = ["typer (>=0.9.0,<0.10.0)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-ai-documentintelligence (>=1.0.0b1,<2.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-search-documents (==11.4.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.6,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cloudpickle (>=2.0.0)", "cohere (>=4,<5)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "elasticsearch (>=8.12.0,<9.0.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "friendli-client (>=1.2.4,<2.0.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "gradientai (>=1.4.0,<2.0.0)", "hdbcli (>=2.19.21,<3.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "httpx (>=0.24.1,<0.25.0)", "httpx-sse (>=0.4.0,<0.5.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.3,<6.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "nvidia-riva-client (>=2.14.0,<3.0.0)", "oci (>=2.119.1,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "oracle-ads (>=2.9.1,<3.0.0)", "oracledb (>=2.2.0,<3.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "premai (>=0.3.25,<0.4.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pyjwt (>=2.8.0,<3.0.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "rdflib (==7.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "tidb-vector (>=0.0.3,<1.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "tree-sitter (>=0.20.2,<0.21.0)", "tree-sitter-languages (>=1.8.0,<2.0.0)", "upstash-redis (>=0.15.0,<0.16.0)", "vdms (>=0.0.20,<0.0.21)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] [[package]] name = "langchain-core" -version = "0.3.37" +version = "0.1.53" description = "Building applications with LLMs through composability" optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] +python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_core-0.3.37-py3-none-any.whl", hash = "sha256:8202fd6506ce139a3a1b1c4c3006216b1c7fffa40bdd1779f7d2c67f75eb5f79"}, - {file = "langchain_core-0.3.37.tar.gz", hash = "sha256:cda8786e616caa2f68f7cc9e811b9b50e3b63fb2094333318b348e5961a7ea01"}, + {file = "langchain_core-0.1.53-py3-none-any.whl", hash = "sha256:02a88a21e3bd294441b5b741625fa4b53b1c684fd58ba6e5d9028e53cbe8542f"}, + {file = "langchain_core-0.1.53.tar.gz", hash = "sha256:df3773a553b5335eb645827b99a61a7018cea4b11dc45efa2613fde156441cec"}, ] [package.dependencies] jsonpatch = ">=1.33,<2.0" -langsmith = ">=0.1.125,<0.4" -packaging = ">=23.2,<25" -pydantic = [ - {version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""}, - {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, -] +langsmith = ">=0.1.0,<0.2.0" +packaging = ">=23.2,<24.0" +pydantic = ">=1,<3" PyYAML = ">=5.3" -tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0" -typing-extensions = ">=4.7" +tenacity = ">=8.1.0,<9.0.0" + +[package.extras] +extended-testing = ["jinja2 (>=3,<4)"] [[package]] name = "langchain-google-genai" -version = "2.0.9" +version = "0.0.9" description = "An integration package connecting Google's genai package and LangChain" optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] +python-versions = ">=3.9,<4.0" files = [ - {file = "langchain_google_genai-2.0.9-py3-none-any.whl", hash = "sha256:48d8c78c42048d54f40dff333db9d359746644e0feb0e08b5eabdf34ad7149ca"}, - {file = "langchain_google_genai-2.0.9.tar.gz", hash = "sha256:65205089da1f72688a0ed6e7c6914af308b6514ab8038fd8126ecb20f1df234c"}, + {file = "langchain_google_genai-0.0.9-py3-none-any.whl", hash = "sha256:82c0ca9540132a59b09fc38ff249a2dd06f8a587ed37c291a4fe7678d5566d15"}, + {file = "langchain_google_genai-0.0.9.tar.gz", hash = "sha256:466a228032bb06b0c1def822e57cbf2dfe9e4d1cc91dffa473a3025eb760f0ef"}, ] [package.dependencies] -filetype = ">=1.2.0,<2.0.0" -google-generativeai = ">=0.8.0,<0.9.0" -langchain-core = ">=0.3.27,<0.4.0" -pydantic = ">=2,<3" +google-generativeai = ">=0.3.1,<0.4.0" +langchain-core = ">=0.1,<0.2" + +[package.extras] +images = ["pillow (>=10.1.0,<11.0.0)"] [[package]] name = "langchain-openai" -version = "0.3.6" +version = "0.0.2.post1" description = "An integration package connecting OpenAI and LangChain" optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] +python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain_openai-0.3.6-py3-none-any.whl", hash = "sha256:05f0869f6cc963e2ec9e2e54ea1038d9c2af784c67f0e217040dfc918b31649a"}, - {file = "langchain_openai-0.3.6.tar.gz", hash = "sha256:7daf92e1cd98865ab5213ec5bec2cbd6c28f011e250714978b3a99c7e4fc88ce"}, + {file = "langchain_openai-0.0.2.post1-py3-none-any.whl", hash = "sha256:ba468b94c23da9d8ccefe5d5a3c1c65b4b9702292523e53acc689a9110022e26"}, + {file = "langchain_openai-0.0.2.post1.tar.gz", hash = "sha256:f8e78db4a663feeac71d9f036b9422406c199ea3ef4c97d99ff392c93530e073"}, ] [package.dependencies] -langchain-core = ">=0.3.35,<1.0.0" -openai = ">=1.58.1,<2.0.0" -tiktoken = ">=0.7,<1" +langchain-core = ">=0.1.7,<0.2" +numpy = ">=1,<2" +openai = ">=1.6.1,<2.0.0" +tiktoken = ">=0.5.2,<0.6.0" [[package]] name = "langchain-text-splitters" -version = "0.3.6" +version = "0.0.2" description = "LangChain text splitting utilities" optional = false -python-versions = "<4.0,>=3.9" -groups = ["main"] +python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_text_splitters-0.3.6-py3-none-any.whl", hash = "sha256:e5d7b850f6c14259ea930be4a964a65fa95d9df7e1dbdd8bad8416db72292f4e"}, - {file = "langchain_text_splitters-0.3.6.tar.gz", hash = "sha256:c537972f4b7c07451df431353a538019ad9dadff7a1073ea363946cea97e1bee"}, + {file = "langchain_text_splitters-0.0.2-py3-none-any.whl", hash = "sha256:13887f32705862c1e1454213cb7834a63aae57c26fcd80346703a1d09c46168d"}, + {file = "langchain_text_splitters-0.0.2.tar.gz", hash = "sha256:ac8927dc0ba08eba702f6961c9ed7df7cead8de19a9f7101ab2b5ea34201b3c1"}, ] [package.dependencies] -langchain-core = ">=0.3.34,<1.0.0" +langchain-core = ">=0.1.28,<0.3" + +[package.extras] +extended-testing = ["beautifulsoup4 (>=4.12.3,<5.0.0)", "lxml (>=4.9.3,<6.0)"] [[package]] name = "langchainhub" @@ -2776,7 +2563,6 @@ version = "0.1.21" description = "The LangChain Hub API client" optional = false python-versions = "<4.0,>=3.8.1" -groups = ["main"] files = [ {file = "langchainhub-0.1.21-py3-none-any.whl", hash = "sha256:1cc002dc31e0d132a776afd044361e2b698743df5202618cf2bad399246b895f"}, {file = "langchainhub-0.1.21.tar.gz", hash = "sha256:723383b3964a47dbaea6ad5d0ef728accefbc9d2c07480e800bdec43510a8c10"}, @@ -2793,7 +2579,6 @@ version = "0.0.46" description = "" optional = false python-versions = ">=3.8.1,<4.0.0" -groups = ["main"] files = [ {file = "langserve-0.0.46-py3-none-any.whl", hash = "sha256:0720710b9d545394f07394f95a9f2ad294fb33359945522e7dc3b9dcdbde1d70"}, {file = "langserve-0.0.46.tar.gz", hash = "sha256:a914c65d9fed356361fdd0ed6c23f765d4ea641eb0173eafd0a82fce9227229e"}, @@ -2817,7 +2602,6 @@ version = "0.1.147" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = "<4.0,>=3.8.1" -groups = ["main"] files = [ {file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"}, {file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"}, @@ -2842,7 +2626,6 @@ version = "5.3.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, @@ -2997,7 +2780,6 @@ version = "3.25.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "marshmallow-3.25.1-py3-none-any.whl", hash = "sha256:ec5d00d873ce473b7f2ffcb7104286a376c354cab0c2fa12f5573dab03e87210"}, {file = "marshmallow-3.25.1.tar.gz", hash = "sha256:f4debda3bb11153d81ac34b0d582bf23053055ee11e791b54b4b35493468040a"}, @@ -3017,7 +2799,6 @@ version = "7.2.14" description = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "minio-7.2.14-py3-none-any.whl", hash = "sha256:868dfe907e1702ce4bec86df1f3ced577a73ca85f344ef898d94fe2b5237f8c1"}, {file = "minio-7.2.14.tar.gz", hash = "sha256:f5c24bf236fefd2edc567cd4455dc49a11ad8ff7ac984bb031b849d82f01222a"}, @@ -3036,7 +2817,6 @@ version = "1.1.0" description = "MessagePack serializer" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, @@ -3110,7 +2890,6 @@ version = "6.1.0" description = "multidict implementation" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, @@ -3212,7 +2991,6 @@ version = "1.47.0" description = "read and write audio tags for many formats" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"}, {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, @@ -3224,7 +3002,6 @@ version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, @@ -3283,7 +3060,6 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" -groups = ["main", "typing"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -3295,7 +3071,6 @@ version = "1.3.0" description = "A network address manipulation library for Python" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe"}, {file = "netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a"}, @@ -3310,7 +3085,6 @@ version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, @@ -3356,7 +3130,6 @@ version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, @@ -3373,7 +3146,6 @@ version = "1.59.7" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "openai-1.59.7-py3-none-any.whl", hash = "sha256:cfa806556226fa96df7380ab2e29814181d56fea44738c2b0e581b462c268692"}, {file = "openai-1.59.7.tar.gz", hash = "sha256:043603def78c00befb857df9f0a16ee76a3af5984ba40cb7ee5e2f40db4646bf"}, @@ -3399,7 +3171,6 @@ version = "3.1.5" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, @@ -3414,7 +3185,6 @@ version = "3.10.14" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "orjson-3.10.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:849ea7845a55f09965826e816cdc7689d6cf74fe9223d79d758c714af955bcb6"}, {file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5947b139dfa33f72eecc63f17e45230a97e741942955a6c9e650069305eb73d"}, @@ -3499,7 +3269,6 @@ version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" -groups = ["main", "test"] files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, @@ -3511,7 +3280,6 @@ version = "10.4.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, @@ -3609,7 +3377,6 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -3625,7 +3392,6 @@ version = "0.21.1" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, @@ -3640,7 +3406,6 @@ version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" -groups = ["main"] files = [ {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, @@ -3655,7 +3420,6 @@ version = "0.2.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, @@ -3747,7 +3511,6 @@ version = "1.25.0" description = "Beautiful, Pythonic protocol buffers." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961"}, {file = "proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91"}, @@ -3765,7 +3528,6 @@ version = "4.25.5" description = "" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "protobuf-4.25.5-cp310-abi3-win32.whl", hash = "sha256:5e61fd921603f58d2f5acb2806a929b4675f8874ff5f330b7d6f7e2e784bbcd8"}, {file = "protobuf-4.25.5-cp310-abi3-win_amd64.whl", hash = "sha256:4be0571adcbe712b282a330c6e89eae24281344429ae95c6d85e79e84780f5ea"}, @@ -3786,7 +3548,6 @@ version = "1.0.2" description = "psycopg2 integration with coroutine libraries" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "psycogreen-1.0.2.tar.gz", hash = "sha256:c429845a8a49cf2f76b71265008760bcd7c7c77d80b806db4dc81116dbcd130d"}, ] @@ -3797,7 +3558,6 @@ version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, @@ -3846,6 +3606,7 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -3874,7 +3635,6 @@ version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -3886,7 +3646,6 @@ version = "0.4.1" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, @@ -3901,7 +3660,6 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -3913,7 +3671,6 @@ version = "3.21.0" description = "Cryptographic library for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -groups = ["main"] files = [ {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, @@ -3955,7 +3712,6 @@ version = "2.10.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, @@ -3976,7 +3732,6 @@ version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -4083,34 +3838,12 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pydantic-settings" -version = "2.7.1" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"}, - {file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"}, -] - -[package.dependencies] -pydantic = ">=2.7.0" -python-dotenv = ">=0.21.0" - -[package.extras] -azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - [[package]] name = "pyjwt" version = "2.10.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, @@ -4128,7 +3861,6 @@ version = "25.0.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90"}, {file = "pyopenssl-25.0.0.tar.gz", hash = "sha256:cd2cef799efa3936bb08e8ccb9433a575722b9dd986023f1cabc4ae64e9dac16"}, @@ -4142,40 +3874,12 @@ typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] -[[package]] -name = "pypandoc" -version = "1.15" -description = "Thin wrapper for pandoc." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "pypandoc-1.15-py3-none-any.whl", hash = "sha256:4ededcc76c8770f27aaca6dff47724578428eca84212a31479403a9731fc2b16"}, - {file = "pypandoc-1.15.tar.gz", hash = "sha256:ea25beebe712ae41d63f7410c08741a3cab0e420f6703f95bc9b3a749192ce13"}, -] - -[[package]] -name = "pyparsing" -version = "3.2.1" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, - {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pypdf2" version = "3.0.1" description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440"}, {file = "pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928"}, @@ -4194,7 +3898,6 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -4215,7 +3918,6 @@ version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, @@ -4234,7 +3936,6 @@ version = "4.9.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"}, {file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"}, @@ -4253,7 +3954,6 @@ version = "2.7.0" description = "Factory Boy support for pytest." optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pytest_factoryboy-2.7.0-py3-none-any.whl", hash = "sha256:bf3222db22d954fbf46f4bff902a0a8d82f3fc3594a47c04bbdc0546ff4c59a6"}, {file = "pytest_factoryboy-2.7.0.tar.gz", hash = "sha256:67fc54ec8669a3feb8ac60094dd57cd71eb0b20b2c319d2957873674c776a77b"}, @@ -4272,7 +3972,6 @@ version = "0.4.2" description = "Wrap tests with fixtures in freeze_time" optional = false python-versions = "*" -groups = ["test"] files = [ {file = "pytest-freezegun-0.4.2.zip", hash = "sha256:19c82d5633751bf3ec92caa481fb5cffaac1787bd485f0df6436fd6242176949"}, {file = "pytest_freezegun-0.4.2-py2.py3-none-any.whl", hash = "sha256:5318a6bfb8ba4b709c8471c94d0033113877b3ee02da5bfcd917c1889cde99a7"}, @@ -4288,7 +3987,6 @@ version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, @@ -4306,7 +4004,6 @@ version = "3.2.0" description = "Python Crontab API" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "python_crontab-3.2.0-py3-none-any.whl", hash = "sha256:82cb9b6a312d41ff66fd3caf3eed7115c28c195bfb50711bc2b4b9592feb9fe5"}, {file = "python_crontab-3.2.0.tar.gz", hash = "sha256:40067d1dd39ade3460b2ad8557c7651514cd3851deffff61c5c60e1227c5c36b"}, @@ -4325,7 +4022,6 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "test"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -4340,7 +4036,6 @@ version = "1.1.2" description = "Create, read, and update Microsoft Word .docx files." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "python_docx-1.1.2-py3-none-any.whl", hash = "sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe"}, {file = "python_docx-1.1.2.tar.gz", hash = "sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd"}, @@ -4356,7 +4051,6 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -4371,7 +4065,6 @@ version = "3.3.0" description = "JOSE implementation in Python" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, @@ -4394,7 +4087,6 @@ version = "3.2.0" description = "OpenID support for modern servers and consumers." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"}, {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"}, @@ -4413,7 +4105,6 @@ version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -4425,7 +4116,6 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -4488,7 +4178,6 @@ version = "5.2.1" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, @@ -4504,7 +4193,6 @@ version = "0.35.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, @@ -4520,7 +4208,6 @@ version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, @@ -4620,21 +4307,22 @@ files = [ [[package]] name = "replicate" -version = "1.0.4" +version = "0.10.0" description = "Python client for Replicate" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ - {file = "replicate-1.0.4-py3-none-any.whl", hash = "sha256:f568f6271ff715067901b6094c23c37373bbcfd7de0ff9b85e9c9ead567e09e7"}, - {file = "replicate-1.0.4.tar.gz", hash = "sha256:f718601863ef1f419aa7dcdab1ea8770ba5489b571b86edf840cd506d68758ef"}, + {file = "replicate-0.10.0-py3-none-any.whl", hash = "sha256:71464d62259b22a17f5383e83ebaf158cace69ba0153a9de21061283ac2c3a4b"}, + {file = "replicate-0.10.0.tar.gz", hash = "sha256:f714944be0c65eef7b3ebf740eb132d573a52e811a02f0031b9f1ae077a50696"}, ] [package.dependencies] -httpx = ">=0.21.0,<1" packaging = "*" -pydantic = ">1.10.7" -typing_extensions = ">=4.5.0" +pydantic = ">1" +requests = ">2" + +[package.extras] +dev = ["black", "mypy", "pytest", "responses", "ruff"] [[package]] name = "requests" @@ -4642,7 +4330,6 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["main", "typing"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -4664,7 +4351,6 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" -groups = ["main"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -4683,7 +4369,6 @@ version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, @@ -4698,7 +4383,6 @@ version = "0.22.3" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, @@ -4811,7 +4495,6 @@ version = "4.9" description = "Pure-Python RSA implementation" optional = false python-versions = ">=3.6,<4" -groups = ["main"] files = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, @@ -4822,30 +4505,28 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.9.9" +version = "0.4.10" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367"}, - {file = "ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7"}, - {file = "ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb"}, - {file = "ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0"}, - {file = "ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17"}, - {file = "ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1"}, - {file = "ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57"}, - {file = "ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e"}, - {file = "ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1"}, - {file = "ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1"}, - {file = "ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf"}, - {file = "ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933"}, +files = [ + {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, + {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, + {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, + {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, + {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, + {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, + {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, + {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, ] [[package]] @@ -4854,7 +4535,6 @@ version = "2.20.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "sentry_sdk-2.20.0-py2.py3-none-any.whl", hash = "sha256:c359a1edf950eb5e80cffd7d9111f3dbeef57994cb4415df37d39fda2cf22364"}, {file = "sentry_sdk-2.20.0.tar.gz", hash = "sha256:afa82713a92facf847df3c6f63cec71eb488d826a50965def3d7722aa6f0fdab"}, @@ -4911,7 +4591,6 @@ version = "24.2.0" description = "Service identity verification for pyOpenSSL & cryptography." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85"}, {file = "service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09"}, @@ -4936,7 +4615,6 @@ version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, @@ -4957,7 +4635,6 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "test"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -4969,7 +4646,6 @@ version = "0.6.0" description = "Snapshot testing for pytest, unittest, Django, and Nose" optional = false python-versions = "*" -groups = ["test"] files = [ {file = "snapshottest-0.6.0-py2.py3-none-any.whl", hash = "sha256:9b177cffe0870c589df8ddbee0a770149c5474b251955bdbde58b7f32a4ec429"}, {file = "snapshottest-0.6.0.tar.gz", hash = "sha256:bbcaf81d92d8e330042e5c928e13d9f035e99e91b314fe55fda949c2f17b653c"}, @@ -4991,7 +4667,6 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -5003,7 +4678,6 @@ version = "5.4.2" description = "Python Social Authentication, Django integration." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "social-auth-app-django-5.4.2.tar.gz", hash = "sha256:c8832c6cf13da6ad76f5613bcda2647d89ae7cfbc5217fadd13477a3406feaa8"}, {file = "social_auth_app_django-5.4.2-py3-none-any.whl", hash = "sha256:0c041a31707921aef9a930f143183c65d8c7b364381364a50f3f7c6fcc9d62f6"}, @@ -5019,7 +4693,6 @@ version = "4.5.4" description = "Python social authentication made simple." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "social-auth-core-4.5.4.tar.gz", hash = "sha256:d3dbeb0999ffd0e68aa4bd73f2ac698a18133fd11b3fc890e1366f18c8889fac"}, {file = "social_auth_core-4.5.4-py3-none-any.whl", hash = "sha256:33cf970a623c442376f9d4a86fb187579e4438649daa5b5be993d05e74d7b2db"}, @@ -5046,7 +4719,6 @@ version = "2.0.37" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, @@ -5142,7 +4814,6 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main", "typing"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -5158,7 +4829,6 @@ version = "3.7.0" description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "tablib-3.7.0-py3-none-any.whl", hash = "sha256:9a6930037cfe0f782377963ca3f2b1dae3fd4cdbf0883848f22f1447e7bb718b"}, {file = "tablib-3.7.0.tar.gz", hash = "sha256:f9db84ed398df5109bd69c11d46613d16cc572fb9ad3213f10d95e2b5f12c18e"}, @@ -5179,7 +4849,6 @@ version = "8.5.0" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"}, {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"}, @@ -5195,7 +4864,6 @@ version = "2.5.0" description = "ANSI color formatting for output in terminal" optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, @@ -5206,43 +4874,47 @@ tests = ["pytest", "pytest-cov"] [[package]] name = "tiktoken" -version = "0.9.0" +version = "0.5.2" description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382"}, - {file = "tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108"}, - {file = "tiktoken-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0968d5beeafbca2a72c595e8385a1a1f8af58feaebb02b227229b69ca5357fd"}, - {file = "tiktoken-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a5fb085a6a3b7350b8fc838baf493317ca0e17bd95e8642f95fc69ecfed1de"}, - {file = "tiktoken-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15a2752dea63d93b0332fb0ddb05dd909371ededa145fe6a3242f46724fa7990"}, - {file = "tiktoken-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:26113fec3bd7a352e4b33dbaf1bd8948de2507e30bd95a44e2b1156647bc01b4"}, - {file = "tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e"}, - {file = "tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348"}, - {file = "tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33"}, - {file = "tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136"}, - {file = "tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336"}, - {file = "tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb"}, - {file = "tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03"}, - {file = "tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210"}, - {file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794"}, - {file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22"}, - {file = "tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2"}, - {file = "tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16"}, - {file = "tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb"}, - {file = "tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63"}, - {file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01"}, - {file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139"}, - {file = "tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a"}, - {file = "tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95"}, - {file = "tiktoken-0.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c6386ca815e7d96ef5b4ac61e0048cd32ca5a92d5781255e13b31381d28667dc"}, - {file = "tiktoken-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75f6d5db5bc2c6274b674ceab1615c1778e6416b14705827d19b40e6355f03e0"}, - {file = "tiktoken-0.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15b16f61e6f4625a57a36496d28dd182a8a60ec20a534c5343ba3cafa156ac7"}, - {file = "tiktoken-0.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebcec91babf21297022882344c3f7d9eed855931466c3311b1ad6b64befb3df"}, - {file = "tiktoken-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e5fd49e7799579240f03913447c0cdfa1129625ebd5ac440787afc4345990427"}, - {file = "tiktoken-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:26242ca9dc8b58e875ff4ca078b9a94d2f0813e6a535dcd2205df5d49d927cc7"}, - {file = "tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d"}, +python-versions = ">=3.8" +files = [ + {file = "tiktoken-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c4e654282ef05ec1bd06ead22141a9a1687991cef2c6a81bdd1284301abc71d"}, + {file = "tiktoken-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b3134aa24319f42c27718c6967f3c1916a38a715a0fa73d33717ba121231307"}, + {file = "tiktoken-0.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6092e6e77730929c8c6a51bb0d7cfdf1b72b63c4d033d6258d1f2ee81052e9e5"}, + {file = "tiktoken-0.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ad8ae2a747622efae75837abba59be6c15a8f31b4ac3c6156bc56ec7a8e631"}, + {file = "tiktoken-0.5.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51cba7c8711afa0b885445f0637f0fcc366740798c40b981f08c5f984e02c9d1"}, + {file = "tiktoken-0.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3d8c7d2c9313f8e92e987d585ee2ba0f7c40a0de84f4805b093b634f792124f5"}, + {file = "tiktoken-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:692eca18c5fd8d1e0dde767f895c17686faaa102f37640e884eecb6854e7cca7"}, + {file = "tiktoken-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:138d173abbf1ec75863ad68ca289d4da30caa3245f3c8d4bfb274c4d629a2f77"}, + {file = "tiktoken-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7388fdd684690973fdc450b47dfd24d7f0cbe658f58a576169baef5ae4658607"}, + {file = "tiktoken-0.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a114391790113bcff670c70c24e166a841f7ea8f47ee2fe0e71e08b49d0bf2d4"}, + {file = "tiktoken-0.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca96f001e69f6859dd52926d950cfcc610480e920e576183497ab954e645e6ac"}, + {file = "tiktoken-0.5.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:15fed1dd88e30dfadcdd8e53a8927f04e1f6f81ad08a5ca824858a593ab476c7"}, + {file = "tiktoken-0.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93f8e692db5756f7ea8cb0cfca34638316dcf0841fb8469de8ed7f6a015ba0b0"}, + {file = "tiktoken-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:bcae1c4c92df2ffc4fe9f475bf8148dbb0ee2404743168bbeb9dcc4b79dc1fdd"}, + {file = "tiktoken-0.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b76a1e17d4eb4357d00f0622d9a48ffbb23401dcf36f9716d9bd9c8e79d421aa"}, + {file = "tiktoken-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01d8b171bb5df4035580bc26d4f5339a6fd58d06f069091899d4a798ea279d3e"}, + {file = "tiktoken-0.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42adf7d4fb1ed8de6e0ff2e794a6a15005f056a0d83d22d1d6755a39bffd9e7f"}, + {file = "tiktoken-0.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3f894dbe0adb44609f3d532b8ea10820d61fdcb288b325a458dfc60fefb7db"}, + {file = "tiktoken-0.5.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:58ccfddb4e62f0df974e8f7e34a667981d9bb553a811256e617731bf1d007d19"}, + {file = "tiktoken-0.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58902a8bad2de4268c2a701f1c844d22bfa3cbcc485b10e8e3e28a050179330b"}, + {file = "tiktoken-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:5e39257826d0647fcac403d8fa0a474b30d02ec8ffc012cfaf13083e9b5e82c5"}, + {file = "tiktoken-0.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bde3b0fbf09a23072d39c1ede0e0821f759b4fa254a5f00078909158e90ae1f"}, + {file = "tiktoken-0.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2ddee082dcf1231ccf3a591d234935e6acf3e82ee28521fe99af9630bc8d2a60"}, + {file = "tiktoken-0.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35c057a6a4e777b5966a7540481a75a31429fc1cb4c9da87b71c8b75b5143037"}, + {file = "tiktoken-0.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c4a049b87e28f1dc60509f8eb7790bc8d11f9a70d99b9dd18dfdd81a084ffe6"}, + {file = "tiktoken-0.5.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5bf5ce759089f4f6521ea6ed89d8f988f7b396e9f4afb503b945f5c949c6bec2"}, + {file = "tiktoken-0.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0c964f554af1a96884e01188f480dad3fc224c4bbcf7af75d4b74c4b74ae0125"}, + {file = "tiktoken-0.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:368dd5726d2e8788e47ea04f32e20f72a2012a8a67af5b0b003d1e059f1d30a3"}, + {file = "tiktoken-0.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2deef9115b8cd55536c0a02c0203512f8deb2447f41585e6d929a0b878a0dd2"}, + {file = "tiktoken-0.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2ed7d380195affbf886e2f8b92b14edfe13f4768ff5fc8de315adba5b773815e"}, + {file = "tiktoken-0.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76fce01309c8140ffe15eb34ded2bb94789614b7d1d09e206838fc173776a18"}, + {file = "tiktoken-0.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60a5654d6a2e2d152637dd9a880b4482267dfc8a86ccf3ab1cec31a8c76bfae8"}, + {file = "tiktoken-0.5.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41d4d3228e051b779245a8ddd21d4336f8975563e92375662f42d05a19bdff41"}, + {file = "tiktoken-0.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c1cdec2c92fcde8c17a50814b525ae6a88e8e5b02030dc120b76e11db93f13"}, + {file = "tiktoken-0.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:84ddb36faedb448a50b246e13d1b6ee3437f60b7169b723a4b2abad75e914f3e"}, + {file = "tiktoken-0.5.2.tar.gz", hash = "sha256:f54c581f134a8ea96ce2023ab221d4d4d81ab614efa0b2fbce926387deb56c80"}, ] [package.dependencies] @@ -5258,7 +4930,6 @@ version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -5280,7 +4951,6 @@ version = "24.11.0" description = "An asynchronous networking framework written in Python" optional = false python-versions = ">=3.8.0" -groups = ["main"] files = [ {file = "twisted-24.11.0-py3-none-any.whl", hash = "sha256:fe403076c71f04d5d2d789a755b687c5637ec3bcd3b2b8252d76f2ba65f54261"}, {file = "twisted-24.11.0.tar.gz", hash = "sha256:695d0556d5ec579dcc464d2856b634880ed1319f45b10d19043f2b57eb0115b5"}, @@ -5319,7 +4989,6 @@ version = "23.1.1" description = "Compatibility API between asyncio/Twisted/Trollius" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490"}, {file = "txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704"}, @@ -5336,7 +5005,6 @@ version = "10.2.0.20240822" description = "Typing stubs for Pillow" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "types-Pillow-10.2.0.20240822.tar.gz", hash = "sha256:559fb52a2ef991c326e4a0d20accb3bb63a7ba8d40eb493e0ecb0310ba52f0d3"}, {file = "types_Pillow-10.2.0.20240822-py3-none-any.whl", hash = "sha256:d9dab025aba07aeb12fd50a6799d4eac52a9603488eca09d7662543983f16c5d"}, @@ -5348,7 +5016,6 @@ version = "2.9.0.20241206" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, @@ -5360,7 +5027,6 @@ version = "2024.2.0.20241221" description = "Typing stubs for pytz" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "types_pytz-2024.2.0.20241221-py3-none-any.whl", hash = "sha256:8fc03195329c43637ed4f593663df721fef919b60a969066e22606edf0b53ad5"}, {file = "types_pytz-2024.2.0.20241221.tar.gz", hash = "sha256:06d7cde9613e9f7504766a0554a270c369434b50e00975b3a4a0f6eed0f2c1a9"}, @@ -5372,7 +5038,6 @@ version = "6.0.12.20241230" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, @@ -5384,7 +5049,6 @@ version = "2.32.0.20241016" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" -groups = ["main", "typing"] files = [ {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, @@ -5399,7 +5063,6 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" -groups = ["main", "test", "typing"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -5411,7 +5074,6 @@ version = "0.9.0" description = "Runtime inspection utilities for typing module." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, @@ -5427,12 +5089,10 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main", "typing"] files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] -markers = {typing = "sys_platform == \"win32\""} [[package]] name = "uritemplate" @@ -5440,7 +5100,6 @@ version = "4.1.1" description = "Implementation of RFC 6570 URI Templates" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, @@ -5452,7 +5111,6 @@ version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["main", "typing"] files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, @@ -5470,7 +5128,6 @@ version = "0.34.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, @@ -5489,7 +5146,6 @@ version = "0.21.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" -groups = ["main"] files = [ {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, @@ -5541,7 +5197,6 @@ version = "5.1.0" description = "Python promises." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, @@ -5553,7 +5208,6 @@ version = "1.1.0" description = "Python extension to run WebAssembly binaries" optional = false python-versions = "*" -groups = ["test"] files = [ {file = "wasmer-1.1.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:c2af4b907ae2dabcac41e316e811d5937c93adf1f8b05c5d49427f8ce0f37630"}, {file = "wasmer-1.1.0-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:ab1ae980021e5ec0bf0c6cdd3b979b1d15a5f3eb2b8a32da8dcb1156e4a1e484"}, @@ -5577,7 +5231,6 @@ version = "1.1.0" description = "The Cranelift compiler for the `wasmer` package (to compile WebAssembly module)" optional = false python-versions = "*" -groups = ["test"] files = [ {file = "wasmer_compiler_cranelift-1.1.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:9869910179f39696a020edc5689f7759257ac1cce569a7a0fcf340c59788baad"}, {file = "wasmer_compiler_cranelift-1.1.0-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:405546ee864ac158a4107f374dfbb1c8d6cfb189829bdcd13050143a4bd98f28"}, @@ -5601,7 +5254,6 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -5613,7 +5265,6 @@ version = "1.17.2" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, @@ -5702,7 +5353,6 @@ version = "1.2.0" description = "WebSockets state-machine based protocol implementation" optional = false python-versions = ">=3.7.0" -groups = ["main"] files = [ {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, @@ -5717,7 +5367,6 @@ version = "1.18.3" description = "Yet another URL library" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, @@ -5814,7 +5463,6 @@ version = "2.5.0" description = "YooKassa API SDK Python Library" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "yookassa-2.5.0.tar.gz", hash = "sha256:5ddb279d6e867c74b66549e3096196606b5f04bf4927bde2513072b7a08ee3ff"}, ] @@ -5832,7 +5480,6 @@ version = "5.0" description = "Very basic event publishing system" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, @@ -5851,7 +5498,6 @@ version = "7.2" description = "Interfaces for Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"}, {file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"}, @@ -5901,6 +5547,6 @@ test = ["coverage[toml]", "zope.event", "zope.testing"] testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = "^3.12" -content-hash = "11aef76704403641d1d4f24a0e6525cb38c7fcf331df77983fd1da009dd8116d" +content-hash = "374eefaf72602369e3ec22a0277f3407a6387f6d20ec4a47be8c440f72729f74" @@ -9,6 +9,7 @@ package-mode = false [tool.poetry.dependencies] python = "^3.12" +replicate = "^0.10.0" deepl = "^1.15.0" django = "5.0.*" django-cors-headers = "^4.2.0" @@ -31,7 +32,10 @@ openpyxl = "^3.1.2" pypdf2 = "^3.0.1" python-docx = "^1.1.0" mutagen = "^1.47.0" +langchain = "^0.1.0" +langchain-openai = "^0.0.2" pillow = "^10.2.0" +langchain-google-genai = "^0.0.9" langserve = {extras = ["client"], version = "^0.0.46"} django-ordered-model = "^3.7.4" langchainhub = "^0.1.15" @@ -53,16 +57,7 @@ httptools = "^0.6.4" uvloop = "^0.21.0" wsproto = "^1.2.0" channels-redis = "^4.2.1" -langchain = "^0.3.19" -langchain-openai = "^0.3.6" -langchain-google-genai = "^2.0.9" -tiktoken = "^0.9.0" -langchain-community = "^0.3.17" -docx2txt = "^0.8" -pypandoc = "^1.15" -faiss-cpu = "^1.10.0" -replicate = "^1.0.4" -ruff = "^0.9.9" +tiktoken = "<0.6.0" [tool.poetry.group.test.dependencies] @@ -78,6 +73,10 @@ pytest-factoryboy = "^2.5.1" pytest-cov = "^4.1.0" +[tool.poetry.group.lint.dependencies] +ruff = "^0.4.8" + + [tool.poetry.group.typing.dependencies] mypy = "^1.5.1" django-stubs = "^4.2.4" @@ -124,7 +123,7 @@ exclude = [ "migrations" ] -line-length = 109 +line-length = 90 indent-width = 4 target-version = "py312"