potato-util 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ from .__version__ import __version__
2
+
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
potato_util/_base.py ADDED
@@ -0,0 +1,117 @@
1
+ import re
2
+ import copy
3
+ import logging
4
+
5
+ from pydantic import validate_call
6
+
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ @validate_call
12
+ def deep_merge(dict1: dict, dict2: dict) -> dict:
13
+ """Return a new dictionary that's the result of a deep merge of two dictionaries.
14
+ If there are conflicts, values from `dict2` will overwrite those in `dict1`.
15
+
16
+ Args:
17
+ dict1 (dict, required): The base dictionary that will be merged.
18
+ dict2 (dict, required): The dictionary to merge into `dict1`.
19
+
20
+ Returns:
21
+ dict: The merged dictionary.
22
+ """
23
+
24
+ _merged = copy.deepcopy(dict1)
25
+ for _key, _val in dict2.items():
26
+ if (
27
+ _key in _merged
28
+ and isinstance(_merged[_key], dict)
29
+ and isinstance(_val, dict)
30
+ ):
31
+ _merged[_key] = deep_merge(_merged[_key], _val)
32
+ else:
33
+ _merged[_key] = copy.deepcopy(_val)
34
+
35
+ return _merged
36
+
37
+
38
+ @validate_call
39
+ def camel_to_snake(val: str) -> str:
40
+ """Convert CamelCase to snake_case.
41
+
42
+ Args:
43
+ val (str): CamelCase string to convert.
44
+
45
+ Returns:
46
+ str: Converted snake_case string.
47
+ """
48
+
49
+ val = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", val)
50
+ val = re.sub("([a-z0-9])([A-Z])", r"\1_\2", val).lower()
51
+ return val
52
+
53
+
54
+ @validate_call
55
+ def clean_obj_dict(obj_dict: dict, cls_name: str) -> dict:
56
+ """Clean class name from object.__dict__ for str(object).
57
+
58
+ Args:
59
+ obj_dict (dict, required): Object dictionary by object.__dict__.
60
+ cls_name (str , required): Class name by cls.__name__.
61
+
62
+ Returns:
63
+ dict: Clean object dictionary.
64
+ """
65
+
66
+ try:
67
+ if not obj_dict:
68
+ raise ValueError("'obj_dict' argument value is empty!")
69
+
70
+ if not cls_name:
71
+ raise ValueError("'cls_name' argument value is empty!")
72
+ except ValueError as err:
73
+ logger.error(err)
74
+ raise
75
+
76
+ _self_dict = obj_dict.copy()
77
+ for _key in _self_dict.copy():
78
+ _class_prefix = f"_{cls_name}__"
79
+ if _key.startswith(_class_prefix):
80
+ _new_key = _key.replace(_class_prefix, "")
81
+ _self_dict[_new_key] = _self_dict.pop(_key)
82
+ return _self_dict
83
+
84
+
85
+ @validate_call(config={"arbitrary_types_allowed": True})
86
+ def obj_to_repr(obj: object) -> str:
87
+ """Modifying object default repr() to custom info.
88
+
89
+ Args:
90
+ obj (object, required): Any python object.
91
+
92
+ Returns:
93
+ str: String for repr() method.
94
+ """
95
+
96
+ try:
97
+ if not obj:
98
+ raise ValueError("'obj' argument value is empty!")
99
+ except ValueError as err:
100
+ logger.error(err)
101
+ raise
102
+
103
+ _self_repr = (
104
+ f"<{obj.__class__.__module__}.{obj.__class__.__name__} object at {hex(id(obj))}: "
105
+ + "{"
106
+ + f"{str(dir(obj)).replace('[', '').replace(']', '')}"
107
+ + "}>"
108
+ )
109
+ return _self_repr
110
+
111
+
112
+ __all__ = [
113
+ "deep_merge",
114
+ "camel_to_snake",
115
+ "clean_obj_dict",
116
+ "obj_to_repr",
117
+ ]
@@ -0,0 +1,5 @@
1
+ # flake8: noqa
2
+
3
+ from ._base import *
4
+ from ._enum import *
5
+ from ._regex import *
@@ -0,0 +1,5 @@
1
+ MAX_PATH_LENGTH = 1024
2
+
3
+ __all__ = [
4
+ "MAX_PATH_LENGTH",
5
+ ]
@@ -0,0 +1,31 @@
1
+ from enum import Enum
2
+
3
+
4
+ class WarnEnum(str, Enum):
5
+ ERROR = "ERROR"
6
+ ALWAYS = "ALWAYS"
7
+ DEBUG = "DEBUG"
8
+ IGNORE = "IGNORE"
9
+
10
+
11
+ class TSUnitEnum(str, Enum):
12
+ SECONDS = "SECONDS"
13
+ MILLISECONDS = "MILLISECONDS"
14
+ MICROSECONDS = "MICROSECONDS"
15
+ NANOSECONDS = "NANOSECONDS"
16
+
17
+
18
+ class HashAlgoEnum(str, Enum):
19
+ md5 = "md5"
20
+ sha1 = "sha1"
21
+ sha224 = "sha224"
22
+ sha256 = "sha256"
23
+ sha384 = "sha384"
24
+ sha512 = "sha512"
25
+
26
+
27
+ __all__ = [
28
+ "WarnEnum",
29
+ "TSUnitEnum",
30
+ "HashAlgoEnum",
31
+ ]
@@ -0,0 +1,23 @@
1
+ REQUEST_ID_REGEX = (
2
+ r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b|"
3
+ r"\b[0-9a-fA-F]{32}\b"
4
+ )
5
+
6
+ # Invalid characters:
7
+ SPECIAL_CHARS_REGEX = r"[&'\"<>]"
8
+ SPECIAL_CHARS_BASE_REGEX = r"[&'\"<>\\\/]"
9
+ SPECIAL_CHARS_LOW_REGEX = r"[&'\"<>\\\/`{}|]"
10
+ SPECIAL_CHARS_MEDIUM_REGEX = r"[&'\"<>\\\/`{}|()\[\]]"
11
+ SPECIAL_CHARS_HIGH_REGEX = r"[&'\"<>\\\/`{}|()\[\]!@#$%^*;:?]"
12
+ SPECIAL_CHARS_STRICT_REGEX = r"[&'\"<>\\\/`{}|()\[\]~!@#$%^*_=\-+;:,.?\t\n ]"
13
+
14
+
15
+ __all__ = [
16
+ "REQUEST_ID_REGEX",
17
+ "SPECIAL_CHARS_REGEX",
18
+ "SPECIAL_CHARS_BASE_REGEX",
19
+ "SPECIAL_CHARS_LOW_REGEX",
20
+ "SPECIAL_CHARS_MEDIUM_REGEX",
21
+ "SPECIAL_CHARS_HIGH_REGEX",
22
+ "SPECIAL_CHARS_STRICT_REGEX",
23
+ ]
potato_util/dt.py ADDED
@@ -0,0 +1,240 @@
1
+ import time
2
+ import logging
3
+ from zoneinfo import ZoneInfo
4
+ from datetime import datetime, timezone, tzinfo, timedelta
5
+
6
+ from pydantic import validate_call
7
+
8
+ from .constants import WarnEnum, TSUnitEnum
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @validate_call(config={"arbitrary_types_allowed": True})
15
+ def add_tzinfo(dt: datetime, tz: ZoneInfo | tzinfo | str) -> datetime:
16
+ """Add or replace timezone info to datetime object.
17
+
18
+ Args:
19
+ dt (datetime , required): Datetime object.
20
+ tz (Union[ZoneInfo, tzinfo, str], required): Timezone info.
21
+
22
+ Returns:
23
+ datetime: Datetime object with timezone info.
24
+ """
25
+
26
+ if isinstance(tz, str):
27
+ tz = ZoneInfo(tz)
28
+
29
+ dt = dt.replace(tzinfo=tz)
30
+ return dt
31
+
32
+
33
+ @validate_call
34
+ def datetime_to_iso(
35
+ dt: datetime, sep: str = "T", warn_mode: WarnEnum = WarnEnum.IGNORE
36
+ ) -> str:
37
+ """Convert datetime object to ISO 8601 format.
38
+
39
+ Args:
40
+ dt (datetime, required): Datetime object.
41
+ sep (str , optional): Separator between date and time. Defaults to "T".
42
+ warn_mode (WarnEnum, optional): Warning mode. Defaults to WarnEnum.IGNORE.
43
+
44
+ Raises:
45
+ ValueError: If `sep` argument length is greater than 8.
46
+ ValueError: If `dt` argument doesn't have any timezone info and `warn_mode` is set to WarnEnum.ERROR.
47
+
48
+ Returns:
49
+ str: Datetime string in ISO 8601 format.
50
+ """
51
+
52
+ sep = sep.strip()
53
+ if 8 < len(sep):
54
+ raise ValueError(
55
+ f"`sep` argument length '{len(sep)}' is too long, must be less than or equal to 8!"
56
+ )
57
+
58
+ if not dt.tzinfo:
59
+ _message = "Not found any timezone info in `dt` argument, assuming it's UTC timezone..."
60
+ if warn_mode == WarnEnum.ALWAYS:
61
+ logger.warning(_message)
62
+ elif warn_mode == WarnEnum.DEBUG:
63
+ logger.debug(_message)
64
+ elif warn_mode == WarnEnum.ERROR:
65
+ _message = "Not found any timezone info in `dt` argument!"
66
+ logger.error(_message)
67
+ raise ValueError(_message)
68
+
69
+ dt = add_tzinfo(dt=dt, tz="UTC")
70
+
71
+ _dt_str = dt.isoformat(sep=sep, timespec="milliseconds")
72
+ return _dt_str
73
+
74
+
75
+ @validate_call(config={"arbitrary_types_allowed": True})
76
+ def convert_tz(
77
+ dt: datetime, tz: ZoneInfo | tzinfo | str, warn_mode: WarnEnum = WarnEnum.ALWAYS
78
+ ) -> datetime:
79
+ """Convert datetime object to another timezone.
80
+
81
+ Args:
82
+ dt (datetime , required): Datetime object to convert.
83
+ tz (Union[ZoneInfo, tzinfo, str], required): Timezone info to convert.
84
+ warn_mode (WarnEnum , optional): Warning mode. Defaults to WarnEnum.ALWAYS.
85
+
86
+ Raises:
87
+ ValueError: If `dt` argument doesn't have any timezone info and `warn_mode` is set to WarnEnum.ERROR.
88
+
89
+ Returns:
90
+ datetime: Datetime object which has been converted to another timezone.
91
+ """
92
+
93
+ if not dt.tzinfo:
94
+ _message = "Not found any timezone info in `dt` argument, assuming it's UTC timezone..."
95
+ if warn_mode == WarnEnum.ALWAYS:
96
+ logger.warning(_message)
97
+ elif warn_mode == WarnEnum.DEBUG:
98
+ logger.debug(_message)
99
+ elif warn_mode == WarnEnum.ERROR:
100
+ _message = "Not found any timezone info in `dt` argument!"
101
+ logger.error(_message)
102
+ raise ValueError(_message)
103
+
104
+ dt = add_tzinfo(dt=dt, tz="UTC")
105
+
106
+ if isinstance(tz, str):
107
+ tz = ZoneInfo(tz)
108
+
109
+ dt = dt.astimezone(tz=tz)
110
+ return dt
111
+
112
+
113
+ def now_utc_dt() -> datetime:
114
+ """Get current datetime in UTC timezone with tzinfo.
115
+
116
+ Returns:
117
+ datetime: Current datetime in UTC timezone with tzinfo.
118
+ """
119
+
120
+ _utc_dt = datetime.now(tz=timezone.utc)
121
+ return _utc_dt
122
+
123
+
124
+ def now_local_dt() -> datetime:
125
+ """Get current datetime in local timezone with tzinfo.
126
+
127
+ Returns:
128
+ datetime: Current datetime in local timezone with tzinfo.
129
+ """
130
+
131
+ _local_dt = datetime.now().astimezone()
132
+ return _local_dt
133
+
134
+
135
+ @validate_call(config={"arbitrary_types_allowed": True})
136
+ def now_dt(tz: ZoneInfo | tzinfo | str) -> datetime:
137
+ """Get current datetime in specified timezone with tzinfo.
138
+
139
+ Args:
140
+ tz (Union[ZoneInfo, tzinfo, str], required): Timezone info.
141
+
142
+ Returns:
143
+ datetime: Current datetime in specified timezone with tzinfo.
144
+ """
145
+
146
+ _dt = now_utc_dt()
147
+ _dt = convert_tz(dt=_dt, tz=tz)
148
+ return _dt
149
+
150
+
151
+ @validate_call
152
+ def now_ts(unit: TSUnitEnum = TSUnitEnum.SECONDS) -> int:
153
+ """Get current timestamp in UTC timezone.
154
+
155
+ Args:
156
+ unit (TSUnitEnum, optional): Type of timestamp unit. Defaults to `TSUnitEnum.SECONDS`.
157
+
158
+ Returns:
159
+ int: Current timestamp.
160
+ """
161
+
162
+ _now_ts: int
163
+ if unit == TSUnitEnum.SECONDS:
164
+ _now_ts = int(time.time())
165
+ elif unit == TSUnitEnum.MILLISECONDS:
166
+ _now_ts = int(time.time() * 1000)
167
+ elif unit == TSUnitEnum.MICROSECONDS:
168
+ _now_ts = int(time.time_ns() / 1000)
169
+ elif unit == TSUnitEnum.NANOSECONDS:
170
+ _now_ts = int(time.time_ns())
171
+
172
+ return _now_ts
173
+
174
+
175
+ @validate_call
176
+ def convert_ts(dt: datetime, unit: TSUnitEnum = TSUnitEnum.SECONDS) -> int:
177
+ """Convert datetime to timestamp.
178
+
179
+ Args:
180
+ dt (datetime , required): Datetime object to convert.
181
+ unit (TSUnitEnum, optional): Type of timestamp unit. Defaults to `TSUnitEnum.SECONDS`.
182
+
183
+ Returns:
184
+ int: Converted timestamp.
185
+ """
186
+
187
+ _ts: int
188
+ if unit == TSUnitEnum.SECONDS:
189
+ _ts = int(dt.timestamp())
190
+ elif unit == TSUnitEnum.MILLISECONDS:
191
+ _ts = int(dt.timestamp() * 1000)
192
+ elif unit == TSUnitEnum.MICROSECONDS:
193
+ _ts = int(dt.timestamp() * 1000000)
194
+ elif unit == TSUnitEnum.NANOSECONDS:
195
+ _ts = int(dt.timestamp() * 1000000000)
196
+
197
+ return _ts
198
+
199
+
200
+ @validate_call(config={"arbitrary_types_allowed": True})
201
+ def calc_future_dt(
202
+ delta: timedelta | int,
203
+ dt: datetime | None = None,
204
+ tz: ZoneInfo | tzinfo | str | None = None,
205
+ ) -> datetime:
206
+ """Calculate future datetime by adding delta time to current or specified datetime.
207
+
208
+ Args:
209
+ delta (Union[timedelta, int] , required): Delta time to add to current or specified datetime.
210
+ dt (Optional[datetime] , optional): Datetime before adding delta time. Defaults to None.
211
+ tz (Union[ZoneInfo, tzinfo, str, None], optional): Timezone info. Defaults to None.
212
+
213
+ Returns:
214
+ datetime: Calculated future datetime.
215
+ """
216
+
217
+ if not dt:
218
+ dt = now_utc_dt()
219
+
220
+ if tz:
221
+ dt = convert_tz(dt=dt, tz=tz)
222
+
223
+ if isinstance(delta, int):
224
+ delta = timedelta(seconds=delta)
225
+
226
+ _future_dt = dt + delta
227
+ return _future_dt
228
+
229
+
230
+ __all__ = [
231
+ "add_tzinfo",
232
+ "datetime_to_iso",
233
+ "convert_tz",
234
+ "now_utc_dt",
235
+ "now_local_dt",
236
+ "now_dt",
237
+ "now_ts",
238
+ "convert_ts",
239
+ "calc_future_dt",
240
+ ]
@@ -0,0 +1,12 @@
1
+ # flake8: noqa
2
+
3
+ import importlib.util
4
+
5
+ from ._base import *
6
+ from ._sync import *
7
+
8
+ _async_package_name = "aiohttp"
9
+ _async_spec = importlib.util.find_spec(_async_package_name)
10
+
11
+ if _async_spec is not None:
12
+ from ._async import *
@@ -0,0 +1,42 @@
1
+ import aiohttp
2
+ from pydantic import validate_call, AnyHttpUrl
3
+
4
+
5
+ @validate_call
6
+ async def async_is_connectable(
7
+ url: AnyHttpUrl = AnyHttpUrl("https://www.google.com"),
8
+ timeout: int = 3,
9
+ check_status: bool = False,
10
+ ) -> bool:
11
+ """Check if the url is connectable.
12
+
13
+ Args:
14
+ url (AnyHttpUrl, optional): URL to check. Defaults to 'https://www.google.com'.
15
+ timeout (int , optional): Timeout in seconds. Defaults to 3.
16
+ check_status (bool , optional): Check HTTP status code (200). Defaults to False.
17
+
18
+ Raise:
19
+ ValueError: If `timeout` is less than 1.
20
+
21
+ Returns:
22
+ bool: True if connectable, False otherwise.
23
+ """
24
+
25
+ if timeout < 1:
26
+ raise ValueError(
27
+ f"`timeout` argument value {timeout} is invalid, must be greater than 0!"
28
+ )
29
+
30
+ try:
31
+ async with aiohttp.ClientSession() as _session:
32
+ async with _session.get(str(url), timeout=timeout) as _response:
33
+ if check_status:
34
+ return _response.status == 200
35
+ return True
36
+ except Exception:
37
+ return False
38
+
39
+
40
+ __all__ = [
41
+ "async_is_connectable",
42
+ ]
@@ -0,0 +1,46 @@
1
+ from http import HTTPStatus
2
+
3
+ from pydantic import validate_call
4
+
5
+
6
+ @validate_call
7
+ def get_http_status(status_code: int) -> tuple[HTTPStatus, bool]:
8
+ """Get HTTP status code enum from integer value.
9
+
10
+ Args:
11
+ status_code (int, required): Status code for HTTP response: [100 <= status_code <= 599].
12
+
13
+ Raises:
14
+ ValueError: If status code is not in range [100 <= status_code <= 599].
15
+
16
+ Returns:
17
+ Tuple[HTTPStatus, bool]: Tuple of HTTP status code enum and boolean value if status code is known.
18
+ """
19
+
20
+ _http_status: HTTPStatus
21
+ _is_known_status = False
22
+ try:
23
+ _http_status = HTTPStatus(status_code)
24
+ _is_known_status = True
25
+ except ValueError:
26
+ if (100 <= status_code) and (status_code < 200):
27
+ status_code = 100
28
+ elif (200 <= status_code) and (status_code < 300):
29
+ status_code = 200
30
+ elif (300 <= status_code) and (status_code < 400):
31
+ status_code = 304
32
+ elif (400 <= status_code) and (status_code < 500):
33
+ status_code = 400
34
+ elif (500 <= status_code) and (status_code < 600):
35
+ status_code = 500
36
+ else:
37
+ raise ValueError(f"Invalid HTTP status code: '{status_code}'!")
38
+
39
+ _http_status = HTTPStatus(status_code)
40
+
41
+ return (_http_status, _is_known_status)
42
+
43
+
44
+ __all__ = [
45
+ "get_http_status",
46
+ ]
@@ -0,0 +1,45 @@
1
+ from urllib import request
2
+ from http.client import HTTPResponse
3
+
4
+ from pydantic import validate_call, AnyHttpUrl
5
+
6
+
7
+ @validate_call
8
+ def is_connectable(
9
+ url: AnyHttpUrl = AnyHttpUrl("https://www.google.com"),
10
+ timeout: int = 3,
11
+ check_status: bool = False,
12
+ ) -> bool:
13
+ """Check if the url is connectable.
14
+
15
+ Args:
16
+ url (AnyHttpUrl, optional): URL to check. Defaults to 'https://www.google.com'.
17
+ timeout (int , optional): Timeout in seconds. Defaults to 3.
18
+ check_status (bool , optional): Check HTTP status code (200). Defaults to False.
19
+
20
+ Raise:
21
+ ValueError: If `timeout` is less than 1.
22
+
23
+ Returns:
24
+ bool: True if connectable, False otherwise.
25
+ """
26
+
27
+ if timeout < 1:
28
+ raise ValueError(
29
+ f"`timeout` argument value {timeout} is invalid, must be greater than 0!"
30
+ )
31
+
32
+ try:
33
+ _response: HTTPResponse = request.urlopen(
34
+ str(url), timeout=timeout
35
+ ) # nosec B310
36
+ if check_status:
37
+ return _response.getcode() == 200
38
+ return True
39
+ except Exception:
40
+ return False
41
+
42
+
43
+ __all__ = [
44
+ "is_connectable",
45
+ ]
@@ -0,0 +1,26 @@
1
+ from pydantic import validate_call
2
+ from starlette.datastructures import URL
3
+ from fastapi import Request
4
+
5
+
6
+ @validate_call(config={"arbitrary_types_allowed": True})
7
+ def get_relative_url(val: Request | URL) -> str:
8
+ """Get relative url only path with query params from request object or URL object.
9
+
10
+ Args:
11
+ val (Union[Request, URL]): Request object or URL object to extract relative url.
12
+
13
+ Returns:
14
+ str: Relative url only path with query params.
15
+ """
16
+
17
+ if isinstance(val, Request):
18
+ val = val.url
19
+
20
+ _relative_url = str(val).replace(f"{val.scheme}://{val.netloc}", "")
21
+ return _relative_url
22
+
23
+
24
+ __all__ = [
25
+ "get_relative_url",
26
+ ]
@@ -0,0 +1,11 @@
1
+ # flake8: noqa
2
+
3
+ import importlib.util
4
+
5
+ from ._sync import *
6
+
7
+ _async_package_name = "aiofiles"
8
+ _async_spec = importlib.util.find_spec(_async_package_name)
9
+
10
+ if _async_spec is not None:
11
+ from ._async import *