nwcommitaverages 2.0.1__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.
- nwcommitaverages-2.0.1.dist-info/METADATA +162 -0
- nwcommitaverages-2.0.1.dist-info/RECORD +8 -0
- nwcommitaverages-2.0.1.dist-info/WHEEL +5 -0
- nwcommitaverages-2.0.1.dist-info/entry_points.txt +2 -0
- nwcommitaverages-2.0.1.dist-info/top_level.txt +3 -0
- nwcommitaverages.py +299 -0
- nwcommitaveragescli.py +434 -0
- setupinfo.py +14 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nwcommitaverages
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: A library designed to calculate the average time between Git commits.
|
|
5
|
+
Home-page: https://github.com/numbworks/nwcommitaverages
|
|
6
|
+
Author: numbworks
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: tabulate>=0.9.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: description
|
|
14
|
+
Dynamic: description-content-type
|
|
15
|
+
Dynamic: home-page
|
|
16
|
+
Dynamic: license
|
|
17
|
+
Dynamic: requires-dist
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
# nwcommitaverages
|
|
22
|
+
|
|
23
|
+
## Introduction
|
|
24
|
+
|
|
25
|
+
This packages consists of the following modules:
|
|
26
|
+
|
|
27
|
+
- `nwcommitaverages` is an application designed to calculate the average time between Git commits.
|
|
28
|
+
- `nwcommitaveragescli` is a command-line application built on the top of `nwcommitaverages`.
|
|
29
|
+
|
|
30
|
+
## Pre-Requisites
|
|
31
|
+
|
|
32
|
+
In order to work, this library requires to be run against a local Git repository and that Git itself is installed on the machine.
|
|
33
|
+
|
|
34
|
+
## Getting Started
|
|
35
|
+
|
|
36
|
+
I'll use Debian as operating system, but the concept is similar for Windows or Mac.
|
|
37
|
+
|
|
38
|
+
Procedure:
|
|
39
|
+
|
|
40
|
+
1. Install `pip` and `venv`:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
sudo apt install python3-pip python3.12-venv
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. Create a virtual environment:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
python3 -m venv /tmp/nwcommitaverages
|
|
50
|
+
source /tmp/nwcommitaverages/bin/activate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. Download and install the package:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pip install nwcommitaverages
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
4. Enter in the interactive Python environment:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
python3
|
|
63
|
+
```
|
|
64
|
+
```
|
|
65
|
+
Python 3.12.3 (main, Aug 31 2026, 10:18:26) [GCC 13.3.0] on linux
|
|
66
|
+
Type "help", "copyright", "credits" or "license" for more information.
|
|
67
|
+
>>>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
5. Try out if the library works:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from nwcommitaverages import CommitAverageCalculator
|
|
74
|
+
|
|
75
|
+
ca_calculator : CommitAverageCalculator = CommitAverageCalculator()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
6. Exit from the interactive Python environment:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
exit()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
7. Try out if the CLI application works:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
nwcommitaveragescli
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
8. Deactivate and delete the virtual environment:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
deactivate
|
|
94
|
+
rm -rf /tmp/nwcommitaverages
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
9. Done!
|
|
98
|
+
|
|
99
|
+
## Examples
|
|
100
|
+
|
|
101
|
+
To run it against the current folder (we assume it contains a `git` repository and that `git` itself is installed):
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
nwcommitaveragescli
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
*****************************************************************
|
|
109
|
+
'##::: ##:'##:::::'##::'######:::::'###::::'##::::'##::'######:::
|
|
110
|
+
###:: ##: ##:'##: ##:'##... ##:::'## ##::: ##:::: ##:'##... ##::
|
|
111
|
+
####: ##: ##: ##: ##: ##:::..:::'##:. ##:: ##:::: ##: ##:::..:::
|
|
112
|
+
## ## ##: ##: ##: ##: ##:::::::'##:::. ##: ##:::: ##: ##::'####:
|
|
113
|
+
##. ####: ##: ##: ##: ##::::::: #########:. ##:: ##:: ##::: ##::
|
|
114
|
+
##:. ###: ##: ##: ##: ##::: ##: ##.... ##::. ## ##::: ##::: ##::
|
|
115
|
+
##::. ##:. ###. ###::. ######:: ##:::: ##:::. ###::::. ######:::
|
|
116
|
+
..::::..:::...::...::::......:::..:::::..:::::...::::::......::::
|
|
117
|
+
**********************************************Version: 2.0.1*****
|
|
118
|
+
|
|
119
|
+
Folder:'/home'
|
|
120
|
+
|
|
121
|
+
+-------------+--------+-----------+---------------+------------+
|
|
122
|
+
| YearMonth | Days | Commits | DailyAvgMin | RefNames |
|
|
123
|
+
+=============+========+===========+===============+============+
|
|
124
|
+
| 2025-05 | 2 | 29 | 16.71 | |
|
|
125
|
+
+-------------+--------+-----------+---------------+------------+
|
|
126
|
+
| 2026-03 | 2 | 26 | 16.43 | |
|
|
127
|
+
+-------------+--------+-----------+---------------+------------+
|
|
128
|
+
| 2026-04 | 1 | 4 | 73.76 | |
|
|
129
|
+
+-------------+--------+-----------+---------------+------------+
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
To run it against another folder (which doesn't contain a `git` repository):
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
nwcommitaveragescli --folder_path /home/test
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
*****************************************************************
|
|
140
|
+
'##::: ##:'##:::::'##::'######:::::'###::::'##::::'##::'######:::
|
|
141
|
+
###:: ##: ##:'##: ##:'##... ##:::'## ##::: ##:::: ##:'##... ##::
|
|
142
|
+
####: ##: ##: ##: ##: ##:::..:::'##:. ##:: ##:::: ##: ##:::..:::
|
|
143
|
+
## ## ##: ##: ##: ##: ##:::::::'##:::. ##: ##:::: ##: ##::'####:
|
|
144
|
+
##. ####: ##: ##: ##: ##::::::: #########:. ##:: ##:: ##::: ##::
|
|
145
|
+
##:. ###: ##: ##: ##: ##::: ##: ##.... ##::. ## ##::: ##::: ##::
|
|
146
|
+
##::. ##:. ###. ###::. ######:: ##:::: ##:::. ###::::. ######:::
|
|
147
|
+
..::::..:::...::...::::......:::..:::::..:::::...::::::......::::
|
|
148
|
+
**********************************************Version: 2.0.1*****
|
|
149
|
+
|
|
150
|
+
Folder:'/home/test'
|
|
151
|
+
|
|
152
|
+
Command '['git', '-C', '/home/test', 'log', '--pretty=format:%cs;%ct;%D', '--reverse']' returned non-zero exit status 128.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Links
|
|
156
|
+
|
|
157
|
+
- Source: https://github.com/numbworks/nwcommitaverages
|
|
158
|
+
- License: https://github.com/numbworks/nwcommitaverages/blob/master/LICENSE
|
|
159
|
+
|
|
160
|
+
## Total Downloads (PyPi)
|
|
161
|
+
|
|
162
|
+
[](https://pepy.tech/projects/nwcommitaverages)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
nwcommitaverages.py,sha256=bluldKPfdUR6mm1WEIyuTAMjC9BH6PfRrht6ePCHypE,10307
|
|
2
|
+
nwcommitaveragescli.py,sha256=C60Xs8UOstkj9P6iWKanOOukxdGn86RJFaoIeNegf6k,14728
|
|
3
|
+
setupinfo.py,sha256=eO1b2hkURCT5LotPPCK0S_Ehj33WYys4ZZqDcHmT8Kg,521
|
|
4
|
+
nwcommitaverages-2.0.1.dist-info/METADATA,sha256=jrZZBbKfPloPaqdmS7b3gAOztyM6JTriKkyIvWYFnKs,4777
|
|
5
|
+
nwcommitaverages-2.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
nwcommitaverages-2.0.1.dist-info/entry_points.txt,sha256=-1fPgBfCMEm-7bwELkDFtdWjJfGytqrAoENyZAXbPdE,65
|
|
7
|
+
nwcommitaverages-2.0.1.dist-info/top_level.txt,sha256=ALyn6uonUIMxthhxGW0glYBCmfiNp9H8kE-ZiBILy5I,47
|
|
8
|
+
nwcommitaverages-2.0.1.dist-info/RECORD,,
|
nwcommitaverages.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'''
|
|
2
|
+
A CLI application designed to calculate the average time between git commits.
|
|
3
|
+
|
|
4
|
+
Alias: nwcavg
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
# GLOBAL MODULES
|
|
8
|
+
import subprocess
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from subprocess import CompletedProcess
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
# NW/LOCAL MODULES
|
|
16
|
+
# CONSTANTS
|
|
17
|
+
# STATIC CLASSES
|
|
18
|
+
# CLASSES
|
|
19
|
+
@dataclass(frozen = True)
|
|
20
|
+
class CommitItem():
|
|
21
|
+
|
|
22
|
+
'''Collects all the necessary information related to a commit timestamp.'''
|
|
23
|
+
|
|
24
|
+
date_str : str
|
|
25
|
+
timestamp_int : int
|
|
26
|
+
timestamp_dt : datetime
|
|
27
|
+
ref_names : list[str]
|
|
28
|
+
@dataclass(frozen = True)
|
|
29
|
+
class DailyStatus():
|
|
30
|
+
|
|
31
|
+
'''Represents a daily status.'''
|
|
32
|
+
|
|
33
|
+
date_str : str
|
|
34
|
+
timestamps : list[int]
|
|
35
|
+
avg_minutes : float
|
|
36
|
+
ref_names : list[str]
|
|
37
|
+
@dataclass(frozen = True)
|
|
38
|
+
class MonthlyStatus():
|
|
39
|
+
|
|
40
|
+
'''Represents a monthly status.'''
|
|
41
|
+
|
|
42
|
+
year_month : str
|
|
43
|
+
dates : int
|
|
44
|
+
timestamps : list[int]
|
|
45
|
+
avg_minutes : float
|
|
46
|
+
ref_names : list[str]
|
|
47
|
+
@dataclass(frozen = True)
|
|
48
|
+
class Summary():
|
|
49
|
+
|
|
50
|
+
'''Represents a summary.'''
|
|
51
|
+
|
|
52
|
+
commit_items : list[CommitItem]
|
|
53
|
+
daily_statuses : list[DailyStatus]
|
|
54
|
+
monthly_statuses : list[MonthlyStatus]
|
|
55
|
+
class CommitAverageCalculator():
|
|
56
|
+
|
|
57
|
+
'''Calculates custom averages related to the current git repository.'''
|
|
58
|
+
|
|
59
|
+
def __create_timestamp_dt(self, timestamp_int : int) -> datetime:
|
|
60
|
+
|
|
61
|
+
'''Converts an integer timestamp to datetime.'''
|
|
62
|
+
|
|
63
|
+
dt : datetime = datetime.fromtimestamp(int(timestamp_int), tz = timezone.utc)
|
|
64
|
+
|
|
65
|
+
return dt
|
|
66
|
+
def __create_commit_item(self, triplet_lst : list[str]) -> CommitItem:
|
|
67
|
+
|
|
68
|
+
'''Creates a CommitItem object out of the provided arguments.'''
|
|
69
|
+
|
|
70
|
+
date_str : str = triplet_lst[0]
|
|
71
|
+
timestamp_int : int = int(triplet_lst[1])
|
|
72
|
+
timestamp_dt : datetime = self.__create_timestamp_dt(timestamp_int = timestamp_int)
|
|
73
|
+
|
|
74
|
+
ref_names : list[str] = []
|
|
75
|
+
if triplet_lst[2]:
|
|
76
|
+
ref_names = triplet_lst[2].split(",")
|
|
77
|
+
|
|
78
|
+
commit_item : CommitItem = CommitItem(
|
|
79
|
+
date_str = date_str,
|
|
80
|
+
timestamp_int = timestamp_int,
|
|
81
|
+
timestamp_dt = timestamp_dt,
|
|
82
|
+
ref_names = ref_names
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return commit_item
|
|
86
|
+
def __update_ref_names(self, commit_item : CommitItem, ref_names : list[str]) -> CommitItem:
|
|
87
|
+
|
|
88
|
+
'''Update commit_item.ref_names with ref_names.'''
|
|
89
|
+
|
|
90
|
+
updated : CommitItem = CommitItem(
|
|
91
|
+
date_str = commit_item.date_str,
|
|
92
|
+
timestamp_int = commit_item.timestamp_int,
|
|
93
|
+
timestamp_dt = commit_item.timestamp_dt,
|
|
94
|
+
ref_names = ref_names
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return updated
|
|
98
|
+
def __clean_ref_names(self, ref_names : list[str]) -> list[str]:
|
|
99
|
+
|
|
100
|
+
'''Clean branch names.'''
|
|
101
|
+
|
|
102
|
+
if not ref_names:
|
|
103
|
+
return ref_names
|
|
104
|
+
|
|
105
|
+
cleaned : list[str] = []
|
|
106
|
+
|
|
107
|
+
for ref_name in ref_names:
|
|
108
|
+
|
|
109
|
+
ref_name = ref_name.replace(" ", "")
|
|
110
|
+
|
|
111
|
+
if ref_name in ["origin/HEAD", "origin/master", "HEAD->master"]:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if ref_name.startswith("tag:"):
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
if ref_name.startswith("origin/"):
|
|
118
|
+
ref_name = ref_name.replace("origin/", "")
|
|
119
|
+
cleaned.append(ref_name)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
return sorted(set(cleaned))
|
|
123
|
+
def __create_year_month(self, date_str : str) -> str:
|
|
124
|
+
|
|
125
|
+
'''"2023-11-17" -> 2023-11'''
|
|
126
|
+
|
|
127
|
+
year_month : str = date_str[:7]
|
|
128
|
+
|
|
129
|
+
return year_month
|
|
130
|
+
def __count_days_in_month(self, daily_statuses : list[DailyStatus]) -> int:
|
|
131
|
+
|
|
132
|
+
'''Counts days in month.'''
|
|
133
|
+
|
|
134
|
+
return len(set(daily_status.date_str for daily_status in daily_statuses))
|
|
135
|
+
def __extract_avg_minutes(self, daily_statuses : list[DailyStatus]) -> list[float]:
|
|
136
|
+
|
|
137
|
+
'''Extracts the avg_minutes from daily_statuses.'''
|
|
138
|
+
|
|
139
|
+
return [status.avg_minutes for status in daily_statuses]
|
|
140
|
+
def __get_commit_items(self, folder_path: Optional[str]) -> list[CommitItem]:
|
|
141
|
+
|
|
142
|
+
'''
|
|
143
|
+
Retrieve a collection of CommitItem objects out of the git log.
|
|
144
|
+
|
|
145
|
+
git log output:
|
|
146
|
+
2023-08-16;1692207406;
|
|
147
|
+
2023-08-21;1692636918;origin/v4.6.0
|
|
148
|
+
2023-08-21;1692637081;HEAD -> master, origin/master, origin/HEAD
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
If "folder_path" is None, "git log" is run against the current folder.
|
|
152
|
+
Otherwise, it's run against the provided folder ("git -C file_path log").
|
|
153
|
+
'''
|
|
154
|
+
|
|
155
|
+
git_command : list[str] = ["git", "log", "--pretty=format:%cs;%ct;%D", "--reverse"]
|
|
156
|
+
|
|
157
|
+
if folder_path:
|
|
158
|
+
git_command = ["git", "-C", folder_path, "log", "--pretty=format:%cs;%ct;%D", "--reverse"]
|
|
159
|
+
|
|
160
|
+
output : CompletedProcess = subprocess.run(
|
|
161
|
+
git_command,
|
|
162
|
+
capture_output = True,
|
|
163
|
+
text = True,
|
|
164
|
+
check = True
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
triplets : list[str] = list(map(str, output.stdout.splitlines()))
|
|
168
|
+
commit_items : list[CommitItem] = []
|
|
169
|
+
|
|
170
|
+
for triplet in triplets:
|
|
171
|
+
triplet_lst : list[str] = triplet.split(";")
|
|
172
|
+
commit_item : CommitItem = self.__create_commit_item(triplet_lst = triplet_lst)
|
|
173
|
+
commit_items.append(commit_item)
|
|
174
|
+
|
|
175
|
+
return commit_items
|
|
176
|
+
def __clean_commit_items(self, commit_items : list[CommitItem]) -> list[CommitItem]:
|
|
177
|
+
|
|
178
|
+
'''Cleans every commit_item.ref_names in commit items.'''
|
|
179
|
+
|
|
180
|
+
cleaned : list[CommitItem] = []
|
|
181
|
+
|
|
182
|
+
for commit_item in commit_items:
|
|
183
|
+
|
|
184
|
+
new_ref_names : list[str] = self.__clean_ref_names(ref_names = commit_item.ref_names)
|
|
185
|
+
new_commit_item : CommitItem = self.__update_ref_names(commit_item = commit_item, ref_names = new_ref_names)
|
|
186
|
+
|
|
187
|
+
cleaned.append(new_commit_item)
|
|
188
|
+
|
|
189
|
+
return cleaned
|
|
190
|
+
def __create_daily_statuses(self, commit_items : list[CommitItem]) -> list[DailyStatus]:
|
|
191
|
+
|
|
192
|
+
'''
|
|
193
|
+
Creates a collection of DailyStatus objects out of commit_items.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
[
|
|
197
|
+
(date_str = "2023-11-17", timestamps = [1700239075, 1700239305, 1700240579, 1700246557], avg_minutes = 41.57, ref_names = ["ref1", "ref2"]),
|
|
198
|
+
(date_str = "2023-11-20", timestamps = [1700508271, 1700508711], avg_minutes = 7.33, ref_names = ["ref3', "ref4']),
|
|
199
|
+
...
|
|
200
|
+
]
|
|
201
|
+
'''
|
|
202
|
+
|
|
203
|
+
grouped_by_date : defaultdict = defaultdict(list)
|
|
204
|
+
|
|
205
|
+
for item in commit_items:
|
|
206
|
+
grouped_by_date[item.date_str].append(item)
|
|
207
|
+
|
|
208
|
+
daily_statuses : list[DailyStatus] = []
|
|
209
|
+
|
|
210
|
+
for date_str, items in grouped_by_date.items():
|
|
211
|
+
|
|
212
|
+
timestamps : list[int] = [item.timestamp_int for item in items]
|
|
213
|
+
timestamps.sort()
|
|
214
|
+
|
|
215
|
+
ref_names : list[str] = list({ref for item in items for ref in item.ref_names})
|
|
216
|
+
|
|
217
|
+
avg_minutes : float = 0.0
|
|
218
|
+
if len(timestamps) > 1:
|
|
219
|
+
differences : list[int] = [timestamps[i + 1] - timestamps[i] for i in range(len(timestamps) - 1)]
|
|
220
|
+
avg_minutes = round((sum(differences) / len(differences) / 60), 2)
|
|
221
|
+
|
|
222
|
+
daily_status : DailyStatus = DailyStatus(date_str = date_str, timestamps = timestamps, avg_minutes = avg_minutes, ref_names = ref_names)
|
|
223
|
+
daily_statuses.append(daily_status)
|
|
224
|
+
|
|
225
|
+
return daily_statuses
|
|
226
|
+
def __create_monthly_statuses(self, daily_statuses : list[DailyStatus]) -> list[MonthlyStatus]:
|
|
227
|
+
|
|
228
|
+
'''
|
|
229
|
+
Creates a collection of MonthlyStatus objects out of daily_statuses.
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
[
|
|
233
|
+
(year_month = "2023-11", timestamps = [1700239075, 1700239305, 1700240579, 1700246557, 1700508271, 1700508711], avg_minutes = 24.45, ref_names = ["ref1", "ref2", "ref3", "ref4"]),
|
|
234
|
+
(year_month = "2023-12", timestamps = [...], avg_minutes = ...),
|
|
235
|
+
...
|
|
236
|
+
]
|
|
237
|
+
'''
|
|
238
|
+
|
|
239
|
+
grouped_by_ym : defaultdict = defaultdict(list)
|
|
240
|
+
|
|
241
|
+
for daily_status in daily_statuses:
|
|
242
|
+
year_month : str = self.__create_year_month(date_str = daily_status.date_str)
|
|
243
|
+
grouped_by_ym[year_month].append(daily_status)
|
|
244
|
+
|
|
245
|
+
monthly_statuses : list[MonthlyStatus] = []
|
|
246
|
+
|
|
247
|
+
for year_month, daily_statuses in grouped_by_ym.items():
|
|
248
|
+
|
|
249
|
+
dates : int = self.__count_days_in_month(daily_statuses = daily_statuses)
|
|
250
|
+
|
|
251
|
+
timestamps : list[int] = []
|
|
252
|
+
ref_names : list[str] = []
|
|
253
|
+
|
|
254
|
+
for daily_status in daily_statuses:
|
|
255
|
+
timestamps.extend(daily_status.timestamps)
|
|
256
|
+
ref_names.extend(daily_status.ref_names)
|
|
257
|
+
|
|
258
|
+
timestamps.sort()
|
|
259
|
+
ref_names = sorted(list(set(ref_names)))
|
|
260
|
+
|
|
261
|
+
daily_avg_minutes : list[float] = self.__extract_avg_minutes(daily_statuses = daily_statuses)
|
|
262
|
+
|
|
263
|
+
monthly_avg_minutes : float = 0.0
|
|
264
|
+
if len(timestamps) > 1:
|
|
265
|
+
monthly_avg_minutes = round(sum(daily_avg_minutes) / len(daily_avg_minutes), 2)
|
|
266
|
+
|
|
267
|
+
monthly_status : MonthlyStatus = MonthlyStatus(
|
|
268
|
+
year_month = year_month,
|
|
269
|
+
dates = dates,
|
|
270
|
+
timestamps = timestamps,
|
|
271
|
+
avg_minutes = monthly_avg_minutes,
|
|
272
|
+
ref_names = ref_names
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
monthly_statuses.append(monthly_status)
|
|
276
|
+
|
|
277
|
+
return monthly_statuses
|
|
278
|
+
|
|
279
|
+
def run(self, folder_path: Optional[str] = None) -> Summary:
|
|
280
|
+
|
|
281
|
+
'''Returns a Summary or raises an Exception.'''
|
|
282
|
+
|
|
283
|
+
commit_items : list[CommitItem] = self.__get_commit_items(folder_path)
|
|
284
|
+
commit_items = self.__clean_commit_items(commit_items = commit_items)
|
|
285
|
+
|
|
286
|
+
daily_statuses : list[DailyStatus] = self.__create_daily_statuses(commit_items = commit_items)
|
|
287
|
+
monthly_statuses : list[MonthlyStatus] = self.__create_monthly_statuses(daily_statuses = daily_statuses)
|
|
288
|
+
|
|
289
|
+
summary : Summary = Summary(
|
|
290
|
+
commit_items = commit_items,
|
|
291
|
+
daily_statuses = daily_statuses,
|
|
292
|
+
monthly_statuses = monthly_statuses
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return summary
|
|
296
|
+
|
|
297
|
+
# MAIN
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
pass
|
nwcommitaveragescli.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
'''
|
|
2
|
+
A CLI application built around nwcommitaverages.
|
|
3
|
+
|
|
4
|
+
Alias: nwcavg
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
# GLOBAL MODULES
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import yaml
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from argparse import ArgumentParser, Namespace
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from shutil import get_terminal_size
|
|
15
|
+
from subprocess import CompletedProcess
|
|
16
|
+
from tabulate import tabulate
|
|
17
|
+
from typing import Callable, Final, Optional
|
|
18
|
+
|
|
19
|
+
# NW/LOCAL MODULES
|
|
20
|
+
from nwcommitaverages import CommitAverageCalculator, MonthlyStatus, Summary
|
|
21
|
+
from setupinfo import CLI_NAME, CLI_DESCRIPTION, PROJECT_VERSION
|
|
22
|
+
|
|
23
|
+
# GENERIC CLASSES
|
|
24
|
+
# CONSTANTS
|
|
25
|
+
class CLISTRING:
|
|
26
|
+
|
|
27
|
+
'''Collects all the CLI-related strings.'''
|
|
28
|
+
|
|
29
|
+
OPTION_FOLDERPATH_FLAGS : Final[list[str]] = ["--folder_path"]
|
|
30
|
+
OPTION_FOLDERPATH_REQUIRED : Final[bool] = False
|
|
31
|
+
OPTION_FOLDERPATH_HELP : Final[str] = "The path to the Git repository folder for which the average commit value is calculated."
|
|
32
|
+
class HEADER(StrEnum):
|
|
33
|
+
|
|
34
|
+
'''Represents a collection of headers.'''
|
|
35
|
+
|
|
36
|
+
YEARMONTH = "YearMonth"
|
|
37
|
+
DAYS = "Days"
|
|
38
|
+
COMMITS = "Commits"
|
|
39
|
+
DAILYAVGMIN = "DailyAvgMin"
|
|
40
|
+
REFNAMES = "RefNames"
|
|
41
|
+
|
|
42
|
+
# STATIC CLASSES
|
|
43
|
+
class _MessageCollectionAsciiBannerManager():
|
|
44
|
+
|
|
45
|
+
'''Collects all the messages used for logging and for the exceptions.'''
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def provided_version_empty_whitespace() -> str:
|
|
49
|
+
return "The provided 'version' is empty or whitespace."
|
|
50
|
+
class _MessageCollectionCLIManager():
|
|
51
|
+
|
|
52
|
+
'''Collects all the messages used for logging and for the exceptions used by CLIManager.'''
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def not_enough_data() -> str:
|
|
56
|
+
return "Not enough data"
|
|
57
|
+
@staticmethod
|
|
58
|
+
def field_equals_to(name: str, value: str) -> str:
|
|
59
|
+
return f"{name}:'{value}'"
|
|
60
|
+
class _MessageCollection(
|
|
61
|
+
_MessageCollectionCLIManager,
|
|
62
|
+
_MessageCollectionAsciiBannerManager):
|
|
63
|
+
|
|
64
|
+
'''Collects all the messages used for logging and for the exceptions.'''
|
|
65
|
+
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# CLASSES
|
|
69
|
+
class AsciiBannerManager:
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
Creates the ASCII banner for the provided library's version.
|
|
73
|
+
|
|
74
|
+
The figlet can be generated using
|
|
75
|
+
- 'http://www.network-science.de/ascii/' (font: "banner3-D", width: 120)
|
|
76
|
+
- 'https://www.askapache.com/online-tools/figlet-ascii/'.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __validate(self, version: str) -> None:
|
|
80
|
+
|
|
81
|
+
"""Validates the provided 'version'."""
|
|
82
|
+
|
|
83
|
+
if not version or not version.strip():
|
|
84
|
+
raise ValueError(_MessageCollection.provided_version_empty_whitespace())
|
|
85
|
+
def __create_figlet(self) -> tuple:
|
|
86
|
+
|
|
87
|
+
"""Returns a tuple containing the figlet and its width."""
|
|
88
|
+
|
|
89
|
+
lines : list[str] = [
|
|
90
|
+
"'##::: ##:'##:::::'##::'######:::::'###::::'##::::'##::'######:::",
|
|
91
|
+
" ###:: ##: ##:'##: ##:'##... ##:::'## ##::: ##:::: ##:'##... ##::",
|
|
92
|
+
" ####: ##: ##: ##: ##: ##:::..:::'##:. ##:: ##:::: ##: ##:::..:::",
|
|
93
|
+
" ## ## ##: ##: ##: ##: ##:::::::'##:::. ##: ##:::: ##: ##::'####:",
|
|
94
|
+
" ##. ####: ##: ##: ##: ##::::::: #########:. ##:: ##:: ##::: ##::",
|
|
95
|
+
" ##:. ###: ##: ##: ##: ##::: ##: ##.... ##::. ## ##::: ##::: ##::",
|
|
96
|
+
" ##::. ##:. ###. ###::. ######:: ##:::: ##:::. ###::::. ######:::",
|
|
97
|
+
"..::::..:::...::...::::......:::..:::::..:::::...::::::......::::"
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
return (os.linesep.join(lines), len(lines[0]))
|
|
101
|
+
def __create_frame(self, version: str, max_length: int) -> tuple:
|
|
102
|
+
|
|
103
|
+
"""Returns a tuple containing the frame of the figlet."""
|
|
104
|
+
|
|
105
|
+
version_token : str = f"Version: {version}"
|
|
106
|
+
|
|
107
|
+
margin_length : int = 5
|
|
108
|
+
total_length : int = max_length - len(version_token) - margin_length
|
|
109
|
+
|
|
110
|
+
top_line : str = "*" * max_length
|
|
111
|
+
bottom_line : str = f"{top_line[:total_length]}{version_token}{'*' * margin_length}"
|
|
112
|
+
|
|
113
|
+
return (top_line, bottom_line)
|
|
114
|
+
|
|
115
|
+
def create_standard(self, version : str) -> str:
|
|
116
|
+
|
|
117
|
+
"""Creates the standard ASCII banner."""
|
|
118
|
+
|
|
119
|
+
self.__validate(version)
|
|
120
|
+
|
|
121
|
+
figlet, max_length = self.__create_figlet()
|
|
122
|
+
top_line, bottom_line = self.__create_frame(version, max_length)
|
|
123
|
+
|
|
124
|
+
ascii_banner : str = os.linesep.join([
|
|
125
|
+
top_line,
|
|
126
|
+
figlet,
|
|
127
|
+
bottom_line,
|
|
128
|
+
""
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
return ascii_banner
|
|
132
|
+
def create_mini(self, version : str) -> str:
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
Creates the mini ASCII banner:
|
|
136
|
+
|
|
137
|
+
*****************
|
|
138
|
+
* NWCAVG v1.0.0 *
|
|
139
|
+
*****************
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
self.__validate(version)
|
|
143
|
+
|
|
144
|
+
assembly_name : str = "NWCAVG"
|
|
145
|
+
middle_line : str = f"* {assembly_name} v{version} *"
|
|
146
|
+
|
|
147
|
+
top_line : str = "*" * len(middle_line)
|
|
148
|
+
bottom_line : str = top_line
|
|
149
|
+
|
|
150
|
+
ascii_banner : str = os.linesep.join([
|
|
151
|
+
top_line,
|
|
152
|
+
middle_line,
|
|
153
|
+
bottom_line,
|
|
154
|
+
""
|
|
155
|
+
])
|
|
156
|
+
|
|
157
|
+
return ascii_banner
|
|
158
|
+
def create(self, version : str, terminal_width : int) -> str:
|
|
159
|
+
|
|
160
|
+
"""Creates either a standard or mini ASCII banner depending on the terminal width."""
|
|
161
|
+
|
|
162
|
+
_, max_length = self.__create_figlet()
|
|
163
|
+
|
|
164
|
+
if max_length <= terminal_width:
|
|
165
|
+
return self.create_standard(version)
|
|
166
|
+
else:
|
|
167
|
+
return self.create_mini(version)
|
|
168
|
+
class TerminalWindowManager:
|
|
169
|
+
|
|
170
|
+
'''Handles terminal window size.'''
|
|
171
|
+
|
|
172
|
+
__shutil_width_function : Callable[[], Optional[int]]
|
|
173
|
+
__stty_width_function : Callable[[], Optional[int]]
|
|
174
|
+
|
|
175
|
+
cutoff_width : Final[int] = 70
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def default_shutil_width_function() -> Optional[int]:
|
|
179
|
+
|
|
180
|
+
"""Get terminal width using shutil (multi-platform)."""
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
|
|
184
|
+
terminal_width : int = get_terminal_size().columns
|
|
185
|
+
|
|
186
|
+
return terminal_width
|
|
187
|
+
|
|
188
|
+
except:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def default_stty_width_function() -> Optional[int]:
|
|
193
|
+
|
|
194
|
+
"""Get terminal width using stty command (Linux)."""
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
|
|
198
|
+
process : CompletedProcess[str] = subprocess.run(
|
|
199
|
+
["/bin/sh", "-c", "stty size | cut -d' ' -f2"],
|
|
200
|
+
capture_output = True,
|
|
201
|
+
text = True,
|
|
202
|
+
check = False,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
stty_output : str = process.stdout.strip()
|
|
206
|
+
terminal_width : int = int(stty_output)
|
|
207
|
+
|
|
208
|
+
if terminal_width >= 0:
|
|
209
|
+
return terminal_width
|
|
210
|
+
|
|
211
|
+
return None
|
|
212
|
+
except:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def __init__(
|
|
216
|
+
self,
|
|
217
|
+
shutil_width_function : Optional[Callable[[], Optional[int]]] = None,
|
|
218
|
+
stty_width_function : Optional[Callable[[], Optional[int]]] = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
|
|
221
|
+
if shutil_width_function is None:
|
|
222
|
+
shutil_width_function = self.default_shutil_width_function
|
|
223
|
+
|
|
224
|
+
if stty_width_function is None:
|
|
225
|
+
stty_width_function = self.default_stty_width_function
|
|
226
|
+
|
|
227
|
+
self.__shutil_width_function = shutil_width_function
|
|
228
|
+
self.__stty_width_function = stty_width_function
|
|
229
|
+
|
|
230
|
+
def get_or_cutoff(self) -> int:
|
|
231
|
+
|
|
232
|
+
terminal_width : Optional[int] = self.__shutil_width_function()
|
|
233
|
+
|
|
234
|
+
if terminal_width is None:
|
|
235
|
+
terminal_width = self.__stty_width_function()
|
|
236
|
+
|
|
237
|
+
if terminal_width is None:
|
|
238
|
+
terminal_width = self.cutoff_width
|
|
239
|
+
|
|
240
|
+
return terminal_width
|
|
241
|
+
class APFactory():
|
|
242
|
+
|
|
243
|
+
'''Encapsulates all the logic related to the creation of a custom instance of argparse.ArgumentParser.'''
|
|
244
|
+
|
|
245
|
+
def create(self) -> ArgumentParser:
|
|
246
|
+
|
|
247
|
+
'''Creates a custom instance of argparse.ArgumentParser.'''
|
|
248
|
+
|
|
249
|
+
argument_parser : ArgumentParser = ArgumentParser(prog = CLI_NAME, description = CLI_DESCRIPTION)
|
|
250
|
+
|
|
251
|
+
argument_parser.add_argument(
|
|
252
|
+
*CLISTRING.OPTION_FOLDERPATH_FLAGS,
|
|
253
|
+
required = CLISTRING.OPTION_FOLDERPATH_REQUIRED,
|
|
254
|
+
help = CLISTRING.OPTION_FOLDERPATH_HELP)
|
|
255
|
+
|
|
256
|
+
return argument_parser
|
|
257
|
+
class APAdapter():
|
|
258
|
+
|
|
259
|
+
'''Customizes argparse.ArgumentParser for this use case.'''
|
|
260
|
+
|
|
261
|
+
__ap_factory : APFactory
|
|
262
|
+
|
|
263
|
+
def __init__(self, ap_factory : APFactory = APFactory()) -> None:
|
|
264
|
+
self.__ap_factory = ap_factory
|
|
265
|
+
|
|
266
|
+
def parse_args(self) -> Optional[str]:
|
|
267
|
+
|
|
268
|
+
'''Parses provided arguments.'''
|
|
269
|
+
|
|
270
|
+
parser : ArgumentParser = self.__ap_factory.create()
|
|
271
|
+
args : Namespace = parser.parse_args()
|
|
272
|
+
|
|
273
|
+
return args.folder_path
|
|
274
|
+
class CLIManager():
|
|
275
|
+
|
|
276
|
+
'''Collects all the logic related to the CLI management.'''
|
|
277
|
+
|
|
278
|
+
__ap_adapter : APAdapter
|
|
279
|
+
__ca_calculator : CommitAverageCalculator
|
|
280
|
+
__ascii_banner_manager : AsciiBannerManager
|
|
281
|
+
__tw_manager : TerminalWindowManager
|
|
282
|
+
__logging_function : Callable[[str], None]
|
|
283
|
+
|
|
284
|
+
def __init__(
|
|
285
|
+
self,
|
|
286
|
+
ap_adapter : APAdapter = APAdapter(),
|
|
287
|
+
ca_calculator : CommitAverageCalculator = CommitAverageCalculator(),
|
|
288
|
+
ascii_banner_manager : AsciiBannerManager = AsciiBannerManager(),
|
|
289
|
+
tw_manager : TerminalWindowManager = TerminalWindowManager(),
|
|
290
|
+
logging_function : Callable[[str], None] = lambda msg : print(msg)) -> None:
|
|
291
|
+
|
|
292
|
+
self.__ap_adapter = ap_adapter
|
|
293
|
+
self.__ca_calculator = ca_calculator
|
|
294
|
+
self.__ascii_banner_manager = ascii_banner_manager
|
|
295
|
+
self.__tw_manager = tw_manager
|
|
296
|
+
self.__logging_function = logging_function
|
|
297
|
+
|
|
298
|
+
def __convert_to_table(self, monthly_statuses : list[MonthlyStatus]) -> str:
|
|
299
|
+
|
|
300
|
+
'''
|
|
301
|
+
Converts monthly_statuses to a table.
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
+-------------+--------+-----------+---------------+--------------------------------+
|
|
305
|
+
| YearMonth | Days | Commits | DailyAvgMin | RefNames |
|
|
306
|
+
+=============+========+===========+===============+================================+
|
|
307
|
+
| 2023-08 | 2 | 6 | 721.28 | v3.2.0, v3.3.0 |
|
|
308
|
+
+-------------+--------+-----------+---------------+--------------------------------+
|
|
309
|
+
...
|
|
310
|
+
'''
|
|
311
|
+
|
|
312
|
+
rows : list[list[object]] = []
|
|
313
|
+
|
|
314
|
+
for monthly_status in monthly_statuses:
|
|
315
|
+
row : list[object] = [
|
|
316
|
+
monthly_status.year_month,
|
|
317
|
+
monthly_status.dates,
|
|
318
|
+
len(monthly_status.timestamps),
|
|
319
|
+
str(f"{monthly_status.avg_minutes:.2f}"),
|
|
320
|
+
", ".join(monthly_status.ref_names)
|
|
321
|
+
]
|
|
322
|
+
row[3] = str(row[3]).replace("0.00", _MessageCollection.not_enough_data())
|
|
323
|
+
rows.append(row)
|
|
324
|
+
|
|
325
|
+
table : str = tabulate(
|
|
326
|
+
rows,
|
|
327
|
+
headers = [HEADER.YEARMONTH, HEADER.DAYS, HEADER.COMMITS, HEADER.DAILYAVGMIN, HEADER.REFNAMES],
|
|
328
|
+
tablefmt = "grid",
|
|
329
|
+
disable_numparse = True
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return table
|
|
333
|
+
def __calculate_table_max_lenght(self, table : str) -> int:
|
|
334
|
+
|
|
335
|
+
"""Calculates table's max lenght."""
|
|
336
|
+
|
|
337
|
+
max_length : int = max((len(line) for line in table.splitlines()), default=0)
|
|
338
|
+
|
|
339
|
+
return max_length
|
|
340
|
+
def __convert_to_yaml(self, monthly_statuses : list[MonthlyStatus]) -> str:
|
|
341
|
+
|
|
342
|
+
'''
|
|
343
|
+
Converts monthly_statuses to a YAML block.
|
|
344
|
+
|
|
345
|
+
Example:
|
|
346
|
+
- YearMonth: "2023-08"
|
|
347
|
+
Days: 2
|
|
348
|
+
Commits: 6
|
|
349
|
+
DailyAvgMin: 721.28
|
|
350
|
+
RefNames: "v3.2.0, v3.3.0"
|
|
351
|
+
- ...
|
|
352
|
+
|
|
353
|
+
Note: "sort_keys = False" preserves the insertion order of the headers.
|
|
354
|
+
'''
|
|
355
|
+
|
|
356
|
+
yaml_data : list[dict[str, object]] = []
|
|
357
|
+
|
|
358
|
+
for monthly_status in monthly_statuses:
|
|
359
|
+
avg_min = str(f"{monthly_status.avg_minutes:.2f}").replace("0.00", _MessageCollection.not_enough_data())
|
|
360
|
+
|
|
361
|
+
record : dict[str, object] = {
|
|
362
|
+
HEADER.YEARMONTH.value: monthly_status.year_month,
|
|
363
|
+
HEADER.DAYS.value: monthly_status.dates,
|
|
364
|
+
HEADER.COMMITS.value: len(monthly_status.timestamps),
|
|
365
|
+
HEADER.DAILYAVGMIN.value: float(avg_min) if avg_min.replace('.', '', 1).isdigit() else avg_min,
|
|
366
|
+
HEADER.REFNAMES.value: ", ".join(monthly_status.ref_names)
|
|
367
|
+
}
|
|
368
|
+
yaml_data.append(record)
|
|
369
|
+
|
|
370
|
+
yaml_str : str = yaml.dump(yaml_data, sort_keys = False, default_flow_style = False)
|
|
371
|
+
yaml_str = yaml_str.replace(f"\n- {HEADER.YEARMONTH.value}:", f"\n\n- {HEADER.YEARMONTH.value}:")
|
|
372
|
+
|
|
373
|
+
return yaml_str
|
|
374
|
+
|
|
375
|
+
def __log_ascii_banner(self):
|
|
376
|
+
|
|
377
|
+
"""Logs the ascii banner."""
|
|
378
|
+
|
|
379
|
+
terminal_width : int = self.__tw_manager.get_or_cutoff()
|
|
380
|
+
ascii_banner : str = self.__ascii_banner_manager.create(PROJECT_VERSION, terminal_width)
|
|
381
|
+
|
|
382
|
+
self.__logging_function("")
|
|
383
|
+
self.__logging_function(ascii_banner)
|
|
384
|
+
def __log_folder_path(self, folder_path : Optional[str]):
|
|
385
|
+
|
|
386
|
+
"""Logs the folder_path."""
|
|
387
|
+
|
|
388
|
+
if (folder_path):
|
|
389
|
+
self.__logging_function(_MessageCollection.field_equals_to("Folder", folder_path))
|
|
390
|
+
else:
|
|
391
|
+
self.__logging_function(_MessageCollection.field_equals_to("Folder", str(Path.cwd())))
|
|
392
|
+
|
|
393
|
+
self.__logging_function("")
|
|
394
|
+
def __log_monthly_statuses(self, monthly_statuses : list[MonthlyStatus]) -> None:
|
|
395
|
+
|
|
396
|
+
'''
|
|
397
|
+
Logs monthly_statuses as table or YAML.
|
|
398
|
+
'''
|
|
399
|
+
|
|
400
|
+
terminal_width : int = self.__tw_manager.get_or_cutoff()
|
|
401
|
+
|
|
402
|
+
table : str = self.__convert_to_table(monthly_statuses)
|
|
403
|
+
table_max_length : int = self.__calculate_table_max_lenght(table)
|
|
404
|
+
|
|
405
|
+
if table_max_length < terminal_width:
|
|
406
|
+
self.__logging_function(table)
|
|
407
|
+
else:
|
|
408
|
+
yaml_str : str = self.__convert_to_yaml(monthly_statuses)
|
|
409
|
+
self.__logging_function(yaml_str)
|
|
410
|
+
|
|
411
|
+
def parse(self) -> None:
|
|
412
|
+
|
|
413
|
+
'''Calculates the average commit value and logs the result.'''
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
|
|
417
|
+
self.__log_ascii_banner()
|
|
418
|
+
|
|
419
|
+
folder_path : Optional[str] = self.__ap_adapter.parse_args()
|
|
420
|
+
|
|
421
|
+
self.__log_folder_path(folder_path = folder_path)
|
|
422
|
+
|
|
423
|
+
summary : Summary = self.__ca_calculator.run(folder_path = folder_path)
|
|
424
|
+
self.__log_monthly_statuses(monthly_statuses = summary.monthly_statuses);
|
|
425
|
+
|
|
426
|
+
except Exception as e:
|
|
427
|
+
|
|
428
|
+
self.__logging_function(str(e))
|
|
429
|
+
|
|
430
|
+
# MAIN
|
|
431
|
+
def main(): CLIManager().parse()
|
|
432
|
+
|
|
433
|
+
if __name__ == "__main__":
|
|
434
|
+
main()
|
setupinfo.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'''Contains project information.'''
|
|
2
|
+
|
|
3
|
+
# INFORMATION
|
|
4
|
+
PROJECT_VERSION : str = "2.0.1"
|
|
5
|
+
PROJECT_AUTHOR : str = "numbworks"
|
|
6
|
+
PROJECT_ALIAS : str = "nwcavg"
|
|
7
|
+
|
|
8
|
+
LIBRARY_NAME : str = "nwcommitaverages"
|
|
9
|
+
LIBRARY_DESCRIPTION : str = "A library designed to calculate the average time between Git commits."
|
|
10
|
+
|
|
11
|
+
CLI_NAME : str = "nwcommitaveragescli"
|
|
12
|
+
CLI_DESCRIPTION : str = "A CLI application designed to calculate the average time between Git commits."
|
|
13
|
+
|
|
14
|
+
PROJECT_URL : str = f"https://github.com/{PROJECT_AUTHOR}/{LIBRARY_NAME}"
|