http-misc 1.0.2__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {http_misc-1.0.2 → http_misc-1.0.4}/PKG-INFO +2 -1
- http_misc-1.0.4/http_misc/cache.py +43 -0
- http_misc-1.0.4/http_misc/transformers.py +115 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc.egg-info/PKG-INFO +2 -1
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc.egg-info/SOURCES.txt +4 -1
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc.egg-info/requires.txt +1 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/setup.py +3 -2
- http_misc-1.0.4/tests/test_transformers.py +49 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/README.md +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc/__init__.py +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc/errors.py +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc/http_utils.py +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc/logger.py +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc/retry_policy.py +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc/services.py +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc.egg-info/dependency_links.txt +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/http_misc.egg-info/top_level.txt +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/setup.cfg +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/tests/test_retry_policy.py +0 -0
- {http_misc-1.0.2 → http_misc-1.0.4}/tests/test_services.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: http-misc
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Утилитарный пакет межсервисного взаимодействия по протоколу HTTP
|
|
5
5
|
Author: Anton Gorinenko
|
|
6
6
|
Author-email: anton.gorinenko@gmail.com
|
|
@@ -20,6 +20,7 @@ Requires-Dist: envparse; extra == "test"
|
|
|
20
20
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
21
21
|
Requires-Dist: pytest-mock; extra == "test"
|
|
22
22
|
Requires-Dist: pytest-env; extra == "test"
|
|
23
|
+
Requires-Dist: freezegun; extra == "test"
|
|
23
24
|
Dynamic: author
|
|
24
25
|
Dynamic: author-email
|
|
25
26
|
Dynamic: classifier
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from abc import abstractmethod, ABC
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TokenCache(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def set_token(self, cache_key: str, access_token: str, expires_in: datetime.datetime):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def get_token(self, cache_key: str):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def remove(self, cache_key: str):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MemoryTokenCache(TokenCache):
|
|
20
|
+
""" Класс, реализующий хранение access_token в памяти.
|
|
21
|
+
Обратите внимание, что если ключом является client_id, то memory_cache не может быть одним на все запросы,
|
|
22
|
+
тк у разных запросов могут быть разные разрешения scopes
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# memory_cache = {}
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.memory_cache = {}
|
|
28
|
+
|
|
29
|
+
def set_token(self, cache_key: str, access_token: str, expires_in: datetime.datetime):
|
|
30
|
+
""" Установка токенов """
|
|
31
|
+
cache_value = {'access_token': access_token, 'expires_in': expires_in}
|
|
32
|
+
|
|
33
|
+
self.memory_cache[cache_key] = cache_value
|
|
34
|
+
|
|
35
|
+
def get_token(self, cache_key: str):
|
|
36
|
+
""" Получение access_token """
|
|
37
|
+
|
|
38
|
+
result = self.memory_cache.get(cache_key, {})
|
|
39
|
+
return result.get('access_token'), result.get('expires_in')
|
|
40
|
+
|
|
41
|
+
def remove(self, cache_key: str):
|
|
42
|
+
if cache_key in self.memory_cache:
|
|
43
|
+
self.memory_cache.pop(cache_key)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from http_misc import services, http_utils, retry_policy
|
|
8
|
+
from http_misc.cache import TokenCache
|
|
9
|
+
from http_misc.services import Transformer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TokenTransformer(Transformer, ABC):
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
async def get_token(self, *args, **kwargs):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def get_token_name(self, *args, **kwargs):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
async def modify(self, *args, **kwargs):
|
|
23
|
+
headers = kwargs.setdefault('cfg', {}).setdefault('headers', {})
|
|
24
|
+
token = await self.get_token(*args, **kwargs)
|
|
25
|
+
token_name = await self.get_token_name(*args, **kwargs)
|
|
26
|
+
headers['Authorization'] = f'{token_name} {token}'
|
|
27
|
+
|
|
28
|
+
return args, kwargs
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SetBasicAuthorization(TokenTransformer):
|
|
32
|
+
""" Указывает Basic token """
|
|
33
|
+
|
|
34
|
+
async def get_token_name(self, *args, **kwargs):
|
|
35
|
+
return 'Basic'
|
|
36
|
+
|
|
37
|
+
async def get_token(self, *args, **kwargs):
|
|
38
|
+
return base64.b64encode(self.client_id + b':' + self.client_secret).decode('utf-8')
|
|
39
|
+
|
|
40
|
+
def __init__(self, client_id: str, client_secret: str):
|
|
41
|
+
self.client_id = client_id.encode('utf-8')
|
|
42
|
+
self.client_secret = client_secret.encode('utf-8')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SetSystemOAuthToken(TokenTransformer):
|
|
46
|
+
""" Указывает Bearer token учетных записей для автоматизации """
|
|
47
|
+
|
|
48
|
+
async def get_token_name(self, *args, **kwargs):
|
|
49
|
+
return 'Bearer'
|
|
50
|
+
|
|
51
|
+
def __init__(self, client_id: str, client_secret: str, scope: str,
|
|
52
|
+
token_url: str, token_cache: TokenCache | None = None, use_utc: bool | None = True):
|
|
53
|
+
self.token_cache = token_cache
|
|
54
|
+
self.client_id = client_id
|
|
55
|
+
self.client_secret = client_secret
|
|
56
|
+
self.token_url = token_url
|
|
57
|
+
self.scope = scope
|
|
58
|
+
self._service = services.HttpService()
|
|
59
|
+
self._policy = retry_policy.AsyncRetryPolicy()
|
|
60
|
+
self.use_utc = use_utc
|
|
61
|
+
|
|
62
|
+
def _init_token_request(self):
|
|
63
|
+
form = aiohttp.FormData(quote_fields=True)
|
|
64
|
+
form.add_field('grant_type', 'client_credentials')
|
|
65
|
+
form.add_field('client_id', self.client_id)
|
|
66
|
+
form.add_field('client_secret', self.client_secret)
|
|
67
|
+
form.add_field('scope', self.scope)
|
|
68
|
+
request = {
|
|
69
|
+
'method': 'POST',
|
|
70
|
+
'url': self.token_url,
|
|
71
|
+
'cfg': {
|
|
72
|
+
'data': form
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return request
|
|
76
|
+
|
|
77
|
+
def _parse_token_response(self, response: dict):
|
|
78
|
+
access_token = response.get('access_token')
|
|
79
|
+
expires_in = response.get('expires_in')
|
|
80
|
+
if not access_token or not expires_in:
|
|
81
|
+
raise ValueError('Invalid response - access_token or expires_in is none.')
|
|
82
|
+
|
|
83
|
+
return access_token, expires_in
|
|
84
|
+
|
|
85
|
+
def _now(self):
|
|
86
|
+
return datetime.now(tz=timezone.utc if self.use_utc else None)
|
|
87
|
+
|
|
88
|
+
async def _init_token(self) -> str:
|
|
89
|
+
request = self._init_token_request()
|
|
90
|
+
response_data = await http_utils.send_and_validate(self._service, request, policy=self._policy)
|
|
91
|
+
access_token, expires_in = self._parse_token_response(response_data)
|
|
92
|
+
if self.token_cache:
|
|
93
|
+
expires_in = self._now() + timedelta(seconds=expires_in)
|
|
94
|
+
self.token_cache.set_token(self.client_id, access_token, expires_in)
|
|
95
|
+
return access_token
|
|
96
|
+
|
|
97
|
+
async def _get_token(self) -> tuple[str | None, datetime | None]:
|
|
98
|
+
if self.token_cache:
|
|
99
|
+
return self.token_cache.get_token(self.client_id)
|
|
100
|
+
|
|
101
|
+
return None, None
|
|
102
|
+
|
|
103
|
+
async def get_token(self, *args, **kwargs):
|
|
104
|
+
access_token, expires_in = await self._get_token()
|
|
105
|
+
# если токен найден
|
|
106
|
+
if access_token:
|
|
107
|
+
# проверяем срок жизни токена и если он истек получаем новый.
|
|
108
|
+
if self._now() < expires_in:
|
|
109
|
+
return access_token
|
|
110
|
+
|
|
111
|
+
# время жизни истекло, инициализируем токен
|
|
112
|
+
return await self._init_token()
|
|
113
|
+
else:
|
|
114
|
+
# если токен не найден, то инициализируем токены
|
|
115
|
+
return await self._init_token()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: http-misc
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Утилитарный пакет межсервисного взаимодействия по протоколу HTTP
|
|
5
5
|
Author: Anton Gorinenko
|
|
6
6
|
Author-email: anton.gorinenko@gmail.com
|
|
@@ -20,6 +20,7 @@ Requires-Dist: envparse; extra == "test"
|
|
|
20
20
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
21
21
|
Requires-Dist: pytest-mock; extra == "test"
|
|
22
22
|
Requires-Dist: pytest-env; extra == "test"
|
|
23
|
+
Requires-Dist: freezegun; extra == "test"
|
|
23
24
|
Dynamic: author
|
|
24
25
|
Dynamic: author-email
|
|
25
26
|
Dynamic: classifier
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
setup.py
|
|
3
3
|
http_misc/__init__.py
|
|
4
|
+
http_misc/cache.py
|
|
4
5
|
http_misc/errors.py
|
|
5
6
|
http_misc/http_utils.py
|
|
6
7
|
http_misc/logger.py
|
|
7
8
|
http_misc/retry_policy.py
|
|
8
9
|
http_misc/services.py
|
|
10
|
+
http_misc/transformers.py
|
|
9
11
|
http_misc.egg-info/PKG-INFO
|
|
10
12
|
http_misc.egg-info/SOURCES.txt
|
|
11
13
|
http_misc.egg-info/dependency_links.txt
|
|
12
14
|
http_misc.egg-info/requires.txt
|
|
13
15
|
http_misc.egg-info/top_level.txt
|
|
14
16
|
tests/test_retry_policy.py
|
|
15
|
-
tests/test_services.py
|
|
17
|
+
tests/test_services.py
|
|
18
|
+
tests/test_transformers.py
|
|
@@ -4,7 +4,7 @@ import setuptools
|
|
|
4
4
|
|
|
5
5
|
setuptools.setup(
|
|
6
6
|
name='http-misc',
|
|
7
|
-
version='1.0.
|
|
7
|
+
version='1.0.4',
|
|
8
8
|
author='Anton Gorinenko',
|
|
9
9
|
author_email='anton.gorinenko@gmail.com',
|
|
10
10
|
description='Утилитарный пакет межсервисного взаимодействия по протоколу HTTP',
|
|
@@ -30,7 +30,8 @@ setuptools.setup(
|
|
|
30
30
|
'envparse',
|
|
31
31
|
'pytest-asyncio',
|
|
32
32
|
'pytest-mock',
|
|
33
|
-
'pytest-env'
|
|
33
|
+
'pytest-env',
|
|
34
|
+
'freezegun'
|
|
34
35
|
]
|
|
35
36
|
},
|
|
36
37
|
python_requires='>=3.10',
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from freezegun import freeze_time
|
|
2
|
+
|
|
3
|
+
from http_misc import transformers, cache
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def test_set_system_oauth_token(mocker):
|
|
7
|
+
send_and_validate_mocker = mocker.patch('http_misc.http_utils.send_and_validate')
|
|
8
|
+
send_and_validate_mocker.side_effect = [
|
|
9
|
+
{
|
|
10
|
+
"access_token": "nb0G0HyVooN5XbSBaN2uYUr6pW75wh",
|
|
11
|
+
"expires_in": 36000,
|
|
12
|
+
"token_type": "Bearer",
|
|
13
|
+
"scope": "read write"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"access_token": "YYPfV0LG1jdTRl6D1qx9Hq0UxJvBKf",
|
|
17
|
+
"expires_in": 36000,
|
|
18
|
+
"token_type": "Bearer",
|
|
19
|
+
"scope": "read write"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
transformer = transformers.SetSystemOAuthToken(
|
|
23
|
+
client_id='6x7ujMdws6tDLbpePzQZvkYd0yFADYNJ11putMRw',
|
|
24
|
+
client_secret='RgCUfgtFHxqZ2amnqS4eTFL6cRsdfc3YYN0lTBrAIarLrt0Icewv6QzC1nFZXusEjqpG0aFmC14f8Jme4z3Q4TpxI9UQM5aU5LQkvuKpOoZ3oF2wDlyC7J41zPGTYuhO',
|
|
25
|
+
scope='read write',
|
|
26
|
+
token_url='http://localhost/api/v1/oauth/token/',
|
|
27
|
+
token_cache=cache.MemoryTokenCache())
|
|
28
|
+
|
|
29
|
+
request = {
|
|
30
|
+
'method': 'POST',
|
|
31
|
+
'url': 'https://localhost',
|
|
32
|
+
'cfg': {
|
|
33
|
+
'json': {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
with freeze_time('2025-01-14 12:00:01'):
|
|
37
|
+
await transformer.modify(**request)
|
|
38
|
+
await transformer.modify(**request)
|
|
39
|
+
|
|
40
|
+
assert 'headers' in request['cfg']
|
|
41
|
+
assert 'Authorization' in request['cfg']['headers']
|
|
42
|
+
token_1 = request['cfg']['headers']['Authorization']
|
|
43
|
+
assert token_1 == 'Bearer nb0G0HyVooN5XbSBaN2uYUr6pW75wh'
|
|
44
|
+
# Протух
|
|
45
|
+
with freeze_time('2025-01-16 12:00:01'):
|
|
46
|
+
await transformer.modify(**request)
|
|
47
|
+
token_2 = request['cfg']['headers']['Authorization']
|
|
48
|
+
assert token_2 == 'Bearer YYPfV0LG1jdTRl6D1qx9Hq0UxJvBKf'
|
|
49
|
+
assert token_1 != token_2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|