karrio 2023.5.1__py3-none-any.whl → 2025.5rc1__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.
- karrio/__init__.py +0 -100
- karrio/addons/renderer.py +1 -1
- karrio/api/gateway.py +58 -35
- karrio/api/interface.py +41 -4
- karrio/api/mapper.py +39 -0
- karrio/api/proxy.py +18 -5
- karrio/core/__init__.py +5 -1
- karrio/core/errors.py +6 -5
- karrio/core/metadata.py +113 -20
- karrio/core/models.py +66 -5
- karrio/core/plugins.py +606 -0
- karrio/core/settings.py +39 -2
- karrio/core/units.py +639 -32
- karrio/core/utils/__init__.py +1 -1
- karrio/core/utils/datetime.py +75 -13
- karrio/core/utils/dict.py +5 -0
- karrio/core/utils/enum.py +132 -34
- karrio/core/utils/helpers.py +92 -35
- karrio/core/utils/number.py +52 -8
- karrio/core/utils/string.py +52 -1
- karrio/core/utils/transformer.py +12 -5
- karrio/core/validators.py +88 -0
- karrio/lib.py +241 -15
- karrio/plugins/__init__.py +6 -0
- karrio/references.py +652 -67
- karrio/schemas/__init__.py +2 -0
- karrio/sdk.py +102 -0
- karrio/universal/mappers/rating_proxy.py +35 -9
- karrio/validators/__init__.py +6 -0
- {karrio-2023.5.1.dist-info → karrio-2025.5rc1.dist-info}/METADATA +13 -15
- karrio-2025.5rc1.dist-info/RECORD +57 -0
- {karrio-2023.5.1.dist-info → karrio-2025.5rc1.dist-info}/WHEEL +1 -1
- {karrio-2023.5.1.dist-info → karrio-2025.5rc1.dist-info}/top_level.txt +1 -0
- karrio-2023.5.1.dist-info/RECORD +0 -51
karrio/core/utils/__init__.py
CHANGED
@@ -7,7 +7,7 @@ from karrio.core.utils.datetime import DATEFORMAT as DF
|
|
7
7
|
from karrio.core.utils.xml import XMLPARSER as XP, Element
|
8
8
|
from karrio.core.utils.serializable import Serializable, Deserializable
|
9
9
|
from karrio.core.utils.pipeline import Pipeline, Job
|
10
|
-
from karrio.core.utils.enum import Enum, Flag, OptionEnum, svcEnum
|
10
|
+
from karrio.core.utils.enum import Enum, Flag, StrEnum, OptionEnum, svcEnum
|
11
11
|
from karrio.core.utils.tracing import Tracer, Record, Trace
|
12
12
|
from karrio.core.utils.transformer import to_multi_piece_rates, to_multi_piece_shipment
|
13
13
|
from karrio.core.utils.caching import Cache
|
karrio/core/utils/datetime.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
|
2
|
-
from datetime import datetime
|
1
|
+
import typing
|
2
|
+
from datetime import datetime, timedelta, timezone
|
3
3
|
|
4
4
|
|
5
5
|
class DATEFORMAT:
|
6
6
|
@staticmethod
|
7
7
|
def date(
|
8
|
-
date_value: Union[str, int, datetime] = None,
|
8
|
+
date_value: typing.Union[str, int, datetime] = None,
|
9
9
|
current_format: str = "%Y-%m-%d",
|
10
|
-
try_formats: List[str] = None,
|
11
|
-
) -> datetime:
|
10
|
+
try_formats: typing.List[str] = None,
|
11
|
+
) -> typing.Optional[datetime]:
|
12
12
|
if date_value is None:
|
13
13
|
return None
|
14
14
|
|
@@ -30,11 +30,58 @@ class DATEFORMAT:
|
|
30
30
|
|
31
31
|
return datetime.strptime(str(date_value), current_format)
|
32
32
|
|
33
|
+
@staticmethod
|
34
|
+
def next_business_datetime(
|
35
|
+
date_value: typing.Union[str, datetime] = None,
|
36
|
+
current_format: str = "%Y-%m-%d %H:%M:%S",
|
37
|
+
try_formats: typing.List[str] = None,
|
38
|
+
start_hour: int = 10,
|
39
|
+
end_hour: int = 17,
|
40
|
+
) -> typing.Optional[datetime]:
|
41
|
+
date = DATEFORMAT.date(
|
42
|
+
date_value, current_format=current_format, try_formats=try_formats
|
43
|
+
)
|
44
|
+
if date is None:
|
45
|
+
return None
|
46
|
+
|
47
|
+
# Define business hours
|
48
|
+
_start_hour = start_hour
|
49
|
+
_end_hour = end_hour
|
50
|
+
|
51
|
+
# If date has no time component, set it to current time
|
52
|
+
if date.hour == 0 and date.minute == 0 and date.second == 0:
|
53
|
+
now = datetime.now()
|
54
|
+
date = date.replace(hour=now.hour, minute=now.minute, second=now.second)
|
55
|
+
|
56
|
+
# If the given datetime is within business hours, return it
|
57
|
+
if DATEFORMAT.is_business_hour(date):
|
58
|
+
return date
|
59
|
+
|
60
|
+
# If it's outside business hours, calculate the next business datetime
|
61
|
+
if date.weekday() >= 5: # If it's Saturday or Sunday
|
62
|
+
# Move to the next Monday
|
63
|
+
days_to_add = 7 - date.weekday()
|
64
|
+
next_business_day = date + timedelta(days=days_to_add)
|
65
|
+
return next_business_day.replace(
|
66
|
+
hour=_start_hour, minute=0, second=0, microsecond=0
|
67
|
+
)
|
68
|
+
elif date.hour >= _end_hour: # If it's after business hours
|
69
|
+
# Move to the next business day
|
70
|
+
next_business_day = date + timedelta(days=1)
|
71
|
+
if next_business_day.weekday() >= 5: # If it's Saturday or Sunday
|
72
|
+
days_to_add = 7 - next_business_day.weekday()
|
73
|
+
next_business_day += timedelta(days=days_to_add)
|
74
|
+
return next_business_day.replace(
|
75
|
+
hour=_start_hour, minute=0, second=0, microsecond=0
|
76
|
+
)
|
77
|
+
else: # If it's before business hours
|
78
|
+
return date.replace(hour=_start_hour, minute=0, second=0, microsecond=0)
|
79
|
+
|
33
80
|
@staticmethod
|
34
81
|
def fdate(
|
35
|
-
date_str: Union[str, int, datetime] = None,
|
82
|
+
date_str: typing.Union[str, int, datetime] = None,
|
36
83
|
current_format: str = "%Y-%m-%d",
|
37
|
-
try_formats: List[str] = None,
|
84
|
+
try_formats: typing.List[str] = None,
|
38
85
|
):
|
39
86
|
date = DATEFORMAT.date(
|
40
87
|
date_str, current_format=current_format, try_formats=try_formats
|
@@ -45,13 +92,15 @@ class DATEFORMAT:
|
|
45
92
|
|
46
93
|
@staticmethod
|
47
94
|
def fdatetime(
|
48
|
-
date_str: Union[str, int, datetime] = None,
|
95
|
+
date_str: typing.Union[str, int, datetime] = None,
|
49
96
|
current_format: str = "%Y-%m-%d %H:%M:%S",
|
50
97
|
output_format: str = "%Y-%m-%d %H:%M:%S",
|
51
|
-
try_formats: List[str] = None,
|
98
|
+
try_formats: typing.List[str] = None,
|
52
99
|
):
|
53
100
|
date = DATEFORMAT.date(
|
54
|
-
date_str,
|
101
|
+
date_str,
|
102
|
+
current_format=current_format,
|
103
|
+
try_formats=try_formats,
|
55
104
|
)
|
56
105
|
if date is None:
|
57
106
|
return None
|
@@ -62,7 +111,7 @@ class DATEFORMAT:
|
|
62
111
|
time_str: str,
|
63
112
|
current_format: str = "%H:%M:%S",
|
64
113
|
output_format: str = "%H:%M",
|
65
|
-
try_formats: List[str] = None,
|
114
|
+
try_formats: typing.List[str] = None,
|
66
115
|
):
|
67
116
|
time = DATEFORMAT.date(
|
68
117
|
time_str, current_format=current_format, try_formats=try_formats
|
@@ -72,7 +121,20 @@ class DATEFORMAT:
|
|
72
121
|
return time.strftime(output_format)
|
73
122
|
|
74
123
|
@staticmethod
|
75
|
-
def ftimestamp(timestamp: Union[str, int] = None):
|
124
|
+
def ftimestamp(timestamp: typing.Union[str, int] = None):
|
76
125
|
if timestamp is None:
|
77
126
|
return None
|
78
|
-
return datetime.
|
127
|
+
return datetime.fromtimestamp(float(timestamp), timezone.utc).strftime("%H:%M")
|
128
|
+
|
129
|
+
@staticmethod
|
130
|
+
def is_business_hour(dt: datetime):
|
131
|
+
# Define business hours
|
132
|
+
start_hour = 9
|
133
|
+
end_hour = 17
|
134
|
+
|
135
|
+
# Check if the given datetime is within business hours
|
136
|
+
if dt.weekday() >= 5: # 5 and 6 correspond to Saturday and Sunday
|
137
|
+
return False
|
138
|
+
if dt.hour < start_hour or dt.hour >= end_hour:
|
139
|
+
return False
|
140
|
+
return True
|
karrio/core/utils/dict.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import re
|
1
2
|
import enum
|
2
3
|
import attr
|
3
4
|
import json
|
@@ -50,6 +51,10 @@ class DICTPARSE:
|
|
50
51
|
:return: a dictionary.
|
51
52
|
"""
|
52
53
|
_clear_empty = clear_empty is not False
|
54
|
+
if isinstance(entity, str):
|
55
|
+
entity = re.sub(",[ \t\r\n]+}", "}", entity)
|
56
|
+
entity = re.sub(r",[ \t\r\n]+\]", "]", entity)
|
57
|
+
|
53
58
|
return json.loads(
|
54
59
|
(
|
55
60
|
DICTPARSE.jsonify(entity)
|
karrio/core/utils/enum.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
import attr
|
2
|
-
|
3
|
-
|
2
|
+
import enum
|
3
|
+
import typing
|
4
4
|
|
5
|
+
BaseStrEnum = getattr(enum, "StrEnum", enum.Flag)
|
5
6
|
|
6
|
-
|
7
|
+
|
8
|
+
class MetaEnum(enum.EnumMeta):
|
7
9
|
def __contains__(cls, item):
|
8
10
|
if item is None:
|
9
11
|
return False
|
@@ -12,15 +14,21 @@ class MetaEnum(EnumMeta):
|
|
12
14
|
|
13
15
|
return super().__contains__(item)
|
14
16
|
|
15
|
-
def map(cls, key: Any):
|
17
|
+
def map(cls, key: typing.Any):
|
16
18
|
if key in cls:
|
17
19
|
return EnumWrapper(key, cls[key])
|
18
|
-
elif key in cast(Any, cls)._value2member_map_:
|
20
|
+
elif key in typing.cast(typing.Any, cls)._value2member_map_:
|
19
21
|
return EnumWrapper(key, cls(key))
|
20
|
-
elif key in [
|
22
|
+
elif key in [
|
23
|
+
str(v.value) for v in typing.cast(typing.Any, cls).__members__.values()
|
24
|
+
]:
|
21
25
|
return EnumWrapper(
|
22
26
|
key,
|
23
|
-
next(
|
27
|
+
next(
|
28
|
+
v
|
29
|
+
for v in typing.cast(typing.Any, cls).__members__.values()
|
30
|
+
if v.value == key
|
31
|
+
),
|
24
32
|
)
|
25
33
|
|
26
34
|
return EnumWrapper(key)
|
@@ -29,18 +37,22 @@ class MetaEnum(EnumMeta):
|
|
29
37
|
return {name: enum.value for name, enum in self.__members__.items()}
|
30
38
|
|
31
39
|
|
32
|
-
class Enum(
|
40
|
+
class Enum(enum.Enum, metaclass=MetaEnum):
|
33
41
|
pass
|
34
42
|
|
35
43
|
|
36
|
-
class Flag(
|
44
|
+
class Flag(enum.Flag, metaclass=MetaEnum):
|
45
|
+
pass
|
46
|
+
|
47
|
+
|
48
|
+
class StrEnum(BaseStrEnum, metaclass=MetaEnum): # type: ignore
|
37
49
|
pass
|
38
50
|
|
39
51
|
|
40
52
|
@attr.s(auto_attribs=True)
|
41
53
|
class EnumWrapper:
|
42
|
-
key: Any
|
43
|
-
enum: Optional[Enum] = None
|
54
|
+
key: typing.Any
|
55
|
+
enum: typing.Optional[Enum] = None
|
44
56
|
|
45
57
|
@property
|
46
58
|
def name(self):
|
@@ -65,87 +77,173 @@ class EnumWrapper:
|
|
65
77
|
|
66
78
|
@attr.s(auto_attribs=True)
|
67
79
|
class OptionEnum:
|
80
|
+
"""An option enumeration class for handling typed options.
|
81
|
+
|
82
|
+
Attributes:
|
83
|
+
code: The option code or identifier
|
84
|
+
type: The type converter function or enum type
|
85
|
+
state: The current state value
|
86
|
+
default: The default value to use when none is provided
|
87
|
+
"""
|
68
88
|
code: str
|
69
|
-
type: Callable = str
|
70
|
-
state: Any = None
|
89
|
+
type: typing.Union[typing.Callable, MetaEnum] = str
|
90
|
+
state: typing.Any = None
|
91
|
+
default: typing.Any = None
|
92
|
+
|
93
|
+
def __getitem__(self, type: typing.Callable = None) -> "OptionEnum":
|
94
|
+
return OptionEnum("", type or self.type, self.state, self.default)
|
71
95
|
|
72
|
-
def
|
73
|
-
|
96
|
+
def __call__(self, value: typing.Any = None) -> "OptionEnum":
|
97
|
+
"""Create a new OptionEnum instance with the specified value.
|
74
98
|
|
75
|
-
|
99
|
+
Args:
|
100
|
+
value: The value to set. If None and default is provided, default will be used.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
A new OptionEnum instance with the appropriate state.
|
104
|
+
"""
|
76
105
|
state = self.state
|
77
106
|
|
107
|
+
# if value is None and default is provided, use default
|
108
|
+
if value is None and self.default is not None:
|
109
|
+
value = self.default
|
110
|
+
|
78
111
|
# if type is bool we have an option defined as Flag.
|
79
112
|
if self.type is bool:
|
80
113
|
state = value is not False
|
81
114
|
|
115
|
+
elif "enum" in str(self.type):
|
116
|
+
state = (
|
117
|
+
(
|
118
|
+
self.type.map(value).name_or_key # type: ignore
|
119
|
+
if hasattr(value, "map")
|
120
|
+
else self.type[value].name # type: ignore
|
121
|
+
)
|
122
|
+
if value is not None and value != ""
|
123
|
+
else None
|
124
|
+
)
|
125
|
+
|
82
126
|
else:
|
83
127
|
state = self.type(value) if value is not None else None
|
84
128
|
|
85
|
-
return OptionEnum(self.code, self.type, state)
|
129
|
+
return OptionEnum(self.code, self.type, state, self.default)
|
86
130
|
|
87
131
|
|
88
132
|
@attr.s(auto_attribs=True)
|
89
133
|
class Spec:
|
134
|
+
"""A specification class for handling typed values with computation logic.
|
135
|
+
|
136
|
+
Attributes:
|
137
|
+
key: The specification key or identifier
|
138
|
+
type: The type of the specification value
|
139
|
+
compute: The computation function to apply
|
140
|
+
value: The current value
|
141
|
+
default: The default value to use when none is provided
|
142
|
+
"""
|
90
143
|
key: str
|
91
|
-
type: Type
|
92
|
-
compute: Callable
|
93
|
-
value: Any = None
|
144
|
+
type: typing.Type
|
145
|
+
compute: typing.Callable
|
146
|
+
value: typing.Any = None
|
147
|
+
default: typing.Any = None
|
94
148
|
|
95
149
|
def apply(self, *args, **kwargs):
|
150
|
+
"""Apply the computation function to the arguments."""
|
96
151
|
return self.compute(*args, **kwargs)
|
97
152
|
|
98
153
|
"""Spec initialization modes"""
|
99
154
|
|
100
155
|
@staticmethod
|
101
|
-
def asFlag(key: str) -> "Spec":
|
156
|
+
def asFlag(key: str, default: typing.Optional[bool] = None) -> "Spec":
|
102
157
|
"""A Spec defined as "Flag" means that when it is specified in the payload,
|
103
158
|
a boolean flag will be returned as value.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
key: The specification key
|
162
|
+
default: Default value to use when none is provided
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
A Spec instance configured as a flag
|
104
166
|
"""
|
105
167
|
|
106
|
-
def compute(value: Optional[bool]) -> bool:
|
168
|
+
def compute(value: typing.Optional[bool]) -> bool:
|
169
|
+
# Use default if value is None
|
170
|
+
if value is None and default is not None:
|
171
|
+
value = default
|
107
172
|
return value is not False
|
108
173
|
|
109
|
-
return Spec(key, bool, compute)
|
174
|
+
return Spec(key, bool, compute, default=default)
|
110
175
|
|
111
176
|
@staticmethod
|
112
|
-
def asKey(key: str) -> "Spec":
|
177
|
+
def asKey(key: str, default: typing.Optional[bool] = None) -> "Spec":
|
113
178
|
"""A Spec defined as "Key" means that when it is specified in a payload and not flagged as False,
|
114
179
|
the spec code will be returned as value.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
key: The specification key
|
183
|
+
default: Default value to use when none is provided
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
A Spec instance configured to return its key
|
115
187
|
"""
|
116
188
|
|
117
|
-
def compute(value: Optional[bool]) -> str:
|
189
|
+
def compute(value: typing.Optional[bool]) -> str:
|
190
|
+
# Use default if value is None
|
191
|
+
if value is None and default is not None:
|
192
|
+
value = default
|
118
193
|
return key if (value is not False) else None
|
119
194
|
|
120
|
-
return Spec(key, bool, compute)
|
195
|
+
return Spec(key, bool, compute, default=default)
|
121
196
|
|
122
197
|
@staticmethod
|
123
|
-
def asValue(key: str, type: Type = str) -> "Spec":
|
124
|
-
"""A Spec defined as "Type" means that when it is specified in a payload,
|
198
|
+
def asValue(key: str, type: typing.Type = str, default: typing.Any = None) -> "Spec":
|
199
|
+
"""A Spec defined as "typing.Type" means that when it is specified in a payload,
|
125
200
|
the value passed by the user will be returned.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
key: The specification key
|
204
|
+
type: The type to convert the value to
|
205
|
+
default: Default value to use when none is provided
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
A Spec instance configured to return the typed value
|
126
209
|
"""
|
127
210
|
|
128
|
-
def compute(value: Optional[type]) -> type: # type: ignore
|
211
|
+
def compute(value: typing.Optional[type]) -> type: # type: ignore
|
212
|
+
# Use default if value is None
|
213
|
+
if value is None and default is not None:
|
214
|
+
value = default
|
129
215
|
return type(value) if value is not None else None
|
130
216
|
|
131
|
-
return Spec(key, type, compute)
|
217
|
+
return Spec(key, type, compute, default=default)
|
132
218
|
|
133
219
|
@staticmethod
|
134
|
-
def asKeyVal(key: str, type: Type = str) -> "Spec":
|
220
|
+
def asKeyVal(key: str, type: typing.Type = str, default: typing.Any = None) -> "Spec":
|
135
221
|
"""A Spec defined as "Value" means that when it is specified in a payload,
|
136
222
|
the a new spec defined as type is returned.
|
223
|
+
|
224
|
+
Args:
|
225
|
+
key: The specification key
|
226
|
+
type: The type to convert the value to
|
227
|
+
default: Default value to use when none is provided
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
A Spec instance configured to return a new Spec with the typed value
|
137
231
|
"""
|
138
232
|
|
139
|
-
def compute_inner_spec(value: Optional[type]) -> Spec: # type: ignore
|
233
|
+
def compute_inner_spec(value: typing.Optional[type]) -> Spec: # type: ignore
|
234
|
+
# Use default if value is None
|
235
|
+
if value is None and default is not None:
|
236
|
+
value = default
|
237
|
+
|
140
238
|
computed_value = (
|
141
239
|
getattr(value, "value", None)
|
142
240
|
if hasattr(value, "value")
|
143
241
|
else (type(value) if value is not None else None)
|
144
242
|
)
|
145
243
|
|
146
|
-
return Spec(key, type, lambda *_: computed_value, computed_value)
|
244
|
+
return Spec(key, type, lambda *_: computed_value, computed_value, default=default)
|
147
245
|
|
148
|
-
return Spec(key, type, compute_inner_spec)
|
246
|
+
return Spec(key, type, compute_inner_spec, default=default)
|
149
247
|
|
150
248
|
|
151
249
|
class svcEnum(str):
|
karrio/core/utils/helpers.py
CHANGED
@@ -4,19 +4,20 @@ import ssl
|
|
4
4
|
import uuid
|
5
5
|
import string
|
6
6
|
import base64
|
7
|
+
import PyPDF2
|
7
8
|
import asyncio
|
8
9
|
import logging
|
9
10
|
import urllib.parse
|
10
|
-
|
11
|
-
|
11
|
+
import PIL.Image
|
12
|
+
import PIL.ImageFile
|
12
13
|
from urllib.error import HTTPError
|
13
|
-
from urllib.request import urlopen, Request
|
14
|
+
from urllib.request import urlopen, Request, ProxyHandler, build_opener, install_opener
|
14
15
|
from typing import List, TypeVar, Callable, Optional, Any, cast
|
15
16
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
16
17
|
|
17
18
|
logger = logging.getLogger(__name__)
|
18
19
|
ssl._create_default_https_context = ssl._create_unverified_context
|
19
|
-
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
20
|
+
PIL.ImageFile.LOAD_TRUNCATED_IMAGES = True
|
20
21
|
T = TypeVar("T")
|
21
22
|
S = TypeVar("S")
|
22
23
|
NEW_LINE = """
|
@@ -46,10 +47,10 @@ def to_buffer(encoded_file: str, **kwargs) -> io.BytesIO:
|
|
46
47
|
|
47
48
|
def image_to_pdf(image_str: str, rotate: int = None, resize: dict = None) -> str:
|
48
49
|
buffer = to_buffer(image_str)
|
49
|
-
_image = Image.open(buffer)
|
50
|
+
_image = PIL.Image.open(buffer)
|
50
51
|
|
51
52
|
image = (
|
52
|
-
_image.rotate(rotate, Image.NEAREST, expand=True)
|
53
|
+
_image.rotate(rotate, PIL.Image.Resampling.NEAREST, expand=True)
|
53
54
|
if rotate is not None
|
54
55
|
else _image
|
55
56
|
)
|
@@ -58,12 +59,12 @@ def image_to_pdf(image_str: str, rotate: int = None, resize: dict = None) -> str
|
|
58
59
|
img = image.copy()
|
59
60
|
wpercent = resize["width"] / float(img.size[0])
|
60
61
|
hsize = int((float(img.size[1]) * float(wpercent)))
|
61
|
-
image = img.resize((resize["width"], hsize), Image.Resampling.LANCZOS)
|
62
|
+
image = img.resize((resize["width"], hsize), PIL.Image.Resampling.LANCZOS)
|
62
63
|
|
63
64
|
if resize is not None:
|
64
65
|
img = image.copy()
|
65
66
|
image = img.resize(
|
66
|
-
(resize["width"], resize["height"]), Image.Resampling.LANCZOS
|
67
|
+
(resize["width"], resize["height"]), PIL.Image.Resampling.LANCZOS
|
67
68
|
)
|
68
69
|
|
69
70
|
new_buffer = io.BytesIO()
|
@@ -72,8 +73,8 @@ def image_to_pdf(image_str: str, rotate: int = None, resize: dict = None) -> str
|
|
72
73
|
return base64.b64encode(new_buffer.getvalue()).decode("utf-8")
|
73
74
|
|
74
75
|
|
75
|
-
def bundle_pdfs(base64_strings: List[str]) -> PdfMerger:
|
76
|
-
merger = PdfMerger(strict=False)
|
76
|
+
def bundle_pdfs(base64_strings: List[str]) -> PyPDF2.PdfMerger:
|
77
|
+
merger = PyPDF2.PdfMerger(strict=False)
|
77
78
|
|
78
79
|
for b64_str in base64_strings:
|
79
80
|
buffer = to_buffer(b64_str)
|
@@ -82,17 +83,17 @@ def bundle_pdfs(base64_strings: List[str]) -> PdfMerger:
|
|
82
83
|
return merger
|
83
84
|
|
84
85
|
|
85
|
-
def bundle_imgs(base64_strings: List[str])
|
86
|
+
def bundle_imgs(base64_strings: List[str]):
|
86
87
|
image_buffers = [
|
87
88
|
io.BytesIO(base64.b64decode(b64_str)) for b64_str in base64_strings
|
88
89
|
]
|
89
|
-
images = [Image.open(buffer) for buffer in image_buffers]
|
90
|
+
images = [PIL.Image.open(buffer) for buffer in image_buffers]
|
90
91
|
widths, heights = zip(*(i.size for i in images))
|
91
92
|
|
92
93
|
max_width = max(widths)
|
93
94
|
total_height = sum(heights)
|
94
95
|
|
95
|
-
image = Image.new("RGB", (max_width, total_height))
|
96
|
+
image = PIL.Image.new("RGB", (max_width, total_height))
|
96
97
|
|
97
98
|
x_offset = 0
|
98
99
|
for im in images:
|
@@ -144,6 +145,12 @@ def zpl_to_pdf(zpl_str: str, width: int, height: int, dpmm: int = 12) -> str:
|
|
144
145
|
return doc
|
145
146
|
|
146
147
|
|
148
|
+
def binary_to_base64(binary_str: str) -> str:
|
149
|
+
buffer = to_buffer(binary_str)
|
150
|
+
|
151
|
+
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
152
|
+
|
153
|
+
|
147
154
|
def decode_bytes(byte):
|
148
155
|
return (
|
149
156
|
failsafe(lambda: byte.decode("utf-8"))
|
@@ -155,6 +162,7 @@ def decode_bytes(byte):
|
|
155
162
|
def process_request(
|
156
163
|
request_id: str,
|
157
164
|
trace: Callable[[Any, str], Any] = None,
|
165
|
+
proxy: str = None,
|
158
166
|
**kwargs,
|
159
167
|
) -> Request:
|
160
168
|
payload = (
|
@@ -175,6 +183,21 @@ def process_request(
|
|
175
183
|
|
176
184
|
_request = Request(**{**kwargs, **payload})
|
177
185
|
|
186
|
+
# Apply proxy settings if provided: Proxy Example` 'username:password@IP_Address:Port'
|
187
|
+
if proxy:
|
188
|
+
proxy_info = proxy.split("@")
|
189
|
+
auth_info, host_port = proxy_info[0], proxy_info[1]
|
190
|
+
auth_info = urllib.parse.unquote(auth_info)
|
191
|
+
auth_encoded = base64.b64encode(auth_info.encode()).decode()
|
192
|
+
proxy_url = f"http://{host_port}"
|
193
|
+
|
194
|
+
# Create a ProxyHandler
|
195
|
+
proxy_handler = ProxyHandler({"http": proxy_url, "https": proxy_url})
|
196
|
+
opener = build_opener(proxy_handler)
|
197
|
+
opener.addheaders = [("Proxy-Authorization", f"Basic {auth_encoded}")]
|
198
|
+
install_opener(opener)
|
199
|
+
logger.info(f"Proxy set to: {proxy_url} with credentials")
|
200
|
+
|
178
201
|
logger.info(f"Request URL:: {_request.full_url}")
|
179
202
|
|
180
203
|
return _request
|
@@ -182,15 +205,16 @@ def process_request(
|
|
182
205
|
|
183
206
|
def process_response(
|
184
207
|
request_id: str,
|
185
|
-
response,
|
208
|
+
response: Any,
|
186
209
|
decoder: Callable,
|
210
|
+
on_ok: Callable[[Any], str] = None,
|
187
211
|
trace: Callable[[Any, str], Any] = None,
|
188
212
|
) -> str:
|
189
|
-
|
190
|
-
_response =
|
191
|
-
|
192
|
-
|
193
|
-
_response =
|
213
|
+
if on_ok is not None:
|
214
|
+
_response = on_ok(response)
|
215
|
+
else:
|
216
|
+
_data = response.read()
|
217
|
+
_response = failsafe(lambda: decoder(_data)) or _data
|
194
218
|
|
195
219
|
if trace:
|
196
220
|
_content = _response if isinstance(_response, str) else "undecoded bytes..."
|
@@ -224,23 +248,29 @@ def process_error(
|
|
224
248
|
|
225
249
|
def request(
|
226
250
|
decoder: Callable = decode_bytes,
|
251
|
+
on_ok: Callable[[Any], str] = None,
|
227
252
|
on_error: Callable[[HTTPError], str] = None,
|
228
253
|
trace: Callable[[Any, str], Any] = None,
|
254
|
+
proxy: str = None,
|
255
|
+
timeout: Optional[int] = None,
|
229
256
|
**kwargs,
|
230
257
|
) -> str:
|
231
258
|
"""Return an HTTP response body.
|
232
259
|
|
233
260
|
make a http request (wrapper around Request method from built in urllib)
|
261
|
+
Proxy example: 'Username:Password@IP_Address:Port'
|
234
262
|
"""
|
235
263
|
|
236
264
|
_request_id = str(uuid.uuid4())
|
237
265
|
logger.debug(f"sending request ({_request_id})...")
|
238
266
|
|
239
267
|
try:
|
240
|
-
_request = process_request(_request_id, trace, **kwargs)
|
268
|
+
_request = process_request(_request_id, trace, proxy, **kwargs)
|
241
269
|
|
242
|
-
with urlopen(_request) as f:
|
243
|
-
_response = process_response(
|
270
|
+
with urlopen(_request, timeout=timeout) as f:
|
271
|
+
_response = process_response(
|
272
|
+
_request_id, f, decoder, on_ok=on_ok, trace=trace
|
273
|
+
)
|
244
274
|
|
245
275
|
except HTTPError as e:
|
246
276
|
_response = process_error(_request_id, e, on_error=on_error, trace=trace)
|
@@ -252,26 +282,45 @@ def exec_parrallel(
|
|
252
282
|
function: Callable, sequence: List[S], max_workers: int = None
|
253
283
|
) -> List[T]:
|
254
284
|
"""Return a list of result for function execution on each element of the sequence."""
|
255
|
-
|
285
|
+
if not sequence:
|
286
|
+
return [] # No work to do
|
287
|
+
|
288
|
+
workers = min(len(sequence), max_workers or len(sequence))
|
289
|
+
|
256
290
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
257
|
-
|
258
|
-
|
291
|
+
# Submit tasks
|
292
|
+
futures = [executor.submit(function, item) for item in sequence]
|
293
|
+
|
294
|
+
# Collect results as tasks complete
|
295
|
+
results = []
|
296
|
+
for future in as_completed(futures):
|
297
|
+
try:
|
298
|
+
results.append(future.result()) # Append result of the completed task
|
299
|
+
except Exception as e:
|
300
|
+
results.append(e) # Optionally handle or log exceptions here
|
301
|
+
|
302
|
+
return results
|
259
303
|
|
260
304
|
|
261
305
|
def exec_async(action: Callable, sequence: List[S]) -> List[T]:
|
262
|
-
async def run_tasks(
|
306
|
+
async def run_tasks():
|
307
|
+
# Use asyncio.to_thread instead of loop.run_in_executor
|
308
|
+
# This ensures proper task scheduling and prevents potential task dropping
|
263
309
|
return await asyncio.gather(
|
264
|
-
*[
|
310
|
+
*[asyncio.to_thread(action, args) for args in sequence]
|
265
311
|
)
|
266
312
|
|
267
|
-
def run_loop():
|
268
|
-
|
269
|
-
|
270
|
-
|
313
|
+
async def run_loop():
|
314
|
+
# Simplified to just return the result of run_tasks
|
315
|
+
# No need for manual loop creation and closing
|
316
|
+
return await run_tasks()
|
271
317
|
|
272
|
-
|
318
|
+
# Use asyncio.run instead of ThreadPoolExecutor
|
319
|
+
# This properly sets up and tears down the event loop
|
320
|
+
# Preventing issues with loop closure and task cleanup
|
321
|
+
result = asyncio.run(run_loop())
|
273
322
|
|
274
|
-
result
|
323
|
+
# Cast the result to the expected type
|
275
324
|
return cast(List[T], result)
|
276
325
|
|
277
326
|
|
@@ -289,8 +338,16 @@ class Location:
|
|
289
338
|
|
290
339
|
@property
|
291
340
|
def as_zip5(self) -> Optional[str]:
|
292
|
-
if not
|
293
|
-
return
|
341
|
+
if not self.value:
|
342
|
+
return None
|
343
|
+
|
344
|
+
# Try to extract exactly 5 digits
|
345
|
+
if match := re.search(r"\d{5}", self.value):
|
346
|
+
return match.group(0)
|
347
|
+
|
348
|
+
# If 4 digits, pad with 0
|
349
|
+
if match := re.search(r"\d{4}", self.value):
|
350
|
+
return f"{match.group(0)}0"
|
294
351
|
|
295
352
|
return None
|
296
353
|
|