absfuyu 4.2.0__py3-none-any.whl → 5.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.

Potentially problematic release.


This version of absfuyu might be problematic. Click here for more details.

Files changed (72) hide show
  1. absfuyu/__init__.py +4 -4
  2. absfuyu/__main__.py +13 -1
  3. absfuyu/cli/__init__.py +2 -2
  4. absfuyu/cli/color.py +9 -2
  5. absfuyu/cli/config_group.py +2 -2
  6. absfuyu/cli/do_group.py +2 -37
  7. absfuyu/cli/game_group.py +2 -2
  8. absfuyu/cli/tool_group.py +7 -7
  9. absfuyu/config/__init__.py +17 -34
  10. absfuyu/core/__init__.py +49 -0
  11. absfuyu/core/baseclass.py +299 -0
  12. absfuyu/core/baseclass2.py +165 -0
  13. absfuyu/core/decorator.py +67 -0
  14. absfuyu/core/docstring.py +166 -0
  15. absfuyu/core/dummy_cli.py +67 -0
  16. absfuyu/core/dummy_func.py +49 -0
  17. absfuyu/dxt/__init__.py +42 -0
  18. absfuyu/dxt/dictext.py +201 -0
  19. absfuyu/dxt/dxt_support.py +79 -0
  20. absfuyu/dxt/intext.py +586 -0
  21. absfuyu/dxt/listext.py +508 -0
  22. absfuyu/dxt/strext.py +530 -0
  23. absfuyu/extra/__init__.py +12 -0
  24. absfuyu/extra/beautiful.py +252 -0
  25. absfuyu/{extensions → extra}/data_analysis.py +51 -82
  26. absfuyu/fun/__init__.py +110 -135
  27. absfuyu/fun/tarot.py +11 -19
  28. absfuyu/game/__init__.py +8 -2
  29. absfuyu/game/game_stat.py +8 -2
  30. absfuyu/game/sudoku.py +9 -3
  31. absfuyu/game/tictactoe.py +14 -7
  32. absfuyu/game/wordle.py +16 -10
  33. absfuyu/general/__init__.py +8 -81
  34. absfuyu/general/content.py +24 -38
  35. absfuyu/general/human.py +108 -228
  36. absfuyu/general/shape.py +1334 -0
  37. absfuyu/logger.py +10 -15
  38. absfuyu/pkg_data/__init__.py +137 -100
  39. absfuyu/pkg_data/deprecated.py +133 -0
  40. absfuyu/sort.py +6 -130
  41. absfuyu/tools/__init__.py +2 -2
  42. absfuyu/tools/checksum.py +33 -22
  43. absfuyu/tools/converter.py +51 -48
  44. absfuyu/{general → tools}/generator.py +17 -42
  45. absfuyu/tools/keygen.py +25 -30
  46. absfuyu/tools/obfuscator.py +246 -112
  47. absfuyu/tools/passwordlib.py +100 -30
  48. absfuyu/tools/shutdownizer.py +68 -47
  49. absfuyu/tools/web.py +4 -11
  50. absfuyu/util/__init__.py +17 -17
  51. absfuyu/util/api.py +10 -15
  52. absfuyu/util/json_method.py +7 -24
  53. absfuyu/util/lunar.py +5 -11
  54. absfuyu/util/path.py +22 -27
  55. absfuyu/util/performance.py +43 -67
  56. absfuyu/util/shorten_number.py +65 -14
  57. absfuyu/util/zipped.py +11 -17
  58. absfuyu/version.py +59 -42
  59. {absfuyu-4.2.0.dist-info → absfuyu-5.0.1.dist-info}/METADATA +41 -14
  60. absfuyu-5.0.1.dist-info/RECORD +68 -0
  61. absfuyu/core.py +0 -57
  62. absfuyu/everything.py +0 -32
  63. absfuyu/extensions/__init__.py +0 -12
  64. absfuyu/extensions/beautiful.py +0 -188
  65. absfuyu/fun/WGS.py +0 -134
  66. absfuyu/general/data_extension.py +0 -1796
  67. absfuyu/tools/stats.py +0 -226
  68. absfuyu/util/pkl.py +0 -67
  69. absfuyu-4.2.0.dist-info/RECORD +0 -59
  70. {absfuyu-4.2.0.dist-info → absfuyu-5.0.1.dist-info}/WHEEL +0 -0
  71. {absfuyu-4.2.0.dist-info → absfuyu-5.0.1.dist-info}/entry_points.txt +0 -0
  72. {absfuyu-4.2.0.dist-info → absfuyu-5.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,252 @@
1
+ """
2
+ Absfuyu: Beautiful
3
+ ------------------
4
+ A decorator that makes output more beautiful
5
+
6
+ Version: 5.0.0
7
+ Date updated: 22/02/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module level
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = [
13
+ "BeautifulOutput",
14
+ "print",
15
+ ]
16
+
17
+
18
+ # Library
19
+ # ---------------------------------------------------------------------------
20
+ import time
21
+ import tracemalloc
22
+ from collections.abc import Callable
23
+ from functools import wraps
24
+ from typing import Any, Literal, NamedTuple, ParamSpec, TypeVar
25
+
26
+ BEAUTIFUL_MODE = False
27
+
28
+ try:
29
+ from rich.align import Align
30
+ from rich.console import Console, Group
31
+ from rich.panel import Panel
32
+ from rich.table import Table
33
+ from rich.text import Text
34
+ except ImportError:
35
+ from subprocess import run
36
+
37
+ from absfuyu.config import ABSFUYU_CONFIG
38
+
39
+ if ABSFUYU_CONFIG._get_setting("auto-install-extra").value:
40
+ cmd = "python -m pip install -U absfuyu[beautiful]".split()
41
+ run(cmd)
42
+ else:
43
+ raise SystemExit("This feature is in absfuyu[beautiful] package") # noqa: B904
44
+ else:
45
+ BEAUTIFUL_MODE = True
46
+
47
+ # Setup
48
+ # ---------------------------------------------------------------------------
49
+ # rich's console.print wrapper
50
+ console = Console(color_system="auto", tab_size=4)
51
+ print = console.print
52
+
53
+
54
+ # Type
55
+ # ---------------------------------------------------------------------------
56
+ P = ParamSpec("P") # Parameter type
57
+ R = TypeVar("R") # Return type - Can be anything
58
+ T = TypeVar("T", bound=type) # Type type - Can be any subtype of `type`
59
+
60
+
61
+ # Class
62
+ # ---------------------------------------------------------------------------
63
+ class PerformanceOutput(NamedTuple):
64
+ runtime: float
65
+ current_memory: int
66
+ peak_memory: int
67
+
68
+ def to_text(self) -> str:
69
+ """
70
+ Beautify the result and ready to print
71
+ """
72
+ out = (
73
+ f"Memory usage: {self.current_memory / 10**6:,.6f} MB\n"
74
+ f"Peak memory usage: {self.peak_memory / 10**6:,.6f} MB\n"
75
+ f"Time elapsed: {self.runtime:,.6f} s"
76
+ )
77
+ return out
78
+
79
+
80
+ # TODO: header and footer layout to 1,2,3 instead of true false
81
+ class BeautifulOutput:
82
+ """A decorator that makes output more beautiful"""
83
+
84
+ def __init__(
85
+ self,
86
+ layout: Literal[1, 2, 3, 4, 5, 6] = 1,
87
+ include_header: bool = True,
88
+ include_footer: bool = True,
89
+ alternate_footer: bool = False,
90
+ ) -> None:
91
+ """
92
+ Show function's signature and measure memory usage
93
+
94
+ Parameters
95
+ ----------
96
+ layout : Literal[1, 2, 3, 4, 5, 6], optional
97
+ Layout to show, by default ``1``
98
+
99
+ include_header : bool, optional
100
+ Include header with function's signature, by default ``True``
101
+
102
+ include_footer : bool, optional
103
+ Include footer, by default ``True``
104
+
105
+ alternate_footer : bool, optional
106
+ Alternative style of footer, by default ``False``
107
+
108
+
109
+ Usage
110
+ -----
111
+ Use this as a decorator (``@BeautifulOutput(<parameters>)``)
112
+ """
113
+ self.layout = layout
114
+ self.include_header = include_header
115
+ self.include_footer = include_footer
116
+ self.alternate_footer = alternate_footer
117
+
118
+ # Data
119
+ self._obj_name = ""
120
+ self._signature = ""
121
+ self._result: Any | None = None
122
+ self._performance: PerformanceOutput | None = None
123
+
124
+ # Setting
125
+ self._header_footer_style = "white on blue"
126
+ self._alignment = "center"
127
+
128
+ def __call__(self, obj: Callable[P, R]) -> Callable[P, Group]:
129
+ # Class wrapper
130
+ if isinstance(obj, type):
131
+ raise NotImplementedError("Classes are not supported")
132
+
133
+ # Function wrapper
134
+ @wraps(obj)
135
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Group:
136
+ """
137
+ Wrapper function that executes the original function.
138
+ """
139
+ # Get all parameters inputed
140
+ args_repr = [repr(a) for a in args]
141
+ kwargs_repr = [f"{k}={repr(v)}" for k, v in kwargs.items()]
142
+ self._signature = ", ".join(args_repr + kwargs_repr)
143
+ self._obj_name = obj.__name__
144
+
145
+ # Performance check
146
+ tracemalloc.start() # Start memory measure
147
+ start_time = time.perf_counter() # Start time measure
148
+
149
+ self._result = obj(*args, **kwargs) # Function run
150
+
151
+ finish_time = time.perf_counter() # Get finished time
152
+ _cur, _peak = tracemalloc.get_traced_memory() # Get memory stats
153
+ tracemalloc.stop() # End memory measure
154
+
155
+ self._performance = PerformanceOutput(
156
+ runtime=finish_time - start_time, current_memory=_cur, peak_memory=_peak
157
+ )
158
+
159
+ return self._get_layout(layout=self.layout)
160
+
161
+ return wrapper
162
+
163
+ # Signature
164
+ def _func_signature(self) -> str:
165
+ """Function's signature"""
166
+ return f"{self._obj_name}({self._signature})"
167
+
168
+ # Layout
169
+ def _make_header(self) -> Table:
170
+ header_table = Table.grid(expand=True)
171
+ header_table.add_row(
172
+ Panel(
173
+ Align(f"[b]{self._func_signature()}", align=self._alignment),
174
+ style=self._header_footer_style,
175
+ )
176
+ )
177
+ return header_table
178
+
179
+ def _make_line(self) -> Table:
180
+ line = Table.grid(expand=True)
181
+ line.add_row(Text("", style=self._header_footer_style))
182
+ return line
183
+
184
+ def _make_footer(self) -> Table:
185
+ if self.alternate_footer:
186
+ return self._make_line()
187
+
188
+ footer_table = Table.grid(expand=True)
189
+ footer_table.add_row(
190
+ Panel(
191
+ Align("[b]BeautifulOutput by absfuyu", align=self._alignment),
192
+ style=self._header_footer_style,
193
+ )
194
+ )
195
+ return footer_table
196
+
197
+ def _make_result_panel(self) -> Panel:
198
+ result_txt = Text(
199
+ str(self._result),
200
+ overflow="fold",
201
+ no_wrap=False,
202
+ tab_size=2,
203
+ )
204
+ result_panel = Panel(
205
+ Align(result_txt, align=self._alignment),
206
+ title="[bold]Result[/]",
207
+ border_style="green",
208
+ highlight=True,
209
+ )
210
+ return result_panel
211
+
212
+ def _make_performance_panel(self) -> Panel:
213
+ if self._performance is not None:
214
+ performance_panel = Panel(
215
+ Align(self._performance.to_text(), align=self._alignment),
216
+ title="[bold]Performance[/]",
217
+ border_style="red",
218
+ highlight=True,
219
+ # height=result_panel.height,
220
+ )
221
+ return performance_panel
222
+ else:
223
+ return Panel("None", title="[bold]Performance[/]")
224
+
225
+ def _make_output(self) -> Table:
226
+ out_table = Table.grid(expand=True)
227
+ out_table.add_column(ratio=3) # result
228
+ out_table.add_column(ratio=2) # performance
229
+
230
+ out_table.add_row(
231
+ self._make_result_panel(),
232
+ self._make_performance_panel(),
233
+ )
234
+ return out_table
235
+
236
+ def _get_layout(self, layout: int) -> Group:
237
+ header = self._make_header() if self.include_header else Text()
238
+ footer = self._make_footer() if self.include_footer else Text()
239
+ layouts = {
240
+ 1: Group(header, self._make_output(), footer),
241
+ 2: Group(header, self._make_result_panel(), self._make_performance_panel()),
242
+ 3: Group(header, self._make_result_panel(), footer),
243
+ 4: Group(self._make_result_panel(), self._make_performance_panel()),
244
+ 5: Group(self._make_output()),
245
+ 6: Group(
246
+ header,
247
+ self._make_result_panel(),
248
+ self._make_performance_panel(),
249
+ footer,
250
+ ),
251
+ }
252
+ return layouts.get(layout, layouts[1])
@@ -3,12 +3,12 @@ Absfuyu: Data Analysis [W.I.P]
3
3
  ------------------------------
4
4
  Extension for ``pd.DataFrame``
5
5
 
6
- Version: 2.2.0
7
- Date updated: 29/11/2024 (dd/mm/yyyy)
6
+ Version: 5.0.0
7
+ Date updated: 25/02/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module level
11
- ###########################################################################
11
+ # ---------------------------------------------------------------------------
12
12
  __all__ = [
13
13
  # Function
14
14
  "compare_2_list",
@@ -24,60 +24,40 @@ __all__ = [
24
24
 
25
25
 
26
26
  # Library
27
- ###########################################################################
27
+ # ---------------------------------------------------------------------------
28
28
  import random
29
29
  import string
30
30
  from collections import deque
31
31
  from datetime import datetime
32
32
  from itertools import chain, product
33
- from typing import Any, Literal, NamedTuple, Self
33
+ from typing import Any, ClassVar, Literal, NamedTuple, Self
34
34
 
35
- # import matplotlib.pyplot as plt
36
- # from scipy import stats
37
- # from dateutil.relativedelta import relativedelta
38
- import numpy as np
39
- import pandas as pd
40
- from deprecated import deprecated
41
- from deprecated.sphinx import deprecated as sphinx_deprecated
42
- from deprecated.sphinx import versionadded
35
+ DA_MODE = False
43
36
 
44
- from absfuyu.logger import logger
45
- from absfuyu.util import set_min, set_min_max
37
+ try:
38
+ import numpy as np
39
+ import pandas as pd
40
+ except ImportError:
41
+ from subprocess import run
46
42
 
43
+ from absfuyu.config import ABSFUYU_CONFIG
47
44
 
48
- # Function
49
- ###########################################################################
50
- @deprecated(reason="Not needed", version="3.1.0")
51
- @sphinx_deprecated(reason="Not needed", version="3.1.0")
52
- def summary(data: list | np.ndarray): # del this
53
- """
54
- Quick summary of data
45
+ if ABSFUYU_CONFIG._get_setting("auto-install-extra").value:
46
+ cmd = "python -m pip install -U absfuyu[full]".split()
47
+ run(cmd)
48
+ else:
49
+ raise SystemExit("This feature is in absfuyu[full] package") # noqa: B904
50
+ else:
51
+ DA_MODE = True
55
52
 
56
- :param data: np.ndarray | list
57
- """
58
53
 
59
- if not isinstance(data, np.ndarray):
60
- data = np.array(data)
61
-
62
- output = {
63
- "Observations": len(data),
64
- "Mean": np.mean(data),
65
- "Median": np.median(data),
66
- # "Mode": stats.mode(data)[0][0],
67
- "Standard deviation": np.std(data),
68
- "Variance": np.var(data),
69
- "Max": max(data),
70
- "Min": min(data),
71
- "Percentiles": {
72
- "1st Quartile": np.quantile(data, 0.25),
73
- "2nd Quartile": np.quantile(data, 0.50),
74
- "3rd Quartile": np.quantile(data, 0.75),
75
- # "IQR": stats.iqr(data),
76
- },
77
- }
78
- return output
54
+ from absfuyu.core import ShowAllMethodsMixin, versionadded # noqa: E402
55
+ from absfuyu.logger import logger # noqa: E402
56
+ from absfuyu.util import set_min, set_min_max # noqa: E402
79
57
 
80
58
 
59
+ # Function
60
+ # ---------------------------------------------------------------------------
81
61
  def equalize_df(data: dict[str, list], fillna=np.nan) -> dict[str, list]:
82
62
  """
83
63
  Make all list in dict have equal length to make pd.DataFrame
@@ -94,9 +74,6 @@ def equalize_df(data: dict[str, list], fillna=np.nan) -> dict[str, list]:
94
74
  return data
95
75
 
96
76
 
97
- ## Update 05/10
98
-
99
-
100
77
  def compare_2_list(*arr) -> pd.DataFrame:
101
78
  """
102
79
  Compare 2 lists then create DataFrame
@@ -147,12 +124,6 @@ def rename_with_dict(df: pd.DataFrame, col: str, rename_dict: dict) -> pd.DataFr
147
124
  :param col: Column name
148
125
  :param rename_dict: Rename dictionary
149
126
  """
150
- # Ver 1.0.1
151
- # name = f"{col}_filtered"
152
- # df[name] = df[col]
153
- # for k, v in rename_dict.items():
154
- # df[name] = df[name].str.replace(k, v)
155
- # return df
156
127
 
157
128
  name = f"{col}_filtered"
158
129
  df[name] = df[col]
@@ -162,7 +133,7 @@ def rename_with_dict(df: pd.DataFrame, col: str, rename_dict: dict) -> pd.DataFr
162
133
 
163
134
 
164
135
  # Class
165
- ###########################################################################
136
+ # ---------------------------------------------------------------------------
166
137
  class CityData(NamedTuple):
167
138
  """
168
139
  Parameters
@@ -182,7 +153,7 @@ class CityData(NamedTuple):
182
153
  area: str
183
154
 
184
155
  @staticmethod
185
- def _sample_city_data(size: int = 100) -> list["CityData"]:
156
+ def _sample_city_data(size: int = 100) -> list:
186
157
  """
187
158
  Generate sample city data (testing purpose)
188
159
  """
@@ -354,7 +325,7 @@ class MatplotlibFormatString:
354
325
  Format string format: `[marker][line][color]` or `[color][marker][line]`
355
326
  """
356
327
 
357
- MARKER_LIST = {
328
+ MARKER_LIST: ClassVar[dict[str, str]] = {
358
329
  ".": "point marker",
359
330
  ",": "pixel marker",
360
331
  "o": "circle marker",
@@ -381,13 +352,13 @@ class MatplotlibFormatString:
381
352
  "|": "vline marker",
382
353
  "_": "hline marker",
383
354
  }
384
- LINE_STYLE_LIST = {
355
+ LINE_STYLE_LIST: ClassVar[dict[str, str]] = {
385
356
  "-": "solid line style",
386
357
  "--": "dashed line style",
387
358
  "-.": "dash-dot line style",
388
359
  ":": "dotted line style",
389
360
  }
390
- COLOR_LIST = {
361
+ COLOR_LIST: ClassVar[dict[str, str]] = {
391
362
  "b": "blue",
392
363
  "g": "green",
393
364
  "r": "red",
@@ -401,12 +372,12 @@ class MatplotlibFormatString:
401
372
  LineStyle = _DictToAtrr(LINE_STYLE_LIST, key_as_atrribute=False)
402
373
  Color = _DictToAtrr(COLOR_LIST, key_as_atrribute=False)
403
374
 
404
- @staticmethod
405
- def all_format_string() -> list[PLTFormatString]:
375
+ @classmethod
376
+ def all_format_string(cls) -> list[PLTFormatString]:
406
377
  fmt_str = [
407
- __class__.MARKER_LIST, # type: ignore
408
- __class__.LINE_STYLE_LIST, # type: ignore
409
- __class__.COLOR_LIST, # type: ignore
378
+ cls.MARKER_LIST,
379
+ cls.LINE_STYLE_LIST,
380
+ cls.COLOR_LIST,
410
381
  ]
411
382
  return [PLTFormatString._make(x) for x in list(product(*fmt_str))]
412
383
 
@@ -420,8 +391,8 @@ class MatplotlibFormatString:
420
391
 
421
392
 
422
393
  # Class - DA
423
- ###########################################################################
424
- class DataAnalystDataFrame(pd.DataFrame):
394
+ # ---------------------------------------------------------------------------
395
+ class DataAnalystDataFrame(ShowAllMethodsMixin, pd.DataFrame):
425
396
  """
426
397
  Data Analyst ``pd.DataFrame``
427
398
  """
@@ -843,13 +814,15 @@ class DataAnalystDataFrame(pd.DataFrame):
843
814
  # ================================================================
844
815
  # Total observation
845
816
  @property
846
- @versionadded(version="3.2.0")
817
+ @versionadded("3.2.0")
847
818
  def total_observation(self) -> int:
848
- """Returns total observation of the DataFrame"""
819
+ """
820
+ Returns total observation of the DataFrame
821
+ """
849
822
  return self.shape[0] * self.shape[1] # type: ignore
850
823
 
851
824
  # Quick info
852
- @versionadded(version="3.2.0")
825
+ @versionadded("3.2.0")
853
826
  def qinfo(self) -> str:
854
827
  """
855
828
  Show quick infomation about DataFrame
@@ -867,7 +840,7 @@ class DataAnalystDataFrame(pd.DataFrame):
867
840
  return info
868
841
 
869
842
  # Quick describe
870
- @versionadded(version="3.2.0")
843
+ @versionadded("3.2.0")
871
844
  def qdescribe(self) -> pd.DataFrame:
872
845
  """
873
846
  Quick ``describe()`` that exclude ``object`` and ``datetime`` dtype
@@ -916,7 +889,7 @@ class DataAnalystDataFrame(pd.DataFrame):
916
889
  return out
917
890
 
918
891
  # Show distribution
919
- @versionadded(version="3.2.0")
892
+ @versionadded("3.2.0")
920
893
  def show_distribution(
921
894
  self,
922
895
  column_name: str,
@@ -964,6 +937,8 @@ class DataAnalystDataFrame(pd.DataFrame):
964
937
  6 800 10 10.0
965
938
  7 100 9 9.0
966
939
  8 500 4 4.0
940
+
941
+
967
942
  """
968
943
  out = self[column_name].value_counts(dropna=dropna).to_frame().reset_index()
969
944
  if show_percentage:
@@ -977,12 +952,12 @@ class DataAnalystDataFrame(pd.DataFrame):
977
952
  return out
978
953
 
979
954
  # Help
980
- @staticmethod
981
- def dadf_help() -> list[str]:
955
+ @classmethod
956
+ def dadf_help(cls) -> list[str]:
982
957
  """
983
958
  Show all available method of DataAnalystDataFrame
984
959
  """
985
- list_of_method = list(set(dir(__class__)) - set(dir(pd.DataFrame))) # type: ignore
960
+ list_of_method = list(set(dir(cls)) - set(dir(pd.DataFrame)))
986
961
  return sorted(list_of_method)
987
962
 
988
963
  # Sample DataFrame
@@ -1066,7 +1041,7 @@ class DADF(DataAnalystDataFrame):
1066
1041
  class DADF_WIP(DADF):
1067
1042
  """W.I.P"""
1068
1043
 
1069
- @versionadded(version="4.0.0")
1044
+ @versionadded("4.0.0")
1070
1045
  def subtract_df(self, other: Self | pd.DataFrame) -> Self:
1071
1046
  """
1072
1047
  Subtract DF to find the different rows
@@ -1079,7 +1054,7 @@ class DADF_WIP(DADF):
1079
1054
  )
1080
1055
  return self.__class__(out)
1081
1056
 
1082
- @versionadded(version="4.0.0")
1057
+ @versionadded("4.0.0")
1083
1058
  def merge_left(
1084
1059
  self,
1085
1060
  other: Self | pd.DataFrame,
@@ -1092,7 +1067,7 @@ class DADF_WIP(DADF):
1092
1067
  :param columns: Columns to take from df2
1093
1068
  """
1094
1069
 
1095
- if columns:
1070
+ if columns is not None:
1096
1071
  current_col = [on]
1097
1072
  current_col.extend(columns)
1098
1073
  col = other.columns.to_list()
@@ -1101,9 +1076,3 @@ class DADF_WIP(DADF):
1101
1076
 
1102
1077
  out = self.merge(other, how="left", on=on)
1103
1078
  return self.__class__(out)
1104
-
1105
-
1106
- # Run
1107
- ###########################################################################
1108
- if __name__ == "__main__":
1109
- logger.setLevel(10)