trainml 0.5.5__py3-none-any.whl → 0.5.7__py3-none-any.whl

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.
@@ -49,16 +49,27 @@ def project_datastore(mock_trainml):
49
49
 
50
50
 
51
51
  @fixture
52
- def project_reservation(mock_trainml):
53
- yield specimen.ProjectReservation(
52
+ def project_data_connector(mock_trainml):
53
+ yield specimen.ProjectDataConnector(
54
+ mock_trainml,
55
+ id="ds-id-1",
56
+ name="connector 1",
57
+ project_uuid="proj-id-1",
58
+ type="custom",
59
+ region_uuid="reg-id-1",
60
+ )
61
+
62
+
63
+ @fixture
64
+ def project_service(mock_trainml):
65
+ yield specimen.ProjectService(
54
66
  mock_trainml,
55
67
  id="res-id-1",
56
- name="reservation 1",
68
+ name="service 1",
57
69
  project_uuid="proj-id-1",
58
70
  region_uuid="reg-id-1",
59
- type="port",
60
- resource="8001",
61
- hostname="service.local",
71
+ public=False,
72
+ hostname="asdf.proximl.cloud",
62
73
  )
63
74
 
64
75
 
@@ -72,9 +83,7 @@ class ProjectsTests:
72
83
  api_response = dict()
73
84
  mock_trainml._query = AsyncMock(return_value=api_response)
74
85
  await projects.get("1234")
75
- mock_trainml._query.assert_called_once_with(
76
- "/project/1234", "GET", dict()
77
- )
86
+ mock_trainml._query.assert_called_once_with("/project/1234", "GET", dict())
78
87
 
79
88
  @mark.asyncio
80
89
  async def test_list_projects(
@@ -96,9 +105,7 @@ class ProjectsTests:
96
105
  api_response = dict()
97
106
  mock_trainml._query = AsyncMock(return_value=api_response)
98
107
  await projects.remove("4567")
99
- mock_trainml._query.assert_called_once_with(
100
- "/project/4567", "DELETE", dict()
101
- )
108
+ mock_trainml._query.assert_called_once_with("/project/4567", "DELETE", dict())
102
109
 
103
110
  @mark.asyncio
104
111
  async def test_create_project_simple(self, projects, mock_trainml):
@@ -156,36 +163,65 @@ class ProjectDatastoreTests:
156
163
  assert not bool(empty_project_datastore)
157
164
 
158
165
 
159
- class ProjectReservationTests:
160
- def test_project_reservation_properties(self, project_reservation):
161
- assert isinstance(project_reservation.id, str)
162
- assert isinstance(project_reservation.name, str)
163
- assert isinstance(project_reservation.project_uuid, str)
164
- assert isinstance(project_reservation.type, str)
165
- assert isinstance(project_reservation.hostname, str)
166
- assert isinstance(project_reservation.resource, str)
167
- assert isinstance(project_reservation.region_uuid, str)
166
+ class ProjectDataConnectorTests:
167
+ def test_project_data_connector_properties(self, project_data_connector):
168
+ assert isinstance(project_data_connector.id, str)
169
+ assert isinstance(project_data_connector.name, str)
170
+ assert isinstance(project_data_connector.project_uuid, str)
171
+ assert isinstance(project_data_connector.type, str)
172
+ assert isinstance(project_data_connector.region_uuid, str)
173
+
174
+ def test_project_data_connector_str(self, project_data_connector):
175
+ string = str(project_data_connector)
176
+ regex = r"^{.*\"id\": \"" + project_data_connector.id + r"\".*}$"
177
+ assert isinstance(string, str)
178
+ assert re.match(regex, string)
179
+
180
+ def test_project_data_connector_repr(self, project_data_connector):
181
+ string = repr(project_data_connector)
182
+ regex = (
183
+ r"^ProjectDataConnector\( trainml , \*\*{.*'id': '"
184
+ + project_data_connector.id
185
+ + r"'.*}\)$"
186
+ )
187
+ assert isinstance(string, str)
188
+ assert re.match(regex, string)
189
+
190
+ def test_project_data_connector_bool(self, project_data_connector, mock_trainml):
191
+ empty_project_data_connector = specimen.ProjectDataConnector(mock_trainml)
192
+ assert bool(project_data_connector)
193
+ assert not bool(empty_project_data_connector)
168
194
 
169
- def test_project_reservation_str(self, project_reservation):
170
- string = str(project_reservation)
171
- regex = r"^{.*\"id\": \"" + project_reservation.id + r"\".*}$"
195
+
196
+ class ProjectServiceTests:
197
+ def test_project_service_properties(self, project_service):
198
+ assert isinstance(project_service.id, str)
199
+ assert isinstance(project_service.name, str)
200
+ assert isinstance(project_service.project_uuid, str)
201
+ assert isinstance(project_service.hostname, str)
202
+ assert isinstance(project_service.public, bool)
203
+ assert isinstance(project_service.region_uuid, str)
204
+
205
+ def test_project_service_str(self, project_service):
206
+ string = str(project_service)
207
+ regex = r"^{.*\"id\": \"" + project_service.id + r"\".*}$"
172
208
  assert isinstance(string, str)
173
209
  assert re.match(regex, string)
174
210
 
175
- def test_project_reservation_repr(self, project_reservation):
176
- string = repr(project_reservation)
211
+ def test_project_service_repr(self, project_service):
212
+ string = repr(project_service)
177
213
  regex = (
178
- r"^ProjectReservation\( trainml , \*\*{.*'id': '"
179
- + project_reservation.id
214
+ r"^ProjectService\( trainml , \*\*{.*'id': '"
215
+ + project_service.id
180
216
  + r"'.*}\)$"
181
217
  )
182
218
  assert isinstance(string, str)
183
219
  assert re.match(regex, string)
184
220
 
185
- def test_project_reservation_bool(self, project_reservation, mock_trainml):
186
- empty_project_reservation = specimen.ProjectReservation(mock_trainml)
187
- assert bool(project_reservation)
188
- assert not bool(empty_project_reservation)
221
+ def test_project_service_bool(self, project_service, mock_trainml):
222
+ empty_project_service = specimen.ProjectService(mock_trainml)
223
+ assert bool(project_service)
224
+ assert not bool(empty_project_service)
189
225
 
190
226
 
191
227
  class ProjectTests:
@@ -203,9 +239,7 @@ class ProjectTests:
203
239
 
204
240
  def test_project_repr(self, project):
205
241
  string = repr(project)
206
- regex = (
207
- r"^Project\( trainml , \*\*{.*'id': '" + project.id + r"'.*}\)$"
208
- )
242
+ regex = r"^Project\( trainml , \*\*{.*'id': '" + project.id + r"'.*}\)$"
209
243
  assert isinstance(string, str)
210
244
  assert re.match(regex, string)
211
245
 
@@ -226,18 +260,14 @@ class ProjectTests:
226
260
  api_response = dict()
227
261
  mock_trainml._query = AsyncMock(return_value=api_response)
228
262
  await project.refresh_datastores()
229
- mock_trainml._query.assert_called_once_with(
230
- "/project/1/datastores", "PATCH"
231
- )
263
+ mock_trainml._query.assert_called_once_with("/project/1/datastores", "PATCH")
232
264
 
233
265
  @mark.asyncio
234
- async def test_project_refresh_reservations(self, project, mock_trainml):
266
+ async def test_project_refresh_services(self, project, mock_trainml):
235
267
  api_response = dict()
236
268
  mock_trainml._query = AsyncMock(return_value=api_response)
237
- await project.refresh_reservations()
238
- mock_trainml._query.assert_called_once_with(
239
- "/project/1/reservations", "PATCH"
240
- )
269
+ await project.refresh_services()
270
+ mock_trainml._query.assert_called_once_with("/project/1/services", "PATCH")
241
271
 
242
272
  @mark.asyncio
243
273
  async def test_project_list_datastores(self, project, mock_trainml):
@@ -259,13 +289,11 @@ class ProjectTests:
259
289
  ]
260
290
  mock_trainml._query = AsyncMock(return_value=api_response)
261
291
  resp = await project.list_datastores()
262
- mock_trainml._query.assert_called_once_with(
263
- "/project/1/datastores", "GET"
264
- )
292
+ mock_trainml._query.assert_called_once_with("/project/1/datastores", "GET")
265
293
  assert len(resp) == 2
266
294
 
267
295
  @mark.asyncio
268
- async def test_project_list_reservations(self, project, mock_trainml):
296
+ async def test_project_list_services(self, project, mock_trainml):
269
297
  api_response = [
270
298
  {
271
299
  "project_uuid": "proj-id-1",
@@ -287,8 +315,6 @@ class ProjectTests:
287
315
  },
288
316
  ]
289
317
  mock_trainml._query = AsyncMock(return_value=api_response)
290
- resp = await project.list_reservations()
291
- mock_trainml._query.assert_called_once_with(
292
- "/project/1/reservations", "GET"
293
- )
318
+ resp = await project.list_services()
319
+ mock_trainml._query.assert_called_once_with("/project/1/services", "GET")
294
320
  assert len(resp) == 2
trainml/__init__.py CHANGED
@@ -13,5 +13,5 @@ logging.basicConfig(
13
13
  logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
- __version__ = "0.5.5"
16
+ __version__ = "0.5.7"
17
17
  __all__ = "TrainML"
trainml/checkpoints.py CHANGED
@@ -23,9 +23,7 @@ class Checkpoints(object):
23
23
 
24
24
  async def list(self, **kwargs):
25
25
  resp = await self.trainml._query(f"/checkpoint", "GET", kwargs)
26
- checkpoints = [
27
- Checkpoint(self.trainml, **checkpoint) for checkpoint in resp
28
- ]
26
+ checkpoints = [Checkpoint(self.trainml, **checkpoint) for checkpoint in resp]
29
27
  return checkpoints
30
28
 
31
29
  async def list_public(self, **kwargs):
@@ -39,8 +37,7 @@ class Checkpoints(object):
39
37
  source_type=source_type,
40
38
  source_uri=source_uri,
41
39
  source_options=kwargs.get("source_options"),
42
- project_uuid=kwargs.get("project_uuid")
43
- or self.trainml.active_project,
40
+ project_uuid=kwargs.get("project_uuid") or self.trainml.active_project,
44
41
  )
45
42
  payload = {k: v for k, v in data.items() if v is not None}
46
43
  logging.info(f"Creating Checkpoint {name}")
@@ -60,9 +57,7 @@ class Checkpoint:
60
57
  def __init__(self, trainml, **kwargs):
61
58
  self.trainml = trainml
62
59
  self._checkpoint = kwargs
63
- self._id = self._checkpoint.get(
64
- "id", self._checkpoint.get("checkpoint_uuid")
65
- )
60
+ self._id = self._checkpoint.get("id", self._checkpoint.get("checkpoint_uuid"))
66
61
  self._status = self._checkpoint.get("status")
67
62
  self._name = self._checkpoint.get("name")
68
63
  self._size = self._checkpoint.get("size")
@@ -123,15 +118,17 @@ class Checkpoint:
123
118
  entity_type="checkpoint",
124
119
  project_uuid=self._checkpoint.get("project_uuid"),
125
120
  cidr=self._checkpoint.get("vpn").get("cidr"),
126
- ssh_port=self._checkpoint.get("vpn")
127
- .get("client")
128
- .get("ssh_port"),
129
- input_path=self._checkpoint.get("source_uri")
130
- if self.status in ["new", "downloading"]
131
- else None,
132
- output_path=self._checkpoint.get("output_uri")
133
- if self.status == "exporting"
134
- else None,
121
+ ssh_port=self._checkpoint.get("vpn").get("client").get("ssh_port"),
122
+ input_path=(
123
+ self._checkpoint.get("source_uri")
124
+ if self.status in ["new", "downloading"]
125
+ else None
126
+ ),
127
+ output_path=(
128
+ self._checkpoint.get("output_uri")
129
+ if self.status == "exporting"
130
+ else None
131
+ ),
135
132
  )
