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,117 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from types import NoneType
|
|
3
|
+
from typing import NamedTuple, Type, Tuple, _GenericAlias, _UnionGenericAlias, Any, Union, Optional, TypeVar, Generic, Callable # type: ignore
|
|
4
|
+
|
|
5
|
+
from ul_api_utils.api_resource.api_response import RootJsonApiResponsePayload
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TypingType(NamedTuple):
|
|
9
|
+
value: Type[Any]
|
|
10
|
+
|
|
11
|
+
def issubclassof(self, dest_type: Union[Type[Any], Tuple[Type[Any], ...]]) -> bool:
|
|
12
|
+
try:
|
|
13
|
+
if isinstance(dest_type, tuple):
|
|
14
|
+
return any(issubclass(self.value, t) for t in dest_type)
|
|
15
|
+
return issubclass(self.value, dest_type)
|
|
16
|
+
except Exception: # noqa: B902
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TypingGeneric(NamedTuple):
|
|
21
|
+
origin: TypingType
|
|
22
|
+
args: Tuple[TypingType, ...]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TypingUnion(NamedTuple):
|
|
26
|
+
args: Tuple[TypingType, ...]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TypingOptional(NamedTuple):
|
|
30
|
+
value: Union[TypingType, TypingUnion, TypingGeneric]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def unwrap_typing(t: Type[Any]) -> Union[TypingGeneric, TypingUnion, TypingType, TypingOptional]:
|
|
34
|
+
tt = type(t)
|
|
35
|
+
if tt == _GenericAlias:
|
|
36
|
+
return TypingGeneric(origin=unwrap_typing(t.__origin__), args=tuple(unwrap_typing(it) for it in t.__args__)) # type: ignore
|
|
37
|
+
if tt == _UnionGenericAlias:
|
|
38
|
+
if t.__origin__ == Union:
|
|
39
|
+
if len(t.__args__) == 2:
|
|
40
|
+
if t.__args__[0] == NoneType:
|
|
41
|
+
return TypingOptional(value=unwrap_typing(t.__args__[1])) # type: ignore
|
|
42
|
+
if t.__args__[1] == NoneType:
|
|
43
|
+
return TypingOptional(value=unwrap_typing(t.__args__[0])) # type: ignore
|
|
44
|
+
|
|
45
|
+
return TypingUnion(args=tuple(unwrap_typing(it) for it in t.__args__)) # type: ignore
|
|
46
|
+
raise NotImplementedError()
|
|
47
|
+
return TypingType(t)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
T = TypeVar('T')
|
|
51
|
+
TVal = TypeVar('TVal')
|
|
52
|
+
TValRoot = TypeVar('TValRoot', bound=RootJsonApiResponsePayload[Any])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def default_constructor(value_type: Type[TVal], data: Any) -> TVal:
|
|
56
|
+
return value_type(data) # type: ignore
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class UnwrappedOptionalObjOrListOfObj(Generic[TVal]):
|
|
61
|
+
many: bool
|
|
62
|
+
optional: bool
|
|
63
|
+
value_type: Type[TVal]
|
|
64
|
+
|
|
65
|
+
def apply(self, payload: Any, constructor: Optional[Callable[[Type[T], Any], T]] = default_constructor) -> Optional[TVal]:
|
|
66
|
+
if payload is None:
|
|
67
|
+
assert self.optional, 'payload must not be None'
|
|
68
|
+
return None
|
|
69
|
+
if self.many:
|
|
70
|
+
try:
|
|
71
|
+
it = iter(payload)
|
|
72
|
+
except Exception: # noqa: B902
|
|
73
|
+
raise AssertionError('payload is not iterable')
|
|
74
|
+
return [constructor(self.value_type, i) for i in it] # type: ignore
|
|
75
|
+
return constructor(self.value_type, payload) # type: ignore
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def parse(t: Type[Any], type_constraint: Optional[Type[TVal | TValRoot]]) -> 'Optional[UnwrappedOptionalObjOrListOfObj[TVal]]':
|
|
79
|
+
unwrapped_t = unwrap_typing(t)
|
|
80
|
+
|
|
81
|
+
many = False
|
|
82
|
+
optional = False
|
|
83
|
+
value_type: TypingType
|
|
84
|
+
|
|
85
|
+
if isinstance(unwrapped_t, TypingType):
|
|
86
|
+
value_type = unwrapped_t
|
|
87
|
+
elif isinstance(unwrapped_t, TypingGeneric) and len(unwrapped_t.args) == 1:
|
|
88
|
+
if unwrapped_t.origin.value != list: # noqa: E721
|
|
89
|
+
return None
|
|
90
|
+
if not isinstance(unwrapped_t.args[0], TypingType):
|
|
91
|
+
return None # type: ignore
|
|
92
|
+
value_type = unwrapped_t.args[0]
|
|
93
|
+
many = True
|
|
94
|
+
elif isinstance(unwrapped_t, TypingOptional):
|
|
95
|
+
optional = True
|
|
96
|
+
if isinstance(unwrapped_t.value, TypingGeneric):
|
|
97
|
+
if unwrapped_t.value.origin.value != list: # noqa: E721
|
|
98
|
+
return None
|
|
99
|
+
if len(unwrapped_t.value.args) != 1 or not isinstance(unwrapped_t.value.args[0], TypingType):
|
|
100
|
+
return None
|
|
101
|
+
value_type = unwrapped_t.value.args[0]
|
|
102
|
+
many = True
|
|
103
|
+
elif isinstance(unwrapped_t.value, TypingType):
|
|
104
|
+
value_type = unwrapped_t.value
|
|
105
|
+
else:
|
|
106
|
+
return None
|
|
107
|
+
else:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
if not isinstance(value_type, TypingType):
|
|
111
|
+
return None # type: ignore
|
|
112
|
+
|
|
113
|
+
if type_constraint is not None:
|
|
114
|
+
if not value_type.issubclassof(type_constraint):
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
return UnwrappedOptionalObjOrListOfObj(many, optional, value_type.value)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any, Union
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from werkzeug.routing.converters import ValidationError, BaseConverter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UUID4Converter(BaseConverter):
|
|
8
|
+
"""
|
|
9
|
+
UUID4 converter for the routing system.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, map: Any) -> None:
|
|
13
|
+
super(UUID4Converter, self).__init__(map)
|
|
14
|
+
|
|
15
|
+
def to_python(self, value: Any) -> Union[UUID, None]:
|
|
16
|
+
try:
|
|
17
|
+
return UUID(hex=value, version=4)
|
|
18
|
+
except ValueError:
|
|
19
|
+
raise ValidationError()
|
|
20
|
+
|
|
21
|
+
def to_url(self, value: Any) -> str:
|
|
22
|
+
return str(value)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import Union, List
|
|
5
|
+
from ul_api_utils.validators.custom_fields import QueryParamsSeparatedList
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelStr(BaseModel):
|
|
9
|
+
param: QueryParamsSeparatedList[str]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelInt(BaseModel):
|
|
13
|
+
param: QueryParamsSeparatedList[int]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize(
|
|
17
|
+
"model, input_data, expected_output",
|
|
18
|
+
[
|
|
19
|
+
pytest.param(ModelStr, "first_array_element,second,third,this", ["first_array_element", "second", "third", "this"]),
|
|
20
|
+
pytest.param(ModelStr, ["first_array_element,second,third,this"], ["first_array_element", "second", "third", "this"]),
|
|
21
|
+
pytest.param(ModelInt, ["1,2,3,4,5"], [1, 2, 3, 4, 5]),
|
|
22
|
+
pytest.param(ModelStr, 'first_array_element,"second,third",this', ["first_array_element", "second,third", "this"]),
|
|
23
|
+
pytest.param(ModelStr, ['first_array_element,"second,third",this'], ["first_array_element", "second,third", "this"]),
|
|
24
|
+
pytest.param(ModelStr, '"first_array_element,second,third",this, "1,2"', ["first_array_element,second,third", "this", "1,2"]),
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
def test__query_params_separated_list(
|
|
28
|
+
model: Union[ModelStr, ModelInt], input_data: Union[List[str], str], expected_output: List[Union[str, int]],
|
|
29
|
+
) -> None:
|
|
30
|
+
instance = model(param=input_data) # type: ignore
|
|
31
|
+
assert isinstance(instance.param, list)
|
|
32
|
+
assert instance.param == expected_output
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from typing import TypeVar, Generic, List, Union, Annotated, Any, get_args
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, StringConstraints, BeforeValidator
|
|
6
|
+
|
|
7
|
+
from ul_api_utils.const import CRON_EXPRESSION_VALIDATION_REGEX, MIN_UTC_OFFSET_SECONDS, MAX_UTC_OFFSET_SECONDS
|
|
8
|
+
|
|
9
|
+
NotEmptyListAnnotation = Annotated[list[Any], Field(min_length=1)]
|
|
10
|
+
NotEmptyListStrAnnotation = Annotated[list[str], Field(min_length=1)]
|
|
11
|
+
NotEmptyListIntAnnotation = Annotated[list[int], Field(min_length=1)]
|
|
12
|
+
NotEmptyListUUIDAnnotation = Annotated[list[UUID], Field(min_length=1)]
|
|
13
|
+
CronScheduleAnnotation = Annotated[str, StringConstraints(pattern=CRON_EXPRESSION_VALIDATION_REGEX)]
|
|
14
|
+
WhiteSpaceStrippedStrAnnotation = Annotated[str, StringConstraints(strip_whitespace=True)]
|
|
15
|
+
UTCOffsetSecondsAnnotation = Annotated[int, Field(ge=MIN_UTC_OFFSET_SECONDS, le=MAX_UTC_OFFSET_SECONDS)]
|
|
16
|
+
PgTypePasswordStrAnnotation = Annotated[str, StringConstraints(min_length=6, max_length=72)]
|
|
17
|
+
PgTypeShortStrAnnotation = Annotated[str, StringConstraints(min_length=0, max_length=255)]
|
|
18
|
+
PgTypeLongStrAnnotation = Annotated[str, StringConstraints(min_length=0, max_length=1000)]
|
|
19
|
+
PgTypeInt16Annotation = Annotated[int, Field(ge=-32768, le=32768)]
|
|
20
|
+
PgTypePositiveInt16Annotation = Annotated[int, Field(ge=0, le=32768)]
|
|
21
|
+
PgTypeInt32Annotation = Annotated[int, Field(ge=-2147483648, le=2147483648)]
|
|
22
|
+
PgTypePositiveInt32Annotation = Annotated[int, Field(ge=0, le=2147483648)]
|
|
23
|
+
PgTypeInt64Annotation = Annotated[int, Field(ge=-9223372036854775808, le=9223372036854775808)]
|
|
24
|
+
PgTypePositiveInt64Annotation = Annotated[int, Field(ge=0, le=9223372036854775808)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
QueryParamsSeparatedListValueType = TypeVar('QueryParamsSeparatedListValueType')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_query_params(value: Union[str, List[str]], type_: type) -> List[QueryParamsSeparatedListValueType]:
|
|
31
|
+
def process_item(item: str) -> QueryParamsSeparatedListValueType:
|
|
32
|
+
return type_(item.strip())
|
|
33
|
+
|
|
34
|
+
if isinstance(value, list):
|
|
35
|
+
result: list[QueryParamsSeparatedListValueType] = []
|
|
36
|
+
for item in value:
|
|
37
|
+
if isinstance(item, str):
|
|
38
|
+
reader = csv.reader([item], skipinitialspace=True)
|
|
39
|
+
result.extend(process_item(sub_item) for row in reader for sub_item in row)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError("List items must be strings")
|
|
42
|
+
elif isinstance(value, str):
|
|
43
|
+
reader = csv.reader([value], skipinitialspace=True)
|
|
44
|
+
result = [process_item(item) for row in reader for item in row]
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError("Value must be a string or a list of strings")
|
|
47
|
+
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class QueryParamsSeparatedList(Generic[QueryParamsSeparatedListValueType]):
|
|
52
|
+
def __init__(self, value: Union[str, List[str]]):
|
|
53
|
+
self._values: List[QueryParamsSeparatedListValueType] = validate_query_params(value)
|
|
54
|
+
|
|
55
|
+
def to_list(self) -> List[QueryParamsSeparatedListValueType]:
|
|
56
|
+
return list(self._values)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def __get_pydantic_core_schema__(cls, source_type: type, handler: Any) -> Any:
|
|
60
|
+
inner_type = get_args(source_type)[0]
|
|
61
|
+
return handler(
|
|
62
|
+
Annotated[
|
|
63
|
+
List[inner_type], # type: ignore
|
|
64
|
+
BeforeValidator(lambda x: validate_query_params(x, inner_type))
|
|
65
|
+
]
|
|
66
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ul_api_utils.errors import SimpleValidateApiError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_empty_object(obj_id: str, model: Any) -> Any:
|
|
7
|
+
obj = model.query.filter_by(id=obj_id).first()
|
|
8
|
+
if not obj:
|
|
9
|
+
raise SimpleValidateApiError(f'{model.__name__} data was not found')
|
|
10
|
+
return obj
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 master
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ul-api-utils
|
|
3
|
+
Version: 9.3.0
|
|
4
|
+
Summary: Python api utils
|
|
5
|
+
Author: Unic-lab
|
|
6
|
+
Author-email:
|
|
7
|
+
License: MIT
|
|
8
|
+
Platform: any
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: ul-unipipeline (==2.0.6)
|
|
19
|
+
Requires-Dist: jinja2 (==3.1.6)
|
|
20
|
+
Requires-Dist: flask (==3.1.0)
|
|
21
|
+
Requires-Dist: flask-wtf (==1.2.2)
|
|
22
|
+
Requires-Dist: flask-limiter (==3.10.1)
|
|
23
|
+
Requires-Dist: flask-caching (==2.3.1)
|
|
24
|
+
Requires-Dist: flask-swagger-ui (==4.11.1)
|
|
25
|
+
Requires-Dist: flask-monitoringdashboard (==3.3.2)
|
|
26
|
+
Requires-Dist: pycryptodome (==3.21.0)
|
|
27
|
+
Requires-Dist: pyjwt (==2.10.1)
|
|
28
|
+
Requires-Dist: gunicorn (==23.0.0)
|
|
29
|
+
Requires-Dist: gevent (==24.11.1)
|
|
30
|
+
Requires-Dist: gevent-websocket (==0.10.1)
|
|
31
|
+
Requires-Dist: pyyaml (==6.0)
|
|
32
|
+
Requires-Dist: requests (==2.32.0)
|
|
33
|
+
Requires-Dist: cryptography (==44.0.2)
|
|
34
|
+
Requires-Dist: colored (==1.4.3)
|
|
35
|
+
Requires-Dist: flask-socketio (==5.5.1)
|
|
36
|
+
Requires-Dist: ormsgpack (==1.8.0)
|
|
37
|
+
Requires-Dist: msgpack (==1.1.0)
|
|
38
|
+
Requires-Dist: msgpack-types (==0.5.0)
|
|
39
|
+
Requires-Dist: fastavro (==1.10.0)
|
|
40
|
+
Requires-Dist: factory-boy (==3.3.0)
|
|
41
|
+
Requires-Dist: sentry-sdk[flask] (==2.22.0)
|
|
42
|
+
Requires-Dist: faker (==37.0.0)
|
|
43
|
+
Requires-Dist: types-requests (==2.32.0.20250306)
|
|
44
|
+
Requires-Dist: types-jinja2 (==2.11.9)
|
|
45
|
+
Requires-Dist: xlsxwriter (==3.2.2)
|
|
46
|
+
Requires-Dist: werkzeug (==3.1.3)
|
|
47
|
+
Requires-Dist: frozendict (==2.4.4)
|
|
48
|
+
Requires-Dist: wtforms (==3.0.1)
|
|
49
|
+
Requires-Dist: wtforms-alchemy (==0.18.0)
|
|
50
|
+
Requires-Dist: pathvalidate (==3.2.3)
|
|
51
|
+
|
|
52
|
+
# Generic library api-utils
|
|
53
|
+
|
|
54
|
+
> Provides common api-related functionality that can be used across different services.
|
|
55
|
+
|
|
56
|
+
> Contains all api-related packages as dependencies.
|
|
57
|
+
If you need to use some package that is not available in your service, you should add it here.
|
|
58
|
+
|
|
59
|
+
## Common functionality & Modules
|
|
60
|
+
> This section describes some classes or methods that are available for use in all services that use api-utils.
|
|
61
|
+
|
|
62
|
+
## ApiResource module
|
|
63
|
+
|
|
64
|
+
### ApiRequestQuery
|
|
65
|
+
```python
|
|
66
|
+
class ApiRequestQuery(BaseModel):
|
|
67
|
+
sort: Optional[str] = None
|
|
68
|
+
filter: Optional[str] = None
|
|
69
|
+
limit: Optional[int] = None
|
|
70
|
+
offset: Optional[int] = None
|
|
71
|
+
page: Optional[int] = None
|
|
72
|
+
```
|
|
73
|
+
> Provides basic functionality to the API request.
|
|
74
|
+
> 1. Validation of empty values (replaces empty string to null/None) in API requests.
|
|
75
|
+
> 2. Pagination if provided.
|
|
76
|
+
> 3. Filtering by filter params if provided.
|
|
77
|
+
> 4. Sorting by sort params if provided.
|
|
78
|
+
|
|
79
|
+
> If you want to add some additional by-default behavior to ApiRequestQuery than you have to add it here.
|
|
80
|
+
|
|
81
|
+
### ApiResource
|
|
82
|
+
```python
|
|
83
|
+
class ApiResource:
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
logger: logging.Logger,
|
|
88
|
+
debugger_enabled: bool,
|
|
89
|
+
type: ApiResourceType,
|
|
90
|
+
config: ApiSdkConfig,
|
|
91
|
+
access: PermissionDefinition,
|
|
92
|
+
headers: Mapping[str, str],
|
|
93
|
+
api_resource_config: Optional[ApiResourceConfig] = None,
|
|
94
|
+
fn_typing: ApiResourceFnTyping,
|
|
95
|
+
) -> None:
|
|
96
|
+
self._debugger_enabled = debugger_enabled
|
|
97
|
+
self._token: Optional[ApiSdkJwt] = None
|
|
98
|
+
self._token_raw: Optional[str] = None
|
|
99
|
+
self._type = type
|
|
100
|
+
self._config = config
|
|
101
|
+
self._api_resource_config = api_resource_config or ApiResourceConfig()
|
|
102
|
+
self._fn_typing = fn_typing
|
|
103
|
+
self._logger = logger
|
|
104
|
+
|
|
105
|
+
self._headers = headers
|
|
106
|
+
self._access = access
|
|
107
|
+
self._method = ApiMethod(str(request.method).strip().upper()) # todo: move it in host function
|
|
108
|
+
self._internal_use__files_to_clean: Set[str] = set()
|
|
109
|
+
self._now = datetime.now()
|
|
110
|
+
```
|
|
111
|
+
> Provides basic functionality to the API resource. Defines common properties for every API resourse.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
| ApiResource properties | Desription |
|
|
115
|
+
|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
116
|
+
| ApiResource.debugger_enabled | Returns boolean value indicating that debugger is enabled/disabled. |
|
|
117
|
+
| ApiResource.logger | Returns ApiResource logger. |
|
|
118
|
+
| ApiResource.method | Returns ApiResource API method (GET, POST, PUT, PATCH, DELETE, OPTIONS). |
|
|
119
|
+
| ApiResource.request_files | Returns files that are attached to Flask API request. |
|
|
120
|
+
| ApiResource.request_headers | Returns request headers that are attached to request. |
|
|
121
|
+
| ApiResource.request_info | Returns request information that is attached to request (user-agent, ip). |
|
|
122
|
+
| ApiResource.auth_token, ApiResource.auth_token_raw | Returns token or raw token. |
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## Internal API module
|
|
126
|
+
|
|
127
|
+
### Internal API
|
|
128
|
+
> Provides *InternalAPI* class with basic functionality to support the requests between internal services.
|
|
129
|
+
> Implements a wrapper for all types of requests: GET, POST, PUT, PATCH, DELETE.
|
|
130
|
+
|
|
131
|
+
### Internal API Response
|
|
132
|
+
> Provides *InternalApiResponse* class with basic properties of Response, such as:
|
|
133
|
+
> .ok, .total_count, .count, .errors, .payload,
|
|
134
|
+
> .status_code, .result_bytes, .result_text, .result_json.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## API SDK Module
|
|
138
|
+
> Provides debugger setup for every service that uses it, useful decorators to build views, etc.
|
|
139
|
+
> In order to use it in another service, SDK should be initialized and here an example.
|
|
140
|
+
```python
|
|
141
|
+
web_sdk = ApiSdk(ApiSdkConfig(
|
|
142
|
+
permissions=your_permissions,
|
|
143
|
+
jwt_validator=your_validator,
|
|
144
|
+
not_found_template=path/to/template,
|
|
145
|
+
rate_limit=your_rate_limit,
|
|
146
|
+
other_params_are_listed_below=please_read_them
|
|
147
|
+
))
|
|
148
|
+
```
|
|
149
|
+
## API SDK Config
|
|
150
|
+
> Provides configuration for the API SDK.
|
|
151
|
+
```python
|
|
152
|
+
class ApiSdkConfig(BaseModel):
|
|
153
|
+
permissions: Optional[Union[Callable[[], PermissionRegistry], PermissionRegistry]] = None
|
|
154
|
+
permissions_check_enabled: bool = True # GLOBAL CHECK OF ACCESS AND PERMISSIONS ENABLE
|
|
155
|
+
permissions_validator: Optional[Callable[[ApiSdkJwt, PermissionDefinition], bool]] = None
|
|
156
|
+
|
|
157
|
+
jwt_validator: Optional[Callable[[ApiSdkJwt], bool]] = None
|
|
158
|
+
jwt_environment_check_enabled: bool = True
|
|
159
|
+
|
|
160
|
+
http_auth: Optional[ApiSdkHttpAuth] = None
|
|
161
|
+
|
|
162
|
+
static_url_path: Optional[str] = None
|
|
163
|
+
|
|
164
|
+
not_found_template: Optional[str] = None
|
|
165
|
+
|
|
166
|
+
rate_limit: Union[str, List[str]] = '100/minute' # [count (int)] [per|/] [second|minute|hour|day|month|year][s]
|
|
167
|
+
rate_limit_storage_uri: str = '' # supports url of redis, memcached, mongodb
|
|
168
|
+
rate_limit_identify: Union[ApiSdkIdentifyTypeEnum, Callable[[], str]] = ApiSdkIdentifyTypeEnum.DISABLED # must be None if disabled
|
|
169
|
+
|
|
170
|
+
api_route_path_prefix: str = '/api'
|
|
171
|
+
|
|
172
|
+
class Config:
|
|
173
|
+
extra = Extra.forbid
|
|
174
|
+
allow_mutation = False
|
|
175
|
+
frozen = True
|
|
176
|
+
arbitrary_types_allowed = True
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> Also, API SDK provides useful decorators that help to create views (web based, API endpoints, file download endpoints).
|
|
180
|
+
```python
|
|
181
|
+
@web_sdk.html_view(method, path, access=permission, config=ApiResourceConfig)
|
|
182
|
+
@web_sdk.rest_api(method, path, access=permission, config=ApiResourceConfig, v='v1')
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Worker SDK
|
|
186
|
+
> Provides a useful decorator for message handling that adds logging, Sentry monitoring and WorkerContext.
|
|
187
|
+
```python
|
|
188
|
+
from src.conf.worker_sdk import worker_sdk
|
|
189
|
+
|
|
190
|
+
initialized_worker = worker_sdk.init(__name__)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
__all__ = (
|
|
194
|
+
'initialized_worker',
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
```python
|
|
198
|
+
@initialized_worker.handle_message()
|
|
199
|
+
def some_worker_function(params):
|
|
200
|
+
...
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Custom Shared Validators
|
|
204
|
+
> Library has some commonly-used validators available for use in other services to avoid code duplication.
|
|
205
|
+
> If you think that some services will benefit from adding a new one that can be shared, you are welcome.
|
|
206
|
+
|
|
207
|
+
#### Validate UUID
|
|
208
|
+
```python
|
|
209
|
+
def validate_uuid4(uuid: Any) -> None:
|
|
210
|
+
try:
|
|
211
|
+
UUID(uuid, version=4)
|
|
212
|
+
except ValueError:
|
|
213
|
+
raise SimpleValidateApiError('invalid uuid')
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Validate empty object
|
|
217
|
+
```python
|
|
218
|
+
def validate_empty_object(obj_id: str, model: Any) -> Any:
|
|
219
|
+
obj = model.query.filter_by(id=obj_id).first()
|
|
220
|
+
if not obj:
|
|
221
|
+
raise SimpleValidateApiError(f'{model.__name__} data was not found')
|
|
222
|
+
return obj
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Custom Exceptions
|
|
226
|
+
|
|
227
|
+
| Exception | Desription |
|
|
228
|
+
|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
229
|
+
| AbstractApiError | Services can inherit from this error and create own service-oriented API Exceptions. Should not be raised. |
|
|
230
|
+
| AbstractInternalApiError | Services can inherit from this error and create own service-oriented Internal-API Exceptions. Should not be raised. |
|
|
231
|
+
| RequestAbstractInternalApiError | Services can inherit from this error and create own Exceptions, that should be used before sending a request to another service. Should not be raised. |
|
|
232
|
+
| ResponseAbstractInternalApiError | Services can inherit from this error and create own Exceptions, that should be used after getting a response from another service. Should not be raised. |
|
|
233
|
+
| ResponseFormatInternalApiError | Should be raised when the response format from another service is incorrect. |
|
|
234
|
+
| ResponseJsonSchemaInternalApiError | Should be raised when the response schema from another service is incorrect. |
|
|
235
|
+
| ResponsePayloadTypeInternalApiError | Should be raised when the response payload types from another services aren't matching. |
|
|
236
|
+
| Server5XXInternalApiError | Should be raised when the response status from another service is 5xx. (something wrong at receiving end) |
|
|
237
|
+
| Client4XXInternalApiError | Should be raised when the response status from another service is 4xx. (something wrong at sender end) |
|
|
238
|
+
| UserAbstractApiError | Services can inherit from this error and create own Exceptions, that should be used only to stop request handling and only in API-routes. Should not be raised. |
|
|
239
|
+
| ValidationListApiError | Should be raised when the incoming request is invalid because of validation. |
|
|
240
|
+
| ValidateApiError | Should be raised when the incoming request is invalid because of validation. |
|
|
241
|
+
| AccessApiError | Should be raised when API-call sender does not have an access to the API. |
|
|
242
|
+
| AccessApiError | Should be raised when API-call sender do es not have an access to the API. |
|
|
243
|
+
| PermissionDeniedApiError | Should be raised when API-call sender does not have required permissions to access the API. |
|
|
244
|
+
| NoResultFoundApiError | Should be raised when we can't return a response because required data does not exist. |
|
|
245
|
+
| HasAlreadyExistsApiError | Should be raised when we can't create a new record because the same one already exists. |
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
## Adding new api-related package
|
|
249
|
+
> First, try to understand why do you need this library and what exactly can you do with it. Look at the list of
|
|
250
|
+
> already existing libraries and think if they can fulfill your needs.
|
|
251
|
+
|
|
252
|
+
> Check this library for deprecation, does it have enough maintenance, library dependencies.
|
|
253
|
+
> If all above satisfies you, perform next steps:
|
|
254
|
+
> 1. Add the package name and version to **Pipfile** under ```[packages]``` section. Example: ```alembic = "==1.8.1"```.
|
|
255
|
+
> 2. Run ```pipenv install```.
|
|
256
|
+
> 3. Add the package name and version to **setup.py** to ```install-requires``` section.
|
|
257
|
+
> 4. Commit changes. ```git commit -m "Add dependency *library-name*"```.
|
|
258
|
+
> 5. Run version patch: ```pipenv run version_patch```.
|
|
259
|
+
> 6. Push changes directly to dev ```git push origin dev --tags``` or raise MR for your changes to be reviewed.
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
## Example
|
|
263
|
+
```bash
|
|
264
|
+
FLASK_DEBUG=1 FLASK_ENV=development FLASK_APP=example.example_app APPLICATION_DIR=$(pwd)/example APPLICATION_DEBUG=1 flask run --port 5001
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## How to debug using PyCharm Professional:
|
|
268
|
+

|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
## How to create keys
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
pipenv run enc_keys --algorithm=RS256 --service-name some --follower-services foo bar --jwt-permissions-module example.permissions --jwt-user-id 03670a66-fb50-437e-96ae-b42bb83e3d04 --jwt-environment=local
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
pipenv run enc_keys --algorithm ES256 --service-name some --follower-services foo bar --jwt-permissions-module example.permissions --jwt-user-id 03670a66-fb50-437e-96ae-b42bb83e3d04 --jwt-environment=local
|
|
279
|
+
```
|