• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / relman-auto-nag / #4349

pending completion
#4349

push

coveralls-python

sosa-e
Revert "Minimizing config file"

This reverts commit 614159597.

564 of 3081 branches covered (18.31%)

24 of 24 new or added lines in 24 files covered. (100.0%)

1804 of 7980 relevant lines covered (22.61%)

0.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.0
/auto_nag/round_robin_calendar.py
1
# This Source Code Form is subject to the terms of the Mozilla Public
2
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
# You can obtain one at http://mozilla.org/MPL/2.0/.
4

5
import json
1✔
6
import os
1✔
7
import re
1✔
8
from bisect import bisect_left
1✔
9
from json.decoder import JSONDecodeError
1✔
10

11
import recurring_ical_events
1✔
12
import requests
1✔
13
from dateutil.relativedelta import relativedelta
1✔
14
from icalendar import Calendar as iCalendar
1✔
15
from libmozdata import utils as lmdutils
1✔
16

17
from auto_nag import utils
1✔
18
from auto_nag.people import People
1✔
19

20

21
class InvalidCalendar(Exception):
1✔
22
    pass
1✔
23

24

25
class BadFallback(Exception):
1✔
26
    pass
1✔
27

28

29
class Calendar:
1✔
30
    def __init__(self, fallback, team_name, people=None):
1✔
31
        self.people = People.get_instance() if people is None else people
1✔
32
        self.fallback = fallback
1✔
33
        self.fb_bzmail = self.people.get_bzmail_from_name(self.fallback)
1✔
34
        self.fb_mozmail = self.people.get_moz_mail(self.fb_bzmail)
1✔
35
        self.team_name = team_name
1✔
36
        self.team = []
1✔
37
        self.cache = {}
1✔
38

39
    def get_fallback(self):
1✔
40
        return self.fallback
1✔
41

42
    def get_fallback_bzmail(self):
1✔
43
        if not self.fb_bzmail:
1!
44
            raise BadFallback("'{}' is an invalid fallback".format(self.fallback))
×
45
        return self.fb_bzmail
1✔
46

47
    def get_fallback_mozmail(self):
1✔
48
        if not self.fb_mozmail:
1✔
49
            raise BadFallback("'{}' is an invalid fallback".format(self.fallback))
1✔
50
        return self.fb_mozmail
1✔
51

52
    def get_team_name(self):
1✔
53
        return self.team_name
1✔
54

55
    def get_persons(self, date):
1✔
56
        return []
×
57

58
    def set_team(self, team, triagers):
1✔
59
        for p in team:
1✔
60
            if p in triagers and "bzmail" in triagers[p]:
1!
61
                bzmail = triagers[p]["bzmail"]
×
62
            else:
63
                bzmail = self.people.get_bzmail_from_name(p)
1✔
64
            self.team.append((p, bzmail))
1✔
65

66
    @staticmethod
1✔
67
    def get(url, fallback, team_name, people=None):
1✔
68
        data = None
1✔
69
        if url.startswith("private://"):
1!
70
            name = url.split("//", 1)[1]
×
71
            url = utils.get_private()[name]
×
72

73
        if url.startswith("http"):
1!
74
            r = requests.get(url)
×
75
            data = r.text
×
76
        elif os.path.isfile(url):
1!
77
            with open(url, "r") as In:
1✔
78
                data = In.read()
1✔
79
        else:
80
            data = url
×
81

82
        if data is None:
1!
83
            raise InvalidCalendar("Cannot read calendar: {}".format(url))
×
84

85
        try:
1✔
86
            cal = json.loads(data)
1✔
87
            return JSONCalendar(cal, fallback, team_name, people=people)
1✔
88
        except JSONDecodeError:
1✔
89
            try:
1✔
90
                # there is an issue with dateutil.rrule parser when until doesn't have a tz
91
                # so a workaround is to add a Z at the end of the string.
92
                pat = re.compile(r"^RRULE:(.*)UNTIL=([0-9Z]+)", re.MULTILINE | re.I)
1✔
93

94
                def sub(m):
1✔
95
                    date = m.group(1)
