oauth-cli-kit 0.1.3__tar.gz → 0.1.5__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.
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/PKG-INFO +2 -2
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/flow.py +46 -70
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/pyproject.toml +1 -1
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/.gitignore +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/LICENSE +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/README.md +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/__init__.py +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/constants.py +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/models.py +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/pkce.py +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/providers/__init__.py +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/providers/openai_codex.py +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/server.py +0 -0
- {oauth_cli_kit-0.1.3 → oauth_cli_kit-0.1.5}/oauth_cli_kit/storage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oauth-cli-kit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Reusable OAuth 2.0 + PKCE helpers for CLI applications
|
|
5
5
|
Author: nanobot contributors
|
|
6
6
|
License: MIT
|
|
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Python: >=3.11
|
|
15
|
-
Requires-Dist: httpx>=0.
|
|
15
|
+
Requires-Dist: httpx>=0.26.0
|
|
16
16
|
Requires-Dist: platformdirs>=4.0.0
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
import threading
|
|
8
9
|
import time
|
|
@@ -25,10 +26,21 @@ from oauth_cli_kit.server import _start_local_server
|
|
|
25
26
|
from oauth_cli_kit.storage import FileTokenStorage, TokenStorage, _FileLock
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
def _httpx_client_kwargs(proxy: str | None) -> dict[str, object]:
|
|
30
|
+
return {"timeout": 30.0, "proxy": proxy, "trust_env": False} if proxy else {"timeout": 30.0}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _should_open_browser() -> bool:
|
|
34
|
+
if sys.platform.startswith("linux"):
|
|
35
|
+
return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
28
39
|
def _exchange_code_for_token_async(
|
|
29
40
|
code: str,
|
|
30
41
|
verifier: str,
|
|
31
42
|
provider: OAuthProviderConfig,
|
|
43
|
+
proxy: str | None = None,
|
|
32
44
|
) -> Callable[[], OAuthToken]:
|
|
33
45
|
async def _run() -> OAuthToken:
|
|
34
46
|
data = {
|
|
@@ -38,7 +50,7 @@ def _exchange_code_for_token_async(
|
|
|
38
50
|
"code_verifier": verifier,
|
|
39
51
|
"redirect_uri": provider.redirect_uri,
|
|
40
52
|
}
|
|
41
|
-
async with httpx.AsyncClient(
|
|
53
|
+
async with httpx.AsyncClient(**_httpx_client_kwargs(proxy)) as client:
|
|
42
54
|
response = await client.post(
|
|
43
55
|
provider.token_url,
|
|
44
56
|
data=data,
|
|
@@ -61,13 +73,13 @@ def _exchange_code_for_token_async(
|
|
|
61
73
|
return _run
|
|
62
74
|
|
|
63
75
|
|
|
64
|
-
def _refresh_token(refresh_token: str, provider: OAuthProviderConfig) -> OAuthToken:
|
|
76
|
+
def _refresh_token(refresh_token: str, provider: OAuthProviderConfig, proxy: str | None = None) -> OAuthToken:
|
|
65
77
|
data = {
|
|
66
78
|
"grant_type": "refresh_token",
|
|
67
79
|
"refresh_token": refresh_token,
|
|
68
80
|
"client_id": provider.client_id,
|
|
69
81
|
}
|
|
70
|
-
with httpx.Client(
|
|
82
|
+
with httpx.Client(**_httpx_client_kwargs(proxy)) as client:
|
|
71
83
|
response = client.post(
|
|
72
84
|
provider.token_url,
|
|
73
85
|
data=data,
|
|
@@ -92,6 +104,7 @@ def get_token(
|
|
|
92
104
|
provider: OAuthProviderConfig = OPENAI_CODEX_PROVIDER,
|
|
93
105
|
storage: TokenStorage | None = None,
|
|
94
106
|
min_ttl_seconds: int = 60,
|
|
107
|
+
proxy: str | None = None,
|
|
95
108
|
) -> OAuthToken:
|
|
96
109
|
"""Get an available token (refresh if needed)."""
|
|
97
110
|
storage = storage or FileTokenStorage(token_filename=provider.token_filename)
|
|
@@ -112,7 +125,7 @@ def get_token(
|
|
|
112
125
|
if token.expires - now_ms > min_ttl_seconds * 1000:
|
|
113
126
|
return token
|
|
114
127
|
try:
|
|
115
|
-
refreshed = _refresh_token(token.refresh, provider)
|
|
128
|
+
refreshed = _refresh_token(token.refresh, provider, proxy)
|
|
116
129
|
storage.save(refreshed)
|
|
117
130
|
return refreshed
|
|
118
131
|
except Exception:
|
|
@@ -123,43 +136,14 @@ def get_token(
|
|
|
123
136
|
raise
|
|
124
137
|
|
|
125
138
|
|
|
126
|
-
async def _read_stdin_line() -> str:
|
|
127
|
-
loop = asyncio.get_running_loop()
|
|
128
|
-
if hasattr(loop, "add_reader") and sys.stdin:
|
|
129
|
-
future: asyncio.Future[str] = loop.create_future()
|
|
130
|
-
|
|
131
|
-
def _on_readable() -> None:
|
|
132
|
-
line = sys.stdin.readline()
|
|
133
|
-
if not future.done():
|
|
134
|
-
future.set_result(line)
|
|
135
|
-
|
|
136
|
-
try:
|
|
137
|
-
loop.add_reader(sys.stdin, _on_readable)
|
|
138
|
-
except Exception:
|
|
139
|
-
return await loop.run_in_executor(None, sys.stdin.readline)
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
return await future
|
|
143
|
-
finally:
|
|
144
|
-
try:
|
|
145
|
-
loop.remove_reader(sys.stdin)
|
|
146
|
-
except Exception:
|
|
147
|
-
pass
|
|
148
|
-
|
|
149
|
-
return await loop.run_in_executor(None, sys.stdin.readline)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
async def _await_manual_input(print_fn: Callable[[str], None]) -> str:
|
|
153
|
-
print_fn("[cyan]Paste the authorization code (or full redirect URL), or wait for the browser callback:[/cyan]")
|
|
154
|
-
return await _read_stdin_line()
|
|
155
|
-
|
|
156
|
-
|
|
157
139
|
def login_oauth_interactive(
|
|
158
140
|
print_fn: Callable[[str], None],
|
|
159
141
|
prompt_fn: Callable[[str], str],
|
|
160
142
|
provider: OAuthProviderConfig = OPENAI_CODEX_PROVIDER,
|
|
161
143
|
originator: str | None = None,
|
|
162
144
|
storage: TokenStorage | None = None,
|
|
145
|
+
proxy: str | None = None,
|
|
146
|
+
open_browser: bool | None = None,
|
|
163
147
|
) -> OAuthToken:
|
|
164
148
|
"""Interactive login flow."""
|
|
165
149
|
|
|
@@ -190,12 +174,17 @@ def login_oauth_interactive(
|
|
|
190
174
|
loop.call_soon_threadsafe(code_future.set_result, code_value)
|
|
191
175
|
|
|
192
176
|
server, server_error = _start_local_server(state, on_code=_notify)
|
|
193
|
-
|
|
177
|
+
should_open_browser = _should_open_browser() if open_browser is None else open_browser
|
|
178
|
+
if should_open_browser:
|
|
179
|
+
print_fn("[cyan]A browser window will open for login. If it doesn't, open this URL manually:[/cyan]")
|
|
180
|
+
else:
|
|
181
|
+
print_fn("[yellow]No graphical browser detected. Open this URL in a browser:[/yellow]")
|
|
194
182
|
print_fn(url)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
183
|
+
if should_open_browser:
|
|
184
|
+
try:
|
|
185
|
+
webbrowser.open(url)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
199
188
|
|
|
200
189
|
if not server and server_error:
|
|
201
190
|
print_fn(
|
|
@@ -207,38 +196,25 @@ def login_oauth_interactive(
|
|
|
207
196
|
|
|
208
197
|
code: str | None = None
|
|
209
198
|
try:
|
|
210
|
-
if server:
|
|
199
|
+
if server and should_open_browser:
|
|
211
200
|
print_fn("[dim]Waiting for browser callback...[/dim]")
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
for task in done:
|
|
224
|
-
try:
|
|
225
|
-
result = task.result()
|
|
226
|
-
except asyncio.TimeoutError:
|
|
227
|
-
result = None
|
|
228
|
-
if not result:
|
|
229
|
-
continue
|
|
230
|
-
if task is manual_task:
|
|
231
|
-
parsed_code, parsed_state = _parse_authorization_input(result)
|
|
232
|
-
if parsed_state and parsed_state != state:
|
|
233
|
-
raise RuntimeError("State validation failed.")
|
|
234
|
-
code = parsed_code
|
|
235
|
-
else:
|
|
236
|
-
code = result
|
|
237
|
-
if code:
|
|
238
|
-
break
|
|
201
|
+
try:
|
|
202
|
+
code = await asyncio.wait_for(code_future, timeout=120)
|
|
203
|
+
except asyncio.TimeoutError:
|
|
204
|
+
server.shutdown()
|
|
205
|
+
server.server_close()
|
|
206
|
+
server = None
|
|
207
|
+
elif server:
|
|
208
|
+
server.shutdown()
|
|
209
|
+
server.server_close()
|
|
210
|
+
server = None
|
|
239
211
|
|
|
240
212
|
if not code:
|
|
241
|
-
prompt =
|
|
213
|
+
prompt = (
|
|
214
|
+
"Please open the URL above in a browser, then paste the full redirect URL:"
|
|
215
|
+
if not should_open_browser
|
|
216
|
+
else "Please paste the callback URL or authorization code:"
|
|
217
|
+
)
|
|
242
218
|
raw = await loop.run_in_executor(None, prompt_fn, prompt)
|
|
243
219
|
parsed_code, parsed_state = _parse_authorization_input(raw)
|
|
244
220
|
if parsed_state and parsed_state != state:
|
|
@@ -249,7 +225,7 @@ def login_oauth_interactive(
|
|
|
249
225
|
raise RuntimeError("Authorization code not found.")
|
|
250
226
|
|
|
251
227
|
print_fn("[dim]Exchanging authorization code for tokens...[/dim]")
|
|
252
|
-
token = await _exchange_code_for_token_async(code, verifier, provider)()
|
|
228
|
+
token = await _exchange_code_for_token_async(code, verifier, provider, proxy)()
|
|
253
229
|
(storage or FileTokenStorage(token_filename=provider.token_filename)).save(token)
|
|
254
230
|
return token
|
|
255
231
|
finally:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|