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 +21 -0
- ures-1.3.1/PKG-INFO +34 -0
- ures-1.3.1/README.md +9 -0
- ures-1.3.1/pyproject.toml +65 -0
- ures-1.3.1/ures/__init__.py +0 -0
- ures-1.3.1/ures/data_structure/__init__.py +4 -0
- ures-1.3.1/ures/data_structure/bi_directional_links.py +99 -0
- ures-1.3.1/ures/data_structure/tree.py +109 -0
- ures-1.3.1/ures/files.py +100 -0
- ures-1.3.1/ures/markdown/__init__.py +4 -0
- ures-1.3.1/ures/markdown/manipulator.py +464 -0
- ures-1.3.1/ures/markdown/zettelkasten.py +155 -0
- ures-1.3.1/ures/network.py +36 -0
- ures-1.3.1/ures/string.py +46 -0
- ures-1.3.1/ures/timedate.py +57 -0
- ures-1.3.1/ures/tools/__init__.py +15 -0
- ures-1.3.1/ures/tools/enum.py +97 -0
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
|
+
[](https://github.com/semantic-release/semantic-release)
|
|
28
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
29
|
+
[](https://github.com/psf/black)
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
[](https://github.com/stone-home/Python-URes/actions/workflows/linter.yaml)
|
|
32
|
+
[](https://github.com/stone-home/Python-URes/actions/workflows/secret-check.yaml)
|
|
33
|
+
[](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
|
+
[](https://github.com/semantic-release/semantic-release)
|
|
4
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
5
|
+
[](https://github.com/psf/black)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/stone-home/Python-URes/actions/workflows/linter.yaml)
|
|
8
|
+
[](https://github.com/stone-home/Python-URes/actions/workflows/secret-check.yaml)
|
|
9
|
+
[](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,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()
|
ures-1.3.1/ures/files.py
ADDED
|
@@ -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,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
|