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.
Files changed (39) hide show
  1. {libmozdata-0.2.3/libmozdata.egg-info → libmozdata-0.2.6}/PKG-INFO +1 -1
  2. libmozdata-0.2.6/VERSION +1 -0
  3. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/bugzilla.py +104 -46
  4. libmozdata-0.2.6/libmozdata/clouseau.py +76 -0
  5. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/connection.py +4 -3
  6. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/phabricator.py +7 -3
  7. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/release_owners.py +2 -4
  8. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/socorro.py +94 -28
  9. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/utils.py +37 -0
  10. {libmozdata-0.2.3 → libmozdata-0.2.6/libmozdata.egg-info}/PKG-INFO +1 -1
  11. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/SOURCES.txt +1 -0
  12. libmozdata-0.2.3/VERSION +0 -1
  13. {libmozdata-0.2.3 → libmozdata-0.2.6}/MANIFEST.in +0 -0
  14. {libmozdata-0.2.3 → libmozdata-0.2.6}/README.md +0 -0
  15. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/BZInfo.py +0 -0
  16. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/FXRevision.py +0 -0
  17. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/FileStats.py +0 -0
  18. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/HGFileInfo.py +0 -0
  19. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/__init__.py +0 -0
  20. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/buildhub.py +1 -1
  21. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/config.py +0 -0
  22. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/fx_trains.py +0 -0
  23. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/handler.py +0 -0
  24. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/hgmozilla.py +0 -0
  25. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/lando.py +0 -0
  26. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/modules.json +0 -0
  27. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/modules.py +0 -0
  28. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/patchanalysis.py +0 -0
  29. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/redash.py +0 -0
  30. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/vcs_map.py +0 -0
  31. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/versions.py +0 -0
  32. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata/wiki_parser.py +0 -0
  33. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/dependency_links.txt +0 -0
  34. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/not-zip-safe +0 -0
  35. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/requires.txt +0 -0
  36. {libmozdata-0.2.3 → libmozdata-0.2.6}/libmozdata.egg-info/top_level.txt +0 -0
  37. {libmozdata-0.2.3 → libmozdata-0.2.6}/requirements.txt +0 -0
  38. {libmozdata-0.2.3 → libmozdata-0.2.6}/setup.cfg +0 -0
  39. {libmozdata-0.2.3 → libmozdata-0.2.6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: libmozdata
3
- Version: 0.2.3
3
+ Version: 0.2.6
4
4
  Summary: Library to access and aggregate several Mozilla data sources.
5
5
  Home-page: https://github.com/mozilla/libmozdata
6
6
  Author: Mozilla Release Management
@@ -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 Bugzilla(Connection):
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(Connection):
822
+ class BugzillaUser(BugzillaBase):
820
823
  """Connection to bugzilla.mozilla.org"""
821
824
 
822
- URL = config.get("Bugzilla", "URL", "https://bugzilla.mozilla.org")
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(Connection):
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
- URL = config.get("Bugzilla", "URL", "https://bugzilla.mozilla.org")
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(Connection):
1012
+ class BugzillaShorten(BugzillaBase):
1024
1013
  """
1025
1014
  Connection to bugzilla.mozilla.org
1026
1015
  """
1027
1016
 
1028
- URL = config.get("Bugzilla", "URL", "https://bugzilla.mozilla.org")
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(Connection):
1054
+ class BugzillaComponent(BugzillaBase):
1073
1055
  """
1074
1056
  Connection to bugzilla.mozilla.org
1075
1057
  """
1076
1058
 
1077
- URL = config.get("Bugzilla", "URL", "https://bugzilla.mozilla.org")
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=True,
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=True,
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=True,
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
- # Test authentication
217
- self.user = self.request("user.whoami")
218
- logger.info("Authenticated on {} as {}".format(self.url, self.user["realName"]))
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[7])
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
- "qa owner": row[5],
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/search/?product=&_dont_run=1"""
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
- url = SuperSearch.URL
54
- unredacted = False
55
- if "_facets" in params:
56
- facets = params["_facets"]
57
- if "url" in facets or "email" in facets:
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(url, params, handler, handlerdata), **kwargs
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: libmozdata
3
- Version: 0.2.3
3
+ Version: 0.2.6
4
4
  Summary: Library to access and aggregate several Mozilla data sources.
5
5
  Home-page: https://github.com/mozilla/libmozdata
6
6
  Author: Mozilla Release Management
@@ -11,6 +11,7 @@ libmozdata/HGFileInfo.py
11
11
  libmozdata/__init__.py
12
12
  libmozdata/bugzilla.py
13
13
  libmozdata/buildhub.py
14
+ libmozdata/clouseau.py
14
15
  libmozdata/config.py
15
16
  libmozdata/connection.py
16
17
  libmozdata/fx_trains.py
libmozdata-0.2.3/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.2.3
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