JeffJing commited on
Commit
b115d50
·
1 Parent(s): 26a39bd

Upload 195 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. steamship/.DS_Store +0 -0
  2. steamship/__init__.py +57 -0
  3. steamship/__pycache__/__init__.cpython-39.pyc +0 -0
  4. steamship/base/__init__.py +15 -0
  5. steamship/base/__pycache__/__init__.cpython-39.pyc +0 -0
  6. steamship/base/__pycache__/client.cpython-39.pyc +0 -0
  7. steamship/base/__pycache__/configuration.cpython-39.pyc +0 -0
  8. steamship/base/__pycache__/environments.cpython-39.pyc +0 -0
  9. steamship/base/__pycache__/error.cpython-39.pyc +0 -0
  10. steamship/base/__pycache__/mime_types.cpython-39.pyc +0 -0
  11. steamship/base/__pycache__/model.cpython-39.pyc +0 -0
  12. steamship/base/__pycache__/package_spec.cpython-39.pyc +0 -0
  13. steamship/base/__pycache__/request.cpython-39.pyc +0 -0
  14. steamship/base/__pycache__/response.cpython-39.pyc +0 -0
  15. steamship/base/__pycache__/tasks.cpython-39.pyc +0 -0
  16. steamship/base/__pycache__/utils.cpython-39.pyc +0 -0
  17. steamship/base/client.py +635 -0
  18. steamship/base/configuration.py +139 -0
  19. steamship/base/environments.py +99 -0
  20. steamship/base/error.py +72 -0
  21. steamship/base/mime_types.py +42 -0
  22. steamship/base/model.py +29 -0
  23. steamship/base/package_spec.py +150 -0
  24. steamship/base/request.py +33 -0
  25. steamship/base/response.py +5 -0
  26. steamship/base/tasks.py +282 -0
  27. steamship/base/utils.py +0 -0
  28. steamship/cli/__init__.py +0 -0
  29. steamship/cli/__pycache__/__init__.cpython-39.pyc +0 -0
  30. steamship/cli/__pycache__/cli.cpython-39.pyc +0 -0
  31. steamship/cli/__pycache__/deploy.cpython-39.pyc +0 -0
  32. steamship/cli/__pycache__/login.cpython-39.pyc +0 -0
  33. steamship/cli/__pycache__/manifest_init_wizard.cpython-39.pyc +0 -0
  34. steamship/cli/__pycache__/requirements_init_wizard.cpython-39.pyc +0 -0
  35. steamship/cli/__pycache__/ship_spinner.cpython-39.pyc +0 -0
  36. steamship/cli/cli.py +195 -0
  37. steamship/cli/deploy.py +285 -0
  38. steamship/cli/login.py +62 -0
  39. steamship/cli/manifest_init_wizard.py +96 -0
  40. steamship/cli/requirements_init_wizard.py +20 -0
  41. steamship/cli/ship_spinner.py +48 -0
  42. steamship/client/__init__.py +3 -0
  43. steamship/client/__pycache__/__init__.cpython-39.pyc +0 -0
  44. steamship/client/__pycache__/skill_to_provider.cpython-39.pyc +0 -0
  45. steamship/client/__pycache__/skills.cpython-39.pyc +0 -0
  46. steamship/client/__pycache__/steamship.cpython-39.pyc +0 -0
  47. steamship/client/__pycache__/vendors.cpython-39.pyc +0 -0
  48. steamship/client/skill_to_provider.py +51 -0
  49. steamship/client/skills.py +11 -0
  50. steamship/client/steamship.py +327 -0
steamship/.DS_Store ADDED
Binary file (8.2 kB). View file
 
steamship/__init__.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from importlib.metadata import PackageNotFoundError, version # pragma: no cover
2
+
3
+ try:
4
+ # Change here if project is renamed and does not equal the package name
5
+ dist_name = __name__
6
+ __version__ = version(dist_name)
7
+ except PackageNotFoundError: # pragma: no cover
8
+ __version__ = "unknown"
9
+ finally:
10
+ del version, PackageNotFoundError
11
+
12
+ from .base import (
13
+ Configuration,
14
+ MimeTypes,
15
+ RuntimeEnvironments,
16
+ SteamshipError,
17
+ Task,
18
+ TaskState,
19
+ check_environment,
20
+ )
21
+ from .data import (
22
+ Block,
23
+ DocTag,
24
+ EmbeddingIndex,
25
+ File,
26
+ Package,
27
+ PackageInstance,
28
+ PackageVersion,
29
+ PluginInstance,
30
+ PluginVersion,
31
+ Tag,
32
+ Workspace,
33
+ )
34
+
35
+ from .client import Steamship # isort:skip
36
+
37
+ __all__ = [
38
+ "Steamship",
39
+ "Configuration",
40
+ "SteamshipError",
41
+ "MimeTypes",
42
+ "Package",
43
+ "PackageInstance",
44
+ "PackageVersion",
45
+ "File",
46
+ "Task",
47
+ "TaskState",
48
+ "Block",
49
+ "Tag",
50
+ "Workspace",
51
+ "PluginInstance",
52
+ "PluginVersion",
53
+ "DocTag",
54
+ "EmbeddingIndex",
55
+ "check_environment",
56
+ "RuntimeEnvironments",
57
+ ]
steamship/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (950 Bytes). View file
 
steamship/base/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .configuration import Configuration
2
+ from .environments import RuntimeEnvironments, check_environment
3
+ from .error import SteamshipError
4
+ from .mime_types import MimeTypes
5
+ from .tasks import Task, TaskState
6
+
7
+ __all__ = [
8
+ "Configuration",
9
+ "SteamshipError",
10
+ "Task",
11
+ "TaskState",
12
+ "MimeTypes",
13
+ "RuntimeEnvironments",
14
+ "check_environment",
15
+ ]
steamship/base/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (512 Bytes). View file
 
steamship/base/__pycache__/client.cpython-39.pyc ADDED
Binary file (14.1 kB). View file
 
steamship/base/__pycache__/configuration.cpython-39.pyc ADDED
Binary file (4.75 kB). View file
 
steamship/base/__pycache__/environments.cpython-39.pyc ADDED
Binary file (2.59 kB). View file
 
steamship/base/__pycache__/error.cpython-39.pyc ADDED
Binary file (2.15 kB). View file
 
steamship/base/__pycache__/mime_types.cpython-39.pyc ADDED
Binary file (1.46 kB). View file
 
steamship/base/__pycache__/model.cpython-39.pyc ADDED
Binary file (1.64 kB). View file
 
steamship/base/__pycache__/package_spec.cpython-39.pyc ADDED
Binary file (5.04 kB). View file
 
steamship/base/__pycache__/request.cpython-39.pyc ADDED
Binary file (1.4 kB). View file
 
steamship/base/__pycache__/response.cpython-39.pyc ADDED
Binary file (391 Bytes). View file
 
steamship/base/__pycache__/tasks.cpython-39.pyc ADDED
Binary file (8.61 kB). View file
 
steamship/base/__pycache__/utils.cpython-39.pyc ADDED
Binary file (176 Bytes). View file
 
