absfuyu 3.2.0__py3-none-any.whl → 3.3.3__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 (61) hide show
  1. absfuyu/__init__.py +3 -10
  2. absfuyu/__main__.py +5 -250
  3. absfuyu/cli/__init__.py +51 -0
  4. absfuyu/cli/color.py +24 -0
  5. absfuyu/cli/config_group.py +56 -0
  6. absfuyu/cli/do_group.py +76 -0
  7. absfuyu/cli/game_group.py +109 -0
  8. absfuyu/config/__init__.py +55 -94
  9. absfuyu/config/config.json +0 -7
  10. absfuyu/core.py +5 -66
  11. absfuyu/everything.py +7 -9
  12. absfuyu/extensions/beautiful.py +30 -23
  13. absfuyu/extensions/dev/__init__.py +11 -8
  14. absfuyu/extensions/dev/password_hash.py +4 -2
  15. absfuyu/extensions/dev/passwordlib.py +7 -5
  16. absfuyu/extensions/dev/project_starter.py +4 -2
  17. absfuyu/extensions/dev/shutdownizer.py +148 -0
  18. absfuyu/extensions/extra/__init__.py +1 -2
  19. absfuyu/extensions/extra/data_analysis.py +110 -58
  20. absfuyu/fun/WGS.py +50 -26
  21. absfuyu/fun/__init__.py +6 -7
  22. absfuyu/fun/tarot.py +1 -1
  23. absfuyu/game/__init__.py +75 -81
  24. absfuyu/game/game_stat.py +36 -0
  25. absfuyu/game/sudoku.py +41 -48
  26. absfuyu/game/tictactoe.py +303 -548
  27. absfuyu/game/wordle.py +56 -47
  28. absfuyu/general/__init__.py +17 -7
  29. absfuyu/general/content.py +16 -15
  30. absfuyu/general/data_extension.py +282 -90
  31. absfuyu/general/generator.py +67 -67
  32. absfuyu/general/human.py +74 -78
  33. absfuyu/logger.py +94 -68
  34. absfuyu/pkg_data/__init__.py +29 -25
  35. absfuyu/py.typed +0 -0
  36. absfuyu/sort.py +61 -47
  37. absfuyu/tools/__init__.py +0 -1
  38. absfuyu/tools/converter.py +80 -62
  39. absfuyu/tools/keygen.py +62 -67
  40. absfuyu/tools/obfuscator.py +57 -53
  41. absfuyu/tools/stats.py +24 -24
  42. absfuyu/tools/web.py +10 -9
  43. absfuyu/util/__init__.py +38 -40
  44. absfuyu/util/api.py +53 -43
  45. absfuyu/util/json_method.py +25 -27
  46. absfuyu/util/lunar.py +20 -24
  47. absfuyu/util/path.py +362 -241
  48. absfuyu/util/performance.py +36 -98
  49. absfuyu/util/pkl.py +8 -8
  50. absfuyu/util/zipped.py +17 -19
  51. absfuyu/version.py +137 -148
  52. absfuyu-3.3.3.dist-info/METADATA +124 -0
  53. absfuyu-3.3.3.dist-info/RECORD +59 -0
  54. {absfuyu-3.2.0.dist-info → absfuyu-3.3.3.dist-info}/WHEEL +1 -2
  55. {absfuyu-3.2.0.dist-info → absfuyu-3.3.3.dist-info}/entry_points.txt +1 -0
  56. {absfuyu-3.2.0.dist-info → absfuyu-3.3.3.dist-info/licenses}/LICENSE +1 -1
  57. absfuyu/extensions/dev/pkglib.py +0 -98
  58. absfuyu/game/tictactoe2.py +0 -318
  59. absfuyu-3.2.0.dist-info/METADATA +0 -216
  60. absfuyu-3.2.0.dist-info/RECORD +0 -55
  61. absfuyu-3.2.0.dist-info/top_level.txt +0 -1
