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.
Files changed (111) hide show
  1. {osa_tool-0.2.2 → osa_tool-0.2.3}/PKG-INFO +6 -5
  2. {osa_tool-0.2.2 → osa_tool-0.2.3}/README.md +1 -1
  3. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/aboutgen/about_generator.py +5 -124
  4. osa_tool-0.2.3/osa_tool/analytics/metadata.py +361 -0
  5. osa_tool-0.2.3/osa_tool/analytics/prompt_builder.py +66 -0
  6. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/report_generator.py +28 -13
  7. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/report_maker.py +4 -8
  8. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/run_report_gen.py +10 -4
  9. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/arguments.yaml +11 -6
  10. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/config.toml +2 -2
  11. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompts.toml +248 -77
  12. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompts_article.toml +56 -32
  13. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings.py +2 -3
  14. osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/autopep8.yml +68 -0
  15. osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/black.yml +9 -0
  16. osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/fix_pep8.yml +30 -0
  17. osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/pep8.yml +18 -0
  18. osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/pypi_publish.yml +21 -0
  19. osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/slash_command_dispatch.yml +17 -0
  20. osa_tool-0.2.3/osa_tool/config/templates/workflow/github_gitverse/unit_test.yml +24 -0
  21. osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/autopep8.yml +7 -0
  22. osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/black.yml +7 -0
  23. osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/fix_pep8.yml +8 -0
  24. osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/pep8.yml +7 -0
  25. osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/pypi_publish.yml +7 -0
  26. osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/slash_command_dispatch.yml +6 -0
  27. osa_tool-0.2.3/osa_tool/config/templates/workflow/gitlab/unit_test.yml +11 -0
  28. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/community.toml +147 -0
  29. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/community.py +58 -12
  30. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/contributing.py +4 -4
  31. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/docs_run.py +9 -3
  32. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/license.py +4 -3
  33. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/git_agent/git_agent.py +509 -489
  34. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/osatreesitter/docgen.py +11 -0
  35. osa_tool-0.2.3/osa_tool/readmegen/context/citation_extractor.py +64 -0
  36. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/base_builder.py +20 -39
  37. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/builder.py +6 -5
  38. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/builder_article.py +10 -12
  39. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/header.py +3 -3
  40. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/installation.py +3 -3
  41. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/models/llm_service.py +60 -49
  42. osa_tool-0.2.3/osa_tool/readmegen/postprocessor/response_cleaner.py +87 -0
  43. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/prompts/prompts_builder.py +121 -43
  44. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/readme_core.py +11 -8
  45. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/run.py +34 -25
  46. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/scheduler/scheduler.py +18 -27
  47. osa_tool-0.2.3/osa_tool/scheduler/workflow_manager.py +323 -0
  48. osa_tool-0.2.3/osa_tool/translation/readme_translator.py +134 -0
  49. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/ui/plan_editor.py +3 -3
  50. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/utils.py +3 -0
  51. osa_tool-0.2.3/osa_tool/workflow/README.md +58 -0
  52. osa_tool-0.2.3/osa_tool/workflow/__init__.py +0 -0
  53. osa_tool-0.2.3/osa_tool/workflow/workflow_generator.py +770 -0
  54. {osa_tool-0.2.2 → osa_tool-0.2.3}/pyproject.toml +1 -1
  55. osa_tool-0.2.2/osa_tool/analytics/metadata.py +0 -294
  56. osa_tool-0.2.2/osa_tool/analytics/prompt_builder.py +0 -65
  57. osa_tool-0.2.2/osa_tool/github_workflow/README.md +0 -334
  58. osa_tool-0.2.2/osa_tool/github_workflow/__init__.py +0 -15
  59. osa_tool-0.2.2/osa_tool/github_workflow/generator.py +0 -496
  60. osa_tool-0.2.2/osa_tool/github_workflow/providers/black.py +0 -69
  61. osa_tool-0.2.2/osa_tool/github_workflow/providers/unit_test.py +0 -81
  62. osa_tool-0.2.2/osa_tool/readmegen/postprocessor/readme_refiner.py +0 -76
  63. osa_tool-0.2.2/osa_tool/readmegen/postprocessor/response_cleaner.py +0 -38
  64. osa_tool-0.2.2/osa_tool/scheduler/workflow_manager.py +0 -158
  65. {osa_tool-0.2.2 → osa_tool-0.2.3}/LICENSE +0 -0
  66. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/__init__.py +0 -0
  67. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/aboutgen/__init__.py +0 -0
  68. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/aboutgen/prompts_about_config.py +0 -0
  69. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/__init__.py +0 -0
  70. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/analytics/sourcerank.py +0 -0
  71. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/arguments_parser.py +0 -0
  72. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/__init__.py +0 -0
  73. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompt_for_analysis.toml +0 -0
  74. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompt_for_scheduler.toml +0 -0
  75. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/settings/prompts_about_section.toml +0 -0
  76. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/config/templates/template.toml +0 -0
  77. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/convertion/__init__.py +0 -0
  78. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/convertion/notebook_converter.py +0 -0
  79. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/images/osa_logo.PNG +0 -0
  80. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/images/osa_logo_transparent.PNG +0 -0
  81. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/ci_config.toml +0 -0
  82. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/contributing.toml +0 -0
  83. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/licenses.toml +0 -0
  84. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs/templates/mkdocs.yml +0 -0
  85. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/docs_generator/__init__.py +0 -0
  86. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/git_agent/__init__.py +0 -0
  87. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/models/__init__.py +0 -0
  88. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/models/models.py +0 -0
  89. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/organization/repo_organizer.py +0 -0
  90. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/osatreesitter/__init__.py +0 -0
  91. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/osatreesitter/osa_treesitter.py +0 -0
  92. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/__init__.py +0 -0
  93. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/__init__.py +0 -0
  94. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/article_content.py +0 -0
  95. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/article_path.py +0 -0
  96. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/dependencies.py +0 -0
  97. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/files_contents.py +0 -0
  98. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/context/pypi_status_checker.py +0 -0
  99. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/__init__.py +0 -0
  100. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/generator/icons/shieldsio_icons.json +0 -0
  101. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/models/__init__.py +0 -0
  102. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/postprocessor/__init__.py +0 -0
  103. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/prompts/__init__.py +0 -0
  104. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/readmegen/utils.py +0 -0
  105. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/scheduler/README.md +0 -0
  106. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/scheduler/__init__.py +0 -0
  107. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/scheduler/prompts.py +0 -0
  108. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/translation/__init__.py +0 -0
  109. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/translation/dir_translator.py +0 -0
  110. {osa_tool-0.2.2 → osa_tool-0.2.3}/osa_tool/ui/__init__.py +0 -0
  111. {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.3
1
+ Metadata-Version: 2.4
2
2
  Name: osa_tool
3
- Version: 0.2.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 [GitHub Action Workflow Generator README](./osa_tool/github_workflow/README.md).
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 [GitHub Action Workflow Generator README](./osa_tool/github_workflow/README.md).
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 = load_data_metadata(self.repo_url)
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._validate_topics(topics)
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"