Skip to content

Commit b29cc42

Browse files
authored
merge: merge pull request #7 from n0rfas/dev
2 parents b02fe04 + 3271d1a commit b29cc42

File tree

14 files changed

+207
-225
lines changed

14 files changed

+207
-225
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: code quality check (dev)
2+
3+
on:
4+
push:
5+
branches: [ dev ]
6+
pull_request:
7+
branches: [ dev]
8+
9+
jobs:
10+
11+
linter:
12+
runs-on: ubuntu-22.04
13+
defaults:
14+
run:
15+
working-directory: .
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: 3.7
21+
- run: pipx install "poetry~=2.0.0"
22+
- run: poetry install --with dev
23+
- run: poetry run ruff check --select I .
24+
- run: poetry run ruff check .
25+
26+
typing:
27+
needs: [ linter ]
28+
runs-on: ubuntu-22.04
29+
defaults:
30+
run:
31+
working-directory: .
32+
steps:
33+
- uses: actions/checkout@v4
34+
- uses: actions/setup-python@v5
35+
with:
36+
python-version: 3.7
37+
- run: pipx install "poetry~=2.0.0"
38+
- run: poetry install --with dev
39+
- run: poetry run ruff check --select I .
40+
- run: poetry run mypy .
41+
42+
tests:
43+
needs: [ typing, linter ]
44+
runs-on: ubuntu-22.04
45+
defaults:
46+
run:
47+
working-directory: .
48+
steps:
49+
- uses: actions/checkout@v4
50+
- uses: actions/setup-python@v5
51+
with:
52+
python-version: 3.7
53+
- run: pipx install "poetry~=2.0.0"
54+
- run: poetry install --with dev
55+
- run: poetry run ruff check --select I .
56+
- run: poetry run pytest --cov=git_analytics --cov-report=term-missing --cov-fail-under=30
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: python version compatibility check (main)
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
tests:
11+
runs-on: ubuntu-22.04
12+
strategy:
13+
matrix:
14+
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: "${{ matrix.python-version }}"
20+
- run: pipx install "poetry~=2.0.0"
21+
- run: poetry install --with dev
22+
- run: poetry run pytest --verbose

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ __pycache__
22
.DS_Store
33
.vscode
44
.venv
5+
.coverage
56
*.egg-info
7+
htmlcov/
68
build/
79
dist/
8-
poetry.lock
10+
poetry.lock
11+
coverage.xml

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ poetry run git-analytics
4444

4545
```bash
4646
poetry run pytest
47+
poetry run pytest --cov=git_analytics --cov-report=term-missing --cov-fail-under=30
4748
```
4849

4950
### Type Checking

git_analytics/analyzers/authors_statistics.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,13 @@ class AuthorsStatisticsAnalyzer(CommitAnalyzer):
2222
name = "authors_statistics"
2323

2424
def __init__(self) -> None:
25-
self._dict_authors = defaultdict(AuthorStatistics)
25+
self._authors: Dict[str, AuthorStatistics] = defaultdict(AuthorStatistics)
2626

2727
def process(self, commit: AnalyticsCommit) -> None:
28-
author = self._dict_authors[commit.commit_author]
28+
author: AuthorStatistics = self._authors[commit.commit_author]
2929
author.commits += 1
3030
author.insertions += commit.lines_insertions
3131
author.deletions += commit.lines_deletions
3232

3333
def result(self) -> Result:
34-
return Result(
35-
authors={
36-
name: AuthorStatistics(
37-
commits=st.commits,
38-
insertions=st.insertions,
39-
deletions=st.deletions,
40-
)
41-
for name, st in self._dict_authors.items()
42-
}
43-
)
34+
return Result(authors=dict(self._authors))

git_analytics/analyzers/commit_type.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class CommitType(Enum):
2323

2424
@dataclass
2525
class Result(AnalyticsResult):
26-
items: Dict[date, Dict[str, int]]
26+
items: Dict[date, Dict[CommitType, int]]
2727

2828

2929
TYPE_COMMIT_LIST: tuple = tuple(ct.value for ct in CommitType)
@@ -33,25 +33,20 @@ def _get_type_list(commit_message: str):
3333
result = [tag for tag in TYPE_COMMIT_LIST if tag in commit_message]
3434
if result:
3535
return result
36-
return [CommitType.UNKNOWN.value]
36+
return [CommitType.UNKNOWN]
3737

3838

3939
class CommitTypeAnalyzer(CommitAnalyzer):
4040
name = "commit_type"
4141

4242
def __init__(self) -> None:
43-
self._commit_types_by_date = defaultdict(Counter)
43+
self._by_date: Dict[date, Counter] = defaultdict(Counter)
4444