absfuyu/util/path.py CHANGED
@@ -1,11 +1,10 @@
1
- # -*- coding: utf-8 -*-
2
1
  """
3
2
  Absfuyu: Path
4
3
  -------------
5
4
  Path related
6
5
 
7
- Version: 1.5.1
8
- Date updated: 24/11/2023 (dd/mm/yyyy)
6
+ Version: 1.6.5
7
+ Date updated: 10/04/2024 (dd/mm/yyyy)
9
8
 
10
9
  Feature:
11
10
  --------
@@ -13,138 +12,152 @@ Feature:
13
12
  - SaveFileAs
14
13
  """
15
14
 
16
-
17
15
  # Module level
18
16
  ###########################################################################
19
17
  __all__ = [
20
- # "here_location", "location_wrap", "get_all_file_path",
18
+ # Main
21
19
  "Directory",
22
- "SaveFileAs"
20
+ "SaveFileAs",
21
+ # Support
22
+ "FileOrFolderWithModificationTime",
23
+ "DirectoryInfo",
23
24
  ]
24
25
 
25
26
 
26
27
  # Library
27
28
  ###########################################################################
28
- from datetime import datetime
29
- from functools import partial
30
29
  import os
31
- from pathlib import Path
32
30
  import re
33
31
  import shutil
34
- from typing import Any, List, Literal, Tuple, Union
32
+ from datetime import datetime
33
+ from functools import partial
34
+ from pathlib import Path
35
+ from typing import Any, List, Literal, NamedTuple, Optional, Union
36
+
37
+ from deprecated.sphinx import versionadded
35
38
 
36
- from absfuyu.logger import logger
39
+ from absfuyu.logger import LogLevel, logger
37
40
 
38
41
 
39
- # Function
42
+ # Support Class
40
43
  ###########################################################################
41
- def here_location(): # Depreciated
44
+ @versionadded(version="3.3.0")
45
+ class FileOrFolderWithModificationTime(NamedTuple):
42
46
  """
43
- Return current file location
44
-
45
- If fail then return current working directory
46
- """
47
- try:
48
- return os.path.abspath(os.path.dirname(__file__))
49
- except:
50
- return os.getcwd()
51
-
52
- # return os.path.abspath(os.path.dirname(__file__))
53
-
54
- def location_wrap(file_location: str): # Depreciated
55
- """
56
- This function fix some `current working directory` error and return `abspath`
57
- """
58
- assert isinstance(file_location, str), "Must be a string"
59
- try:
60
- here = here_location()
61
- except:
62
- here = ""
63
- return os.path.join(here, file_location)
64
-
65
- def get_all_file_path(folder: str, *file_type: str) -> list: # Depreciated
66
- """
67
- Return a list of tuple: (path to choosen file type, filename)
68
-
69
- - ``folder``: Folder path to search in
70
- - ``file_type``: File type/extension without the ``"."`` symbol.
71
-
72
- Support multiple file type (separate with ``","`` (coma))
73
- (Example: ``jpg``, ``png``, ``npy``)
47
+ File or Folder with modification time
48
+
49
+ :param path: Original path
50
+ :param modification_time: Modification time
74
51
  """
