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.
Files changed (34) hide show
  1. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.github/workflows/build.yml +2 -2
  2. pyopensourceprojects-0.4.0/PKG-INFO +44 -0
  3. pyopensourceprojects-0.4.0/README.md +17 -0
  4. pyopensourceprojects-0.4.0/osprojects/__init__.py +1 -0
  5. pyopensourceprojects-0.4.0/osprojects/check_project.py +389 -0
  6. pyopensourceprojects-0.4.0/osprojects/checkos.py +140 -0
  7. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/osprojects/editor.py +7 -11
  8. pyopensourceprojects-0.4.0/osprojects/github_api.py +338 -0
  9. pyopensourceprojects-0.4.0/osprojects/osproject.py +544 -0
  10. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/pyproject.toml +13 -7
  11. pyopensourceprojects-0.4.0/scripts/blackisort +15 -0
  12. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/tests/basetest.py +14 -21
  13. pyopensourceprojects-0.4.0/tests/test_github.py +111 -0
  14. pyopensourceprojects-0.4.0/tests/test_github_api.py +81 -0
  15. pyopensourceprojects-0.4.0/tests/test_osproject.py +92 -0
  16. pyopensourceprojects-0.2.3/PKG-INFO +0 -43
  17. pyopensourceprojects-0.2.3/README.md +0 -18
  18. pyopensourceprojects-0.2.3/osprojects/__init__.py +0 -1
  19. pyopensourceprojects-0.2.3/osprojects/checkos.py +0 -397
  20. pyopensourceprojects-0.2.3/osprojects/osproject.py +0 -564
  21. pyopensourceprojects-0.2.3/scripts/blackisort +0 -7
  22. pyopensourceprojects-0.2.3/tests/test_osproject.py +0 -193
  23. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.github/workflows/upload-to-pypi.yml +0 -0
  24. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.gitignore +0 -0
  25. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.project +0 -0
  26. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/.pydevproject +0 -0
  27. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/LICENSE +0 -0
  28. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/mkdocs.yml +0 -0
  29. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/doc +0 -0
  30. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/install +0 -0
  31. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/installAndTest +0 -0
  32. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/release +0 -0
  33. {pyopensourceprojects-0.2.3 → pyopensourceprojects-0.4.0}/scripts/test +0 -0
  34. {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.9', '3.10', '3.11', '3.12' ]
24
- python-version: ["3.10"]
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** | [![PyPI Status](https://img.shields.io/pypi/v/pyOpenSourceProjects.svg)](https://pypi.python.org/pypi/pyOpenSourceProjects/) [![License](https://img.shields.io/github/license/WolfgangFahl/pyOpenSourceProjects.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![pypi](https://img.shields.io/pypi/pyversions/pyOpenSourceProjects)](https://pypi.org/project/pyOpenSourceProjects/) [![format](https://img.shields.io/pypi/format/pyOpenSourceProjects)](https://pypi.org/project/pyOpenSourceProjects/) [![downloads](https://img.shields.io/pypi/dd/pyOpenSourceProjects)](https://pypi.org/project/pyOpenSourceProjects/) |
35
+ | **GitHub** | [![Github Actions Build](https://github.com/WolfgangFahl/pyOpenSourceProjects/actions/workflows/build.yml/badge.svg)](https://github.com/WolfgangFahl/pyOpenSourceProjects/actions/workflows/build.yml) [![Release](https://img.shields.io/github/v/release/WolfgangFahl/pyOpenSourceProjects)](https://github.com/WolfgangFahl/pyOpenSourceProjects/releases) [![Contributors](https://img.shields.io/github/contributors/WolfgangFahl/pyOpenSourceProjects)](https://github.com/WolfgangFahl/pyOpenSourceProjects/graphs/contributors) [![Last Commit](https://img.shields.io/github/last-commit/WolfgangFahl/pyOpenSourceProjects)](https://github.com/WolfgangFahl/pyOpenSourceProjects/commits/) [![GitHub issues](https://img.shields.io/github/issues/WolfgangFahl/pyOpenSourceProjects.svg)](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues) [![GitHub closed issues](https://img.shields.io/github/issues-closed/WolfgangFahl/pyOpenSourceProjects.svg)](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues/?q=is%3Aissue+is%3Aclosed) |
36
+ | **Code** | [![style-black](https://img.shields.io/badge/%20style-black-000000.svg)](https://github.com/psf/black) [![imports-isort](https://img.shields.io/badge/%20imports-isort-%231674b1)](https://pycqa.github.io/isort/) |
37
+ | **Docs** | [![API Docs](https://img.shields.io/badge/API-Documentation-blue)](https://WolfgangFahl.github.io/pyOpenSourceProjects/) [![formatter-docformatter](https://img.shields.io/badge/%20formatter-docformatter-fedcba.svg)](https://github.com/PyCQA/docformatter) [![style-google](https://img.shields.io/badge/%20style-google-3666d6.svg)](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** | [![PyPI Status](https://img.shields.io/pypi/v/pyOpenSourceProjects.svg)](https://pypi.python.org/pypi/pyOpenSourceProjects/) [![License](https://img.shields.io/github/license/WolfgangFahl/pyOpenSourceProjects.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![pypi](https://img.shields.io/pypi/pyversions/pyOpenSourceProjects)](https://pypi.org/project/pyOpenSourceProjects/) [![format](https://img.shields.io/pypi/format/pyOpenSourceProjects)](https://pypi.org/project/pyOpenSourceProjects/) [![downloads](https://img.shields.io/pypi/dd/pyOpenSourceProjects)](https://pypi.org/project/pyOpenSourceProjects/) |
8
+ | **GitHub** | [![Github Actions Build](https://github.com/WolfgangFahl/pyOpenSourceProjects/actions/workflows/build.yml/badge.svg)](https://github.com/WolfgangFahl/pyOpenSourceProjects/actions/workflows/build.yml) [![Release](https://img.shields.io/github/v/release/WolfgangFahl/pyOpenSourceProjects)](https://github.com/WolfgangFahl/pyOpenSourceProjects/releases) [![Contributors](https://img.shields.io/github/contributors/WolfgangFahl/pyOpenSourceProjects)](https://github.com/WolfgangFahl/pyOpenSourceProjects/graphs/contributors) [![Last Commit](https://img.shields.io/github/last-commit/WolfgangFahl/pyOpenSourceProjects)](https://github.com/WolfgangFahl/pyOpenSourceProjects/commits/) [![GitHub issues](https://img.shields.io/github/issues/WolfgangFahl/pyOpenSourceProjects.svg)](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues) [![GitHub closed issues](https://img.shields.io/github/issues-closed/WolfgangFahl/pyOpenSourceProjects.svg)](https://github.com/WolfgangFahl/pyOpenSourceProjects/issues/?q=is%3Aissue+is%3Aclosed) |
9
+ | **Code** | [![style-black](https://img.shields.io/badge/%20style-black-000000.svg)](https://github.com/psf/black) [![imports-isort](https://img.shields.io/badge/%20imports-isort-%231674b1)](https://pycqa.github.io/isort/) |
10
+ | **Docs** | [![API Docs](https://img.shields.io/badge/API-Documentation-blue)](https://WolfgangFahl.github.io/pyOpenSourceProjects/) [![formatter-docformatter](https://img.shields.io/badge/%20formatter-docformatter-fedcba.svg)](https://github.com/PyCQA/docformatter) [![style-google](https://img.shields.io/badge/%20style-google-3666d6.svg)](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** | [![PyPI Status](https://img.shields.io/pypi/v/{project_name}.svg)](https://pypi.python.org/pypi/{project_name}/) [![License](https://img.shields.io/github/license/{owner}/{project_id}.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![pypi](https://img.shields.io/pypi/pyversions/{project_name})](https://pypi.org/project/{project_name}/) [![format](https://img.shields.io/pypi/format/{project_name})](https://pypi.org/project/{project_name}/) [![downloads](https://img.shields.io/pypi/dd/{project_name})](https://pypi.org/project/{project_name}/) |
109
+ | **GitHub** | [![Github Actions Build](https://github.com/{owner}/{project_id}/actions/workflows/build.yml/badge.svg)](https://github.com/{owner}/{project_id}/actions/workflows/build.yml) [![Release](https://img.shields.io/github/v/release/{owner}/{project_id})](https://github.com/{owner}/{project_id}/releases) [![Contributors](https://img.shields.io/github/contributors/{owner}/{project_id})](https://github.com/{owner}/{project_id}/graphs/contributors) [![Last Commit](https://img.shields.io/github/last-commit/{owner}/{project_id})](https://github.com/{owner}/{project_id}/commits/) [![GitHub issues](https://img.shields.io/github/issues/{owner}/{project_id}.svg)](https://github.com/{owner}/{project_id}/issues) [![GitHub closed issues](https://img.shields.io/github/issues-closed/{owner}/{project_id}.svg)](https://github.com/{owner}/{project_id}/issues/?q=is%3Aissue+is%3Aclosed) |
110
+ | **Code** | [![style-black](https://img.shields.io/badge/%20style-black-000000.svg)](https://github.com/psf/black) [![imports-isort](https://img.shields.io/badge/%20imports-isort-%231674b1)](https://pycqa.github.io/isort/) |
111
+ | **Docs** | [![API Docs](https://img.shields.io/badge/API-Documentation-blue)](https://{owner}.github.io/{project_id}/) [![formatter-docformatter](https://img.shields.io/badge/%20formatter-docformatter-fedcba.svg)](https://github.com/PyCQA/docformatter) [![style-google](https://img.shields.io/badge/%20style-google-3666d6.svg)](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
+ "[![pypi](https://img.shields.io/pypi/pyversions/{self.project_name})](https://pypi.org/project/{self.project_name}/)",
225
+ "[![Github Actions Build](https://github.com/{self.project.fqid}/actions/workflows/build.yml/badge.svg)](https://github.com/{self.project.fqid}/actions/workflows/build.yml)",
226
+ "[![PyPI Status](https://img.shields.io/pypi/v/{self.project_name}.svg)](https://pypi.python.org/pypi/{self.project_name}/)",
227
+ "[![GitHub issues](https://img.shields.io/github/issues/{self.project.fqid}.svg)](https://github.com/{self.project.fqid}/issues)",
228
+ "[![GitHub closed issues](https://img.shields.io/github/issues-closed/{self.project.fqid}.svg)](https://github.com/{self.project.fqid}/issues/?q=is%3Aissue+is%3Aclosed)",
229
+ "[![API Docs](https://img.shields.io/badge/API-Documentation-blue)](https://{self.project.owner}.github.io/{self.project.project_id}/)",
230
+ "[![License](https://img.shields.io/github/license/{self.project.fqid}.svg)](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 https://stackoverflow.com/questions/1442841/lauch-default-editor-like-webbrowser-module
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