Skip to content

Commit b37d225

Browse files
robzor92aversey
authored andcommitted
[HWORKS-1048] Support multiple modularized project environments (logicalclocks#213)
1 parent 9a090a3 commit b37d225

File tree

4 files changed

+104
-48
lines changed

4 files changed

+104
-48
lines changed

python/hopsworks/core/environment_api.py

+40-17
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
# limitations under the License.
1515
#
1616

17+
import json
18+
from typing import List, Optional
19+
1720
from hopsworks import client, environment
1821
from hopsworks.engine import environment_engine
1922

@@ -29,7 +32,7 @@ def __init__(
2932

3033
self._environment_engine = environment_engine.EnvironmentEngine(project_id)
3134

32-
def create_environment(self, await_creation=True):
35+
def create_environment(self, name: str, description: Optional[str] = None, base_environment_name: Optional[str] = "python-feature-pipeline", await_creation: Optional[bool] = True) -> environment.Environment:
3336
"""Create Python environment for the project
3437
3538
```python
@@ -40,10 +43,13 @@ def create_environment(self, await_creation=True):
4043
4144
env_api = project.get_environment_api()
4245
43-
env = env_api.create_environment()
46+
new_env = env_api.create_environment("my_custom_environment", base_environment_name="python-feature-pipeline")
47+
4448
4549
```
4650
# Arguments
51+
name: name of the environment
52+
base_environment_name: the name of the environment to clone from
4753
await_creation: bool. If True the method returns only when the creation is finished. Default True
4854
# Returns
4955
`Environment`: The Environment object
@@ -57,21 +63,26 @@ def create_environment(self, await_creation=True):
5763
self._project_id,
5864
"python",
5965
"environments",
60-
client.get_python_version(),
66+
name,
6167
]
6268
headers = {"content-type": "application/json"}
69+
data = {"name": name,
70+
"baseImage": {
71+
"name": base_environment_name,
72+
"description": description
73+
}}
6374
env = environment.Environment.from_response_json(
64-
_client._send_request("POST", path_params, headers=headers),
75+
_client._send_request("POST", path_params, headers=headers, data=json.dumps(data)),
6576
self._project_id,
6677
self._project_name,
6778
)
6879

6980
if await_creation:
70-
self._environment_engine.await_environment_command()
81+
self._environment_engine.await_environment_command(name)
7182

7283
return env
7384

74-
def _get_environments(self):
85+
def get_environments(self) -> List[environment.Environment]:
7586
"""
7687
Get all available python environments in the project
7788
"""
@@ -88,7 +99,7 @@ def _get_environments(self):
8899
self._project_name,
89100
)
90101

91-
def get_environment(self):
102+
def get_environment(self, name: str) -> environment.Environment:
92103
"""Get handle for the Python environment for the project
93104
94105
```python
@@ -99,30 +110,42 @@ def get_environment(self):
99110
100111
env_api = project.get_environment_api()
101112
102-
env = env_api.get_environment()
113+
env = env_api.get_environment("my_custom_environment")
103114
104115
```
116+
# Arguments
117+
name: name of the environment
105118
# Returns
106119
`Environment`: The Environment object
107120
# Raises
108121
`RestAPIError`: If unable to get the environment
109122
"""
110-
project_envs = self._get_environments()
111-
if len(project_envs) == 0:
112-
return None
113-
elif len(project_envs) > 0:
114-
return project_envs[0]
115-
116-
def _delete(self, python_version):
117-
"""Delete the project Python environment"""
123+
_client = client.get_instance()
124+
125+
path_params = ["project", self._project_id, "python", "environments", name]
126+
query_params = {"expand": ["libraries", "commands"]}
127+
headers = {"content-type": "application/json"}
128+
return environment.Environment.from_response_json(
129+
_client._send_request(
130+
"GET", path_params, query_params=query_params, headers=headers
131+
),
132+
self._project_id,
133+
self._project_name,
134+
)
135+
136+
def _delete(self, name):
137+
"""Delete the Python environment.
138+
:param name: name of environment to delete
139+
:type environment: Environment
140+
"""
118141
_client = client.get_instance()
119142

120143
path_params = [
121144
"project",
122145
self._project_id,
123146
"python",
124147
"environments",
125-
python_version,
148+
name,
126149
]
127150
headers = {"content-type": "application/json"}
128151
_client._send_request("DELETE", path_params, headers=headers),

python/hopsworks/core/library_api.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,27 @@ def __init__(
2828
self._project_id = project_id
2929
self._project_name = project_name
3030

31-
def install(self, library_name: str, python_version: str, library_spec: dict):
32-
"""Create Python environment for the project"""
31+
def _install(self, library_name: str, name: str, library_spec: dict):
32+
"""Install a library in the environment
33+
34+
# Arguments
35+
library_name: Name of the library.
36+
name: Name of the environment.
37+
library_spec: installation payload
38+
# Returns
39+
`Library`: The library object
40+
# Raises
41+
`RestAPIError`: If unable to install library
42+
"""
43+
3344
_client = client.get_instance()
3445

3546
path_params = [
3647
"project",
3748
self._project_id,
3849
"python",
3950
"environments",
40-
python_version,
51+
name,
4152
"libraries",
4253
library_name,
4354
]

python/hopsworks/engine/environment_engine.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,29 @@
1616

1717
import time
1818

19-
from hopsworks import client, library, environment, command
20-
from hopsworks.client.exceptions import RestAPIError, EnvironmentException
19+
from hopsworks import client, command, environment, library
20+
from hopsworks.client.exceptions import EnvironmentException, RestAPIError
2121

2222

2323
class EnvironmentEngine:
2424
def __init__(self, project_id):
2525
self._project_id = project_id
2626

27-
def await_library_command(self, library_name=None):
27+
def await_library_command(self, environment_name, library_name):
2828
commands = [command.Command(status="ONGOING")]
2929
while len(commands) > 0 and not self._is_final_status(commands[0]):
3030
time.sleep(5)
31-
library = self._poll_commands_library(library_name)
31+
library = self._poll_commands_library(environment_name, library_name)
3232
if library is None:
3333
commands = []
3434
else:
3535
commands = library._commands
3636

37-
def await_environment_command(self):
37+
def await_environment_command(self, environment_name):
3838
commands = [command.Command(status="ONGOING")]
3939
while len(commands) > 0 and not self._is_final_status(commands[0]):
4040
time.sleep(5)
41-
environment = self._poll_commands_environment()
41+
environment = self._poll_commands_environment(environment_name)
4242
if environment is None:
4343
commands = []
4444
else:
@@ -54,15 +54,15 @@ def _is_final_status(self, command):
5454
else:
5555
return False
5656

57-
def _poll_commands_library(self, library_name):
57+
def _poll_commands_library(self, environment_name, library_name):
5858
_client = client.get_instance()
5959

6060
path_params = [
6161
"project",
6262
self._project_id,
6363
"python",
6464
"environments",
65-
client.get_python_version(),
65+
environment_name,
6666
"libraries",
6767
library_name,
6868
]
@@ -85,15 +85,15 @@ def _poll_commands_library(self, library_name):
8585
):
8686
return None
8787

88-
def _poll_commands_environment(self):
88+
def _poll_commands_environment(self, environment_name):
8989
_client = client.get_instance()
9090

9191
path_params = [
9292
"project",
9393
self._project_id,
9494
"python",
9595
"environments",
96-
client.get_python_version(),
96+
environment_name,
9797
]
9898

9999
query_params = {"expand": "commands"}

python/hopsworks/environment.py

+40-18
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
# limitations under the License.
1515
#
1616

17-
import humps
1817
import os
18+
from typing import Optional
1919

20+
import humps
2021
from hopsworks import command, util
2122
from hopsworks.core import environment_api, library_api
2223
from hopsworks.engine import environment_engine
@@ -25,9 +26,11 @@
2526
class Environment:
2627
def __init__(
2728
self,
28-
python_version,
29-
python_conflicts,
30-
pip_search_enabled,
29+
name=None,
30+
description=None,
31+
python_version=None,
32+
python_conflicts=None,
33+
pip_search_enabled=None,
3134
conflicts=None,
3235
conda_channel=None,
3336
libraries=None,
@@ -38,6 +41,8 @@ def __init__(
3841
project_name=None,
3942
**kwargs,
4043
):
44+
self._name = name
45+
self._description = description
4146
self._python_version = python_version
4247
self._python_conflicts = python_conflicts
4348
self._pip_search_enabled = pip_search_enabled
@@ -77,7 +82,17 @@ def python_version(self):
7782
"""Python version of the environment"""
7883
return self._python_version
7984

80-
def install_wheel(self, path, await_installation=True):
85+
@property
86+
def name(self):
87+
"""Name of the environment"""
88+
return self._name
89+
90+
@property
91+
def description(self):
92+
"""Description of the environment"""
93+
return self._description
94+
95+
def install_wheel(self, path: str, await_installation: Optional[bool] = True):
8196
"""Install a python library packaged in a wheel file
8297
8398
```python
@@ -92,18 +107,21 @@ def install_wheel(self, path, await_installation=True):
92107
93108
# Install
94109
env_api = project.get_environment_api()
95-
env = env_api.get_environment()
110+
env = env_api.get_environment("my_custom_environment")
111+
96112
env.install_wheel(whl_path)
97113
98114
```
99115
100116
# Arguments
101117
path: str. The path on Hopsworks where the wheel file is located
102118
await_installation: bool. If True the method returns only when the installation finishes. Default True
119+
# Returns
120+
`Library`: The library object
103121
"""
104122

105123
# Wait for any ongoing environment operations
106-
self._environment_engine.await_environment_command()
124+
self._environment_engine.await_environment_command(self.name)
107125

108126
library_name = os.path.basename(path)
109127

@@ -115,16 +133,16 @@ def install_wheel(self, path, await_installation=True):
115133
"packageSource": "WHEEL",
116134
}
117135

118-
library_rest = self._library_api.install(
119-
library_name, self.python_version, library_spec
136+
library_rest = self._library_api._install(
137+
library_name, self.name, library_spec
120138
)
121139

122140
if await_installation:
123-
return self._environment_engine.await_library_command(library_name)
141+
return self._environment_engine.await_library_command(self.name, library_name)
124142

125143
return library_rest
126144

127-
def install_requirements(self, path, await_installation=True):
145+
def install_requirements(self, path: str, await_installation: Optional[bool] = True):
128146
"""Install libraries specified in a requirements.txt file
129147
130148
```python
@@ -139,18 +157,22 @@ def install_requirements(self, path, await_installation=True):
139157
140158
# Install
141159
env_api = project.get_environment_api()
142-
env = env_api.get_environment()
160+
env = env_api.get_environment("my_custom_environment")
161+
162+
143163
env.install_requirements(requirements_path)
144164
145165
```
146166
147167
# Arguments
148168
path: str. The path on Hopsworks where the requirements.txt file is located
149169
await_installation: bool. If True the method returns only when the installation is finished. Default True
170+
# Returns
171+
`Library`: The library object
150172
"""
151173

152174
# Wait for any ongoing environment operations
153-
self._environment_engine.await_environment_command()
175+
self._environment_engine.await_environment_command(self.name)
154176

155177
library_name = os.path.basename(path)
156178

@@ -162,12 +184,12 @@ def install_requirements(self, path, await_installation=True):
162184
"packageSource": "REQUIREMENTS_TXT",
163185
}
164186

165-
library_rest = self._library_api.install(
166-
library_name, self.python_version, library_spec
187+
library_rest = self._library_api._install(
188+
library_name, self.name, library_spec
167189
)
168190

169191
if await_installation:
170-
return self._environment_engine.await_library_command(library_name)
192+
return self._environment_engine.await_library_command(self.name, library_name)
171193

172194
return library_rest
173195

@@ -178,7 +200,7 @@ def delete(self):
178200
# Raises
179201
`RestAPIError`.
180202
"""
181-
self._environment_api._delete(self.python_version)
203+
self._environment_api._delete(self.name)
182204

183205
def __repr__(self):
184-
return f"Environment({self._python_version!r})"
206+
return f"Environment({self.name!r})"

0 commit comments

Comments
 (0)