reminder-aggregator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.3
2
+ Name: reminder-aggregator
3
+ Version: 0.1.0
4
+ Summary: A simple cli application that aggregates codereminder tags like TODO, etc. in a report
5
+ Requires-Dist: click>=8.3.0
6
+ Requires-Dist: pathspec==0.12.1
7
+ Requires-Dist: pip==25.2
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Reminder Aggregator
12
+
13
+ A simple python tool that scans files in a directory for common reminder tags such as `TODO`, `FIXME`, etc. and generates a report from them.
14
+
15
+ ## Requirements
16
+
17
+ - Python 3.13+
18
+
19
+ ## Installation
20
+
21
+ With [uv](https://docs.astral.sh/uv/getting-started/installation/) (recommended):
22
+
23
+ ```bash
24
+ uv sync
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ reminder_aggregator --help
31
+ ```
32
+
33
+ ## Future Changes
34
+
35
+ - Filter to check whether a reminder-tag is inside a comment (currently causes false positives)
36
+ - Support for multiple output formats
37
+
38
+ ## License
39
+
40
+ GNU General Public License v3.0 or later
41
+
42
+ See [LICENSE](./LICENSE) to see the full text.
@@ -0,0 +1,32 @@
1
+ # Reminder Aggregator
2
+
3
+ A simple python tool that scans files in a directory for common reminder tags such as `TODO`, `FIXME`, etc. and generates a report from them.
4
+
5
+ ## Requirements
6
+
7
+ - Python 3.13+
8
+
9
+ ## Installation
10
+
11
+ With [uv](https://docs.astral.sh/uv/getting-started/installation/) (recommended):
12
+
13
+ ```bash
14
+ uv sync
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ reminder_aggregator --help
21
+ ```
22
+
23
+ ## Future Changes
24
+
25
+ - Filter to check whether a reminder-tag is inside a comment (currently causes false positives)
26
+ - Support for multiple output formats
27
+
28
+ ## License
29
+
30
+ GNU General Public License v3.0 or later
31
+
32
+ See [LICENSE](./LICENSE) to see the full text.
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "reminder-aggregator"
3
+ version = "0.1.0"
4
+ description = "A simple cli application that aggregates codereminder tags like TODO, etc. in a report"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "click>=8.3.0",
9
+ "pathspec==0.12.1",
10
+ "pip==25.2",
11
+ ]
12
+
13
+ [project.scripts]
14
+ reminder-aggregator = "reminder_aggregator.cli:cli"
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.9.2,<0.10.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [tool.ruff]
21
+ line-length = 120
22
+ indent-width = 4
23
+
24
+ [tool.ruff.format]
25
+ quote-style = "double"
26
+ indent-style = "space"
27
+ line-ending = "auto"
28
+
29
+ [tool.ruff.lint]
30
+ select = [
31
+ "F", # pyflakes
32
+ "E", # pycodestyle
33
+ "I", # isort
34
+ "ANN", # flake8 type annotations
35
+ "RUF", # ruff-specific rules
36
+ ]
37
+ fixable = ["ALL"]
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "ruff>=0.14.0",
42
+ ]
@@ -0,0 +1,4 @@
1
+ if __name__ == "__main__":
2
+ from reminder_aggregator.cli import cli
3
+
4
+ cli()
@@ -0,0 +1,144 @@
1
+ import json
2
+ import os
3
+ import pathlib
4
+ import re
5
+ from typing import Any, Counter
6
+
7
+ import click
8
+ import pathspec
9
+
10
+
11
+ def _write_report(filename: str, data: list[dict[str, Any]]) -> None:
12
+ counter = Counter(match["type"].upper() for match in data)
13
+ summary = dict(counter)
14
+ summary["total"] = sum(counter.values())
15
+
16
+ report = {
17
+ "summary": summary,
18
+ "details": data,
19
+ }
20
+
21
+ with open(filename, "w", encoding="utf-8") as file:
22
+ json.dump(report, file, indent=2)
23
+
24
+
25
+ def _parse_file(path: pathlib.Path, path_root: pathlib.Path, pattern: re.Pattern) -> list[dict[str, Any]]:
26
+ # FIXME: Ensure that only comments count to matches.
27
+ matches: list[dict[str, Any]] = []
28
+
29
+ try:
30
+ for line_number, line in enumerate(open(path)):
31
+ line = line.strip()
32
+
33
+ if match := re.search(pattern, line):
34
+ matches.append(
35
+ {
36
+ "type": match.group(1),
37
+ "file": str(path.relative_to(path_root)),
38
+ "line": line_number + 1,
39
+ "pos": match.start(1),
40
+ "content": line.strip(),
41
+ }
42
+ )
43
+
44
+ except UnicodeDecodeError:
45
+ print(f"Error reading {path}")
46
+
47
+ return matches
48
+
49
+
50
+ def _parse_directory(
51
+ directory: pathlib.Path,
52
+ path_root: pathlib.Path,
53
+ match_regex: re.Pattern,
54
+ ignore_spec: pathspec.PathSpec,
55
+ ) -> list[dict[str, Any]]:
56
+ matches: list[dict[str, Any]] = []
57
+
58
+ for path in directory.rglob("*"):
59
+ if not path.is_file():
60
+ continue
61
+
62
+ if ignore_spec.match_file(path):
63
+ continue
64
+
65
+ match = _parse_file(path, path_root, match_regex)
66
+
67
+ if len(match) == 0:
68
+ continue
69
+
70
+ matches.extend(match)
71
+
72
+ return matches
73
+
74
+
75
+ def _load_ignore_spec(file_path: str) -> pathspec.PathSpec:
76
+ file = pathlib.Path(file_path)
77
+
78
+ if not file.is_file():
79
+ return pathspec.PathSpec.from_lines("gitwildmatch", [])
80
+
81
+ with open(file, "r", encoding="utf-8") as file:
82
+ return pathspec.PathSpec.from_lines("gitwildmatch", file)
83
+
84
+
85
+ CONTEXT_SETTINGS = {"max_content_width": os.get_terminal_size().columns - 10}
86
+
87
+
88
+ @click.group(context_settings=CONTEXT_SETTINGS)
89
+ def cli() -> None:
90
+ pass
91
+
92
+
93
+ @cli.command("report", short_help="Generate a report")
94
+ @click.option(
95
+ "--out-file",
96
+ "-o",
97
+ default="report.json",
98
+ show_default=True,
99
+ type=click.Path(),
100
+ help=" Specify path where the report will be saved",
101
+ )
102
+ @click.option(
103
+ "--format",
104
+ "-f",
105
+ default="json",
106
+ show_default=True,
107
+ type=click.Choice(["json"]),
108
+ help="Specify the format of the generated report",
109
+ )
110
+ @click.option(
111
+ "--ignore-file",
112
+ default=".gitignore",
113
+ show_default=True,
114
+ type=click.Path(exists=True),
115
+ help="Specify ignore file to use",
116
+ )
117
+ @click.argument("path", envvar="RA_SEARCH_DIR", default=".", type=click.Path())
118
+ def report(path: str, out_file: str, format: str, ignore_file: str) -> None:
119
+ """
120
+ \b
121
+ positional arguments:
122
+ PATH Specify the path that will be scanned [default: .]
123
+ """
124
+ # TODO: Add support for multiple output formats (junitxml, json, etc.)
125
+
126
+ path_root: pathlib.Path = pathlib.Path("./")
127
+ search_directory: pathlib.Path = pathlib.Path(path)
128
+
129
+ output_path: str = out_file
130
+
131
+ re_match_str: str = r"(TODO|FIXME|HACK|OPTIMIZE|REVIEW)"
132
+ re_match: re.Pattern = re.compile(re_match_str)
133
+
134
+ matches: list[dict[str, Any]] = []
135
+
136
+ ignore_spec: pathspec.PathSpec = _load_ignore_spec(ignore_file)
137
+
138
+ matches = _parse_directory(search_directory, path_root, re_match, ignore_spec)
139
+
140
+ _write_report(output_path, matches)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ cli()