Spaces:
Runtime error
Runtime error
Upload 195 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- steamship/.DS_Store +0 -0
- steamship/__init__.py +57 -0
- steamship/__pycache__/__init__.cpython-39.pyc +0 -0
- steamship/base/__init__.py +15 -0
- steamship/base/__pycache__/__init__.cpython-39.pyc +0 -0
- steamship/base/__pycache__/client.cpython-39.pyc +0 -0
- steamship/base/__pycache__/configuration.cpython-39.pyc +0 -0
- steamship/base/__pycache__/environments.cpython-39.pyc +0 -0
- steamship/base/__pycache__/error.cpython-39.pyc +0 -0
- steamship/base/__pycache__/mime_types.cpython-39.pyc +0 -0
- steamship/base/__pycache__/model.cpython-39.pyc +0 -0
- steamship/base/__pycache__/package_spec.cpython-39.pyc +0 -0
- steamship/base/__pycache__/request.cpython-39.pyc +0 -0
- steamship/base/__pycache__/response.cpython-39.pyc +0 -0
- steamship/base/__pycache__/tasks.cpython-39.pyc +0 -0
- steamship/base/__pycache__/utils.cpython-39.pyc +0 -0
- steamship/base/client.py +635 -0
- steamship/base/configuration.py +139 -0
- steamship/base/environments.py +99 -0
- steamship/base/error.py +72 -0
- steamship/base/mime_types.py +42 -0
- steamship/base/model.py +29 -0
- steamship/base/package_spec.py +150 -0
- steamship/base/request.py +33 -0
- steamship/base/response.py +5 -0
- steamship/base/tasks.py +282 -0
- steamship/base/utils.py +0 -0
- steamship/cli/__init__.py +0 -0
- steamship/cli/__pycache__/__init__.cpython-39.pyc +0 -0
- steamship/cli/__pycache__/cli.cpython-39.pyc +0 -0
- steamship/cli/__pycache__/deploy.cpython-39.pyc +0 -0
- steamship/cli/__pycache__/login.cpython-39.pyc +0 -0
- steamship/cli/__pycache__/manifest_init_wizard.cpython-39.pyc +0 -0
- steamship/cli/__pycache__/requirements_init_wizard.cpython-39.pyc +0 -0
- steamship/cli/__pycache__/ship_spinner.cpython-39.pyc +0 -0
- steamship/cli/cli.py +195 -0
- steamship/cli/deploy.py +285 -0
- steamship/cli/login.py +62 -0
- steamship/cli/manifest_init_wizard.py +96 -0
- steamship/cli/requirements_init_wizard.py +20 -0
- steamship/cli/ship_spinner.py +48 -0
- steamship/client/__init__.py +3 -0
- steamship/client/__pycache__/__init__.cpython-39.pyc +0 -0
- steamship/client/__pycache__/skill_to_provider.cpython-39.pyc +0 -0
- steamship/client/__pycache__/skills.cpython-39.pyc +0 -0
- steamship/client/__pycache__/steamship.cpython-39.pyc +0 -0
- steamship/client/__pycache__/vendors.cpython-39.pyc +0 -0
- steamship/client/skill_to_provider.py +51 -0
- steamship/client/skills.py +11 -0
- 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
|