tastytrade 11.0.2__tar.gz → 11.0.4__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.
- {tastytrade-11.0.2 → tastytrade-11.0.4}/PKG-INFO +1 -1
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/sessions.rst +0 -9
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/__init__.py +1 -1
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/order.py +6 -2
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/session.py +9 -11
- {tastytrade-11.0.2 → tastytrade-11.0.4}/uv.lock +5 -3
- tastytrade-11.0.2/tastytrade/oauth.py +0 -129
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.github/CONTRIBUTING.md +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.github/FUNDING.yml +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.github/pull_request_template.md +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.github/workflows/python-app.yml +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.github/workflows/python-publish-test.yml +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.github/workflows/python-publish.yml +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.gitignore +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.python-version +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/.readthedocs.yaml +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/LICENSE +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/Makefile +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/README.md +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/Makefile +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/account-streamer.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/accounts.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/account.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/dxfeed.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/instruments.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/market-data.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/market-sessions.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/metrics.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/order.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/search.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/session.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/streamer.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/utils.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/api/watchlists.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/conf.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/data-streamer.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/img/netliq.png +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/index.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/installation.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/instruments.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/make.bat +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/market-data.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/market-sessions.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/orders.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/sync-async.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/docs/watchlists.rst +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/pyproject.toml +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/account.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/__init__.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/candle.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/event.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/greeks.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/profile.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/quote.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/summary.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/theoprice.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/timeandsale.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/trade.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/dxfeed/underlying.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/instruments.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/market_data.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/market_sessions.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/metrics.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/py.typed +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/search.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/streamer.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/utils.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tastytrade/watchlists.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/__init__.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/conftest.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_account.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_dxfeed.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_instruments.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_market_data.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_market_sessions.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_metrics.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_search.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_session.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_streamer.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_utils.py +0 -0
- {tastytrade-11.0.2 → tastytrade-11.0.4}/tests/test_watchlists.py +0 -0
|
@@ -13,15 +13,6 @@ Generating an initial refresh token
|
|
|
13
13
|
|
|
14
14
|
In order to generate an initial refresh token, you have two options. The easiest way is to simply generate one from Tastytrade's website: go to OAuth Applications > Manage > Create Grant to get a new refresh token, **which you should also save**.
|
|
15
15
|
|
|
16
|
-
The other option (which is actually the only option for sandbox accounts) is to run this helper code:
|
|
17
|
-
|
|
18
|
-
.. code-block:: python
|
|
19
|
-
|
|
20
|
-
from tastytrade.oauth import login
|
|
21
|
-
login(is_test=True)
|
|
22
|
-
|
|
23
|
-
This will open up a web interface in your browser where you'll be prompted to paste your client ID and client secret. These credentials will then be used to connect your application to Tastytrade. After following the steps in your browser, you should see your new refresh token in the browser and in the console.
|
|
24
|
-
|
|
25
16
|
At this point, OAuth is now setup correctly! Doing these steps once is sufficient for **indefinite usage** of ``Session`` for authentication to the API, since refresh tokens never expire. From now on you can simply authenticate with your client secret and refresh token.
|
|
26
17
|
|
|
27
18
|
Creating a session
|
|
@@ -4,7 +4,7 @@ API_URL = "https://api.tastyworks.com"
|
|
|
4
4
|
API_VERSION = "20251101"
|
|
5
5
|
CERT_URL = "https://api.cert.tastyworks.com"
|
|
6
6
|
VAST_URL = "https://vast.tastyworks.com"
|
|
7
|
-
VERSION = "11.0.
|
|
7
|
+
VERSION = "11.0.4"
|
|
8
8
|
|
|
9
9
|
__version__ = VERSION
|
|
10
10
|
version_str: str = f"tastyware/tastytrade:v{VERSION}"
|
|
@@ -254,6 +254,8 @@ class NewOrder(TastytradeData):
|
|
|
254
254
|
preflight_id: str | None = None
|
|
255
255
|
rules: OrderRule | None = None
|
|
256
256
|
advanced_instructions: AdvancedInstructions | None = None
|
|
257
|
+
#: External identifier for the order, used to track orders across systems
|
|
258
|
+
external_identifier: str | None = None
|
|
257
259
|
|
|
258
260
|
@computed_field # type: ignore[misc]
|
|
259
261
|
@property
|
|
@@ -316,8 +318,8 @@ class PlacedOrder(TastytradeData):
|
|
|
316
318
|
cancelled_at: datetime | None = None
|
|
317
319
|
cancel_user_id: str | None = None
|
|
318
320
|
cancel_username: str | None = None
|
|
319
|
-
replacing_order_id:
|
|
320
|
-
replaces_order_id:
|
|
321
|
+
replacing_order_id: int | None = None
|
|
322
|
+
replaces_order_id: int | None = None
|
|
321
323
|
in_flight_at: datetime | None = None
|
|
322
324
|
live_at: datetime | None = None
|
|
323
325
|
received_at: datetime | None = None
|
|
@@ -330,6 +332,8 @@ class PlacedOrder(TastytradeData):
|
|
|
330
332
|
preflight_id: str | int | None = None
|
|
331
333
|
order_rule: OrderRule | None = None
|
|
332
334
|
source: str | None = None
|
|
335
|
+
#: External identifier for the order, used to track orders across systems
|
|
336
|
+
external_identifier: str | None = None
|
|
333
337
|
|
|
334
338
|
@model_validator(mode="before")
|
|
335
339
|
@classmethod
|
|
@@ -9,8 +9,8 @@ from typing_extensions import Self
|
|
|
9
9
|
|
|
10
10
|
from tastytrade import API_URL, API_VERSION, CERT_URL, logger
|
|
11
11
|
from tastytrade.utils import (
|
|
12
|
-
TZ,
|
|
13
12
|
TastytradeData,
|
|
13
|
+
now_in_new_york,
|
|
14
14
|
validate_and_parse,
|
|
15
15
|
validate_response,
|
|
16
16
|
)
|
|
@@ -274,11 +274,9 @@ class Session:
|
|
|
274
274
|
#: Refresh token for the user
|
|
275
275
|
self.refresh_token = refresh_token
|
|
276
276
|
# The headers to use for API requests
|
|
277
|
-
headers = {
|
|
278
|
-
|
|
279
|
-
"Accept-Version"
|
|
280
|
-
"Content-Type": "application/json",
|
|
281
|
-
}
|
|
277
|
+
headers = {"Accept": "application/json", "Content-Type": "application/json"}
|
|
278
|
+
if not is_test: # not accepted in sandbox
|
|
279
|
+
headers["Accept-Version"] = API_VERSION
|
|
282
280
|
#: httpx client for sync requests
|
|
283
281
|
self.sync_client = Client(
|
|
284
282
|
base_url=(CERT_URL if is_test else API_URL), headers=headers, proxy=proxy
|
|
@@ -288,7 +286,7 @@ class Session:
|
|
|
288
286
|
base_url=self.sync_client.base_url, headers=headers, proxy=proxy
|
|
289
287
|
)
|
|
290
288
|
#: expiration for streamer token
|
|
291
|
-
self.streamer_expiration =
|
|
289
|
+
self.streamer_expiration = now_in_new_york()
|
|
292
290
|
self.refresh()
|
|
293
291
|
|
|
294
292
|
def _streamer_refresh(self) -> None:
|
|
@@ -325,14 +323,14 @@ class Session:
|
|
|
325
323
|
self.session_token = data["access_token"]
|
|
326
324
|
token_lifetime: int = data.get("expires_in", 900)
|
|
327
325
|
#: expiration for session token
|
|
328
|
-
self.session_expiration =
|
|
326
|
+
self.session_expiration = now_in_new_york() + timedelta(seconds=token_lifetime)
|
|
329
327
|
logger.debug(f"Refreshed token, expires in {token_lifetime}ms")
|
|
330
328
|
auth_headers = {"Authorization": f"Bearer {self.session_token}"}
|
|
331
329
|
# update the httpx clients with the new token
|
|
332
330
|
self.sync_client.headers.update(auth_headers)
|
|
333
331
|
self.async_client.headers.update(auth_headers)
|
|
334
332
|
# update the streamer token if necessary
|
|
335
|
-
if self.streamer_expiration < self.session_expiration:
|
|
333
|
+
if not self.is_test and self.streamer_expiration < self.session_expiration:
|
|
336
334
|
self._streamer_refresh()
|
|
337
335
|
|
|
338
336
|
async def a_refresh(self) -> None:
|
|
@@ -357,14 +355,14 @@ class Session:
|
|
|
357
355
|
# update the relevant tokens
|
|
358
356
|
self.session_token = data["access_token"]
|
|
359
357
|
token_lifetime: int = data.get("expires_in", 900)
|
|
360
|
-
self.session_expiration =
|
|
358
|
+
self.session_expiration = now_in_new_york() + timedelta(token_lifetime)
|
|
361
359
|
logger.debug(f"Refreshed token, expires in {token_lifetime}ms")
|
|
362
360
|
auth_headers = {"Authorization": f"Bearer {self.session_token}"}
|
|
363
361
|
# update the httpx clients with the new token
|
|
364
362
|
self.sync_client.headers.update(auth_headers)
|
|
365
363
|
self.async_client.headers.update(auth_headers)
|
|
366
364
|
# update the streamer token if necessary
|
|
367
|
-
if self.streamer_expiration < self.session_expiration:
|
|
365
|
+
if not self.is_test and self.streamer_expiration < self.session_expiration:
|
|
368
366
|
# Pull streamer tokens and urls
|
|
369
367
|
data = await self._a_get("/api-quote-tokens")
|
|
370
368
|
# Auth token for dxfeed websocket
|
|
@@ -1368,15 +1368,15 @@ wheels = [
|
|
|
1368
1368
|
|
|
1369
1369
|
[[package]]
|
|
1370
1370
|
name = "pyright"
|
|
1371
|
-
version = "1.1.
|
|
1371
|
+
version = "1.1.407"
|
|
1372
1372
|
source = { registry = "https://pypi.org/simple" }
|
|
1373
1373
|
dependencies = [
|
|
1374
1374
|
{ name = "nodeenv" },
|
|
1375
1375
|
{ name = "typing-extensions" },
|
|
1376
1376
|
]
|
|
1377
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1377
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" }
|
|
1378
1378
|
wheels = [
|
|
1379
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1379
|
+
{ url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" },
|
|
1380
1380
|
]
|
|
1381
1381
|
|
|
1382
1382
|
[[package]]
|
|
@@ -1549,6 +1549,8 @@ wheels = [
|
|
|
1549
1549
|
{ url = "https://files.pythonhosted.org/packages/42/cd/85b422d24ee2096eaf6faa360c95ef9bdb59097d19b9624cebce4dd9bc2a/ruamel.yaml.clib-0.2.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:808c7190a0fe7ae7014c42f73897cf8e9ef14ff3aa533450e51b1e72ec5239ad", size = 725028, upload-time = "2025-09-22T19:51:19.782Z" },
|
|
1550
1550
|
{ url = "https://files.pythonhosted.org/packages/4d/ac/99e6e0ea2584f84f447069d0187fe411e9b5deb7e3ddecda25001cfc7a95/ruamel.yaml.clib-0.2.14-cp39-cp39-win32.whl", hash = "sha256:6d5472f63a31b042aadf5ed28dd3ef0523da49ac17f0463e10fda9c4a2773352", size = 100915, upload-time = "2025-09-22T19:51:21.764Z" },
|
|
1551
1551
|
{ url = "https://files.pythonhosted.org/packages/5d/8d/846e43369658958c99d959bb7774136fff9210f9017d91a4277818ceafbf/ruamel.yaml.clib-0.2.14-cp39-cp39-win_amd64.whl", hash = "sha256:8dd3c2cc49caa7a8d64b67146462aed6723a0495e44bf0aa0a2e94beaa8432f6", size = 118706, upload-time = "2025-09-22T19:51:20.878Z" },
|
|
1552
|
+
{ url = "https://files.pythonhosted.org/packages/e7/cd/150fdb96b8fab27fe08d8a59fe67554568727981806e6bc2677a16081ec7/ruamel_yaml_clib-0.2.14-cp314-cp314-win32.whl", hash = "sha256:9b4104bf43ca0cd4e6f738cb86326a3b2f6eef00f417bd1e7efb7bdffe74c539", size = 102394, upload-time = "2025-11-14T21:57:36.703Z" },
|
|
1553
|
+
{ url = "https://files.pythonhosted.org/packages/bd/e6/a3fa40084558c7e1dc9546385f22a93949c890a8b2e445b2ba43935f51da/ruamel_yaml_clib-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:13997d7d354a9890ea1ec5937a219817464e5cc344805b37671562a401ca3008", size = 122673, upload-time = "2025-11-14T21:57:38.177Z" },
|
|
1552
1554
|
]
|
|
1553
1555
|
|
|
1554
1556
|
[[package]]
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import webbrowser
|
|
3
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
4
|
-
from urllib.parse import parse_qs
|
|
5
|
-
|
|
6
|
-
import httpx
|
|
7
|
-
|
|
8
|
-
PORT = 8000
|
|
9
|
-
REDIRECT_URI = f"http://localhost:{PORT}"
|
|
10
|
-
SCOPES = ["read", "trade", "openid"]
|
|
11
|
-
|
|
12
|
-
authorize_url = "https://my.tastytrade.com/auth.html"
|
|
13
|
-
token_url = "https://api.tastyworks.com/oauth/token"
|
|
14
|
-
client_id = ""
|
|
15
|
-
client_secret = ""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
root_page = """
|
|
19
|
-
<!DOCTYPE html>
|
|
20
|
-
<html lang="en-us">
|
|
21
|
-
<head>
|
|
22
|
-
<meta charset="utf-8">
|
|
23
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
24
|
-
<title>OAuth Setup</title>
|
|
25
|
-
<!-- Favicon -->
|
|
26
|
-
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
|
27
|
-
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
|
28
|
-
rel="stylesheet"
|
|
29
|
-
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
|
30
|
-
crossorigin="anonymous">
|
|
31
|
-
</head>
|
|
32
|
-
<body>
|
|
33
|
-
<div class="container position-absolute top-50 start-50 translate-middle"
|
|
34
|
-
style="width: 400px">
|
|
35
|
-
<form method="POST">
|
|
36
|
-
<div class="row mb-3">
|
|
37
|
-
<input type="text"
|
|
38
|
-
required
|
|
39
|
-
placeholder="Client ID"
|
|
40
|
-
name="client_id"
|
|
41
|
-
class="form-control">
|
|
42
|
-
</div>
|
|
43
|
-
<div class="row mb-3">
|
|
44
|
-
<input type="password"
|
|
45
|
-
required
|
|
46
|
-
placeholder="Client Secret"
|
|
47
|
-
name="client_secret"
|
|
48
|
-
class="form-control">
|
|
49
|
-
</div>
|
|
50
|
-
<div class="row mb-3">
|
|
51
|
-
<button type="submit" class="btn btn-success">Connect</button>
|
|
52
|
-
</div>
|
|
53
|
-
</form>
|
|
54
|
-
</div>
|
|
55
|
-
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
56
|
-
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
|
57
|
-
crossorigin="anonymous"></script>
|
|
58
|
-
</body>
|
|
59
|
-
</html>
|
|
60
|
-
""".encode()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class RequestHandler(BaseHTTPRequestHandler):
|
|
64
|
-
def do_POST(self) -> None:
|
|
65
|
-
global client_id, client_secret
|
|
66
|
-
content_length = int(self.headers["Content-Length"])
|
|
67
|
-
raw = self.rfile.read(content_length)
|
|
68
|
-
data = parse_qs(raw.decode("utf-8"))
|
|
69
|
-
client_id = data["client_id"][0]
|
|
70
|
-
client_secret = data["client_secret"][0]
|
|
71
|
-
|
|
72
|
-
# Redirect to login page using API key submitted by user
|
|
73
|
-
self.send_response(302)
|
|
74
|
-
query_string = "&".join(
|
|
75
|
-
[
|
|
76
|
-
"response_type=code",
|
|
77
|
-
f"redirect_uri={REDIRECT_URI}",
|
|
78
|
-
f"client_id={data['client_id'][0]}",
|
|
79
|
-
f"scope={' '.join(SCOPES)}",
|
|
80
|
-
]
|
|
81
|
-
)
|
|
82
|
-
url = f"{authorize_url}?{query_string}"
|
|
83
|
-
self.send_header("Location", url)
|
|
84
|
-
self.end_headers()
|
|
85
|
-
|
|
86
|
-
def do_GET(self) -> None:
|
|
87
|
-
global client_id, client_secret
|
|
88
|
-
# Serve root page with sign in link
|
|
89
|
-
if self.path == "/":
|
|
90
|
-
self.send_response(200)
|
|
91
|
-
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
92
|
-
self.end_headers()
|
|
93
|
-
self.wfile.write(root_page)
|
|
94
|
-
else:
|
|
95
|
-
# Check if query path contains case insensitive "code="
|
|
96
|
-
code_match = re.search(r"code=(.+)", self.path, re.I)
|
|
97
|
-
if code_match and client_id and client_secret:
|
|
98
|
-
user_auth_code = code_match[1]
|
|
99
|
-
post_data = {
|
|
100
|
-
"grant_type": "authorization_code",
|
|
101
|
-
"client_id": client_id,
|
|
102
|
-
"client_secret": client_secret,
|
|
103
|
-
"redirect_uri": REDIRECT_URI,
|
|
104
|
-
"code": user_auth_code,
|
|
105
|
-
}
|
|
106
|
-
response = httpx.post(token_url, data=post_data)
|
|
107
|
-
token_access = response.json()
|
|
108
|
-
refresh_token: str = token_access["refresh_token"]
|
|
109
|
-
print(refresh_token)
|
|
110
|
-
|
|
111
|
-
self.send_response(200)
|
|
112
|
-
self.send_header("Content-type", "text/html; charset=utf-8")
|
|
113
|
-
self.end_headers()
|
|
114
|
-
self.wfile.write(refresh_token.encode())
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def login(is_test: bool = False) -> None:
|
|
118
|
-
"""
|
|
119
|
-
Starts a local HTTP server and opens the browser to OAuth login.
|
|
120
|
-
Designed for one-time use to get a refresh token.
|
|
121
|
-
"""
|
|
122
|
-
global authorize_url, token_url
|
|
123
|
-
if is_test:
|
|
124
|
-
authorize_url = "https://cert-my.staging-tasty.works/auth.html"
|
|
125
|
-
token_url = "https://api.cert.tastyworks.com/oauth/token"
|
|
126
|
-
httpd = HTTPServer(("", PORT), RequestHandler)
|
|
127
|
-
print(f"Opening url: {REDIRECT_URI}")
|
|
128
|
-
webbrowser.open(REDIRECT_URI)
|
|
129
|
-
httpd.serve_forever()
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|