75
- # Check file type
76
- # If no `file_type` entered then proceed to print available file type
77
- if len(file_type) < 1:
78
- available_file_type = []
79
- for _, _, files in os.walk(folder):
80
- for file in files:
81
- temp = re.search(r"\b.*[.](\w+$)\b", file)
82
- if temp is not None:
83
- available_file_type.append(temp[1])
84
- # print(f"Available file type: {set(available_file_type)}")
85
- # return list(set(available_file_type))
86
- # return None
87
- raise ValueError(f"Available file type: {set(available_file_type)}")
88
-
89
- # Generate regex pattern
90
- temp_pattern = "|".join(f"[.]{x}" for x in file_type)
91
- pattern = f"\\b^([\w ]+)({temp_pattern}$)\\b"
92
- # print("Search pattern: ", pattern)
93
-
94
- # Iter through each folder to find file
95
- file_location = []
96
- # for root, dirs, files in os.walk(folder):
97
- for root, _, files in os.walk(folder):
98
- for file in files:
99
- result = re.search(pattern, file)
100
- if result is not None:
101
- file_location.append((os.path.join(root, file), result[1]))
102
- return file_location
103
-
104
- def here_sniplet():
105
- """Return current file location code"""
106
- snip = """\
107
- import os
108
- here = os.path.abspath(os.path.dirname(__file__))
109
52
 
110
- from pathlib import Path
111
- here = Path(__file__)
112
- """
113
- return snip
53
+ path: Path
54
+ modification_time: datetime
55
+
56
+
57
+ @versionadded(version="3.3.0")
58
+ class DirectoryInfo(NamedTuple):
59
+ """Information of a directory"""
60
+
61
+ files: int
62
+ folders: int
63
+ creation_time: datetime
64
+ modification_time: datetime
114
65
 
115
66
 
116
- # Class
67
+ # Class - Directory | version 3.4.0: Remake Directory into modular class
117
68
  ###########################################################################
118
- class Directory:
119
- """
120
- Some shortcuts for directory
121
-
122
- - list_structure
123
- - delete, rename, copy, move
124
- - zip
125
- """
69
+ class DirectoryBase:
126
70
  def __init__(
127
- self,
128
- source_path: Union[str, Path],
129
- create_if_not_exist: bool = False,
130
- ) -> None:
71
+ self,
72
+ source_path: Union[str, Path],
73
+ create_if_not_exist: bool = False,
74
+ ) -> None:
131
75
  """
132
- source_path: Source folder
133
- create_if_not_exist: Create directory when not exist
76
+ Parameters
77
+ ----------
78
+ source_path : str | Path
79
+ Source folder
80
+
81
+ create_if_not_exist : bool
82
+ Create directory when not exist
83
+ (Default: ``False``)
134
84
  """
135
- self.source_path = Path(source_path).absolute()
85
+ self.source_path = Path(source_path)
136
86
  if create_if_not_exist:
137
- self.source_path.mkdir(exist_ok=True)
87
+ if not self.source_path.exists():
88
+ self.source_path.mkdir(exist_ok=True, parents=True)
89
+
138
90
  def __str__(self) -> str:
139
- return f"{self.__class__.__name__}({self.source_path})"
91
+ return self.source_path.__str__()
92
+
140
93
  def __repr__(self) -> str:
141
- return self.__str__()
142
-
94
+ return f"{self.__class__.__name__}({self.source_path})"
95
+
96
+ def __format__(self, __format_spec: str) -> str:
97
+ """
98
+ Change format of an object.
99
+ Avaiable option: ``info``
100
+
101
+ Usage
102
+ -----
103
+ >>> print(f"{<object>:<format_spec>}")
104
+ >>> print(<object>.__format__(<format_spec>))
105
+ >>> print(format(<object>, <format_spec>))
106
+ """
107
+ # Show quick info
108
+ if __format_spec.lower().startswith("info"):
109
+ return self.quick_info().__repr__()
110
+
111
+ # No format spec
112
+ return self.__repr__()
113
+
114
+ # Everything
115
+ @property
116
+ @versionadded(version="3.3.0")
117
+ def everything(self) -> List[Path]:
118
+ """
119
+ Every folders and files in this Directory
120
+ """
121
+ return list(x for x in self.source_path.glob("**/*"))
122
+
123
+ @versionadded(version="3.3.0")
124
+ def _every_folder(self) -> List[Path]:
125
+ """
126
+ Every folders in this Directory
127
+ """
128
+ return list(x for x in self.source_path.glob("**/*") if x.is_dir())
129
+
130
+ @versionadded(version="3.3.0")
131
+ def _every_file(self) -> List[Path]:
132
+ """
133
+ Every folders in this Directory
134
+ """
135
+ return list(x for x in self.source_path.glob("**/*") if x.is_file())
136
+
137
+ # Quick information
138
+ @versionadded(version="3.3.0")
139
+ def quick_info(self) -> DirectoryInfo:
140
+ """
141
+ Quick information about this Directory
142
+
143
+ :rtype: DirectoryInfo
144
+ """
145
+ source_stat: os.stat_result = self.source_path.stat()
146
+ out = DirectoryInfo(
147
+ files=len(self._every_file()),
148
+ folders=len(self._every_folder()),
149
+ creation_time=datetime.fromtimestamp(source_stat.st_ctime),
150
+ modification_time=datetime.fromtimestamp(source_stat.st_mtime),
151
+ )
152
+ return out
153
+
154
+
155
+ class DirectoryBasicOperation(DirectoryBase):
143
156
  # Rename
