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
ul_api_utils/errors.py ADDED
@@ -0,0 +1,200 @@
1
+ from typing import Dict, List, Any
2
+
3
+ from ul_api_utils.internal_api.internal_api_error import InternalApiResponseErrorObj
4
+
5
+
6
+ class AbstractApiError(Exception):
7
+ """
8
+ Ошибки которые наследуются от этого класса используются
9
+ - для отделения ошибок НАШЕГО приложения от всех других.
10
+ АБСТРАКТНЫЙ КЛАСС = НАПРЯМУЮ НЕ ИСПОЛЬЗУЕТСЯ для raise
11
+ """
12
+ pass
13
+
14
+
15
+ class AbstractInternalApiError(AbstractApiError):
16
+ """
17
+ Ошибки которые наследуются от этого класса используются
18
+ - ТОЛЬКО для отделения ошибок запроса во внешнее API
19
+ АБСТРАКТНЫЙ КЛАСС = НАПРЯМУЮ НЕ ИСПОЛЬЗУЕТСЯ для raise
20
+ """
21
+ pass
22
+
23
+
24
+ class RequestAbstractInternalApiError(AbstractInternalApiError):
25
+ """
26
+ Ошибки которые наследуются от этого класса используются
27
+ - для отделения ошибок ДО обработки запроса сторонним сервером
28
+ АБСТРАКТНЫЙ КЛАСС = НАПРЯМУЮ НЕ ИСПОЛЬЗУЕТСЯ для raise
29
+ """
30
+ def __init__(self, message: str, error: Exception) -> None:
31
+ assert isinstance(message, str), f'message must be str. "{type(message).__name__}" was given'
32
+ assert isinstance(error, Exception), f'error must be Exception. "{type(error).__name__}" was given'
33
+ super(RequestAbstractInternalApiError, self).__init__(f'{message} :: {error}')
34
+ self.error = error
35
+
36
+
37
+ class NotFinishedRequestInternalApiError(RequestAbstractInternalApiError):
38
+ pass
39
+
40
+
41
+ class ResponseAbstractInternalApiError(AbstractInternalApiError):
42
+ """
43
+ Ошибки которые наследуются от этого класса используются
44
+ - для фильтрации ошибок произошедших После успешного получения информации от стороннеего АПИ
45
+ - но произошедшего по причине некорркетного статуса/формата пэйлоада и пр
46
+ АБСТРАКТНЫЙ КЛАСС = НАПРЯМУЮ НЕ ИСПОЛЬЗУЕТСЯ для raise
47
+ """
48
+ pass
49
+
50
+
51
+ class NotCheckedResponseInternalApiError(ResponseAbstractInternalApiError):
52
+ pass
53
+
54
+
55
+ class ResponseFormatInternalApiError(ResponseAbstractInternalApiError):
56
+ pass
57
+
58
+
59
+ class ResponseDataAbstractInternalApiError(ResponseAbstractInternalApiError):
60
+ """
61
+ Ошибки которые наследуются от этого класса используются
62
+ - для фильтрации ошибок произошедших После успешного получения информации от стороннеего АПИ
63
+ - но произошедшего по причине некорркетного статуса/формата пэйлоада и пр
64
+ АБСТРАКТНЫЙ КЛАСС = НАПРЯМУЮ НЕ ИСПОЛЬЗУЕТСЯ для raise
65
+ """
66
+ def __init__(self, message: str, error: Exception) -> None:
67
+ assert isinstance(message, str), f'message must be str. "{type(message).__name__}" was given'
68
+ assert isinstance(error, Exception), f'error must be Exception. "{type(error).__name__}" was given'
69
+ super(ResponseDataAbstractInternalApiError, self).__init__(f'{message} :: {error}')
70
+ self.error = error
71
+
72
+
73
+ class ResponseJsonInternalApiError(ResponseDataAbstractInternalApiError):
74
+ pass
75
+
76
+
77
+ class ResponseJsonSchemaInternalApiError(ResponseDataAbstractInternalApiError):
78
+ pass
79
+
80
+
81
+ class ResponsePayloadTypeInternalApiError(ResponseDataAbstractInternalApiError):
82
+ pass
83
+
84
+
85
+ class ResponseStatusAbstractInternalApiError(ResponseAbstractInternalApiError):
86
+ """
87
+ Ошибки которые наследуются от этого класса используются
88
+ - если сервер выдал ошибку со статусом (явным или неявным) >= 400
89
+ АБСТРАКТНЫЙ КЛАСС = НАПРЯМУЮ НЕ ИСПОЛЬЗУЕТСЯ для raise
90
+ """
91
+
92
+ def __init__(self, status_code: int, errors: List[InternalApiResponseErrorObj]) -> None:
93
+ assert isinstance(status_code, int), f'status_code must be int. "{type(status_code).__name__}" was given'
94
+ assert status_code >= 400
95
+ super(ResponseStatusAbstractInternalApiError, self).__init__(f'status code error :: {status_code} :: {[e.model_dump() for e in errors]}')
96
+ self.status_code = status_code
97
+ self.errors = errors
98
+
99
+
100
+ class Server5XXInternalApiError(ResponseStatusAbstractInternalApiError):
101
+ """
102
+ Ошибки которые наследуются от этого класса используются
103
+ - если сервер выдал ошибку со статусом (явным или неявным) >= 500
104
+ = ЧТОТО ПОШЛО НЕ ТАК на сервере
105
+ """
106
+ def __init__(self, status_code: int, errors: List[InternalApiResponseErrorObj]) -> None:
107
+ assert 500 <= status_code
108
+ super(Server5XXInternalApiError, self).__init__(status_code, errors)
109
+
110
+
111
+ class Client4XXInternalApiError(ResponseStatusAbstractInternalApiError):
112
+ """
113
+ Ошибки которые наследуются от этого класса используются
114
+ - если сервер выдал ошибку со статусом (явным или неявным) >= 400 < 500
115
+ = ЧТО ТО ОТПРАВИЛ НЕ ТО с клиента
116
+ """
117
+ def __init__(self, status_code: int, errors: List[InternalApiResponseErrorObj]) -> None:
118
+ assert 400 <= status_code < 500
119
+ super(Client4XXInternalApiError, self).__init__(status_code, errors)
120
+
121
+
122
+ class UserAbstractApiError(AbstractApiError):
123
+ """
124
+ Ошибки которые наследуются от этого класса используются
125
+ - ТОЛЬКО для остановки обработки запроса с мгновенным выходом
126
+ - используются СТРОГО в обработчиках ресурсов (роутах) API-приложения
127
+ АБСТРАКТНЫЙ КЛАСС = НАПРЯМУЮ НЕ ИСПОЛЬЗУЕТСЯ для raise
128
+ """
129
+ pass
130
+
131
+
132
+ class ValidationListApiError(UserAbstractApiError):
133
+ def __init__(self, errors: List[Dict[str, str]]):
134
+ super().__init__(f'validation errors: {errors}')
135
+ self.errors = errors
136
+
137
+
138
+ class ValidateApiError(UserAbstractApiError):
139
+ def __init__(self, code: str, location: list[Any], msg_template: str, input: Any = None):
140
+ self.code = code
141
+ self.location = location
142
+ self.msg_template = msg_template
143
+ self.input = input
144
+
145
+ def __str__(self): # type: ignore
146
+ return (
147
+ f"{self.msg_template} (code={self.code}, location={self.location}, "
148
+ f"input={self.input})"
149
+ )
150
+
151
+
152
+ class SimpleValidateApiError(UserAbstractApiError):
153
+ pass
154
+
155
+
156
+ class AccessApiError(UserAbstractApiError):
157
+ pass
158
+
159
+
160
+ class PermissionDeniedApiError(UserAbstractApiError):
161
+ pass
162
+
163
+
164
+ class NoResultFoundApiError(UserAbstractApiError):
165
+ pass
166
+
167
+
168
+ class HasAlreadyExistsApiError(UserAbstractApiError):
169
+ pass
170
+
171
+
172
+ class InvalidContentTypeError(UserAbstractApiError):
173
+ pass
174
+
175
+
176
+ class RuntimeAbstractApiError(AbstractApiError):
177
+ """
178
+ Ошибки которые наследуются от этого класса используются
179
+ - ТОЛЬКО для обозначения внештатной ситуации
180
+ - НЕ используется для пользовательского кода.
181
+ - СТРОГО внутрення ошибка сервера (назначенная АПИ-УТИЛС)
182
+ = обозначает то что чтото пошло не так
183
+ АБСТРАКТНЫЙ КЛАСС = НАПРЯМУЮ НЕ ИСПОЛЬЗУЕТСЯ для raise
184
+ """
185
+ pass
186
+
187
+
188
+ class ResourceRuntimeApiError(RuntimeAbstractApiError):
189
+ pass
190
+
191
+
192
+ class ResponseTypeRuntimeApiError(RuntimeAbstractApiError):
193
+ pass
194
+
195
+
196
+ class WrapInternalApiError(Exception):
197
+
198
+ def __init__(self, message: str, error: AbstractInternalApiError) -> None:
199
+ super().__init__(message)
200
+ self.error = error
File without changes
File without changes
@@ -0,0 +1,29 @@
1
+ from ul_api_utils.internal_api.internal_api import InternalApi
2
+ from ul_api_utils.internal_api.internal_api_response import InternalApiResponseCheckLevel
3
+ from ul_api_utils.utils.api_method import ApiMethod
4
+ from ul_api_utils.utils.api_path_version import ApiPathVersion
5
+
6
+
7
+ def test_internal_api() -> None:
8
+ ia = InternalApi('http://someurl.com')
9
+
10
+ assert ia._path_prefix == '/api'
11
+ assert ia._entry_point == 'http://someurl.com'
12
+
13
+ ia = InternalApi('http://someurl.com/with-prefix/some')
14
+ assert ia._path_prefix == '/with-prefix/some'
15
+ assert ia._entry_point == 'http://someurl.com'
16
+
17
+ ia = InternalApi('http://someurl.com/', path_prefix='')
18
+ assert ia._path_prefix == ''
19
+ assert ia._entry_point == 'http://someurl.com'
20
+
21
+
22
+ def test_internal_api_google() -> None:
23
+ ia = InternalApi('https://google.com')
24
+
25
+ ia.test_override(ApiMethod.GET, '/search', 200, response_json={"value": "123123"}, v=ApiPathVersion.NO_PREFIX)
26
+
27
+ resp = ia.request_get('/search', v=ApiPathVersion.NO_PREFIX).check(level=InternalApiResponseCheckLevel.STATUS_CODE)
28
+
29
+ assert resp.payload_raw == {"value": "123123"}
@@ -0,0 +1,22 @@
1
+ from ul_api_utils.const import MIME__MSGPCK, MIME__JSON
2
+ from ul_api_utils.utils.api_format import ApiFormat
3
+
4
+
5
+ def test_serialize() -> None:
6
+ cn = {"a": "b", "c": "рудддщ"}
7
+
8
+ assert ApiFormat.MESSAGE_PACK.serialize_bytes(cn)
9
+
10
+ assert ApiFormat.JSON.mime == MIME__JSON
11
+ assert ApiFormat.MESSAGE_PACK.mime == MIME__MSGPCK
12
+ assert ApiFormat.accept_mimes() == (MIME__MSGPCK, MIME__JSON)
13
+
14
+ format_msgpck = ApiFormat.from_mime(MIME__MSGPCK)
15
+ assert format_msgpck is not None
16
+ format_json = ApiFormat.from_mime(MIME__JSON)
17
+ assert format_json is not None
18
+
19
+ assert (
20
+ format_msgpck.parse_bytes(ApiFormat.MESSAGE_PACK.serialize_bytes(cn))
21
+ == format_json.parse_bytes(ApiFormat.JSON.serialize_bytes(cn))
22
+ )
@@ -0,0 +1,369 @@
1
+ import time
2
+ import urllib.parse
3
+ from json import dumps
4
+ from typing import Optional, Dict, Any, Tuple, Callable
5
+
6
+ import requests
7
+ from flask import g
8
+ from werkzeug.datastructures import FileStorage
9
+
10
+ from ul_api_utils.const import REQUEST_HEADER__INTERNAL, RESPONSE_HEADER__AUTHORIZATION, RESPONSE_HEADER__CONTENT_TYPE, INTERNAL_API__DEFAULT_PATH_PREFIX, REQUEST_HEADER__ACCEPT, \
11
+ RESPONSE_PROP_OK, RESPONSE_PROP_STATUS, RESPONSE_PROP_PAYLOAD, RESPONSE_PROP_COUNT, RESPONSE_PROP_TOTAL, RESPONSE_PROP_ERRORS, REQUEST_HEADER__CONTENT_TYPE, \
12
+ REQUEST_HEADER__CONTENT_ENCODING, REQUEST_HEADER__ACCEPT_CONTENT_ENCODING
13
+ from ul_api_utils.debug import stat
14
+ from ul_api_utils.errors import NotFinishedRequestInternalApiError, ResponseFormatInternalApiError
15
+ from ul_api_utils.internal_api.internal_api_check_context import internal_api_check_context_add_response
16
+ from ul_api_utils.internal_api.internal_api_response import InternalApiResponse, InternalApiOverriddenResponse
17
+ from ul_api_utils.utils.api_encoding import ApiEncoding
18
+ from ul_api_utils.utils.api_format import ApiFormat
19
+ from ul_api_utils.utils.api_method import ApiMethod, NO_REQUEST_BODY_METHODS
20
+ from ul_api_utils.utils.api_path_version import ApiPathVersion
21
+ from ul_api_utils.utils.json_encoder import CustomJSONEncoder
22
+
23
+
24
+ def get_or_create_session(testing_mode: bool) -> requests.Session:
25
+ if testing_mode:
26
+ return requests.Session()
27
+
28
+ try:
29
+ return g.internal_api_requests_session # type: ignore
30
+ except AttributeError:
31
+ s = g.internal_api_requests_session = requests.Session() # type: ignore
32
+ return s
33
+
34
+
35
+ internal_api_registry: Dict[str, 'InternalApi'] = {}
36
+
37
+
38
+ class InternalApi:
39
+ __slots__ = (
40
+ '_path_prefix',
41
+ '_entry_point',
42
+ '_default_auth_token',
43
+ '_auth_method',
44
+ '_override',
45
+ '_testing_mode',
46
+ '_force_content_type',
47
+ '_force_encoding',
48
+ )
49
+
50
+ def __init__(
51
+ self,
52
+ entry_point: str,
53
+ *,
54
+ force_content_type: Optional[ApiFormat] = None,
55
+ force_encoding: Optional[ApiEncoding] = None,
56
+ default_auth_token: Optional[str] = None,
57
+ auth_method: str = 'Bearer',
58
+ path_prefix: str = INTERNAL_API__DEFAULT_PATH_PREFIX,
59
+ ) -> None:
60
+ r = urllib.parse.urlparse(entry_point)
61
+ has_path_pref_in_entry_point = not (r.path == '' or r.path == '/')
62
+ assert '?' not in path_prefix, f'restricted symbol "?" was found in path_prefix="{path_prefix}"'
63
+ assert '&' not in path_prefix, f'restricted symbol "&" was found in path_prefix="{path_prefix}"'
64
+ assert r.query == ''
65
+ assert r.fragment == ''
66
+ assert r.scheme in {'http', 'https'}
67
+ assert not has_path_pref_in_entry_point or (has_path_pref_in_entry_point and path_prefix == INTERNAL_API__DEFAULT_PATH_PREFIX)
68
+ self._path_prefix = path_prefix if not has_path_pref_in_entry_point else r.path
69
+ self._entry_point = urllib.parse.urlunparse(r._replace(path=''))
70
+ self._default_auth_token = default_auth_token
71
+ self._auth_method = auth_method
72
+ internal_api_registry[self._entry_point] = self
73
+ self._override: Dict[str, Dict[ApiMethod, Callable[[bool], InternalApiResponse[Any]]]] = dict()
74
+ self._testing_mode = False
75
+
76
+ self._force_content_type = force_content_type
77
+ self._force_encoding = force_encoding
78
+
79
+ def test_override(
80
+ self,
81
+ method: ApiMethod,
82
+ path: str,
83
+ status_code: int,
84
+ *,
85
+ v: ApiPathVersion = ApiPathVersion.V01,
86
+ headers: Optional[Dict[str, str]] = None,
87
+ infinite: bool = False,
88
+ response_json: Any,
89
+ insert_std_schema: bool = True,
90
+ ) -> None:
91
+ self._testing_mode = True
92
+
93
+ path = v.compile_path(path, self._path_prefix)
94
+
95
+ if path not in self._override:
96
+ self._override[path] = dict()
97
+
98
+ if method not in self._override[path]:
99
+ if insert_std_schema:
100
+ response_data = {
101
+ RESPONSE_PROP_OK: 200 >= status_code > 400,
102
+ RESPONSE_PROP_COUNT: len(response_json) if isinstance(response_json, (list, tuple)) else (1 if response_json is not None else 0),
103
+ RESPONSE_PROP_TOTAL: len(response_json) if isinstance(response_json, (list, tuple)) else (1 if response_json is not None else 0),
104
+ RESPONSE_PROP_ERRORS: [],
105
+ RESPONSE_PROP_STATUS: status_code,
106
+ RESPONSE_PROP_PAYLOAD: response_json,
107
+ }
108
+ else:
109
+ response_data = response_json
110
+ text = dumps(response_data, cls=CustomJSONEncoder)
111
+
112
+ def _override(has_std_schema: bool) -> InternalApiResponse[Any]:
113
+ if not infinite:
114
+ self._override[path].pop(method)
115
+ resp = InternalApiOverriddenResponse(status_code=status_code, text=text, headers=headers or {})
116
+ return InternalApiResponse(f'{method.value} {path}', resp, has_std_schema, None)
117
+
118
+ self._override[path][method] = _override
119
+ return
120
+ raise OverflowError(f'method "{method.value}" has already defined for override path response "{path}"')
121
+
122
+ @property
123
+ def default_auth(self) -> Optional[Tuple[str, str]]:
124
+ if self._auth_method is None or self._default_auth_token is None:
125
+ return None
126
+ return self._auth_method, self._default_auth_token
127
+
128
+ @property
129
+ def entry_point(self) -> str:
130
+ return self._entry_point
131
+
132
+ @property
133
+ def path_prefix(self) -> str:
134
+ return self._path_prefix
135
+
136
+ def request_get( # Explicit params for mypy checking
137
+ self,
138
+ path: str,
139
+ *,
140
+ v: ApiPathVersion = ApiPathVersion.V01,
141
+ q: Optional[Dict[str, Any]] = None,
142
+ private: bool = True,
143
+ access_token: Optional[str] = None,
144
+ headers: Optional[Dict[str, str]] = None,
145
+ has_std_schema: bool = True,
146
+ ) -> InternalApiResponse[Any]:
147
+ return self.request(
148
+ ApiMethod.GET,
149
+ path,
150
+ v=v,
151
+ q=q,
152
+ private=private,
153
+ access_token=access_token,
154
+ headers=headers,
155
+ has_std_schema=has_std_schema,
156
+ )
157
+
158
+ def request_post( # Explicit params for mypy checking
159
+ self,
160
+ path: str,
161
+ *,
162
+ v: ApiPathVersion = ApiPathVersion.V01,
163
+ q: Optional[Dict[str, Any]] = None,
164
+ json: Optional[Any] = None,
165
+ files: Optional[Dict[str, FileStorage]] = None,
166
+ private: bool = True,
167
+ access_token: Optional[str] = None,
168
+ headers: Optional[Dict[str, str]] = None,
169
+ has_std_schema: bool = True,
170
+ ) -> InternalApiResponse[Any]:
171
+ return self.request(
172
+ ApiMethod.POST,
173
+ path,
174
+ v=v,
175
+ q=q,
176
+ json=json,
177
+ files=files,
178
+ private=private,
179
+ access_token=access_token,
180
+ headers=headers,
181
+ has_std_schema=has_std_schema,
182
+ )
183
+
184
+ def request_patch( # Explicit params for mypy checking
185
+ self,
186
+ path: str,
187
+ *,
188
+ v: ApiPathVersion = ApiPathVersion.V01,
189
+ q: Optional[Dict[str, Any]] = None,
190
+ json: Optional[Any] = None,
191
+ private: bool = True,
192
+ access_token: Optional[str] = None,
193
+ headers: Optional[Dict[str, str]] = None,
194
+ has_std_schema: bool = True,
195
+ ) -> InternalApiResponse[Any]:
196
+ return self.request(
197
+ ApiMethod.PATCH,
198
+ path,
199
+ v=v,
200
+ q=q,
201
+ json=json,
202
+ private=private,
203
+ access_token=access_token,
204
+ headers=headers,
205
+ has_std_schema=has_std_schema,
206
+ )
207
+
208
+ def request_delete( # Explicit params for mypy checking
209
+ self,
210
+ path: str,
211
+ *,
212
+ v: ApiPathVersion = ApiPathVersion.V01,
213
+ q: Optional[Dict[str, Any]] = None,
214
+ json: Optional[Any] = None,
215
+ private: bool = True,
216
+ access_token: Optional[str] = None,
217
+ headers: Optional[Dict[str, str]] = None,
218
+ has_std_schema: bool = True,
219
+ ) -> InternalApiResponse[Any]:
220
+ return self.request(
221
+ ApiMethod.DELETE,
222
+ path,
223
+ v=v,
224
+ q=q,
225
+ json=json,
226
+ private=private,
227
+ access_token=access_token,
228
+ headers=headers,
229
+ has_std_schema=has_std_schema,
230
+ )
231
+
232
+ def request_put( # Explicit params for mypy checking
233
+ self,
234
+ path: str,
235
+ *,
236
+ v: ApiPathVersion = ApiPathVersion.V01,
237
+ q: Optional[Dict[str, Any]] = None,
238
+ json: Optional[Any] = None,
239
+ private: bool = True,
240
+ access_token: Optional[str] = None,
241
+ headers: Optional[Dict[str, str]] = None,
242
+ has_std_schema: bool = True,
243
+ ) -> InternalApiResponse[Any]:
244
+ return self.request(
245
+ ApiMethod.PUT,
246
+ path,
247
+ v=v,
248
+ q=q,
249
+ json=json,
250
+ private=private,
251
+ access_token=access_token,
252
+ headers=headers,
253
+ has_std_schema=has_std_schema,
254
+ )
255
+
256
+ def request(
257
+ self,
258
+ method: ApiMethod,
259
+ path: str,
260
+ *,
261
+ v: ApiPathVersion = ApiPathVersion.V01,
262
+ q: Optional[Dict[str, Any]] = None,
263
+ json: Optional[Any] = None,
264
+ files: Optional[Dict[str, FileStorage]] = None,
265
+ private: bool = True,
266
+ access_token: Optional[str] = None,
267
+ headers: Optional[Dict[str, str]] = None,
268
+ has_std_schema: bool = True,
269
+ ) -> InternalApiResponse[Any]:
270
+ assert isinstance(method, ApiMethod), f'method must be ApiMethod. "{type(method).__name__}" was given'
271
+
272
+ path = v.compile_path(path, self._path_prefix)
273
+
274
+ if (
275
+ self._testing_mode
276
+ and path in self._override
277
+ and method in self._override[path]
278
+ ):
279
+ return self._override[path][method](has_std_schema)
280
+
281
+ started_at = time.perf_counter()
282
+ q = ApiPathVersion.cleanup_q(q)
283
+ url = f'{self._entry_point.rstrip("/")}{path}'
284
+
285
+ req_headers = {
286
+ REQUEST_HEADER__INTERNAL: REQUEST_HEADER__INTERNAL,
287
+ REQUEST_HEADER__ACCEPT: ', '.join(ApiFormat.accept_mimes(self._force_content_type)),
288
+ **stat.get_stats_request_headers(),
289
+ }
290
+
291
+ if accept_enc := ', '.join(ApiEncoding.accept_mimes(self._force_encoding)):
292
+ req_headers[REQUEST_HEADER__ACCEPT_CONTENT_ENCODING] = accept_enc
293
+
294
+ req_headers.update(headers or {})
295
+
296
+ data: Optional[bytes] = None
297
+ debug_data: Optional[str] = None
298
+ if json is not None:
299
+ assert method not in NO_REQUEST_BODY_METHODS, f'{method.value} {url} :: must have no body'
300
+ content_type = ApiFormat.JSON if self._force_content_type is None else self._force_content_type
301
+ data = content_type.serialize_bytes(json)
302
+
303
+ if stat.collecting_enabled():
304
+ debug_data = (ApiFormat.JSON.serialize_bytes(json) if content_type is not ApiFormat.JSON else data).decode('utf-8')
305
+ req_headers[REQUEST_HEADER__CONTENT_TYPE] = content_type.mime
306
+
307
+ if data is not None:
308
+ if self._force_encoding is None:
309
+ pass
310
+ # if len(data) > AUTO_GZIP_THRESHOLD_LENGTH:
311
+ # data = ApiEncoding.GZIP.encode(data)
312
+ # req_headers[REQUEST_HEADER__CONTENT_ENCODING] = ApiEncoding.GZIP.mime
313
+ else:
314
+ if self._force_encoding is not ApiEncoding.NONE:
315
+ data = self._force_encoding.encode(data)
316
+ req_headers[REQUEST_HEADER__CONTENT_ENCODING] = self._force_encoding.mime
317
+
318
+ if private and (access_token or self._default_auth_token):
319
+ req_headers[RESPONSE_HEADER__AUTHORIZATION] = f'Bearer {access_token or self._default_auth_token}'
320
+
321
+ response_text = ''
322
+ status_code = None
323
+ internal_stat = []
324
+ error = None
325
+ requests_response = None
326
+ try:
327
+ requests_response = get_or_create_session(self._testing_mode).request(
328
+ method.value,
329
+ url=url,
330
+ files=({name: (fs.filename, fs.stream, fs.content_type, fs.headers) for name, fs in files.items()} if files is not None else None),
331
+ headers=req_headers,
332
+ data=data,
333
+ params=q,
334
+ )
335
+
336
+ if self._force_content_type is None:
337
+ self._force_content_type = ApiFormat.MESSAGE_PACK if requests_response.headers.get(RESPONSE_HEADER__CONTENT_TYPE, '') == ApiFormat.MESSAGE_PACK.mime else None
338
+
339
+ status_code = requests_response.status_code
340
+ except Exception as e: # noqa: B902
341
+ error = e
342
+
343
+ try:
344
+ if requests_response is not None:
345
+ internal_response: InternalApiResponse[Any] = InternalApiResponse(f'{method} {requests_response.url}', requests_response, has_std_schema, None)
346
+
347
+ internal_api_check_context_add_response(internal_response)
348
+
349
+ if has_std_schema:
350
+ response_text = requests_response.text
351
+ internal_stat = internal_response.internal_use__debug_stats
352
+ except Exception as e: # noqa: B902
353
+ error = ResponseFormatInternalApiError(str(e))
354
+
355
+ if stat.collecting_enabled():
356
+ stat.add_http_request_stat(
357
+ started_at=started_at,
358
+ method=method,
359
+ url=requests_response.url if requests_response else url,
360
+ status_code=status_code,
361
+ internal_stats=internal_stat,
362
+ request=debug_data,
363
+ response=response_text,
364
+ )
365
+
366
+ if error:
367
+ raise NotFinishedRequestInternalApiError('request not finished', error)
368
+
369
+ return internal_response
@@ -0,0 +1,42 @@
1
+ import contextlib
2
+ from typing import Generator, Any, TYPE_CHECKING
3
+ from uuid import UUID
4
+
5
+ from flask import g
6
+
7
+ from ul_api_utils.errors import NotCheckedResponseInternalApiError
8
+
9
+ if TYPE_CHECKING:
10
+ from ul_api_utils.internal_api.internal_api_response import InternalApiResponse
11
+
12
+
13
+ @contextlib.contextmanager
14
+ def internal_api_check_context() -> Generator[None, None, None]:
15
+ g._api_utils_internal_api_context = [] # type: ignore
16
+
17
+ try:
18
+ yield
19
+
20
+ invalid_resp = []
21
+ for resp in g._api_utils_internal_api_context: # type: ignore
22
+ if not resp._internal_use__checked_once:
23
+ invalid_resp.append(resp)
24
+
25
+ if len(invalid_resp) > 0:
26
+ info = ", ".join(f"\"{r._internal_use__info}\"" for r in invalid_resp)
27
+ raise NotCheckedResponseInternalApiError(
28
+ f'internal api responses must be checked once at least :: [{info}]',
29
+ )
30
+ finally:
31
+ g._api_utils_internal_api_context.clear() # type: ignore
32
+
33
+
34
+ def internal_api_check_context_add_response(resp: 'InternalApiResponse[Any]') -> None:
35
+ if hasattr(g, '_api_utils_internal_api_context'):
36
+ g._api_utils_internal_api_context.append(resp) # type: ignore
37
+
38
+
39
+ def internal_api_check_context_rm_response(id: UUID) -> None:
40
+ if hasattr(g, '_api_utils_internal_api_context'):
41
+ prev = g._api_utils_internal_api_context # type: ignore
42
+ g._api_utils_internal_api_context = [r for r in prev if r.id != id] # type: ignore
@@ -0,0 +1,17 @@
1
+ from typing import Optional, Any, Dict, List, Tuple, Union
2
+
3
+ from pydantic import ConfigDict, BaseModel
4
+
5
+
6
+ class InternalApiResponseErrorObj(BaseModel):
7
+ error_type: str
8
+ error_message: str
9
+ error_location: Optional[Union[List[str], str, Tuple[str, ...]]] = None
10
+ error_kind: Optional[str] = None
11
+ error_input: Optional[Any] = None
12
+ other: Optional[Dict[str, Any]] = None
13
+
14
+ model_config = ConfigDict(
15
+ extra="ignore",
16
+ frozen=True,
17
+ )