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 +7 -0
- setech-1.0.0/PKG-INFO +92 -0
- setech-1.0.0/README.md +69 -0
- setech-1.0.0/pyproject.toml +83 -0
- setech-1.0.0/setup.cfg +4 -0
- setech-1.0.0/src/setech/__init__.py +4 -0
- setech-1.0.0/src/setech/api_client/__init__.py +4 -0
- setech-1.0.0/src/setech/api_client/_base.py +124 -0
- setech-1.0.0/src/setech/api_client/async_client.py +46 -0
- setech-1.0.0/src/setech/api_client/sync_client.py +47 -0
- setech-1.0.0/src/setech/constants/__init__.py +3 -0
- setech-1.0.0/src/setech/constants/date.py +28 -0
- setech-1.0.0/src/setech/logging/__init__.py +3 -0
- setech-1.0.0/src/setech/logging/formatters.py +47 -0
- setech-1.0.0/src/setech/py.typed +0 -0
- setech-1.0.0/src/setech/utils/__init__.py +21 -0
- setech-1.0.0/src/setech/utils/numeric.py +21 -0
- setech-1.0.0/src/setech/utils/parse.py +70 -0
- setech-1.0.0/src/setech/utils/ssn.py +134 -0
- setech-1.0.0/src/setech/utils/text.py +35 -0
- setech-1.0.0/src/setech/utils/validators.py +31 -0
- setech-1.0.0/src/setech/utils/various.py +35 -0
- setech-1.0.0/src/setech.egg-info/PKG-INFO +92 -0
- setech-1.0.0/src/setech.egg-info/SOURCES.txt +25 -0
- setech-1.0.0/src/setech.egg-info/dependency_links.txt +1 -0
- setech-1.0.0/src/setech.egg-info/requires.txt +3 -0
- setech-1.0.0/src/setech.egg-info/top_level.txt +1 -0
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,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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
setech
|