144
157
  def rename(self, new_name: str) -> None:
145
158
  """
146
159
  Rename directory
147
-
160
+
148
161
  Parameters
149
162
  ----------
150
163
  new_name : str
@@ -152,13 +165,12 @@ class Directory:
152
165
  """
153
166
  try:
154
167
  logger.debug(f"Renaming to {new_name}...")
155
- self.source_path.rename(
156
- self.source_path.parent.joinpath(new_name)
157
- )
168
+ self.source_path.rename(self.source_path.with_name(new_name))
158
169
  logger.debug(f"Renaming to {new_name}...DONE")
159
170
  except Exception as e:
160
171
  logger.error(e)
161
-
172
+ # return self.source_path
173
+
162
174
  # Copy
163
175
  def copy(self, dst: Path) -> None:
164
176
  """
@@ -173,14 +185,14 @@ class Directory:
173
185
  try:
174
186
  try:
175
187
  shutil.copytree(self.source_path, Path(dst), dirs_exist_ok=True)
176
- except:
188
+ except Exception:
177
189
  shutil.copytree(self.source_path, Path(dst))
178
190
  logger.debug(f"Copying to {dst}...DONE")
179
191
  except Exception as e:
180
192
  logger.error(e)
181
-
193
+
182
194
  # Move
183
- def move(self, dst: Path) -> None:
195
+ def move(self, dst: Path, content_only: bool = False) -> None:
184
196
  """
185
197
  Move entire directory
186
198
 
@@ -188,99 +200,40 @@ class Directory:
188
200
  ----------
189
201
  dst : Path
190
202
  Destination
203
+
204
+ content_only : bool
205
+ Only move content inside the folder (Default: ``False``; Move entire folder)
191
206
  """
192
207
  try:
193
208
  logger.debug(f"Moving to {dst}...")
194
- shutil.move(self.source_path, Path(dst))
209
+ if content_only:
210
+ for x in self.source_path.iterdir():
211
+ shutil.move(x, Path(dst))
212
+ else:
213
+ shutil.move(self.source_path, Path(dst))
195
214
  logger.debug(f"Moving to {dst}...DONE")
196
- except Exception as e:
197
- logger.error(e)
198
-
199
- # Directory structure
200
- def _list_dir(self, *ignore: str) -> List[Path]:
201
- """List all directories and files"""
202
- logger.debug(f"Base folder: {self.source_path.name}")
203
-
204
- temp = self.source_path.glob("**/*")
205
- # No ignore rules
206
- if len(ignore) == 0:
207
- return [x.relative_to(self.source_path) for x in temp]
208
215
 
209
- # With ignore rules
210
- ignore_pattern = "|".join(ignore)
211
- logger.debug(f"Ignore pattern: {ignore_pattern}")
212
- return [x.relative_to(self.source_path) for x in temp if re.search(ignore_pattern, x.name) is None]
216
+ except shutil.Error as e: # File already exists
217
+ logger.error(e)
218
+ logger.debug("Overwriting file...")
219
+ if content_only:
220
+ for x in self.source_path.iterdir():
221
+ shutil.move(x, Path(dst).joinpath(x.name))
222
+ else:
223
+ shutil.move(self.source_path, Path(dst))
224
+ logger.debug("Overwriting file...DONE")
213
225
 
214
- def _separate_dir_and_files(
215
- self,
216
- list_of_paths: List[Path],
217
- *,
218
- tab_symbol: str = None,
219
- sub_dir_symbol: str = None
220
- ) -> List[str]:
226
+ # Delete folder
227
+ def _mtime_folder(self) -> List[FileOrFolderWithModificationTime]:
221
228
  """
