instacart_api 0.2.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 709de7ef54e38bf9ddd109b29ec88de2edf21878bf99ca0d12cf93b0b9ff5718
4
+ data.tar.gz: c19bcb606a6108a23db2ff2c7e405feb77290a93749cb4bfcbba98cbf6eac755
5
+ SHA512:
6
+ metadata.gz: 80973fb6564acf12e660fabbd4905bdee1c0124e2c9205f5b48f805a59878e54681e7679c3cb89e0027b43b3a92849636bbd62d13dabade717655e6f8c1eaf90
7
+ data.tar.gz: 9fe73567686b8bf022f2215382b27d626ffb805ce3a922a0733b0b4aaed0e484234b5fc5aabe8ede9034985992bb26951ac78c133bd2b31c08904a6eef93a288
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [ main, master ]
7
+
8
+ jobs:
9
+ rspec:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v5
15
+
16
+ - name: Set up Ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: .ruby-version
20
+ bundler-cache: true
21
+
22
+ - name: Run RSpec
23
+ run: bundle exec rspec
24
+
25
+ - name: Run RuboCop
26
+ run: bundle exec rubocop
@@ -0,0 +1,36 @@
1
+ name: Release
2
+
3
+ # Publishes the gem to RubyGems whenever a version tag (e.g. v0.2.0) is pushed.
4
+ # Uses RubyGems trusted publishing (OIDC) — no API key secret required. See
5
+ # https://guides.rubygems.org/trusted-publishing/ and configure this repo +
6
+ # workflow as a trusted publisher for the gem on rubygems.org first.
7
+
8
+ on:
9
+ push:
10
+ tags:
11
+ - "v*"
12
+
13
+ jobs:
14
+ release:
15
+ runs-on: ubuntu-latest
16
+ environment: release
17
+
18
+ permissions:
19
+ contents: write # create the GitHub release
20
+ id-token: write # mint the OIDC token for trusted publishing
21
+
22
+ steps:
23
+ - name: Checkout code
24
+ uses: actions/checkout@v5
25
+
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: .ruby-version
30
+ bundler-cache: true
31
+
32
+ - name: Verify tests pass before releasing
33
+ run: bundle exec rspec
34
+
35
+ - name: Publish to RubyGems
36
+ uses: rubygems/release-gem@v1
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ # built gems
14
+ /*.gem
15
+
16
+ # capture tooling (node deps + recorded traffic, which contains session data)
17
+ /capture/node_modules/
18
+ /capture/calls.jsonl
19
+ /capture/package*.json
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,47 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+ NewCops: disable
4
+ SuggestExtensions: false
5
+ Exclude:
6
+ - "bin/**/*"
7
+ - "vendor/**/*"
8
+
9
+ Layout/LineLength:
10
+ Max: 80
11
+
12
+ Metrics/BlockLength:
13
+ Exclude:
14
+ - "spec/**/*"
15
+ - "*.gemspec"
16
+
17
+ # GraphQL operations carry verbose variable hashes; allow roomier methods.
18
+ Metrics/MethodLength:
19
+ Max: 25
20
+
21
+ Metrics/AbcSize:
22
+ Max: 20
23
+
24
+ Lint/AmbiguousBlockAssociation:
25
+ Exclude:
26
+ - "spec/**/*"
27
+
28
+ Style/Documentation:
29
+ Enabled: false
30
+
31
+ Layout/DotPosition:
32
+ EnforcedStyle: trailing
33
+ Enabled: true
34
+
35
+ Style/StringLiterals:
36
+ EnforcedStyle: double_quotes
37
+ ConsistentQuotesInMultiline: true
38
+ Enabled: true
39
+
40
+ Style/LambdaCall:
41
+ Enabled: false
42
+
43
+ Style/SymbolArray:
44
+ EnforcedStyle: brackets
45
+
46
+ Style/WordArray:
47
+ EnforcedStyle: percent
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.5
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ Complete rewrite for Instacart's modern web GraphQL API.
6
+
7
+ - **Auth:** session-cookie based (`session_cookie:`) instead of scripted
8
+ email/password login, which is now bot-protected.
9
+ - **Transport:** `GraphqlClient` speaks Instacart's Apollo persisted-query
10
+ protocol; hashes live in `PersistedQueries` and are refreshable via the
11
+ `capture/` tooling.
12
+ - **Client operations:** `#search`, `#items`, `#add_item_to_cart`,
13
+ `#add_items_to_cart`, `#active_carts`, returning `Item`, `SearchResult`, and
14
+ `Cart` models.
15
+ - Removed the legacy `#login`, `#available_stores`, and v3 container endpoints.
16
+ - Modernized tooling: Ruby 4.0, GitHub Actions CI, RuboCop, 100% test coverage,
17
+ and a RubyGems trusted-publishing release workflow.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ ruby File.read("./.ruby-version").strip
8
+
9
+ # Specify your gem's dependencies in instacart_api.gemspec
10
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,171 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ instacart_api (0.2.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ addressable (2.9.0)
10
+ public_suffix (>= 2.0.2, < 8.0)
11
+ ast (2.4.3)
12
+ bigdecimal (4.1.2)
13
+ coderay (1.1.3)
14
+ crack (1.0.1)
15
+ bigdecimal
16
+ rexml
17
+ diff-lcs (1.6.2)
18
+ docile (1.4.1)
19
+ erb (6.0.4)
20
+ hashdiff (1.2.1)
21
+ io-console (0.8.2)
22
+ irb (1.18.0)
23
+ pp (>= 0.6.0)
24
+ prism (>= 1.3.0)
25
+ rdoc (>= 4.0.0)
26
+ reline (>= 0.4.2)
27
+ json (2.21.1)
28
+ language_server-protocol (3.17.0.6)
29
+ lint_roller (1.1.0)
30
+ logger (1.7.0)
31
+ method_source (1.1.0)
32
+ parallel (2.1.0)
33
+ parser (3.3.11.1)
34
+ ast (~> 2.4.1)
35
+ racc
36
+ pp (0.6.4)
37
+ prettyprint
38
+ prettyprint (0.2.0)
39
+ prism (1.9.0)
40
+ pry (0.16.0)
41
+ coderay (~> 1.1)
42
+ method_source (~> 1.0)
43
+ reline (>= 0.6.0)
44
+ public_suffix (7.0.5)
45
+ racc (1.8.1)
46
+ rainbow (3.1.1)
47
+ rake (13.4.2)
48
+ rbs (4.0.3)
49
+ logger
50
+ prism (>= 1.6.0)
51
+ tsort
52
+ rdoc (8.0.0)
53
+ erb
54
+ prism (>= 1.6.0)
55
+ rbs (>= 4.0.0)
56
+ tsort
57
+ regexp_parser (2.12.0)
58
+ reline (0.6.3)
59
+ io-console (~> 0.5)
60
+ rexml (3.4.4)
61
+ rspec (3.13.2)
62
+ rspec-core (~> 3.13.0)
63
+ rspec-expectations (~> 3.13.0)
64
+ rspec-mocks (~> 3.13.0)
65
+ rspec-core (3.13.6)
66
+ rspec-support (~> 3.13.0)
67
+ rspec-expectations (3.13.5)
68
+ diff-lcs (>= 1.2.0, < 2.0)
69
+ rspec-support (~> 3.13.0)
70
+ rspec-mocks (3.13.8)
71
+ diff-lcs (>= 1.2.0, < 2.0)
72
+ rspec-support (~> 3.13.0)
73
+ rspec-support (3.13.7)
74
+ rubocop (1.88.2)
75
+ json (~> 2.3)
76
+ language_server-protocol (~> 3.17.0.2)
77
+ lint_roller (~> 1.1.0)
78
+ parallel (>= 1.10)
79
+ parser (>= 3.3.0.2)
80
+ rainbow (>= 2.2.2, < 4.0)
81
+ regexp_parser (>= 2.9.3, < 3.0)
82
+ rubocop-ast (>= 1.49.0, < 2.0)
83
+ ruby-progressbar (~> 1.7)
84
+ unicode-display_width (>= 2.4.0, < 4.0)
85
+ rubocop-ast (1.50.0)
86
+ parser (>= 3.3.7.2)
87
+ prism (~> 1.7)
88
+ ruby-progressbar (1.13.0)
89
+ simplecov (0.22.0)
90
+ docile (~> 1.1)
91
+ simplecov-html (~> 0.11)
92
+ simplecov_json_formatter (~> 0.1)
93
+ simplecov-html (0.13.2)
94
+ simplecov_json_formatter (0.1.4)
95
+ tsort (0.2.0)
96
+ unicode-display_width (3.2.0)
97
+ unicode-emoji (~> 4.1)
98
+ unicode-emoji (4.2.0)
99
+ webmock (3.26.2)
100
+ addressable (>= 2.8.0)
101
+ crack (>= 0.3.2)
102
+ hashdiff (>= 0.4.0, < 2.0.0)
103
+
104
+ PLATFORMS
105
+ arm64-darwin-25
106
+ ruby
107
+
108
+ DEPENDENCIES
109
+ instacart_api!
110
+ irb
111
+ pry (~> 0.14)
112
+ rake (~> 13.0)
113
+ rspec (~> 3.13)
114
+ rubocop (~> 1.60)
115
+ simplecov (~> 0.22)
116
+ webmock (~> 3.20)
117
+
118
+ CHECKSUMS
119
+ addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
120
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
121
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
122
+ coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
123
+ crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
124
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
125
+ docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
126
+ erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
127
+ hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
128
+ instacart_api (0.2.0)
129
+ io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
130
+ irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
131
+ json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
132
+ language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
133
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
134
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
135
+ method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
136
+ parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
137
+ parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
138
+ pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
139
+ prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
140
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
141
+ pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e
142
+ public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
143
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
144
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
145
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
146
+ rbs (4.0.3) sha256=5a7bf70e2628549d9a1f44eae447b2cfe55968a9c60cfff52693a4bdcc020e14
147
+ rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
148
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
149
+ reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
150
+ rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
151
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
152
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
153
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
154
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
155
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
156
+ rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf
157
+ rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db
158
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
159
+ simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
160
+ simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
161
+ simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
162
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
163
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
164
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
165
+ webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
166
+
167
+ RUBY VERSION
168
+ ruby 4.0.5
169
+
170
+ BUNDLED WITH
171
+ 4.0.10
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 James Klein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # Instacart Api
2
+
3
+ [![CI](https://github.com/kleinjm/instacart_api/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/kleinjm/instacart_api/actions/workflows/ci.yml)
4
+
5
+ Ruby wrapper around Instacart's (undocumented) web GraphQL API: search for
6
+ products across the stores serving an address, look up item details, and add
7
+ items to the logged-in user's cart.
8
+
9
+ > ⚠️ This talks to Instacart's private web API, not an official/supported
10
+ > product. Endpoints and persisted-query hashes change without notice, and
11
+ > automated access may be against Instacart's Terms of Service. Use it for
12
+ > personal automation at your own risk.
13
+
14
+ ## Installation
15
+
16
+ ```ruby
17
+ gem "instacart_api"
18
+ ```
19
+
20
+ ## Authentication
21
+
22
+ Instacart's login is bot-protected and can't be scripted, so the gem
23
+ authenticates with the **session cookie** from a real browser login rather than
24
+ an email/password. After logging in at instacart.com, copy the `Cookie` header
25
+ (at minimum the `__Host-instacart_sid` cookie and its companions) from your
26
+ browser devtools.
27
+
28
+ You also need your delivery `postal_code` and `zone_id` (both visible in the
29
+ GraphQL request variables on instacart.com), which scope search and pricing to
30
+ the stores serving that location.
31
+
32
+ ## Usage
33
+
34
+ ```ruby
35
+ require "instacart_api"
36
+
37
+ client = InstacartApi::Client.new(
38
+ session_cookie: ENV.fetch("INSTACART_COOKIE"),
39
+ postal_code: "97202",
40
+ zone_id: "103"
41
+ )
42
+
43
+ # Search across the stores serving your location.
44
+ results = client.search(term: "bananas", shop_ids: %w[1292 580])
45
+ group = results.first
46
+
47
+ # Fetch full details (name, price, availability) for the matched item ids.
48
+ items = client.items(ids: group.item_ids, shop_id: group.shop_id)
49
+ cheapest = items.select(&:available?).min # Item is Comparable by price
50
+
51
+ # Add it to the cart (quantity: 0 removes it). Returns the updated cart.
52
+ cart = client.add_item_to_cart(item_id: cheapest.id, quantity: 1)
53
+ cart.item_count # => 1
54
+
55
+ # Or add several at once, and inspect the active carts.
56
+ client.add_items_to_cart(items: [
57
+ { item_id: cheapest.id, quantity: 2 }
58
+ ])
59
+ client.active_carts
60
+ ```
61
+
62
+ See [example_usage.rb](example_usage.rb) for a runnable version.
63
+
64
+ ## Keeping it working
65
+
66
+ The gem pins the persisted-query hashes Instacart's web client uses. When
67
+ Instacart ships a new frontend build these rotate and calls start raising
68
+ `GraphqlError`/`UnknownOperationError`. Recapture them with the tool in
69
+ [`capture/`](capture/README.md) and update
70
+ `lib/instacart_api/persisted_queries.rb`.
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ bundle install
76
+ bundle exec rspec # test suite (100% line coverage enforced)
77
+ bundle exec rubocop # lint
78
+ ```
79
+
80
+ ## Releasing
81
+
82
+ Releases publish to RubyGems via GitHub Actions using
83
+ [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (OIDC — no
84
+ API key stored). One-time setup: on rubygems.org, add this repository and the
85
+ `Release` workflow as a trusted publisher for the gem.
86
+
87
+ Then bump `InstacartApi::VERSION`, commit, and push a matching tag:
88
+
89
+ ```bash
90
+ git tag v0.2.0
91
+ git push origin v0.2.0
92
+ ```
93
+
94
+ The `Release` workflow runs the tests and publishes the gem.
95
+
96
+ ## Contributing
97
+
98
+ Bug reports and pull requests are welcome at
99
+ https://github.com/kleinjm/instacart_api.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "instacart_api"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("bundle", __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/capture/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Refreshing persisted-query hashes
2
+
3
+ Instacart's web client uses Apollo **persisted queries**: it sends an operation
4
+ name plus a SHA-256 hash instead of the full GraphQL document, and the server
5
+ only honors hashes it already knows. Those hashes change every time Instacart
6
+ ships a new frontend build, so `lib/instacart_api/persisted_queries.rb` goes
7
+ stale periodically — you'll see `GraphqlError`/`UnknownOperationError` when it
8
+ does.
9
+
10
+ This directory holds a tool to recapture them from a real browser session.
11
+
12
+ ## Usage
13
+
14
+ ```bash
15
+ cd capture
16
+ npm install playwright
17
+ npx playwright install chromium
18
+
19
+ node capture.js # opens a browser — log in, then search + add an item
20
+ ruby extract_hashes.rb
21
+ ```
22
+
23
+ `capture.js` records every `/graphql` call to `calls.jsonl` while you drive the
24
+ shopping flow. `extract_hashes.rb` reads that file and prints an updated
25
+ `HASHES = { ... }` block. Paste it into
26
+ `lib/instacart_api/persisted_queries.rb`, run the specs, and commit.
27
+
28
+ ## Notes
29
+
30
+ - `calls.jsonl` contains data from your authenticated session and is gitignored
31
+ — don't commit it.
32
+ - If `extract_hashes.rb` warns that an operation is missing, you didn't exercise
33
+ that part of the flow (e.g. you never added an item, so
34
+ `UpdateCartItemsMutation` wasn't fired). Re-run and complete every step.
@@ -0,0 +1,61 @@
1
+ // Capture Instacart's current persisted-query hashes.
2
+ //
3
+ // Instacart's web client sends Apollo persisted queries (an operation name +
4
+ // a SHA-256 hash). Those hashes rotate whenever Instacart ships a new frontend
5
+ // build, so `lib/instacart_api/persisted_queries.rb` needs periodic refreshing.
6
+ //
7
+ // This opens a real browser, waits for you to log in and drive the shopping
8
+ // flow (search, add an item to the cart), and records every /graphql call to
9
+ // calls.jsonl. Then run `ruby extract_hashes.rb` to turn that into the hash
10
+ // map you paste into persisted_queries.rb.
11
+ //
12
+ // npm install playwright && npx playwright install chromium
13
+ // node capture.js
14
+ // ruby extract_hashes.rb
15
+ const { chromium } = require("playwright");
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+
19
+ const OUT = path.join(__dirname, "calls.jsonl");
20
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
+
22
+ (async () => {
23
+ const browser = await chromium.launch({ headless: false });
24
+ const context = await browser.newContext();
25
+ const page = await context.newPage();
26
+
27
+ const out = fs.createWriteStream(OUT, { flags: "w" });
28
+ page.on("request", (req) => {
29
+ if (!/instacart\.com\/graphql/.test(req.url())) return;
30
+ out.write(JSON.stringify({
31
+ method: req.method(), url: req.url(), post: req.postData() || null,
32
+ }) + "\n");
33
+ });
34
+
35
+ await page.goto("https://www.instacart.com/login", {
36
+ waitUntil: "domcontentloaded",
37
+ });
38
+ console.log(
39
+ "\nLog in, then: pick a store, SEARCH for an item, and ADD one to your cart."
40
+ );
41
+ console.log("Capturing /graphql calls... this window closes automatically.\n");
42
+
43
+ // Capture for a fixed window; exit ~15s after a cart mutation is seen.
44
+ let sawMutation = false;
45
+ page.on("request", (req) => {
46
+ if (/UpdateCartItemsMutation|updateCartItems/.test(req.postData() || "")) {
47
+ sawMutation = true;
48
+ }
49
+ });
50
+ let mutAt = null;
51
+ for (let i = 0; i < 200; i++) {
52
+ await sleep(3000);
53
+ if (sawMutation && mutAt === null) mutAt = i;
54
+ if (mutAt !== null && i - mutAt >= 5) break;
55
+ }
56
+
57
+ out.end();
58
+ console.log(`Wrote ${OUT}. Now run: ruby ${__dirname}/extract_hashes.rb`);
59
+ await browser.close();
60
+ process.exit(0);
61
+ })();
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reads calls.jsonl (produced by capture.js) and prints the persisted-query
4
+ # hashes for the operations the gem uses, formatted to paste straight into
5
+ # lib/instacart_api/persisted_queries.rb.
6
+ #
7
+ # ruby extract_hashes.rb
8
+
9
+ require "json"
10
+ require "uri"
11
+
12
+ OPERATIONS = %w[
13
+ SearchCrossRetailerGroupResults
14
+ Items
15
+ UpdateCartItemsMutation
16
+ PersonalActiveCarts
17
+ ].freeze
18
+
19
+ calls_path = File.join(__dir__, "calls.jsonl")
20
+ unless File.exist?(calls_path)
21
+ abort "Missing #{calls_path} — run capture.js first."
22
+ end
23
+
24
+ hashes = {}
25
+
26
+ File.foreach(calls_path) do |line|
27
+ line = line.strip
28
+ next if line.empty?
29
+
30
+ call = JSON.parse(line)
31
+ operation = nil
32
+ extensions = nil
33
+
34
+ if call["post"]
35
+ begin
36
+ body = JSON.parse(call["post"])
37
+ rescue JSON::ParserError
38
+ next
39
+ end
40
+ operation = body["operationName"]
41
+ extensions = body["extensions"]
42
+ else
43
+ query = URI.decode_www_form(URI(call["url"]).query || "").to_h
44
+ operation = query["operationName"]
45
+ raw = query["extensions"]
46
+ extensions = JSON.parse(raw) if raw
47
+ end
48
+
49
+ next unless OPERATIONS.include?(operation)
50
+
51
+ sha = extensions&.dig("persistedQuery", "sha256Hash")
52
+ hashes[operation] = sha if sha
53
+ end
54
+
55
+ missing = OPERATIONS - hashes.keys
56
+ warn "WARNING: no hash captured for #{missing.join(', ')}" unless missing.empty?
57
+
58
+ puts " HASHES = {"
59
+ puts hashes.map { |op, sha| %( "#{op}" =>\n "#{sha}") }.join(",\n")
60
+ puts " }.freeze"
data/example_usage.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Run this file with `ruby -I lib example_usage.rb`.
4
+ #
5
+ # Instacart's login is bot-protected and can't be scripted, so authentication
6
+ # is the session cookie from a real browser login. Grab it from your browser's
7
+ # devtools (the `__Host-instacart_sid` cookie and its companions) or with the
8
+ # capture script in `capture/`, and export it along with your delivery zone:
9
+ #
10
+ # export INSTACART_COOKIE="__Host-instacart_sid=...; ..."
11
+ # export INSTACART_POSTAL_CODE="97202"
12
+ # export INSTACART_ZONE_ID="103"
13
+ require "instacart_api"
14
+
15
+ client = InstacartApi::Client.new(
16
+ session_cookie: ENV.fetch("INSTACART_COOKIE"),
17
+ postal_code: ENV.fetch("INSTACART_POSTAL_CODE"),
18
+ zone_id: ENV.fetch("INSTACART_ZONE_ID")
19
+ )
20
+
21
+ # Search across the stores serving your location. `shop_ids` are the shops to
22
+ # search; you can get them from your store context (the search response also
23
+ # echoes the retailers that matched).
24
+ results = client.search(term: "bananas", shop_ids: %w[1292 580])
25
+ group = results.first
26
+ puts "#{group.item_ids.size} matches at shop #{group.shop_id}"
27
+
28
+ # Look up full details (name, price, availability) for those item ids.
29
+ items = client.items(ids: group.item_ids, shop_id: group.shop_id)
30
+ cheapest = items.select(&:available?).min # Item is Comparable by price
31
+ puts "Cheapest: #{cheapest.name} — $#{cheapest.price}"
32
+
33
+ # Add it to your cart (quantity: 0 would remove it). Returns the updated cart.
34
+ cart = client.add_item_to_cart(item_id: cheapest.id, quantity: 1)
35
+ puts "Cart #{cart.id} now has #{cart.item_count} item(s)"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "instacart_api/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "instacart_api"
9
+ spec.version = InstacartApi::VERSION
10
+ spec.authors = ["James Klein"]
11
+ spec.email = ["kleinjm007@gmail.com"]
12
+
13
+ spec.summary = "A Ruby wrapper for Instacart's API"
14
+ spec.description = "A Ruby wrapper for Instacart's API"
15
+ spec.homepage = "https://github.com/kleinjm/instacart_api"
16
+ spec.license = "MIT"
17
+
18
+ spec.required_ruby_version = ">= 3.1"
19
+
20
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/kleinjm/instacart_api"
23
+ spec.metadata["changelog_uri"] = "https://github.com/kleinjm/instacart_api/blob/master/CHANGELOG.md"
24
+ spec.metadata["rubygems_mfa_required"] = "true"
25
+
26
+ # Files shipped in the gem: everything tracked by git except the test tree.
27
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ f.match(%r{^(test|spec|features)/})
30
+ end
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_development_dependency "irb"
37
+ spec.add_development_dependency "pry", "~> 0.14"
38
+ spec.add_development_dependency "rake", "~> 13.0"
39
+ spec.add_development_dependency "rspec", "~> 3.13"
40
+ spec.add_development_dependency "rubocop", "~> 1.60"
41
+ spec.add_development_dependency "simplecov", "~> 0.22"
42
+ spec.add_development_dependency "webmock", "~> 3.20"
43
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstacartApi
4
+ # High-level Instacart client: search for products, look up item details, and
5
+ # add items to the logged-in user's cart.
6
+ #
7
+ # client = InstacartApi::Client.new(
8
+ # session_cookie: ENV.fetch("INSTACART_COOKIE"),
9
+ # postal_code: "97202",
10
+ # zone_id: "103"
11
+ # )
12
+ #
13
+ # `postal_code` and `zone_id` come from the account's delivery address; they
14
+ # scope search and pricing to the stores that serve that location.
15
+ class Client
16
+ def initialize(session_cookie:, postal_code:, zone_id:, shop_id: "0")
17
+ @postal_code = postal_code
18
+ @zone_id = zone_id
19
+ @shop_id = shop_id
20
+ @graphql = GraphqlClient.new(session_cookie: session_cookie)
21
+ end
22
+
23
+ # Search across the retailers serving this location. `shop_ids` is the list
24
+ # of shop ids to search (from the account's store context). Returns one
25
+ # SearchResult per retailer that had matches.
26
+ def search(term:, shop_ids:, first: 7)
27
+ data = graphql.query(
28
+ "SearchCrossRetailerGroupResults",
29
+ searchSource: "cross_retailer_search",
30
+ query: term,
31
+ shopIds: shop_ids,
32
+ first: first,
33
+ shopId: shop_id,
34
+ zoneId: zone_id,
35
+ postalCode: postal_code,
36
+ disableAutocorrect: false,
37
+ includeDebugInfo: false,
38
+ overrideFeatureStates: [],
39
+ autosuggestImpressionId: nil,
40
+ pageViewId: nil
41
+ )
42
+
43
+ data.fetch("searchCrossRetailerGroupResults").
44
+ fetch("results").
45
+ map { |result| SearchResult.new(result) }
46
+ end
47
+
48
+ # Full item details for a set of item ids at one shop. Returns [Item].
49
+ def items(ids:, shop_id:)
50
+ data = graphql.query(
51
+ "Items",
52
+ ids: ids,
53
+ shopId: shop_id,
54
+ zoneId: zone_id,
55
+ postalCode: postal_code
56
+ )
57
+
58
+ data.fetch("items").map { |item| Item.new(item) }
59
+ end
60
+
61
+ # Add (or update) a single item in the active cart. A `quantity` of 0
62
+ # removes it. Returns the updated Cart.
63
+ def add_item_to_cart(item_id:, quantity: 1, quantity_type: "each")
64
+ add_items_to_cart(
65
+ items: [
66
+ { item_id: item_id, quantity: quantity, quantity_type: quantity_type }
67
+ ]
68
+ )
69
+ end
70
+
71
+ # Add (or update) several items in one mutation. Each entry is a hash with
72
+ # :item_id, :quantity, and optional :quantity_type. Returns the updated
73
+ # Cart.
74
+ def add_items_to_cart(items:)
75
+ updates = items.map do |item|
76
+ {
77
+ itemId: item.fetch(:item_id),
78
+ quantity: item.fetch(:quantity),
79
+ quantityType: item.fetch(:quantity_type, "each"),
80
+ trackingParams: { trackingProperties: {} }
81
+ }
82
+ end
83
+
84
+ data = graphql.mutate(
85
+ "UpdateCartItemsMutation", cartItemUpdates: updates
86
+ )
87
+
88
+ Cart.new(data.fetch("updateCartItems").fetch("cart"))
89
+ end
90
+
91
+ # The logged-in user's active carts. Returns [Cart].
92
+ def active_carts
93
+ data = graphql.query("PersonalActiveCarts", {})
94
+
95
+ data.fetch("userCarts").fetch("carts").map { |cart| Cart.new(cart) }
96
+ end
97
+
98
+ private
99
+
100
+ attr_reader :graphql, :postal_code, :zone_id, :shop_id
101
+ end
102
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstacartApi
4
+ # Base class for every error the gem raises.
5
+ class Error < StandardError; end
6
+
7
+ # A non-200 HTTP response from Instacart.
8
+ class ResponseError < Error; end
9
+
10
+ # A 200 response whose GraphQL body carried an `errors` array.
11
+ class GraphqlError < Error; end
12
+
13
+ # A request for an operation we have no persisted-query hash for.
14
+ # Usually means the captured hashes are stale — re-run the capture script.
15
+ class UnknownOperationError < Error; end
16
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstacartApi
4
+ # Thin transport over Instacart's GraphQL endpoint.
5
+ #
6
+ # Authentication is the logged-in session cookie (the
7
+ # `__Host-instacart_sid` cookie, plus its companions) captured from a real
8
+ # browser login — Instacart's login flow is bot-protected and can't be
9
+ # scripted, so the caller supplies the cookie string. Every request also
10
+ # needs the `X-Client-Identifier: web` header, without which the
11
+ # persisted-query resolver rejects the variables.
12
+ class GraphqlClient
13
+ ENDPOINT = URI("https://www.instacart.com/graphql")
14
+
15
+ # A recent desktop Chrome UA; Instacart serves the web GraphQL API to it.
16
+ DEFAULT_USER_AGENT =
17
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " \
18
+ "AppleWebKit/537.36 (KHTML, like Gecko) " \
19
+ "Chrome/126.0.0.0 Safari/537.36"
20
+
21
+ def initialize(session_cookie:, user_agent: DEFAULT_USER_AGENT)
22
+ @session_cookie = session_cookie
23
+ @user_agent = user_agent
24
+ end
25
+
26
+ # Run a persisted query (HTTP GET). Returns the `data` hash.
27
+ def query(operation, variables)
28
+ uri = ENDPOINT.dup
29
+ uri.query = URI.encode_www_form(
30
+ operationName: operation,
31
+ variables: JSON.generate(variables),
32
+ extensions: JSON.generate(extensions(operation))
33
+ )
34
+ perform(Net::HTTP::Get.new(uri))
35
+ end
36
+
37
+ # Run a persisted mutation (HTTP POST). Returns the `data` hash.
38
+ def mutate(operation, variables)
39
+ request = Net::HTTP::Post.new(ENDPOINT)
40
+ request.body = JSON.generate(
41
+ operationName: operation,
42
+ variables: variables,
43
+ extensions: extensions(operation)
44
+ )
45
+ perform(request)
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :session_cookie, :user_agent
51
+
52
+ def extensions(operation)
53
+ {
54
+ persistedQuery: {
55
+ version: 1,
56
+ sha256Hash: PersistedQueries.hash_for(operation)
57
+ }
58
+ }
59
+ end
60
+
61
+ def perform(request)
62
+ configure(request)
63
+
64
+ response = Net::HTTP.start(
65
+ ENDPOINT.hostname, ENDPOINT.port, use_ssl: true
66
+ ) { |http| http.request(request) }
67
+
68
+ unless response.code == "200"
69
+ raise ResponseError, "Instacart responded with HTTP #{response.code}"
70
+ end
71
+
72
+ parse(response.body)
73
+ end
74
+
75
+ def configure(request)
76
+ request["Accept"] = "application/json"
77
+ request["Content-Type"] = "application/json"
78
+ request["X-Client-Identifier"] = "web"
79
+ request["X-Requested-With"] = "XMLHttpRequest"
80
+ request["User-Agent"] = user_agent
81
+ request["Cookie"] = session_cookie
82
+ end
83
+
84
+ def parse(body)
85
+ json = JSON.parse(body)
86
+
87
+ if (errors = json["errors"])
88
+ messages = errors.map { |error| error["message"] }.join("; ")
89
+ raise GraphqlError, messages
90
+ end
91
+
92
+ json.fetch("data")
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstacartApi
4
+ # A shopping cart, as returned by the cart mutation/queries. Instacart keeps a
5
+ # separate cart per retailer; `item_count` is the total line quantity.
6
+ class Cart
7
+ # A single line in the cart.
8
+ class LineItem
9
+ attr_reader :id, :item_id, :product_id, :quantity, :quantity_type
10
+
11
+ def initialize(data)
12
+ @id = data["id"]
13
+ basket_product = data["basketProduct"] || {}
14
+ @item_id = basket_product["id"]
15
+ @product_id = basket_product["productId"]
16
+ @quantity = data["quantity"]
17
+ @quantity_type = data["quantityType"]
18
+ end
19
+ end
20
+
21
+ attr_reader :id, :cart_type, :retailer_id, :item_count, :items
22
+
23
+ def initialize(data)
24
+ @id = data["id"]
25
+ @cart_type = data["cartType"]
26
+ @retailer_id = data["retailerId"]
27
+ @item_count = data["itemCount"]
28
+
29
+ line_items = data.dig("cartItemCollection", "cartItems") || []
30
+ @items = line_items.map { |line_item| LineItem.new(line_item) }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstacartApi
4
+ # A single product returned by the `Items` query.
5
+ #
6
+ # Instacart item ids look like `items_<retailerLocationId>-<productId>`
7
+ # (e.g. `items_3113-22373938`); that full id is what the cart mutation
8
+ # expects.
9
+ class Item
10
+ include Comparable
11
+
12
+ attr_reader :id, :name, :size, :product_id, :brand_name,
13
+ :price, :full_price, :quantity_type, :stock_level
14
+
15
+ def initialize(data)
16
+ @id = data["id"]
17
+ @name = data["name"]
18
+ @size = data["size"]
19
+ @product_id = data["productId"]
20
+ @brand_name = data["brandName"]
21
+
22
+ pricing = data.dig("price", "viewSection") || {}
23
+ @price = pricing["priceValueString"]&.to_f
24
+ @full_price = pricing["fullPriceString"]
25
+
26
+ availability = data["availability"] || {}
27
+ @available = availability["available"]
28
+ @stock_level = availability["stockLevel"]
29
+
30
+ @quantity_type =
31
+ (data["quantityAttributes"] || {})["quantityType"] || "each"
32
+ end
33
+
34
+ def available?
35
+ @available == true
36
+ end
37
+
38
+ # Compare by price so a list of items can be sorted cheapest-first.
39
+ def <=>(other)
40
+ price <=> other.price
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstacartApi
4
+ # One retailer's slice of a cross-retailer search: which store it is and the
5
+ # ids of the items that matched there. Fetch full item details by passing
6
+ # `item_ids` (with `shop_id`) to `Client#items`.
7
+ class SearchResult
8
+ attr_reader :retailer_id, :shop_id, :item_ids
9
+
10
+ def initialize(data)
11
+ @retailer_id = data["retailerId"]
12
+ @shop_id = data["shopId"]
13
+ @item_ids = data["itemIds"] || []
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstacartApi
4
+ # Instacart's web client sends Apollo *persisted queries*: instead of the
5
+ # full GraphQL document, it sends an operation name plus a SHA-256 hash the
6
+ # server already knows. The server rejects unknown hashes
7
+ # ("PersistedQueryNotSupported"), so we must send the exact hashes the live
8
+ # client uses.
9
+ #
10
+ # These hashes are tied to a frontend build and rotate when Instacart
11
+ # deploys. When calls start failing with UnknownOperationError or
12
+ # GraphqlError, refresh them by re-running the capture script (see
13
+ # capture/README.md) and pasting the new values here.
14
+ module PersistedQueries
15
+ HASHES = {
16
+ "SearchCrossRetailerGroupResults" =>
17
+ "0ef32d339ed761d8609b91d8232a26b7b6b05206baf32694c6fa47f7f8e73a33",
18
+ "Items" =>
19
+ "9ad66078d7fa81276b6bd4eb6a6f6fcdd1f4022ff0c3f5b4663c62877f06692a",
20
+ "UpdateCartItemsMutation" =>
21
+ "ba4bf465d294d1d528d82a4ac48ac13980d528149874c0e52082dc1d833bdb09",
22
+ "PersonalActiveCarts" =>
23
+ "eac9d17bd45b099fbbdabca2e111acaf2a4fa486f2ce5bc4e8acbab2f31fd8c0"
24
+ }.freeze
25
+
26
+ # The persisted-query hash for an operation, or raise if we don't have one.
27
+ def self.hash_for(operation)
28
+ HASHES.fetch(operation) do
29
+ raise UnknownOperationError,
30
+ "No persisted-query hash for #{operation.inspect}. " \
31
+ "The captured hashes may be stale — re-run the capture script."
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstacartApi
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ require "instacart_api/version"
8
+ require "instacart_api/errors"
9
+ require "instacart_api/persisted_queries"
10
+ require "instacart_api/graphql_client"
11
+ require "instacart_api/models/item"
12
+ require "instacart_api/models/search_result"
13
+ require "instacart_api/models/cart"
14
+ require "instacart_api/client"
15
+
16
+ # Ruby wrapper around Instacart's (undocumented) web GraphQL API.
17
+ module InstacartApi
18
+ end
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: instacart_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - James Klein
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: irb
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: pry
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.14'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.14'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.13'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.13'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.60'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.60'
82
+ - !ruby/object:Gem::Dependency
83
+ name: simplecov
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.22'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.22'
96
+ - !ruby/object:Gem::Dependency
97
+ name: webmock
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.20'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.20'
110
+ description: A Ruby wrapper for Instacart's API
111
+ email:
112
+ - kleinjm007@gmail.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - ".github/workflows/ci.yml"
118
+ - ".github/workflows/release.yml"
119
+ - ".gitignore"
120
+ - ".rspec"
121
+ - ".rubocop.yml"
122
+ - ".ruby-version"
123
+ - CHANGELOG.md
124
+ - Gemfile
125
+ - Gemfile.lock
126
+ - LICENSE
127
+ - README.md
128
+ - Rakefile
129
+ - bin/console
130
+ - bin/rspec
131
+ - bin/setup
132
+ - capture/README.md
133
+ - capture/capture.js
134
+ - capture/extract_hashes.rb
135
+ - example_usage.rb
136
+ - instacart_api.gemspec
137
+ - lib/instacart_api.rb
138
+ - lib/instacart_api/client.rb
139
+ - lib/instacart_api/errors.rb
140
+ - lib/instacart_api/graphql_client.rb
141
+ - lib/instacart_api/models/cart.rb
142
+ - lib/instacart_api/models/item.rb
143
+ - lib/instacart_api/models/search_result.rb
144
+ - lib/instacart_api/persisted_queries.rb
145
+ - lib/instacart_api/version.rb
146
+ homepage: https://github.com/kleinjm/instacart_api
147
+ licenses:
148
+ - MIT
149
+ metadata:
150
+ allowed_push_host: https://rubygems.org
151
+ homepage_uri: https://github.com/kleinjm/instacart_api
152
+ source_code_uri: https://github.com/kleinjm/instacart_api
153
+ changelog_uri: https://github.com/kleinjm/instacart_api/blob/master/CHANGELOG.md
154
+ rubygems_mfa_required: 'true'
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '3.1'
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubygems_version: 4.0.10
170
+ specification_version: 4
171
+ summary: A Ruby wrapper for Instacart's API
172
+ test_files: []