pyOpenSourceProjects 0.1.2__tar.gz → 0.2.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.
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/.github/workflows/build.yml +2 -4
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/PKG-INFO +4 -2
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/README.md +2 -1
- pyopensourceprojects-0.2.1/mkdocs.yml +18 -0
- pyopensourceprojects-0.2.1/osprojects/__init__.py +1 -0
- pyopensourceprojects-0.2.1/osprojects/checkos.py +320 -0
- pyopensourceprojects-0.2.1/osprojects/editor.py +132 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/osprojects/osproject.py +175 -45
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/pyproject.toml +9 -4
- pyopensourceprojects-0.2.1/scripts/doc +88 -0
- pyopensourceprojects-0.2.1/scripts/release +8 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/tests/basetest.py +1 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/tests/test_osproject.py +72 -1
- pyopensourceprojects-0.1.2/osprojects/__init__.py +0 -1
- pyopensourceprojects-0.1.2/scripts/doc +0 -88
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/.github/workflows/upload-to-pypi.yml +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/.gitignore +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/.project +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/.pydevproject +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/LICENSE +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/scripts/blackisort +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/scripts/install +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/scripts/installAndTest +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/scripts/test +0 -0
- {pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/tests/__init__.py +0 -0
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
matrix:
|
|
21
21
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
22
22
|
#os: [ubuntu-latest]
|
|
23
|
-
python-version: [3.9,
|
|
23
|
+
python-version: [ 3.9, '3.10', '3.11', '3.12' ]
|
|
24
24
|
#python-version: ["3.10"]
|
|
25
25
|
|
|
26
26
|
steps:
|
|
@@ -32,10 +32,8 @@ jobs:
|
|
|
32
32
|
- name: Install dependencies
|
|
33
33
|
run: |
|
|
34
34
|
python -m pip install --upgrade pip
|
|
35
|
-
pip install sphinx
|
|
36
|
-
pip install sphinx_rtd_theme
|
|
37
35
|
scripts/install
|
|
38
36
|
scripts/doc
|
|
39
37
|
- name: Run tests
|
|
40
38
|
run: |
|
|
41
|
-
scripts/
|
|
39
|
+
scripts/test
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyOpenSourceProjects
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Dynamic: Summary
|
|
5
5
|
Project-URL: Home, https://github.com/WolfgangFahl/pyOpenSourceProjects
|
|
6
6
|
Project-URL: Documentation, http://wiki.bitplan.com/index.php/pyOpenSourceProjects
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: packaging>=24.1
|
|
18
19
|
Requires-Dist: py-3rdparty-mediawiki>=0.11.3
|
|
19
20
|
Requires-Dist: pylodstorage>=0.11.6
|
|
20
21
|
Requires-Dist: python-dateutil>=2.8.2
|
|
@@ -25,10 +26,11 @@ Description-Content-Type: text/markdown
|
|
|
25
26
|
Helper Library to organize open source Projects
|
|
26
27
|
|
|
27
28
|
[](https://pypi.org/project/pyOpenSourceProjects/)
|
|
28
|
-
[](https://github.com/WolfgangFahl/pyOpenSourceProjects/actions/workflows/build.yml)
|
|
29
30
|
[](https://pypi.python.org/pypi/pyOpenSourceProjects/)
|
|
30
31
|
[](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues)
|
|
31
32
|
[](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues/?q=is%3Aissue+is%3Aclosed)
|
|
33
|
+
[](https://WolfgangFahl.github.io/pyOpenSourceProjects/)
|
|
32
34
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
33
35
|
|
|
34
36
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
Helper Library to organize open source Projects
|
|
3
3
|
|
|
4
4
|
[](https://pypi.org/project/pyOpenSourceProjects/)
|
|
5
|
-
[](https://github.com/WolfgangFahl/pyOpenSourceProjects/actions/workflows/build.yml)
|
|
6
6
|
[](https://pypi.python.org/pypi/pyOpenSourceProjects/)
|
|
7
7
|
[](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues)
|
|
8
8
|
[](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues/?q=is%3Aissue+is%3Aclosed)
|
|
9
|
+
[](https://WolfgangFahl.github.io/pyOpenSourceProjects/)
|
|
9
10
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
10
11
|
|
|
11
12
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
site_name: pyOpenSourceProjects API Documentation
|
|
2
|
+
theme:
|
|
3
|
+
name: material
|
|
4
|
+
plugins:
|
|
5
|
+
- search
|
|
6
|
+
- mkdocstrings:
|
|
7
|
+
handlers:
|
|
8
|
+
python:
|
|
9
|
+
setup_commands:
|
|
10
|
+
- import sys
|
|
11
|
+
- import os
|
|
12
|
+
- sys.path.insert(0, os.path.abspath("."))
|
|
13
|
+
selection:
|
|
14
|
+
docstring_style: google
|
|
15
|
+
rendering:
|
|
16
|
+
show_source: true
|
|
17
|
+
nav:
|
|
18
|
+
- API: index.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Created on 2024-07-30
|
|
4
|
+
|
|
5
|
+
@author: wf
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import os
|
|
9
|
+
from argparse import Namespace
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import List
|
|
12
|
+
# original at ngwidgets - use redundant local copy ...
|
|
13
|
+
from osprojects.editor import Editor
|
|
14
|
+
from osprojects.osproject import GitHub, OsProject
|
|
15
|
+
import tomllib
|
|
16
|
+
import traceback
|
|
17
|
+
from packaging import version
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Check:
|
|
21
|
+
ok: bool = False
|
|
22
|
+
path: str = None
|
|
23
|
+
msg: str = ""
|
|
24
|
+
content: str = None
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def marker(self) -> str:
|
|
28
|
+
return f"✅" if self.ok else f"❌"
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def file_exists(cls, path) -> "Check":
|
|
32
|
+
ok = os.path.exists(path)
|
|
33
|
+
content = None
|
|
34
|
+
if ok and os.path.isfile(path):
|
|
35
|
+
with open(path, "r") as f:
|
|
36
|
+
content = f.read()
|
|
37
|
+
check = Check(ok, path, msg=path, content=content)
|
|
38
|
+
return check
|
|
39
|
+
|
|
40
|
+
class CheckOS:
|
|
41
|
+
"""
|
|
42
|
+
check the open source projects
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, args: Namespace, project: OsProject):
|
|
46
|
+
self.args = args
|
|
47
|
+
self.verbose = args.verbose
|
|
48
|
+
self.workspace = args.workspace
|
|
49
|
+
self.project = project
|
|
50
|
+
self.project_path = os.path.join(self.workspace, project.id)
|
|
51
|
+
self.checks = []
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def total(self) -> int:
|
|
55
|
+
return len(self.checks)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def ok_checks(self) -> List[Check]:
|
|
59
|
+
ok_checks = [check for check in self.checks if check.ok]
|
|
60
|
+
return ok_checks
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def failed_checks(self) -> List[Check]:
|
|
64
|
+
failed_checks = [check for check in self.checks if not check.ok]
|
|
65
|
+
return failed_checks
|
|
66
|
+
|
|
67
|
+
def add_check(self, ok, msg:str="",path: str=None,negative:bool=False) -> Check:
|
|
68
|
+
if not path:
|
|
69
|
+
raise ValueError("path parameter missing")
|
|
70
|
+
marker=""
|
|
71
|
+
if negative:
|
|
72
|
+
ok=not ok
|
|
73
|
+
marker="⚠ ️"
|
|
74
|
+
check = Check(ok=ok, path=path, msg=f"{marker}{msg}{path}")
|
|
75
|
+
self.checks.append(check)
|
|
76
|
+
return check
|
|
77
|
+
|
|
78
|
+
def add_content_check(self, content: str, needle: str, path: str, negative:bool=False) -> Check:
|
|
79
|
+
ok=needle in content
|
|
80
|
+
check=self.add_check(ok, msg=f"{needle} in ", path=path,negative=negative)
|
|
81
|
+
return check
|
|
82
|
+
|
|
83
|
+
def add_path_check(self, path) -> Check:
|
|
84
|
+
# Check if path exists
|
|
85
|
+
path_exists = Check.file_exists(path)
|
|
86
|
+
self.checks.append(path_exists)
|
|
87
|
+
return path_exists
|
|
88
|
+
|
|
89
|
+
def check_local(self) -> Check:
|
|
90
|
+
local = Check.file_exists(self.project_path)
|
|
91
|
+
return local
|
|
92
|
+
|
|
93
|
+
def check_github_workflows(self):
|
|
94
|
+
workflows_path = os.path.join(self.project_path, ".github", "workflows")
|
|
95
|
+
workflows_exist = self.add_path_check(workflows_path)
|
|
96
|
+
|
|
97
|
+
if workflows_exist.ok:
|
|
98
|
+
required_files = ["build.yml", "upload-to-pypi.yml"]
|
|
99
|
+
for file in required_files:
|
|
100
|
+
file_path = os.path.join(workflows_path, file)
|
|
101
|
+
file_exists = self.add_path_check(file_path)
|
|
102
|
+
|
|
103
|
+
if file_exists.ok:
|
|
104
|
+
content = file_exists.content
|
|
105
|
+
|
|
106
|
+
if file == "build.yml":
|
|
107
|
+
min_version = int(self.requires_python.split('.')[-1])
|
|
108
|
+
python_versions = f"""python-version: [ {', '.join([f"'3.{i}'" for i in range(min_version, 13)])} ]"""
|
|
109
|
+
self.add_content_check(
|
|
110
|
+
content,
|
|
111
|
+
python_versions,
|
|
112
|
+
file_path,
|
|
113
|
+
)
|
|
114
|
+
self.add_content_check(
|
|
115
|
+
content,
|
|
116
|
+
"os: [ubuntu-latest, macos-latest, windows-latest]",
|
|
117
|
+
file_path,
|
|
118
|
+
)
|
|
119
|
+
self.add_content_check(content, "uses: actions/checkout@v4", file_path)
|
|
120
|
+
self.add_content_check(
|
|
121
|
+
content,
|
|
122
|
+
"uses: actions/setup-python@v5",
|
|
123
|
+
file_path,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self.add_content_check(
|
|
127
|
+
content,
|
|
128
|
+
"sphinx",
|
|
129
|
+
file_path,
|
|
130
|
+
negative=True
|
|
131
|
+
)
|
|
132
|
+
scripts_ok="scripts/install" in content and "scripts/test" in content or "scripts/installAndTest" in content
|
|
133
|
+
self.add_check(scripts_ok,"install and test", file_path)
|
|
134
|
+
|
|
135
|
+
elif file == "upload-to-pypi.yml":
|
|
136
|
+
self.add_content_check(content, "id-token: write", file_path)
|
|
137
|
+
self.add_content_check(content, "uses: actions/checkout@v4", file_path)
|
|
138
|
+
self.add_content_check(
|
|
139
|
+
content,
|
|
140
|
+
"uses: actions/setup-python@v5",
|
|
141
|
+
file_path,
|
|
142
|
+
)
|
|
143
|
+
self.add_content_check(
|
|
144
|
+
content,
|
|
145
|
+
"uses: pypa/gh-action-pypi-publish@release/v1",
|
|
146
|
+
file_path,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def check_scripts(self):
|
|
150
|
+
scripts_path = os.path.join(self.project_path, "scripts")
|
|
151
|
+
scripts_exist = self.add_path_check(scripts_path)
|
|
152
|
+
if scripts_exist.ok:
|
|
153
|
+
required_files = ["blackisort", "test", "install", "doc", "release"]
|
|
154
|
+
for file in required_files:
|
|
155
|
+
file_path = os.path.join(scripts_path, file)
|
|
156
|
+
file_exists = self.add_path_check(file_path)
|
|
157
|
+
if file_exists.ok:
|
|
158
|
+
content = file_exists.content
|
|
159
|
+
if file=="doc":
|
|
160
|
+
self.add_content_check(content, "sphinx", file_path, negative=True)
|
|
161
|
+
self.add_content_check(content,"WF 2024-07-30 - updated",file_path)
|
|
162
|
+
if file=="release":
|
|
163
|
+
self.add_content_check(content, "scripts/docs -d", file_path, negative=True)
|
|
164
|
+
|
|
165
|
+
def check_readme(self):
|
|
166
|
+
readme_path = os.path.join(self.project_path, "README.md")
|
|
167
|
+
readme_exists = self.add_path_check(readme_path)
|
|
168
|
+
if readme_exists.ok:
|
|
169
|
+
readme_content = readme_exists.content
|
|
170
|
+
badge_lines = [
|
|
171
|
+
"[](https://pypi.org/project/{self.project_name}/)",
|
|
172
|
+
"[](https://github.com/{self.project.fqid}/actions/workflows/build.yml)",
|
|
173
|
+
"[](https://pypi.python.org/pypi/{self.project_name}/)",
|
|
174
|
+
"[](https://github.com/{self.project.fqid}/issues)",
|
|
175
|
+
"[](https://github.com/{self.project.fqid}/issues/?q=is%3Aissue+is%3Aclosed)",
|
|
176
|
+
"[](https://{self.project.owner}.github.io/{self.project.id}/)",
|
|
177
|
+
"[](https://www.apache.org/licenses/LICENSE-2.0)",
|
|
178
|
+
]
|
|
179
|
+
for line in badge_lines:
|
|
180
|
+
formatted_line = line.format(self=self)
|
|
181
|
+
self.add_content_check(
|
|
182
|
+
content=readme_content,
|
|
183
|
+
needle=formatted_line,
|
|
184
|
+
path=readme_path,
|
|
185
|
+
)
|
|
186
|
+
self.add_content_check(readme_content, "readthedocs", readme_path, negative=True)
|
|
187
|
+
|
|
188
|
+
def check_pyproject_toml(self):
|
|
189
|
+
toml_path = os.path.join(self.project_path, "pyproject.toml")
|
|
190
|
+
toml_exists = self.add_path_check(toml_path)
|
|
191
|
+
if toml_exists.ok:
|
|
192
|
+
content=toml_exists.content
|
|
193
|
+
toml_dict = tomllib.loads(content)
|
|
194
|
+
project_check=self.add_check("project" in toml_dict, "[project]", toml_path)
|
|
195
|
+
if project_check.ok:
|
|
196
|
+
self.project_name=toml_dict["project"]["name"]
|
|
197
|
+
requires_python_check=self.add_check("requires-python" in toml_dict["project"], "requires-python", toml_path)
|
|
198
|
+
if requires_python_check.ok:
|
|
199
|
+
self.requires_python = toml_dict["project"]["requires-python"]
|
|
200
|
+
self.min_python_version = version.parse(self.requires_python.replace(">=", ""))
|
|
201
|
+
min_version_needed="3.9"
|
|
202
|
+
version_ok=self.min_python_version >= version.parse(min_version_needed)
|
|
203
|
+
self.add_check(version_ok, f"requires-python>={min_version_needed}", toml_path)
|
|
204
|
+
self.add_content_check(content, "hatchling", toml_path)
|
|
205
|
+
self.add_content_check(content,"[tool.hatch.build.targets.wheel.sources]",toml_path)
|
|
206
|
+
|
|
207
|
+
def check(self):
|
|
208
|
+
"""
|
|
209
|
+
Check the given project and print results
|
|
210
|
+
"""
|
|
211
|
+
self.check_local()
|
|
212
|
+
self.check_pyproject_toml()
|
|
213
|
+
self.check_readme()
|
|
214
|
+
self.check_scripts()
|
|
215
|
+
self.check_github_workflows()
|
|
216
|
+
|
|
217
|
+
# ok_count=len(ok_checks)
|
|
218
|
+
failed_count = len(self.failed_checks)
|
|
219
|
+
summary = f"❌ {failed_count}/{self.total}" if failed_count > 0 else f"✅ {self.total}/{self.total}"
|
|
220
|
+
print(f"{self.project} {summary}: {self.project.url}")
|
|
221
|
+
if failed_count > 0:
|
|
222
|
+
# Sort checks by path
|
|
223
|
+
sorted_checks = sorted(self.checks, key=lambda c: c.path or "")
|
|
224
|
+
|
|
225
|
+
# Group checks by path
|
|
226
|
+
checks_by_path = {}
|
|
227
|
+
for check in sorted_checks:
|
|
228
|
+
if check.path not in checks_by_path:
|
|
229
|
+
checks_by_path[check.path] = []
|
|
230
|
+
checks_by_path[check.path].append(check)
|
|
231
|
+
|
|
232
|
+
# Display results
|
|
233
|
+
for path, path_checks in checks_by_path.items():
|
|
234
|
+
path_failed = sum(1 for c in path_checks if not c.ok)
|
|
235
|
+
if path_failed > 0 or self.args.debug:
|
|
236
|
+
print(f"❌ {path}: {path_failed}")
|
|
237
|
+
i=0
|
|
238
|
+
for check in path_checks:
|
|
239
|
+
show=not check.ok or self.args.debug
|
|
240
|
+
if show:
|
|
241
|
+
i+=1
|
|
242
|
+
print(f" {i:3}{check.marker}:{check.msg}")
|
|
243
|
+
|
|
244
|
+
if self.args.editor and path_failed > 0:
|
|
245
|
+
if os.path.isfile(path):
|
|
246
|
+
# @TODO Make editor configurable
|
|
247
|
+
Editor.open(path,default_editor_cmd="/usr/local/bin/atom")
|
|
248
|
+
else:
|
|
249
|
+
Editor.open_filepath(path)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def main(_argv=None):
|
|
253
|
+
"""
|
|
254
|
+
main command line entry point
|
|
255
|
+
"""
|
|
256
|
+
parser = argparse.ArgumentParser(description="Check open source projects")
|
|
257
|
+
parser.add_argument(
|
|
258
|
+
"-d",
|
|
259
|
+
"--debug",
|
|
260
|
+
action="store_true",
|
|
261
|
+
help="add debug output",
|
|
262
|
+
)
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
"-e",
|
|
265
|
+
"--editor",
|
|
266
|
+
action="store_true",
|
|
267
|
+
help="open default editor on failed files",
|
|
268
|
+
)
|
|
269
|
+
parser.add_argument(
|
|
270
|
+
"-o", "--owner", help="project owner or organization", required=True
|
|
271
|
+
)
|
|
272
|
+
parser.add_argument("-p", "--project", help="name of the project")
|
|
273
|
+
parser.add_argument("-l", "--language", help="filter projects by language")
|
|
274
|
+
parser.add_argument(
|
|
275
|
+
"--local", action="store_true", help="check only locally available projects"
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"-v", "--verbose", action="store_true", help="show verbose output"
|
|
279
|
+
)
|
|
280
|
+
parser.add_argument(
|
|
281
|
+
"-ws",
|
|
282
|
+
"--workspace",
|
|
283
|
+
help="(Eclipse) workspace directory",
|
|
284
|
+
default=os.path.expanduser("~/py-workspace"),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
args = parser.parse_args(args=_argv)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
github = GitHub()
|
|
291
|
+
if args.project:
|
|
292
|
+
# Check specific project
|
|
293
|
+
projects = github.list_projects_as_os_projects(
|
|
294
|
+
args.owner, project_name=args.project
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
# Check all projects
|
|
298
|
+
projects = github.list_projects_as_os_projects(args.owner)
|
|
299
|
+
|
|
300
|
+
if args.language:
|
|
301
|
+
projects = [p for p in projects if p.language == args.language]
|
|
302
|
+
|
|
303
|
+
if args.local:
|
|
304
|
+
local_projects = []
|
|
305
|
+
for project in projects:
|
|
306
|
+
checker = CheckOS(args=args, project=project)
|
|
307
|
+
if checker.check_local().ok:
|
|
308
|
+
local_projects.append(project)
|
|
309
|
+
projects = local_projects
|
|
310
|
+
|
|
311
|
+
for project in projects:
|
|
312
|
+
checker = CheckOS(args=args, project=project)
|
|
313
|
+
checker.check()
|
|
314
|
+
except Exception as ex:
|
|
315
|
+
if args.debug:
|
|
316
|
+
print(traceback.format_exc())
|
|
317
|
+
raise ex
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
main()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2022-11-27
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from urllib.request import urlopen
|
|
13
|
+
|
|
14
|
+
from bs4 import BeautifulSoup
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Editor:
|
|
18
|
+
"""
|
|
19
|
+
helper class to open the system defined editor
|
|
20
|
+
|
|
21
|
+
see https://stackoverflow.com/questions/1442841/lauch-default-editor-like-webbrowser-module
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def open_filepath(cls, filepath: str):
|
|
26
|
+
if platform.system() == "Darwin": # macOS
|
|
27
|
+
subprocess.call(("open", filepath))
|
|
28
|
+
elif platform.system() == "Windows": # Windows
|
|
29
|
+
os.startfile(filepath, "open")
|
|
30
|
+
else: # linux variants
|
|
31
|
+
subprocess.call(("xdg-open", filepath))
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def extract_text(cls, html_text: str) -> str:
|
|
35
|
+
"""
|
|
36
|
+
extract the text from the given html_text
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
html_text(str): the input for the html text
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
str: the plain text
|
|
43
|
+
"""
|
|
44
|
+
soup = BeautifulSoup(html_text, features="html.parser")
|
|
45
|
+
|
|
46
|
+
# kill all script and style elements
|
|
47
|
+
for script in soup(["script", "style"]):
|
|
48
|
+
script.extract() # rip it out
|
|
49
|
+
|
|
50
|
+
# get text
|
|
51
|
+
text = soup.get_text()
|
|
52
|
+
|
|
53
|
+
# break into lines and remove leading and trailing space on each
|
|
54
|
+
lines = (line.strip() for line in text.splitlines())
|
|
55
|
+
# break multi-headlines into a line each
|
|
56
|
+
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
|
57
|
+
# drop blank lines
|
|
58
|
+
text = "\n".join(chunk for chunk in chunks if chunk)
|
|
59
|
+
return text
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def open(
|
|
63
|
+
cls,
|
|
64
|
+
file_source: str,
|
|
65
|
+
extract_text: bool = True,
|
|
66
|
+
default_editor_cmd: str = "/usr/local/bin/atom",
|
|
67
|
+
) -> str:
|
|
68
|
+
"""
|
|
69
|
+
open an editor for the given file_source
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
file_source(str): the path to the file
|
|
73
|
+
extract_text(bool): if True extract the text from html sources
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
str: the path to the file e.g. a temporary file if the file_source points to an url
|
|
77
|
+
"""
|
|
78
|
+
# handle urls
|
|
79
|
+
# https://stackoverflow.com/a/45886824/1497139
|
|
80
|
+
if file_source.startswith("http"):
|
|
81
|
+
url_source = urlopen(file_source)
|
|
82
|
+
# https://stackoverflow.com/a/19156107/1497139
|
|
83
|
+
charset = url_source.headers.get_content_charset()
|
|
84
|
+
# if charset fails here you might want to set it to utf-8 as a default!
|
|
85
|
+
text = url_source.read().decode(charset)
|
|
86
|
+
if extract_text:
|
|
87
|
+
# https://stackoverflow.com/a/24618186/1497139
|
|
88
|
+
text = cls.extract_text(text)
|
|
89
|
+
|
|
90
|
+
return cls.open_tmp_text(text)
|
|
91
|
+
|
|
92
|
+
editor_cmd = None
|
|
93
|
+
editor_env = os.getenv("EDITOR")
|
|
94
|
+
if editor_env:
|
|
95
|
+
editor_cmd = editor_env
|
|
96
|
+
if platform.system() == "Darwin":
|
|
97
|
+
if not editor_env:
|
|
98
|
+
# https://stackoverflow.com/questions/22390709/how-can-i-open-the-atom-editor-from-the-command-line-in-os-x
|
|
99
|
+
editor_cmd = default_editor_cmd
|
|
100
|
+
if editor_cmd:
|
|
101
|
+
os_cmd = f"{editor_cmd} {file_source}"
|
|
102
|
+
os.system(os_cmd)
|
|
103
|
+
return file_source
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def open_tmp_text(cls, text: str, file_name: str = None) -> str:
|
|
107
|
+
"""
|
|
108
|
+
open an editor for the given text in a newly created temporary file
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
text(str): the text to write to a temporary file and then open
|
|
112
|
+
file_name(str): the name to use for the file
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
str: the path to the temp file
|
|
116
|
+
"""
|
|
117
|
+
# see https://stackoverflow.com/a/8577226/1497139
|
|
118
|
+
# https://stackoverflow.com/a/3924253/1497139
|
|
119
|
+
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
120
|
+
with open(tmp.name, "w") as tmp_file:
|
|
121
|
+
tmp_file.write(text)
|
|
122
|
+
tmp_file.close()
|
|
123
|
+
if file_name is None:
|
|
124
|
+
file_path = tmp.name
|
|
125
|
+
else:
|
|
126
|
+
# https://stackoverflow.com/questions/3167154/how-to-split-a-dos-path-into-its-components-in-python
|
|
127
|
+
path = Path(tmp.name)
|
|
128
|
+
# https://stackoverflow.com/a/49798311/1497139
|
|
129
|
+
file_path = path.parent / file_name
|
|
130
|
+
os.rename(tmp.name, file_path)
|
|
131
|
+
|
|
132
|
+
return cls.open(str(file_path))
|
|
@@ -3,15 +3,17 @@ Created on 2022-01-24
|
|
|
3
3
|
|
|
4
4
|
@author: wf
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from __future__ import annotations
|
|
7
|
-
|
|
8
|
+
|
|
8
9
|
import argparse
|
|
9
10
|
import datetime
|
|
10
11
|
import json
|
|
12
|
+
import os
|
|
11
13
|
import re
|
|
12
14
|
import subprocess
|
|
13
15
|
import sys
|
|
14
|
-
from typing import List, Type
|
|
16
|
+
from typing import List, Optional, Type
|
|
15
17
|
|
|
16
18
|
import requests
|
|
17
19
|
from dateutil.parser import parse
|
|
@@ -55,23 +57,26 @@ class GitHub(TicketSystem):
|
|
|
55
57
|
"""
|
|
56
58
|
wrapper for the GitHub api
|
|
57
59
|
"""
|
|
60
|
+
|
|
58
61
|
@classmethod
|
|
59
|
-
def load_access_token(cls)->str:
|
|
62
|
+
def load_access_token(cls) -> str:
|
|
60
63
|
"""
|
|
61
64
|
if $HOME/.github/access_token.json exists read the token from there
|
|
62
65
|
"""
|
|
63
66
|
# Specify the path to the access token file
|
|
64
|
-
token_file_path = os.path.join(
|
|
65
|
-
|
|
67
|
+
token_file_path = os.path.join(
|
|
68
|
+
os.getenv("HOME"), ".github", "access_token.json"
|
|
69
|
+
)
|
|
70
|
+
|
|
66
71
|
# Check if the file exists and read the token
|
|
67
72
|
if os.path.exists(token_file_path):
|
|
68
|
-
with open(token_file_path,
|
|
73
|
+
with open(token_file_path, "r") as token_file:
|
|
69
74
|
token_data = json.load(token_file)
|
|
70
|
-
return token_data.get(
|
|
71
|
-
|
|
75
|
+
return token_data.get("access_token")
|
|
76
|
+
|
|
72
77
|
# Return None if no token file is found
|
|
73
78
|
return None
|
|
74
|
-
|
|
79
|
+
|
|
75
80
|
@classmethod
|
|
76
81
|
def prepare_headers(cls, access_token: str = None) -> dict:
|
|
77
82
|
"""
|
|
@@ -80,15 +85,108 @@ class GitHub(TicketSystem):
|
|
|
80
85
|
if access_token is None:
|
|
81
86
|
access_token = cls.load_access_token()
|
|
82
87
|
|
|
83
|
-
headers = {
|
|
88
|
+
headers = {"Authorization": f"token {access_token}"} if access_token else {}
|
|
84
89
|
return headers
|
|
85
90
|
|
|
86
91
|
@classmethod
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
def list_projects_as_os_projects(
|
|
93
|
+
cls, owner: str, access_token: str = None, project_name: Optional[str] = None
|
|
94
|
+
) -> List[OsProject]:
|
|
95
|
+
"""
|
|
96
|
+
List all public repositories for a given owner and return them as OsProject instances.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
owner (str): The GitHub username or organization name.
|
|
100
|
+
access_token (str, optional): GitHub personal access token for authentication.
|
|
101
|
+
project_name (str, optional): If provided, return only this specific project.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List[OsProject]: A list of OsProject instances representing the repositories.
|
|
105
|
+
"""
|
|
106
|
+
headers = cls.prepare_headers(access_token)
|
|
107
|
+
|
|
108
|
+
if project_name:
|
|
109
|
+
url = f"https://api.github.com/repos/{owner}/{project_name}"
|
|
110
|
+
response = requests.get(url, headers=headers)
|
|
111
|
+
if response.status_code != 200:
|
|
112
|
+
raise Exception(
|
|
113
|
+
f"Failed to fetch repository: {response.status_code} - {response.text}"
|
|
114
|
+
)
|
|
115
|
+
repos = [response.json()]
|
|
116
|
+
else:
|
|
117
|
+
url = f"https://api.github.com/users/{owner}/repos"
|
|
118
|
+
params = {
|
|
119
|
+
"type": "all",
|
|
120
|
+
"per_page": 100,
|
|
121
|
+
} # Include all repo types, 100 per page
|
|
122
|
+
all_repos = []
|
|
123
|
+
page = 1
|
|
124
|
+
|
|
125
|
+
while True:
|
|
126
|
+
params["page"] = page
|
|
127
|
+
response = requests.get(url, headers=headers, params=params)
|
|
128
|
+
|
|
129
|
+
if response.status_code != 200:
|
|
130
|
+
raise Exception(
|
|
131
|
+
f"Failed to fetch repositories: {response.status_code} - {response.text}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
repos = response.json()
|
|
135
|
+
if not repos:
|
|
136
|
+
break # No more repositories to fetch
|
|
137
|
+
|
|
138
|
+
all_repos.extend(repos)
|
|
139
|
+
page += 1
|
|
140
|
+
|
|
141
|
+
repos = all_repos
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
OsProject(
|
|
145
|
+
owner=owner,
|
|
146
|
+
id=repo["name"],
|
|
147
|
+
ticketSystem=cls,
|
|
148
|
+
title=repo["name"],
|
|
149
|
+
url=repo["html_url"],
|
|
150
|
+
description=repo["description"],
|
|
151
|
+
language=repo["language"],
|
|
152
|
+
created_at=datetime.datetime.fromisoformat(
|
|
153
|
+
repo["created_at"].rstrip("Z")
|
|
154
|
+
),
|
|
155
|
+
updated_at=datetime.datetime.fromisoformat(
|
|
156
|
+
repo["updated_at"].rstrip("Z")
|
|
157
|
+
),
|
|
158
|
+
stars=repo["stargazers_count"],
|
|
159
|
+
forks=repo["forks_count"],
|
|
160
|
+
)
|
|
161
|
+
for repo in repos
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def get_project(
|
|
166
|
+
cls, owner: str, project_id: str, access_token: str = None
|
|
167
|
+
) -> OsProject:
|
|
168
|
+
"""
|
|
169
|
+
Get a specific project as an OsProject instance.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
owner (str): The GitHub username or organization name.
|
|
173
|
+
project_id (str): The name of the project.
|
|
174
|
+
access_token (str, optional): GitHub personal access token for authentication.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
OsProject: An OsProject instance representing the repository.
|
|
178
|
+
"""
|
|
179
|
+
projects = cls.list_projects_as_os_projects(
|
|
180
|
+
owner, access_token, project_name=project_id
|
|
181
|
+
)
|
|
182
|
+
if projects:
|
|
183
|
+
return projects[0]
|
|
184
|
+
raise Exception(f"Project {owner}/{project_id} not found")
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def getIssues(
|
|
188
|
+
cls, project: OsProject, access_token: str = None, limit: int = None, **params
|
|
189
|
+
) -> List[Ticket]:
|
|
92
190
|
payload = {}
|
|
93
191
|
headers = cls.prepare_headers(access_token)
|
|
94
192
|
issues = []
|
|
@@ -104,20 +202,24 @@ class GitHub(TicketSystem):
|
|
|
104
202
|
data=payload,
|
|
105
203
|
params=params,
|
|
106
204
|
)
|
|
107
|
-
if response.status_code == 403 and
|
|
205
|
+
if response.status_code == 403 and "rate limit" in response.text:
|
|
108
206
|
raise Exception("rate limit - you might want to use an access token")
|
|
109
207
|
issue_records = json.loads(response.text)
|
|
110
208
|
for record in issue_records:
|
|
111
209
|
tr = {
|
|
112
210
|
"project": project,
|
|
113
211
|
"title": record.get("title"),
|
|
114
|
-
"body": record.get("body", ""),
|
|
115
|
-
"createdAt":
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
212
|
+
"body": record.get("body", ""),
|
|
213
|
+
"createdAt": (
|
|
214
|
+
parse(record.get("created_at"))
|
|
215
|
+
if record.get("created_at")
|
|
216
|
+
else ""
|
|
217
|
+
),
|
|
218
|
+
"closedAt": (
|
|
219
|
+
parse(record.get("closed_at"))
|
|
220
|
+
if record.get("closed_at")
|
|
221
|
+
else ""
|
|
222
|
+
),
|
|
121
223
|
"state": record.get("state"),
|
|
122
224
|
"number": record.get("number"),
|
|
123
225
|
"url": f"{cls.projectUrl(project)}/issues/{record.get('number')}",
|
|
@@ -126,7 +228,7 @@ class GitHub(TicketSystem):
|
|
|
126
228
|
fetched_count += 1
|
|
127
229
|
# Check if we have reached the limit
|
|
128
230
|
if limit is not None and fetched_count >= limit:
|
|
129
|
-
nextResults=False
|
|
231
|
+
nextResults = False
|
|
130
232
|
break
|
|
131
233
|
|
|
132
234
|
if len(issue_records) < 100:
|
|
@@ -134,9 +236,11 @@ class GitHub(TicketSystem):
|
|
|
134
236
|
else:
|
|
135
237
|
params["page"] += 1
|
|
136
238
|
return issues
|
|
137
|
-
|
|
239
|
+
|
|
138
240
|
@classmethod
|
|
139
|
-
def getComments(
|
|
241
|
+
def getComments(
|
|
242
|
+
cls, project: OsProject, issue_number: int, access_token: str = None
|
|
243
|
+
) -> List[dict]:
|
|
140
244
|
"""
|
|
141
245
|
Fetch all comments for a specific issue number from GitHub.
|
|
142
246
|
"""
|
|
@@ -146,7 +250,9 @@ class GitHub(TicketSystem):
|
|
|
146
250
|
if response.status_code == 200:
|
|
147
251
|
return response.json()
|
|
148
252
|
else:
|
|
149
|
-
raise Exception(
|
|
253
|
+
raise Exception(
|
|
254
|
+
f"Failed to fetch comments: {response.status_code} - {response.text}"
|
|
255
|
+
)
|
|
150
256
|
return []
|
|
151
257
|
|
|
152
258
|
@staticmethod
|
|
@@ -160,7 +266,7 @@ class GitHub(TicketSystem):
|
|
|
160
266
|
@staticmethod
|
|
161
267
|
def commitUrl(project: OsProject, id: str):
|
|
162
268
|
return f"{GitHub.projectUrl(project)}/commit/{id}"
|
|
163
|
-
|
|
269
|
+
|
|
164
270
|
@staticmethod
|
|
165
271
|
def commentUrl(project: OsProject, issue_number: int):
|
|
166
272
|
"""
|
|
@@ -168,7 +274,6 @@ class GitHub(TicketSystem):
|
|
|
168
274
|
"""
|
|
169
275
|
return f"https://api.github.com/repos/{project.owner}/{project.id}/issues/{issue_number}/comments"
|
|
170
276
|
|
|
171
|
-
|
|
172
277
|
@staticmethod
|
|
173
278
|
def resolveProjectUrl(url: str) -> (str, str):
|
|
174
279
|
"""
|
|
@@ -201,29 +306,53 @@ class OsProject(object):
|
|
|
201
306
|
self,
|
|
202
307
|
owner: str = None,
|
|
203
308
|
id: str = None,
|
|
204
|
-
ticketSystem: Type[TicketSystem] =
|
|
309
|
+
ticketSystem: Type[TicketSystem] = None,
|
|
310
|
+
title: str = None,
|
|
311
|
+
url: str = None,
|
|
312
|
+
description: str = None,
|
|
313
|
+
language: str = None,
|
|
314
|
+
created_at: datetime.datetime = None,
|
|
315
|
+
updated_at: datetime.datetime = None,
|
|
316
|
+
stars: int = 0,
|
|
317
|
+
forks: int = 0,
|
|
205
318
|
):
|
|
206
319
|
"""
|
|
207
320
|
Constructor
|
|
208
321
|
"""
|
|
209
322
|
self.owner = owner
|
|
210
323
|
self.id = id
|
|
211
|
-
self.ticketSystem = ticketSystem
|
|
324
|
+
self.ticketSystem = ticketSystem or GitHub
|
|
325
|
+
self.title = title
|
|
326
|
+
self.url = url
|
|
327
|
+
self.description = description
|
|
328
|
+
self.language = language
|
|
329
|
+
self.created_at = created_at
|
|
330
|
+
self.updated_at = updated_at
|
|
331
|
+
self.stars = stars
|
|
332
|
+
self.forks = forks
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def fqid(self):
|
|
336
|
+
fqid = f"{self.owner}/{self.id}"
|
|
337
|
+
return fqid
|
|
338
|
+
|
|
339
|
+
def __str__(self):
|
|
340
|
+
return self.fqid
|
|
212
341
|
|
|
213
342
|
@staticmethod
|
|
214
343
|
def getSamples():
|
|
215
344
|
samples = [
|
|
216
345
|
{
|
|
217
346
|
"id": "pyOpenSourceProjects",
|
|
218
|
-
"state": "",
|
|
219
347
|
"owner": "WolfgangFahl",
|
|
220
348
|
"title": "pyOpenSourceProjects",
|
|
221
349
|
"url": "https://github.com/WolfgangFahl/pyOpenSourceProjects",
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"
|
|
350
|
+
"description": "Helper Library to organize open source Projects",
|
|
351
|
+
"language": "Python",
|
|
352
|
+
"created_at": datetime.datetime(year=2022, month=1, day=24),
|
|
353
|
+
"updated_at": datetime.datetime(year=2022, month=1, day=24),
|
|
354
|
+
"stars": 5,
|
|
355
|
+
"forks": 2,
|
|
227
356
|
}
|
|
228
357
|
]
|
|
229
358
|
return samples
|
|
@@ -235,8 +364,7 @@ class OsProject(object):
|
|
|
235
364
|
"""
|
|
236
365
|
url = subprocess.check_output(["git", "config", "--get", "remote.origin.url"])
|
|
237
366
|
url = url.decode().strip("\n")
|
|
238
|
-
|
|
239
|
-
return osProject
|
|
367
|
+
return cls.fromUrl(url)
|
|
240
368
|
|
|
241
369
|
@classmethod
|
|
242
370
|
def fromUrl(cls, url: str) -> OsProject:
|
|
@@ -244,9 +372,11 @@ class OsProject(object):
|
|
|
244
372
|
Init OsProject from given url
|
|
245
373
|
"""
|
|
246
374
|
if "github.com" in url:
|
|
247
|
-
owner,
|
|
248
|
-
if owner and
|
|
249
|
-
|
|
375
|
+
owner, project_id = GitHub.resolveProjectUrl(url)
|
|
376
|
+
if owner and project_id:
|
|
377
|
+
github = GitHub()
|
|
378
|
+
project = github.get_project(owner, project_id)
|
|
379
|
+
return project
|
|
250
380
|
raise Exception(f"Could not resolve the url '{url}' to a OsProject object")
|
|
251
381
|
|
|
252
382
|
def getIssues(self, **params) -> list:
|
|
@@ -254,14 +384,14 @@ class OsProject(object):
|
|
|
254
384
|
tickets.sort(key=lambda r: getattr(r, "number"), reverse=True)
|
|
255
385
|
return tickets
|
|
256
386
|
|
|
257
|
-
def getAllTickets(self, limit:int=None
|
|
387
|
+
def getAllTickets(self, limit: int = None, **params):
|
|
258
388
|
"""
|
|
259
389
|
Get all Tickets of the project - closed and open ones
|
|
260
|
-
|
|
390
|
+
|
|
261
391
|
Args:
|
|
262
392
|
limit(int): if set limit the number of tickets retrieved
|
|
263
393
|
"""
|
|
264
|
-
issues= self.getIssues(state="all",limit=limit, **params)
|
|
394
|
+
issues = self.getIssues(state="all", limit=limit, **params)
|
|
265
395
|
return issues
|
|
266
396
|
|
|
267
397
|
def getCommits(self) -> List[Commit]:
|
|
@@ -18,7 +18,9 @@ dependencies = [
|
|
|
18
18
|
"pyLodStorage>=0.11.6",
|
|
19
19
|
"py-3rdparty-mediawiki>=0.11.3",
|
|
20
20
|
# https://pypi.org/project/python-dateutil/
|
|
21
|
-
"python-dateutil>=2.8.2"
|
|
21
|
+
"python-dateutil>=2.8.2",
|
|
22
|
+
# https://github.com/pypa/packaging
|
|
23
|
+
"packaging>=24.1"
|
|
22
24
|
]
|
|
23
25
|
requires-python = ">=3.9"
|
|
24
26
|
|
|
@@ -44,10 +46,13 @@ path = "osprojects/__init__.py"
|
|
|
44
46
|
test = []
|
|
45
47
|
|
|
46
48
|
[tool.hatch.build.targets.wheel]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
]
|
|
49
|
+
only-include = ["osprojects"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
52
|
+
"osprojects" = "osprojects"
|
|
53
|
+
|
|
50
54
|
|
|
51
55
|
[project.scripts]
|
|
52
56
|
issue2ticket = "osprojects.osproject:main"
|
|
53
57
|
gitlog2wiki = "osprojects.osproject:gitlog2wiki"
|
|
58
|
+
checkos = "osprojects.checkos:main"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# create docs for a configurable project
|
|
3
|
+
# WF 2024-07-30 - updated
|
|
4
|
+
|
|
5
|
+
# Extract project name from pyproject.toml
|
|
6
|
+
PROJECT_NAME=$(grep "\[project\]" pyproject.toml -A1 | grep name | cut -d '=' -f2 | tr -d ' "')
|
|
7
|
+
PACKAGE_NAME=$(grep "\[tool.hatch.build.targets.wheel.sources\]" pyproject.toml -A1 | tail -1 | cut -d '=' -f2 | tr -d ' "')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Function to print usage information
|
|
11
|
+
print_usage() {
|
|
12
|
+
echo "Usage: $0 [OPTIONS]"
|
|
13
|
+
echo "Options:"
|
|
14
|
+
echo " -pr, --project NAME Set the project name (default: $PROJECT_NAME)"
|
|
15
|
+
echo " -pa, --package NAME Set the package name (default: $PACKAGE_NAME)"
|
|
16
|
+
echo " -d, --deploy Deploy the documentation after building"
|
|
17
|
+
echo " -h, --help Display this help message"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Parse command line arguments
|
|
21
|
+
DEPLOY=false
|
|
22
|
+
while [[ "$#" -gt 0 ]]; do
|
|
23
|
+
case $1 in
|
|
24
|
+
-pr|--project) PROJECT_NAME="$2"; shift ;;
|
|
25
|
+
-pa|--package) PACKAGE_NAME="$2"; shift ;;
|
|
26
|
+
-d|--deploy) DEPLOY=true ;;
|
|
27
|
+
-h|--help) print_usage; exit 0 ;;
|
|
28
|
+
*) echo "Unknown parameter: $1"; print_usage; exit 1 ;;
|
|
29
|
+
esac
|
|
30
|
+
shift
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
# Ensure we're in the correct directory
|
|
34
|
+
if [[ ! -d "$PACKAGE_NAME" ]]; then
|
|
35
|
+
echo "Error: $PACKAGE_NAME package directory not found. Are you in the correct directory?"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Check if mkdocs is installed
|
|
40
|
+
if ! command -v mkdocs &> /dev/null; then
|
|
41
|
+
pip install mkdocs mkdocs-material mkdocstrings[python]
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Create or update mkdocs.yml
|
|
45
|
+
cat << EOF > mkdocs.yml
|
|
46
|
+
site_name: $PROJECT_NAME API Documentation
|
|
47
|
+
theme:
|
|
48
|
+
name: material
|
|
49
|
+
plugins:
|
|
50
|
+
- search
|
|
51
|
+
- mkdocstrings:
|
|
52
|
+
handlers:
|
|
53
|
+
python:
|
|
54
|
+
setup_commands:
|
|
55
|
+
- import sys
|
|
56
|
+
- import os
|
|
57
|
+
- sys.path.insert(0, os.path.abspath("."))
|
|
58
|
+
selection:
|
|
59
|
+
docstring_style: google
|
|
60
|
+
rendering:
|
|
61
|
+
show_source: true
|
|
62
|
+
nav:
|
|
63
|
+
- API: index.md
|
|
64
|
+
EOF
|
|
65
|
+
|
|
66
|
+
# Create or update index.md
|
|
67
|
+
index_md=docs/index.md
|
|
68
|
+
mkdir -p docs
|
|
69
|
+
cat << EOF > $index_md
|
|
70
|
+
# $PROJECT_NAME API Documentation
|
|
71
|
+
|
|
72
|
+
::: $PACKAGE_NAME
|
|
73
|
+
options:
|
|
74
|
+
show_submodules: true
|
|
75
|
+
EOF
|
|
76
|
+
|
|
77
|
+
# Ignore DeprecationWarnings during build
|
|
78
|
+
export PYTHONWARNINGS="ignore::DeprecationWarning"
|
|
79
|
+
|
|
80
|
+
# Build the documentation
|
|
81
|
+
mkdocs build --config-file ./mkdocs.yml
|
|
82
|
+
|
|
83
|
+
# Deploy if requested
|
|
84
|
+
if [ "$DEPLOY" = true ]; then
|
|
85
|
+
mkdocs gh-deploy --force --config-file ./mkdocs.yml
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
echo "Documentation process completed for $PROJECT_NAME."
|
|
@@ -3,6 +3,7 @@ Created on 2022-01-24
|
|
|
3
3
|
|
|
4
4
|
@author: wf
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import unittest
|
|
7
8
|
|
|
8
9
|
from osprojects.osproject import Commit, GitHub, OsProject, Ticket, gitlog2wiki, main
|
|
@@ -23,7 +24,7 @@ class TestOsProject(BaseTest):
|
|
|
23
24
|
expectedTicket = self.getSampleById(Ticket, "number", 2)
|
|
24
25
|
expectedTicket.project = osProject
|
|
25
26
|
comparison_ticket_dict = tickets[-2].__dict__
|
|
26
|
-
comparison_ticket_dict.pop(
|
|
27
|
+
comparison_ticket_dict.pop("body", None)
|
|
27
28
|
self.assertDictEqual(expectedTicket.__dict__, comparison_ticket_dict)
|
|
28
29
|
commit = Commit()
|
|
29
30
|
ticket = Ticket()
|
|
@@ -102,6 +103,76 @@ class TestGitHub(BaseTest):
|
|
|
102
103
|
self.assertEqual(expectedOwner, owner)
|
|
103
104
|
self.assertEqual(expectedProject, project)
|
|
104
105
|
|
|
106
|
+
def testListProjects(self):
|
|
107
|
+
"""
|
|
108
|
+
tests the list_projects_as_os_projects method
|
|
109
|
+
"""
|
|
110
|
+
owner = "WolfgangFahl"
|
|
111
|
+
github = GitHub()
|
|
112
|
+
|
|
113
|
+
# Test list_projects_as_os_projects
|
|
114
|
+
projects = github.list_projects_as_os_projects(owner)
|
|
115
|
+
debug = self.debug
|
|
116
|
+
debug = True
|
|
117
|
+
if debug:
|
|
118
|
+
for project in projects:
|
|
119
|
+
print(project)
|
|
120
|
+
self.assertIsInstance(projects, list)
|
|
121
|
+
self.assertTrue(len(projects) > 0, "No projects found for WolfgangFahl")
|
|
122
|
+
|
|
123
|
+
# Check if pyOpenSourceProjects is in the list
|
|
124
|
+
pyosp_found = any(project.id == "pyOpenSourceProjects" for project in projects)
|
|
125
|
+
self.assertTrue(
|
|
126
|
+
pyosp_found, "pyOpenSourceProjects not found in the list of projects"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Test a sample project's structure
|
|
130
|
+
sample_project = projects[0]
|
|
131
|
+
expected_attributes = {
|
|
132
|
+
"id",
|
|
133
|
+
"owner",
|
|
134
|
+
"title",
|
|
135
|
+
"url",
|
|
136
|
+
"description",
|
|
137
|
+
"language",
|
|
138
|
+
"created_at",
|
|
139
|
+
"updated_at",
|
|
140
|
+
"stars",
|
|
141
|
+
"forks",
|
|
142
|
+
}
|
|
143
|
+
self.assertTrue(
|
|
144
|
+
all(hasattr(sample_project, attr) for attr in expected_attributes),
|
|
145
|
+
"OsProject instance is missing expected attributes",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Check if all items are OsProject instances
|
|
149
|
+
self.assertTrue(
|
|
150
|
+
all(isinstance(project, OsProject) for project in projects),
|
|
151
|
+
"Not all items are OsProject instances",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Test a sample OsProject
|
|
155
|
+
sample_os_project = projects[0]
|
|
156
|
+
self.assertEqual(sample_os_project.owner, owner)
|
|
157
|
+
self.assertIsInstance(sample_os_project.id, str)
|
|
158
|
+
self.assertEqual(sample_os_project.ticketSystem, GitHub)
|
|
159
|
+
|
|
160
|
+
def testGetSpecificProject(self):
|
|
161
|
+
"""
|
|
162
|
+
tests getting a specific project
|
|
163
|
+
"""
|
|
164
|
+
owner = "WolfgangFahl"
|
|
165
|
+
project_name = "pyOpenSourceProjects"
|
|
166
|
+
github = GitHub()
|
|
167
|
+
|
|
168
|
+
project = github.list_projects_as_os_projects(owner, project_name=project_name)[
|
|
169
|
+
0
|
|
170
|
+
]
|
|
171
|
+
self.assertIsInstance(project, OsProject)
|
|
172
|
+
self.assertEqual(project.id, project_name)
|
|
173
|
+
self.assertEqual(project.owner, owner)
|
|
174
|
+
self.assertEqual(project.ticketSystem, GitHub)
|
|
175
|
+
|
|
105
176
|
|
|
106
177
|
class TestCommit(BaseTest):
|
|
107
178
|
"""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.2"
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# WF 2020-01-31
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
# check whether the given command is installed
|
|
6
|
-
#
|
|
7
|
-
checkinstalled() {
|
|
8
|
-
local l_cmd="$1"
|
|
9
|
-
which $l_cmd > /dev/null
|
|
10
|
-
if [ $? -ne 0 ]
|
|
11
|
-
then
|
|
12
|
-
echo "$l_cmd need to be installed" 1>&2
|
|
13
|
-
exit 1
|
|
14
|
-
fi
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
fixconf() {
|
|
18
|
-
local l_year="$1"
|
|
19
|
-
local l_author="$2"
|
|
20
|
-
conf=conf.py
|
|
21
|
-
# fix sys path
|
|
22
|
-
# https://stackoverflow.com/questions/10324393/sphinx-build-fail-autodoc-cant-import-find-module
|
|
23
|
-
grep "# sys.path" $conf
|
|
24
|
-
if [ $? -eq 0 ]
|
|
25
|
-
then
|
|
26
|
-
tmpconf=/tmp/conf$$.py
|
|
27
|
-
cat $conf | awk -v author="$l_author" -v year="$l_year" '
|
|
28
|
-
BEGIN {
|
|
29
|
-
quote="\x27"
|
|
30
|
-
squote="\047"
|
|
31
|
-
}
|
|
32
|
-
/# import os/ { next }
|
|
33
|
-
/# import sys/ { next }
|
|
34
|
-
/copyright/ {
|
|
35
|
-
printf "copyright = %s%s, %s%s\n",squote,year,author,squote
|
|
36
|
-
next
|
|
37
|
-
}
|
|
38
|
-
/author/ {
|
|
39
|
-
printf "author = %s%s%s\n",squote,author,squote
|
|
40
|
-
next
|
|
41
|
-
}
|
|
42
|
-
/html_theme = / {
|
|
43
|
-
# html_theme = 'alabaster'
|
|
44
|
-
printf "html_theme = %ssphinx_rtd_theme%s\n",squote,squote
|
|
45
|
-
printf "master_doc = %sindex%s\n",squote,squote
|
|
46
|
-
next
|
|
47
|
-
}
|
|
48
|
-
# add sphinx_rtd extension
|
|
49
|
-
/extensions = / {
|
|
50
|
-
print $0
|
|
51
|
-
printf "\t%ssphinx_rtd_theme%s,\n",squote,squote
|
|
52
|
-
printf "\t%ssphinx.ext.napoleon%s,\n",squote,squote
|
|
53
|
-
next
|
|
54
|
-
}
|
|
55
|
-
/# sys.path/ {
|
|
56
|
-
print("#https://stackoverflow.com/a/44980548/1497139")
|
|
57
|
-
print("import os")
|
|
58
|
-
print("import sys")
|
|
59
|
-
print("import sphinx_rtd_theme")
|
|
60
|
-
printf("basepath=os.path.abspath(%s../..%s)\n",squote,squote)
|
|
61
|
-
printf("print(%sadding basepath %%s%s %% (basepath))\n",squote,squote)
|
|
62
|
-
print("sys.path.insert(0, basepath)")
|
|
63
|
-
printf("print(%ssys.path is now: %%s%s %% (sys.path))\n",squote,squote)
|
|
64
|
-
next
|
|
65
|
-
}
|
|
66
|
-
{ print }
|
|
67
|
-
END {
|
|
68
|
-
print ("#additional settings")
|
|
69
|
-
print ("#https://stackoverflow.com/a/5599712/1497139")
|
|
70
|
-
print ("autoclass_content = '\''both'\''")
|
|
71
|
-
}' > $tmpconf
|
|
72
|
-
#diff $tmpconf $conf
|
|
73
|
-
mv $tmpconf $conf
|
|
74
|
-
echo "$src/conf.py has been fixed"
|
|
75
|
-
fi
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
src=docs/source
|
|
79
|
-
checkinstalled sphinx-apidoc
|
|
80
|
-
sphinx-apidoc --full -f -o $src .
|
|
81
|
-
cd $src
|
|
82
|
-
|
|
83
|
-
fixconf 2020-2021 "Wolfgang Fahl"
|
|
84
|
-
make clean html
|
|
85
|
-
if [ "$GHACTIONS" != "ACTIVE" ]
|
|
86
|
-
then
|
|
87
|
-
open _build/html/index.html
|
|
88
|
-
fi
|
{pyopensourceprojects-0.1.2 → pyopensourceprojects-0.2.1}/.github/workflows/upload-to-pypi.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|