metripy 0.2.5__py3-none-any.whl

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.

Potentially problematic release.


This version of metripy might be problematic. Click here for more details.

Files changed (66) hide show
  1. metripy-0.2.5.dist-info/METADATA +112 -0
  2. metripy-0.2.5.dist-info/RECORD +66 -0
  3. metripy-0.2.5.dist-info/WHEEL +5 -0
  4. metripy-0.2.5.dist-info/entry_points.txt +2 -0
  5. metripy-0.2.5.dist-info/licenses/LICENSE +21 -0
  6. metripy-0.2.5.dist-info/top_level.txt +1 -0
  7. src/Application/Analyzer.py +105 -0
  8. src/Application/Application.py +54 -0
  9. src/Application/Config/Config.py +13 -0
  10. src/Application/Config/File/ConfigFileReaderFactory.py +22 -0
  11. src/Application/Config/File/ConfigFileReaderInterface.py +14 -0
  12. src/Application/Config/File/JsonConfigFileReader.py +81 -0
  13. src/Application/Config/GitConfig.py +10 -0
  14. src/Application/Config/Parser.py +30 -0
  15. src/Application/Config/ProjectConfig.py +27 -0
  16. src/Application/Config/ReportConfig.py +10 -0
  17. src/Application/__init__.py +0 -0
  18. src/Component/Debug/Debugger.py +20 -0
  19. src/Component/File/Finder.py +37 -0
  20. src/Component/Output/CliOutput.py +49 -0
  21. src/Component/Output/ProgressBar.py +27 -0
  22. src/Dependency/Composer/Composer.py +30 -0
  23. src/Dependency/Composer/Packegist.py +55 -0
  24. src/Dependency/Dependency.py +30 -0
  25. src/Dependency/Npm/Npm.py +30 -0
  26. src/Dependency/Npm/NpmOrg.py +47 -0
  27. src/Dependency/Pip/Pip.py +69 -0
  28. src/Dependency/Pip/PyPi.py +49 -0
  29. src/Git/GitAnalyzer.py +86 -0
  30. src/LangAnalyzer/AbstractLangAnalyzer.py +65 -0
  31. src/LangAnalyzer/Generic/HalSteadAnalyzer.py +58 -0
  32. src/LangAnalyzer/Generic/__init__.py +0 -0
  33. src/LangAnalyzer/Php/PhpAnalyzer.py +193 -0
  34. src/LangAnalyzer/Php/PhpBasicAstParser.py +56 -0
  35. src/LangAnalyzer/Php/PhpBasicLocAnalyzer.py +174 -0
  36. src/LangAnalyzer/Php/PhpHalSteadAnalyzer.py +44 -0
  37. src/LangAnalyzer/Python/PythonAnalyzer.py +129 -0
  38. src/LangAnalyzer/Typescript/TypescriptAnalyzer.py +210 -0
  39. src/LangAnalyzer/Typescript/TypescriptAstParser.py +68 -0
  40. src/LangAnalyzer/Typescript/TypescriptBasicComplexityAnalyzer.py +114 -0
  41. src/LangAnalyzer/Typescript/TypescriptBasicLocAnalyzer.py +69 -0
  42. src/LangAnalyzer/Typescript/TypescriptHalSteadAnalyzer.py +55 -0
  43. src/LangAnalyzer/__init__.py +0 -0
  44. src/Metric/Code/AggregatedMetrics.py +42 -0
  45. src/Metric/Code/FileMetrics.py +33 -0
  46. src/Metric/Code/ModuleMetrics.py +32 -0
  47. src/Metric/Code/SegmentedMetrics.py +65 -0
  48. src/Metric/FileTree/FileTree.py +15 -0
  49. src/Metric/FileTree/FileTreeParser.py +42 -0
  50. src/Metric/Git/GitCodeHotspot.py +37 -0
  51. src/Metric/Git/GitContributor.py +37 -0
  52. src/Metric/Git/GitKnowledgeSilo.py +27 -0
  53. src/Metric/Git/GitMetrics.py +148 -0
  54. src/Metric/ProjectMetrics.py +55 -0
  55. src/Report/Csv/Reporter.py +12 -0
  56. src/Report/Html/Reporter.py +210 -0
  57. src/Report/Json/AbstractJsonReporter.py +10 -0
  58. src/Report/Json/GitJsonReporter.py +21 -0
  59. src/Report/Json/JsonReporter.py +12 -0
  60. src/Report/ReporterFactory.py +22 -0
  61. src/Report/ReporterInterface.py +17 -0
  62. src/Tree/ClassNode.py +32 -0
  63. src/Tree/FunctionNode.py +49 -0
  64. src/Tree/ModuleNode.py +42 -0
  65. src/__init__.py +0 -0
  66. src/codemetrics.py +15 -0
