task2md 1.0.0__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.
- task2md/__init__.py +0 -0
- task2md/task/__init__.py +0 -0
- task2md/task/file.py +249 -0
- task2md/task/header.py +71 -0
- task2md/task/task.py +137 -0
- task2md/task/variable.py +30 -0
- task2md/task2md.py +126 -0
- task2md/util/__init__.py +0 -0
- task2md/util/dir.py +37 -0
- task2md-1.0.0.dist-info/METADATA +122 -0
- task2md-1.0.0.dist-info/RECORD +14 -0
- task2md-1.0.0.dist-info/WHEEL +4 -0
- task2md-1.0.0.dist-info/entry_points.txt +2 -0
- task2md-1.0.0.dist-info/licenses/LICENSE +21 -0
task2md/__init__.py
ADDED
|
File without changes
|
task2md/task/__init__.py
ADDED
|
File without changes
|
task2md/task/file.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from task2md.task.header import Header
|
|
11
|
+
from task2md.task.task import Task
|
|
12
|
+
from task2md.task.variable import Variable
|
|
13
|
+
from task2md.util.dir import Dir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class File(BaseModel):
|
|
17
|
+
"""File Task content class"""
|
|
18
|
+
|
|
19
|
+
path: str = ""
|
|
20
|
+
content: str = ""
|
|
21
|
+
yaml: Dict[str, Any] = {}
|
|
22
|
+
header: Header = Header()
|
|
23
|
+
global_variables: List[Variable] = []
|
|
24
|
+
tasks: List[Task] = []
|
|
25
|
+
parsed: bool = False
|
|
26
|
+
|
|
27
|
+
def get_filename(self) -> str:
|
|
28
|
+
"""Get filename without extension
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: The filename
|
|
32
|
+
"""
|
|
33
|
+
return Path(self.path).stem
|
|
34
|
+
|
|
35
|
+
def generate(self, dir: Dir) -> None:
|
|
36
|
+
"""Generate markdown file in output directory
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
dir (Dir): The dir object
|
|
40
|
+
"""
|
|
41
|
+
self.load()
|
|
42
|
+
self.parse()
|
|
43
|
+
output = self.to_md()
|
|
44
|
+
|
|
45
|
+
output_filename = dir.path + "/" + self.get_filename() + ".md"
|
|
46
|
+
|
|
47
|
+
with open(output_filename, "w") as f:
|
|
48
|
+
f.write(output)
|
|
49
|
+
|
|
50
|
+
def load(self) -> None:
|
|
51
|
+
"""Load file content from path"""
|
|
52
|
+
with open(self.path) as f:
|
|
53
|
+
self.content = f.read()
|
|
54
|
+
|
|
55
|
+
def parse(self) -> None:
|
|
56
|
+
"""Parse yaml content"""
|
|
57
|
+
|
|
58
|
+
yaml_content = yaml.safe_load(self.content)
|
|
59
|
+
|
|
60
|
+
if (yaml_content is not None) and isinstance(yaml_content, dict):
|
|
61
|
+
self.yaml = yaml_content
|
|
62
|
+
|
|
63
|
+
# 1 - Parse header:
|
|
64
|
+
header_lines = re.findall(r"# @.*", self.content, flags=re.MULTILINE)
|
|
65
|
+
self.header.parse(header_lines)
|
|
66
|
+
|
|
67
|
+
# 2 - Parse vars:
|
|
68
|
+
if ("vars" in self.yaml) and (self.yaml["vars"] is not None):
|
|
69
|
+
sorted_keys = sorted(self.yaml["vars"].keys())
|
|
70
|
+
for name in sorted_keys:
|
|
71
|
+
v = Variable(name=name)
|
|
72
|
+
if (self.yaml["vars"][name] is not None) and isinstance(
|
|
73
|
+
self.yaml["vars"][name], str
|
|
74
|
+
):
|
|
75
|
+
v.value = self.yaml["vars"][name]
|
|
76
|
+
|
|
77
|
+
# Get comment if any
|
|
78
|
+
line_var = re.findall(
|
|
79
|
+
r"^ " + v.name + ": .*$", self.content, flags=re.MULTILINE
|
|
80
|
+
)
|
|
81
|
+
if len(line_var) > 0:
|
|
82
|
+
# Remove name and value of line
|
|
83
|
+
desc = line_var[0].replace(" " + v.name + ": " + v.value, "")
|
|
84
|
+
# if # remove -> description
|
|
85
|
+
if "#" in desc:
|
|
86
|
+
value_desc = desc.split("#", 1)
|
|
87
|
+
v.description = value_desc[1].strip()
|
|
88
|
+
|
|
89
|
+
self.global_variables.append(v)
|
|
90
|
+
|
|
91
|
+
# 3 - Parse tasks:
|
|
92
|
+
if ("tasks" in self.yaml) and (self.yaml["tasks"] is not None):
|
|
93
|
+
sorted_keys = sorted(self.yaml["tasks"].keys())
|
|
94
|
+
for name in sorted_keys:
|
|
95
|
+
# Get task only if desc is defined
|
|
96
|
+
if (
|
|
97
|
+
("desc" in self.yaml["tasks"][name])
|
|
98
|
+
and (self.yaml["tasks"][name]["desc"] is not None)
|
|
99
|
+
and isinstance(self.yaml["tasks"][name]["desc"], str)
|
|
100
|
+
and (len(self.yaml["tasks"][name]["desc"]) > 0)
|
|
101
|
+
):
|
|
102
|
+
t = Task(name=name, desc=self.yaml["tasks"][name]["desc"])
|
|
103
|
+
if (
|
|
104
|
+
("summary" in self.yaml["tasks"][name])
|
|
105
|
+
and (self.yaml["tasks"][name]["summary"] is not None)
|
|
106
|
+
and isinstance(self.yaml["tasks"][name]["summary"], str)
|
|
107
|
+
and (len(self.yaml["tasks"][name]["summary"]) > 0)
|
|
108
|
+
):
|
|
109
|
+
t.summary = self.yaml["tasks"][name]["summary"]
|
|
110
|
+
|
|
111
|
+
self.tasks.append(t)
|
|
112
|
+
|
|
113
|
+
self.parsed = True
|
|
114
|
+
|
|
115
|
+
def to_md(self) -> str:
|
|
116
|
+
"""Return the content of the file in markdown
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
str: Markdown content
|
|
120
|
+
"""
|
|
121
|
+
output = ""
|
|
122
|
+
# Tag
|
|
123
|
+
output += self.tags_to_md()
|
|
124
|
+
|
|
125
|
+
# Top
|
|
126
|
+
output += "---\n\n# " + self.get_filename() + "\n\n"
|
|
127
|
+
output += self.header.description + "\n\n"
|
|
128
|
+
output += '!!! info "' + self.get_filename() + ' template details"\n\n'
|
|
129
|
+
output += self.header_to_md()
|
|
130
|
+
|
|
131
|
+
# Tasks list
|
|
132
|
+
output += "## :material-list-box: List of tasks\n"
|
|
133
|
+
output += self.tasks_list_to_md() + "\n"
|
|
134
|
+
|
|
135
|
+
# Global variables
|
|
136
|
+
output += "## :material-variable: global variables\n"
|
|
137
|
+
output += self.global_variables_to_md() + "\n"
|
|
138
|
+
|
|
139
|
+
# Tasks details
|
|
140
|
+
output += self.tasks_details_to_md()
|
|
141
|
+
|
|
142
|
+
return output
|
|
143
|
+
|
|
144
|
+
def tags_to_md(self) -> str:
|
|
145
|
+
"""Return the tags list
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
str: markdown content
|
|
149
|
+
"""
|
|
150
|
+
output = ""
|
|
151
|
+
if len(self.header.tags) > 0:
|
|
152
|
+
output += "---\ntags:\n"
|
|
153
|
+
for tag in self.header.tags:
|
|
154
|
+
output += f" - {tag}\n"
|
|
155
|
+
|
|
156
|
+
return output
|
|
157
|
+
|
|
158
|
+
def header_to_md(self) -> str:
|
|
159
|
+
"""Return the header data
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
str: markdown content
|
|
163
|
+
"""
|
|
164
|
+
match self.header.status:
|
|
165
|
+
case "stable":
|
|
166
|
+
status_icon = "material-check-circle"
|
|
167
|
+
status_label = "stable"
|
|
168
|
+
case "deprecated":
|
|
169
|
+
status_icon = "material-delete"
|
|
170
|
+
status_label = "deprecated"
|
|
171
|
+
case "beta":
|
|
172
|
+
status_icon = "material-beta"
|
|
173
|
+
status_label = "beta"
|
|
174
|
+
case _:
|
|
175
|
+
status_icon = "material-draw"
|
|
176
|
+
status_label = "draft"
|
|
177
|
+
|
|
178
|
+
output = ""
|
|
179
|
+
output += f" * :{status_icon}: Status: {status_label}\n"
|
|
180
|
+
output += (
|
|
181
|
+
" * :material-bookmark-check: File: ["
|
|
182
|
+
+ self.header.file_raw
|
|
183
|
+
+ "]("
|
|
184
|
+
+ self.header.file_ui
|
|
185
|
+
+ ")\n"
|
|
186
|
+
)
|
|
187
|
+
output += (
|
|
188
|
+
" * :material-home: Home: ["
|
|
189
|
+
+ self.header.home
|
|
190
|
+
+ "]("
|
|
191
|
+
+ self.header.home
|
|
192
|
+
+ ")\n"
|
|
193
|
+
)
|
|
194
|
+
output += f" * :material-license: License: {self.header.license}\n\n"
|
|
195
|
+
|
|
196
|
+
return output
|
|
197
|
+
|
|
198
|
+
def tasks_list_to_md(self) -> str:
|
|
199
|
+
"""Return the Tasks list to a markdown table
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
str: markdown table
|
|
203
|
+
"""
|
|
204
|
+
output = """
|
|
205
|
+
| Tasks | Description |
|
|
206
|
+
| ----- | ----------- |
|
|
207
|
+
"""
|
|
208
|
+
for task in self.tasks:
|
|
209
|
+
file_name = self.get_filename()
|
|
210
|
+
output += (
|
|
211
|
+
f"| [`{file_name}:{task.name}`](#:simple-task:-{file_name}:{task.name})"
|
|
212
|
+
f" | {task.desc} |\n"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return output
|
|
216
|
+
|
|
217
|
+
def global_variables_to_md(self) -> str:
|
|
218
|
+
"""Return the global variables to a markdown table
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
str: markdown table
|
|
222
|
+
"""
|
|
223
|
+
output = """
|
|
224
|
+
| Variables | Description | Default value |
|
|
225
|
+
| --------- | ----------- | ------------- |
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
if len(self.global_variables) == 0:
|
|
229
|
+
# Empty variable to generate a line with no value
|
|
230
|
+
v = Variable()
|
|
231
|
+
output += v.to_md() + "\n"
|
|
232
|
+
else:
|
|
233
|
+
for var in self.global_variables:
|
|
234
|
+
output += var.to_md() + "\n"
|
|
235
|
+
|
|
236
|
+
return output
|
|
237
|
+
|
|
238
|
+
def tasks_details_to_md(self) -> str:
|
|
239
|
+
"""Return the Tasks details
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
str: List of tasks details
|
|
243
|
+
"""
|
|
244
|
+
output = ""
|
|
245
|
+
|
|
246
|
+
for task in self.tasks:
|
|
247
|
+
output += task.to_md(self.get_filename())
|
|
248
|
+
|
|
249
|
+
return output
|
task2md/task/header.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import re
|
|
3
|
+
from typing import ClassVar, List
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Header(BaseModel):
|
|
9
|
+
"""Header Task file class"""
|
|
10
|
+
|
|
11
|
+
PATTERN: ClassVar[str] = r"@([\w-]+): (.*)"
|
|
12
|
+
|
|
13
|
+
description: str = "-"
|
|
14
|
+
tags: List[str] = []
|
|
15
|
+
authors: List[str] = []
|
|
16
|
+
file_raw: str = ""
|
|
17
|
+
file_ui: str = ""
|
|
18
|
+
home: str = ""
|
|
19
|
+
links: List[str] = []
|
|
20
|
+
license: str = ""
|
|
21
|
+
status: str = ""
|
|
22
|
+
deprecated_tasks: List[str] = []
|
|
23
|
+
|
|
24
|
+
def parse(self, header_lines: List[str]) -> None:
|
|
25
|
+
"""Parse the header comment of a Task file
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
header_lines (List[str]): List of header lines
|
|
29
|
+
"""
|
|
30
|
+
for line in header_lines:
|
|
31
|
+
match = re.search(Header.PATTERN, line)
|
|
32
|
+
if match:
|
|
33
|
+
label, value = match.groups()
|
|
34
|
+
match label.strip():
|
|
35
|
+
case "description":
|
|
36
|
+
self.description = value.strip()
|
|
37
|
+
case "tags":
|
|
38
|
+
self.tags = Header.string2list(value)
|
|
39
|
+
case "authors":
|
|
40
|
+
self.authors = Header.string2list(value)
|
|
41
|
+
case "file-raw":
|
|
42
|
+
self.file_raw = value.strip()
|
|
43
|
+
case "file-ui":
|
|
44
|
+
self.file_ui = value.strip()
|
|
45
|
+
case "home":
|
|
46
|
+
self.home = value.strip()
|
|
47
|
+
case "links":
|
|
48
|
+
self.links = Header.string2list(value)
|
|
49
|
+
case "license":
|
|
50
|
+
self.license = value.strip()
|
|
51
|
+
case "status":
|
|
52
|
+
self.status = value.strip()
|
|
53
|
+
case "deprecated-tasks":
|
|
54
|
+
self.deprecated_tasks = Header.string2list(value)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def string2list(cls, input: str) -> List[str]:
|
|
58
|
+
"""Split a string with comma separator
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
input (str): string to split
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List[str]: A list of string
|
|
65
|
+
"""
|
|
66
|
+
list = input.split(",")
|
|
67
|
+
output = []
|
|
68
|
+
for element in list:
|
|
69
|
+
output.append(element.strip())
|
|
70
|
+
|
|
71
|
+
return output
|
task2md/task/task.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import ClassVar, List
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Argument(BaseModel):
|
|
10
|
+
label: str = ""
|
|
11
|
+
value: str = ""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Task(BaseModel):
|
|
15
|
+
"""Task class"""
|
|
16
|
+
|
|
17
|
+
LINE_ARG: ClassVar[str] = "Arguments:"
|
|
18
|
+
PATTERN_ARG: ClassVar[str] = r"([\w-]+\s*[|]\s*[\w-]*): (.*)"
|
|
19
|
+
LINE_REQ: ClassVar[str] = "Requirements:"
|
|
20
|
+
PATTERN_REQ: ClassVar[str] = r"(-\s.*)"
|
|
21
|
+
|
|
22
|
+
name: str = ""
|
|
23
|
+
desc: str = ""
|
|
24
|
+
summary: str = ""
|
|
25
|
+
summary_head: str = ""
|
|
26
|
+
summary_args: List[Argument] = []
|
|
27
|
+
summary_req: List[str] = []
|
|
28
|
+
summary_comments: str = ""
|
|
29
|
+
parsed: bool = False
|
|
30
|
+
|
|
31
|
+
def parse(self) -> None:
|
|
32
|
+
"""Parse the Task summary"""
|
|
33
|
+
# Head - get beginning until blank line
|
|
34
|
+
lines = self.summary.splitlines()
|
|
35
|
+
head: List[str] = []
|
|
36
|
+
for line in lines:
|
|
37
|
+
if line.strip():
|
|
38
|
+
head.append(line)
|
|
39
|
+
else:
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
self.summary_head = "\n".join(head) + "\n"
|
|
43
|
+
# Get partial summary without head lines
|
|
44
|
+
count_line_head = len(head)
|
|
45
|
+
index_partial_summary = max(0, count_line_head)
|
|
46
|
+
partial_summary_lines = lines[index_partial_summary:]
|
|
47
|
+
partial_summary_lines_count = len(partial_summary_lines)
|
|
48
|
+
|
|
49
|
+
# Arguments
|
|
50
|
+
found_arguments = False
|
|
51
|
+
for i, line in enumerate(partial_summary_lines):
|
|
52
|
+
if (line.strip() == Task.LINE_ARG) and (
|
|
53
|
+
(i + 1) < partial_summary_lines_count
|
|
54
|
+
):
|
|
55
|
+
found_arguments = True
|
|
56
|
+
# Parse arguments
|
|
57
|
+
args_summary_lines = partial_summary_lines[(i + 1) :]
|
|
58
|
+
for j, line_arg in enumerate(args_summary_lines):
|
|
59
|
+
match = re.search(Task.PATTERN_ARG, line_arg.strip())
|
|
60
|
+
if match:
|
|
61
|
+
label, value = match.groups()
|
|
62
|
+
arg = Argument(label=label.strip(), value=value.strip())
|
|
63
|
+
self.summary_args.append(arg)
|
|
64
|
+
else:
|
|
65
|
+
break
|
|
66
|
+
if found_arguments:
|
|
67
|
+
del partial_summary_lines[i : (i + j + 2)]
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
# Requirements
|
|
71
|
+
partial_summary_lines_count = len(partial_summary_lines)
|
|
72
|
+
found_req = False
|
|
73
|
+
for i, line in enumerate(partial_summary_lines):
|
|
74
|
+
if (line.strip() == Task.LINE_REQ) and (
|
|
75
|
+
(i + 1) < partial_summary_lines_count
|
|
76
|
+
):
|
|
77
|
+
found_req = True
|
|
78
|
+
# Parse req
|
|
79
|
+
req_summary_lines = partial_summary_lines[(i + 1) :]
|
|
80
|
+
for j, line_req in enumerate(req_summary_lines):
|
|
81
|
+
match = re.search(Task.PATTERN_REQ, line_req.strip())
|
|
82
|
+
if match:
|
|
83
|
+
req = match.groups()
|
|
84
|
+
self.summary_req.append(req[0])
|
|
85
|
+
else:
|
|
86
|
+
break
|
|
87
|
+
if found_req:
|
|
88
|
+
del partial_summary_lines[i : (i + j + 2)]
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
# Comments
|
|
92
|
+
self.summary_comments = "\n".join(partial_summary_lines).strip("\n")
|
|
93
|
+
|
|
94
|
+
self.parsed = True
|
|
95
|
+
|
|
96
|
+
def to_md(self, file_name: str) -> str:
|
|
97
|
+
"""Return the details of the task
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
file_name (str): File_name of the task file.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
str: Markdown of the task details
|
|
104
|
+
"""
|
|
105
|
+
self.parse()
|
|
106
|
+
output = f"\n## :simple-task: {file_name}:{self.name}\n\n"
|
|
107
|
+
|
|
108
|
+
output += f"{self.desc} \n\n"
|
|
109
|
+
output += "```shell\n"
|
|
110
|
+
output += self.summary_head
|
|
111
|
+
output += "```\n"
|
|
112
|
+
|
|
113
|
+
# Arguments
|
|
114
|
+
output += """
|
|
115
|
+
| Arguments | Description |
|
|
116
|
+
| --------- | ----------- |
|
|
117
|
+
"""
|
|
118
|
+
if len(self.summary_args) == 0:
|
|
119
|
+
output += "| - | - |\n"
|
|
120
|
+
else:
|
|
121
|
+
for arg in self.summary_args:
|
|
122
|
+
label = arg.label.replace("|", "\\|")
|
|
123
|
+
output += f"| `{label}` | {arg.value} |\n"
|
|
124
|
+
output += "\n"
|
|
125
|
+
|
|
126
|
+
# Comments
|
|
127
|
+
output += f"{self.summary_comments}\n\n"
|
|
128
|
+
|
|
129
|
+
# Requirements
|
|
130
|
+
output += '!!! info "Requirements:"\n\n'
|
|
131
|
+
if len(self.summary_req) == 0:
|
|
132
|
+
output += " - None\n"
|
|
133
|
+
else:
|
|
134
|
+
for req in self.summary_req:
|
|
135
|
+
output += f" {req}\n"
|
|
136
|
+
|
|
137
|
+
return output
|
task2md/task/variable.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Variable(BaseModel):
|
|
7
|
+
name: str = ""
|
|
8
|
+
value: str = ""
|
|
9
|
+
description: str = ""
|
|
10
|
+
|
|
11
|
+
def to_md(self) -> str:
|
|
12
|
+
"""Return the details of the variable
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
str: Markdown columns of the variable details
|
|
16
|
+
"""
|
|
17
|
+
if self.name == "":
|
|
18
|
+
name = " - "
|
|
19
|
+
else:
|
|
20
|
+
name = "`" + self.name + "`"
|
|
21
|
+
if self.description == "":
|
|
22
|
+
description = " - "
|
|
23
|
+
else:
|
|
24
|
+
description = self.description
|
|
25
|
+
if self.value == "":
|
|
26
|
+
value = " - "
|
|
27
|
+
else:
|
|
28
|
+
value = "`" + self.value + "`"
|
|
29
|
+
|
|
30
|
+
return f"| {name} | {description} | {value} |"
|
task2md/task2md.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from task2md.task.file import File
|
|
9
|
+
from task2md.util.dir import Dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
@click.version_option("1.0.0", prog_name="task2md")
|
|
14
|
+
def cli() -> None:
|
|
15
|
+
"""A CLI tool to generate markdown documentation files from Task files."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@cli.command()
|
|
20
|
+
@click.option(
|
|
21
|
+
"-i",
|
|
22
|
+
"--input",
|
|
23
|
+
"input_dir",
|
|
24
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
25
|
+
required=True,
|
|
26
|
+
help="Input directory",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"-d",
|
|
30
|
+
"--dir",
|
|
31
|
+
"output_dir",
|
|
32
|
+
type=click.Path(exists=False, file_okay=False, dir_okay=True, writable=True),
|
|
33
|
+
required=False,
|
|
34
|
+
help="Output markdown documentation files directory. Default current directory.",
|
|
35
|
+
)
|
|
36
|
+
def dir(
|
|
37
|
+
input_dir: click.Path,
|
|
38
|
+
output_dir: click.Path,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Command to generate a markdown documentation file from a directory.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
click.ClickException: Error when reading input file or writing output file
|
|
44
|
+
"""
|
|
45
|
+
task_files: List[str] = []
|
|
46
|
+
in_dir = click.format_filename(str(input_dir))
|
|
47
|
+
for filename in os.listdir(in_dir):
|
|
48
|
+
if filename.endswith(".yml") or filename.endswith(".yaml"):
|
|
49
|
+
task_files.append(filename)
|
|
50
|
+
|
|
51
|
+
if len(task_files) == 0:
|
|
52
|
+
click.echo(f"No yaml file found in: {in_dir}")
|
|
53
|
+
else:
|
|
54
|
+
try:
|
|
55
|
+
out_dir = Dir(output_dir, True)
|
|
56
|
+
|
|
57
|
+
except OSError as error:
|
|
58
|
+
raise click.ClickException(
|
|
59
|
+
"Output directory can not be created!\n" + str(error)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
for filename in task_files:
|
|
64
|
+
task_file = File(path=f"{in_dir}/{filename}")
|
|
65
|
+
task_file.generate(out_dir)
|
|
66
|
+
|
|
67
|
+
click.echo(
|
|
68
|
+
f"Task documentation generated: {task_file.get_filename()}.md"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
except ValueError as ve:
|
|
72
|
+
raise click.ClickException(
|
|
73
|
+
"Error on reading file {} :\n {}".format(filename, str(ve))
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@cli.command()
|
|
78
|
+
@click.option(
|
|
79
|
+
"-i",
|
|
80
|
+
"--input",
|
|
81
|
+
"input_file",
|
|
82
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
|
|
83
|
+
required=True,
|
|
84
|
+
help="Input Task yaml file.",
|
|
85
|
+
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"-d",
|
|
88
|
+
"--dir",
|
|
89
|
+
"output_dir",
|
|
90
|
+
type=click.Path(exists=False, file_okay=False, dir_okay=True, writable=True),
|
|
91
|
+
required=False,
|
|
92
|
+
help="Output markdown documentation files directory. Default current directory.",
|
|
93
|
+
)
|
|
94
|
+
def file(
|
|
95
|
+
input_file: click.Path,
|
|
96
|
+
output_dir: click.Path,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Command to generate a markdown documentation file from a Task file.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
click.ClickException: Error when reading input file or writing output file
|
|
102
|
+
"""
|
|
103
|
+
input_filename = click.format_filename(str(input_file))
|
|
104
|
+
task_file = File(path=input_filename)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
dir = Dir(output_dir, True)
|
|
108
|
+
|
|
109
|
+
except OSError as error:
|
|
110
|
+
raise click.ClickException(
|
|
111
|
+
"Output directory can not be created!\n" + str(error)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
task_file.generate(dir)
|
|
116
|
+
|
|
117
|
+
click.echo(f"Task documentation generated: {task_file.get_filename()}.md")
|
|
118
|
+
|
|
119
|
+
except ValueError as ve:
|
|
120
|
+
raise click.ClickException(
|
|
121
|
+
"Error on reading file {} :\n {}".format(input_filename, str(ve))
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
cli() # pragma: no cover
|
task2md/util/__init__.py
ADDED
|
File without changes
|
task2md/util/dir.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Dir(BaseModel):
|
|
10
|
+
"""Utility class for a directory."""
|
|
11
|
+
|
|
12
|
+
path: str
|
|
13
|
+
|
|
14
|
+
def __init__(self, dir_path: click.Path | None = None, create: bool = False):
|
|
15
|
+
"""Constructor.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
dir_path (click.Path | None): Directory file path. \
|
|
19
|
+
Current working directory if None.
|
|
20
|
+
create (bool, optional): Create directory if doesn't exist. \
|
|
21
|
+
Defaults to False.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
FileNotFoundError: When create is False and directory not found.
|
|
25
|
+
"""
|
|
26
|
+
if dir_path is None:
|
|
27
|
+
path = os.getcwd()
|
|
28
|
+
else:
|
|
29
|
+
path = click.format_filename(str(dir_path))
|
|
30
|
+
|
|
31
|
+
if not os.path.exists(path):
|
|
32
|
+
if create:
|
|
33
|
+
os.makedirs(path)
|
|
34
|
+
else:
|
|
35
|
+
raise FileNotFoundError(f"Directory {path} not found!")
|
|
36
|
+
|
|
37
|
+
super().__init__(path=path)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: task2md
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A program to generate markdown documentation files from Task files
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/op_so/task/task2md
|
|
6
|
+
Project-URL: Documentation, https://op_so.gitlab.io/task/task2md/
|
|
7
|
+
Author-email: FX Soubirou <soubirou@yahoo.fr>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: click>=8.1.7
|
|
15
|
+
Requires-Dist: pydantic>=2.6.3
|
|
16
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
17
|
+
Requires-Dist: types-pyyaml>=6.0.12.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# `task2md`
|
|
21
|
+
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
[](https://github.com/semantic-release/semantic-release)
|
|
24
|
+
[](https://gitlab.com/op_so/task/task2md/pipelines)
|
|
25
|
+
|
|
26
|
+
[](https://op_so.gitlab.io/task/task2md/) Source code documentation
|
|
27
|
+
|
|
28
|
+
A CLI tool to generate from [Task](https://taskfile.dev/) files, some markdown
|
|
29
|
+
documentation files for [`mkdocs`](https://squidfunk.github.io/mkdocs-material/) static site.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
Usage: task2md [OPTIONS] COMMAND [ARGS]...
|
|
33
|
+
|
|
34
|
+
A CLI tool to generate markdown documentation files from Task files.
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--version Show the version and exit.
|
|
38
|
+
--help Show this message and exit.
|
|
39
|
+
|
|
40
|
+
Commands:
|
|
41
|
+
dir Command to generate a markdown documentation file from a directory.
|
|
42
|
+
file Command to generate a markdown documentation file from a Task file.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## `dir`
|
|
46
|
+
|
|
47
|
+
Get all files with `yaml/yml` extension from the input directory and generate the
|
|
48
|
+
markdown files in the output directory.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
Usage: task2md dir [OPTIONS]
|
|
52
|
+
|
|
53
|
+
Command to generate a markdown documentation file from a directory.
|
|
54
|
+
|
|
55
|
+
Raises: click.ClickException: Error when reading input file or writing
|
|
56
|
+
output file
|
|
57
|
+
|
|
58
|
+
Options:
|
|
59
|
+
-i, --input DIRECTORY Input directory [required]
|
|
60
|
+
-d, --dir DIRECTORY Output markdown documentation files directory.
|
|
61
|
+
Default current directory.
|
|
62
|
+
--help Show this message and exit.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
task2md dir --input Taskfile.d/ -d doc_dir/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## `file`
|
|
72
|
+
|
|
73
|
+
Generate from the input file a markdown file in the output directory.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
Usage: task2md file [OPTIONS]
|
|
77
|
+
|
|
78
|
+
Command to generate a markdown documentation file from a Task file.
|
|
79
|
+
|
|
80
|
+
Raises: click.ClickException: Error when reading input file or writing
|
|
81
|
+
output file
|
|
82
|
+
|
|
83
|
+
Options:
|
|
84
|
+
-i, --input FILE Input Task yaml file. [required]
|
|
85
|
+
-d, --dir DIRECTORY Output markdown documentation files directory. Default
|
|
86
|
+
current directory.
|
|
87
|
+
--help Show this message and exit.
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
task2md file --input Taskfile.d/lint.yml -d doc_dir/
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
### With `Python` environment
|
|
99
|
+
|
|
100
|
+
To use:
|
|
101
|
+
|
|
102
|
+
- Minimal Python version: 3.10
|
|
103
|
+
|
|
104
|
+
Installation with Python `pip`:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python3 -m pip install task2md
|
|
108
|
+
task2md --help
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Authors
|
|
112
|
+
|
|
113
|
+
<!-- vale off -->
|
|
114
|
+
- **FX Soubirou** - *Initial work* - [GitLab repositories](https://gitlab.com/op_so)
|
|
115
|
+
<!-- vale on -->
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
<!-- vale off -->
|
|
120
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the MIT License (MIT).
|
|
121
|
+
See the [LICENSE](https://opensource.org/licenses/MIT) for details.
|
|
122
|
+
<!-- vale on -->
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
task2md/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
task2md/task2md.py,sha256=AoCIV0xE3-a5WibvXNGqLpaWDxZXQsVIIOwtmj3lq7U,3328
|
|
3
|
+
task2md/task/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
task2md/task/file.py,sha256=09YxIXDgQShrnLdvMPh-yoB4UCjbOlbFxPYm-FgsgpQ,7466
|
|
5
|
+
task2md/task/header.py,sha256=MY_uqIGgFPY6cGVs52ZegPtD0Szq0-Tq6PxTshl7Pgc,2180
|
|
6
|
+
task2md/task/task.py,sha256=wrYNAql_LZhS9YVEj85OIjVcjF8ZdChenO-zb1dqUYc,4314
|
|
7
|
+
task2md/task/variable.py,sha256=jSmG9jHeyHz7_gEu2bY2LYvJGBG_NRo1HRjfypM44FQ,713
|
|
8
|
+
task2md/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
task2md/util/dir.py,sha256=2MeWmhMGkxFiJuZI6xcf71YRhhqBq1HqbDI4tKc2FOc,965
|
|
10
|
+
task2md-1.0.0.dist-info/METADATA,sha256=TtuyDxej0KNYgjEncpnmVje-nWsFDlaPY8ewRxvltAI,3686
|
|
11
|
+
task2md-1.0.0.dist-info/WHEEL,sha256=uNdcs2TADwSd5pVaP0Z_kcjcvvTUklh2S7bxZMF8Uj0,87
|
|
12
|
+
task2md-1.0.0.dist-info/entry_points.txt,sha256=zXCjf5tbisw5ZzMarzBDlSkNgzV1YDsMx3s8h9azjkc,48
|
|
13
|
+
task2md-1.0.0.dist-info/licenses/LICENSE,sha256=6j37YDpY2ikwX8yV80eUpAGPDShonchJW9nyFpqAGKM,1095
|
|
14
|
+
task2md-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2024 FX Soubirou soubirou@yahoo.fr
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|