Skip to content

Commit aea1b17

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

File tree

5 files changed

+113
-0
lines changed

5 files changed

+113
-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):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
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

tests/client/test_job.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from scrapinghub.client.logs import Logs
99
from scrapinghub.client.requests import Requests
1010
from scrapinghub.client.samples import Samples
11+
from scrapinghub.client.exceptions import BadRequest
1112

1213
from ..conftest import TEST_PROJECT_ID
1314
from ..conftest import TEST_SPIDER_NAME
@@ -44,6 +45,49 @@ def test_job_update_tags(spider):
4445
assert job2.metadata.get('tags') == ['tag2']
4546

4647

48+
def test_cancel_jobs(spider):
49+
job1 = spider.jobs.run(job_args={'subid': 'tags-1'}, add_tag=['tag1'])
50+
job2 = spider.jobs.run(job_args={'subid': 'tags-2'}, add_tag=['tag2'])
51+
assert job1.metadata.get('state') == 'pending'
52+
assert job2.metadata.get('state') == 'pending'
53+
54+
with pytest.raises(ValueError) as err:
55+
output = spider.jobs.cancel_jobs()
56+
57+
assert 'keys or count should be defined' in str(err)
58+
59+
with pytest.raises(ValueError) as err:
60+
output = spider.jobs.cancel_jobs(['2222222/1/1'], count=2)
61+
62+
assert "keys and count can't be defined simultaneously" in str(err)
63+
64+
with pytest.raises(ValueError) as err:
65+
output = spider.jobs.cancel_jobs(keys="testing")
66+
67+
assert 'keys should be a list' in str(err)
68+
69+
with pytest.raises(ValueError) as err:
70+
output = spider.jobs.cancel_jobs(count=[1,2])
71+
72+
assert 'count should be an int' in str(err)
73+
74+
with pytest.raises(ValueError) as err:
75+
output = spider.jobs.cancel_jobs(['2222222/1/1', '2222226/1/1'])
76+
77+
assert 'all keys should belong to project' in str(err)
78+
79+
# Non-existent job
80+
output = spider.jobs.cancel_jobs(['%s/1/10000' % job1.project_id])
81+
assert output == {'count': 0}
82+
83+
# The expected behavior
84+
output = spider.jobs.cancel_jobs([job1.key, job2.key])
85+
86+
assert job1.metadata.get('state') == 'finished'
87+
assert job2.metadata.get('state') == 'finished'
88+
assert output == {'count': 2}
89+
90+
4791
def test_job_start(spider):
4892
job = spider.jobs.run()
4993
assert job.metadata.get('state') == 'pending'

0 commit comments

Comments
 (0)