Почему-то если пользователь пользуется получением токена в первый раз, то происходит ошибка sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called; c
an't call await_only() here. Was IO attempted in an unexpected place?
 (Background on this error at: https://sqlalche.me/e/20/xd2s). Однако когда появляются связи ошибка исчезает. Ч предполагаю что проблема в классе, т.к. она полностью исчезнет. Когда у пользователя появляется свой токен. Исправь
import asyncio
from datetime import datetime, timedelta, timezone
import uuid
import aiohttp
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.exc import NoResultFound

from .database.models import User, Credential, UserToken


class FeloTokenManager:
    def __init__(self, session: AsyncSession, utc_offset: int):
        self.session = session
        self.utc_offset = utc_offset
        self.lock = asyncio.Lock()  # Для предотвращения конкурентного доступа

    async def login_api(self, email, password):
        url = 'https://account.felo.ai/api/user/sign.in'
        headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json, text/plain, */*',
            'Origin': 'https://account.felo.ai',
            'Referer': 'https://account.felo.ai/register',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
        }
        headers = {
 #   "Host": "account.felo.ai",
    "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1",
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Content-Type": "application/json",
  #  "Content-Length": "202",
    "Origin": "https://account.felo.ai",
    "DNT": "1",
    "Sec-GPC": "1",
    "Connection": "keep-alive",
    "Referer": "https://account.felo.ai/login?app_id=glaritySearch&redirect_uri=https://felo.ai/callback?redirect_uri=https://felo.ai/ru/search",
    "Cookie": ("visitor_id=7155a674-477c-4c52-9af6-3253f4dcd78c; "
               "_ga_70F8QP140X=GS1.1.1730683792.1.1.1730685982.0.0.0; "
               "_ga=GA1.1.759643035.1730683793; "
               "Authorization=eM_960216a9e3fb4b1992d46461d8b1eaaf__; "
               "_ga_8SZPRV97HV=GS1.1.1730683828.1.1.1730685978.0.0.0; "
               "_ga_Q9Q1E734CC=GS1.1.1730683828.1.1.1730685978.0.0.0"),
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin",
    "Priority": "u=0",
}

        device_id = uuid.uuid4().hex

        payload = {
            "email": email,
            "password": password,
            "app_id": "glaritySearch",
            "client_type": "WEB",
            "device_id": device_id
        }
        token_value=""
        async with aiohttp.ClientSession() as session:
            async with session.post(url, headers=headers, json=payload) as response:
                if response.status == 200:
                    data = await response.json()
                    try:
                        token_value = data["data"]["token"]["token_value"]
                        #return token_value
                    except KeyError:
                        raise Exception("Token value not found in the response data")
                else:
                    raise Exception(f"Request failed with status {response.status}")
            headers["Authorization"]=token_value
            async with session.post("https://account.felo.ai/api/user/token.code.create", headers=headers, json={"app_id":"glaritySearch","redirect_uri":"https://felo.ai/callback?redirect_uri=https://felo.ai/ru/search","client_type":"WEB","device_id":device_id}) as response:
                if response.status == 200:
                    data = await response.json()
                    print(data)
                    try:
                        pass
                        #token_value = data["data"]["token"]["token_value"]
                        #return token_value
                    except KeyError:
                        raise Exception("Token value not found in the response data")
                else:
                    raise Exception(f"Request failed with status {response.status}")
            url = "https://api.felo.ai/search/user"
            async with session.get(url, headers=headers) as response:
                if response.status == 200:
                    data = await response.json()
                    print(data)
                    try:
                        #token_value = data["data"]["token"]["token_value"]
                        pass
                    except KeyError:
                        raise Exception("Token value not found in the response data")
                else:
                    raise Exception(f"Request failed with status {response.status}")
            url = "https://api-ext.felo.ai/user/plan"
            async with session.get(url, headers=headers) as response:
                if response.status == 200:
                    data = await response.json()
                    print(data)
                    try:
                        #token_value = data["data"]["token"]["token_value"]
                        return token_value
                    except KeyError:
                        raise Exception("Token value not found in the response data")
                else:
                    raise Exception(f"Request failed with status {response.status}")
    async def get_token_by_telegram_id(self, telegram_id):
        async with self.lock:
            try:
                # Получаем пользователя
                result = await self.session.execute(
                    select(User).where(User.telegram_id == telegram_id)
                )
                user = result.scalars().one()

                # Получаем или создаем UserToken
                user_token = await self._get_or_create_user_token(user)

                # Проверяем необходимость сброса usage_count
                await self._reset_usage_count_if_needed(user_token.credential)

                # Проверяем необходимость обновления токена
                if await self._needs_token_refresh(user_token.credential):
                    await self._refresh_token(user_token.credential)

                # Увеличиваем счётчик использования
                user_token.credential.usage_count += 1

                await self.session.commit()

                return user_token.credential.token

            except NoResultFound:
                raise Exception(f"User with telegram_id {telegram_id} not found.")
            except Exception as e:
                await self.session.rollback()
                raise e

    async def _get_or_create_user_token(self, user: User) -> UserToken:
        result = await self.session.execute(
            select(UserToken).where(UserToken.user_id == user.id)
        )
        user_token = result.scalars().first()

        if not user_token:
            # Находим доступный Credential
            credential = await self._get_available_credential()
            if not credential:
                raise Exception("No available credentials found.")
            # Связываем Credential с пользователем
            user_token = UserToken(user_id=user.id, credential_id=credential.id)
            self.session.add(user_token)
            await self.session.commit()
        else:
            credential = user_token.credential
            # Проверяем, не достиг ли токен лимита, возможно, потребуется смена
            if credential.usage_count >= 5:
                # Попробуем найти другой Credential
                alternative_credential = await self._find_alternative_credential(exclude_id=credential.id)
                if alternative_credential:
                    user_token.credential = alternative_credential
                    await self.session.commit()
        return user_token

    async def _find_alternative_credential(self, exclude_id: int) -> Credential:
        now = datetime.utcnow().replace(tzinfo=timezone.utc)
        adjusted_now = now + timedelta(hours=self.utc_offset)
        user_midnight = datetime.combine(adjusted_now.date(), datetime.min.time(), tzinfo=timezone.utc)
        reset_time = user_midnight - timedelta(hours=self.utc_offset)

        result = await self.session.execute(select(Credential))
        credentials = result.scalars().all()

        for credential in credentials:
            if credential.id == exclude_id:
                continue

            # Обеспечиваем, что даты timezone-aware
            if credential.expires_at and credential.expires_at.tzinfo is None:
                credential.expires_at = credential.expires_at.replace(tzinfo=timezone.utc)

            if credential.reset_date and credential.reset_date.tzinfo is None:
                credential.reset_date = credential.reset_date.replace(tzinfo=timezone.utc)

            # Сброс usage_count при необходимости
            if not credential.reset_date or credential.reset_date < reset_time:
                credential.usage_count = 0
                credential.reset_date = reset_time

            # Проверяем, нужно ли обновить токен
            token_expired = not credential.expires_at or credential.expires_at < now
            token_empty = not credential.token

            if token_empty or token_expired or credential.usage_count < 5:
                if token_empty or token_expired:
                    token_value = await self.login_api(credential.login, credential.password)
                    credential.token = token_value
                    credential.expires_at = now + timedelta(days=15)
                # Если токен не пустой и не истек, но usage_count < 5, ничего не делаем

            if credential.token and credential.usage_count < 5:
                return credential

        return None

    async def _get_available_credential(self) -> Credential:
        now = datetime.utcnow().replace(tzinfo=timezone.utc)
        adjusted_now = now + timedelta(hours=self.utc_offset)
        user_midnight = datetime.combine(adjusted_now.date(), datetime.min.time(), tzinfo=timezone.utc)
        reset_time = user_midnight - timedelta(hours=self.utc_offset)

        result = await self.session.execute(select(Credential))
        credentials = result.scalars().all()

        for credential in credentials:
            # Обеспечиваем, что даты timezone-aware
            if credential.expires_at and credential.expires_at.tzinfo is None:
                credential.expires_at = credential.expires_at.replace(tzinfo=timezone.utc)

            if credential.reset_date and credential.reset_date.tzinfo is None:
                credential.reset_date = credential.reset_date.replace(tzinfo=timezone.utc)

            # Сброс usage_count при необходимости
            if not credential.reset_date or credential.reset_date < reset_time:
                credential.usage_count = 0
                credential.reset_date = reset_time

            # Проверяем, нужно ли обновить токен
            token_expired = not credential.expires_at or credential.expires_at < now
            token_empty = not credential.token

            if token_empty or token_expired:
                token_value = await self.login_api(credential.login, credential.password)
                credential.token = token_value
                credential.expires_at = now + timedelta(days=15)
                credential.usage_count = 0
                credential.reset_date = reset_time
                await self.session.commit()

            if credential.token and credential.usage_count < 5:
                credential.usage_count += 1
                await self.session.commit()
                return credential

        return None

    async def _reset_usage_count_if_needed(self, credential: Credential):
        now = datetime.utcnow().replace(tzinfo=timezone.utc)
        adjusted_now = now + timedelta(hours=self.utc_offset)
        user_midnight = datetime.combine(adjusted_now.date(), datetime.min.time(), tzinfo=timezone.utc)
        reset_time = user_midnight - timedelta(hours=self.utc_offset)

        if not credential.reset_date or credential.reset_date < reset_time:
            credential.usage_count = 0
            credential.reset_date = reset_time
            await self.session.commit()

    async def _needs_token_refresh(self, credential: Credential) -> bool:
        now = datetime.utcnow().replace(tzinfo=timezone.utc)
        token_expired = not credential.expires_at or credential.expires_at < now
        token_empty = not credential.token
        usage_limit_reached = credential.usage_count >= 5
        return token_empty or token_expired or usage_limit_reached

    async def _refresh_token(self, credential: Credential):
        token_value = await self.login_api(credential.login, credential.password)
        now = datetime.utcnow().replace(tzinfo=timezone.utc)
        adjusted_now = now + timedelta(hours=self.utc_offset)
        user_midnight = datetime.combine(adjusted_now.date(), datetime.min.time(), tzinfo=timezone.utc)
        reset_time = user_midnight - timedelta(hours=self.utc_offset)

        credential.token = token_value
        credential.expires_at = now + timedelta(days=15)
        credential.usage_count = 0
        credential.reset_date = reset_time

    async def add_credential(self, login, password):
        async with self.lock:
            result = await self.session.execute(
                select(Credential).where(Credential.login == login)
            )
            existing_credential = result.scalars().first()
            if existing_credential:
                raise Exception("Credential with this login already exists.")

            new_credential = Credential(
                login=login,
                password=password,
                usage_count=0,
                reset_date=None,
                expires_at=None,
                token=None,
            )
            self.session.add(new_credential)
            await self.session.commit()
            return new_credential

    async def assign_credential_to_user(self, telegram_id, credential_login):
        async with self.lock:
            try:
                # Получаем пользователя
                result = await self.session.execute(
                    select(User).where(User.telegram_id == telegram_id)
                )
                user = result.scalars().one()

                # Получаем Credential
                result = await self.session.execute(
                    select(Credential).where(Credential.login == credential_login)
                )
                credential = result.scalars().one()

                # Получаем или создаем UserToken
                user_token = await self._get_or_create_user_token(user)

                if user_token.credential_id != credential.id:
                    user_token.credential_id = credential.id
                    await self.session.commit()
                else:
                    raise Exception("This credential is already assigned to the user.")

                return user_token

            except NoResultFound as e:
                raise Exception("User or Credential not found.") from e
            except Exception as e:
                await self.session.rollback()
                raise e