steamship/base/client.py ADDED
@@ -0,0 +1,635 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing
5
+ from abc import ABC
6
+ from inspect import isclass
7
+ from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
8
+
9
+ import inflection
10
+ from pydantic import BaseModel, PrivateAttr
11
+ from requests import Session
12
+
13
+ from steamship.base.configuration import Configuration
14
+ from steamship.base.error import SteamshipError
15
+ from steamship.base.mime_types import MimeTypes
16
+ from steamship.base.model import CamelModel, to_camel
17
+ from steamship.base.request import Request
18
+ from steamship.base.tasks import Task, TaskState
19
+ from steamship.utils.url import Verb, is_local
20
+
21
+ _logger = logging.getLogger(__name__)
22
+
23
+ T = TypeVar("T") # TODO (enias): Do we need this?
24
+
25
+
26
+ def _multipart_name(path: str, val: Any) -> List[Tuple[Optional[str], str, Optional[str]]]:
27
+ """Decode any object into a series of HTTP Multi-part segments that Vapor will consume.
28
+
29
+ https://github.com/vapor/multipart-kit
30
+ When sending a JSON object in a MultiPart request, Vapor wishes to see multi part segments as follows:
31
+
32
+ single_key
33
+ array_key[idx]
34
+ obj_key[prop]
35
+
36
+ So a File with a list of one tag with kind=Foo would be transmitted as setting the part:
37
+ [tags][0][kind]
38
+ """
39
+ ret = []
40
+ if isinstance(val, dict):
41
+ for key, subval in val.items():
42
+ ret.extend(_multipart_name(f"{path}[{key}]", subval))
43
+ elif isinstance(val, list):
44
+ for idx, subval in enumerate(val):
45
+ ret.extend(_multipart_name(f"{path}[{idx}]", subval))
46
+ elif val is not None:
47
+ ret.append((path, val, None))
48
+ return ret
49
+
50
+
51
+ class Client(CamelModel, ABC):
52
+ """Client model.py class.
53
+
54
+ Separated primarily as a hack to prevent circular imports.
55
+ """
56
+
57
+ config: Configuration
58
+ _session: Session = PrivateAttr()
59
+
60
+ def __init__(
61
+ self,
62
+ api_key: str = None,
63
+ api_base: str = None,
64
+ app_base: str = None,
65
+ web_base: str = None,
66
+ workspace: str = None,
67
+ fail_if_workspace_exists: bool = False,
68
+ profile: str = None,
69
+ config_file: str = None,
70
+ config: Configuration = None,
71
+ trust_workspace_config: bool = False, # For use by lambda_handler; don't fetch the workspace
72
+ **kwargs,
73
+ ):
74
+ """Create a new client.
75
+
76
+ If `workspace` is provided, it will anchor the client in a workspace by that name, creating it if necessary.
77
+ Otherwise the `default` workspace will be used.
78
+ """
79
+ if config is not None and not isinstance(config, Configuration):
80
+ config = Configuration.parse_obj(config)
81
+
82
+ self._session = Session()
83
+ config = config or Configuration(
84
+ api_key=api_key,
85
+ api_base=api_base,
86
+ app_base=app_base,
87
+ web_base=web_base,
88
+ workspace_handle=workspace,
89
+ profile=profile,
90
+ config_file=config_file,
91
+ )
92
+
93
+ super().__init__(config=config)
94
+ # The lambda_handler will pass in the workspace via the workspace_id, so we need to plumb this through to make sure
95
+ # that the workspace switch performed doesn't mistake `workspace=None` as a request for the default workspace
96
+ self.switch_workspace(
97
+ workspace_handle=workspace or config.workspace_handle,
98
+ workspace_id=config.workspace_id,
99
+ fail_if_workspace_exists=fail_if_workspace_exists,
100
+ trust_workspace_config=trust_workspace_config,
101
+ )
102
+
103
+ def switch_workspace( # noqa: C901
104
+ self,
105
+ workspace_handle: str = None,
106
+ workspace_id: str = None,
107
+ fail_if_workspace_exists: bool = False,
108
+ trust_workspace_config: bool = False,
109
+ # For use by lambda_handler; don't fetch the workspacetrust_workspace_config: bool = False, # For use by lambda_handler; don't fetch the workspace
110
+ ):
111
+ """Switches this client to the requested workspace, possibly creating it. If all arguments are None, the client
112
+ actively switches into the default workspace.
113
+
114
+ - API calls are performed manually to not result in circular imports.
115
+ - Note that the default workspace is technically not necessary for API usage; it will be assumed by the Engine
116
+ in the absense of a Workspace ID or Handle being manually specified in request headers.
117
+ """
118
+ workspace = None
119
+
120
+ if workspace_handle is None and workspace_id is None:
121
+ # Switch to the default workspace since no named or ID'ed workspace was provided
122
+ workspace_handle = "default"
123
+
124
+ if fail_if_workspace_exists:
125
+ logging.info(
126
+ f"[Client] Creating workspace with handle/id: {workspace_handle}/{workspace_id}."
127
+ )
128
+ else:
129
+ logging.info(
130
+ f"[Client] Creating/Fetching workspace with handle/id: {workspace_handle}/{workspace_id}."
131
+ )
132
+
133
+ # Zero out the workspace_handle on the config block in case we're being invoked from
134
+ # `init` (otherwise we'll attempt to create the space IN that non-existant workspace)
135
+ old_workspace_handle = self.config.workspace_handle
136
+ self.config.workspace_handle = None
137
+
138
+ if trust_workspace_config:
139
+ if workspace_handle is None or workspace_id is None:
140
+ raise SteamshipError(
141
+ message="Attempted a trusted workspace switch without providing both workspace handle and workspace id."
142
+ )
143
+ return_id = workspace_id
144
+ return_handle = workspace_handle
145
+ else:
146
+ try:
147
+ if workspace_handle is not None and workspace_id is not None:
148
+ get_params = {
149
+ "handle": workspace_handle,
150
+ "id": workspace_id,
151
+ "fetchIfExists": False,
152
+ }
153
+ workspace = self.post("workspace/get", get_params)
154
+ elif workspace_handle is not None:
155
+ get_params = {
156
+ "handle": workspace_handle,
157
+ "fetchIfExists": not fail_if_workspace_exists,
158
+ }
159
+ workspace = self.post("workspace/create", get_params)
160
+ elif workspace_id is not None:
161
+ get_params = {"id": workspace_id}
162
+ workspace = self.post("workspace/get", get_params)
163
+
164
+ except SteamshipError as e:
165
+ self.config.workspace_handle = old_workspace_handle
166
+ raise e
167
+
168
+ if workspace is None:
169
+ raise SteamshipError(
170
+ message="Was unable to switch to new workspace: server returned empty Workspace."
171
+ )
172
+
173
+ return_id = workspace.get("workspace", {}).get("id")
174
+ return_handle = workspace.get("workspace", {}).get("handle")
175
+
176
+ if return_id is None or return_handle is None:
177
+ raise SteamshipError(
178
+ message="Was unable to switch to new workspace: server returned empty ID and Handle."
179
+ )
180
+
181
+ # Finally, set the new workspace.
182
+ self.config.workspace_id = return_id
183
+ self.config.workspace_handle = return_handle
184
+ logging.info(f"[Client] Switched to workspace {return_handle}/{return_id}")
185
+
186
+ def dict(self, **kwargs) -> dict:
187
+ # Because of the trick we do to hack these in as both static and member methods (with different
188
+ # implementations), Pydantic will try to include them by default. So we have to suppress that otherwise
189
+ # downstream serialization into JSON will fail.
190
+ if "exclude" not in kwargs:
191
+ kwargs["exclude"] = {
192
+ "use": True,
193
+ "use_plugin": True,
194
+ "_instance_use": True,
195
+ "_instance_use_plugin": True,
196
+ "config": {"api_key"},
197
+ }
198
+ elif isinstance(kwargs["exclude"], set):
199
+ kwargs["exclude"].add("use")
200
+ kwargs["exclude"].add("use_plugin")
201
+ kwargs["exclude"].add("_instance_use")
202
+ kwargs["exclude"].add("_instance_use_plugin")
203
+ kwargs["exclude"].add(
204
+ "config"
205
+ ) # the set version cannot exclude subcomponents; we must remove all config
206
+ elif isinstance(kwargs["exclude"], dict):
207
+ kwargs["exclude"]["use"] = True
208
+ kwargs["exclude"]["use_plugin"] = True
209
+ kwargs["exclude"]["_instance_use"] = True
210
+ kwargs["exclude"]["_instance_use_plugin"] = True
211
+ kwargs["exclude"]["config"] = {"api_key"}
212
+
213
+ return super().dict(**kwargs)
214
+
215
+ def _url(
216
+ self,
217
+ is_package_call: bool = False,
218
+ package_owner: str = None,
219
+ operation: str = None,
220
+ ):
221
+ if not is_package_call:
222
+ # Regular API call
223
+ base = self.config.api_base
224
+ else:
225
+ # Do the invocable version
226
+ if package_owner is None:
227
+ return SteamshipError(
228
+ code="UserMissing",
229
+ message="Cannot invoke a package endpoint without the package owner's user handle.",
230
+ suggestion="Provide the package_owner option, or initialize your package with a lookup.",
231
+ )
232
+
233
+ base = self.config.app_base
234
+ if not is_local(base):
235
+ # We want to prepend the user handle
236
+ parts = base.split("//")
237
+ base = f"{parts[0]}//{package_owner}.{'//'.join(parts[1:])}"
238
+
239
+ # Clean leading and trailing "/"
240
+ if base[len(base) - 1] == "/":
241
+ base = base[:-1]
242
+ if operation[0] == "/":
243
+ operation = operation[1:]
244
+
245
+ return f"{base}/{operation}"
246
+
247
+ def _headers( # noqa: C901
248
+ self,
249
+ is_package_call: bool = False,
250
+ package_owner: str = None,
251
+ package_id: str = None,
252
+ package_instance_id: str = None,
253
+ as_background_task: bool = False,
254
+ wait_on_tasks: List[Union[str, Task]] = None,
255
+ ):
256
+ headers = {"Authorization": f"Bearer {self.config.api_key.get_secret_value()}"}
257
+
258
+ if self.config.workspace_id:
259
+ headers["X-Workspace-Id"] = self.config.workspace_id
260
+ elif self.config.workspace_handle:
261
+ headers["X-Workspace-Handle"] = self.config.workspace_handle
262
+
263
+ if is_package_call:
264
+ if package_owner:
265
+ headers["X-Package-Owner-Handle"] = package_owner
266
+ if package_id:
267
+ headers["X-Package-Id"] = package_id
268
+ if package_instance_id:
269
+ headers["X-Package-Instance-Id"] = package_instance_id
270
+
271
+ if wait_on_tasks:
272
+ # Will result in the engine persisting the inbound HTTP request as a Task for deferred
273
+ # execution. Additionally, the task will be scheduled to first wait on the other tasks
274
+ # provided in the list of IDs. Accepts a list of EITHER Task objects OR task_id strings.
275
+ as_background_task = True
276
+ task_ids = []
277
+ for task_or_id in wait_on_tasks:
278
+ if isinstance(task_or_id, str):
279
+ task_ids.append(task_or_id)
280
+ elif isinstance(task_or_id, Task):
281
+ task_ids.append(task_or_id.task_id)
282
+ else:
283
+ raise SteamshipError(
284
+ message=f"`wait_on_tasks` should only contain Task or str objects. Got a {type(task_or_id)}."
285
+ )
286
+
287
+ headers["X-Task-Dependency"] = ",".join(task_ids)
288
+
289
+ if as_background_task:
290
+ # Will result in the engine persisting the inbound HTTP request as a Task for deferred
291
+ # execution. The client will receive task information back instead of the synchronous API response.
292
+ # That task can be polled for eventual completion.
293
+ headers["X-Task-Background"] = "true"
294
+
295
+ return headers
296
+
297
+ @staticmethod
298
+ def _prepare_data(payload: Union[Request, dict]):
299
+ if payload is None:
300
+ data = {}
301
+ elif isinstance(payload, dict):
302
+ data = payload
303
+ elif isinstance(payload, BaseModel):
304
+ data = payload.dict(by_alias=True)
305
+ else:
306
+ raise RuntimeError(f"Unable to parse payload of type {type(payload)}")
307
+
308
+ return data
309
+
310
+ @staticmethod
311
+ def _response_data(resp, raw_response: bool = False):
312
+ if resp is None:
313
+ return None
314
+
315
+ if raw_response:
316
+ return resp.content
317
+
318
+ if resp.headers:
319
+ ct = None
320
+ if "Content-Type" in resp.headers:
321
+ ct = resp.headers["Content-Type"]
322
+ if "content-type" in resp.headers:
323
+ ct = resp.headers["content-type"]
324
+ if ct is not None:
325
+ ct = ct.split(";")[0] # application/json; charset=utf-8
326
+ if ct in [MimeTypes.TXT, MimeTypes.MKD, MimeTypes.HTML]:
327
+ return resp.text
328
+ elif ct == MimeTypes.JSON:
329
+ return resp.json()
330
+ else:
331
+ return resp.content
332
+
333
+ @staticmethod
334
+ def _prepare_multipart_data(data, file):
335
+ # Note: requests seems to have a bug passing boolean (and maybe numeric?)
336
+ # values in the midst of multipart form data. You need to manually convert
337
+ # it to a string; otherwise it will pass as False or True (with the capital),
338
+ # which is not standard notation outside of Python.
339
+ for key in data:
340
+ if data[key] is False:
341
+ data[key] = "false"
342
+ elif data[key] is True:
343
+ data[key] = "true"
344
+
345
+ result = {}
346
+ for key, val in data.items():
347
+ for t in _multipart_name(key, val):
348
+ result[t[0]] = t
349
+ result["file"] = file
350
+ return result
351
+
352
+ def _add_client_to_response(self, expect: Type, response_data: Any):
353
+ if isinstance(response_data, dict):
354
+ self._add_client_to_object(expect, response_data)
355
+ elif isinstance(response_data, list):
356
+ for el in response_data:
357
+ typing_parameters = typing.get_args(expect)
358
+ self._add_client_to_response(
359
+ typing_parameters[0] if typing_parameters else None, el
360
+ )
361
+
362
+ return response_data
363
+
364
+ def _add_client_to_object(self, expect, response_data):
365
+ if expect and isclass(expect):
366
+ if len(response_data.keys()) == 1 and list(response_data.keys())[0] in (
367
+ to_camel(expect.__name__),
368
+ to_camel(expect.__name__).replace("package", "invocable"),
369
+ # Hack since engine uses "App" instead of "Package"
370
+ "index",
371
+ "pluginInstance", # Inlined here since `expect` may be a subclass of pluginInstance
372
+ ):
373
+ # TODO (enias): Hack since the engine responds with incosistent formats e.g. {"plugin" : {plugin_fields}}
374
+ for _, v in response_data.items():
375
+ self._add_client_to_response(expect, v)
376
+ elif issubclass(expect, BaseModel):
377
+ response_data["client"] = self
378
+ try:
379
+ key_to_type = typing.get_type_hints(expect)
380
+ for k, v in response_data.items():
381
+ self._add_client_to_response(key_to_type.get(inflection.underscore(k)), v)
382
+ except NameError:
383
+ # typing.get_type_hints fails for Workspace
384
+ pass
385
+
386
+ def call( # noqa: C901
387
+ self,
388
+ verb: Verb,
389
+ operation: str,
390
+ payload: Union[Request, dict] = None,
391
+ file: Any = None,
392
+ expect: Type[T] = None,
393
+ debug: bool = False,
394
+ raw_response: bool = False,
395
+ is_package_call: bool = False,
396
+ package_owner: str = None,
397
+ package_id: str = None,
398
+ package_instance_id: str = None,
399
+ as_background_task: bool = False,
400
+ wait_on_tasks: List[Union[str, Task]] = None,
401
+ timeout_s: Optional[float] = None,
402
+ ) -> Union[
403
+ Any, Task
404
+ ]: # TODO (enias): I would like to list all possible return types using interfaces instead of Any
405
+ """Post to the Steamship API.
406
+
407
+ All responses have the format::
408
+
409
+ .. code-block:: json
410
+
411
+ {
412
+ "data": "<actual response>",
413
+ "error": {"reason": "<message>"}
414
+ } # noqa: RST203
415
+
416
+ For the Python client we return the contents of the `data` field if present, and we raise an exception
417
+ if the `error` field is filled in.
418
+ """
419
+ # TODO (enias): Review this codebase
420
+ url = self._url(
421
+ is_package_call=is_package_call,
422
+ package_owner=package_owner,
423
+ operation=operation,
424
+ )
425
+
426
+ headers = self._headers(
427
+ is_package_call=is_package_call,
428
+ package_owner=package_owner,
429
+ package_id=package_id,
430
+ package_instance_id=package_instance_id,
431
+ as_background_task=as_background_task,
432
+ wait_on_tasks=wait_on_tasks,
433
+ )
434
+
435
+ data = self._prepare_data(payload=payload)
436
+
437
+ logging.debug(
438
+ f"Making {verb} to {url} in workspace {self.config.workspace_handle}/{self.config.workspace_id}"
439
+ )
440
+ if verb == Verb.POST:
441
+ if file is not None:
442
+ files = self._prepare_multipart_data(data, file)
443
+ resp = self._session.post(url, files=files, headers=headers, timeout=timeout_s)
444
+ else:
445
+ resp = self._session.post(url, json=data, headers=headers, timeout=timeout_s)
446
+ elif verb == Verb.GET:
447
+ resp = self._session.get(url, params=data, headers=headers, timeout=timeout_s)
448
+ else:
449
+ raise Exception(f"Unsupported verb: {verb}")
450
+
451
+ logging.debug(f"From {verb} to {url} got HTTP {resp.status_code}")
452
+
453
+ if debug is True:
454
+ logging.debug(f"Got response {resp}")
455
+
456
+ response_data = self._response_data(resp, raw_response=raw_response)
457
+
458
+ logging.debug(f"Response JSON {response_data}")
459
+
460
+ task = None
461
+ error = None
462
+
463
+ if isinstance(response_data, dict):
464
+ if "status" in response_data:
465
+ try:
466
+ task = Task.parse_obj(
467
+ {**response_data["status"], "client": self, "expect": expect}
468
+ )
469
+ if "state" in response_data["status"]:
470
+ if response_data["status"]["state"] == "failed":
471
+ error = SteamshipError.from_dict(response_data["status"])
472
+ logging.warning(f"Client received error from server: {error}")
473
+ except TypeError as e:
474
+ # There's an edge case here -- if a Steamship package returns the JSON dictionary
475
+ #
476
+ # { "status": "status string" }
477
+ #
478
+ # Then the above handler will attempt to parse it and throw... But we don't actually want to throw
479
+ # since we don't take a strong opinion on what the response type of a package endpoint ought to be.
480
+ # It *may* choose to conform to the SteamshipResponse<T> type, but it doesn't have to.
481
+ if not is_package_call:
482
+ raise e
483
+
484
+ if task is not None and task.state == TaskState.failed:
485
+ error = task.as_error()
486
+
487
+ if "data" in response_data:
488
+ if expect is not None:
489
+ if issubclass(expect, SteamshipError):
490
+ data = expect.from_dict({**response_data["data"], "client": self})
491
+ elif issubclass(expect, BaseModel):
492
+ data = expect.parse_obj(
493
+ self._add_client_to_response(expect, response_data["data"])
494
+ )
495
+ else:
496
+ raise RuntimeError(f"obj of type {expect} does not have a from_dict method")
497
+ else:
498
+ data = response_data["data"]
499
+
500
+ if task:
501
+ task.output = data
502
+ else:
503
+ data = response_data
504
+
505
+ else:
506
+ data = response_data
507
+
508
+ if error is not None:
509
+ logging.warning(f"Client received error from server: {error}", exc_info=error)
510
+ raise error
511
+
512
+ if not resp.ok:
513
+ raise SteamshipError(
514
+ f"API call did not complete successfully. Server returned: {response_data}"
515
+ )
516
+
517
+ elif task is not None:
518
+ return task
519
+ elif data is not None and expect is not None:
520
+ # if we have data AND we expect it to be of a certain type,
521
+ # we should probably make sure that expectation is met.
522
+ if not isinstance(data, expect):
523
+ raise SteamshipError(
524
+ message=f"Inconsistent response from server (data does not match expected type: {expect}.)",
525
+ suggestion="Please contact support via hello@steamship.com and report what caused this error.",
526
+ )
527
+ return data
528
+ elif data is not None:
529
+ return data
530
+ else:
531
+ raise SteamshipError("Inconsistent response from server. Please contact support.")
532
+
533
+ def post(
534
+ self,
535
+ operation: str,
536
+ payload: Union[Request, dict, BaseModel] = None,
537
+ file: Any = None,
538
+ expect: Any = None,
539
+ debug: bool = False,
540
+ raw_response: bool = False,
541
+ is_package_call: bool = False,
542
+ package_owner: str = None,
543
+ package_id: str = None,
544
+ package_instance_id: str = None,
545
+ as_background_task: bool = False,
546
+ wait_on_tasks: List[Union[str, Task]] = None,
547
+ timeout_s: Optional[float] = None,
548
+ ) -> Union[
549
+ Any, Task
550
+ ]: # TODO (enias): I would like to list all possible return types using interfaces instead of Any
551
+ return self.call(
552
+ verb=Verb.POST,
553
+ operation=operation,
554
+ payload=payload,
555
+ file=file,
556
+ expect=expect,
557
+ debug=debug,
558
+ raw_response=raw_response,
559
+ is_package_call=is_package_call,
560
+ package_owner=package_owner,
561
+ package_id=package_id,
562
+ package_instance_id=package_instance_id,
563
+ as_background_task=as_background_task,
564
+ wait_on_tasks=wait_on_tasks,
565
+ timeout_s=timeout_s,
566
+ )
567
+
568
+ def get(
569
+ self,
570
+ operation: str,
571
+ payload: Union[Request, dict] = None,
572
+ file: Any = None,
573
+ expect: Any = None,
574
+ debug: bool = False,
575
+ raw_response: bool = False,
576
+ is_package_call: bool = False,
577
+ package_owner: str = None,
578
+ package_id: str = None,
579
+ package_instance_id: str = None,
580
+ as_background_task: bool = False,
581
+ wait_on_tasks: List[Union[str, Task]] = None,
582
+ timeout_s: Optional[float] = None,
583
+ ) -> Union[
584
+ Any, Task
585
+ ]: # TODO (enias): I would like to list all possible return types using interfaces instead of Any
586
+ return self.call(
587
+ verb=Verb.GET,
588
+ operation=operation,
589
+ payload=payload,
590
+ file=file,
591
+ expect=expect,
592
+ debug=debug,
593
+ raw_response=raw_response,
594
+ is_package_call=is_package_call,
595
+ package_owner=package_owner,
596
+ package_id=package_id,
597
+ package_instance_id=package_instance_id,
598
+ as_background_task=as_background_task,
599
+ wait_on_tasks=wait_on_tasks,
600
+ timeout_s=timeout_s,
601
+ )
602
+
603
+ def logs(
604
+ self,
605
+ offset: int = 0,
606
+ number: int = 50,
607
+ invocable_handle: Optional[str] = None,
608
+ instance_handle: Optional[str] = None,
609
+ invocable_version_handle: Optional[str] = None,
610
+ path: Optional[str] = None,
611
+ ) -> Dict[str, Any]:
612
+ """Return generated logs for a client.
613
+
614
+ The logs will be workspace-scoped. You will only receive logs
615
+ for packages and plugins that you own.
616
+
617
+ :param offset: The index of the first log entry to return. This can be used with `number` to page through logs.
618
+ :param number: The number of log entries to return. This can be used with `offset` to page through logs.
619
+ :param invocable_handle: Enables optional filtering based on the handle of package or plugin. Example: `my-package`
620
+ :param instance_handle: Enables optional filtering based on the handle of package instance or plugin instance. Example: `my-instance`
621
+ :param invocable_version_handle: Enables optional filtering based on the version handle of package or plugin. Example: `0.0.2`
622
+ :param path: Enables optional filtering based on request path. Example: `/generate`.
623
+ :return: Returns a dictionary containing the offset and number of log entries as well as a list of `entries` that match the specificed filters.
624
+ """
625
+ args = {"from": offset, "size": number}
626
+ if invocable_handle:
627
+ args["invocableHandle"] = invocable_handle
628
+ if instance_handle:
629
+ args["invocableInstanceHandle"] = instance_handle
630
+ if invocable_version_handle:
631
+ args["invocableVersionHandle"] = invocable_version_handle
632
+ if path:
633
+ args["invocablePath"] = path
634
+
635
+ return self.post("logs/list", args)
steamship/base/configuration.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import inflection
9
+ from pydantic import HttpUrl, SecretStr
10
+
11
+ from steamship.base.model import CamelModel
12
+ from steamship.cli.login import login
13
+ from steamship.utils.utils import format_uri
14
+
15
+ DEFAULT_WEB_BASE = "https://steamship.com/"
16
+ DEFAULT_APP_BASE = "https://steamship.run/"
17
+ DEFAULT_API_BASE = "https://api.steamship.com/api/v1/"
18
+
19
+ ENVIRONMENT_VARIABLES_TO_PROPERTY = {
20
+ "STEAMSHIP_API_KEY": "api_key",
21
+ "STEAMSHIP_API_BASE": "api_base",
22
+ "STEAMSHIP_APP_BASE": "app_base",
23
+ "STEAMSHIP_WEB_BASE": "web_base",
24
+ "STEAMSHIP_WORKSPACE_ID": "workspace_id",
25
+ "STEAMSHIP_WORKSPACE_HANDLE": "workspace_handle",
26
+ }
27
+ DEFAULT_CONFIG_FILE = Path.home() / ".steamship.json"
28
+
29
+ # This stops us from including the `client` object in the dict() output, which is fine in a dict()
30
+ # but explodes if that dict() is turned into JSON. Sadly the `exclude` option in Pydantic doesn't
31
+ # cascade down nested objects, so we have to use this structure to catch all the possible combinations
32
+ EXCLUDE_FROM_DICT = {
33
+ "client": True,
34
+ "blocks": {"__all__": {"client": True, "tags": {"__all__": {"client": True}}}},
35
+ "tags": {"__all__": {"client": True}},
36
+ }
37
+
38
+
39
+ class Configuration(CamelModel):
40
+ api_key: SecretStr
41
+ api_base: HttpUrl = DEFAULT_API_BASE
42
+ app_base: HttpUrl = DEFAULT_APP_BASE
43
+ web_base: HttpUrl = DEFAULT_WEB_BASE
44
+ workspace_id: str = None
45
+ workspace_handle: str = None
46
+ profile: Optional[str] = None
47
+
48
+ def __init__(
49
+ self,
50
+ config_file: Optional[Path] = None,
51
+ **kwargs,
52
+ ):
53
+ # First set the profile
54
+ kwargs["profile"] = profile = kwargs.get("profile") or os.getenv("STEAMSHIP_PROFILE")
55
+
56
+ # Then load configuration from a file if provided
57
+ config_dict = self._load_from_file(
58
+ config_file or DEFAULT_CONFIG_FILE,
59
+ profile,
60
+ raise_on_exception=config_file is not None,
61
+ )
62
+ config_dict.update(self._get_config_dict_from_environment())
63
+ kwargs.update({k: v for k, v in config_dict.items() if kwargs.get(k) is None})
64
+
65
+ kwargs["api_base"] = format_uri(kwargs.get("api_base"))
66
+ kwargs["app_base"] = format_uri(kwargs.get("app_base"))
67
+ kwargs["web_base"] = format_uri(kwargs.get("web_base"))
68
+
69
+ if not kwargs.get("api_key") and not kwargs.get("apiKey"):
70
+ api_key = login(
71
+ kwargs.get("api_base") or DEFAULT_API_BASE,
72
+ kwargs.get("web_base") or DEFAULT_WEB_BASE,
73
+ )
74
+ Configuration._save_api_key_to_file(
75
+ api_key, profile, config_file or DEFAULT_CONFIG_FILE
76
+ )
77
+ kwargs["api_key"] = api_key
78
+
79
+ super().__init__(**kwargs)
80
+
81
+ @staticmethod
82
+ def _load_from_file(
83
+ file: Path, profile: str = None, raise_on_exception: bool = False
84
+ ) -> Optional[dict]:
85
+ try:
86
+ with file.open() as f:
87
+ config_file = json.load(f)
88
+ if profile:
89
+ if "profiles" not in config_file or profile not in config_file["profiles"]:
90
+ raise RuntimeError(f"Profile {profile} requested but not found in {file}")
91
+ config = config_file["profiles"][profile]
92
+ else:
93
+ config = config_file
94
+ return {inflection.underscore(k): v for k, v in config.items()}
95
+ except FileNotFoundError:
96
+ if raise_on_exception:
97
+ raise Exception(f"Tried to load configuration file at {file} but it did not exist.")
98
+ except Exception as err:
99
+ if raise_on_exception:
100
+ raise err
101
+ return {}
102
+
103
+ @staticmethod
104
+ def _get_config_dict_from_environment():
105
+ """Overrides configuration with environment variables."""
106
+ return {
107
+ property_name: os.getenv(environment_variable_name, None)
108
+ for environment_variable_name, property_name in ENVIRONMENT_VARIABLES_TO_PROPERTY.items()
109
+ if environment_variable_name in os.environ
110
+ }
111
+
112
+ @staticmethod
113
+ def _save_api_key_to_file(new_api_key: Optional[str], profile: Optional[str], file_path: Path):
114
+ # Minimally rewrite config file, adding api key
115
+ try:
116
+ with file_path.open() as f:
117
+ config_file = json.load(f)
118
+ if profile:
119
+ if "profiles" not in config_file or profile not in config_file["profiles"]:
120
+ raise RuntimeError(f"Could not update API key for {profile} in {file_path}")
121
+ config = config_file["profiles"][profile]
122
+ else:
123
+ config = config_file
124
+ except FileNotFoundError:
125
+ config_file = {}
126
+ config = config_file
127
+
128
+ config["apiKey"] = new_api_key
129
+
130
+ with file_path.open("w") as f:
131
+ json.dump(config_file, f, indent="\t")
132
+
133
+ @staticmethod
134
+ def default_config_file_has_api_key() -> bool:
135
+ return Configuration._load_from_file(DEFAULT_CONFIG_FILE).get("api_key") is not None
136
+
137
+ @staticmethod
138
+ def remove_api_key_from_default_config():
139
+ Configuration._save_api_key_to_file(None, None, DEFAULT_CONFIG_FILE)
steamship/base/environments.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from enum import Enum
3
+
4
+ from steamship.base.configuration import Configuration
5
+ from steamship.base.error import SteamshipError
6
+
7
+
8
+ class RuntimeEnvironments(str, Enum):
9
+ REPLIT = "replit"
10
+ LOCALHOST = "localhost"
11
+
12
+
13
+ def _interactively_get_key(env: RuntimeEnvironments):
14
+ print(
15
+ """Get your free API key here: https://steamship.com/account/api
16
+
17
+ You'll get immediate access to our SDK for AI models, including OpenAI, GPT, Cohere, and more.
18
+ """
19
+ )
20
+
21
+ api_key = input("Paste your API key to run: ")
22
+
23
+ while len(api_key.strip()) == 0:
24
+ api_key = input("API Key: ")
25
+
26
+ os.environ["STEAMSHIP_API_KEY"] = api_key
27
+
28
+ if env == RuntimeEnvironments.REPLIT:
29
+ print(
30
+ """
31
+ This key is set temporarily. In the future, you can:
32
+ - Set the STEAMSHIP_API_KEY Replit Secret
33
+ - Close and re-open any Replit shells to make sure secrets are refreshed.
34
+
35
+ """
36
+ )
37
+ elif env == RuntimeEnvironments.LOCALHOST:
38
+ print(
39
+ """
40
+ This key is set temporarily. In the future, you can:
41
+ - Set the STEAMSHIP_API_KEY environment variable
42
+ - Run `ship login` to create a ~/.steamship.json credential file
43
+
44
+ """
45
+ )
46
+
47
+
48
+ def _report_error_and_exit(env: RuntimeEnvironments):
49
+ if env == RuntimeEnvironments.REPLIT:
50
+ print(
51
+ """To run this Replit, you will need a Steamship API Key.
52
+
53
+ 1) If you're viewing someone else's Replit, clone it
54
+
55
+ 2) Visit https://steamship.com/account/api to get a key
56
+
57
+ 3) Add your key as a Replit secret named STEAMSHIP_API_KEY
58
+
59
+ 4) Close and re-open any shells to make sure your new secret is available
60
+
61
+ Then try running again!"""
62
+ )
63
+ elif env == RuntimeEnvironments.LOCALHOST:
64
+ print(
65
+ """To run this script, you will need a Steamship API Key.
66
+
67
+ 1) Visit https://steamship.com/account/api to get a key
68
+
69
+ 2) Set your key as the environment variable STEAMSHIP_API_KEY
70
+
71
+ Then try running again!
72
+
73
+ If you have pip-installed `steamship`, you can also try setting your key by simply running `ship login`.
74
+ """
75
+ )
76
+ exit(-1)
77
+
78
+
79
+ def check_environment(env: RuntimeEnvironments, interactively_set_key: bool = True):
80
+ # This will try loading from STEAMSHIP_API_KEY and also ~/.steamship.json
81
+ try:
82
+ config = Configuration()
83
+
84
+ # If an API key is set, we're good to go!
85
+ if config.api_key:
86
+ return
87
+ except SteamshipError:
88
+ # The Configuration object will throw an error if there is no API Key found.
89
+ # Since that error is expected from the context of this function, we pass on it to handle it in a more
90
+ # user-interactive way.
91
+ pass
92
+
93
+ # If we're hot-loading config, do it here!
94
+ if interactively_set_key:
95
+ _interactively_get_key(env)
96
+ return
97
+
98
+ # If we're still here, we're not interactively setting the key. Display an error message and exit.
99
+ _report_error_and_exit(env)
steamship/base/error.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Union
5
+
6
+ DEFAULT_ERROR_MESSAGE = "Undefined remote error"
7
+
8
+
9
+ class SteamshipError(Exception):
10
+ message: str = None
11
+ internal_message: str = None
12
+ suggestion: str = None
13
+ code: str = None
14
+ error: str = None
15
+
16
+ def __init__(
17
+ self,
18
+ message: str = DEFAULT_ERROR_MESSAGE,
19
+ internal_message: str = None,
20
+ suggestion: str = None,
21
+ code: str = None,
22
+ error: Union[Exception, str] = None,
23
+ ):
24
+ super().__init__()
25
+ self.message = message
26
+ self.suggestion = suggestion
27
+ self.internal_message = internal_message
28
+ self.code = code
29
+ if error is not None:
30
+ self.error = str(error)
31
+
32
+ parts = []
33
+ if code is not None:
34
+ parts.append(f"[{code}]")
35
+ if message is not None:
36
+ parts.append(message)
37
+ if internal_message is not None:
38
+ parts.append(f"Internal Message: {internal_message}")
39
+ if suggestion is not None:
40
+ parts.append(f"Suggestion: {suggestion}")
41
+
42
+ super().__init__("\n".join(parts))
43
+
44
+ def log(self):
45
+ logging.error(
46
+ f"[{self.code}] {self.message}. [Internal: {self.internal_message}] [Suggestion: {self.suggestion}]",
47
+ exc_info=self,
48
+ )
49
+ if self.error:
50
+ logging.error(self.error)
51
+
52
+ def to_dict(self) -> dict:
53
+ # Required since Exception cannot be combined with pydantic.BaseModel
54
+ return {
55
+ "message": self.message,
56
+ "internalMessage": self.internal_message,
57
+ "suggestion": self.suggestion,
58
+ "code": self.code,
59
+ "error": self.error,
60
+ }
61
+
62
+ @staticmethod
63
+ def from_dict(d: Any) -> SteamshipError:
64
+ """Last resort if subclass doesn't override: pass through."""
65
+ # Required since Exception cannot be combined with pydantic.BaseModel
66
+ return SteamshipError(
67
+ message=d.get("statusMessage", d.get("message")),
68
+ internal_message=d.get("internalMessage"),
69
+ suggestion=d.get("statusSuggestion", d.get("suggestion")),
70
+ code=d.get("statusCode", d.get("code")),
71
+ error=d.get("error", d.get("error")),
72
+ )
steamship/base/mime_types.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class MimeTypes(str, Enum):
5
+ UNKNOWN = "unknown"
6
+ TXT = "text/plain"
7
+ JSON = "application/json"
8
+ MKD = "text/markdown"
9
+ EPUB = "application/epub+zip"
10
+ PDF = "application/pdf"
11
+ JPG = "image/jpeg"
12
+ PNG = "image/png"
13
+ TIFF = "image/tiff"
14
+ GIF = "image/gif"
15
+ HTML = "text/html"
16
+ DOC = "application/msword"
17
+ DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
18
+ PPT = "applicatino/ms-powerpoint"
19
+ PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
20
+ RTF = "application/rtf"
21
+ BINARY = "application/octet-stream"
22
+ STEAMSHIP_BLOCK_JSON = "application/vnd.steamship-block.json.v1"
23
+ WAV = "audio/wav"
24
+ MP3 = "audio/mp3"
25
+ MP4_VIDEO = "video/mp4"
26
+ MP4_AUDIO = "audio/mp4"
27
+ WEBM_VIDEO = "video/webm"
28
+ WEBM_AUDIO = "audio/webm"
29
+ FILE_JSON = "fileJson"
30
+
31
+
32
+ class ContentEncodings:
33
+ BASE64 = "base64"
34
+
35
+
36
+ TEXT_MIME_TYPES = [
37
+ MimeTypes.TXT,
38
+ MimeTypes.MKD,
39
+ MimeTypes.HTML,
40
+ MimeTypes.DOCX,
41
+ MimeTypes.PPTX,
42
+ ]
steamship/base/model.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from typing import TypeVar
3
+
4
+ import inflection
5
+ from pydantic import BaseModel
6
+ from pydantic.generics import GenericModel
7
+
8
+ T = TypeVar("T") # Declare type variable
9
+
10
+
11
+ def to_camel(s: str) -> str:
12
+ s = re.sub("_(url)$", lambda m: f"_{m.group(1).upper()}", s)
13
+ return inflection.camelize(s, uppercase_first_letter=False)
14
+
15
+
16
+ class CamelModel(BaseModel):
17
+ def __init__(self, **kwargs):
18
+ kwargs = {k: v for k, v in kwargs.items() if v is not None}
19
+ super().__init__(**kwargs)
20
+
21
+ class Config:
22
+ alias_generator = to_camel
23
+ allow_population_by_field_name = True
24
+ # Populate enum values with their value, rather than the raw enum. Important to serialise model.dict()
25
+ use_enum_values = True
26
+
27
+
28
+ class GenericCamelModel(CamelModel, GenericModel):
29
+ pass
steamship/base/package_spec.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Objects for recording and reporting upon the introspected interface of a Steamship Package."""
2
+ import inspect
3
+ from enum import Enum
4
+ from typing import Dict, List, Optional, Union, get_args, get_origin
5
+
6
+ from steamship import SteamshipError
7
+ from steamship.base.configuration import CamelModel
8
+ from steamship.utils.url import Verb
9
+
10
+
11
+ class ArgSpec(CamelModel):
12
+ """An argument passed to a method."""
13
+
14
+ # The name of the argument.
15
+ name: str
16
+ # The kind of the argument, reported by str(annotation) via the `inspect` library. E.g. <class 'int'>
17
+ kind: str
18
+ # Possible values, if the kind is an enum type
19
+ values: Optional[List[str]]
20
+
21
+ def __init__(self, name: str, parameter: inspect.Parameter):
22
+ if name == "self":
23
+ raise SteamshipError(
24
+ message="Attempt to interpret the `self` object as a method parameter."
25
+ )
26
+ values = None
27
+ if isinstance(parameter.annotation, type):
28
+ if issubclass(parameter.annotation, Enum):
29
+ values = [choice.value for choice in parameter.annotation]
30
+ elif get_origin(parameter.annotation) is Union:
31
+ args = get_args(parameter.annotation)
32
+ # For now, only deal with the case where the Union is an Optional[Enum]
33
+ if len(args) == 2 and type(None) in args:
34
+ optional_arg = [t for t in args if t != type(None)][0] # noqa: E721
35
+ if issubclass(optional_arg, Enum):
36
+ values = [choice.value for choice in optional_arg]
37
+
38
+ super().__init__(name=name, kind=str(parameter.annotation), values=values)
39
+
40
+ def pprint(self, name_width: Optional[int] = None, prefix: str = "") -> str:
41
+ """Returns a pretty printable representation of this argument."""
42
+ width = name_width or len(self.name)
43
+ ret = f"{prefix}{self.name.ljust(width)} - {self.kind}"
44
+ return ret
45
+
46
+
47
+ class MethodSpec(CamelModel):
48
+ """A method, callable remotely, on an object."""
49
+
50
+ # The HTTP Path at which the method is callable.
51
+ path: str
52
+
53
+ # The HTTP Verb at which the method is callable. Defaults to POST
54
+ verb: str
55
+
56
+ # The return type. Reported by str(annotation) via the `inspect` library. E.g. <class 'int'>
57
+ returns: str
58
+
59
+ # The docstring of the method.
60
+ doc: Optional[str] = None
61
+
62
+ # The named arguments of the method. Positional arguments are not permitted.
63
+ args: Optional[List[ArgSpec]] = None
64
+
65
+ # Additional configuration around this endpoint.
66
+ # Note: The actual type of this is Optional[Dict[str, Union[str, bool, int, float]]]
67
+ # But if Pydantic sees that, it attempts to force all values to be str, which is wrong.
68
+ config: Optional[Dict] = None
69
+
70
+ @staticmethod
71
+ def clean_path(path: str = "") -> str:
72
+ """Ensure that the path always starts with /, and at minimum must be at least /."""
73
+ if not path:
74
+ path = "/"
75
+ elif path[0] != "/":
76
+ path = f"/{path}"
77
+ return path
78
+
79
+ def __init__(
80
+ self,
81
+ cls: object,
82
+ name: str,
83
+ path: str = None,
84
+ verb: Verb = Verb.POST,
85
+ config: Dict[str, Union[str, bool, int, float]] = None,
86
+ ):
87
+ # Set the path
88
+ if path is None and name is not None:
89
+ path = f"/{name}"
90
+ path = MethodSpec.clean_path(path)
91
+
92
+ # Get the function on the class so that we can inspect it
93
+ func = getattr(cls, name)
94
+ sig = inspect.signature(func)
95
+
96
+ # Set the return type
97
+ returns = str(sig.return_annotation)
98
+
99
+ # Set the docstring
100
+ doc = func.__doc__
101
+
102
+ # Set the arguments
103
+ args = []
104
+ for p in sig.parameters:
105
+ if p == "self":
106
+ continue
107
+ args.append(ArgSpec(p, sig.parameters[p]))
108
+
109
+ super().__init__(path=path, verb=verb, returns=returns, doc=doc, args=args, config=config)
110
+
111
+ def pprint(self, name_width: Optional[int] = None, prefix: str = " ") -> str:
112
+ """Returns a pretty printable representation of this method."""
113
+
114
+ width = name_width or len(self.path)
115
+ ret = f"{self.verb.ljust(4)} {self.path.lstrip('/').ljust(width)} -> {self.returns}"
116
+ if self.args:
117
+ name_width = max([(len(arg.name) if arg.name else 0) for arg in self.args])
118
+ for arg in self.args:
119
+ arg_doc_string = arg.print(name_width, prefix)
120
+ ret += f"\n{arg_doc_string}"
121
+ return ret
122
+
123
+
124
+ class PackageSpec(CamelModel):
125
+ """A package, representing a remotely instantiable service."""
126
+
127
+ # The name of the package
128
+ name: str
129
+
130
+ # The docstring of the package
131
+ doc: Optional[str] = None
132
+
133
+ # The list of methods the package exposes remotely
134
+ methods: Optional[List[MethodSpec]] = None
135
+
136
+ def pprint(self, prefix: str = " ") -> str:
137
+ """Returns a pretty printable representation of this package."""
138
+ underline = "=" * len(self.name)
139
+ ret = f"{self.name}\n{underline}\n"
140
+ if self.doc:
141
+ ret += f"{self.doc}\n\n"
142
+ else:
143
+ ret += "\n"
144
+
145
+ if self.methods:
146
+ name_width = max([len(method.path) or 0 for method in self.methods])
147
+ for method in self.methods:
148
+ method_doc_string = method.pprint(name_width, prefix)
149
+ ret += f"\n{method_doc_string}"
150
+ return ret
steamship/base/request.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from steamship.base.model import CamelModel
2
+
3
+
4
+ class Request(CamelModel):
5
+ pass
6
+
7
+
8
+ class GetRequest(Request):
9
+ id: str = None
10
+ handle: str = None
11
+
12
+
13
+ class CreateRequest(Request):
14
+ id: str = None
15
+ handle: str = None
16
+
17
+
18
+ class UpdateRequest(Request):
19
+ id: str = None
20
+ handle: str = None
21
+
22
+
23
+ class IdentifierRequest(Request):
24
+ id: str = None
25
+ handle: str = None
26
+
27
+
28
+ class ListRequest(Request):
29
+ pass
30
+
31
+
32
+ class DeleteRequest(Request):
33
+ id: str
steamship/base/response.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from steamship.base.model import CamelModel
2
+
3
+
4
+ class Response(CamelModel):
5
+ pass
steamship/base/tasks.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, Callable, Dict, Generic, List, Optional, Set, Type, TypeVar
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from steamship.base.error import SteamshipError
9
+ from steamship.base.model import CamelModel, GenericCamelModel
10
+ from steamship.base.request import DeleteRequest, IdentifierRequest, Request
11
+ from steamship.utils.metadata import metadata_to_str, str_to_metadata
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ class CreateTaskCommentRequest(Request):
17
+ task_id: str
18
+ external_id: str = None
19
+ external_type: str = None
20
+ external_group: str = None
21
+ metadata: str = None
22
+
23
+
24
+ class ListTaskCommentRequest(Request):
25
+ task_id: str = None
26
+ external_id: str = None
27
+ external_type: str = None
28
+ external_group: str = None
29
+
30
+
31
+ class TaskComment(CamelModel):
32
+ client: Client = Field(None, exclude=True)
33
+ id: str = None
34
+ user_id: str = None
35
+ task_id: str = None
36
+ external_id: str = None
37
+ external_type: str = None
38
+ external_group: str = None
39
+ metadata: Any = None
40
+ created_at: str = None
41
+
42
+ def __init__(self, **kwargs):
43
+ kwargs["metadata"] = str_to_metadata(kwargs.get("metadata"))
44
+ super().__init__(**kwargs)
45
+
46
+ @classmethod
47
+ def parse_obj(cls: Type[BaseModel], obj: Any) -> BaseModel:
48
+ # TODO (enias): This needs to be solved at the engine side
49
+ obj = obj["taskComment"] if "taskComment" in obj else obj
50
+ return super().parse_obj(obj)
51
+
52
+ @staticmethod
53
+ def create(
54
+ client: Client,
55
+ task_id: str = None,
56
+ external_id: str = None,
57
+ external_type: str = None,
58
+ external_group: str = None,
59
+ metadata: Any = None,
60
+ ) -> TaskComment:
61
+ req = CreateTaskCommentRequest(
62
+ taskId=task_id,
63
+ external_id=external_id,
64
+ external_type=external_type,
65
+ externalGroup=external_group,
66
+ metadata=metadata_to_str(metadata),
67
+ )
68
+ return client.post(
69
+ "task/comment/create",
70
+ req,
71
+ expect=TaskComment,
72
+ )
73
+
74
+ @staticmethod
75
+ def list(
76
+ client: Client,
77
+ task_id: str = None,
78
+ external_id: str = None,
79
+ external_type: str = None,
80
+ external_group: str = None,
81
+ ) -> TaskCommentList:
82
+ req = ListTaskCommentRequest(
83
+ taskId=task_id,
84
+ external_id=external_id,
85
+ external_type=external_type,
86
+ externalGroup=external_group,
87
+ )
88
+ return client.post(
89
+ "task/comment/list",
90
+ req,
91
+ expect=TaskCommentList,
92
+ )
93
+
94
+ def delete(self) -> TaskComment:
95
+ req = DeleteRequest(id=self.id)
96
+ return self.client.post(
97
+ "task/comment/delete",
98
+ req,
99
+ expect=TaskComment,
100
+ )
101
+
102
+
103
+ class TaskCommentList(CamelModel):
104
+ comments: List[TaskComment]
105
+
106
+
107
+ class TaskState:
108
+ waiting = "waiting"
109
+ running = "running"
110
+ succeeded = "succeeded"
111
+ failed = "failed"
112
+
113
+
114
+ class TaskType:
115
+ internal_api = "internalApi"
116
+ train = "train"
117
+ infer = "infer"
118
+
119
+
120
+ class TaskRunRequest(Request):
121
+ task_id: str
122
+
123
+
124
+ class TaskStatusRequest(Request):
125
+ task_id: str
126
+
127
+
128
+ class Task(GenericCamelModel, Generic[T]):
129
+ """Encapsulates a unit of asynchronously performed work."""
130
+
131
+ # Note: The Field object prevents this from being serialized into JSON (and causing a crash)
132
+ client: Client = Field(None, exclude=True) # Steamship client
133
+
134
+ task_id: str = None # The id of this task
135
+ user_id: str = None # The user who requested this task
136
+ workspace_id: str = None # The workspace in which this task is executing
137
+
138
+ # Note: The Field object prevents this from being serialized into JSON (and causing a crash)
139
+ expect: Type = Field(
140
+ None, exclude=True
141
+ ) # Type of the expected output once the output is complete.
142
+
143
+ input: str = None # The input provided to the task
144
+ output: T = None # The output of the task
145
+ state: str = None # A value in class TaskState
146
+
147
+ status_message: str = None # User-facing message concerning task status
148
+ status_suggestion: str = None # User-facing suggestion concerning error remediation
149
+ status_code: str = None # User-facing error code for support assistance
150
+ status_created_on: str = None # When the status fields were last set
151
+
152
+ task_type: str = None # A value in class TaskType; for internal routing
153
+ task_executor: str = None #
154
+ task_created_on: str = None # When the task object was created
155
+ task_last_modified_on: str = None # When the task object was last modified
156
+
157
+ # Long Running Plugin Support
158
+ # The `remote_status_*` fields govern how Steamship Plugins can communicate long-running work back to the engine.
159
+ # If instead of sending data, the plugin sends a status with these fields set, the engine will begin polling for
160
+ # updates, echoing the contents of these fields back to the plugin to communicate, e.g., the jobId of the work
161
+ # being checked. When the work is complete, simply respond with the Response `data` field set as per usual.
162
+ remote_status_input: Optional[
163
+ Dict
164
+ ] = None # For re-hydrating state in order to check remote status.
165
+ remote_status_output: Optional[
166
+ Dict
167
+ ] = None # For reporting structured JSON state for error diagnostics.
168
+ remote_status_message: str = None # User facing message string to report on remote status.
169
+
170
+ assigned_worker: str = None # The worker assigned to complete this task
171
+ started_at: str = None # When the work on this task began
172
+
173
+ max_retries: int = None # The maximum number of retries allowed for this task
174
+ retries: int = None # The number of retries already used.
175
+
176
+ def as_error(self) -> SteamshipError:
177
+ return SteamshipError(
178
+ message=self.status_message, suggestion=self.status_suggestion, code=self.status_code
179
+ )
180
+
181
+ @classmethod
182
+ def parse_obj(cls: Type[BaseModel], obj: Any) -> Task:
183
+ obj = obj["task"] if "task" in obj else obj
184
+ return super().parse_obj(obj)
185
+
186
+ @staticmethod
187
+ def get(
188
+ client,
189
+ _id: str = None,
190
+ handle: str = None,
191
+ ) -> Task:
192
+ return client.post(
193
+ "task/get",
194
+ IdentifierRequest(id=_id, handle=handle),
195
+ expect=Task,
196
+ )
197
+
198
+ def update(self, other: Optional[Task] = None):
199
+ """Incorporates a `Task` into this object."""
200
+ other = other or Task()
201
+ for k, v in other.__dict__.items():
202
+ self.__dict__[k] = v
203
+
204
+ def add_comment(
205
+ self,
206
+ external_id: str = None,
207
+ external_type: str = None,
208
+ external_group: str = None,
209
+ metadata: Any = None,
210
+ ) -> TaskComment:
211
+ return TaskComment.create(
212
+ client=self.client,
213
+ task_id=self.task_id,
214
+ external_id=external_id,
215
+ external_type=external_type,
216
+ external_group=external_group,
217
+ metadata=metadata,
218
+ )
219
+
220
+ def post_update(self, fields: Set[str] = None) -> Task:
221
+ """Updates this task in the Steamship Engine."""
222
+ if not isinstance(fields, set):
223
+ raise RuntimeError(f'Unexpected type of "fields": {type(fields)}. Expected type set.')
224
+ body = self.dict(by_alias=True, include={*fields, "task_id"})
225
+ return self.client.post("task/update", body, expect=Task)
226
+
227
+ def wait(
228
+ self,
229
+ max_timeout_s: float = 180,
230
+ retry_delay_s: float = 1,
231
+ on_each_refresh: "Optional[Callable[[int, float, Task], None]]" = None,
232
+ ):
233
+ """Polls and blocks until the task has succeeded or failed (or timeout reached).
234
+
235
+ Parameters
236
+ ----------
237
+ max_timeout_s : int
238
+ Max timeout in seconds. Default: 180s. After this timeout, an exception will be thrown.
239
+ retry_delay_s : float
240
+ Delay between status checks. Default: 1s.
241
+ on_each_refresh : Optional[Callable[[int, float, Task], None]]
242
+ Optional call back you can get after each refresh is made, including success state refreshes.
243
+ The signature represents: (refresh #, total elapsed time, task)
244
+
245
+ WARNING: Do not pass a long-running function to this variable. It will block the update polling.
246
+ """
247
+ t0 = time.perf_counter()
248
+ refresh_count = 0
249
+ while time.perf_counter() - t0 < max_timeout_s and self.state not in (
250
+ TaskState.succeeded,
251
+ TaskState.failed,
252
+ ):
253
+ time.sleep(retry_delay_s)
254
+ self.refresh()
255
+ refresh_count += 1
256
+
257
+ # Possibly make a callback so the caller knows we've tried again
258
+ if on_each_refresh:
259
+ on_each_refresh(refresh_count, time.perf_counter() - t0, self)
260
+
261
+ # If the task did not complete within the timeout, throw an error
262
+ if self.state not in (TaskState.succeeded, TaskState.failed):
263
+ raise SteamshipError(
264
+ message=f"Task {self.task_id} did not complete within requested timeout of {max_timeout_s}s. The task is still running on the server. You can retrieve its status via Task.get() or try waiting again with wait()."
265
+ )
266
+
267
+ def refresh(self):
268
+ if self.task_id is None:
269
+ raise SteamshipError(message="Unable to refresh task because `task_id` is None")
270
+
271
+ req = TaskStatusRequest(taskId=self.task_id)
272
+ # TODO (enias): A status call can return both data and task
273
+ # In this case both task and data will include the output (one is string serialized, the other is parsed)
274
+ # Ideally task status only returns the status, not the full output object
275
+ resp = self.client.post("task/status", payload=req, expect=self.expect)
276
+ self.update(resp)
277
+
278
+
279
+ from .client import Client # noqa: E402
280
+
281
+ Task.update_forward_refs()
282
+ TaskComment.update_forward_refs()
steamship/base/utils.py ADDED
File without changes
steamship/cli/__init__.py ADDED
File without changes
steamship/cli/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (178 Bytes). View file
 
steamship/cli/__pycache__/cli.cpython-39.pyc ADDED
Binary file (5.05 kB). View file
 
steamship/cli/__pycache__/deploy.cpython-39.pyc ADDED
Binary file (9.14 kB). View file
 
steamship/cli/__pycache__/login.cpython-39.pyc ADDED
Binary file (1.77 kB). View file
 
steamship/cli/__pycache__/manifest_init_wizard.cpython-39.pyc ADDED
Binary file (2.73 kB). View file
 
steamship/cli/__pycache__/requirements_init_wizard.cpython-39.pyc ADDED
Binary file (984 Bytes). View file
 
steamship/cli/__pycache__/ship_spinner.cpython-39.pyc ADDED
Binary file (1.84 kB). View file
 
steamship/cli/cli.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import sys
4
+ import time
5
+ from os import path
6
+ from typing import Optional
7
+
8
+ import click
9
+
10
+ import steamship
11
+ from steamship import Steamship, SteamshipError
12
+ from steamship.base.configuration import Configuration
13
+ from steamship.cli.deploy import (
14
+ PackageDeployer,
15
+ PluginDeployer,
16
+ bundle_deployable,
17
+ update_config_template,
18
+ )
19
+ from steamship.cli.manifest_init_wizard import manifest_init_wizard
20
+ from steamship.cli.requirements_init_wizard import requirements_init_wizard
21
+ from steamship.cli.ship_spinner import ship_spinner
22
+ from steamship.data.manifest import DeployableType, Manifest
23
+ from steamship.data.user import User
24
+
25
+
26
+ @click.group()
27
+ def cli():
28
+ pass
29
+
30
+
31
+ def initialize(suppress_message: bool = False):
32
+ logging.root.setLevel(logging.FATAL)
33
+ if not suppress_message:
34
+ click.echo(f"Steamship PYTHON cli version {steamship.__version__}")
35
+
36
+
37
+ @click.command()
38
+ def login():
39
+ """Log in to Steamship, creating ~/.steamship.json"""
40
+ initialize()
41
+ click.echo("Logging into Steamship.")
42
+ if sys.argv[1] == "login":
43
+ if Configuration.default_config_file_has_api_key():
44
+ overwrite = click.confirm(
45
+ text="You already have an API key in your .steamship.json file. Do you want to remove it and login?",
46
+ default=False,
47
+ )
48
+ if not overwrite:
49
+ sys.exit(0)
50
+ Configuration.remove_api_key_from_default_config()
51
+
52
+ # Carry on with login
53
+ client = Steamship()
54
+ user = User.current(client)
55
+ click.secho(f"🚢🚢🚢 Hooray! You're logged in with user handle: {user.handle} 🚢🚢🚢", fg="green")
56
+
57
+
58
+ @click.command()
59
+ def ships():
60
+ """Ship some ships"""
61
+ initialize()
62
+ click.secho("Here are some ships:", fg="cyan")
63
+ with ship_spinner():
64
+ time.sleep(5)
65
+ click.secho()
66
+
67
+
68
+ @click.command()
69
+ def deploy():
70
+ """Deploy the package or plugin in this directory"""
71
+ initialize()
72
+ client = None
73
+ try:
74
+ client = Steamship()
75
+ except SteamshipError as e:
76
+ click.secho(e.message, fg="red")
77
+ click.get_current_context().abort()
78
+
79
+ user = User.current(client)
80
+ if path.exists("steamship.json"):
81
+ manifest = Manifest.load_manifest()
82
+ else:
83
+ manifest = manifest_init_wizard(client)
84
+ manifest.save()
85
+
86
+ if not path.exists("requirements.txt"):
87
+ requirements_init_wizard()
88
+
89
+ deployable_type = manifest.type
90
+
91
+ update_config_template(manifest)
92
+
93
+ deployer = None
94
+ if deployable_type == DeployableType.PACKAGE:
95
+ deployer = PackageDeployer()
96
+ elif deployable_type == DeployableType.PLUGIN:
97
+ deployer = PluginDeployer()
98
+ else:
99
+ click.secho("Deployable must be of type package or plugin.", fg="red")
100
+ click.get_current_context().abort()
101
+
102
+ deployable = deployer.create_or_fetch_deployable(client, user, manifest)
103
+
104
+ click.echo("Bundling content... ", nl=False)
105
+ bundle_deployable(manifest)
106
+ click.echo("Done. 📦")
107
+
108
+ _ = deployer.create_version(client, manifest, deployable.id)
109
+
110
+ thing_url = f"{client.config.web_base}{deployable_type}s/{manifest.handle}"
111
+ click.echo(
112
+ f"Deployment was successful. View and share your new {deployable_type} here:\n\n{thing_url}\n"
113
+ )
114
+
115
+ # Common error conditions:
116
+ # - Package/plugin handle already taken. [handled; asks user for new]
117
+ # - Version handle already deployed. [handled; asks user for new]
118
+ # - Bad parameter configuration. [mitigated by deriving template from Config object]
119
+ # - Package content fails health check (ex. bad import) [Error caught while checking config object]
120
+
121
+
122
+ @click.command()
123
+ @click.option(
124
+ "--workspace",
125
+ "-w",
126
+ required=True,
127
+ type=str,
128
+ help="Workspace handle used for scoping logs request. All requests MUST be scoped by workspace.",
129
+ )
130
+ @click.option(
131
+ "--offset",
132
+ "-o",
133
+ default=0,
134
+ type=int,
135
+ help="Starting index from sorted logs to return a chunk. Used for paging. Defaults to 0.",
136
+ )
137
+ @click.option(
138
+ "--number",
139
+ "-n",
140
+ default=50,
141
+ type=int,
142
+ help="Number of logs to return in a single batch. Defaults to 50.",
143
+ )
144
+ @click.option(
145
+ "--package",
146
+ "-p",
147
+ type=str,
148
+ help="Package handle. Used to filter logs returend to a specific package (across all instances).",
149
+ )
150
+ @click.option(
151
+ "--instance",
152
+ "-i",
153
+ type=str,
154
+ help="Instance handle. Used to filter logs returned to a specific instance of a package.",
155
+ )
156
+ @click.option(
157
+ "--version",
158
+ "-v",
159
+ type=str,
160
+ help="Version handle. Used to filter logs returned to a specific version of a package.",
161
+ )
162
+ @click.option(
163
+ "--path",
164
+ "request_path",
165
+ type=str,
166
+ help="Path invoked by a client operation. Used to filter logs returned to a specific invocation path.",
167
+ )
168
+ def logs(
169
+ workspace: str,
170
+ offset: int,
171
+ number: int,
172
+ package: Optional[str] = None,
173
+ instance: Optional[str] = None,
174
+ version: Optional[str] = None,
175
+ request_path: Optional[str] = None,
176
+ ):
177
+ initialize(suppress_message=True)
178
+ client = None
179
+ try:
180
+ client = Steamship(workspace=workspace)
181
+ except SteamshipError as e:
182
+ raise click.UsageError(message=e.message)
183
+
184
+ click.echo(json.dumps(client.logs(offset, number, package, instance, version, request_path)))
185
+
186
+
187
+ cli.add_command(login)
188
+ cli.add_command(deploy)
189
+ cli.add_command(deploy, name="it")
190
+ cli.add_command(ships)
191
+ cli.add_command(logs)
192
+
193
+
194
+ if __name__ == "__main__":
195
+ deploy([])
steamship/cli/deploy.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib.machinery as machinery
2
+ import os
3
+ import sys
4
+ import traceback
5
+ import zipfile
6
+ from abc import ABC, abstractmethod
7
+ from pathlib import Path
8
+
9
+ import click
10
+ from semver import VersionInfo
11
+
12
+ from steamship import Package, PackageVersion, PluginVersion, Steamship, SteamshipError
13
+ from steamship.cli.manifest_init_wizard import validate_handle, validate_version_handle
14
+ from steamship.cli.ship_spinner import ship_spinner
15
+ from steamship.data import Plugin
16
+ from steamship.data.manifest import Manifest
17
+ from steamship.data.user import User
18
+ from steamship.invocable.lambda_handler import get_class_from_module
19
+
20
+ DEFAULT_BUILD_IGNORE = [
21
+ "build",
22
+ ".git",
23
+ ".venv",
24
+ ".ipynb_checkpoints",
25
+ ".DS_Store",
26
+ "venv",
27
+ "tests",
28
+ "examples",
29
+ ".idea",
30
+ "__pycache__",
31
+ ]
32
+
33
+
34
+ def update_config_template(manifest: Manifest):
35
+
36
+ path = Path("src/api.py")
37
+ if not path.exists():
38
+ path = Path("api.py")
39
+ if not path.exists():
40
+ raise SteamshipError("Could not find api.py either in root directory or in src.")
41
+
42
+ api_module = None
43
+ try:
44
+ sys.path.append(str(path.parent.absolute()))
45
+
46
+ # load the API module to allow config inspection / generation
47
+ api_module = machinery.SourceFileLoader("api", str(path)).load_module()
48
+ except Exception:
49
+ click.secho(
50
+ "An error occurred while loading your api.py to check configuration parameters. Full stack trace below.",
51
+ fg="red",
52
+ )
53
+ traceback.print_exc()
54
+ click.get_current_context().abort()
55
+
56
+ invocable_type = get_class_from_module(api_module)
57
+
58
+ config_parameters = invocable_type.config_cls().get_config_parameters()
59
+
60
+ if manifest.configTemplate != config_parameters:
61
+ if len(config_parameters) > 0:
62
+ click.secho("Config parameters changed; updating steamship.json.", fg="cyan")
63
+ for param_name, param in config_parameters.items():
64
+ click.echo(f"{param_name}:")
65
+ click.echo(f"\tType: {param.type}")
66
+ click.echo(f"\tDefault: {param.default}")
67
+ click.echo(f"\tDescription: {param.description}")
68
+ else:
69
+ click.secho("Config parameters removed; updating steamship.json.", fg="cyan")
70
+
71
+ manifest.configTemplate = config_parameters
72
+ manifest.save()
73
+
74
+
75
+ def get_archive_path(manifest: Manifest) -> Path:
76
+ return Path(".") / "build" / "archives" / f"{manifest.handle}_v{manifest.version}.zip"
77
+
78
+
79
+ def bundle_deployable(manifest: Manifest):
80
+ archive_path = get_archive_path(manifest)
81
+ archive_path.parent.mkdir(parents=True, exist_ok=True)
82
+ excludes = DEFAULT_BUILD_IGNORE + manifest.build_config.get("ignore", [])
83
+
84
+ archive_path.unlink(missing_ok=True)
85
+
86
+ # This zipfile packaging is modeled after the typescript CLI.
87
+ # Items in non-excluded root folders are moved to the top-level.
88
+
89
+ with zipfile.ZipFile(
90
+ file=archive_path, mode="a", compression=zipfile.ZIP_DEFLATED, allowZip64=False
91
+ ) as zip_file:
92
+
93
+ root = Path(".")
94
+ for file_path in root.iterdir():
95
+ if file_path.name not in excludes:
96
+ if file_path.is_dir():
97
+ for directory, _, files in os.walk(file_path):
98
+ subdirectory_path = Path(directory)
99
+ if Path(directory).name not in excludes:
100
+ for file in files:
101
+ pypi_file = subdirectory_path / file
102
+ relative_to = pypi_file.relative_to(file_path)
103
+ zip_file.write(pypi_file, relative_to)
104
+
105
+ else:
106
+ zip_file.write(file_path)
107
+
108
+
109
+ class DeployableDeployer(ABC):
110
+ @abstractmethod
111
+ def _create_version(self, client: Steamship, manifest: Manifest, thing_id: str):
112
+ pass
113
+
114
+ @abstractmethod
115
+ def create_object(self, client: Steamship, manifest: Manifest):
116
+ pass
117
+
118
+ @abstractmethod
119
+ def update_object(self, deployable, client: Steamship, manifest: Manifest):
120
+ pass
121
+
122
+ @abstractmethod
123
+ def deployable_type(self):
124
+ pass
125
+
126
+ def create_or_fetch_deployable(self, client: Steamship, user: User, manifest: Manifest):
127
+ if not manifest.handle or len(manifest.handle) == 0:
128
+ self.ask_for_new_handle(manifest, was_missing=True)
129
+
130
+ deployable = None
131
+ while deployable is None:
132
+ click.echo(
133
+ f"Creating / fetching {self.deployable_type()} with handle [{manifest.handle}]... ",
134
+ nl=False,
135
+ )
136
+ try:
137
+ deployable = self.create_object(client, manifest)
138
+ if deployable.user_id != user.id:
139
+ self.ask_for_new_handle(manifest)
140
+ deployable = None
141
+ except SteamshipError as e:
142
+ if e.message == "Something went wrong.":
143
+ self.ask_for_new_handle(manifest)
144
+ else:
145
+ click.secho(
146
+ f"Unable to create / fetch {self.deployable_type()}. Server returned message: {e.message}"
147
+ )
148
+ click.get_current_context().abort()
149
+
150
+ self.update_object(deployable, client, manifest)
151
+
152
+ click.echo("Done.")
153
+ return deployable
154
+
155
+ def ask_for_new_handle(self, manifest: Manifest, was_missing: bool = False):
156
+ if not was_missing:
157
+ try_again = click.confirm(
158
+ click.style(
159
+ f"\nIt looks like that handle [{manifest.handle}] is already in use. Would you like to change the handle and try again?",
160
+ fg="yellow",
161
+ ),
162
+ default=True,
163
+ )
164
+ if not try_again:
165
+ click.get_current_context().abort()
166
+
167
+ new_handle = click.prompt(
168
+ f"What handle would you like to use for your {self.deployable_type()}? Valid characters are a-z and -",
169
+ value_proc=validate_handle,
170
+ )
171
+ manifest.handle = new_handle
172
+ manifest.save()
173
+
174
+ def create_version(self, client: Steamship, manifest: Manifest, thing_id: str):
175
+ version = None
176
+
177
+ if not manifest.version or len(manifest.version) == 0:
178
+ self.ask_for_new_version_handle(manifest, was_missing=True)
179
+
180
+ while version is None:
181
+ click.echo(f"Deploying version {manifest.version} of [{manifest.handle}]... ", nl=False)
182
+ try:
183
+ with ship_spinner():
184
+ version = self._create_version(client, manifest, thing_id)
185
+ except SteamshipError as e:
186
+ if "The object you are trying to create already exists." in e.message:
187
+ self.ask_for_new_version_handle(manifest)
188
+ else:
189
+ click.secho(f"\nUnable to deploy {self.deployable_type()} version.", fg="red")
190
+ click.secho(f"Server returned message: {e.message}")
191
+ if "ModuleNotFoundError" in e.message:
192
+ click.secho(
193
+ "It looks like you may be missing a dependency in your requirements.txt.",
194
+ fg="yellow",
195
+ )
196
+ click.get_current_context().abort()
197
+ click.echo("\nDone. 🚢")
198
+
199
+ def ask_for_new_version_handle(self, manifest: Manifest, was_missing: bool = False):
200
+ if not was_missing:
201
+ try_again = click.confirm(
202
+ click.style(
203
+ f"\nIt looks like that version [{manifest.version}] has already been deployed. Would you like to change the version handle and try again?",
204
+ fg="yellow",
205
+ ),
206
+ default=True,
207
+ )
208
+ if not try_again:
209
+ click.get_current_context().abort()
210
+
211
+ default_new = "1.0.0"
212
+ try:
213
+ default_new = str(VersionInfo.parse(manifest.version).bump_prerelease())
214
+ except ValueError:
215
+ pass
216
+ old_archive_path = get_archive_path(manifest)
217
+ new_version_handle = click.prompt(
218
+ "What should the new version be? Valid characters are a-z, 0-9, . and -",
219
+ value_proc=validate_version_handle,
220
+ default=default_new,
221
+ )
222
+ manifest.version = new_version_handle
223
+ manifest.save()
224
+ new_archive_path = get_archive_path(manifest)
225
+ old_archive_path.rename(new_archive_path)
226
+
227
+
228
+ class PackageDeployer(DeployableDeployer):
229
+ def _create_version(self, client: Steamship, manifest: Manifest, thing_id: str):
230
+ return PackageVersion.create(
231
+ client=client,
232
+ config_template=manifest.config_template_as_dict(),
233
+ handle=manifest.version,
234
+ filename=f"build/archives/{manifest.handle}_v{manifest.version}.zip",
235
+ package_id=thing_id,
236
+ )
237
+
238
+ def create_object(self, client: Steamship, manifest: Manifest):
239
+ return Package.create(
240
+ client,
241
+ handle=manifest.handle,
242
+ profile=manifest,
243
+ is_public=manifest.public,
244
+ fetch_if_exists=True,
245
+ )
246
+
247
+ def update_object(self, deployable, client: Steamship, manifest: Manifest):
248
+ deployable.profile = manifest
249
+
250
+ package = deployable.update(client)
251
+ return package
252
+
253
+ def deployable_type(self):
254
+ return "package"
255
+
256
+
257
+ class PluginDeployer(DeployableDeployer):
258
+ def _create_version(self, client: Steamship, manifest: Manifest, thing_id: str):
259
+ return PluginVersion.create(
260
+ client=client,
261
+ config_template=manifest.config_template_as_dict(),
262
+ handle=manifest.version,
263
+ filename=f"build/archives/{manifest.handle}_v{manifest.version}.zip",
264
+ plugin_id=thing_id,
265
+ )
266
+
267
+ def create_object(self, client: Steamship, manifest: Manifest):
268
+ return Plugin.create(
269
+ client,
270
+ description=manifest.description,
271
+ is_public=manifest.public,
272
+ transport=manifest.plugin.transport,
273
+ type_=manifest.plugin.type,
274
+ handle=manifest.handle,
275
+ fetch_if_exists=True,
276
+ )
277
+
278
+ def update_object(self, deployable, client: Steamship, manifest: Manifest):
279
+ deployable.profile = manifest
280
+
281
+ plugin = deployable.update(client)
282
+ return plugin
283
+
284
+ def deployable_type(self):
285
+ return "plugin"
steamship/cli/login.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import webbrowser
3
+
4
+ import requests
5
+
6
+ from steamship.base.error import SteamshipError
7
+
8
+
9
+ def login(api_base: str, web_base: str) -> str: # noqa: C901
10
+
11
+ # create login token
12
+ try:
13
+ token_result = requests.post(api_base + "account/create_login_attempt")
14
+ token_data = token_result.json().get("data")
15
+ except Exception as e:
16
+ raise SteamshipError("Could not create login token when attempting login.", error=e)
17
+
18
+ if token_data is None:
19
+ raise SteamshipError("Could not create login token when attempting login.")
20
+ token = token_data.get("token")
21
+ if token is None:
22
+ raise SteamshipError("Could not create login token when attempting login.")
23
+
24
+ # Launch login attempt in browser
25
+ login_browser_url = (
26
+ f"{web_base}account/client-login?attemptToken={token}&client=pycli&version=0.0.1"
27
+ )
28
+ try:
29
+ opened_browser = webbrowser.open(login_browser_url)
30
+ except Exception as e:
31
+ raise SteamshipError("Exception attempting to launch browser for login.", error=e)
32
+
33
+ if not opened_browser:
34
+ raise SteamshipError(
35
+ """Could not launch browser to log in to Steamship.
36
+
37
+ If you are in Replit:
38
+
39
+ 1) Get an API key at https://steamship.com/account/api
40
+ 2) Set the STEAMSHIP_API_KEY Replit Secret
41
+ 3) Close and reopen this shell so that secrets refresh
42
+
43
+ If you are in a different headless environment, visit https://docs.steamship.com/configuration/authentication.html"""
44
+ )
45
+
46
+ # Wait on result
47
+ total_poll_time_s = 0
48
+ time_between_polls_s = 1
49
+ api_key = None
50
+ while total_poll_time_s < 300: # Five minutes
51
+ params = {"token": token}
52
+ login_response = requests.post(f"{api_base}account/poll_login_attempt", json=params).json()
53
+ if login_response.get("data", {}).get("status") == "done":
54
+ api_key = login_response.get("data", {}).get("apiKey")
55
+ break
56
+ time.sleep(time_between_polls_s)
57
+ total_poll_time_s += time_between_polls_s
58
+
59
+ if api_key is None:
60
+ raise SteamshipError("Could not fetch api key after login attempt in allotted time.")
61
+
62
+ return api_key
steamship/cli/manifest_init_wizard.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ import click
4
+ from click import BadParameter
5
+
6
+ from steamship import Steamship
7
+ from steamship.data.manifest import Manifest, PluginConfig, SteamshipRegistry
8
+ from steamship.data.user import User
9
+
10
+
11
+ def validate_handle(handle: str) -> str:
12
+ if re.fullmatch(r"[a-z\-]+", handle) is not None:
13
+ return handle
14
+ else:
15
+ raise BadParameter("Handle must only include lowercase letters and -")
16
+
17
+
18
+ def validate_version_handle(handle: str) -> str:
19
+ if re.fullmatch(r"[a-z0-9\-.]+", handle) is not None:
20
+ return handle
21
+ else:
22
+ raise BadParameter("Handle must only include lowercase letters, numbers, . and -")
23
+
24
+
25
+ def manifest_init_wizard(client: Steamship):
26
+ click.secho(
27
+ "It looks like you don't yet have a steamship.json to deploy. Let's create one.",
28
+ fg="cyan",
29
+ )
30
+
31
+ deployable_type = click.prompt(
32
+ "Is this a package or a plugin?",
33
+ default="package",
34
+ type=click.Choice(["package", "plugin"]),
35
+ show_choices=False,
36
+ )
37
+
38
+ handle = click.prompt(
39
+ f"What handle would you like to use for your {deployable_type}? Valid characters are a-z and -",
40
+ value_proc=validate_handle,
41
+ )
42
+
43
+ # TODO: claim the handle right here!
44
+
45
+ version_handle = "0.0.1"
46
+
47
+ plugin_detail = None
48
+ if deployable_type == "plugin":
49
+ plugin_type = click.prompt(
50
+ "What type of plugin is this?",
51
+ default="tagger",
52
+ type=click.Choice(
53
+ ["tagger", "blockifier", "exporter", "fileImporter", "corpusImporter", "generator"]
54
+ ),
55
+ show_choices=True,
56
+ )
57
+ if plugin_type == "tagger":
58
+ trainable = click.confirm("Is the plugin trainable?", default=False)
59
+ else:
60
+ trainable = False
61
+ plugin_detail = PluginConfig(isTrainable=trainable, type=plugin_type)
62
+
63
+ public = click.confirm(f"Do you want this {deployable_type} to be public?", default=True)
64
+
65
+ user = User.current(client)
66
+
67
+ author = click.prompt("How should we list your author name?", default=user.handle)
68
+
69
+ tagline = None
70
+ author_github = None
71
+ if public:
72
+ tagline = click.prompt(f"Want to give the {deployable_type} a tagline?", default="")
73
+ author_github = click.prompt(
74
+ "If you'd like this associated with your github account, please your github username",
75
+ default="",
76
+ )
77
+
78
+ tag_string = click.prompt(
79
+ f"Want to give the {deployable_type} some tags? (comma separated)", default="Prompt API"
80
+ )
81
+ tags = [tag.strip() for tag in tag_string.split(",")]
82
+
83
+ return Manifest(
84
+ type=deployable_type,
85
+ handle=handle,
86
+ version=version_handle,
87
+ description="",
88
+ author=author,
89
+ public=public,
90
+ plugin=plugin_detail,
91
+ build_config={"ignore": ["tests", "examples"]},
92
+ configTemplate={},
93
+ steamshipRegistry=SteamshipRegistry(
94
+ tagline=tagline, authorGithub=author_github, authorName=author, tags=tags
95
+ ),
96
+ )
steamship/cli/requirements_init_wizard.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import click
2
+
3
+ import steamship
4
+
5
+
6
+ def requirements_init_wizard():
7
+ click.secho(
8
+ "Steamship uses requirements.txt to specify dependencies. You do not currently have a requirements.txt in this directory.",
9
+ fg="yellow",
10
+ )
11
+ if not click.confirm("Would you like to create one automatically?", default=True):
12
+ click.secho("Please manually create a requirements.txt and try again.")
13
+ click.get_current_context().abort()
14
+
15
+ with open("requirements.txt", "w") as requirements_file:
16
+ requirements_file.write(f"steamship=={steamship.__version__}\n")
17
+
18
+ click.secho(
19
+ "Created a requirements.txt with the steamship dependency. If you need others, they must be added manually."
20
+ )
steamship/cli/ship_spinner.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import itertools
2
+ import threading
3
+
4
+ import click
5
+
6
+
7
+ class Spinner(object):
8
+ # [" 🚢", " 🚢 ", " 🚢 ", "🚢 "]
9
+ # Unfortunately, backspacing doesn't seem to work correctly for emoji in iTerm, so leaving the "spinner"
10
+ # as adding ships for now
11
+ spinner_cycle = itertools.cycle(["🚢"])
12
+
13
+ def __init__(self):
14
+ self.stop_running = None
15
+ self.spin_thread = None
16
+
17
+ def start(self):
18
+ self.stop_running = threading.Event()
19
+ self.spin_thread = threading.Thread(target=self.init_spin)
20
+ self.spin_thread.start()
21
+
22
+ def stop(self):
23
+ if self.spin_thread:
24
+ self.stop_running.set()
25
+ self.spin_thread.join()
26
+
27
+ def init_spin(self):
28
+ while not self.stop_running.is_set():
29
+ click.echo(next(self.spinner_cycle), nl=False)
30
+ self.stop_running.wait(1)
31
+ # click.echo("\b", nl=False)
32
+
33
+ def __enter__(self):
34
+ self.start()
35
+ return self
36
+
37
+ def __exit__(self, exc_type, exc_val, exc_tb):
38
+ self.stop()
39
+ return False
40
+
41
+
42
+ def ship_spinner():
43
+ """This function creates a context manager that is used to display a
44
+ spinner on stdout as long as the context has not exited.
45
+ The spinner is created only if stdout is not redirected, or if the spinner
46
+ is forced using the `force` parameter.
47
+ """
48
+ return Spinner()
steamship/client/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .steamship import Steamship
2
+
3
+ __all__ = ["Steamship"]
steamship/client/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (246 Bytes). View file
 