1✔
96
                    if date.lower().endswith("z"):
1!
97
                        return date
×
98
                    return date + "Z"
1✔
99

100
                data = pat.sub(sub, data)
1✔
101

102
                return ICSCalendar(data, fallback, team_name, people=people)
1✔
103
            except ValueError:
×
104
                raise InvalidCalendar(
×
105
                    f"Cannot decode calendar: {url} for team {team_name}"
106
                )
107

108
    def __str__(self):
1✔
109
        return f"""Round robin calendar:
×
110
team name: {self.team_name}
111
fallback: {self.fallback}, bz: {self.fb_bzmail}, moz: {self.fb_mozmail}
112
team: {self.team}"""
113

114
    def __repr__(self):
1✔
115
        return self.__str__()
×
116

117

118
class JSONCalendar(Calendar):
1✔
119
    def __init__(self, cal, fallback, team_name, people=None):
1✔
120
        super().__init__(fallback, team_name, people=people)
1✔
121
        start_dates = cal.get("duty-start-dates", {})
1✔
122
        if start_dates:
1!
123
            dates = sorted((lmdutils.get_date_ymd(d), d) for d in start_dates.keys())
1✔
124
            self.set_team(
1✔
125
                list(start_dates[d] for _, d in dates), cal.get("triagers", {})
126
            )
127
            self.dates = [d for d, _ in dates]
1✔
128
            cycle = self.guess_cycle()
1✔
129
            self.dates.append(self.dates[-1] + relativedelta(days=cycle))
1✔
130
            self.team.append(None)
1✔
131
        else:
132
            triagers = cal["triagers"]
×
133
            self.set_team(triagers.keys(), triagers)
×
134
            self.dates = []
×
135

136
    def get_persons(self, date):
1✔
137
        date = lmdutils.get_date_ymd(date)
1✔
138
        if date in self.cache:
1✔
139
            return self.cache[date]
1✔
140

141
        if not self.dates:
1!
142
            # no dates so only triagers
143
            return self.team
×
144

145
        i = bisect_left(self.dates, date)
1✔
146
        if i == len(self.dates):
1✔
147
            self.cache[date] = []
1✔
148
            return []
1✔
149

150
        if date == self.dates[i]:
1✔
151
            person = self.team[i][0]
1✔
152
        else:
153
            person = self.team[i - 1][0] if i != 0 else self.team[0][0]
1✔
154

155
        self.cache[date] = res = [(person, self.people.get_bzmail_from_name(person))]
1✔
156

157
        return res
1✔
158

159
    def guess_cycle(self):
1✔
160
        diffs = [(x - y).days for x, y in zip(self.dates[1:], self.dates[:-1])]
1✔
161
        mean = sum(diffs) / len(diffs)
1✔
162
        return int(round(mean))
1✔
163

164

165
class ICSCalendar(Calendar):
1✔
166

167
    # The summary can be "[Gfx Triage] Foo Bar" or just "Foo Bar"
168
    SUM_PAT = re.compile(r"\s*(?:\[[^\]]*\])?\s*(.*)")
1✔
169

170
    def __init__(self, cal, fallback, team_name, people=None):
1✔
171
        super().__init__(fallback, team_name, people=people)
1✔
172
        self.cal = iCalendar.from_ical(cal)
1✔
173

174
    def get_person(self, p):
1✔
175
        g = ICSCalendar.SUM_PAT.match(p)
1✔
176
        if g:
1!
177
            p = g.group(1)
1✔
178
            p = p.strip()
1✔
179
        return p
1✔
180

181
    def get_persons(self, date):
1✔
182
        date = lmdutils.get_date_ymd(date)
1✔
183
        if date in self.cache:
1✔
184
            return self.cache[date]
1✔
185

186
        events = recurring_ical_events.of(self.cal).between(date, date)
1✔
187
        persons = [self.get_person(event["SUMMARY"]) for event in events]
1✔
188
        self.cache[date] = res = [
1✔
189
            (person, self.people.get_bzmail_from_name(person)) for person in persons
190
        ]
191

192
        return res
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc