Skip to content

Commit d2d85dd

Browse files
committed
update README
1 parent 0cb8c39 commit d2d85dd

File tree

5 files changed

+206
-11
lines changed

5 files changed

+206
-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/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+

tests/test_client.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import pytest
22

3+
from webflow import authorize_url, get_access_token
4+
from webflow.client import Webflow, AsyncWebflow
5+
36
# Get started with writing tests with pytest at https://docs.pytest.org
47
@pytest.mark.skip(reason="Unimplemented")
58
def test_client() -> None:
6-
assert True == True
9+
client = Webflow(access_token="")
10+
client = AsyncWebflow(access_token="")
11+
client.collections.create()

0 commit comments

Comments
 (0)