Skip to content

Commit 5aac1ec

Browse files
feat: Implement SeamMultiWorkspace client (#44)
* Bump generator to 1.10.1 * ci: Generate code * Fix tests * Bump generator to 1.10.2 * ci: Generate code * Remove unnecessary import * Bump generator to 1.10.3 to fix circular imports with a separate file options.py * ci: Generate code * Add SeamMultiWorkspace * Add constants.py * Add get_endpoint helper fn * Add RequestMixin to define common make_request method * ci: Format code * Disable generate workflow * Add abstract classes, add helper for getting PAT, fix create ws test * Enforce keyword only args in WorkspacesProxy methods * Define WorkspacesProxy outside SeamMultiWorkspace * Fix proxy class usage * Enable generation workflow --------- Co-authored-by: Seam Bot <devops@getseam.com>
1 parent c8669c4 commit 5aac1ec

File tree

11 files changed

+240
-70
lines changed

11 files changed

+240
-70
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,7 @@ dist
321321
.yarn/build-state.yml
322322
.yarn/install-state.gz
323323
.pnp.*
324+
325+
# yalc
326+
.yalc/
327+
yalc.lock

seam/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# flake8: noqa
22
# type: ignore
33

4-
from seam.seam import Seam, SeamApiException
4+
from seam.seam import Seam
5+
from seam.types import SeamApiException
6+
from seam.seam_multi_workspace import SeamMultiWorkspace
57
from seam.options import SeamHttpInvalidOptionsError
68
from seam.auth import SeamHttpInvalidTokenError
79
from seam.routes.action_attempts import (

seam/auth.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,31 @@ def get_auth_headers_for_personal_access_token(
9999
"authorization": f"Bearer {personal_access_token}",
100100
"seam-workspace": workspace_id,
101101
}
102+
103+
104+
def get_auth_headers_for_multi_workspace_personal_access_token(
105+
personal_access_token: str,
106+
) -> dict:
107+
if is_jwt(personal_access_token):
108+
raise SeamHttpInvalidTokenError(
109+
"A JWT cannot be used as a personal_access_token"
110+
)
111+
112+
if is_client_session_token(personal_access_token):
113+
raise SeamHttpInvalidTokenError(
114+
"A Client Session Token cannot be used as a personal_access_token"
115+
)
116+
117+
if is_publishable_key(personal_access_token):
118+
raise SeamHttpInvalidTokenError(
119+
"A Publishable Key cannot be used as a personal_access_token"
120+
)
121+
122+
if not is_access_token(personal_access_token):
123+
raise SeamHttpInvalidTokenError(
124+
f"Unknown or invalid personal_access_token format, expected token to start with {ACCESS_TOKEN_PREFIX}"
125+
)
126+
127+
return {
128+
"authorization": f"Bearer {personal_access_token}",
129+
}

seam/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
LTS_VERSION = "1.0.0"
2+
3+
DEFAULT_ENDPOINT = "https://connect.getseam.com"

seam/options.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import os
22
from typing import Optional
33

4+
from seam.constants import DEFAULT_ENDPOINT
5+
6+
7+
def get_endpoint(endpoint: Optional[str] = None):
8+
return endpoint or get_endpoint_from_env() or DEFAULT_ENDPOINT
9+
410

511
def get_endpoint_from_env():
612
seam_api_url = os.getenv("SEAM_API_URL")

seam/parse_options.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
from typing import Optional
33

44
from seam.auth import get_auth_headers
5-
from seam.options import get_endpoint_from_env
6-
7-
DEFAULT_ENDPOINT = "https://connect.getseam.com"
5+
from seam.options import get_endpoint
86

97

108
def parse_options(
@@ -21,6 +19,6 @@ def parse_options(
2119
personal_access_token=personal_access_token,
2220
workspace_id=workspace_id,
2321
)
24-
endpoint = endpoint or get_endpoint_from_env() or DEFAULT_ENDPOINT
22+
endpoint = get_endpoint(endpoint)
2523

2624
return auth_headers, endpoint

seam/request.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import requests
2+
from importlib.metadata import version
3+
4+
from seam.types import AbstractRequestMixin, SeamApiException
5+
6+
7+
class RequestMixin(AbstractRequestMixin):
8+
def make_request(self, method: str, path: str, **kwargs):
9+
"""
10+
Makes a request to the API
11+
12+
Parameters
13+
----------
14+
method : str
15+
Request method
16+
path : str
17+
Request path
18+
**kwargs
19+
Keyword arguments passed to requests.request
20+
21+
Raises
22+
------
23+
SeamApiException: If the response status code is not successful.
24+
"""
25+
26+
url = self._endpoint + path
27+
sdk_version = version("seam")
28+
headers = {
29+
**self._auth_headers,
30+
"Content-Type": "application/json",
31+
"User-Agent": "Python SDK v"
32+
+ sdk_version
33+
+ " (https://github.com/seamapi/python-next)",
34+
"seam-sdk-name": "seamapi/python",
35+
"seam-sdk-version": sdk_version,
36+
"seam-lts-version": self.lts_version,
37+
}
38+
39+
response = requests.request(method, url, headers=headers, **kwargs)
40+
41+
if response.status_code != 200:
42+
raise SeamApiException(response)
43+
44+
if "application/json" in response.headers["content-type"]:
45+
return response.json()
46+
47+
return response.text

seam/seam.py

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import requests
2-
from importlib.metadata import version
31
from typing import Optional, Union, Dict
42
from typing_extensions import Self
53

4+
from .constants import LTS_VERSION
65
from .parse_options import parse_options
6+
from .request import RequestMixin
77
from .routes.routes import Routes
8-
from .types import AbstractSeam, SeamApiException
8+
from .types import AbstractSeam
99

1010

11-
class Seam(AbstractSeam):
11+
class Seam(AbstractSeam, RequestMixin):
1212
"""
1313
Initial Seam class used to interact with Seam API
1414
"""
1515

16-
lts_version: str = "1.0.0"
16+
lts_version: str = LTS_VERSION
1717

1818
def __init__(
1919
self,
@@ -49,45 +49,8 @@ def __init__(
4949
workspace_id=workspace_id,
5050
endpoint=endpoint,
5151
)
52-
self.__auth_headers = auth_headers
53-
self.__endpoint = endpoint
54-
55-
def make_request(self, method: str, path: str, **kwargs):
56-
"""
57-
Makes a request to the API
58-
59-
Parameters
60-
----------
61-
method : str
62-
Request method
63-
path : str
64-
Request path
65-
**kwargs
66-
Keyword arguments passed to requests.request
67-
"""
68-
69-
url = self.__endpoint + path
70-
sdk_version = version("seam")
71-
headers = {
72-
**self.__auth_headers,
73-
"Content-Type": "application/json",
74-
"User-Agent": "Python SDK v"
75-
+ sdk_version
76-
+ " (https://github.com/seamapi/python-next)",
77-
"seam-sdk-name": "seamapi/python",
78-
"seam-sdk-version": sdk_version,
79-
"seam-lts-version": self.lts_version,
80-
}
81-
82-
response = requests.request(method, url, headers=headers, **kwargs)
83-
84-
if response.status_code != 200:
85-
raise SeamApiException(response)
86-
87-
if "application/json" in response.headers["content-type"]:
88-
return response.json()
89-
90-
return response.text
52+
self._auth_headers = auth_headers
53+
self._endpoint = endpoint
9154

9255
@classmethod
9356
def from_api_key(

seam/seam_multi_workspace.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from typing import Dict, Optional, Union
2+
from typing_extensions import Self
3+
4+
from .auth import get_auth_headers_for_multi_workspace_personal_access_token
5+
from .constants import LTS_VERSION
6+
from .options import get_endpoint
7+
from .request import RequestMixin
8+
from .types import AbstractSeamMultiWorkspace
9+
from .routes.workspaces import Workspaces
10+
11+
12+
class WorkspacesProxy:
13+
"""Proxy to expose only the 'create' and 'list' methods of Workspaces."""
14+
15+
def __init__(self, workspaces):
16+
self._workspaces = workspaces
17+
18+
def list(self, **kwargs):
19+
return self._workspaces.list(**kwargs)
20+
21+
def create(self, **kwargs):
22+
return self._workspaces.create(**kwargs)
23+
24+
25+
class SeamMultiWorkspace(AbstractSeamMultiWorkspace, RequestMixin):
26+
"""
27+
Seam class used to interact with Seam API without being scoped to any specific workspace.
28+
"""
29+
30+
lts_version: str = LTS_VERSION
31+
32+
def __init__(
33+
self,
34+
personal_access_token: str,
35+
*,
36+
endpoint: Optional[str] = None,
37+
wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = False,
38+
):
39+
"""
40+
Parameters
41+
----------
42+
personal_access_token : str, optional
43+
Personal access token.
44+
endpoint : str, optional
45+
The API endpoint to which the request should be sent.
46+
wait_for_action_attempt : bool or dict, optional
47+
Controls whether to wait for an action attempt to complete, either as a boolean or as a dictionary specifying `timeout` and `poll_interval`. Defaults to `False`.
48+
"""
49+
50+
self.lts_version = SeamMultiWorkspace.lts_version
51+
self.wait_for_action_attempt = wait_for_action_attempt
52+
self._auth_headers = get_auth_headers_for_multi_workspace_personal_access_token(
53+
personal_access_token
54+
)
55+
self._endpoint = get_endpoint(endpoint)
56+
57+
self._workspaces = Workspaces(seam=self)
58+
self.workspaces = WorkspacesProxy(self._workspaces)
59+
60+
@classmethod
61+
def from_personal_access_token(
62+
cls,
63+
personal_access_token: str,
64+
*,
65+
endpoint: Optional[str] = None,
66+
wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = False,
67+
) -> Self:
68+
return cls(
69+
personal_access_token=personal_access_token,
70+
endpoint=endpoint,
71+
wait_for_action_attempt=wait_for_action_attempt,
72+
)

seam/types.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from typing import Any, Dict, Optional, Union
1+
from typing import Any, Dict, List, Optional, Union
22
from typing_extensions import Self
33
import abc
44

5-
from seam.routes.types import AbstractRoutes
5+
from seam.routes.types import AbstractRoutes, Workspace
66

77

88
class SeamApiException(Exception):
@@ -23,8 +23,15 @@ def __init__(
2323
)
2424

2525

26-
class AbstractSeam(AbstractRoutes):
26+
class AbstractRequestMixin(abc.ABC):
27+
@abc.abstractmethod
28+
def make_request(self, method: str, path: str, **kwargs):
29+
raise NotImplementedError
30+
31+
32+
class AbstractSeam(AbstractRoutes, AbstractRequestMixin):
2733
lts_version: str
34+
wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]]
2835

2936
@abc.abstractmethod
3037
def __init__(
@@ -36,11 +43,6 @@ def __init__(
3643
endpoint: Optional[str] = None,
3744
wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = False,
3845
):
39-
self.wait_for_action_attempt = wait_for_action_attempt
40-
self.lts_version = AbstractSeam.lts_version
41-
42-
@abc.abstractmethod
43-
def make_request(self, method: str, path: str, **kwargs) -> Any:
4446
raise NotImplementedError
4547

4648
@classmethod
@@ -65,3 +67,49 @@ def from_personal_access_token(
6567
wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = False,
6668
) -> Self:
6769
raise NotImplementedError
70+
71+
72+
class AbstractSeamMultiWorkspaceWorkspaces(abc.ABC):
73+
@abc.abstractmethod
74+
def create(
75+
self,
76+
*,
77+
connect_partner_name: str,
78+
name: str,
79+
is_sandbox: Optional[bool] = None,
80+
webview_logo_shape: Optional[str] = None,
81+
webview_primary_button_color: Optional[str] = None,
82+
) -> Workspace:
83+
raise NotImplementedError()
84+
85+
@abc.abstractmethod
86+
def list(
87+
self,
88+
) -> List[Workspace]:
89+
raise NotImplementedError()
90+
91+
92+
class AbstractSeamMultiWorkspace(AbstractRequestMixin):
93+
lts_version: str
94+
wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]]
95+
96+
@abc.abstractmethod
97+
def __init__(
98+
self,
99+
personal_access_token: str,
100+
*,
101+
endpoint: Optional[str] = None,
102+
wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = False,
103+
):
104+
raise NotImplementedError
105+
106+
@classmethod
107+
@abc.abstractmethod
108+
def from_personal_access_token(
109+
cls,
110+
personal_access_token: str,
111+
*,
112+
endpoint: Optional[str] = None,
113+
wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = False,
114+
) -> Self:
115+
raise NotImplementedError

0 commit comments

Comments
 (0)