Skip to content

Commit 8f4395a

Browse files
committed
[FSTORE-1389] get_secrets_api and create_project should be accessible as hopsworks module functions
1 parent 52ca273 commit 8f4395a

File tree

9 files changed

+164
-30
lines changed

9 files changed

+164
-30
lines changed

auto_doc.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
PAGES = {
2323
"api/login.md": {
2424
"login": ["hopsworks.login"],
25+
"get_current_project": ["hopsworks.get_current_project"],
2526
"fs_api": ["hopsworks.project.Project.get_feature_store"],
2627
"mr_api": ["hopsworks.project.Project.get_model_registry"],
2728
"ms_api": ["hopsworks.project.Project.get_model_serving"],
@@ -36,9 +37,7 @@
3637
),
3738
},
3839
"api/projects.md": {
39-
"project_create": ["hopsworks.connection.Connection.create_project"],
40-
"project_get": ["hopsworks.connection.Connection.get_project"],
41-
"project_get_all": ["hopsworks.connection.Connection.get_projects"],
40+
"project_create": ["hopsworks.create_project"],
4241
"project_properties": keras_autodoc.get_properties("hopsworks.project.Project"),
4342
"project_methods": keras_autodoc.get_methods(
4443
"hopsworks.project.Project", exclude=["from_response_json", "json"]
@@ -163,9 +162,10 @@
163162
),
164163
},
165164
"api/secrets.md": {
166-
"secret_api_handle": ["hopsworks.connection.Connection.get_secrets_api"],
165+
"secret_api_handle": ["hopsworks.get_secrets_api"],
167166
"secret_create": ["hopsworks.core.secret_api.SecretsApi.create_secret"],
168167
"secret_get": ["hopsworks.core.secret_api.SecretsApi.get_secret"],
168+
"secret_get_simplified": ["hopsworks.core.secret_api.SecretsApi.get"],
169169
"secret_get_all": ["hopsworks.core.secret_api.SecretsApi.get_secrets"],
170170
"secret_properties": keras_autodoc.get_properties("hopsworks.secret.Secret"),
171171
"secret_methods": keras_autodoc.get_methods(

docs/templates/api/login.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
{{login}}
44

5+
{{get_current_project}}
6+
57
## Feature Store API
68

79
{{fs_api}}

docs/templates/api/projects.md

-6
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44

55
{{project_create}}
66

7-
## Retrieval
8-
9-
{{project_get}}
10-
11-
{{project_get_all}}
12-
137
## Properties
148

159
{{project_properties}}

docs/templates/api/secrets.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
## Retrieval
1212

13+
{{secret_get_simplified}}
14+
1315
{{secret_get}}
1416

1517
{{secret_get_all}}

python/hopsworks/__init__.py

+117-12
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
import warnings
2424
from pathlib import Path
2525

26-
from hopsworks import client, constants, project, version
27-
from hopsworks.client.exceptions import ProjectException, RestAPIError
26+
from hopsworks.client.exceptions import RestAPIError, ProjectException
27+
from hopsworks import version, constants, client, project
2828
from hopsworks.connection import Connection
29+
from hopsworks.core import project_api, secret_api
30+
from hopsworks.decorators import NoHopsworksConnectionError
2931

3032

3133
# Needs to run before import of hsml and hsfs
@@ -42,7 +44,8 @@
4244
_hw_connection = Connection.connection
4345

4446
_connected_project = None
45-
47+
_secrets_api = None
48+
_project_api = None
4649

4750
def hw_formatwarning(message, category, filename, lineno, line=None):
4851
return "{}: {}\n".format(category.__name__, message)
@@ -113,6 +116,7 @@ def login(
113116
if "REST_ENDPOINT" in os.environ:
114117
_hw_connection = _hw_connection()
115118
_connected_project = _hw_connection.get_project()
119+
_initialize_module_apis()
116120
print("\nLogged in to project, explore it here " + _connected_project.get_url())
117121
return _connected_project
118122

@@ -140,6 +144,8 @@ def login(
140144
elif host is None: # Always do a fallback to Serverless Hopsworks if not defined
141145
host = constants.HOSTS.APP_HOST
142146

147+
is_app = (host == constants.HOSTS.APP_HOST)
148+
143149
# If port same as default, get HOPSWORKS_HOST environment variable
144150
if port == 443 and "HOPSWORKS_PORT" in os.environ:
145151
port = os.environ["HOPSWORKS_PORT"]
@@ -166,23 +172,24 @@ def login(
166172
"Could not find api key file on path: {}".format(api_key_file)
167173
)
168174
# If user connected to Serverless Hopsworks, and the cached .hw_api_key exists, then use it.
169-
elif os.path.exists(api_key_path) and host == constants.HOSTS.APP_HOST:
175+
elif os.path.exists(api_key_path) and is_app:
170176
try:
171177
_hw_connection = _hw_connection(
172178
host=host, port=port, api_key_file=api_key_path
173179
)
174-
_connected_project = _prompt_project(_hw_connection, project)
180+
_connected_project = _prompt_project(_hw_connection, project, is_app)
175181
print(
176182
"\nLogged in to project, explore it here "
177183
+ _connected_project.get_url()
178184
)
185+
_initialize_module_apis()
179186
return _connected_project
180187
except RestAPIError:
181188
logout()
182189
# API Key may be invalid, have the user supply it again
183190
os.remove(api_key_path)
184191

185-
if api_key is None and host == constants.HOSTS.APP_HOST:
192+
if api_key is None and is_app:
186193
print(
187194
"Copy your Api Key (first register/login): https://c.app.hopsworks.ai/account/api/generated"
188195
)
@@ -198,12 +205,17 @@ def login(
198205

199206
try:
200207
_hw_connection = _hw_connection(host=host, port=port, api_key_value=api_key)
201-
_connected_project = _prompt_project(_hw_connection, project)
208+
_connected_project = _prompt_project(_hw_connection, project, is_app)
202209
except RestAPIError as e:
203210
logout()
204211
raise e
205212

206-
print("\nLogged in to project, explore it here " + _connected_project.get_url())
213+
if _connected_project is None:
214+
print("Could not find any project, use hopsworks.create_project('my_project') to create one")
215+
else:
216+
print("\nLogged in to project, explore it here " + _connected_project.get_url())
217+
218+
_initialize_module_apis()
207219
return _connected_project
208220

209221

@@ -245,11 +257,14 @@ def _get_cached_api_key_path():
245257
return api_key_path
246258

247259

248-
def _prompt_project(valid_connection, project):
260+
def _prompt_project(valid_connection, project, is_app):
249261
saas_projects = valid_connection.get_projects()
250262
if project is None:
251263
if len(saas_projects) == 0:
252-
raise ProjectException("Could not find any project")
264+
if is_app:
265+
raise ProjectException("Could not find any project")
266+
else:
267+
return None
253268
elif len(saas_projects) == 1:
254269
return saas_projects[0]
255270
else:
@@ -258,7 +273,7 @@ def _prompt_project(valid_connection, project):
258273
for index in range(len(saas_projects)):
259274
print("\t (" + str(index + 1) + ") " + saas_projects[index].name)
260275
while True:
261-
project_index = input("\nEnter project to access: ")
276+
project_index = input("\nEnter number corresponding to the project to use: ")
262277
# Handle invalid input type
263278
try:
264279
project_index = int(project_index)
@@ -285,8 +300,98 @@ def _prompt_project(valid_connection, project):
285300

286301

287302
def logout():
303+
"""Cleans up and closes the connection for the hopsworks, hsfs and hsml libraries.
304+
"""
288305
global _hw_connection
289-
if isinstance(_hw_connection, Connection):
306+
global _project_api
307+
global _secrets_api
308+
309+
if _is_connection_active():
290310
_hw_connection.close()
311+
291312
client.stop()
313+
_project_api = None
314+
_secrets_api = None
292315
_hw_connection = Connection.connection
316+
317+
def _is_connection_active():
318+
global _hw_connection
319+
return isinstance(_hw_connection, Connection)
320+
321+
def get_current_project() -> project.Project:
322+
"""Get a reference to the current logged in project.
323+
324+
Example for creating a new project
325+
326+
```python
327+
328+
import hopsworks
329+
330+
hopsworks.login()
331+
332+
project = hopsworks.get_current_project()
333+
334+
```
335+
336+
# Returns
337+
`Project`. A project handle object to perform operations on.
338+
"""
339+
global _connected_project
340+
if _connected_project is None:
341+
raise ProjectException("No project is set for this session")
342+
return _connected_project
343+
344+
def _initialize_module_apis():
345+
global _project_api
346+
global _secrets_api
347+
_project_api = project_api.ProjectApi()
348+
_secrets_api = secret_api.SecretsApi()
349+
350+
def create_project(
351+
name: str, description: str = None, feature_store_topic: str = None
352+
):
353+
"""Create a new project.
354+
355+
Example for creating a new project
356+
357+
```python
358+
359+
import hopsworks
360+
361+
hopsworks.login()
362+
363+
hopsworks.create_project("my_hopsworks_project", description="An example Hopsworks project")
364+
365+
```
366+
# Arguments
367+
name: The name of the project.
368+
description: optional description of the project
369+
feature_store_topic: optional feature store topic name
370+
371+
# Returns
372+
`Project`. A project handle object to perform operations on.
373+
"""
374+
global _hw_connection
375+
global _connected_project
376+
377+
if not _is_connection_active():
378+
raise NoHopsworksConnectionError()
379+
380+
new_project = _hw_connection._project_api._create_project(name, description, feature_store_topic)
381+
if _connected_project is None:
382+
_connected_project = new_project
383+
print("Setting {} as the current project, a reference can be retrieved by calling hopsworks.get_current_project()".format(_connected_project.name))
384+
return _connected_project
385+
else:
386+
print("You are already using the project {}, to access the new project use hopsworks.login(..., project='{}')".format(_connected_project.name, new_project.name))
387+
388+
def get_secrets_api():
389+
"""Get the secrets api.
390+
391+
# Returns
392+
`SecretsApi`: The Secrets Api handle
393+
"""
394+
global _secrets_api
395+
if not _is_connection_active():
396+
raise NoHopsworksConnectionError()
397+
return _secrets_api

python/hopsworks/core/secret_api.py

+32-7
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
# limitations under the License.
1515
#
1616

17-
from hopsworks import client, secret
17+
from hopsworks import client, secret, util
1818
from hopsworks.core import project_api
1919
import json
20+
import getpass
21+
from hopsworks.client.exceptions import RestAPIError
2022

2123

2224
class SecretsApi:
@@ -42,11 +44,11 @@ def get_secrets(self):
4244
_client._send_request("GET", path_params)
4345
)
4446

45-
def get_secret(self, name: str, owner: str = None):
47+
def get_secret(self, name: str, owner: str = None) -> secret.Secret:
4648
"""Get a secret.
4749
4850
# Arguments
49-
name: Name of the project.
51+
name: Name of the secret.
5052
owner: email of the owner for a secret shared with the current project.
5153
# Returns
5254
`Secret`: The Secret object
@@ -69,11 +71,34 @@ def get_secret(self, name: str, owner: str = None):
6971
"shared",
7072
]
7173

72-
return secret.Secret.from_response_json(
73-
_client._send_request("GET", path_params, query_params=query_params)
74-
)[0]
74+
return secret.Secret.from_response_json(_client._send_request("GET", path_params, query_params=query_params))[0]
75+
76+
def get(self, name: str, owner: str = None) -> str:
77+
"""Get the secret's value.
78+
If the secret does not exist, it prompts the user to create the secret if the application is running interactively
7579
76-
def create_secret(self, name: str, value: str, project: str = None):
80+
# Arguments
81+
name: Name of the secret.
82+
owner: email of the owner for a secret shared with the current project.
83+
# Returns
84+
`str`: The secret value
85+
# Raises
86+
`RestAPIError`: If unable to get the secret
87+
"""
88+
try:
89+
return self.get_secret(name=name, owner=owner).value
90+
except RestAPIError as e:
91+
if (
92+
e.response.json().get("errorCode", "") == 160048
93+
and e.response.status_code == 404
94+
and util.is_interactive()
95+
):
96+
secret_input = getpass.getpass(prompt="\nCould not find secret, enter value here to create it: ")
97+
return self.create_secret(name, secret_input).value
98+
else:
99+
raise e
100+
101+
def create_secret(self, name: str, value: str, project: str = None) -> secret.Secret:
77102
"""Create a new secret.
78103
79104
```python

python/hopsworks/project.py

+2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def get_feature_store(self, name: str = None) -> feature_store.FeatureStore:
109109
Defaulting to the project name of default feature store. To get a
110110
shared feature store, the project name of the feature store is required.
111111
112+
# Arguments
113+
name: Project name of the feature store.
112114
# Returns
113115
`hsfs.feature_store.FeatureStore`: The Feature Store API
114116
# Raises

python/hopsworks/secret.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def __init__(
4848
@classmethod
4949
def from_response_json(cls, json_dict):
5050
json_decamelized = humps.decamelize(json_dict)
51-
if len(json_decamelized["items"]) == 0:
51+
if "items" not in json_decamelized or len(json_decamelized["items"]) == 0:
5252
return []
5353
return [cls(**secret) for secret in json_decamelized["items"]]
5454

python/hopsworks/util.py

+4
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,7 @@ def get_hostname_replaced_url(sub_path: str):
7979
href = urljoin(client.get_instance()._base_url, sub_path)
8080
url_parsed = client.get_instance().replace_public_host(urlparse(href))
8181
return url_parsed.geturl()
82+
83+
def is_interactive():
84+
import __main__ as main
85+
return not hasattr(main, '__file__')

0 commit comments

Comments
 (0)