ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

okay limited to 250 but still a succes

+1548 -13
+11 -1
cmd/careme/main.go
··· 46 46 if err != nil { 47 47 return fmt.Errorf("failed to create recipe generator: %w", err) 48 48 } 49 - formatter := recipes.NewFormatter() 49 + 50 + ingredients, err := generator.GetIngredients(location, "Meat & Seafood", 0) //Meat \u0026 Seafood 51 + if err != nil { 52 + return fmt.Errorf("failed to get ingredients: %w", err) 53 + } 54 + for _, ingredient := range ingredients { 55 + fmt.Printf(" - %s\n", ingredient) 56 + } 57 + 58 + /*formatter := recipes.NewFormatter() 50 59 51 60 fmt.Printf("🍽️ Generating 4 weekly recipes for location: %s\n", location) 52 61 fmt.Println("🏷️ Checking current sales at local QFC/Fred Meyer...") ··· 60 69 61 70 output := formatter.FormatRecipes(generatedRecipes) 62 71 fmt.Print(output) 72 + */ 63 73 64 74 return nil 65 75 }
+9 -1
internal/config/config.go
··· 1 1 package config 2 2 3 3 import ( 4 + "fmt" 4 5 "os" 5 6 ) 6 7 ··· 43 44 }, 44 45 } 45 46 46 - return config, nil 47 + return config, validate(config) 48 + } 49 + 50 + func validate(cfg *Config) error { 51 + if cfg.Kroger.ClientID == "" || cfg.Kroger.ClientSecret == "" { 52 + return fmt.Errorf("Kroger client ID and secret must be set") 53 + } 54 + return nil 47 55 } 48 56 49 57 func getEnvOrDefault(key, defaultValue string) string {
+2
internal/kroger/client.gen.go
··· 2689 2689 Errors *string `json:"errors,omitempty"` 2690 2690 } 2691 2691 if err := json.Unmarshal(bodyBytes, &dest); err != nil { 2692 + fmt.Printf("failing at 400: %s\n", bodyBytes) 2692 2693 return nil, err 2693 2694 } 2694 2695 response.JSON400 = &dest ··· 2701 2702 } `json:"errors,omitempty"` 2702 2703 } 2703 2704 if err := json.Unmarshal(bodyBytes, &dest); err != nil { 2705 + fmt.Printf("failing at 401: %s\n", bodyBytes) 2704 2706 return nil, err 2705 2707 } 2706 2708 response.JSON401 = &dest
+71
internal/kroger/client.go
··· 1 1 package kroger 2 2 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "net/http" 9 + "net/url" 10 + "strings" 11 + ) 12 + 3 13 //go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml swagger.yaml 14 + 15 + // this wasn't in the swagger? try the jsons added next 16 + // OAuth2TokenResponse represents the response from Kroger OAuth2 token endpoint 17 + // LoggingDoer wraps an HttpRequestDoer and logs requests and responses 18 + type LoggingDoer struct { 19 + Wrapped HttpRequestDoer 20 + } 21 + 22 + func (l *LoggingDoer) Do(req *http.Request) (*http.Response, error) { 23 + fmt.Printf("Kroger Request: %s %s\nHeaders: %v\n", req.Method, req.URL.String(), req.Header) 24 + resp, err := l.Wrapped.Do(req) 25 + if err != nil { 26 + fmt.Printf("Kroger Response Error: %v\n", err) 27 + return resp, err 28 + } 29 + fmt.Printf("Kroger Response: %d %s\n", resp.StatusCode, resp.Status) 30 + return resp, err 31 + } 32 + 33 + type OAuth2TokenResponse struct { 34 + AccessToken string `json:"access_token"` 35 + TokenType string `json:"token_type"` 36 + ExpiresIn int `json:"expires_in"` 37 + Scope string `json:"scope"` 38 + } 39 + 40 + // GetOAuth2Token fetches an access token using client credentials grant 41 + func GetOAuth2Token(ctx context.Context, clientID, clientSecret string) (string, error) { 42 + endpoint := "https://api.kroger.com/v1/connect/oauth2/token" 43 + data := url.Values{} 44 + data.Set("grant_type", "client_credentials") 45 + data.Set("scope", "product.compact") 46 + 47 + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, strings.NewReader(data.Encode())) 48 + if err != nil { 49 + return "", err 50 + } 51 + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 52 + req.SetBasicAuth(clientID, clientSecret) 53 + 54 + resp, err := http.DefaultClient.Do(req) 55 + if err != nil { 56 + return "", err 57 + } 58 + defer resp.Body.Close() 59 + 60 + body, err := io.ReadAll(resp.Body) 61 + if err != nil { 62 + return "", err 63 + } 64 + 65 + if resp.StatusCode != http.StatusOK { 66 + return "", fmt.Errorf("failed to get token: %s", string(body)) 67 + } 68 + 69 + var tokenResp OAuth2TokenResponse 70 + if err := json.Unmarshal(body, &tokenResp); err != nil { 71 + return "", err 72 + } 73 + return tokenResp.AccessToken, nil 74 + }
+840
internal/kroger/opeanapi-products.json
··· 1 + { 2 + "openapi": "3.0.3", 3 + "info": { 4 + "title": "Products API", 5 + "description": "The Products API allows you to search the Kroger product catalog. <br><br>\n\n### Rate Limit\n\nThe Public Products API has a **10,000 call per day** rate limit. \n\nFor all Public APIs, we enforce the rate limit by the number of calls the client makes to the endpoint, not individual API operations. This means you can distribute the 10,000 calls across all API operations using the `/products` endpoint as you see fit. <br><br>\n\n### Pagination\n\nThe Product Search operation supports pagination with a default value of 10 results per page. Using the following parameters, you can extend and skip results in the response:\n\n`filter.limit` - Sets a limit on the number of products returned.<br>\n`filter.start` - Sets a number of results to skip in the response. \n\n**Note**: Since searching by a term acts as a fuzzy search, the order of the results can change with each new request. <br><br>\n\n### API Operations\n\nThe Products API supports the following operations: <br>\n<table>\n<tr>\n <th>Name</th>\n <th>Method</th>\n <th>Description</th>\n</tr>\n<tr>\n <td>Product search</td>\n <td>GET</td>\n <td>Allows you to find products by passing in either a search term or product Id.</td>\n</tr>\n<tr>\n <td>Product details</td>\n <td>GET</td>\n <td>Returns product details for a specific product.</td>\n</tr>\n</table><br><br>\n\n### Additional Response Data \n\nTo return the following data from the `/products` endpoint, you must include a <code>locationId</code> in the request. All operations for the products endpoint accept the <code>filter.locationId</code> query parameter.<br><br>\n<ul>\n<li>Price<br><br>Returns the following price objects:<br><br><code>price</code> - Includes both the <code>regular</code> price of the item and the <code>promo</code> price of the item.<br><code>nationalPrice</code> - Includes both the <code>regular</code> national price of the item and the national <code>promo</code> price of the item.<br><br><b>Note</b>: Seasonal products only return a price when available. Some items may not have a national prices available.</li><br>\n\n<li>Fulfillment Type<br><br>Returns the following boolean objects to indicate an item's fulfillment availability:<br><br><code>instore</code> - The item is sold in store at the given location.<br><code>shiptohome</code> - The item is available to be shipped to home.<br><code>delivery</code> - The item is available for delivery from the given location.<br><code>curbside</code> - The item is available for curbside pickup from the given location.<br><br> <b>Note</b>: The <code>instore</code> fulfillment type only indicated that the item is sold by the given location, not that it is in stock.</li><br>\n\n<li>Aisle Locations<br><br>Returns the aisle locations of the item for the given location.</li><br>\n\n<li>Inventory<br><br>Returns the <code>stockLevel</code> of the item. This property is omitted when unavailable:<br><br><code>HIGH</code> - The stock level is high.<br><code>LOW</code> - The stock level is low.<br><code>TEMPORARILY_OUT_OF_STOCK</code> - The item is temporarily out of stock.</li>\n</ul>\n", 6 + "termsOfService": "https://developer.kroger.com/terms", 7 + "contact": { 8 + "name": "API Support", 9 + "email": "APISupport@kroger.com", 10 + "url": "https://developer.kroger.com" 11 + }, 12 + "version": "1.2.4" 13 + }, 14 + "servers": [ 15 + { 16 + "url": "https://api.kroger.com", 17 + "description": "Production Environment" 18 + }, 19 + { 20 + "url": "https://api-ce.kroger.com", 21 + "description": "Certification Environment" 22 + } 23 + ], 24 + "security": [ 25 + { 26 + "ClientContext": [ 27 + "product.compact" 28 + ] 29 + } 30 + ], 31 + "paths": { 32 + "/v1/products": { 33 + "get": { 34 + "tags": [ 35 + "Products" 36 + ], 37 + "summary": "Product search", 38 + "description": "Allows you to find products by passing in either a search term or product Id.\n\n### Initial Search Value Required\n\nAn initial search value is requred for all requests. You can use either of the following parameters as an initial search value: \n\n`filter.term` - When using the term parameter, the API performs a fuzzy search based on the term provided in the string. Search results are based on how relevant the term is to the product description.\n\n`filter.brand` - When using the brand parameter, the API performs a search based on the brand provided in the string. Search results only contain products that match the brand queried for.\n\n`filter.productId` - When using the productId parameter, the API performs a query to find an exact match. The format for the productId is a 13 digit number. \n\n NOTE: If converting from a barcode omit the check digit.\n", 39 + "operationId": "productGet", 40 + "parameters": [ 41 + { 42 + "name": "filter.term", 43 + "in": "query", 44 + "description": "A search term to filter product results. As an example, you could input _milk_, _bread_, or _salt_. <br><br><b>Note</b> - Search terms are limited to a maximum of 8 words. Each new space in the search term denotes a new word.", 45 + "example": "milk", 46 + "schema": { 47 + "minLength": 3, 48 + "type": "string" 49 + } 50 + }, 51 + { 52 + "name": "filter.locationId", 53 + "in": "query", 54 + "description": "The locationId of the location. When using this filter, only products available at that location are returned.", 55 + "example": "01400943", 56 + "schema": { 57 + "maxLength": 8, 58 + "minLength": 8, 59 + "type": "string" 60 + } 61 + }, 62 + { 63 + "name": "filter.productId", 64 + "in": "query", 65 + "description": "The productId of the products(s) to return. For more than one item, the list must be comma-separated. When used, all other query parameters are ignored.", 66 + "example": "0001111060903", 67 + "schema": { 68 + "maxLength": 13, 69 + "minLength": 13, 70 + "type": "string", 71 + "maximum": 50 72 + } 73 + }, 74 + { 75 + "name": "filter.brand", 76 + "in": "query", 77 + "description": "The brand name of the products to return. When using this filter, only products by that brand are returned. Brand names are case-sensitive, and lists must be pipe-separated.", 78 + "example": "Kroger", 79 + "schema": { 80 + "type": "string" 81 + } 82 + }, 83 + { 84 + "name": "filter.fulfillment", 85 + "in": "query", 86 + "description": "'The available fulfillment types of the product(s) to return.\nFulfillment types are case-sensitive, and lists must be comma-separated.\nMust be one or more of the follow types: <ul> <li> `ais` - Available In\nStore</li> <li> `csp` - Curbside Pickup</li> <li> `dth` - Delivery To Home</li>\n<li> `sth` - Ship To Home</li> </ui>'\n", 87 + "schema": { 88 + "type": "string", 89 + "enum": [ 90 + "ais", 91 + "csp", 92 + "dth", 93 + "sth" 94 + ] 95 + } 96 + }, 97 + { 98 + "name": "filter.start", 99 + "in": "query", 100 + "description": "The number of products to skip.", 101 + "schema": { 102 + "maximum": 250, 103 + "minimum": 1, 104 + "type": "integer" 105 + } 106 + }, 107 + { 108 + "name": "filter.limit", 109 + "in": "query", 110 + "description": "The number of products to return.", 111 + "schema": { 112 + "maximum": 50, 113 + "minimum": 1, 114 + "type": "integer" 115 + } 116 + } 117 + ], 118 + "responses": { 119 + "200": { 120 + "description": "OK", 121 + "content": { 122 + "application/json": { 123 + "schema": { 124 + "$ref": "#/components/schemas/products.productsPayloadModel" 125 + } 126 + } 127 + } 128 + }, 129 + "400": { 130 + "description": "Bad Request", 131 + "content": { 132 + "application/json": { 133 + "schema": { 134 + "oneOf": [ 135 + { 136 + "$ref": "#/components/schemas/APIError" 137 + }, 138 + { 139 + "$ref": "#/components/schemas/Invalid_locationId" 140 + }, 141 + { 142 + "$ref": "#/components/schemas/Invalid_parameter" 143 + }, 144 + { 145 + "$ref": "#/components/schemas/Invalid_limit" 146 + } 147 + ] 148 + } 149 + } 150 + } 151 + }, 152 + "401": { 153 + "description": "Unauthorized", 154 + "content": { 155 + "application/json": { 156 + "schema": { 157 + "$ref": "#/components/schemas/APIError.unauthorized" 158 + } 159 + } 160 + } 161 + }, 162 + "403": { 163 + "description": "Forbidden", 164 + "content": { 165 + "application/json": { 166 + "schema": { 167 + "$ref": "#/components/schemas/APIError.forbidden" 168 + } 169 + } 170 + } 171 + }, 172 + "500": { 173 + "description": "Internal Server Error", 174 + "content": { 175 + "application/json": { 176 + "schema": { 177 + "$ref": "#/components/schemas/APIError.products.serverError" 178 + } 179 + } 180 + } 181 + } 182 + }, 183 + "security": [ 184 + { 185 + "ClientContext": [ 186 + "product.compact" 187 + ] 188 + } 189 + ], 190 + "x-code-samples": [ 191 + { 192 + "lang": "Shell", 193 + "source": "curl -X GET \\\n 'https://api.kroger.com/v1/products?filter.brand={{BRAND}}&filter.term={{TERM}}&filter.locationId={{LOCATION_ID}}' \\\n -H 'Accept: application/json' \\\n -H 'Authorization: Bearer {{TOKEN}}'\n" 194 + }, 195 + { 196 + "lang": "Go", 197 + "source": "package main\n\nimport (\n \"fmt\"\n \"net/http\"\n \"io/ioutil\"\n)\n\nfunc main() {\n\n url := \"https://api.kroger.com/v1/products?filter.brand={{BRAND}}&filter.term={{TERM}}&filter.locationId={{LOCATION_ID}}\"\n\n req, _ := http.NewRequest(\"GET\", url, nil)\n\n req.Header.Add(\"Accept\", \"application/json\")\n req.Header.Add(\"Authorization\", \"Bearer {{TOKEN}}\")\n\n res, _ := http.DefaultClient.Do(req)\n\n defer res.Body.Close()\n body, _ := ioutil.ReadAll(res.Body)\n\n fmt.Println(res)\n fmt.Println(string(body))\n\n}\n" 198 + }, 199 + { 200 + "lang": "JavaScript", 201 + "source": "var settings = {\n \"async\": true,\n \"crossDomain\": true,\n \"url\": \"https://api.kroger.com/v1/products?filter.brand={{BRAND}}&filter.term={{TERM}}&filter.locationId={{LOCATION_ID}}\",\n \"method\": \"GET\",\n \"headers\": {\n \"Accept\": \"application/json\",\n \"Authorization\": \"Bearer {{TOKEN}}\"\n }\n}\n\n$.ajax(settings).done(function (response) {\n console.log(response);\n});\n" 202 + }, 203 + { 204 + "lang": "Java", 205 + "source": "OkHttpClient client = new OkHttpClient();\n\nRequest request = new Request.Builder()\n .url(\"https://api.kroger.com/v1/products?filter.brand={{BRAND}}&filter.term={{TERM}}&filter.locationId={{LOCATION_ID}}\")\n .get()\n .addHeader(\"Accept\", \"application/json\")\n .addHeader(\"Authorization\", \"Bearer {{TOKEN}}\")\n .build();\n\nResponse response = client.newCall(request).execute();\n" 206 + } 207 + ] 208 + } 209 + }, 210 + "/v1/products/{id}": { 211 + "get": { 212 + "tags": [ 213 + "Products" 214 + ], 215 + "summary": "Product details", 216 + "description": "Provides access to the details of a specific product by using the 13 digit `productId`. If converting from a barcode omit the check digit. \n\n To return the product price, availability, and aisle location, you must include the `filter.locationId` query parameter.", 217 + "operationId": "productGetID", 218 + "parameters": [ 219 + { 220 + "name": "id", 221 + "description": "The id of the product", 222 + "in": "path", 223 + "required": true, 224 + "schema": { 225 + "oneOf": [ 226 + { 227 + "$ref": "#/components/schemas/productId" 228 + }, 229 + { 230 + "$ref": "#/components/schemas/UPC" 231 + } 232 + ] 233 + } 234 + }, 235 + { 236 + "name": "filter.locationId", 237 + "in": "query", 238 + "description": "The locationId of the location. When using this filter, only products available at that location are returned.", 239 + "example": "01400943", 240 + "schema": { 241 + "maxLength": 8, 242 + "minLength": 8, 243 + "type": "string" 244 + } 245 + } 246 + ], 247 + "responses": { 248 + "200": { 249 + "description": "OK", 250 + "content": { 251 + "application/json": { 252 + "schema": { 253 + "$ref": "#/components/schemas/products.productPayloadModel" 254 + } 255 + } 256 + } 257 + }, 258 + "400": { 259 + "description": "Bad Request", 260 + "content": { 261 + "application/json": { 262 + "schema": { 263 + "oneOf": [ 264 + { 265 + "$ref": "#/components/schemas/APIError" 266 + }, 267 + { 268 + "$ref": "#/components/schemas/Invalid_locationId" 269 + }, 270 + { 271 + "$ref": "#/components/schemas/Invalid_UPC" 272 + } 273 + ] 274 + } 275 + } 276 + } 277 + }, 278 + "401": { 279 + "description": "Unauthorized", 280 + "content": { 281 + "application/json": { 282 + "schema": { 283 + "$ref": "#/components/schemas/APIError.unauthorized" 284 + } 285 + } 286 + } 287 + }, 288 + "403": { 289 + "description": "Forbidden", 290 + "content": { 291 + "application/json": { 292 + "schema": { 293 + "$ref": "#/components/schemas/APIError.forbidden" 294 + } 295 + } 296 + } 297 + }, 298 + "500": { 299 + "description": "Internal Server Error", 300 + "content": { 301 + "application/json": { 302 + "schema": { 303 + "$ref": "#/components/schemas/APIError.products.serverError" 304 + } 305 + } 306 + } 307 + } 308 + }, 309 + "security": [ 310 + { 311 + "ClientContext": [ 312 + "product.compact" 313 + ] 314 + } 315 + ], 316 + "x-code-samples": [ 317 + { 318 + "lang": "Shell", 319 + "source": "curl -X GET \\\n 'https://api.kroger.com/v1/products/{{ID}}?filter.locationId={{LOCATION_ID}}' \\\n -H 'Accept: application/json' \\\n -H 'Authorization: Bearer {{TOKEN}}'\n" 320 + }, 321 + { 322 + "lang": "Go", 323 + "source": "package main\n\nimport (\n \"fmt\"\n \"net/http\"\n \"io/ioutil\"\n)\n\nfunc main() {\n\n url := \"https://api.kroger.com/v1/products/{{ID}}?filter.locationId={{LOCATION_ID}}\"\n\n req, _ := http.NewRequest(\"GET\", url, nil)\n\n req.Header.Add(\"Accept\", \"application/json\")\n req.Header.Add(\"Authorization\", \"Bearer {{TOKEN}}\")\n\n res, _ := http.DefaultClient.Do(req)\n\n defer res.Body.Close()\n body, _ := ioutil.ReadAll(res.Body)\n\n fmt.Println(res)\n fmt.Println(string(body))\n\n}\n" 324 + }, 325 + { 326 + "lang": "JavaScript", 327 + "source": "var settings = {\n \"async\": true,\n \"crossDomain\": true,\n \"url\": \"https://api.kroger.com/v1/products/{{ID}}?filter.locationId={{LOCATION_ID}}\",\n \"method\": \"GET\",\n \"headers\": {\n \"Accept\": \"application/json\",\n \"Authorization\": \"Bearer {{TOKEN}}\"\n }\n}\n\n$.ajax(settings).done(function (response) {\n console.log(response);\n});\n" 328 + }, 329 + { 330 + "lang": "Java", 331 + "source": "OkHttpClient client = new OkHttpClient();\n\nRequest request = new Request.Builder()\n .url(\"https://api.kroger.com/v1/products/{{ID}}?filter.locationId={{LOCATION_ID}}\")\n .get()\n .addHeader(\"Accept\", \"application/json\")\n .addHeader(\"Authorization\", \"Bearer {{TOKEN}}\")\n .build();\n\nResponse response = client.newCall(request).execute();\n" 332 + } 333 + ] 334 + } 335 + } 336 + }, 337 + "components": { 338 + "schemas": { 339 + "Invalid_locationId": { 340 + "type": "object", 341 + "properties": { 342 + "timestamp": { 343 + "type": "number", 344 + "example": 1569851999383 345 + }, 346 + "code": { 347 + "type": "string", 348 + "example": "API-4101-400" 349 + }, 350 + "reason": { 351 + "type": "string", 352 + "example": "Field 'locationId' must have a length of 8 characters" 353 + } 354 + } 355 + }, 356 + "Invalid_limit": { 357 + "type": "object", 358 + "properties": { 359 + "timestamp": { 360 + "type": "number", 361 + "example": 1569851999383 362 + }, 363 + "code": { 364 + "type": "string", 365 + "example": "API-4101-400" 366 + }, 367 + "reason": { 368 + "type": "string", 369 + "example": "Field 'limit' must be a number between 1 and 200 (inclusive)" 370 + } 371 + } 372 + }, 373 + "Invalid_parameter": { 374 + "type": "object", 375 + "properties": { 376 + "timestamp": { 377 + "type": "number", 378 + "example": 1569851999383 379 + }, 380 + "code": { 381 + "type": "string", 382 + "example": "API-4101-400" 383 + }, 384 + "reason": { 385 + "type": "string", 386 + "example": "Invalid parameters" 387 + } 388 + } 389 + }, 390 + "Invalid_UPC": { 391 + "type": "object", 392 + "properties": { 393 + "timestamp": { 394 + "type": "number", 395 + "example": 1569851999383 396 + }, 397 + "code": { 398 + "type": "string", 399 + "example": "API-4101-400" 400 + }, 401 + "reason": { 402 + "type": "string", 403 + "example": "UPC must have a length of 13 characters" 404 + } 405 + } 406 + }, 407 + "APIError": { 408 + "type": "object", 409 + "properties": { 410 + "timestamp": { 411 + "type": "number" 412 + }, 413 + "code": { 414 + "type": "string" 415 + }, 416 + "reason": { 417 + "type": "string" 418 + } 419 + } 420 + }, 421 + "APIError.unauthorized": { 422 + "type": "object", 423 + "properties": { 424 + "errors": { 425 + "type": "object", 426 + "properties": { 427 + "error_description": { 428 + "type": "string", 429 + "example": "The access token is invalid or has expired" 430 + }, 431 + "error": { 432 + "type": "string", 433 + "example": "invalid_token" 434 + } 435 + } 436 + } 437 + } 438 + }, 439 + "APIError.forbidden": { 440 + "type": "object", 441 + "properties": { 442 + "errors": { 443 + "type": "object", 444 + "properties": { 445 + "reason": { 446 + "type": "string", 447 + "example": "missing required scopes" 448 + }, 449 + "code": { 450 + "type": "string", 451 + "example": "Forbidden" 452 + }, 453 + "timestamp": { 454 + "type": "number", 455 + "example": 1564143270221 456 + } 457 + } 458 + } 459 + } 460 + }, 461 + "APIError.products.serverError": { 462 + "type": "object", 463 + "properties": { 464 + "errors": { 465 + "type": "object", 466 + "properties": { 467 + "reason": { 468 + "type": "string", 469 + "example": "Internal server error" 470 + }, 471 + "code": { 472 + "type": "string", 473 + "example": "PRODUCT-4xxx-xxx" 474 + }, 475 + "timestamp": { 476 + "type": "number", 477 + "example": 1564159296910 478 + } 479 + } 480 + } 481 + } 482 + }, 483 + "productId": { 484 + "type": "string", 485 + "description": "The productId of the product to return.", 486 + "example": "0001111060903", 487 + "maxLength": 13, 488 + "minLength": 13, 489 + "maximum": 200 490 + }, 491 + "products.productModel": { 492 + "type": "object", 493 + "properties": { 494 + "productId": { 495 + "type": "string", 496 + "description": "The UPC of the product.", 497 + "example": "0001111041700" 498 + }, 499 + "productPageURI": { 500 + "type": "string", 501 + "description": "The URI of the product page.", 502 + "example": "/p/kroger-2-reduced-fat-milk/0001111041700?cid=dis.api.tpi_products-api_20240521_b:all_c:p_t:" 503 + }, 504 + "aisleLocations": { 505 + "type": "array", 506 + "items": { 507 + "$ref": "#/components/schemas/products.productAisleLocationModel" 508 + } 509 + }, 510 + "brand": { 511 + "type": "string", 512 + "description": "The brand name of the product.", 513 + "example": "Kroger" 514 + }, 515 + "categories": { 516 + "type": "array", 517 + "description": "The category the product belongs to.", 518 + "items": { 519 + "type": "string", 520 + "example": "Dairy" 521 + } 522 + }, 523 + "countryOrigin": { 524 + "type": "string", 525 + "description": "The country of origin of the product.", 526 + "example": "United States" 527 + }, 528 + "description": { 529 + "type": "string", 530 + "description": "The name of the product.", 531 + "example": "Kroger 2% Reduced Fat Milk" 532 + }, 533 + "items": { 534 + "type": "array", 535 + "items": { 536 + "$ref": "#/components/schemas/products.productItemModel" 537 + } 538 + }, 539 + "itemInformation": { 540 + "$ref": "#/components/schemas/products.productBoxedDimensionsModel" 541 + }, 542 + "temperature": { 543 + "$ref": "#/components/schemas/products.productTemperatureModel" 544 + }, 545 + "images": { 546 + "type": "array", 547 + "items": { 548 + "$ref": "#/components/schemas/products.productImageModel" 549 + } 550 + }, 551 + "upc": { 552 + "type": "string", 553 + "description": "The UPC of the product.", 554 + "example": "0001111041700" 555 + } 556 + } 557 + }, 558 + "products.productPayloadModel": { 559 + "type": "object", 560 + "properties": { 561 + "data": { 562 + "$ref": "#/components/schemas/products.productModel" 563 + }, 564 + "meta": { 565 + "type": "object", 566 + "properties": {} 567 + } 568 + } 569 + }, 570 + "products.productsPayloadModel": { 571 + "type": "object", 572 + "properties": { 573 + "data": { 574 + "type": "array", 575 + "items": { 576 + "$ref": "#/components/schemas/products.productModel" 577 + } 578 + }, 579 + "meta": { 580 + "type": "object", 581 + "properties": {} 582 + } 583 + } 584 + }, 585 + "products.productAisleLocationModel": { 586 + "type": "object", 587 + "properties": { 588 + "bayNumber": { 589 + "type": "string", 590 + "description": "The bay number of the aisle.", 591 + "example": "13" 592 + }, 593 + "description": { 594 + "type": "string", 595 + "description": "The location in the store.", 596 + "example": "Aisle 35" 597 + }, 598 + "number": { 599 + "type": "string", 600 + "description": "The aisle number in the store.", 601 + "example": "35" 602 + }, 603 + "numberOfFacings": { 604 + "type": "string", 605 + "description": "The number of facings.", 606 + "example": "5" 607 + }, 608 + "sequenceNumber": { 609 + "type": "string", 610 + "description": "The sequence of the aisle in the store.", 611 + "example": "3" 612 + }, 613 + "side": { 614 + "type": "string", 615 + "description": "The side of the aisle where the product is located.", 616 + "example": "L" 617 + }, 618 + "shelfNumber": { 619 + "type": "string", 620 + "description": "The shelf number in the aisle.", 621 + "example": "2" 622 + }, 623 + "shelfPositionInBay": { 624 + "type": "string", 625 + "description": "The position of the shelf in the bay.", 626 + "example": "1" 627 + } 628 + } 629 + }, 630 + "products.productBoxedDimensionsModel": { 631 + "type": "object", 632 + "description": "Information about the product's size.", 633 + "properties": { 634 + "depth": { 635 + "type": "string", 636 + "description": "The depth of the product.", 637 + "example": "3.5" 638 + }, 639 + "height": { 640 + "type": "string", 641 + "description": "The height of the product.", 642 + "example": "2.0" 643 + }, 644 + "width": { 645 + "type": "string", 646 + "description": "The length of the product.", 647 + "example": "4.75" 648 + } 649 + } 650 + }, 651 + "products.productItemModel": { 652 + "type": "object", 653 + "properties": { 654 + "itemId": { 655 + "type": "string", 656 + "description": "The UPC of the item.", 657 + "example": "0001111041700" 658 + }, 659 + "inventory": { 660 + "$ref": "#/components/schemas/products.productItemInventoryModel" 661 + }, 662 + "favorite": { 663 + "type": "boolean" 664 + }, 665 + "fulfillment": { 666 + "$ref": "#/components/schemas/products.productItemFulfillmentModel" 667 + }, 668 + "price": { 669 + "$ref": "#/components/schemas/products.productItemPriceModel" 670 + }, 671 + "nationalPrice": { 672 + "$ref": "#/components/schemas/products.productItemPriceModel" 673 + }, 674 + "size": { 675 + "type": "string", 676 + "description": "A description of the item size.", 677 + "example": "1 gal" 678 + }, 679 + "soldBy": { 680 + "type": "string", 681 + "description": "Indicates how this item is sold. Values returned are typically either \"weight\" or \"unit\"", 682 + "example": "unit" 683 + } 684 + } 685 + }, 686 + "products.productItemInventoryModel": { 687 + "type": "object", 688 + "properties": { 689 + "stockLevel": { 690 + "type": "string", 691 + "enum": [ 692 + "HIGH", 693 + "LOW", 694 + "TEMPORARILY_OUT_OF_STOCK" 695 + ], 696 + "description": "Indicates the level of stock.", 697 + "example": "HIGH" 698 + } 699 + } 700 + }, 701 + "products.productItemFulfillmentModel": { 702 + "type": "object", 703 + "properties": { 704 + "curbside": { 705 + "type": "boolean", 706 + "description": "Indicates if the product is available for curbside pickup." 707 + }, 708 + "delivery": { 709 + "type": "boolean", 710 + "description": "Indicates if the product is available for home delivery." 711 + }, 712 + "instore": { 713 + "type": "boolean", 714 + "description": "Indicates if the product is available in store. This does not indicate that the item is in stock." 715 + }, 716 + "shiptohome": { 717 + "type": "boolean", 718 + "description": "Indicates if the product is available to be shipped from a fulfillment center." 719 + } 720 + } 721 + }, 722 + "products.productItemPriceModel": { 723 + "type": "object", 724 + "properties": { 725 + "regular": { 726 + "type": "number", 727 + "description": "The regular price of the item.", 728 + "example": 1.99 729 + }, 730 + "promo": { 731 + "type": "number", 732 + "description": "The sale price of the item.", 733 + "example": 1.59 734 + }, 735 + "regularPerUnitEstimate": { 736 + "type": "number", 737 + "description": "The estimated price of 1 unit of the item.", 738 + "example": 1.99 739 + }, 740 + "promoPerUnitEstimate": { 741 + "type": "number", 742 + "description": "The estimated sale price of 1 unit of the item.", 743 + "example": 1.59 744 + } 745 + } 746 + }, 747 + "products.productImageModel": { 748 + "type": "object", 749 + "description": "Information about the product's image.", 750 + "properties": { 751 + "id": { 752 + "type": "string", 753 + "description": "An optional identifier of the image size." 754 + }, 755 + "perspective": { 756 + "type": "string", 757 + "description": "A description of the product image view.", 758 + "example": "front" 759 + }, 760 + "default": { 761 + "type": "boolean" 762 + }, 763 + "sizes": { 764 + "type": "array", 765 + "description": "An array of image sizes.", 766 + "items": { 767 + "$ref": "#/components/schemas/products.productImageSizeModel" 768 + } 769 + } 770 + } 771 + }, 772 + "products.productImageSizeModel": { 773 + "type": "object", 774 + "description": "Information about the product's image.", 775 + "properties": { 776 + "id": { 777 + "type": "string", 778 + "description": "An optional identifier of the image size.", 779 + "example": "7df2d0a3-8349-44d4-9512-1dab89e675a9" 780 + }, 781 + "size": { 782 + "type": "string", 783 + "description": "A description of the image size.", 784 + "example": "medium" 785 + }, 786 + "url": { 787 + "type": "string", 788 + "description": "The URL location of the image.", 789 + "example": "https://www.kroger.com/product/images/medium/front/0001111041700" 790 + } 791 + } 792 + }, 793 + "products.productTemperatureModel": { 794 + "type": "object", 795 + "description": "Information about the item's temperature requirements.", 796 + "properties": { 797 + "indicator": { 798 + "type": "string", 799 + "description": "Information about the product's storage temperature.", 800 + "example": "Refrigerated" 801 + }, 802 + "heatSensitive": { 803 + "type": "boolean", 804 + "description": "Indicates if the item is heat sensitive." 805 + } 806 + } 807 + }, 808 + "UPC": { 809 + "type": "string", 810 + "description": "The UPC of the product to return.", 811 + "example": "0001111060903", 812 + "maxLength": 13, 813 + "minLength": 13, 814 + "maximum": 200 815 + } 816 + }, 817 + "securitySchemes": { 818 + "ClientContext": { 819 + "type": "oauth2", 820 + "description": "To make an API request that interacts with generalized information and does not require customer consent, use the [Client Credentials Grant Type](https://developer.kroger.com/reference/api/authorization-endpoints-public#tag/OAuth2/operation/accessToken) to authenticate your OAuth2 application.\n", 821 + "flows": { 822 + "clientCredentials": { 823 + "tokenUrl": "https://api.kroger.com/v1/connect/oauth2/token", 824 + "scopes": { 825 + "product.compact": "Grants read access to general product information." 826 + } 827 + } 828 + } 829 + } 830 + } 831 + }, 832 + "x-tagGroups": [ 833 + { 834 + "name": "API Reference", 835 + "tags": [ 836 + "Products" 837 + ] 838 + } 839 + ] 840 + }
+545
internal/kroger/openapi-auth.json
··· 1 + { 2 + "openapi": "3.0.3", 3 + "info": { 4 + "title": "Authorization Endpoints", 5 + "description": "The authorization endpoints provide a token that will allow your service or application to call Kroger APIs.\n<br><br>\nLearn more about how The Kroger Co uses Oauth2:\n<a href=\"https://developer.kroger.com/documentation/public/security/guides-oauth\">Understanding OAuth2</a>\n", 6 + "termsOfService": "https://developer.kroger.com/terms", 7 + "contact": { 8 + "name": "API Support", 9 + "email": "APISupport@kroger.com", 10 + "url": "https://developer.kroger.com" 11 + }, 12 + "version": "1.0.13" 13 + }, 14 + "servers": [ 15 + { 16 + "url": "https://api.kroger.com", 17 + "description": "Production Environment" 18 + }, 19 + { 20 + "url": "https://api-ce.kroger.com", 21 + "description": "Certification Environment" 22 + } 23 + ], 24 + "paths": { 25 + "/v1/connect/oauth2/authorize": { 26 + "get": { 27 + "tags": [ 28 + "OAuth2" 29 + ], 30 + "summary": "Authorization Code", 31 + "description": "This endpoint is used when the end user must approve access to a protected resource (such as a cart) before a service can act on the user's behalf. Here's how it works:\n\n* Your service or application calls this endpoint and includes the redirect URL (what the end user should see after this service executes) and scopes (defines the actions your application or service can take on behalf of the user).\n* This endpoint displays a login screen, where the end user enters their email address and password.\n* The end user is then asked to give the application permission to access the resource on their behalf.\n* When the user agrees, this service returns the redirect URL with an authorization `code` as a parameter.\n\nAfter this endpoint returns, your service or application must call the `/token` endpoint with the `authorization_code` grant type and `code` value to get an access token. The access token is then sent to the endpoint being called to prove that your service or application is authorized to act on a user's behalf.\n", 32 + "operationId": "authorizationCode", 33 + "parameters": [ 34 + { 35 + "name": "scope", 36 + "in": "query", 37 + "description": "The level of access your application is requesting.", 38 + "required": true, 39 + "schema": { 40 + "type": "string" 41 + } 42 + }, 43 + { 44 + "name": "client_id", 45 + "in": "query", 46 + "description": "Your application's client ID.", 47 + "required": true, 48 + "schema": { 49 + "type": "string" 50 + } 51 + }, 52 + { 53 + "name": "redirect_uri", 54 + "in": "query", 55 + "description": "Your registered redirect URL. The redirect URL tells this endpoint which URL to display after the user approves access to the protected resource.", 56 + "required": true, 57 + "schema": { 58 + "type": "string" 59 + } 60 + }, 61 + { 62 + "name": "response_type", 63 + "in": "query", 64 + "description": "Is always `code`.", 65 + "required": true, 66 + "schema": { 67 + "type": "string" 68 + } 69 + }, 70 + { 71 + "name": "state", 72 + "in": "query", 73 + "description": "A random string to verify that the response belongs to the initiated request. The server should always return the same state value as the one specified in the request to protect against forgery attacks.", 74 + "required": false, 75 + "schema": { 76 + "type": "string" 77 + } 78 + }, 79 + { 80 + "name": "banner", 81 + "in": "query", 82 + "description": "Sets the chain specific branding displayed on the authorization consent screen presented to the end user for shopper logins. When this parameter is not supplied the default chain branding (Kroger) will be used. Only one option may be used at a time and the available options are the following:\n\n\"bakers\" - Bakers' Plus \n\"citymarket\" - City Market \n\"dillons\" - Dillons \n\"fredmeyer\" - Fred Meyer \n\"frys\" - Fry's Food \n\"gerbes\" - Gerbes \n\"kingsoopers\" - King Soopers \n\"kroger\" - Kroger (default if no banner provided) \n\"metromarket\" - Metro Market \n\"picknsave\" - Pick 'n Save \n\"qfc\" - QFC \n\"ralphs\" - Ralphs \n\"smiths\" - Smiths Food and Drug \n\"food4less\" - Food 4 Less\" \n\"foodsco\" - Foods Co. \n\"harristeeter\" - Harris Teeter \n\"vons\" - Vons \n\"fredmeyerjewelers\" - Fred Meyer \n\"jaycfoods\" - Jay C \n\"marianos\" - Marianos \n\"payless\" - Pay Less \n\"ppsrx\" - Postal Prescription Services (PPSRX) \n\"rulerfoods\" - Ruler Foods \n\"copps\" - Copps \n", 83 + "required": false, 84 + "schema": { 85 + "type": "string" 86 + } 87 + } 88 + ], 89 + "responses": { 90 + "301": { 91 + "description": "Moved Permanently", 92 + "content": { 93 + "application/json": { 94 + "schema": { 95 + "$ref": "#/components/schemas/oauth2_code_response" 96 + } 97 + } 98 + } 99 + }, 100 + "400": { 101 + "description": "Bad Request", 102 + "content": { 103 + "application/json": { 104 + "schema": { 105 + "oneOf": [ 106 + { 107 + "$ref": "#/components/schemas/invalid_scope" 108 + }, 109 + { 110 + "$ref": "#/components/schemas/invalid_grant_type" 111 + }, 112 + { 113 + "$ref": "#/components/schemas/invalid_redirect_uri" 114 + }, 115 + { 116 + "$ref": "#/components/schemas/invalid_access" 117 + }, 118 + { 119 + "$ref": "#/components/schemas/invalid_credentials" 120 + } 121 + ] 122 + } 123 + } 124 + } 125 + }, 126 + "500": { 127 + "description": "Internal Server Error", 128 + "content": { 129 + "application/json": { 130 + "schema": { 131 + "$ref": "#/components/schemas/auth_server_error" 132 + } 133 + } 134 + } 135 + } 136 + }, 137 + "x-code-samples": [ 138 + { 139 + "lang": "html", 140 + "source": "https://api.kroger.com/v1/connect/oauth2/authorize?scope={{SCOPES}}&response_type=code&client_id={{CLIENT_ID}}&redirect_uri={{REDIRECT_URI}}" 141 + } 142 + ] 143 + } 144 + }, 145 + "/v1/connect/oauth2/token": { 146 + "post": { 147 + "tags": [ 148 + "OAuth2" 149 + ], 150 + "summary": "Access Token", 151 + "description": "All Oauth2 applications are issued \"client credentials\" in the form of a unique client_id and client_secret after registration. The credentials are used to authorize the application.\n<br/><br/>\nThere are 3 grant type flows that the token endpoint offers:\n1. `authorization_code` - Uses the `code` returned from the `/authorize` endpoint to get a token, allowing your service or application to make API requests on an end user's behalf, including accessing personal data. The service is only able to perform the actions specified in the approved scopes.\n2. `client_credentials` - Uses the client credentials to provide a token that allows your service or application to call endpoints that do not require user approval.\n3. `refresh_token`\t- Allows the application to \"refresh\" an access token that has expired. Refresh tokens are only granted when using the Authorization Code grant type. Using the refresh token eliminates the need to re-authenticate the customer when the access token expires.\n\nThe `access_token` received from this step is sent to the endpoint being called to prove that your service or application is authorized to call the API. The `token_type` field indicates what type of token it is so that you can correctly pass it to the API.\n", 152 + "operationId": "accessToken", 153 + "parameters": [ 154 + { 155 + "name": "Authorization", 156 + "in": "header", 157 + "description": "Your `client_id:client_secret` base64 encoded.", 158 + "required": true, 159 + "schema": { 160 + "type": "string" 161 + } 162 + } 163 + ], 164 + "requestBody": { 165 + "content": { 166 + "application/x-www-form-urlencoded": { 167 + "schema": { 168 + "oneOf": [ 169 + { 170 + "$ref": "#/components/schemas/authorization_code" 171 + }, 172 + { 173 + "$ref": "#/components/schemas/client_credentials" 174 + }, 175 + { 176 + "$ref": "#/components/schemas/refresh_token" 177 + } 178 + ], 179 + "discriminator": { 180 + "propertyName": "grant_type" 181 + } 182 + } 183 + } 184 + }, 185 + "required": false 186 + }, 187 + "responses": { 188 + "200": { 189 + "description": "OK", 190 + "content": { 191 + "application/json": { 192 + "schema": { 193 + "oneOf": [ 194 + { 195 + "$ref": "#/components/schemas/authorization_code_response" 196 + }, 197 + { 198 + "$ref": "#/components/schemas/client_credentials_response" 199 + }, 200 + { 201 + "$ref": "#/components/schemas/refresh_token_response" 202 + } 203 + ] 204 + } 205 + } 206 + } 207 + }, 208 + "400": { 209 + "description": "Bad Request", 210 + "content": { 211 + "application/json": { 212 + "schema": { 213 + "oneOf": [ 214 + { 215 + "$ref": "#/components/schemas/invalid_code" 216 + }, 217 + { 218 + "$ref": "#/components/schemas/invalid_scope" 219 + }, 220 + { 221 + "$ref": "#/components/schemas/invalid_grant_type" 222 + }, 223 + { 224 + "$ref": "#/components/schemas/invalid_refresh_token" 225 + }, 226 + { 227 + "$ref": "#/components/schemas/invalid_credentials" 228 + } 229 + ] 230 + } 231 + } 232 + } 233 + }, 234 + "500": { 235 + "description": "Internal Server Error", 236 + "content": { 237 + "application/json": { 238 + "schema": { 239 + "$ref": "#/components/schemas/auth_server_error" 240 + } 241 + } 242 + } 243 + } 244 + }, 245 + "x-code-samples": [ 246 + { 247 + "lang": "Shell", 248 + "source": "curl -X POST \\\n 'https://api.kroger.com/v1/connect/oauth2/token' \\\n -H 'Content-Type: application/x-www-form-urlencoded' \\\n -H 'Authorization: Basic {{base64(“CLIENT_ID:CLIENT_SECRET”)}}' \\\n -d 'grant_type=client_credentials&scope={{SCOPE}}'\n" 249 + }, 250 + { 251 + "lang": "Go", 252 + "source": "package main\n\nimport (\n \"fmt\"\n \"strings\"\n \"net/http\"\n \"io/ioutil\"\n)\n\nfunc main() {\n\n url := \"https://api.kroger.com/v1/connect/oauth2/token\"\n\n payload := strings.NewReader(\"grant_type=client_credentials&scope={{SCOPE}}\")\n\n req, _ := http.NewRequest(\"POST\", url, payload)\n\n req.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n req.Header.Add(\"Authorization\", \"Basic {{base64(“CLIENT_ID:CLIENT_SECRET”)}}\")\n\n res, _ := http.DefaultClient.Do(req)\n\n defer res.Body.Close()\n body, _ := ioutil.ReadAll(res.Body)\n\n fmt.Println(res)\n fmt.Println(string(body))\n\n}\n" 253 + }, 254 + { 255 + "lang": "JavaScript", 256 + "source": "var settings = {\n \"async\": true,\n \"crossDomain\": true,\n \"url\": \"https://api.kroger.com/v1/connect/oauth2/token\",\n \"method\": \"POST\",\n \"headers\": {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n \"Authorization\": \"Basic {{base64(“CLIENT_ID:CLIENT_SECRET”)}}\"\n },\n \"data\": {\n \"grant_type\": \"client_credentials\",\n \"scope\": \"{{scope}}\"\n }\n}\n\n$.ajax(settings).done(function (response) {\n console.log(response);\n});\n" 257 + }, 258 + { 259 + "lang": "Java", 260 + "source": "OkHttpClient client = new OkHttpClient();\n\nMediaType mediaType = MediaType.parse(\"application/x-www-form-urlencoded\");\nRequestBody body = RequestBody.create(mediaType, \"grant_type=client_credentials&scope={{SCOPE}}\");\nRequest request = new Request.Builder()\n .url(\"https://api.kroger.com/v1/connect/oauth2/token\")\n .post(body)\n .addHeader(\"Content-Type\", \"application/x-www-form-urlencoded\")\n .addHeader(\"Authorization\", \"Basic {{base64(“CLIENT_ID:CLIENT_SECRET”)}}\")\n .build();\n\nResponse response = client.newCall(request).execute();\n" 261 + } 262 + ] 263 + } 264 + } 265 + }, 266 + "components": { 267 + "schemas": { 268 + "auth_server_error": { 269 + "type": "object", 270 + "properties": { 271 + "errors": { 272 + "type": "object", 273 + "properties": { 274 + "reason": { 275 + "type": "string", 276 + "description": "Description of the reason for the error", 277 + "example": "Internal server error" 278 + }, 279 + "code": { 280 + "type": "string", 281 + "description": "Unique identifier for the error used for troubleshooting purposes", 282 + "example": "Auth-4xxx-xxx" 283 + }, 284 + "timestamp": { 285 + "type": "number", 286 + "description": "Timestamp of the resposne", 287 + "example": 1564159296910 288 + } 289 + } 290 + } 291 + } 292 + }, 293 + "authorization_code": { 294 + "required": [ 295 + "code", 296 + "grant_type", 297 + "redirect_uri" 298 + ], 299 + "type": "object", 300 + "properties": { 301 + "grant_type": { 302 + "type": "string", 303 + "description": "Must be `authorization_code`.", 304 + "example": "code" 305 + }, 306 + "code": { 307 + "type": "string", 308 + "description": "The value of the `code` parameter returned from the `/authorization` endpoint on the redirect URL.", 309 + "example": "zWrT1GkdshSadIowJW0Rm4w2kKhOzv1W" 310 + }, 311 + "redirect_uri": { 312 + "type": "string", 313 + "description": "Your registered redirect URL. Must be the same redirect URL that was used for the authorizations code request.", 314 + "example": "https://example.com/callback" 315 + } 316 + } 317 + }, 318 + "authorization_code_response": { 319 + "type": "object", 320 + "properties": { 321 + "expires_in": { 322 + "type": "number", 323 + "description": "How long the access token is valid for in seconds.", 324 + "example": 1800 325 + }, 326 + "access_token": { 327 + "type": "string", 328 + "description": "The access token string", 329 + "example": "eyJh5GciOiJSUzI1NiGsImtpZCI6Ilo0RnQzbXNrSUj4OGlydDdMQjVjNmc2PSIsInR5cCI6IkpXVmJ9.eqJzY29wZSI6InByb2T1Y3QuY29tcGFjdCBjb3Vwb24uYmFzaWMiLCJhdXRoQXQiOjE1NjUwOTk0OTUzMzIzOTIxMTIsImF1ZCI6InBlcmsvcm1hbmNlLWFnZW50LXB1YmxpYyIsImV4cCI6MTU2NTEwMTI5NSwiaWF0IjoxNTY1MDk5BDkwLCJpc3MiOiJhcGkua3JvZ2VyLmNvbSIsInN1YiI6IjBmZjdkMGIwLWVkOGItNDJmOS1hNTExLWEzMGQyYTAyZDljNSJ9.ej0mov6SGV4n4HiAvduTdYCceMlSo3T06M4Nfh3MfpIjSKzKaLWgd5S0W1EKDXrWz8IE7NTg8EIrL-WKhwdZPt-TWaS7LLjRXLJ0w5rKc44DStgBdvDiCcnKeMsnimjhBlHOiiKUV5y3GbVqJzaDVZwg0j8lP9qtwZP9EIIQ7k409nkskY1pz7l1lZrGotYRJKmnteN5vVQeZ3R8jywIwOOSEbKSgQALVA3Oj02964P7lI6h1GsZ66V5FLA9KU8QXm4ejrFHf1beAIA2zi_fQI3dmW7yj57pWoCECZIjq7Sfo3nGR5rkjEwfyXEK7aTn8oj4_14YHgKRTY-28L96cw" 330 + }, 331 + "token_type": { 332 + "type": "string", 333 + "description": "The type of token.", 334 + "example": "bearer" 335 + }, 336 + "refresh_token": { 337 + "type": "string", 338 + "description": "A token that can be used to request a new token on behalf of the end user. Refresh tokens have a longer expiration, typically 24 hours.", 339 + "example": "FN20LbaF2EWC6MPMWdemBwwnP4ZmX8" 340 + } 341 + } 342 + }, 343 + "client_credentials": { 344 + "required": [ 345 + "grant_type" 346 + ], 347 + "type": "object", 348 + "properties": { 349 + "grant_type": { 350 + "type": "string", 351 + "description": "Must be `client_credentials`.", 352 + "example": "client_credentials" 353 + }, 354 + "scope": { 355 + "type": "string", 356 + "description": "The level of access your application is requesting. Available options can be found on your app page.", 357 + "example": "product.compact" 358 + } 359 + } 360 + }, 361 + "client_credentials_response": { 362 + "type": "object", 363 + "properties": { 364 + "expires_in": { 365 + "type": "number", 366 + "description": "How long the access token is valid for in seconds.", 367 + "example": 1800 368 + }, 369 + "access_token": { 370 + "type": "string", 371 + "description": "The access token string.", 372 + "example": "eyJh5GciOiJSUzI1NiGsImtpZCI6Ilo0RnQzbXNrSUj4OGlydDdMQjVjNmc2PSIsInR5cCI6IkpXVmJ9.eqJzY29wZSI6InByb2T1Y3QuY29tcGFjdCBjb3Vwb24uYmFzaWMiLCJhdXRoQXQiOjE1NjUwOTk0OTUzMzIzOTIxMTIsImF1ZCI6InBlcmsvcm1hbmNlLWFnZW50LXB1YmxpYyIsImV4cCI6MTU2NTEwMTI5NSwiaWF0IjoxNTY1MDk5BDkwLCJpc3MiOiJhcGkua3JvZ2VyLmNvbSIsInN1YiI6IjBmZjdkMGIwLWVkOGItNDJmOS1hNTExLWEzMGQyYTAyZDljNSJ9.ej0mov6SGV4n4HiAvduTdYCceMlSo3T06M4Nfh3MfpIjSKzKaLWgd5S0W1EKDXrWz8IE7NTg8EIrL-WKhwdZPt-TWaS7LLjRXLJ0w5rKc44DStgBdvDiCcnKeMsnimjhBlHOiiKUV5y3GbVqJzaDVZwg0j8lP9qtwZP9EIIQ7k409nkskY1pz7l1lZrGotYRJKmnteN5vVQeZ3R8jywIwOOSEbKSgQALVA3Oj02964P7lI6h1GsZ66V5FLA9KU8QXm4ejrFHf1beAIA2zi_fQI3dmW7yj57pWoCECZIjq7Sfo3nGR5rkjEwfyXEK7aTn8oj4_14YHgKRTY-28L96cw" 373 + }, 374 + "token_type": { 375 + "type": "string", 376 + "description": "The type of token.", 377 + "example": "bearer" 378 + } 379 + } 380 + }, 381 + "invalid_access": { 382 + "type": "object", 383 + "properties": { 384 + "error": { 385 + "type": "string", 386 + "description": "The error message", 387 + "example": "invalid_request" 388 + }, 389 + "error_description": { 390 + "type": "string", 391 + "description": "Detailed error description", 392 + "example": "The resource owner denied the request" 393 + } 394 + } 395 + }, 396 + "invalid_code": { 397 + "type": "object", 398 + "properties": { 399 + "error": { 400 + "type": "string", 401 + "description": "The error message", 402 + "example": "invalid_request" 403 + }, 404 + "error_description": { 405 + "type": "string", 406 + "description": "Detailed error description", 407 + "example": "invalid code" 408 + } 409 + } 410 + }, 411 + "invalid_credentials": { 412 + "type": "object", 413 + "properties": { 414 + "error": { 415 + "type": "string", 416 + "description": "The error message", 417 + "example": "unauthorized" 418 + }, 419 + "error_description": { 420 + "type": "string", 421 + "description": "Detailed error description", 422 + "example": "invalid credentials" 423 + } 424 + } 425 + }, 426 + "invalid_grant_type": { 427 + "type": "object", 428 + "properties": { 429 + "error": { 430 + "type": "string", 431 + "description": "The error message", 432 + "example": "unsupported_grant_type" 433 + }, 434 + "error_description": { 435 + "type": "string", 436 + "description": "Detailed error description", 437 + "example": "invalid grant_type" 438 + } 439 + } 440 + }, 441 + "invalid_redirect_uri": { 442 + "type": "object", 443 + "properties": { 444 + "error": { 445 + "type": "string", 446 + "description": "The error message", 447 + "example": "invalid_request" 448 + }, 449 + "error_description": { 450 + "type": "string", 451 + "description": "Detailed error description", 452 + "example": "The redirect_uri did not match the registered redirect_uri for this application" 453 + } 454 + } 455 + }, 456 + "invalid_refresh_token": { 457 + "type": "object", 458 + "properties": { 459 + "error": { 460 + "type": "string", 461 + "description": "The error message", 462 + "example": "invalid_request" 463 + }, 464 + "error_description": { 465 + "type": "string", 466 + "description": "Detailed error description", 467 + "example": "invalid refresh_token" 468 + } 469 + } 470 + }, 471 + "invalid_scope": { 472 + "type": "object", 473 + "properties": { 474 + "error": { 475 + "type": "string", 476 + "description": "The error message", 477 + "example": "invalid_scope" 478 + }, 479 + "error_description": { 480 + "type": "string", 481 + "description": "Detailed error description", 482 + "example": "invalid scope" 483 + } 484 + } 485 + }, 486 + "oauth2_code_response": { 487 + "type": "string", 488 + "description": "Your registered redirect with the authorization code appended to the URL.", 489 + "example": "https://YourRedirectUri.com/callback?code=zWrT1GkdshSadIowJW0Rm4w2kKhOzv1W" 490 + }, 491 + "refresh_token": { 492 + "required": [ 493 + "refresh_token", 494 + "grant_type" 495 + ], 496 + "type": "object", 497 + "properties": { 498 + "grant_type": { 499 + "type": "string", 500 + "description": "Must be `refresh_token`.", 501 + "example": "refresh_token" 502 + }, 503 + "refresh_token": { 504 + "type": "string", 505 + "description": "The refresh token returned from a call to this endpoint with the `authorization_code` or `refresh_token` grant type.", 506 + "example": "FN20LbaF2EWC6MPMWdemBwwnP4ZmX8" 507 + } 508 + } 509 + }, 510 + "refresh_token_response": { 511 + "type": "object", 512 + "properties": { 513 + "expires_in": { 514 + "type": "number", 515 + "description": "How long the access token is valid for in seconds.", 516 + "example": 1800 517 + }, 518 + "access_token": { 519 + "type": "string", 520 + "description": "The access token string.", 521 + "example": "eyJh5GciOiJSUzI1NiGsImtpZCI6Ilo0RnQzbXNrSUj4OGlydDdMQjVjNmc2PSIsInR5cCI6IkpXVmJ9.eqJzY29wZSI6InByb2T1Y3QuY29tcGFjdCBjb3Vwb24uYmFzaWMiLCJhdXRoQXQiOjE1NjUwOTk0OTUzMzIzOTIxMTIsImF1ZCI6InBlcmsvcm1hbmNlLWFnZW50LXB1YmxpYyIsImV4cCI6MTU2NTEwMTI5NSwiaWF0IjoxNTY1MDk5BDkwLCJpc3MiOiJhcGkua3JvZ2VyLmNvbSIsInN1YiI6IjBmZjdkMGIwLWVkOGItNDJmOS1hNTExLWEzMGQyYTAyZDljNSJ9.ej0mov6SGV4n4HiAvduTdYCceMlSo3T06M4Nfh3MfpIjSKzKaLWgd5S0W1EKDXrWz8IE7NTg8EIrL-WKhwdZPt-TWaS7LLjRXLJ0w5rKc44DStgBdvDiCcnKeMsnimjhBlHOiiKUV5y3GbVqJzaDVZwg0j8lP9qtwZP9EIIQ7k409nkskY1pz7l1lZrGotYRJKmnteN5vVQeZ3R8jywIwOOSEbKSgQALVA3Oj02964P7lI6h1GsZ66V5FLA9KU8QXm4ejrFHf1beAIA2zi_fQI3dmW7yj57pWoCECZIjq7Sfo3nGR5rkjEwfyXEK7aTn8oj4_14YHgKRTY-28L96cw" 522 + }, 523 + "token_type": { 524 + "type": "string", 525 + "description": "The type of token.", 526 + "example": "bearer" 527 + }, 528 + "refresh_token": { 529 + "type": "string", 530 + "description": "A token that can be used to request a new token on behalf of the end user. Refresh tokens have a longer expiration, typically 24 hours.", 531 + "example": "FN20LbaF2EWC6MPMWdemBwwnP4ZmX8" 532 + } 533 + } 534 + } 535 + } 536 + }, 537 + "x-tagGroups": [ 538 + { 539 + "name": "Authorization Endpoints", 540 + "tags": [ 541 + "OAuth2" 542 + ] 543 + } 544 + ] 545 + }
+70 -11
internal/recipes/generator.go
··· 6 6 "fmt" 7 7 "log" 8 8 "net/http" 9 + "strconv" 9 10 10 11 "careme/internal/ai" 11 12 "careme/internal/config" ··· 28 29 29 30 func NewGenerator(cfg *config.Config) (*Generator, error) { 30 31 31 - basicAuth, err := securityprovider.NewSecurityProviderBasicAuth(cfg.Kroger.ClientID, cfg.Kroger.ClientSecret) 32 + bearer, err := kroger.GetOAuth2Token(context.TODO(), cfg.Kroger.ClientID, cfg.Kroger.ClientSecret) 32 33 if err != nil { 33 34 return nil, err 34 35 } 35 36 36 - client, err := kroger.NewClientWithResponses("https://api.kroger.com/", kroger.WithRequestEditorFn(basicAuth.Intercept)) 37 + bearerAuth, err := securityprovider.NewSecurityProviderBearerToken(bearer) 38 + if err != nil { 39 + return nil, err 40 + } 41 + 42 + // Add LoggingDoer to log all requests/responses 43 + //loggingDoer := &kroger.LoggingDoer{Wrapped: http.DefaultClient} 44 + client, err := kroger.NewClientWithResponses("https://api.kroger.com/v1", 45 + kroger.WithRequestEditorFn(bearerAuth.Intercept), 46 + // kroger.WithHTTPClient(loggingDoer), 47 + ) 37 48 if err != nil { 38 49 return nil, err 39 50 } ··· 48 59 func (g *Generator) GenerateWeeklyRecipes(location string) ([]history.Recipe, error) { 49 60 log.Printf("Generating recipes for location: %s", location) 50 61 51 - saleIngredients, err := g.getSaleIngredients(location, "steak") 62 + ingredients, err := g.GetIngredients(location, "steak", 0) //Meat \u0026 Seafood 52 63 if err != nil { 53 - log.Printf("Warning: Could not fetch sale ingredients: %v", err) 54 - saleIngredients = []string{} 64 + return nil, fmt.Errorf("could not fetch sale ingredients: %w", err) 55 65 } 56 66 57 67 previousRecipes, err := g.getPreviousRecipes() ··· 61 71 } 62 72 63 73 log.Printf("Found %d sale ingredients, %d previous recipes", 64 - len(saleIngredients), len(previousRecipes)) 74 + len(ingredients), len(previousRecipes)) 65 75 66 - response, err := g.aiClient.GenerateRecipes(location, saleIngredients, previousRecipes) 76 + response, err := g.aiClient.GenerateRecipes(location, ingredients, previousRecipes) 67 77 if err != nil { 68 78 return nil, fmt.Errorf("failed to generate recipes with AI: %w", err) 69 79 } ··· 80 90 return recipes, nil 81 91 } 82 92 83 - func (g *Generator) getSaleIngredients(location, term string) ([]string, error) { 93 + func (g *Generator) GetIngredients(location, term string, skip int) ([]string, error) { 94 + limit := 50 95 + limitStr := strconv.Itoa(limit) 96 + startStr := strconv.Itoa(skip) 97 + fulfillment := "ais" 84 98 products, err := g.krogerClient.ProductSearchWithResponse(context.TODO(), &kroger.ProductSearchParams{ 85 - FilterLocationId: &location, 86 - FilterTerm: &term, 99 + FilterLocationId: &location, 100 + FilterTerm: &term, 101 + FilterLimit: &limitStr, 102 + FilterStart: &startStr, 103 + FilterFulfillment: &fulfillment, 87 104 }) 88 105 if err != nil { 106 + fmt.Printf("failing here: %v\n", err) 89 107 return nil, err 90 108 } 91 109 92 110 if products.StatusCode() != http.StatusOK { 111 + fmt.Printf("Kroger ProductSearchWithResponse returned status: %d\n", products.StatusCode()) 93 112 return nil, fmt.Errorf("Got %d code from kroger", products.StatusCode()) 94 113 } 114 + bytes, _ := json.Marshal(*products.JSON200.Meta.Pagination) 115 + fmt.Printf("Pagination:%s\n", bytes) 95 116 96 117 var ingredients []string 118 + 97 119 for _, product := range *products.JSON200.Data { 98 120 for _, item := range *product.Items { 99 121 //does just giving the model json work better here? 100 - ingredients = append(ingredients, fmt.Sprintf("%s %s price %s sale %s", product.Description, *item.Size, item.Price.Regular, item.Price.Promo)) 122 + if item.Price == nil { 123 + //fmt.Printf("Warning: Item %s has no price information\n", toStr(product.Description)) 124 + continue 125 + } 126 + 127 + ingredients = append(ingredients, fmt.Sprintf( 128 + "%s %s price %.2f sale %.2f", 129 + toStr(product.Description), 130 + toStr(item.Size), 131 + toFloat32(item.Price.Regular), 132 + toFloat32(item.Price.Promo), 133 + //strings.Join(*product.Categories, ", "), 134 + )) 101 135 } 102 136 } 103 137 138 + //recursion is pretty dumb pagination 139 + if len(*products.JSON200.Data) == limit { //fence post error 140 + page, err := g.GetIngredients(location, term, skip+limit) 141 + if err != nil { 142 + return nil, err 143 + } 144 + ingredients = append(ingredients, page...) 145 + } 146 + 104 147 return ingredients, nil 148 + } 149 + 150 + // toStr returns the string value if non-nil, or "empty" otherwise. 151 + func toStr(s *string) string { 152 + if s == nil { 153 + return "empty" 154 + } 155 + return *s 156 + } 157 + 158 + // toFloat32 returns the float32 value if non-nil, or 0.0 otherwise. 159 + func toFloat32(f *float32) float32 { 160 + if f == nil { 161 + return 0.0 162 + } 163 + return *f 105 164 } 106 165 107 166 func (g *Generator) getPreviousRecipes() ([]string, error) {