Skip to content

Commit 97b5ba0

Browse files
🌿 Fern Regeneration -- support OAuth 2.0 Authentication (#3)
* SDK regeneration * update README * fix --------- Co-authored-by: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Co-authored-by: dsinghvi <[email protected]>
1 parent 1f65393 commit 97b5ba0

File tree

8 files changed

+306
-11
lines changed

8 files changed

+306
-11
lines changed

.fernignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22

33
README.md
44
assets/
5+
6+
src/webflow/client.py
7+
src/webflow/oauth.py

README.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ Simply import `Webflow` and start making calls to our API.
2626
```python
2727
from webflow.client import Webflow
2828

29-
client = Webflow(access_token="YOUR_ACCESS_TOKEN")
29+
client = Webflow(
30+
client_id="YOUR_CLIENT_ID",
31+
client_secret="YOUR_CLIENT_SECRET",
32+
code="YOUR_AUTHORIZATION_CODE"
33+
)
3034
site = client.sites.get("site-id")
3135
```
3236

@@ -38,7 +42,9 @@ calls to our API.
3842
from webflow.client import AsyncWebflow
3943

4044
client = AsyncWebflow(
41-
access_token="YOUR_ACCESS_TOKEN",
45+
client_id="YOUR_CLIENT_ID",
46+
client_secret="YOUR_CLIENT_SECRET",
47+
code="YOUR_AUTHORIZATION_CODE"
4248
)
4349

4450
async def main() -> None:
@@ -48,6 +54,58 @@ async def main() -> None:
4854
asyncio.run(main())
4955
```
5056

57+
## OAuth
58+
59+
To implement OAuth, you'll need a registred Webflow App.
60+
61+
### Step 1: Authorize URL
62+
63+
The first step in OAuth is to generate an authorization url. Use this URL
64+
to fetch your authorization code. See the [docs](https://docs.developers.webflow.com/v1.0.0/docs/oauth#user-authorization
65+
for more details.
66+
67+
```python
68+
from webflow.oauth import authorize_url
69+
from webflow import OauthScope
70+
71+
url = webflow.authorize_url({
72+
client_id = "[CLIENT ID]",
73+
scope = OauthScope.ReadUsers, # or [OauthScope.ReadUsers, OauthScope.WriteUsers]
74+
state = "1234567890", # optional
75+
redirect_uri = "https://my.server.com/oauth/callback", # optional
76+
});
77+
78+
print(url)
79+
```
80+
81+
### Step 2: Instantiate the client
82+
Pass in your `client_id`, `client_secret`, `authorization_code` when instantiating
83+
the client. Our SDK handles generating an access token and passing that to every endpoint.
84+
85+
```python
86+
from webflow.client import Webflow
87+
88+
client = Webflow(
89+
client_id="YOUR_CLIENT_ID",
90+
client_secret="YOUR_CLIENT_SECRET",
91+
code="YOUR_AUTHORIZATION_CODE",
92+
redirect_uri = "https://my.server.com/oauth/callback", # optional
93+
)
94+
```
95+
96+
If you want to generate an access token yourself, simply import the
97+
`get_access_token` function.
98+
99+
```python
100+
from webflow.oauth import get_access_token
101+
102+
access_token = get_access_token(
103+
client_id="YOUR_CLIENT_ID",
104+
client_secret="YOUR_CLIENT_SECRET",
105+
code="YOUR_AUTHORIZATION_CODE"
106+
)
107+
```
108+
51109
## Webflow Module
52110
All of the models are nested within the Webflow module. Let Intellisense
53111
guide you!

src/webflow/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
ListCustomCodeBlocks,
5151
MissingScopes,
5252
NoDomains,
53+
OauthScope,
5354
Order,
5455
OrderAddress,
5556
OrderAddressJapanType,
@@ -214,6 +215,7 @@
214215
"MissingScopes",
215216
"NoDomains",
216217
"NotFoundError",
218+
"OauthScope",
217219
"Order",
218220
"OrderAddress",
219221
"OrderAddressJapanType",

src/webflow/client.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
88
from .environment import WebflowEnvironment
9+
from .oauth import get_access_token
910
from .resources.access_groups.client import AccessGroupsClient, AsyncAccessGroupsClient
1011
from .resources.assets.client import AssetsClient, AsyncAssetsClient
1112
from .resources.collections.client import AsyncCollectionsClient, CollectionsClient
@@ -26,15 +27,22 @@ class Webflow:
2627
def __init__(
2728
self,
2829
*,
29-
base_url: typing.Optional[str] = None,
30+
client_id: str,
31+
client_secret: str,
32+
code: str,
33+
redirect_uri: typing.Optional[str] = None,
3034
environment: WebflowEnvironment = WebflowEnvironment.DEFAULT,
31-
access_token: typing.Union[str, typing.Callable[[], str]],
3235
timeout: typing.Optional[float] = 60,
3336
httpx_client: typing.Optional[httpx.Client] = None
3437
):
38+
self._token = get_access_token(
39+
client_id=client_id,
40+
client_secret=client_secret,
41+
code=code,
42+
redirect_uri=redirect_uri)
3543
self._client_wrapper = SyncClientWrapper(
36-
base_url=_get_base_url(base_url=base_url, environment=environment),
37-
access_token=access_token,
44+
base_url=_get_base_url(base_url=None, environment=environment),
45+
access_token=self._token,
3846
httpx_client=httpx.Client(timeout=timeout) if httpx_client is None else httpx_client,
3947
)
4048
self.token = TokenClient(client_wrapper=self._client_wrapper)
@@ -57,15 +65,22 @@ class AsyncWebflow:
5765
def __init__(
5866
self,
5967
*,
60-
base_url: typing.Optional[str] = None,
68+
client_id: str,
69+
client_secret: str,
70+
code: str,
71+
redirect_uri: typing.Optional[str] = None,
6172
environment: WebflowEnvironment = WebflowEnvironment.DEFAULT,
62-
access_token: typing.Union[str, typing.Callable[[], str]],
6373
timeout: typing.Optional[float] = 60,
6474
httpx_client: typing.Optional[httpx.AsyncClient] = None
6575
):
76+
self._token = get_access_token(
77+
client_id=client_id,
78+
client_secret=client_secret,
79+
code=code,
80+
redirect_uri=redirect_uri)
6681
self._client_wrapper = AsyncClientWrapper(
67-
base_url=_get_base_url(base_url=base_url, environment=environment),
68-
access_token=access_token,
82+
base_url=_get_base_url(base_url=None, environment=environment),
83+
access_token=self._token,
6984
httpx_client=httpx.AsyncClient(timeout=timeout) if httpx_client is None else httpx_client,
7085
)
7186
self.token = AsyncTokenClient(client_wrapper=self._client_wrapper)

src/webflow/oauth.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
2+
import typing
3+
import httpx
4+
import urllib.parse
5+
from json.decoder import JSONDecodeError
6+
7+
from .core.api_error import ApiError
8+
from .core.jsonable_encoder import jsonable_encoder
9+
from .environment import WebflowEnvironment
10+
from .types import OauthScope
11+
12+
try:
13+
import pydantic.v1 as pydantic # type: ignore
14+
except ImportError:
15+
import pydantic # type: ignore
16+
17+
# this is used as the default value for optional parameters
18+
OMIT = typing.cast(typing.Any, ...)
19+
20+
21+
def authorize_url(
22+
*,
23+
client_id: str,
24+
state: typing.Optional[str] = OMIT,
25+
redirect_uri: typing.Optional[str] = OMIT,
26+
scope: typing.Optional[typing.Union[OauthScope, typing.List[OauthScope]]] = OMIT,
27+
) -> str:
28+
"""
29+
Get the URL to authorize a user
30+
31+
Parameters:
32+
- client_id: str. The OAuth client ID
33+
34+
- state: typing.Optional[str]. The state.
35+
36+
- redirect_uri: typing.Optional[str]. The redirect URI.
37+
38+
- scope: typing.Optional[typing.Union[OauthScope, typing.List[OauthScope]]].
39+
OAuth Scopes.
40+
---
41+
from webflow.oauth import authorize_url
42+
from webflow import OauthScope
43+
44+
url = authorize_url(
45+
client_id = "<YOUR_CLIENT_ID>",
46+
redirect_uri = "https://my.server.com/oauth/callback",
47+
scopes = [OauthScope.ReadSites, OauthScope.WriteItems", OauthScope.ReadUsers],
48+
)
49+
"""
50+
params: typing.Dict[str, typing.Any] = {
51+
"client_id": client_id,
52+
"response_type": "code",
53+
}
54+
if state is not OMIT:
55+
params["state"] = state
56+
if redirect_uri is not OMIT:
57+
params["redirect_uri"] = redirect_uri
58+
if scope is not OMIT and isinstance(scope, str):
59+
params["scope"] = scope.value
60+
elif scope is not OMIT:
61+
params["scope"] = ", ".join([s.value for s in scope]) # type: ignore
62+
return f"https://webflow.com/oauth/authorize?{urllib.parse.urlencode(params)}"
63+
64+
65+
def get_access_token(
66+
*,
67+
client_id: str,
68+
client_secret: str,
69+
code: str,
70+
redirect_uri: typing.Optional[str] = OMIT,
71+
) -> str:
72+
"""
73+
Get the URL to authorize a user
74+
75+
Parameters:
76+
- client_id: str. The OAuth client ID
77+
78+
- client_secret: str. The OAuth client secret
79+
80+
- code: str. The OAuth code
81+
82+
- redirect_uri: typing.Optional[str]. The redirect URI.
83+
---
84+
from webflow.oauth import get_access_token
85+
86+
token = get_access_token(
87+
client_id = "<YOUR_CLIENT_ID>",
88+
client_secret = "<YOUR_CLIENT_ID>",
89+
code= "<YOUR_CODE>"
90+
redirect_uri = "https://my.server.com/oauth/callback",
91+
)
92+
"""
93+
request: typing.Dict[str, typing.Any] = {
94+
"client_id": client_id,
95+
"client_secret": client_secret,
96+
"code": code,
97+
"grant_type": "authorization_code",
98+
}
99+
if redirect_uri is not OMIT:
100+
request["redirect_uri"] = redirect_uri
101+
response = httpx.request(
102+
"POST",
103+
"https://api.webflow.com/oauth/access_token",
104+
json=jsonable_encoder(request),
105+
timeout=60,
106+
)
107+
if 200 <= response.status_code < 300:
108+
_response_json = response.json()
109+
return _response_json["access_token"]
110+
try:
111+
raise ApiError(status_code=response.status_code, body=response.json())
112+
except JSONDecodeError:
113+
raise ApiError(status_code=response.status_code, body=response.text)
114+

src/webflow/types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from .list_custom_code_blocks import ListCustomCodeBlocks
5050
from .missing_scopes import MissingScopes
5151
from .no_domains import NoDomains
52+
from .oauth_scope import OauthScope
5253
from .order import Order
5354
from .order_address import OrderAddress
5455
from .order_address_japan_type import OrderAddressJapanType
@@ -173,6 +174,7 @@
173174
"ListCustomCodeBlocks",
174175
"MissingScopes",
175176
"NoDomains",
177+
"OauthScope",
176178
"Order",
177179
"OrderAddress",
178180
"OrderAddressJapanType",

src/webflow/types/oauth_scope.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# This file was auto-generated by Fern from our API Definition.
2+
3+
import enum
4+
import typing
5+
6+
T_Result = typing.TypeVar("T_Result")
7+
8+
9+
class OauthScope(str, enum.Enum):
10+
AUTHORIZED_USER_READ = "authorized_user:read"
11+
"""
12+
read details about the authorized user
13+
"""
14+
15+
READ_PAGES = "read:pages"
16+
"""
17+
read pages on the site
18+
"""
19+
20+
SITES_READ = "sites:read"
21+
"""
22+
read sites on the site
23+
"""
24+
25+
SITES_WRITE = "sites:write"
26+
"""
27+
modify pages on the site
28+
"""
29+
30+
CUSTOM_CODE_READ = "custom_code:read"
31+
"""
32+
read custom code on the site
33+
"""
34+
35+
CUSTOM_CODE_WRITE = "custom_code:write"
36+
"""
37+
modify custom code on the site
38+
"""
39+
40+
CUSTOM_CODE_DELETE = "custom_code:delete"
41+
"""
42+
delete custom code on the site
43+
"""
44+
45+
USERS_READ = "users:read"
46+
"""
47+
read users on the site
48+
"""
49+
50+
USERS_WRITE = "users:write"
51+
"""
52+
modify users on the site
53+
"""
54+
55+
ECOMMERCE_READ = "ecommerce:read"
56+
"""
57+
read ecommerce data
58+
"""
59+
60+
ECOMMERCE_WRITE = "ecommerce:write"
61+
"""
62+
edit ecommerce data
63+
"""
64+
65+
def visit(
66+
self,
67+
authorized_user_read: typing.Callable[[], T_Result],
68+
read_pages: typing.Callable[[], T_Result],
69+
sites_read: typing.Callable[[], T_Result],
70+
sites_write: typing.Callable[[], T_Result],
71+
custom_code_read: typing.Callable[[], T_Result],
72+
custom_code_write: typing.Callable[[], T_Result],
73+
custom_code_delete: typing.Callable[[], T_Result],
74+
users_read: typing.Callable[[], T_Result],
75+
users_write: typing.Callable[[], T_Result],
76+
ecommerce_read: typing.Callable[[], T_Result],
77+
ecommerce_write: typing.Callable[[], T_Result],
78+
) -> T_Result:
79+
if self is OauthScope.AUTHORIZED_USER_READ:
80+
return authorized_user_read()
81+
if self is OauthScope.READ_PAGES:
82+
return read_pages()
83+
if self is OauthScope.SITES_READ:
84+
return sites_read()
85+
if self is OauthScope.SITES_WRITE:
86+
return sites_write()
87+
if self is OauthScope.CUSTOM_CODE_READ:
88+
return custom_code_read()
89+
if self is OauthScope.CUSTOM_CODE_WRITE:
90+
return custom_code_write()
91+
if self is OauthScope.CUSTOM_CODE_DELETE:
92+
return custom_code_delete()
93+
if self is OauthScope.USERS_READ:
94+
return users_read()
95+
if self is OauthScope.USERS_WRITE:
96+
return users_write()
97+
if self is OauthScope.ECOMMERCE_READ:
98+
return ecommerce_read()
99+
if self is OauthScope.ECOMMERCE_WRITE:
100+
return ecommerce_write()

0 commit comments

Comments
 (0)