ul-api-utils 7.6.0__tar.gz → 7.7.0__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.

Potentially problematic release.


This version of ul-api-utils might be problematic. Click here for more details.

Files changed (138) hide show
  1. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/PKG-INFO +1 -1
  2. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/conf.py +2 -0
  3. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/routes/api_some.py +13 -7
  4. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/setup.py +5 -3
  5. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/const.py +1 -1
  6. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/api_sdk.py +45 -4
  7. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/api_sdk_config.py +3 -0
  8. ul-api-utils-7.7.0/ul_api_utils/resources/caching.py +187 -0
  9. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/api_method.py +3 -1
  10. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils.egg-info/PKG-INFO +1 -1
  11. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils.egg-info/SOURCES.txt +1 -0
  12. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils.egg-info/requires.txt +2 -0
  13. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/LICENSE +0 -0
  14. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/README.md +0 -0
  15. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/__init__.py +0 -0
  16. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/main.py +0 -0
  17. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/models/__init__.py +0 -0
  18. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/permissions.py +0 -0
  19. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/pure_flask_example.py +0 -0
  20. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/rate_limit_load.py +0 -0
  21. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/routes/__init__.py +0 -0
  22. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/workers/__init__.py +0 -0
  23. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/example/workers/worker.py +0 -0
  24. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/setup.cfg +0 -0
  25. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/__init__.py +0 -0
  26. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/access/__init__.py +0 -0
  27. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/__init__.py +0 -0
  28. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/api_request.py +0 -0
  29. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/api_resource.py +0 -0
  30. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/api_resource_config.py +0 -0
  31. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/api_resource_fn_typing.py +0 -0
  32. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/api_resource_type.py +0 -0
  33. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/api_response.py +0 -0
  34. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/api_response_db.py +0 -0
  35. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/api_response_payload_alias.py +0 -0
  36. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/db_types.py +0 -0
  37. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/api_resource/signature_check.py +0 -0
  38. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/__init__.py +0 -0
  39. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/cmd_enc_keys.py +0 -0
  40. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/cmd_gen_api_user_token.py +0 -0
  41. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/cmd_gen_new_api_user.py +0 -0
  42. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/cmd_start.py +0 -0
  43. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/cmd_worker_start.py +0 -0
  44. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/start/__init__.py +0 -0
  45. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/start/gunicorn.conf.py +0 -0
  46. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/commands/start/wsgi.py +0 -0
  47. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/conf/ul-debugger-main.js +0 -0
  48. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/conf/ul-debugger-ui.js +0 -0
  49. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/conf.py +0 -0
  50. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/debug/__init__.py +0 -0
  51. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/debug/debugger.py +0 -0
  52. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/debug/malloc.py +0 -0
  53. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/debug/stat.py +0 -0
  54. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/encrypt/__init__.py +0 -0
  55. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/encrypt/encrypt_decrypt_abstract.py +0 -0
  56. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/encrypt/encrypt_decrypt_aes_xtea.py +0 -0
  57. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/errors.py +0 -0
  58. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/internal_api/__init__.py +0 -0
  59. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/internal_api/__tests__/__init__.py +0 -0
  60. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/internal_api/__tests__/internal_api.py +0 -0
  61. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/internal_api/__tests__/internal_api_content_type.py +0 -0
  62. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/internal_api/internal_api.py +0 -0
  63. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/internal_api/internal_api_check_context.py +0 -0
  64. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/internal_api/internal_api_error.py +0 -0
  65. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/internal_api/internal_api_response.py +0 -0
  66. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/main.py +0 -0
  67. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/__init__.py +0 -0
  68. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/__tests__/__init__.py +0 -0
  69. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/__tests__/test_api_sdk_jwt.py +0 -0
  70. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/api_sdk_jwt.py +0 -0
  71. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/intermediate_state.py +0 -0
  72. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/worker_context.py +0 -0
  73. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/worker_sdk.py +0 -0
  74. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/modules/worker_sdk_config.py +0 -0
  75. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/py.typed +0 -0
  76. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/__init__.py +0 -0
  77. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/debugger_scripts.py +0 -0
  78. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/health_check/__init__.py +0 -0
  79. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/health_check/const.py +0 -0
  80. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/health_check/health_check.py +0 -0
  81. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/health_check/health_check_template.py +0 -0
  82. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/health_check/resource.py +0 -0
  83. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/not_implemented.py +0 -0
  84. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/permissions.py +0 -0
  85. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/rate_limitter.py +0 -0
  86. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/resources/swagger.py +0 -0
  87. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/sentry.py +0 -0
  88. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/__init__.py +0 -0
  89. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/__tests__/__init__.py +0 -0
  90. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/__tests__/api_path_version.py +0 -0
  91. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/__tests__/unwrap_typing.py +0 -0
  92. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/api_encoding.py +0 -0
  93. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/api_format.py +0 -0
  94. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/api_pagination.py +0 -0
  95. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/api_path_version.py +0 -0
  96. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/api_request_info.py +0 -0
  97. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/avro.py +0 -0
  98. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/broker_topics_message_count.py +0 -0
  99. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/cached_per_request.py +0 -0
  100. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/colors.py +0 -0
  101. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/constants.py +0 -0
  102. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/decode_base64.py +0 -0
  103. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/deprecated.py +0 -0
  104. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flags.py +0 -0
  105. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/__init__.py +0 -0
  106. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/conf.py +0 -0
  107. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/exceptions.py +0 -0
  108. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/specifiers/__init__.py +0 -0
  109. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_models.py +0 -0
  110. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_specifier.py +0 -0
  111. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_three_specifier.py +0 -0
  112. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_version.py +0 -0
  113. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/utils/__init__.py +0 -0
  114. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/utils/input_type.py +0 -0
  115. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/utils/parameter_type.py +0 -0
  116. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/utils/replace_in_dict.py +0 -0
  117. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/utils/request_type.py +0 -0
  118. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/utils/schema_type.py +0 -0
  119. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/flask_swagger_generator/utils/security_type.py +0 -0
  120. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/imports.py +0 -0
  121. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/jinja/__init__.py +0 -0
  122. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/jinja/t_url_for.py +0 -0
  123. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/jinja/to_pretty_json.py +0 -0
  124. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/json_encoder.py +0 -0
  125. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/load_modules.py +0 -0
  126. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/token_check.py +0 -0
  127. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/token_check_through_request.py +0 -0
  128. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/unwrap_typing.py +0 -0
  129. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/utils/uuid_converter.py +0 -0
  130. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/validators/__init__.py +0 -0
  131. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/validators/__tests__/__init__.py +0 -0
  132. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/validators/__tests__/test_custom_fields.py +0 -0
  133. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/validators/custom_fields.py +0 -0
  134. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/validators/validate_empty_object.py +0 -0
  135. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils/validators/validate_uuid.py +0 -0
  136. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils.egg-info/dependency_links.txt +0 -0
  137. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils.egg-info/entry_points.txt +0 -0
  138. {ul-api-utils-7.6.0 → ul-api-utils-7.7.0}/ul_api_utils.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ul-api-utils