136
133
  else:
137
134
  details = dict()
@@ -195,9 +192,7 @@ class Checkpoint:
195
192
  if msg_handler:
196
193
  msg_handler(data)
197
194
  else:
198
- timestamp = datetime.fromtimestamp(
199
- int(data.get("time")) / 1000
200
- )
195
+ timestamp = datetime.fromtimestamp(int(data.get("time")) / 1000)
201
196
  print(
202
197
  f"{timestamp.strftime('%m/%d/%Y, %H:%M:%S')}: {data.get('msg').rstrip()}"
203
198
  )
@@ -224,19 +219,24 @@ class Checkpoint:
224
219
  return self
225
220
 
226
221
  async def wait_for(self, status, timeout=300):
222
+ if self.status == status:
223
+ return
227
224
  valid_statuses = ["downloading", "ready", "archived"]
228
225
  if not status in valid_statuses:
229
226
  raise SpecificationError(
230
227
  "status",
231
228
  f"Invalid wait_for status {status}. Valid statuses are: {valid_statuses}",
232
229
  )
233
- if self.status == status:
234
- return
230
+
231
+ MAX_TIMEOUT = 24 * 60 * 60
232
+ if timeout > MAX_TIMEOUT:
233
+ raise SpecificationError(
234
+ "timeout",
235
+ f"timeout must be less than {MAX_TIMEOUT} seconds.",
236
+ )
235
237
  POLL_INTERVAL_MIN = 5
