ul-api-utils 7.5.1__py3-none-any.whl → 7.7.0__py3-none-any.whl
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.
Potentially problematic release.
This version of ul-api-utils might be problematic. Click here for more details.
- example/conf.py +2 -0
- example/routes/api_some.py +13 -7
- ul_api_utils/const.py +1 -1
- ul_api_utils/modules/api_sdk.py +57 -7
- ul_api_utils/modules/api_sdk_config.py +3 -0
- ul_api_utils/resources/caching.py +187 -0
- ul_api_utils/utils/api_method.py +3 -1
- {ul_api_utils-7.5.1.dist-info → ul_api_utils-7.7.0.dist-info}/METADATA +3 -1
- {ul_api_utils-7.5.1.dist-info → ul_api_utils-7.7.0.dist-info}/RECORD +13 -12
- {ul_api_utils-7.5.1.dist-info → ul_api_utils-7.7.0.dist-info}/LICENSE +0 -0
- {ul_api_utils-7.5.1.dist-info → ul_api_utils-7.7.0.dist-info}/WHEEL +0 -0
- {ul_api_utils-7.5.1.dist-info → ul_api_utils-7.7.0.dist-info}/entry_points.txt +0 -0
- {ul_api_utils-7.5.1.dist-info → ul_api_utils-7.7.0.dist-info}/top_level.txt +0 -0
example/conf.py
CHANGED
|
@@ -9,6 +9,8 @@ from example.permissions import permissions
|
|
|
9
9
|
sdk = ApiSdk(ApiSdkConfig(
|
|
10
10
|
service_name='example_service',
|
|
11
11
|
permissions=permissions,
|
|
12
|
+
cache_storage_uri='redis://localhost:16379',
|
|
13
|
+
cache_default_ttl=60,
|
|
12
14
|
rate_limit_storage_uri='redis://localhost:16379',
|
|
13
15
|
rate_limit_identify=ApiSdkIdentifyTypeEnum.JWT_USER_ID,
|
|
14
16
|
flask_debugging_plugins=ApiSdkFlaskDebuggingPluginsEnabled(
|
example/routes/api_some.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
2
1
|
from time import sleep
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
3
|
from typing import List, Optional, Tuple
|
|
4
4
|
|
|
5
|
-
from ul_db_utils.modules.db import db
|
|
6
5
|
from flask import jsonify
|
|
7
6
|
from pydantic import BaseModel
|
|
8
|
-
|
|
7
|
+
from ul_db_utils.modules.db import db
|
|
9
8
|
from werkzeug import Response as BaseResponse
|
|
9
|
+
|
|
10
|
+
from example.conf import sdk
|
|
11
|
+
from example.permissions import SOME_PERMISSION, SOME_PERMISSION2
|
|
10
12
|
from ul_api_utils.api_resource.api_request import ApiRequestQuery
|
|
11
13
|
from ul_api_utils.api_resource.api_resource import ApiResource
|
|
12
14
|
from ul_api_utils.api_resource.api_resource_config import ApiResourceConfig
|
|
@@ -16,8 +18,6 @@ from ul_api_utils.internal_api.internal_api import InternalApi
|
|
|
16
18
|
from ul_api_utils.utils.api_encoding import ApiEncoding
|
|
17
19
|
from ul_api_utils.resources.health_check.health_check import HealthCheckContext
|
|
18
20
|
from ul_api_utils.validators.custom_fields import QueryParamsSeparatedList
|
|
19
|
-
from example.conf import sdk
|
|
20
|
-
from example.permissions import SOME_PERMISSION, SOME_PERMISSION2
|
|
21
21
|
|
|
22
22
|
internal_api = InternalApi(
|
|
23
23
|
entry_point='http://localhost:5000',
|
|
@@ -47,10 +47,11 @@ class SomeBody(BaseModel):
|
|
|
47
47
|
|
|
48
48
|
class Some2Query(ApiRequestQuery):
|
|
49
49
|
sleep: float = 0.4
|
|
50
|
-
some: QueryParamsSeparatedList[str]
|
|
50
|
+
some: QueryParamsSeparatedList[str] = '' # type: ignore
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
@sdk.rest_api('POST', '/example-resource-simple', access=sdk.ACCESS_PUBLIC)
|
|
54
|
+
@sdk.cache_api('REFRESH', 'example')
|
|
54
55
|
def some5(api_resource: ApiResource, body: SomeBody) -> JsonApiResponse[List[RespObject]]:
|
|
55
56
|
return api_resource.response_ok([
|
|
56
57
|
RespObject(now=datetime.now() + timedelta(seconds=body.seconds)),
|
|
@@ -62,6 +63,7 @@ class Some7Query(ApiRequestQuery):
|
|
|
62
63
|
need_redirect: int
|
|
63
64
|
|
|
64
65
|
|
|
66
|
+
@sdk.cache_api('READ', 'example')
|
|
65
67
|
@sdk.rest_api('POST', '/example-resource-simple-any', access=sdk.ACCESS_PUBLIC)
|
|
66
68
|
def some9(api_resource: ApiResource) -> AnyJsonApiResponse:
|
|
67
69
|
return api_resource.response_ok([RespObject(now=datetime.now(), notes="some9")], 1)
|
|
@@ -91,7 +93,7 @@ def some7(api_resource: ApiResource, query: Some7Query) -> JsonApiResponse[List[
|
|
|
91
93
|
|
|
92
94
|
@sdk.rest_api('GET', '/example-resource-simple-proxy', access=sdk.ACCESS_PUBLIC)
|
|
93
95
|
def some6(api_resource: ApiResource) -> ProxyJsonApiResponse[List[RespObject]]: # type: ignore
|
|
94
|
-
res = internal_api.request_post('/example-resource-simple', json={'seconds': 123}).typed(List[RespObject]).check()
|
|
96
|
+
res = internal_api.request_post('/example-resource-simple-any', json={'seconds': 123}).typed(List[RespObject]).check()
|
|
95
97
|
assert res.payload[0].now is not None
|
|
96
98
|
|
|
97
99
|
return api_resource.response_proxy(res)
|
|
@@ -151,6 +153,7 @@ def health_check(context: HealthCheckContext) -> None:
|
|
|
151
153
|
|
|
152
154
|
|
|
153
155
|
@sdk.rest_api('GET', '/example-resource', access=sdk.ACCESS_PUBLIC)
|
|
156
|
+
@sdk.cache_api('READ', ('example', 'resource', 'simple'))
|
|
154
157
|
def some3(api_resource: ApiResource) -> JsonApiResponse[RespObject]:
|
|
155
158
|
api_resource.logger.info('some 1')
|
|
156
159
|
sleep(0.1)
|
|
@@ -182,6 +185,7 @@ def some2html(api_resource: ApiResource, query: Some2Query) -> HtmlApiResponse:
|
|
|
182
185
|
|
|
183
186
|
|
|
184
187
|
@sdk.rest_api('GET', '/example-resource-for-loong-sleep', access=sdk.ACCESS_PUBLIC)
|
|
188
|
+
@sdk.cache_api('READ', ('example', 'resource', 'simple'))
|
|
185
189
|
def some2(api_resource: ApiResource, query: Some2Query) -> JsonApiResponse[RespObject]:
|
|
186
190
|
sess = db.session()
|
|
187
191
|
sess.execute('SELECT * FROM information_schema.tables LIMIT 3;')
|
|
@@ -191,6 +195,7 @@ def some2(api_resource: ApiResource, query: Some2Query) -> JsonApiResponse[RespO
|
|
|
191
195
|
|
|
192
196
|
|
|
193
197
|
@sdk.rest_api('GET', '/example-resource-empty', access=sdk.ACCESS_PUBLIC)
|
|
198
|
+
@sdk.cache_api('READ', ('example', 'resource'))
|
|
194
199
|
def some1(api_resource: ApiResource) -> JsonApiResponse[RespObject]:
|
|
195
200
|
return api_resource.response_ok(RespObject(now=datetime.now()))
|
|
196
201
|
|
|
@@ -213,6 +218,7 @@ def some12(api_resource: ApiResource, body: List[SomeBody]) -> JsonApiResponse[R
|
|
|
213
218
|
|
|
214
219
|
|
|
215
220
|
@sdk.html_view(('GET', 'POST'), '/', access=sdk.ACCESS_PUBLIC)
|
|
221
|
+
@sdk.cache_api('READ', 'some')
|
|
216
222
|
def view_home(api_resource: ApiResource, body: Optional[List[SomeBody]]) -> HtmlApiResponse:
|
|
217
223
|
sleep(0.02)
|
|
218
224
|
sess = db.session()
|
ul_api_utils/const.py
CHANGED
|
@@ -41,10 +41,10 @@ MIME__MSGPCK = 'application/x-msgpack'
|
|
|
41
41
|
|
|
42
42
|
ENCODING_MIME__GZIP = 'gzip'
|
|
43
43
|
|
|
44
|
-
|
|
45
44
|
REQUEST_METHOD__PUT = 'PUT'
|
|
46
45
|
REQUEST_METHOD__GET = 'GET'
|
|
47
46
|
REQUEST_METHOD__POST = 'POST'
|
|
47
|
+
REQUEST_METHOD__QUERY = 'QUERY'
|
|
48
48
|
REQUEST_METHOD__PATCH = 'PATCH'
|
|
49
49
|
REQUEST_METHOD__DELETE = 'DELETE'
|
|
50
50
|
REQUEST_METHOD__OPTIONS = 'OPTIONS'
|
ul_api_utils/modules/api_sdk.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import List, Union, Callable, Tuple, Any, Optional, TypeVar, cast, S
|
|
|
11
11
|
|
|
12
12
|
from flask import Response, request, Flask, url_for as flask_url_for, _app_ctx_stack
|
|
13
13
|
from pydantic import BaseModel
|
|
14
|
+
from redis.connection import parse_url
|
|
14
15
|
from ul_py_tool.utils.arg_files_glob import arg_files_print
|
|
15
16
|
from werkzeug import Response as BaseResponse
|
|
16
17
|
|
|
@@ -29,6 +30,7 @@ from ul_api_utils.errors import UserAbstractApiError, ResourceRuntimeApiError, R
|
|
|
29
30
|
from ul_api_utils.internal_api.internal_api_check_context import internal_api_check_context
|
|
30
31
|
from ul_api_utils.modules.api_sdk_config import ApiSdkConfig
|
|
31
32
|
from ul_api_utils.modules.intermediate_state import try_init, try_configure
|
|
33
|
+
from ul_api_utils.resources.caching import ULCache, TCacheMode, ULCacheMode, ULCacheConfig
|
|
32
34
|
from ul_api_utils.resources.debugger_scripts import load_debugger_static_scripts
|
|
33
35
|
from ul_api_utils.resources.health_check.health_check import HealthCheckContext
|
|
34
36
|
from ul_api_utils.resources.health_check.resource import init_health_check_resource
|
|
@@ -50,7 +52,9 @@ from ul_api_utils.utils.uuid_converter import UUID4Converter
|
|
|
50
52
|
|
|
51
53
|
if TYPE_CHECKING:
|
|
52
54
|
import flask_sqlalchemy
|
|
55
|
+
from flask_pymongo import PyMongo # type: ignore # lib without mypy stubs
|
|
53
56
|
from ul_db_utils.modules.db import DbConfig
|
|
57
|
+
from ul_db_utils.modules.mongo_db_modules.db import MongoDbConfig
|
|
54
58
|
|
|
55
59
|
TFn = TypeVar("TFn", bound=Callable[..., ApiResponse])
|
|
56
60
|
|
|
@@ -103,6 +107,7 @@ class ApiSdk:
|
|
|
103
107
|
'_fn_registry',
|
|
104
108
|
'_flask_app_cache',
|
|
105
109
|
'_limiter_enabled',
|
|
110
|
+
'_cache',
|
|
106
111
|
'_db',
|
|
107
112
|
)
|
|
108
113
|
|
|
@@ -115,22 +120,30 @@ class ApiSdk:
|
|
|
115
120
|
self._initialized_flask_name: Optional[str] = None
|
|
116
121
|
self._flask_app_cache: Optional[Flask] = None
|
|
117
122
|
self._limiter_enabled = False
|
|
123
|
+
self._cache = None
|
|
118
124
|
|
|
119
125
|
self._templates_dir = os.path.join(APPLICATION_DIR, 'templates')
|
|
120
126
|
|
|
121
127
|
self._fn_registry: List[ApiSdkResource] = []
|
|
122
|
-
self._db: Optional['flask_sqlalchemy.SQLAlchemy'] = None
|
|
128
|
+
self._db: Optional['flask_sqlalchemy.SQLAlchemy'] | Optional['PyMongo'] = None
|
|
123
129
|
|
|
124
130
|
@property
|
|
125
131
|
def config(self) -> ApiSdkConfig:
|
|
126
132
|
return self._config
|
|
127
133
|
|
|
128
|
-
def init_with_flask(self, app_name: str, *, db_config: Optional['DbConfig'] = None) -> Flask:
|
|
134
|
+
def init_with_flask(self, app_name: str, *, db_config: Optional['MongoDbConfig'] | Optional['DbConfig'] = None) -> Flask:
|
|
129
135
|
self._initialized_flask_name = try_init(self, app_name)
|
|
130
136
|
|
|
131
|
-
if db_config is not None:
|
|
137
|
+
if db_config is not None and type(db_config).__name__ == 'MongoDbConfig':
|
|
138
|
+
from ul_db_utils.utils.waiting_for_mongo import waiting_for_mongo
|
|
139
|
+
from ul_db_utils.modules.mongo_db_modules.db import db
|
|
140
|
+
db_config._init_from_sdk_with_flask(self)
|
|
141
|
+
waiting_for_mongo(db_config.uri)
|
|
142
|
+
self._db = db
|
|
143
|
+
|
|
144
|
+
if db_config is not None and type(db_config).__name__ == 'DbConfig':
|
|
132
145
|
from ul_db_utils.utils.waiting_for_postgres import waiting_for_postgres
|
|
133
|
-
from ul_db_utils.modules.db import db
|
|
146
|
+
from ul_db_utils.modules.db import db # type: ignore # yes, db already defined, but can not use two conigs
|
|
134
147
|
db_config._init_from_sdk_with_flask(self)
|
|
135
148
|
waiting_for_postgres(db_config.uri)
|
|
136
149
|
self._db = db
|
|
@@ -143,6 +156,26 @@ class ApiSdk:
|
|
|
143
156
|
rate_limit=self._config.rate_limit,
|
|
144
157
|
storage_uri=self._config.rate_limit_storage_uri,
|
|
145
158
|
)
|
|
159
|
+
if self._config.cache_storage_uri:
|
|
160
|
+
cache_config: ULCacheConfig = {
|
|
161
|
+
'CACHE_TYPE': "RedisCache",
|
|
162
|
+
'CACHE_REDIS_HOST': '',
|
|
163
|
+
'CACHE_REDIS_PORT': '',
|
|
164
|
+
'CACHE_REDIS_PASSWORD': '',
|
|
165
|
+
'CACHE_DEFAULT_TIMEOUT': self._config.cache_default_ttl,
|
|
166
|
+
'CACHE_KEY_PREFIX': f'CACHE__{self._config.service_name}',
|
|
167
|
+
'CACHE_SOURCE_CHECK': True,
|
|
168
|
+
}
|
|
169
|
+
try:
|
|
170
|
+
redis_url = parse_url(self._config.cache_storage_uri)
|
|
171
|
+
except ValueError as e:
|
|
172
|
+
logger.error(f'broken redis uri :: {e}')
|
|
173
|
+
else:
|
|
174
|
+
assert all(('host' in redis_url, 'port' in redis_url)), 'missing part of redis uri'
|
|
175
|
+
cache_config['CACHE_REDIS_HOST'] = redis_url['host']
|
|
176
|
+
cache_config['CACHE_REDIS_PORT'] = redis_url['port']
|
|
177
|
+
cache_config['CACHE_REDIS_PASSWORD'] = redis_url.get('password', '')
|
|
178
|
+
self._cache = ULCache(self._flask_app, config=cache_config) # type: ignore
|
|
146
179
|
|
|
147
180
|
route_files, ignored_route_files = load_modules_by_template([
|
|
148
181
|
os.path.join(APPLICATION_DIR, 'routes', 'api_*.py'),
|
|
@@ -206,7 +239,7 @@ class ApiSdk:
|
|
|
206
239
|
|
|
207
240
|
flask_app.before_request(self._before_request)
|
|
208
241
|
flask_app.after_request(self._after_request)
|
|
209
|
-
flask_app.teardown_request(self.
|
|
242
|
+
flask_app.teardown_request(self._teardown_request)
|
|
210
243
|
|
|
211
244
|
flask_app.errorhandler(404)(functools.partial(
|
|
212
245
|
not_implemented_handler,
|
|
@@ -217,7 +250,11 @@ class ApiSdk:
|
|
|
217
250
|
return flask_app
|
|
218
251
|
|
|
219
252
|
@property
|
|
220
|
-
def
|
|
253
|
+
def ul_cache_factory(self) -> Optional['ULCache']:
|
|
254
|
+
return self._cache
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def db(self) -> Optional['flask_sqlalchemy.SQLAlchemy'] | Optional['PyMongo']:
|
|
221
258
|
return self._db
|
|
222
259
|
|
|
223
260
|
def _before_request(self) -> None:
|
|
@@ -238,7 +275,7 @@ class ApiSdk:
|
|
|
238
275
|
def _debugger_enabled_with_pin(self) -> bool:
|
|
239
276
|
return request.headers.get(REQUEST_HEADER__DEBUGGER) == APPLICATION_DEBUGGER_PIN or request.cookies.get(REQUEST_HEADER__DEBUGGER, "") == APPLICATION_DEBUGGER_PIN
|
|
240
277
|
|
|
241
|
-
def
|
|
278
|
+
def _teardown_request(self, err: Optional[BaseException]) -> None:
|
|
242
279
|
clean_files()
|
|
243
280
|
|
|
244
281
|
def _debug_internal_request(self) -> bool:
|
|
@@ -276,6 +313,18 @@ class ApiSdk:
|
|
|
276
313
|
def health_check(self) -> Callable[[Callable[[HealthCheckContext], None]], None]:
|
|
277
314
|
return functools.partial(init_health_check_resource, api_sdk=self)
|
|
278
315
|
|
|
316
|
+
def cache_api(
|
|
317
|
+
self,
|
|
318
|
+
mode: TCacheMode,
|
|
319
|
+
tags: Tuple[str, ...] | str,
|
|
320
|
+
timeout: Optional[int] = None,
|
|
321
|
+
source_check: Optional[bool] = True,
|
|
322
|
+
) -> Callable[[TFn], TFn]:
|
|
323
|
+
assert self.ul_cache_factory is not None, 'cache not configured !'
|
|
324
|
+
if ULCacheMode.compile_mode(mode) == ULCacheMode.READ.value:
|
|
325
|
+
return self.ul_cache_factory.cache_read_wrap(tags, timeout, source_check)
|
|
326
|
+
return self.ul_cache_factory.cache_refresh_wrap(tags)
|
|
327
|
+
|
|
279
328
|
def rest_api(
|
|
280
329
|
self,
|
|
281
330
|
method: TMethodShort,
|
|
@@ -427,6 +476,7 @@ class ApiSdk:
|
|
|
427
476
|
res = api_resource_config.override_flask_response(res)
|
|
428
477
|
|
|
429
478
|
ri = api_resource.request_info
|
|
479
|
+
# TODO: add cache hit flag
|
|
430
480
|
logger.info('AUDIT ' + json.dumps({
|
|
431
481
|
'user_id': str(scope_user_id) if scope_user_id else None,
|
|
432
482
|
'token_id': str(scope_token_id) if scope_token_id else None,
|
|
@@ -48,6 +48,9 @@ class ApiSdkConfig(BaseModel):
|
|
|
48
48
|
rate_limit_storage_uri: str = '' # supports url of redis, memcached, mongodb
|
|
49
49
|
rate_limit_identify: Union[ApiSdkIdentifyTypeEnum, Callable[[], str]] = ApiSdkIdentifyTypeEnum.DISABLED # must be None if disabled
|
|
50
50
|
|
|
51
|
+
cache_storage_uri: str = '' # supports only redis
|
|
52
|
+
cache_default_ttl: int = 60 # seconds
|
|
53
|
+
|
|
51
54
|
flask_debugging_plugins: Optional[ApiSdkFlaskDebuggingPluginsEnabled] = None
|
|
52
55
|
|
|
53
56
|
api_route_path_prefix: str = '/api'
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import hashlib
|
|
3
|
+
import inspect
|
|
4
|
+
import itertools
|
|
5
|
+
import logging
|
|
6
|
+
from _hashlib import HASH # type: ignore
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional, Callable, Dict, Tuple, List, Union, Literal, cast, TypeVar, Any, Set, TypedDict
|
|
10
|
+
|
|
11
|
+
import ormsgpack
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
from werkzeug import Response as BaseResponse
|
|
14
|
+
|
|
15
|
+
from flask import request
|
|
16
|
+
from flask_caching import Cache, CachedResponse
|
|
17
|
+
|
|
18
|
+
from ul_api_utils.api_resource.api_response import ApiResponse, JsonApiResponse
|
|
19
|
+
from ul_api_utils.conf import APPLICATION_DEBUG
|
|
20
|
+
from ul_api_utils.utils.constants import TKwargs
|
|
21
|
+
from ul_api_utils.utils.json_encoder import CustomJSONEncoder
|
|
22
|
+
|
|
23
|
+
TCacheModeStr = Union[Literal['READ'], Literal['REFRESH']]
|
|
24
|
+
TFn = TypeVar("TFn", bound=Callable[..., ApiResponse])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ULCacheMode(Enum):
|
|
28
|
+
READ = 'READ'
|
|
29
|
+
REFRESH = 'REFRESH'
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def compile_mode(mode: Union[TCacheModeStr, 'ULCacheMode']) -> str:
|
|
33
|
+
if isinstance(mode, ULCacheMode):
|
|
34
|
+
return mode.value
|
|
35
|
+
assert isinstance(mode, str)
|
|
36
|
+
m = mode.strip().upper()
|
|
37
|
+
return ULCacheMode(m).value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
TCacheMode = Union[TCacheModeStr, 'ULCacheMode']
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ULCacheConfig(TypedDict): # https://flask-caching.readthedocs.io/en/latest/#configuring-flask-caching
|
|
44
|
+
CACHE_TYPE: str
|
|
45
|
+
CACHE_REDIS_HOST: str
|
|
46
|
+
CACHE_REDIS_PORT: str
|
|
47
|
+
CACHE_REDIS_PASSWORD: str
|
|
48
|
+
CACHE_KEY_PREFIX: str
|
|
49
|
+
CACHE_DEFAULT_TIMEOUT: int
|
|
50
|
+
CACHE_SOURCE_CHECK: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ULCache(Cache):
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def tags_map(self) -> Dict[Tuple[str, ...], Set[str]]:
|
|
57
|
+
if self.cache.has(':tags_map'):
|
|
58
|
+
return self.cache.get(':tags_map')
|
|
59
|
+
else:
|
|
60
|
+
return defaultdict(set)
|
|
61
|
+
|
|
62
|
+
@tags_map.setter
|
|
63
|
+
def tags_map(self, value: Dict[Tuple[str], Set[str]]) -> None:
|
|
64
|
+
self.cache.set(':tags_map', value, -1)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _has_common_elements(seq: Set[Tuple[str, ...]]) -> bool:
|
|
68
|
+
return bool(functools.reduce(set.intersection, map(set, seq))) # type: ignore
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _format_cache_key(parts: List[Any]) -> str:
|
|
72
|
+
return ':' + ':'.join([p for p in parts if p])
|
|
73
|
+
|
|
74
|
+
def make_cache_key(
|
|
75
|
+
self,
|
|
76
|
+
fn: TFn,
|
|
77
|
+
source_check: bool,
|
|
78
|
+
hash_method: Callable[[bytes], HASH],
|
|
79
|
+
) -> str:
|
|
80
|
+
cache_key_parts = [request.path]
|
|
81
|
+
cache_hash = None
|
|
82
|
+
if request.args:
|
|
83
|
+
args_as_sorted_tuple = tuple(sorted(pair for pair in request.args.items(multi=True)))
|
|
84
|
+
|
|
85
|
+
args_as_bytes = str(args_as_sorted_tuple).encode()
|
|
86
|
+
cache_hash = hash_method(args_as_bytes)
|
|
87
|
+
|
|
88
|
+
# Use the source code if source_check is True and update the
|
|
89
|
+
# cache_hash before generating the hashing and using it in cache_key
|
|
90
|
+
if source_check and callable(fn):
|
|
91
|
+
func_source_code = inspect.getsource(fn)
|
|
92
|
+
if cache_hash is not None:
|
|
93
|
+
cache_hash.update(func_source_code.encode("utf-8"))
|
|
94
|
+
else:
|
|
95
|
+
cache_hash = hash_method(func_source_code.encode("utf-8"))
|
|
96
|
+
cache_hash = str(cache_hash.hexdigest()) if cache_hash is not None else ''
|
|
97
|
+
cache_key_parts.append(cache_hash)
|
|
98
|
+
|
|
99
|
+
return self._format_cache_key(cache_key_parts)
|
|
100
|
+
|
|
101
|
+
def cache_refresh_wrap(
|
|
102
|
+
self,
|
|
103
|
+
tags: Tuple[str, ...] | str,
|
|
104
|
+
) -> Callable[[TFn], TFn]:
|
|
105
|
+
def wrap(fn: TFn) -> TFn:
|
|
106
|
+
assert fn.__module__, 'empty __module__ of function'
|
|
107
|
+
func_tags = tags if isinstance(tags, tuple) else (tags,)
|
|
108
|
+
|
|
109
|
+
@functools.wraps(fn)
|
|
110
|
+
def wrapper(*args: Any, **kwargs: TKwargs) -> Tuple[BaseResponse, int]:
|
|
111
|
+
dependent_keys = []
|
|
112
|
+
tags_map = self.tags_map.copy()
|
|
113
|
+
|
|
114
|
+
for t in tags_map.keys():
|
|
115
|
+
stored_tags = t if isinstance(t, tuple) else (t,)
|
|
116
|
+
if self._has_common_elements({stored_tags, func_tags}):
|
|
117
|
+
dependent_keys.append(tags_map[stored_tags].copy())
|
|
118
|
+
tags_map[stored_tags] = set()
|
|
119
|
+
cache_list_to_reset: List[str] = list(itertools.chain.from_iterable(dependent_keys))
|
|
120
|
+
self.delete_many(*cache_list_to_reset)
|
|
121
|
+
tags_map[func_tags] = set()
|
|
122
|
+
self.tags_map = tags_map
|
|
123
|
+
return self._call_fn(fn, *args, **kwargs)
|
|
124
|
+
|
|
125
|
+
return cast(TFn, wrapper)
|
|
126
|
+
return wrap
|
|
127
|
+
|
|
128
|
+
def cache_read_wrap(
|
|
129
|
+
self,
|
|
130
|
+
tags: Tuple[str, ...] | str,
|
|
131
|
+
timeout: Optional[int] = None,
|
|
132
|
+
source_check: Optional[bool] = None,
|
|
133
|
+
hash_method: Callable[[bytes], HASH] = hashlib.md5,
|
|
134
|
+
) -> Callable[[TFn], TFn]:
|
|
135
|
+
def cache_wrap(fn: TFn) -> TFn:
|
|
136
|
+
assert fn.__module__, 'empty __module__ of function'
|
|
137
|
+
|
|
138
|
+
nonlocal source_check
|
|
139
|
+
if source_check is None:
|
|
140
|
+
source_check = self.source_check or False
|
|
141
|
+
|
|
142
|
+
func_tags = tags if isinstance(tags, tuple) else (tags,)
|
|
143
|
+
if self.tags_map.get(func_tags) is None:
|
|
144
|
+
self.tags_map[func_tags] = set()
|
|
145
|
+
|
|
146
|
+
logger = logging.getLogger(fn.__module__)
|
|
147
|
+
|
|
148
|
+
@functools.wraps(fn)
|
|
149
|
+
def cache_wrapper(*args: Any, **kwargs: TKwargs) -> Tuple[BaseResponse | CachedResponse, int]:
|
|
150
|
+
try:
|
|
151
|
+
assert source_check is not None # just for mypy
|
|
152
|
+
cache_key = self.make_cache_key(fn, source_check, hash_method)
|
|
153
|
+
if resp := self.cache.get(cache_key):
|
|
154
|
+
try:
|
|
155
|
+
resp = JsonApiResponse(**ormsgpack.unpackb(resp))
|
|
156
|
+
except ValidationError:
|
|
157
|
+
logger.error(f'cache read error of {fn.__name__} :: response not type of {JsonApiResponse.__name__}')
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
found = True
|
|
161
|
+
# If the value returned by cache.get() is None
|
|
162
|
+
# it might be because the key is not found in the cache
|
|
163
|
+
# or because the cached value is actually None
|
|
164
|
+
if resp is None:
|
|
165
|
+
found = self.cache.has(cache_key)
|
|
166
|
+
except Exception as e: # noqa: B902 # cuz lot of variations of cache factory impementaions
|
|
167
|
+
logger.error(f'error due cache check :: {e}')
|
|
168
|
+
return self._call_fn(fn, *args, **kwargs)
|
|
169
|
+
if not found:
|
|
170
|
+
tags_map = self.tags_map.copy()
|
|
171
|
+
tags_map[func_tags].add(cache_key)
|
|
172
|
+
resp = self._call_fn(fn, *args, **kwargs)
|
|
173
|
+
try:
|
|
174
|
+
self.cache.set(
|
|
175
|
+
key=cache_key,
|
|
176
|
+
value=ormsgpack.packb(resp, default=CustomJSONEncoder().default),
|
|
177
|
+
timeout=timeout,
|
|
178
|
+
)
|
|
179
|
+
except Exception as e: # noqa: B902 # cuz lot of variations of cache factory impementaions
|
|
180
|
+
if APPLICATION_DEBUG:
|
|
181
|
+
raise
|
|
182
|
+
logger.error(f'exception possibly due to cache response :: {e}')
|
|
183
|
+
self.tags_map = self.tags_map | tags_map
|
|
184
|
+
return resp
|
|
185
|
+
|
|
186
|
+
return cast(TFn, cache_wrapper)
|
|
187
|
+
return cache_wrap
|
ul_api_utils/utils/api_method.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from enum import Enum, unique
|
|
2
2
|
from typing import Union, List, Tuple, Any, Literal
|
|
3
3
|
|
|
4
|
-
from ul_api_utils.const import REQUEST_METHOD__PUT, REQUEST_METHOD__GET, REQUEST_METHOD__POST,
|
|
4
|
+
from ul_api_utils.const import REQUEST_METHOD__PUT, REQUEST_METHOD__GET, REQUEST_METHOD__POST, \
|
|
5
|
+
REQUEST_METHOD__PATCH, REQUEST_METHOD__DELETE, REQUEST_METHOD__OPTIONS, REQUEST_METHOD__QUERY
|
|
5
6
|
|
|
6
7
|
TMethodStr = Union[Literal['GET'], Literal['POST'], Literal['PUT'], Literal['PATCH'], Literal['DELETE'], Literal['OPTIONS']]
|
|
7
8
|
TMethod = Union[TMethodStr, 'ApiMethod', List[TMethodStr], Tuple[TMethodStr, ...], List['ApiMethod'], Tuple['ApiMethod', ...]]
|
|
@@ -13,6 +14,7 @@ class ApiMethod(Enum):
|
|
|
13
14
|
PUT = REQUEST_METHOD__PUT
|
|
14
15
|
GET = REQUEST_METHOD__GET
|
|
15
16
|
POST = REQUEST_METHOD__POST
|
|
17
|
+
QUERY = REQUEST_METHOD__QUERY
|
|
16
18
|
PATCH = REQUEST_METHOD__PATCH
|
|
17
19
|
DELETE = REQUEST_METHOD__DELETE
|
|
18
20
|
OPTIONS = REQUEST_METHOD__OPTIONS
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ul-api-utils
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.7.0
|
|
4
4
|
Summary: Python api utils
|
|
5
5
|
Author: Unic-lab
|
|
6
6
|
Author-email:
|
|
@@ -23,6 +23,7 @@ Requires-Dist: jinja2 (==3.1.2)
|
|
|
23
23
|
Requires-Dist: flask (==2.1.3)
|
|
24
24
|
Requires-Dist: flask-wtf (==1.0.1)
|
|
25
25
|
Requires-Dist: flask-limiter (==2.5.1)
|
|
26
|
+
Requires-Dist: flask-caching (==2.1.0)
|
|
26
27
|
Requires-Dist: flask-swagger-ui (==4.11.1)
|
|
27
28
|
Requires-Dist: flask-monitoringdashboard (==3.1.2)
|
|
28
29
|
Requires-Dist: pycryptodome (==3.15.0)
|
|
@@ -32,6 +33,7 @@ Requires-Dist: pyyaml (==6.0)
|
|
|
32
33
|
Requires-Dist: requests (==2.28.1)
|
|
33
34
|
Requires-Dist: cryptography (==38.0.1)
|
|
34
35
|
Requires-Dist: colored (==1.4.3)
|
|
36
|
+
Requires-Dist: ormsgpack (==1.4.1)
|
|
35
37
|
Requires-Dist: msgpack (==1.0.4)
|
|
36
38
|
Requires-Dist: msgpack-types (==0.2.0)
|
|
37
39
|
Requires-Dist: fastavro (==1.7.0)
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
example/conf.py,sha256=
|
|
2
|
+
example/conf.py,sha256=WPMSjEdxkxp7Vnwc-zBJrX3dE2KfbSMegsIiHktbY-M,844
|
|
3
3
|
example/main.py,sha256=uTHooRO8ovcQkS3ejR_T5eVB7hQhUCwAjCxqbcn9a9k,405
|
|
4
4
|
example/permissions.py,sha256=i8_zOOPdra3oMXZfyTspewRYNdn21PCqOD1ATG69Itk,277
|
|
5
5
|
example/pure_flask_example.py,sha256=A7cbcjTr28FS1sVNAsQbj1N9EgEFIXDB4aRwOV6_tbU,1329
|
|
6
6
|
example/rate_limit_load.py,sha256=U2Bgp8UztT4TNKdv9NVioxWfE68aCsC7uKz7xPCy6XM,225
|
|
7
7
|
example/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
example/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
example/routes/api_some.py,sha256=
|
|
9
|
+
example/routes/api_some.py,sha256=omsHmHyzT_DfYqzZRakmyI7uDldn2S6Nrjqdv8te_Rk,11859
|
|
10
10
|
example/workers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
example/workers/worker.py,sha256=flMYq50OhLtNSaA2qyDJSMeXSNXIqhdBIsaxcmO5-xQ,681
|
|
12
12
|
ul_api_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
ul_api_utils/conf.py,sha256=JfT8g2MlxDR2iTkvlW13o7-2NZJORsG1MYx86OjOzPQ,3232
|
|
14
|
-
ul_api_utils/const.py,sha256=
|
|
14
|
+
ul_api_utils/const.py,sha256=pzY-zRznCJjZ0mFlte6XEsQQCU7EydN2WweEsVHSE7k,2563
|
|
15
15
|
ul_api_utils/errors.py,sha256=kmmgNXJtgVczOskVG8Ye1WSHbqSSAWC2OzUjPUGCkGo,8137
|
|
16
16
|
ul_api_utils/main.py,sha256=-32Qbz3ZeDUg7P2Xu67OIjIDUddPoHH6ibIa11xPl_k,779
|
|
17
17
|
ul_api_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -55,8 +55,8 @@ ul_api_utils/internal_api/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQ
|
|
|
55
55
|
ul_api_utils/internal_api/__tests__/internal_api.py,sha256=X2iopeso6vryszeeA__lcqXQVtz3Nwt3ngH7M4OuN1U,1116
|
|
56
56
|
ul_api_utils/internal_api/__tests__/internal_api_content_type.py,sha256=mfiYPkzKtfZKFpi4RSnWAoCd6mRijr6sFsa2TF-s5t8,749
|
|
57
57
|
ul_api_utils/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
58
|
-
ul_api_utils/modules/api_sdk.py,sha256=
|
|
59
|
-
ul_api_utils/modules/api_sdk_config.py,sha256=
|
|
58
|
+
ul_api_utils/modules/api_sdk.py,sha256=xnU4qN16uJ5rbKYTbfT7gfQ0S9-P0P2mTCcRlmeoFo4,25059
|
|
59
|
+
ul_api_utils/modules/api_sdk_config.py,sha256=cKQhIuEaLp4o9UVA4fmnR4QuEZPO9pqEjs_Xqgk8Mlw,2042
|
|
60
60
|
ul_api_utils/modules/api_sdk_jwt.py,sha256=2XRfb0LxHUnldSL67S60v1uyoDpVPNaq4zofUtkeg88,15112
|
|
61
61
|
ul_api_utils/modules/intermediate_state.py,sha256=7ZZ3Sypbb8LaSfrVhaXaWRDnj8oyy26NUbmFK7vr-y4,1270
|
|
62
62
|
ul_api_utils/modules/worker_context.py,sha256=jGjopeuYuTtIDmsrqK7TcbTD-E81t8OWvWS1JpTC6b0,802
|
|
@@ -65,6 +65,7 @@ ul_api_utils/modules/worker_sdk_config.py,sha256=GL64FYYFZHBqqux_cygkSlJL-i6GgO2
|
|
|
65
65
|
ul_api_utils/modules/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
66
|
ul_api_utils/modules/__tests__/test_api_sdk_jwt.py,sha256=9JJTtva2z4pjvTWQqo_0EOvzf4wBgvq0G77jM0SC3Bg,10719
|
|
67
67
|
ul_api_utils/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
|
+
ul_api_utils/resources/caching.py,sha256=FBxcRa0KZBGoq-IIBgYZTlTPXMXAaQavqO9Q31BIQIQ,7308
|
|
68
69
|
ul_api_utils/resources/debugger_scripts.py,sha256=FnZ9UfAzi7WokEi6bRkrUAEH8lbR3oo1eWr2BXZO4Pc,5077
|
|
69
70
|
ul_api_utils/resources/not_implemented.py,sha256=OQE5LGA4KqZDwP5Wtub3Aw-icwzbqCSKcEFoFp4w7_k,973
|
|
70
71
|
ul_api_utils/resources/permissions.py,sha256=8c8cEPkm69zxgXbDiwUkW6Mi_496-MZXbPOxHITetKs,1436
|
|
@@ -78,7 +79,7 @@ ul_api_utils/resources/health_check/resource.py,sha256=SPd9kMzBOVhFZgMVfV26bDpZy
|
|
|
78
79
|
ul_api_utils/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
80
|
ul_api_utils/utils/api_encoding.py,sha256=dqXknOKgc3HBOk48zDoVOLLwc1vRFATkBY8JW_RVLCo,1450
|
|
80
81
|
ul_api_utils/utils/api_format.py,sha256=wGuk5QP6b7oyl9SJ1ebNVirzCipmyTxqjoEPh2cJzAY,2025
|
|
81
|
-
ul_api_utils/utils/api_method.py,sha256=
|
|
82
|
+
ul_api_utils/utils/api_method.py,sha256=fGFlvS_S65CLTbjW4qvWlwi_woAUzPBQzEMkFJ9-K34,2020
|
|
82
83
|
ul_api_utils/utils/api_pagination.py,sha256=dXCrDcZ3dNu3gKP2XExp7EUoOKaOgzO-6JOdKcFDylU,1827
|
|
83
84
|
ul_api_utils/utils/api_path_version.py,sha256=gOwe0bcKs9ovwgh0XsSzih5rq5coL9rNZy8iyeB-xJc,1965
|
|
84
85
|
ul_api_utils/utils/api_request_info.py,sha256=vxfqs_6-HSd-0o_k8e9KFKWhLNXL0KUHvGB0_9g9bgE,100
|
|
@@ -124,9 +125,9 @@ ul_api_utils/validators/validate_empty_object.py,sha256=3Ck_iwyJE_M5e7l6s1i88aqb
|
|
|
124
125
|
ul_api_utils/validators/validate_uuid.py,sha256=EfvlRirv2EW0Z6w3s8E8rUa9GaI8qXZkBWhnPs8NFrA,257
|
|
125
126
|
ul_api_utils/validators/__tests__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
126
127
|
ul_api_utils/validators/__tests__/test_custom_fields.py,sha256=QLZ7DFta01Z7DOK9Z5Iq4uf_CmvDkVReis-GAl_QN48,1447
|
|
127
|
-
ul_api_utils-7.
|
|
128
|
-
ul_api_utils-7.
|
|
129
|
-
ul_api_utils-7.
|
|
130
|
-
ul_api_utils-7.
|
|
131
|
-
ul_api_utils-7.
|
|
132
|
-
ul_api_utils-7.
|
|
128
|
+
ul_api_utils-7.7.0.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
|
|
129
|
+
ul_api_utils-7.7.0.dist-info/METADATA,sha256=3a01IY_kSPT6jR0YTy9WF1OPtIyss5qdCmTjgAPnckU,14454
|
|
130
|
+
ul_api_utils-7.7.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
131
|
+
ul_api_utils-7.7.0.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
|
|
132
|
+
ul_api_utils-7.7.0.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
|
|
133
|
+
ul_api_utils-7.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|