3
- Version: 7.6.0
3
+ Version: 7.7.0
4
4
  Summary: Python api utils
5
5
  Author: Unic-lab
6
6
  Author-email:
@@ -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()
@@ -9,7 +9,7 @@ with open(path.join(HERE, 'README.md'), encoding='utf-8') as f:
9
9
 
10
10
  setup(
11
11
  name='ul-api-utils',
12
- version='7.6.0',
12
+ version='7.7.0',
13
13
  description='Python api utils',
14
14
  author='Unic-lab',
15
15
  long_description=long_description,
@@ -55,6 +55,7 @@ setup(
55
55
  "flask==2.1.3",
56
56
  "flask-wtf==1.0.1",
57
57
  "flask-limiter==2.5.1",
58
+ "flask-caching==2.1.0",
58
59
  "flask-swagger-ui==4.11.1",
59
60
  "flask-monitoringdashboard==3.1.2",
60
61
  "pycryptodome==3.15.0",
@@ -65,6 +66,7 @@ setup(
65
66
  "cryptography==38.0.1",
66
67
  "colored==1.4.3",
67
68
 
69
+ "ormsgpack==1.4.1",
68
70
  "msgpack==1.0.4",
69
71
  "msgpack-types==0.2.0",
70
72
  "fastavro==1.7.0",
@@ -82,7 +84,7 @@ setup(
82
84
  # "opentelemetry-instrumentation-requests==0.27b0",
83
85
  # "opentelemetry-exporter-jaeger==1.8.0",
84
86
  # "opentelemetry-instrumentation-sqlalchemy==0.27b0",
85
- # "ul-db-utils==2.10.9", # ACTUALIZE, BUT DO NOT UNCOMMENT PLEASE
86
- # "ul-py-tool==1.15.20", # ACTUALIZE, BUT DO NOT UNCOMMENT PLEASE
87
+ # "ul-db-utils==3.1.0", # ACTUALIZE, BUT DO NOT UNCOMMENT PLEASE
88
+ # "ul-py-tool==1.15.42", # ACTUALIZE, BUT DO NOT UNCOMMENT PLEASE
87
89
  ],
88
90
  )
@@ -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,7 @@ from ul_api_utils.utils.uuid_converter import UUID4Converter
50
52
 
51
53
  if TYPE_CHECKING:
52
54
  import flask_sqlalchemy
53
- from flask_pymongo import PyMongo
55
+ from flask_pymongo import PyMongo # type: ignore # lib without mypy stubs
54
56
  from ul_db_utils.modules.db import DbConfig
55
57
  from ul_db_utils.modules.mongo_db_modules.db import MongoDbConfig
56
58
 
@@ -105,6 +107,7 @@ class ApiSdk:
105
107
  '_fn_registry',
106
108
  '_flask_app_cache',
107
109
  '_limiter_enabled',
110
+ '_cache',
108
111
  '_db',
109
112
  )
110
113
 
@@ -117,6 +120,7 @@ class ApiSdk:
117
120
  self._initialized_flask_name: Optional[str] = None
118
121
  self._flask_app_cache: Optional[Flask] = None
119
122
  self._limiter_enabled = False
123
+ self._cache = None
120
124
 
121
125
  self._templates_dir = os.path.join(APPLICATION_DIR, 'templates')
122
126
 
@@ -139,7 +143,7 @@ class ApiSdk:
139
143
 
140
144
  if db_config is not None and type(db_config).__name__ == 'DbConfig':
141
145
  from ul_db_utils.utils.waiting_for_postgres import waiting_for_postgres
142
- 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
143
147
  db_config._init_from_sdk_with_flask(self)
144
148
  waiting_for_postgres(db_config.uri)
145
149
  self._db = db
@@ -152,6 +156,26 @@ class ApiSdk:
152
156
  rate_limit=self._config.rate_limit,
153
157
  storage_uri=self._config.rate_limit_storage_uri,
154
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
155
179
 
156
180
  route_files, ignored_route_files = load_modules_by_template([
157
181
  os.path.join(APPLICATION_DIR, 'routes', 'api_*.py'),
@@ -215,7 +239,7 @@ class ApiSdk:
215
239
 
216
240
  flask_app.before_request(self._before_request)
217
241
  flask_app.after_request(self._after_request)
218
- flask_app.teardown_request(self._teardown)
242
+ flask_app.teardown_request(self._teardown_request)
219
243
 
220
244
  flask_app.errorhandler(404)(functools.partial(
221
245
  not_implemented_handler,
@@ -225,6 +249,10 @@ class ApiSdk:
225
249
 
226
250
  return flask_app
227
251
 
252
+ @property
253
+ def ul_cache_factory(self) -> Optional['ULCache']:
254
+ return self._cache
255
+
228
256
  @property
229
257
  def db(self) -> Optional['flask_sqlalchemy.SQLAlchemy'] | Optional['PyMongo']:
230
258
  return self._db
@@ -247,7 +275,7 @@ class ApiSdk:
247
275
  def _debugger_enabled_with_pin(self) -> bool:
248
276
  return request.headers.get(REQUEST_HEADER__DEBUGGER) == APPLICATION_DEBUGGER_PIN or request.cookies.get(REQUEST_HEADER__DEBUGGER, "") == APPLICATION_DEBUGGER_PIN
249
277
 
250
- def _teardown(self, err: Optional[BaseException]) -> None:
278
+ def _teardown_request(self, err: Optional[BaseException]) -> None:
251
279
  clean_files()
252
280
 
253
281
  def _debug_internal_request(self) -> bool:
@@ -285,6 +313,18 @@ class ApiSdk:
285
313
  def health_check(self) -> Callable[[Callable[[HealthCheckContext], None]], None]:
286
314
  return functools.partial(init_health_check_resource, api_sdk=self)
287
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
+
288
328
  def rest_api(
289
329
  self,
290
330
  method: TMethodShort,
@@ -436,6 +476,7 @@ class ApiSdk:
436
476
  res = api_resource_config.override_flask_response(res)
437
477
 
438
478
  ri = api_resource.request_info
479
+ # TODO: add cache hit flag
439
480
  logger.info('AUDIT ' + json.dumps({
440
481
  'user_id': str(scope_user_id) if scope_user_id else None,
441
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.6.0
3
+ Version: 7.7.0
4
4
  Summary: Python api utils
5
5
  Author: Unic-lab
6
6
  Author-email:
@@ -74,6 +74,7 @@ ul_api_utils/modules/worker_sdk_config.py
74
74
  ul_api_utils/modules/__tests__/__init__.py
75
75
  ul_api_utils/modules/__tests__/test_api_sdk_jwt.py
76
76
  ul_api_utils/resources/__init__.py
77
+ ul_api_utils/resources/caching.py
77
78
  ul_api_utils/resources/debugger_scripts.py
78
79
  ul_api_utils/resources/not_implemented.py
79
80
  ul_api_utils/resources/permissions.py
@@ -5,6 +5,7 @@ jinja2==3.1.2
5
5
  flask==2.1.3
6
6
  flask-wtf==1.0.1
7
7
  flask-limiter==2.5.1
8
+ flask-caching==2.1.0
8
9
  flask-swagger-ui==4.11.1
9
10
  flask-monitoringdashboard==3.1.2
10
11
  pycryptodome==3.15.0
@@ -14,6 +15,7 @@ pyyaml==6.0
14
15
  requests==2.28.1
15
16
  cryptography==38.0.1
16
17
  colored==1.4.3
18
+ ormsgpack==1.4.1
17
19
  msgpack==1.0.4
18
20
  msgpack-types==0.2.0
19
21
  fastavro==1.7.0
File without changes
File without changes
File without changes