@@ -0,0 +1,65 @@
1
+ from typing import Self
2
+
3
+
4
+ class SegmentedMetrics:
5
+ def __init__(self):
6
+ self.good = 0
7
+ self.ok = 0
8
+ self.warning = 0
9
+ self.critical = 0
10
+
11
+ def to_dict(self) -> dict:
12
+ return {
13
+ "good": self.good,
14
+ "ok": self.ok,
15
+ "warning": self.warning,
16
+ "critical": self.critical,
17
+ }
18
+
19
+ def set_loc(self, values: list[int]) -> Self:
20
+ for value in values:
21
+ if value <= 200:
22
+ self.good += 1
23
+ elif value <= 500:
24
+ self.ok += 1
25
+ elif value <= 1000:
26
+ self.warning += 1
27
+ else:
28
+ self.critical += 1
29
+ return self
30
+
31
+ def set_complexity(self, values: list[int]) -> Self:
32
+ for value in values:
33
+ if value <= 5:
34
+ self.good += 1
35
+ elif value <= 10:
36
+ self.ok += 1
37
+ elif value <= 20:
38
+ self.warning += 1
39
+ else:
40
+ self.critical += 1
41
+ return self
42
+
43
+ def set_maintainability(self, values: list[int]) -> Self:
44
+ for value in values:
45
+ if value <= 80:
46
+ self.critical += 1
47
+ elif value <= 60:
48
+ self.warning += 1
49
+ elif value <= 40:
50
+ self.ok += 1
51
+ else:
52
+ self.good += 1
53
+ return self
54
+
55
+ def set_method_size(self, values: list[int]) -> Self:
56
+ for value in values:
57
+ if value <= 15:
58
+ self.good += 1
59
+ elif value <= 30:
60
+ self.ok += 1
61
+ elif value <= 50:
62
+ self.warning += 1
63
+ else:
64
+ self.critical += 1
65
+ return self
@@ -0,0 +1,15 @@
1
+ from typing import Self
2
+
3
+
4
+ class FileTree:
5
+ def __init__(self, name: str, full_name: str, children: list[Self] | None = None):
6
+ self.name = name
7
+ self.full_name = full_name
8
+ self.children: list[Self] = children if children is not None else []
9
+
10
+ def to_dict(self) -> dict:
11
+ return {
12
+ "name": self.name,
13
+ "full_name": self.full_name,
14
+ "children": [child.to_dict() for child in self.children],
15
+ }
@@ -0,0 +1,42 @@
1
+ from Metric.FileTree.FileTree import FileTree
2
+
3
+
4
+ class FileTreeParser:
5
+ @staticmethod
6
+ def parse(paths: list[str], shorten: bool = False) -> FileTree:
7
+ root = FileTree(".", ".")
8
+
9
+ for path in paths:
10
+ parts = path.strip("./").split("/")
11
+ current = root
12
+
13
+ for part in parts:
14
+ # Check if part already exists in current children
15
+ found = next(
16
+ (child for child in current.children if child.name == part), None
17
+ )
18
+ if not found:
19
+ found = FileTree(part, path)
20
+ current.children.append(found)
21
+ current = found
22
+
23
+ if shorten:
24
+ FileTreeParser._shorten_tree(root)
25
+
26
+ return root
27
+
28
+ @staticmethod
29
+ def _shorten_tree(node: FileTree):
30
+ """shorten tree nodes that only have a single child"""
31
+ while len(node.children) == 1:
32
+ child = node.children[0]
33
+ node.name += "/" + child.name
34
+ node.full_name = child.full_name
35
+ node.children = child.children
36
+
37
+ for child in node.children:
38
+ FileTreeParser._shorten_tree(child)
39
+
40
+ # print(json.dumps(root.to_dict(), indent=4))
41
+ # exit()
42
+ # return root
@@ -0,0 +1,37 @@
1
+ class GitCodeHotspot:
2
+
3
+ def __init__(self, file_path: str, changes_count: int, contributors_count: int):
4
+ self.file_path = file_path
5
+ self.changes_count = changes_count
6
+ self.contributors_count = contributors_count
7
+ self.risk_level = self._calc_risk_level(changes_count, contributors_count)
8
+ self.risk_label = self._label_from_risk_level()
9
+
10
+ def _calc_risk_level(self, changes_count: int, contributors_count: int) -> str:
11
+ if changes_count < 10:
12
+ if contributors_count < 5:
13
+ return "low"
14
+ else:
15
+ return "medium"
16
+ elif changes_count < 50:
17
+ if contributors_count < 5:
18
+ return "medium"
19
+ else:
20
+ return "high"
21
+ else:
22
+ if contributors_count < 1:
23
+ return "medium"
24
+ else:
25
+ return "high"
26
+
27
+ def _label_from_risk_level(self) -> str:
28
+ return self.risk_level.capitalize()
29
+
30
+ def to_dict(self) -> dict[str, str]:
31
+ return {
32
+ "file_path": self.file_path,
33
+ "changes_count": self.changes_count,
34
+ "risk_level": self.risk_level,
35
+ "risk_label": self.risk_label,
36
+ "contributors_count": self.contributors_count,
37
+ }
@@ -0,0 +1,37 @@
1
+ class GitContributor:
2
+ def __init__(
3
+ self,
4
+ name: str,
5
+ commits_count: int,
6
+ lines_added: int,
7
+ lines_removed: int,
8
+ contribution_percentage: int,
9
+ ):
10
+ self.name = name
11
+ self.initials = self._get_initials(name)
12
+ self.commits_count = commits_count
13
+ self.lines_added = lines_added
14
+ self.lines_removed = lines_removed
15
+ self.contribution_percentage = contribution_percentage
16
+
17
+ def _get_initials(self, name: str) -> str:
18
+ try:
19
+ parts = name.split()
20
+ if len(parts) >= 2:
21
+ return (parts[0][0] + parts[1][0]).upper()
22
+ elif len(parts) == 1:
23
+ return parts[0][:2].upper()
24
+ else:
25
+ return "UN"
26
+ except Exception:
27
+ return "UN"
28
+
29
+ def to_dict(self) -> dict[str, int]:
30
+ return {
31
+ "name": self.name,
32
+ "initials": self.initials,
33
+ "commits_count": self.commits_count,
34
+ "lines_added": self.lines_added,
35
+ "lines_removed": self.lines_removed,
36
+ "contribution_percentage": self.contribution_percentage,
37
+ }
@@ -0,0 +1,27 @@
1
+ class GitKnowledgeSilo:
2
+ def __init__(self, file_path: str, owner: str, commits_count: int):
3
+ self.file_path = file_path
4
+ self.owner = owner
5
+ self.commits_count = commits_count
6
+ self.risk_level = self._calc_risk_level(commits_count)
7
+ self.risk_label = self._calc_risk_label(self.risk_level)
8
+
9
+ def _calc_risk_level(self, commits_count: int) -> str:
10
+ if commits_count >= 15:
11
+ return "high"
12
+ elif commits_count >= 8:
13
+ return "medium"
14
+ else:
15
+ return "low"
16
+
17
+ def _calc_risk_label(self, risk_level: str) -> str:
18
+ return risk_level.capitalize()
19
+
20
+ def to_dict(self) -> dict[str, int]:
21
+ return {
22
+ "file_path": f"{self.file_path}",
23
+ "owner": f"{self.owner}",
24
+ "commits_count": f"{self.commits_count}",
25
+ "risk_level": f"{self.risk_level}",
26
+ "risk_label": f"{self.risk_label}",
27
+ }
@@ -0,0 +1,148 @@
1
+ from Metric.Git.GitCodeHotspot import GitCodeHotspot
2
+ from Metric.Git.GitContributor import GitContributor
3
+ from Metric.Git.GitKnowledgeSilo import GitKnowledgeSilo
4
+
5
+
6
+ class GitMetrics:
7
+ def __init__(
8
+ self,
9
+ analysis_start_date: str,
10
+ commit_stats_per_month: dict[str, int],
11
+ churn_per_month: dict[str, dict[str, int]],
12
+ contributor_stats: dict[str, dict[str, int]],
13
+ file_contributors: dict[str, dict[str, int]],
14
+ ):
15
+ self.analysis_start_date = analysis_start_date
16
+ self._commit_stats_per_month: dict[str, int] = commit_stats_per_month
17
+ self._churn_per_month: dict[str, dict[str, int]] = churn_per_month
18
+ self._contributor_stats = contributor_stats
19
+ self.total_commits = sum(data["commits"] for data in contributor_stats.values())
20
+ self.contributors: list[GitContributor] = self._calc_contributors(
21
+ contributor_stats
22
+ )
23
+ self.core_contributors, self.core_percentage = self._calc_core_contributors(
24
+ self.contributors
25
+ )
26
+ self.silos: list[GitKnowledgeSilo] = self._calc_silos(file_contributors)
27
+ self.hotspots: list[GitCodeHotspot] = self._calc_hotspots(file_contributors)
28
+
29
+ def get_analysis_start_date(self) -> str:
30
+ return self.analysis_start_date
31
+
32
+ def get_commit_stats_per_month(self) -> dict[str, int]:
33
+ return self._commit_stats_per_month
34
+
35
+ def get_churn_per_month(self) -> dict[str, dict[str, int]]:
36
+ return self._churn_per_month
37
+
38
+ def get_avg_commit_size(self) -> float:
39
+ return (
40
+ sum(
41
+ data["lines_added"] + data["lines_removed"]
42
+ for data in self._contributor_stats.values()
43
+ )
44
+ / self.total_commits
45
+ )
46
+
47
+ def get_contributors_dict(self) -> dict[str, dict[str, int]]:
48
+ return {
49
+ contributor.name: contributor.to_dict() for contributor in self.contributors
50
+ }
51
+
52
+ def get_contributors_list(self) -> list[dict[str, int]]:
53
+ return [contributor.to_dict() for contributor in self.contributors]
54
+
55
+ def get_hotspots_list(self) -> list[dict[str, int]]:
56
+ return [hotspot.to_dict() for hotspot in self.hotspots]
57
+
58
+ def get_silos_dict(self) -> dict[str, dict[str, int]]:
59
+ return {silo.file_path: silo.to_dict() for silo in self.silos}
60
+
61
+ def get_silos_list(self) -> list[dict[str, int]]:
62
+ return [silo.to_dict() for silo in self.silos]
63
+
64
+ def _calc_hotspots(
65
+ self, file_contributors: dict[str, dict[str, int]]
66
+ ) -> list[GitCodeHotspot]:
67
+ hotspots = []
68
+ for file_path, data in file_contributors.items():
69
+ changes_count = data["commits"]
70
+ if changes_count < 10:
71
+ continue
72
+ hotspots.append(
73
+ GitCodeHotspot(
74
+ file_path=file_path,
75
+ changes_count=changes_count,
76
+ contributors_count=len(data["contributors"]),
77
+ )
78
+ )
79
+ return sorted(hotspots, key=lambda x: x.changes_count, reverse=True)
80
+
81
+ def _calc_contributors(
82
+ self, contributor_stats: dict[str, dict[str, int]]
83
+ ) -> list[GitContributor]:
84
+ # determine top contributors
85
+ total_commits = self.total_commits
86
+ contributors = []
87
+
88
+ for name, data in contributor_stats.items():
89
+ percentage = (
90
+ (data["commits"] / total_commits * 100) if total_commits > 0 else 0
91
+ )
92
+ contributors.append(
93
+ GitContributor(
94
+ name=name,
95
+ commits_count=data["commits"],
96
+ lines_added=data["lines_added"],
97
+ lines_removed=data["lines_removed"],
98
+ contribution_percentage=int(percentage),
99
+ )
100
+ )
101
+
102
+ return sorted(contributors, key=lambda x: x.commits_count, reverse=True)
103
+
104
+ def _calc_core_contributors(
105
+ self, contributors: list[GitContributor]
106
+ ) -> tuple[int, int]:
107
+ # Calculate core contributors (top contributors making up 67% of commits)
108
+ core_contributors = 0
109
+ core_percentage = 0
110
+ for contributor in contributors:
111
+ core_percentage += contributor.contribution_percentage
112
+ core_contributors += 1
113
+ if core_percentage >= 67:
114
+ break
115
+ core_percentage = min(67, core_percentage)
116
+
117
+ return core_contributors, core_percentage
118
+
119
+ def _calc_silos(
120
+ self, file_contributors: dict[str, dict[str, int]]
121
+ ) -> list[GitKnowledgeSilo]:
122
+ silos = []
123
+ for file_path, data in file_contributors.items():
124
+ if len(data["contributors"]) > 1 or data["commits"] < 3:
125
+ continue
126
+ owner = list(data["contributors"])[0]
127
+ silos.append(
128
+ GitKnowledgeSilo(
129
+ file_path=file_path,
130
+ owner=owner,
131
+ commits_count=data["commits"],
132
+ )
133
+ )
134
+ return sorted(silos, key=lambda x: x.commits_count, reverse=True)
135
+
136
+ def to_dict(self) -> dict[str, any]:
137
+ return {
138
+ "analysis_start_date": self.analysis_start_date,
139
+ "avg_commit_size": f"{self.get_avg_commit_size():.2f}",
140
+ "commit_stats_per_month": self.get_commit_stats_per_month(),
141
+ "churn_per_month": self.get_churn_per_month(),
142
+ "total_commits": self.total_commits,
143
+ "active_contributors": len(self.contributors),
144
+ "contributors": self.get_contributors_dict(),
145
+ "core_contributors": self.core_contributors,
146
+ "core_percentage": self.core_percentage,
147
+ "silos": self.get_silos_list(),
148
+ }
@@ -0,0 +1,55 @@
1
+ from Dependency.Dependency import Dependency
2
+ from Metric.Code.AggregatedMetrics import AggregatedMetrics
3
+ from Metric.Code.FileMetrics import FileMetrics
4
+ from Metric.Code.SegmentedMetrics import SegmentedMetrics
5
+ from Metric.Git.GitMetrics import GitMetrics
6
+
7
+
8
+ class ProjectMetrics:
9
+ def __init__(
10
+ self,
11
+ file_metrics: list[FileMetrics],
12
+ git_metrics: GitMetrics | None,
13
+ dependencies: list[Dependency] | None,
14
+ ):
15
+ self.file_metrics = file_metrics
16
+ self.git_metrics = git_metrics
17
+ self.dependencies = dependencies
18
+ self.total_code_metrics = self._compile_total_metrics(self.file_metrics)
19
+
20
+ def _compile_total_metrics(
21
+ self, file_metrics: list[FileMetrics]
22
+ ) -> AggregatedMetrics:
23
+ files = 0
24
+ locs = []
25
+ avgCcPerFunctions = []
26
+ maintainabilityIndices = []
27
+ avgLocPerFunctions = []
28
+ for file_metric in file_metrics:
29
+ files += 1
30
+ locs.append(file_metric.loc)
31
+ avgCcPerFunctions.append(file_metric.avgCcPerFunction)
32
+ maintainabilityIndices.append(file_metric.maintainabilityIndex)
33
+ avgLocPerFunctions.append(file_metric.avgLocPerFunction)
34
+
35
+ if files == 0:
36
+ return AggregatedMetrics()
37
+
38
+ return AggregatedMetrics(
39
+ loc=sum(locs),
40
+ avgCcPerFunction=self._avg(avgCcPerFunctions),
41
+ maintainabilityIndex=self._avg(maintainabilityIndices),
42
+ avgLocPerFunction=self._avg(avgLocPerFunctions),
43
+ num_files=files,
44
+ segmented_loc=SegmentedMetrics().set_loc(locs),
45
+ segmented_complexity=SegmentedMetrics().set_complexity(avgCcPerFunctions),
46
+ segmented_maintainability=SegmentedMetrics().set_maintainability(
47
+ maintainabilityIndices
48
+ ),
49
+ segmented_method_size=SegmentedMetrics().set_method_size(
50
+ avgLocPerFunctions
51
+ ),
52
+ )
53
+
54
+ def _avg(self, items: list[float | int]) -> float:
55
+ return sum(items) / len(items)
@@ -0,0 +1,12 @@
1
+ from Application.Config.Config import Config
2
+ from Component.Output.CliOutput import CliOutput
3
+ from Metric.ProjectMetrics import ProjectMetrics
4
+
5
+
6
+ class Reporter:
7
+ def __init__(self, config: Config, output: CliOutput):
8
+ self.config = config
9
+ self.output = output
10
+
11
+ def generate(self, metrics: ProjectMetrics):
12
+ raise NotImplementedError("CSV metrics report is not yet implemented")
@@ -0,0 +1,210 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ from datetime import datetime
5
+
6
+ from py_template_engine import TemplateEngine
7
+
8
+ from Application.Config.ReportConfig import ReportConfig
9
+ from Component.Output.CliOutput import CliOutput
10
+ from Metric.FileTree.FileTreeParser import FileTreeParser
11
+ from Metric.ProjectMetrics import ProjectMetrics
12
+ from Report.ReporterInterface import ReporterInterface
13
+
14
+
15
+ class Reporter(ReporterInterface):
16
+ def __init__(
17
+ self, config: ReportConfig, output: CliOutput, project_name: str = "foobar"
18
+ ):
19
+ self.config: ReportConfig = config
20
+ self.output = output
21
+ self.template_dir = os.path.join(os.getcwd(), "templates/html_report")
22
+
23
+ self.global_template_args = {
24
+ "project_name": project_name,
25
+ "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
26
+ }
27
+
28
+ def generate(self, metrics: ProjectMetrics):
29
+
30
+ self.output.writeln("<info>Generating HTML report...</info>")
31
+
32
+ # copy sources
33
+ if not os.path.exists(os.path.join(self.config.path, "js")):
34
+ os.makedirs(os.path.join(self.config.path, "js"))
35
+ if not os.path.exists(os.path.join(self.config.path, "css")):
36
+ os.makedirs(os.path.join(self.config.path, "css"))
37
+ if not os.path.exists(os.path.join(self.config.path, "images")):
38
+ os.makedirs(os.path.join(self.config.path, "images"))
39
+ if not os.path.exists(os.path.join(self.config.path, "fonts")):
40
+ os.makedirs(os.path.join(self.config.path, "fonts"))
41
+ if not os.path.exists(os.path.join(self.config.path, "data")):
42
+ os.makedirs(os.path.join(self.config.path, "data"))
43
+
44
+ # shutil.copy(os.path.join(self.template_dir, "favicon.ico"), os.path.join(self.config.path, "favicon.ico"))
45
+
46
+ shutil.copytree(
47
+ os.path.join(self.template_dir, "js"),
48
+ os.path.join(self.config.path, "js"),
49
+ dirs_exist_ok=True,
50
+ )
51
+ shutil.copytree(
52
+ os.path.join(self.template_dir, "css"),
53
+ os.path.join(self.config.path, "css"),
54
+ dirs_exist_ok=True,
55
+ )
56
+ # shutil.copytree(os.path.join(self.template_dir, "images"), os.path.join(self.config.path, "images"), dirs_exist_ok=True)
57
+ # shutil.copytree(os.path.join(self.template_dir, "fonts"), os.path.join(self.config.path, "fonts"), dirs_exist_ok=True)
58
+
59
+ # render templates
60
+ git_stats_data = {}
61
+ if metrics.git_metrics:
62
+ git_stats_data = metrics.git_metrics.get_commit_stats_per_month()
63
+
64
+ self.output.writeln("<info>Rendering index page</info>")
65
+ # Render main index page
66
+ self.render_template(
67
+ "index.html",
68
+ {
69
+ "git_stats_data": json.dumps(git_stats_data, indent=4),
70
+ "total_code_metrics": metrics.total_code_metrics.to_dict(),
71
+ "segmentation_data": json.dumps(
72
+ metrics.total_code_metrics.to_dict_segmentation(), indent=4
73
+ ),
74
+ "project_name": "CodeMetrics",
75
+ "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
76
+ "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
77
+ "author": "CodeMetrics",
78
+ "version": "1.0.0",
79
+ },
80
+ )
81
+ self.output.writeln("<success>Done rendering index page</success>")
82
+
83
+ # Render files page
84
+ self.render_files_page(metrics)
85
+ # Render git analysis page
86
+ self.render_git_analysis_page(metrics)
87
+
88
+ self.render_dependencies_page(metrics)
89
+
90
+ self.output.writeln(
91
+ f"<success>HTML report generated in {self.config.path} directory</success>"
92
+ )
93
+
94
+ def render_template(self, template_name: str, data: dict) -> str:
95
+ engine = TemplateEngine(os.path.join(self.template_dir, template_name))
96
+ content = engine.render(**data)
97
+ with open(os.path.join(self.config.path, template_name), "w") as file:
98
+ file.write(content)
99
+
100
+ def render_dependencies_page(self, metrics: ProjectMetrics):
101
+ """Render the dependencies page with dependency details and stats"""
102
+ if not metrics.dependencies:
103
+ self.output.writeln("<success>No dependencies to render</success>")
104
+ return
105
+
106
+ self.output.writeln("<info>Rendering dependencies page</info>")
107
+
108
+ dependencies = metrics.dependencies if metrics.dependencies is not None else []
109
+
110
+ license_by_type = {}
111
+
112
+ # TODO render a pie chart
113
+ for dependency in dependencies:
114
+ for license_name in dependency.license:
115
+ if license_name not in license_by_type.keys():
116
+ license_by_type[license_name] = 0
117
+ license_by_type[license_name] += 1
118
+
119
+ print(json.dumps(license_by_type, indent=2))
120
+
121
+ self.render_template(
122
+ "dependencies.html",
123
+ {
124
+ "dependencies": [d.to_dict() for d in dependencies],
125
+ "project_name": "CodeMetrics",
126
+ "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
127
+ },
128
+ )
129
+ self.output.writeln(
130
+ "<success>Dependencies page generated successfully</success>"
131
+ )
132
+
133
+ def render_files_page(self, metrics: ProjectMetrics):
134
+ """Render the files page with file details and analysis"""
135
+ self.output.writeln("<info>Rendering files page</info>")
136
+
137
+ file_names = []
138
+ file_details = {}
139
+ for file_metrics in metrics.file_metrics:
140
+ file_name = file_metrics.full_name
141
+ file_details[file_name] = file_metrics.to_dict()
142
+ file_names.append(file_name)
143
+
144
+ filetree = FileTreeParser.parse(file_names, shorten=True)
145
+
146
+ self.render_template(
147
+ "files.html",
148
+ {
149
+ "filetree": json.dumps(filetree.to_dict()),
150
+ "file_details": json.dumps(file_details),
151
+ "project_name": "CodeMetrics",
152
+ "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
153
+ },
154
+ )
155
+ self.output.writeln("<success>Files page generated successfully</success>")
156
+
157
+ def render_git_analysis_page(self, metrics: ProjectMetrics):
158
+ """Render the git analysis page with comprehensive git data"""
159
+ if not metrics.git_metrics:
160
+ self.output.writeln("<success>No git metrics to render</success>")
161
+ return
162
+
163
+ def stringify_values(obj):
164
+ if isinstance(obj, dict):
165
+ return {key: stringify_values(value) for key, value in obj.items()}
166
+ elif isinstance(obj, list):
167
+ return [stringify_values(item) for item in obj]
168
+ else:
169
+ return str(obj)
170
+
171
+ self.output.writeln("<info>Rendering git analysis page</info>")
172
+ try:
173
+ # Render git analysis template
174
+ self.render_template(
175
+ "git_analysis.html",
176
+ stringify_values(
177
+ {
178
+ "git_analysis": metrics.git_metrics.to_dict(),
179
+ "git_analysis_json": json.dumps(
180
+ metrics.git_metrics.get_contributors_dict(), indent=4
181
+ ),
182
+ "git_stats_data": json.dumps(
183
+ metrics.git_metrics.get_commit_stats_per_month(), indent=4
184
+ ), # git commit graph
185
+ "git_churn_data": json.dumps(
186
+ metrics.git_metrics.get_churn_per_month(), indent=4
187
+ ), # git chrun graph
188
+ "git_silos_data": metrics.git_metrics.get_silos_list()[
189
+ :10
190
+ ], # silos list
191
+ "git_contributors": metrics.git_metrics.get_contributors_list()[
192
+ :10
193
+ ], # contributors list
194
+ "git_hotspots_data": metrics.git_metrics.get_hotspots_list()[
195
+ :10
196
+ ], # hotspots list
197
+ "project_name": "CodeMetrics",
198
+ "last_updated": metrics.git_metrics.get_analysis_start_date(),
199
+ }
200
+ ),
201
+ )
202
+
203
+ self.output.writeln(
204
+ "<success>Git analysis page generated successfully</success>"
205
+ )
206
+ except Exception as e:
207
+ raise e
208
+ self.output.writeln(
209
+ f"<error>Error generating git analysis page: {e}</error>"
210
+ )
@@ -0,0 +1,10 @@
1
+ from Report.ReporterInterface import ReporterInterface
2
+ import json
3
+ import os
4
+
5
+
6
+ class AbstractJsonReporter(ReporterInterface):
7
+ def put_data(self, data: dict) -> None:
8
+ os.makedirs(os.path.dirname(self.config.path), exist_ok=True)
9
+ with open(self.config.path, "w") as file:
10
+ json.dump(data, file, indent=2)