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,84 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from typing import Tuple, Optional, Callable, Union, List
|
|
3
|
+
|
|
4
|
+
from flask import request, Response, Flask, jsonify
|
|
5
|
+
from jwt import InvalidTokenError
|
|
6
|
+
|
|
7
|
+
from ul_api_utils.conf import APPLICATION_JWT_PUBLIC_KEY
|
|
8
|
+
from ul_api_utils.const import REQUEST_HEADER__X_FORWARDED_FOR, RESPONSE_PROP_OK, RESPONSE_PROP_PAYLOAD, RESPONSE_PROP_ERRORS, MIME__JSON
|
|
9
|
+
from ul_api_utils.debug.debugger import Debugger
|
|
10
|
+
from ul_api_utils.errors import AccessApiError
|
|
11
|
+
from ul_api_utils.modules.api_sdk_config import ApiSdkIdentifyTypeEnum
|
|
12
|
+
from ul_api_utils.modules.api_sdk_jwt import ApiSdkJwt
|
|
13
|
+
from ul_api_utils.utils.api_method import ApiMethod
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _rate_limiter_key_ipv4() -> str:
|
|
17
|
+
x_forwarded_for = request.headers.get(REQUEST_HEADER__X_FORWARDED_FOR)
|
|
18
|
+
if x_forwarded_for:
|
|
19
|
+
ip_list = x_forwarded_for.split(",")
|
|
20
|
+
return ip_list[0]
|
|
21
|
+
else:
|
|
22
|
+
return request.remote_addr or 'UNKNOWN'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _rate_limiter_key_jwt_user_id(get_auth_token: Callable[[], Optional[Tuple[Optional[str], str]]]) -> str:
|
|
26
|
+
token = None
|
|
27
|
+
request_token = get_auth_token()
|
|
28
|
+
if request_token is not None:
|
|
29
|
+
auth_username, auth_token = request_token
|
|
30
|
+
try:
|
|
31
|
+
token = ApiSdkJwt.decode(token=auth_token, username=auth_username, certificate=APPLICATION_JWT_PUBLIC_KEY)
|
|
32
|
+
except (InvalidTokenError, TypeError, AccessApiError):
|
|
33
|
+
# case when token invalid, request rate limited by IP
|
|
34
|
+
return _rate_limiter_key_ipv4()
|
|
35
|
+
return str(token.user_id) if token is not None else _rate_limiter_key_ipv4()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def init_rate_limiter(
|
|
39
|
+
*,
|
|
40
|
+
flask_app: Flask,
|
|
41
|
+
debugger_enabled: Callable[[], bool],
|
|
42
|
+
rate_limit: Union[str, List[str]],
|
|
43
|
+
get_auth_token: Callable[[], Optional[Tuple[Optional[str], str]]],
|
|
44
|
+
identify: Union[ApiSdkIdentifyTypeEnum, Callable[[], str]],
|
|
45
|
+
storage_uri: str,
|
|
46
|
+
) -> bool:
|
|
47
|
+
if identify == ApiSdkIdentifyTypeEnum.DISABLED or not storage_uri:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
from flask_limiter import Limiter, RateLimitExceeded
|
|
51
|
+
if identify == ApiSdkIdentifyTypeEnum.CLIENT_IP:
|
|
52
|
+
identify_func = _rate_limiter_key_ipv4
|
|
53
|
+
elif identify == ApiSdkIdentifyTypeEnum.JWT_USER_ID:
|
|
54
|
+
identify_func = functools.partial(_rate_limiter_key_jwt_user_id, get_auth_token)
|
|
55
|
+
else:
|
|
56
|
+
assert not isinstance(identify, ApiSdkIdentifyTypeEnum)
|
|
57
|
+
identify_func = identify
|
|
58
|
+
|
|
59
|
+
@flask_app.errorhandler(429)
|
|
60
|
+
def _rate_lmiter_error_handler(e: RateLimitExceeded) -> Tuple[Response, int]:
|
|
61
|
+
d = Debugger(flask_app.import_name, debugger_enabled(), ApiMethod(request.method), request.url)
|
|
62
|
+
|
|
63
|
+
if MIME__JSON not in request.headers.get('accept', ''):
|
|
64
|
+
return Response(f'429. request outside of rate limit - {e.limit.limit}'), 429
|
|
65
|
+
return jsonify({
|
|
66
|
+
RESPONSE_PROP_OK: False,
|
|
67
|
+
RESPONSE_PROP_PAYLOAD: None,
|
|
68
|
+
RESPONSE_PROP_ERRORS: [
|
|
69
|
+
{
|
|
70
|
+
"error_type": "rate-limit-exceeded",
|
|
71
|
+
"error_message": f"request outside of rate limit - {e.limit.limit}",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
**(d.render_dict(429) if d is not None else {}),
|
|
75
|
+
}), 429
|
|
76
|
+
|
|
77
|
+
Limiter(
|
|
78
|
+
app=flask_app,
|
|
79
|
+
application_limits=[rate_limit] if isinstance(rate_limit, str) else rate_limit, # type: ignore
|
|
80
|
+
key_func=identify_func,
|
|
81
|
+
storage_uri=storage_uri,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return True
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from flask_socketio import SocketIO # type: ignore
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SocketIOConfigType(Enum):
|
|
8
|
+
"""
|
|
9
|
+
Defines two basic approaches of creating Socket.IO instance:
|
|
10
|
+
1. SERVER - Socket.IO instance is being attached to the Flask App,
|
|
11
|
+
async gunicorn workers (gevent, eventlet), requires monkey-patching of
|
|
12
|
+
psycopg and default library. Socket application.
|
|
13
|
+
2. EXTERNAL_PROCESS - Socket.IO instance isn't attached to Flask App,
|
|
14
|
+
sync gunicorn workers, doesn't require monkey-patching. Could be any
|
|
15
|
+
API, worker, etc. Used only to submit events from an external process
|
|
16
|
+
to the server clients.
|
|
17
|
+
|
|
18
|
+
Further reading: https://flask-socketio.readthedocs.io/en/latest/deployment.html#emitting-from-an-external-process
|
|
19
|
+
"""
|
|
20
|
+
SERVER = "SERVER"
|
|
21
|
+
EXTERNAL_PROCESS = "EXTERNAL_PROCESS"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SocketIOConfig(BaseModel):
|
|
25
|
+
app_type: SocketIOConfigType = SocketIOConfigType.SERVER
|
|
26
|
+
message_queue: str
|
|
27
|
+
channel: str | None = "flask-socketio"
|
|
28
|
+
cors_allowed_origins: str | None = "*"
|
|
29
|
+
logs_enabled: bool | None = False
|
|
30
|
+
engineio_logs_enabled: bool | None = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SocketAsyncModesEnum(Enum):
|
|
34
|
+
THREADING = 'threading'
|
|
35
|
+
GEVENT = 'gevent'
|
|
36
|
+
EVENTLET = 'eventlet'
|
|
37
|
+
GEVENT_UWSGI = 'gevent_uwsgi'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def init_socket_io(config: SocketIOConfig | None) -> SocketIO | None:
|
|
41
|
+
socket_io = None
|
|
42
|
+
if config is None:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
if config.app_type is SocketIOConfigType.SERVER:
|
|
46
|
+
socket_io = SocketIO()
|
|
47
|
+
|
|
48
|
+
if config.app_type is SocketIOConfigType.EXTERNAL_PROCESS:
|
|
49
|
+
socket_io = SocketIO(
|
|
50
|
+
message_queue=config.message_queue,
|
|
51
|
+
channel=config.channel,
|
|
52
|
+
logger=config.logs_enabled,
|
|
53
|
+
engineio_logger=config.engineio_logs_enabled,
|
|
54
|
+
)
|
|
55
|
+
return socket_io
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING, NamedTuple, List, Callable
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from flask import Response, request
|
|
9
|
+
|
|
10
|
+
from ul_api_utils.access import GLOBAL_PERMISSION__PUBLIC, PermissionDefinition
|
|
11
|
+
from ul_api_utils.api_resource.api_resource import ApiResource
|
|
12
|
+
from ul_api_utils.api_resource.api_resource_config import ApiResourceConfig
|
|
13
|
+
from ul_api_utils.api_resource.api_resource_fn_typing import ApiResourceFnTyping
|
|
14
|
+
from ul_api_utils.api_resource.api_resource_type import ApiResourceType
|
|
15
|
+
from ul_api_utils.api_resource.api_response import EmptyJsonApiResponse, ApiResponse
|
|
16
|
+
from ul_api_utils.conf import APPLICATION_TMP, APPLICATION_DIR, APPLICATION_F, APPLICATION_SWAGGER_PATH, APPLICATION_SWAGGER_SPECIFICATION_PATH
|
|
17
|
+
from ul_api_utils.debug.debugger import Debugger, AJAX_INTERSEPTOR
|
|
18
|
+
from ul_api_utils.utils.api_method import ApiMethod
|
|
19
|
+
from ul_api_utils.utils.api_path_version import ApiPathVersion
|
|
20
|
+
from ul_api_utils.utils.flask_swagger_generator.specifiers.swagger_three_specifier import SwaggerThreeSpecifier
|
|
21
|
+
from ul_api_utils.utils.flask_swagger_generator.utils.security_type import SecurityType
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ul_api_utils.modules.api_sdk import ApiSdk
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SLUG_REPLACE_RE = re.compile(r'[^\w\d]+')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ApiSdkResource(NamedTuple):
|
|
31
|
+
config: ApiResourceConfig
|
|
32
|
+
wrapper_fn: Callable[..., ApiResponse]
|
|
33
|
+
methods: List[ApiMethod]
|
|
34
|
+
path: str
|
|
35
|
+
access: PermissionDefinition
|
|
36
|
+
fn_typing: ApiResourceFnTyping
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_swagger(sdk: 'ApiSdk', resources: List[ApiSdkResource], api_route_path_prefix: str) -> None:
|
|
40
|
+
flask_app = sdk._flask_app
|
|
41
|
+
swagger_open_api_cache_file = os.path.join(APPLICATION_TMP, f"swagger-{SLUG_REPLACE_RE.sub('_', flask_app.import_name).lower()}-{int(time.time())}.yml")
|
|
42
|
+
|
|
43
|
+
from flask_swagger_ui import get_swaggerui_blueprint # type: ignore
|
|
44
|
+
|
|
45
|
+
bp = get_swaggerui_blueprint(
|
|
46
|
+
blueprint_name='swagger_ui',
|
|
47
|
+
base_url=ApiPathVersion.NO_VERSION.compile_path(APPLICATION_SWAGGER_PATH, api_route_path_prefix),
|
|
48
|
+
api_url=ApiPathVersion.NO_VERSION.compile_path(APPLICATION_SWAGGER_SPECIFICATION_PATH, api_route_path_prefix),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@bp.after_request
|
|
52
|
+
def after_request(response: Response) -> Response:
|
|
53
|
+
d = Debugger(flask_app.import_name, sdk._debugger_enabled_with_pin(), ApiMethod(request.method), request.url)
|
|
54
|
+
if isinstance(response.response, list) and b'<!DOCTYPE html>' in response.response[0]:
|
|
55
|
+
resp = response.get_data(as_text=True)
|
|
56
|
+
resp = resp.replace('</body>', f'{d.render_html(response.status_code)}</body>')
|
|
57
|
+
resp = resp.replace('<head>', f'<head>{AJAX_INTERSEPTOR}')
|
|
58
|
+
response.set_data(resp)
|
|
59
|
+
return response
|
|
60
|
+
|
|
61
|
+
flask_app.register_blueprint(bp)
|
|
62
|
+
|
|
63
|
+
@sdk.rest_api('GET', APPLICATION_SWAGGER_SPECIFICATION_PATH, v=ApiPathVersion.NO_VERSION, access=GLOBAL_PERMISSION__PUBLIC, config=ApiResourceConfig(swagger_disabled=True))
|
|
64
|
+
def swagger_specification(api_resource: ApiResource) -> EmptyJsonApiResponse:
|
|
65
|
+
try:
|
|
66
|
+
if not os.path.exists(swagger_open_api_cache_file):
|
|
67
|
+
with open(swagger_open_api_cache_file, 'wt') as f:
|
|
68
|
+
specifier = SwaggerThreeSpecifier()
|
|
69
|
+
_index_endpoints(specifier, [r for r in resources if APPLICATION_F.has_or_unset(r.access.flags)])
|
|
70
|
+
specifier.set_application_name('API')
|
|
71
|
+
specifier.set_application_version('1.0.0')
|
|
72
|
+
specifier.write(f)
|
|
73
|
+
specifier.clean()
|
|
74
|
+
with open(swagger_open_api_cache_file, "r") as docs_file:
|
|
75
|
+
swagger_open_api_cache_file_data = docs_file.read()
|
|
76
|
+
docs_object = yaml.load(
|
|
77
|
+
swagger_open_api_cache_file_data,
|
|
78
|
+
Loader=yaml.FullLoader,
|
|
79
|
+
)
|
|
80
|
+
return api_resource.response_root(docs_object)
|
|
81
|
+
except OSError:
|
|
82
|
+
return EmptyJsonApiResponse(ok=False, status_code=404)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _index_endpoints(specifier: SwaggerThreeSpecifier, fn_registry: List[ApiSdkResource]) -> None:
|
|
86
|
+
for resource in fn_registry:
|
|
87
|
+
if resource.fn_typing.api_resource_type == ApiResourceType.WEB:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
if resource.config.swagger_disabled:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
fn = resource.fn_typing.fn
|
|
94
|
+
fn_file = os.path.abspath(sys.modules[fn.__module__].__file__) # type: ignore
|
|
95
|
+
fn_file = os.path.relpath(fn_file, APPLICATION_DIR) # type: ignore
|
|
96
|
+
group = resource.config.swagger_group or re.sub(r'^.*?/?(?:routes|views)/([^/]+)(?:/.+)?$', r'\1', fn_file[:-len('.py')])
|
|
97
|
+
|
|
98
|
+
specifier.add_endpoint(
|
|
99
|
+
function_name=fn.__name__,
|
|
100
|
+
function_object=resource.wrapper_fn,
|
|
101
|
+
path=resource.path,
|
|
102
|
+
request_types=[str(m) for m in resource.methods],
|
|
103
|
+
group=group,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if resource.fn_typing.body_typing is not None:
|
|
107
|
+
specifier.add_request_body(fn.__name__, resource.fn_typing.get_body_schema())
|
|
108
|
+
|
|
109
|
+
response_model = resource.fn_typing.get_return_schema()
|
|
110
|
+
|
|
111
|
+
specifier.add_response(
|
|
112
|
+
function_name=fn.__name__,
|
|
113
|
+
status_code=200,
|
|
114
|
+
schema=response_model,
|
|
115
|
+
description=(response_model.__doc__ or '').strip(),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if resource.access != GLOBAL_PERMISSION__PUBLIC:
|
|
119
|
+
specifier.add_security(fn.__name__, SecurityType.BEARER_AUTH)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from markupsafe import Markup
|
|
4
|
+
from wtforms import SelectField # type: ignore
|
|
5
|
+
from wtforms.widgets.core import html_params, Select # type: ignore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CustomLiveSearchPlaceholderSelect(Select):
|
|
9
|
+
"""
|
|
10
|
+
Renders a CUSTOM select field.
|
|
11
|
+
|
|
12
|
+
If `multiple` is True, then the `size` property should be specified on
|
|
13
|
+
rendering to make the field useful.
|
|
14
|
+
|
|
15
|
+
The field must provide an `iter_choices()` method which the widget will
|
|
16
|
+
call on rendering; this method must yield tuples of
|
|
17
|
+
`(value, label, selected)`.
|
|
18
|
+
It also must provide a `has_groups()` method which tells whether choices
|
|
19
|
+
are divided into groups, and if they do, the field must have an
|
|
20
|
+
`iter_groups()` method that yields tuples of `(label, choices)`, where
|
|
21
|
+
`choices` is an iterable of `(value, label, selected)` tuples.
|
|
22
|
+
Otherwise, `selected` is False for any option field in select item group.
|
|
23
|
+
"""
|
|
24
|
+
def __call__(self, field: SelectField, **kwargs: Any) -> Markup:
|
|
25
|
+
kwargs.setdefault("id", field.id)
|
|
26
|
+
if self.multiple:
|
|
27
|
+
kwargs["multiple"] = True
|
|
28
|
+
flags = getattr(field, "flags", {})
|
|
29
|
+
for k in dir(flags):
|
|
30
|
+
if k in self.validation_attrs and k not in kwargs:
|
|
31
|
+
kwargs[k] = getattr(flags, k)
|
|
32
|
+
|
|
33
|
+
html = ["<select data-live-search='true' data-show-subtext='true' %s>" % html_params(name=field.name, **kwargs),
|
|
34
|
+
"<option value='' disabled selected>Select something...</option>"]
|
|
35
|
+
|
|
36
|
+
if field.has_groups():
|
|
37
|
+
for group, choices in field.iter_groups():
|
|
38
|
+
html.append("<optgroup %s>" % html_params(label=group))
|
|
39
|
+
for val, label, selected in choices:
|
|
40
|
+
html.append(self.render_option(val, label, selected))
|
|
41
|
+
html.append("</optgroup>")
|
|
42
|
+
else:
|
|
43
|
+
for val, label, _ in field.iter_choices():
|
|
44
|
+
html.append(self.render_option(val, label, False))
|
|
45
|
+
html.append("</select>")
|
|
46
|
+
return Markup("".join(html))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CustomLiveSearchSelect(Select):
|
|
50
|
+
"""
|
|
51
|
+
Renders a CUSTOM select field.
|
|
52
|
+
|
|
53
|
+
If `multiple` is True, then the `size` property should be specified on
|
|
54
|
+
rendering to make the field useful.
|
|
55
|
+
|
|
56
|
+
The field must provide an `iter_choices()` method which the widget will
|
|
57
|
+
call on rendering; this method must yield tuples of
|
|
58
|
+
`(value, label, selected)`.
|
|
59
|
+
It also must provide a `has_groups()` method which tells whether choices
|
|
60
|
+
are divided into groups, and if they do, the field must have an
|
|
61
|
+
`iter_groups()` method that yields tuples of `(label, choices)`, where
|
|
62
|
+
`choices` is an iterable of `(value, label, selected)` tuples.
|
|
63
|
+
Otherwise, `selected` is False for any option field in select item group.
|
|
64
|
+
"""
|
|
65
|
+
def __call__(self, field: SelectField, **kwargs: Any) -> Markup:
|
|
66
|
+
kwargs.setdefault("id", field.id)
|
|
67
|
+
if self.multiple:
|
|
68
|
+
kwargs["multiple"] = True
|
|
69
|
+
flags = getattr(field, "flags", {})
|
|
70
|
+
for k in dir(flags):
|
|
71
|
+
if k in self.validation_attrs and k not in kwargs:
|
|
72
|
+
kwargs[k] = getattr(flags, k)
|
|
73
|
+
|
|
74
|
+
html = ["<select data-live-search='true' data-show-subtext='true' %s>" % html_params(name=field.name, **kwargs)]
|
|
75
|
+
|
|
76
|
+
if field.has_groups():
|
|
77
|
+
for group, choices in field.iter_groups():
|
|
78
|
+
html.append("<optgroup %s>" % html_params(label=group))
|
|
79
|
+
for val, label, selected in choices:
|
|
80
|
+
html.append(self.render_option(val, label, selected))
|
|
81
|
+
html.append("</optgroup>")
|
|
82
|
+
else:
|
|
83
|
+
for val, label, selected in field.iter_choices():
|
|
84
|
+
html.append(self.render_option(val, label, selected))
|
|
85
|
+
html.append("</select>")
|
|
86
|
+
return Markup("".join(html))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
from wtforms import StringField # type: ignore
|
|
4
|
+
from wtforms.widgets import TextInput # type: ignore
|
|
5
|
+
from markupsafe import Markup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CustomTextInput(TextInput):
|
|
9
|
+
"""
|
|
10
|
+
Render a single-line text input with optional input attributes ("required", "maxlength", "minlength", "pattern").
|
|
11
|
+
|
|
12
|
+
examples: TextInputCustom(required=True, min_length=5, max_length=255, pattern=r"^(d+(,d+)*)?$")
|
|
13
|
+
TextInputCustom(max_length=255, pattern=r"^(d+(,d+)*)?$")
|
|
14
|
+
TextInputCustom(required=True, pattern=r"^(d+(,d+)*)?$")
|
|
15
|
+
TextInputCustom(max_length=255)
|
|
16
|
+
In model usage: ... info={"label": "LABEL:", "widget": TextInputCustom(pattern=r"^(d+(,d+)*)?$")}
|
|
17
|
+
"""
|
|
18
|
+
validation_attrs = ["required", "maxlength", "minlength", "pattern"]
|
|
19
|
+
|
|
20
|
+
def __init__(self, required: Optional[bool] = None, max_length: Optional[int] = None, min_length: Optional[int] = None, pattern: Optional[str] = None):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.required = required
|
|
23
|
+
self.max_length = max_length
|
|
24
|
+
self.min_length = min_length
|
|
25
|
+
self.pattern = pattern
|
|
26
|
+
|
|
27
|
+
def __call__(self, field: StringField, **kwargs: Any) -> Markup:
|
|
28
|
+
kwargs.setdefault("id", field.id)
|
|
29
|
+
kwargs.setdefault("type", self.input_type)
|
|
30
|
+
if self.min_length:
|
|
31
|
+
kwargs.setdefault("minlength", self.min_length)
|
|
32
|
+
if self.max_length:
|
|
33
|
+
kwargs.setdefault("maxlength", self.max_length)
|
|
34
|
+
if self.pattern:
|
|
35
|
+
kwargs.setdefault("pattern", self.pattern)
|
|
36
|
+
if "value" not in kwargs:
|
|
37
|
+
kwargs["value"] = field._value()
|
|
38
|
+
flags = getattr(field, "flags", {})
|
|
39
|
+
for k in dir(flags):
|
|
40
|
+
if k in self.validation_attrs and k not in kwargs:
|
|
41
|
+
kwargs[k] = getattr(flags, k)
|
|
42
|
+
return Markup("<input %s>" % self.html_params(name=field.name, **kwargs))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Type, Any, Dict, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from ul_db_utils.modules.postgres_modules.db import db
|
|
4
|
+
from sqlalchemy.dialects import postgresql
|
|
5
|
+
from sqlalchemy.dialects.postgresql import ARRAY
|
|
6
|
+
from wtforms_alchemy import ModelForm, ClassMap, FormGenerator # type: ignore
|
|
7
|
+
from wtforms.fields import SelectField # type: ignore
|
|
8
|
+
|
|
9
|
+
from ul_db_utils.model.base_model import BaseModel
|
|
10
|
+
|
|
11
|
+
from ul_api_utils.resources.web_forms.custom_fields.custom_checkbox_select import MultiCheckboxField
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def form_factory(model_obj: Type[BaseModel], edition: bool = False, *, extra_fields: Optional[Dict[str, Any]] = None) -> Type[ModelForm]:
|
|
15
|
+
"""
|
|
16
|
+
Returns generated model form.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
model_obj (type_of(BaseModel)): model object which will use for form generation
|
|
20
|
+
edition (bool): flag to indicate creation or edition form will be generated
|
|
21
|
+
extra_fields (optional(dict(str, any))): additional fields for generated form
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Form (type_of(ModelForm)): web model form
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
class ExtraFieldsFormGenerator(FormGenerator):
|
|
28
|
+
def create_fields(self, form: Any, properties: Dict[str, Any]) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Creates fields for given form based on given model attributes.
|
|
31
|
+
|
|
32
|
+
:param form: form to attach the generated fields into
|
|
33
|
+
:param properties: model attributes to generate the form fields from
|
|
34
|
+
"""
|
|
35
|
+
super(ExtraFieldsFormGenerator, self).create_fields(form, properties)
|
|
36
|
+
|
|
37
|
+
if extra_fields:
|
|
38
|
+
for field_name, field in extra_fields.items():
|
|
39
|
+
setattr(form, field_name, field)
|
|
40
|
+
|
|
41
|
+
class Form(ModelForm):
|
|
42
|
+
"""
|
|
43
|
+
A class for representing a web form.
|
|
44
|
+
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
Attributes
|
|
48
|
+
----------
|
|
49
|
+
property_columns : dict(str, any)
|
|
50
|
+
Model property columns such as ID, USER_CREATED etc.
|
|
51
|
+
|
|
52
|
+
Methods
|
|
53
|
+
-------
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
property_columns: Dict[str, Any] = {}
|
|
58
|
+
|
|
59
|
+
def __init__(self, *args: Tuple[Any], **kwargs: Dict[str, Any]) -> None:
|
|
60
|
+
super(Form, self).__init__(*args, **kwargs)
|
|
61
|
+
self.property_columns = model_obj.get_property_columns(self._obj) if self._obj else {}
|
|
62
|
+
|
|
63
|
+
class Meta:
|
|
64
|
+
model = model_obj
|
|
65
|
+
type_map = ClassMap({postgresql.UUID: SelectField, ARRAY: MultiCheckboxField})
|
|
66
|
+
only = model_obj.get_edit_columns() if edition else model_obj.get_create_columns()
|
|
67
|
+
form_generator = ExtraFieldsFormGenerator
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def get_session(cls) -> Any:
|
|
71
|
+
return db.session
|
|
72
|
+
|
|
73
|
+
Form.__name__ = f"{model_obj.__name__}Form"
|
|
74
|
+
|
|
75
|
+
return Form
|
ul_api_utils/sentry.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from ul_api_utils.conf import APPLICATION_SENTRY_DSN, APPLICATION_SENTRY_ENABLED_FLASK, DOCKER_BUILD__CONTAINER_CODE_COMMIT_HASH, DOCKER_BUILD__CONTAINER_SERVER_TIME, \
|
|
2
|
+
DOCKER_BUILD__CONTAINER_CODE_TAG, APPLICATION_ENV, APPLICATION_DEBUG
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FakeSentryScope:
|
|
6
|
+
|
|
7
|
+
def set_tag(self, *args, **kwargs) -> None: # type: ignore
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
def set_user(self, *args, **kwargs) -> None: # type: ignore
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def clear(self, *args, **kwargs) -> None: # type: ignore
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def __enter__(self) -> 'FakeSentryScope':
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FakeSentrySdk:
|
|
24
|
+
def init(self, *args, **kwargs) -> None: # type: ignore
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def capture_exception(self, *args, **kwargs) -> None: # type: ignore
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def configure_scope(self) -> FakeSentryScope:
|
|
31
|
+
return FakeSentryScope()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
sentry = FakeSentrySdk()
|
|
35
|
+
|
|
36
|
+
if APPLICATION_SENTRY_DSN:
|
|
37
|
+
import sentry_sdk
|
|
38
|
+
sentry = sentry_sdk # type: ignore
|
|
39
|
+
|
|
40
|
+
if APPLICATION_SENTRY_ENABLED_FLASK:
|
|
41
|
+
from sentry_sdk.integrations.flask import FlaskIntegration
|
|
42
|
+
sentry_sdk.init(APPLICATION_SENTRY_DSN, integrations=[FlaskIntegration()])
|
|
43
|
+
else:
|
|
44
|
+
sentry_sdk.init(APPLICATION_SENTRY_DSN)
|
|
45
|
+
if DOCKER_BUILD__CONTAINER_CODE_COMMIT_HASH:
|
|
46
|
+
sentry_sdk.set_tag('release', DOCKER_BUILD__CONTAINER_CODE_COMMIT_HASH)
|
|
47
|
+
if DOCKER_BUILD__CONTAINER_SERVER_TIME:
|
|
48
|
+
sentry_sdk.set_tag('docker_built_at', DOCKER_BUILD__CONTAINER_SERVER_TIME)
|
|
49
|
+
if DOCKER_BUILD__CONTAINER_CODE_TAG:
|
|
50
|
+
sentry_sdk.set_tag('docker_tag', DOCKER_BUILD__CONTAINER_CODE_TAG)
|
|
51
|
+
sentry_sdk.set_tag('app_environment', APPLICATION_ENV)
|
|
52
|
+
sentry_sdk.set_tag('app_debug', 'True' if APPLICATION_DEBUG else 'False')
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from ul_api_utils.utils.api_path_version import ApiPathVersion
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_path_compilation() -> None:
|
|
5
|
+
assert ApiPathVersion.NO_VERSION.compile_path('some') == '/some'
|
|
6
|
+
assert ApiPathVersion.NO_VERSION.compile_path('some', 'additional') == '/additional/some'
|
|
7
|
+
assert ApiPathVersion.NO_VERSION.compile_path('some', 'additional', q={"a": 123}) == '/additional/some?a=123'
|
|
8
|
+
|
|
9
|
+
assert ApiPathVersion.V01.compile_path('some', 'additional') == '/additional/v1/some'
|
|
10
|
+
assert ApiPathVersion.V01.compile_path('some', 'additional', q={"a": 123}) == '/additional/v1/some?a=123'
|
|
11
|
+
|
|
12
|
+
assert ApiPathVersion.NO_PREFIX.compile_path('some', 'additional') == '/some'
|
|
13
|
+
assert ApiPathVersion.NO_PREFIX.compile_path('some', 'additional', q={"a": 123}) == '/some?a=123'
|
|
14
|
+
|
|
15
|
+
assert ApiPathVersion.NO_VERSION.compile_path('') == ''
|
|
16
|
+
assert ApiPathVersion.NO_VERSION.compile_path('', '/api') == '/api'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from ul_api_utils.api_resource.signature_check import set_model
|
|
6
|
+
from ul_api_utils.utils.unwrap_typing import UnwrappedOptionalObjOrListOfObj
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_unwrap_list_obj_apply() -> None:
|
|
10
|
+
class Some(BaseModel):
|
|
11
|
+
value: int
|
|
12
|
+
|
|
13
|
+
parsed1: UnwrappedOptionalObjOrListOfObj[Some] = UnwrappedOptionalObjOrListOfObj.parse(Some, None) # type: ignore
|
|
14
|
+
parsed1_v = parsed1.apply({"value": 1}, set_model)
|
|
15
|
+
assert parsed1_v is not None
|
|
16
|
+
assert parsed1_v.value == 1
|
|
17
|
+
|
|
18
|
+
parsed2: UnwrappedOptionalObjOrListOfObj[List[Some]] = UnwrappedOptionalObjOrListOfObj.parse(List[Some], None) # type: ignore
|
|
19
|
+
parsed2_v = parsed2.apply([{"value": 1}], set_model)
|
|
20
|
+
assert parsed2_v is not None
|
|
21
|
+
assert len(parsed2_v) == 1
|
|
22
|
+
assert parsed2_v[0].value == 1
|
|
23
|
+
|
|
24
|
+
parsed3: UnwrappedOptionalObjOrListOfObj[Optional[List[Some]]] = UnwrappedOptionalObjOrListOfObj.parse(Optional[List[Some]], None) # type: ignore
|
|
25
|
+
parsed3_v = parsed3.apply([{"value": 1}], set_model)
|
|
26
|
+
assert parsed3_v is not None
|
|
27
|
+
assert len(parsed3_v) == 1
|
|
28
|
+
assert parsed3_v[0].value == 1
|
|
29
|
+
assert parsed3.apply(None, set_model) is None
|
|
30
|
+
|
|
31
|
+
parsed4: UnwrappedOptionalObjOrListOfObj[Optional[Some]] = UnwrappedOptionalObjOrListOfObj.parse(Optional[Some], None) # type: ignore
|
|
32
|
+
assert parsed4.apply(None, set_model) is None
|
|
33
|
+
parsed4_v = parsed4.apply({"value": 1}, set_model)
|
|
34
|
+
assert parsed4_v is not None
|
|
35
|
+
assert parsed4_v.value == 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_unwrap_list_obj() -> None:
|
|
39
|
+
class Some:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
parsed = UnwrappedOptionalObjOrListOfObj.parse(Some, None) # type: ignore
|
|
43
|
+
assert parsed is not None
|
|
44
|
+
assert parsed.value_type == Some
|
|
45
|
+
assert not parsed.optional
|
|
46
|
+
assert not parsed.many
|
|
47
|
+
|
|
48
|
+
parsed = UnwrappedOptionalObjOrListOfObj.parse(List[Some], None)
|
|
49
|
+
assert parsed is not None
|
|
50
|
+
assert parsed.value_type == Some
|
|
51
|
+
assert not parsed.optional
|
|
52
|
+
assert parsed.many
|
|
53
|
+
|
|
54
|
+
parsed = UnwrappedOptionalObjOrListOfObj.parse(Optional[Some], None) # type: ignore
|
|
55
|
+
assert parsed is not None
|
|
56
|
+
assert parsed.value_type == Some
|
|
57
|
+
assert parsed.optional
|
|
58
|
+
assert not parsed.many
|
|
59
|
+
|
|
60
|
+
parsed = UnwrappedOptionalObjOrListOfObj.parse(Optional[List[Some]], None) # type: ignore
|
|
61
|
+
assert parsed is not None
|
|
62
|
+
assert parsed.value_type == Some
|
|
63
|
+
assert parsed.optional
|
|
64
|
+
assert parsed.many
|
|
65
|
+
|
|
66
|
+
parsed = UnwrappedOptionalObjOrListOfObj.parse(List[Optional[Some]], None)
|
|
67
|
+
assert parsed is None
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Tuple, Optional
|
|
4
|
+
|
|
5
|
+
from ul_api_utils.const import ENCODING_MIME__GZIP
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiEncoding(Enum):
|
|
9
|
+
NONE = 'NONE'
|
|
10
|
+
GZIP = 'GZIP'
|
|
11
|
+
|
|
12
|
+
def __repr__(self) -> str:
|
|
13
|
+
return f'{type(self).__name__}.{self.name}'
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def mime(self) -> str:
|
|
17
|
+
if self is ApiEncoding.NONE:
|
|
18
|
+
return ''
|
|
19
|
+
if self is ApiEncoding.GZIP:
|
|
20
|
+
return ENCODING_MIME__GZIP
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def accept_mimes(force: Optional['ApiEncoding'] = None) -> Tuple[str, ...]:
|
|
25
|
+
if force is ApiEncoding.GZIP:
|
|
26
|
+
return ENCODING_MIME__GZIP, # noqa: C818
|
|
27
|
+
if force is ApiEncoding.NONE:
|
|
28
|
+
return tuple()
|
|
29
|
+
return ENCODING_MIME__GZIP, # noqa: C818
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_mime(mime: str) -> Optional['ApiEncoding']:
|
|
33
|
+
if mime == ENCODING_MIME__GZIP:
|
|
34
|
+
return ApiEncoding.GZIP
|
|
35
|
+
if mime == '':
|
|
36
|
+
return ApiEncoding.NONE
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def encode(self, data: bytes) -> bytes:
|
|
40
|
+
if self is ApiEncoding.GZIP:
|
|
41
|
+
return gzip.compress(data)
|
|
42
|
+
if self is ApiEncoding.NONE:
|
|
43
|
+
return data
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
def decode(self, data: bytes) -> bytes:
|
|
47
|
+
if self is ApiEncoding.GZIP:
|
|
48
|
+
return gzip.decompress(data)
|
|
49
|
+
if self is ApiEncoding.NONE:
|
|
50
|
+
return data
|
|
51
|
+
raise NotImplementedError
|