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,62 @@
|
|
|
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__ = ['JSONRequest', 'Request']
|
|
11
|
+
|
|
12
|
+
from starlette.requests import Request as _Request
|
|
13
|
+
|
|
14
|
+
from everysk.core.compress import decompress
|
|
15
|
+
from everysk.core.log import Logger
|
|
16
|
+
from everysk.core.serialize import loads
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
log = Logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
###############################################################################
|
|
23
|
+
# Request Class Implementation
|
|
24
|
+
###############################################################################
|
|
25
|
+
class Request(_Request):
|
|
26
|
+
|
|
27
|
+
async def body(self) -> bytes:
|
|
28
|
+
"""
|
|
29
|
+
Method to return the body of the request.
|
|
30
|
+
If we receive a request with a Content-Encoding header, we decompress the body.
|
|
31
|
+
"""
|
|
32
|
+
# If self._body is set then body was already read.
|
|
33
|
+
if not hasattr(self, '_body'):
|
|
34
|
+
body = await super().body()
|
|
35
|
+
# We only decompress the body if it is not empty.
|
|
36
|
+
if body:
|
|
37
|
+
content_encoding = self.headers.get('Content-Encoding', '').lower()
|
|
38
|
+
if 'gzip' in content_encoding:
|
|
39
|
+
try:
|
|
40
|
+
body = decompress(body, protocol='gzip', serialize=None )
|
|
41
|
+
except Exception: # pylint: disable=broad-except
|
|
42
|
+
log.error('Error decompressing the request body.', extra={'labels': {'body': body}})
|
|
43
|
+
|
|
44
|
+
self._body = body # pylint: disable=attribute-defined-outside-init
|
|
45
|
+
return self._body
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
###############################################################################
|
|
49
|
+
# JSONRequest Class Implementation
|
|
50
|
+
###############################################################################
|
|
51
|
+
class JSONRequest(Request):
|
|
52
|
+
|
|
53
|
+
async def json(self):
|
|
54
|
+
"""
|
|
55
|
+
Method to return the JSON content of the request body.
|
|
56
|
+
It uses the loads function from the core.serialize module to parse the body.
|
|
57
|
+
"""
|
|
58
|
+
if not hasattr(self, '_json'):
|
|
59
|
+
body = await self.body()
|
|
60
|
+
self._json = loads(body, protocol='json', use_undefined=True) # pylint: disable=attribute-defined-outside-init
|
|
61
|
+
|
|
62
|
+
return self._json
|
|
@@ -0,0 +1,119 @@
|
|
|
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__ = [
|
|
11
|
+
'FileResponse',
|
|
12
|
+
'HTMLResponse',
|
|
13
|
+
'JSONResponse',
|
|
14
|
+
'PlainTextResponse',
|
|
15
|
+
'RedirectResponse',
|
|
16
|
+
'Response',
|
|
17
|
+
'StreamingResponse',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
from typing import Any, Literal
|
|
21
|
+
|
|
22
|
+
from starlette.background import BackgroundTask
|
|
23
|
+
from starlette.responses import (
|
|
24
|
+
FileResponse,
|
|
25
|
+
HTMLResponse,
|
|
26
|
+
PlainTextResponse,
|
|
27
|
+
RedirectResponse,
|
|
28
|
+
Response,
|
|
29
|
+
StreamingResponse,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from everysk.core.object import BaseObject
|
|
33
|
+
from everysk.core.serialize import dumps
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
###############################################################################
|
|
37
|
+
# JSONResponse Class Implementation
|
|
38
|
+
###############################################################################
|
|
39
|
+
class DumpsParams(BaseObject):
|
|
40
|
+
add_class_path: bool = True
|
|
41
|
+
date_format: str | None = None
|
|
42
|
+
datetime_format: str | None = None
|
|
43
|
+
indent: int | None = None
|
|
44
|
+
protocol: Literal['json', 'orjson'] = 'json'
|
|
45
|
+
return_type: Literal['bytes', 'str'] = 'bytes'
|
|
46
|
+
separators: tuple[str] = (',', ':')
|
|
47
|
+
sort_keys: bool = False
|
|
48
|
+
use_undefined: bool = True
|
|
49
|
+
decode_bytes: bool = True
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
add_class_path: bool = True,
|
|
54
|
+
date_format: str | None = None,
|
|
55
|
+
datetime_format: str | None = None,
|
|
56
|
+
indent: int | None = None,
|
|
57
|
+
protocol: Literal['json', 'orjson'] = 'json',
|
|
58
|
+
return_type: Literal['bytes', 'str'] = 'bytes',
|
|
59
|
+
separators: tuple[str] = (',', ':'),
|
|
60
|
+
sort_keys: bool = False,
|
|
61
|
+
use_undefined: bool = True,
|
|
62
|
+
decode_bytes: bool = True,
|
|
63
|
+
**kwargs,
|
|
64
|
+
):
|
|
65
|
+
super().__init__(
|
|
66
|
+
add_class_path=add_class_path,
|
|
67
|
+
date_format=date_format,
|
|
68
|
+
datetime_format=datetime_format,
|
|
69
|
+
indent=indent,
|
|
70
|
+
protocol=protocol,
|
|
71
|
+
return_type=return_type,
|
|
72
|
+
separators=separators,
|
|
73
|
+
sort_keys=sort_keys,
|
|
74
|
+
use_undefined=use_undefined,
|
|
75
|
+
decode_bytes=decode_bytes,
|
|
76
|
+
**kwargs,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def to_dict(self): # pylint: disable=arguments-differ
|
|
80
|
+
dct = super().to_dict(add_class_path=True, recursion=True)
|
|
81
|
+
# we remove all private keys because we don't want them in the serialized output
|
|
82
|
+
return {key: value for key, value in dct.items() if not key.startswith('_')}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class JSONResponse(Response):
|
|
86
|
+
## Public attributes
|
|
87
|
+
media_type = 'application/json; charset=UTF-8'
|
|
88
|
+
serialize_dumps_params: DumpsParams = DumpsParams()
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
content: Any = None,
|
|
93
|
+
status_code: int = 200,
|
|
94
|
+
headers: dict[str, str] | None = None,
|
|
95
|
+
media_type: str | None = None,
|
|
96
|
+
background: BackgroundTask | None = None,
|
|
97
|
+
serialize_dumps_params: DumpsParams | None = None
|
|
98
|
+
) -> None:
|
|
99
|
+
# Must be before the render method that runs inside init
|
|
100
|
+
if serialize_dumps_params:
|
|
101
|
+
self.serialize_dumps_params = serialize_dumps_params
|
|
102
|
+
|
|
103
|
+
super().__init__(
|
|
104
|
+
content=content,
|
|
105
|
+
status_code=status_code,
|
|
106
|
+
headers=headers,
|
|
107
|
+
media_type=media_type,
|
|
108
|
+
background=background
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def render(self, content: Any) -> bytes:
|
|
112
|
+
"""
|
|
113
|
+
Serialize the content to JSON format using the dumps function from the core.serialize module.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
content (Any): The content to be serialized.
|
|
117
|
+
"""
|
|
118
|
+
# with indent=None and separators=(",", ":"), the JSON will be minified
|
|
119
|
+
return dumps(content, **self.serialize_dumps_params.to_dict())
|
|
@@ -0,0 +1,64 @@
|
|
|
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__ = ['Route', 'RouteLazy']
|
|
11
|
+
|
|
12
|
+
from starlette.routing import Route, compile_path
|
|
13
|
+
from starlette.types import Receive, Scope, Send
|
|
14
|
+
|
|
15
|
+
from everysk.core.string import import_from_string
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
###############################################################################
|
|
19
|
+
# RouteLazy Class Implementation
|
|
20
|
+
###############################################################################
|
|
21
|
+
class RouteLazy(Route):
|
|
22
|
+
app: callable = None
|
|
23
|
+
endpoint: str = None
|
|
24
|
+
include_in_schema: bool = True
|
|
25
|
+
methods: set = None
|
|
26
|
+
name: str = None
|
|
27
|
+
path: str = None
|
|
28
|
+
|
|
29
|
+
def __init__(self, path: str, endpoint: str, name: str | None = None) -> None: # pylint: disable=super-init-not-called
|
|
30
|
+
"""
|
|
31
|
+
Route class that will lazy load the endpoint class.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path (str): The path of the route always start with '/'.
|
|
35
|
+
endpoint (str): Full doted class path of the endpoint.
|
|
36
|
+
name (str | None, optional): A name for this endpoint. Defaults to the same value in endpoint.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If the path doesn't start with '/'.
|
|
40
|
+
"""
|
|
41
|
+
if not path.startswith('/'):
|
|
42
|
+
raise ValueError("Routed paths must start with '/'")
|
|
43
|
+
|
|
44
|
+
self.path = path
|
|
45
|
+
self.endpoint = endpoint
|
|
46
|
+
self.name = endpoint if name is None else name
|
|
47
|
+
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
|
|
48
|
+
|
|
49
|
+
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Method to handle the incoming request and execute it.
|
|
52
|
+
If the endpoint is a string, it will import the endpoint class and execute it.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
scope (Scope): ASGI scope dictionary.
|
|
56
|
+
receive (Receive): ASGI receive data.
|
|
57
|
+
send (Send): ASGI send data.
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(self.endpoint, str):
|
|
60
|
+
self.app = import_from_string(self.endpoint)
|
|
61
|
+
else:
|
|
62
|
+
self.app = self.endpoint
|
|
63
|
+
|
|
64
|
+
await self.app(scope, receive, send)
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
|
|
11
|
+
EVERYSK_SERVER_DEBUG: bool = False
|
|
12
|
+
|
|
13
|
+
# Used to check access on JSONEndpoints
|
|
14
|
+
EVERYSK_SERVER_REST_KEY_NAME: str = 'X-Rest-Key'
|
|
15
|
+
EVERYSK_SERVER_REST_KEY_VALUE: str = '12345'
|
|
16
|
+
|
|
17
|
+
# Used by the GZipMiddleware to compress the response
|
|
18
|
+
EVERYSK_SERVER_GZIP_MINIMUM_SIZE: int = 1024
|
|
19
|
+
EVERYSK_SERVER_GZIP_COMPRESS_LEVEL: int = 9
|
|
20
|
+
|
|
21
|
+
# The codes that will be logged by the Logger
|
|
22
|
+
EVERYSK_SERVER_CODES_LOG: tuple = (500,)
|
|
23
|
+
|
|
24
|
+
# URL to use in the redirect response
|
|
25
|
+
EVERYSK_SERVER_REDIRECT_URL: str = None
|
|
26
|
+
|
|
27
|
+
# To enable GZipMiddleware
|
|
28
|
+
EVERYSK_SERVER_GZIP_MIDDLEWARE_ENABLED: bool = True
|
|
29
|
+
|
|
30
|
+
# To enable SecurityMiddleware
|
|
31
|
+
EVERYSK_SERVER_SECURITY_MIDDLEWARE_ENABLED: bool = True
|
|
32
|
+
|
|
33
|
+
# Size of the error message to log in the HTTP error response
|
|
34
|
+
# If the error message is larger than this size, it will be truncated
|
|
35
|
+
# and the full message will be in the extra context of the log
|
|
36
|
+
EVERYSK_SERVER_HTTP_ERROR_MESSAGE_SIZE: int = 256
|
everysk/server/tests.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
# pylint: disable=unused-import
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from everysk.server._tests.applications import ServerApplication as EveryskLibServerApplication
|
|
14
|
+
|
|
15
|
+
from everysk.server._tests.endpoints import (
|
|
16
|
+
BaseEndpointTestCase as EveryskLibBaseEndpointTestCase,
|
|
17
|
+
BaseEndpointTestCaseAsync as EveryskLibBaseEndpointTestCaseAsync,
|
|
18
|
+
DumpsParamsTestCase as EveryskLibDumpsParamsTestCase,
|
|
19
|
+
HealthCheckEndpointTestCaseAsync as EveryskLibHealthCheckEndpointTestCaseAsync,
|
|
20
|
+
JSONEndpointTestCase as EveryskLibJSONEndpointTestCase,
|
|
21
|
+
JSONEndpointTestCaseAsync as EveryskLibJSONEndpointTestCaseAsync,
|
|
22
|
+
NotAllowedMethodsTestCaseAsync as EveryskLibNotAllowedMethodsTestCaseAsync,
|
|
23
|
+
RedirectEndpointTestCase as EveryskLibRedirectEndpointTestCase,
|
|
24
|
+
RedirectEndpointTestCaseAsync as EveryskLibRedirectEndpointTestCaseAsync,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from everysk.server._tests.middlewares import (
|
|
28
|
+
GZipMiddlewareTestCaseAsync as EveryskLibGZipMiddlewareTestCaseAsync,
|
|
29
|
+
SecurityHeadersMiddlewareTestCaseAsync as EveryskLibSecurityHeadersMiddlewareTestCaseAsync,
|
|
30
|
+
UpdateMiddlewaresTestCase as EveryskLibUpdateMiddlewaresTestCase
|
|
31
|
+
)
|
|
32
|
+
from everysk.server._tests.routing import RouteLazyTestCaseAsync as EveryskLibRouteLazyTestCaseAsync
|
|
33
|
+
except ModuleNotFoundError as error:
|
|
34
|
+
# This will prevent running these tests if requests is not installed
|
|
35
|
+
if not error.args[0].startswith("No module named 'starlette'"):
|
|
36
|
+
raise error
|
everysk/settings.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
#
|
|
3
|
+
# (C) Copyright 2023 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
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
from everysk.core.fields import BoolField, ChoiceField, DictField, FloatField, IntField, ListField, StrField
|
|
13
|
+
|
|
14
|
+
## Project settings
|
|
15
|
+
DEBUG = BoolField(default=True)
|
|
16
|
+
|
|
17
|
+
# PROD | DEV | LOCAL
|
|
18
|
+
PROFILE = ChoiceField(default='DEV', choices=('PROD', 'DEV', 'LOCAL'))
|
|
19
|
+
|
|
20
|
+
## Redis
|
|
21
|
+
REDIS_HOST = StrField(default='0.0.0.0') # noqa: S104
|
|
22
|
+
REDIS_PORT = IntField(default=6379)
|
|
23
|
+
REDIS_NAMESPACE = StrField()
|
|
24
|
+
REDIS_HEALTH_CHECK_INTERVAL = IntField(default=30) # seconds
|
|
25
|
+
REDIS_RETRY_ATTEMPTS = IntField(default=3)
|
|
26
|
+
REDIS_RETRY_BACKOFF_MIN = FloatField(default=0.05) # Minimum backoff between each retry in seconds
|
|
27
|
+
REDIS_RETRY_BACKOFF_MAX = FloatField(default=0.5) # Maximum backoff between each retry in seconds
|
|
28
|
+
REDIS_RETRY_EXTRA_ERROR_LIST = ListField() # Added more errors to retry
|
|
29
|
+
REDIS_SOCKET_KEEPALIVE = BoolField(default=True)
|
|
30
|
+
REDIS_SOCKET_TIMEOUT = IntField(default=120) # seconds
|
|
31
|
+
REDIS_SHOW_LOGS = BoolField(default=False)
|
|
32
|
+
|
|
33
|
+
## Everysk SIGNING key is used to verify the integrity of the data
|
|
34
|
+
EVERYSK_SIGNING_KEY = StrField(default=None)
|
|
35
|
+
|
|
36
|
+
## Google Cloud
|
|
37
|
+
EVERYSK_GOOGLE_CLOUD_LOCATION = StrField(default='us-central1')
|
|
38
|
+
EVERYSK_GOOGLE_CLOUD_PROJECT = StrField()
|
|
39
|
+
|
|
40
|
+
## Logger settings
|
|
41
|
+
# This is the default app server to infer the headers and payload
|
|
42
|
+
LOGGING_APP_SERVER = StrField(default='flask')
|
|
43
|
+
|
|
44
|
+
# This is the default string to create the Google Cloud Trace ID
|
|
45
|
+
LOGGING_GOOGLE_CLOUD_TRACE_ID = StrField(default='projects/{EVERYSK_GOOGLE_CLOUD_PROJECT}/traces')
|
|
46
|
+
|
|
47
|
+
# This is the default Formatter for the logs
|
|
48
|
+
LOGGING_JSON = BoolField(default=False)
|
|
49
|
+
|
|
50
|
+
## Slack URL to send messages
|
|
51
|
+
SLACK_URL = StrField()
|
|
52
|
+
|
|
53
|
+
## Activate/deactivate HTTP Log for every request/response
|
|
54
|
+
EVERYSK_HTTP_LOG_RESPONSE = BoolField(default=False)
|
|
55
|
+
|
|
56
|
+
## Activate/deactivate HTTP Log for retry connections
|
|
57
|
+
EVERYSK_HTTP_LOG_RETRY = BoolField(default=False)
|
|
58
|
+
|
|
59
|
+
## List of HTTP Success status codes that do not raise HTTPError
|
|
60
|
+
HTTP_SUCCESS_STATUS_CODES = ListField(default=[200, 201, 202, 204, 303], readonly=True)
|
|
61
|
+
|
|
62
|
+
## HTTP default headers used to send requests
|
|
63
|
+
HTTP_DEFAULT_HEADERS = DictField(
|
|
64
|
+
default={
|
|
65
|
+
'Accept-Encoding': 'gzip',
|
|
66
|
+
'Accept-Language': 'en-US, en;q=0.9, pt-BR;q=0.8, pt;q=0.7',
|
|
67
|
+
'Cache-control': 'no-cache',
|
|
68
|
+
'Content-Type': 'text/html; charset=UTF-8',
|
|
69
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', # noqa: E501
|
|
70
|
+
},
|
|
71
|
+
readonly=True,
|
|
72
|
+
)
|
|
73
|
+
HTTP_DEFAULT_TIMEOUT = IntField(default=30)
|
|
74
|
+
HTTP_DEFAULT_SSL_VERIFY = BoolField(default=True)
|
|
75
|
+
HTTP_DEFAULT_RETRY_LIMIT = IntField(default=5)
|
|
76
|
+
HTTP_DEFAULT_RETRY_END_SECONDS = IntField(default=30)
|
|
77
|
+
HTTP_DEFAULT_RETRY_START_SECONDS = IntField(default=5)
|
|
78
|
+
|
|
79
|
+
## Serialization settings
|
|
80
|
+
SERIALIZE_CONVERT_METHOD_NAME = StrField(default='to_native')
|
|
81
|
+
SERIALIZE_DATE_KEY = StrField(default='__date__')
|
|
82
|
+
SERIALIZE_DATETIME_KEY = StrField('__datetime__')
|
|
83
|
+
SERIALIZE_UNDEFINED_KEY = StrField('__undefined__')
|
|
84
|
+
SERIALIZE_USE_UNDEFINED = BoolField(default=False)
|
|
85
|
+
|
|
86
|
+
## Activate/deactivate the use of the verify flag on HTTP requests.
|
|
87
|
+
# By default the value is Undefined and the value comes from the self._config.ssl_verify
|
|
88
|
+
# attribute in the HttpConnection class, if it is defined we use it.
|
|
89
|
+
HTTP_REQUESTS_VERIFY = BoolField(default=Undefined)
|
|
90
|
+
|
|
91
|
+
# Activate/deactivate the use of random user agents on HTTP requests
|
|
92
|
+
HTTP_USE_RANDOM_USER_AGENT = BoolField(default=False)
|
|
93
|
+
|
|
94
|
+
# Default directory to use the known_hosts file and other SFTP configurations
|
|
95
|
+
EVERYSK_SFTP_DIR = StrField(default=f'{tempfile.gettempdir()}/sftp')
|
|
96
|
+
|
|
97
|
+
# Enable to show retry logs
|
|
98
|
+
RETRY_SHOW_LOGS = BoolField(default=False)
|
everysk/sql/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
###############################################################################
|
|
@@ -0,0 +1,232 @@
|
|
|
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 collections.abc import Callable
|
|
11
|
+
from contextvars import ContextVar, Token
|
|
12
|
+
from os import getpid
|
|
13
|
+
from types import TracebackType
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from psycopg import Connection, OperationalError
|
|
17
|
+
from psycopg_pool import ConnectionPool as _ConnectionPool
|
|
18
|
+
|
|
19
|
+
from everysk.config import settings
|
|
20
|
+
from everysk.core.log import Logger
|
|
21
|
+
from everysk.core.retry import retry
|
|
22
|
+
from everysk.sql.row_factory import cls_row, dict_row
|
|
23
|
+
|
|
24
|
+
_CONNECTIONS: dict[str, 'ConnectionPool'] = {}
|
|
25
|
+
log = Logger('everysk-lib-sql-query')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _log(message: str, extra: dict | None = None) -> None:
|
|
29
|
+
if settings.POSTGRESQL_LOG_QUERIES:
|
|
30
|
+
log.debug(message, extra=extra)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConnectionPool(_ConnectionPool):
|
|
34
|
+
def __del__(self) -> None:
|
|
35
|
+
# To close the connections when the pool is deleted
|
|
36
|
+
# https://everysk.atlassian.net/browse/COD-8885
|
|
37
|
+
try:
|
|
38
|
+
return super().__del__()
|
|
39
|
+
except RuntimeError:
|
|
40
|
+
# The connection is already closed or discarded because we cannot join the current thread
|
|
41
|
+
# RuntimeError: cannot join current thread
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Transaction:
|
|
48
|
+
"""
|
|
49
|
+
Context manager for PostgreSQL transactions.
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
|
|
53
|
+
>>> from everysk.sql.connection import Transaction, execute
|
|
54
|
+
>>> with Transaction():
|
|
55
|
+
... result = execute("SELECT * FROM my_table WHERE id = %s", params={"id": 1})
|
|
56
|
+
... # Perform other database operations within the transaction
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
## Private attributes
|
|
61
|
+
_connection: Connection
|
|
62
|
+
_pool: ConnectionPool
|
|
63
|
+
_token: Token
|
|
64
|
+
|
|
65
|
+
## Public attributes
|
|
66
|
+
connection: ContextVar[Connection] = ContextVar('postgresql-psqlpy-transaction', default=None)
|
|
67
|
+
|
|
68
|
+
def __init__(self, dsn: str | None = None) -> None:
|
|
69
|
+
self._pool: ConnectionPool = get_pool(dsn=dsn)
|
|
70
|
+
|
|
71
|
+
def __enter__(self) -> None:
|
|
72
|
+
self._connection = self._pool.getconn()
|
|
73
|
+
self._token = self.connection.set(self._connection)
|
|
74
|
+
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def __exit__(
|
|
78
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
79
|
+
) -> None:
|
|
80
|
+
if exc_type is None:
|
|
81
|
+
self._connection.commit()
|
|
82
|
+
else:
|
|
83
|
+
self._connection.rollback()
|
|
84
|
+
|
|
85
|
+
self.connection.reset(self._token)
|
|
86
|
+
# Return the connection to the pool
|
|
87
|
+
self._pool.putconn(self._connection)
|
|
88
|
+
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def make_connection_dsn(
|
|
93
|
+
host: str | None = None,
|
|
94
|
+
port: int | None = None,
|
|
95
|
+
user: str | None = None,
|
|
96
|
+
password: str | None = None,
|
|
97
|
+
database: str | None = None,
|
|
98
|
+
sslmode: str | None = None,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Create a PostgreSQL connection DSN from settings.
|
|
102
|
+
Supports both TCP and Unix socket connections.
|
|
103
|
+
If parameters are provided, they override the settings.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
host (str | None): The database host. If None, uses the setting.
|
|
107
|
+
port (int | None): The database port. If None, uses the setting.
|
|
108
|
+
user (str | None): The database user. If None, uses the setting.
|
|
109
|
+
password (str | None): The database password. If None, uses the setting.
|
|
110
|
+
database (str | None): The database name. If None, uses the setting.
|
|
111
|
+
sslmode (str | None): The SSL mode for connection. If None, uses the setting if defined.
|
|
112
|
+
"""
|
|
113
|
+
sslmode: str | None = sslmode or settings.POSTGRESQL_CONNECTION_SSLMODE
|
|
114
|
+
options: dict[str, str | int] = {
|
|
115
|
+
'host': host or settings.POSTGRESQL_CONNECTION_HOST,
|
|
116
|
+
'port': port or settings.POSTGRESQL_CONNECTION_PORT,
|
|
117
|
+
'user': user or settings.POSTGRESQL_CONNECTION_USER,
|
|
118
|
+
'password': password or settings.POSTGRESQL_CONNECTION_PASSWORD,
|
|
119
|
+
'database': database or settings.POSTGRESQL_CONNECTION_DATABASE,
|
|
120
|
+
'sslmode': f'?sslmode={sslmode}' if sslmode else '',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Handle Unix socket connections
|
|
124
|
+
if options['host'].startswith('/'):
|
|
125
|
+
return 'postgresql:///{database}?host={host}&user={user}&password={password}'.format(**options)
|
|
126
|
+
|
|
127
|
+
# Standard TCP connection
|
|
128
|
+
return 'postgresql://{user}:{password}@{host}:{port}/{database}{sslmode}'.format(**options)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_pool(dsn: str | None = None, **kwargs) -> ConnectionPool:
|
|
132
|
+
"""
|
|
133
|
+
Retrieve a database connection pool for the given DSN.
|
|
134
|
+
|
|
135
|
+
If no DSN is provided, a default DSN is generated. The connection pool is cached
|
|
136
|
+
based on the process ID and DSN hash to ensure reuse within the same process.
|
|
137
|
+
If a pool for the given key does not exist, a new one is created with the specified
|
|
138
|
+
maximum size and SSL mode.
|
|
139
|
+
|
|
140
|
+
Importantly, this is necessary because connections cannot be shared between processes.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
dsn (str | None): The Data Source Name for the database connection. If None, a default DSN is used.
|
|
144
|
+
**kwargs: Additional keyword arguments to configure the connection pool.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
ConnectionPool: The connection pool associated with the given DSN.
|
|
148
|
+
"""
|
|
149
|
+
dsn = dsn or make_connection_dsn()
|
|
150
|
+
# https://www.psycopg.org/psycopg3/docs/api/pool.html
|
|
151
|
+
kwargs['check'] = ConnectionPool.check_connection
|
|
152
|
+
kwargs['min_size'] = kwargs.get('min_size', settings.POSTGRESQL_POOL_MIN_SIZE)
|
|
153
|
+
kwargs['max_size'] = kwargs.get('max_size', settings.POSTGRESQL_POOL_MAX_SIZE)
|
|
154
|
+
kwargs['max_idle'] = kwargs.get('max_idle', settings.POSTGRESQL_POOL_MAX_IDLE)
|
|
155
|
+
kwargs['max_lifetime'] = kwargs.get('max_lifetime', settings.POSTGRESQL_POOL_MAX_LIFETIME)
|
|
156
|
+
kwargs['max_waiting'] = kwargs.get('max_waiting', settings.POSTGRESQL_POOL_MAX_WAITING)
|
|
157
|
+
kwargs['reconnect_timeout'] = kwargs.get('reconnect_timeout', settings.POSTGRESQL_POOL_RECONNECT_TIMEOUT)
|
|
158
|
+
kwargs['timeout'] = kwargs.get('timeout', settings.POSTGRESQL_POOL_TIMEOUT)
|
|
159
|
+
kwargs['open'] = kwargs.get('open', settings.POSTGRESQL_POOL_OPEN)
|
|
160
|
+
|
|
161
|
+
key = f'{getpid()}:{hash(dsn)}'
|
|
162
|
+
if key not in _CONNECTIONS:
|
|
163
|
+
_CONNECTIONS[key] = ConnectionPool(conninfo=dsn, **kwargs)
|
|
164
|
+
|
|
165
|
+
return _CONNECTIONS[key]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def execute(
|
|
169
|
+
query: str,
|
|
170
|
+
params: dict | None = None,
|
|
171
|
+
return_type: Literal['dict', 'list'] = 'list',
|
|
172
|
+
dsn: str | None = None,
|
|
173
|
+
cls: type | None = None,
|
|
174
|
+
loads: Callable | None = None,
|
|
175
|
+
) -> list[dict] | list[object] | dict | None:
|
|
176
|
+
"""
|
|
177
|
+
Execute a query and return the results.
|
|
178
|
+
If return_type is a class, return a list of instances of that class.
|
|
179
|
+
If return_type is a string, return a dictionary keyed by that string.
|
|
180
|
+
Otherwise, return a list of dictionaries.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
query (str): The SQL query to execute.
|
|
184
|
+
params (dict | None, optional): The parameters to include in the query. Defaults to None.
|
|
185
|
+
return_type (Literal['dict', 'list'], optional): The type of return value. Defaults to 'list'.
|
|
186
|
+
dsn (str | None, optional): The DSN to use for the connection. Defaults to None.
|
|
187
|
+
cls (type | None, optional): The class to map the results to. Defaults to None.
|
|
188
|
+
loads (Callable | None, optional): Optional function to process each value. Defaults to None.
|
|
189
|
+
retry (int, optional): The current retry count. Defaults to 0.
|
|
190
|
+
"""
|
|
191
|
+
conn: Connection = Transaction.connection.get()
|
|
192
|
+
if not conn:
|
|
193
|
+
pool: ConnectionPool = get_pool(dsn=dsn)
|
|
194
|
+
conn: Connection = pool.getconn()
|
|
195
|
+
is_transactional = False
|
|
196
|
+
log_message = 'PostgreSQL query executed.'
|
|
197
|
+
else:
|
|
198
|
+
is_transactional = True
|
|
199
|
+
log_message = 'PostgreSQL query executed within transaction.'
|
|
200
|
+
|
|
201
|
+
_log(log_message, extra={'labels': {'query': query, 'params': params}})
|
|
202
|
+
|
|
203
|
+
row_factory = cls_row(cls, loads) if cls else dict_row(loads)
|
|
204
|
+
# For transactions we let it be controlled externally by the context manager
|
|
205
|
+
try:
|
|
206
|
+
with conn.cursor(row_factory=row_factory) as cur:
|
|
207
|
+
result = retry(cur.execute, {'query': query, 'params': params}, retries=3, exceptions=OperationalError)
|
|
208
|
+
|
|
209
|
+
if result.description:
|
|
210
|
+
result = cur.fetchall()
|
|
211
|
+
else:
|
|
212
|
+
result = None
|
|
213
|
+
except Exception:
|
|
214
|
+
# On error we need to rollback
|
|
215
|
+
if not is_transactional:
|
|
216
|
+
conn.rollback()
|
|
217
|
+
raise
|
|
218
|
+
|
|
219
|
+
else:
|
|
220
|
+
# Block that only executes if no exception was raised in the try block
|
|
221
|
+
if not is_transactional:
|
|
222
|
+
conn.commit()
|
|
223
|
+
|
|
224
|
+
finally:
|
|
225
|
+
# We only return the connection to the pool if we are not in a transaction
|
|
226
|
+
if not is_transactional:
|
|
227
|
+
pool.putconn(conn)
|
|
228
|
+
|
|
229
|
+
if result and cls and return_type == 'dict':
|
|
230
|
+
return {row[cls._primary_key]: row for row in result}
|
|
231
|
+
|
|
232
|
+
return result
|