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 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(
@@ -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'
@@ -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._teardown)
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 db(self) -> Optional['flask_sqlalchemy.SQLAlchemy']:
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 _teardown(self, err: Optional[BaseException]) -> None:
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
@@ -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, REQUEST_METHOD__PATCH, REQUEST_METHOD__DELETE, REQUEST_METHOD__OPTIONS
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.5.1
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=Be8GhNk3jj5mRGQp4XscwOQx-8moZqt_JVvNGkZCj1k,769
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=uRY79chGIQRUSpStCmr_-drvMLd4P3EQANsATYGBEQM,11562
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=vtJNAldaB6WdhW_Klq83SEsrl50_HDZYtD_X3lMUXdM,2532
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=B3uP23hRAhsXwbQAzgiL52NbMVpg9MqwNz0NUThr2vQ,22454
59
- ul_api_utils/modules/api_sdk_config.py,sha256=SwaVA67kxE95v9IXY6OEAzhSJdmnq4KzZQWkMo2sYhw,1943
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=CsR8QmQ9LIsFzdq6JY7TNIFlhjxOE2jLY-oyerbNIwo,1957
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.5.1.dist-info/LICENSE,sha256=6Qo8OdcqI8aGrswJKJYhST-bYqxVQBQ3ujKdTSdq-80,1062
128
- ul_api_utils-7.5.1.dist-info/METADATA,sha256=gd8zAplZsQ9cBv4zcN9lmkDs2eDCkW3gsyvY7yiz6Ac,14380
129
- ul_api_utils-7.5.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
130
- ul_api_utils-7.5.1.dist-info/entry_points.txt,sha256=8tL3ySHWTyJMuV1hx1fHfN8zumDVOCOm63w3StphkXg,53
131
- ul_api_utils-7.5.1.dist-info/top_level.txt,sha256=1XsW8iOSFaH4LOzDcnNyxHpHrbKU3fSn-aIAxe04jmw,21
132
- ul_api_utils-7.5.1.dist-info/RECORD,,
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,,