pyOpenSourceProjects 0.2.3__tar.gz → 0.4.0__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.2.3 → pyopensourceprojects-0.4.0}/.github/workflows/build.yml +2 -2
- pyopensourceprojects-0.4.0/PKG-INFO +44 -0
- pyopensourceprojects-0.4.0/README.md +17 -0
- pyopensourceprojects-0.4.0/osprojects/__init__.py +1 -0
- pyopensourceprojects-0.4.0/osprojects/check_project.py +389 -0
- pyopensourceprojects-0.4.0/osprojects/checkos.py +140 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/osprojects/editor.py +7 -11
- pyopensourceprojects-0.4.0/osprojects/github_api.py +338 -0
- pyopensourceprojects-0.4.0/osprojects/osproject.py +544 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/pyproject.toml +13 -7
- pyopensourceprojects-0.4.0/scripts/blackisort +15 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/tests/basetest.py +14 -21
- pyopensourceprojects-0.4.0/tests/test_github.py +111 -0
- pyopensourceprojects-0.4.0/tests/test_github_api.py +81 -0
- pyopensourceprojects-0.4.0/tests/test_osproject.py +92 -0
- pyopensourceprojects-0.2.3/PKG-INFO +0 -43
- pyopensourceprojects-0.2.3/README.md +0 -18
- pyopensourceprojects-0.2.3/osprojects/__init__.py +0 -1
- pyopensourceprojects-0.2.3/osprojects/checkos.py +0 -397
- pyopensourceprojects-0.2.3/osprojects/osproject.py +0 -564
- pyopensourceprojects-0.2.3/scripts/blackisort +0 -7
- pyopensourceprojects-0.2.3/tests/test_osproject.py +0 -193
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.github/workflows/upload-to-pypi.yml +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.gitignore +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.project +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.pydevproject +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/LICENSE +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/mkdocs.yml +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/doc +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/install +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/installAndTest +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/release +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/test +0 -0
- {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/tests/__init__.py +0 -0
|
@@ -20,8 +20,8 @@ jobs:
|
|
|
20
20
|
matrix:
|
|
21
21
|
#os: [ubuntu-latest, macos-latest, windows-latest]
|
|
22
22
|
os: [ubuntu-latest]
|
|
23
|
-
#python-version: [ '3.
|
|
24
|
-
python-version: ["3.
|
|
23
|
+
#python-version: [ '3.11', '3.12', '3.13' ]
|
|
24
|
+
python-version: ["3.12"]
|
|
25
25
|
|
|
26
26
|
steps:
|
|
27
27
|
- uses: actions/checkout@v4
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyOpenSourceProjects
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Dynamic: Summary
|
|
5
|
+
Project-URL: Home, https://github.com/WolfgangFahl/pyOpenSourceProjects
|
|
6
|
+
Project-URL: Documentation, http://wiki.bitplan.com/index.php/pyOpenSourceProjects
|
|
7
|
+
Project-URL: Source, https://github.com/WolfgangFahl/pyOpenSourceProjects
|
|
8
|
+
Author-email: Wolfgang Fahl <wf@bitplan.com>
|
|
9
|
+
Maintainer-email: Wolfgang Fahl <wf@bitplan.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: beautifulsoup4>=4.14.2
|
|
18
|
+
Requires-Dist: gitpython
|
|
19
|
+
Requires-Dist: packaging>=24.1
|
|
20
|
+
Requires-Dist: py-3rdparty-mediawiki>=0.18.1
|
|
21
|
+
Requires-Dist: pylodstorage>=0.17.0
|
|
22
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
23
|
+
Requires-Dist: requests
|
|
24
|
+
Requires-Dist: tqdm>=4.66.5
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# pyOpenSourceProjects
|
|
29
|
+
|
|
30
|
+
Helper Library to organize open source Projects
|
|
31
|
+
|
|
32
|
+
| | |
|
|
33
|
+
| :--- | :--- |
|
|
34
|
+
| **PyPi** | [](https://pypi.python.org/pypi/pyOpenSourceProjects/) [](https://www.apache.org/licenses/LICENSE-2.0) [](https://pypi.org/project/pyOpenSourceProjects/) [](https://pypi.org/project/pyOpenSourceProjects/) [](https://pypi.org/project/pyOpenSourceProjects/) |
|
|
35
|
+
| **GitHub** | [](https://github.com/WolfgangFahl/pyOpenSourceProjects/actions/workflows/build.yml) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/releases) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/graphs/contributors) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/commits/) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues/?q=is%3Aissue+is%3Aclosed) |
|
|
36
|
+
| **Code** | [](https://github.com/psf/black) [](https://pycqa.github.io/isort/) |
|
|
37
|
+
| **Docs** | [](https://WolfgangFahl.github.io/pyOpenSourceProjects/) [](https://github.com/PyCQA/docformatter) [](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) |
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
[Wiki](https://wiki.bitplan.com/index.php/PyOpenSourceProjects)
|
|
41
|
+
|
|
42
|
+
### Authors
|
|
43
|
+
* [Tim Holzheim](https://www.semantic-mediawiki.org/wiki/Tim_Holzheim)
|
|
44
|
+
* [Wolfgang Fahl](http://www.bitplan.com/Wolfgang_Fahl)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# pyOpenSourceProjects
|
|
2
|
+
|
|
3
|
+
Helper Library to organize open source Projects
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
| :--- | :--- |
|
|
7
|
+
| **PyPi** | [](https://pypi.python.org/pypi/pyOpenSourceProjects/) [](https://www.apache.org/licenses/LICENSE-2.0) [](https://pypi.org/project/pyOpenSourceProjects/) [](https://pypi.org/project/pyOpenSourceProjects/) [](https://pypi.org/project/pyOpenSourceProjects/) |
|
|
8
|
+
| **GitHub** | [](https://github.com/WolfgangFahl/pyOpenSourceProjects/actions/workflows/build.yml) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/releases) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/graphs/contributors) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/commits/) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues) [](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues/?q=is%3Aissue+is%3Aclosed) |
|
|
9
|
+
| **Code** | [](https://github.com/psf/black) [](https://pycqa.github.io/isort/) |
|
|
10
|
+
| **Docs** | [](https://WolfgangFahl.github.io/pyOpenSourceProjects/) [](https://github.com/PyCQA/docformatter) [](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) |
|
|
11
|
+
|
|
12
|
+
## Documentation
|
|
13
|
+
[Wiki](https://wiki.bitplan.com/index.php/PyOpenSourceProjects)
|
|
14
|
+
|
|
15
|
+
### Authors
|
|
16
|
+
* [Tim Holzheim](https://www.semantic-mediawiki.org/wiki/Tim_Holzheim)
|
|
17
|
+
* [Wolfgang Fahl](http://www.bitplan.com/Wolfgang_Fahl)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Created on 2024-08-28.
|
|
2
|
+
|
|
3
|
+
@author: wf
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import tomllib
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from git import Repo
|
|
12
|
+
from git.exc import InvalidGitRepositoryError, NoSuchPathError
|
|
13
|
+
from packaging import version
|
|
14
|
+
|
|
15
|
+
# original at ngwidgets - use redundant local copy ...
|
|
16
|
+
from osprojects.editor import Editor
|
|
17
|
+
from osprojects.github_api import GitHubAction
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Check:
|
|
22
|
+
ok: bool = False
|
|
23
|
+
path: str = None
|
|
24
|
+
msg: str = ""
|
|
25
|
+
content: str = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def marker(self) -> str:
|
|
29
|
+
return f"✅" if self.ok else f"❌"
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def file_exists(cls, path) -> "Check":
|
|
33
|
+
ok = os.path.exists(path)
|
|
34
|
+
content = None
|
|
35
|
+
if ok and os.path.isfile(path):
|
|
36
|
+
with open(path, "r") as f:
|
|
37
|
+
content = f.read()
|
|
38
|
+
check = Check(ok, path, msg=path, content=content)
|
|
39
|
+
return check
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CheckProject:
|
|
43
|
+
"""Checker for an individual open source project."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, parent, project, args):
|
|
46
|
+
self.parent = parent
|
|
47
|
+
self.project = project
|
|
48
|
+
self.args = args
|
|
49
|
+
self.checks: List[Check] = []
|
|
50
|
+
self.project_path = project.folder
|
|
51
|
+
self.project_name = None
|
|
52
|
+
self.requires_python = None
|
|
53
|
+
self.min_python_version_minor = None
|
|
54
|
+
self.max_python_version_minor = 13 # python 3.13 is max version
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def total(self) -> int:
|
|
58
|
+
return len(self.checks)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def ok_checks(self) -> List[Check]:
|
|
62
|
+
ok_checks = [check for check in self.checks if check.ok]
|
|
63
|
+
return ok_checks
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def failed_checks(self) -> List[Check]:
|
|
67
|
+
failed_checks = [check for check in self.checks if not check.ok]
|
|
68
|
+
return failed_checks
|
|
69
|
+
|
|
70
|
+
def add_error(self, ex, path: str):
|
|
71
|
+
self.parent.handle_exception(ex)
|
|
72
|
+
self.add_check(False, msg=f"{str(ex)}", path=path)
|
|
73
|
+
|
|
74
|
+
def add_check(
|
|
75
|
+
self, ok, msg: str = "", path: str = None, negative: bool = False
|
|
76
|
+
) -> Check:
|
|
77
|
+
if not path:
|
|
78
|
+
raise ValueError("path parameter missing")
|
|
79
|
+
marker = ""
|
|
80
|
+
if negative:
|
|
81
|
+
ok = not ok
|
|
82
|
+
marker = "⚠ ️"
|
|
83
|
+
check = Check(ok=ok, path=path, msg=f"{marker}{msg}{path}")
|
|
84
|
+
self.checks.append(check)
|
|
85
|
+
return check
|
|
86
|
+
|
|
87
|
+
def add_content_check(
|
|
88
|
+
self, content: str, needle: str, path: str, negative: bool = False
|
|
89
|
+
) -> Check:
|
|
90
|
+
ok = needle in content
|
|
91
|
+
check = self.add_check(ok, msg=f"{needle} in ", path=path, negative=negative)
|
|
92
|
+
return check
|
|
93
|
+
|
|
94
|
+
def add_path_check(self, path) -> Check:
|
|
95
|
+
# Check if path exists
|
|
96
|
+
path_exists = Check.file_exists(path)
|
|
97
|
+
self.checks.append(path_exists)
|
|
98
|
+
return path_exists
|
|
99
|
+
|
|
100
|
+
def generate_badge_markdown(self) -> str:
|
|
101
|
+
"""Generate README.md badge table markup."""
|
|
102
|
+
project_name = self.project_name
|
|
103
|
+
owner = self.project.owner
|
|
104
|
+
project_id = self.project.project_id
|
|
105
|
+
|
|
106
|
+
markup= f"""| | |
|
|
107
|
+
| :--- | :--- |
|
|
108
|
+
| **PyPi** | [](https://pypi.python.org/pypi/{project_name}/) [](https://www.apache.org/licenses/LICENSE-2.0) [](https://pypi.org/project/{project_name}/) [](https://pypi.org/project/{project_name}/) [](https://pypi.org/project/{project_name}/) |
|
|
109
|
+
| **GitHub** | [](https://github.com/{owner}/{project_id}/actions/workflows/build.yml) [](https://github.com/{owner}/{project_id}/releases) [](https://github.com/{owner}/{project_id}/graphs/contributors) [](https://github.com/{owner}/{project_id}/commits/) [](https://github.com/{owner}/{project_id}/issues) [](https://github.com/{owner}/{project_id}/issues/?q=is%3Aissue+is%3Aclosed) |
|
|
110
|
+
| **Code** | [](https://github.com/psf/black) [](https://pycqa.github.io/isort/) |
|
|
111
|
+
| **Docs** | [](https://{owner}.github.io/{project_id}/) [](https://github.com/PyCQA/docformatter) [](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) |"""
|
|
112
|
+
return markup
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_local(self) -> Check:
|
|
117
|
+
local = Check.file_exists(self.project_path)
|
|
118
|
+
return local
|
|
119
|
+
|
|
120
|
+
def check_github_workflows(self):
|
|
121
|
+
"""Check the github workflow files."""
|
|
122
|
+
workflows_path = os.path.join(self.project_path, ".github", "workflows")
|
|
123
|
+
workflows_exist = self.add_path_check(workflows_path)
|
|
124
|
+
|
|
125
|
+
if workflows_exist.ok:
|
|
126
|
+
required_files = ["build.yml", "upload-to-pypi.yml"]
|
|
127
|
+
for file in required_files:
|
|
128
|
+
file_path = os.path.join(workflows_path, file)
|
|
129
|
+
file_exists = self.add_path_check(file_path)
|
|
130
|
+
|
|
131
|
+
if file_exists.ok:
|
|
132
|
+
content = file_exists.content
|
|
133
|
+
|
|
134
|
+
if file == "build.yml":
|
|
135
|
+
min_python_version_minor = int(
|
|
136
|
+
self.requires_python.split(".")[-1]
|
|
137
|
+
)
|
|
138
|
+
self.add_check(
|
|
139
|
+
min_python_version_minor == self.min_python_version_minor,
|
|
140
|
+
msg=f"{min_python_version_minor} (build.yml)!={self.min_python_version_minor} (pyprojec.toml)",
|
|
141
|
+
path=file_path,
|
|
142
|
+
)
|
|
143
|
+
python_versions = f"""python-version: [ {', '.join([f"'3.{i}'" for i in range(self.min_python_version_minor, self.max_python_version_minor+1)])} ]"""
|
|
144
|
+
self.add_content_check(
|
|
145
|
+
content,
|
|
146
|
+
python_versions,
|
|
147
|
+
file_path,
|
|
148
|
+
)
|
|
149
|
+
self.add_content_check(
|
|
150
|
+
content,
|
|
151
|
+
"os: [ubuntu-latest, macos-latest, windows-latest]",
|
|
152
|
+
file_path,
|
|
153
|
+
)
|
|
154
|
+
self.add_content_check(
|
|
155
|
+
content, "uses: actions/checkout@v4", file_path
|
|
156
|
+
)
|
|
157
|
+
self.add_content_check(
|
|
158
|
+
content,
|
|
159
|
+
"uses: actions/setup-python@v5",
|
|
160
|
+
file_path,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.add_content_check(
|
|
164
|
+
content, "sphinx", file_path, negative=True
|
|
165
|
+
)
|
|
166
|
+
scripts_ok = (
|
|
167
|
+
"scripts/install" in content
|
|
168
|
+
and "scripts/test" in content
|
|
169
|
+
or "scripts/installAndTest" in content
|
|
170
|
+
)
|
|
171
|
+
self.add_check(scripts_ok, "install and test", file_path)
|
|
172
|
+
|
|
173
|
+
elif file == "upload-to-pypi.yml":
|
|
174
|
+
self.add_content_check(content, "id-token: write", file_path)
|
|
175
|
+
self.add_content_check(
|
|
176
|
+
content, "uses: actions/checkout@v4", file_path
|
|
177
|
+
)
|
|
178
|
+
self.add_content_check(
|
|
179
|
+
content,
|
|
180
|
+
"uses: actions/setup-python@v5",
|
|
181
|
+
file_path,
|
|
182
|
+
)
|
|
183
|
+
self.add_content_check(
|
|
184
|
+
content,
|
|
185
|
+
"uses: pypa/gh-action-pypi-publish@release/v1",
|
|
186
|
+
file_path,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def check_scripts(self):
|
|
190
|
+
scripts_path = os.path.join(self.project_path, "scripts")
|
|
191
|
+
scripts_exist = self.add_path_check(scripts_path)
|
|
192
|
+
if scripts_exist.ok:
|
|
193
|
+
required_files = ["blackisort", "test", "install", "doc", "release"]
|
|
194
|
+
for file in required_files:
|
|
195
|
+
file_path = os.path.join(scripts_path, file)
|
|
196
|
+
file_exists = self.add_path_check(file_path)
|
|
197
|
+
if file_exists.ok:
|
|
198
|
+
content = file_exists.content
|
|
199
|
+
if file == "doc":
|
|
200
|
+
self.add_content_check(
|
|
201
|
+
content, "sphinx", file_path, negative=True
|
|
202
|
+
)
|
|
203
|
+
self.add_content_check(
|
|
204
|
+
content, "WF 2024-07-30 - updated", file_path
|
|
205
|
+
)
|
|
206
|
+
if file == "test":
|
|
207
|
+
self.add_content_check(content, "WF 2024-08-03", file_path)
|
|
208
|
+
if file == "release":
|
|
209
|
+
self.add_content_check(content, "scripts/doc -d", file_path)
|
|
210
|
+
|
|
211
|
+
def check_readme(self):
|
|
212
|
+
readme_path = os.path.join(self.project_path, "README.md")
|
|
213
|
+
readme_exists = self.add_path_check(readme_path)
|
|
214
|
+
if not hasattr(self, "project_name"):
|
|
215
|
+
self.add_check(
|
|
216
|
+
False,
|
|
217
|
+
"project_name from pyproject.toml needed for README.md check",
|
|
218
|
+
self.project_path,
|
|
219
|
+
)
|
|
220
|
+
return
|
|
221
|
+
if readme_exists.ok:
|
|
222
|
+
readme_content = readme_exists.content
|
|
223
|
+
badge_lines = [
|
|
224
|
+
"[](https://pypi.org/project/{self.project_name}/)",
|
|
225
|
+
"[](https://github.com/{self.project.fqid}/actions/workflows/build.yml)",
|
|
226
|
+
"[](https://pypi.python.org/pypi/{self.project_name}/)",
|
|
227
|
+
"[](https://github.com/{self.project.fqid}/issues)",
|
|
228
|
+
"[](https://github.com/{self.project.fqid}/issues/?q=is%3Aissue+is%3Aclosed)",
|
|
229
|
+
"[](https://{self.project.owner}.github.io/{self.project.project_id}/)",
|
|
230
|
+
"[](https://www.apache.org/licenses/LICENSE-2.0)",
|
|
231
|
+
]
|
|
232
|
+
for line in badge_lines:
|
|
233
|
+
formatted_line = line.format(self=self)
|
|
234
|
+
self.add_content_check(
|
|
235
|
+
content=readme_content,
|
|
236
|
+
needle=formatted_line,
|
|
237
|
+
path=readme_path,
|
|
238
|
+
)
|
|
239
|
+
self.add_content_check(
|
|
240
|
+
readme_content, "readthedocs", readme_path, negative=True
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def check_pyproject_toml(self) -> bool:
|
|
244
|
+
"""pyproject.toml."""
|
|
245
|
+
toml_path = os.path.join(self.project_path, "pyproject.toml")
|
|
246
|
+
toml_exists = self.add_path_check(toml_path)
|
|
247
|
+
if toml_exists.ok:
|
|
248
|
+
content = toml_exists.content
|
|
249
|
+
toml_dict = tomllib.loads(content)
|
|
250
|
+
project_check = self.add_check(
|
|
251
|
+
"project" in toml_dict, "[project]", toml_path
|
|
252
|
+
)
|
|
253
|
+
if project_check.ok:
|
|
254
|
+
self.project_name = toml_dict["project"]["name"]
|
|
255
|
+
requires_python_check = self.add_check(
|
|
256
|
+
"requires-python" in toml_dict["project"],
|
|
257
|
+
"requires-python",
|
|
258
|
+
toml_path,
|
|
259
|
+
)
|
|
260
|
+
if requires_python_check.ok:
|
|
261
|
+
self.requires_python = toml_dict["project"]["requires-python"]
|
|
262
|
+
min_python_version = version.parse(
|
|
263
|
+
self.requires_python.replace(">=", "")
|
|
264
|
+
)
|
|
265
|
+
min_version_needed = "3.9"
|
|
266
|
+
version_ok = min_python_version >= version.parse(min_version_needed)
|
|
267
|
+
self.add_check(
|
|
268
|
+
version_ok, f"requires-python>={min_version_needed}", toml_path
|
|
269
|
+
)
|
|
270
|
+
self.min_python_version_minor = int(
|
|
271
|
+
str(min_python_version).split(".")[-1]
|
|
272
|
+
)
|
|
273
|
+
for minor_version in range(
|
|
274
|
+
self.min_python_version_minor, self.max_python_version_minor + 1
|
|
275
|
+
):
|
|
276
|
+
needle = f"Programming Language :: Python :: 3.{minor_version}"
|
|
277
|
+
self.add_content_check(content, needle, toml_path)
|
|
278
|
+
self.add_content_check(content, "hatchling", toml_path)
|
|
279
|
+
self.add_content_check(
|
|
280
|
+
content, "[tool.hatch.build.targets.wheel.sources]", toml_path
|
|
281
|
+
)
|
|
282
|
+
return toml_exists.ok
|
|
283
|
+
|
|
284
|
+
def check_git(self) -> bool:
|
|
285
|
+
"""Check git repository information using GitHub class.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
bool: True if git owner matches project owner and the repo is not a fork
|
|
289
|
+
"""
|
|
290
|
+
owner_match = False
|
|
291
|
+
is_fork = False
|
|
292
|
+
try:
|
|
293
|
+
local_owner = self.project.owner
|
|
294
|
+
remote_owner = self.project.repo_info["owner"]["login"]
|
|
295
|
+
is_fork = self.project.repo_info["fork"]
|
|
296
|
+
owner_match = local_owner.lower() == remote_owner.lower() and not is_fork
|
|
297
|
+
self.add_check(
|
|
298
|
+
owner_match,
|
|
299
|
+
f"Git owner ({remote_owner}) matches project owner ({local_owner}) and is not a fork",
|
|
300
|
+
self.project_path,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
local_project_id = self.project.project_id
|
|
304
|
+
remote_repo_name = self.project.repo_info["name"]
|
|
305
|
+
repo_match = local_project_id.lower() == remote_repo_name.lower()
|
|
306
|
+
self.add_check(
|
|
307
|
+
repo_match,
|
|
308
|
+
f"Git repo name ({remote_repo_name}) matches project id ({local_project_id})",
|
|
309
|
+
self.project_path,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Check if there are uncommitted changes (this still requires local git access)
|
|
313
|
+
local_repo = Repo(self.project_path)
|
|
314
|
+
self.add_check(
|
|
315
|
+
not local_repo.is_dirty(), "uncomitted changes for", self.project_path
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Check latest GitHub Actions workflow run
|
|
319
|
+
latest_run = GitHubAction.get_latest_workflow_run(self.project)
|
|
320
|
+
if latest_run:
|
|
321
|
+
self.add_check(
|
|
322
|
+
latest_run["conclusion"] == "success",
|
|
323
|
+
f"Latest GitHub Actions run: {latest_run['conclusion']}",
|
|
324
|
+
latest_run["html_url"],
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
self.add_check(
|
|
328
|
+
False,
|
|
329
|
+
"No GitHub Actions runs found",
|
|
330
|
+
self.project.repo.ticketUrl(),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
except InvalidGitRepositoryError:
|
|
334
|
+
self.add_check(False, "Not a valid git repository", self.project_path)
|
|
335
|
+
except NoSuchPathError:
|
|
336
|
+
self.add_check(
|
|
337
|
+
False, "Git repository path does not exist", self.project_path
|
|
338
|
+
)
|
|
339
|
+
except Exception as ex:
|
|
340
|
+
self.add_error(ex, self.project_path)
|
|
341
|
+
|
|
342
|
+
return owner_match and not is_fork
|
|
343
|
+
|
|
344
|
+
def check(self, title: str):
|
|
345
|
+
"""Check the given project and print results."""
|
|
346
|
+
self.check_local()
|
|
347
|
+
self.check_git()
|
|
348
|
+
if self.check_pyproject_toml():
|
|
349
|
+
self.check_github_workflows()
|
|
350
|
+
self.check_readme()
|
|
351
|
+
self.check_scripts()
|
|
352
|
+
|
|
353
|
+
# ok_count=len(ok_checks)
|
|
354
|
+
failed_count = len(self.failed_checks)
|
|
355
|
+
summary = (
|
|
356
|
+
f"❌ {failed_count:2}/{self.total:2}"
|
|
357
|
+
if failed_count > 0
|
|
358
|
+
else f"✅ {self.total:2}/{self.total:2}"
|
|
359
|
+
)
|
|
360
|
+
print(f"{title}{summary}:{self.project}→{self.project.url}")
|
|
361
|
+
if failed_count > 0:
|
|
362
|
+
# Sort checks by path
|
|
363
|
+
sorted_checks = sorted(self.checks, key=lambda c: c.path or "")
|
|
364
|
+
|
|
365
|
+
# Group checks by path
|
|
366
|
+
checks_by_path = {}
|
|
367
|
+
for check in sorted_checks:
|
|
368
|
+
if check.path not in checks_by_path:
|
|
369
|
+
checks_by_path[check.path] = []
|
|
370
|
+
checks_by_path[check.path].append(check)
|
|
371
|
+
|
|
372
|
+
# Display results
|
|
373
|
+
for path, path_checks in checks_by_path.items():
|
|
374
|
+
path_failed = sum(1 for c in path_checks if not c.ok)
|
|
375
|
+
if path_failed > 0 or self.args.debug:
|
|
376
|
+
print(f"❌ {path}: {path_failed}")
|
|
377
|
+
i = 0
|
|
378
|
+
for check in path_checks:
|
|
379
|
+
show = not check.ok or self.args.debug
|
|
380
|
+
if show:
|
|
381
|
+
i += 1
|
|
382
|
+
print(f" {i:3}{check.marker}:{check.msg}")
|
|
383
|
+
|
|
384
|
+
if self.args.editor and path_failed > 0:
|
|
385
|
+
if os.path.isfile(path):
|
|
386
|
+
# @TODO Make editor configurable
|
|
387
|
+
Editor.open(path, default_editor_cmd="/usr/local/bin/atom")
|
|
388
|
+
else:
|
|
389
|
+
Editor.open_filepath(path)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Created on 2024-07-30.
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import traceback
|
|
10
|
+
from argparse import Namespace
|
|
11
|
+
|
|
12
|
+
from osprojects.check_project import CheckProject
|
|
13
|
+
from osprojects.osproject import OsProjects
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CheckOS:
|
|
17
|
+
"""Checker for a set of open source projects."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self, args: Namespace, osprojects: OsProjects, max_python_version_minor=12
|
|
21
|
+
):
|
|
22
|
+
self.args = args
|
|
23
|
+
self.verbose = args.verbose
|
|
24
|
+
self.workspace = args.workspace
|
|
25
|
+
self.osprojects = osprojects
|
|
26
|
+
self.checks = []
|
|
27
|
+
# python 3.12 is max version
|
|
28
|
+
self.max_python_version_minor = max_python_version_minor
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_args(cls, args: Namespace):
|
|
32
|
+
osprojects = OsProjects.from_folder(args.workspace, with_progress=True)
|
|
33
|
+
return cls(args, osprojects)
|
|
34
|
+
|
|
35
|
+
def select_projects(self):
|
|
36
|
+
try:
|
|
37
|
+
if self.args.project:
|
|
38
|
+
if self.args.owners:
|
|
39
|
+
return self.osprojects.select_projects(
|
|
40
|
+
owners=self.args.owners, project_id=self.args.project
|
|
41
|
+
)
|
|
42
|
+
elif self.args.local:
|
|
43
|
+
return self.osprojects.select_projects(
|
|
44
|
+
project_id=self.args.project, local_only=True
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError("--local or --owner needed with --project")
|
|
48
|
+
elif self.args.owners:
|
|
49
|
+
return self.osprojects.select_projects(owners=self.args.owners)
|
|
50
|
+
elif self.args.local:
|
|
51
|
+
return self.osprojects.select_projects(local_only=True)
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"Please provide --owner and --project, or use --local option."
|
|
55
|
+
)
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
print(f"Error: {str(e)}")
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
def filter_projects(self):
|
|
61
|
+
if self.args.language:
|
|
62
|
+
self.osprojects.filter_projects(language=self.args.language)
|
|
63
|
+
if self.args.local:
|
|
64
|
+
self.osprojects.filter_projects(local_only=True)
|
|
65
|
+
|
|
66
|
+
def check_projects(self):
|
|
67
|
+
"""Select, filter, and check all projects based on the provided
|
|
68
|
+
arguments."""
|
|
69
|
+
self.select_projects()
|
|
70
|
+
self.filter_projects()
|
|
71
|
+
|
|
72
|
+
for i, (_url, project) in enumerate(
|
|
73
|
+
self.osprojects.selected_projects.items(), 1
|
|
74
|
+
):
|
|
75
|
+
checker = CheckProject(self, project, self.args)
|
|
76
|
+
checker.check(f"{i:3}:")
|
|
77
|
+
if self.args.badges:
|
|
78
|
+
print(checker.generate_badge_markdown())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def handle_exception(self, ex: Exception):
|
|
82
|
+
CheckOS.show_exception(ex, self.args.debug)
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def show_exception(ex: Exception, debug: bool = False):
|
|
86
|
+
err_msg = f"Error: {str(ex)}"
|
|
87
|
+
logging.error(err_msg)
|
|
88
|
+
if debug:
|
|
89
|
+
print(traceback.format_exc())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main(_argv=None):
|
|
93
|
+
"""Main command line entry point."""
|
|
94
|
+
parser = argparse.ArgumentParser(description="Check open source projects")
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"-d",
|
|
97
|
+
"--debug",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="add debug output",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"-b",
|
|
103
|
+
"--badges",
|
|
104
|
+
action="store_true",
|
|
105
|
+
help="create and output standard README.md badges markup",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"-e",
|
|
109
|
+
"--editor",
|
|
110
|
+
action="store_true",
|
|
111
|
+
help="open default editor on failed files",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument("-o", "--owners", nargs="+", help="project owners")
|
|
114
|
+
parser.add_argument("-p", "--project", help="name of the project")
|
|
115
|
+
parser.add_argument("-l", "--language", help="filter projects by language")
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--local", action="store_true", help="check only locally available projects"
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"-v", "--verbose", action="store_true", help="show verbose output"
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"-ws",
|
|
124
|
+
"--workspace",
|
|
125
|
+
help="(Eclipse) workspace directory",
|
|
126
|
+
default=os.path.expanduser("~/py-workspace"),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
args = parser.parse_args(args=_argv)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
checker = CheckOS.from_args(args)
|
|
133
|
+
checker.check_projects()
|
|
134
|
+
except Exception as ex:
|
|
135
|
+
CheckOS.show_exception(ex, debug=args.debug)
|
|
136
|
+
raise ex
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Created on 2022-11-27
|
|
1
|
+
"""Created on 2022-11-27.
|
|
3
2
|
|
|
4
3
|
@author: wf
|
|
5
4
|
"""
|
|
@@ -15,10 +14,10 @@ from bs4 import BeautifulSoup
|
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class Editor:
|
|
18
|
-
"""
|
|
19
|
-
helper class to open the system defined editor
|
|
17
|
+
"""Helper class to open the system defined editor.
|
|
20
18
|
|
|
21
|
-
see
|
|
19
|
+
see
|
|
20
|
+
https://stackoverflow.com/questions/1442841/lauch-default-editor-like-webbrowser-module
|
|
22
21
|
"""
|
|
23
22
|
|
|
24
23
|
@classmethod
|
|
@@ -32,8 +31,7 @@ class Editor:
|
|
|
32
31
|
|
|
33
32
|
@classmethod
|
|
34
33
|
def extract_text(cls, html_text: str) -> str:
|
|
35
|
-
"""
|
|
36
|
-
extract the text from the given html_text
|
|
34
|
+
"""Extract the text from the given html_text.
|
|
37
35
|
|
|
38
36
|
Args:
|
|
39
37
|
html_text(str): the input for the html text
|
|
@@ -65,8 +63,7 @@ class Editor:
|
|
|
65
63
|
extract_text: bool = True,
|
|
66
64
|
default_editor_cmd: str = "/usr/local/bin/atom",
|
|
67
65
|
) -> str:
|
|
68
|
-
"""
|
|
69
|
-
open an editor for the given file_source
|
|
66
|
+
"""Open an editor for the given file_source.
|
|
70
67
|
|
|
71
68
|
Args:
|
|
72
69
|
file_source(str): the path to the file
|
|
@@ -104,8 +101,7 @@ class Editor:
|
|
|
104
101
|
|
|
105
102
|
@classmethod
|
|
106
103
|
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
|
|
104
|
+
"""Open an editor for the given text in a newly created temporary file.
|
|
109
105
|
|
|
110
106
|
Args:
|
|
111
107
|
text(str): the text to write to a temporary file and then open
|