236
238
  POLL_INTERVAL_MAX = 60
237
- POLL_INTERVAL = max(
238
- min(timeout / 60, POLL_INTERVAL_MAX), POLL_INTERVAL_MIN
239
- )
239
+ POLL_INTERVAL = max(min(timeout / 60, POLL_INTERVAL_MAX), POLL_INTERVAL_MIN)
240
240
  retry_count = math.ceil(timeout / POLL_INTERVAL)
241
241
  count = 0
242
242
  while count < retry_count:
@@ -15,4 +15,5 @@ from trainml.cli.cloudbender.region import region
15
15
  from trainml.cli.cloudbender.node import node
16
16
  from trainml.cli.cloudbender.device import device
17
17
  from trainml.cli.cloudbender.datastore import datastore
18
- from trainml.cli.cloudbender.reservation import reservation
18
+ from trainml.cli.cloudbender.data_connector import data_connector
19
+ from trainml.cli.cloudbender.service import service
@@ -0,0 +1,159 @@
1
+ import click
2
+ from trainml.cli import cli, pass_config, search_by_id_name
3
+ from trainml.cli.cloudbender import cloudbender
4
+
5
+
6
+ @cloudbender.group()
7
+ @pass_config
8
+ def data_connector(config):
9
+ """trainML CloudBender data connector commands."""
10
+ pass
11
+
12
+
13
+ @data_connector.command()
14
+ @click.option(
15
+ "--provider",
16
+ "-p",
17
+ type=click.STRING,
18
+ required=True,
19
+ help="The provider ID of the region.",
20
+ )
21
+ @click.option(
22
+ "--region",
23
+ "-r",
24
+ type=click.STRING,
25
+ required=True,
26
+ help="The region ID to list data connectors for.",
27
+ )
28
+ @pass_config
29
+ def list(config, provider, region):
30
+ """List data connectors."""
31
+ data = [
32
+ ["ID", "NAME", "TYPE"],
33
+ [
34
+ "-" * 80,
35
+ "-" * 80,
36
+ "-" * 80,
37
+ ],
38
+ ]
39
+
40
+ data_connectors = config.trainml.run(
41
+ config.trainml.client.cloudbender.data_connectors.list(
42
+ provider_uuid=provider, region_uuid=region
43
+ )
44
+ )
45
+
46
+ for data_connector in data_connectors:
47
+ data.append(
48
+ [
49
+ data_connector.id,
50
+ data_connector.name,
51
+ data_connector.type,
52
+ ]
53
+ )
54
+
55
+ for row in data:
56
+ click.echo(
57
+ "{: >37.36} {: >29.28} {: >9.8}" "".format(*row),
58
+ file=config.stdout,
59
+ )
60
+
61
+
62
+ @data_connector.command()
63
+ @click.option(
64
+ "--provider",
65
+ "-p",
66
+ type=click.STRING,
67
+ required=True,
68
+ help="The provider ID of the region.",
69
+ )
70
+ @click.option(
71
+ "--region",
72
+ "-r",
73
+ type=click.STRING,
74
+ required=True,
75
+ help="The region ID to create the data_connector in.",
76
+ )
77
+ @click.option(
78
+ "--type",
79
+ "-t",
80
+ type=click.Choice(
81
+ [
82
+ "custom",
83
+ ],
84
+ case_sensitive=False,
85
+ ),
86
+ required=True,
87
+ help="The type of data connector to create.",
88
+ )
89
+ @click.option(
90
+ "--protocol",
91
+ "-r",
92
+ type=click.STRING,
93
+ help="The transport protocol of the data connector",
94
+ )
95
+ @click.option(
96
+ "--port-range",
97
+ "-p",
98
+ type=click.STRING,
99
+ help="The port range of the data connector",
100
+ )
101
+ @click.option(
102
+ "--cidr",
103
+ "-i",
104
+ type=click.STRING,
105
+ help="The IP range to allow in CIDR notation",
106
+ )
107
+ @click.argument("name", type=click.STRING, required=True)
108
+ @pass_config
109
+ def create(config, provider, region, type, protocol, port_range, cidr, name):
110
+ """
111
+ Creates a data_connector.
112
+ """
113
+ return config.trainml.run(
114
+ config.trainml.client.cloudbender.data_connectors.create(
115
+ provider_uuid=provider,
116
+ region_uuid=region,
117
+ name=name,
118
+ type=type,
119
+ protocol=protocol,
120
+ port_range=port_range,
121
+ cidr=cidr,
122
+ )
123
+ )
124
+
125
+
126
+ @data_connector.command()
127
+ @click.option(
128
+ "--provider",
129
+ "-p",
130
+ type=click.STRING,
131
+ required=True,
132
+ help="The provider ID of the region.",
133
+ )
134
+ @click.option(
135
+ "--region",
136
+ "-r",
137
+ type=click.STRING,
138
+ required=True,
139
+ help="The region ID to remove the data_connector from.",
140
+ )
141
+ @click.argument("data_connector", type=click.STRING)
142
+ @pass_config
143
+ def remove(config, provider, region, data_connector):
144
+ """
145
+ Remove a data_connector.
146
+
147
+ DATASTORE may be specified by name or ID, but ID is preferred.
148
+ """
149
+ data_connectors = config.trainml.run(
150
+ config.trainml.client.cloudbender.data_connectors.list(
151
+ provider_uuid=provider, region_uuid=region
152
+ )
153
+ )
154
+
155
+ found = search_by_id_name(data_connector, data_connectors)
156
+ if None is found:
157
+ raise click.UsageError("Cannot find specified data_connector.")
158
+
159
+ return config.trainml.run(found.remove())
@@ -0,0 +1,146 @@
1
+ import click
2
+ from trainml.cli import cli, pass_config, search_by_id_name
3
+ from trainml.cli.cloudbender import cloudbender
4
+
5
+
6
+ @cloudbender.group()
7
+ @pass_config
8
+ def service(config):
9
+ """trainML CloudBender service commands."""
10
+ pass
11
+
12
+
13
+ @service.command()
14
+ @click.option(
15
+ "--provider",
16
+ "-p",
17
+ type=click.STRING,
18
+ required=True,
19
+ help="The provider ID of the region.",
20
+ )
21
+ @click.option(
22
+ "--region",
23
+ "-r",
24
+ type=click.STRING,
25
+ required=True,
26
+ help="The region ID to list services for.",
27
+ )
28
+ @pass_config
29
+ def list(config, provider, region):
30
+ """List services."""
31
+ data = [
32
+ ["ID", "NAME", "HOSTNAME"],
33
+ [
34
+ "-" * 80,
35
+ "-" * 80,
36
+ "-" * 80,
37
+ ],
38
+ ]
39
+
40
+ services = config.trainml.run(
41
+ config.trainml.client.cloudbender.services.list(
42
+ provider_uuid=provider, region_uuid=region
43
+ )
44
+ )
45
+
46
+ for service in services:
47
+ data.append(
48
+ [
49
+ service.id,
50
+ service.name,
51
+ service.hostname,
52
+ ]
53
+ )
54
+
55
+ for row in data:
56
+ click.echo(
57
+ "{: >25.24} {: >29.28} {: >40.39}" "".format(*row),
58
+ file=config.stdout,
59
+ )
60
+
61
+
62
+ @service.command()
63
+ @click.option(
64
+ "--provider",
65
+ "-p",
66
+ type=click.STRING,
67
+ required=True,
68
+ help="The provider ID of the region.",
69
+ )
70
+ @click.option(
71
+ "--region",
72
+ "-r",
73
+ type=click.STRING,
74
+ required=True,
75
+ help="The region ID to create the service in.",
76
+ )
77
+ @click.option(
78
+ "--type",
79
+ "-t",
80
+ type=click.Choice(
81
+ [
82
+ "https",
83
+ "tcp",
84
+ "udp",
85
+ ],
86
+ ),
87
+ required=True,
88
+ help="The type of regional service.",
89
+ )
90
+ @click.option(
91
+ "--public/--no-public",
92
+ default=True,
93
+ show_default=True,
94
+ help="Service should be accessible from the public internet.",
95
+ )
96
+ @click.argument("name", type=click.STRING, required=True)
97
+ @pass_config
98
+ def create(config, provider, region, type, public, name):
99
+ """
100
+ Creates a service.
101
+ """
102
+ return config.trainml.run(
103
+ config.trainml.client.cloudbender.services.create(
104
+ provider_uuid=provider,
105
+ region_uuid=region,
106
+ name=name,
107
+ type=type,
108
+ public=public,
109
+ )
110
+ )
111
+
112
+
113
+ @service.command()
114
+ @click.option(
115
+ "--provider",
116
+ "-p",
117
+ type=click.STRING,
118
+ required=True,
119
+ help="The provider ID of the region.",
120
+ )
121
+ @click.option(
122
+ "--region",
123
+ "-r",
124
+ type=click.STRING,
125
+ required=True,
126
+ help="The region ID to remove the service from.",
127
+ )
128
+ @click.argument("service", type=click.STRING)
129
+ @pass_config
130
+ def remove(config, provider, region, service):
131
+ """
132
+ Remove a service.
133
+
134
+ RESERVATION may be specified by name or ID, but ID is preferred.
135
+ """
136
+ services = config.trainml.run(
137
+ config.trainml.client.cloudbender.services.list(
138
+ provider_uuid=provider, region_uuid=region
139
+ )
140
+ )
141
+
142
+ found = search_by_id_name(service, services)
143
+ if None is found:
144
+ raise click.UsageError("Cannot find specified service.")
145
+
146
+ return config.trainml.run(found.remove())
trainml/cli/project.py CHANGED
@@ -115,40 +115,35 @@ def list_datastores(config):
115
115
 
