ul-api-utils 9.3.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.
- example/__init__.py +0 -0
- example/conf.py +35 -0
- example/main.py +24 -0
- example/models/__init__.py +0 -0
- example/permissions.py +6 -0
- example/pure_flask_example.py +65 -0
- example/rate_limit_load.py +10 -0
- example/redis_repository.py +22 -0
- example/routes/__init__.py +0 -0
- example/routes/api_some.py +335 -0
- example/sockets/__init__.py +0 -0
- example/sockets/on_connect.py +16 -0
- example/sockets/on_disconnect.py +14 -0
- example/sockets/on_json.py +10 -0
- example/sockets/on_message.py +13 -0
- example/sockets/on_open.py +16 -0
- example/workers/__init__.py +0 -0
- example/workers/worker.py +28 -0
- ul_api_utils/__init__.py +0 -0
- ul_api_utils/access/__init__.py +122 -0
- ul_api_utils/api_resource/__init__.py +0 -0
- ul_api_utils/api_resource/api_request.py +105 -0
- ul_api_utils/api_resource/api_resource.py +414 -0
- ul_api_utils/api_resource/api_resource_config.py +20 -0
- ul_api_utils/api_resource/api_resource_error_handling.py +21 -0
- ul_api_utils/api_resource/api_resource_fn_typing.py +356 -0
- ul_api_utils/api_resource/api_resource_type.py +16 -0
- ul_api_utils/api_resource/api_response.py +300 -0
- ul_api_utils/api_resource/api_response_db.py +26 -0
- ul_api_utils/api_resource/api_response_payload_alias.py +25 -0
- ul_api_utils/api_resource/db_types.py +9 -0
- ul_api_utils/api_resource/signature_check.py +41 -0
- ul_api_utils/commands/__init__.py +0 -0
- ul_api_utils/commands/cmd_enc_keys.py +172 -0
- ul_api_utils/commands/cmd_gen_api_user_token.py +77 -0
- ul_api_utils/commands/cmd_gen_new_api_user.py +106 -0
- ul_api_utils/commands/cmd_generate_api_docs.py +181 -0
- ul_api_utils/commands/cmd_start.py +110 -0
- ul_api_utils/commands/cmd_worker_start.py +76 -0
- ul_api_utils/commands/start/__init__.py +0 -0
- ul_api_utils/commands/start/gunicorn.conf.local.py +0 -0
- ul_api_utils/commands/start/gunicorn.conf.py +26 -0
- ul_api_utils/commands/start/wsgi.py +22 -0
- ul_api_utils/conf/ul-debugger-main.js +1 -0
- ul_api_utils/conf/ul-debugger-ui.js +1 -0
- ul_api_utils/conf.py +70 -0
- ul_api_utils/const.py +78 -0
- ul_api_utils/debug/__init__.py +0 -0
- ul_api_utils/debug/debugger.py +119 -0
- ul_api_utils/debug/malloc.py +93 -0
- ul_api_utils/debug/stat.py +444 -0
- ul_api_utils/encrypt/__init__.py +0 -0
- ul_api_utils/encrypt/encrypt_decrypt_abstract.py +15 -0
- ul_api_utils/encrypt/encrypt_decrypt_aes_xtea.py +59 -0
- ul_api_utils/errors.py +200 -0
- ul_api_utils/internal_api/__init__.py +0 -0
- ul_api_utils/internal_api/__tests__/__init__.py +0 -0
- ul_api_utils/internal_api/__tests__/internal_api.py +29 -0
- ul_api_utils/internal_api/__tests__/internal_api_content_type.py +22 -0
- ul_api_utils/internal_api/internal_api.py +369 -0
- ul_api_utils/internal_api/internal_api_check_context.py +42 -0
- ul_api_utils/internal_api/internal_api_error.py +17 -0
- ul_api_utils/internal_api/internal_api_response.py +296 -0
- ul_api_utils/main.py +29 -0
- ul_api_utils/modules/__init__.py +0 -0
- ul_api_utils/modules/__tests__/__init__.py +0 -0
- ul_api_utils/modules/__tests__/test_api_sdk_jwt.py +195 -0
- ul_api_utils/modules/api_sdk.py +555 -0
- ul_api_utils/modules/api_sdk_config.py +63 -0
- ul_api_utils/modules/api_sdk_jwt.py +377 -0
- ul_api_utils/modules/intermediate_state.py +34 -0
- ul_api_utils/modules/worker_context.py +35 -0
- ul_api_utils/modules/worker_sdk.py +109 -0
- ul_api_utils/modules/worker_sdk_config.py +13 -0
- ul_api_utils/py.typed +0 -0
- ul_api_utils/resources/__init__.py +0 -0
- ul_api_utils/resources/caching.py +196 -0
- ul_api_utils/resources/debugger_scripts.py +97 -0
- ul_api_utils/resources/health_check/__init__.py +0 -0
- ul_api_utils/resources/health_check/const.py +2 -0
- ul_api_utils/resources/health_check/health_check.py +439 -0
- ul_api_utils/resources/health_check/health_check_template.py +64 -0
- ul_api_utils/resources/health_check/resource.py +97 -0
- ul_api_utils/resources/not_implemented.py +25 -0
- ul_api_utils/resources/permissions.py +29 -0
- ul_api_utils/resources/rate_limitter.py +84 -0
- ul_api_utils/resources/socketio.py +55 -0
- ul_api_utils/resources/swagger.py +119 -0
- ul_api_utils/resources/web_forms/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_fields/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_fields/custom_checkbox_select.py +5 -0
- ul_api_utils/resources/web_forms/custom_widgets/__init__.py +0 -0
- ul_api_utils/resources/web_forms/custom_widgets/custom_select_widget.py +86 -0
- ul_api_utils/resources/web_forms/custom_widgets/custom_text_input_widget.py +42 -0
- ul_api_utils/resources/web_forms/uni_form.py +75 -0
- ul_api_utils/sentry.py +52 -0
- ul_api_utils/utils/__init__.py +0 -0
- ul_api_utils/utils/__tests__/__init__.py +0 -0
- ul_api_utils/utils/__tests__/api_path_version.py +16 -0
- ul_api_utils/utils/__tests__/unwrap_typing.py +67 -0
- ul_api_utils/utils/api_encoding.py +51 -0
- ul_api_utils/utils/api_format.py +61 -0
- ul_api_utils/utils/api_method.py +55 -0
- ul_api_utils/utils/api_pagination.py +58 -0
- ul_api_utils/utils/api_path_version.py +60 -0
- ul_api_utils/utils/api_request_info.py +6 -0
- ul_api_utils/utils/avro.py +131 -0
- ul_api_utils/utils/broker_topics_message_count.py +47 -0
- ul_api_utils/utils/cached_per_request.py +23 -0
- ul_api_utils/utils/colors.py +31 -0
- ul_api_utils/utils/constants.py +7 -0
- ul_api_utils/utils/decode_base64.py +9 -0
- ul_api_utils/utils/deprecated.py +19 -0
- ul_api_utils/utils/flags.py +29 -0
- ul_api_utils/utils/flask_swagger_generator/__init__.py +0 -0
- ul_api_utils/utils/flask_swagger_generator/conf.py +4 -0
- ul_api_utils/utils/flask_swagger_generator/exceptions.py +7 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/__init__.py +0 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_models.py +57 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_specifier.py +48 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_three_specifier.py +777 -0
- ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_version.py +40 -0
- ul_api_utils/utils/flask_swagger_generator/utils/__init__.py +0 -0
- ul_api_utils/utils/flask_swagger_generator/utils/input_type.py +77 -0
- ul_api_utils/utils/flask_swagger_generator/utils/parameter_type.py +51 -0
- ul_api_utils/utils/flask_swagger_generator/utils/replace_in_dict.py +18 -0
- ul_api_utils/utils/flask_swagger_generator/utils/request_type.py +52 -0
- ul_api_utils/utils/flask_swagger_generator/utils/schema_type.py +15 -0
- ul_api_utils/utils/flask_swagger_generator/utils/security_type.py +39 -0
- ul_api_utils/utils/imports.py +16 -0
- ul_api_utils/utils/instance_checks.py +16 -0
- ul_api_utils/utils/jinja/__init__.py +0 -0
- ul_api_utils/utils/jinja/t_url_for.py +19 -0
- ul_api_utils/utils/jinja/to_pretty_json.py +11 -0
- ul_api_utils/utils/json_encoder.py +126 -0
- ul_api_utils/utils/load_modules.py +15 -0
- ul_api_utils/utils/memory_db/__init__.py +0 -0
- ul_api_utils/utils/memory_db/__tests__/__init__.py +0 -0
- ul_api_utils/utils/memory_db/errors.py +8 -0
- ul_api_utils/utils/memory_db/repository.py +102 -0
- ul_api_utils/utils/token_check.py +14 -0
- ul_api_utils/utils/token_check_through_request.py +16 -0
- ul_api_utils/utils/unwrap_typing.py +117 -0
- ul_api_utils/utils/uuid_converter.py +22 -0
- ul_api_utils/validators/__init__.py +0 -0
- ul_api_utils/validators/__tests__/__init__.py +0 -0
- ul_api_utils/validators/__tests__/test_custom_fields.py +32 -0
- ul_api_utils/validators/custom_fields.py +66 -0
- ul_api_utils/validators/validate_empty_object.py +10 -0
- ul_api_utils/validators/validate_uuid.py +11 -0
- ul_api_utils-9.3.0.dist-info/LICENSE +21 -0
- ul_api_utils-9.3.0.dist-info/METADATA +279 -0
- ul_api_utils-9.3.0.dist-info/RECORD +156 -0
- ul_api_utils-9.3.0.dist-info/WHEEL +5 -0
- ul_api_utils-9.3.0.dist-info/entry_points.txt +2 -0
- ul_api_utils-9.3.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
from base64 import b64decode
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import List, Union, Callable, Tuple, Any, Optional, TypeVar, cast, Set, TYPE_CHECKING
|
|
11
|
+
from flask import Response, request, Flask, url_for as flask_url_for, g
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
from redis.connection import parse_url
|
|
14
|
+
from ul_py_tool.utils.arg_files_glob import arg_files_print
|
|
15
|
+
from werkzeug import Response as BaseResponse
|
|
16
|
+
from ul_api_utils.access import PermissionDefinition, GLOBAL_PERMISSION__PRIVATE, GLOBAL_PERMISSION__PRIVATE_RT, \
|
|
17
|
+
GLOBAL_PERMISSION__PUBLIC, PermissionRegistry
|
|
18
|
+
from ul_api_utils.api_resource.api_resource import ApiResource
|
|
19
|
+
from ul_api_utils.api_resource.api_resource_config import ApiResourceConfig
|
|
20
|
+
from ul_api_utils.api_resource.api_resource_fn_typing import ApiResourceFnTyping
|
|
21
|
+
from ul_api_utils.api_resource.api_resource_type import ApiResourceType
|
|
22
|
+
from ul_api_utils.api_resource.api_response import ApiResponse, JsonApiResponse, RootJsonApiResponse, ProxyJsonApiResponse, AnyJsonApiResponse
|
|
23
|
+
from ul_api_utils.conf import APPLICATION_DEBUGGER_PIN, APPLICATION_DEBUG, APPLICATION_DIR, APPLICATION_ENV_IS_LOCAL, APPLICATION_START_DT, APPLICATION_DEBUG_LOGGING
|
|
24
|
+
from ul_api_utils.const import REQUEST_HEADER__DEBUGGER, REQUEST_HEADER__INTERNAL
|
|
25
|
+
from ul_api_utils.debug import stat
|
|
26
|
+
from ul_api_utils.debug.debugger import Debugger
|
|
27
|
+
from ul_api_utils.errors import UserAbstractApiError, ResourceRuntimeApiError, ResponseTypeRuntimeApiError
|
|
28
|
+
from ul_api_utils.internal_api.internal_api_check_context import internal_api_check_context
|
|
29
|
+
from ul_api_utils.modules.api_sdk_config import ApiSdkConfig
|
|
30
|
+
from ul_api_utils.modules.intermediate_state import try_init, try_configure
|
|
31
|
+
from ul_api_utils.resources.caching import ULCache, TCacheMode, ULCacheMode, ULCacheConfig
|
|
32
|
+
from ul_api_utils.resources.debugger_scripts import load_debugger_static_scripts
|
|
33
|
+
from ul_api_utils.resources.health_check.health_check import HealthCheckContext
|
|
34
|
+
from ul_api_utils.resources.health_check.resource import init_health_check_resource
|
|
35
|
+
from ul_api_utils.resources.not_implemented import not_implemented_handler
|
|
36
|
+
from ul_api_utils.resources.permissions import load_permissions
|
|
37
|
+
from ul_api_utils.resources.rate_limitter import init_rate_limiter
|
|
38
|
+
from ul_api_utils.resources.socketio import init_socket_io, SocketAsyncModesEnum, SocketIOConfigType
|
|
39
|
+
from ul_api_utils.resources.swagger import load_swagger, ApiSdkResource
|
|
40
|
+
from ul_api_utils.sentry import sentry
|
|
41
|
+
from ul_api_utils.utils.api_encoding import ApiEncoding
|
|
42
|
+
from ul_api_utils.utils.api_method import TMethod, ApiMethod, TMethodShort
|
|
43
|
+
from ul_api_utils.utils.api_path_version import ApiPathVersion
|
|
44
|
+
from ul_api_utils.utils.cached_per_request import cached_per_request
|
|
45
|
+
from ul_api_utils.utils.constants import TKwargs
|
|
46
|
+
from ul_api_utils.utils.jinja.t_url_for import t_url_for
|
|
47
|
+
from ul_api_utils.utils.jinja.to_pretty_json import to_pretty_json
|
|
48
|
+
from ul_api_utils.utils.json_encoder import CustomJSONProvider, SocketIOJsonWrapper
|
|
49
|
+
from ul_api_utils.utils.load_modules import load_modules_by_template
|
|
50
|
+
from ul_api_utils.utils.uuid_converter import UUID4Converter
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
import flask_socketio # type: ignore # lib without mypy stubs
|
|
54
|
+
import flask_sqlalchemy
|
|
55
|
+
from flask_mongoengine import MongoEngine # type: ignore # lib without mypy stubs
|
|
56
|
+
from ul_db_utils.modules.postgres_modules.db import DbConfig
|
|
57
|
+
from ul_db_utils.modules.mongo_db_modules.db import MongoDbConfig
|
|
58
|
+
|
|
59
|
+
TFn = TypeVar("TFn", bound=Callable[..., ApiResponse])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def add_files_to_clean(files: Set[str]) -> None:
|
|
66
|
+
if hasattr(g, '_api_utils_files_to_clean'):
|
|
67
|
+
for f in files:
|
|
68
|
+
g._api_utils_files_to_clean.add(f) # type: ignore
|
|
69
|
+
else:
|
|
70
|
+
g._api_utils_files_to_clean = files # type: ignore
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clean_files() -> None:
|
|
74
|
+
files: Set[str] = getattr(g, '_api_utils_files_to_clean', set())
|
|
75
|
+
for f in files:
|
|
76
|
+
try:
|
|
77
|
+
os.unlink(f)
|
|
78
|
+
except Exception as e: # noqa: B902
|
|
79
|
+
logger.warning(f'file {f} deleted before clean :: {e}')
|
|
80
|
+
files.clear()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _get_error_types(response: ApiResponse) -> Optional[str]:
|
|
84
|
+
if isinstance(response, AnyJsonApiResponse) and isinstance(response.errors, (list, tuple)) and len(response.errors) > 0:
|
|
85
|
+
for err in response.errors:
|
|
86
|
+
err_t = None
|
|
87
|
+
if isinstance(err, BaseModel): # type: ignore
|
|
88
|
+
err_t = getattr(err, 'error_type', None) # type: ignore
|
|
89
|
+
elif isinstance(err, dict):
|
|
90
|
+
err_t = err.get('error_type', None)
|
|
91
|
+
if isinstance(err_t, str):
|
|
92
|
+
return err_t
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ApiSdk:
|
|
97
|
+
ACCESS_PUBLIC = GLOBAL_PERMISSION__PUBLIC
|
|
98
|
+
ACCESS_PRIVATE = GLOBAL_PERMISSION__PRIVATE
|
|
99
|
+
ACCESS_PRIVATE_RT = GLOBAL_PERMISSION__PRIVATE_RT
|
|
100
|
+
|
|
101
|
+
__slots__ = (
|
|
102
|
+
'_config',
|
|
103
|
+
'_routes_loaded',
|
|
104
|
+
'_request_started_at',
|
|
105
|
+
'_initialized_flask_name',
|
|
106
|
+
'_templates_dir',
|
|
107
|
+
'_fn_registry',
|
|
108
|
+
'_flask_app_cache',
|
|
109
|
+
'_sio',
|
|
110
|
+
'_limiter_enabled',
|
|
111
|
+
'_cache',
|
|
112
|
+
'_db',
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def __init__(self, config: ApiSdkConfig) -> None:
|
|
116
|
+
try_configure(self)
|
|
117
|
+
|
|
118
|
+
self._config = config
|
|
119
|
+
self._routes_loaded = False
|
|
120
|
+
self._request_started_at = time.perf_counter()
|
|
121
|
+
self._initialized_flask_name: Optional[str] = None
|
|
122
|
+
self._flask_app_cache: Optional[Flask] = None
|
|
123
|
+
self._limiter_enabled = False
|
|
124
|
+
self._cache = None
|
|
125
|
+
self._sio: Optional['flask_socketio.SocketIO'] = None
|
|
126
|
+
self._templates_dir = os.path.join(APPLICATION_DIR, 'templates')
|
|
127
|
+
|
|
128
|
+
self._fn_registry: List[ApiSdkResource] = []
|
|
129
|
+
self._db: Optional['flask_sqlalchemy.SQLAlchemy'] | Optional['MongoEngine'] = None
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def config(self) -> ApiSdkConfig:
|
|
133
|
+
return self._config
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def socket(self) -> 'flask_socketio.SocketIO':
|
|
137
|
+
assert self._sio is not None, "SocketIO is not configured, try adding SocketIOConfig to your ApiSdk first."
|
|
138
|
+
return self._sio
|
|
139
|
+
|
|
140
|
+
def init_with_flask(self, app_name: str, *, db_config: Optional['MongoDbConfig'] | Optional['DbConfig'] = None) -> Flask:
|
|
141
|
+
self._initialized_flask_name = try_init(self, app_name)
|
|
142
|
+
self._sio = init_socket_io(config=self._config.socket_config)
|
|
143
|
+
|
|
144
|
+
if db_config is not None and type(db_config).__name__ == 'MongoDbConfig':
|
|
145
|
+
from ul_db_utils.utils.waiting_for_mongo import waiting_for_mongo
|
|
146
|
+
from ul_db_utils.modules.mongo_db_modules.db import db
|
|
147
|
+
db_config._init_from_sdk_with_flask(self)
|
|
148
|
+
waiting_for_mongo(db_config.uri)
|
|
149
|
+
self._db = db
|
|
150
|
+
|
|
151
|
+
if db_config is not None and type(db_config).__name__ == 'DbConfig':
|
|
152
|
+
from ul_db_utils.utils.waiting_for_postgres import waiting_for_postgres
|
|
153
|
+
from ul_db_utils.modules.postgres_modules.db import db # yes, db already defined, but can not use two conigs
|
|
154
|
+
db_config._init_from_sdk_with_flask(self)
|
|
155
|
+
waiting_for_postgres(db_config.uri)
|
|
156
|
+
self._db = db
|
|
157
|
+
|
|
158
|
+
self._limiter_enabled = init_rate_limiter(
|
|
159
|
+
flask_app=self._flask_app,
|
|
160
|
+
debugger_enabled=self._debugger_enabled_with_pin,
|
|
161
|
+
get_auth_token=self._get_auth_token,
|
|
162
|
+
identify=self._config.rate_limit_identify,
|
|
163
|
+
rate_limit=self._config.rate_limit,
|
|
164
|
+
storage_uri=self._config.rate_limit_storage_uri,
|
|
165
|
+
)
|
|
166
|
+
if self._config.cache_storage_uri:
|
|
167
|
+
cache_config: ULCacheConfig = {
|
|
168
|
+
'CACHE_TYPE': "RedisCache",
|
|
169
|
+
'CACHE_REDIS_HOST': '',
|
|
170
|
+
'CACHE_REDIS_PORT': '',
|
|
171
|
+
'CACHE_REDIS_PASSWORD': '',
|
|
172
|
+
'CACHE_DEFAULT_TIMEOUT': self._config.cache_default_ttl,
|
|
173
|
+
'CACHE_KEY_PREFIX': f'CACHE__{self._config.service_name}',
|
|
174
|
+
'CACHE_SOURCE_CHECK': True,
|
|
175
|
+
}
|
|
176
|
+
try:
|
|
177
|
+
redis_url = parse_url(self._config.cache_storage_uri)
|
|
178
|
+
except ValueError as e:
|
|
179
|
+
logger.error(f'broken redis uri :: {e}')
|
|
180
|
+
else:
|
|
181
|
+
assert all(('host' in redis_url, 'port' in redis_url)), 'missing part of redis uri'
|
|
182
|
+
cache_config['CACHE_REDIS_HOST'] = redis_url['host']
|
|
183
|
+
cache_config['CACHE_REDIS_PORT'] = redis_url['port']
|
|
184
|
+
cache_config['CACHE_REDIS_PASSWORD'] = redis_url.get('password', '')
|
|
185
|
+
self._cache = ULCache(self._flask_app, config=cache_config) # type: ignore
|
|
186
|
+
|
|
187
|
+
route_files, ignored_route_files = load_modules_by_template([
|
|
188
|
+
os.path.join(APPLICATION_DIR, 'routes', 'api_*.py'),
|
|
189
|
+
os.path.join(APPLICATION_DIR, 'routes', '**', 'api_*.py'),
|
|
190
|
+
os.path.join(APPLICATION_DIR, 'views', 'view_*.py'),
|
|
191
|
+
os.path.join(APPLICATION_DIR, 'views', '**', 'view_*.py'),
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
if APPLICATION_DEBUG:
|
|
195
|
+
arg_files_print(1000, route_files, ignored_files=ignored_route_files, name='files loaded')
|
|
196
|
+
|
|
197
|
+
if (plugins_config := self.config.flask_debugging_plugins) is not None:
|
|
198
|
+
if plugins_config.flask_monitoring_dashboard:
|
|
199
|
+
import flask_monitoringdashboard as dashboard # type: ignore
|
|
200
|
+
if os.environ.get('FLASK_MONITORING_DASHBOARD_CONFIG'):
|
|
201
|
+
dashboard.config.init_from(envvar='FLASK_MONITORING_DASHBOARD_CONFIG', log_verbose=True)
|
|
202
|
+
dashboard.bind(self._flask_app)
|
|
203
|
+
|
|
204
|
+
load_permissions(self, self._initialized_flask_name, self._config.permissions)
|
|
205
|
+
|
|
206
|
+
load_debugger_static_scripts(self)
|
|
207
|
+
|
|
208
|
+
load_swagger(self, self._fn_registry, self._config.api_route_path_prefix)
|
|
209
|
+
|
|
210
|
+
return self._flask_app
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def _flask_app(self) -> Flask:
|
|
214
|
+
if self._flask_app_cache is not None:
|
|
215
|
+
return self._flask_app_cache
|
|
216
|
+
|
|
217
|
+
if not self._initialized_flask_name:
|
|
218
|
+
raise OverflowError('app was not initialized')
|
|
219
|
+
|
|
220
|
+
flask_app = Flask(
|
|
221
|
+
import_name=self._initialized_flask_name,
|
|
222
|
+
static_url_path=self._config.static_url_path,
|
|
223
|
+
static_folder=os.path.join(APPLICATION_DIR, 'static'),
|
|
224
|
+
template_folder=self._templates_dir,
|
|
225
|
+
)
|
|
226
|
+
self._flask_app_cache = flask_app
|
|
227
|
+
|
|
228
|
+
flask_app.json = CustomJSONProvider(flask_app) # type: ignore
|
|
229
|
+
|
|
230
|
+
flask_app.url_map.converters['uuid'] = UUID4Converter
|
|
231
|
+
|
|
232
|
+
flask_app.config['DEBUG'] = APPLICATION_ENV_IS_LOCAL and APPLICATION_DEBUG
|
|
233
|
+
flask_app.config['EXPLAIN_TEMPLATE_LOADING'] = False
|
|
234
|
+
flask_app.config['ENV'] = 'development' if APPLICATION_DEBUG else 'production'
|
|
235
|
+
flask_app.config['SECRET_KEY'] = 'some-long-long-secret-only-for-wtforms-string-be-brave-if-you-use-it-on-prod'
|
|
236
|
+
flask_app.config['TEMPLATES_AUTO_RELOAD'] = APPLICATION_DEBUG and APPLICATION_ENV_IS_LOCAL
|
|
237
|
+
flask_app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
|
|
238
|
+
flask_app.config['JSON_SORT_KEYS'] = False
|
|
239
|
+
flask_app.config['PREFERRED_URL_SCHEME'] = 'http'
|
|
240
|
+
flask_app.config['APPLICATION_ROOT'] = '/'
|
|
241
|
+
|
|
242
|
+
flask_app.add_template_filter(to_pretty_json, 'tojson_pretty')
|
|
243
|
+
flask_app.add_template_filter(to_pretty_json, 'to_json_pretty')
|
|
244
|
+
|
|
245
|
+
flask_app.add_template_global(t_url_for, 't_url_for')
|
|
246
|
+
|
|
247
|
+
flask_app.before_request(self._before_request)
|
|
248
|
+
flask_app.after_request(self._after_request)
|
|
249
|
+
flask_app.teardown_request(self._teardown_request)
|
|
250
|
+
|
|
251
|
+
flask_app.errorhandler(404)(functools.partial(
|
|
252
|
+
not_implemented_handler,
|
|
253
|
+
import_name=self._initialized_flask_name,
|
|
254
|
+
debugger_enabled=self._debugger_enabled_with_pin,
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
if self._config.socket_config and self._config.socket_config.app_type is SocketIOConfigType.SERVER:
|
|
258
|
+
assert self._sio # mypy
|
|
259
|
+
self._sio.init_app(
|
|
260
|
+
flask_app,
|
|
261
|
+
json=SocketIOJsonWrapper,
|
|
262
|
+
message_queue=self._config.socket_config.message_queue,
|
|
263
|
+
async_mode=SocketAsyncModesEnum.GEVENT.value,
|
|
264
|
+
channel=self._config.socket_config.channel,
|
|
265
|
+
cors_allowed_origins=self._config.socket_config.cors_allowed_origins,
|
|
266
|
+
logger=self._config.socket_config.logs_enabled,
|
|
267
|
+
engineio_logger=self._config.socket_config.engineio_logs_enabled,
|
|
268
|
+
)
|
|
269
|
+
load_modules_by_template([os.path.join(APPLICATION_DIR, 'sockets', '*')])
|
|
270
|
+
|
|
271
|
+
return flask_app
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def ul_cache_factory(self) -> Optional['ULCache']:
|
|
275
|
+
return self._cache
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def db(self) -> Optional['flask_sqlalchemy.SQLAlchemy'] | Optional['MongoEngine']:
|
|
279
|
+
return self._db
|
|
280
|
+
|
|
281
|
+
def _before_request(self) -> None:
|
|
282
|
+
stat.mark_request_started()
|
|
283
|
+
stat.collecting_enable(self._debugger_enabled_with_pin() or (APPLICATION_DEBUG and APPLICATION_ENV_IS_LOCAL))
|
|
284
|
+
|
|
285
|
+
if encoding := ApiEncoding.from_mime(request.content_encoding):
|
|
286
|
+
request._cached_data = encoding.decode(request.get_data())
|
|
287
|
+
|
|
288
|
+
def _after_request(self, response: Response) -> Response:
|
|
289
|
+
assert self._initialized_flask_name is not None
|
|
290
|
+
if APPLICATION_DEBUG and (APPLICATION_DEBUG_LOGGING or APPLICATION_ENV_IS_LOCAL):
|
|
291
|
+
d = Debugger(self._initialized_flask_name, True, ApiMethod(request.method), request.url)
|
|
292
|
+
d.render_console()
|
|
293
|
+
# if not self._debugger_enabled_with_pin() and APPLICATION_ENV_IS_LOCAL and self._debug_internal_request():
|
|
294
|
+
return response
|
|
295
|
+
|
|
296
|
+
def _debugger_enabled_with_pin(self) -> bool:
|
|
297
|
+
return request.headers.get(REQUEST_HEADER__DEBUGGER) == APPLICATION_DEBUGGER_PIN or request.cookies.get(REQUEST_HEADER__DEBUGGER, "") == APPLICATION_DEBUGGER_PIN
|
|
298
|
+
|
|
299
|
+
def _teardown_request(self, err: Optional[BaseException]) -> None:
|
|
300
|
+
clean_files()
|
|
301
|
+
|
|
302
|
+
def _debug_internal_request(self) -> bool:
|
|
303
|
+
return request.headers.get(REQUEST_HEADER__INTERNAL, '') != REQUEST_HEADER__INTERNAL
|
|
304
|
+
|
|
305
|
+
def url_for(self, fn_or_str: Union[Callable, str], **kwargs: Any) -> str: # type: ignore
|
|
306
|
+
assert self._initialized_flask_name is not None
|
|
307
|
+
if not isinstance(fn_or_str, str):
|
|
308
|
+
fn_or_str = fn_or_str.__name__
|
|
309
|
+
return flask_url_for(fn_or_str, **kwargs)
|
|
310
|
+
|
|
311
|
+
def file_download(
|
|
312
|
+
self,
|
|
313
|
+
method: TMethod,
|
|
314
|
+
path: str,
|
|
315
|
+
*,
|
|
316
|
+
config: Optional[ApiResourceConfig] = None,
|
|
317
|
+
v: ApiPathVersion = ApiPathVersion.V01,
|
|
318
|
+
access: Optional[PermissionDefinition] = None,
|
|
319
|
+
) -> Callable[[TFn], TFn]:
|
|
320
|
+
assert isinstance(v, ApiPathVersion)
|
|
321
|
+
path = v.compile_path(path, self._config.api_route_path_prefix)
|
|
322
|
+
return self._wrap(ApiResourceType.FILE, method, path, config, access)
|
|
323
|
+
|
|
324
|
+
def html_view(
|
|
325
|
+
self,
|
|
326
|
+
method: TMethod,
|
|
327
|
+
path: str,
|
|
328
|
+
*,
|
|
329
|
+
config: Optional[ApiResourceConfig] = None,
|
|
330
|
+
access: Optional[PermissionDefinition] = None,
|
|
331
|
+
) -> Callable[[TFn], TFn]:
|
|
332
|
+
return self._wrap(ApiResourceType.WEB, method, path, config, access)
|
|
333
|
+
|
|
334
|
+
def health_check(self) -> Callable[[Callable[[HealthCheckContext], None]], None]:
|
|
335
|
+
return functools.partial(init_health_check_resource, api_sdk=self)
|
|
336
|
+
|
|
337
|
+
def cache_api(
|
|
338
|
+
self,
|
|
339
|
+
mode: TCacheMode,
|
|
340
|
+
tags: Tuple[str, ...] | str,
|
|
341
|
+
timeout: Optional[int] = None,
|
|
342
|
+
source_check: Optional[bool] = True,
|
|
343
|
+
) -> Callable[[TFn], TFn]:
|
|
344
|
+
if self.ul_cache_factory is None:
|
|
345
|
+
logger.warning("Cache URI is not configured, cache will not work.")
|
|
346
|
+
return lambda fn: fn
|
|
347
|
+
if ULCacheMode.compile_mode(mode) == ULCacheMode.READ.value:
|
|
348
|
+
return self.ul_cache_factory.cache_read_wrap(tags, timeout, source_check)
|
|
349
|
+
return self.ul_cache_factory.cache_refresh_wrap(tags)
|
|
350
|
+
|
|
351
|
+
def rest_api(
|
|
352
|
+
self,
|
|
353
|
+
method: TMethodShort,
|
|
354
|
+
path: str,
|
|
355
|
+
*,
|
|
356
|
+
config: Optional[ApiResourceConfig] = None,
|
|
357
|
+
v: ApiPathVersion = ApiPathVersion.V01,
|
|
358
|
+
access: Optional[PermissionDefinition] = None,
|
|
359
|
+
) -> Callable[[TFn], TFn]:
|
|
360
|
+
assert isinstance(v, ApiPathVersion)
|
|
361
|
+
path = v.compile_path(path, self._config.api_route_path_prefix)
|
|
362
|
+
return self._wrap(ApiResourceType.API, method, path, config, access)
|
|
363
|
+
|
|
364
|
+
def _wrap(
|
|
365
|
+
self,
|
|
366
|
+
api_type: ApiResourceType,
|
|
367
|
+
method: TMethod,
|
|
368
|
+
path: str,
|
|
369
|
+
api_resource_config: Optional[ApiResourceConfig] = None,
|
|
370
|
+
access: Optional[PermissionDefinition] = None,
|
|
371
|
+
) -> Callable[[TFn], TFn]:
|
|
372
|
+
config = api_resource_config if api_resource_config is not None else ApiResourceConfig()
|
|
373
|
+
assert isinstance(config, ApiResourceConfig), f'config must be ApiResourceConfig. "{type(config).__name__}" was given'
|
|
374
|
+
access = GLOBAL_PERMISSION__PRIVATE if access is None else access
|
|
375
|
+
|
|
376
|
+
assert isinstance(access, PermissionDefinition)
|
|
377
|
+
if access not in (GLOBAL_PERMISSION__PRIVATE, GLOBAL_PERMISSION__PUBLIC, GLOBAL_PERMISSION__PRIVATE_RT):
|
|
378
|
+
assert isinstance(self._config.permissions, PermissionRegistry)
|
|
379
|
+
assert self._config.permissions.has(access)
|
|
380
|
+
|
|
381
|
+
flask_methods, enum_methods = ApiMethod.compile_methods(method)
|
|
382
|
+
|
|
383
|
+
def wrap(fn: TFn) -> TFn:
|
|
384
|
+
assert self._initialized_flask_name is not None, 'app must be initialized'
|
|
385
|
+
assert fn.__module__, 'empty __module__ of function'
|
|
386
|
+
|
|
387
|
+
fn_module = fn.__module__
|
|
388
|
+
fn_name = fn.__name__
|
|
389
|
+
|
|
390
|
+
logger = logging.getLogger(fn.__module__)
|
|
391
|
+
|
|
392
|
+
fn_typing = ApiResourceFnTyping.parse_fn(api_type, enum_methods, fn)
|
|
393
|
+
|
|
394
|
+
@wraps(fn)
|
|
395
|
+
def wrapper(**kwargs: TKwargs) -> Tuple[BaseResponse, int]:
|
|
396
|
+
now = time.time()
|
|
397
|
+
assert access is not None # for mypy
|
|
398
|
+
debugger_enabled = self._debugger_enabled_with_pin()
|
|
399
|
+
|
|
400
|
+
api_resource = ApiResource(
|
|
401
|
+
logger=logger,
|
|
402
|
+
debugger_enabled=debugger_enabled,
|
|
403
|
+
access=access,
|
|
404
|
+
type=fn_typing.api_resource_type,
|
|
405
|
+
headers=request.headers,
|
|
406
|
+
config=self._config,
|
|
407
|
+
api_resource_config=config,
|
|
408
|
+
fn_typing=fn_typing,
|
|
409
|
+
limiter_enabled=self._limiter_enabled,
|
|
410
|
+
db_initialized=self._db is not None,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
assert self._initialized_flask_name is not None # just for mypy
|
|
414
|
+
d = Debugger(self._initialized_flask_name, debugger_enabled, ApiMethod(request.method), request.url)
|
|
415
|
+
|
|
416
|
+
scope_user_id = None
|
|
417
|
+
scope_token_id = None
|
|
418
|
+
|
|
419
|
+
res: Optional[Tuple[BaseResponse, int]] = None
|
|
420
|
+
|
|
421
|
+
with sentry.configure_scope() as sentry_scope:
|
|
422
|
+
sentry_scope.set_tag('app_name', self._initialized_flask_name)
|
|
423
|
+
sentry_scope.set_tag('app_type', 'api')
|
|
424
|
+
sentry_scope.set_tag('app_uptime', f'{(datetime.now() - APPLICATION_START_DT).seconds // 60}s')
|
|
425
|
+
sentry_scope.set_tag('app_api_name', fn_name)
|
|
426
|
+
sentry_scope.set_tag('app_api_access', access.id)
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
with internal_api_check_context():
|
|
430
|
+
result = None
|
|
431
|
+
|
|
432
|
+
if result is None:
|
|
433
|
+
try:
|
|
434
|
+
api_resource._internal_use__check_access(self._get_auth_token())
|
|
435
|
+
if access is not GLOBAL_PERMISSION__PUBLIC:
|
|
436
|
+
scope_user_id = api_resource.auth_token.user_id
|
|
437
|
+
scope_token_id = api_resource.auth_token.id
|
|
438
|
+
sentry_scope.set_user({
|
|
439
|
+
'id': api_resource.auth_token.user_id,
|
|
440
|
+
'username': api_resource.auth_token.username,
|
|
441
|
+
})
|
|
442
|
+
except Exception as e: # noqa: B902
|
|
443
|
+
if config.exc_handler_access is None:
|
|
444
|
+
raise
|
|
445
|
+
result = config.exc_handler_access(e)
|
|
446
|
+
|
|
447
|
+
if result is None:
|
|
448
|
+
try:
|
|
449
|
+
kwargs = fn_typing.runtime_validate_request_input(ApiMethod(request.method.upper()), kwargs)
|
|
450
|
+
except Exception as e: # noqa: B902
|
|
451
|
+
if config.exc_handler_bad_request is None:
|
|
452
|
+
raise
|
|
453
|
+
result = config.exc_handler_bad_request(e)
|
|
454
|
+
|
|
455
|
+
if result is None:
|
|
456
|
+
try:
|
|
457
|
+
result = self._flask_app.ensure_sync(fn)(api_resource, **kwargs)
|
|
458
|
+
except Exception as e: # noqa: B902
|
|
459
|
+
if config.exc_handler_endpoint is None:
|
|
460
|
+
raise
|
|
461
|
+
result = config.exc_handler_endpoint(e)
|
|
462
|
+
|
|
463
|
+
if not isinstance(result, ApiResponse):
|
|
464
|
+
raise ResponseTypeRuntimeApiError(f'invalid type of response. must be instance of ApiResponse. {repr(result)} was given')
|
|
465
|
+
if isinstance(result, JsonApiResponse):
|
|
466
|
+
payload, total_count = fn_typing.runtime_validate_api_response_payload(result.payload, result.total_count, quick=False)
|
|
467
|
+
result.payload = payload
|
|
468
|
+
result.total_count = total_count
|
|
469
|
+
elif isinstance(result, RootJsonApiResponse):
|
|
470
|
+
payload, _1 = fn_typing.runtime_validate_api_response_payload(result.root, 0, quick=False)
|
|
471
|
+
result.root = payload
|
|
472
|
+
elif isinstance(result, ProxyJsonApiResponse):
|
|
473
|
+
fn_typing.runtime_validate_api_proxy_payload(result.response, quick=False)
|
|
474
|
+
if len(api_resource._internal_use__files_to_clean) > 0:
|
|
475
|
+
add_files_to_clean(api_resource._internal_use__files_to_clean)
|
|
476
|
+
res = result.to_flask_response(d), result.status_code
|
|
477
|
+
except Exception as e: # noqa: B902
|
|
478
|
+
tb = traceback.format_exc()
|
|
479
|
+
if not isinstance(e, UserAbstractApiError):
|
|
480
|
+
with stat.measure('') as mes:
|
|
481
|
+
mes.add_error(tb)
|
|
482
|
+
if APPLICATION_DEBUG or APPLICATION_ENV_IS_LOCAL: # MAY BE IT SHOULD BE ENABLED BY DEFAULT ?
|
|
483
|
+
print(tb) # noqa
|
|
484
|
+
if not APPLICATION_ENV_IS_LOCAL:
|
|
485
|
+
sentry.capture_exception(e)
|
|
486
|
+
if api_resource._type == ApiResourceType.API:
|
|
487
|
+
result = api_resource.response_api_error(e)
|
|
488
|
+
elif api_resource._type == ApiResourceType.WEB:
|
|
489
|
+
result = api_resource.response_web_error(e)
|
|
490
|
+
elif api_resource._type == ApiResourceType.FILE:
|
|
491
|
+
result = api_resource.response_api_error(e)
|
|
492
|
+
else:
|
|
493
|
+
result = api_resource.response_web_error( # type: ignore
|
|
494
|
+
ResourceRuntimeApiError(f'unsupported type "{api_resource._type.value}" of error response'),
|
|
495
|
+
)
|
|
496
|
+
res = result.to_flask_response(d), result.status_code
|
|
497
|
+
sentry_scope.clear()
|
|
498
|
+
if api_resource_config is not None and api_resource_config.override_flask_response is not None:
|
|
499
|
+
res = api_resource_config.override_flask_response(res)
|
|
500
|
+
|
|
501
|
+
ri = api_resource.request_info
|
|
502
|
+
# TODO: add cache hit flag
|
|
503
|
+
logger.info('AUDIT ' + json.dumps({
|
|
504
|
+
'user_id': str(scope_user_id) if scope_user_id else None,
|
|
505
|
+
'token_id': str(scope_token_id) if scope_token_id else None,
|
|
506
|
+
'method': request.method,
|
|
507
|
+
'url': request.url,
|
|
508
|
+
'ipv4': ri.ipv4,
|
|
509
|
+
'user_agent': ri.user_agent,
|
|
510
|
+
'duration': time.time() - now,
|
|
511
|
+
'status_code': res[1] if res is not None else 500,
|
|
512
|
+
'fn_id': fn_name,
|
|
513
|
+
'fn_mdl': fn_module,
|
|
514
|
+
'error_code': _get_error_types(result),
|
|
515
|
+
}))
|
|
516
|
+
|
|
517
|
+
return res
|
|
518
|
+
|
|
519
|
+
assert access is not None # for mypy
|
|
520
|
+
self._fn_registry.append(ApiSdkResource(
|
|
521
|
+
path=path,
|
|
522
|
+
config=config,
|
|
523
|
+
wrapper_fn=wrapper, # type: ignore
|
|
524
|
+
methods=enum_methods,
|
|
525
|
+
fn_typing=fn_typing,
|
|
526
|
+
access=access,
|
|
527
|
+
))
|
|
528
|
+
|
|
529
|
+
wrapper = self._flask_app.route(path, methods=flask_methods)(wrapper)
|
|
530
|
+
|
|
531
|
+
return cast(TFn, wrapper)
|
|
532
|
+
return wrap
|
|
533
|
+
|
|
534
|
+
@cached_per_request('_api_utils__get_auth_token')
|
|
535
|
+
def _get_auth_token(self) -> Optional[Tuple[Optional[str], str]]:
|
|
536
|
+
auth_header = request.headers.get('Authorization')
|
|
537
|
+
if not auth_header:
|
|
538
|
+
return None
|
|
539
|
+
if not auth_header.startswith('Bearer'):
|
|
540
|
+
value = auth_header.encode('utf-8')
|
|
541
|
+
try:
|
|
542
|
+
_scheme, credentials = value.split(b' ', 1)
|
|
543
|
+
encoded_username, encoded_password = b64decode(credentials).split(b':', 1)
|
|
544
|
+
except (ValueError, TypeError):
|
|
545
|
+
return None
|
|
546
|
+
try:
|
|
547
|
+
return encoded_username.decode('utf-8'), encoded_password.decode('utf-8')
|
|
548
|
+
except UnicodeDecodeError:
|
|
549
|
+
return encoded_username.decode('latin1'), encoded_password.decode('latin1')
|
|
550
|
+
except Exception: # noqa: B902
|
|
551
|
+
return None
|
|
552
|
+
auth_segm = auth_header.split(" ")
|
|
553
|
+
if len(auth_segm) < 2:
|
|
554
|
+
return None
|
|
555
|
+
return None, auth_segm[1]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import List, Optional, Callable, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import ConfigDict, BaseModel
|
|
5
|
+
|
|
6
|
+
from ul_api_utils.access import PermissionRegistry, PermissionDefinition
|
|
7
|
+
from ul_api_utils.modules.api_sdk_jwt import ApiSdkJwt
|
|
8
|
+
from ul_api_utils.resources.socketio import SocketIOConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def join_route_paths(prev_sect: str, next_sect: str) -> str:
|
|
12
|
+
return prev_sect.rstrip('/') + '/' + next_sect.lstrip('/')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApiSdkIdentifyTypeEnum(Enum):
|
|
16
|
+
DISABLED = 'DISABLED'
|
|
17
|
+
CLIENT_IP = 'IP'
|
|
18
|
+
JWT_USER_ID = 'JWT_USER_ID'
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return f'{type(self).__name__}.{self.name}'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ApiSdkHttpAuth(BaseModel):
|
|
25
|
+
realm: str = 'Hidden Zone'
|
|
26
|
+
scheme: str = 'Basic'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApiSdkFlaskDebuggingPluginsEnabled(BaseModel):
|
|
30
|
+
flask_monitoring_dashboard: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ApiSdkConfig(BaseModel):
|
|
34
|
+
service_name: str
|
|
35
|
+
permissions: Optional[Union[Callable[[], PermissionRegistry], PermissionRegistry]] = None
|
|
36
|
+
permissions_check_enabled: bool = True # GLOBAL CHECK OF ACCESS AND PERMISSIONS ENABLE
|
|
37
|
+
permissions_validator: Optional[Callable[[ApiSdkJwt, PermissionDefinition], bool]] = None
|
|
38
|
+
|
|
39
|
+
jwt_validator: Optional[Callable[[ApiSdkJwt], bool]] = None
|
|
40
|
+
jwt_environment_check_enabled: bool = True
|
|
41
|
+
|
|
42
|
+
http_auth: Optional[ApiSdkHttpAuth] = None
|
|
43
|
+
|
|
44
|
+
static_url_path: Optional[str] = None
|
|
45
|
+
socket_config: Optional[SocketIOConfig] = None
|
|
46
|
+
web_error_template: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
rate_limit: Union[str, List[str]] = '100/minute' # [count (int)] [per|/] [second|minute|hour|day|month|year][s]
|
|
49
|
+
rate_limit_storage_uri: str = '' # supports url of redis, memcached, mongodb
|
|
50
|
+
rate_limit_identify: Union[ApiSdkIdentifyTypeEnum, Callable[[], str]] = ApiSdkIdentifyTypeEnum.DISABLED # must be None if disabled
|
|
51
|
+
|
|
52
|
+
cache_storage_uri: str = '' # supports only redis
|
|
53
|
+
cache_default_ttl: int = 60 # seconds
|
|
54
|
+
|
|
55
|
+
flask_debugging_plugins: Optional[ApiSdkFlaskDebuggingPluginsEnabled] = None
|
|
56
|
+
|
|
57
|
+
api_route_path_prefix: str = '/api'
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(
|
|
60
|
+
extra="forbid",
|
|
61
|
+
frozen=True,
|
|
62
|
+
arbitrary_types_allowed=True,
|
|
63
|
+
)
|