Skip to content

Commit 6beb48f

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

File tree

9 files changed

+184
-31
lines changed

9 files changed

+184
-31
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

+131-9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from hopsworks import client, constants, project, version
2727
from hopsworks.client.exceptions import ProjectException, RestAPIError
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,6 +44,8 @@
4244
_hw_connection = Connection.connection
4345

4446
_connected_project = None
47+
_secrets_api = None
48+
_project_api = None
4549

4650

4751
def hw_formatwarning(message, category, filename, lineno, line=None):
@@ -113,6 +117,7 @@ def login(
113117
if "REST_ENDPOINT" in os.environ:
114118
_hw_connection = _hw_connection()
115119
_connected_project = _hw_connection.get_project()
120+
_initialize_module_apis()
116121
print("\nLogged in to project, explore it here " + _connected_project.get_url())
117122
return _connected_project
118123

@@ -140,6 +145,8 @@ def login(
140145
elif host is None: # Always do a fallback to Serverless Hopsworks if not defined
141146
host = constants.HOSTS.APP_HOST
142147

148+
is_app = host == constants.HOSTS.APP_HOST
149+
143150
# If port same as default, get HOPSWORKS_HOST environment variable
144151
if port == 443 and "HOPSWORKS_PORT" in os.environ:
145152
port = os.environ["HOPSWORKS_PORT"]
@@ -166,23 +173,24 @@ def login(
166173
"Could not find api key file on path: {}".format(api_key_file)
167174
)
168175
# 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:
176+
elif os.path.exists(api_key_path) and is_app:
170177
try:
171178
_hw_connection = _hw_connection(
172179
host=host, port=port, api_key_file=api_key_path
173180
)
174-
_connected_project = _prompt_project(_hw_connection, project)
181+
_connected_project = _prompt_project(_hw_connection, project, is_app)
175182
print(
176183
"\nLogged in to project, explore it here "
177184
+ _connected_project.get_url()
178185
)
186+
_initialize_module_apis()
179187
return _connected_project
180188
except RestAPIError:
181189
logout()
182190
# API Key may be invalid, have the user supply it again
183191
os.remove(api_key_path)
184192

185-
if api_key is None and host == constants.HOSTS.APP_HOST:
193+
if api_key is None and is_app:
186194
print(
187195
"Copy your Api Key (first register/login): https://c.app.hopsworks.ai/account/api/generated"
188196
)
@@ -198,12 +206,19 @@ def login(
198206

199207
try:
200208
_hw_connection = _hw_connection(host=host, port=port, api_key_value=api_key)
201-
_connected_project = _prompt_project(_hw_connection, project)
209+
_connected_project = _prompt_project(_hw_connection, project, is_app)
202210
except RestAPIError as e:
203211
logout()
204212
raise e
205213

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

209224

@@ -245,11 +260,14 @@ def _get_cached_api_key_path():
245260
return api_key_path
246261

247262

248-
def _prompt_project(valid_connection, project):
263+
def _prompt_project(valid_connection, project, is_app):
249264
saas_projects = valid_connection.get_projects()
250265
if project is None:
251266
if len(saas_projects) == 0:
252-
raise ProjectException("Could not find any project")
267+
if is_app:
268+
raise ProjectException("Could not find any project")
269+
else:
270+
return None
253271
elif len(saas_projects) == 1:
254272
return saas_projects[0]
255273
else:
@@ -258,7 +276,9 @@ def _prompt_project(valid_connection, project):
258276
for index in range(len(saas_projects)):
259277
print("\t (" + str(index + 1) + ") " + saas_projects[index].name)
260278
while True:
261-
project_index = input("\nEnter project to access: ")
279+
project_index = input(
280+
"\nEnter number corresponding to the project to use: "
281+
)
262282
# Handle invalid input type
263283
try:
264284
project_index = int(project_index)
@@ -285,8 +305,110 @@ def _prompt_project(valid_connection, project):
285305

286306

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

python/hopsworks/core/secret_api.py

+34-8
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
# limitations under the License.
1515
#
1616

17-
from hopsworks import client, secret
18-
from hopsworks.core import project_api
17+
import getpass
1918
import json
2019

20+
from hopsworks import client, secret, util
21+
from hopsworks.client.exceptions import RestAPIError
22+
from hopsworks.core import project_api
23+
2124

2225
class SecretsApi:
2326
def __init__(
@@ -42,11 +45,11 @@ def get_secrets(self):
4245
_client._send_request("GET", path_params)
4346
)
4447

45-
def get_secret(self, name: str, owner: str = None):
48+
def get_secret(self, name: str, owner: str = None) -> secret.Secret:
4649
"""Get a secret.
4750
4851
# Arguments
49-
name: Name of the project.
52+
name: Name of the secret.
5053
owner: email of the owner for a secret shared with the current project.
5154
# Returns
5255
`Secret`: The Secret object
@@ -69,11 +72,34 @@ def get_secret(self, name: str, owner: str = None):
6972
"shared",
7073
]
7174

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
#
1616

1717
import json
18-
import humps
1918

19+
import humps
2020
from hopsworks import util
2121
from hopsworks.core import secret_api
2222

@@ -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

0 commit comments

Comments
 (0)