tausi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tausi-0.1.0/.gitignore +7 -0
- tausi-0.1.0/CHANGELOG.md +16 -0
- tausi-0.1.0/LICENSE +21 -0
- tausi-0.1.0/PKG-INFO +235 -0
- tausi-0.1.0/README.md +201 -0
- tausi-0.1.0/pyproject.toml +47 -0
- tausi-0.1.0/src/tausi/__init__.py +39 -0
- tausi-0.1.0/src/tausi/_http.py +119 -0
- tausi-0.1.0/src/tausi/backend.py +209 -0
- tausi-0.1.0/src/tausi/client.py +298 -0
- tausi-0.1.0/src/tausi/django/__init__.py +189 -0
- tausi-0.1.0/src/tausi/django/rest_framework.py +49 -0
- tausi-0.1.0/src/tausi/errors.py +33 -0
- tausi-0.1.0/src/tausi/flask/__init__.py +146 -0
- tausi-0.1.0/src/tausi/models.py +192 -0
- tausi-0.1.0/src/tausi/pkce.py +17 -0
- tausi-0.1.0/src/tausi/py.typed +0 -0
- tausi-0.1.0/src/tausi/token_store.py +45 -0
- tausi-0.1.0/tests/__init__.py +0 -0
- tausi-0.1.0/tests/conftest.py +51 -0
- tausi-0.1.0/tests/test_core.py +232 -0
- tausi-0.1.0/tests/test_django.py +108 -0
- tausi-0.1.0/tests/test_flask.py +64 -0
tausi-0.1.0/.gitignore
ADDED
tausi-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- `captcha_token=` for `sign_up()`, `sign_in()` and `forgot_password()`, and `get_captcha_config()`.
|
|
6
|
+
- `PASSWORD_BREACHED`, `CAPTCHA_REQUIRED` and `CAPTCHA_INVALID` error codes.
|
|
7
|
+
- `redirect_to=` for `sign_up()`, `send_verification_email()` and `forgot_password()`: choose where email links open.
|
|
8
|
+
- Backend: `users.ban()`, `unban()`, `suspend(hours=|until=)`, `unsuspend()` and `bulk()`; `users.list(access=)`; `User.access`, `is_blocked` and the ban/suspension fields.
|
|
9
|
+
- `ACCOUNT_BANNED` and `ACCOUNT_SUSPENDED` error codes and OAuth error messages.
|
|
10
|
+
- `User` gains `first_name`, `last_name`, `username`, `phone_number`, `phone_number_verified`, `locale`, `timezone`, `public_metadata`, `last_sign_in_at`, and from the backend `private_metadata`, `last_sign_out_at`, `sign_in_count`, `active_sessions`, `last_active_at`, `status` and `is_signed_in`.
|
|
11
|
+
- `sign_up()`, `update_user()` and `users.update()` accept the new attributes (and metadata for the backend).
|
|
12
|
+
- `EMAIL_ALREADY_REGISTERED` and `USERNAME_TAKEN` error codes.
|
|
13
|
+
|
|
14
|
+
## 0.1.0
|
|
15
|
+
|
|
16
|
+
- First release: `TausiBackend` (verify sessions, manage users), `TausiClient` (server-rendered sign-in), Django decorators and middleware, Django REST framework authentication, and a Flask extension.
|
tausi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tausi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tausi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tausi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Tausi: verify sessions and manage users from your backend, with Django and Flask integrations.
|
|
5
|
+
Project-URL: Homepage, https://tausi.app
|
|
6
|
+
Project-URL: Documentation, https://github.com/Nelacommunity/tausi-auth/tree/main/sdks/python#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/Nelacommunity/tausi-auth
|
|
8
|
+
Project-URL: Issues, https://github.com/Nelacommunity/tausi-auth/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Nelacommunity/tausi-auth/blob/main/sdks/python/CHANGELOG.md
|
|
10
|
+
Author: Tausi
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: auth,authentication,django,flask,oauth,sessions,tausi
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: Django
|
|
16
|
+
Classifier: Framework :: Flask
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Session
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: django>=4.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: djangorestframework>=3.15; extra == 'dev'
|
|
27
|
+
Requires-Dist: flask>=2.3; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Provides-Extra: django
|
|
30
|
+
Requires-Dist: django>=4.2; extra == 'django'
|
|
31
|
+
Provides-Extra: flask
|
|
32
|
+
Requires-Dist: flask>=2.3; extra == 'flask'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# tausi (Python)
|
|
36
|
+
|
|
37
|
+
Official Python SDK for [Tausi](https://tausi.app), with Django, Django REST framework and Flask integrations. It needs Python 3.9 or later and has **no runtime dependencies**: it uses the standard library for HTTP.
|
|
38
|
+
|
|
39
|
+
| | Key | Use it for |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `tausi.TausiBackend` | secret key `sk_…` | Protecting your API (verifying session tokens) and managing your project's users. |
|
|
42
|
+
| `tausi.TausiClient` | publishable key `pk_…` | Server-rendered apps where Python signs the user in and keeps their token in the session, plus scripts and CLIs. |
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install tausi # core
|
|
46
|
+
pip install "tausi[django]" # + Django
|
|
47
|
+
pip install "tausi[flask]" # + Flask
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Protect your API
|
|
51
|
+
|
|
52
|
+
Your frontend (built with `@tausi-auth/js`, Flutter, etc.) sends the user's session token as `Authorization: Bearer <token>`. Your backend checks it with the secret key:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import os
|
|
56
|
+
from tausi import TausiBackend
|
|
57
|
+
|
|
58
|
+
tausi = TausiBackend("https://tausi.app", os.environ["TAUSI_SECRET_KEY"])
|
|
59
|
+
|
|
60
|
+
auth = tausi.authenticate_request(request) # any request with .headers, or the header value itself
|
|
61
|
+
if auth is None:
|
|
62
|
+
... # 401
|
|
63
|
+
print(auth.user.id, auth.user.email, auth.session.expires_at)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`authenticate_request()` and `verify_session(token)` return `None` when a token is invalid, expired, revoked or belongs to another project. They raise `TausiError` only when the problem is on your side: a wrong key, a network failure or a 5xx response.
|
|
67
|
+
|
|
68
|
+
### Django
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# settings.py
|
|
72
|
+
TAUSI_URL = "https://tausi.app"
|
|
73
|
+
TAUSI_SECRET_KEY = os.environ["TAUSI_SECRET_KEY"]
|
|
74
|
+
|
|
75
|
+
# views.py
|
|
76
|
+
from django.http import JsonResponse
|
|
77
|
+
from tausi.django import tausi_required, tausi_optional
|
|
78
|
+
|
|
79
|
+
@tausi_required # 401 JSON without a valid token; works on async views too
|
|
80
|
+
def me(request):
|
|
81
|
+
return JsonResponse(request.tausi.user.to_dict())
|
|
82
|
+
|
|
83
|
+
@tausi_optional # request.tausi is an AuthResult or None
|
|
84
|
+
def feed(request):
|
|
85
|
+
...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To set `request.tausi` on every request, add `"tausi.django.TausiAuthMiddleware"` to `MIDDLEWARE`. Only requests that carry a bearer token cost a call to Tausi. When Tausi can't be reached, the decorators and middleware answer `503` with code `AUTH_UNAVAILABLE`.
|
|
89
|
+
|
|
90
|
+
Each setting falls back to the environment variable of the same name (`TAUSI_URL`, `TAUSI_SECRET_KEY`, `TAUSI_PUBLISHABLE_KEY`). `TAUSI_TIMEOUT` sets the request timeout in seconds (default 10).
|
|
91
|
+
|
|
92
|
+
### Django REST framework
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
REST_FRAMEWORK = {
|
|
96
|
+
"DEFAULT_AUTHENTICATION_CLASSES": ["tausi.django.rest_framework.TausiAuthentication"],
|
|
97
|
+
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
In views, `request.user` is a `tausi.User` (with `is_authenticated = True`) and `request.auth` is the full `AuthResult`. Requests without a bearer token fall through to your other authentication classes.
|
|
102
|
+
|
|
103
|
+
### Flask
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from flask import Flask, g
|
|
107
|
+
from tausi.flask import Tausi
|
|
108
|
+
|
|
109
|
+
app = Flask(__name__)
|
|
110
|
+
app.config["TAUSI_URL"] = "https://tausi.app"
|
|
111
|
+
app.config["TAUSI_SECRET_KEY"] = os.environ["TAUSI_SECRET_KEY"]
|
|
112
|
+
tausi = Tausi(app) # or tausi = Tausi(); tausi.init_app(app)
|
|
113
|
+
|
|
114
|
+
@app.get("/api/me")
|
|
115
|
+
@tausi.required # 401 JSON without a valid token
|
|
116
|
+
def me():
|
|
117
|
+
return g.tausi.user.to_dict()
|
|
118
|
+
|
|
119
|
+
@app.get("/api/feed")
|
|
120
|
+
@tausi.optional # g.tausi is an AuthResult or None
|
|
121
|
+
def feed():
|
|
122
|
+
...
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Manage users
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
page = tausi.users.list(q="asha", verified=True, page=1, page_size=50) # Page[User]
|
|
129
|
+
for user in page.items:
|
|
130
|
+
print(user.email, user.status, user.active_sessions, user.last_sign_in_at)
|
|
131
|
+
|
|
132
|
+
detail = tausi.users.get(user_id) # {"user": User, "accounts": [...], "sessions": [...], "recentAudit": [...]}
|
|
133
|
+
tausi.users.update(user_id, name="Asha", email_verified=True)
|
|
134
|
+
tausi.users.revoke_sessions(user_id) # sign out everywhere
|
|
135
|
+
tausi.users.delete(user_id)
|
|
136
|
+
tausi.sessions.list(page=1)
|
|
137
|
+
tausi.get_project() # startup check that the key works
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
From Django you get the configured backend with `tausi.django.get_backend()`; from Flask, `tausi.backend` inside an app context.
|
|
141
|
+
|
|
142
|
+
## Server-rendered sign-in
|
|
143
|
+
|
|
144
|
+
When your Python app renders the sign-in form itself, use `TausiClient` with the publishable key. The token stays in the server-side session and never reaches the browser.
|
|
145
|
+
|
|
146
|
+
### Django
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# settings.py: TAUSI_PUBLISHABLE_KEY = os.environ["TAUSI_PUBLISHABLE_KEY"], plus SessionMiddleware
|
|
150
|
+
from django.shortcuts import redirect, render
|
|
151
|
+
from tausi import TausiError
|
|
152
|
+
from tausi.django import get_client, tausi_login_required
|
|
153
|
+
|
|
154
|
+
def sign_in(request):
|
|
155
|
+
if request.method == "POST":
|
|
156
|
+
try:
|
|
157
|
+
get_client(request).sign_in(request.POST["email"], request.POST["password"])
|
|
158
|
+
return redirect(request.GET.get("next", "/"))
|
|
159
|
+
except TausiError as e:
|
|
160
|
+
return render(request, "sign_in.html", {"error": e.message, "code": e.code})
|
|
161
|
+
return render(request, "sign_in.html")
|
|
162
|
+
|
|
163
|
+
def sign_out(request):
|
|
164
|
+
get_client(request).sign_out()
|
|
165
|
+
return redirect("/")
|
|
166
|
+
|
|
167
|
+
@tausi_login_required # redirects to settings.LOGIN_URL with ?next=
|
|
168
|
+
def dashboard(request):
|
|
169
|
+
return render(request, "dashboard.html", {"user": request.tausi.user})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Flask
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
@app.post("/sign-in")
|
|
176
|
+
def login():
|
|
177
|
+
try:
|
|
178
|
+
tausi.client().sign_in(request.form["email"], request.form["password"])
|
|
179
|
+
except TausiError as e:
|
|
180
|
+
return render_template("sign_in.html", error=e.message), 400
|
|
181
|
+
return redirect(request.args.get("next", "/"))
|
|
182
|
+
|
|
183
|
+
@app.get("/dashboard")
|
|
184
|
+
@tausi.login_required(login_endpoint="login")
|
|
185
|
+
def dashboard():
|
|
186
|
+
return render_template("dashboard.html", user=g.tausi.user)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Social sign-in (PKCE)
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
def start_google(request):
|
|
193
|
+
start = get_client(request).start_oauth("google", redirect_to="https://example.com/auth/callback")
|
|
194
|
+
request.session["tausi_verifier"] = start.code_verifier
|
|
195
|
+
return redirect(start.url)
|
|
196
|
+
|
|
197
|
+
def callback(request):
|
|
198
|
+
verifier = request.session.pop("tausi_verifier", "")
|
|
199
|
+
try:
|
|
200
|
+
get_client(request).handle_oauth_redirect(request.build_absolute_uri(), verifier)
|
|
201
|
+
except TausiError as e:
|
|
202
|
+
return render(request, "sign_in.html", {"error": e.message})
|
|
203
|
+
return redirect("/")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`redirect_to` must be on one of the project's allowed origins.
|
|
207
|
+
|
|
208
|
+
### All client methods
|
|
209
|
+
|
|
210
|
+
`sign_up`, `sign_in`, `sign_out`, `get_session`, `send_verification_email`, `verify_email`, `forgot_password`, `reset_password`, `change_password`, `get_user`, `update_user`, `delete_account`, `list_sessions`, `revoke_session`, `revoke_other_sessions`, `get_providers`, `start_oauth`, `exchange_oauth_code`, `handle_oauth_redirect`.
|
|
211
|
+
|
|
212
|
+
Outside a web framework, `TausiClient` keeps the token in memory. Pass `token_store=` with any object that has `get()` and `set(token)` to keep it elsewhere; `MappingTokenStore(some_dict)` stores it in a dict-like session.
|
|
213
|
+
|
|
214
|
+
## Errors
|
|
215
|
+
|
|
216
|
+
Every failure is a `TausiError` with `status`, `code`, `message`, `details`, `request_id` and `retry_after` (seconds, for `RATE_LIMITED`). Network failures have `status == 0` and code `NETWORK_ERROR` or `TIMEOUT`. Branch on `code`, not on the message.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
try:
|
|
220
|
+
client.sign_in(email, password)
|
|
221
|
+
except TausiError as e:
|
|
222
|
+
if e.code == "EMAIL_NOT_VERIFIED":
|
|
223
|
+
...
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Custom HTTP
|
|
227
|
+
|
|
228
|
+
The default transport is `urllib` from the standard library. To use `requests`, `httpx` or a test double, pass `transport=` to either client: a callable `(method, url, headers, body, timeout) -> tausi.HttpResponse`.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
234
|
+
.venv/bin/pytest
|
|
235
|
+
```
|
tausi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# tausi (Python)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [Tausi](https://tausi.app), with Django, Django REST framework and Flask integrations. It needs Python 3.9 or later and has **no runtime dependencies**: it uses the standard library for HTTP.
|
|
4
|
+
|
|
5
|
+
| | Key | Use it for |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `tausi.TausiBackend` | secret key `sk_…` | Protecting your API (verifying session tokens) and managing your project's users. |
|
|
8
|
+
| `tausi.TausiClient` | publishable key `pk_…` | Server-rendered apps where Python signs the user in and keeps their token in the session, plus scripts and CLIs. |
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pip install tausi # core
|
|
12
|
+
pip install "tausi[django]" # + Django
|
|
13
|
+
pip install "tausi[flask]" # + Flask
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Protect your API
|
|
17
|
+
|
|
18
|
+
Your frontend (built with `@tausi-auth/js`, Flutter, etc.) sends the user's session token as `Authorization: Bearer <token>`. Your backend checks it with the secret key:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import os
|
|
22
|
+
from tausi import TausiBackend
|
|
23
|
+
|
|
24
|
+
tausi = TausiBackend("https://tausi.app", os.environ["TAUSI_SECRET_KEY"])
|
|
25
|
+
|
|
26
|
+
auth = tausi.authenticate_request(request) # any request with .headers, or the header value itself
|
|
27
|
+
if auth is None:
|
|
28
|
+
... # 401
|
|
29
|
+
print(auth.user.id, auth.user.email, auth.session.expires_at)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`authenticate_request()` and `verify_session(token)` return `None` when a token is invalid, expired, revoked or belongs to another project. They raise `TausiError` only when the problem is on your side: a wrong key, a network failure or a 5xx response.
|
|
33
|
+
|
|
34
|
+
### Django
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# settings.py
|
|
38
|
+
TAUSI_URL = "https://tausi.app"
|
|
39
|
+
TAUSI_SECRET_KEY = os.environ["TAUSI_SECRET_KEY"]
|
|
40
|
+
|
|
41
|
+
# views.py
|
|
42
|
+
from django.http import JsonResponse
|
|
43
|
+
from tausi.django import tausi_required, tausi_optional
|
|
44
|
+
|
|
45
|
+
@tausi_required # 401 JSON without a valid token; works on async views too
|
|
46
|
+
def me(request):
|
|
47
|
+
return JsonResponse(request.tausi.user.to_dict())
|
|
48
|
+
|
|
49
|
+
@tausi_optional # request.tausi is an AuthResult or None
|
|
50
|
+
def feed(request):
|
|
51
|
+
...
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
To set `request.tausi` on every request, add `"tausi.django.TausiAuthMiddleware"` to `MIDDLEWARE`. Only requests that carry a bearer token cost a call to Tausi. When Tausi can't be reached, the decorators and middleware answer `503` with code `AUTH_UNAVAILABLE`.
|
|
55
|
+
|
|
56
|
+
Each setting falls back to the environment variable of the same name (`TAUSI_URL`, `TAUSI_SECRET_KEY`, `TAUSI_PUBLISHABLE_KEY`). `TAUSI_TIMEOUT` sets the request timeout in seconds (default 10).
|
|
57
|
+
|
|
58
|
+
### Django REST framework
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
REST_FRAMEWORK = {
|
|
62
|
+
"DEFAULT_AUTHENTICATION_CLASSES": ["tausi.django.rest_framework.TausiAuthentication"],
|
|
63
|
+
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
In views, `request.user` is a `tausi.User` (with `is_authenticated = True`) and `request.auth` is the full `AuthResult`. Requests without a bearer token fall through to your other authentication classes.
|
|
68
|
+
|
|
69
|
+
### Flask
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from flask import Flask, g
|
|
73
|
+
from tausi.flask import Tausi
|
|
74
|
+
|
|
75
|
+
app = Flask(__name__)
|
|
76
|
+
app.config["TAUSI_URL"] = "https://tausi.app"
|
|
77
|
+
app.config["TAUSI_SECRET_KEY"] = os.environ["TAUSI_SECRET_KEY"]
|
|
78
|
+
tausi = Tausi(app) # or tausi = Tausi(); tausi.init_app(app)
|
|
79
|
+
|
|
80
|
+
@app.get("/api/me")
|
|
81
|
+
@tausi.required # 401 JSON without a valid token
|
|
82
|
+
def me():
|
|
83
|
+
return g.tausi.user.to_dict()
|
|
84
|
+
|
|
85
|
+
@app.get("/api/feed")
|
|
86
|
+
@tausi.optional # g.tausi is an AuthResult or None
|
|
87
|
+
def feed():
|
|
88
|
+
...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Manage users
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
page = tausi.users.list(q="asha", verified=True, page=1, page_size=50) # Page[User]
|
|
95
|
+
for user in page.items:
|
|
96
|
+
print(user.email, user.status, user.active_sessions, user.last_sign_in_at)
|
|
97
|
+
|
|
98
|
+
detail = tausi.users.get(user_id) # {"user": User, "accounts": [...], "sessions": [...], "recentAudit": [...]}
|
|
99
|
+
tausi.users.update(user_id, name="Asha", email_verified=True)
|
|
100
|
+
tausi.users.revoke_sessions(user_id) # sign out everywhere
|
|
101
|
+
tausi.users.delete(user_id)
|
|
102
|
+
tausi.sessions.list(page=1)
|
|
103
|
+
tausi.get_project() # startup check that the key works
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
From Django you get the configured backend with `tausi.django.get_backend()`; from Flask, `tausi.backend` inside an app context.
|
|
107
|
+
|
|
108
|
+
## Server-rendered sign-in
|
|
109
|
+
|
|
110
|
+
When your Python app renders the sign-in form itself, use `TausiClient` with the publishable key. The token stays in the server-side session and never reaches the browser.
|
|
111
|
+
|
|
112
|
+
### Django
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# settings.py: TAUSI_PUBLISHABLE_KEY = os.environ["TAUSI_PUBLISHABLE_KEY"], plus SessionMiddleware
|
|
116
|
+
from django.shortcuts import redirect, render
|
|
117
|
+
from tausi import TausiError
|
|
118
|
+
from tausi.django import get_client, tausi_login_required
|
|
119
|
+
|
|
120
|
+
def sign_in(request):
|
|
121
|
+
if request.method == "POST":
|
|
122
|
+
try:
|
|
123
|
+
get_client(request).sign_in(request.POST["email"], request.POST["password"])
|
|
124
|
+
return redirect(request.GET.get("next", "/"))
|
|
125
|
+
except TausiError as e:
|
|
126
|
+
return render(request, "sign_in.html", {"error": e.message, "code": e.code})
|
|
127
|
+
return render(request, "sign_in.html")
|
|
128
|
+
|
|
129
|
+
def sign_out(request):
|
|
130
|
+
get_client(request).sign_out()
|
|
131
|
+
return redirect("/")
|
|
132
|
+
|
|
133
|
+
@tausi_login_required # redirects to settings.LOGIN_URL with ?next=
|
|
134
|
+
def dashboard(request):
|
|
135
|
+
return render(request, "dashboard.html", {"user": request.tausi.user})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Flask
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
@app.post("/sign-in")
|
|
142
|
+
def login():
|
|
143
|
+
try:
|
|
144
|
+
tausi.client().sign_in(request.form["email"], request.form["password"])
|
|
145
|
+
except TausiError as e:
|
|
146
|
+
return render_template("sign_in.html", error=e.message), 400
|
|
147
|
+
return redirect(request.args.get("next", "/"))
|
|
148
|
+
|
|
149
|
+
@app.get("/dashboard")
|
|
150
|
+
@tausi.login_required(login_endpoint="login")
|
|
151
|
+
def dashboard():
|
|
152
|
+
return render_template("dashboard.html", user=g.tausi.user)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Social sign-in (PKCE)
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
def start_google(request):
|
|
159
|
+
start = get_client(request).start_oauth("google", redirect_to="https://example.com/auth/callback")
|
|
160
|
+
request.session["tausi_verifier"] = start.code_verifier
|
|
161
|
+
return redirect(start.url)
|
|
162
|
+
|
|
163
|
+
def callback(request):
|
|
164
|
+
verifier = request.session.pop("tausi_verifier", "")
|
|
165
|
+
try:
|
|
166
|
+
get_client(request).handle_oauth_redirect(request.build_absolute_uri(), verifier)
|
|
167
|
+
except TausiError as e:
|
|
168
|
+
return render(request, "sign_in.html", {"error": e.message})
|
|
169
|
+
return redirect("/")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`redirect_to` must be on one of the project's allowed origins.
|
|
173
|
+
|
|
174
|
+
### All client methods
|
|
175
|
+
|
|
176
|
+
`sign_up`, `sign_in`, `sign_out`, `get_session`, `send_verification_email`, `verify_email`, `forgot_password`, `reset_password`, `change_password`, `get_user`, `update_user`, `delete_account`, `list_sessions`, `revoke_session`, `revoke_other_sessions`, `get_providers`, `start_oauth`, `exchange_oauth_code`, `handle_oauth_redirect`.
|
|
177
|
+
|
|
178
|
+
Outside a web framework, `TausiClient` keeps the token in memory. Pass `token_store=` with any object that has `get()` and `set(token)` to keep it elsewhere; `MappingTokenStore(some_dict)` stores it in a dict-like session.
|
|
179
|
+
|
|
180
|
+
## Errors
|
|
181
|
+
|
|
182
|
+
Every failure is a `TausiError` with `status`, `code`, `message`, `details`, `request_id` and `retry_after` (seconds, for `RATE_LIMITED`). Network failures have `status == 0` and code `NETWORK_ERROR` or `TIMEOUT`. Branch on `code`, not on the message.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
try:
|
|
186
|
+
client.sign_in(email, password)
|
|
187
|
+
except TausiError as e:
|
|
188
|
+
if e.code == "EMAIL_NOT_VERIFIED":
|
|
189
|
+
...
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Custom HTTP
|
|
193
|
+
|
|
194
|
+
The default transport is `urllib` from the standard library. To use `requests`, `httpx` or a test double, pass `transport=` to either client: a callable `(method, url, headers, body, timeout) -> tausi.HttpResponse`.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
200
|
+
.venv/bin/pytest
|
|
201
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tausi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for Tausi: verify sessions and manage users from your backend, with Django and Flask integrations."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "Tausi" }]
|
|
14
|
+
keywords = ["tausi", "auth", "authentication", "sessions", "oauth", "django", "flask"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Framework :: Django",
|
|
18
|
+
"Framework :: Flask",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP :: Session",
|
|
23
|
+
"Topic :: Security",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
django = ["django>=4.2"]
|
|
30
|
+
flask = ["flask>=2.3"]
|
|
31
|
+
dev = ["pytest>=8", "django>=4.2", "flask>=2.3", "djangorestframework>=3.15"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://tausi.app"
|
|
35
|
+
Documentation = "https://github.com/Nelacommunity/tausi-auth/tree/main/sdks/python#readme"
|
|
36
|
+
Repository = "https://github.com/Nelacommunity/tausi-auth"
|
|
37
|
+
Issues = "https://github.com/Nelacommunity/tausi-auth/issues"
|
|
38
|
+
Changelog = "https://github.com/Nelacommunity/tausi-auth/blob/main/sdks/python/CHANGELOG.md"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/tausi"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
include = ["src/tausi", "tests", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Official Python SDK for Tausi.
|
|
2
|
+
|
|
3
|
+
* :class:`TausiBackend` (secret key ``sk_...``): verify session tokens and manage users.
|
|
4
|
+
* :class:`TausiClient` (publishable key ``pk_...``): sign users in from server-rendered apps, scripts and CLIs.
|
|
5
|
+
* ``tausi.django`` and ``tausi.flask``: framework integrations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._http import SDK_VERSION, HttpResponse, Transport, urllib_transport
|
|
9
|
+
from .backend import TausiBackend, bearer_token
|
|
10
|
+
from .client import TausiClient, oauth_error_message, parse_oauth_redirect, random_device_id
|
|
11
|
+
from .errors import TausiError
|
|
12
|
+
from .models import AuthResult, OAuthStart, Page, Session, User
|
|
13
|
+
from .pkce import create_pkce
|
|
14
|
+
from .token_store import MappingTokenStore, MemoryTokenStore, TokenStore
|
|
15
|
+
|
|
16
|
+
__version__ = SDK_VERSION
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"SDK_VERSION",
|
|
20
|
+
"AuthResult",
|
|
21
|
+
"HttpResponse",
|
|
22
|
+
"MappingTokenStore",
|
|
23
|
+
"MemoryTokenStore",
|
|
24
|
+
"OAuthStart",
|
|
25
|
+
"Page",
|
|
26
|
+
"Session",
|
|
27
|
+
"TausiBackend",
|
|
28
|
+
"TausiClient",
|
|
29
|
+
"TausiError",
|
|
30
|
+
"TokenStore",
|
|
31
|
+
"Transport",
|
|
32
|
+
"User",
|
|
33
|
+
"bearer_token",
|
|
34
|
+
"create_pkce",
|
|
35
|
+
"oauth_error_message",
|
|
36
|
+
"parse_oauth_redirect",
|
|
37
|
+
"random_device_id",
|
|
38
|
+
"urllib_transport",
|
|
39
|
+
]
|