Skip to content

Commit 943f3f6

Browse files
committed
KUMO-3323: It adds the ability to cancel multiple jobs
WIP: It is missing tests.
1 parent 8319db9 commit 943f3f6

File tree

3 files changed

+68
-0
lines changed

3 files changed

+68
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ docs/_build
1414

1515
.DS_Store
1616
pytestdebug.log
17+
.idea

scrapinghub/client/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class Unauthorized(ScrapinghubAPIError):
4040
"""Request lacks valid authentication credentials for the target resource."""
4141

4242

43+
class Forbidden(ScrapinghubAPIError):
44+
"""You don't have the permission to access the requested resource.
45+
It is either read-protected or not readable by the server."""
46+
47+
4348
class NotFound(ScrapinghubAPIError):
4449
"""Entity doesn't exist (e.g. spider or project)."""
4550

@@ -68,6 +73,8 @@ def wrapped(*args, **kwargs):
6873
raise BadRequest(http_error=exc)
6974
elif status_code == 401:
7075
raise Unauthorized(http_error=exc)
76+
elif status_code == 403:
77+
raise Forbidden(http_error=exc)
7178
elif status_code == 404:
7279
raise NotFound(http_error=exc)
7380
elif status_code == 413:

scrapinghub/client/jobs.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import absolute_import
22

3+
import json
4+
35
from ..hubstorage.job import JobMeta as _JobMeta
46
from ..hubstorage.job import Items as _Items
57
from ..hubstorage.job import Logs as _Logs
@@ -77,6 +79,64 @@ def count(self, spider=None, state=None, has_tag=None, lacks_tag=None,
7779
params['spider'] = self.spider.name
7880
return next(self._project.jobq.apiget(('count',), params=params))
7981

82+
def cancel_jobs(self, keys=None, count=None, **params):
83+
"""Cancel a list of jobs using the keys provided.
84+
85+
:param keys: (optional) a list of strings containing the job keys in
86+
the format: <project>/<spider>/<job_id>.
87+
:param count: (optional) it requires admin access. Used for admins
88+
to bulk cancel an amount of ``count`` jobs.
89+
90+
:return: a dict with the amount of jobs cancelled.
91+
:rtype: :class:`dict`
92+
93+
Usage:
94+
95+
- cancel jobs 123 and 321 from project 111 and spiders 222 and 333::
96+
97+
>>> project.jobs.cancel_jobs(['111/222/123', '111/333/321'])
98+
{'count': 2}
99+
100+
- cancel 100 jobs asynchronously::
101+
102+
>>> project.jobs.cancel_jobs(count=100)
103+
{'count': 100}
104+
"""
105+
update_kwargs(params, count=count, keys=keys)
106+
keys = params.get('keys')
107+
count = params.get('count')
108+
109+
if keys and count:
110+
raise ValueError("keys and count can't be defined simultaneously")
111+
112+
elif not keys and not count:
113+
raise ValueError("keys or count should be defined")
114+
115+
elif keys:
116+
if not isinstance(keys, list):
117+
raise ValueError("keys should be a list")
118+
119+
# it raises ValueError if invalid
120+
keys = [parse_job_key(k) for k in keys]
121+
122+
if not all([key.project_id == self.project_id for key in keys]):
123+
raise ValueError(
124+
"all keys should belong to project: %s" % self.project_id
125+
)
126+
127+
# change it to the format in which JobQ expects.
128+
data = [{"key": str(k)} for k in keys]
129+
130+
# may raise BadRequest if JobQ doesn't validate
131+
return list(self._project.jobq.apipost("cancel",
132+
data=json.dumps(data)))[0]
133+
elif count:
134+
if not isinstance(count, int):
135+
raise ValueError("count should be an int")
136+
137+
# may raise Forbidden
138+
return self._project.jobq.apipost("cancel?count=%s" % count)
139+
80140
def iter(self, count=None, start=None, spider=None, state=None,
81141
has_tag=None, lacks_tag=None, startts=None, endts=None,
82142
meta=None, **params):

0 commit comments

Comments
 (0)