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

mozilla / relman-auto-nag / #4767

13 Oct 2023 01:27AM CUT coverage: 22.091%. Remained the same
#4767

push

coveralls-python

suhaibmujahid
Format the .pre-commit-config.yaml file

716 of 3558 branches covered (0.0%)

1925 of 8714 relevant lines covered (22.09%)

0.22 hits per line

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

67.05
/bugbot/people.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 re
1✔
7
from math import sqrt
1✔
8
from typing import Set
1✔
9

10
import numpy as np
1✔
11

12
WORDS = re.compile(r"(\w+)")
1✔
13
MAIL = re.compile(r"^([^@]+@[^ ]+)")
1✔
14
IMs = [
1✔
15
    "irc",
16
    "slack",
17
    "skype",
18
    "xmpp",
19
    "github",
20
    "aim",
21
    "telegram",
22
    "irc.mozilla.org",
23
    "google talk",
24
    "gtalk",
25
    "blog",
26
    "twitter",
27
]
28
IM_NICK = re.compile(r"([\w\.@]+)")
1✔
29

30

31
class People:
1✔
32
    _instance = None
1✔
33

34
    def __init__(self, p=None):
1✔
35
        if p is None:
1!
36
            with open("./configs/people.json", "r") as In:
×
37
                self.data = json.load(In)
×
38
        else:
39
            self.data = p
1✔
40

41
        self.people = self._get_people()
1✔
42
        self.people_by_bzmail = {}
1✔
43
        self.managers = set()
1✔
44
        self.people_with_bzmail = set()
1✔
45
        self.release_managers = set()
1✔
46
        self.rm_or_directors = set()
1✔
47
        self.nicks = {}
1✔
48
        self.directors = set()
1✔
49
        self.vps = set()
1✔
50
        self.names = {}
1✔
51
        self._amend()
1✔
52
        self.matrix = None
1✔
53

54
    @staticmethod
1✔
55
    def get_instance():
1✔
56
        if People._instance is None:
×
57
            People._instance = People()
×
58
        return People._instance
×
59

60
    def _get_name_parts(self, name):
1✔
61
        """Get names from name"""
62
        return set(s.lower() for s in WORDS.findall(name))
1✔
63

64
    def _get_people(self):
1✔
65
        people = {}
1✔
66
        for person in self.data:
1✔
67
            mail = self.get_preferred_mail(person)
1✔
68
            person["mail"] = mail
1✔
69
            people[mail] = person
1✔
70
        return people
1✔
71

72
    def _get_names(self):
1✔
73
        if not self.names:
1!
74
            for person in self.data:
1!
75
                cn = person["cn"]
×
76
                parts = self._get_name_parts(cn)
×
77
                parts = tuple(sorted(parts))
×
78
                self.names[parts] = person
×
79
        return self.names
1✔
80

81
    def _get_bigrams(self, text):
1✔
82
        text = "".join(s.lower() for s in WORDS.findall(text))
1✔
83
        return [text[i : (i + 2)] for i in range(len(text) - 1)]
1✔
84

85
    def _get_bigrams_stats(self, text):
1✔
86
        stats = {}
1✔
87
        for bi in self._get_bigrams(text):
1✔
88
            stats[bi] = stats.get(bi, 0) + 1
1✔
89

90
        return stats
1✔
91

92
    def _get_matrix_names(self):
1✔
93
        if self.matrix is None:
1✔
94
            res = {}
1✔
95
            bigrams = set()
1✔
96
            cns = {}
1✔
97
            for person in self.data:
1✔
98
                cn = person["cn"]
1✔
99
                cns[cn] = person
1✔
100
                res[cn] = stats = self._get_bigrams_stats(cn)
1✔
101
                L = sqrt(sum(v * v for v in stats.values()))
1✔
102
                for k, v in stats.items():
1✔
103
                    stats[k] = float(v) / L
1✔
104
                    bigrams.add(k)
1✔
105

106
            bigrams = sorted(bigrams)
1✔
107
            self.bigrams = {x: i for i, x in enumerate(bigrams)}
1✔
108
            self.matrix = np.zeros((len(res), len(self.bigrams)))
1✔
109
            self.matrix_map = [None] * len(res.items())
1✔
110
            for i, (name, stats) in enumerate(res.items()):
1✔
111
                for b, n in stats.items():
1✔
112
                    self.matrix[i][self.bigrams[b]] = n
1✔
113
                self.matrix_map[i] = cns[name]
1✔
114

115
    def search_by_name(self, name):
1✔
116
        # Try to find name in using cosine similarity
