Skip to content

Commit becbb5f

Browse files
committed
Initial commit.
0 parents  commit becbb5f

File tree

15 files changed

+632
-0
lines changed

15 files changed

+632
-0
lines changed

.gitignore

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
env/
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
*.egg-info/
24+
.installed.cfg
25+
*.egg
26+
27+
# PyInstaller
28+
# Usually these files are written by a python script from a template
29+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.coverage
41+
.coverage.*
42+
.cache
43+
nosetests.xml
44+
coverage.xml
45+
*,cover
46+
.hypothesis/
47+
48+
# Translations
49+
*.mo
50+
*.pot
51+
52+
# Django stuff:
53+
*.log
54+
local_settings.py
55+
56+
# Flask stuff:
57+
instance/
58+
.webassets-cache
59+
60+
# Scrapy stuff:
61+
.scrapy
62+
63+
# Sphinx documentation
64+
docs/_build/
65+
docs/_static/
66+
docs/_templates/
67+
68+
# PyBuilder
69+
target/
70+
71+
# IPython Notebook
72+
.ipynb_checkpoints
73+
74+
# pyenv
75+
.python-version
76+
77+
# celery beat schedule file
78+
celerybeat-schedule
79+
80+
# dotenv
81+
.env
82+
83+
# virtualenv
84+
.venv/
85+
venv/
86+
ENV/
87+
88+
# Spyder project settings
89+
.spyderproject
90+
91+
# Rope project settings
92+
.ropeproject
93+
94+
.DS_Store
95+
Thumbs.db
96+
.pypirc
97+
docs

