everysk-lib 1.10.2__cp312-cp312-win_amd64.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.
- everysk/__init__.py +30 -0
- everysk/_version.py +683 -0
- everysk/api/__init__.py +61 -0
- everysk/api/api_requestor.py +167 -0
- everysk/api/api_resources/__init__.py +23 -0
- everysk/api/api_resources/api_resource.py +371 -0
- everysk/api/api_resources/calculation.py +779 -0
- everysk/api/api_resources/custom_index.py +42 -0
- everysk/api/api_resources/datastore.py +81 -0
- everysk/api/api_resources/file.py +42 -0
- everysk/api/api_resources/market_data.py +223 -0
- everysk/api/api_resources/parser.py +66 -0
- everysk/api/api_resources/portfolio.py +43 -0
- everysk/api/api_resources/private_security.py +42 -0
- everysk/api/api_resources/report.py +65 -0
- everysk/api/api_resources/report_template.py +39 -0
- everysk/api/api_resources/tests.py +115 -0
- everysk/api/api_resources/worker_execution.py +64 -0
- everysk/api/api_resources/workflow.py +65 -0
- everysk/api/api_resources/workflow_execution.py +93 -0
- everysk/api/api_resources/workspace.py +42 -0
- everysk/api/http_client.py +63 -0
- everysk/api/tests.py +32 -0
- everysk/api/utils.py +262 -0
- everysk/config.py +451 -0
- everysk/core/_tests/serialize/test_json.py +336 -0
- everysk/core/_tests/serialize/test_orjson.py +295 -0
- everysk/core/_tests/serialize/test_pickle.py +48 -0
- everysk/core/cloud_function/main.py +78 -0
- everysk/core/cloud_function/tests.py +86 -0
- everysk/core/compress.py +245 -0
- everysk/core/datetime/__init__.py +12 -0
- everysk/core/datetime/calendar.py +144 -0
- everysk/core/datetime/date.py +424 -0
- everysk/core/datetime/date_expression.py +299 -0
- everysk/core/datetime/date_mixin.py +1475 -0
- everysk/core/datetime/date_settings.py +30 -0
- everysk/core/datetime/datetime.py +713 -0
- everysk/core/exceptions.py +435 -0
- everysk/core/fields.py +1176 -0
- everysk/core/firestore.py +555 -0
- everysk/core/fixtures/_settings.py +29 -0
- everysk/core/fixtures/other/_settings.py +18 -0
- everysk/core/fixtures/user_agents.json +88 -0
- everysk/core/http.py +691 -0
- everysk/core/lists.py +92 -0
- everysk/core/log.py +709 -0
- everysk/core/number.py +37 -0
- everysk/core/object.py +1469 -0
- everysk/core/redis.py +1021 -0
- everysk/core/retry.py +51 -0
- everysk/core/serialize.py +674 -0
- everysk/core/sftp.py +414 -0
- everysk/core/signing.py +53 -0
- everysk/core/slack.py +127 -0
- everysk/core/string.py +199 -0
- everysk/core/tests.py +240 -0
- everysk/core/threads.py +199 -0
- everysk/core/undefined.py +70 -0
- everysk/core/unittests.py +73 -0
- everysk/core/workers.py +241 -0
- everysk/sdk/__init__.py +23 -0
- everysk/sdk/base.py +98 -0
- everysk/sdk/brutils/cnpj.py +391 -0
- everysk/sdk/brutils/cnpj_pd.py +129 -0
- everysk/sdk/engines/__init__.py +26 -0
- everysk/sdk/engines/cache.py +185 -0
- everysk/sdk/engines/compliance.py +37 -0
- everysk/sdk/engines/cryptography.py +69 -0
- everysk/sdk/engines/expression.cp312-win_amd64.pyd +0 -0
- everysk/sdk/engines/expression.pyi +55 -0
- everysk/sdk/engines/helpers.cp312-win_amd64.pyd +0 -0
- everysk/sdk/engines/helpers.pyi +26 -0
- everysk/sdk/engines/lock.py +120 -0
- everysk/sdk/engines/market_data.py +244 -0
- everysk/sdk/engines/settings.py +19 -0
- everysk/sdk/entities/__init__.py +23 -0
- everysk/sdk/entities/base.py +784 -0
- everysk/sdk/entities/base_list.py +131 -0
- everysk/sdk/entities/custom_index/base.py +209 -0
- everysk/sdk/entities/custom_index/settings.py +29 -0
- everysk/sdk/entities/datastore/base.py +160 -0
- everysk/sdk/entities/datastore/settings.py +17 -0
- everysk/sdk/entities/fields.py +375 -0
- everysk/sdk/entities/file/base.py +215 -0
- everysk/sdk/entities/file/settings.py +63 -0
- everysk/sdk/entities/portfolio/base.py +248 -0
- everysk/sdk/entities/portfolio/securities.py +241 -0
- everysk/sdk/entities/portfolio/security.py +580 -0
- everysk/sdk/entities/portfolio/settings.py +97 -0
- everysk/sdk/entities/private_security/base.py +226 -0
- everysk/sdk/entities/private_security/settings.py +17 -0
- everysk/sdk/entities/query.py +603 -0
- everysk/sdk/entities/report/base.py +214 -0
- everysk/sdk/entities/report/settings.py +23 -0
- everysk/sdk/entities/script.py +310 -0
- everysk/sdk/entities/secrets/base.py +128 -0
- everysk/sdk/entities/secrets/script.py +119 -0
- everysk/sdk/entities/secrets/settings.py +17 -0
- everysk/sdk/entities/settings.py +48 -0
- everysk/sdk/entities/tags.py +174 -0
- everysk/sdk/entities/worker_execution/base.py +307 -0
- everysk/sdk/entities/worker_execution/settings.py +63 -0
- everysk/sdk/entities/workflow_execution/base.py +113 -0
- everysk/sdk/entities/workflow_execution/settings.py +32 -0
- everysk/sdk/entities/workspace/base.py +99 -0
- everysk/sdk/entities/workspace/settings.py +27 -0
- everysk/sdk/settings.py +67 -0
- everysk/sdk/tests.py +105 -0
- everysk/sdk/worker_base.py +47 -0
- everysk/server/__init__.py +9 -0
- everysk/server/applications.py +63 -0
- everysk/server/endpoints.py +516 -0
- everysk/server/example_api.py +69 -0
- everysk/server/middlewares.py +80 -0
- everysk/server/requests.py +62 -0
- everysk/server/responses.py +119 -0
- everysk/server/routing.py +64 -0
- everysk/server/settings.py +36 -0
- everysk/server/tests.py +36 -0
- everysk/settings.py +98 -0
- everysk/sql/__init__.py +9 -0
- everysk/sql/connection.py +232 -0
- everysk/sql/model.py +376 -0
- everysk/sql/query.py +417 -0
- everysk/sql/row_factory.py +63 -0
- everysk/sql/settings.py +49 -0
- everysk/sql/utils.py +129 -0
- everysk/tests.py +23 -0
- everysk/utils.py +81 -0
- everysk/version.py +15 -0
- everysk_lib-1.10.2.dist-info/.gitignore +5 -0
- everysk_lib-1.10.2.dist-info/METADATA +326 -0
- everysk_lib-1.10.2.dist-info/RECORD +137 -0
- everysk_lib-1.10.2.dist-info/WHEEL +5 -0
- everysk_lib-1.10.2.dist-info/licenses/LICENSE.txt +9 -0
- everysk_lib-1.10.2.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# (C) Copyright 2025 EVERYSK TECHNOLOGIES
|
|
4
|
+
#
|
|
5
|
+
# This is an unpublished work containing confidential and proprietary
|
|
6
|
+
# information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
|
|
7
|
+
# without authorization of EVERYSK TECHNOLOGIES is prohibited.
|
|
8
|
+
#
|
|
9
|
+
###############################################################################
|
|
10
|
+
__all__ = ['BaseEndpoint', 'JSONEndpoint']
|
|
11
|
+
|
|
12
|
+
from collections.abc import Generator
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
from starlette.exceptions import HTTPException
|
|
17
|
+
from starlette.types import Receive, Scope, Send
|
|
18
|
+
|
|
19
|
+
from everysk.config import settings
|
|
20
|
+
from everysk.core.exceptions import HttpError
|
|
21
|
+
from everysk.core.log import Logger, LoggerManager, _get_trace_data
|
|
22
|
+
from everysk.core.object import BaseObject
|
|
23
|
+
from everysk.core.serialize import loads
|
|
24
|
+
from everysk.server.requests import JSONRequest, Request
|
|
25
|
+
from everysk.server.responses import DumpsParams, JSONResponse, Response
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
log = Logger(__name__)
|
|
32
|
+
HTTP_METHODS = ('GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')
|
|
33
|
+
HTTP_METHODS_WITH_PAYLOAD = ('POST', 'PUT', 'PATCH')
|
|
34
|
+
HTTP_STATUS_CODES_LOG = settings.EVERYSK_SERVER_CODES_LOG
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
###############################################################################
|
|
38
|
+
# BaseEndpoint Class Implementation
|
|
39
|
+
###############################################################################
|
|
40
|
+
class BaseEndpoint:
|
|
41
|
+
# Based in starlette.endpoints.HTTPEndpoint
|
|
42
|
+
## Private attributes
|
|
43
|
+
_allowed_methods: list[str] = None
|
|
44
|
+
_request_class: Request = Request
|
|
45
|
+
_response_class: Response = Response
|
|
46
|
+
|
|
47
|
+
## Public attributes
|
|
48
|
+
receive: Receive = None
|
|
49
|
+
request: Request = None
|
|
50
|
+
scope: Scope = None
|
|
51
|
+
send: Send = None
|
|
52
|
+
|
|
53
|
+
## Internal methods
|
|
54
|
+
def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Base class for all endpoints in the application.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
scope (Scope): ASGI scope dictionary.
|
|
60
|
+
receive (Receive): ASGI receive data.
|
|
61
|
+
send (Send): ASGI send data.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
HttpError: 500 - Request is not an HTTP request.
|
|
65
|
+
"""
|
|
66
|
+
type_request = scope.get('type', '')
|
|
67
|
+
if type_request.lower() != 'http':
|
|
68
|
+
raise HttpError(status_code=500, msg='Request is not an HTTP request.')
|
|
69
|
+
|
|
70
|
+
self._allowed_methods = [method for method in HTTP_METHODS if hasattr(self, method.lower())]
|
|
71
|
+
self.receive = receive
|
|
72
|
+
self.request = self._request_class(scope, receive=receive)
|
|
73
|
+
self.scope = scope
|
|
74
|
+
self.send = send
|
|
75
|
+
|
|
76
|
+
def __await__(self) -> Generator[Any, None, None]:
|
|
77
|
+
"""
|
|
78
|
+
Method to allow the use of the await keyword in the class.
|
|
79
|
+
This method will call the dispatch method and return the result.
|
|
80
|
+
It's the default behavior of the Starlette HTTPEndpoint class.
|
|
81
|
+
Don't change this method.
|
|
82
|
+
"""
|
|
83
|
+
return self.dispatch().__await__()
|
|
84
|
+
|
|
85
|
+
## Private methods
|
|
86
|
+
def _make_response(self, content: Any = None, status_code: int = 200) -> Response | JSONResponse:
|
|
87
|
+
"""
|
|
88
|
+
Create a response object with the given content and status code.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
content (Any, optional): The content to include in the response. Defaults to None.
|
|
92
|
+
status_code (int, optional): The HTTP status code for the response. Defaults to 200.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Response | JSONResponse: The response object.
|
|
96
|
+
"""
|
|
97
|
+
# Create the response object with the content and status code.
|
|
98
|
+
return self._response_class(content=content, status_code=status_code)
|
|
99
|
+
|
|
100
|
+
def _has_error_handlers(self) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Check if the application has error handlers.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
bool: True if error handlers are defined, False otherwise.
|
|
106
|
+
"""
|
|
107
|
+
# https://stackoverflow.com/a/71298949
|
|
108
|
+
# Inside the app we have the exception_handlers attribute that is a dictionary
|
|
109
|
+
# and it contains the error handlers that are customized by the user.
|
|
110
|
+
# Inside the scope we have the starlette.exception_handlers that are the default handlers
|
|
111
|
+
# We could not predict when the error will be raised so we need to check every attribute
|
|
112
|
+
request = getattr(self, 'request', None)
|
|
113
|
+
if request is None:
|
|
114
|
+
# If the request is not set, we assume there are no error handlers
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
app = getattr(request, 'app', None)
|
|
118
|
+
if app is None:
|
|
119
|
+
# If the app is not set, we assume there are no error handlers
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
exception_handlers = getattr(app, 'exception_handlers', None)
|
|
123
|
+
return bool(exception_handlers)
|
|
124
|
+
|
|
125
|
+
async def _log_error(self, error: Exception) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Log the error if it is an internal server error (500) or any other status code defined in HTTP_STATUS_CODES_LOG.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
error (Exception): The error to log.
|
|
131
|
+
"""
|
|
132
|
+
# We only log internal server errors in GCP
|
|
133
|
+
if getattr(error, 'status_code', 500) in HTTP_STATUS_CODES_LOG:
|
|
134
|
+
# Only these methods can have a payload
|
|
135
|
+
if self.request.method in HTTP_METHODS_WITH_PAYLOAD:
|
|
136
|
+
payload = await self.get_http_payload()
|
|
137
|
+
else:
|
|
138
|
+
payload = {}
|
|
139
|
+
|
|
140
|
+
# Headers are already in the LoggerManager
|
|
141
|
+
msg = str(error)
|
|
142
|
+
extra = {'http_payload': payload}
|
|
143
|
+
if len(msg) > settings.EVERYSK_SERVER_HTTP_ERROR_MESSAGE_SIZE:
|
|
144
|
+
extra['labels'] = {'error': msg}
|
|
145
|
+
msg = msg[: settings.EVERYSK_SERVER_HTTP_ERROR_MESSAGE_SIZE]
|
|
146
|
+
|
|
147
|
+
log.error(msg, extra=extra)
|
|
148
|
+
|
|
149
|
+
## Public sync methods
|
|
150
|
+
def get_http_headers(self) -> dict[str, str]:
|
|
151
|
+
"""
|
|
152
|
+
Get the HTTP headers from the request.
|
|
153
|
+
Returns dictionary were the key is the header name in lower case and the value is the header value.
|
|
154
|
+
"""
|
|
155
|
+
return dict(self.request.headers)
|
|
156
|
+
|
|
157
|
+
def get_http_method_function(self) -> callable:
|
|
158
|
+
"""
|
|
159
|
+
Get the function that for the http method of the request.
|
|
160
|
+
If the function doesn't exist, it will return the method_not_allowed function.
|
|
161
|
+
"""
|
|
162
|
+
name = self.get_http_method_name()
|
|
163
|
+
# Check if the method is allowed, we create a list of allowed methods in the __init__ method
|
|
164
|
+
if name.upper() not in self._allowed_methods:
|
|
165
|
+
return self.method_not_allowed
|
|
166
|
+
|
|
167
|
+
return getattr(self, name)
|
|
168
|
+
|
|
169
|
+
def get_http_method_name(self) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Get the name of the HTTP method from the request.
|
|
172
|
+
If the request method is HEAD and the class doesn't
|
|
173
|
+
have a head method, it will return get instead.
|
|
174
|
+
"""
|
|
175
|
+
if self.request.method == 'HEAD' and not hasattr(self, 'head'):
|
|
176
|
+
name = 'get'
|
|
177
|
+
else:
|
|
178
|
+
name = self.request.method.lower()
|
|
179
|
+
|
|
180
|
+
return name
|
|
181
|
+
|
|
182
|
+
## Public async methods
|
|
183
|
+
async def dispatch(self) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Main method that will always be executed for each request, takes
|
|
186
|
+
the function related to the HTTP method of the request and executes it.
|
|
187
|
+
"""
|
|
188
|
+
# Because the ASGI protocol copy the context to the event loop
|
|
189
|
+
# for every request, we create an empty LoggerManager to avoid
|
|
190
|
+
# shared values between requests.
|
|
191
|
+
with LoggerManager(http_headers={}, http_payload={}, labels={}, stacklevel=None, traceback=''):
|
|
192
|
+
headers = self.get_http_headers()
|
|
193
|
+
# Insert the headers in the Logger Context to propagate them to the logs
|
|
194
|
+
with LoggerManager(http_headers=headers):
|
|
195
|
+
try:
|
|
196
|
+
response = await self.get_http_response()
|
|
197
|
+
except Exception as error: # pylint: disable=broad-except
|
|
198
|
+
# If something goes wrong, we catch the exception and return a response
|
|
199
|
+
response = await self.get_http_exception_response(error)
|
|
200
|
+
|
|
201
|
+
# Log the error if it is an internal server error
|
|
202
|
+
await self._log_error(error)
|
|
203
|
+
|
|
204
|
+
# If we have error handlers, we raise the error again to be handled by them
|
|
205
|
+
# and all errors must be raised as HTTPException
|
|
206
|
+
if self._has_error_handlers():
|
|
207
|
+
# To be compatible with the starlette error handlers and catch
|
|
208
|
+
# the correct status code, we need to raise an HTTPException
|
|
209
|
+
if isinstance(error, HttpError):
|
|
210
|
+
raise HTTPException(status_code=error.status_code, detail=error.msg) from error
|
|
211
|
+
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
await response(self.scope, self.receive, self.send)
|
|
215
|
+
|
|
216
|
+
# To avoid shared values between requests, we reset the LoggerManager
|
|
217
|
+
LoggerManager.reset()
|
|
218
|
+
|
|
219
|
+
async def get_http_exception_response(self, error: Exception) -> Response:
|
|
220
|
+
"""
|
|
221
|
+
Method to return a response when an exception is raised during the request.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
error (Exception): The exception raised during the request.
|
|
225
|
+
"""
|
|
226
|
+
status_code = getattr(error, 'status_code', 500)
|
|
227
|
+
return self._make_response(content=str(error), status_code=status_code)
|
|
228
|
+
|
|
229
|
+
async def get_http_payload(self) -> bytes:
|
|
230
|
+
"""
|
|
231
|
+
Get the HTTP payload from the request.
|
|
232
|
+
The payload is the body of the request and it's a bytes object.
|
|
233
|
+
"""
|
|
234
|
+
return await self.request.body()
|
|
235
|
+
|
|
236
|
+
async def get_http_response(self) -> Response:
|
|
237
|
+
"""
|
|
238
|
+
Get the correct function for the HTTP method of the request
|
|
239
|
+
and execute it to create a response.
|
|
240
|
+
If the method doesn't exist, it will return a 405 response.
|
|
241
|
+
"""
|
|
242
|
+
method_function = self.get_http_method_function()
|
|
243
|
+
response = await method_function()
|
|
244
|
+
|
|
245
|
+
if not isinstance(response, Response):
|
|
246
|
+
response = self._make_response(content=response)
|
|
247
|
+
|
|
248
|
+
return response
|
|
249
|
+
|
|
250
|
+
async def method_not_allowed(self) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Default method for when the HTTP method is not found in the class.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
HttpError: 405 - Method not allowed
|
|
256
|
+
"""
|
|
257
|
+
raise HttpError(status_code=405, msg=f'Method {self.request.method} not allowed.')
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
###############################################################################
|
|
261
|
+
# JSONEndpoint Class Implementation
|
|
262
|
+
###############################################################################
|
|
263
|
+
class LoadsParams(BaseObject):
|
|
264
|
+
date_format: str | None = None
|
|
265
|
+
datetime_format: str | None = None
|
|
266
|
+
instantiate_object: bool = True
|
|
267
|
+
protocol: Literal['json', 'orjson'] = 'json'
|
|
268
|
+
use_undefined: bool = True
|
|
269
|
+
|
|
270
|
+
def __init__(
|
|
271
|
+
self,
|
|
272
|
+
*,
|
|
273
|
+
date_format: str | None = None,
|
|
274
|
+
datetime_format: str | None = None,
|
|
275
|
+
instantiate_object: bool = True,
|
|
276
|
+
protocol: Literal['json', 'orjson'] = 'json',
|
|
277
|
+
use_undefined: bool = True,
|
|
278
|
+
**kwargs: Any,
|
|
279
|
+
) -> None:
|
|
280
|
+
super().__init__(
|
|
281
|
+
date_format=date_format,
|
|
282
|
+
datetime_format=datetime_format,
|
|
283
|
+
instantiate_object=instantiate_object,
|
|
284
|
+
protocol=protocol,
|
|
285
|
+
use_undefined=use_undefined,
|
|
286
|
+
**kwargs,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def to_dict(self) -> dict:
|
|
290
|
+
dct = super().to_dict(add_class_path=True, recursion=True)
|
|
291
|
+
# we remove all private keys because we don't want them in the serialized output
|
|
292
|
+
return {key: value for key, value in dct.items() if not key.startswith('_')}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class JSONEndpoint(BaseEndpoint):
|
|
296
|
+
## Private attributes
|
|
297
|
+
_request_class: JSONRequest = JSONRequest
|
|
298
|
+
_response_class: JSONResponse = JSONResponse
|
|
299
|
+
_serialize_dumps_params: DumpsParams = DumpsParams()
|
|
300
|
+
_serialize_loads_params: LoadsParams = LoadsParams()
|
|
301
|
+
|
|
302
|
+
## Public attributes
|
|
303
|
+
rest_key_name: str = Undefined
|
|
304
|
+
rest_key_value: str = Undefined
|
|
305
|
+
|
|
306
|
+
def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Class to handle JSON requests and responses.
|
|
309
|
+
Inherit from this class and implement the HTTP methods to create an endpoint.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
scope (Scope): ASGI scope dictionary.
|
|
313
|
+
receive (Receive): ASGI receive data.
|
|
314
|
+
send (Send): ASGI send data.
|
|
315
|
+
"""
|
|
316
|
+
super().__init__(scope, receive, send)
|
|
317
|
+
|
|
318
|
+
if self.rest_key_name is Undefined:
|
|
319
|
+
self.rest_key_name = settings.EVERYSK_SERVER_REST_KEY_NAME
|
|
320
|
+
|
|
321
|
+
if self.rest_key_value is Undefined:
|
|
322
|
+
self.rest_key_value = settings.EVERYSK_SERVER_REST_KEY_VALUE
|
|
323
|
+
|
|
324
|
+
def check_rest_key(self) -> bool:
|
|
325
|
+
"""
|
|
326
|
+
Check if the rest key is present in the request headers and if it's the correct value.
|
|
327
|
+
If the rest key name or value is not set, it will always return True.
|
|
328
|
+
"""
|
|
329
|
+
if not self.rest_key_name or not self.rest_key_value:
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
rest_key_value = self.request.headers.get(self.rest_key_name)
|
|
333
|
+
return rest_key_value == self.rest_key_value
|
|
334
|
+
|
|
335
|
+
def _make_response(self, content: Any = None, status_code: int = 200) -> JSONResponse:
|
|
336
|
+
"""
|
|
337
|
+
Create a JSONResponse object with the content and status code.
|
|
338
|
+
The content is serialized to JSON using the specified serializer.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
content (Any): The content to be serialized and returned in the response.
|
|
342
|
+
status_code (int): The HTTP status code for the response.
|
|
343
|
+
"""
|
|
344
|
+
return self._response_class(
|
|
345
|
+
content=content, status_code=status_code, serialize_dumps_params=self._serialize_dumps_params
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
async def get_http_exception_response(self, error: Exception) -> JSONResponse:
|
|
349
|
+
"""
|
|
350
|
+
Method to return a JSONResponse when an exception is raised during the request.
|
|
351
|
+
The trace_id is added to the response to help with debugging.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
error (Exception): The exception raised during the request.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
JSONResponse: A JSONResponse with the error message, status code and trace_id.
|
|
358
|
+
"""
|
|
359
|
+
trace_data = _get_trace_data(headers=self.get_http_headers())
|
|
360
|
+
msg = str(error)
|
|
361
|
+
status_code = getattr(error, 'status_code', 500)
|
|
362
|
+
return self._make_response(
|
|
363
|
+
content={'error': msg, 'code': status_code, 'trace_id': trace_data['trace_id']}, status_code=status_code
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
async def get_http_payload(self) -> Any:
|
|
367
|
+
"""
|
|
368
|
+
Get the HTTP payload from the request and deserialize it to a
|
|
369
|
+
Python object or raises a HttpError if the body is empty and is
|
|
370
|
+
not an instance of string or bytes.
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
HttpError: When we have an empty json payload.
|
|
374
|
+
"""
|
|
375
|
+
body = await super().get_http_payload()
|
|
376
|
+
|
|
377
|
+
if body and isinstance(body, (bytes, str)):
|
|
378
|
+
return loads(body, **self._serialize_loads_params.to_dict())
|
|
379
|
+
|
|
380
|
+
raise HttpError(status_code=400, msg='Invalid Payload')
|
|
381
|
+
|
|
382
|
+
async def get_http_response(self) -> JSONResponse:
|
|
383
|
+
"""
|
|
384
|
+
Changes the return of the get_http_response method to return a JSONResponse.
|
|
385
|
+
If the response is not a Response object, it will be converted to a JSONResponse
|
|
386
|
+
otherwise it will be returned as is.
|
|
387
|
+
If the rest key is incorrect, it will raise a 401 error.
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
HttpError: 401 - Unauthorized access to this resource.
|
|
391
|
+
"""
|
|
392
|
+
if not self.check_rest_key():
|
|
393
|
+
raise HttpError(status_code=401, msg='Unauthorized access to this resource.')
|
|
394
|
+
|
|
395
|
+
return await super().get_http_response()
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
###############################################################################
|
|
399
|
+
# HealthCheckEndpoint Class Implementation
|
|
400
|
+
###############################################################################
|
|
401
|
+
class HealthCheckEndpoint(JSONEndpoint):
|
|
402
|
+
"""
|
|
403
|
+
Endpoint to check if the service is running.
|
|
404
|
+
By default, it will return a JSONResponse with the status 'SENTA_A_PUA'.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
default_response: dict = {'status': 'SENTA_A_PUA'} # noqa: RUF012
|
|
408
|
+
# These are set to None so the endpoint can be accessed without the rest key
|
|
409
|
+
rest_key_name: str = None
|
|
410
|
+
rest_key_value: str = None
|
|
411
|
+
|
|
412
|
+
async def get(self) -> JSONResponse:
|
|
413
|
+
return JSONResponse(self.default_response)
|
|
414
|
+
|
|
415
|
+
async def post(self) -> JSONResponse:
|
|
416
|
+
return JSONResponse(self.default_response)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
###############################################################################
|
|
420
|
+
# RedirectEndpoint Class Implementation
|
|
421
|
+
###############################################################################
|
|
422
|
+
## WARNING:
|
|
423
|
+
# httpx imports are placed inside the functions to load this module only for this class
|
|
424
|
+
class RedirectEndpoint(BaseEndpoint):
|
|
425
|
+
"""
|
|
426
|
+
Endpoint to redirect requests to another host.
|
|
427
|
+
We use this endpoint to redirect requests to another host
|
|
428
|
+
and return the response to the client, acting as a proxy.
|
|
429
|
+
|
|
430
|
+
Raises:
|
|
431
|
+
ValueError: If the host_url is not set in the class.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
host_url: str = settings.EVERYSK_SERVER_REDIRECT_URL
|
|
435
|
+
timeout: int = 600
|
|
436
|
+
|
|
437
|
+
def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
438
|
+
super().__init__(scope, receive, send)
|
|
439
|
+
if not self.host_url:
|
|
440
|
+
raise ValueError('host_url is required for redirect endpoint.')
|
|
441
|
+
|
|
442
|
+
def _get_client(self) -> 'httpx.Client':
|
|
443
|
+
from httpx import Client # noqa: PLC0415
|
|
444
|
+
|
|
445
|
+
return Client(headers=self.get_request_headers())
|
|
446
|
+
|
|
447
|
+
def get_full_url(self) -> str:
|
|
448
|
+
"""
|
|
449
|
+
Get the full URL to be used in the connection.
|
|
450
|
+
This method will return the URL with the host, path and query string.
|
|
451
|
+
"""
|
|
452
|
+
url = self.request.url
|
|
453
|
+
result = f'{self.host_url}{url.path}'
|
|
454
|
+
if url.query:
|
|
455
|
+
result = f'{result}?{url.query}'
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
def get_host(self) -> str:
|
|
459
|
+
"""Get the host from the host_url attribute."""
|
|
460
|
+
url = urlparse(self.host_url)
|
|
461
|
+
return url.netloc
|
|
462
|
+
|
|
463
|
+
def get_request_headers(self) -> dict:
|
|
464
|
+
"""Get the headers received in the request and update the Host header with the destination host."""
|
|
465
|
+
headers = dict(self.request.headers)
|
|
466
|
+
|
|
467
|
+
# We need to update the Host header with the destination host
|
|
468
|
+
headers['host'] = self.get_host()
|
|
469
|
+
|
|
470
|
+
return headers
|
|
471
|
+
|
|
472
|
+
def get_response_headers(self, response: 'httpx.Response') -> dict:
|
|
473
|
+
"""
|
|
474
|
+
Get the headers from the redirected response and keep only the content_type to be used in the response.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
response (httpx.Response): The response from the redirect request.
|
|
478
|
+
"""
|
|
479
|
+
return {'content-type': response.headers.get('content-type')}
|
|
480
|
+
|
|
481
|
+
def get_timeout(self) -> 'httpx.Timeout':
|
|
482
|
+
"""Return the timeout to be used in the connection."""
|
|
483
|
+
from httpx import Timeout # noqa: PLC0415
|
|
484
|
+
|
|
485
|
+
return Timeout(
|
|
486
|
+
timeout=30, # Default timeout for all operations
|
|
487
|
+
read=self.timeout, # Timeout for reading the response
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def make_response(self, response: 'httpx.Response') -> Response | JSONResponse:
|
|
491
|
+
"""
|
|
492
|
+
Create a Response or JSONResponse object based on the content type of the response.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
response (httpx.Response): The response from the redirect request.
|
|
496
|
+
"""
|
|
497
|
+
content_type = response.headers.get('content-type')
|
|
498
|
+
cls = Response if 'json' in content_type else JSONResponse
|
|
499
|
+
headers = self.get_response_headers(response=response)
|
|
500
|
+
|
|
501
|
+
return cls(status_code=response.status_code, content=response.content, headers=headers)
|
|
502
|
+
|
|
503
|
+
async def get(self) -> Response | JSONResponse:
|
|
504
|
+
"""HTTP GET method to redirect the request to another host."""
|
|
505
|
+
with self._get_client() as connection:
|
|
506
|
+
response = connection.get(self.get_full_url(), timeout=self.get_timeout())
|
|
507
|
+
|
|
508
|
+
return self.make_response(response=response)
|
|
509
|
+
|
|
510
|
+
async def post(self) -> Response | JSONResponse:
|
|
511
|
+
"""HTTP POST method to redirect the request to another host."""
|
|
512
|
+
body = await self.request.body()
|
|
513
|
+
with self._get_client() as connection:
|
|
514
|
+
response = connection.post(self.get_full_url(), content=body, timeout=self.get_timeout())
|
|
515
|
+
|
|
516
|
+
return self.make_response(response=response)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# (C) Copyright 2025 EVERYSK TECHNOLOGIES
|
|
4
|
+
#
|
|
5
|
+
# This is an unpublished work containing confidential and proprietary
|
|
6
|
+
# information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
|
|
7
|
+
# without authorization of EVERYSK TECHNOLOGIES is prohibited.
|
|
8
|
+
#
|
|
9
|
+
###############################################################################
|
|
10
|
+
from everysk.server.applications import create_application
|
|
11
|
+
from everysk.server.endpoints import JSONEndpoint
|
|
12
|
+
from everysk.server.routing import Route, RouteLazy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
###############################################################################
|
|
16
|
+
# TestPublicEndpoint Class Implementation
|
|
17
|
+
###############################################################################
|
|
18
|
+
class TestPublicEndpoint(JSONEndpoint):
|
|
19
|
+
rest_key_name: str = None
|
|
20
|
+
rest_key_value: str = None
|
|
21
|
+
|
|
22
|
+
async def get(self):
|
|
23
|
+
return {'message': 'Hello, World!'}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
###############################################################################
|
|
27
|
+
# TestPrivateEndpoint Class Implementation
|
|
28
|
+
###############################################################################
|
|
29
|
+
class TestPrivateEndpoint(JSONEndpoint):
|
|
30
|
+
rest_key_name: str = 'X-Api-Key'
|
|
31
|
+
rest_key_value: str = '123456'
|
|
32
|
+
|
|
33
|
+
async def get(self):
|
|
34
|
+
return {'message': 'Hello, World Private!'}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
###############################################################################
|
|
38
|
+
# Application Implementation
|
|
39
|
+
###############################################################################
|
|
40
|
+
routes = [
|
|
41
|
+
RouteLazy(path='/', endpoint='everysk.server.example_api.TestPublicEndpoint'),
|
|
42
|
+
Route(path='/private', endpoint=TestPrivateEndpoint)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
app = create_application(routes=routes)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
###############################################################################
|
|
49
|
+
# How to run this example
|
|
50
|
+
###############################################################################
|
|
51
|
+
## To run this example, execute the following command:
|
|
52
|
+
# ./run.sh starlette everysk.server.example_api:app
|
|
53
|
+
|
|
54
|
+
## To test using requests:
|
|
55
|
+
# ./run.sh shell
|
|
56
|
+
#
|
|
57
|
+
# In [1]: import requests
|
|
58
|
+
#
|
|
59
|
+
# In [2]: requests.get('http://127.0.0.1:8000')
|
|
60
|
+
# Out[2]: <Response [200]>
|
|
61
|
+
#
|
|
62
|
+
# In [3]: requests.get('http://127.0.0.1:8000').content
|
|
63
|
+
# Out[3]: b'{"message":"Hello, World!"}'
|
|
64
|
+
#
|
|
65
|
+
# In [4]: requests.get('http://127.0.0.1:8000/private').content
|
|
66
|
+
# Out[4]: b'{"error":"401 -> Unauthorized access to this resource.","code":401,"trace_id":""}'
|
|
67
|
+
#
|
|
68
|
+
# In [5]: requests.get('http://127.0.0.1:8000/private', headers={'X-Api-Key': '123456'}).content
|
|
69
|
+
# Out[5]: b'{"message":"Hello, World Private!"}'
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# (C) Copyright 2025 EVERYSK TECHNOLOGIES
|
|
4
|
+
#
|
|
5
|
+
# This is an unpublished work containing confidential and proprietary
|
|
6
|
+
# information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
|
|
7
|
+
# without authorization of EVERYSK TECHNOLOGIES is prohibited.
|
|
8
|
+
#
|
|
9
|
+
###############################################################################
|
|
10
|
+
__all__ = ['Middleware', 'GZipMiddleware', 'SecurityHeadersMiddleware']
|
|
11
|
+
|
|
12
|
+
from starlette.middleware import Middleware
|
|
13
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
14
|
+
from starlette.middleware.gzip import GZipMiddleware
|
|
15
|
+
|
|
16
|
+
from everysk.config import settings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
GZIP_MINIMUM_SIZE = settings.EVERYSK_SERVER_GZIP_MINIMUM_SIZE
|
|
20
|
+
GZIP_COMPRESS_LEVEL = settings.EVERYSK_SERVER_GZIP_COMPRESS_LEVEL
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
###############################################################################
|
|
24
|
+
# Public Functions Implementation
|
|
25
|
+
###############################################################################
|
|
26
|
+
def update_with_default_middlewares(middlewares: list[Middleware]) -> list[Middleware]:
|
|
27
|
+
"""
|
|
28
|
+
Update the given middleware list with the default ones.
|
|
29
|
+
The default middleware are the GZipMiddleware and SecurityHeadersMiddleware.
|
|
30
|
+
"""
|
|
31
|
+
if middlewares is None:
|
|
32
|
+
middlewares = []
|
|
33
|
+
|
|
34
|
+
if settings.EVERYSK_SERVER_GZIP_MIDDLEWARE_ENABLED:
|
|
35
|
+
middlewares.insert(0, Middleware(GZipMiddleware, minimum_size=GZIP_MINIMUM_SIZE, compresslevel=GZIP_COMPRESS_LEVEL))
|
|
36
|
+
|
|
37
|
+
if settings.EVERYSK_SERVER_SECURITY_MIDDLEWARE_ENABLED:
|
|
38
|
+
middlewares.append(Middleware(SecurityHeadersMiddleware))
|
|
39
|
+
|
|
40
|
+
return middlewares
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
###############################################################################
|
|
44
|
+
# SecurityHeadersMiddleware Class Implementation
|
|
45
|
+
###############################################################################
|
|
46
|
+
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
47
|
+
"""
|
|
48
|
+
Middleware to add security headers to the response.
|
|
49
|
+
These headers are used to protect the application against
|
|
50
|
+
some types of attacks and will be added to every response.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
async def dispatch(self, request, call_next):
|
|
54
|
+
response = await call_next(request)
|
|
55
|
+
|
|
56
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
|
|
57
|
+
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
|
58
|
+
|
|
59
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
|
|
60
|
+
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
61
|
+
|
|
62
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
|
|
63
|
+
response.headers['X-DNS-Prefetch-Control'] = 'off'
|
|
64
|
+
|
|
65
|
+
# https://webtechsurvey.com/response-header/x-download-options
|
|
66
|
+
# Only works on IE8
|
|
67
|
+
response.headers['X-Download-Options'] = 'noopen'
|
|
68
|
+
|
|
69
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
|
|
70
|
+
response.headers['X-Frame-Options'] = 'DENY'
|
|
71
|
+
|
|
72
|
+
# https://webtechsurvey.com/response-header/x-permitted-cross-domain-policies
|
|
73
|
+
response.headers['X-Permitted-Cross-Domain-Policies'] = 'none'
|
|
74
|
+
|
|
75
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
|
|
76
|
+
# Warning: Even though this feature can protect users of older web browsers that don't yet support CSP, in some cases,
|
|
77
|
+
# XSS protection can create XSS vulnerabilities in otherwise safe websites. See the section below for more information.
|
|
78
|
+
# response.headers['X-XSS-Protection'] = '1; mode=block'
|
|
79
|
+
|
|
80
|
+
return response
|