metripy 0.2.7__py3-none-any.whl → 0.3.6__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.
- metripy/Application/Analyzer.py +23 -3
- metripy/Application/Application.py +16 -2
- metripy/Application/Config/Config.py +34 -0
- metripy/Application/Config/File/ConfigFileReaderFactory.py +6 -5
- metripy/Application/Config/File/ConfigFileReaderInterface.py +70 -3
- metripy/Application/Config/File/JsonConfigFileReader.py +5 -70
- metripy/Application/Config/File/YamlConfigFileReader.py +17 -0
- metripy/Application/Config/Parser.py +24 -11
- metripy/Application/Config/ProjectConfig.py +64 -0
- metripy/Application/Info.py +61 -0
- metripy/Dependency/Dependency.py +17 -1
- metripy/Dependency/Pip/Pip.py +21 -31
- metripy/Dependency/Pip/PyPi.py +1 -0
- metripy/Git/GitAnalyzer.py +0 -3
- metripy/Import/Json/JsonImporter.py +17 -0
- metripy/LangAnalyzer/AbstractLangAnalyzer.py +4 -3
- metripy/LangAnalyzer/Php/PhpAnalyzer.py +2 -1
- metripy/LangAnalyzer/Python/PythonAnalyzer.py +31 -9
- metripy/LangAnalyzer/Python/PythonHalSteadAnalyzer.py +55 -0
- metripy/LangAnalyzer/Typescript/TypescriptAnalyzer.py +12 -9
- metripy/LangAnalyzer/Typescript/TypescriptAstParser.py +1 -1
- metripy/Metric/Code/AggregatedMetrics.py +12 -5
- metripy/Metric/Code/FileMetrics.py +32 -1
- metripy/Metric/Code/ModuleMetrics.py +5 -5
- metripy/Metric/Code/SegmentedMetrics.py +72 -36
- metripy/Metric/Code/Segmentor.py +44 -0
- metripy/Metric/FileTree/FileTreeParser.py +0 -4
- metripy/Metric/Git/GitMetrics.py +1 -1
- metripy/Metric/ProjectMetrics.py +29 -0
- metripy/Metric/Trend/AggregatedTrendMetric.py +101 -0
- metripy/Metric/Trend/ClassTrendMetric.py +20 -0
- metripy/Metric/Trend/FileTrendMetric.py +46 -0
- metripy/Metric/Trend/FunctionTrendMetric.py +28 -0
- metripy/Metric/Trend/SegmentedTrendMetric.py +29 -0
- metripy/Report/Html/DependencyPageRenderer.py +21 -0
- metripy/Report/Html/FilesPageRenderer.py +28 -0
- metripy/Report/Html/GitAnalysisPageRenderer.py +55 -0
- metripy/Report/Html/IndexPageRenderer.py +47 -0
- metripy/Report/Html/PageRenderer.py +43 -0
- metripy/Report/Html/PageRendererFactory.py +37 -0
- metripy/Report/Html/Reporter.py +78 -137
- metripy/Report/Html/TopOffendersPageRenderer.py +84 -0
- metripy/Report/Html/TrendsPageRenderer.py +137 -0
- metripy/Report/Json/GitJsonReporter.py +3 -0
- metripy/Report/Json/JsonReporter.py +6 -2
- metripy/Report/ReporterFactory.py +6 -3
- metripy/Tree/ClassNode.py +21 -0
- metripy/Tree/FunctionNode.py +66 -1
- metripy/Trend/TrendAnalyzer.py +150 -0
- metripy/templates/html_report/css/styles.css +1386 -0
- metripy/templates/html_report/dependencies.html +411 -0
- metripy/templates/html_report/files.html +1080 -0
- metripy/templates/html_report/git_analysis.html +325 -0
- metripy/templates/html_report/images/logo.svg +31 -0
- metripy/templates/html_report/index.html +374 -0
- metripy/templates/html_report/js/charts.js +313 -0
- metripy/templates/html_report/js/dashboard.js +546 -0
- metripy/templates/html_report/js/git_analysis.js +383 -0
- metripy/templates/html_report/top_offenders.html +267 -0
- metripy/templates/html_report/trends.html +468 -0
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/METADATA +27 -9
- metripy-0.3.6.dist-info/RECORD +96 -0
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/licenses/LICENSE +1 -1
- metripy-0.2.7.dist-info/RECORD +0 -66
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/WHEEL +0 -0
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/entry_points.txt +0 -0
- {metripy-0.2.7.dist-info → metripy-0.3.6.dist-info}/top_level.txt +0 -0
metripy/Application/Analyzer.py
CHANGED
|
@@ -7,14 +7,15 @@ from metripy.Dependency.Dependency import Dependency
|
|
|
7
7
|
from metripy.Dependency.Npm.Npm import Npm
|
|
8
8
|
from metripy.Dependency.Pip.Pip import Pip
|
|
9
9
|
from metripy.Git.GitAnalyzer import GitAnalyzer
|
|
10
|
+
from metripy.Import.Json.JsonImporter import JsonImporter
|
|
10
11
|
from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
|
|
11
12
|
from metripy.LangAnalyzer.Php.PhpAnalyzer import PhpAnalyzer
|
|
12
13
|
from metripy.LangAnalyzer.Python.PythonAnalyzer import PythonAnalyzer
|
|
13
|
-
from metripy.LangAnalyzer.Typescript.TypescriptAnalyzer import
|
|
14
|
-
TypescriptAnalyzer
|
|
14
|
+
from metripy.LangAnalyzer.Typescript.TypescriptAnalyzer import TypescriptAnalyzer
|
|
15
15
|
from metripy.Metric.Code.FileMetrics import FileMetrics
|
|
16
16
|
from metripy.Metric.Git.GitMetrics import GitMetrics
|
|
17
17
|
from metripy.Metric.ProjectMetrics import ProjectMetrics
|
|
18
|
+
from metripy.Trend.TrendAnalyzer import TrendAnalyzer
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class Analyzer:
|
|
@@ -85,6 +86,18 @@ class Analyzer:
|
|
|
85
86
|
|
|
86
87
|
return dependencies
|
|
87
88
|
|
|
89
|
+
def add_trends(self, project_metrics: ProjectMetrics):
|
|
90
|
+
self.output.writeln("<info>Analyzing trends...</info>")
|
|
91
|
+
importer = JsonImporter(self.output)
|
|
92
|
+
historical_project_metrics = importer.import_data(self.config.history_path)
|
|
93
|
+
TrendAnalyzer().add_historical_file_trends(
|
|
94
|
+
project_metrics.file_metrics, historical_project_metrics.file_metrics
|
|
95
|
+
)
|
|
96
|
+
TrendAnalyzer().add_historical_project_trends(
|
|
97
|
+
project_metrics, historical_project_metrics
|
|
98
|
+
)
|
|
99
|
+
self.output.writeln("<success>Trends analyzed</success>")
|
|
100
|
+
|
|
88
101
|
def run(self, files: list[str]) -> ProjectMetrics:
|
|
89
102
|
git_stats = None
|
|
90
103
|
if self.config.git:
|
|
@@ -103,4 +116,11 @@ class Analyzer:
|
|
|
103
116
|
elif self.config.npm:
|
|
104
117
|
packages = self.analyze_npm()
|
|
105
118
|
|
|
106
|
-
|
|
119
|
+
if not self.config.history_path:
|
|
120
|
+
return ProjectMetrics(file_metrics, git_stats, packages)
|
|
121
|
+
|
|
122
|
+
# analyze trends
|
|
123
|
+
project_metrics = ProjectMetrics(file_metrics, git_stats, packages)
|
|
124
|
+
self.add_trends(project_metrics)
|
|
125
|
+
|
|
126
|
+
return project_metrics
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
|
|
3
3
|
from metripy.Application.Analyzer import Analyzer
|
|
4
4
|
from metripy.Application.Config.Parser import Parser
|
|
5
|
+
from metripy.Application.Info import Info
|
|
5
6
|
from metripy.Component.Debug.Debugger import Debugger
|
|
6
7
|
from metripy.Component.File.Finder import Finder
|
|
7
8
|
from metripy.Component.Output.CliOutput import CliOutput
|
|
@@ -14,9 +15,22 @@ class Application:
|
|
|
14
15
|
output = CliOutput()
|
|
15
16
|
|
|
16
17
|
# issues and debug
|
|
17
|
-
debugger = Debugger(output)
|
|
18
|
+
debugger = Debugger(output)
|
|
18
19
|
|
|
19
20
|
config = Parser().parse(argv)
|
|
21
|
+
if config.debug:
|
|
22
|
+
debugger.enable()
|
|
23
|
+
|
|
24
|
+
if config.version:
|
|
25
|
+
output.writeln(Info().get_version_info())
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if config.help:
|
|
29
|
+
output.writeln(Info().get_help())
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
if config.quiet:
|
|
33
|
+
output.set_quiet(True)
|
|
20
34
|
|
|
21
35
|
finder = Finder()
|
|
22
36
|
files = finder.fetch(config.project_configs)
|
|
@@ -46,7 +60,7 @@ class Application:
|
|
|
46
60
|
)
|
|
47
61
|
for report_config in project_config.reports:
|
|
48
62
|
reporter: ReporterInterface = ReporterFactory.create(
|
|
49
|
-
report_config, output
|
|
63
|
+
report_config, output, project_config.name
|
|
50
64
|
)
|
|
51
65
|
reporter.generate(project_metrics)
|
|
52
66
|
output.writeln(
|
|
@@ -4,6 +4,10 @@ from metripy.Application.Config.ProjectConfig import ProjectConfig
|
|
|
4
4
|
class Config:
|
|
5
5
|
def __init__(self):
|
|
6
6
|
self.project_configs: list[ProjectConfig] = []
|
|
7
|
+
self.quiet: bool = False
|
|
8
|
+
self.version: bool = False
|
|
9
|
+
self.help: bool = False
|
|
10
|
+
self.debug: bool = False
|
|
7
11
|
|
|
8
12
|
def to_dict(self) -> dict:
|
|
9
13
|
return {
|
|
@@ -11,3 +15,33 @@ class Config:
|
|
|
11
15
|
project_config.to_dict() for project_config in self.project_configs
|
|
12
16
|
],
|
|
13
17
|
}
|
|
18
|
+
|
|
19
|
+
def set(self, param: str, value: any) -> None:
|
|
20
|
+
if param == "quiet":
|
|
21
|
+
self.quiet = value
|
|
22
|
+
elif param == "version":
|
|
23
|
+
self.version = value
|
|
24
|
+
elif param == "help":
|
|
25
|
+
self.help = value
|
|
26
|
+
elif param == "debug":
|
|
27
|
+
self.debug = value
|
|
28
|
+
elif param.startswith("configs."):
|
|
29
|
+
self._set_project_value(param[len("configs.") :], value)
|
|
30
|
+
else:
|
|
31
|
+
# ignore unknown parameters
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
def _set_project_value(self, param: str, value: any) -> None:
|
|
35
|
+
keys = param.split(".")
|
|
36
|
+
project_name = keys[0]
|
|
37
|
+
project_config = next(
|
|
38
|
+
(pc for pc in self.project_configs if pc.name == project_name), None
|
|
39
|
+
)
|
|
40
|
+
if not project_config:
|
|
41
|
+
project_config = ProjectConfig(project_name)
|
|
42
|
+
self.project_configs.append(project_config)
|
|
43
|
+
if len(keys) > 1:
|
|
44
|
+
project_config.set(keys[1:], value)
|
|
45
|
+
else:
|
|
46
|
+
# weird but okay
|
|
47
|
+
return
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pathlib
|
|
3
3
|
|
|
4
|
-
from metripy.Application.Config.File.ConfigFileReaderInterface import
|
|
5
|
-
ConfigFileReaderInterface
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
from metripy.Application.Config.File.ConfigFileReaderInterface import (
|
|
5
|
+
ConfigFileReaderInterface,
|
|
6
|
+
)
|
|
7
|
+
from metripy.Application.Config.File.JsonConfigFileReader import JsonConfigFileReader
|
|
8
|
+
from metripy.Application.Config.File.YamlConfigFileReader import YamlConfigFileReader
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class ConfigFileReaderFactory:
|
|
@@ -17,7 +18,7 @@ class ConfigFileReaderFactory:
|
|
|
17
18
|
if extension == ".json":
|
|
18
19
|
return JsonConfigFileReader(filename)
|
|
19
20
|
elif extension == ".yaml" or extension == ".yml":
|
|
20
|
-
|
|
21
|
+
return YamlConfigFileReader(filename)
|
|
21
22
|
elif extension == ".xml":
|
|
22
23
|
raise NotImplementedError("XML support is not implemented yet")
|
|
23
24
|
else:
|
|
@@ -1,14 +1,81 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
3
|
|
|
3
4
|
from metripy.Application.Config.Config import Config
|
|
5
|
+
from metripy.Application.Config.GitConfig import GitConfig
|
|
6
|
+
from metripy.Application.Config.ProjectConfig import ProjectConfig
|
|
7
|
+
from metripy.Application.Config.ReportConfig import ReportConfig
|
|
4
8
|
|
|
5
9
|
|
|
6
10
|
class ConfigFileReaderInterface(ABC):
|
|
7
|
-
|
|
8
|
-
@abstractmethod
|
|
9
11
|
def __init__(self, filename: str):
|
|
10
|
-
|
|
12
|
+
self.filename = filename
|
|
11
13
|
|
|
12
14
|
@abstractmethod
|
|
13
15
|
def read(self, config: Config) -> None:
|
|
14
16
|
pass
|
|
17
|
+
|
|
18
|
+
def resolve_path(self, path: str) -> str:
|
|
19
|
+
return path
|
|
20
|
+
|
|
21
|
+
def parse_data(self, data: dict, config: Config) -> None:
|
|
22
|
+
# configs
|
|
23
|
+
if configs := data.get("configs"):
|
|
24
|
+
for project_name, project_config in configs.items():
|
|
25
|
+
project_config = self.parse_config_json(project_name, project_config)
|
|
26
|
+
config.project_configs.append(project_config)
|
|
27
|
+
|
|
28
|
+
def parse_config_json(self, project_name: str, data: dict) -> ProjectConfig:
|
|
29
|
+
project_config = ProjectConfig(project_name)
|
|
30
|
+
|
|
31
|
+
# extensions
|
|
32
|
+
if base_path := data.get("base_path"):
|
|
33
|
+
project_config.base_path = base_path
|
|
34
|
+
|
|
35
|
+
# includes
|
|
36
|
+
if includes := data.get("includes"):
|
|
37
|
+
files = []
|
|
38
|
+
# with config file, includes are relative to the config file
|
|
39
|
+
for include in includes:
|
|
40
|
+
include = self.resolve_path(include)
|
|
41
|
+
files.append(include)
|
|
42
|
+
|
|
43
|
+
project_config.includes = files
|
|
44
|
+
|
|
45
|
+
# extensions
|
|
46
|
+
if extensions := data.get("extensions"):
|
|
47
|
+
project_config.extensions = extensions
|
|
48
|
+
|
|
49
|
+
# excludes
|
|
50
|
+
if excludes := data.get("excludes"):
|
|
51
|
+
project_config.excludes = excludes
|
|
52
|
+
|
|
53
|
+
# reports
|
|
54
|
+
if reports := data.get("reports"):
|
|
55
|
+
for report_type, path in reports.items():
|
|
56
|
+
path = self.resolve_path(path)
|
|
57
|
+
project_config.reports.append(ReportConfig(report_type, path))
|
|
58
|
+
|
|
59
|
+
# git
|
|
60
|
+
if git := data.get("git"):
|
|
61
|
+
project_config.git = GitConfig()
|
|
62
|
+
project_config.git.repo = project_config.base_path
|
|
63
|
+
project_config.git.branch = git.get("branch", project_config.git.branch)
|
|
64
|
+
|
|
65
|
+
# composer
|
|
66
|
+
if composer := data.get("composer"):
|
|
67
|
+
project_config.composer = composer
|
|
68
|
+
|
|
69
|
+
# pip
|
|
70
|
+
if pip := data.get("pip"):
|
|
71
|
+
project_config.pip = pip
|
|
72
|
+
|
|
73
|
+
# npm
|
|
74
|
+
if npm := data.get("npm"):
|
|
75
|
+
project_config.npm = npm
|
|
76
|
+
|
|
77
|
+
# trends
|
|
78
|
+
if history_path := data.get("trends"):
|
|
79
|
+
project_config.history_path = self.resolve_path(history_path)
|
|
80
|
+
|
|
81
|
+
return project_config
|
|
@@ -1,82 +1,17 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import os
|
|
3
2
|
|
|
4
3
|
from metripy.Application.Config.Config import Config
|
|
5
|
-
from metripy.Application.Config.File.ConfigFileReaderInterface import
|
|
6
|
-
ConfigFileReaderInterface
|
|
7
|
-
|
|
8
|
-
from metripy.Application.Config.ProjectConfig import ProjectConfig
|
|
9
|
-
from metripy.Application.Config.ReportConfig import ReportConfig
|
|
4
|
+
from metripy.Application.Config.File.ConfigFileReaderInterface import (
|
|
5
|
+
ConfigFileReaderInterface,
|
|
6
|
+
)
|
|
10
7
|
|
|
11
8
|
|
|
12
9
|
class JsonConfigFileReader(ConfigFileReaderInterface):
|
|
13
10
|
def __init__(self, filename: str):
|
|
14
|
-
|
|
11
|
+
super().__init__(filename)
|
|
15
12
|
|
|
16
13
|
def read(self, config: Config) -> None:
|
|
17
14
|
with open(self.filename, "r") as file:
|
|
18
15
|
json_data = json.load(file)
|
|
19
16
|
|
|
20
|
-
self.
|
|
21
|
-
|
|
22
|
-
def resolve_path(self, path: str) -> str:
|
|
23
|
-
return os.path.join(os.path.dirname(self.filename), path)
|
|
24
|
-
|
|
25
|
-
def parse_json(self, json_data: dict, config: Config) -> None:
|
|
26
|
-
|
|
27
|
-
# configs
|
|
28
|
-
if configs := json_data.get("configs"):
|
|
29
|
-
for project_name, project_config in configs.items():
|
|
30
|
-
project_config = self.parse_config_json(project_name, project_config)
|
|
31
|
-
config.project_configs.append(project_config)
|
|
32
|
-
|
|
33
|
-
def parse_config_json(self, project_name: str, json_data: dict) -> ProjectConfig:
|
|
34
|
-
project_config = ProjectConfig(project_name)
|
|
35
|
-
|
|
36
|
-
# extensions
|
|
37
|
-
if base_path := json_data.get("base_path"):
|
|
38
|
-
project_config.base_path = base_path
|
|
39
|
-
|
|
40
|
-
# includes
|
|
41
|
-
if includes := json_data.get("includes"):
|
|
42
|
-
files = []
|
|
43
|
-
# with config file, includes are relative to the config file
|
|
44
|
-
for include in includes:
|
|
45
|
-
include = self.resolve_path(include)
|
|
46
|
-
files.append(include)
|
|
47
|
-
|
|
48
|
-
project_config.includes = files
|
|
49
|
-
|
|
50
|
-
# extensions
|
|
51
|
-
if extensions := json_data.get("extensions"):
|
|
52
|
-
project_config.extensions = extensions
|
|
53
|
-
|
|
54
|
-
# excludes
|
|
55
|
-
if excludes := json_data.get("excludes"):
|
|
56
|
-
project_config.excludes = excludes
|
|
57
|
-
|
|
58
|
-
# reports
|
|
59
|
-
if reports := json_data.get("reports"):
|
|
60
|
-
for report_type, path in reports.items():
|
|
61
|
-
path = self.resolve_path(path)
|
|
62
|
-
project_config.reports.append(ReportConfig(report_type, path))
|
|
63
|
-
|
|
64
|
-
# git
|
|
65
|
-
if git := json_data.get("git"):
|
|
66
|
-
project_config.git = GitConfig()
|
|
67
|
-
project_config.git.repo = project_config.base_path
|
|
68
|
-
project_config.git.branch = git.get("branch", project_config.git.branch)
|
|
69
|
-
|
|
70
|
-
# composer
|
|
71
|
-
if composer := json_data.get("composer"):
|
|
72
|
-
project_config.composer = composer
|
|
73
|
-
|
|
74
|
-
# pip
|
|
75
|
-
if pip := json_data.get("pip"):
|
|
76
|
-
project_config.pip = pip
|
|
77
|
-
|
|
78
|
-
# npm
|
|
79
|
-
if npm := json_data.get("npm"):
|
|
80
|
-
project_config.npm = npm
|
|
81
|
-
|
|
82
|
-
return project_config
|
|
17
|
+
self.parse_data(json_data, config)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
|
|
3
|
+
from metripy.Application.Config.Config import Config
|
|
4
|
+
from metripy.Application.Config.File.ConfigFileReaderInterface import (
|
|
5
|
+
ConfigFileReaderInterface,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class YamlConfigFileReader(ConfigFileReaderInterface):
|
|
10
|
+
def __init__(self, filename: str):
|
|
11
|
+
super().__init__(filename)
|
|
12
|
+
|
|
13
|
+
def read(self, config: Config) -> None:
|
|
14
|
+
with open(self.filename, "r") as file:
|
|
15
|
+
yaml_data = yaml.safe_load(file)
|
|
16
|
+
|
|
17
|
+
self.parse_data(yaml_data, config)
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
3
|
from metripy.Application.Config.Config import Config
|
|
4
|
-
from metripy.Application.Config.File.ConfigFileReaderFactory import
|
|
5
|
-
ConfigFileReaderFactory
|
|
4
|
+
from metripy.Application.Config.File.ConfigFileReaderFactory import (
|
|
5
|
+
ConfigFileReaderFactory,
|
|
6
|
+
)
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class Parser:
|
|
9
10
|
def parse(self, argv: list[str]) -> Config:
|
|
10
11
|
config = Config()
|
|
11
12
|
|
|
12
|
-
if argv[0]
|
|
13
|
-
|
|
14
|
-
pass
|
|
15
|
-
argv.pop(0)
|
|
13
|
+
if argv[0].endswith("metripy.py") or argv[0].endswith("metripy"):
|
|
14
|
+
argv.pop(0)
|
|
16
15
|
|
|
17
16
|
# check for a config file
|
|
18
|
-
|
|
17
|
+
key = 0
|
|
18
|
+
while key < len(argv):
|
|
19
|
+
arg = argv[key]
|
|
19
20
|
if matches := re.search(r"^--config=(.+)$", arg):
|
|
20
21
|
fileReader = ConfigFileReaderFactory.createFromFileName(
|
|
21
22
|
matches.group(1)
|
|
@@ -23,9 +24,21 @@ class Parser:
|
|
|
23
24
|
fileReader.read(config)
|
|
24
25
|
argv.pop(key)
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
# arguments with options
|
|
28
|
+
elif matches := re.search(r"^--([\w]+(?:\.[\w]+)*)=(.*)$", arg):
|
|
29
|
+
param = matches.group(1)
|
|
30
|
+
value = matches.group(2)
|
|
31
|
+
config.set(param, value)
|
|
32
|
+
argv.pop(key)
|
|
33
|
+
|
|
34
|
+
# arguments without options
|
|
35
|
+
elif matches := re.search(r"^--([\w]+(?:\.[\w]+)*)$", arg):
|
|
36
|
+
param = matches.group(1)
|
|
37
|
+
config.set(param, True)
|
|
38
|
+
argv.pop(key)
|
|
39
|
+
else:
|
|
40
|
+
key += 1
|
|
41
|
+
|
|
42
|
+
# TODO handle remaining arguments
|
|
30
43
|
|
|
31
44
|
return config
|
|
@@ -14,6 +14,7 @@ class ProjectConfig:
|
|
|
14
14
|
self.pip: bool = False
|
|
15
15
|
self.npm: bool = False
|
|
16
16
|
self.reports: list[ReportConfig] = []
|
|
17
|
+
self.history_path: str | None = None
|
|
17
18
|
|
|
18
19
|
def to_dict(self) -> dict:
|
|
19
20
|
return {
|
|
@@ -22,6 +23,69 @@ class ProjectConfig:
|
|
|
22
23
|
"includes": self.includes,
|
|
23
24
|
"excludes": self.excludes,
|
|
24
25
|
"extensions": self.extensions,
|
|
26
|
+
"composer": self.composer,
|
|
27
|
+
"pip": self.pip,
|
|
28
|
+
"npm": self.npm,
|
|
25
29
|
"git": self.git.to_dict() if self.git else None,
|
|
26
30
|
"reports": [report.to_dict() for report in self.reports],
|
|
31
|
+
"history_path": self.history_path,
|
|
27
32
|
}
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def str_to_bool(value):
|
|
36
|
+
if isinstance(value, bool):
|
|
37
|
+
return value
|
|
38
|
+
return str(value).lower() in ("true", "1", "yes")
|
|
39
|
+
|
|
40
|
+
def set(self, keys: list[str], value: any) -> None:
|
|
41
|
+
if len(keys) == 0:
|
|
42
|
+
return
|
|
43
|
+
primary_key = keys[0]
|
|
44
|
+
# single value
|
|
45
|
+
if primary_key == "base_path":
|
|
46
|
+
self.base_path = value
|
|
47
|
+
elif primary_key == "pip":
|
|
48
|
+
self.pip = self.str_to_bool(value)
|
|
49
|
+
elif primary_key == "npm":
|
|
50
|
+
self.npm = self.str_to_bool(value)
|
|
51
|
+
elif primary_key == "composer":
|
|
52
|
+
self.composer = self.str_to_bool(value)
|
|
53
|
+
elif primary_key == "trends":
|
|
54
|
+
self.history_path = value
|
|
55
|
+
elif primary_key == "git":
|
|
56
|
+
self.git = GitConfig()
|
|
57
|
+
self.git.repo = self.base_path
|
|
58
|
+
self.git.branch = value
|
|
59
|
+
|
|
60
|
+
# list values
|
|
61
|
+
elif primary_key == "includes":
|
|
62
|
+
if value == "":
|
|
63
|
+
self.includes = []
|
|
64
|
+
else:
|
|
65
|
+
self.includes.append(value)
|
|
66
|
+
elif primary_key == "excludes":
|
|
67
|
+
if value == "":
|
|
68
|
+
self.excludes = []
|
|
69
|
+
else:
|
|
70
|
+
self.excludes.append(value)
|
|
71
|
+
elif primary_key == "extensions":
|
|
72
|
+
if value == "":
|
|
73
|
+
self.extensions = []
|
|
74
|
+
else:
|
|
75
|
+
self.extensions.append(value)
|
|
76
|
+
|
|
77
|
+
# dict values
|
|
78
|
+
elif primary_key == "reports":
|
|
79
|
+
if len(keys) == 1:
|
|
80
|
+
return
|
|
81
|
+
report_type = keys[1]
|
|
82
|
+
report_path = value
|
|
83
|
+
if value != "":
|
|
84
|
+
self.reports.append(ReportConfig(report_type, report_path))
|
|
85
|
+
else:
|
|
86
|
+
report_config = next(
|
|
87
|
+
(rc for rc in self.reports if rc.type == report_type), None
|
|
88
|
+
)
|
|
89
|
+
if not report_config:
|
|
90
|
+
return
|
|
91
|
+
self.reports.remove(report_config)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from importlib.metadata import version, metadata
|
|
2
|
+
import toml
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Info:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.version = self._get_version()
|
|
8
|
+
self.url = self._get_homepage_url()
|
|
9
|
+
|
|
10
|
+
def _get_pyproject_data(self) -> dict:
|
|
11
|
+
with open("pyproject.toml", "r") as file:
|
|
12
|
+
data = toml.load(file)
|
|
13
|
+
return data
|
|
14
|
+
|
|
15
|
+
def _get_version(self) -> str:
|
|
16
|
+
"""Get version from installed package metadata"""
|
|
17
|
+
try:
|
|
18
|
+
return version("metripy")
|
|
19
|
+
except Exception:
|
|
20
|
+
# Fallback for development if not installed
|
|
21
|
+
return self._get_pyproject_data()["project"]["version"]
|
|
22
|
+
|
|
23
|
+
def _get_homepage_url(self) -> str:
|
|
24
|
+
"""Get homepage URL from installed package metadata"""
|
|
25
|
+
try:
|
|
26
|
+
meta = metadata("metripy")
|
|
27
|
+
# Try to get Home-Page from metadata
|
|
28
|
+
homepage = meta.get("Home-Page")
|
|
29
|
+
if not homepage:
|
|
30
|
+
# Try Project-URL field
|
|
31
|
+
for line in meta.get_all("Project-URL") or []:
|
|
32
|
+
if line.startswith("Homepage"):
|
|
33
|
+
homepage = line.split(",", 1)[1].strip()
|
|
34
|
+
break
|
|
35
|
+
return homepage or "no homepage found"
|
|
36
|
+
except Exception:
|
|
37
|
+
# Fallback
|
|
38
|
+
return self._get_pyproject_data()["project"]["urls"]["Homepage"]
|
|
39
|
+
|
|
40
|
+
def get_version(self) -> str:
|
|
41
|
+
return self.version
|
|
42
|
+
|
|
43
|
+
def get_version_info(self) -> str:
|
|
44
|
+
return f"""
|
|
45
|
+
Metripy {self.get_version()}
|
|
46
|
+
{self.url}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def get_help(self) -> str:
|
|
50
|
+
return (
|
|
51
|
+
self.get_version_info()
|
|
52
|
+
+ """
|
|
53
|
+
Usage: metripy [options]
|
|
54
|
+
Options:
|
|
55
|
+
--config=<file> Use a custom config file
|
|
56
|
+
--version Show the version and exit
|
|
57
|
+
--help Show this help message and exit
|
|
58
|
+
--debug Enable debug mode
|
|
59
|
+
--quiet Disable output
|
|
60
|
+
"""
|
|
61
|
+
)
|
metripy/Dependency/Dependency.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class Dependency:
|
|
2
|
-
def __init__(self, name: str, version: str):
|
|
5
|
+
def __init__(self, name: str, version: str | None):
|
|
3
6
|
self.name = name
|
|
4
7
|
self.version = version
|
|
5
8
|
self.latest: str = ""
|
|
@@ -28,3 +31,16 @@ class Dependency:
|
|
|
28
31
|
"downloads_monthly": self.downloads_monthly,
|
|
29
32
|
"licenses": ",".join(self.license),
|
|
30
33
|
}
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def get_lisence_distribution(dependencies: list[Self]) -> dict[str, int]:
|
|
37
|
+
license_by_type = {}
|
|
38
|
+
|
|
39
|
+
dependency: Dependency
|
|
40
|
+
for dependency in dependencies:
|
|
41
|
+
for license_name in dependency.license:
|
|
42
|
+
if license_name not in license_by_type.keys():
|
|
43
|
+
license_by_type[license_name] = 0
|
|
44
|
+
license_by_type[license_name] += 1
|
|
45
|
+
|
|
46
|
+
return license_by_type
|
metripy/Dependency/Pip/Pip.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import re
|
|
3
2
|
|
|
4
3
|
import toml
|
|
5
4
|
|
|
@@ -23,47 +22,38 @@ class Pip:
|
|
|
23
22
|
return [item for item in packages if item is not None]
|
|
24
23
|
|
|
25
24
|
def get_from_requirements_txt(self, path: str) -> list[Dependency]:
|
|
26
|
-
requirements = []
|
|
27
|
-
|
|
28
|
-
pattern = re.compile(r"([a-zA-Z0-9_\-]+)([<>=!~]+[^\s]+)?")
|
|
29
25
|
with open(os.path.join(path, "requirements.txt"), "r") as file:
|
|
30
26
|
lines = file.readlines()
|
|
31
|
-
|
|
27
|
+
return self._parse_dependencies(lines)
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
if line and not line.startswith("#"):
|
|
35
|
-
match = pattern.match(line)
|
|
36
|
-
if match:
|
|
37
|
-
name = match.group(1)
|
|
38
|
-
version = match.group(2) if match.group(2) else None
|
|
39
|
-
requirements.append(Dependency(name, version))
|
|
40
|
-
return requirements
|
|
29
|
+
return []
|
|
41
30
|
|
|
42
31
|
def get_from_pyproject_toml(self, path: str) -> list[Dependency]:
|
|
43
|
-
dependencies = []
|
|
44
|
-
|
|
45
32
|
with open(os.path.join(path, "pyproject.toml"), "r") as f:
|
|
46
33
|
data = toml.load(f)
|
|
47
34
|
|
|
48
35
|
# For PEP 621 / setuptools projects
|
|
49
36
|
if "project" in data:
|
|
50
37
|
deps = data["project"].get("dependencies", [])
|
|
51
|
-
|
|
52
|
-
# dep is a string like "requests>=2.32.5"
|
|
53
|
-
# You can split it if needed
|
|
54
|
-
if "==" in dep:
|
|
55
|
-
name, version = dep.split("==")
|
|
56
|
-
elif ">=" in dep:
|
|
57
|
-
name, version = dep.split(">=")
|
|
58
|
-
else:
|
|
59
|
-
name, version = dep, None
|
|
60
|
-
dependencies.append(
|
|
61
|
-
Dependency(name.strip(), version.strip() if version else None)
|
|
62
|
-
)
|
|
38
|
+
return self._parse_dependencies(deps)
|
|
63
39
|
|
|
64
|
-
return
|
|
40
|
+
return []
|
|
65
41
|
|
|
42
|
+
def _parse_dependencies(self, lines: list[str]) -> list[Dependency]:
|
|
43
|
+
dependencies = []
|
|
44
|
+
for dep in lines:
|
|
45
|
+
dep = dep.strip()
|
|
46
|
+
if not dep or dep.startswith("#"):
|
|
47
|
+
continue
|
|
48
|
+
# dep is a string like "requests>=2.32.5"
|
|
49
|
+
if "==" in dep:
|
|
50
|
+
name, version = dep.split("==")
|
|
51
|
+
elif ">=" in dep:
|
|
52
|
+
name, version = dep.split(">=")
|
|
53
|
+
else:
|
|
54
|
+
name, version = dep, None
|
|
55
|
+
dependencies.append(
|
|
56
|
+
Dependency(name.strip(), version.strip() if version else None)
|
|
57
|
+
)
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
pip = Pip()
|
|
69
|
-
pip.get_dependencies("./")
|
|
59
|
+
return dependencies
|
metripy/Dependency/Pip/PyPi.py
CHANGED
|
@@ -16,6 +16,7 @@ class PyPi:
|
|
|
16
16
|
print(f"Package '{dependency.name}' has no info section")
|
|
17
17
|
return dependency
|
|
18
18
|
|
|
19
|
+
dependency.type = "pip"
|
|
19
20
|
dependency.description = info.get("summary")
|
|
20
21
|
dependency.repository = info.get("project_url") or info.get("home_page")
|
|
21
22
|
if info.get("license"):
|
metripy/Git/GitAnalyzer.py
CHANGED
|
@@ -9,8 +9,6 @@ from metripy.Metric.Git.GitMetrics import GitMetrics
|
|
|
9
9
|
|
|
10
10
|
class GitAnalyzer:
|
|
11
11
|
def __init__(self, git_config: GitConfig):
|
|
12
|
-
print(git_config.repo)
|
|
13
|
-
print(git_config.branch)
|
|
14
12
|
self.repo = Repo(git_config.repo)
|
|
15
13
|
self.branch_name = git_config.branch
|
|
16
14
|
|
|
@@ -22,7 +20,6 @@ class GitAnalyzer:
|
|
|
22
20
|
first_of_month_last_year = datetime(now.year - 1, now.month, 1)
|
|
23
21
|
# first_of_month_last_year = datetime(now.year, now.month, 1)
|
|
24
22
|
after_date = first_of_month_last_year.strftime("%Y-%m-%d")
|
|
25
|
-
print(f"analyzing from {after_date}")
|
|
26
23
|
|
|
27
24
|
return self.get_metrics(after_date)
|
|
28
25
|
|