Skip to content

Commit bbb23aa

Browse files
authored
Merge pull request SeequentEvo#97 from wordsworthc/extensible-oauth-scopes
Refactor oauth scopes
2 parents 250c251 + f673328 commit bbb23aa

18 files changed

Lines changed: 200 additions & 130 deletions

packages/evo-sdk-common/docs/examples/oauth.ipynb

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@
5656
"metadata": {},
5757
"outputs": [],
5858
"source": [
59-
"from evo.oauth import AuthorizationCodeAuthorizer, OAuthScopes\n",
59+
"from evo.oauth import AuthorizationCodeAuthorizer, EvoScopes\n",
6060
"\n",
6161
"authorizer = AuthorizationCodeAuthorizer(\n",
6262
" oauth_connector=connector,\n",
6363
" redirect_url=REDIRECT_URL,\n",
64-
" scopes=OAuthScopes.all_evo | OAuthScopes.offline_access,\n",
64+
" scopes=EvoScopes.all_evo | EvoScopes.offline_access,\n",
6565
")\n",
6666
"await authorizer.login()\n",
6767
"print(await authorizer.get_default_headers())"
@@ -105,10 +105,10 @@
105105
"metadata": {},
106106
"outputs": [],
107107
"source": [
108-
"from evo.oauth import OAuthRedirectHandler, OAuthScopes\n",
108+
"from evo.oauth import EvoScopes, OAuthRedirectHandler\n",
109109
"\n",
110110
"async with OAuthRedirectHandler(connector, REDIRECT_URL) as handler:\n",
111-
" result = await handler.login(OAuthScopes.all_evo | OAuthScopes.offline_access)\n",
111+
" result = await handler.login(EvoScopes.all_evo | EvoScopes.offline_access)\n",
112112
"\n",
113113
"print(f\"Access token: {result.access_token}\")"
114114
]
@@ -129,7 +129,7 @@
129129
"outputs": [],
130130
"source": [
131131
"from evo.aio import AioTransport\n",
132-
"from evo.oauth import ClientCredentialsAuthorizer, OAuthConnector, OAuthScopes\n",
132+
"from evo.oauth import ClientCredentialsAuthorizer, EvoScopes, OAuthConnector\n",
133133
"\n",
134134
"# OAuth client app credentials\n",
135135
"# See: https://developer.seequent.com/docs/guides/getting-started/apps-and-tokens\n",
@@ -143,16 +143,49 @@
143143
" client_id=CLIENT_ID,\n",
144144
" client_secret=CLIENT_SECRET,\n",
145145
" ),\n",
146-
" scopes=OAuthScopes.evo_discovery | OAuthScopes.evo_workspace,\n",
146+
" scopes=EvoScopes.evo_discovery | EvoScopes.evo_workspace,\n",
147147
")\n",
148148
"\n",
149149
"print(await authorizer.get_default_headers())"
150150
]
151+
},
152+
{
153+
"cell_type": "markdown",
154+
"metadata": {},
155+
"source": [
156+
"## Custom OAuth Scopes\n",
157+
"\n",
158+
"The `Scopes` type makes it easy to request custom OAuth scopes. You can combine predefined scope groups with custom scopes using the bitwise OR operator (`|`)."
159+
]
160+
},
161+
{
162+
"cell_type": "code",
163+
"execution_count": null,
164+
"metadata": {},
165+
"outputs": [],
166+
"source": [
167+
"from evo.oauth import EvoScopes, Scopes\n",
168+
"\n",
169+
"print(EvoScopes.default) # Predefined default scopes\n",
170+
"\n",
171+
"# Predefined scopes can be combined with Scopes instances:\n",
172+
"print(EvoScopes.default | Scopes(\"custom-scope1 custom-scope2\"))\n",
173+
"\n",
174+
"# Predefined scopes can be combined with str instances:\n",
175+
"print(EvoScopes.default | \"custom-scope1 custom-scope2\")\n",
176+
"\n",
177+
"# Scopes instances can be combined with str instances:\n",
178+
"print(Scopes(\"custom-scope1\") | \"custom-scope2\")\n",
179+
"\n",
180+
"# Combining scopes is commutative:\n",
181+
"print(EvoScopes.default | \"custom-scope1 custom-scope2\")\n",
182+
"print(\"custom-scope1 custom-scope2\" | EvoScopes.default)"
183+
]
151184
}
152185
],
153186
"metadata": {
154187
"kernelspec": {
155-
"display_name": "venv",
188+
"display_name": "evo-sdk",
156189
"language": "python",
157190
"name": "python3"
158191
},

packages/evo-sdk-common/docs/examples/quickstart.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
"metadata": {},
108108
"outputs": [],
109109
"source": [
110-
"from evo.oauth import ClientCredentialsAuthorizer, OAuthConnector, OAuthScopes\n",
110+
"from evo.oauth import ClientCredentialsAuthorizer, EvoScopes, OAuthConnector\n",
111111
"\n",
112112
"CLIENT_NAME = \"Your Client Name\"\n",
113113
"CLIENT_ID = \"your-client-id\"\n",
@@ -119,7 +119,7 @@
119119
" client_id=CLIENT_ID,\n",
120120
" client_secret=CLIENT_SECRET,\n",
121121
" ),\n",
122-
" scopes=OAuthScopes.all_evo,\n",
122+
" scopes=EvoScopes.all_evo,\n",
123123
")\n",
124124
"\n",
125125
"# Authorize the client.\n",

packages/evo-sdk-common/docs/examples/workspace-client.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"outputs": [],
1717
"source": [
1818
"from evo.aio import AioTransport\n",
19-
"from evo.oauth import AuthorizationCodeAuthorizer, OAuthConnector, OAuthScopes\n",
19+
"from evo.oauth import AuthorizationCodeAuthorizer, EvoScopes, OAuthConnector\n",
2020
"\n",
2121
"# OAuth client app credentials\n",
2222
"# See: https://developer.seequent.com/docs/guides/getting-started/apps-and-tokens\n",
@@ -36,7 +36,7 @@
3636
"authorizer = AuthorizationCodeAuthorizer(\n",
3737
" oauth_connector=connector,\n",
3838
" redirect_url=REDIRECT_URL,\n",
39-
" scopes=OAuthScopes.all_evo | OAuthScopes.offline_access,\n",
39+
" scopes=EvoScopes.all_evo | EvoScopes.offline_access,\n",
4040
")\n",
4141
"\n",
4242
"# Login to the Evo platform.\n",

packages/evo-sdk-common/docs/oauth.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ The `OAuth` library provides authorizer classes to handle different OAuth flows.
3939
`AuthorizationCodeAuthorizer` is the simplest way to manage user access tokens. Logging in will open a browser window to the authorisation URL and wait for the user to authenticate and authorise the application. The `AuthorizationCodeAuthorizer` object allows the user access token to be used in API requests.
4040

4141
```python
42-
from evo.oauth import AuthorizationCodeAuthorizer, OAuthScopes
42+
from evo.oauth import AuthorizationCodeAuthorizer, EvoScopes
4343

4444
authorizer = AuthorizationCodeAuthorizer(
4545
oauth_connector=connector,
4646
redirect_url=REDIRECT_URL,
47-
scopes=OAuthScopes.all_evo | OAuthScopes.offline_access,
47+
scopes=EvoScopes.all_evo | EvoScopes.offline_access,
4848
)
4949
await authorizer.login()
5050
print(await authorizer.get_default_headers())
@@ -68,10 +68,10 @@ print(f"The token was {'' if refreshed else 'not '}refreshed.")
6868
The `OAuthRedirectHandler` wraps the `OAuthConnector` and implements a localhost HTTP server to handle the OAuth redirect. This is useful for applications that cannot open a browser window, such as a command-line application. The `OAuthRedirectHandler` is an asynchronous context manager that manages the lifecycle of the HTTP server.
6969

7070
```python
71-
from evo.oauth import OAuthRedirectHandler, OAuthScopes
71+
from evo.oauth import OAuthRedirectHandler, EvoScopes
7272

7373
async with OAuthRedirectHandler(connector, REDIRECT_URL) as handler:
74-
result = await handler.login(OAuthScopes.offline_access)
74+
result = await handler.login(EvoScopes.offline_access)
7575

7676
print(f"Access token: {result.access_token}")
7777
```
@@ -82,7 +82,7 @@ The `ClientCredientialsAuthorizer` allows you to handle service-to-service authe
8282

8383
```python
8484
from evo.aio import AioTransport
85-
from evo.oauth import ClientCredentialsAuthorizer, OAuthScopes, OAuthConnector
85+
from evo.oauth import ClientCredentialsAuthorizer, EvoScopes, OAuthConnector
8686

8787
# OAuth client app credentials
8888
# See: https://developer.seequent.com/docs/guides/getting-started/apps-and-tokens
@@ -96,7 +96,7 @@ authorizer = ClientCredentialsAuthorizer(
9696
client_id=CLIENT_ID,
9797
client_secret=CLIENT_SECRET,
9898
),
99-
scopes=OAuthScopes.evo_discovery | OAuthScopes.evo_workspace,
99+
scopes=EvoScopes.evo_discovery | EvoScopes.evo_workspace,
100100
)
101101

102102
print(await authorizer.get_default_headers())

packages/evo-sdk-common/docs/quickstart.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ If you already have an access token, and don't need to worry about whether it's
9999
Alternatively, a client of `client credentials` grant type can use the `ClientCredentialsAuthorizer` for authorization into Evo. This allows for service to service requests, instead of user login and redirects.
100100

101101
```python
102-
from evo.oauth import OAuthConnector, ClientCredentialsAuthorizer, OAuthScopes
102+
from evo.oauth import OAuthConnector, ClientCredentialsAuthorizer, EvoScopes
103103

104104
CLIENT_NAME = "Your Client Name"
105105
CLIENT_ID = "your-client-id"
@@ -111,7 +111,7 @@ authorizer = ClientCredentialsAuthorizer(
111111
client_id=CLIENT_ID,
112112
client_secret=CLIENT_SECRET,
113113
),
114-
scopes=OAuthScopes.all_evo,
114+
scopes=EvoScopes.all_evo,
115115
)
116116

117117
# Authorize the client.

packages/evo-sdk-common/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "evo-sdk-common"
33
description = "Python package that establishes a common framework for use by client libraries that interact with Seequent Evo APIs"
4-
version = "0.4.5"
4+
version = "0.5.0"
55
requires-python = ">=3.10"
66
license-files = ["LICENSE.md"]
77
dynamic = ["readme"]

packages/evo-sdk-common/src/evo/notebooks/authorizer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def __init__(
9696
self,
9797
oauth_connector: oauth.OAuthConnector,
9898
redirect_url: str,
99-
scopes: oauth.OAuthScopes,
99+
scopes: oauth.AnyScopes,
100100
env: DotEnv,
101101
) -> None:
102102
"""

packages/evo-sdk-common/src/evo/notebooks/widgets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from evo.common.exceptions import UnauthorizedException
2828
from evo.common.interfaces import IAuthorizer, ICache, IFeedback, ITransport
2929
from evo.discovery import Hub, Organization
30-
from evo.oauth import OAuthConnector, OAuthScopes
30+
from evo.oauth import AnyScopes, EvoScopes, OAuthConnector
3131
from evo.service_manager import ServiceManager
3232
from evo.workspaces import Workspace
3333

@@ -283,7 +283,7 @@ def with_auth_code(
283283
redirect_url: str = DEFAULT_REDIRECT_URL,
284284
client_secret: str | None = None,
285285
cache_location: FileName = DEFAULT_CACHE_LOCATION,
286-
oauth_scopes: OAuthScopes = OAuthScopes.all_evo | OAuthScopes.offline_access,
286+
oauth_scopes: AnyScopes = EvoScopes.all_evo | EvoScopes.offline_access,
287287
proxy: StrOrURL | None = None,
288288
) -> ServiceManagerWidget:
289289
"""Create a ServiceManagerWidget with an authorization code authorizer.

packages/evo-sdk-common/src/evo/oauth/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@
1717
ClientCredentialsAuthorizer,
1818
)
1919
from .connector import OAuthConnector
20-
from .data import AccessToken, OAuthScopes
20+
from .data import AccessToken, AnyScopes, EvoScopes, OAuthScopes, Scopes
2121
from .exceptions import OAuthError
2222
from .oauth_redirect_handler import OAuthRedirectHandler
2323

2424
__all__ = [
2525
"AccessToken",
2626
"AccessTokenAuthorizer",
27+
"AnyScopes",
2728
"AuthorizationCodeAuthorizer",
2829
"ClientCredentialsAuthorizer",
30+
"EvoScopes",
2931
"OAuthConnector",
3032
"OAuthError",
3133
"OAuthRedirectHandler",
3234
"OAuthScopes",
35+
"Scopes",
3336
]

packages/evo-sdk-common/src/evo/oauth/authorizer.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from evo.common.interfaces import IAuthorizer
2020

2121
from .connector import OAuthConnector
22-
from .data import AccessToken, OAuthScopes
22+
from .data import AccessToken, AnyScopes, EvoScopes, Scopes
2323
from .exceptions import OAuthError
2424
from .oauth_redirect_handler import OAuthRedirectHandler
2525

@@ -37,7 +37,7 @@
3737
class _BaseAuthorizer(IAuthorizer, Generic[T]):
3838
pi_partial_implementation = True # Suppress warning about missing interface methods.
3939

40-
def __init__(self, oauth_connector: OAuthConnector, scopes: OAuthScopes = OAuthScopes.default) -> None:
40+
def __init__(self, oauth_connector: OAuthConnector, scopes: AnyScopes = EvoScopes.default) -> None:
4141
"""
4242
:param oauth_connector: The connector to use for fetching tokens.
4343
:param scopes: The OAuth scopes to request.
@@ -46,9 +46,7 @@ def __init__(self, oauth_connector: OAuthConnector, scopes: OAuthScopes = OAuthS
4646

4747
self._connector = oauth_connector
4848
self.__token: T | None = None
49-
50-
assert isinstance(scopes, OAuthScopes), "Scopes must be an instance of OAuthScopes."
51-
self._scopes: OAuthScopes = scopes
49+
self._scopes = Scopes(scopes)
5250

5351
def _get_token(self) -> T | None:
5452
"""Get the current access token, or None if the token is not available.
@@ -114,7 +112,7 @@ async def _fetch_token(self) -> AccessToken:
114112
"""
115113
data = { # The payload to send to the OAuth server in the token request.
116114
"grant_type": "client_credentials",
117-
"scope": str(self._scopes),
115+
"scope": self._scopes,
118116
}
119117

120118
logger.debug("Fetching access token...")
@@ -167,7 +165,7 @@ class AuthorizationCodeAuthorizer(_BaseAuthorizer[AccessToken]):
167165
"""
168166

169167
def __init__(
170-
self, oauth_connector: OAuthConnector, redirect_url: str, scopes: OAuthScopes = OAuthScopes.default
168+
self, oauth_connector: OAuthConnector, redirect_url: str, scopes: AnyScopes = EvoScopes.default
171169
) -> None:
172170
"""
173171
:param oauth_connector: The OAuth connector to use for fetching tokens.

0 commit comments

Comments
 (0)