Calendar: remove past event
[tails/test.git] / bin / generate-report
blob5e9905ac2a7de3e8b1810894f40a229d04db8753
1 #! /usr/bin/python3
3 # Documentation: https://tails.boum.org/contribute/working_together/GitLab/#api
5 import functools
6 import sys
7 import logging
8 import os
9 from datetime import datetime
11 try:
12     import gitlab  # type: ignore
13 except ImportError:
14     sys.exit("You need to install python3-gitlab to use this program.")
16 try:
17     from dateutil.relativedelta import relativedelta
18 except ImportError:
19     sys.exit("You need to install python3-dateutil to use this program.")
20 from pathlib import Path
23 PYTHON_GITLAB_CONFIG_FILE = os.getenv('PYTHON_GITLAB_CONFIG_FILE',
24                                       default=Path.home() /
25                                       '.python-gitlab.cfg')
27 PYTHON_GITLAB_NAME = os.getenv('GITLAB_NAME', default='Tails')
29 GROUP_NAME = 'tails'
31 # By default, only changes in these projects are considered
32 PROJECTS = [
33     GROUP_NAME + '/' + project for project in [
34         'chutney',
35         'installer',
36         'tails',
37         'whisperback',
38         'workarounds',
39     ]
42 LOG_FORMAT = "%(asctime)-15s %(levelname)s %(message)s"
43 log = logging.getLogger()
46 class GitLabWrapper(gitlab.Gitlab):
47     @functools.lru_cache(maxsize=None)
48     def project(self, project_id):
49         return self.projects.get(project_id)
51     @functools.lru_cache(maxsize=None)
52     def project_from_name(self, project_name):
53         project = [
54             p for p in self.projects.list(all=True)
55             # Disambiguate between projects whose names share a common prefix
56             if p.path_with_namespace == project_name
57         ][0]
58         assert isinstance(project, gitlab.v4.objects.Project)
59         return project
62 class ReportGenerator(object):
63     def __init__(self, gl, group, projects: list, label: str, year: int,
64                  month: int):
65         self.gl = gl
66         self.group = group
67         self.projects = projects
68         self.label = label
69         self.after = end_of_previous_month(year, month)
70         self.before = beginning_of_next_month(year, month)
72     def closed_issues_in_project(self, project_name) -> list:
73         closed_issues = []
74         project = self.gl.project_from_name(project_name)
75         closed_issues_events = project.events.list(as_list=False,
76                                                    target_type='issue',
77                                                    action='closed',
78                                                    after=self.after,
79                                                    before=self.before)
81         gl_closed_issues_with_duplicates = [{
82             "project_id": event.project_id,
83             "iid": event.target_iid
84         } for event in closed_issues_events]
85         gl_closed_issues = []
86         for issue in gl_closed_issues_with_duplicates:
87             if issue not in gl_closed_issues:
88                 gl_closed_issues.append(issue)
90         for issue in gl_closed_issues:
91             project = self.gl.project(issue["project_id"])
92             issue = project.issues.get(issue["iid"])
93             if self.label is not None and self.label not in issue.labels:
94                 continue
95             closed_issues.append({
96                 "title": issue.title,
97                 "web_url": issue.web_url,
98             })
100         return closed_issues
102     def closed_issues(self) -> list:
103         closed_issues = []
104         for project in self.projects:
105             closed_issues = closed_issues + self.closed_issues_in_project(
106                 project)
107         return closed_issues
110 def beginning_of_next_month(year, month):
111     return (datetime(year, month, 1) + relativedelta(months=1)).replace(day=1)
114 def end_of_previous_month(year, month):
115     return datetime(year, month, 1) + relativedelta(seconds=-1)
118 if __name__ == '__main__':
119     import argparse
120     parser = argparse.ArgumentParser()
121     parser.add_argument('--year', type=int, required=True)
122     parser.add_argument('--month', type=int, required=True)
123     parser.add_argument('--label', default=None)
124     parser.add_argument('--project')
125     parser.add_argument("--debug", action="store_true", help="debug output")
126     args = parser.parse_args()
128     if args.debug:
129         logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
130     else:
131         logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
133     gl = GitLabWrapper.from_config(PYTHON_GITLAB_NAME,
134                                    config_files=[PYTHON_GITLAB_CONFIG_FILE])
135     gl.auth()
137     group = gl.groups.list(search=GROUP_NAME)[0]
138     assert isinstance(group, gitlab.v4.objects.Group)
140     if args.project:
141         projects = [args.project]
142     else:
143         projects = PROJECTS
145     report_generator = ReportGenerator(gl, group, projects, args.label,
146                                        args.year, args.month)
148     print("Closed issues")
149     print("=============")
150     print()
151     for closed_issue in report_generator.closed_issues():
152         print(f'- {closed_issue["title"]}')
153         print(f'  {closed_issue["web_url"]}')
154         print()