222
- Separate dir and file and transform into folder structure
223
-
224
- list_of_paths: List of paths
225
- tab_symbol: Tab symbol
226
- sub_dir_symbol: Sub-directory symbol
229
+ Get modification time of file/folder (first level only)
227
230
  """
228
- if not tab_symbol:
229
- tab_symbol = "\t"
230
- if not sub_dir_symbol:
231
- sub_dir_symbol = "|-- "
232
-
233
- temp = sorted([str(x).split("/") for x in list_of_paths]) # Linux
234
- if max(map(len, temp)) == 1:
235
- temp = sorted([str(x).split("\\") for x in list_of_paths]) # Windows
236
-
237
- return [f"{tab_symbol*(len(x)-1)}{sub_dir_symbol}{x[-1]}" for x in temp]
238
-
239
- def list_structure(self, *ignore: str) -> str:
240
- """
241
- List folder structure
242
-
243
- Parameters
244
- ----------
245
- ignore : str
246
- Tuple contains patterns to ignore
247
-
248
- Returns
249
- -------
250
- str
251
- Directory structure
252
-
253
-
254
- Example (For typical python library):
255
- -------------------------------------
256
- >>> test = Directory(<source path>)
257
- >>> test.list_structure(
258
- "__pycache__",
259
- ".pyc",
260
- "__init__",
261
- "__main__",
231
+ return [
232
+ FileOrFolderWithModificationTime(
233
+ path, datetime.fromtimestamp(path.stat().st_mtime)
262
234
  )
263
- ...
264
- """
265
- temp = self._list_dir(*ignore)
266
- out = self._separate_dir_and_files(temp)
267
- return "\n".join(out)
268
-
269
- def list_structure_pkg(self) -> str:
270
- """
271
- List folder structure of a typical python package
272
-
273
- Returns
274
- -------
275
- str
276
- Directory structure
277
- """
278
- return self.list_structure("__pycache__", ".pyc")
279
-
280
- # Delete folder
281
- def _mtime_folder(self) -> List[Tuple[Path, datetime]]:
282
- """Get modification time of file/folder"""
283
- return [(x, datetime.fromtimestamp(x.stat().st_mtime)) for x in self.source_path.glob("*")]
235
+ for path in self.source_path.glob("*")
236
+ ]
284
237
 
285
238
  @staticmethod
286
239
  def _delete_files(list_of_files: List[Path]) -> None:
@@ -295,39 +248,34 @@ class Directory:
295
248
  shutil.rmtree(x)
296
249
  else:
297
250
  x.unlink()
298
- logger.debug(f"Removing {x}...REMOVED")
299
- except:
251
+ logger.debug(f"Removing {x}...SUCCEED")
252
+ except Exception:
300
253
  logger.error(f"Removing {x}...FAILED")
301
254
 
302
255
  @staticmethod
303
256
  def _date_filter(
304
- value: Tuple[Path, datetime],
305
- period: Literal["Y", "M", "D"] = "Y"
306
- ) -> bool:
257
+ value: FileOrFolderWithModificationTime,
258
+ period: Literal["Y", "M", "D"] = "Y",
259
+ ) -> bool:
307
260
  """
308
261
  Filter out file with current Year|Month|Day
309
262
  """
310
- period = period.upper().strip()
311
263
  data = {
312
- "Y": value[1].year,
313
- "M": value[1].month,
314
- "D": value[1].day
264
+ "Y": value.modification_time.year,
265
+ "M": value.modification_time.month,
266
+ "D": value.modification_time.day,
315
267
  }
316
268
  now = datetime.now()
317
- ntime = {
318
- "Y": now.year,
319
- "M": now.month,
320
- "D": now.day
321
- }
269
+ ntime = {"Y": now.year, "M": now.month, "D": now.day}
322
270
  return data[period] != ntime[period]
323
271
 
324
272
  def delete(
325
- self,
326
- entire: bool = False,
327
- *,
328
- based_on_time: bool = False,
329
- keep: Literal["Y", "M", "D"] = "Y"
330
- ) -> None:
273
+ self,
274
+ entire: bool = False,
275
+ *,
276
+ based_on_time: bool = False,
277
+ keep: Literal["Y", "M", "D"] = "Y",
278
+ ) -> None:
331
279
  """
332
280
  Deletes everything
333
281
 
@@ -337,36 +285,41 @@ class Directory:
337
285
  | ``True``: Deletes the folder itself
338
286
  | ``False``: Deletes content inside only
339
287
  | (Default: ``False``)
340
-
288
+
341
289
  based_on_time : bool
342
290
  | ``True``: Deletes everything except ``keep`` period
343
291
  | ``False``: Works normal
344
292
  | (Default: ``False``)
345
-
293
+
346
294
  keep : Literal["Y", "M", "D"]
347
295
  Delete all file except current ``Year`` | ``Month`` | ``Day``
348
296
  """
349
297
  try:
350
298
  logger.info(f"Removing {self.source_path}...")
351
-
299
+
352
300
  if entire:
353
301
  shutil.rmtree(self.source_path)
354
302
  else:
355
303
  if based_on_time:
356
304
  filter_func = partial(self._date_filter, period=keep)
357
- self._delete_files([x[0] for x in filter(filter_func, self._mtime_folder())])
305
+ # self._delete_files([x[0] for x in filter(filter_func, self._mtime_folder())])
306
+ self._delete_files(
307
+ [x.path for x in filter(filter_func, self._mtime_folder())]
308
+ )
358
309
  else:
359
- self._delete_files(map(lambda x: x[0], self._mtime_folder()))
360
-
361
- logger.info(f"Removing {self.source_path}...COMPLETED")
362
- except:
363
- logger.error(f"Removing {self.source_path}...FAILED")
310
+ self._delete_files(
311
+ map(lambda x: x.path, self._mtime_folder()) # type: ignore
312
+ )
313
+
314
+ logger.info(f"Removing {self.source_path}...SUCCEED")
315
+ except Exception as e:
316
+ logger.error(f"Removing {self.source_path}...FAILED\n{e}")
364
317
 
365
318
  # Zip
366
- def compress(self, *, format: str = "zip") -> None:
319
+ def compress(self, *, format: str = "zip") -> Union[Path, None]:
367
320
  """
368
321
  Compress the directory (Default: Create ``.zip`` file)
369
-
322
+
370
323
  Parameters
371
324
  ----------
372
325
  format : Literal["zip", "tar", "gztar", "bztar", "xztar"]
@@ -375,38 +328,201 @@ class Directory:
375
328
  - ``gztar``: gzip'ed tar-file (if the ``zlib`` module is available).
376
329
  - ``bztar``: bzip2'ed tar-file (if the ``bz2`` module is available).
377
330
  - ``xztar``: xz'ed tar-file (if the ``lzma`` module is available).
331
+
332
+ Returns
333
+ -------
334
+ Path
335
+ Compressed path
336
+ None
337
+ When fail to compress
378
338
  """
379
339
  logger.debug(f"Zipping {self.source_path}...")
380
340
  try:
381
- zip_name = self.source_path.parent.joinpath(self.source_path.name)
382
- shutil.make_archive(zip_name, format=format, root_dir=self.source_path)
341
+ # zip_name = self.source_path.parent.joinpath(self.source_path.name).__str__()
342
+ # shutil.make_archive(zip_name, format=format, root_dir=self.source_path)
343
+ zip_path = shutil.make_archive(
344
+ self.source_path.__str__(), format=format, root_dir=self.source_path
345
+ )
383
346
  logger.debug(f"Zipping {self.source_path}...DONE")
384
- except:
385
- logger.error(f"Zipping {self.source_path}...FAILED")
347
+ logger.debug(f"Path: {zip_path}")
348
+ return Path(zip_path)
349
+ except Exception as e:
350
+ logger.error(f"Zipping {self.source_path}...FAILED\n{e}")
351
+ return None
352
+
353
+
354
+ class DirectoryTree(DirectoryBase):
355
+ pass
356
+
357
+
358
+ class Directory(DirectoryBasicOperation, DirectoryTree):
359
+ """
360
+ Some shortcuts for directory
361
+
362
+ - list_structure
363
+ - delete, rename, copy, move
364
+ - zip
365
+ - quick_info
366
+ """
367
+
368
+ # Directory structure
369
+ def _list_dir(self, *ignore: str) -> List[Path]:
370
+ """
371
+ List all directories and files
372
+
373
+ Parameters
374
+ ----------
375
+ ignore : str
376
+ List of pattern to ignore. Example: "__pycache__", ".pyc"
377
+ """
378
+ logger.debug(f"Base folder: {self.source_path.name}")
386
379
 
380
+ list_of_path = self.source_path.glob("**/*")
387
381
 
382
+ # No ignore rules
383
+ if len(ignore) == 0: # No ignore pattern
384
+ return [path.relative_to(self.source_path) for path in list_of_path]
385
+
386
+ # With ignore rules
387
+ # ignore_pattern = "|".join(ignore)
388
+ ignore_pattern = re.compile("|".join(ignore))
389
+ logger.debug(f"Ignore pattern: {ignore_pattern}")
390
+ return [
391
+ path.relative_to(self.source_path)
392
+ for path in list_of_path
393
+ if re.search(ignore_pattern, path.name) is None
394
+ ]
395
+
396
+ @staticmethod
397
+ @versionadded(version="3.3.0")
398
+ def _split_dir(list_of_path: List[Path]) -> List[List[str]]:
399
+ """
400
+ Split pathname by ``os.sep``
401
+
402
+ Parameters
403
+ ----------
404
+ list_of_path : list[Path]
405
+ List of Path
406
+
407
+ Returns
408
+ -------
409
+ list[list[str]]
410
+ List of splitted dir
411
+
412
+
413
+ Example:
414
+ --------
415
+ >>> test = [Path(test_root/test_not_root), ...]
416
+ >>> Directory._split_dir(test)
417
+ [[test_root, test_not_root], [...]...]
418
+ """
419
+
420
+ return sorted([str(path).split(os.sep) for path in list_of_path])
421
+
422
+ def _separate_dir_and_files(
423
+ self,
424
+ list_of_path: List[Path],
425
+ *,
426
+ tab_symbol: Optional[str] = None,
427
+ sub_dir_symbol: Optional[str] = None,
428
+ ) -> List[str]:
429
+ """
430
+ Separate dir and file and transform into folder structure
431
+
432
+ Parameters
433
+ ----------
434
+ list_of_path : list[Path]
435
+ List of paths
436
+
437
+ tab_symbol : str | None
438
+ Tab symbol
439
+ (Default: ``"\\t"``)
440
+
441
+ sub_dir_symbol : str | None
442
+ Sub-directory symbol
443
+ (Default: ``"|-- "``)
444
+
445
+ Returns
446
+ -------
447
+ list[str]
448
+ Folder structure ready to print
449
+ """
450
+ # Check for tab and sub-dir symbol
451
+ if not tab_symbol:
452
+ tab_symbol = "\t"
453
+ if not sub_dir_symbol:
454
+ sub_dir_symbol = "|-- "
455
+
456
+ temp: List[List[str]] = self._split_dir(list_of_path)
457
+
458
+ return [ # Returns n-tab space with sub-dir-symbol for the last item in x
459
+ f"{tab_symbol * (len(x) - 1)}{sub_dir_symbol}{x[-1]}" for x in temp
460
+ ]
461
+
462
+ def list_structure(self, *ignore: str) -> str:
463
+ """
464
+ List folder structure
465
+
466
+ Parameters
467
+ ----------
468
+ ignore : str
469
+ Tuple contains patterns to ignore
470
+
471
+ Returns
472
+ -------
473
+ str
474
+ Directory structure
475
+
476
+
477
+ Example (For typical python library):
478
+ -------------------------------------
479
+ >>> test = Directory(<source path>)
480
+ >>> test.list_structure(
481
+ "__pycache__",
482
+ ".pyc",
483
+ "__init__",
484
+ "__main__",
485
+ )
486
+ ...
487
+ """
488
+ temp: List[Path] = self._list_dir(*ignore)
489
+ out: List[str] = self._separate_dir_and_files(temp)
490
+ return "\n".join(out) # Join the list
491
+
492
+ def list_structure_pkg(self) -> str:
493
+ """
494
+ List folder structure of a typical python package
495
+
496
+ Returns
497
+ -------
498
+ str
499
+ Directory structure
500
+ """
501
+ return self.list_structure("__pycache__", ".pyc")
502
+
503
+
504
+ # Class - SaveFileAs
505
+ ###########################################################################
388
506
  class SaveFileAs:
389
507
  """File as multiple file type"""
390
- def __init__(
391
- self,
392
- data: Any,
393
- *,
394
- encoding: Union[str, None] = "utf-8"
395
- ) -> None:
508
+
509
+ def __init__(self, data: Any, *, encoding: Union[str, None] = "utf-8") -> None:
396
510
  """
397
511
  :param encoding: Default: utf-8
398
512
  """
399
513
  self.data = data
400
514
  self.encoding = encoding
515
+
401
516
  def __str__(self) -> str:
402
517
  return f"{self.__class__.__name__}()"
518
+
403
519
  def __repr__(self) -> str:
404
520
  return self.__str__()
405
521
 
406
522
  def to_txt(self, path: Union[str, Path]) -> None:
407
523
  """
408
524
  Save as ``.txt`` file
409
-
525
+
410
526
  Parameters
411
527
  ----------
412
528
  path : Path
@@ -414,11 +530,11 @@ class SaveFileAs:
414
530
  """
415
531
  with open(path, "w", encoding=self.encoding) as file:
416
532
  file.writelines(self.data)
417
-
533
+
418
534
  # def to_pickle(self, path: Union[str, Path]) -> None:
419
535
  # """
420
536
  # Save as .pickle file
421
-
537
+
422
538
  # :param path: Save location
423
539
  # """
424
540
  # from absfuyu.util.pkl import Pickler
@@ -427,7 +543,7 @@ class SaveFileAs:
427
543
  # def to_json(self, path: Union[str, Path]) -> None:
428
544
  # """
429
545
  # Save as .json file
430
-
546
+
431
547
  # :param path: Save location
432
548
  # """
433
549
  # from absfuyu.util.json_method import JsonFile
@@ -435,8 +551,13 @@ class SaveFileAs:
435
551
  # temp.save_json()
436
552
 
437
553
 
554
+ # Dev and Test new feature before get added to the main class
555
+ ###########################################################################
556
+ class _NewDirFeature(Directory):
557
+ pass
558
+
559
+
438
560
  # Run
439
561
  ###########################################################################
440
562
  if __name__ == "__main__":
441
- logger.setLevel(10)
442
-
563
+ logger.setLevel(LogLevel.DEBUG)