116
116
  @project.command()
117
117
  @pass_config
118
- def list_reservations(config):
119
- """List project reservations."""
118
+ def list_services(config):
119
+ """List project services."""
120
120
  data = [
121
- ["ID", "NAME", "TYPE", "RESOURCE", "HOSTNAME", "REGION_UUID"],
121
+ ["ID", "NAME", "HOSTNAME", "REGION_UUID"],
122
122
  [
123
123
  "-" * 80,
124
124
  "-" * 80,
125
125
  "-" * 80,
126
126
  "-" * 80,
127
- "-" * 80,
128
- "-" * 80,
129
127
  ],
130
128
  ]
131
129
  project = config.trainml.run(
132
130
  config.trainml.client.projects.get(config.trainml.client.project)
133
131
  )
134
132
 
135
- reservations = config.trainml.run(project.list_reservations())
133
+ services = config.trainml.run(project.list_services())
136
134
 
137
- for reservation in reservations:
135
+ for service in services:
138
136
  data.append(
139
137
  [
140
- reservation.id,
141
- reservation.name,
142
- reservation.type,
143
- reservation.resource,
144
- reservation.hostname,
145
- reservation.region_uuid,
138
+ service.id,
139
+ service.name,
140
+ service.hostname,
141
+ service.region_uuid,
146
142
  ]
147
143
  )
148
144
 
149
145
  for row in data:
150
146
  click.echo(
151
- "{: >38.36} {: >30.28} {: >8.6} {: >15.13} {: >30.28} {: >38.36}"
152
- "".format(*row),
147
+ "{: >38.36} {: >30.28} {: >30.28} {: >38.36}" "".format(*row),
153
148
  file=config.stdout,
154
149
  )
@@ -3,7 +3,8 @@ from .regions import Regions
3
3
  from .nodes import Nodes
4
4
  from .devices import Devices
5
5
  from .datastores import Datastores
6
- from .reservations import Reservations
6
+ from .data_connectors import DataConnectors
7
+ from .services import Services
7
8
  from .device_configs import DeviceConfigs
8
9
 
9
10
 
@@ -15,5 +16,6 @@ class Cloudbender(object):
15
16
  self.nodes = Nodes(trainml)
16
17
  self.devices = Devices(trainml)
17
18
  self.datastores = Datastores(trainml)
18
- self.reservations = Reservations(trainml)
19
+ self.data_connectors = DataConnectors(trainml)
20
+ self.services = Services(trainml)
19
21
  self.device_configs = DeviceConfigs(trainml)