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.
Files changed (156) hide show
  1. example/__init__.py +0 -0
  2. example/conf.py +35 -0
  3. example/main.py +24 -0
  4. example/models/__init__.py +0 -0
  5. example/permissions.py +6 -0
  6. example/pure_flask_example.py +65 -0
  7. example/rate_limit_load.py +10 -0
  8. example/redis_repository.py +22 -0
  9. example/routes/__init__.py +0 -0
  10. example/routes/api_some.py +335 -0
  11. example/sockets/__init__.py +0 -0
  12. example/sockets/on_connect.py +16 -0
  13. example/sockets/on_disconnect.py +14 -0
  14. example/sockets/on_json.py +10 -0
  15. example/sockets/on_message.py +13 -0
  16. example/sockets/on_open.py +16 -0
  17. example/workers/__init__.py +0 -0
  18. example/workers/worker.py +28 -0
  19. ul_api_utils/__init__.py +0 -0
  20. ul_api_utils/access/__init__.py +122 -0
  21. ul_api_utils/api_resource/__init__.py +0 -0
  22. ul_api_utils/api_resource/api_request.py +105 -0
  23. ul_api_utils/api_resource/api_resource.py +414 -0
  24. ul_api_utils/api_resource/api_resource_config.py +20 -0
  25. ul_api_utils/api_resource/api_resource_error_handling.py +21 -0
  26. ul_api_utils/api_resource/api_resource_fn_typing.py +356 -0
  27. ul_api_utils/api_resource/api_resource_type.py +16 -0
  28. ul_api_utils/api_resource/api_response.py +300 -0
  29. ul_api_utils/api_resource/api_response_db.py +26 -0
  30. ul_api_utils/api_resource/api_response_payload_alias.py +25 -0
  31. ul_api_utils/api_resource/db_types.py +9 -0
  32. ul_api_utils/api_resource/signature_check.py +41 -0
  33. ul_api_utils/commands/__init__.py +0 -0
  34. ul_api_utils/commands/cmd_enc_keys.py +172 -0
  35. ul_api_utils/commands/cmd_gen_api_user_token.py +77 -0
  36. ul_api_utils/commands/cmd_gen_new_api_user.py +106 -0
  37. ul_api_utils/commands/cmd_generate_api_docs.py +181 -0
  38. ul_api_utils/commands/cmd_start.py +110 -0
  39. ul_api_utils/commands/cmd_worker_start.py +76 -0
  40. ul_api_utils/commands/start/__init__.py +0 -0
  41. ul_api_utils/commands/start/gunicorn.conf.local.py +0 -0
  42. ul_api_utils/commands/start/gunicorn.conf.py +26 -0
  43. ul_api_utils/commands/start/wsgi.py +22 -0
  44. ul_api_utils/conf/ul-debugger-main.js +1 -0
  45. ul_api_utils/conf/ul-debugger-ui.js +1 -0
  46. ul_api_utils/conf.py +70 -0
  47. ul_api_utils/const.py +78 -0
  48. ul_api_utils/debug/__init__.py +0 -0
  49. ul_api_utils/debug/debugger.py +119 -0
  50. ul_api_utils/debug/malloc.py +93 -0
  51. ul_api_utils/debug/stat.py +444 -0
  52. ul_api_utils/encrypt/__init__.py +0 -0
  53. ul_api_utils/encrypt/encrypt_decrypt_abstract.py +15 -0
  54. ul_api_utils/encrypt/encrypt_decrypt_aes_xtea.py +59 -0
  55. ul_api_utils/errors.py +200 -0
  56. ul_api_utils/internal_api/__init__.py +0 -0
  57. ul_api_utils/internal_api/__tests__/__init__.py +0 -0
  58. ul_api_utils/internal_api/__tests__/internal_api.py +29 -0
  59. ul_api_utils/internal_api/__tests__/internal_api_content_type.py +22 -0
  60. ul_api_utils/internal_api/internal_api.py +369 -0
  61. ul_api_utils/internal_api/internal_api_check_context.py +42 -0
  62. ul_api_utils/internal_api/internal_api_error.py +17 -0
  63. ul_api_utils/internal_api/internal_api_response.py +296 -0
  64. ul_api_utils/main.py +29 -0
  65. ul_api_utils/modules/__init__.py +0 -0
  66. ul_api_utils/modules/__tests__/__init__.py +0 -0
  67. ul_api_utils/modules/__tests__/test_api_sdk_jwt.py +195 -0
  68. ul_api_utils/modules/api_sdk.py +555 -0
  69. ul_api_utils/modules/api_sdk_config.py +63 -0
  70. ul_api_utils/modules/api_sdk_jwt.py +377 -0
  71. ul_api_utils/modules/intermediate_state.py +34 -0
  72. ul_api_utils/modules/worker_context.py +35 -0
  73. ul_api_utils/modules/worker_sdk.py +109 -0
  74. ul_api_utils/modules/worker_sdk_config.py +13 -0
  75. ul_api_utils/py.typed +0 -0
  76. ul_api_utils/resources/__init__.py +0 -0
  77. ul_api_utils/resources/caching.py +196 -0
  78. ul_api_utils/resources/debugger_scripts.py +97 -0
  79. ul_api_utils/resources/health_check/__init__.py +0 -0
  80. ul_api_utils/resources/health_check/const.py +2 -0
  81. ul_api_utils/resources/health_check/health_check.py +439 -0
  82. ul_api_utils/resources/health_check/health_check_template.py +64 -0
  83. ul_api_utils/resources/health_check/resource.py +97 -0
  84. ul_api_utils/resources/not_implemented.py +25 -0
  85. ul_api_utils/resources/permissions.py +29 -0
  86. ul_api_utils/resources/rate_limitter.py +84 -0
  87. ul_api_utils/resources/socketio.py +55 -0
  88. ul_api_utils/resources/swagger.py +119 -0
  89. ul_api_utils/resources/web_forms/__init__.py +0 -0
  90. ul_api_utils/resources/web_forms/custom_fields/__init__.py +0 -0
  91. ul_api_utils/resources/web_forms/custom_fields/custom_checkbox_select.py +5 -0
  92. ul_api_utils/resources/web_forms/custom_widgets/__init__.py +0 -0
  93. ul_api_utils/resources/web_forms/custom_widgets/custom_select_widget.py +86 -0
  94. ul_api_utils/resources/web_forms/custom_widgets/custom_text_input_widget.py +42 -0
  95. ul_api_utils/resources/web_forms/uni_form.py +75 -0
  96. ul_api_utils/sentry.py +52 -0
  97. ul_api_utils/utils/__init__.py +0 -0
  98. ul_api_utils/utils/__tests__/__init__.py +0 -0
  99. ul_api_utils/utils/__tests__/api_path_version.py +16 -0
  100. ul_api_utils/utils/__tests__/unwrap_typing.py +67 -0
  101. ul_api_utils/utils/api_encoding.py +51 -0
  102. ul_api_utils/utils/api_format.py +61 -0
  103. ul_api_utils/utils/api_method.py +55 -0
  104. ul_api_utils/utils/api_pagination.py +58 -0
  105. ul_api_utils/utils/api_path_version.py +60 -0
  106. ul_api_utils/utils/api_request_info.py +6 -0
  107. ul_api_utils/utils/avro.py +131 -0
  108. ul_api_utils/utils/broker_topics_message_count.py +47 -0
  109. ul_api_utils/utils/cached_per_request.py +23 -0
  110. ul_api_utils/utils/colors.py +31 -0
  111. ul_api_utils/utils/constants.py +7 -0
  112. ul_api_utils/utils/decode_base64.py +9 -0
  113. ul_api_utils/utils/deprecated.py +19 -0
  114. ul_api_utils/utils/flags.py +29 -0
  115. ul_api_utils/utils/flask_swagger_generator/__init__.py +0 -0
  116. ul_api_utils/utils/flask_swagger_generator/conf.py +4 -0
  117. ul_api_utils/utils/flask_swagger_generator/exceptions.py +7 -0
  118. ul_api_utils/utils/flask_swagger_generator/specifiers/__init__.py +0 -0
  119. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_models.py +57 -0
  120. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_specifier.py +48 -0
  121. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_three_specifier.py +777 -0
  122. ul_api_utils/utils/flask_swagger_generator/specifiers/swagger_version.py +40 -0
  123. ul_api_utils/utils/flask_swagger_generator/utils/__init__.py +0 -0
  124. ul_api_utils/utils/flask_swagger_generator/utils/input_type.py +77 -0
  125. ul_api_utils/utils/flask_swagger_generator/utils/parameter_type.py +51 -0
  126. ul_api_utils/utils/flask_swagger_generator/utils/replace_in_dict.py +18 -0
  127. ul_api_utils/utils/flask_swagger_generator/utils/request_type.py +52 -0
  128. ul_api_utils/utils/flask_swagger_generator/utils/schema_type.py +15 -0
  129. ul_api_utils/utils/flask_swagger_generator/utils/security_type.py +39 -0
  130. ul_api_utils/utils/imports.py +16 -0
  131. ul_api_utils/utils/instance_checks.py +16 -0
  132. ul_api_utils/utils/jinja/__init__.py +0 -0
  133. ul_api_utils/utils/jinja/t_url_for.py +19 -0
  134. ul_api_utils/utils/jinja/to_pretty_json.py +11 -0
  135. ul_api_utils/utils/json_encoder.py +126 -0
  136. ul_api_utils/utils/load_modules.py +15 -0
  137. ul_api_utils/utils/memory_db/__init__.py +0 -0
  138. ul_api_utils/utils/memory_db/__tests__/__init__.py +0 -0
  139. ul_api_utils/utils/memory_db/errors.py +8 -0
  140. ul_api_utils/utils/memory_db/repository.py +102 -0
  141. ul_api_utils/utils/token_check.py +14 -0
  142. ul_api_utils/utils/token_check_through_request.py +16 -0
  143. ul_api_utils/utils/unwrap_typing.py +117 -0
  144. ul_api_utils/utils/uuid_converter.py +22 -0
  145. ul_api_utils/validators/__init__.py +0 -0
  146. ul_api_utils/validators/__tests__/__init__.py +0 -0
  147. ul_api_utils/validators/__tests__/test_custom_fields.py +32 -0
  148. ul_api_utils/validators/custom_fields.py +66 -0
  149. ul_api_utils/validators/validate_empty_object.py +10 -0
  150. ul_api_utils/validators/validate_uuid.py +11 -0
  151. ul_api_utils-9.3.0.dist-info/LICENSE +21 -0
  152. ul_api_utils-9.3.0.dist-info/METADATA +279 -0
  153. ul_api_utils-9.3.0.dist-info/RECORD +156 -0
  154. ul_api_utils-9.3.0.dist-info/WHEEL +5 -0
  155. ul_api_utils-9.3.0.dist-info/entry_points.txt +2 -0
  156. 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,11 @@
1
+ from typing import Any
2
+ from uuid import UUID
3
+
4
+ from ul_api_utils.errors import SimpleValidateApiError
5
+
6
+
7
+ def validate_uuid4(uuid: Any) -> None:
8
+ try:
9
+ UUID(uuid, version=4)
10
+ except ValueError:
11
+ raise SimpleValidateApiError('invalid uuid')
@@ -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
+ ![debug_example_pycharm.png](debug_example_pycharm.png)
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
+ ```