Skip to content

Commit 007e2a9

Browse files
feat(job-deployments): add OpenAPI serverless jobs service
Co-authored-by: imagene-shahar <imagene-shahar@users.noreply.github.com>
1 parent 50276b4 commit 007e2a9

8 files changed

Lines changed: 372 additions & 0 deletions

File tree

datacrunch_compat/datacrunch/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
images,
1616
instance_types,
1717
instances,
18+
job_deployments,
1819
long_term,
1920
locations,
2021
ssh_keys,
@@ -42,6 +43,7 @@
4243
'images',
4344
'instance_types',
4445
'instances',
46+
'job_deployments',
4547
'long_term',
4648
'locations',
4749
'ssh_keys',

datacrunch_compat/datacrunch/datacrunch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from verda.images import ImagesService
1313
from verda.instance_types import InstanceTypesService
1414
from verda.instances import InstancesService
15+
from verda.job_deployments import JobDeploymentsService
1516
from verda.long_term import LongTermService
1617
from verda.locations import LocationsService
1718
from verda.ssh_keys import SSHKeysService
@@ -32,6 +33,7 @@
3233
'ImagesService',
3334
'InstanceTypesService',
3435
'InstancesService',
36+
'JobDeploymentsService',
3537
'LongTermService',
3638
'LocationsService',
3739
'SSHKeysService',
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Job Deployments
2+
===============
3+
4+
.. autoclass:: verda.job_deployments.JobDeploymentsService
5+
:members:
6+
7+
.. autoclass:: verda.job_deployments.JobDeployment
8+
:members:
9+
10+
.. autoclass:: verda.job_deployments.JobDeploymentSummary
11+
:members:
12+
13+
.. autoclass:: verda.job_deployments.JobScalingOptions
14+
:members:
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import json
2+
3+
import pytest
4+
import responses # https://github.com/getsentry/responses
5+
6+
from verda.containers import ComputeResource, Container, ContainerRegistrySettings
7+
from verda.exceptions import APIException
8+
from verda.job_deployments import (
9+
JobDeployment,
10+
JobDeploymentsService,
11+
JobDeploymentStatus,
12+
JobDeploymentSummary,
13+
JobScalingOptions,
14+
)
15+
16+
JOB_NAME = 'test-job'
17+
CONTAINER_NAME = 'worker'
18+
INVALID_REQUEST = 'INVALID_REQUEST'
19+
INVALID_REQUEST_MESSAGE = 'Invalid request'
20+
21+
JOB_SUMMARY_PAYLOAD = [
22+
{
23+
'name': JOB_NAME,
24+
'created_at': '2024-01-01T00:00:00Z',
25+
'compute': {
26+
'name': 'H100',
27+
'size': 1,
28+
},
29+
}
30+
]
31+
32+
JOB_PAYLOAD = {
33+
'name': JOB_NAME,
34+
'containers': [
35+
{
36+
'name': CONTAINER_NAME,
37+
'image': 'busybox:latest',
38+
'exposed_port': 8080,
39+
'env': [],
40+
'volume_mounts': [],
41+
}
42+
],
43+
'endpoint_base_url': 'https://test-job.datacrunch.io',
44+
'created_at': '2024-01-01T00:00:00Z',
45+
'compute': {
46+
'name': 'H100',
47+
'size': 1,
48+
},
49+
'container_registry_settings': {
50+
'is_private': False,
51+
'credentials': None,
52+
},
53+
}
54+
55+
SCALING_PAYLOAD = {
56+
'max_replica_count': 5,
57+
'queue_message_ttl_seconds': 600,
58+
'deadline_seconds': 1800,
59+
}
60+
61+
62+
class TestJobDeploymentsService:
63+
@pytest.fixture
64+
def service(self, http_client):
65+
return JobDeploymentsService(http_client)
66+
67+
@pytest.fixture
68+
def endpoint(self, http_client):
69+
return http_client._base_url + '/job-deployments'
70+
71+
@responses.activate
72+
def test_get_job_deployments(self, service, endpoint):
73+
responses.add(responses.GET, endpoint, json=JOB_SUMMARY_PAYLOAD, status=200)
74+
75+
deployments = service.get()
76+
77+
assert isinstance(deployments, list)
78+
assert len(deployments) == 1
79+
assert isinstance(deployments[0], JobDeploymentSummary)
80+
assert deployments[0].name == JOB_NAME
81+
assert deployments[0].compute.name == 'H100'
82+
assert responses.assert_call_count(endpoint, 1) is True
83+
84+
@responses.activate
85+
def test_get_job_deployment_by_name(self, service, endpoint):
86+
url = f'{endpoint}/{JOB_NAME}'
87+
responses.add(responses.GET, url, json=JOB_PAYLOAD, status=200)
88+
89+
deployment = service.get_by_name(JOB_NAME)
90+
91+
assert isinstance(deployment, JobDeployment)
92+
assert deployment.name == JOB_NAME
93+
assert deployment.endpoint_base_url == 'https://test-job.datacrunch.io'
94+
assert deployment.compute.size == 1
95+
assert deployment.containers[0].name == CONTAINER_NAME
96+
assert responses.assert_call_count(url, 1) is True
97+
98+
@responses.activate
99+
def test_get_job_deployment_by_name_error(self, service, endpoint):
100+
url = f'{endpoint}/missing-job'
101+
responses.add(
102+
responses.GET,
103+
url,
104+
json={'code': INVALID_REQUEST, 'message': INVALID_REQUEST_MESSAGE},
105+
status=400,
106+
)
107+
108+
with pytest.raises(APIException) as excinfo:
109+
service.get_by_name('missing-job')
110+
111+
assert excinfo.value.code == INVALID_REQUEST
112+
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
113+
assert responses.assert_call_count(url, 1) is True
114+
115+
@responses.activate
116+
def test_create_job_deployment(self, service, endpoint):
117+
responses.add(responses.POST, endpoint, json=JOB_PAYLOAD, status=201)
118+
119+
deployment = JobDeployment(
120+
name=JOB_NAME,
121+
containers=[Container(image='busybox:latest', exposed_port=8080, name=CONTAINER_NAME)],
122+
compute=ComputeResource(name='H100', size=1),
123+
container_registry_settings=ContainerRegistrySettings(is_private=False),
124+
scaling=JobScalingOptions(**SCALING_PAYLOAD),
125+
)
126+
127+
created = service.create(deployment)
128+
129+
assert isinstance(created, JobDeployment)
130+
assert created.name == JOB_NAME
131+
request_body = json.loads(responses.calls[0].request.body.decode('utf-8'))
132+
assert request_body['scaling'] == SCALING_PAYLOAD
133+
assert responses.assert_call_count(endpoint, 1) is True
134+
135+
@responses.activate
136+
def test_update_job_deployment(self, service, endpoint):
137+
url = f'{endpoint}/{JOB_NAME}'
138+
responses.add(responses.PATCH, url, json=JOB_PAYLOAD, status=200)
139+
140+
deployment = JobDeployment(
141+
name=JOB_NAME,
142+
containers=[Container(image='busybox:latest', exposed_port=8080, name=CONTAINER_NAME)],
143+
compute=ComputeResource(name='H100', size=1),
144+
scaling=JobScalingOptions(**SCALING_PAYLOAD),
145+
)
146+
147+
updated = service.update(JOB_NAME, deployment)
148+
149+
assert isinstance(updated, JobDeployment)
150+
assert updated.name == JOB_NAME
151+
assert responses.assert_call_count(url, 1) is True
152+
153+
@responses.activate
154+
def test_delete_job_deployment(self, service, endpoint):
155+
url = f'{endpoint}/{JOB_NAME}?timeout=120000'
156+
responses.add(responses.DELETE, url, status=200)
157+
158+
service.delete(JOB_NAME, timeout=120000)
159+
160+
assert responses.assert_call_count(url, 1) is True
161+
162+
@responses.activate
163+
def test_get_job_status(self, service, endpoint):
164+
url = f'{endpoint}/{JOB_NAME}/status'
165+
responses.add(responses.GET, url, json={'status': 'running'}, status=200)
166+
167+
status = service.get_status(JOB_NAME)
168+
169+
assert status == JobDeploymentStatus.RUNNING
170+
assert responses.assert_call_count(url, 1) is True
171+
172+
@responses.activate
173+
def test_get_job_scaling_options(self, service, endpoint):
174+
url = f'{endpoint}/{JOB_NAME}/scaling'
175+
responses.add(responses.GET, url, json=SCALING_PAYLOAD, status=200)
176+
177+
scaling = service.get_scaling_options(JOB_NAME)
178+
179+
assert isinstance(scaling, JobScalingOptions)
180+
assert scaling.max_replica_count == 5
181+
assert scaling.deadline_seconds == 1800
182+
assert responses.assert_call_count(url, 1) is True
183+
184+
@responses.activate
185+
def test_pause_job_deployment(self, service, endpoint):
186+
url = f'{endpoint}/{JOB_NAME}/pause'
187+
responses.add(responses.POST, url, status=204)
188+
189+
service.pause(JOB_NAME)
190+
191+
assert responses.assert_call_count(url, 1) is True
192+
193+
@responses.activate
194+
def test_resume_job_deployment(self, service, endpoint):
195+
url = f'{endpoint}/{JOB_NAME}/resume'
196+
responses.add(responses.POST, url, status=204)
197+
198+
service.resume(JOB_NAME)
199+
200+
assert responses.assert_call_count(url, 1) is True
201+
202+
@responses.activate
203+
def test_purge_job_deployment_queue(self, service, endpoint):
204+
url = f'{endpoint}/{JOB_NAME}/purge-queue'
205+
responses.add(responses.POST, url, status=204)
206+
207+
service.purge_queue(JOB_NAME)
208+
209+
assert responses.assert_call_count(url, 1) is True

