Skip to content

Commit 07cfbaa

Browse files
committedJul 2, 2024
[HWORKS-1048] Support multiple modularized project environments
1 parent bdaff4c commit 07cfbaa

File tree

5 files changed

+78
-43
lines changed

5 files changed

+78
-43
lines changed
 

‎python/hopsworks/core/environment_api.py

+34-17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
from hopsworks import client, environment
1818
from hopsworks.engine import environment_engine
19+
from typing import Optional, List
20+
import json
1921

2022

2123
class EnvironmentApi:
@@ -29,7 +31,7 @@ def __init__(
2931

3032
self._environment_engine = environment_engine.EnvironmentEngine(project_id)
3133

32-
def create_environment(self, await_creation=True):
34+
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:
3335
"""Create Python environment for the project
3436
3537
```python
@@ -40,10 +42,13 @@ def create_environment(self, await_creation=True):
4042
4143
env_api = project.get_environment_api()
4244
43-
env = env_api.create_environment()
45+
new_env = env_api.create_environment("my_custom_environment", base_environment_name="python-feature-pipeline")
46+
4447
4548
```
4649
# Arguments
50+
name: name of the environment
51+
base_environment_name: the name of the environment to copy from
4752
await_creation: bool. If True the method returns only when the creation is finished. Default True
4853
# Returns
4954
`Environment`: The Environment object
@@ -57,21 +62,26 @@ def create_environment(self, await_creation=True):
5762
self._project_id,
5863
"python",
5964
"environments",
60-
client.get_python_version(),
65+
name,
6166
]
6267
headers = {"content-type": "application/json"}
68+
data = {"name": name,
69+
"baseImage": {
70+
"name": base_environment_name,
71+
"description": description
72+
}}
6373
env = environment.Environment.from_response_json(
64-
_client._send_request("POST", path_params, headers=headers),
74+
_client._send_request("POST", path_params, headers=headers, data=json.dumps(data)),
6575
self._project_id,
6676
self._project_name,
6777
)
6878

6979
if await_creation:
70-
self._environment_engine.await_environment_command()
80+
self._environment_engine.await_environment_command(name)
7181

7282
return env
7383

74-
def _get_environments(self):
84+
def get_environments(self) -> List[environment.Environment]:
7585
"""
7686
Get all available python environments in the project
7787
"""
@@ -88,7 +98,7 @@ def _get_environments(self):
8898
self._project_name,
8999
)
90100

91-
def get_environment(self):
101+
def get_environment(self, name: str) -> environment.Environment:
92102
"""Get handle for the Python environment for the project
93103
94104
```python
@@ -99,30 +109,37 @@ def get_environment(self):
99109
100110
env_api = project.get_environment_api()
101111
102-
env = env_api.get_environment()
112+
env = env_api.get_environment("my_custom_environment")
103113
104114
```
105115
# Returns
106116
`Environment`: The Environment object
107117
# Raises
108118
`RestAPIError`: If unable to get the environment
109119
"""
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"""
120+
_client = client.get_instance()
121+
122+
path_params = ["project", self._project_id, "python", "environments", name]
123+
query_params = {"expand": ["libraries", "commands"]}
124+
headers = {"content-type": "application/json"}
125+
return environment.Environment.from_response_json(
126+
_client._send_request(
127+
"GET", path_params, query_params=query_params, headers=headers
128+
),
129+
self._project_id,
130+
self._project_name,
131+
)
132+
133+
def _delete(self, name):
134+
"""Delete the Python environment"""
118135
_client = client.get_instance()
119136

120137
path_params = [
121138
"project",
122139
self._project_id,
123140
"python",
124141
"environments",
125-
python_version,
142+
name,
126143
]
127144
headers = {"content-type": "application/json"}
128145
_client._send_request("DELETE", path_params, headers=headers),

‎python/hopsworks/core/library_api.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ 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):
31+
def install(self, library_name: str, name: str, library_spec: dict):
3232
"""Create Python environment for the project"""
3333
_client = client.get_instance()
3434

@@ -37,7 +37,7 @@ def install(self, library_name: str, python_version: str, library_spec: dict):
3737
self._project_id,
3838
"python",
3939
"environments",
40-
python_version,
40+
name,
4141
"libraries",
4242
library_name,
4343
]

‎python/hopsworks/engine/environment_engine.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,21 @@ 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

+33-15
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@
2020
from hopsworks import command, util
2121
from hopsworks.core import environment_api, library_api
2222
from hopsworks.engine import environment_engine
23+
from typing import Optional
2324

2425

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, await_installation: Optional[bool] = True):
8196
"""Install a python library packaged in a wheel file
8297
8398
```python
@@ -92,7 +107,8 @@ 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
```
@@ -103,7 +119,7 @@ def install_wheel(self, path, await_installation=True):
103119
"""
104120

105121
# Wait for any ongoing environment operations
106-
self._environment_engine.await_environment_command()
122+
self._environment_engine.await_environment_command(self.name)
107123

108124
library_name = os.path.basename(path)
109125

@@ -116,15 +132,15 @@ def install_wheel(self, path, await_installation=True):
116132
}
117133

118134
library_rest = self._library_api.install(
119-
library_name, self.python_version, library_spec
135+
library_name, self.name, library_spec
120136
)
121137

122138
if await_installation:
123-
return self._environment_engine.await_library_command(library_name)
139+
return self._environment_engine.await_library_command(self.name, library_name)
124140

125141
return library_rest
126142

127-
def install_requirements(self, path, await_installation=True):
143+
def install_requirements(self, path, await_installation: Optional[bool] = True):
128144
"""Install libraries specified in a requirements.txt file
129145
130146
```python
@@ -139,7 +155,9 @@ def install_requirements(self, path, await_installation=True):
139155
140156
# Install
141157
env_api = project.get_environment_api()
142-
env = env_api.get_environment()
158+
env = env_api.get_environment("my_custom_environment")
159+
160+
143161
env.install_requirements(requirements_path)
144162
145163
```
@@ -150,7 +168,7 @@ def install_requirements(self, path, await_installation=True):
150168
"""
151169

152170
# Wait for any ongoing environment operations
153-
self._environment_engine.await_environment_command()
171+
self._environment_engine.await_environment_command(self.name)
154172

155173
library_name = os.path.basename(path)
156174

@@ -163,11 +181,11 @@ def install_requirements(self, path, await_installation=True):
163181
}
164182

165183
library_rest = self._library_api.install(
166-
library_name, self.python_version, library_spec
184+
library_name, self.name, library_spec
167185
)
168186

169187
if await_installation:
170-
return self._environment_engine.await_library_command(library_name)
188+
return self._environment_engine.await_library_command(self.name, library_name)
171189

172190
return library_rest
173191

@@ -178,7 +196,7 @@ def delete(self):
178196
# Raises
179197
`RestAPIError`.
180198
"""
181-
self._environment_api._delete(self.python_version)
199+
self._environment_api._delete(self.name)
182200

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

‎python/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ classifiers = [
2323

2424
dependencies = [
2525
"hsfs[python] @ git+https://git@github.com/logicalclocks/feature-store-api@master#subdirectory=python",
26-
"hsml @ git+https://git@github.com/logicalclocks/machine-learning-api@main#subdirectory=python",
26+
"hsml @ git+https://git@github.com/robzor92/machine-learning-api@HWORKS-1048#subdirectory=python",
2727
"pyhumps==1.6.1",
2828
"requests",
2929
"furl",

0 commit comments

Comments
 (0)