lowcoai-auth 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.
@@ -0,0 +1,17 @@
1
+ node_modules
2
+ npm/engage/dist
3
+ dist
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.egg-info/
9
+ .venv/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ build/
14
+
15
+ # Dart / Flutter
16
+ .dart_tool/
17
+ pubspec.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lowco AI
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.
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.5
2
+ Name: lowcoai-auth
3
+ Version: 0.1.0
4
+ Summary: Framework-agnostic Python helpers for the lowco OAuth 2.0 Authorization Code + PKCE flow.
5
+ Project-URL: Repository, https://github.com/lowcoai/lowco-sdk
6
+ Project-URL: Source, https://github.com/lowcoai/lowco-sdk/tree/main/python/auth
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: auth,lowco,oauth,oauth2,oidc,pkce,sdk
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: httpx<1,>=0.25
15
+ Requires-Dist: typing-extensions>=4.7
16
+ Provides-Extra: dev
17
+ Requires-Dist: mypy>=1.10; extra == 'dev'
18
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
19
+ Requires-Dist: pytest>=8; extra == 'dev'
20
+ Requires-Dist: ruff>=0.5; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # lowcoai-auth
24
+
25
+ Framework-agnostic Python helpers for the **lowco** OAuth 2.0 Authorization Code + PKCE flow: the server-side counterpart of the React SDK [`@lowcoai/auth`](../../npm/auth), with the same wire behaviour and the same persisted token shape. Works with Flask, Django, FastAPI, Starlette, CLIs with a loopback redirect, and so on. Sync and asyncio clients with the same surface, built on [httpx](https://www.python-httpx.org/). Python 3.10+.
26
+
27
+ ```bash
28
+ pip install lowcoai-auth
29
+ ```
30
+
31
+ ## Quick start (Flask)
32
+
33
+ ```python
34
+ from flask import Flask, redirect, request, session
35
+ from lowcoai.auth import AuthorizationRequest, LowcoAuthClient, LowcoAuthError, get_user
36
+
37
+ app = Flask(__name__)
38
+ app.secret_key = "..."
39
+
40
+ auth = LowcoAuthClient(
41
+ "https://api.lowco.ai/v1/identity", # domain
42
+ "https://auth.lowco.ai/login", # login_page_url
43
+ "tenant_123",
44
+ "client_abc",
45
+ redirect_uri="https://app.example.com/auth/callback",
46
+ )
47
+
48
+
49
+ @app.get("/login")
50
+ def login():
51
+ req = auth.create_authorization_request(app_state={"returnTo": request.args.get("next", "/")})
52
+ session["lowco_auth"] = req.to_dict() # holds the PKCE verifier until the callback
53
+ return redirect(req.url)
54
+
55
+
56
+ @app.get("/auth/callback")
57
+ def callback():
58
+ req = AuthorizationRequest.from_dict(session.pop("lowco_auth"))
59
+ try:
60
+ session["tokens"] = auth.handle_redirect_callback(request.url, req)
61
+ except LowcoAuthError as err:
62
+ return f"Sign-in failed: {err.message}", 400
63
+ return redirect((req.app_state or {}).get("returnTo", "/"))
64
+
65
+
66
+ @app.get("/me")
67
+ def me():
68
+ try:
69
+ tokens = auth.get_valid_tokens(session.get("tokens")) # refreshes when needed
70
+ except LowcoAuthError:
71
+ return redirect("/login")
72
+ session["tokens"] = tokens
73
+ return {"user": get_user(tokens), "access_token_expires_at": tokens["expires_at"]}
74
+ ```
75
+
76
+ ### asyncio
77
+
78
+ ```python
79
+ from lowcoai.auth import AsyncLowcoAuthClient
80
+
81
+ async with AsyncLowcoAuthClient(
82
+ domain, login_page_url, tenant_id, client_id, redirect_uri=redirect_uri
83
+ ) as auth:
84
+ req = auth.create_authorization_request() # sync: no I/O
85
+ tokens = await auth.handle_redirect_callback(callback_url, req)
86
+ tokens = await auth.get_valid_tokens(tokens)
87
+ ```
88
+
89
+ `AsyncLowcoAuthClient` has the same methods as `LowcoAuthClient`; the network methods are coroutines and `create_authorization_request` stays synchronous. Close it with `await auth.aclose()` if you don't use `async with`.
90
+
91
+ ### CLI with a loopback redirect
92
+
93
+ ```python
94
+ req = auth.create_authorization_request(redirect_uri="http://127.0.0.1:8765/callback")
95
+ webbrowser.open(req.url)
96
+ callback_path = wait_for_one_request_on_port(8765) # e.g. "/callback?code=...&state=..."
97
+ tokens = auth.handle_redirect_callback(callback_path, req)
98
+ ```
99
+
100
+ ## Options
101
+
102
+ | Argument | Description |
103
+ | ---------------- | ----------------------------------------------------------------------------------------- |
104
+ | `domain` | Auth server base, e.g. `https://api.lowco.ai/v1/identity`. Token endpoint: `{domain}/tenants/{tenant_id}/oauth/token`. |
105
+ | `login_page_url` | Hosted login page the user is sent to. A query string already on it is kept. |
106
+ | `tenant_id` | Tenant identifier. |
107
+ | `client_id` | OAuth client identifier (public client: no secret is sent, PKCE protects the code). |
108
+ | `redirect_uri` | Default callback URL. Required here or per `create_authorization_request` call. |
109
+ | `scope` | Default scope, `"openid profile email offline_access"`. |
110
+ | `timeout` | Per-request timeout in seconds (default `30`). `None` disables it. |
111
+ | `http_client` | Your own `httpx.Client` / `httpx.AsyncClient` (proxies, retries, test transports). It is not closed by the SDK. |
112
+
113
+ The client stores nothing per user, so one instance can be shared by the whole app. `auth.token_endpoint` returns the token URL.
114
+
115
+ ## Surface
116
+
117
+ | Call | What it does |
118
+ | ---- | ------------ |
119
+ | `create_authorization_request(*, redirect_uri=None, scope=None, app_state=None)` | Generates `state`, `nonce` (24 random bytes each) and a PKCE `code_verifier` (48 bytes), all base64url without padding, and returns an `AuthorizationRequest` whose `url` is `login_page_url` plus `tenant_id`, `client_id`, `redirect_uri`, `response_type=code`, `scope`, `state`, `nonce`, `code_challenge` (S256) and `code_challenge_method=S256`. No I/O. |
120
+ | `handle_redirect_callback(callback_url, request)` | Reads `code` / `state` (full URL, `/path?query` or bare query), raises `LowcoAuthError` on an OAuth `error`, a missing `code` / `state` or a `state` mismatch (constant-time compare), then calls `exchange_code`. |
121
+ | `exchange_code(code, request)` | `POST` form `grant_type=authorization_code`, `tenant_id`, `client_id`, `code`, `code_verifier`, `redirect_uri`. Returns a `TokenSet`. |
122
+ | `refresh_tokens(tokens_or_refresh_token)` | `POST` form `grant_type=refresh_token`, `tenant_id`, `client_id`, `refresh_token`. Given a `TokenSet`, the response is merged over it (a refresh token the server does not rotate is kept). |
123
+ | `get_valid_tokens(tokens)` | Returns `tokens` unchanged (same object) while the access token is valid, refreshes when there is a refresh token, else raises `LowcoAuthError("Session expired.")` (`"Not authenticated."` without tokens). |
124
+
125
+ Module-level helpers:
126
+
127
+ | Helper | Description |
128
+ | ------ | ----------- |
129
+ | `is_access_token_valid(tokens, skew_seconds=60)` | `expires_at` is more than `skew_seconds` in the future. |
130
+ | `can_recover_session(tokens)` | Access token still valid, or a refresh token is present. |
131
+ | `get_user(tokens)` | Claims of the ID token (else the access token) as a `UserProfile`: `sub`, `email`, `name`, `picture` plus every other claim. |
132
+
133
+ > **`get_user` does not verify the JWT signature.** Use it for display only (showing the user's name, avatar, email). For authorisation decisions, validate the access token on the server that receives it.
134
+
135
+ ## Data shapes
136
+
137
+ `TokenSet` is the dict the React SDK persists (under `lowco:tokens:<tenantId>:<clientId>`), so the two can share stored sessions:
138
+
139
+ ```python
140
+ {
141
+ "access_token": "…", # required
142
+ "refresh_token": "…",
143
+ "id_token": "…",
144
+ "expires_at": 1790000000000, # epoch MILLISECONDS (JavaScript Date.now() units)
145
+ "scope": "openid profile email offline_access",
146
+ "token_type": "Bearer",
147
+ "expires_in": 3600, # other response fields are kept, like the React SDK
148
+ }
149
+ ```
150
+
151
+ `expires_at` is `now_ms + expires_in * 1000`, computed when the tokens arrive. Note the unit: compare it with `time.time() * 1000`, not `time.time()`.
152
+
153
+ `AuthorizationRequest` is a frozen dataclass (`url`, `state`, `nonce`, `code_verifier`, `redirect_uri`, `scope`, `app_state`). `to_dict()` / `AuthorizationRequest.from_dict(...)` turn it into and back from a JSON-serialisable dict, so it can live in a server-side session or an encrypted cookie between the redirect and the callback. `from_dict` needs `state`, `code_verifier` and `redirect_uri` only, so you can drop `url` to save space. `code_verifier` must only travel back to your own server: keep it in the session (server-side, or a signed / encrypted cookie) and never hand it to page scripts or third parties. It is left out of `repr()`.
154
+
155
+ ## Errors
156
+
157
+ Flow failures raise `LowcoAuthError` with `message`, `status_code`, `code` and `body`:
158
+
159
+ | Situation | `message` | Other fields |
160
+ | --------- | --------- | ------------ |
161
+ | Token endpoint non-2xx (code grant) | `Authorization code exchange failed.` | `status_code`, `body`, `code` = the response's `error` (e.g. `"invalid_grant"`) |
162
+ | Token endpoint non-2xx (refresh) | `Refresh token exchange failed.` | same |
163
+ | 2xx without a usable `access_token` | `… The token response has no access_token.` | `status_code`, `body` |
164
+ | Callback with `?error=…` | `error_description`, or `Authorization failed (<error>).` | `code` = `error` |
165
+ | Callback without `code` / `state` | `Missing code or state in the OAuth callback URL.` | |
166
+ | Callback `state` differs from the request's | `OAuth state mismatch.` | |
167
+ | `get_valid_tokens` without tokens / expired without refresh token | `Not authenticated.` / `Session expired.` | |
168
+ | `refresh_tokens` with a `TokenSet` lacking `refresh_token` | `No refresh token available.` | |
169
+
170
+ `ValueError` is raised for missing constructor arguments, a missing `redirect_uri`, or a blank `code` / refresh token string. Network failures and timeouts surface as `httpx` exceptions.
171
+
172
+ ## Not included
173
+
174
+ Logout in the React SDK only clears local storage and navigates away; there is no server call to port. Delete the stored `TokenSet` from your session to log a user out.
@@ -0,0 +1,152 @@
1
+ # lowcoai-auth
2
+
3
+ Framework-agnostic Python helpers for the **lowco** OAuth 2.0 Authorization Code + PKCE flow: the server-side counterpart of the React SDK [`@lowcoai/auth`](../../npm/auth), with the same wire behaviour and the same persisted token shape. Works with Flask, Django, FastAPI, Starlette, CLIs with a loopback redirect, and so on. Sync and asyncio clients with the same surface, built on [httpx](https://www.python-httpx.org/). Python 3.10+.
4
+
5
+ ```bash
6
+ pip install lowcoai-auth
7
+ ```
8
+
9
+ ## Quick start (Flask)
10
+
11
+ ```python
12
+ from flask import Flask, redirect, request, session
13
+ from lowcoai.auth import AuthorizationRequest, LowcoAuthClient, LowcoAuthError, get_user
14
+
15
+ app = Flask(__name__)
16
+ app.secret_key = "..."
17
+
18
+ auth = LowcoAuthClient(
19
+ "https://api.lowco.ai/v1/identity", # domain
20
+ "https://auth.lowco.ai/login", # login_page_url
21
+ "tenant_123",
22
+ "client_abc",
23
+ redirect_uri="https://app.example.com/auth/callback",
24
+ )
25
+
26
+
27
+ @app.get("/login")
28
+ def login():
29
+ req = auth.create_authorization_request(app_state={"returnTo": request.args.get("next", "/")})
30
+ session["lowco_auth"] = req.to_dict() # holds the PKCE verifier until the callback
31
+ return redirect(req.url)
32
+
33
+
34
+ @app.get("/auth/callback")
35
+ def callback():
36
+ req = AuthorizationRequest.from_dict(session.pop("lowco_auth"))
37
+ try:
38
+ session["tokens"] = auth.handle_redirect_callback(request.url, req)
39
+ except LowcoAuthError as err:
40
+ return f"Sign-in failed: {err.message}", 400
41
+ return redirect((req.app_state or {}).get("returnTo", "/"))
42
+
43
+
44
+ @app.get("/me")
45
+ def me():
46
+ try:
47
+ tokens = auth.get_valid_tokens(session.get("tokens")) # refreshes when needed
48
+ except LowcoAuthError:
49
+ return redirect("/login")
50
+ session["tokens"] = tokens
51
+ return {"user": get_user(tokens), "access_token_expires_at": tokens["expires_at"]}
52
+ ```
53
+
54
+ ### asyncio
55
+
56
+ ```python
57
+ from lowcoai.auth import AsyncLowcoAuthClient
58
+
59
+ async with AsyncLowcoAuthClient(
60
+ domain, login_page_url, tenant_id, client_id, redirect_uri=redirect_uri
61
+ ) as auth:
62
+ req = auth.create_authorization_request() # sync: no I/O
63
+ tokens = await auth.handle_redirect_callback(callback_url, req)
64
+ tokens = await auth.get_valid_tokens(tokens)
65
+ ```
66
+
67
+ `AsyncLowcoAuthClient` has the same methods as `LowcoAuthClient`; the network methods are coroutines and `create_authorization_request` stays synchronous. Close it with `await auth.aclose()` if you don't use `async with`.
68
+
69
+ ### CLI with a loopback redirect
70
+
71
+ ```python
72
+ req = auth.create_authorization_request(redirect_uri="http://127.0.0.1:8765/callback")
73
+ webbrowser.open(req.url)
74
+ callback_path = wait_for_one_request_on_port(8765) # e.g. "/callback?code=...&state=..."
75
+ tokens = auth.handle_redirect_callback(callback_path, req)
76
+ ```
77
+
78
+ ## Options
79
+
80
+ | Argument | Description |
81
+ | ---------------- | ----------------------------------------------------------------------------------------- |
82
+ | `domain` | Auth server base, e.g. `https://api.lowco.ai/v1/identity`. Token endpoint: `{domain}/tenants/{tenant_id}/oauth/token`. |
83
+ | `login_page_url` | Hosted login page the user is sent to. A query string already on it is kept. |
84
+ | `tenant_id` | Tenant identifier. |
85
+ | `client_id` | OAuth client identifier (public client: no secret is sent, PKCE protects the code). |
86
+ | `redirect_uri` | Default callback URL. Required here or per `create_authorization_request` call. |
87
+ | `scope` | Default scope, `"openid profile email offline_access"`. |
88
+ | `timeout` | Per-request timeout in seconds (default `30`). `None` disables it. |
89
+ | `http_client` | Your own `httpx.Client` / `httpx.AsyncClient` (proxies, retries, test transports). It is not closed by the SDK. |
90
+
91
+ The client stores nothing per user, so one instance can be shared by the whole app. `auth.token_endpoint` returns the token URL.
92
+
93
+ ## Surface
94
+
95
+ | Call | What it does |
96
+ | ---- | ------------ |
97
+ | `create_authorization_request(*, redirect_uri=None, scope=None, app_state=None)` | Generates `state`, `nonce` (24 random bytes each) and a PKCE `code_verifier` (48 bytes), all base64url without padding, and returns an `AuthorizationRequest` whose `url` is `login_page_url` plus `tenant_id`, `client_id`, `redirect_uri`, `response_type=code`, `scope`, `state`, `nonce`, `code_challenge` (S256) and `code_challenge_method=S256`. No I/O. |
98
+ | `handle_redirect_callback(callback_url, request)` | Reads `code` / `state` (full URL, `/path?query` or bare query), raises `LowcoAuthError` on an OAuth `error`, a missing `code` / `state` or a `state` mismatch (constant-time compare), then calls `exchange_code`. |
99
+ | `exchange_code(code, request)` | `POST` form `grant_type=authorization_code`, `tenant_id`, `client_id`, `code`, `code_verifier`, `redirect_uri`. Returns a `TokenSet`. |
100
+ | `refresh_tokens(tokens_or_refresh_token)` | `POST` form `grant_type=refresh_token`, `tenant_id`, `client_id`, `refresh_token`. Given a `TokenSet`, the response is merged over it (a refresh token the server does not rotate is kept). |
101
+ | `get_valid_tokens(tokens)` | Returns `tokens` unchanged (same object) while the access token is valid, refreshes when there is a refresh token, else raises `LowcoAuthError("Session expired.")` (`"Not authenticated."` without tokens). |
102
+
103
+ Module-level helpers:
104
+
105
+ | Helper | Description |
106
+ | ------ | ----------- |
107
+ | `is_access_token_valid(tokens, skew_seconds=60)` | `expires_at` is more than `skew_seconds` in the future. |
108
+ | `can_recover_session(tokens)` | Access token still valid, or a refresh token is present. |
109
+ | `get_user(tokens)` | Claims of the ID token (else the access token) as a `UserProfile`: `sub`, `email`, `name`, `picture` plus every other claim. |
110
+
111
+ > **`get_user` does not verify the JWT signature.** Use it for display only (showing the user's name, avatar, email). For authorisation decisions, validate the access token on the server that receives it.
112
+
113
+ ## Data shapes
114
+
115
+ `TokenSet` is the dict the React SDK persists (under `lowco:tokens:<tenantId>:<clientId>`), so the two can share stored sessions:
116
+
117
+ ```python
118
+ {
119
+ "access_token": "…", # required
120
+ "refresh_token": "…",
121
+ "id_token": "…",
122
+ "expires_at": 1790000000000, # epoch MILLISECONDS (JavaScript Date.now() units)
123
+ "scope": "openid profile email offline_access",
124
+ "token_type": "Bearer",
125
+ "expires_in": 3600, # other response fields are kept, like the React SDK
126
+ }
127
+ ```
128
+
129
+ `expires_at` is `now_ms + expires_in * 1000`, computed when the tokens arrive. Note the unit: compare it with `time.time() * 1000`, not `time.time()`.
130
+
131
+ `AuthorizationRequest` is a frozen dataclass (`url`, `state`, `nonce`, `code_verifier`, `redirect_uri`, `scope`, `app_state`). `to_dict()` / `AuthorizationRequest.from_dict(...)` turn it into and back from a JSON-serialisable dict, so it can live in a server-side session or an encrypted cookie between the redirect and the callback. `from_dict` needs `state`, `code_verifier` and `redirect_uri` only, so you can drop `url` to save space. `code_verifier` must only travel back to your own server: keep it in the session (server-side, or a signed / encrypted cookie) and never hand it to page scripts or third parties. It is left out of `repr()`.
132
+
133
+ ## Errors
134
+
135
+ Flow failures raise `LowcoAuthError` with `message`, `status_code`, `code` and `body`:
136
+
137
+ | Situation | `message` | Other fields |
138
+ | --------- | --------- | ------------ |
139
+ | Token endpoint non-2xx (code grant) | `Authorization code exchange failed.` | `status_code`, `body`, `code` = the response's `error` (e.g. `"invalid_grant"`) |
140
+ | Token endpoint non-2xx (refresh) | `Refresh token exchange failed.` | same |
141
+ | 2xx without a usable `access_token` | `… The token response has no access_token.` | `status_code`, `body` |
142
+ | Callback with `?error=…` | `error_description`, or `Authorization failed (<error>).` | `code` = `error` |
143
+ | Callback without `code` / `state` | `Missing code or state in the OAuth callback URL.` | |
144
+ | Callback `state` differs from the request's | `OAuth state mismatch.` | |
145
+ | `get_valid_tokens` without tokens / expired without refresh token | `Not authenticated.` / `Session expired.` | |
146
+ | `refresh_tokens` with a `TokenSet` lacking `refresh_token` | `No refresh token available.` | |
147
+
148
+ `ValueError` is raised for missing constructor arguments, a missing `redirect_uri`, or a blank `code` / refresh token string. Network failures and timeouts surface as `httpx` exceptions.
149
+
150
+ ## Not included
151
+
152
+ Logout in the React SDK only clears local storage and navigates away; there is no server call to port. Delete the stored `TokenSet` from your session to log a user out.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lowcoai-auth"
7
+ version = "0.1.0"
8
+ description = "Framework-agnostic Python helpers for the lowco OAuth 2.0 Authorization Code + PKCE flow."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "httpx>=0.25,<1",
14
+ "typing-extensions>=4.7",
15
+ ]
16
+ keywords = ["auth", "oauth", "oauth2", "pkce", "oidc", "lowco", "sdk"]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "Typing :: Typed",
20
+ "Framework :: AsyncIO",
21
+ ]
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/lowcoai/lowco-sdk"
25
+ Source = "https://github.com/lowcoai/lowco-sdk/tree/main/python/auth"
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8", "pytest-asyncio>=0.23", "mypy>=1.10", "ruff>=0.5"]
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/lowcoai"]
32
+
33
+ [tool.pytest.ini_options]
34
+ pythonpath = ["src"]
35
+ asyncio_mode = "auto"
36
+
37
+ [tool.mypy]
38
+ strict = true
39
+ mypy_path = "src"
40
+ explicit_package_bases = true
41
+ warn_return_any = false
42
+
43
+ [tool.ruff]
44
+ line-length = 100
45
+ target-version = "py310"
46
+
47
+ [tool.ruff.lint]
48
+ select = ["E", "F", "W", "I", "UP", "B"]
@@ -0,0 +1,28 @@
1
+ """Framework-agnostic helpers for the lowco OAuth 2.0 Authorization Code + PKCE flow."""
2
+
3
+ from . import types as _types
4
+ from ._base import DEFAULT_SCOPE
5
+ from ._client import AsyncLowcoAuthClient, LowcoAuthClient
6
+ from ._errors import LowcoAuthError
7
+ from ._tokens import (
8
+ TOKEN_REFRESH_SKEW_SECONDS,
9
+ can_recover_session,
10
+ get_user,
11
+ is_access_token_valid,
12
+ )
13
+ from .types import * # noqa: F403
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "DEFAULT_SCOPE",
19
+ "TOKEN_REFRESH_SKEW_SECONDS",
20
+ "AsyncLowcoAuthClient",
21
+ "LowcoAuthClient",
22
+ "LowcoAuthError",
23
+ "__version__",
24
+ "can_recover_session",
25
+ "get_user",
26
+ "is_access_token_valid",
27
+ ]
28
+ __all__ += _types.__all__