kroger-api 0.1.0__py3-none-any.whl
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.
- kroger_api/__init__.py +6 -0
- kroger_api/api/__init__.py +0 -0
- kroger_api/api/authorization.py +107 -0
- kroger_api/api/cart.py +37 -0
- kroger_api/api/identity.py +27 -0
- kroger_api/api/location.py +185 -0
- kroger_api/api/product.py +84 -0
- kroger_api/auth/__init__.py +6 -0
- kroger_api/auth/interactive.py +173 -0
- kroger_api/client.py +376 -0
- kroger_api/kroger_api.py +41 -0
- kroger_api/token_storage.py +81 -0
- kroger_api/utils/__init__.py +3 -0
- kroger_api/utils/env.py +76 -0
- kroger_api/utils/oauth.py +133 -0
- kroger_api-0.1.0.dist-info/METADATA +244 -0
- kroger_api-0.1.0.dist-info/RECORD +20 -0
- kroger_api-0.1.0.dist-info/WHEEL +5 -0
- kroger_api-0.1.0.dist-info/licenses/LICENSE +21 -0
- kroger_api-0.1.0.dist-info/top_level.txt +1 -0
kroger_api/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Dict, Optional, Any
|
|
2
|
+
|
|
3
|
+
from kroger_api.client import KrogerClient
|
|
4
|
+
from kroger_api.token_storage import get_refresh_token, load_token
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthorizationAPI:
|
|
8
|
+
"""
|
|
9
|
+
Provides access to the Kroger Authorization API endpoints.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, client: KrogerClient):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the Authorization API
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
client: The KrogerClient instance
|
|
18
|
+
"""
|
|
19
|
+
self.client = client
|
|
20
|
+
|
|
21
|
+
def get_authorization_url(self, scope: str, state: Optional[str] = None, banner: Optional[str] = None) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Get the URL to redirect the user to for authorization
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
scope: The level of access your application is requesting
|
|
27
|
+
state: A random string to verify that the response belongs to the initiated request
|
|
28
|
+
banner: Sets the chain specific branding displayed on the authorization consent screen
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The authorization URL
|
|
32
|
+
"""
|
|
33
|
+
return self.client.get_authorization_url(scope, state, banner)
|
|
34
|
+
|
|
35
|
+
def get_token_with_client_credentials(self, scope: str) -> Dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Get an access token using client credentials
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
scope: The level of access your application is requesting
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The token information
|
|
44
|
+
"""
|
|
45
|
+
return self.client.get_token_with_client_credentials(scope)
|
|
46
|
+
|
|
47
|
+
def get_token_with_authorization_code(self, code: str) -> Dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Get an access token using an authorization code
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
code: The authorization code received from the redirect
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The token information
|
|
56
|
+
"""
|
|
57
|
+
return self.client.get_token_with_authorization_code(code)
|
|
58
|
+
|
|
59
|
+
def refresh_token(self, refresh_token: str) -> Dict[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Refresh an access token using a refresh token
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
refresh_token: The refresh token received from a previous token request
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The token information
|
|
68
|
+
"""
|
|
69
|
+
return self.client.refresh_token(refresh_token)
|
|
70
|
+
|
|
71
|
+
def refresh_token_if_needed(self, token_file: str = None) -> Dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Check if the token needs refreshing and refresh it if necessary
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
token_file: The file path to load the token from
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The token information (either existing or refreshed)
|
|
80
|
+
"""
|
|
81
|
+
# Determine which token file to use
|
|
82
|
+
if token_file is None:
|
|
83
|
+
if self.client.token_file:
|
|
84
|
+
token_file = self.client.token_file
|
|
85
|
+
else:
|
|
86
|
+
token_file = ".kroger_token_user.json"
|
|
87
|
+
|
|
88
|
+
# Load the token
|
|
89
|
+
token_info = load_token(token_file)
|
|
90
|
+
|
|
91
|
+
if token_info:
|
|
92
|
+
# Test if the token is valid
|
|
93
|
+
if self.client.test_token(token_info):
|
|
94
|
+
# Token is valid, return it
|
|
95
|
+
return token_info
|
|
96
|
+
|
|
97
|
+
# Token is invalid, check if we have a refresh token
|
|
98
|
+
refresh_token = get_refresh_token(token_file)
|
|
99
|
+
if refresh_token:
|
|
100
|
+
# Try to refresh the token
|
|
101
|
+
try:
|
|
102
|
+
return self.refresh_token(refresh_token)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Failed to refresh token: {e}")
|
|
105
|
+
|
|
106
|
+
# If we couldn't refresh or there was no token, return None
|
|
107
|
+
return None
|
kroger_api/api/cart.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Dict, List, Any
|
|
2
|
+
|
|
3
|
+
from kroger_api.client import KrogerClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CartAPI:
|
|
7
|
+
"""
|
|
8
|
+
Provides access to the Kroger Cart API endpoints.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, client: KrogerClient):
|
|
12
|
+
"""
|
|
13
|
+
Initialize the Cart API
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
client: The KrogerClient instance
|
|
17
|
+
"""
|
|
18
|
+
self.client = client
|
|
19
|
+
|
|
20
|
+
def add_to_cart(self, items: List[Dict[str, Any]]) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Add items to an authenticated customer's cart
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
items: A list of items to add to the cart. Each item should be a dictionary with keys:
|
|
26
|
+
- upc: The UPC of the item
|
|
27
|
+
- quantity: The quantity of the item
|
|
28
|
+
- modality: (Optional) The modality including: DELIVERY, PICKUP
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
None if successful
|
|
32
|
+
"""
|
|
33
|
+
data = {
|
|
34
|
+
"items": items
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return self.client._make_request('PUT', '/v1/cart/add', data=data)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Dict, Any
|
|
2
|
+
|
|
3
|
+
from kroger_api.client import KrogerClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class IdentityAPI:
|
|
7
|
+
"""
|
|
8
|
+
Provides access to the Kroger Identity API endpoints.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, client: KrogerClient):
|
|
12
|
+
"""
|
|
13
|
+
Initialize the Identity API
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
client: The KrogerClient instance
|
|
17
|
+
"""
|
|
18
|
+
self.client = client
|
|
19
|
+
|
|
20
|
+
def get_profile(self) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Get the profile ID of an authenticated customer
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The user profile information
|
|
26
|
+
"""
|
|
27
|
+
return self.client._make_request('GET', '/v1/identity/profile')
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from typing import Dict, Optional, Any, List, Union
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
from kroger_api.client import KrogerClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LocationAPI:
|
|
8
|
+
"""
|
|
9
|
+
Provides access to the Kroger Location API endpoints.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, client: KrogerClient):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the Location API
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
client: The KrogerClient instance
|
|
18
|
+
"""
|
|
19
|
+
self.client = client
|
|
20
|
+
|
|
21
|
+
def search_locations(self,
|
|
22
|
+
zip_code: Optional[str] = None,
|
|
23
|
+
lat_long: Optional[str] = None,
|
|
24
|
+
lat: Optional[str] = None,
|
|
25
|
+
lon: Optional[str] = None,
|
|
26
|
+
radius_in_miles: Optional[int] = None,
|
|
27
|
+
limit: Optional[int] = None,
|
|
28
|
+
chain: Optional[str] = None,
|
|
29
|
+
department: Optional[str] = None,
|
|
30
|
+
location_id: Optional[str] = None) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Search for locations
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
zip_code: The zip code to use as a starting point for results
|
|
36
|
+
lat_long: The latitude and longitude to use as a starting point for results (comma separated)
|
|
37
|
+
lat: The latitude to use as a starting point for results
|
|
38
|
+
lon: The longitude to use as a starting point for results
|
|
39
|
+
radius_in_miles: The mile radius of results (1-100)
|
|
40
|
+
limit: The number of results to return (1-200)
|
|
41
|
+
chain: The chain name
|
|
42
|
+
department: The departmentId of the department (comma separated)
|
|
43
|
+
location_id: Comma-separated list of locationIds
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The locations matching the search criteria
|
|
47
|
+
"""
|
|
48
|
+
params = {}
|
|
49
|
+
|
|
50
|
+
if zip_code:
|
|
51
|
+
params['filter.zipCode.near'] = zip_code
|
|
52
|
+
|
|
53
|
+
if lat_long:
|
|
54
|
+
params['filter.latLong.near'] = lat_long
|
|
55
|
+
|
|
56
|
+
if lat:
|
|
57
|
+
params['filter.lat.near'] = lat
|
|
58
|
+
|
|
59
|
+
if lon:
|
|
60
|
+
params['filter.lon.near'] = lon
|
|
61
|
+
|
|
62
|
+
if radius_in_miles:
|
|
63
|
+
params['filter.radiusInMiles'] = radius_in_miles
|
|
64
|
+
|
|
65
|
+
if limit:
|
|
66
|
+
params['filter.limit'] = limit
|
|
67
|
+
|
|
68
|
+
if chain:
|
|
69
|
+
params['filter.chain'] = chain
|
|
70
|
+
|
|
71
|
+
if department:
|
|
72
|
+
params['filter.department'] = department
|
|
73
|
+
|
|
74
|
+
if location_id:
|
|
75
|
+
params['filter.locationId'] = location_id
|
|
76
|
+
|
|
77
|
+
return self.client._make_request('GET', '/v1/locations', params=params)
|
|
78
|
+
|
|
79
|
+
def get_location(self, location_id: str) -> Dict[str, Any]:
|
|
80
|
+
"""
|
|
81
|
+
Get details for a specific location
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
location_id: The locationId of the store
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The location details
|
|
88
|
+
"""
|
|
89
|
+
return self.client._make_request('GET', f'/v1/locations/{location_id}')
|
|
90
|
+
|
|
91
|
+
def location_exists(self, location_id: str) -> bool:
|
|
92
|
+
"""
|
|
93
|
+
Check if a location exists
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
location_id: The locationId of the store
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True if the location exists, False otherwise
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
self.client._make_request('HEAD', f'/v1/locations/{location_id}')
|
|
103
|
+
return True
|
|
104
|
+
except requests.exceptions.HTTPError as e:
|
|
105
|
+
if e.response.status_code == 404:
|
|
106
|
+
return False
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
def list_chains(self) -> Dict[str, Any]:
|
|
110
|
+
"""
|
|
111
|
+
Get a list of all chains
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
A list of all chains
|
|
115
|
+
"""
|
|
116
|
+
return self.client._make_request('GET', '/v1/chains')
|
|
117
|
+
|
|
118
|
+
def get_chain(self, name: str) -> Dict[str, Any]:
|
|
119
|
+
"""
|
|
120
|
+
Get details for a specific chain
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: The name of the chain
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The chain details
|
|
127
|
+
"""
|
|
128
|
+
return self.client._make_request('GET', f'/v1/chains/{name}')
|
|
129
|
+
|
|
130
|
+
def chain_exists(self, name: str) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Check if a chain exists
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
name: The name of the chain
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if the chain exists, False otherwise
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
self.client._make_request('HEAD', f'/v1/chains/{name}')
|
|
142
|
+
return True
|
|
143
|
+
except requests.exceptions.HTTPError as e:
|
|
144
|
+
if e.response.status_code == 404:
|
|
145
|
+
return False
|
|
146
|
+
raise
|
|
147
|
+
|
|
148
|
+
def list_departments(self) -> Dict[str, Any]:
|
|
149
|
+
"""
|
|
150
|
+
Get a list of all departments
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A list of all departments
|
|
154
|
+
"""
|
|
155
|
+
return self.client._make_request('GET', '/v1/departments')
|
|
156
|
+
|
|
157
|
+
def get_department(self, department_id: str) -> Dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Get details for a specific department
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
department_id: The departmentId of the department
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The department details
|
|
166
|
+
"""
|
|
167
|
+
return self.client._make_request('GET', f'/v1/departments/{department_id}')
|
|
168
|
+
|
|
169
|
+
def department_exists(self, department_id: str) -> bool:
|
|
170
|
+
"""
|
|
171
|
+
Check if a department exists
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
department_id: The departmentId of the department
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
True if the department exists, False otherwise
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
self.client._make_request('HEAD', f'/v1/departments/{department_id}')
|
|
181
|
+
return True
|
|
182
|
+
except requests.exceptions.HTTPError as e:
|
|
183
|
+
if e.response.status_code == 404:
|
|
184
|
+
return False
|
|
185
|
+
raise
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Dict, Optional, Any, List, Union
|
|
2
|
+
|
|
3
|
+
from kroger_api.client import KrogerClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProductAPI:
|
|
7
|
+
"""
|
|
8
|
+
Provides access to the Kroger Product API endpoints.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, client: KrogerClient):
|
|
12
|
+
"""
|
|
13
|
+
Initialize the Product API
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
client: The KrogerClient instance
|
|
17
|
+
"""
|
|
18
|
+
self.client = client
|
|
19
|
+
|
|
20
|
+
def search_products(self,
|
|
21
|
+
term: Optional[str] = None,
|
|
22
|
+
location_id: Optional[str] = None,
|
|
23
|
+
product_id: Optional[str] = None,
|
|
24
|
+
brand: Optional[str] = None,
|
|
25
|
+
fulfillment: Optional[str] = None,
|
|
26
|
+
start: Optional[int] = None,
|
|
27
|
+
limit: Optional[int] = None) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Search for products
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
term: A search term to filter product results
|
|
33
|
+
location_id: The locationId of the location
|
|
34
|
+
product_id: The productId of the products(s) to return (comma-separated)
|
|
35
|
+
brand: The brand name of the products to return (pipe-separated)
|
|
36
|
+
fulfillment: The available fulfillment types (comma-separated)
|
|
37
|
+
start: The number of products to skip
|
|
38
|
+
limit: The number of products to return
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The products matching the search criteria
|
|
42
|
+
"""
|
|
43
|
+
params = {}
|
|
44
|
+
|
|
45
|
+
if term:
|
|
46
|
+
params['filter.term'] = term
|
|
47
|
+
|
|
48
|
+
if location_id:
|
|
49
|
+
params['filter.locationId'] = location_id
|
|
50
|
+
|
|
51
|
+
if product_id:
|
|
52
|
+
params['filter.productId'] = product_id
|
|
53
|
+
|
|
54
|
+
if brand:
|
|
55
|
+
params['filter.brand'] = brand
|
|
56
|
+
|
|
57
|
+
if fulfillment:
|
|
58
|
+
params['filter.fulfillment'] = fulfillment
|
|
59
|
+
|
|
60
|
+
if start:
|
|
61
|
+
params['filter.start'] = start
|
|
62
|
+
|
|
63
|
+
if limit:
|
|
64
|
+
params['filter.limit'] = limit
|
|
65
|
+
|
|
66
|
+
return self.client._make_request('GET', '/v1/products', params=params)
|
|
67
|
+
|
|
68
|
+
def get_product(self, product_id: str, location_id: Optional[str] = None) -> Dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Get details for a specific product
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
product_id: The productId of the product
|
|
74
|
+
location_id: The locationId of the location for pricing and availability
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The product details
|
|
78
|
+
"""
|
|
79
|
+
params = {}
|
|
80
|
+
|
|
81
|
+
if location_id:
|
|
82
|
+
params['filter.locationId'] = location_id
|
|
83
|
+
|
|
84
|
+
return self.client._make_request('GET', f'/v1/products/{product_id}', params=params)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive authentication utilities for Kroger API client.
|
|
3
|
+
This module provides functions for interactive authentication flows.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from typing import Dict, Any, Tuple, Optional
|
|
9
|
+
|
|
10
|
+
from ..utils.oauth import (
|
|
11
|
+
start_oauth_server,
|
|
12
|
+
extract_port_from_redirect_uri,
|
|
13
|
+
generate_random_state,
|
|
14
|
+
open_browser_for_auth
|
|
15
|
+
)
|
|
16
|
+
from ..utils.env import get_redirect_uri
|
|
17
|
+
from ..kroger_api import KrogerAPI
|
|
18
|
+
from ..token_storage import load_token, get_refresh_token
|
|
19
|
+
|
|
20
|
+
def authenticate_user(scopes: str = "cart.basic:write profile.compact",
|
|
21
|
+
token_file: str = ".kroger_token_user.json",
|
|
22
|
+
timeout: int = 120) -> KrogerAPI:
|
|
23
|
+
"""
|
|
24
|
+
Authenticate a user and return a KrogerAPI client with valid tokens
|
|
25
|
+
|
|
26
|
+
This function implements the complete OAuth2 authorization code flow,
|
|
27
|
+
including token refresh and browser-based authorization.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
scopes: The OAuth scopes to request
|
|
31
|
+
token_file: The file to store the token in
|
|
32
|
+
timeout: The timeout for waiting for the authorization code (in seconds)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A KrogerAPI instance with valid tokens
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TimeoutError: If the authorization flow times out
|
|
39
|
+
ValueError: If the authentication fails for other reasons
|
|
40
|
+
"""
|
|
41
|
+
# Initialize the API client
|
|
42
|
+
kroger = KrogerAPI()
|
|
43
|
+
|
|
44
|
+
# Set the token file
|
|
45
|
+
kroger.client.token_file = token_file
|
|
46
|
+
|
|
47
|
+
# Try to load a saved token and test if it's valid
|
|
48
|
+
token_info = load_token(token_file)
|
|
49
|
+
if token_info:
|
|
50
|
+
kroger.client.token_info = token_info
|
|
51
|
+
|
|
52
|
+
# Test if the token is valid
|
|
53
|
+
try:
|
|
54
|
+
print("Found saved token, testing if it's still valid...")
|
|
55
|
+
is_valid = kroger.test_current_token()
|
|
56
|
+
if is_valid:
|
|
57
|
+
print("Token is valid - no need to log in again!")
|
|
58
|
+
return kroger
|
|
59
|
+
except Exception:
|
|
60
|
+
print("Token validation failed, will try to refresh")
|
|
61
|
+
# Token is invalid, continue with the flow
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# If token is invalid, try to refresh it
|
|
65
|
+
try:
|
|
66
|
+
refresh_token = get_refresh_token(token_file)
|
|
67
|
+
if refresh_token:
|
|
68
|
+
print("Refreshing token...")
|
|
69
|
+
token_info = kroger.authorization.refresh_token(refresh_token)
|
|
70
|
+
# Test the refreshed token
|
|
71
|
+
is_valid = kroger.test_current_token()
|
|
72
|
+
if is_valid:
|
|
73
|
+
print("Token refreshed successfully.")
|
|
74
|
+
return kroger
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"Token refresh failed: {e}")
|
|
77
|
+
print("Will proceed with new authorization...")
|
|
78
|
+
# Refresh failed, continue with the full authorization flow
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
print("Starting new OAuth authorization flow...")
|
|
82
|
+
|
|
83
|
+
# Extract the port from the redirect URI
|
|
84
|
+
redirect_uri = get_redirect_uri()
|
|
85
|
+
port = extract_port_from_redirect_uri(redirect_uri)
|
|
86
|
+
|
|
87
|
+
# Variables to store the authorization code
|
|
88
|
+
auth_code = None
|
|
89
|
+
auth_state = None
|
|
90
|
+
auth_event = threading.Event()
|
|
91
|
+
|
|
92
|
+
# Callback for when the authorization code is received
|
|
93
|
+
def on_code_received(code, state):
|
|
94
|
+
nonlocal auth_code, auth_state
|
|
95
|
+
auth_code = code
|
|
96
|
+
auth_state = state
|
|
97
|
+
auth_event.set()
|
|
98
|
+
|
|
99
|
+
# Start the server to handle the OAuth2 redirect
|
|
100
|
+
server, _ = start_oauth_server(port, on_code_received)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Generate a random state value for security
|
|
104
|
+
state = generate_random_state()
|
|
105
|
+
|
|
106
|
+
# Get the authorization URL with the required scopes
|
|
107
|
+
auth_url = kroger.authorization.get_authorization_url(
|
|
108
|
+
scope=scopes,
|
|
109
|
+
state=state
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Open the authorization URL in the default browser
|
|
113
|
+
open_browser_for_auth(auth_url)
|
|
114
|
+
|
|
115
|
+
# Wait for the authorization code (timeout after specified seconds)
|
|
116
|
+
print(f"Waiting for authorization... (timeout: {timeout} seconds)")
|
|
117
|
+
if not auth_event.wait(timeout):
|
|
118
|
+
raise TimeoutError("Authorization flow timed out. Please try again.")
|
|
119
|
+
|
|
120
|
+
# Verify the state parameter to prevent CSRF attacks
|
|
121
|
+
if auth_state != state:
|
|
122
|
+
raise ValueError("State parameter mismatch. Possible CSRF attack.")
|
|
123
|
+
|
|
124
|
+
# Exchange the authorization code for an access token
|
|
125
|
+
print("Authorization code received, exchanging for access token...")
|
|
126
|
+
token_info = kroger.authorization.get_token_with_authorization_code(auth_code)
|
|
127
|
+
|
|
128
|
+
print("Authentication successful.")
|
|
129
|
+
return kroger
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
# Ensure the server is shut down, then re-raise the exception
|
|
133
|
+
server.shutdown()
|
|
134
|
+
raise e
|
|
135
|
+
finally:
|
|
136
|
+
# Ensure the server is shut down
|
|
137
|
+
server.shutdown()
|
|
138
|
+
|
|
139
|
+
def switch_to_client_credentials(kroger: KrogerAPI, scope: str = "product.compact") -> Tuple[KrogerAPI, Dict, str]:
|
|
140
|
+
"""
|
|
141
|
+
Switch the KrogerAPI client to use client credentials flow
|
|
142
|
+
|
|
143
|
+
This is useful when switching from user token to client credentials
|
|
144
|
+
for operations that don't require user authentication.
|
|
145
|
+
|
|
146
|
+
This function DOES NOT modify the kroger.client.user_token_info or similar attributes.
|
|
147
|
+
Instead, it returns the original token info for the caller to manage.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
kroger: The KrogerAPI instance
|
|
151
|
+
scope: The scope to request
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A tuple containing:
|
|
155
|
+
- The same KrogerAPI instance with updated token
|
|
156
|
+
- The original token_info (or None)
|
|
157
|
+
- The original token_file (or None)
|
|
158
|
+
"""
|
|
159
|
+
# Save the current token info and file for later
|
|
160
|
+
user_token_info = None
|
|
161
|
+
user_token_file = None
|
|
162
|
+
|
|
163
|
+
if hasattr(kroger.client, 'token_info'):
|
|
164
|
+
user_token_info = kroger.client.token_info
|
|
165
|
+
|
|
166
|
+
if hasattr(kroger.client, 'token_file'):
|
|
167
|
+
user_token_file = kroger.client.token_file
|
|
168
|
+
|
|
169
|
+
# Get a client credentials token
|
|
170
|
+
kroger.authorization.get_token_with_client_credentials(scope)
|
|
171
|
+
|
|
172
|
+
# Return the kroger instance and the saved token info
|
|
173
|
+
return kroger, user_token_info, user_token_file
|