зеркало из
https://github.com/viginum-datalab/twscrape.git
synced 2025-10-29 05:04:22 +02:00
родитель
09c820cade
Коммит
14b68a93e8
@ -183,8 +183,6 @@ twscrape login_accounts
|
|||||||
|
|
||||||
`twscrape` will start login flow for each new account. If X will ask to verify email and you provided `email_password` in `add_account`, then `twscrape` will try to receive verification code by IMAP protocol. After success login account cookies will be saved to db file for future use.
|
`twscrape` will start login flow for each new account. If X will ask to verify email and you provided `email_password` in `add_account`, then `twscrape` will try to receive verification code by IMAP protocol. After success login account cookies will be saved to db file for future use.
|
||||||
|
|
||||||
_Note:_ You can increase timeout for verification code with `TWS_WAIT_EMAIL_CODE` environment variable (default: `40`, in seconds).
|
|
||||||
|
|
||||||
#### Manual email verification
|
#### Manual email verification
|
||||||
|
|
||||||
In case your email provider not support IMAP protocol (ProtonMail, Tutanota, etc) or IMAP is disabled in settings, you can enter email verification code manually. To do this run login command with `--manual` flag.
|
In case your email provider not support IMAP protocol (ProtonMail, Tutanota, etc) or IMAP is disabled in settings, you can enter email verification code manually. To do this run login command with `--manual` flag.
|
||||||
@ -306,6 +304,11 @@ So if you want to use proxy PER ACCOUNT, do NOT override proxy with env variable
|
|||||||
|
|
||||||
_Note:_ If proxy not working, exception will be raised from API class.
|
_Note:_ If proxy not working, exception will be raised from API class.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
- `TWS_WAIT_EMAIL_CODE` – timeout for email verification code during login (default: `30`, in seconds)
|
||||||
|
- `TWS_RAISE_WHEN_NO_ACCOUNT` – raise `NoAccountError` exception when no available accounts right now, instead of waiting for availability (default: `false`, possible value: `false` / `0` / `true` / `1`)
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
After 1 July 2023 Twitter [introduced new limits](https://twitter.com/elonmusk/status/1675187969420828672) and still continue to update it periodically.
|
After 1 July 2023 Twitter [introduced new limits](https://twitter.com/elonmusk/status/1675187969420828672) and still continue to update it periodically.
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from twscrape.accounts_pool import NoAccountError
|
||||||
from twscrape.api import API
|
from twscrape.api import API
|
||||||
from twscrape.logger import set_log_level
|
from twscrape.logger import set_log_level
|
||||||
from twscrape.utils import gather
|
from twscrape.utils import gather, get_env_bool
|
||||||
|
|
||||||
set_log_level("DEBUG")
|
set_log_level("DEBUG")
|
||||||
|
|
||||||
@ -40,3 +45,21 @@ async def test_gql_params(api_mock: API, monkeypatch):
|
|||||||
assert len(args) == 1, f"{func} not called once"
|
assert len(args) == 1, f"{func} not called once"
|
||||||
assert args[0][1]["limit"] == 100, f"limit not changed in {func}"
|
assert args[0][1]["limit"] == 100, f"limit not changed in {func}"
|
||||||
assert args[0][0][1]["count"] == 100, f"count not changed in {func}"
|
assert args[0][0][1]["count"] == 100, f"count not changed in {func}"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_raise_when_no_account(api_mock: API):
|
||||||
|
await api_mock.pool.delete_accounts(["user1"])
|
||||||
|
assert len(await api_mock.pool.get_all()) == 0
|
||||||
|
|
||||||
|
assert get_env_bool("TWS_RAISE_WHEN_NO_ACCOUNT") is False
|
||||||
|
os.environ["TWS_RAISE_WHEN_NO_ACCOUNT"] = "1"
|
||||||
|
assert get_env_bool("TWS_RAISE_WHEN_NO_ACCOUNT") is True
|
||||||
|
|
||||||
|
with pytest.raises(NoAccountError):
|
||||||
|
await gather(api_mock.search("foo", limit=10))
|
||||||
|
|
||||||
|
with pytest.raises(NoAccountError):
|
||||||
|
await api_mock.user_by_id(123)
|
||||||
|
|
||||||
|
del os.environ["TWS_RAISE_WHEN_NO_ACCOUNT"]
|
||||||
|
assert get_env_bool("TWS_RAISE_WHEN_NO_ACCOUNT") is False
|
||||||
|
|||||||
@ -11,7 +11,11 @@ from .account import Account
|
|||||||
from .db import execute, fetchall, fetchone
|
from .db import execute, fetchall, fetchone
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
from .login import LoginConfig, login
|
from .login import LoginConfig, login
|
||||||
from .utils import parse_cookies, utc
|
from .utils import get_env_bool, parse_cookies, utc
|
||||||
|
|
||||||
|
|
||||||
|
class NoAccountError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AccountInfo(TypedDict):
|
class AccountInfo(TypedDict):
|
||||||
@ -32,9 +36,15 @@ class AccountsPool:
|
|||||||
# _order_by: str = "RANDOM()"
|
# _order_by: str = "RANDOM()"
|
||||||
_order_by: str = "username"
|
_order_by: str = "username"
|
||||||
|
|
||||||
def __init__(self, db_file="accounts.db", login_config: LoginConfig | None = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
db_file="accounts.db",
|
||||||
|
login_config: LoginConfig | None = None,
|
||||||
|
raise_when_no_account=False,
|
||||||
|
):
|
||||||
self._db_file = db_file
|
self._db_file = db_file
|
||||||
self._login_config = login_config or LoginConfig()
|
self._login_config = login_config or LoginConfig()
|
||||||
|
self._raise_when_no_account = raise_when_no_account
|
||||||
|
|
||||||
async def load_from_file(self, filepath: str, line_format: str):
|
async def load_from_file(self, filepath: str, line_format: str):
|
||||||
line_delim = guess_delim(line_format)
|
line_delim = guess_delim(line_format)
|
||||||
@ -270,6 +280,9 @@ class AccountsPool:
|
|||||||
while True:
|
while True:
|
||||||
account = await self.get_for_queue(queue)
|
account = await self.get_for_queue(queue)
|
||||||
if not account:
|
if not account:
|
||||||
|
if self._raise_when_no_account or get_env_bool("TWS_RAISE_WHEN_NO_ACCOUNT"):
|
||||||
|
raise NoAccountError(f"No account available for queue {queue}")
|
||||||
|
|
||||||
if not msg_shown:
|
if not msg_shown:
|
||||||
nat = await self.next_available_at(queue)
|
nat = await self.next_available_at(queue)
|
||||||
if not nat:
|
if not nat:
|
||||||
@ -279,6 +292,7 @@ class AccountsPool:
|
|||||||
msg = f'No account available for queue "{queue}". Next available at {nat}'
|
msg = f'No account available for queue "{queue}". Next available at {nat}'
|
||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
msg_shown = True
|
msg_shown = True
|
||||||
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -59,14 +59,18 @@ class API:
|
|||||||
pool: AccountsPool
|
pool: AccountsPool
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, pool: AccountsPool | str | None = None, debug=False, proxy: str | None = None
|
self,
|
||||||
|
pool: AccountsPool | str | None = None,
|
||||||
|
debug=False,
|
||||||
|
proxy: str | None = None,
|
||||||
|
raise_when_no_account=False,
|
||||||
):
|
):
|
||||||
if isinstance(pool, AccountsPool):
|
if isinstance(pool, AccountsPool):
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
elif isinstance(pool, str):
|
elif isinstance(pool, str):
|
||||||
self.pool = AccountsPool(pool)
|
self.pool = AccountsPool(db_file=pool, raise_when_no_account=raise_when_no_account)
|
||||||
else:
|
else:
|
||||||
self.pool = AccountsPool()
|
self.pool = AccountsPool(raise_when_no_account=raise_when_no_account)
|
||||||
|
|
||||||
self.proxy = proxy
|
self.proxy = proxy
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, AsyncGenerator, Callable, TypeVar
|
from typing import Any, AsyncGenerator, Callable, TypeVar
|
||||||
@ -206,3 +207,10 @@ def parse_cookies(val: str) -> dict[str, str]:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
raise ValueError(f"Invalid cookie value: {val}")
|
raise ValueError(f"Invalid cookie value: {val}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_bool(key: str, default_val: bool = False) -> bool:
|
||||||
|
val = os.getenv(key)
|
||||||
|
if val is None:
|
||||||
|
return default_val
|
||||||
|
return val.lower() in ("1", "true", "yes")
|
||||||
|
|||||||
Загрузка…
x
Ссылка в новой задаче
Block a user