libmozdata 0.2.3__tar.gz → 0.2.6__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.
- {libmozdata-0.2.3/libmozdata.egg-info → libmozdata-0.2.6}/PKG-INFO +1 -1
- libmozdata-0.2.6/VERSION +1 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/bugzilla.py +104 -46
- libmozdata-0.2.6/libmozdata/clouseau.py +76 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/connection.py +4 -3
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/phabricator.py +7 -3
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/release_owners.py +2 -4
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/socorro.py +94 -28
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/utils.py +37 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6/libmozdata.egg-info}/PKG-INFO +1 -1
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/SOURCES.txt +1 -0
- libmozdata-0.2.3/VERSION +0 -1
- {libmozdata-0.2.3 → libmozdata-0.2.6}/MANIFEST.in +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/README.md +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/BZInfo.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/FXRevision.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/FileStats.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/HGFileInfo.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/__init__.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/buildhub.py +1 -1
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/config.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/fx_trains.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/handler.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/hgmozilla.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/lando.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/modules.json +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/modules.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/patchanalysis.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/redash.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/vcs_map.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/versions.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/wiki_parser.py +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/dependency_links.txt +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/not-zip-safe +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/requires.txt +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/top_level.txt +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/requirements.txt +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/setup.cfg +0 -0
- {libmozdata-0.2.3 → libmozdata-0.2.6}/setup.py +0 -0
libmozdata-0.2.6/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.6
|
|
@@ -16,15 +16,23 @@ from .connection import Connection, Query
|
|
|
16
16
|
from .handler import Handler
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class
|
|
20
|
-
"""Connection to bugzilla.mozilla.org"""
|
|
21
|
-
|
|
19
|
+
class BugzillaBase(Connection):
|
|
22
20
|
URL = config.get("Bugzilla", "URL", "https://bugzilla.mozilla.org")
|
|
23
21
|
# URL = config.get('Allizgub', 'URL', 'https://bugzilla-dev.allizom.org')
|
|
24
|
-
API_URL = URL + "/rest/bug"
|
|
25
|
-
ATTACHMENT_API_URL = API_URL + "/attachment"
|
|
26
22
|
TOKEN = config.get("Bugzilla", "token", "")
|
|
27
23
|
# TOKEN = config.get('Allizgub', 'token', '')
|
|
24
|
+
|
|
25
|
+
def get_header(self):
|
|
26
|
+
header = super(BugzillaBase, self).get_header()
|
|
27
|
+
header["X-Bugzilla-API-Key"] = self.get_apikey()
|
|
28
|
+
return header
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Bugzilla(BugzillaBase):
|
|
32
|
+
"""Connection to bugzilla.mozilla.org"""
|
|
33
|
+
|
|
34
|
+
API_URL = BugzillaBase.URL + "/rest/bug"
|
|
35
|
+
ATTACHMENT_API_URL = API_URL + "/attachment"
|
|
28
36
|
BUGZILLA_CHUNK_SIZE = 100
|
|
29
37
|
|
|
30
38
|
def __init__(
|
|
@@ -94,11 +102,6 @@ class Bugzilla(Connection):
|
|
|
94
102
|
self.got_data = False
|
|
95
103
|
self.no_private_bugids = None
|
|
96
104
|
|
|
97
|
-
def get_header(self):
|
|
98
|
-
header = super(Bugzilla, self).get_header()
|
|
99
|
-
header["X-Bugzilla-API-Key"] = self.get_apikey()
|
|
100
|
-
return header
|
|
101
|
-
|
|
102
105
|
def put(self, data, attachment=False, retry_on_failure=False):
|
|
103
106
|
"""Put some data in bugs
|
|
104
107
|
|
|
@@ -816,12 +819,10 @@ class Bugzilla(Connection):
|
|
|
816
819
|
)
|
|
817
820
|
|
|
818
821
|
|
|
819
|
-
class BugzillaUser(
|
|
822
|
+
class BugzillaUser(BugzillaBase):
|
|
820
823
|
"""Connection to bugzilla.mozilla.org"""
|
|
821
824
|
|
|
822
|
-
|
|
823
|
-
API_URL = URL + "/rest/user"
|
|
824
|
-
TOKEN = config.get("Bugzilla", "token", "")
|
|
825
|
+
API_URL = BugzillaBase.URL + "/rest/user"
|
|
825
826
|
|
|
826
827
|
def __init__(
|
|
827
828
|
self,
|
|
@@ -892,11 +893,6 @@ class BugzillaUser(Connection):
|
|
|
892
893
|
|
|
893
894
|
super(BugzillaUser, self).__init__(BugzillaUser.URL, queries, **kwargs)
|
|
894
895
|
|
|
895
|
-
def get_header(self):
|
|
896
|
-
header = super(BugzillaUser, self).get_header()
|
|
897
|
-
header["X-Bugzilla-API-Key"] = self.get_apikey()
|
|
898
|
-
return header
|
|
899
|
-
|
|
900
896
|
def __users_cb(self, res):
|
|
901
897
|
if self.user_handler.isactive():
|
|
902
898
|
for user in res["users"]:
|
|
@@ -907,16 +903,14 @@ class BugzillaUser(Connection):
|
|
|
907
903
|
self.fault_user_handler.handle(user)
|
|
908
904
|
|
|
909
905
|
|
|
910
|
-
class BugzillaProduct(
|
|
906
|
+
class BugzillaProduct(BugzillaBase):
|
|
911
907
|
"""
|
|
912
908
|
Connection to bugzilla.mozilla.org
|
|
913
909
|
|
|
914
910
|
API docs: https://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html
|
|
915
911
|
"""
|
|
916
912
|
|
|
917
|
-
|
|
918
|
-
API_URL = URL + "/rest/product"
|
|
919
|
-
TOKEN = config.get("Bugzilla", "token", "")
|
|
913
|
+
API_URL = BugzillaBase.URL + "/rest/product"
|
|
920
914
|
|
|
921
915
|
def __init__(
|
|
922
916
|
self,
|
|
@@ -1007,11 +1001,6 @@ class BugzillaProduct(Connection):
|
|
|
1007
1001
|
**kwargs,
|
|
1008
1002
|
)
|
|
1009
1003
|
|
|
1010
|
-
def get_header(self):
|
|
1011
|
-
header = super(BugzillaProduct, self).get_header()
|
|
1012
|
-
header["X-Bugzilla-API-Key"] = self.get_apikey()
|
|
1013
|
-
return header
|
|
1014
|
-
|
|
1015
1004
|
def __products_cb(self, res):
|
|
1016
1005
|
if not self.product_handler.isactive():
|
|
1017
1006
|
return
|
|
@@ -1020,14 +1009,12 @@ class BugzillaProduct(Connection):
|
|
|
1020
1009
|
self.product_handler.handle(product)
|
|
1021
1010
|
|
|
1022
1011
|
|
|
1023
|
-
class BugzillaShorten(
|
|
1012
|
+
class BugzillaShorten(BugzillaBase):
|
|
1024
1013
|
"""
|
|
1025
1014
|
Connection to bugzilla.mozilla.org
|
|
1026
1015
|
"""
|
|
1027
1016
|
|
|
1028
|
-
|
|
1029
|
-
API_URL = URL + "/rest/bitly/shorten"
|
|
1030
|
-
TOKEN = config.get("Bugzilla", "token", "")
|
|
1017
|
+
API_URL = BugzillaBase.URL + "/rest/bitly/shorten"
|
|
1031
1018
|
|
|
1032
1019
|
def __init__(self, url, url_data=None, url_handler=None, **kwargs):
|
|
1033
1020
|
"""Constructor
|
|
@@ -1049,11 +1036,6 @@ class BugzillaShorten(Connection):
|
|
|
1049
1036
|
**kwargs,
|
|
1050
1037
|
)
|
|
1051
1038
|
|
|
1052
|
-
def get_header(self):
|
|
1053
|
-
header = super(BugzillaShorten, self).get_header()
|
|
1054
|
-
header["X-Bugzilla-API-Key"] = self.get_apikey()
|
|
1055
|
-
return header
|
|
1056
|
-
|
|
1057
1039
|
def __urls_cb(self, res):
|
|
1058
1040
|
if not self.url_handler.isactive():
|
|
1059
1041
|
return
|
|
@@ -1069,14 +1051,12 @@ class BugzillaShorten(Connection):
|
|
|
1069
1051
|
self.url_handler.handle(res["url"])
|
|
1070
1052
|
|
|
1071
1053
|
|
|
1072
|
-
class BugzillaComponent(
|
|
1054
|
+
class BugzillaComponent(BugzillaBase):
|
|
1073
1055
|
"""
|
|
1074
1056
|
Connection to bugzilla.mozilla.org
|
|
1075
1057
|
"""
|
|
1076
1058
|
|
|
1077
|
-
|
|
1078
|
-
API_URL = URL + "/rest/component"
|
|
1079
|
-
TOKEN = config.get("Bugzilla", "token", "")
|
|
1059
|
+
API_URL = BugzillaBase.URL + "/rest/component"
|
|
1080
1060
|
|
|
1081
1061
|
def __init__(
|
|
1082
1062
|
self, product, component, component_data=None, component_handler=None, **kwargs
|
|
@@ -1105,11 +1085,6 @@ class BugzillaComponent(Connection):
|
|
|
1105
1085
|
**kwargs,
|
|
1106
1086
|
)
|
|
1107
1087
|
|
|
1108
|
-
def get_header(self):
|
|
1109
|
-
header = super(BugzillaComponent, self).get_header()
|
|
1110
|
-
header["X-Bugzilla-API-Key"] = self.get_apikey()
|
|
1111
|
-
return header
|
|
1112
|
-
|
|
1113
1088
|
def __components_cb(self, res):
|
|
1114
1089
|
if not self.component_handler.isactive():
|
|
1115
1090
|
return
|
|
@@ -1135,3 +1110,86 @@ class BugzillaComponent(Connection):
|
|
|
1135
1110
|
response.raise_for_status()
|
|
1136
1111
|
|
|
1137
1112
|
return response.json()
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
class BugFields(BugzillaBase):
|
|
1116
|
+
"""Fetching bug fields information from Bugzilla
|
|
1117
|
+
|
|
1118
|
+
Docs: https://bmo.readthedocs.io/en/latest/api/core/v1/field.html#fields
|
|
1119
|
+
"""
|
|
1120
|
+
|
|
1121
|
+
API_URL = BugzillaBase.URL + "/rest/field/bug"
|
|
1122
|
+
|
|
1123
|
+
def __init__(
|
|
1124
|
+
self, field=None, handler=None, handlerdata=None, queries=None, **kwargs
|
|
1125
|
+
):
|
|
1126
|
+
"""Constructor
|
|
1127
|
+
|
|
1128
|
+
Args:
|
|
1129
|
+
field (Optional[str]): the name of the field, if empty will fetch all fields
|
|
1130
|
+
handler (Optional[function]): the handler to use with each retrieved field
|
|
1131
|
+
handlerdata (Optional): the data to use with the field handler
|
|
1132
|
+
queries (Optional[Query]): the queries to use
|
|
1133
|
+
"""
|
|
1134
|
+
if not queries:
|
|
1135
|
+
endpoint_url = f"{self.API_URL}/{field}" if field else self.API_URL
|
|
1136
|
+
queries = Query(endpoint_url, None, handler, handlerdata)
|
|
1137
|
+
|
|
1138
|
+
super(BugFields, self).__init__(self.URL, queries, **kwargs)
|
|
1139
|
+
|
|
1140
|
+
@classmethod
|
|
1141
|
+
def fetch_all_fields_info(cls):
|
|
1142
|
+
"""Get information about all fields
|
|
1143
|
+
|
|
1144
|
+
Returns:
|
|
1145
|
+
list: the information about the fields
|
|
1146
|
+
"""
|
|
1147
|
+
|
|
1148
|
+
def handler(resp, data):
|
|
1149
|
+
data.update(resp)
|
|
1150
|
+
|
|
1151
|
+
data = {}
|
|
1152
|
+
cls(
|
|
1153
|
+
handler=handler,
|
|
1154
|
+
handlerdata=data,
|
|
1155
|
+
).wait()
|
|
1156
|
+
|
|
1157
|
+
return data["fields"]
|
|
1158
|
+
|
|
1159
|
+
@classmethod
|
|
1160
|
+
def fetch_field_info(cls, field):
|
|
1161
|
+
"""Get information about a field
|
|
1162
|
+
|
|
1163
|
+
Args:
|
|
1164
|
+
field (str): the name of the field
|
|
1165
|
+
|
|
1166
|
+
Returns:
|
|
1167
|
+
dict: the information about the field
|
|
1168
|
+
"""
|
|
1169
|
+
|
|
1170
|
+
def handler(resp, data):
|
|
1171
|
+
data.update(resp)
|
|
1172
|
+
|
|
1173
|
+
data = {}
|
|
1174
|
+
cls(
|
|
1175
|
+
field=field,
|
|
1176
|
+
handler=handler,
|
|
1177
|
+
handlerdata=data,
|
|
1178
|
+
).wait()
|
|
1179
|
+
|
|
1180
|
+
field_info = data["fields"][0]
|
|
1181
|
+
assert field_info["name"] == field
|
|
1182
|
+
|
|
1183
|
+
return field_info
|
|
1184
|
+
|
|
1185
|
+
@classmethod
|
|
1186
|
+
def fetch_field_values(cls, field):
|
|
1187
|
+
"""Get the legal values of a field
|
|
1188
|
+
|
|
1189
|
+
Args:
|
|
1190
|
+
field (str): the name of the field
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
List[str]: the legal values of the field
|
|
1194
|
+
"""
|
|
1195
|
+
return [value["name"] for value in cls.fetch_field_info(field)["values"]]
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
|
|
6
|
+
from libmozdata.utils import batched
|
|
7
|
+
|
|
8
|
+
from . import config
|
|
9
|
+
from .connection import Connection, Query
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CrashClouseau(Connection):
|
|
13
|
+
URL = config.get("Clouseau", "URL", "https://clouseau.moz.tools")
|
|
14
|
+
API_URL = URL + "/api"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Reports(CrashClouseau):
|
|
18
|
+
API_URL = CrashClouseau.API_URL + "/reports"
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self, params=None, handler=None, handlerdata=None, queries=None, **kwargs
|
|
22
|
+
):
|
|
23
|
+
"""Constructor
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
params (Optional[dict]): the params for the query
|
|
27
|
+
handler (Optional[function]): handler to use with the result of the query
|
|
28
|
+
handlerdata (Optional): data used in second argument of the handler
|
|
29
|
+
queries (Optional[List[Query]]): queries to execute
|
|
30
|
+
"""
|
|
31
|
+
if queries is not None:
|
|
32
|
+
super().__init__(self.URL, queries, **kwargs)
|
|
33
|
+
else:
|
|
34
|
+
super().__init__(
|
|
35
|
+
self.URL, Query(self.API_URL, params, handler, handlerdata), **kwargs
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _default_handler(res, data):
|
|
40
|
+
for report in res:
|
|
41
|
+
signature = report["signature"]
|
|
42
|
+
if signature not in data:
|
|
43
|
+
data[signature] = [report]
|
|
44
|
+
else:
|
|
45
|
+
data[signature].append(report)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_by_signatures(cls, signatures, product=None, channel=None):
|
|
49
|
+
"""Get reports by signatures
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
signatures: signatures to get their reports.
|
|
53
|
+
product: filter out reports that are not from this product.
|
|
54
|
+
channel: filter out reports that are not from this release channel.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
dict: the reports by signatures
|
|
58
|
+
"""
|
|
59
|
+
data = {}
|
|
60
|
+
requests = [
|
|
61
|
+
cls(
|
|
62
|
+
params={
|
|
63
|
+
"signatures": signatures_batch,
|
|
64
|
+
"product": product,
|
|
65
|
+
"channel": channel,
|
|
66
|
+
},
|
|
67
|
+
handler=cls._default_handler,
|
|
68
|
+
handlerdata=data,
|
|
69
|
+
)
|
|
70
|
+
for signatures_batch in batched(signatures, 20)
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for request in requests:
|
|
74
|
+
request.wait()
|
|
75
|
+
|
|
76
|
+
return data
|
|
@@ -48,6 +48,7 @@ class Query(object):
|
|
|
48
48
|
class Connection(object):
|
|
49
49
|
"""Represents a connection to a server"""
|
|
50
50
|
|
|
51
|
+
VERIFY_SSL = True
|
|
51
52
|
TIMEOUT = 30
|
|
52
53
|
MAX_RETRIES = 256
|
|
53
54
|
MAX_WORKERS = multiprocessing.cpu_count()
|
|
@@ -190,7 +191,7 @@ class Connection(object):
|
|
|
190
191
|
params=query.params,
|
|
191
192
|
headers=header,
|
|
192
193
|
auth=auth,
|
|
193
|
-
verify=
|
|
194
|
+
verify=self.VERIFY_SSL,
|
|
194
195
|
timeout=self.TIMEOUT,
|
|
195
196
|
hooks={"response": cb},
|
|
196
197
|
)
|
|
@@ -203,7 +204,7 @@ class Connection(object):
|
|
|
203
204
|
params=p,
|
|
204
205
|
headers=header,
|
|
205
206
|
auth=auth,
|
|
206
|
-
verify=
|
|
207
|
+
verify=self.VERIFY_SSL,
|
|
207
208
|
timeout=self.TIMEOUT,
|
|
208
209
|
hooks={"response": cb},
|
|
209
210
|
)
|
|
@@ -214,7 +215,7 @@ class Connection(object):
|
|
|
214
215
|
query.url,
|
|
215
216
|
headers=header,
|
|
216
217
|
auth=auth,
|
|
217
|
-
verify=
|
|
218
|
+
verify=self.VERIFY_SSL,
|
|
218
219
|
timeout=self.TIMEOUT,
|
|
219
220
|
hooks={"response": cb},
|
|
220
221
|
)
|
|
@@ -7,6 +7,7 @@ import collections
|
|
|
7
7
|
import enum
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
|
+
from functools import cached_property
|
|
10
11
|
from urllib.parse import urlencode, urlparse
|
|
11
12
|
|
|
12
13
|
import hglib
|
|
@@ -213,9 +214,12 @@ class PhabricatorAPI(object):
|
|
|
213
214
|
self.url = url
|
|
214
215
|
assert self.url.endswith("/api/"), "Phabricator API must end with /api/"
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
@cached_property
|
|
218
|
+
def user(self):
|
|
219
|
+
"""
|
|
220
|
+
Return the authenticated user details
|
|
221
|
+
"""
|
|
222
|
+
return self.request("user.whoami")
|
|
219
223
|
|
|
220
224
|
@property
|
|
221
225
|
def hostname(self):
|
|
@@ -48,7 +48,6 @@ def get_owners():
|
|
|
48
48
|
"Secondary",
|
|
49
49
|
"Engineering REO",
|
|
50
50
|
"Release Duty",
|
|
51
|
-
"QA Owner",
|
|
52
51
|
"Corresponding ESR",
|
|
53
52
|
"Release Date",
|
|
54
53
|
] != table[0]:
|
|
@@ -58,7 +57,7 @@ def get_owners():
|
|
|
58
57
|
for row in table[1:]:
|
|
59
58
|
try:
|
|
60
59
|
# sometimes the date is 2019-XX-XX (when the date is not known)
|
|
61
|
-
release_date = utils.get_date_ymd(row[
|
|
60
|
+
release_date = utils.get_date_ymd(row[6])
|
|
62
61
|
except (AssertionError, ValueError):
|
|
63
62
|
continue
|
|
64
63
|
|
|
@@ -69,8 +68,7 @@ def get_owners():
|
|
|
69
68
|
"secondary": row[2],
|
|
70
69
|
"engineering reo": row[3],
|
|
71
70
|
"release duty": _get_list_people(row[4]),
|
|
72
|
-
"
|
|
73
|
-
"corresponding esr": row[6],
|
|
71
|
+
"corresponding esr": row[5],
|
|
74
72
|
"release date": release_date,
|
|
75
73
|
}
|
|
76
74
|
)
|
|
@@ -30,10 +30,9 @@ class Socorro(Connection):
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class SuperSearch(Socorro):
|
|
33
|
-
"""SuperSearch: https://crash-stats.mozilla.org/
|
|
33
|
+
"""SuperSearch: https://crash-stats.mozilla.org/api/#SuperSearch"""
|
|
34
34
|
|
|
35
35
|
URL = Socorro.API_URL + "/SuperSearch/"
|
|
36
|
-
URL_UNREDACTED = Socorro.API_URL + "/SuperSearchUnredacted/"
|
|
37
36
|
WEB_URL = Socorro.CRASH_STATS_URL + "/search/"
|
|
38
37
|
|
|
39
38
|
def __init__(
|
|
@@ -50,35 +49,49 @@ class SuperSearch(Socorro):
|
|
|
50
49
|
if queries is not None:
|
|
51
50
|
super(SuperSearch, self).__init__(queries, **kwargs)
|
|
52
51
|
else:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
url = SuperSearch.URL_UNREDACTED
|
|
59
|
-
unredacted = True
|
|
60
|
-
if not unredacted and "_columns" in params:
|
|
61
|
-
columns = params["_columns"]
|
|
62
|
-
if "url" in columns or "email" in columns:
|
|
63
|
-
url = SuperSearch.URL_UNREDACTED
|
|
64
|
-
if not unredacted:
|
|
65
|
-
for k, v in params.items():
|
|
66
|
-
if (
|
|
67
|
-
"url" in k
|
|
68
|
-
or "email" in k
|
|
69
|
-
or (
|
|
70
|
-
(isinstance(v, list) or isinstance(v, six.string_types))
|
|
71
|
-
and ("url" in v or "email" in v)
|
|
72
|
-
)
|
|
73
|
-
):
|
|
74
|
-
url = SuperSearch.URL_UNREDACTED
|
|
75
|
-
unredacted = True
|
|
76
|
-
break
|
|
77
|
-
|
|
52
|
+
if self.__has_deprecated_unredacted_params(params):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"Requesting PII data using the `SuperSearch` class is not "
|
|
55
|
+
"supported anymore. Please use `SuperSearchUnredacted` instead."
|
|
56
|
+
)
|
|
78
57
|
super(SuperSearch, self).__init__(
|
|
79
|
-
Query(
|
|
58
|
+
Query(self.URL, params, handler, handlerdata), **kwargs
|
|
80
59
|
)
|
|
81
60
|
|
|
61
|
+
def __has_deprecated_unredacted_params(self, params):
|
|
62
|
+
"""Check if the params is requesting PII data that we used to
|
|
63
|
+
automatically support retrieving it by redirecting the request to the
|
|
64
|
+
unredacted endpoint (i.e., SuperSearchUnredacted).
|
|
65
|
+
"""
|
|
66
|
+
if not isinstance(params, dict):
|
|
67
|
+
# This is a workaround to avoid crashing when the params values is a
|
|
68
|
+
# list of params. We could instead of this, check each item in the
|
|
69
|
+
# list, but it's not worth it since this method is just for backward
|
|
70
|
+
# compatibility.
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
unredacted = False
|
|
74
|
+
if "_facets" in params:
|
|
75
|
+
facets = params["_facets"]
|
|
76
|
+
if "url" in facets or "email" in facets:
|
|
77
|
+
unredacted = True
|
|
78
|
+
if not unredacted and "_columns" in params:
|
|
79
|
+
columns = params["_columns"]
|
|
80
|
+
if "url" in columns or "email" in columns:
|
|
81
|
+
unredacted = True
|
|
82
|
+
if not unredacted:
|
|
83
|
+
for k, v in params.items():
|
|
84
|
+
if (
|
|
85
|
+
"url" in k
|
|
86
|
+
or "email" in k
|
|
87
|
+
or (
|
|
88
|
+
(isinstance(v, list) or isinstance(v, six.string_types))
|
|
89
|
+
and ("url" in v or "email" in v)
|
|
90
|
+
)
|
|
91
|
+
):
|
|
92
|
+
unredacted = True
|
|
93
|
+
return unredacted
|
|
94
|
+
|
|
82
95
|
@staticmethod
|
|
83
96
|
def get_link(params):
|
|
84
97
|
return utils.get_url(SuperSearch.WEB_URL) + utils.get_params_for_url(params)
|
|
@@ -109,6 +122,12 @@ class SuperSearch(Socorro):
|
|
|
109
122
|
return search_date
|
|
110
123
|
|
|
111
124
|
|
|
125
|
+
class SuperSearchUnredacted(SuperSearch):
|
|
126
|
+
"""SuperSearchUnredacted: https://crash-stats.mozilla.org/api/#SuperSearchUnredacted"""
|
|
127
|
+
|
|
128
|
+
URL = Socorro.API_URL + "/SuperSearchUnredacted/"
|
|
129
|
+
|
|
130
|
+
|
|
112
131
|
class ProcessedCrash(Socorro):
|
|
113
132
|
"""ProcessedCrash: https://crash-stats.mozilla.org/api/#ProcessedCrash"""
|
|
114
133
|
|
|
@@ -244,3 +263,50 @@ class Bugs(Socorro):
|
|
|
244
263
|
data[k] = list(v)
|
|
245
264
|
|
|
246
265
|
return data
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class SignatureFirstDate(Socorro):
|
|
269
|
+
"""SignatureFirstDate: https://crash-stats.mozilla.org/api/#SignatureFirstDate"""
|
|
270
|
+
|
|
271
|
+
URL = Socorro.API_URL + "/SignatureFirstDate/"
|
|
272
|
+
|
|
273
|
+
def __init__(
|
|
274
|
+
self, params=None, handler=None, handlerdata=None, queries=None, **kwargs
|
|
275
|
+
):
|
|
276
|
+
"""Constructor
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
params (Optional[dict]): the params for the query
|
|
280
|
+
handler (Optional[function]): handler to use with the result of the query
|
|
281
|
+
handlerdata (Optional): data used in second argument of the handler
|
|
282
|
+
queries (Optional[List[Query]]): queries to execute
|
|
283
|
+
"""
|
|
284
|
+
if queries:
|
|
285
|
+
super(SignatureFirstDate, self).__init__(queries, **kwargs)
|
|
286
|
+
else:
|
|
287
|
+
super(SignatureFirstDate, self).__init__(
|
|
288
|
+
Query(SignatureFirstDate.URL, params, handler, handlerdata), **kwargs
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def get_signatures(signatures):
|
|
293
|
+
"""Get the first date and build id for the specified signatures.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
signatures (List[str]): signatures to check.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
dict: the first date for each signature.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def default_handler(json, data):
|
|
303
|
+
data.extend(json["hits"])
|
|
304
|
+
|
|
305
|
+
data = []
|
|
306
|
+
SignatureFirstDate(
|
|
307
|
+
params={"signatures": signatures},
|
|
308
|
+
handler=default_handler,
|
|
309
|
+
handlerdata=data,
|
|
310
|
+
).wait()
|
|
311
|
+
|
|
312
|
+
return data
|
|
@@ -8,6 +8,7 @@ import operator
|
|
|
8
8
|
import os.path
|
|
9
9
|
import random
|
|
10
10
|
from datetime import date, datetime, timedelta
|
|
11
|
+
from itertools import count
|
|
11
12
|
|
|
12
13
|
import dateutil.parser
|
|
13
14
|
import pytz
|
|
@@ -381,3 +382,39 @@ def get_language(path):
|
|
|
381
382
|
for lang, names in langs.items():
|
|
382
383
|
if name in names or extension in names:
|
|
383
384
|
return lang
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# This is roughly equivalent to the itertools.islice which was added in
|
|
388
|
+
# Python 3.10.
|
|
389
|
+
# Copied from https://docs.python.org/3/library/itertools.html#itertools.islice
|
|
390
|
+
def islice(iterable, *args):
|
|
391
|
+
# islice('ABCDEFG', 2) → A B
|
|
392
|
+
# islice('ABCDEFG', 2, 4) → C D
|
|
393
|
+
# islice('ABCDEFG', 2, None) → C D E F G
|
|
394
|
+
# islice('ABCDEFG', 0, None, 2) → A C E G
|
|
395
|
+
|
|
396
|
+
s = slice(*args)
|
|
397
|
+
start = 0 if s.start is None else s.start
|
|
398
|
+
stop = s.stop
|
|
399
|
+
step = 1 if s.step is None else s.step
|
|
400
|
+
if start < 0 or (stop is not None and stop < 0) or step <= 0:
|
|
401
|
+
raise ValueError
|
|
402
|
+
|
|
403
|
+
indices = count() if stop is None else range(max(start, stop))
|
|
404
|
+
next_i = start
|
|
405
|
+
for i, element in zip(indices, iterable):
|
|
406
|
+
if i == next_i:
|
|
407
|
+
yield element
|
|
408
|
+
next_i += step
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# This is roughly equivalent to the itertools.batched which was added in
|
|
412
|
+
# Python 3.12.
|
|
413
|
+
# Copied from https://docs.python.org/3/library/itertools.html#itertools.batched
|
|
414
|
+
def batched(iterable, n):
|
|
415
|
+
# batched('ABCDEFG', 3) → ABC DEF G
|
|
416
|
+
if n < 1:
|
|
417
|
+
raise ValueError("n must be at least one")
|
|
418
|
+
iterator = iter(iterable)
|
|
419
|
+
while batch := tuple(islice(iterator, n)):
|
|
420
|
+
yield batch
|
libmozdata-0.2.3/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.2.3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -6,9 +6,9 @@ import json
|
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
8
|
from collections import defaultdict
|
|
9
|
+
from distutils.version import LooseVersion
|
|
9
10
|
|
|
10
11
|
import requests
|
|
11
|
-
from distutils.version import LooseVersion
|
|
12
12
|
|
|
13
13
|
SEARCH_URL = os.getenv("BUILDHUB_SEARCH_URL", "https://buildhub.moz.tools/api/search")
|
|
14
14
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|