.vscode/launch.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Python: Unit Tests",
9+
"type": "python",
10+
"request": "launch",
11+
"module": "unittest",
12+
"envFile": "${workspaceFolder}/.env"
13+
}
14+
]
15+
}

Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
init:
2+
pip3 install -r requirements.txt
3+
4+
init-dev: init
5+
pip3 install -r requirements-dev.txt
6+
7+
test:
8+
python3 -m unittest
9+
10+
build:
11+
python3 -m build
12+
13+
docs:
14+
python3 -m pdoc --html -o docs src/autodesk_forge_sdk
15+
16+
prepublish-check: build
17+
python3 -m twine check dist/*
18+
19+
publish-test: test build
20+
python3 -m twine upload --repository testpypi dist/*

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# autodesk-forge-sdk
2+
3+
Unofficial [Autodesk Forge](https://forge.autodesk.com) SDK for Python 3.6 and above.

requirements-dev.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build
2+
twine
3+
wheel
4+
setuptools
5+
pdoc3

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
requests

setup.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import setuptools
2+
3+
with open("README.md") as f:
4+
readme = f.read()
5+
6+
setuptools.setup(
7+
name="autodesk-forge-sdk",
8+
version="0.0.1",
9+
author="Petr Broz",
10+
author_email="[email protected]",
11+
description="Unofficial Autodesk Forge SDK for Python.",
12+
long_description=readme,
13+
long_description_content_type="text/markdown",
14+
url="https://github.com/petrbroz/forge-sdk-python",
15+
project_urls={
16+
"Bug Tracker": "https://github.com/petrbroz/forge-sdk-python/issues",
17+
},
18+
classifiers=[
19+
"Programming Language :: Python :: 3",
20+
"License :: OSI Approved :: MIT License",
21+
"Operating System :: OS Independent",
22+
],
23+
package_dir={"": "src"},
24+
packages=setuptools.find_packages(where="src"),
25+
python_requires=">=3.6",
26+
)

src/autodesk_forge_sdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .auth import AuthenticationClient, PassiveTokenProvider, ActiveTokenProvider
2+
from .oss import OSSClient

src/autodesk_forge_sdk/auth.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import requests
2+
from datetime import datetime, timedelta
3+
from urllib.parse import quote
4+
from .base import BaseClient
5+
6+
BASE_URL = 'https://developer.api.autodesk.com/authentication/v1'
7+
8+
class AuthenticationClient(BaseClient):
9+
"""
10+
Forge Authentication service client.
11+
12+
For more details, see https://forge.autodesk.com/en/docs/oauth/v2/reference/http.
13+
"""
14+
15+
def __init__(self, base_url=BASE_URL):
16+
BaseClient.__init__(self, base_url)
17+
18+
def authenticate(self, client_id, client_secret, scopes):
19+
"""Get a two-legged access token by providing your app’s client ID and secret.
20+
21+
Parameters:
22+
client_id (string): Client ID of the app.
23+
client_secret (string): Client secret of the app.
24+
scopes (list): List of required scopes.
25+
26+
Returns:
27+
object: Parsed response object with properties 'token_type', 'access_token', and 'expires_in'.
28+
"""
29+
form = {
30+
'client_id': client_id,
31+
'client_secret': client_secret,
32+
'grant_type': 'client_credentials',
33+
'scope': ' '.join(scopes)
34+
}
35+
return self._post('/authenticate', form=form).json()
36+
37+
def get_authorization_url(self, client_id, response_type, redirect_uri, scopes, state=None):
38+
"""Generate a URL to redirect an end user to
39+
in order to acquire the user’s consent for your app to access the specified resources.
40+
41+
Parameters:
42+
client_id (string): Client ID of the app.
43+
response_type (string): Must be either 'code' for authorization code grant flow or 'token' for implicit grant flow.
44+
redirect_uri (string): URL-encoded callback URL that the end user will be redirected to after completing the authorization flow.
45+
scopes (list): List of required scopes.
46+
state (string): Optional payload containing arbitrary data that the authentication flow will pass back verbatim in a state query parameter to the callback URL.
47+
48+
Returns:
49+
string: Complete authorization URL.
50+
"""
51+
url = 'https://developer.api.autodesk.com/authentication/v1/authorize'
52+
url = url + '?client_id={}'.format(quote(client_id))
53+
url = url + '&response_type={}'.format(response_type)
54+
url = url + '&redirect_uri={}'.format(quote(redirect_uri))
55+
url = url + '&scope={}'.format(quote(' '.join(scopes)))
56+
if state:
57+
url += '&state={}'.format(quote(state))
58+
return url
59+
60+
def get_token(self, client_id, client_secret, code, redirect_uri):
61+
"""Exchange an authorization code extracted from a 'GET authorize' callback
62+
for a three-legged access token. This API will only be used when the 'Authorization Code' grant type
63+
is being adopted.
64+
65+
Parameters:
66+
client_id (string): Client ID of the app.
67+
client_secret (string): Client secret of the app.
68+
code (string): The authorization code captured from the code query parameter when the 'GET authorize' redirected back to the callback URL.
69+
redirect_uri (string): Must match the redirect_uri parameter used in 'GET authorize'.
70+
71+
Returns:
72+
object: Parsed response object with properties 'token_type', 'access_token', 'refresh_token', and 'expires_in'.
73+
"""
74+
form = {
75+
'client_id': client_id,
76+
'client_secret': client_secret,
77+
'grant_type': 'authorization_code',
78+
'code': code,
79+
'redirect_uri': redirect_uri
80+
}
81+
return self._post('/gettoken', form=form).json()
82+
83+
def refresh_token(self, client_id, client_secret, refresh_token, scopes):
84+
"""Acquire a new access token by using the refresh token provided by the `POST gettoken` endpoint.
85+
86+
Parameters:
87+
client_id (string): Client ID of the app.
88+
client_secret (string): Client secret of the app.
89+
refresh_token (string): The refresh token used to acquire a new access token.
90+
scopes (list): List of required scopes.
91+
92+
Returns:
93+
object: Parsed response object with properties 'token_type', 'access_token', 'refresh_token', and 'expires_in'.
94+
"""
95+
form = {
96+
'client_id': client_id,
97+
'client_secret': client_secret,
98+
'grant_type': 'refresh_token',
99+
'refresh_token': refresh_token,
100+
'scope': ' '.join(scopes)
101+
}
102+
return self._post('/refreshtoken', form=form).json()
103+
104+
def get_profile(self, access_token):
105+
"""Get the profile information of an authorizing end user in a three-legged context.
106+
107+
Parameters:
108+
access_token (string): Token obtained via a three-legged OAuth flow.
109+
110+
Returns:
111+
object: Parsed response object with properties 'userId', 'userName', 'emaillId', 'firstName', 'lastName', etc.
112+
"""
113+
headers = {
114+
'Authorization': 'Bearer {}'.format(access_token)
115+
}
116+
return self._get('/users/@me', headers=headers).json()
117+
118+
class PassiveTokenProvider:
119+
def __init__(self, token):
120+
self.token = token
121+
def get_token(self, scopes):
122+
return self.token
123+
124+
class ActiveTokenProvider:
125+
def __init__(self, client_id, client_secret):
126+
self.client_id = client_id
127+
self.client_secret = client_secret
128+
self.auth_client = AuthenticationClient()
129+
self.cache = {}
130+
def get_token(self, scopes):
131+
cache_key = '+'.join(scopes)
132+
now = datetime.now()
133+
if cache_key in self.cache:
134+
auth = self.cache[cache_key]
135+
if auth['expires_at'] > now:
136+
return auth
137+
auth = self.auth_client.authenticate(self.client_id, self.client_secret, scopes)
138+
auth['expires_at'] = now + timedelta(0, auth['expires_in'])
139+
return auth
140+
141+
class BaseOAuthClient(BaseClient):
142+
def __init__(self, token_provider, base_url):
143+
BaseClient.__init__(self, base_url)
144+
self.token_provider = token_provider
145+
146+
def _get(self, url, scopes, params=None, headers=None):
147+
if not headers:
148+
headers = {}
149+
self._set_auth_headers(headers, scopes)
150+
return BaseClient._get(self, url, params, headers)
151+
152+
def _post(self, url, scopes, form=None, json=None, buff=None, params=None, headers=None):
153+
if not headers:
154+
headers = {}
155+
self._set_auth_headers(headers, scopes)
156+
return BaseClient._post(self, url, form, json, buff, params, headers)
157+
158+
def _put(self, url, scopes, form=None, json=None, buff=None, params=None, headers=None):
159+
if not headers:
160+
headers = {}
161+
self._set_auth_headers(headers, scopes)
162+
return BaseClient._put(self, url, form, json, buff, params, headers)
163+
164+
def _delete(self, url, scopes, params=None, headers=None):
165+
if not headers:
166+
headers = {}
167+
self._set_auth_headers(headers, scopes)
168+
return BaseClient._delete(self, url, params, headers)
169+
170+
def _set_auth_headers(self, headers, scopes):
171+
if not 'Authorization' in headers:
172+
auth = self.token_provider.get_token(scopes)
173+
headers['Authorization'] = 'Bearer {}'.format(auth['access_token'])

0 commit comments

Comments
 (0)