tests/unit_tests/test_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_client(self):
2929
assert hasattr(client, 'container_types')
3030
assert hasattr(client, 'cluster_types')
3131
assert hasattr(client, 'long_term')
32+
assert hasattr(client, 'job_deployments')
3233

3334
@responses.activate
3435
def test_client_with_default_base_url(self):

verda/_verda.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from verda.images import ImagesService
1111
from verda.instance_types import InstanceTypesService
1212
from verda.instances import InstancesService
13+
from verda.job_deployments import JobDeploymentsService
1314
from verda.long_term import LongTermService
1415
from verda.locations import LocationsService
1516
from verda.ssh_keys import SSHKeysService
@@ -83,6 +84,9 @@ def __init__(
8384
self.containers: ContainersService = ContainersService(self._http_client, inference_key)
8485
"""Containers service. Deploy, manage, and monitor container deployments"""
8586

87+
self.job_deployments: JobDeploymentsService = JobDeploymentsService(self._http_client)
88+
"""Job deployments service. Deploy and manage serverless jobs"""
89+
8690
self.container_types: ContainerTypesService = ContainerTypesService(self._http_client)
8791
"""Container types service. Get available serverless container SKUs"""
8892

verda/job_deployments/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from verda.job_deployments._job_deployments import (
2+
JobDeployment,
3+
JobDeploymentsService,
4+
JobDeploymentStatus,
5+
JobDeploymentSummary,
6+
JobScalingOptions,
7+
)
8+
9+
__all__ = [
10+
'JobDeployment',
11+
'JobDeploymentStatus',
12+
'JobDeploymentSummary',
13+
'JobDeploymentsService',
14+
'JobScalingOptions',
15+
]

0 commit comments

Comments
 (0)