4545
def process(self, commit: AnalyticsCommit) -> None:
46+
commit_date = commit.committed_datetime.date()
4647
commit_types = _get_type_list(commit.message)
4748
for commit_type in commit_types:
48-
self._commit_types_by_date[commit.committed_datetime.date()][
49-
commit_type
50-
] += 1
49+
self._by_date[commit_date][commit_type] += 1
5150

5251
def result(self) -> Result:
53-
days = defaultdict(dict)
54-
for dt, st in self._commit_types_by_date.items():
55-
days[str(dt)] = dict(st)
56-
57-
return Result(items=dict(days))
52+
return Result(items={dt: dict(counter) for dt, counter in self._by_date.items()})

git_analytics/analyzers/commits_summary.py

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,28 @@
88

99
@dataclass
1010
class Result(AnalyticsResult):
11-
date_first_commit: date
12-
date_last_commit: date
13-
total_number_commit: int
14-
total_number_authors: int
11+
date_first_commit: Optional[date] = None
12+
date_last_commit: Optional[date] = None
13+
total_number_commit: int = 0
14+
total_number_authors: int = 0
1515

1616

1717
class CommitsSummaryAnalyzer(CommitAnalyzer):
1818
name = "commits_summary"
1919

2020
def __init__(self) -> None:
21-
self._date_first_commit: Optional[date] = None
22-
self._date_last_commit: Optional[date] = None
23-
self._total_number_commit: int = 0
24-
self._list_authors: Set = set()
21+
self._result = Result()
22+
self._authors: Set[str] = set()
2523

2624
def process(self, commit: AnalyticsCommit) -> None:
27-
if (
28-
self._date_first_commit is None
29-
or commit.committed_datetime < self._date_first_commit
30-
):
31-
self._date_first_commit = commit.committed_datetime
32-
if (
33-
self._date_last_commit is None
34-
or commit.committed_datetime > self._date_last_commit
35-
):
36-
self._date_last_commit = commit.committed_datetime
37-
self._total_number_commit += 1
38-
self._list_authors.add(commit.commit_author)
25+
dt = commit.committed_datetime.date()
26+
if self._result.date_first_commit is None or dt < self._result.date_first_commit:
27+
self._result.date_first_commit = dt
28+
if self._result.date_last_commit is None or dt > self._result.date_last_commit:
29+
self._result.date_last_commit = dt
30+
self._result.total_number_commit += 1
31+
self._authors.add(commit.commit_author)
3932

4033
def result(self) -> Result:
41-
return Result(
42-
date_first_commit=self._date_first_commit.date(),
43-
date_last_commit=self._date_last_commit.date(),
44-
total_number_commit=self._total_number_commit,
45-
total_number_authors=len(self._list_authors),
46-
)
34+
self._result.total_number_authors = len(self._authors)
35+
return self._result
Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from collections import Counter, defaultdict
2-
from dataclasses import dataclass
1+
from dataclasses import dataclass, field
32
from typing import Dict
43

54
from git_analytics.entities import AnalyticsCommit, AnalyticsResult
@@ -8,12 +7,14 @@
87

98
@dataclass
109
class Result(AnalyticsResult):
11-
hour_of_day: Dict[int, int]
12-
day_of_week: Dict[str, int]
13-
day_of_month: Dict[int, int]
10+
hour_of_day: Dict[int, Dict[str, int]] = field(default_factory=lambda: {h: {} for h in range(24)})
11+
day_of_week: Dict[str, Dict[str, int]] = field(
12+
default_factory=lambda: {d: {} for d in [_get_day_name(i) for i in range(7)]}
13+
)
14+
day_of_month: Dict[int, Dict[str, int]] = field(default_factory=lambda: {d: {} for d in range(1, 32)})
1415

1516

