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.
Files changed (137) hide show
  1. everysk/__init__.py +30 -0
  2. everysk/_version.py +683 -0
  3. everysk/api/__init__.py +61 -0
  4. everysk/api/api_requestor.py +167 -0
  5. everysk/api/api_resources/__init__.py +23 -0
  6. everysk/api/api_resources/api_resource.py +371 -0
  7. everysk/api/api_resources/calculation.py +779 -0
  8. everysk/api/api_resources/custom_index.py +42 -0
  9. everysk/api/api_resources/datastore.py +81 -0
  10. everysk/api/api_resources/file.py +42 -0
  11. everysk/api/api_resources/market_data.py +223 -0
  12. everysk/api/api_resources/parser.py +66 -0
  13. everysk/api/api_resources/portfolio.py +43 -0
  14. everysk/api/api_resources/private_security.py +42 -0
  15. everysk/api/api_resources/report.py +65 -0
  16. everysk/api/api_resources/report_template.py +39 -0
  17. everysk/api/api_resources/tests.py +115 -0
  18. everysk/api/api_resources/worker_execution.py +64 -0
  19. everysk/api/api_resources/workflow.py +65 -0
  20. everysk/api/api_resources/workflow_execution.py +93 -0
  21. everysk/api/api_resources/workspace.py +42 -0
  22. everysk/api/http_client.py +63 -0
  23. everysk/api/tests.py +32 -0
  24. everysk/api/utils.py +262 -0
  25. everysk/config.py +451 -0
  26. everysk/core/_tests/serialize/test_json.py +336 -0
  27. everysk/core/_tests/serialize/test_orjson.py +295 -0
  28. everysk/core/_tests/serialize/test_pickle.py +48 -0
  29. everysk/core/cloud_function/main.py +78 -0
  30. everysk/core/cloud_function/tests.py +86 -0
  31. everysk/core/compress.py +245 -0
  32. everysk/core/datetime/__init__.py +12 -0
  33. everysk/core/datetime/calendar.py +144 -0
  34. everysk/core/datetime/date.py +424 -0
  35. everysk/core/datetime/date_expression.py +299 -0
  36. everysk/core/datetime/date_mixin.py +1475 -0
  37. everysk/core/datetime/date_settings.py +30 -0
  38. everysk/core/datetime/datetime.py +713 -0
  39. everysk/core/exceptions.py +435 -0
  40. everysk/core/fields.py +1176 -0
  41. everysk/core/firestore.py +555 -0
  42. everysk/core/fixtures/_settings.py +29 -0
  43. everysk/core/fixtures/other/_settings.py +18 -0
  44. everysk/core/fixtures/user_agents.json +88 -0
  45. everysk/core/http.py +691 -0
  46. everysk/core/lists.py +92 -0
  47. everysk/core/log.py +709 -0
  48. everysk/core/number.py +37 -0
  49. everysk/core/object.py +1469 -0
  50. everysk/core/redis.py +1021 -0
  51. everysk/core/retry.py +51 -0
  52. everysk/core/serialize.py +674 -0
  53. everysk/core/sftp.py +414 -0
  54. everysk/core/signing.py +53 -0
  55. everysk/core/slack.py +127 -0
  56. everysk/core/string.py +199 -0
  57. everysk/core/tests.py +240 -0
  58. everysk/core/threads.py +199 -0
  59. everysk/core/undefined.py +70 -0
  60. everysk/core/unittests.py +73 -0
  61. everysk/core/workers.py +241 -0
  62. everysk/sdk/__init__.py +23 -0
  63. everysk/sdk/base.py +98 -0
  64. everysk/sdk/brutils/cnpj.py +391 -0
  65. everysk/sdk/brutils/cnpj_pd.py +129 -0
  66. everysk/sdk/engines/__init__.py +26 -0
  67. everysk/sdk/engines/cache.py +185 -0
  68. everysk/sdk/engines/compliance.py +37 -0
  69. everysk/sdk/engines/cryptography.py +69 -0
  70. everysk/sdk/engines/expression.cp312-win_amd64.pyd +0 -0
  71. everysk/sdk/engines/expression.pyi +55 -0
  72. everysk/sdk/engines/helpers.cp312-win_amd64.pyd +0 -0
  73. everysk/sdk/engines/helpers.pyi +26 -0
  74. everysk/sdk/engines/lock.py +120 -0
  75. everysk/sdk/engines/market_data.py +244 -0
  76. everysk/sdk/engines/settings.py +19 -0
  77. everysk/sdk/entities/__init__.py +23 -0
  78. everysk/sdk/entities/base.py +784 -0
  79. everysk/sdk/entities/base_list.py +131 -0
  80. everysk/sdk/entities/custom_index/base.py +209 -0
  81. everysk/sdk/entities/custom_index/settings.py +29 -0
  82. everysk/sdk/entities/datastore/base.py +160 -0
  83. everysk/sdk/entities/datastore/settings.py +17 -0
  84. everysk/sdk/entities/fields.py +375 -0
  85. everysk/sdk/entities/file/base.py +215 -0
  86. everysk/sdk/entities/file/settings.py +63 -0
  87. everysk/sdk/entities/portfolio/base.py +248 -0
  88. everysk/sdk/entities/portfolio/securities.py +241 -0
  89. everysk/sdk/entities/portfolio/security.py +580 -0
  90. everysk/sdk/entities/portfolio/settings.py +97 -0
  91. everysk/sdk/entities/private_security/base.py +226 -0
  92. everysk/sdk/entities/private_security/settings.py +17 -0
  93. everysk/sdk/entities/query.py +603 -0
  94. everysk/sdk/entities/report/base.py +214 -0
  95. everysk/sdk/entities/report/settings.py +23 -0
  96. everysk/sdk/entities/script.py +310 -0
  97. everysk/sdk/entities/secrets/base.py +128 -0
  98. everysk/sdk/entities/secrets/script.py +119 -0
  99. everysk/sdk/entities/secrets/settings.py +17 -0
  100. everysk/sdk/entities/settings.py +48 -0
  101. everysk/sdk/entities/tags.py +174 -0
  102. everysk/sdk/entities/worker_execution/base.py +307 -0
  103. everysk/sdk/entities/worker_execution/settings.py +63 -0
  104. everysk/sdk/entities/workflow_execution/base.py +113 -0
  105. everysk/sdk/entities/workflow_execution/settings.py +32 -0
  106. everysk/sdk/entities/workspace/base.py +99 -0
  107. everysk/sdk/entities/workspace/settings.py +27 -0
  108. everysk/sdk/settings.py +67 -0
  109. everysk/sdk/tests.py +105 -0
  110. everysk/sdk/worker_base.py +47 -0
  111. everysk/server/__init__.py +9 -0
  112. everysk/server/applications.py +63 -0
  113. everysk/server/endpoints.py +516 -0
  114. everysk/server/example_api.py +69 -0
  115. everysk/server/middlewares.py +80 -0
  116. everysk/server/requests.py +62 -0
  117. everysk/server/responses.py +119 -0
  118. everysk/server/routing.py +64 -0
  119. everysk/server/settings.py +36 -0
  120. everysk/server/tests.py +36 -0
  121. everysk/settings.py +98 -0
  122. everysk/sql/__init__.py +9 -0
  123. everysk/sql/connection.py +232 -0
  124. everysk/sql/model.py +376 -0
  125. everysk/sql/query.py +417 -0
  126. everysk/sql/row_factory.py +63 -0
  127. everysk/sql/settings.py +49 -0
  128. everysk/sql/utils.py +129 -0
  129. everysk/tests.py +23 -0
  130. everysk/utils.py +81 -0
  131. everysk/version.py +15 -0
  132. everysk_lib-1.10.2.dist-info/.gitignore +5 -0
  133. everysk_lib-1.10.2.dist-info/METADATA +326 -0
  134. everysk_lib-1.10.2.dist-info/RECORD +137 -0
  135. everysk_lib-1.10.2.dist-info/WHEEL +5 -0
  136. everysk_lib-1.10.2.dist-info/licenses/LICENSE.txt +9 -0
  137. 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