setech 1.0.0__tar.gz

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.
setech-1.0.0/LICENCE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2024 "Sefinance"
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
setech-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.1
2
+ Name: setech
3
+ Version: 1.0.0
4
+ Summary: Setech utilities
5
+ Author-email: Eriks Karls <eriks.karls@sefinance.lv>
6
+ Project-URL: Homepage, https://pypi.org/project/setech/
7
+ Keywords: setech,logging,api-client,utility,utils
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Requires-Python: ~=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENCE
20
+ Requires-Dist: httpx[http2]~=0.27.0
21
+ Requires-Dist: pydantic~=2.6
22
+ Requires-Dist: num2words~=0.5
23
+
24
+ # Example code
25
+ ```python
26
+ # client.py
27
+ from setech import SyncClient
28
+
29
+
30
+ class LocalClient(SyncClient):
31
+ name = "local"
32
+ _base_url = "https://obligari.serveo.net/ping/local"
33
+
34
+ def __init__(self, nonce=None):
35
+ super().__init__(nonce)
36
+ self._session.headers.update(
37
+ {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:123.0) Gecko/20100101 Firefox/123.0"}
38
+ )
39
+
40
+ def send_post_ping(self, var1: str, var2: int) -> bool:
41
+ res = self.post("/some-post", json={"variable_one": var1, "second_variable": var2})
42
+ return res.json().get("status")
43
+
44
+ def send_put_ping(self, var1: str, var2: int) -> bool:
45
+ res = self.put("/some-put", data={"variable_one": var1, "second_variable": var2})
46
+ return res.json().get("status")
47
+
48
+ def send_get_ping(self, var1: str, var2: int) -> bool:
49
+ res = self.get("/some-get", params={"variable_one": var1, "second_variable": var2})
50
+ return res.json().get("status")
51
+
52
+ def send_patch_ping(self, var1: str, var2: int) -> bool:
53
+ res = self.put("/some-patch", data=(("variable_one", var1), ("variable_one", var2)))
54
+ return res.json().get("status")
55
+
56
+ def send_trace_ping(self, var1: str, var2: int) -> bool:
57
+ res = self.trace("/some-trace", params=(("variable_one", var1), ("variable_one", var2)))
58
+ return res.json().get("status")
59
+ ```
60
+
61
+ ```python
62
+ # main.py
63
+ from .client import LocalClient
64
+
65
+
66
+ client = LocalClient()
67
+ client.send_post_ping("asd", 123)
68
+ client.send_put_ping("asd", 123)
69
+ client.send_get_ping("asd", 123)
70
+ client.send_patch_ping("asd", 123)
71
+ client.send_trace_ping("asd", 123)
72
+ ```
73
+
74
+ ## Log output
75
+ ### Simple
76
+ ```text
77
+ [14d709e02c0c] Preparing POST request to "https://obligari.serveo.net/ping/local/some-post"
78
+ [14d709e02c0c] Sending request with payload=b'{"variable_one": "asd", "second_variable": 123}'
79
+ [14d709e02c0c] Response response.status_code=200 str_repr_content='{"status":true,"request_id":62}'
80
+ [14d709e02c0c] Preparing GET request to "https://obligari.serveo.net/ping/local/some-get"
81
+ [14d709e02c0c] Sending request with payload=None
82
+ [14d709e02c0c] Response response.status_code=200 str_repr_content='{"status":true,"request_id":63}'
83
+ ```
84
+ ### Structured
85
+ ```json
86
+ {"app": "dev", "level": "DEBUG", "name": "APIClient", "date_time": "2024-03-09 22:59:24", "location": "api_client/client.py:_request:71", "message": "[cfbdadc56f53] Preparing POST request to \"https://obligari.serveo.net/ping/local/some-post\"", "extra_data": {"hooks": {"response": []}, "method": "POST", "url": "https://obligari.serveo.net/ping/local/some-post", "headers": {}, "files": [], "data": [], "json": {"variable_one": "asd", "second_variable": 123}, "params": {}, "auth": null, "cookies": null}}
87
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:24", "location": "api_client/client.py:_request:74", "message": "[cfbdadc56f53] Sending request with payload=b'{\"variable_one\": \"asd\", \"second_variable\": 123}'", "extra_data": {"payload": "{\"variable_one\": \"asd\", \"second_variable\": 123}"}}
88
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:81", "message": "[cfbdadc56f53] Response response.status_code=200 str_repr_content='{\"status\":true,\"request_id\":72}'", "extra_data": {"status_code": 200, "content": "{\"status\":true,\"request_id\":72}"}}
89
+ {"app": "dev", "level": "DEBUG", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:71", "message": "[cfbdadc56f53] Preparing GET request to \"https://obligari.serveo.net/ping/local/some-get\"", "extra_data": {"hooks": {"response": []}, "method": "GET", "url": "https://obligari.serveo.net/ping/local/some-get", "headers": {}, "files": [], "data": [], "json": null, "params": {"variable_one": "asd", "second_variable": 123}, "auth": null, "cookies": null}}
90
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:74", "message": "[cfbdadc56f53] Sending request with payload=None", "extra_data": {"payload": "{}"}}
91
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:81", "message": "[cfbdadc56f53] Response response.status_code=200 str_repr_content='{\"status\":true,\"request_id\":74}'", "extra_data": {"status_code": 200, "content": "{\"status\":true,\"request_id\":73}"}}
92
+ ```
setech-1.0.0/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Example code
2
+ ```python
3
+ # client.py
4
+ from setech import SyncClient
5
+
6
+
7
+ class LocalClient(SyncClient):
8
+ name = "local"
9
+ _base_url = "https://obligari.serveo.net/ping/local"
10
+
11
+ def __init__(self, nonce=None):
12
+ super().__init__(nonce)
13
+ self._session.headers.update(
14
+ {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:123.0) Gecko/20100101 Firefox/123.0"}
15
+ )
16
+
17
+ def send_post_ping(self, var1: str, var2: int) -> bool:
18
+ res = self.post("/some-post", json={"variable_one": var1, "second_variable": var2})
19
+ return res.json().get("status")
20
+
21
+ def send_put_ping(self, var1: str, var2: int) -> bool:
22
+ res = self.put("/some-put", data={"variable_one": var1, "second_variable": var2})
23
+ return res.json().get("status")
24
+
25
+ def send_get_ping(self, var1: str, var2: int) -> bool:
26
+ res = self.get("/some-get", params={"variable_one": var1, "second_variable": var2})
27
+ return res.json().get("status")
28
+
29
+ def send_patch_ping(self, var1: str, var2: int) -> bool:
30
+ res = self.put("/some-patch", data=(("variable_one", var1), ("variable_one", var2)))
31
+ return res.json().get("status")
32
+
33
+ def send_trace_ping(self, var1: str, var2: int) -> bool:
34
+ res = self.trace("/some-trace", params=(("variable_one", var1), ("variable_one", var2)))
35
+ return res.json().get("status")
36
+ ```
37
+
38
+ ```python
39
+ # main.py
40
+ from .client import LocalClient
41
+
42
+
43
+ client = LocalClient()
44
+ client.send_post_ping("asd", 123)
45
+ client.send_put_ping("asd", 123)
46
+ client.send_get_ping("asd", 123)
47
+ client.send_patch_ping("asd", 123)
48
+ client.send_trace_ping("asd", 123)
49
+ ```
50
+
51
+ ## Log output
52
+ ### Simple
53
+ ```text
54
+ [14d709e02c0c] Preparing POST request to "https://obligari.serveo.net/ping/local/some-post"
55
+ [14d709e02c0c] Sending request with payload=b'{"variable_one": "asd", "second_variable": 123}'
56
+ [14d709e02c0c] Response response.status_code=200 str_repr_content='{"status":true,"request_id":62}'
57
+ [14d709e02c0c] Preparing GET request to "https://obligari.serveo.net/ping/local/some-get"
58
+ [14d709e02c0c] Sending request with payload=None
59
+ [14d709e02c0c] Response response.status_code=200 str_repr_content='{"status":true,"request_id":63}'
60
+ ```
61
+ ### Structured
62
+ ```json
63
+ {"app": "dev", "level": "DEBUG", "name": "APIClient", "date_time": "2024-03-09 22:59:24", "location": "api_client/client.py:_request:71", "message": "[cfbdadc56f53] Preparing POST request to \"https://obligari.serveo.net/ping/local/some-post\"", "extra_data": {"hooks": {"response": []}, "method": "POST", "url": "https://obligari.serveo.net/ping/local/some-post", "headers": {}, "files": [], "data": [], "json": {"variable_one": "asd", "second_variable": 123}, "params": {}, "auth": null, "cookies": null}}
64
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:24", "location": "api_client/client.py:_request:74", "message": "[cfbdadc56f53] Sending request with payload=b'{\"variable_one\": \"asd\", \"second_variable\": 123}'", "extra_data": {"payload": "{\"variable_one\": \"asd\", \"second_variable\": 123}"}}
65
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:81", "message": "[cfbdadc56f53] Response response.status_code=200 str_repr_content='{\"status\":true,\"request_id\":72}'", "extra_data": {"status_code": 200, "content": "{\"status\":true,\"request_id\":72}"}}
66
+ {"app": "dev", "level": "DEBUG", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:71", "message": "[cfbdadc56f53] Preparing GET request to \"https://obligari.serveo.net/ping/local/some-get\"", "extra_data": {"hooks": {"response": []}, "method": "GET", "url": "https://obligari.serveo.net/ping/local/some-get", "headers": {}, "files": [], "data": [], "json": null, "params": {"variable_one": "asd", "second_variable": 123}, "auth": null, "cookies": null}}
67
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:74", "message": "[cfbdadc56f53] Sending request with payload=None", "extra_data": {"payload": "{}"}}
68
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:81", "message": "[cfbdadc56f53] Response response.status_code=200 str_repr_content='{\"status\":true,\"request_id\":74}'", "extra_data": {"status_code": 200, "content": "{\"status\":true,\"request_id\":73}"}}
69
+ ```
@@ -0,0 +1,83 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "setech"
7
+ dynamic = ["version"]
8
+ description = "Setech utilities"
9
+ readme = "README.md"
10
+ requires-python = "~=3.10"
11
+ license = { file = "LICENSE" }
12
+ keywords = ["setech", "logging", "api-client", "utility", "utils"]
13
+ authors = [
14
+ { name = "Eriks Karls", email = "eriks.karls@sefinance.lv" },
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Operating System :: OS Independent",
26
+ ]
27
+ dependencies = [
28
+ "httpx[http2]~=0.27.0",
29
+ "pydantic~=2.6",
30
+ "num2words~=0.5"
31
+ ]
32
+
33
+
34
+ [project.urls]
35
+ "Homepage" = "https://pypi.org/project/setech/"
36
+
37
+
38
+ [tool.setuptools.dynamic]
39
+ version = { attr = "setech.__version__" }
40
+
41
+
42
+ [tool.bumpversion]
43
+ current_version = "1.0.0"
44
+ commit = true
45
+ tag = true
46
+ tag_name = "v{new_version}"
47
+ tag_message = "Bump version: {current_version} → {new_version}"
48
+ allow_dirty = false
49
+ parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
50
+ serialize = ["{major}.{minor}.{patch}"]
51
+ message = "Bump version: {current_version} → {new_version}"
52
+
53
+
54
+ [[tool.bumpversion.files]]
55
+ filename = "src/setech/__init__.py"
56
+
57
+
58
+ [tool.black]
59
+ line-length = 120
60
+ target-version = ['py311']
61
+ include = '\.pyi?$'
62
+ extend-exclude = '''(
63
+ | .git/*
64
+ )'''
65
+ workers = 4
66
+
67
+
68
+ [tool.isort]
69
+ profile = "black"
70
+ line_length = 120
71
+ skip = ["env", "venv", ".venv", ".git"]
72
+
73
+
74
+ [tool.mypy]
75
+ python_version = "3.11"
76
+ exclude = ['^\.?venv/',]
77
+ plugins = ["pydantic.mypy"]
78
+ warn_unused_configs = true
79
+ disallow_untyped_defs = true
80
+ implicit_optional = true
81
+ warn_redundant_casts = true
82
+ warn_no_return = true
83
+ ignore_missing_imports = false
setech-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from setech.api_client import AsyncClient, SyncClient
2
+
3
+ __version__ = "1.0.0"
4
+ __all__ = ["SyncClient", "AsyncClient"]
@@ -0,0 +1,4 @@
1
+ from .async_client import AsyncClient
2
+ from .sync_client import SyncClient
3
+
4
+ __all__ = ["AsyncClient", "SyncClient"]
@@ -0,0 +1,124 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Coroutine
4
+
5
+ import httpx
6
+ from pydantic import HttpUrl
7
+
8
+ from setech.utils import get_logger, get_nonce, shortify_log_dict
9
+
10
+ _TypeSyncAsyncResponse = httpx.Response | Coroutine[Any, Any, httpx.Response]
11
+
12
+
13
+ class BaseClient(ABC):
14
+ base_url: HttpUrl
15
+ _session: httpx._client.BaseClient
16
+ _nonce: str
17
+ _logger: logging.Logger
18
+
19
+ def __init__(self, nonce: str = "", session: httpx._client.BaseClient | None = None):
20
+ self._nonce = nonce or get_nonce()
21
+ self._session = session or httpx.Client()
22
+ self._logger = get_logger("APIClient")
23
+
24
+ @abstractmethod
25
+ def get(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
26
+ pass
27
+
28
+ @abstractmethod
29
+ def post(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
30
+ pass
31
+
32
+ @abstractmethod
33
+ def put(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
34
+ pass
35
+
36
+ @abstractmethod
37
+ def patch(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
38
+ pass
39
+
40
+ @abstractmethod
41
+ def delete(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
42
+ pass
43
+
44
+ @abstractmethod
45
+ def head(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
46
+ pass
47
+
48
+ @abstractmethod
49
+ def options(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
50
+ pass
51
+
52
+ @abstractmethod
53
+ def trace(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
54
+ pass
55
+
56
+ @abstractmethod
57
+ def connect(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> _TypeSyncAsyncResponse:
58
+ pass
59
+
60
+ @abstractmethod
61
+ def _request(self, method: str, endpoint: str, **kwargs: Any) -> _TypeSyncAsyncResponse:
62
+ pass
63
+
64
+ def _make_full_url(self, endpoint: str) -> str:
65
+ return f"{self.base_url}{endpoint}"
66
+
67
+ def prepare_authentication(self, request: httpx.Request) -> httpx.Request:
68
+ return request
69
+
70
+ def _prepare_request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Request:
71
+ full_url = self._make_full_url(endpoint)
72
+
73
+ self._debug_log_request(method, full_url)
74
+ request: httpx.Request = self._session.build_request(method=method, url=full_url, **kwargs)
75
+ self._debug_log_prepared_request(request)
76
+ self._debug(f"Prepared {request.method} request to '{request.url}'", extra=request.__dict__)
77
+
78
+ self._info_log_request_sending(
79
+ request, kwargs.get("content") or kwargs.get("files") or kwargs.get("data") or kwargs.get("json")
80
+ )
81
+ return request
82
+
83
+ def _debug_log_request(self, method: str, full_url: str) -> None:
84
+ self._debug(f"Preparing {method} request for '{full_url}'")
85
+
86
+ def _debug_log_prepared_request(self, request: httpx.Request) -> None:
87
+ self._debug(f"Prepared {request.method} request to '{request.url}'", extra=request.__dict__)
88
+
89
+ def _info_log_request_sending(self, request: httpx.Request, log_payload: Any) -> None:
90
+ self._info(
91
+ f"Sending {request.method} request to '{request.url}' with payload={shortify_log_dict(log_payload)!r}",
92
+ extra={"payload": shortify_log_dict(log_payload)},
93
+ )
94
+
95
+ def _info_log_response(self, response: httpx.Response) -> None:
96
+ str_repr_content = response.content.decode("utf8")[:500]
97
+ self._info(
98
+ f"Response {response.status_code=} {str_repr_content=}",
99
+ extra={"status_code": response.status_code, "content": str_repr_content},
100
+ )
101
+
102
+ def _info(self, msg: str, *args: Any, **kwargs: Any) -> None:
103
+ self._log("INFO", f"[{self._nonce}] {msg}", *args, **kwargs)
104
+
105
+ def _debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
106
+ self._log("DEBUG", f"[{self._nonce}] {msg}", *args, **kwargs)
107
+
108
+ def _warn(self, msg: str, *args: Any, **kwargs: Any) -> None:
109
+ self._log("WARNING", f"[{self._nonce}] {msg}", *args, **kwargs)
110
+
111
+ def _error(self, msg: str, *args: Any, **kwargs: Any) -> None:
112
+ self._log("ERROR", f"[{self._nonce}] {msg}", *args, **kwargs)
113
+
114
+ def _critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
115
+ self._log("CRITICAL", f"[{self._nonce}] {msg}", *args, **kwargs)
116
+
117
+ def _log(
118
+ self, level: str, msg: object, *args: object, stacklevel: int = 5, extra: dict | None = None, **kwargs: Any
119
+ ) -> None:
120
+ extra = extra or {}
121
+ extra.update(nonce=self._nonce)
122
+ self._logger.log(
123
+ logging.getLevelNamesMapping()[level], msg, *args, stacklevel=stacklevel, extra=extra, **kwargs
124
+ )
@@ -0,0 +1,46 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from ._base import BaseClient
6
+
7
+
8
+ class AsyncClient(BaseClient):
9
+ _session: httpx.AsyncClient
10
+
11
+ def __init__(self, nonce: str = "", session: httpx.AsyncClient | None = None):
12
+ super().__init__(nonce, session or httpx.AsyncClient(http2=True))
13
+
14
+ async def get(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
15
+ return await self._request("GET", endpoint, params=params, **kwargs)
16
+
17
+ async def post(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
18
+ return await self._request("POST", endpoint, json=json, data=data, **kwargs)
19
+
20
+ async def put(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
21
+ return await self._request("PUT", endpoint, json=json, data=data, **kwargs)
22
+
23
+ async def patch(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
24
+ return await self._request("GET", endpoint, json=json, data=data, **kwargs)
25
+
26
+ async def delete(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
27
+ return await self._request("GET", endpoint, params=params, **kwargs)
28
+
29
+ async def head(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
30
+ return await self._request("GET", endpoint, params=params, **kwargs)
31
+
32
+ async def options(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
33
+ return await self._request("GET", endpoint, params=params, **kwargs)
34
+
35
+ async def trace(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
36
+ return await self._request("GET", endpoint, params=params, **kwargs)
37
+
38
+ async def connect(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
39
+ return await self._request("GET", endpoint, params=params, **kwargs)
40
+
41
+ async def _request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
42
+ request = self._prepare_request(method, endpoint, **kwargs)
43
+ response = await self._session.send(request, auth=self.prepare_authentication)
44
+ self._info_log_response(response)
45
+
46
+ return response
@@ -0,0 +1,47 @@
1
+ from abc import ABC
2
+ from typing import Any
3
+
4
+ import httpx
5
+
6
+ from ._base import BaseClient
7
+
8
+
9
+ class SyncClient(BaseClient, ABC):
10
+ _session: httpx.Client
11
+
12
+ def __init__(self, nonce: str = "", session: httpx.Client | None = None):
13
+ super().__init__(nonce, session or httpx.Client(http2=True))
14
+
15
+ def get(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
16
+ return self._request("GET", endpoint, params=params, **kwargs)
17
+
18
+ def post(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
19
+ return self._request("POST", endpoint, json=json, data=data, **kwargs)
20
+
21
+ def put(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
22
+ return self._request("PUT", endpoint, json=json, data=data, **kwargs)
23
+
24
+ def patch(self, endpoint: str, *, json: Any = None, data: Any = None, **kwargs: Any) -> httpx.Response:
25
+ return self._request("PATCH", endpoint, json=json, data=data, **kwargs)
26
+
27
+ def delete(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
28
+ return self._request("DELETE", endpoint, params=params, **kwargs)
29
+
30
+ def head(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
31
+ return self._request("HEAD", endpoint, params=params, **kwargs)
32
+
33
+ def options(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
34
+ return self._request("OPTIONS", endpoint, params=params, **kwargs)
35
+
36
+ def trace(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
37
+ return self._request("TRACE", endpoint, params=params, **kwargs)
38
+
39
+ def connect(self, endpoint: str, *, params: Any = None, **kwargs: Any) -> httpx.Response:
40
+ return self._request("CONNECT", endpoint, params=params, **kwargs)
41
+
42
+ def _request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
43
+ request = self._prepare_request(method, endpoint, **kwargs)
44
+ response = self._session.send(request, auth=self.prepare_authentication)
45
+ self._info_log_response(response)
46
+
47
+ return response
@@ -0,0 +1,3 @@
1
+ from .date import LATVIAN_MONTH_MAP_GEN, LATVIAN_MONTH_MAP_NOM
2
+
3
+ __all__ = ["LATVIAN_MONTH_MAP_NOM", "LATVIAN_MONTH_MAP_GEN"]
@@ -0,0 +1,28 @@
1
+ LATVIAN_MONTH_MAP_NOM = {
2
+ 1: "janvāris",
3
+ 2: "februāris",
4
+ 3: "marts",
5
+ 4: "aprīlis",
6
+ 5: "maijs",
7
+ 6: "jūnijs",
8
+ 7: "jūlijs",
9
+ 8: "augusts",
10
+ 9: "septembris",
11
+ 10: "oktobris",
12
+ 11: "novembris",
13
+ 12: "decembris",
14
+ }
15
+ LATVIAN_MONTH_MAP_GEN = {
16
+ 1: "janvārī",
17
+ 2: "februārī",
18
+ 3: "martā",
19
+ 4: "aprīlī",
20
+ 5: "maijā",
21
+ 6: "jūnijā",
22
+ 7: "jūlijā",
23
+ 8: "augustā",
24
+ 9: "septembrī",
25
+ 10: "oktobrī",
26
+ 11: "novembrī",
27
+ 12: "decembrī",
28
+ }
@@ -0,0 +1,3 @@
1
+ from .formatters import LogJSONFormatter
2
+
3
+ __all__ = ["LogJSONFormatter"]
@@ -0,0 +1,47 @@
1
+ import datetime
2
+ import json
3
+ import os
4
+ from logging import Formatter, LogRecord
5
+
6
+ from setech.utils import SetechJSONEncoder
7
+
8
+
9
+ class LogJSONFormatter(Formatter):
10
+ default_time_format = "%Y-%m-%d %H:%M:%S"
11
+
12
+ def format(self, record: LogRecord) -> str:
13
+ record_default_keys = [
14
+ "name",
15
+ "msg",
16
+ "args",
17
+ "levelname",
18
+ "levelno",
19
+ "pathname",
20
+ "exc_info",
21
+ "filename",
22
+ "lineno",
23
+ "funcName",
24
+ "created",
25
+ "msecs",
26
+ "relativeCreated",
27
+ "thread",
28
+ "threadName",
29
+ "processName",
30
+ "process",
31
+ "message",
32
+ "asctime",
33
+ "module",
34
+ "exc_text",
35
+ "stack_info",
36
+ ]
37
+ structured_data = dict(
38
+ app=os.environ.get("APP_NAME", "dev"),
39
+ level=record.levelname,
40
+ name=record.name,
41
+ date_time=datetime.datetime.fromtimestamp(record.created).strftime(self.default_time_format),
42
+ location=f"{record.pathname or record.filename}:{record.funcName}:{record.lineno}",
43
+ message=record.getMessage(),
44
+ extra_data={k: record.__dict__[k] for k in record.__dict__.keys() if k not in record_default_keys},
45
+ )
46
+
47
+ return json.dumps(structured_data, cls=SetechJSONEncoder)
File without changes
@@ -0,0 +1,21 @@
1
+ from .numeric import round_decimal
2
+ from .parse import SetechJSONEncoder
3
+ from .ssn import generate_aged_latvian_personal_code, generate_random_latvian_personal_code
4
+ from .text import convert_datetime_to_latvian_words, convert_number_to_latvian_words
5
+ from .validators import validate_iban, validate_latvian_personal_code
6
+ from .various import get_logger, get_nonce, shorten_dict_values, shortify_log_dict
7
+
8
+ __all__ = [
9
+ "round_decimal",
10
+ "SetechJSONEncoder",
11
+ "convert_datetime_to_latvian_words",
12
+ "convert_number_to_latvian_words",
13
+ "generate_aged_latvian_personal_code",
14
+ "generate_random_latvian_personal_code",
15
+ "validate_iban",
16
+ "validate_latvian_personal_code",
17
+ "get_logger",
18
+ "get_nonce",
19
+ "shorten_dict_values",
20
+ "shortify_log_dict",
21
+ ]
@@ -0,0 +1,21 @@
1
+ import decimal
2
+
3
+
4
+ def round_decimal(dec: decimal.Decimal, precision: int = 4) -> decimal.Decimal:
5
+ """
6
+ :param dec: Decimal value to round
7
+ :param precision: how many digits since the start of the value to keep
8
+ dec=123.456, precision=7 -> 123.4560,
9
+ dec=123.456, precision=5 -> 123.46,
10
+ dec=123.456, precision=3 -> 123,
11
+ dec=123.456, precision=2 -> 120;
12
+ :return: rounded Decimal
13
+ """
14
+ with decimal.localcontext() as ctx:
15
+ ctx.rounding = decimal.ROUND_HALF_UP
16
+ value = decimal.Decimal(dec)
17
+ dec_tuple = dec.as_tuple()
18
+ whole_numbers = len(dec_tuple.digits) + dec_tuple.exponent # type: ignore
19
+ ctx.prec = precision + whole_numbers
20
+ value = value * 1
21
+ return value
@@ -0,0 +1,70 @@
1
+ import dataclasses
2
+ import datetime
3
+ import decimal
4
+ import json
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+
9
+ __all__ = [
10
+ "SetechJSONEncoder",
11
+ "str_as_date",
12
+ "str_as_date_or_none",
13
+ "as_decimal",
14
+ "as_decimal_or_none",
15
+ ]
16
+
17
+
18
+ def str_as_date(date_str: str, date_format: str = "%Y-%m-%d") -> datetime.date:
19
+ datetime_object = datetime.datetime.strptime(date_str, date_format)
20
+ return datetime_object.date()
21
+
22
+
23
+ def str_as_date_or_none(date_str: str | None, date_format: str = "%Y-%m-%d") -> datetime.date | None:
24
+ if not isinstance(date_str, str):
25
+ return None
26
+ try:
27
+ return str_as_date(date_str, date_format)
28
+ except (ValueError, TypeError):
29
+ return None
30
+
31
+
32
+ def as_decimal(decimal_str: str | int | float) -> decimal.Decimal:
33
+ return decimal.Decimal(str(decimal_str))
34
+
35
+
36
+ def as_decimal_or_none(decimal_str: str | int | float | None) -> decimal.Decimal | None:
37
+ if not isinstance(decimal_str, (str | int | float)):
38
+ return None
39
+ try:
40
+ return as_decimal(decimal_str)
41
+ except (decimal.DecimalException, TypeError):
42
+ return None
43
+
44
+
45
+ class SetechJSONEncoder(json.JSONEncoder):
46
+ def default(self, obj: Any) -> Any:
47
+ try:
48
+ if isinstance(obj, decimal.Decimal):
49
+ return str(obj)
50
+ if isinstance(obj, (datetime.datetime, datetime.date)):
51
+ return obj.isoformat()
52
+ if isinstance(obj, BaseModel):
53
+ return obj.model_dump_json()
54
+ if isinstance(obj, datetime.timedelta):
55
+ return dict(__type__="timedelta", total_seconds=obj.total_seconds())
56
+ if hasattr(obj, "as_dict"):
57
+ if callable(getattr(obj, "as_dict")):
58
+ return super().default(obj.as_dict())
59
+ return super().default(obj.as_dict)
60
+ if dataclasses.is_dataclass(obj):
61
+ return super().default(dataclasses.asdict(obj))
62
+ if isinstance(obj, set):
63
+ return list(obj)
64
+ if hasattr(obj, "__dict__"):
65
+ return obj.__dict__
66
+ return super().default(obj)
67
+ except TypeError as exc:
68
+ if "not JSON serializable" in str(exc):
69
+ return str(obj)
70
+ raise exc
@@ -0,0 +1,134 @@
1
+ import datetime
2
+ from random import randint
3
+ from typing import NamedTuple
4
+
5
+ __all__ = [
6
+ "generate_random_latvian_personal_code",
7
+ "generate_aged_latvian_personal_code",
8
+ "PersonalCode",
9
+ ]
10
+
11
+ START_LEGACY_DATE: datetime.date = datetime.date(1923, 1, 1)
12
+ FINAL_LEGACY_DATE: datetime.date = datetime.date(2017, 7, 1)
13
+
14
+
15
+ def generate_random_latvian_personal_code(anonymous: bool = False) -> str:
16
+ if anonymous:
17
+ return PersonalCode.generate_anonymous_with_check_digit().dashed
18
+ return PersonalCode.generate_legacy_with_check_digit().dashed
19
+
20
+
21
+ def generate_aged_latvian_personal_code(years: int) -> str:
22
+ return PersonalCode.generate_legacy_with_check_digit(years).dashed
23
+
24
+
25
+ class PersonalCode(NamedTuple):
26
+ first_part: str
27
+ second_part: str
28
+ has_check_digit: bool = False
29
+
30
+ @classmethod
31
+ def generate_anonymous_personal_code(cls) -> "PersonalCode":
32
+ pc = f"3{randint(2 * 10 ** 9, 10 * 10 ** 9 - 1)}"
33
+ first_part = pc[:6]
34
+ second_part = pc[6:]
35
+ instance = cls(first_part, second_part)
36
+ return instance
37
+
38
+ @classmethod
39
+ def generate_anonymous_with_check_digit(cls) -> "PersonalCode":
40
+ tmp = cls.generate_anonymous_personal_code()
41
+ check_digit = tmp._get_checksum_digit()
42
+ if check_digit == 10:
43
+ return tmp.generate_anonymous_with_check_digit()
44
+ second_part = tmp.second_part[:-1] + str(check_digit)
45
+ instance = cls(tmp.first_part, second_part, True)
46
+ return instance
47
+
48
+ @classmethod
49
+ def generate_legacy_with_check_digit(cls, years: int = None) -> "PersonalCode":
50
+ if years is not None:
51
+ if years < (datetime.date.today() - FINAL_LEGACY_DATE).days // 365:
52
+ raise ValueError(
53
+ "Too young for legacy Personal Code! years < "
54
+ f"{(datetime.date.today() - FINAL_LEGACY_DATE).days // 365}"
55
+ )
56
+ min_age_in_days = abs(years) * 365
57
+ max_age_in_days = abs(years + 1) * 365
58
+ birthdate = datetime.date.today() - datetime.timedelta(days=randint(min_age_in_days, max_age_in_days))
59
+ else:
60
+ max_age_in_days = (FINAL_LEGACY_DATE - START_LEGACY_DATE).days
61
+ min_age_in_days = 1
62
+ birthdate = FINAL_LEGACY_DATE - datetime.timedelta(days=randint(min_age_in_days, max_age_in_days))
63
+ return cls._create_from_birthday(birthdate)
64
+
65
+ @classmethod
66
+ def generate_legacy_for_birthday(cls, *args: datetime.date | int) -> "PersonalCode":
67
+ if len(args) == 1:
68
+ if not isinstance(args[0], datetime.date):
69
+ raise ValueError(
70
+ f"Calling method with one parameter, it must be of type `datetime.date` not `{type(args[0])}`"
71
+ )
72
+ birthday = args[0]
73
+ elif len(args) == 3:
74
+ if isinstance(args[0], int) and isinstance(args[1], int) and isinstance(args[2], int):
75
+ birthday = datetime.date(args[0], args[1], args[2])
76
+ else:
77
+ raise ValueError(
78
+ "When calling method with three parameters, "
79
+ f"they all must be of type `int` not `{[type(arg) for arg in args]}`"
80
+ )
81
+ else:
82
+ raise ValueError(
83
+ "Method must be called with either one argument which is `datetime.date` or three int parameters "
84
+ "which represent year, month, day representing dates between "
85
+ f"{START_LEGACY_DATE} and {FINAL_LEGACY_DATE}"
86
+ )
87
+ if not START_LEGACY_DATE <= birthday < FINAL_LEGACY_DATE:
88
+ raise ValueError(
89
+ "Legacy non-anonymous personal codes are generated for "
90
+ f"birthdays since {START_LEGACY_DATE} till {FINAL_LEGACY_DATE}!\n"
91
+ f"{START_LEGACY_DATE} <= {birthday} < {FINAL_LEGACY_DATE}"
92
+ )
93
+ return cls._create_from_birthday(birthday)
94
+
95
+ @classmethod
96
+ def _create_from_birthday(cls, birthday: datetime.date) -> "PersonalCode":
97
+ first_part = f"{birthday.day:02d}{birthday.month:02d}{str(birthday.year)[2:]}"
98
+ century_digit = 2 if birthday.year // 2000 else 1 if birthday.year // 1900 else 0
99
+ second_part = f"{century_digit}{randint(0, 999):03}"
100
+ tmp = cls(first_part, second_part)
101
+ check_digit = tmp._get_checksum_digit()
102
+ if check_digit == 10:
103
+ return cls.generate_legacy_with_check_digit()
104
+ second_part = f"{second_part}{check_digit}"
105
+ return cls(first_part, second_part, True)
106
+
107
+ def as_tuple(self) -> tuple[str, str]:
108
+ return self.first_part, self.second_part
109
+
110
+ def __str__(self) -> str:
111
+ return f"{self.first_part}{self.second_part}"
112
+
113
+ def _get_checksum_digit(self) -> int:
114
+ _factors = [1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
115
+ return (1101 - sum(map(lambda p, f: int(p) * f, str(self)[:10], _factors))) % 11
116
+
117
+ @property
118
+ def dashed(self) -> str:
119
+ return f"{self.first_part}-{self.second_part}"
120
+
121
+ @property
122
+ def is_valid(self) -> bool:
123
+ return str(self._get_checksum_digit()) == self.second_part[-1]
124
+
125
+ @property
126
+ def date_of_birth(self) -> datetime.date:
127
+ if int(self.first_part[:2]) > 31:
128
+ raise ValueError("Unable to get date of birth for anonymous personal codes!")
129
+ return datetime.date(
130
+ (1800 if self.second_part[0] == "0" else 1900 if self.second_part[0] == "1" else 2000)
131
+ + int(self.first_part[4:]),
132
+ int(self.first_part[2:4]),
133
+ int(self.first_part[:2]),
134
+ )
@@ -0,0 +1,35 @@
1
+ import datetime
2
+ import decimal
3
+
4
+ from num2words import num2words # type: ignore
5
+
6
+ from setech.constants import LATVIAN_MONTH_MAP_GEN, LATVIAN_MONTH_MAP_NOM
7
+
8
+
9
+ def convert_number_to_latvian_words(number: decimal.Decimal, with_currency: bool = True) -> str:
10
+ """Convert a number into words in Latvian language."""
11
+ if not number:
12
+ return ""
13
+
14
+ whole_part = int(number)
15
+ fraction_part = round((number - whole_part) * 100)
16
+ text = num2words(whole_part, lang="lv")
17
+
18
+ if with_currency:
19
+ text += f" eiro, {fraction_part:02d} centi"
20
+ else:
21
+ text += f", {fraction_part:02d}"
22
+
23
+ if whole_part in [100, 1000]:
24
+ text = "viens " + text
25
+
26
+ return text
27
+
28
+
29
+ def convert_datetime_to_latvian_words(date: datetime.date | None = None, genitive: bool = False) -> str:
30
+ """Convert a date into words in Latvian language."""
31
+ if date is None:
32
+ date = datetime.date.today()
33
+ date_sign_contract = date.strftime("%Y. gada %d. ")
34
+ date_sign_contract += (LATVIAN_MONTH_MAP_GEN if genitive else LATVIAN_MONTH_MAP_NOM)[date.month]
35
+ return date_sign_contract
@@ -0,0 +1,31 @@
1
+ import re
2
+ from string import ascii_uppercase
3
+
4
+ from setech.utils.ssn import PersonalCode
5
+
6
+
7
+ def validate_iban(iban: str) -> bool:
8
+ # Sanitization and sanity check
9
+ _iban = iban.upper().replace(" ", "")
10
+ if not re.search(r"^[A-Z0-9]{10,32}$", _iban):
11
+ return False
12
+ char_map = {str(i): i for i in range(10)}
13
+ char_map.update({l: i for i, l in enumerate(ascii_uppercase, start=10)})
14
+
15
+ letters = {ord(k): str(v) for k, v in char_map.items()}
16
+
17
+ zeros_iban = _iban[:2] + "00" + _iban[4:]
18
+ iban_inverted = zeros_iban[4:] + zeros_iban[:4]
19
+ iban_numbered = iban_inverted.translate(letters)
20
+
21
+ verification_chars = 98 - (int(iban_numbered) % 97)
22
+
23
+ if f"{int(verification_chars):02}" == _iban[2:4]:
24
+ iban_inverted = _iban[4:] + _iban[:4]
25
+ iban_numbered = iban_inverted.translate(letters)
26
+ return int(iban_numbered) % 97 == 1
27
+ return False
28
+
29
+
30
+ def validate_latvian_personal_code(personal_code: str) -> bool:
31
+ return PersonalCode(personal_code[:6], personal_code[-5:], True).is_valid
@@ -0,0 +1,35 @@
1
+ import dataclasses
2
+ import json
3
+ import logging
4
+ from dataclasses import asdict
5
+ from typing import Any
6
+ from uuid import uuid4
7
+
8
+ from .parse import SetechJSONEncoder
9
+
10
+ __all__ = ["get_logger", "get_nonce", "shorten_dict_values", "shortify_log_dict"]
11
+
12
+
13
+ def get_logger(name: str = "service") -> logging.Logger:
14
+ return logging.getLogger(name)
15
+
16
+
17
+ def shorten_dict_values(dct: dict) -> dict:
18
+ res = {}
19
+ for k, v in dct.items():
20
+ if isinstance(v, str) and len(v) > 64:
21
+ v = f"{v[:30]}...{v[-30:]}"
22
+ elif isinstance(v, dict):
23
+ v = shorten_dict_values(v)
24
+ res[k] = v
25
+ return res
26
+
27
+
28
+ def shortify_log_dict(dct: Any) -> dict[str, Any]:
29
+ if dataclasses.is_dataclass(dct):
30
+ dct = asdict(dct)
31
+ return json.loads(json.dumps(shorten_dict_values(dct), cls=SetechJSONEncoder))
32
+
33
+
34
+ def get_nonce() -> str:
35
+ return uuid4().hex[:12]
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.1
2
+ Name: setech
3
+ Version: 1.0.0
4
+ Summary: Setech utilities
5
+ Author-email: Eriks Karls <eriks.karls@sefinance.lv>
6
+ Project-URL: Homepage, https://pypi.org/project/setech/
7
+ Keywords: setech,logging,api-client,utility,utils
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Requires-Python: ~=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENCE
20
+ Requires-Dist: httpx[http2]~=0.27.0
21
+ Requires-Dist: pydantic~=2.6
22
+ Requires-Dist: num2words~=0.5
23
+
24
+ # Example code
25
+ ```python
26
+ # client.py
27
+ from setech import SyncClient
28
+
29
+
30
+ class LocalClient(SyncClient):
31
+ name = "local"
32
+ _base_url = "https://obligari.serveo.net/ping/local"
33
+
34
+ def __init__(self, nonce=None):
35
+ super().__init__(nonce)
36
+ self._session.headers.update(
37
+ {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:123.0) Gecko/20100101 Firefox/123.0"}
38
+ )
39
+
40
+ def send_post_ping(self, var1: str, var2: int) -> bool:
41
+ res = self.post("/some-post", json={"variable_one": var1, "second_variable": var2})
42
+ return res.json().get("status")
43
+
44
+ def send_put_ping(self, var1: str, var2: int) -> bool:
45
+ res = self.put("/some-put", data={"variable_one": var1, "second_variable": var2})
46
+ return res.json().get("status")
47
+
48
+ def send_get_ping(self, var1: str, var2: int) -> bool:
49
+ res = self.get("/some-get", params={"variable_one": var1, "second_variable": var2})
50
+ return res.json().get("status")
51
+
52
+ def send_patch_ping(self, var1: str, var2: int) -> bool:
53
+ res = self.put("/some-patch", data=(("variable_one", var1), ("variable_one", var2)))
54
+ return res.json().get("status")
55
+
56
+ def send_trace_ping(self, var1: str, var2: int) -> bool:
57
+ res = self.trace("/some-trace", params=(("variable_one", var1), ("variable_one", var2)))
58
+ return res.json().get("status")
59
+ ```
60
+
61
+ ```python
62
+ # main.py
63
+ from .client import LocalClient
64
+
65
+
66
+ client = LocalClient()
67
+ client.send_post_ping("asd", 123)
68
+ client.send_put_ping("asd", 123)
69
+ client.send_get_ping("asd", 123)
70
+ client.send_patch_ping("asd", 123)
71
+ client.send_trace_ping("asd", 123)
72
+ ```
73
+
74
+ ## Log output
75
+ ### Simple
76
+ ```text
77
+ [14d709e02c0c] Preparing POST request to "https://obligari.serveo.net/ping/local/some-post"
78
+ [14d709e02c0c] Sending request with payload=b'{"variable_one": "asd", "second_variable": 123}'
79
+ [14d709e02c0c] Response response.status_code=200 str_repr_content='{"status":true,"request_id":62}'
80
+ [14d709e02c0c] Preparing GET request to "https://obligari.serveo.net/ping/local/some-get"
81
+ [14d709e02c0c] Sending request with payload=None
82
+ [14d709e02c0c] Response response.status_code=200 str_repr_content='{"status":true,"request_id":63}'
83
+ ```
84
+ ### Structured
85
+ ```json
86
+ {"app": "dev", "level": "DEBUG", "name": "APIClient", "date_time": "2024-03-09 22:59:24", "location": "api_client/client.py:_request:71", "message": "[cfbdadc56f53] Preparing POST request to \"https://obligari.serveo.net/ping/local/some-post\"", "extra_data": {"hooks": {"response": []}, "method": "POST", "url": "https://obligari.serveo.net/ping/local/some-post", "headers": {}, "files": [], "data": [], "json": {"variable_one": "asd", "second_variable": 123}, "params": {}, "auth": null, "cookies": null}}
87
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:24", "location": "api_client/client.py:_request:74", "message": "[cfbdadc56f53] Sending request with payload=b'{\"variable_one\": \"asd\", \"second_variable\": 123}'", "extra_data": {"payload": "{\"variable_one\": \"asd\", \"second_variable\": 123}"}}
88
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:81", "message": "[cfbdadc56f53] Response response.status_code=200 str_repr_content='{\"status\":true,\"request_id\":72}'", "extra_data": {"status_code": 200, "content": "{\"status\":true,\"request_id\":72}"}}
89
+ {"app": "dev", "level": "DEBUG", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:71", "message": "[cfbdadc56f53] Preparing GET request to \"https://obligari.serveo.net/ping/local/some-get\"", "extra_data": {"hooks": {"response": []}, "method": "GET", "url": "https://obligari.serveo.net/ping/local/some-get", "headers": {}, "files": [], "data": [], "json": null, "params": {"variable_one": "asd", "second_variable": 123}, "auth": null, "cookies": null}}
90
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:74", "message": "[cfbdadc56f53] Sending request with payload=None", "extra_data": {"payload": "{}"}}
91
+ {"app": "dev", "level": "INFO", "name": "APIClient", "date_time": "2024-03-09 22:59:25", "location": "api_client/client.py:_request:81", "message": "[cfbdadc56f53] Response response.status_code=200 str_repr_content='{\"status\":true,\"request_id\":74}'", "extra_data": {"status_code": 200, "content": "{\"status\":true,\"request_id\":73}"}}
92
+ ```
@@ -0,0 +1,25 @@
1
+ LICENCE
2
+ README.md
3
+ pyproject.toml
4
+ src/setech/__init__.py
5
+ src/setech/py.typed
6
+ src/setech.egg-info/PKG-INFO
7
+ src/setech.egg-info/SOURCES.txt
8
+ src/setech.egg-info/dependency_links.txt
9
+ src/setech.egg-info/requires.txt
10
+ src/setech.egg-info/top_level.txt
11
+ src/setech/api_client/__init__.py
12
+ src/setech/api_client/_base.py
13
+ src/setech/api_client/async_client.py
14
+ src/setech/api_client/sync_client.py
15
+ src/setech/constants/__init__.py
16
+ src/setech/constants/date.py
17
+ src/setech/logging/__init__.py
18
+ src/setech/logging/formatters.py
19
+ src/setech/utils/__init__.py
20
+ src/setech/utils/numeric.py
21
+ src/setech/utils/parse.py
22
+ src/setech/utils/ssn.py
23
+ src/setech/utils/text.py
24
+ src/setech/utils/validators.py
25
+ src/setech/utils/various.py
@@ -0,0 +1,3 @@
1
+ httpx[http2]~=0.27.0
2
+ pydantic~=2.6
3
+ num2words~=0.5
@@ -0,0 +1 @@
1
+ setech