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/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=units.ShippingOptions(options or {}, package_option_type),
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(decoder=decoder, on_error=on_error, trace=trace, **kwargs)
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
- ) -> utils.PdfMerger:
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
- ) -> utils.Image:
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
@@ -0,0 +1,6 @@
1
+ """
2
+ Karrio Plugins Package.
3
+
4
+ This package contains plugins for the Karrio shipping system.
5
+ """
6
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore