osa_tool 0.2.2__tar.gz → 0.2.3__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.
- {osa_tool-0.2.2 → osa_tool-0.2.3}/PKG-INFO +6 -5
- {osa_tool-0.2.2 → osa_tool-0.2.3}/README.md +1 -1
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/aboutgen/about_generator.py +5 -124
- osa_tool-0.2.3/osa_tool/analytics/metadata.py +361 -0
- osa_tool-0.2.3/osa_tool/analytics/prompt_builder.py +66 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/report_generator.py +28 -13
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/report_maker.py +4 -8
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/run_report_gen.py +10 -4
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/arguments.yaml +11 -6
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/config.toml +2 -2
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompts.toml +248 -77
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompts_article.toml +56 -32
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings.py +2 -3
- osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/autopep8.yml +68 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/black.yml +9 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/fix_pep8.yml +30 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/pep8.yml +18 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/pypi_publish.yml +21 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/slash_command_dispatch.yml +17 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/unit_test.yml +24 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/autopep8.yml +7 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/black.yml +7 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/fix_pep8.yml +8 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/pep8.yml +7 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/pypi_publish.yml +7 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/slash_command_dispatch.yml +6 -0
- osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/unit_test.yml +11 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/community.toml +147 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/community.py +58 -12
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/contributing.py +4 -4
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/docs_run.py +9 -3
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/license.py +4 -3
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/git_agent/git_agent.py +509 -489
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/osatreesitter/docgen.py +11 -0
- osa_tool-0.2.3/osa_tool/readmegen/context/citation_extractor.py +64 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/base_builder.py +20 -39
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/builder.py +6 -5
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/builder_article.py +10 -12
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/header.py +3 -3
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/installation.py +3 -3
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/models/llm_service.py +60 -49
- osa_tool-0.2.3/osa_tool/readmegen/postprocessor/response_cleaner.py +87 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/prompts/prompts_builder.py +121 -43
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/readme_core.py +11 -8
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/run.py +34 -25
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/scheduler/scheduler.py +18 -27
- osa_tool-0.2.3/osa_tool/scheduler/workflow_manager.py +323 -0
- osa_tool-0.2.3/osa_tool/translation/readme_translator.py +134 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/ui/plan_editor.py +3 -3
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/utils.py +3 -0
- osa_tool-0.2.3/osa_tool/workflow/README.md +58 -0
- osa_tool-0.2.3/osa_tool/workflow/__init__.py +0 -0
- osa_tool-0.2.3/osa_tool/workflow/workflow_generator.py +770 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/pyproject.toml +1 -1
- osa_tool-0.2.2/osa_tool/analytics/metadata.py +0 -294
- osa_tool-0.2.2/osa_tool/analytics/prompt_builder.py +0 -65
- osa_tool-0.2.2/osa_tool/github_workflow/README.md +0 -334
- osa_tool-0.2.2/osa_tool/github_workflow/__init__.py +0 -15
- osa_tool-0.2.2/osa_tool/github_workflow/generator.py +0 -496
- osa_tool-0.2.2/osa_tool/github_workflow/providers/black.py +0 -69
- osa_tool-0.2.2/osa_tool/github_workflow/providers/unit_test.py +0 -81
- osa_tool-0.2.2/osa_tool/readmegen/postprocessor/readme_refiner.py +0 -76
- osa_tool-0.2.2/osa_tool/readmegen/postprocessor/response_cleaner.py +0 -38
- osa_tool-0.2.2/osa_tool/scheduler/workflow_manager.py +0 -158
- {osa_tool-0.2.2 → osa_tool-0.2.3}/LICENSE +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/aboutgen/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/aboutgen/prompts_about_config.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/sourcerank.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/arguments_parser.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompt_for_analysis.toml +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompt_for_scheduler.toml +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompts_about_section.toml +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/templates/template.toml +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/convertion/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/convertion/notebook_converter.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/images/osa_logo.PNG +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/images/osa_logo_transparent.PNG +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/ci_config.toml +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/contributing.toml +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/licenses.toml +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/mkdocs.yml +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/git_agent/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/models/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/models/models.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/organization/repo_organizer.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/osatreesitter/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/osatreesitter/osa_treesitter.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/article_content.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/article_path.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/dependencies.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/files_contents.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/pypi_status_checker.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/icons/shieldsio_icons.json +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/models/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/postprocessor/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/prompts/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/utils.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/scheduler/README.md +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/scheduler/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/scheduler/prompts.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/translation/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/translation/dir_translator.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/ui/__init__.py +0 -0
- {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/ui/web_plan_editor.py +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: osa_tool
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Tool that just makes your open source project better!
|
|
5
|
-
License: BSD-3-Clause
|
|
5
|
+
License-Expression: BSD-3-Clause
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: ITMO-NSS-team
|
|
7
8
|
Author-email: itmo.nss.team@gmail.com
|
|
8
9
|
Requires-Python: >=3.10,<4.0
|
|
9
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
16
|
Requires-Dist: ProtoLLM (>=0.1.5)
|
|
16
17
|
Requires-Dist: PyYAML (==6.0.2)
|
|
17
18
|
Requires-Dist: Pygments (==2.19.1)
|
|
@@ -271,7 +272,7 @@ Docker, ensure that you upload the PDF file to the OSA folder before building th
|
|
|
271
272
|
/app/OSA/... or just use volume mounting to access the file.
|
|
272
273
|
|
|
273
274
|
The --generate-workflows option is intended to create customizable CI/CD pipelines for Python repositories. For detailed
|
|
274
|
-
documentation, see the [
|
|
275
|
+
documentation, see the [Workflow Generator README](./osa_tool/workflow/README.md).
|
|
275
276
|
|
|
276
277
|
### Configuration
|
|
277
278
|
|
|
@@ -185,7 +185,7 @@ Docker, ensure that you upload the PDF file to the OSA folder before building th
|
|
|
185
185
|
/app/OSA/... or just use volume mounting to access the file.
|
|
186
186
|
|
|
187
187
|
The --generate-workflows option is intended to create customizable CI/CD pipelines for Python repositories. For detailed
|
|
188
|
-
documentation, see the [
|
|
188
|
+
documentation, see the [Workflow Generator README](./osa_tool/workflow/README.md).
|
|
189
189
|
|
|
190
190
|
### Configuration
|
|
191
191
|
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
-
import time
|
|
4
3
|
from typing import List
|
|
5
4
|
|
|
6
|
-
import requests
|
|
7
|
-
|
|
8
5
|
from osa_tool.aboutgen.prompts_about_config import PromptAboutLoader
|
|
9
|
-
from osa_tool.analytics.metadata import detect_platform, load_data_metadata
|
|
10
6
|
from osa_tool.config.settings import ConfigLoader
|
|
7
|
+
from osa_tool.git_agent.git_agent import GitAgent
|
|
11
8
|
from osa_tool.models.models import ModelHandler, ModelHandlerFactory
|
|
12
9
|
from osa_tool.utils import extract_readme_content, logger, parse_folder_name
|
|
13
10
|
|
|
@@ -26,15 +23,15 @@ HOMEPAGE_KEYS = [
|
|
|
26
23
|
class AboutGenerator:
|
|
27
24
|
"""Generates Git repository About section content."""
|
|
28
25
|
|
|
29
|
-
def __init__(self, config_loader: ConfigLoader):
|
|
26
|
+
def __init__(self, config_loader: ConfigLoader, git_agent: GitAgent):
|
|
30
27
|
self.config = config_loader.config
|
|
31
28
|
self.model_handler: ModelHandler = ModelHandlerFactory.build(self.config)
|
|
32
29
|
self.repo_url = self.config.git.repository
|
|
33
|
-
self.metadata =
|
|
30
|
+
self.metadata = git_agent.metadata
|
|
34
31
|
self.base_path = os.path.join(os.getcwd(), parse_folder_name(self.repo_url))
|
|
35
32
|
self.readme_content = extract_readme_content(self.base_path)
|
|
36
|
-
self.platform = detect_platform(self.repo_url)
|
|
37
33
|
self.prompts = PromptAboutLoader().prompts
|
|
34
|
+
self.validate_topics = git_agent.validate_topics
|
|
38
35
|
|
|
39
36
|
self._content: dict | None = None
|
|
40
37
|
|
|
@@ -136,128 +133,12 @@ class AboutGenerator:
|
|
|
136
133
|
response = self.model_handler.send_request(formatted_prompt)
|
|
137
134
|
topics = [topic.strip().lower().replace(" ", "-") for topic in response.split(",") if topic.strip()]
|
|
138
135
|
logger.debug(f"Generated topics from LLM: {topics}")
|
|
139
|
-
validated_topics = self.
|
|
136
|
+
validated_topics = self.validate_topics(topics)
|
|
140
137
|
return list({*existing_topics, *validated_topics})
|
|
141
138
|
except Exception as e:
|
|
142
139
|
logger.error(f"Error generating topics: {e}")
|
|
143
140
|
return []
|
|
144
141
|
|
|
145
|
-
def _validate_topics(self, topics: List[str]) -> List[str]:
|
|
146
|
-
"""Validates topics against platform-specific APIs.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
topics (List[str]): List of potential topics to validate
|
|
150
|
-
|
|
151
|
-
Returns:
|
|
152
|
-
List[str]: List of validated topics that exist on platform
|
|
153
|
-
"""
|
|
154
|
-
if self.platform == "github":
|
|
155
|
-
return self._validate_github_topics(topics)
|
|
156
|
-
elif self.platform == "gitlab":
|
|
157
|
-
return self._validate_gitlab_topics(topics)
|
|
158
|
-
elif self.platform == "gitverse":
|
|
159
|
-
return self._validate_gitverse_topics(topics)
|
|
160
|
-
else:
|
|
161
|
-
logger.warning(f"Topic validation not implemented for platform: {self.platform}")
|
|
162
|
-
return topics
|
|
163
|
-
|
|
164
|
-
@staticmethod
|
|
165
|
-
def _validate_github_topics(topics: List[str]) -> List[str]:
|
|
166
|
-
"""Validates topics against GitHub Topics API.
|
|
167
|
-
|
|
168
|
-
Args:
|
|
169
|
-
topics (List[str]): List of potential topics to validate
|
|
170
|
-
|
|
171
|
-
Returns:
|
|
172
|
-
List[str]: List of validated topics that exist on GitHub
|
|
173
|
-
"""
|
|
174
|
-
logger.info("Validating topics against GitHub Topics API...")
|
|
175
|
-
min_repo = 5
|
|
176
|
-
validated_topics = []
|
|
177
|
-
|
|
178
|
-
for topic in topics:
|
|
179
|
-
try:
|
|
180
|
-
response = requests.get(
|
|
181
|
-
f"https://api.github.com/search/topics?q={topic}+repositories:>{min_repo}",
|
|
182
|
-
headers={"Accept": "application/vnd.github.v3+json"},
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
if response.status_code == 200:
|
|
186
|
-
data = response.json()
|
|
187
|
-
if (total := data.get("total_count", 0)) > 0:
|
|
188
|
-
if total == 1:
|
|
189
|
-
valid_topic = data.get("items")[0].get("name")
|
|
190
|
-
logger.debug(f"Applied transformation for topic: '{topic} -> {valid_topic}'")
|
|
191
|
-
else:
|
|
192
|
-
valid_topic = topic
|
|
193
|
-
validated_topics.append(valid_topic)
|
|
194
|
-
else:
|
|
195
|
-
logger.debug(f"Generated topic '{topic}' is not valid, skipping")
|
|
196
|
-
elif response.status_code == 403:
|
|
197
|
-
logger.warning("Rate limit exceeded, waiting 60 seconds")
|
|
198
|
-
time.sleep(60)
|
|
199
|
-
|
|
200
|
-
time.sleep(1)
|
|
201
|
-
|
|
202
|
-
except Exception as e:
|
|
203
|
-
logger.error(f"Error validating topic '{topic}': {e}")
|
|
204
|
-
continue
|
|
205
|
-
|
|
206
|
-
logger.info(f"Validated {len(validated_topics)} topics out of {len(topics)}.")
|
|
207
|
-
return validated_topics
|
|
208
|
-
|
|
209
|
-
@staticmethod
|
|
210
|
-
def _validate_gitlab_topics(topics: List[str]) -> List[str]:
|
|
211
|
-
"""Validates topics against GitLab Topics API.
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
topics (List[str]): List of potential topics to validate
|
|
215
|
-
|
|
216
|
-
Returns:
|
|
217
|
-
List[str]: List of validated topics that exist on GitLab
|
|
218
|
-
"""
|
|
219
|
-
logger.info("Validating topics against GitLab Topics API...")
|
|
220
|
-
validated_topics = []
|
|
221
|
-
base_url = "https://gitlab.com/api/v4/topics"
|
|
222
|
-
headers = {"Accept": "application/json"}
|
|
223
|
-
|
|
224
|
-
for topic in topics:
|
|
225
|
-
try:
|
|
226
|
-
params = {"search": topic}
|
|
227
|
-
response = requests.get(base_url, headers=headers, params=params)
|
|
228
|
-
if response.status_code == 200:
|
|
229
|
-
data = response.json()
|
|
230
|
-
for entry in data:
|
|
231
|
-
if entry.get("name") == topic:
|
|
232
|
-
validated_topics.append(topic)
|
|
233
|
-
logger.debug(f"Validated GitLab topic: {topic}")
|
|
234
|
-
break
|
|
235
|
-
else:
|
|
236
|
-
logger.debug(f"Topic '{topic}' not found on GitLab, skipping")
|
|
237
|
-
elif response.status_code == 403:
|
|
238
|
-
logger.warning("Rate limit exceeded, waiting 60 seconds")
|
|
239
|
-
time.sleep(60)
|
|
240
|
-
time.sleep(1)
|
|
241
|
-
except Exception as e:
|
|
242
|
-
logger.error(f"Error validating topic '{topic}': {e}")
|
|
243
|
-
continue
|
|
244
|
-
|
|
245
|
-
logger.info(f"Validated {len(validated_topics)} topics out of {len(topics)}.")
|
|
246
|
-
return validated_topics
|
|
247
|
-
|
|
248
|
-
@staticmethod
|
|
249
|
-
def _validate_gitverse_topics(topics: List[str]) -> List[str]:
|
|
250
|
-
"""Validates topics for Gitverse platform.
|
|
251
|
-
|
|
252
|
-
Args:
|
|
253
|
-
topics (List[str]): List of potential topics to validate
|
|
254
|
-
|
|
255
|
-
Returns:
|
|
256
|
-
List[str]: List of topics (no validation performed)
|
|
257
|
-
"""
|
|
258
|
-
logger.warning("Topic validation is not implemented for Gitverse. Returning original topics list.")
|
|
259
|
-
return topics
|
|
260
|
-
|
|
261
142
|
def detect_homepage(self) -> str:
|
|
262
143
|
"""
|
|
263
144
|
Detects the homepage URL for a project.
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from requests import HTTPError
|
|
10
|
+
|
|
11
|
+
from osa_tool.utils import get_base_repo_url, logger
|
|
12
|
+
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RepositoryMetadata:
|
|
18
|
+
"""
|
|
19
|
+
Dataclass to store Git repository metadata.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
full_name: str
|
|
24
|
+
owner: str
|
|
25
|
+
owner_url: str | None
|
|
26
|
+
description: str | None
|
|
27
|
+
|
|
28
|
+
# Repository statistics
|
|
29
|
+
stars_count: int
|
|
30
|
+
forks_count: int
|
|
31
|
+
watchers_count: int
|
|
32
|
+
open_issues_count: int
|
|
33
|
+
|
|
34
|
+
# Repository details
|
|
35
|
+
default_branch: str
|
|
36
|
+
created_at: str
|
|
37
|
+
updated_at: str
|
|
38
|
+
pushed_at: str
|
|
39
|
+
size_kb: int
|
|
40
|
+
|
|
41
|
+
# Repository URLs
|
|
42
|
+
clone_url_http: str
|
|
43
|
+
clone_url_ssh: str
|
|
44
|
+
contributors_url: str | None
|
|
45
|
+
languages_url: str
|
|
46
|
+
issues_url: str | None
|
|
47
|
+
|
|
48
|
+
# Programming languages and topics
|
|
49
|
+
language: str | None
|
|
50
|
+
languages: list[str]
|
|
51
|
+
topics: list[str]
|
|
52
|
+
|
|
53
|
+
# Additional repository settings
|
|
54
|
+
has_wiki: bool
|
|
55
|
+
has_issues: bool
|
|
56
|
+
has_projects: bool
|
|
57
|
+
is_private: bool
|
|
58
|
+
homepage_url: str | None
|
|
59
|
+
|
|
60
|
+
# License information
|
|
61
|
+
license_name: str | None
|
|
62
|
+
license_url: str | None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MetadataLoader(ABC):
|
|
66
|
+
"""
|
|
67
|
+
Abstract base class for repository metadata loaders.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def load_data(cls, repo_url: str) -> RepositoryMetadata:
|
|
72
|
+
"""
|
|
73
|
+
General method to load repository metadata for a given URL.
|
|
74
|
+
Calls the platform-specific loader method.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
repo_url (str): The full URL of the repository.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
RepositoryMetadata: Parsed repository metadata.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
return cls._load_platform_data(repo_url)
|
|
84
|
+
|
|
85
|
+
except HTTPError as http_exc:
|
|
86
|
+
status_code = getattr(http_exc.response, "status_code", None)
|
|
87
|
+
logger.error(f"Error while fetching repository metadata: {http_exc}")
|
|
88
|
+
|
|
89
|
+
if status_code == 401:
|
|
90
|
+
logger.error("Authentication failed: please check your Git token (missing or expired).")
|
|
91
|
+
elif status_code == 404:
|
|
92
|
+
logger.error("Repository not found: please check the repository URL.")
|
|
93
|
+
elif status_code == 403:
|
|
94
|
+
logger.error("Access denied: your token may lack sufficient permissions or you hit a rate limit.")
|
|
95
|
+
else:
|
|
96
|
+
logger.error("Unexpected HTTP error occurred while accessing the repository metadata.")
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
logger.error(f"Unexpected error while fetching repository metadata: {exc}")
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def _load_platform_data(cls, repo_url: str) -> RepositoryMetadata:
|
|
106
|
+
"""
|
|
107
|
+
Abstract method to load metadata from a platform-specific API.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
repo_url (str): The full URL of the repository.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
RepositoryMetadata: Parsed repository metadata.
|
|
114
|
+
"""
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
@abstractmethod
|
|
119
|
+
def _parse_metadata(cls, repo_data: dict) -> RepositoryMetadata:
|
|
120
|
+
"""
|
|
121
|
+
Abstract method to parse raw API response dictionary into RepositoryMetadata.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
repo_data (dict): Raw API response data.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
RepositoryMetadata: Parsed repository metadata.
|
|
128
|
+
"""
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class GitHubMetadataLoader(MetadataLoader):
|
|
133
|
+
@classmethod
|
|
134
|
+
def _load_platform_data(cls, repo_url: str) -> RepositoryMetadata:
|
|
135
|
+
"""
|
|
136
|
+
Load GitHub repository metadata via GitHub API.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
repo_url (str): URL of the GitHub repository.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
RepositoryMetadata: Parsed metadata object.
|
|
143
|
+
"""
|
|
144
|
+
base_url = get_base_repo_url(repo_url)
|
|
145
|
+
headers = {
|
|
146
|
+
"Authorization": f"token {os.getenv('GIT_TOKEN', os.getenv('GITHUB_TOKEN', ''))}",
|
|
147
|
+
"Accept": "application/vnd.github.v3+json",
|
|
148
|
+
}
|
|
149
|
+
url = f"https://api.github.com/repos/{base_url}"
|
|
150
|
+
response = requests.get(url=url, headers=headers)
|
|
151
|
+
response.raise_for_status()
|
|
152
|
+
data = response.json()
|
|
153
|
+
logger.info(f"Successfully fetched GitHub metadata for repository: '{base_url}'")
|
|
154
|
+
return GitHubMetadataLoader._parse_metadata(data)
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def _parse_metadata(cls, repo_data: dict) -> RepositoryMetadata:
|
|
158
|
+
"""
|
|
159
|
+
Parse GitHub API response into RepositoryMetadata.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
repo_data (dict): Raw GitHub API response.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
RepositoryMetadata: Parsed repository metadata.
|
|
166
|
+
"""
|
|
167
|
+
languages = repo_data.get("languages", {})
|
|
168
|
+
license_info = repo_data.get("license", {}) or {}
|
|
169
|
+
owner_info = repo_data.get("owner", {}) or {}
|
|
170
|
+
|
|
171
|
+
return RepositoryMetadata(
|
|
172
|
+
name=repo_data.get("name", ""),
|
|
173
|
+
full_name=repo_data.get("full_name", ""),
|
|
174
|
+
owner=owner_info.get("login", ""),
|
|
175
|
+
owner_url=owner_info.get("html_url", ""),
|
|
176
|
+
description=repo_data.get("description", ""),
|
|
177
|
+
stars_count=repo_data.get("stargazers_count", 0),
|
|
178
|
+
forks_count=repo_data.get("forks_count", 0),
|
|
179
|
+
watchers_count=repo_data.get("watchers_count", 0),
|
|
180
|
+
open_issues_count=repo_data.get("open_issues_count", 0),
|
|
181
|
+
default_branch=repo_data.get("default_branch", ""),
|
|
182
|
+
created_at=repo_data.get("created_at", ""),
|
|
183
|
+
updated_at=repo_data.get("updated_at", ""),
|
|
184
|
+
pushed_at=repo_data.get("pushed_at", ""),
|
|
185
|
+
size_kb=repo_data.get("size", 0),
|
|
186
|
+
clone_url_http=repo_data.get("clone_url", ""),
|
|
187
|
+
clone_url_ssh=repo_data.get("ssh_url", ""),
|
|
188
|
+
contributors_url=repo_data.get("contributors_url"),
|
|
189
|
+
languages_url=repo_data.get("languages_url", ""),
|
|
190
|
+
issues_url=repo_data.get("issues_url"),
|
|
191
|
+
language=repo_data.get("language", ""),
|
|
192
|
+
languages=list(languages.keys()) if languages else [],
|
|
193
|
+
topics=repo_data.get("topics", []),
|
|
194
|
+
has_wiki=repo_data.get("has_wiki", False),
|
|
195
|
+
has_issues=repo_data.get("has_issues", False),
|
|
196
|
+
has_projects=repo_data.get("has_projects", False),
|
|
197
|
+
is_private=repo_data.get("private", False),
|
|
198
|
+
homepage_url=repo_data.get("homepage", ""),
|
|
199
|
+
license_name=license_info.get("name", ""),
|
|
200
|
+
license_url=license_info.get("url", ""),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class GitLabMetadataLoader(MetadataLoader):
|
|
205
|
+
@classmethod
|
|
206
|
+
def _load_platform_data(cls, repo_url: str) -> RepositoryMetadata:
|
|
207
|
+
"""
|
|
208
|
+
Load GitLab repository metadata via GitLab API.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
repo_url (str): URL of the GitLab repository.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
RepositoryMetadata: Parsed metadata object.
|
|
215
|
+
"""
|
|
216
|
+
base_url = get_base_repo_url(repo_url)
|
|
217
|
+
gitlab_instance_match = re.match(r"(https?://[^/]*gitlab[^/]*)", repo_url)
|
|
218
|
+
if not gitlab_instance_match:
|
|
219
|
+
raise ValueError("Invalid GitLab repository URL")
|
|
220
|
+
gitlab_instance = gitlab_instance_match.group(1)
|
|
221
|
+
|
|
222
|
+
headers = {
|
|
223
|
+
"Authorization": f"Bearer {os.getenv('GITLAB_TOKEN', os.getenv('GIT_TOKEN'))}",
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
}
|
|
226
|
+
project_path = base_url.replace("/", "%2F")
|
|
227
|
+
url = f"{gitlab_instance}/api/v4/projects/{project_path}"
|
|
228
|
+
|
|
229
|
+
response = requests.get(url=url, headers=headers)
|
|
230
|
+
response.raise_for_status()
|
|
231
|
+
data = response.json()
|
|
232
|
+
logger.info(f"Successfully fetched GitLab metadata for repository: '{base_url}'")
|
|
233
|
+
return GitLabMetadataLoader._parse_metadata(data)
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def _parse_metadata(cls, repo_data: dict) -> RepositoryMetadata:
|
|
237
|
+
"""
|
|
238
|
+
Parse GitLab API response into RepositoryMetadata.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
repo_data (dict): Raw GitLab API response.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
RepositoryMetadata: Parsed repository metadata.
|
|
245
|
+
"""
|
|
246
|
+
owner_info = repo_data.get("owner", {}) or {}
|
|
247
|
+
namespace = repo_data.get("namespace", {}) or {}
|
|
248
|
+
|
|
249
|
+
created_raw = repo_data.get("created_at", "")
|
|
250
|
+
if created_raw:
|
|
251
|
+
created_time = datetime.strptime(created_raw.split(".")[0], "%Y-%m-%dT%H:%M:%S").strftime(
|
|
252
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
created_time = ""
|
|
256
|
+
|
|
257
|
+
return RepositoryMetadata(
|
|
258
|
+
name=repo_data.get("name", ""),
|
|
259
|
+
full_name=repo_data.get("path_with_namespace", ""),
|
|
260
|
+
owner=namespace.get("name", "") or owner_info.get("name", ""),
|
|
261
|
+
owner_url=namespace.get("web_url", "") or owner_info.get("web_url", ""),
|
|
262
|
+
description=repo_data.get("description", ""),
|
|
263
|
+
stars_count=repo_data.get("star_count", 0),
|
|
264
|
+
forks_count=repo_data.get("forks_count", 0),
|
|
265
|
+
watchers_count=0, # GitLab does not have watchers, set to 0
|
|
266
|
+
open_issues_count=repo_data.get("open_issues_count", 0),
|
|
267
|
+
default_branch=repo_data.get("default_branch", ""),
|
|
268
|
+
created_at=created_time,
|
|
269
|
+
updated_at=repo_data.get("last_activity_at", ""),
|
|
270
|
+
pushed_at=repo_data.get("last_activity_at", ""),
|
|
271
|
+
size_kb=repo_data.get("repository_size", 0) // 1024, # Convert bytes to KB
|
|
272
|
+
clone_url_http=repo_data.get("http_url_to_repo", ""),
|
|
273
|
+
clone_url_ssh=repo_data.get("ssh_url_to_repo", ""),
|
|
274
|
+
contributors_url=f"{repo_data.get('web_url', '')}/contributors" if repo_data.get("web_url") else None,
|
|
275
|
+
languages_url=f"{repo_data.get('web_url', '')}/languages" if repo_data.get("web_url") else "",
|
|
276
|
+
issues_url=f"{repo_data.get('web_url', '')}/issues" if repo_data.get("web_url") else None,
|
|
277
|
+
language="", # GitLab API does not provide primary language
|
|
278
|
+
languages=[],
|
|
279
|
+
topics=repo_data.get("tag_list", []),
|
|
280
|
+
has_wiki=repo_data.get("wiki_enabled", False),
|
|
281
|
+
has_issues=repo_data.get("issues_enabled", False),
|
|
282
|
+
has_projects=True, # GitLab always has project management features enabled
|
|
283
|
+
is_private=repo_data.get("visibility", "public") != "public",
|
|
284
|
+
homepage_url="",
|
|
285
|
+
license_name="",
|
|
286
|
+
license_url="",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class GitverseMetadataLoader(MetadataLoader):
|
|
291
|
+
@classmethod
|
|
292
|
+
def _load_platform_data(cls, repo_url: str) -> RepositoryMetadata:
|
|
293
|
+
"""
|
|
294
|
+
Load Gitverse repository metadata via Gitverse API.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
repo_url (str): URL of the Gitverse repository.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
RepositoryMetadata: Parsed metadata object.
|
|
301
|
+
"""
|
|
302
|
+
base_url = get_base_repo_url(repo_url)
|
|
303
|
+
headers = {
|
|
304
|
+
"Authorization": f"Bearer {os.getenv('GITVERSE_TOKEN', os.getenv('GIT_TOKEN'))}",
|
|
305
|
+
"Accept": "application/vnd.gitverse.object+json;version=1",
|
|
306
|
+
"Content-Type": "application/json",
|
|
307
|
+
"User-Agent": "Mozilla/5.0",
|
|
308
|
+
}
|
|
309
|
+
url = f"https://api.gitverse.ru/repos/{base_url}"
|
|
310
|
+
|
|
311
|
+
response = requests.get(url=url, headers=headers)
|
|
312
|
+
response.raise_for_status()
|
|
313
|
+
data = response.json()
|
|
314
|
+
logger.info(f"Successfully fetched Gitverse metadata for repository: '{base_url}'")
|
|
315
|
+
return GitverseMetadataLoader._parse_metadata(data)
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def _parse_metadata(cls, repo_data: dict) -> RepositoryMetadata:
|
|
319
|
+
"""
|
|
320
|
+
Parse Gitverse API response into RepositoryMetadata.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
repo_data (dict): Raw Gitverse API response.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
RepositoryMetadata: Parsed repository metadata.
|
|
327
|
+
"""
|
|
328
|
+
owner_info = repo_data.get("owner", {}) or {}
|
|
329
|
+
license_info = repo_data.get("license", {}) or {}
|
|
330
|
+
|
|
331
|
+
return RepositoryMetadata(
|
|
332
|
+
name=repo_data.get("name", ""),
|
|
333
|
+
full_name=repo_data.get("full_name", ""),
|
|
334
|
+
owner=owner_info.get("login", ""),
|
|
335
|
+
owner_url=owner_info.get("html_url", ""),
|
|
336
|
+
description=repo_data.get("description", ""),
|
|
337
|
+
stars_count=repo_data.get("stargazers_count", 0),
|
|
338
|
+
forks_count=repo_data.get("forks_count", 0),
|
|
339
|
+
watchers_count=repo_data.get("watchers_count", 0),
|
|
340
|
+
open_issues_count=repo_data.get("open_issues_count", 0),
|
|
341
|
+
default_branch=repo_data.get("default_branch", ""),
|
|
342
|
+
created_at=repo_data.get("created_at", ""),
|
|
343
|
+
updated_at=repo_data.get("updated_at", ""),
|
|
344
|
+
pushed_at=repo_data.get("pushed_at", ""),
|
|
345
|
+
size_kb=repo_data.get("size", 0),
|
|
346
|
+
clone_url_http=repo_data.get("clone_url", ""),
|
|
347
|
+
clone_url_ssh=repo_data.get("ssh_url", ""),
|
|
348
|
+
contributors_url=repo_data.get("contributors_url"),
|
|
349
|
+
languages_url=repo_data.get("languages_url", ""),
|
|
350
|
+
issues_url=repo_data.get("issues_url"),
|
|
351
|
+
language=repo_data.get("language", ""),
|
|
352
|
+
languages=repo_data.get("languages", []) or [],
|
|
353
|
+
topics=repo_data.get("topics", []) or [],
|
|
354
|
+
has_wiki=repo_data.get("has_wiki", False),
|
|
355
|
+
has_issues=repo_data.get("has_issues", False),
|
|
356
|
+
has_projects=repo_data.get("has_projects", False),
|
|
357
|
+
is_private=repo_data.get("private", False),
|
|
358
|
+
homepage_url=repo_data.get("homepage", ""),
|
|
359
|
+
license_name=license_info.get("name", ""),
|
|
360
|
+
license_url=license_info.get("url", ""),
|
|
361
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class YesNoPartial(str, Enum):
|
|
8
|
+
YES = "Yes"
|
|
9
|
+
NO = "No"
|
|
10
|
+
PARTIAL = "Partial"
|
|
11
|
+
UNKNOWN = "Unknown"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RepositoryStructure(BaseModel):
|
|
15
|
+
compliance: Annotated[str, "Compliance with standard structure"] = "Unknown"
|
|
16
|
+
missing_files: Annotated[
|
|
17
|
+
list[str],
|
|
18
|
+
"List of missing critical files that impact project usability and clarity",
|
|
19
|
+
] = []
|
|
20
|
+
organization: Annotated[
|
|
21
|
+
str,
|
|
22
|
+
"Evaluation of the overall organization of directories and files for maintainability and clarity",
|
|
23
|
+
] = "Unknown"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReadmeEvaluation(BaseModel):
|
|
27
|
+
readme_quality: Annotated[str, "Assessment of the README quality with a brief comment"] = "Unknown"
|
|
28
|
+
project_description: YesNoPartial = YesNoPartial.UNKNOWN
|
|
29
|
+
installation: YesNoPartial = YesNoPartial.UNKNOWN
|
|
30
|
+
usage_examples: YesNoPartial = YesNoPartial.UNKNOWN
|
|
31
|
+
contribution_guidelines: YesNoPartial = YesNoPartial.UNKNOWN
|
|
32
|
+
license_specified: YesNoPartial = YesNoPartial.UNKNOWN
|
|
33
|
+
badges_present: YesNoPartial = YesNoPartial.UNKNOWN
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CodeDocumentation(BaseModel):
|
|
37
|
+
tests_present: YesNoPartial = YesNoPartial.UNKNOWN
|
|
38
|
+
docs_quality: Annotated[
|
|
39
|
+
str,
|
|
40
|
+
"Evaluation of the quality of code documentation, including API references, inline comments, and guides",
|
|
41
|
+
] = "Unknown"
|
|
42
|
+
outdated_content: Annotated[
|
|
43
|
+
bool,
|
|
44
|
+
"Flags whether the documentation contains outdated or misleading information",
|
|
45
|
+
] = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class OverallAssessment(BaseModel):
|
|
49
|
+
key_shortcomings: Annotated[
|
|
50
|
+
list[str],
|
|
51
|
+
"List of the most significant and critical issues that need to be addressed",
|
|
52
|
+
] = ["There are no critical issues"]
|
|
53
|
+
recommendations: Annotated[
|
|
54
|
+
list[str],
|
|
55
|
+
"Specific improvements to address issues or optimize the process",
|
|
56
|
+
] = ["No recommendations"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RepositoryReport(BaseModel):
|
|
60
|
+
structure: RepositoryStructure = RepositoryStructure()
|
|
61
|
+
readme: ReadmeEvaluation = ReadmeEvaluation()
|
|
62
|
+
documentation: CodeDocumentation = CodeDocumentation()
|
|
63
|
+
assessment: OverallAssessment = OverallAssessment()
|
|
64
|
+
|
|
65
|
+
class Config:
|
|
66
|
+
extra = "ignore"
|