steamship/client/__pycache__/skill_to_provider.cpython-39.pyc ADDED
Binary file (1.18 kB). View file
 
steamship/client/__pycache__/skills.cpython-39.pyc ADDED
Binary file (555 Bytes). View file
 
steamship/client/__pycache__/steamship.cpython-39.pyc ADDED
Binary file (8.95 kB). View file
 
steamship/client/__pycache__/vendors.cpython-39.pyc ADDED
Binary file (394 Bytes). View file
 
steamship/client/skill_to_provider.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from steamship.client.skills import Skill
6
+ from steamship.client.vendors import Vendor
7
+
8
+
9
+ class SkillVendorConfig(BaseModel):
10
+ plugin_handle: str
11
+ config: Dict[str, Any]
12
+
13
+
14
+ SKILL_TO_PROVIDER: Dict[Skill, Dict[Vendor, SkillVendorConfig]] = {
15
+ Skill.ENTITIES: {
16
+ Vendor.OneAI: SkillVendorConfig(
17
+ plugin_handle="oneai-tagger",
18
+ config={"skills": ["names", "numbers", "business-entities"]},
19
+ )
20
+ },
21
+ Skill.SUMMARY: {
22
+ Vendor.OneAI: SkillVendorConfig(
23
+ plugin_handle="oneai-tagger", config={"skills": ["summarize"]}
24
+ )
25
+ },
26
+ Skill.SENTIMENTS: {
27
+ Vendor.OneAI: SkillVendorConfig(
28
+ plugin_handle="oneai-tagger", config={"skills": ["sentiments"]}
29
+ )
30
+ },
31
+ Skill.EMOTIONS: {
32
+ Vendor.OneAI: SkillVendorConfig(
33
+ plugin_handle="oneai-tagger", config={"skills": ["emotions"]}
34
+ )
35
+ },
36
+ Skill.TOPICS: {
37
+ Vendor.OneAI: SkillVendorConfig(
38
+ plugin_handle="oneai-tagger", config={"skills": ["article-topics"]}
39
+ ),
40
+ },
41
+ Skill.HIGHLIGHTS: {
42
+ Vendor.OneAI: SkillVendorConfig(
43
+ plugin_handle="oneai-tagger", config={"skills": ["highlights"]}
44
+ )
45
+ },
46
+ Skill.KEYWORDS: {
47
+ Vendor.OneAI: SkillVendorConfig(
48
+ plugin_handle="oneai-tagger", config={"skills": ["keywords"]}
49
+ )
50
+ },
51
+ }
steamship/client/skills.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class Skill(str, Enum):
5
+ ENTITIES = "entities"
6
+ SUMMARY = "summary"
7
+ SENTIMENTS = "sentiments"
8
+ EMOTIONS = "emotions"
9
+ TOPICS = "topics"
10
+ HIGHLIGHTS = "highlights"
11
+ KEYWORDS = "keywords"
steamship/client/steamship.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import uuid
5
+ from contextlib import contextmanager
6
+ from typing import Any, Dict, Generator, List, Optional
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from steamship.base.client import Client
11
+ from steamship.base.configuration import Configuration
12
+ from steamship.base.error import SteamshipError
13
+ from steamship.client.skill_to_provider import SKILL_TO_PROVIDER
14
+ from steamship.client.skills import Skill
15
+ from steamship.client.vendors import Vendor
16
+ from steamship.data.embeddings import EmbedAndSearchRequest, QueryResults
17
+ from steamship.data.package.package_instance import PackageInstance
18
+ from steamship.data.plugin.index_plugin_instance import EmbeddingIndexPluginInstance
19
+ from steamship.data.plugin.plugin_instance import PluginInstance
20
+ from steamship.data.plugin.prompt_generation_plugin_instance import PromptGenerationPluginInstance
21
+ from steamship.data.workspace import Workspace
22
+ from steamship.utils.metadata import hash_dict
23
+
24
+ _logger = logging.getLogger(__name__)
25
+
26
+
27
+ class Steamship(Client):
28
+ """Steamship Python Client."""
29
+
30
+ # Some plugin instances use special subclasses which provide helper methods and/or more complex
31
+ # behavior than typical PluginInstance subclass permits. Examples are:
32
+ #
33
+ # - Embedding indices (which much coordinate both embedding taggers & vector indices)
34
+ # - Prompt generators (which benefit from supporting, prompt-specific, methods)
35
+ _PLUGIN_INSTANCE_SUBCLASS_OVERRIDES = {
36
+ "prompt-generation-default": PromptGenerationPluginInstance,
37
+ "prompt-generation-trainable-default": PromptGenerationPluginInstance,
38
+ "gpt3": PromptGenerationPluginInstance,
39
+ "gpt-3": PromptGenerationPluginInstance,
40
+ "cerebrium": PromptGenerationPluginInstance,
41
+ "embedding-index": EmbeddingIndexPluginInstance,
42
+ }
43
+
44
+ def __init__(
45
+ self,
46
+ api_key: str = None,
47
+ api_base: str = None,
48
+ app_base: str = None,
49
+ web_base: str = None,
50
+ workspace: str = None,
51
+ fail_if_workspace_exists: bool = False,
52
+ profile: str = None,
53
+ config_file: str = None,
54
+ config: Configuration = None,
55
+ trust_workspace_config: bool = False, # For use by lambda_handler; don't fetch the workspace
56
+ **kwargs,
57
+ ):
58
+ super().__init__(
59
+ api_key=api_key,
60
+ api_base=api_base,
61
+ app_base=app_base,
62
+ web_base=web_base,
63
+ workspace=workspace,
64
+ fail_if_workspace_exists=fail_if_workspace_exists,
65
+ profile=profile,
66
+ config_file=config_file,
67
+ config=config,
68
+ trust_workspace_config=trust_workspace_config,
69
+ **kwargs,
70
+ )
71
+ # We use object.__setattr__ here in order to bypass Pydantic's overloading of it (which would block this
72
+ # set unless we were to add this as a field)
73
+ object.__setattr__(self, "use", self._instance_use)
74
+ object.__setattr__(self, "use_plugin", self._instance_use_plugin)
75
+
76
+ def __repr_args__(self: BaseModel) -> Any:
77
+ """Because of the trick we've done with `use` and `use_plugin`, we need to exclude these from __repr__
78
+ otherwise we'll get an infinite recursion."""
79
+ return [
80
+ (key, value)
81
+ for key, value in self.__dict__.items()
82
+ if key != "use" and key != "use_plugin"
83
+ ]
84
+
85
+ def embed_and_search(
86
+ self,
87
+ query: str,
88
+ docs: List[str],
89
+ plugin_instance: str,
90
+ k: int = 1,
91
+ ) -> QueryResults:
92
+ req = EmbedAndSearchRequest(query=query, docs=docs, plugin_instance=plugin_instance, k=k)
93
+ return self.post(
94
+ "plugin/instance/embeddingSearch",
95
+ req,
96
+ expect=QueryResults,
97
+ )
98
+
99
+ @staticmethod
100
+ @contextmanager
101
+ def temporary_workspace(**kwargs) -> Generator["Steamship", None, None]:
102
+ """Create a client rooted in a temporary workspace that will be deleted after use."""
103
+ # Create a new client and switch to a temporary workspace
104
+ client = Steamship(**kwargs)
105
+ temporary_handle = "temp-" + str(uuid.uuid4())
106
+ client.switch_workspace(temporary_handle)
107
+
108
+ # Safety check that we are now working form the new workspace.
109
+ if client.config.workspace_handle != temporary_handle:
110
+ raise SteamshipError(
111
+ message=f"Attempted to switch to temporary workspace {temporary_handle} but the client claimed to be working from {client.config.workspace_handle}"
112
+ )
113
+
114
+ yield client
115
+
116
+ # Safely delete the temporary workspace. Here we re-fetch the workspace using the temporary_handle
117
+ # in case the user switched workspaces yet again upon the client.
118
+ workspace = Workspace.get(client, handle=temporary_handle)
119
+ if workspace.handle != temporary_handle:
120
+ raise SteamshipError(
121
+ message=f"Was about to delete temporary workspace {temporary_handle} but its handle is different: {workspace.handle}"
122
+ )
123
+ else:
124
+ workspace.delete()
125
+
126
+ @staticmethod
127
+ def use(
128
+ package_handle: str,
129
+ instance_handle: Optional[str] = None,
130
+ config: Optional[Dict[str, Any]] = None,
131
+ version: Optional[str] = None,
132
+ fetch_if_exists: bool = True,
133
+ workspace_handle: Optional[str] = None,
134
+ **kwargs,
135
+ ) -> PackageInstance:
136
+ """Creates/loads an instance of package `package_handle`.
137
+
138
+ The instance is named `instance_handle` and located in the Workspace named `instance_handle`. If no
139
+ `instance_handle` is provided, the default is `package_handle`.
140
+
141
+ For example, one may write the following to always get back the same package instance, no matter how many
142
+ times you run it, scoped into its own workspace:
143
+
144
+ ```python
145
+ instance = Steamship.use('package-handle', 'instance-handle')
146
+ ```
147
+
148
+ One may also write:
149
+
150
+ ```python
151
+ instance = Steamship.use('package-handle') # Instance will also be named `package-handle`
152
+ ```
153
+
154
+ If you wish to override the usage of a workspace named `instance_handle`, you can provide the `workspace_handle`
155
+ parameter.
156
+ """
157
+ if instance_handle is None:
158
+ instance_handle = package_handle
159
+ kwargs["workspace"] = workspace_handle or instance_handle
160
+ client = Steamship(**kwargs)
161
+ return client._instance_use(
162
+ package_handle=package_handle,
163
+ instance_handle=instance_handle,
164
+ config=config,
165
+ version=version,
166
+ fetch_if_exists=fetch_if_exists,
167
+ )
168
+
169
+ def _instance_use(
170
+ self,
171
+ package_handle: str,
172
+ instance_handle: Optional[str] = None,
173
+ config: Optional[Dict[str, Any]] = None,
174
+ version: Optional[str] = None,
175
+ fetch_if_exists: bool = True,
176
+ ) -> PackageInstance:
177
+ """Creates/loads an instance of package `package_handle`.
178
+
179
+ The instance is named `instance_handle` and located in the workspace this client is anchored to.
180
+ If no `instance_handle` is provided, the default is `package_handle`.
181
+ """
182
+
183
+ if instance_handle is None:
184
+ if config is None:
185
+ instance_handle = package_handle
186
+ else:
187
+ instance_handle = f"{package_handle}-{hash_dict(config)}"
188
+
189
+ return PackageInstance.create(
190
+ self,
191
+ package_handle=package_handle,
192
+ package_version_handle=version,
193
+ handle=instance_handle,
194
+ config=config,
195
+ fetch_if_exists=fetch_if_exists,
196
+ )
197
+
198
+ @staticmethod
199
+ def use_plugin(
200
+ plugin_handle: str,
201
+ instance_handle: Optional[str] = None,
202
+ config: Optional[Dict[str, Any]] = None,
203
+ version: Optional[str] = None,
204
+ fetch_if_exists: bool = True,
205
+ workspace_handle: Optional[str] = None,
206
+ **kwargs,
207
+ ) -> PluginInstance:
208
+ """Creates/loads an instance of plugin `plugin_handle`.
209
+
210
+ The instance is named `instance_handle` and located in the Workspace named `instance_handle`.
211
+ If no `instance_handle` is provided, the default is `plugin_handle`.
212
+
213
+ For example, one may write the following to always get back the same plugin instance, no matter how many
214
+ times you run it, scoped into its own workspace:
215
+
216
+ ```python
217
+ instance = Steamship.use_plugin('plugin-handle', 'instance-handle')
218
+ ```
219
+
220
+ One may also write:
221
+
222
+ ```python
223
+ instance = Steamship.use('plugin-handle') # Instance will also be named `plugin-handle`
224
+ ```
225
+ """
226
+ if instance_handle is None:
227
+ instance_handle = plugin_handle
228
+ kwargs["workspace"] = workspace_handle or instance_handle
229
+ client = Steamship(**kwargs)
230
+ return client._instance_use_plugin(
231
+ plugin_handle=plugin_handle,
232
+ instance_handle=instance_handle,
233
+ config=config,
234
+ version=version,
235
+ fetch_if_exists=fetch_if_exists,
236
+ )
237
+
238
+ def use_skill(
239
+ self,
240
+ skill: Skill,
241
+ provider: Optional[Vendor] = None,
242
+ instance_handle: Optional[str] = None,
243
+ fetch_if_exists: Optional[bool] = True,
244
+ ) -> PluginInstance:
245
+
246
+ if skill not in SKILL_TO_PROVIDER:
247
+ raise SteamshipError(
248
+ f"Unsupported skill provided. "
249
+ f"Use one of our supported skills: {','.join(SKILL_TO_PROVIDER)}"
250
+ )
251
+
252
+ if provider and provider not in SKILL_TO_PROVIDER[skill]:
253
+ raise SteamshipError(
254
+ f"The provider {provider} has no support for the skill {skill}."
255
+ f"Use one of the providers that support your skill: "
256
+ f"{','.join(SKILL_TO_PROVIDER[skill])}"
257
+ )
258
+
259
+ plugin_setup = (
260
+ SKILL_TO_PROVIDER[skill][provider]
261
+ if provider
262
+ else list(SKILL_TO_PROVIDER[skill].values())[0]
263
+ )
264
+ return self._instance_use_plugin(
265
+ plugin_handle=plugin_setup["plugin_handle"],
266
+ instance_handle=instance_handle,
267
+ config=plugin_setup["config"],
268
+ fetch_if_exists=fetch_if_exists,
269
+ )
270
+
271
+ def _instance_use_plugin(
272
+ self,
273
+ plugin_handle: str,
274
+ instance_handle: Optional[str] = None,
275
+ config: Optional[Dict[str, Any]] = None,
276
+ version: Optional[str] = None,
277
+ fetch_if_exists: Optional[bool] = True,
278
+ ) -> PluginInstance:
279
+ """Creates/loads an instance of plugin `plugin_handle`.
280
+
281
+ The instance is named `instance_handle` and located in the workspace this client is anchored to.
282
+ If no `instance_handle` is provided, the default is `plugin_handle`.
283
+ """
284
+
285
+ if instance_handle is None:
286
+ if config is None:
287
+ instance_handle = plugin_handle
288
+ else:
289
+ instance_handle = f"{plugin_handle}-{hash_dict(config)}"
290
+
291
+ if plugin_handle in Steamship._PLUGIN_INSTANCE_SUBCLASS_OVERRIDES:
292
+ return Steamship._PLUGIN_INSTANCE_SUBCLASS_OVERRIDES[plugin_handle].create(
293
+ self,
294
+ plugin_handle=plugin_handle,
295
+ plugin_version_handle=version,
296
+ handle=instance_handle,
297
+ config=config,
298
+ fetch_if_exists=fetch_if_exists,
299
+ )
300
+
301
+ return PluginInstance.create(
302
+ self,
303
+ plugin_handle=plugin_handle,
304
+ plugin_version_handle=version,
305
+ handle=instance_handle,
306
+ config=config,
307
+ fetch_if_exists=fetch_if_exists,
308
+ )
309
+
310
+ def get_workspace(self) -> Workspace:
311
+ # We should probably add a hard-coded way to get this. The client in a Steamship Plugin/App comes
312
+ # pre-configured with an API key and the Workspace in which this client should be operating.
313
+ # This is a way to load the model object for that workspace.
314
+ logging.info(
315
+ f"get_workspace() called on client with config workspace {self.config.workspace_handle}/{self.config.workspace_id}"
316
+ )
317
+ workspace = Workspace.get(
318
+ self, id_=self.config.workspace_id, handle=self.config.workspace_handle
319
+ )
320
+ if not workspace:
321
+ logging.error("Unable to get workspace.")
322
+ raise SteamshipError(
323
+ message="Error while retrieving the Workspace associated with this client config.",
324
+ internal_message=f"workspace_id={self.config.workspace_id} workspace_handle={self.config.workspace_handle}",
325
+ )
326
+ logging.info(f"Got workspace: {workspace.handle}/{workspace.id}")
327
+ return workspace