117
        self._get_matrix_names()
1✔
118
        stats = self._get_bigrams_stats(name)
1✔
119
        for k in set(stats.keys()) - set(self.bigrams.keys()):
1✔
120
            del stats[k]
1✔
121
        L = sqrt(sum(v * v for v in stats.values()))
1✔
122
        x = np.zeros((len(self.bigrams), 1))
1✔
123
        for k, v in stats.items():
1✔
124
            x[self.bigrams[k]][0] = float(v) / L
1✔
125
        res = np.matmul(self.matrix, x)
1✔
126
        for cos in [0.99, 0.9, 0.8, 0.7]:
1✔
127
            index = np.argwhere(res > cos)
1✔
128
            if index.shape[0] == 1:
1✔
129
                return self.matrix_map[index[0][0]]
1✔
130

131
        found = None
1✔
132
        name_parts = self._get_name_parts(name)
1✔
133
        for parts, info in self._get_names().items():
1!
134
            if name_parts <= set(parts):
×
135
                if found is None:
×
136
                    found = info
×
137
                else:
138
                    found = None
×
139
                    break
×
140
        return found
1✔
141

142
    def _get_people_by_bzmail(self):
1✔
143
        if not self.people_by_bzmail:
1✔
144
            for person in self.data:
1✔
145
                bzmail = person["bugzillaEmail"]
1✔
146
                if not bzmail:
1!
147
                    bzmail = person["mail"]
1✔
148
                self.people_by_bzmail[bzmail] = person
1✔
149
        return self.people_by_bzmail
1✔
150

151
    def get_managers(self):
1✔
152
        """Get all the managers"""
153
        if not self.managers:
×
154
            for person in self.data:
×
155
                manager = person["manager"]
×
156
                if manager:
×
157
                    self.managers.add(manager["dn"])
×
158
        return self.managers
×
159

160
    def get_people_with_bzmail(self):
1✔
161
        """Get all the people who have a bugzilla email"""
162
        if not self.people_with_bzmail:
×
163
            for person, info in self.people.items():
×
164
                mail = info["bugzillaEmail"]
×
165
                if mail:
×
166
                    self.people_with_bzmail.add(mail)
×
167
        return self.people_with_bzmail
×
168

169
    def get_info_by_nick(self, nick):
1✔
170
        """Get info for a nickname"""
171
        if not self.nicks:
1!
172
            doubloons = set()
1✔
173
            for person, info in self.people.items():
1✔
174
                bz_mail = info["bugzillaEmail"]
1✔
175
                if not bz_mail:
1!
176
                    continue
1✔
177
                nicks = self.get_nicks_from_im(info)
×
178
                nicks |= {person, bz_mail, info["mail"], info.get("githubprofile")}
×
179
                nicks |= set(self.get_aliases(info))
×
180
                nicks = {self.get_mail_prefix(n) for n in nicks if n}
×
181
                for n in nicks:
×
182
                    if n not in self.nicks:
×
183
                        self.nicks[n] = info
×
184
                    else:
185
                        doubloons.add(n)
×
186
            # doubloons are not identifiable so remove them
187
            for n in doubloons:
1!
188
                del self.nicks[n]
×
189
        return self.nicks.get(nick)
1✔
190

191
    def get_rm(self):
1✔
192
        """Get the release managers as defined in configs/rm.json"""
193
        if not self.release_managers:
×
194
            with open("./configs/rm.json", "r") as In:
×
195
                self.release_managers = set(json.load(In))
×
196
        return self.release_managers
×
197

198
    def get_directors(self):
1✔
199
        """Get the directors: people who 'director' in their job title"""
200
        if not self.directors:
1✔
201
            for person, info in self.people.items():
1✔
202
                title = info.get("title", "").lower()
1✔
203
                if "director" in title:
1✔
204
                    self.directors.add(person)
1✔
205
        return self.directors
1✔
206

207
    def get_vps(self):
1✔
208
        """Get the vp: people who've 'vp' in their job title"""
209
        if not self.vps:
1✔
210
            for person, info in self.people.items():
1✔
211
                title = info.get("title", "").lower()
1✔
212
                if (
1✔
213
                    title.startswith("vp") or title.startswith("vice president")
214
                ) and self.get_distance(person) <= 3:
215
                    self.vps.add(person)
1✔
216
        return self.vps
1✔
217

218
    def get_distance(self, mail):
1✔
219
        rank = -1
1✔
220
        while mail:
1✔
221
            rank += 1
1✔
222
            prev = mail
