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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: http-misc
3
- Version: 1.0.2
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.2
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
@@ -7,3 +7,4 @@ envparse
7
7
  pytest-asyncio
8
8
  pytest-mock
9
9
  pytest-env
10
+ freezegun
@@ -4,7 +4,7 @@ import setuptools
4
4
 
5
5
  setuptools.setup(
6
6
  name='http-misc',
7
- version='1.0.2',
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