nohead 0.1.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +298 -0
- data/lib/nohead/client.rb +265 -0
- data/lib/nohead/errors.rb +119 -0
- data/lib/nohead/nohead_object.rb +63 -0
- data/lib/nohead/operations.rb +70 -0
- data/lib/nohead/page.rb +52 -0
- data/lib/nohead/resources/assets.rb +71 -0
- data/lib/nohead/resources/other.rb +38 -0
- data/lib/nohead/resources/records.rb +137 -0
- data/lib/nohead/resources/schema.rb +148 -0
- data/lib/nohead/transport.rb +34 -0
- data/lib/nohead/uploads.rb +65 -0
- data/lib/nohead/version.rb +5 -0
- data/lib/nohead/webhooks.rb +120 -0
- data/lib/nohead.rb +25 -0
- metadata +64 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nohead
|
|
4
|
+
module Resources
|
|
5
|
+
# Collections of the key's project. `collection` is an ID or slug.
|
|
6
|
+
class Collections < Resource
|
|
7
|
+
attr_reader :schema_changes, :search_index
|
|
8
|
+
|
|
9
|
+
def initialize(client)
|
|
10
|
+
super
|
|
11
|
+
@schema_changes = SchemaChanges.new(client)
|
|
12
|
+
@search_index = SearchIndexes.new(client)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# deleted: true lists deleted collections instead (restorable for 30 days).
|
|
16
|
+
def list(deleted: nil, limit: nil, cursor: nil)
|
|
17
|
+
@client.paginate("collections_list", query: { deleted: deleted, limit: limit, cursor: cursor })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get(collection)
|
|
21
|
+
@client.request("collections_get", path: { collection_id: collection })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Creates a collection, optionally with its fields.
|
|
25
|
+
def create(idempotency_key: nil, change_note: nil, **params)
|
|
26
|
+
@client.request("collections_create", body: params, idempotency_key: idempotency_key,
|
|
27
|
+
change_note: change_note)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update(collection, idempotency_key: nil, change_note: nil, **params)
|
|
31
|
+
@client.request("collections_update", path: { collection_id: collection }, body: params,
|
|
32
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Soft-deletes the collection and its records for 30 days.
|
|
36
|
+
def delete(collection, idempotency_key: nil, change_note: nil)
|
|
37
|
+
@client.request("collections_delete", path: { collection_id: collection },
|
|
38
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def restore(collection, idempotency_key: nil, change_note: nil)
|
|
42
|
+
@client.request("collections_restore", path: { collection_id: collection },
|
|
43
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The collection's schema, now or at a past `version`.
|
|
47
|
+
def schema(collection, version: nil)
|
|
48
|
+
@client.request("collections_get_schema", path: { collection_id: collection },
|
|
49
|
+
query: { version: version })
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A collection's schema history, newest first.
|
|
54
|
+
class SchemaChanges < Resource
|
|
55
|
+
def list(collection, limit: nil, cursor: nil)
|
|
56
|
+
@client.paginate("schema_changes_list", path: { collection_id: collection },
|
|
57
|
+
query: { limit: limit, cursor: cursor })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def get(collection, schema_change)
|
|
61
|
+
@client.request("schema_changes_get",
|
|
62
|
+
path: { collection_id: collection, schema_change_id: schema_change })
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A collection's search index.
|
|
67
|
+
class SearchIndexes < Resource
|
|
68
|
+
def get(collection)
|
|
69
|
+
@client.request("search_index_get", path: { collection_id: collection })
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Rebuilds the index from the records; searches keep working meanwhile.
|
|
73
|
+
def rebuild(collection, idempotency_key: nil)
|
|
74
|
+
@client.request("search_index_rebuild", path: { collection_id: collection },
|
|
75
|
+
idempotency_key: idempotency_key)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Fields of a collection. `field` is a field ID ("fld_...").
|
|
80
|
+
class Fields < Resource
|
|
81
|
+
def list(collection, deleted: nil, limit: nil, cursor: nil)
|
|
82
|
+
@client.paginate("fields_list", path: { collection_id: collection },
|
|
83
|
+
query: { deleted: deleted, limit: limit, cursor: cursor })
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def create(collection, idempotency_key: nil, change_note: nil, **params)
|
|
87
|
+
@client.request("fields_create", path: { collection_id: collection }, body: params,
|
|
88
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Renames, describes or loosens a field. Changes that rewrite record
|
|
92
|
+
# values (type, multiple, tighter rules) are `migrate`.
|
|
93
|
+
def update(field, idempotency_key: nil, change_note: nil, **params)
|
|
94
|
+
@client.request("fields_update", path: { field_id: field }, body: params,
|
|
95
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Soft-deletes the field: its values return with `restore` for 30 days.
|
|
99
|
+
def delete(field, idempotency_key: nil, change_note: nil)
|
|
100
|
+
@client.request("fields_delete", path: { field_id: field },
|
|
101
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def restore(field, idempotency_key: nil, change_note: nil)
|
|
105
|
+
@client.request("fields_restore", path: { field_id: field },
|
|
106
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Puts the collection's fields in this order.
|
|
110
|
+
def reorder(collection, field_ids, idempotency_key: nil, change_note: nil)
|
|
111
|
+
@client.request("fields_reorder", path: { collection_id: collection },
|
|
112
|
+
body: { field_ids: field_ids },
|
|
113
|
+
idempotency_key: idempotency_key, change_note: change_note)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Stops accepting a renamed field's old API key before its 6 months end.
|
|
117
|
+
def remove_alias(field, alias_key, idempotency_key: nil)
|
|
118
|
+
@client.request("fields_remove_alias", path: { field_id: field, alias: alias_key },
|
|
119
|
+
idempotency_key: idempotency_key)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Starts a field migration (type, multiple, tighter configuration or a
|
|
123
|
+
# backfill) that rewrites every record. With dry_run: true, previews it.
|
|
124
|
+
def migrate(field, dry_run: false, idempotency_key: nil, change_note: nil, **params)
|
|
125
|
+
@client.request("fields_migrate", path: { field_id: field }, query: { dry_run: dry_run || nil },
|
|
126
|
+
body: params, idempotency_key: idempotency_key,
|
|
127
|
+
change_note: change_note)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Field migrations. While one runs, its collection is read-only.
|
|
132
|
+
class Migrations < Resource
|
|
133
|
+
def list(collection, limit: nil, cursor: nil)
|
|
134
|
+
@client.paginate("migrations_list", path: { collection_id: collection },
|
|
135
|
+
query: { limit: limit, cursor: cursor })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def get(migration)
|
|
139
|
+
@client.request("migrations_get", path: { migration_id: migration })
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def cancel(migration, idempotency_key: nil)
|
|
143
|
+
@client.request("migrations_cancel", path: { migration_id: migration },
|
|
144
|
+
idempotency_key: idempotency_key)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Nohead
|
|
8
|
+
# A response: status, headers (lowercase names) and body text.
|
|
9
|
+
Response = Struct.new(:status, :headers, :body)
|
|
10
|
+
|
|
11
|
+
# Sends requests with Net::HTTP. A client's `transport:` can be anything
|
|
12
|
+
# with the same `call`, e.g. for tests.
|
|
13
|
+
class Transport
|
|
14
|
+
# `body` is a String, an IO (streamed, with Content-Length), or nil.
|
|
15
|
+
def call(method, url, headers, body, timeout)
|
|
16
|
+
uri = URI(url)
|
|
17
|
+
request = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri, headers)
|
|
18
|
+
if body.respond_to?(:read)
|
|
19
|
+
request.body_stream = body
|
|
20
|
+
elsif body
|
|
21
|
+
request.body = body
|
|
22
|
+
end
|
|
23
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
24
|
+
http.use_ssl = uri.scheme == "https"
|
|
25
|
+
http.open_timeout = http.read_timeout = http.write_timeout = timeout
|
|
26
|
+
response = http.start { http.request(request) }
|
|
27
|
+
Response.new(response.code.to_i, response.each_header.to_h, response.body.to_s)
|
|
28
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
|
|
29
|
+
raise TimeoutError, "The request to #{uri.host} timed out after #{timeout} s"
|
|
30
|
+
rescue SocketError, SystemCallError, IOError, OpenSSL::SSL::SSLError => e
|
|
31
|
+
raise ConnectionError, "Could not reach #{uri.host}: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Nohead
|
|
6
|
+
# Reading files for assets.upload: a path (String or Pathname) or an IO
|
|
7
|
+
# opened in binary mode (File, StringIO).
|
|
8
|
+
class UploadSource
|
|
9
|
+
CONTENT_TYPES = {
|
|
10
|
+
".avif" => "image/avif", ".gif" => "image/gif", ".jpeg" => "image/jpeg",
|
|
11
|
+
".jpg" => "image/jpeg", ".png" => "image/png", ".svg" => "image/svg+xml",
|
|
12
|
+
".webp" => "image/webp", ".pdf" => "application/pdf", ".json" => "application/json",
|
|
13
|
+
".txt" => "text/plain", ".md" => "text/markdown", ".csv" => "text/csv",
|
|
14
|
+
".mp3" => "audio/mpeg", ".mp4" => "video/mp4", ".webm" => "video/webm",
|
|
15
|
+
".zip" => "application/zip"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :filename, :content_type, :byte_size, :io
|
|
19
|
+
|
|
20
|
+
def initialize(file, filename: nil, content_type: nil, byte_size: nil)
|
|
21
|
+
name, size = file.is_a?(String) || file.is_a?(Pathname) ? open_path(Pathname(file)) : open_io(file)
|
|
22
|
+
@filename = filename || name || "upload"
|
|
23
|
+
@byte_size = byte_size || size or
|
|
24
|
+
raise UploadError, "Uploading a stream that cannot seek needs byte_size:"
|
|
25
|
+
@content_type = content_type ||
|
|
26
|
+
CONTENT_TYPES.fetch(File.extname(@filename).downcase, "application/octet-stream")
|
|
27
|
+
@start = @io.respond_to?(:pos) ? @io.pos : 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Whether the bytes can be sent again (for retries).
|
|
31
|
+
def replayable? = @io.respond_to?(:rewind) && @io.respond_to?(:pos)
|
|
32
|
+
|
|
33
|
+
def rewind
|
|
34
|
+
@io.pos = @start if replayable?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close
|
|
38
|
+
@io.close if @owned
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def open_path(path)
|
|
44
|
+
@io = path.open("rb")
|
|
45
|
+
@owned = true
|
|
46
|
+
[path.basename.to_s, path.size]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def open_io(io)
|
|
50
|
+
raise ArgumentError, "Upload a path or an IO; wrap bytes in StringIO.new(bytes)" unless io.respond_to?(:read)
|
|
51
|
+
|
|
52
|
+
@io = io
|
|
53
|
+
name = File.basename(io.path) if io.respond_to?(:path) && io.path
|
|
54
|
+
[name, remaining(io)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def remaining(io)
|
|
58
|
+
return io.size - io.pos if io.respond_to?(:size) && io.respond_to?(:pos)
|
|
59
|
+
|
|
60
|
+
nil
|
|
61
|
+
rescue IOError, SystemCallError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Nohead
|
|
7
|
+
# Verifying webhook requests (Standard Webhooks), with or without a client:
|
|
8
|
+
#
|
|
9
|
+
# event = Nohead::Webhooks.unwrap(request.raw_post, request.headers,
|
|
10
|
+
# secret: ENV.fetch("NOHEAD_WEBHOOK_SECRET"))
|
|
11
|
+
# puts event.data.record.id if event.type == "record.published"
|
|
12
|
+
module Webhooks
|
|
13
|
+
# Checks a webhook request's signature and timestamp and returns its event
|
|
14
|
+
# (a NoheadObject: `type`, `data`...). Pass the raw body exactly as
|
|
15
|
+
# received: parsing and re-serializing JSON changes the bytes, and the
|
|
16
|
+
# signature with them. Raises WebhookVerificationError when anything does
|
|
17
|
+
# not match. `headers` is a Hash or anything with `[]` (any case).
|
|
18
|
+
def self.unwrap(body, headers, secret:, tolerance: 300)
|
|
19
|
+
id = header(headers, "webhook-id")
|
|
20
|
+
timestamp = header(headers, "webhook-timestamp")
|
|
21
|
+
signatures = header(headers, "webhook-signature")
|
|
22
|
+
unless id && timestamp && signatures
|
|
23
|
+
raise WebhookVerificationError, "Missing webhook-id, webhook-timestamp or webhook-signature header"
|
|
24
|
+
end
|
|
25
|
+
unless timestamp.match?(/\A\d+\z/) && (Time.now.to_i - timestamp.to_i).abs <= tolerance
|
|
26
|
+
raise WebhookVerificationError, "The webhook timestamp is too far from the current time"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
key = secret.delete_prefix("whsec_").unpack1("m0")
|
|
30
|
+
expected = [OpenSSL::HMAC.digest("SHA256", key, "#{id}.#{timestamp}.#{body}")].pack("m0")
|
|
31
|
+
matched = signatures.split.any? do |entry|
|
|
32
|
+
version, signature = entry.split(",", 2)
|
|
33
|
+
version == "v1" && signature && OpenSSL.secure_compare(signature, expected)
|
|
34
|
+
end
|
|
35
|
+
raise WebhookVerificationError, "No webhook signature matches" unless matched
|
|
36
|
+
|
|
37
|
+
NoheadObject.wrap(JSON.parse(body))
|
|
38
|
+
rescue ArgumentError
|
|
39
|
+
raise WebhookVerificationError, "The webhook secret is not base64"
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
raise WebhookVerificationError, "The webhook body is not JSON"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.header(headers, name)
|
|
45
|
+
return headers[name] || headers[name.split("-").map(&:capitalize).join("-")] unless headers.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
headers.find { |key, _| key.to_s.downcase == name }&.last
|
|
48
|
+
end
|
|
49
|
+
private_class_method :header
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module Resources
|
|
53
|
+
# Webhooks of the key's project. `webhook` is a webhook ID ("wh_...").
|
|
54
|
+
class Webhooks < Resource
|
|
55
|
+
attr_reader :deliveries
|
|
56
|
+
|
|
57
|
+
def initialize(client)
|
|
58
|
+
super
|
|
59
|
+
@deliveries = Deliveries.new(client)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def list(limit: nil, cursor: nil)
|
|
63
|
+
@client.paginate("webhooks_list", query: { limit: limit, cursor: cursor })
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def get(webhook)
|
|
67
|
+
@client.request("webhooks_get", path: { webhook_id: webhook })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Subscribes a URL. The response is the only time `secret` is shown.
|
|
71
|
+
def create(idempotency_key: nil, **params)
|
|
72
|
+
@client.request("webhooks_create", body: params, idempotency_key: idempotency_key)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def update(webhook, idempotency_key: nil, **params)
|
|
76
|
+
@client.request("webhooks_update", path: { webhook_id: webhook }, body: params,
|
|
77
|
+
idempotency_key: idempotency_key)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def delete(webhook, idempotency_key: nil)
|
|
81
|
+
@client.request("webhooks_delete", path: { webhook_id: webhook }, idempotency_key: idempotency_key)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A new signing secret; the old one stops working at once.
|
|
85
|
+
def rotate_secret(webhook, idempotency_key: nil)
|
|
86
|
+
@client.request("webhooks_rotate_secret", path: { webhook_id: webhook },
|
|
87
|
+
idempotency_key: idempotency_key)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Sends a `webhook.test` event to the URL.
|
|
91
|
+
def test(webhook, idempotency_key: nil)
|
|
92
|
+
@client.request("webhooks_test", path: { webhook_id: webhook }, idempotency_key: idempotency_key)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Checks a webhook request's signature and returns its event (see
|
|
96
|
+
# Nohead::Webhooks.unwrap, which needs no client).
|
|
97
|
+
def unwrap(body, headers, secret:, tolerance: 300)
|
|
98
|
+
Nohead::Webhooks.unwrap(body, headers, secret: secret, tolerance: tolerance)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Delivery attempts of a webhook's events.
|
|
103
|
+
class Deliveries < Resource
|
|
104
|
+
def list(webhook, status: nil, limit: nil, cursor: nil)
|
|
105
|
+
@client.paginate("webhook_deliveries_list", path: { webhook_id: webhook },
|
|
106
|
+
query: { status: status, limit: limit, cursor: cursor })
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def get(delivery)
|
|
110
|
+
@client.request("webhook_deliveries_get", path: { delivery_id: delivery })
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Sends the event again.
|
|
114
|
+
def retry(delivery, idempotency_key: nil)
|
|
115
|
+
@client.request("webhook_deliveries_retry", path: { delivery_id: delivery },
|
|
116
|
+
idempotency_key: idempotency_key)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/nohead.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The official Ruby SDK for the Nohead API.
|
|
4
|
+
#
|
|
5
|
+
# require "nohead"
|
|
6
|
+
#
|
|
7
|
+
# nohead = Nohead::Client.new # NOHEAD_API_KEY
|
|
8
|
+
# nohead.records.list("posts", filter: { status: "published" }).each do |post|
|
|
9
|
+
# puts post.data["title"]
|
|
10
|
+
# end
|
|
11
|
+
module Nohead; end
|
|
12
|
+
|
|
13
|
+
require_relative "nohead/version"
|
|
14
|
+
require_relative "nohead/nohead_object"
|
|
15
|
+
require_relative "nohead/errors"
|
|
16
|
+
require_relative "nohead/operations"
|
|
17
|
+
require_relative "nohead/page"
|
|
18
|
+
require_relative "nohead/transport"
|
|
19
|
+
require_relative "nohead/uploads"
|
|
20
|
+
require_relative "nohead/resources/records"
|
|
21
|
+
require_relative "nohead/resources/schema"
|
|
22
|
+
require_relative "nohead/resources/assets"
|
|
23
|
+
require_relative "nohead/resources/other"
|
|
24
|
+
require_relative "nohead/webhooks"
|
|
25
|
+
require_relative "nohead/client"
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nohead
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nohead
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'A client for the Nohead API: records, collections, fields, assets, webhooks
|
|
13
|
+
and search, with pagination, safe retries, uploads and webhook verification. No
|
|
14
|
+
runtime dependencies.'
|
|
15
|
+
email:
|
|
16
|
+
- hello@nohead.io
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/nohead.rb
|
|
25
|
+
- lib/nohead/client.rb
|
|
26
|
+
- lib/nohead/errors.rb
|
|
27
|
+
- lib/nohead/nohead_object.rb
|
|
28
|
+
- lib/nohead/operations.rb
|
|
29
|
+
- lib/nohead/page.rb
|
|
30
|
+
- lib/nohead/resources/assets.rb
|
|
31
|
+
- lib/nohead/resources/other.rb
|
|
32
|
+
- lib/nohead/resources/records.rb
|
|
33
|
+
- lib/nohead/resources/schema.rb
|
|
34
|
+
- lib/nohead/transport.rb
|
|
35
|
+
- lib/nohead/uploads.rb
|
|
36
|
+
- lib/nohead/version.rb
|
|
37
|
+
- lib/nohead/webhooks.rb
|
|
38
|
+
homepage: https://nohead.io
|
|
39
|
+
licenses:
|
|
40
|
+
- MIT
|
|
41
|
+
metadata:
|
|
42
|
+
homepage_uri: https://nohead.io
|
|
43
|
+
source_code_uri: https://github.com/nohead-io/nohead-ruby
|
|
44
|
+
changelog_uri: https://github.com/nohead-io/nohead-ruby/blob/main/CHANGELOG.md
|
|
45
|
+
bug_tracker_uri: https://github.com/nohead-io/nohead-ruby/issues
|
|
46
|
+
rubygems_mfa_required: 'true'
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.3'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 4.0.20
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Official Ruby SDK for the Nohead API
|
|
64
|
+
test_files: []
|