ures 1.3.1__tar.gz

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.
ures-1.3.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 stone-home
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ures-1.3.1/PKG-INFO ADDED
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.1
2
+ Name: ures
3
+ Version: 1.3.1
4
+ Summary: The project is designed to provide standard utilization functions for improving the reusability of frequently used functions.
5
+ Home-page: https://github.com/stone-home/Python-URes
6
+ License: MIT
7
+ Keywords: utilization,Research Tool,Python
8
+ Author: stone
9
+ Author-email: github@stone-bo.com
10
+ Requires-Python: >=3.11,<4.0
11
+ Classifier: Development Status :: 1 - Planning
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Utilities
21
+ Requires-Dist: python-frontmatter (>=1.1.0,<2.0.0)
22
+ Project-URL: Repository, https://github.com/stone-home/Python-URes
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Utility for Research (URes)
26
+
27
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
28
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
29
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+ [![Super-Linter](https://github.com/stone-home/Python-URes/actions/workflows/linter.yaml/badge.svg)](https://github.com/stone-home/Python-URes/actions/workflows/linter.yaml)
32
+ [![Trufflehog](https://github.com/stone-home/Python-URes/actions/workflows/secret-check.yaml/badge.svg)](https://github.com/stone-home/Python-URes/actions/workflows/secret-check.yaml)
33
+ [![Code Testing](https://github.com/stone-home/Python-URes/actions/workflows/test.yaml/badge.svg)](https://github.com/stone-home/Python-URes/actions/workflows/test.yaml)
34
+
ures-1.3.1/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Utility for Research (URes)
2
+
3
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
4
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
5
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Super-Linter](https://github.com/stone-home/Python-URes/actions/workflows/linter.yaml/badge.svg)](https://github.com/stone-home/Python-URes/actions/workflows/linter.yaml)
8
+ [![Trufflehog](https://github.com/stone-home/Python-URes/actions/workflows/secret-check.yaml/badge.svg)](https://github.com/stone-home/Python-URes/actions/workflows/secret-check.yaml)
9
+ [![Code Testing](https://github.com/stone-home/Python-URes/actions/workflows/test.yaml/badge.svg)](https://github.com/stone-home/Python-URes/actions/workflows/test.yaml)
@@ -0,0 +1,65 @@
1
+ [tool.poetry]
2
+ name = "ures"
3
+ version = "1.3.1"
4
+ description = "The project is designed to provide standard utilization functions for improving the reusability of frequently used functions."
5
+ authors = ["stone <github@stone-bo.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ repository = "https://github.com/stone-home/Python-URes"
9
+ keywords = [
10
+ "utilization",
11
+ "Research Tool",
12
+ "Python"
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 1 - Planning",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: Implementation :: PyPy",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Topic :: Utilities",
23
+ ]
24
+ packages = [
25
+ { include = "ures"},
26
+ ]
27
+ exclude = [
28
+ "tests",
29
+ "docs",
30
+ "examples",
31
+ "dist",
32
+ "build",
33
+ "ures.egg-info",
34
+ ".git",
35
+ ".gitignore",
36
+ ".github",
37
+ ".pre-commit-config.yaml",
38
+ "commitlint.config.js",
39
+ "run_ut.sh",
40
+ ".env"
41
+ ]
42
+
43
+ [tool.poetry.dependencies]
44
+ python = "^3.11"
45
+ python-frontmatter = "^1.1.0"
46
+
47
+
48
+ [tool.poetry.group.test.dependencies]
49
+ pytest = "^8.3.4"
50
+ pytest-cov = "^6.0.0"
51
+
52
+
53
+
54
+ [build-system]
55
+ requires = ["poetry-core"]
56
+ build-backend = "poetry.core.masonry.api"
57
+
58
+
59
+ # pyproject.toml
60
+ [tool.pytest.ini_options]
61
+ minversion = "8.0"
62
+ addopts = "-q --cache-clear"
63
+ testpaths = [
64
+ "tests",
65
+ ]
File without changes
@@ -0,0 +1,4 @@
1
+ from .tree import TreeNode
2
+ from .bi_directional_links import BiDirection
3
+
4
+ __all__ = ["TreeNode", "BiDirection"]
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+ import uuid
3
+ from typing import Any, AnyStr, Dict, Optional
4
+
5
+
6
+ class BiDirection:
7
+ def __init__(self, value: Any):
8
+ """Create a bi-directional link node.
9
+
10
+ Args:
11
+ value (Any): The value of the node.
12
+ """
13
+ self._prev: Optional[BiDirection] = self
14
+ self._next: Optional[BiDirection] = self
15
+ self._value: Any = value
16
+ self._id = uuid.uuid4().hex
17
+
18
+ @property
19
+ def prev(self) -> BiDirection:
20
+ return self._prev
21
+
22
+ @property
23
+ def next(self) -> BiDirection:
24
+ return self._next
25
+
26
+ @property
27
+ def value(self) -> Any:
28
+ return self._value
29
+
30
+ @property
31
+ def id(self) -> str:
32
+ return self._id
33
+
34
+ def insert_after(self, node: BiDirection):
35
+ """Insert a node after the current node.
36
+
37
+ Args:
38
+ node (BiDirection): The node to be inserted.
39
+
40
+ Returns:
41
+ None
42
+
43
+ """
44
+ node._prev = self
45
+ node._next = self._next
46
+ self._next._prev = node
47
+ self._next = node
48
+
49
+ def insert_before(self, node: BiDirection):
50
+ """Insert a node before the current node.
51
+
52
+ Args:
53
+ node (BiDirection): The node to be inserted.
54
+
55
+ Returns:
56
+ None
57
+
58
+ """
59
+ node._prev = self._prev
60
+ node._next = self
61
+ self._prev._next = node
62
+ self._prev = node
63
+
64
+ def remove(self):
65
+ """Remove the current node.
66
+
67
+ Returns:
68
+ None
69
+ """
70
+ self._prev._next = self._next
71
+ self._next._prev = self._prev
72
+ self._prev = self
73
+ self._next = self
74
+
75
+ def search(self, value: Any) -> Optional[BiDirection]:
76
+ """Search a node by value.
77
+
78
+ Args:
79
+ value (Any): The value to be searched.
80
+
81
+ Returns:
82
+ Optional[BiDirection]: The node with the value.
83
+ """
84
+ node = self
85
+ while node.value != value and node.next != self:
86
+ node = node.next
87
+ return node if node.value == value else None
88
+
89
+ def __eq__(self, other: BiDirection) -> bool:
90
+ """Check if two nodes are equal."""
91
+ return self.id == other.id
92
+
93
+ def __str__(self):
94
+ """Return the string representation of the node."""
95
+ return str(self.value)
96
+
97
+ def __repr__(self):
98
+ """Return the string representation of the node."""
99
+ return f"BiDirection({self.value})"
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+ import uuid
3
+ from typing import Any, AnyStr, Dict, Iterator, List, Optional
4
+
5
+
6
+ class TreeNode:
7
+ def __init__(self, value: Any):
8
+ """Initialize a TreeNode object.
9
+
10
+ Args:
11
+ value (Any): The value of the node.
12
+ """
13
+ self._parent: Optional[TreeNode] = None
14
+ self._children: Dict[AnyStr, TreeNode] = {}
15
+ self._value: Any = value
16
+ self._id = uuid.uuid4().hex
17
+
18
+ @property
19
+ def parent(self) -> Optional[TreeNode]:
20
+ return self._parent
21
+
22
+ @property
23
+ def children(self) -> Dict[AnyStr, TreeNode]:
24
+ return self._children
25
+
26
+ @property
27
+ def is_leaf(self) -> bool:
28
+ return len(self.children) == 0
29
+
30
+ @property
31
+ def value(self) -> Any:
32
+ return self._value
33
+
34
+ @property
35
+ def id(self) -> str:
36
+ return self._id
37
+
38
+ def add_child(self, child: TreeNode):
39
+ """Add a child node to the current node.
40
+
41
+ Args:
42
+ child (TreeNode): The child node to be added.
43
+
44
+ Returns:
45
+ None
46
+ """
47
+ if child.id not in self.children.keys():
48
+ self._children[child.id] = child
49
+ child.set_parent(self)
50
+
51
+ def remove_child(self, child: TreeNode):
52
+ """Remove a child node from the current node.
53
+
54
+ Args:
55
+ child (TreeNode): The child node to be removed.
56
+
57
+ Returns:
58
+ None
59
+
60
+ """
61
+ if child.id in self.children.keys():
62
+ self._children.pop(child.id)
63
+ child.set_parent(None)
64
+
65
+ def set_parent(self, parent: Optional[TreeNode]):
66
+ """Set the parent node of the current node.
67
+
68
+ Args:
69
+ parent (Optional[TreeNode]): The parent node to be set.
70
+
71
+ Returns:
72
+ None
73
+ """
74
+ if parent is not None:
75
+ if isinstance(self.parent, TreeNode):
76
+ self.parent.remove_child(self)
77
+ self._parent = parent
78
+
79
+ def backward_stack(self) -> Iterator[TreeNode]:
80
+ """Get the backward stack of the current node. The direction is from the current node to the root node."""
81
+ current = self
82
+ while current is not None:
83
+ yield current
84
+ current = current.parent
85
+
86
+ def forward_stack(self, **kwargs) -> List[List[Any]]:
87
+ """Get the forward stack of the current node. The direction is from the current node to the leaf node."""
88
+ all_paths = []
89
+ self._dfs(self, [], all_paths, **kwargs)
90
+ return all_paths
91
+
92
+ def _dfs(self, node: TreeNode, current_path: list, all_paths: list, **kwargs):
93
+ assert isinstance(node, TreeNode)
94
+ _attr = kwargs.get("attr", None)
95
+ _key = node
96
+ if _attr is not None:
97
+ _key = getattr(node, _attr)
98
+ current_path.append(_key)
99
+
100
+ if not node.children:
101
+ all_paths.append(list(current_path))
102
+ else:
103
+ for child in node.children.values():
104
+ self._dfs(child, current_path, all_paths, **kwargs)
105
+ # pop up the last index of the current path, and go back to the parent node
106
+ # Example:
107
+ # current_path = [1, 2, 3]
108
+ # after pop: [1, 2]
109
+ current_path.pop()
@@ -0,0 +1,100 @@
1
+ import os
2
+ import tempfile
3
+ from typing import List, Optional
4
+
5
+
6
+ def get_file_paths(directory) -> List[str]:
7
+ """Get all file paths in the specified directory.
8
+
9
+ Args:
10
+ directory (str): the directory path
11
+
12
+ Returns:
13
+ List[str]: a list of file paths
14
+
15
+ """
16
+ file_paths = []
17
+ # Walk through the directory and its subdirectories
18
+ for root, _, files in os.walk(directory):
19
+ for file in files:
20
+ # Join the root path with the file name to get the absolute path
21
+ file_path = os.path.join(root, file)
22
+ file_paths.append(str(file_path))
23
+
24
+ return file_paths
25
+
26
+
27
+ def filter_files(part_file_name: str, directory: str, fuzz: bool = True) -> List[str]:
28
+ """Fetch all file path that contains the specified part of file name.
29
+
30
+ Args:
31
+ part_file_name (str): the part of file name or keyword
32
+ directory (str): the directory path
33
+ fuzz (bool): if True, return all files that contain the part of file name. Defaults to True.
34
+
35
+ Returns:
36
+ List[str]: a list of file paths
37
+ """
38
+ file_paths = fetch_file_paths(directory)
39
+ if fuzz:
40
+ return [
41
+ str(file_path)
42
+ for file_path in file_paths
43
+ if part_file_name in os.path.basename(str(file_path))
44
+ ]
45
+ else:
46
+ return [
47
+ str(file_path)
48
+ for file_path in file_paths
49
+ if part_file_name == os.path.basename(str(file_path))
50
+ ]
51
+
52
+
53
+ def list_directories(path: str) -> Optional[List[str]]:
54
+ """list all directories in the specified path.
55
+
56
+ Args:
57
+ path (str): the path to a directories
58
+
59
+ Returns:
60
+ Optional[List[str]]: a list of directories or None if the path does not exist
61
+
62
+ """
63
+ # Ensure the path exists
64
+ if not os.path.exists(path):
65
+ print("The specified path does not exist.")
66
+ return None
67
+
68
+ # List all entries in the path
69
+ entries = os.listdir(path)
70
+ directories = [
71
+ entry for entry in entries if os.path.isdir(os.path.join(path, entry))
72
+ ]
73
+
74
+ return directories
75
+
76
+
77
+ def get_temp_folder() -> str:
78
+ """Get a temporary folder.
79
+
80
+ Returns:
81
+ str: a temporary folder path
82
+
83
+ """
84
+ return tempfile.TemporaryDirectory().name
85
+
86
+
87
+ def get_temp_dir_with_specific_path(*args):
88
+ """Get a temporary directory with a specific path.
89
+
90
+ Args:
91
+ *args: Variable length argument list to specify the subdirectory path.
92
+
93
+ Returns:
94
+ str: The path of the created temporary directory.
95
+ """
96
+ temp_dir = tempfile.gettempdir()
97
+ flame_temp = os.path.join(temp_dir, *args)
98
+ if not os.path.isdir(flame_temp):
99
+ os.makedirs(flame_temp, exist_ok=True)
100
+ return flame_temp
@@ -0,0 +1,4 @@
1
+ from .manipulator import MarkdownDocument
2
+ from .zettelkasten import Zettelkasten
3
+
4
+ __all__ = ["MarkdownDocument", "Zettelkasten"]
@@ -0,0 +1,464 @@
1
+ import frontmatter
2
+ from typing import Any, Dict, Optional, List, AnyStr
3
+ import os
4
+ from copy import deepcopy
5
+
6
+
7
+ class MarkdownDocument:
8
+ """
9
+ A low-level class for manipulating Markdown files with front matter.
10
+
11
+ This class provides methods to add and modify Markdown content and front matter,
12
+ supporting nested structures in front matter (e.g., dictionaries within YAML front matter).
13
+ """
14
+
15
+ MANDATORY_FIELDS: List[AnyStr] = []
16
+
17
+ def __init__(self, content: str = "", metadata: Optional[Dict[str, Any]] = None):
18
+ """
19
+ Initializes a new MarkdownDocument instance.
20
+
21
+ Args:
22
+ content (str): The Markdown content. Defaults to an empty string.
23
+ metadata (Optional[Dict[str, Any]]): The front matter metadata as a dictionary. Defaults to None.
24
+
25
+ Example:
26
+ >>> doc = MarkdownDocument(
27
+ ... content="# Hello World",
28
+ ... metadata={"title": "Greeting", "tags": ["intro", "welcome"]}
29
+ ... )
30
+ """
31
+ if metadata is None:
32
+ metadata = {}
33
+ self.post = frontmatter.Post(content, **metadata)
34
+
35
+ @classmethod
36
+ def from_file(cls, file_path: str) -> "MarkdownDocument":
37
+ """
38
+ Creates a MarkdownDocument instance by loading a Markdown file.
39
+
40
+ Args:
41
+ file_path (str): The path to the Markdown file.
42
+
43
+ Returns:
44
+ MarkdownDocument: An instance representing the loaded Markdown file.
45
+
46
+ Raises:
47
+ FileNotFoundError: If the specified file does not exist.
48
+ frontmatter.InvalidFrontMatterError: If the front matter is malformed.
49
+ """
50
+ if not os.path.isfile(file_path):
51
+ raise FileNotFoundError(f"The file '{file_path}' does not exist.")
52
+
53
+ with open(file_path, "r", encoding="utf-8") as f:
54
+ post = frontmatter.load(f)
55
+ return cls(content=post.content, metadata=deepcopy(post.metadata))
56
+
57
+ @property
58
+ def content(self) -> str:
59
+ """
60
+ Retrieves the Markdown content.
61
+
62
+ Returns:
63
+ str: The Markdown content.
64
+
65
+ Example:
66
+ >>> doc = MarkdownDocument(content="# Hello World")
67
+ >>> doc.content
68
+ "# Hello World"
69
+ """
70
+ return self.post.content
71
+
72
+ @content.setter
73
+ def content(self, new_content: str) -> None:
74
+ """
75
+ Sets the Markdown content.
76
+
77
+ Args:
78
+ new_content (str): The new Markdown content.
79
+
80
+ Example:
81
+ >>> doc = MarkdownDocument()
82
+ >>> doc.content = "# New Title"
83
+ """
84
+ self.post.content = new_content
85
+
86
+ @property
87
+ def metadata(self) -> Dict[str, Any]:
88
+ """
89
+ Retrieves the front matter metadata.
90
+
91
+ Returns:
92
+ Dict[str, Any]: The metadata dictionary.
93
+
94
+ Example:
95
+ >>> doc = MarkdownDocument(metadata={"title": "Greeting", "tags": ["intro", "welcome"]})
96
+ >>> doc.metadata
97
+ {"title": "Greeting", "tags": ["intro", "welcome"]}
98
+ """
99
+ return self.post.metadata
100
+
101
+ @metadata.setter
102
+ def metadata(self, new_metadata: Dict[str, Any]) -> None:
103
+ """
104
+ Sets the front matter metadata.
105
+
106
+ Args:
107
+ new_metadata (Dict[str, Any]): The new metadata dictionary.
108
+
109
+ Example:
110
+ >>> doc = MarkdownDocument()
111
+ >>> doc.metadata = {"title": "New Greeting", "tags": ["updated"]}
112
+ """
113
+ self.post.metadata = new_metadata
114
+
115
+ def add_content(self, content: str, append: bool = True) -> None:
116
+ """
117
+ Adds content to the Markdown document.
118
+
119
+ Args:
120
+ content (str): The Markdown content to add.
121
+ append (bool): If True, appends to existing content; otherwise, prepends.
122
+ Defaults to True.
123
+
124
+ Example:
125
+ >>> doc = MarkdownDocument()
126
+ >>> doc.add_content("# Introduction")
127
+ >>> doc.add_content("Some introductory text.", append=True)
128
+ """
129
+ if append:
130
+ if self.post.content:
131
+ self.post.content += "\n" + content
132
+ else:
133
+ self.post.content = content
134
+ else:
135
+ if self.post.content:
136
+ self.post.content = content + "\n" + self.post.content
137
+ else:
138
+ self.post.content = content
139
+
140
+ def set_frontmatter(
141
+ self, key_path: str, value: Any, overwrite: bool = True
142
+ ) -> None:
143
+ """
144
+ Sets a front matter key to a specified value. Supports nested keys using dot notation,
145
+ including mixed types such as dictionaries within lists.
146
+
147
+ Args:
148
+ key_path (str): The front matter key path. Use dot notation for nested keys
149
+ (e.g., "author.name" or "sections.0.title").
150
+ value (Any): The value to set for the key.
151
+ overwrite (bool): If True, overwrites the existing value; otherwise, appends to lists
152
+ or creates new entries in lists. Defaults to True.
153
+
154
+ Example:
155
+ >>> doc = MarkdownDocument()
156
+ >>> doc.set_frontmatter("author.name", "John Doe")
157
+ >>> doc.set_frontmatter("author.contact.email", "john@example.com")
158
+ >>> doc.set_frontmatter("sections.0.title", "Introduction")
159
+ >>> doc.set_frontmatter("sections.0.content", "Welcome to the introduction.")
160
+ >>> doc.set_frontmatter("sections.1.title", "Conclusion")
161
+ >>> doc.set_frontmatter("sections.1.content", "Wrapping up.")
162
+ """
163
+ keys = key_path.split(".")
164
+ current = self.post.metadata
165
+
166
+ for i, key in enumerate(keys):
167
+ is_last = i == len(keys) - 1
168
+ # Determine if the current key is meant to be a list index
169
+ if key.isdigit():
170
+ index = int(key)
171
+ if not isinstance(current, list):
172
+ if overwrite:
173
+ # Initialize as list
174
+ parent = self.post.metadata
175
+ for k in keys[:i]:
176
+ if k.isdigit():
177
+ parent = parent[int(k)]
178
+ else:
179
+ parent = parent[k]
180
+ parent[int(keys[i - 1])] = []
181
+ current = parent[int(keys[i - 1])]
182
+ else:
183
+ raise TypeError(
184
+ f"Expected list at {'.'.join(keys[:i])}, found {type(current).__name__}"
185
+ )
186
+ # Extend the list if necessary
187
+ while len(current) <= index:
188
+ current.append({})
189
+ if is_last:
190
+ if isinstance(current[index], list) and not overwrite:
191
+ current[index].append(value)
192
+ elif isinstance(current[index], dict):
193
+ if isinstance(value, dict):
194
+ current[index].update(value)
195
+ else:
196
+ current[index]["value"] = value
197
+ elif not overwrite:
198
+ current[index] = [current[index], value]
199
+ else:
200
+ current[index] = value
201
+ else:
202
+ if not isinstance(current[index], (dict, list)):
203
+ # Initialize as dict or list based on next key
204
+ next_key = keys[i + 1]
205
+ if next_key.isdigit():
206
+ current[index] = []
207
+ else:
208
+ current[index] = {}
209
+ current = current[index]
210
+ else:
211
+ if not isinstance(current, dict):
212
+ if overwrite:
213
+ # Initialize as dict
214
+ parent = self.post.metadata
215
+ for k in keys[:i]:
216
+ if k.isdigit():
217
+ parent = parent[int(k)]
218
+ else:
219
+ parent = parent[k]
220
+ parent[keys[i - 1]] = {}
221
+ current = parent[keys[i - 1]]
222
+ else:
223
+ raise TypeError(
224
+ f"Expected dict at {'.'.join(keys[:i])}, found {type(current).__name__}"
225
+ )
226
+ if is_last:
227
+ if key in current:
228
+ if isinstance(current[key], list) and not overwrite:
229
+ current[key].append(value)
230
+ elif isinstance(current[key], dict):
231
+ if isinstance(value, dict):
232
+ current[key].update(value)
233
+ else:
234
+ current[key]["value"] = value
235
+ elif not overwrite:
236
+ current[key] = [current[key], value]
237
+ else:
238
+ current[key] = value
239
+ else:
240
+ current[key] = value
241
+ else:
242
+ if key not in current or not isinstance(current[key], (dict, list)):
243
+ # Initialize as dict or list based on next key
244
+ next_key = keys[i + 1]
245
+ if next_key.isdigit():
246
+ current[key] = []
247
+ else:
248
+ current[key] = {}
249
+ current = current[key]
250
+
251
+ def get_frontmatter(self, key_path: str) -> Any:
252
+ """
253
+ Retrieves the value of a front matter key. Supports nested keys using dot notation,
254
+ including list indices.
255
+
256
+ Args:
257
+ key_path (str): The front matter key path. Use dot notation for nested keys
258
+ (e.g., "author.name" or "sections.0.title").
259
+
260
+ Returns:
261
+ Any: The value associated with the key, or None if the key does not exist.
262
+
263
+ Example:
264
+ >>> doc = MarkdownDocument(
265
+ ... metadata={
266
+ ... "author": {"name": "John Doe", "contact": {"email": "john@example.com"}},
267
+ ... "sections": [
268
+ ... {"title": "Introduction", "content": "Welcome."},
269
+ ... {"title": "Conclusion", "content": "Goodbye."}
270
+ ... ]
271
+ ... }
272
+ ... )
273
+ >>> doc.get_frontmatter("author.name")
274
+ "John Doe"
275
+ >>> doc.get_frontmatter("sections.0.title")
276
+ "Introduction"
277
+ >>> doc.get_frontmatter("sections.1.content")
278
+ "Goodbye."
279
+ """
280
+ keys = key_path.split(".")
281
+ metadata = self.post.metadata
282
+
283
+ for key in keys:
284
+ if isinstance(metadata, dict):
285
+ metadata = metadata.get(key, None)
286
+ elif isinstance(metadata, list):
287
+ if key.isdigit():
288
+ index = int(key)
289
+ if 0 <= index < len(metadata):
290
+ metadata = metadata[index]
291
+ else:
292
+ return None
293
+ else:
294
+ return None
295
+ else:
296
+ return None
297
+
298
+ if metadata is None:
299
+ return None
300
+
301
+ return metadata
302
+
303
+ def remove_frontmatter(self, key_path: str) -> None:
304
+ """
305
+ Removes a front matter key. Supports nested keys using dot notation,
306
+ including list indices.
307
+
308
+ Args:
309
+ key_path (str): The front matter key path to remove. Use dot notation for nested keys
310
+ (e.g., "author.contact.email" or "sections.0.title").
311
+
312
+ Example:
313
+ >>> doc = MarkdownDocument(
314
+ ... metadata={
315
+ ... "author": {"name": "John Doe", "contact": {"email": "john@example.com"}},
316
+ ... "sections": [
317
+ ... {"title": "Introduction", "content": "Welcome."},
318
+ ... {"title": "Conclusion", "content": "Goodbye."}
319
+ ... ]
320
+ ... }
321
+ ... )
322
+ >>> doc.remove_frontmatter("author.contact.email")
323
+ >>> doc.get_frontmatter("author.contact.email") is None
324
+ True
325
+ >>> doc.remove_frontmatter("sections.1.title")
326
+ >>> doc.get_frontmatter("sections.1.title") is None
327
+ True
328
+ """
329
+ keys = key_path.split(".")
330
+ metadata = self.post.metadata
331
+
332
+ for i, key in enumerate(keys):
333
+ is_last = i == len(keys) - 1
334
+ if isinstance(metadata, dict):
335
+ if key not in metadata:
336
+ return # Key path does not exist; nothing to remove
337
+ if is_last:
338
+ del metadata[key]
339
+ return
340
+ metadata = metadata[key]
341
+ elif isinstance(metadata, list):
342
+ if key.isdigit():
343
+ index = int(key)
344
+ if 0 <= index < len(metadata):
345
+ if is_last:
346
+ del metadata[index]
347
+ return
348
+ metadata = metadata[index]
349
+ else:
350
+ return # Index out of range; nothing to remove
351
+ else:
352
+ return # Invalid key for list; nothing to remove
353
+ else:
354
+ return # Neither dict nor list; nothing to remove
355
+
356
+ def to_markdown(self) -> str:
357
+ """
358
+ Serializes the MarkdownDocument to a Markdown-formatted string, including front matter.
359
+
360
+ Returns:
361
+ str: The complete Markdown content with front matter.
362
+
363
+ ERROR:
364
+ ValueError: If the front matter is missing mandatory fields.
365
+
366
+ Example:
367
+ >>> doc = MarkdownDocument(
368
+ ... content="# Hello World",
369
+ ... metadata={"title": "Greeting", "tags": ["intro", "welcome"]}
370
+ ... )
371
+ >>> print(doc.to_markdown())
372
+ ---
373
+ title: Greeting
374
+ tags:
375
+ - intro
376
+ - welcome
377
+ ---
378
+
379
+ # Hello World
380
+ """
381
+ self.validation_frontmatter()
382
+ return frontmatter.dumps(self.post)
383
+
384
+ def save(self, file_path: str) -> None:
385
+ """
386
+ Saves the MarkdownDocument to a specified file.
387
+
388
+ Args:
389
+ file_path (str): The path where the Markdown file will be saved.
390
+
391
+ Example:
392
+ >>> doc = MarkdownDocument(
393
+ ... content="# Hello World",
394
+ ... metadata={"title": "Greeting", "tags": ["intro", "welcome"]}
395
+ ... )
396
+ >>> doc.save_to_file("greeting.md")
397
+ """
398
+ markdown_str = self.to_markdown()
399
+ with open(file_path, "w", encoding="utf-8") as f:
400
+ f.write(markdown_str)
401
+
402
+ def load_from_file(self, file_path: str) -> None:
403
+ """
404
+ Loads Markdown content and front matter from a specified file into the current instance.
405
+
406
+ Args:
407
+ file_path (str): The path to the Markdown file to load.
408
+
409
+ Raises:
410
+ FileNotFoundError: If the specified file does not exist.
411
+ frontmatter.InvalidFrontMatterError: If the front matter is malformed.
412
+
413
+ Example:
414
+ >>> doc = MarkdownDocument()
415
+ >>> doc.load_from_file("existing.md")
416
+ """
417
+ if not os.path.isfile(file_path):
418
+ raise FileNotFoundError(f"The file '{file_path}' does not exist.")
419
+
420
+ with open(file_path, "r", encoding="utf-8") as f:
421
+ post = frontmatter.load(f)
422
+
423
+ self.post.content = post.content
424
+ self.post.metadata = deepcopy(post.metadata)
425
+
426
+ def clear_content(self) -> None:
427
+ """
428
+ Clears all Markdown content, leaving only the front matter.
429
+
430
+ Example:
431
+ >>> doc = MarkdownDocument(content="# Hello World")
432
+ >>> doc.clear_content()
433
+ >>> print(doc.content)
434
+ ""
435
+ """
436
+ self.post.content = ""
437
+
438
+ def clear_frontmatter(self) -> None:
439
+ """
440
+ Clears all front matter metadata, leaving only the Markdown content.
441
+
442
+ Example:
443
+ >>> doc = MarkdownDocument(
444
+ ... content="# Hello World",
445
+ ... metadata={"title": "Greeting", "tags": ["intro", "welcome"]}
446
+ ... )
447
+ >>> doc.clear_frontmatter()
448
+ >>> print(doc.metadata)
449
+ {}
450
+ """
451
+ self.post.metadata = {}
452
+
453
+ def validation_frontmatter(self):
454
+ """
455
+ Validate the frontmatter metadata against the mandatory fields.
456
+ """
457
+ missing_fields = []
458
+ for field in self.MANDATORY_FIELDS:
459
+ if self.get_frontmatter(field) is None:
460
+ missing_fields.append(field)
461
+
462
+ if missing_fields:
463
+ missing = ", ".join(missing_fields)
464
+ raise ValueError(f"Missing mandatory front matter fields: {missing}")
@@ -0,0 +1,155 @@
1
+ import os
2
+ from typing import Optional
3
+ from ures.timedate import time_now
4
+ from ures.string import unique_id
5
+ from .manipulator import MarkdownDocument, frontmatter
6
+
7
+
8
+ class Zettelkasten(MarkdownDocument):
9
+ """
10
+ Class for handling Zettelkasten markdown files. In my case, a list of mandatory fields is defined in a class
11
+ variable.
12
+
13
+ In Zettelkasten note-taking system, only three types of notes are supported: 'fleeting', 'literature', 'permanent',
14
+ but I added 'atom' type for my own use.
15
+ """
16
+
17
+ MANDATORY_FIELDS = ["title", "type", "url", "create", "id", "tags", "aliases"]
18
+ ALLOWED_TYPES = ["fleeting", "literature", "permanent", "atom"]
19
+
20
+ def __init__(
21
+ self,
22
+ title: str,
23
+ n_type: str,
24
+ url: Optional[str] = None,
25
+ tags: Optional[list] = None,
26
+ aliases: Optional[list] = None,
27
+ ):
28
+ """
29
+ Initialize a Zettelkasten object
30
+ Args:
31
+ title (str): The title of the note.
32
+ n_type (str): The type of the note, only support 'fleeting', 'literature', 'permanent' and 'atom'.
33
+ url (str): The url of the note.
34
+ tags (list): The tags of the note.
35
+ aliases
36
+ """
37
+ if not isinstance(title, str) or not title.strip():
38
+ raise ValueError("Title must be a non-empty string.")
39
+ if n_type not in self.ALLOWED_TYPES:
40
+ raise ValueError(
41
+ f"Invalid type '{n_type}'. Allowed types are: {', '.join(self.ALLOWED_TYPES)}."
42
+ )
43
+ if url is not None and not isinstance(url, str):
44
+ raise ValueError("URL must be a string.")
45
+ if tags is not None and not isinstance(tags, list):
46
+ raise ValueError("Tags must be a list.")
47
+ if aliases is not None and not isinstance(aliases, list):
48
+ raise ValueError("Aliases must be a list.")
49
+
50
+ _metadata = {
51
+ "id": unique_id(),
52
+ "create": time_now(),
53
+ }
54
+ super().__init__(metadata=_metadata)
55
+ self.title = title
56
+ self.type = n_type
57
+ self.url = url or ""
58
+ self.tags = tags or []
59
+ self.aliases = aliases or []
60
+
61
+ @classmethod
62
+ def from_file(cls, file_path: str) -> "MarkdownDocument":
63
+ """
64
+ Creates a MarkdownDocument instance by loading a Markdown file.
65
+
66
+ Args:
67
+ file_path (str): The path to the Markdown file.
68
+
69
+ Returns:
70
+ MarkdownDocument: An instance representing the loaded Markdown file.
71
+
72
+ Raises:
73
+ FileNotFoundError: If the specified file does not exist.
74
+ frontmatter.InvalidFrontMatterError: If the front matter is malformed.
75
+ """
76
+ if not os.path.isfile(file_path):
77
+ raise FileNotFoundError(f"The file '{file_path}' does not exist.")
78
+
79
+ with open(file_path, "r", encoding="utf-8") as f:
80
+ post = frontmatter.load(f)
81
+ zk = cls(
82
+ title=post.metadata.get("title", None),
83
+ n_type=post.metadata.get("type", None),
84
+ url=post.metadata.get("url", None),
85
+ tags=post.metadata.get("tags", None),
86
+ aliases=post.metadata.get("aliases", None),
87
+ )
88
+ zk.metadata["id"] = post.metadata.get("id", zk.metadata["id"])
89
+ zk.metadata["create"] = post.metadata.get("create", zk.metadata["create"])
90
+ zk.add_content(post.content)
91
+ return zk
92
+
93
+ @property
94
+ def title(self) -> str:
95
+ return self.get_frontmatter("title")
96
+
97
+ @title.setter
98
+ def title(self, value):
99
+ if not isinstance(value, str) or not value.strip():
100
+ raise ValueError("Title must be a non-empty string.")
101
+ self.set_frontmatter("title", value)
102
+
103
+ @property
104
+ def type(self) -> str:
105
+ return self.get_frontmatter("type")
106
+
107
+ @type.setter
108
+ def type(self, value):
109
+ if value not in self.ALLOWED_TYPES:
110
+ raise ValueError(
111
+ f"Invalid type '{value}'. Allowed types are: {', '.join(self.ALLOWED_TYPES)}."
112
+ )
113
+ self.set_frontmatter("type", value)
114
+
115
+ @property
116
+ def url(self) -> str:
117
+ return self.get_frontmatter("url")
118
+
119
+ @url.setter
120
+ def url(self, value):
121
+ if not isinstance(value, str):
122
+ raise ValueError("URL must be a string.")
123
+ self.set_frontmatter("url", value)
124
+
125
+ @property
126
+ def tags(self) -> list:
127
+ return self.get_frontmatter("tags")
128
+
129
+ @tags.setter
130
+ def tags(self, value):
131
+ if not isinstance(value, list):
132
+ raise ValueError("Tags must be a list.")
133
+ self.set_frontmatter("tags", value)
134
+
135
+ @property
136
+ def aliases(self) -> list:
137
+ return self.get_frontmatter("aliases")
138
+
139
+ @aliases.setter
140
+ def aliases(self, value):
141
+ if not isinstance(value, list):
142
+ raise ValueError("Aliases must be a list.")
143
+ self.set_frontmatter("aliases", value)
144
+
145
+ def add_tag(self, tag: str):
146
+ self.tags.append(tag)
147
+
148
+ def remove_tag(self, tag: str):
149
+ self.tags.remove(tag)
150
+
151
+ def add_alias(self, alias: str):
152
+ self.aliases.append(alias)
153
+
154
+ def remove_alias(self, alias: str):
155
+ self.aliases.remove(alias)
@@ -0,0 +1,36 @@
1
+ import ipaddress
2
+
3
+
4
+ def verify_ip_in_subnet(ip, subnet) -> bool:
5
+ """Verify if an ip address is in a subnet.
6
+
7
+ Args:
8
+ ip (str): an ip address
9
+ subnet (str): a subnet
10
+
11
+ Returns:
12
+ bool: True if the ip address is in the subnet, False otherwise
13
+ """
14
+ ip = ipaddress.ip_address(ip)
15
+ subnet = ipaddress.ip_network(subnet)
16
+ return ip in subnet
17
+
18
+
19
+ def generate_ip(subnet, last_index):
20
+ """Generate an ip address based on the subnet and the last index.
21
+
22
+ Args:
23
+ subnet (str): a subnet
24
+ last_index (int): the last index of the ip address.
25
+
26
+ Examples:
27
+ generate_ip("192.168.0.0/24", 100)
28
+ # Output: 192.168.0.101
29
+
30
+ Returns:
31
+ str: a new ip address
32
+
33
+ """
34
+ network = ipaddress.ip_network(subnet)
35
+ new_ip = network.network_address + last_index
36
+ return str(new_ip)
@@ -0,0 +1,46 @@
1
+ import uuid
2
+
3
+
4
+ def zettelkasten_id() -> str:
5
+ """Generate an id used for zettelkasten note-taking system only.
6
+
7
+ Returns:
8
+ str: a zettelkasten id
9
+
10
+ """
11
+ _id = uuid.uuid4().hex
12
+ return f"{_id[:9]}.{_id[-11:]}"
13
+
14
+
15
+ def unique_id() -> str:
16
+ """generate a unique id.
17
+
18
+ Returns:
19
+ str: a unique id
20
+
21
+ """
22
+ return uuid.uuid4().hex
23
+
24
+
25
+ def format_memory(nbytes: int) -> str:
26
+ """Format memory size in human-readable format.
27
+
28
+ Args:
29
+ nbytes (int): the memory size in bytes
30
+
31
+ Returns:
32
+ str: the memory size in human-readable format
33
+
34
+ **The function is copied from PyTorch source code.**
35
+ """
36
+ KB = 1024
37
+ MB = 1024 * KB
38
+ GB = 1024 * MB
39
+ if abs(nbytes) >= GB:
40
+ return f"{nbytes * 1.0 / GB:.2f} GB"
41
+ elif abs(nbytes) >= MB:
42
+ return f"{nbytes * 1.0 / MB:.2f} MB"
43
+ elif abs(nbytes) >= KB:
44
+ return f"{nbytes * 1.0 / KB:.2f} KB"
45
+ else:
46
+ return str(nbytes) + " B"
@@ -0,0 +1,57 @@
1
+ import datetime
2
+
3
+
4
+ def datetime_converter(
5
+ time: datetime.datetime, iso8601: bool = True, format: str = "%Y%m%d-%H%M%S"
6
+ ) -> str:
7
+ """COnvert `datetime.datetime` object to iso8601 format or custom format.
8
+
9
+ Args:
10
+ time (datetime.datetime): the time want to be converted
11
+ iso8601 (bool): if True, return iso8601 format. Defaults to True.
12
+ format (str): custom format, and it is only available when iso8601 set to false. Defaults to "%Y%m%d-%H%M%S".
13
+
14
+ Returns:
15
+ str: the time in expected format
16
+ """
17
+ if iso8601:
18
+ return time.isoformat() + "Z"
19
+ else:
20
+ return time.strftime(format)
21
+
22
+
23
+ def timestamp_converter(
24
+ timestamp: int, iso8601: bool = True, format: str = "%Y%m%d-%H%M%S"
25
+ ) -> str:
26
+ """Convert unix timestamp to iso8601 format or custom format.
27
+
28
+ Args:
29
+ timestamp (int): a unix timestamp
30
+ iso8601 (bool): if True, return iso8601 format. Defaults to True.
31
+ format (str): custom format, and it is only available when iso8601 set to false. Defaults to "%Y%m%d-%H%M%S".
32
+
33
+ Returns:
34
+ str: the time in expected format
35
+ """
36
+ return datetime_converter(
37
+ datetime.datetime.fromtimestamp(timestamp),
38
+ iso8601=iso8601,
39
+ format=format,
40
+ # datetime.datetime.utcfromtimestamp(timestamp), iso8601=iso8601, format=format
41
+ )
42
+
43
+
44
+ def time_now(iso8601: bool = True, format: str = "%Y%m%d-%H%M%S") -> str:
45
+ """Generate a current time in iso8601 format or custom format.
46
+
47
+ Args:
48
+ iso8601 (bool): if True, return iso8601 format. Defaults to True.
49
+ format (str): custom format, and it is only available when iso8601 set to false. Defaults to "%Y%m%d-%H%M%S".
50
+
51
+ Returns:
52
+ str: the time in expected format
53
+
54
+ """
55
+ return datetime_converter(
56
+ datetime.datetime.now(datetime.UTC), iso8601=iso8601, format=format
57
+ )
@@ -0,0 +1,15 @@
1
+ import os
2
+ import importlib
3
+ import pkgutil
4
+
5
+ package_dir = os.path.dirname(__file__)
6
+
7
+ for _, module_name, _ in pkgutil.iter_modules([package_dir]):
8
+ module = importlib.import_module(f".{module_name}", package=__name__)
9
+
10
+ for attribute_name in dir(module):
11
+ attribute = getattr(module, attribute_name)
12
+ if isinstance(attribute, type) and not attribute_name.startswith("_"):
13
+ globals()[attribute_name] = attribute
14
+
15
+ __all__ = [name for name in globals() if not name.startswith("_")]
@@ -0,0 +1,97 @@
1
+ from enum import Enum, EnumMeta
2
+ from typing import List, Optional
3
+
4
+
5
+ class EnumManipulator:
6
+ """A class to manipulate Enum object."""
7
+
8
+ def __init__(self, input_enum: EnumMeta):
9
+ """initialize the class.
10
+
11
+ Args:
12
+ input_enum (EnumMeta): an Enum object
13
+ """
14
+ self._enum = input_enum
15
+
16
+ @property
17
+ def fetch_enums(self) -> EnumMeta:
18
+ """Get the Enum object."""
19
+ return self._enum
20
+
21
+ def fetch_keys(self) -> List[str]:
22
+ """Get all keys in the Enum object."""
23
+ return self.fetch_enums._member_names_
24
+
25
+ def fetch_enum(self, key_name: str) -> Optional[Enum]:
26
+ """Fetch the Enum object by key name.
27
+
28
+ Args:
29
+ key_name (str): the key name
30
+
31
+ Returns:
32
+ Optional[Enum]: the Enum object or None if the key does not exist
33
+
34
+ """
35
+ key = None
36
+ for _key in self.fetch_keys():
37
+ if key_name.lower() == str(_key).lower():
38
+ key = self.fetch_enums[_key]
39
+ break
40
+ return key
41
+
42
+ def check_key(self, key_name: str) -> bool:
43
+ """Check if the key exists in the Enum object.
44
+
45
+ Args:
46
+ key_name (str): the key name
47
+
48
+ Returns:
49
+ bool: True if the key exists, False otherwise
50
+
51
+ """
52
+ return False if self.fetch_enum(key_name) is None else True
53
+
54
+ def fetch_value(self, key_name: str):
55
+ """Fetch the value of the Enum object by key name.
56
+
57
+ Args:
58
+ key_name (str): the key name
59
+
60
+ Returns:
61
+ any: the value of the Enum object or None if the key does not exist
62
+
63
+ """
64
+ key = self.fetch_enum(key_name)
65
+ value = None
66
+ if key is not None:
67
+ value = key.value
68
+ return value
69
+
70
+ def filter_by(self, keyword: str, field: str = None) -> list:
71
+ """Fetch the key when the keyword is found in the field of the Enum object.
72
+ If the field is None, the keyword is compared with the value of the Enum object.
73
+
74
+ Args:
75
+ keyword (str): the keyword to search
76
+ field (str): the field to search. Defaults to None. if not none, the field points to the attribute of the value.
77
+
78
+ Returns:
79
+ list: a list of keys
80
+
81
+ """
82
+ keys = self.fetch_keys()
83
+ result = []
84
+ for key in keys:
85
+ _value = self.fetch_enums[key].value
86
+ if field is None:
87
+ if keyword == str(_value):
88
+ result.append(key)
89
+ else:
90
+ _field_value = getattr(_value, field)
91
+ if isinstance(_field_value, str):
92
+ if keyword.lower() in _field_value.lower():
93
+ result.append(key)
94
+ elif isinstance(_field_value, list):
95
+ if keyword.lower() in [str(_).lower() for _ in _field_value]:
96
+ result.append(key)
97
+ return result