norairrecord 0.1.2

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: e43689c499a7d1d54963791d0dacd275d2cc9c8b0c2dc27b636884b51596bbee
4
+ data.tar.gz: '01970ece2dddb567110ebaaec5f855df7240d329f1403c33aad49e3632aa978e'
5
+ SHA512:
6
+ metadata.gz: '097efbecab4facd4d22299b7b5629b015c032c93808342f88cb6cdb652d572b72e7d1a5c48d165827ea5d35e9be41c0d6d37aafb82a411474d302e14e911fb8b'
7
+ data.tar.gz: f166ce6e3be2bdd525c78168df6c9f0e7dc10bd95c779abe55b742a710fb904f8694bd2604c42d93264ff607a2df962fb07dc88d7d9610c7dc269f8ef2ea5e64
@@ -0,0 +1,26 @@
1
+ name: gemerald
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main" ]
6
+
7
+ jobs:
8
+ push:
9
+ name: Push gem to RubyGems.org
10
+ runs-on: ubuntu-latest
11
+
12
+ permissions:
13
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
14
+ contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
15
+
16
+ steps:
17
+ # Set up
18
+ - uses: actions/checkout@v4
19
+ - name: Set up Ruby
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ bundler-cache: true
23
+ ruby-version: 3.3.5
24
+
25
+ # Release
26
+ - uses: rubygems/release-gem@v1
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ vendor/
11
+ test/fixtures
12
+ .byebug_history
13
+ *.gem
14
+ .idea
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.7.2
5
+ - 3.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in airtable.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2017 Simon Eskildsen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Norairrecord
2
+
3
+ [airrecord](https://github.com/sirupsen/airrecord) : norairrecord :: epinephrine : norepinephrine
4
+
5
+ stuff not in the OG:
6
+ * `Table#comment`
7
+ * ```ruby
8
+ rec.comment "pretty cool record!"
9
+ ```
10
+ * `Table#patch`!
11
+ * ```ruby
12
+ rec.patch({ # this will fire off a request
13
+ "field 1" => "new value 1", # that only modifies
14
+ "field 2" => "new value 2", # the specified fields
15
+ })
16
+ ```
17
+ * """""transactions"""""!
18
+ * they're not great but they kinda do a thing!
19
+ * ```ruby
20
+ rec.transaction do |rec| # pipes optional
21
+ # do some stuff to rec...
22
+ # all changes inside the block happen in 1 request
23
+ # none of the changes happen if an error is hit
24
+ end
25
+ ```
26
+ * custom endpoint URL
27
+ * handy for inspecting/ratelimiting
28
+ * `Norairrecord.base_url = "https://somewhere_else"`
29
+ * custom UA
30
+ * `Norairrecord.user_agent = "i'm the reason why you're getting 429s!"`
31
+ * `Table#airtable_url`
32
+ * what it says on the tin!
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "norairrecord"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,11 @@
1
+ require 'norairrecord'
2
+
3
+ p Faraday::VERSION
4
+
5
+ Tea = Norairrecord.table(
6
+ ENV["AIRTABLE_TOKEN"],
7
+ "appZJC9q8TBYPDF7j",
8
+ "Teas"
9
+ )
10
+
11
+ # p Tea.all
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
@@ -0,0 +1,50 @@
1
+ require_relative 'faraday_rate_limiter'
2
+ require 'erb'
3
+
4
+ module Norairrecord
5
+ class Client
6
+ attr_reader :api_key
7
+ attr_writer :connection
8
+
9
+ # Per Airtable's documentation you will get throttled for 30 seconds if you
10
+ # issue more than 5 requests per second. Airrecord is a good citizen.
11
+ AIRTABLE_RPS_LIMIT = 5
12
+
13
+ def initialize(api_key)
14
+ @api_key = api_key
15
+ end
16
+
17
+ def connection
18
+ @connection ||= Faraday.new(
19
+ url: Norairrecord.base_url || "https://api.airtable.com",
20
+ headers: {
21
+ "Authorization" => "Bearer #{api_key}",
22
+ "User-Agent" => Norairrecord.user_agent || "Airrecord (nora's version)/#{Norairrecord::VERSION}",
23
+ },
24
+ ) do |conn|
25
+ if Norairrecord.throttle?
26
+ conn.request :airrecord_rate_limiter, requests_per_second: AIRTABLE_RPS_LIMIT
27
+ end
28
+ conn.adapter :net_http_persistent
29
+ end
30
+ end
31
+
32
+ def escape(*args)
33
+ ERB::Util.url_encode(*args)
34
+ end
35
+
36
+ def parse(body)
37
+ JSON.parse(body)
38
+ rescue JSON::ParserError
39
+ nil
40
+ end
41
+
42
+ def handle_error(status, error)
43
+ if error.is_a?(Hash) && error['error']
44
+ raise Error, "HTTP #{status}: #{error['error']['type']}: #{error['error']['message']}"
45
+ else
46
+ raise Error, "HTTP #{status}: Communication error: #{error}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,61 @@
1
+ require 'thread'
2
+
3
+ module Norairrecord
4
+ class FaradayRateLimiter < Faraday::Middleware
5
+ class << self
6
+ attr_accessor :requests
7
+ end
8
+
9
+ def initialize(app, requests_per_second: nil, sleeper: nil)
10
+ super(app)
11
+ @rps = requests_per_second
12
+ @sleeper = sleeper || ->(seconds) { sleep(seconds) }
13
+ @mutex = Mutex.new
14
+ clear
15
+ end
16
+
17
+ def call(env)
18
+ @mutex.synchronize do
19
+ wait if too_many_requests_in_last_second?
20
+ @app.call(env).on_complete do |_response_env|
21
+ requests << Process.clock_gettime(Process::CLOCK_MONOTONIC)
22
+ requests.shift if requests.size > @rps
23
+ end
24
+ end
25
+ end
26
+
27
+ def clear
28
+ self.class.requests = []
29
+ end
30
+
31
+ private
32
+
33
+ def requests
34
+ self.class.requests
35
+ end
36
+
37
+ def too_many_requests_in_last_second?
38
+ return false unless @rps
39
+ return false unless requests.size >= @rps
40
+
41
+ window_span < 1.0
42
+ end
43
+
44
+ def wait
45
+ # Time to wait until making the next request to stay within limits.
46
+ # [1.1, 1.2, 1.3, 1.4, 1.5] => 1 - 0.4 => 0.6
47
+ wait_time = 1.0 - window_span
48
+ @sleeper.call(wait_time)
49
+ end
50
+
51
+ # [1.1, 1.2, 1.3, 1.4, 1.5] => 1.5 - 1.1 => 0.4
52
+ def window_span
53
+ requests.last - requests.first
54
+ end
55
+ end
56
+ end
57
+
58
+ Faraday::Request.register_middleware(
59
+ # Avoid polluting the global middleware namespace with a prefix.
60
+ airrecord_rate_limiter: Norairrecord::FaradayRateLimiter
61
+ )
@@ -0,0 +1,306 @@
1
+ require 'rubygems' # For Gem::Version
2
+
3
+ module Norairrecord
4
+ class Table
5
+ class << self
6
+ attr_accessor :base_key, :table_name
7
+ attr_writer :api_key
8
+
9
+ def client
10
+ @@clients ||= {}
11
+ @@clients[api_key] ||= Client.new(api_key)
12
+ end
13
+
14
+ def api_key
15
+ defined?(@api_key) ? @api_key : Norairrecord.api_key
16
+ end
17
+
18
+ def has_many(method_name, options)
19
+ define_method(method_name.to_sym) do
20
+ # Get association ids in reverse order, because Airtable's UI and API
21
+ # sort associations in opposite directions. We want to match the UI.
22
+ ids = (self[options.fetch(:column)] || []).reverse
23
+ table = Kernel.const_get(options.fetch(:class))
24
+ return table.find_many(ids) unless options[:single]
25
+
26
+ (id = ids.first) ? table.find(id) : nil
27
+ end
28
+
29
+ define_method("#{method_name}=".to_sym) do |value|
30
+ self[options.fetch(:column)] = Array(value).map(&:id).reverse
31
+ end
32
+ end
33
+
34
+ def belongs_to(method_name, options)
35
+ has_many(method_name, options.merge(single: true))
36
+ end
37
+
38
+ alias has_one belongs_to
39
+
40
+ def find(id)
41
+ response = client.connection.get("v0/#{base_key}/#{client.escape(table_name)}/#{id}")
42
+ parsed_response = client.parse(response.body)
43
+
44
+ if response.success?
45
+ self.new(parsed_response["fields"], id: id, created_at: parsed_response["createdTime"])
46
+ else
47
+ client.handle_error(response.status, parsed_response)
48
+ end
49
+ end
50
+
51
+ def find_many(ids)
52
+ return [] if ids.empty?
53
+
54
+ or_args = ids.map { |id| "RECORD_ID() = '#{id}'"}.join(',')
55
+ formula = "OR(#{or_args})"
56
+ records(filter: formula).sort_by { |record| or_args.index(record.id) }
57
+ end
58
+
59
+ def update(id, update_hash = {}, options = {})
60
+ # To avoid trying to update computed fields we *always* use PATCH
61
+ body = {
62
+ fields: update_hash,
63
+ **options
64
+ }.to_json
65
+
66
+ response = client.connection.patch("v0/#{base_key}/#{client.escape(table_name)}/#{id}", body, { 'Content-Type' => 'application/json' })
67
+ parsed_response = client.parse(response.body)
68
+
69
+ if response.success?
70
+ parsed_response["fields"]
71
+ else
72
+ client.handle_error(response.status, parsed_response)
73
+ end
74
+ end
75
+
76
+
77
+ def create(fields, options = {})
78
+ new(fields).tap { |record| record.save(options) }
79
+ end
80
+
81
+ def records(filter: nil, sort: nil, view: nil, offset: nil, paginate: true, fields: nil, max_records: nil, page_size: nil)
82
+ options = {}
83
+ options[:filterByFormula] = filter if filter
84
+
85
+ if sort
86
+ options[:sort] = sort.map { |field, direction|
87
+ { field: field.to_s, direction: direction }
88
+ }
89
+ end
90
+
91
+ options[:view] = view if view
92
+ options[:offset] = offset if offset
93
+ options[:fields] = fields if fields
94
+ options[:maxRecords] = max_records if max_records
95
+ options[:pageSize] = page_size if page_size
96
+
97
+ path = "v0/#{base_key}/#{client.escape(table_name)}/listRecords"
98
+ response = client.connection.post(path, options.to_json, { 'Content-Type' => 'application/json' })
99
+ parsed_response = client.parse(response.body)
100
+
101
+ if response.success?
102
+ records = parsed_response["records"]
103
+ records = records.map { |record|
104
+ self.new(record["fields"], id: record["id"], created_at: record["createdTime"])
105
+ }
106
+
107
+ if paginate && parsed_response["offset"]
108
+ records.concat(records(
109
+ filter: filter,
110
+ sort: sort,
111
+ view: view,
112
+ paginate: paginate,
113
+ fields: fields,
114
+ offset: parsed_response["offset"],
115
+ max_records: max_records,
116
+ page_size: page_size,
117
+ ))
118
+ end
119
+
120
+ records
121
+ else
122
+ client.handle_error(response.status, parsed_response)
123
+ end
124
+ end
125
+ alias all records
126
+ end
127
+
128
+ attr_reader :fields, :id, :created_at, :updated_keys
129
+
130
+ # This is an awkward definition for Ruby 3 to remain backwards compatible.
131
+ # It's easier to read by reading the 2.x definition below.
132
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0.0")
133
+ def initialize(*one, **two)
134
+ @id = one.first && two.delete(:id)
135
+ self.created_at = one.first && two.delete(:created_at)
136
+ self.fields = one.first || two
137
+ end
138
+ else
139
+ def initialize(fields, id: nil, created_at: nil)
140
+ @id = id
141
+ self.created_at = created_at
142
+ self.fields = fields
143
+ end
144
+ end
145
+
146
+ def new_record?
147
+ !id
148
+ end
149
+
150
+ def [](key)
151
+ validate_key(key)
152
+ fields[key]
153
+ end
154
+
155
+ def []=(key, value)
156
+ validate_key(key)
157
+ return if fields[key] == value # no-op
158
+
159
+ @updated_keys << key
160
+ fields[key] = value
161
+ end
162
+
163
+ def patch(update_hash = {}, options = {})
164
+ update_hash.reject! { |key, value| @fields[key] == value }
165
+ return @fields if update_hash.empty? # don't hit AT if we don't have real changes
166
+ @fields.merge!(self.class.update(self.id, update_hash, options).reject { |key, _| updated_keys.include?(key) })
167
+ end
168
+
169
+ def create(options = {})
170
+ raise Error, "Record already exists (record has an id)" unless new_record?
171
+
172
+ body = {
173
+ fields: serializable_fields,
174
+ **options
175
+ }.to_json
176
+
177
+ response = client.connection.post("v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}", body, { 'Content-Type' => 'application/json' })
178
+ parsed_response = client.parse(response.body)
179
+
180
+ if response.success?
181
+ @id = parsed_response["id"]
182
+ self.created_at = parsed_response["createdTime"]
183
+ self.fields = parsed_response["fields"]
184
+ else
185
+ client.handle_error(response.status, parsed_response)
186
+ end
187
+ end
188
+
189
+ def save(options = {})
190
+ return create(options) if new_record?
191
+ return true if @updated_keys.empty?
192
+
193
+ update_hash = Hash[@updated_keys.map { |key|
194
+ [key, fields[key]]
195
+ }]
196
+
197
+ self.patch(update_hash, options)
198
+ end
199
+
200
+ def destroy
201
+ raise Error, "Unable to destroy new record" if new_record?
202
+
203
+ response = client.connection.delete("v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}/#{self.id}")
204
+ parsed_response = client.parse(response.body)
205
+
206
+ if response.success?
207
+ true
208
+ else
209
+ client.handle_error(response.status, parsed_response)
210
+ end
211
+ end
212
+
213
+ def serializable_fields
214
+ fields
215
+ end
216
+
217
+ def comment(text)
218
+ response = client.connection.post("v0/#{self.class.base_key}/#{client.escape(self.class.table_name)}/#{self.id}/comments", {text:}.to_json, { 'Content-Type' => 'application/json' })
219
+ parsed_response = client.parse(response.body)
220
+
221
+ if response.success?
222
+ parsed_response['id']
223
+ else
224
+ client.handle_error(response.status, parsed_response)
225
+ end
226
+ end
227
+
228
+ def airtable_url
229
+ "https://airtable.com/#{self.class.base_key}/#{self.class.table_name}/#{self.id}"
230
+ end
231
+
232
+ def ==(other)
233
+ self.class == other.class &&
234
+ serializable_fields == other.serializable_fields
235
+ end
236
+ alias eql? ==
237
+
238
+ def hash
239
+ serializable_fields.hash
240
+ end
241
+
242
+ # ahahahahaha
243
+ def transaction(&block)
244
+ txn_updates = {}
245
+
246
+ singleton_class.alias_method :original_setter, :[]=
247
+
248
+ define_singleton_method(:[]=) do |key, value|
249
+ txn_updates[key] = value
250
+ end
251
+
252
+ begin
253
+ result = yield self
254
+ @updated_keys -= txn_updates.keys
255
+ if new_record?
256
+ @fields.merge!(txn_updates)
257
+ save
258
+ else
259
+ self.patch(txn_updates)
260
+ end
261
+ rescue => e
262
+ raise
263
+ ensure
264
+ singleton_class.alias_method :[]=, :original_setter
265
+ singleton_class.remove_method :original_setter
266
+ end
267
+ result
268
+ end
269
+
270
+
271
+ protected
272
+
273
+ def fields=(fields)
274
+ @updated_keys = []
275
+ @fields = fields
276
+ end
277
+
278
+ def created_at=(created_at)
279
+ return unless created_at
280
+
281
+ @created_at = Time.parse(created_at)
282
+ end
283
+
284
+ def client
285
+ self.class.client
286
+ end
287
+
288
+ def validate_key(key)
289
+ return true unless key.is_a?(Symbol)
290
+
291
+ raise(Error, [
292
+ "Airrecord 1.0 dropped support for Symbols as field names.",
293
+ "Please use the raw field name, a String, instead.",
294
+ "You might try: record['#{key.to_s.tr('_', ' ')}']"
295
+ ].join("\n"))
296
+ end
297
+ end
298
+
299
+ def self.table(api_key, base_key, table_name)
300
+ Class.new(Table) do |klass|
301
+ klass.table_name = table_name
302
+ klass.api_key = api_key
303
+ klass.base_key = base_key
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,3 @@
1
+ module Norairrecord
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,20 @@
1
+ require "json"
2
+ require "faraday"
3
+ require 'faraday/net_http_persistent'
4
+ require "time"
5
+ require "norairrecord/version"
6
+ require "norairrecord/client"
7
+ require "norairrecord/table"
8
+
9
+ module Norairrecord
10
+ extend self
11
+ attr_accessor :api_key, :throttle, :base_url, :user_agent
12
+
13
+ Error = Class.new(StandardError)
14
+
15
+ def throttle?
16
+ return true if @throttle.nil?
17
+
18
+ @throttle
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'norairrecord/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "norairrecord"
9
+ spec.version = Norairrecord::VERSION
10
+ spec.authors = ["nora"]
11
+ spec.email = ["nora@hcb.pizza"]
12
+
13
+ spec.summary = %q{Airtable client}
14
+ spec.description = %q{screwed a cookie to the tabel}
15
+ spec.homepage = "https://github.com/24c02/norairrecord"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+ spec.required_ruby_version = ">= 2.2"
23
+
24
+ spec.add_dependency "faraday", [">= 1.0", "< 3.0"]
25
+ spec.add_dependency "net-http-persistent"
26
+ spec.add_dependency "faraday-net_http_persistent"
27
+
28
+ spec.add_development_dependency "bundler", "~> 2"
29
+ spec.add_development_dependency "rake"
30
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: norairrecord
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - nora
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: net-http-persistent
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: faraday-net_http_persistent
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ description: screwed a cookie to the tabel
90
+ email:
91
+ - nora@hcb.pizza
92
+ executables: []
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - ".github/workflows/gem-push.yml"
97
+ - ".gitignore"
98
+ - ".travis.yml"
99
+ - Gemfile
100
+ - LICENSE
101
+ - README.md
102
+ - Rakefile
103
+ - bin/console
104
+ - bin/production-test.rb
105
+ - bin/setup
106
+ - lib/norairrecord.rb
107
+ - lib/norairrecord/client.rb
108
+ - lib/norairrecord/faraday_rate_limiter.rb
109
+ - lib/norairrecord/table.rb
110
+ - lib/norairrecord/version.rb
111
+ - norairrecord.gemspec
112
+ homepage: https://github.com/24c02/norairrecord
113
+ licenses:
114
+ - MIT
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '2.2'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubygems_version: 3.5.16
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: Airtable client
135
+ test_files: []