1✔
223
            mail = self.get_manager_mail(mail)
1✔
224
            if mail == prev:
1!
225
                break
×
226
        return rank
1✔
227

228
    def get_rm_or_directors(self):
1✔
229
        """Get a set of release managers and directors who've a bugzilla email"""
230
        if not self.rm_or_directors:
×
231
            ms = self.get_directors() | self.get_rm()
×
232
            for m in ms:
×
233
                info = self.people[m]
×
234
                mail = info["bugzillaEmail"]
×
235
                if mail:
×
236
                    self.rm_or_directors.add(mail)
×
237
        return self.rm_or_directors
×
238

239
    def _get_mail_from_dn(self, dn):
1✔
240
        dn = dn.split(",")
1✔
241
        assert len(dn) >= 2
1✔
242
        dn = dn[0].split("=")
1✔
243
        assert len(dn) == 2
1✔
244
        return dn[1]
1✔
245

246
    def _amend(self):
1✔
247
        for person in self.data:
1✔
248
            if "manager" not in person:
1✔
249
                person["manager"] = {}
1✔
250
            if "title" not in person:
1✔
251
                person["title"] = ""
1✔
252
            manager = person["manager"]
1✔
253
            if manager:
1✔
254
                manager["dn"] = self._get_mail_from_dn(manager["dn"])
1✔
255
            if "bugzillaEmail" in person:
1!
256
                person["bugzillaEmail"] = person["bugzillaEmail"].lower()
×
257
            elif "bugzillaemail" in person:
1!
258
                person["bugzillaEmail"] = person["bugzillaemail"].lower()
×
259
                del person["bugzillaemail"]
×
260
            else:
261
                person["bugzillaEmail"] = ""
1✔
262

263
    def is_mozilla(self, mail):
1✔
264
        """Check if the mail is the one from a mozilla employee"""
265
        return mail in self._get_people_by_bzmail() or mail in self.people
1✔
266

267
    def is_manager(self, mail):
1✔
268
        """Check if the mail is the one from a mozilla manager"""
269
        if mail in self._get_people_by_bzmail():
×
270
            person = self._get_people_by_bzmail()[mail]
×
271
            return person["mail"] in self._get_managers()
×
272
        elif mail in self.people:
×
273
            return mail in self._get_managers()
×
274

275
        return False
×
276

277
    def get_manager_mail(self, mail):
1✔
278
        """Get the manager of the person with this mail"""
279
        person = self._get_people_by_bzmail().get(mail, None)
1✔
280
        if not person:
1!
281
            person = self.people.get(mail, None)
×
282
        if not person:
1!
283
            return None
×
284

285
        manager = person["manager"]
1✔
286
        if not manager:
1✔
287
            return None
1✔
288

289
        manager_mail = manager["dn"]
1✔
290
        if manager_mail == mail:
1!
291
            return None
×
292

293
        return manager_mail
1✔
294

295
    def get_nth_manager_mail(self, mail, rank):
1✔
296
        """Get the nth manager of the person with this mail"""
297
        for _ in range(rank):
1✔
298
            prev = mail
1✔
299
            mail = self.get_manager_mail(mail)
1✔
300
            if not mail or mail == prev:
1!
301
                return prev
×
302
        return mail
1✔
303

304
    def get_management_chain_mails(
1✔
305
        self, person: str, superior: str, raise_on_missing: bool = True
306
    ) -> Set[str]:
307
        """Get the mails of people in the management chain between a person and
308
        their superior.
309

310
        Args:
311
            person: the moz email of an employee.
312
            superior: the moz email of one of the employee's superiors.
313
            raise_on_missing: If True, an exception will be raised when the
314
                superior is not in the management hierarchy of the employee. If
315
                False, an empty set will be returned instead of raising an
316
                exception.
317

318
        Returns:
319
            A set of moz emails for people in the management chain between
320
            `person` and `superior`. Emails for `person` and `superior` will not
321
            be returned with the result.
322
        """
323
        result: Set[str] = set()
1✔
324

325
        assert person in self.people
1✔
326
        assert superior in self.people
1✔
327
        if person == superior:
1!
328
            return result
×
329

330
        manager = self.get_manager_mail(person)
1✔
331
        while manager != superior:
1✔
332
            result.add(manager)
1✔
333
            manager = self.get_manager_mail(manager)
1✔
334

335
            if not manager:
1✔
336
                if not raise_on_missing:
1!
337
                    return set()
×
338
                raise Exception(f"Cannot identify {superior} as a superior of {person}")
1✔
339

340
            if manager in result:
