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 ADDED
@@ -0,0 +1,6 @@
1
+ # Make the KrogerAPI class available at the package level
2
+ from kroger_api.kroger_api import KrogerAPI
3
+ from . import auth
4
+ from . import utils
5
+
6
+ __all__ = ['KrogerAPI', 'auth', 'utils']
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,6 @@
1
+ """
2
+ Authentication modules for Kroger API client.
3
+ """
4
+ from .interactive import authenticate_user, switch_to_client_credentials
5
+
6
+ __all__ = ['authenticate_user', 'switch_to_client_credentials']
@@ -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