Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Авторизация по OAuth #149

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,29 @@ leads = await b.get_all('crm.lead.list')

Внутри объекта ведётся учёт скорости отправки запросов к серверу, поэтому важно, чтобы все запросы приложения в отношении одного аккаунта с одного IP-адреса отправлялись из одного экземпляра `Bitrix`.

### Метод ` __init__(self, webhook: str, verbose: bool = True, respect_velocity_policy: bool = False):`
### Метод ` __init__(self, webhook: str, verbose: bool = True, respect_velocity_policy: bool = False, token_func: Callable = None):`
Создаёт экземпляр объекта `Bitrix`.

#### Параметры
* `webhook: str` - URL вебхука, полученного от сервера Битрикс.
* `verbose: bool = True` - показывать прогрессбар при выполнении запроса.
* `respect_velocity_policy: bool = False` - соблюдать политику Битрикса о скорости запросов.
* `token_func: Callable = None` - функция, которая должна возвращать новый токен авторизации каждый раз, когда клиент получает от сервера ответ `403 Not authorized`.

Например:

```python
def get_new_token() -> str:
# Тут идет ваша реализация получения нового токена от сервера авторизации
return new_token


b = Bitrix(webhook, token_func=get_new_token())

# Дальше идут ваши вызовы к Битриксу. `get_new_token` будет вызываться каждый раз,
# когда старый токен перестает работать.
```


### Метод `get_all(self, method: str, params: dict = None) -> list | dict`
Получить полный список сущностей по запросу `method`.
Expand Down
6 changes: 4 additions & 2 deletions fast_bitrix24/bitrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
class BitrixAbstract(object):

def __init__(self, webhook: str, verbose: bool = True,
respect_velocity_policy: bool = False):
respect_velocity_policy: bool = False, token_func=None):
'''
Создает объект класса Bitrix.

Expand All @@ -23,9 +23,11 @@ def __init__(self, webhook: str, verbose: bool = True,
запроса
- `respect_velocity_policy: bool = False` - соблюдать ли политику
Битрикса о скорости запросов
- `token_func = None` - функция без парамтеров, которая
отдает новый токен для авторизации при каждом обращении к ней
'''

self.srh = ServerRequestHandler(webhook, respect_velocity_policy)
self.srh = ServerRequestHandler(webhook, respect_velocity_policy, token_func)
self.verbose = verbose


Expand Down
56 changes: 51 additions & 5 deletions fast_bitrix24/srh.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class ServerError(Exception):
pass


class TokenRejected(Exception):
pass


class ServerRequestHandler():
'''
Используется для контроля скорости доступа к серверам Битрикс.
Expand All @@ -43,10 +47,17 @@ class ServerRequestHandler():
последовательных запросов к серверу.
'''

def __init__(self, webhook, respect_velocity_policy):
def __init__(self, webhook: str, respect_velocity_policy: bool, token_func=None):
self.webhook = self.standardize_webhook(webhook)
self.respect_velocity_policy = respect_velocity_policy

self.token_func = token_func
self.token = None # Если None, то требуется получить новый токен

# процесс получения токена уже запущен
self.token_received = Event()
self.token_received.set()

self.requests_per_second = BITRIX_RPS
self.pool_size = BITRIX_POOL_SIZE

Expand Down Expand Up @@ -135,23 +146,34 @@ async def single_request(self, method, params=None):
self.success()
return result

except (ClientPayloadError,
except (ClientPayloadError, TokenRejected,
ServerDisconnectedError, ServerError) as err:
self.failure(err)

async def request_attempt(self, method, params=None):
'''Делает попытку запроса к серверу, ожидая при необходимости.'''

try:

url = self.webhook + method + await self.get_token_param()

async with self.acquire(), self.session.post(
url=self.webhook + method, json=params) as response:
url=url, json=params) as response:

return ServerResponse(await response.json(encoding='utf-8'))

except ClientResponseError as error:

if error.status // 100 == 5: # ошибки вида 5XX
raise ServerError('The server returned an error') from error
else:
raise

# нужно получить или освежить токен
elif error.status == 403 and self.token_func:
await self.update_token()
raise TokenRejected(
'The server rejected the auth token') from error

raise # иначе повторяем полученное исключение

def success(self):
'''Увеличить счетчик удачных попыток.'''
Expand Down Expand Up @@ -204,6 +226,10 @@ async def limit_concurrent_requests(self):
'''Не позволяет оновременно выполнять
более `self.mcr_cur_limit` запросов.'''

'''Не использует семафоры, потому что в них отсутствует возможность
изменять размер семафора после инициализации,
а нам это надо для `Bitrix.slow()`.'''

while self.concurrent_requests > self.mcr_cur_limit:
self.request_complete.clear()
await self.request_complete.wait()
Expand Down Expand Up @@ -244,3 +270,23 @@ async def limit_request_velocity(self):
trim_time = start_time - self.pool_size / self.requests_per_second
while self.rr[-1] < trim_time:
self.rr.pop()

async def get_token_param(self):
'''Получить часть строки запроса с токеном авторизации.'''

if not self.token_func:
return ''

await self.token_received.wait()

if not self.token:
await self.update_token()

return '&auth=' + self.token

async def update_token(self):
'''Запросить новый токен авторизации.'''

self.token_received.clear()
self.token = await self.token_func()
self.token_received.set()
36 changes: 36 additions & 0 deletions tests/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,42 @@ async def test_acquire_speed(self):
assert 10 + 15 < elapsed < 10 + 15 + 1


@pytest.mark.asyncio
async def test_get_token():

q = []
start = monotonic()

def log(msg):
q.append(f'{round(monotonic() - start, 1)}: {msg}')

async def get_token():
log('get_token called')
await sleep(0.5)
log('token returned')
return 'token'

async def request(srh):
log('requested')
await srh.get_token_param()
log('proceeded')

srh = ServerRequestHandler('http://www.bitrix.ru/', get_token)

tasks = set()
for n in range(5):
tasks |= {create_task(request(srh))}
_, tasks = await wait(tasks, timeout=0.2)

log('finished')

assert ', '.join(q) == (
'0.0: requested, 0.0: get_token called, 0.2: requested, '
'0.4: requested, 0.5: token returned, 0.5: proceeded, 0.5: proceeded, '
'0.5: proceeded, 0.5: requested, 0.5: proceeded, '
'0.5: requested, 0.5: proceeded, 0.5: finished')


class MockResponse(object):
def __init__(self, stored_json=None):
self.stored_json = stored_json
Expand Down