1!
341
                raise Exception("Circular management chain")
×
342

343
        return result
1✔
344

345
    def get_director_mail(self, mail):
1✔
346
        """Get the director of the person with this mail"""
347
        directors = self.get_directors()
1✔
348
        while True:
1✔
349
            prev = mail
1✔
350
            mail = self.get_manager_mail(mail)
1✔
351
            if not mail:
1!
352
                break
×
353
            if mail in directors:
1✔
354
                return mail
1✔
355
            if mail == prev:
1!
356
                break
×
357
        return None
×
358

359
    def get_vp_mail(self, mail):
1✔
360
        """Get the VP of the person with this mail"""
361
        vps = self.get_vps()
1✔
362
        while True:
1✔
363
            prev = mail
1✔
364
            mail = self.get_manager_mail(mail)
1✔
365
            if not mail:
1!
366
                break
×
367
            if mail in vps:
1✔
368
                return mail
1✔
369
            if mail == prev:
1!
370
                break
×
371
        return None
×
372

373
    def get_mail_prefix(self, mail):
1✔
374
        return mail.split("@", 1)[0].lower()
×
375

376
    def get_im(self, person):
1✔
377
        im = person.get("im", "")
×
378
        if not im:
×
379
            return []
×
380
        if isinstance(im, str):
×
381
            return [im]
×
382
        return im
×
383

384
    def get_nicks_from_im(self, person):
1✔
385
        im = self.get_im(person)
×
386
        nicks = set()
×
387
        for info in im:
×
388
            info = info.lower()
×
389
            for i in IMs:
×
390
                info = info.replace(i, "")
×
391
            for nick in IM_NICK.findall(info):
×
392
                if nick.startswith("@"):
×
393
                    nick = nick[1:]
×
394
                nicks.add(nick)
×
395
        return nicks
×
396

397
    def get_aliases(self, person):
1✔
398
        aliases = person.get("emailalias", "")
1✔
399
        if not aliases:
1!
400
            return []
1✔
401
        if isinstance(aliases, str):
×
402
            return [aliases]
×
403
        return aliases
×
404

405
    def get_preferred_mail(self, person):
1✔
406
        aliases = self.get_aliases(person)
1✔
407
        for alias in aliases:
1!
408
            alias = alias.strip()
×
409
            if "preferred" in alias:
×
410
                m = MAIL.search(alias)
×
411
                if m:
×
412
                    return m.group(1)
×
413
        return person["mail"]
1✔
414

415
    def get_moz_mail(self, mail):
1✔
416
        """Get the Mozilla email of the person with this Bugzilla email"""
417
        person = self._get_people_by_bzmail().get(mail, None)
1✔
418
        if person:
1✔
419
            return person["mail"]
1✔
420
        return mail
1✔
421

422
    def get_moz_name(self, mail):
1✔
423
        """Get the name of the person with this Bugzilla email"""
424
        person = self._get_people_by_bzmail().get(mail, None)
×
425
        if person is None:
×
426
            return None
×
427
        return person["cn"]
×
428

429
    def get_info(self, mail):
1✔
430
        """Get info on person with this mail"""
431
        person = self._get_people_by_bzmail().get(mail, None)
1✔
432
        if not person:
1!
433
            person = self.people.get(mail, None)
1✔
434
        return person
1✔
435

436
    def is_under(self, mail, manager):
1✔
437
        """Check if someone is under manager in the hierarchy"""
438
        m = mail
×
439
        while True:
×
440
            m = self.get_manager_mail(m)
×
441
            if m is None:
×
442
                return False
×
443
            if m == manager:
×
444
                return True
×
445

446
    def get_bzmail_from_name(self, name):
1✔
447
        """Search bz mail for a given name"""
448

449
        if "@" in name:
1✔
450
            info = self.get_info(name)
1✔
451
        else:
452
            info = self.get_info_by_nick(name)
1✔
453
            if not info:
1!
454
                info = self.search_by_name(name)
1✔
455

456
        if info:
1✔
457
            mail = info["bugzillaEmail"]
1✔
458
            return mail if mail else info["mail"]
1✔
459

460
        return None
1✔
461

462
    def get_mozmail_from_name(self, name):
1✔
463
        """Search moz mail for a given name"""
464

465
        if "@" in name:
×
466
            info = self.get_info(name)
×
467
        else:
468
            info = self.get_info_by_nick(name)
×
469
            if not info:
×
470
                info = self.search_by_name(name)
×
471

472
        if info:
×
473
            return info["mail"]
×
474

475
        return None
×
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