16-
def get_day_name(day_index: int) -> str:
17+
def _get_day_name(day_index: int) -> str:
1718
days = [
1819
"Monday",
1920
"Tuesday",
@@ -30,31 +31,18 @@ class HistoricalStatisticsAnalyzer(CommitAnalyzer):
3031
name = "historical_statistics"
3132

3233
def __init__(self) -> None:
33-
self._dict_hour_of_day = defaultdict(Counter)
34-
self._dict_day_of_week = defaultdict(Counter)
35-
self._dict_day_of_month = defaultdict(Counter)
34+
self._result = Result()
3635

3736
def process(self, commit: AnalyticsCommit) -> None:
38-
c = commit
39-
self._dict_day_of_week[c.committed_datetime.weekday()][c.commit_author] += 1
40-
self._dict_hour_of_day[c.committed_datetime.hour][c.commit_author] += 1
41-
self._dict_day_of_month[c.committed_datetime.day][c.commit_author] += 1
37+
author = commit.commit_author
38+
hour = commit.committed_datetime.hour
39+
day = commit.committed_datetime.weekday()
40+
month_day = commit.committed_datetime.day
41+
42+
self._result.hour_of_day[hour][author] = self._result.hour_of_day[hour].get(author, 0) + 1
43+
day_name = _get_day_name(day)
44+
self._result.day_of_week[day_name][author] = self._result.day_of_week[day_name].get(author, 0) + 1
45+
self._result.day_of_month[month_day][author] = self._result.day_of_month[month_day].get(author, 0) + 1
4246

4347
def result(self) -> Result:
44-
hour_of_day = {hour: 0 for hour in range(24)}
45-
for hour, authors in self._dict_hour_of_day.items():
46-
hour_of_day[hour] = dict(authors)
47-
48-
day_of_week = {get_day_name(day): 0 for day in range(7)}
49-
for day, authors in self._dict_day_of_week.items():
50-
day_of_week[get_day_name(day)] = dict(authors)
51-
52-
day_of_month = {day: 0 for day in range(1, 32)}
53-
for day, authors in self._dict_day_of_month.items():
54-
day_of_month[day] = dict(authors)
55-
56-
return Result(
57-
hour_of_day=hour_of_day,
58-
day_of_week=day_of_week,
59-
day_of_month=day_of_month,
60-
)
48+
return self._result
Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from collections import defaultdict
21
from dataclasses import dataclass
32
from datetime import date
4-
from typing import List
3+
from typing import Dict, List
54

65
from git_analytics.entities import AnalyticsCommit, AnalyticsResult
76
from git_analytics.interfaces import CommitAnalyzer
@@ -22,27 +21,17 @@ class LinesAnalyzer(CommitAnalyzer):
2221
name = "lines_statistics"
2322

2423
def __init__(self) -> None:
25-
self._lines_in_day = {}
24+
self._daily_delta: Dict[date, int] = {}
2625

2726
def process(self, commit: AnalyticsCommit) -> None:
28-
self._lines_in_day[commit.committed_datetime] = (
29-
commit.lines_insertions - commit.lines_deletions
30-
)
27+
d = commit.committed_datetime.date()
28+
delta = commit.lines_insertions - commit.lines_deletions
29+
self._daily_delta[d] = self._daily_delta.get(d, 0) + delta
3130

3231
def result(self) -> Result:
33-
dict_number_lines = defaultdict(int)
34-
35-
old_rows = 0
36-
sort_day = list(self._lines_in_day.keys())
37-
sort_day.sort()
38-
for day in sort_day:
39-
number_rows = self._lines_in_day[day]
40-
dict_number_lines[day] = old_rows + number_rows
41-
old_rows = old_rows + number_rows
42-
43-
return Result(
44-
items=[
45-
LinesStatistics(date=day.date(), lines=lines)
46-
for day, lines in dict_number_lines.items()
47-
]
48-
)
32+
items: List[LinesStatistics] = []
33+
total = 0
34+
for d in sorted(self._daily_delta.keys()):
35+
total += self._daily_delta[d]
36+
items.append(LinesStatistics(date=d, lines=total))
37+
return Result(items=items)

git_analytics/entities.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from collections.abc import Iterable, Mapping
12
from dataclasses import asdict, dataclass
23
from datetime import date, datetime
4+
from enum import Enum
35
from typing import Any, Dict
46

57

@@ -20,11 +22,28 @@ def to_dict(self) -> Dict[str, Any]:
2022
return self._make_json_safe(asdict(self))
2123

2224
@staticmethod
23-
def _make_json_safe(obj):
24-
if isinstance(obj, date):
25+
def _make_json_safe(obj: Any) -> Any:
26+
if isinstance(obj, (date, datetime)):
2527
return obj.isoformat()
26-
if isinstance(obj, list):
28+
29+
if isinstance(obj, Enum):
30+
return obj.value
31+
32+
if isinstance(obj, Mapping):
33+
safe: Dict[str, Any] = {}
34+
for k, v in obj.items():
35+
if isinstance(k, (date, datetime)):
36+
key = k.isoformat()
37+
elif isinstance(k, Enum):
38+
key = k.value
39+
elif isinstance(k, (str, int, float, bool)) or k is None:
40+
key = str(k)
41+
else:
42+
key = str(k)
43+
safe[key] = AnalyticsResult._make_json_safe(v)
44+
return safe
45+
46+
if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)):
2747
return [AnalyticsResult._make_json_safe(x) for x in obj]
28-
if isinstance(obj, dict):
29-
return {k: AnalyticsResult._make_json_safe(v) for k, v in obj.items()}
48+
3049
return obj

0 commit comments

Comments
 (0)