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/lib.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
import typing
|
2
|
+
import base64
|
3
|
+
import PyPDF2
|
2
4
|
import logging
|
3
5
|
import datetime
|
4
6
|
import functools
|
7
|
+
import urllib.parse
|
5
8
|
import karrio.core.utils as utils
|
6
9
|
import karrio.core.units as units
|
7
10
|
import karrio.core.models as models
|
@@ -25,11 +28,14 @@ Job = utils.Job
|
|
25
28
|
OptionEnum = utils.OptionEnum
|
26
29
|
Enum = utils.Enum
|
27
30
|
Flag = utils.Flag
|
31
|
+
StrEnum = utils.StrEnum
|
32
|
+
identity = utils.identity
|
28
33
|
|
29
34
|
|
30
35
|
# -----------------------------------------------------------
|
31
36
|
# raw types utility functions.
|
32
37
|
# -----------------------------------------------------------
|
38
|
+
# region
|
33
39
|
|
34
40
|
|
35
41
|
def join(
|
@@ -63,6 +69,7 @@ def text(
|
|
63
69
|
*values: typing.Union[str, None],
|
64
70
|
max: int = None,
|
65
71
|
separator: str = None,
|
72
|
+
trim: bool = False,
|
66
73
|
) -> typing.Optional[str]:
|
67
74
|
"""Returns a joined text
|
68
75
|
|
@@ -76,15 +83,20 @@ def text(
|
|
76
83
|
result3 = text("string text 1", "string text 2", separator=", ")
|
77
84
|
print(result3) # "string text 1, string text 2"
|
78
85
|
|
86
|
+
result4 = text("string text 1 ", trim=True)
|
87
|
+
print(result4) # "string text 1"
|
88
|
+
|
79
89
|
:param values: a set of string values.
|
80
90
|
:param join: indicate whether to join into a single string.
|
81
91
|
:param separator: the text separator if joined into a single string.
|
92
|
+
:param trim: indicate whether to trim the string values.
|
82
93
|
:return: a string, list of string or None.
|
83
94
|
"""
|
84
95
|
_text = utils.SF.concat_str(
|
85
96
|
*values,
|
86
97
|
join=True,
|
87
98
|
separator=(separator or " "),
|
99
|
+
trim=trim,
|
88
100
|
)
|
89
101
|
|
90
102
|
if _text is None:
|
@@ -93,6 +105,19 @@ def text(
|
|
93
105
|
return typing.cast(str, _text[0:max] if max else _text)
|
94
106
|
|
95
107
|
|
108
|
+
def to_snake_case(input_string: typing.Optional[str]) -> typing.Optional[str]:
|
109
|
+
"""Convert any string format to snake case."""
|
110
|
+
return utils.SF.to_snake_case(input_string)
|
111
|
+
|
112
|
+
|
113
|
+
def to_slug(
|
114
|
+
*values,
|
115
|
+
separator: str = "_",
|
116
|
+
) -> typing.Optional[str]:
|
117
|
+
"""Convert a set of string values into a slug string, changing camel case to snake_case."""
|
118
|
+
return utils.SF.to_slug(*values, separator=separator)
|
119
|
+
|
120
|
+
|
96
121
|
def to_int(
|
97
122
|
value: typing.Union[str, int, bytes] = None,
|
98
123
|
base: int = None,
|
@@ -142,6 +167,34 @@ def to_decimal(
|
|
142
167
|
return utils.NF.decimal(value, quant)
|
143
168
|
|
144
169
|
|
170
|
+
def to_numeric_decimal(
|
171
|
+
value: typing.Union[str, float, bytes] = None,
|
172
|
+
total_digits: int = 6,
|
173
|
+
decimal_digits: int = 3,
|
174
|
+
) -> str:
|
175
|
+
"""Convert a float to a zero-padded string with customizable total length and decimal places.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
input_float (float): A floating point number to be formatted.
|
179
|
+
total_digits (int): The total length of the output string (including both numeric and decimal parts).
|
180
|
+
decimal_digits (int): The number of decimal digits (d) in the final output.
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
str: A zero-padded string of total_digits length, with the last decimal_digits as decimals.
|
184
|
+
|
185
|
+
Examples:
|
186
|
+
>>> format_to_custom_numeric_decimal(1.0, 7, 3) # NNNNddd
|
187
|
+
'0001000'
|
188
|
+
|
189
|
+
>>> format_to_custom_numeric_decimal(1.0, 8, 3) # NNNNNddd
|
190
|
+
'00001000'
|
191
|
+
|
192
|
+
>>> format_to_custom_numeric_decimal(1.0, 6, 3) # NNNddd
|
193
|
+
'001000'
|
194
|
+
"""
|
195
|
+
return utils.NF.numeric_decimal(value, total_digits, decimal_digits)
|
196
|
+
|
197
|
+
|
145
198
|
def to_money(
|
146
199
|
value: typing.Union[str, float, bytes] = None,
|
147
200
|
) -> typing.Optional[float]:
|
@@ -159,9 +212,34 @@ def to_money(
|
|
159
212
|
return None
|
160
213
|
|
161
214
|
|
215
|
+
def to_list(
|
216
|
+
value: typing.Union[T, typing.List[T]] = None,
|
217
|
+
) -> typing.List[T]:
|
218
|
+
"""Ensures the input value is a list.
|
219
|
+
|
220
|
+
Example:
|
221
|
+
result1 = to_list("test")
|
222
|
+
print(result1) # ["test"]
|
223
|
+
|
224
|
+
result2 = to_int(["test"])
|
225
|
+
print(result2) # ["test"]
|
226
|
+
|
227
|
+
:param value: a value that can be parsed into integer.
|
228
|
+
:return: a list of values.
|
229
|
+
"""
|
230
|
+
|
231
|
+
if value is None:
|
232
|
+
return []
|
233
|
+
|
234
|
+
return value if isinstance(value, list) else [value]
|
235
|
+
|
236
|
+
|
237
|
+
# endregion
|
238
|
+
|
162
239
|
# -----------------------------------------------------------
|
163
240
|
# Date and Time utility functions.
|
164
241
|
# -----------------------------------------------------------
|
242
|
+
# region
|
165
243
|
|
166
244
|
|
167
245
|
def ftime(
|
@@ -172,9 +250,23 @@ def ftime(
|
|
172
250
|
) -> typing.Optional[str]:
|
173
251
|
return utils.DF.ftime(
|
174
252
|
time_str,
|
175
|
-
current_format,
|
176
|
-
output_format,
|
177
|
-
try_formats,
|
253
|
+
current_format=current_format,
|
254
|
+
output_format=output_format,
|
255
|
+
try_formats=try_formats,
|
256
|
+
)
|
257
|
+
|
258
|
+
|
259
|
+
def flocaltime(
|
260
|
+
time_str: str,
|
261
|
+
current_format: str = "%H:%M:%S",
|
262
|
+
output_format: str = "%H:%M %p",
|
263
|
+
try_formats: typing.List[str] = None,
|
264
|
+
) -> typing.Optional[str]:
|
265
|
+
return utils.DF.ftime(
|
266
|
+
time_str,
|
267
|
+
current_format=current_format,
|
268
|
+
output_format=output_format,
|
269
|
+
try_formats=try_formats,
|
178
270
|
)
|
179
271
|
|
180
272
|
|
@@ -185,8 +277,8 @@ def fdate(
|
|
185
277
|
) -> typing.Optional[str]:
|
186
278
|
return utils.DF.fdate(
|
187
279
|
date_str,
|
188
|
-
current_format,
|
189
|
-
try_formats,
|
280
|
+
current_format=current_format,
|
281
|
+
try_formats=try_formats,
|
190
282
|
)
|
191
283
|
|
192
284
|
|
@@ -198,9 +290,9 @@ def fdatetime(
|
|
198
290
|
) -> typing.Optional[str]:
|
199
291
|
return utils.DF.fdatetime(
|
200
292
|
date_str,
|
201
|
-
current_format,
|
202
|
-
output_format,
|
203
|
-
try_formats,
|
293
|
+
current_format=current_format,
|
294
|
+
output_format=output_format,
|
295
|
+
try_formats=try_formats,
|
204
296
|
)
|
205
297
|
|
206
298
|
|
@@ -217,14 +309,29 @@ def to_date(
|
|
217
309
|
) -> datetime.datetime:
|
218
310
|
return utils.DF.date(
|
219
311
|
date_value,
|
220
|
-
current_format,
|
221
|
-
try_formats,
|
312
|
+
current_format=current_format,
|
313
|
+
try_formats=try_formats,
|
314
|
+
)
|
315
|
+
|
316
|
+
|
317
|
+
def to_next_business_datetime(
|
318
|
+
date_value: typing.Union[str, datetime.datetime] = None,
|
319
|
+
current_format: str = "%Y-%m-%d %H:%M:%S",
|
320
|
+
try_formats: typing.List[str] = None,
|
321
|
+
) -> datetime.datetime:
|
322
|
+
return utils.DF.next_business_datetime(
|
323
|
+
date_value,
|
324
|
+
current_format=current_format,
|
325
|
+
try_formats=try_formats,
|
222
326
|
)
|
223
327
|
|
224
328
|
|
329
|
+
# endregion
|
330
|
+
|
225
331
|
# -----------------------------------------------------------
|
226
332
|
# JSON, XML and object utility functions.
|
227
333
|
# -----------------------------------------------------------
|
334
|
+
# region
|
228
335
|
|
229
336
|
|
230
337
|
def to_object(
|
@@ -246,7 +353,7 @@ def to_object(
|
|
246
353
|
def to_dict(
|
247
354
|
value: typing.Any,
|
248
355
|
clear_empty: bool = None,
|
249
|
-
) -> dict:
|
356
|
+
) -> typing.Union[dict, list, typing.Any]:
|
250
357
|
"""Parse value into a Python dictionay.
|
251
358
|
|
252
359
|
:param value: a value that can converted in dictionary.
|
@@ -269,6 +376,7 @@ def to_json(
|
|
269
376
|
def to_xml(
|
270
377
|
value: typing.Union[utils.Element, typing.Any],
|
271
378
|
encoding: str = "utf-8",
|
379
|
+
prefixes: dict = None,
|
272
380
|
**kwargs,
|
273
381
|
) -> str:
|
274
382
|
"""Turn a XML typed object into a XML text.
|
@@ -277,7 +385,12 @@ def to_xml(
|
|
277
385
|
:param encoding: an optional encoding type.
|
278
386
|
:return: a XML string.
|
279
387
|
"""
|
388
|
+
|
280
389
|
if utils.XP.istypedxmlobject(value):
|
390
|
+
if prefixes is not None:
|
391
|
+
_prefix = prefixes.get(value.__class__.__name__) or ""
|
392
|
+
apply_namespaceprefix(value, _prefix, prefixes)
|
393
|
+
|
281
394
|
return utils.XP.export(value, **kwargs)
|
282
395
|
|
283
396
|
return utils.XP.xml_tostring(value, encoding)
|
@@ -310,6 +423,27 @@ def to_element(
|
|
310
423
|
return utils.XP.to_xml_or_html_element(xml_text, encoding=encoding)
|
311
424
|
|
312
425
|
|
426
|
+
def to_query_string(data: dict) -> str:
|
427
|
+
param_list: list = functools.reduce(
|
428
|
+
lambda acc, item: [
|
429
|
+
*acc,
|
430
|
+
*(
|
431
|
+
[(item[0], _) for _ in item[1]]
|
432
|
+
if isinstance(item[1], list)
|
433
|
+
else [(item[0], item[1])]
|
434
|
+
),
|
435
|
+
],
|
436
|
+
data.items(),
|
437
|
+
[],
|
438
|
+
)
|
439
|
+
|
440
|
+
return urllib.parse.urlencode(param_list)
|
441
|
+
|
442
|
+
|
443
|
+
def to_query_unquote(query_string: str) -> str:
|
444
|
+
return urllib.parse.unquote(query_string)
|
445
|
+
|
446
|
+
|
313
447
|
def find_element(
|
314
448
|
tag: str,
|
315
449
|
in_element: utils.Element,
|
@@ -361,9 +495,51 @@ def envelope_serializer(
|
|
361
495
|
return to_xml(envelope, namespacedef_=namespace)
|
362
496
|
|
363
497
|
|
498
|
+
def load_json(path: str):
|
499
|
+
"""Load and parse a JSON file from the given path.
|
500
|
+
|
501
|
+
Args:
|
502
|
+
path (str): The path to the JSON file to be loaded.
|
503
|
+
|
504
|
+
Returns:
|
505
|
+
dict: The parsed JSON content as a Python dictionary.
|
506
|
+
|
507
|
+
Raises:
|
508
|
+
FileNotFoundError: If the specified file is not found.
|
509
|
+
JSONDecodeError: If the file content is not valid JSON.
|
510
|
+
IOError: If there's an error reading the file.
|
511
|
+
"""
|
512
|
+
return to_dict(load_file_content(path))
|
513
|
+
|
514
|
+
|
515
|
+
def load_file_content(path: str) -> str:
|
516
|
+
"""Load the content of a file from the given path.
|
517
|
+
|
518
|
+
Args:
|
519
|
+
path (str): The path to the file to be read.
|
520
|
+
|
521
|
+
Returns:
|
522
|
+
str: The content of the file as a string.
|
523
|
+
|
524
|
+
Raises:
|
525
|
+
FileNotFoundError: If the specified file is not found.
|
526
|
+
IOError: If there's an error reading the file.
|
527
|
+
"""
|
528
|
+
try:
|
529
|
+
with open(path, "r", encoding="utf-8") as file:
|
530
|
+
return file.read()
|
531
|
+
except FileNotFoundError:
|
532
|
+
raise FileNotFoundError(f"File not found: {path}")
|
533
|
+
except IOError as e:
|
534
|
+
raise IOError(f"Error reading file {path}: {str(e)}")
|
535
|
+
|
536
|
+
|
537
|
+
# endregion
|
538
|
+
|
364
539
|
# -----------------------------------------------------------
|
365
540
|
# Shipping request options utility functions.
|
366
541
|
# -----------------------------------------------------------
|
542
|
+
# region
|
367
543
|
|
368
544
|
|
369
545
|
def to_shipping_options(
|
@@ -442,9 +618,12 @@ def to_connection_config(
|
|
442
618
|
)
|
443
619
|
|
444
620
|
|
621
|
+
# endregion
|
622
|
+
|
445
623
|
# -----------------------------------------------------------
|
446
624
|
# Address utility functions.
|
447
625
|
# -----------------------------------------------------------
|
626
|
+
# region
|
448
627
|
|
449
628
|
|
450
629
|
def to_zip4(
|
@@ -484,9 +663,12 @@ def to_address(
|
|
484
663
|
return units.ComputedAddress(address)
|
485
664
|
|
486
665
|
|
666
|
+
# endregion
|
667
|
+
|
487
668
|
# -----------------------------------------------------------
|
488
669
|
# Multi-piece shipment utility functions.
|
489
670
|
# -----------------------------------------------------------
|
671
|
+
# region
|
490
672
|
|
491
673
|
|
492
674
|
def to_multi_piece_rates(
|
@@ -544,20 +726,29 @@ def to_packages(
|
|
544
726
|
max_weight: units.Weight = None,
|
545
727
|
options: dict = None,
|
546
728
|
package_option_type: typing.Type[utils.Enum] = utils.Enum,
|
729
|
+
shipping_options_initializer: typing.Callable = None,
|
547
730
|
) -> units.Packages:
|
548
731
|
return units.Packages(
|
549
732
|
parcels=parcels,
|
550
733
|
presets=presets,
|
551
734
|
required=required,
|
552
735
|
max_weight=max_weight,
|
553
|
-
options=
|
736
|
+
options=(
|
737
|
+
units.ShippingOptions(options or {}, package_option_type)
|
738
|
+
if (isinstance(options, dict) or options is None)
|
739
|
+
else options
|
740
|
+
),
|
554
741
|
package_option_type=package_option_type,
|
742
|
+
shipping_options_initializer=shipping_options_initializer,
|
555
743
|
)
|
556
744
|
|
557
745
|
|
746
|
+
# endregion
|
747
|
+
|
558
748
|
# -----------------------------------------------------------
|
559
749
|
# async and backgroung code execution utility functions.
|
560
750
|
# -----------------------------------------------------------
|
751
|
+
# region
|
561
752
|
|
562
753
|
|
563
754
|
def run_concurently(
|
@@ -575,23 +766,40 @@ def run_asynchronously(
|
|
575
766
|
return utils.exec_async(predicate, sequence)
|
576
767
|
|
577
768
|
|
769
|
+
# endregion
|
770
|
+
|
578
771
|
# -----------------------------------------------------------
|
579
772
|
# HTTP requests utility functions.
|
580
773
|
# -----------------------------------------------------------
|
774
|
+
# region
|
581
775
|
|
582
776
|
|
583
777
|
def request(
|
584
778
|
decoder: typing.Callable = utils.decode_bytes,
|
779
|
+
on_ok: typing.Callable = None,
|
585
780
|
on_error: typing.Callable = None,
|
586
781
|
trace: typing.Callable[[typing.Any, str], typing.Any] = None,
|
782
|
+
proxy: str = None,
|
783
|
+
timeout: typing.Optional[int] = None,
|
587
784
|
**kwargs,
|
588
785
|
) -> str:
|
589
|
-
return utils.request(
|
786
|
+
return utils.request(
|
787
|
+
decoder=decoder,
|
788
|
+
on_ok=on_ok,
|
789
|
+
on_error=on_error,
|
790
|
+
trace=trace,
|
791
|
+
proxy=proxy,
|
792
|
+
timeout=timeout,
|
793
|
+
**kwargs,
|
794
|
+
)
|
795
|
+
|
590
796
|
|
797
|
+
# endregion
|
591
798
|
|
592
799
|
# -----------------------------------------------------------
|
593
800
|
# image and document processing utility functions.
|
594
801
|
# -----------------------------------------------------------
|
802
|
+
# region
|
595
803
|
|
596
804
|
|
597
805
|
def image_to_pdf(
|
@@ -604,13 +812,13 @@ def image_to_pdf(
|
|
604
812
|
|
605
813
|
def bundle_pdfs(
|
606
814
|
base64_strings: typing.List[str],
|
607
|
-
) ->
|
815
|
+
) -> PyPDF2.PdfMerger:
|
608
816
|
return utils.bundle_pdfs(base64_strings)
|
609
817
|
|
610
818
|
|
611
819
|
def bundle_imgs(
|
612
820
|
base64_strings: typing.List[str],
|
613
|
-
)
|
821
|
+
):
|
614
822
|
return utils.bundle_imgs(base64_strings)
|
615
823
|
|
616
824
|
|
@@ -652,9 +860,24 @@ def decode(byte: bytes):
|
|
652
860
|
)
|
653
861
|
|
654
862
|
|
863
|
+
def encode_base64(byte: bytes):
|
864
|
+
return (
|
865
|
+
failsafe(lambda: base64.encodebytes(byte).decode("utf-8"))
|
866
|
+
or failsafe(lambda: base64.encodebytes(byte).decode("ISO-8859-1"))
|
867
|
+
or base64.encodebytes(byte).decode("utf-8")
|
868
|
+
)
|
869
|
+
|
870
|
+
|
871
|
+
def binary_to_base64(binary_string: str):
|
872
|
+
return utils.binary_to_base64(binary_string)
|
873
|
+
|
874
|
+
|
875
|
+
# endregion
|
876
|
+
|
655
877
|
# -----------------------------------------------------------
|
656
878
|
# other utilities functions
|
657
879
|
# -----------------------------------------------------------
|
880
|
+
# region
|
658
881
|
|
659
882
|
|
660
883
|
def failsafe(callable: typing.Callable[[], T], warning: str = None) -> T:
|
@@ -664,3 +887,6 @@ def failsafe(callable: typing.Callable[[], T], warning: str = None) -> T:
|
|
664
887
|
don't mind if it fails.
|
665
888
|
"""
|
666
889
|
return utils.failsafe(callable, warning=warning)
|
890
|
+
|
891
|
+
|
892
|
+
# endregion
|