kit-rb 0.0.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 +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +57 -0
- data/Rakefile +17 -0
- data/Steepfile +10 -0
- data/docs/DESIGN.md +50 -0
- data/lib/kit/auth/api_key.rb +24 -0
- data/lib/kit/auth/oauth.rb +28 -0
- data/lib/kit/client.rb +25 -0
- data/lib/kit/configuration.rb +28 -0
- data/lib/kit/connection.rb +78 -0
- data/lib/kit/errors.rb +69 -0
- data/lib/kit/objects/account.rb +36 -0
- data/lib/kit/resources/account.rb +16 -0
- data/lib/kit/resources/base.rb +31 -0
- data/lib/kit/version.rb +5 -0
- data/lib/kit-rb.rb +24 -0
- data/sig/kit-rb.rbs +100 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cd1696be6db17ef8f8d6a5c010d7e827bb914f7ea32e1b8e0e68a55f4a548cfc
|
|
4
|
+
data.tar.gz: f68f531db1eaa19bd17f46f4a764f994419fbf01cf2349d141686f00c33ccc7f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fea1917e81a6eef748518e0609b9d0b4e6c5f347b5527d12ba5575dbf02a210ea3db32037163d98e30e83cb6d35a258a0a5a712a969306960ca9ffaedbafe612
|
|
7
|
+
data.tar.gz: '058e607d279ad4c3f462a50495874c5d4ce8792dc73961ca193488c7e44e5442fc2cb057b3563fc295b382d2184e4c467f4742d1b4478ac4fb1ea7c4a3493d86'
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.0.0] - 2026-09-04
|
|
8
|
+
|
|
9
|
+
Name-reservation release: the P0 foundation and a working `GET /v4/account`
|
|
10
|
+
vertical slice. Not yet feature-complete — resources land in 0.1.0 per docs/DESIGN.md.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- P0 foundations: gem skeleton on the clean `Kit` namespace, with rspec +
|
|
14
|
+
rubocop + steep gates green on CI (Ruby 3.2–3.4).
|
|
15
|
+
- `Kit::Client` with API-key (`X-Kit-Api-Key`) and OAuth 2.0 bearer auth.
|
|
16
|
+
- `http.rb`-backed `Kit::Connection` with JSON handling and a typed error
|
|
17
|
+
hierarchy (`Kit::AuthenticationError`, `NotFoundError`, `RateLimitError`, …).
|
|
18
|
+
- `GET /v4/account` vertical slice returning immutable `Data` value objects.
|
|
19
|
+
- RBS signatures for the public surface.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lawrence Lin
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# kit-rb
|
|
2
|
+
|
|
3
|
+
A modern, fully-typed Ruby client for the **Kit** (formerly ConvertKit) **API v4**.
|
|
4
|
+
|
|
5
|
+
The gem is named `kit-rb`; the public namespace is the clean `Kit`.
|
|
6
|
+
|
|
7
|
+
> Status: early. P0 (foundations + the `GET /v4/account` vertical slice) is in
|
|
8
|
+
> place; resources are landing per [`docs/DESIGN.md`](docs/DESIGN.md).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "kit-rb"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Ruby >= 3.2.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require "kit-rb"
|
|
22
|
+
|
|
23
|
+
# API key — simplest, for your own account (120 req / 60s):
|
|
24
|
+
client = Kit::Client.new(api_key: ENV.fetch("KIT_API_KEY"))
|
|
25
|
+
|
|
26
|
+
# or OAuth 2.0 (600 req / 60s; required for bulk & purchase endpoints):
|
|
27
|
+
client = Kit::Client.new(access_token: oauth_access_token)
|
|
28
|
+
|
|
29
|
+
info = client.account.get # => Kit::Objects::AccountInfo
|
|
30
|
+
info.account.plan_type # => "creator_pro"
|
|
31
|
+
info.user.email # => "you@example.com"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Responses are immutable `Data` value objects. Errors are typed:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
begin
|
|
38
|
+
client.account.get
|
|
39
|
+
rescue Kit::AuthenticationError => e # 401
|
|
40
|
+
warn e.status # => 401
|
|
41
|
+
rescue Kit::RateLimitError => e # 429
|
|
42
|
+
sleep e.retry_after
|
|
43
|
+
rescue Kit::APIError => e # any other non-2xx
|
|
44
|
+
warn e.errors # => ["..."] from Kit's body
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
bin/setup
|
|
52
|
+
bundle exec rake # spec + rubocop + steep
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
require "rubocop/rake_task"
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
desc "Type-check with Steep"
|
|
13
|
+
task :steep do
|
|
14
|
+
sh "bundle exec steep check"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
task default: %i[spec rubocop steep]
|
data/Steepfile
ADDED
data/docs/DESIGN.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# kit-rb — design
|
|
2
|
+
|
|
3
|
+
A modern, fully-typed Ruby client for the **Kit** (formerly ConvertKit) **API v4**.
|
|
4
|
+
The existing Ruby gems all stop at API v3/v2; kit-rb targets v4 to a high bar.
|
|
5
|
+
|
|
6
|
+
## Locked decisions
|
|
7
|
+
|
|
8
|
+
| area | choice | why |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| HTTP | **http.rb** (`~> 5.2`) | one modern dependency; cleaner than Net::HTTP boilerplate, lighter than Faraday |
|
|
11
|
+
| Auth | **API key + OAuth 2.0 from day one** | Kit's bulk & purchase-creation endpoints require OAuth |
|
|
12
|
+
| Types | **RBS + steep**, response = **`Data` value objects** | Ruby-native, immutable, zero runtime type dep |
|
|
13
|
+
| Ruby floor | **3.2** | `Data.define` |
|
|
14
|
+
| Build process | P1 by hand; **P2 resources via local models + `forge`** | dogfood the reliable-edit workflow; Ruby verified across the fleet benchmarks |
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Kit::Client # entry: picks an auth strategy, holds one Connection
|
|
20
|
+
├─ Kit::Configuration # immutable; validates exactly-one-credential
|
|
21
|
+
├─ Kit::Auth::ApiKey # X-Kit-Api-Key header
|
|
22
|
+
├─ Kit::Auth::OAuth # Bearer; authorize/refresh/PKCE land in P1
|
|
23
|
+
├─ Kit::Connection # http.rb transport, JSON, auth injection, error mapping
|
|
24
|
+
├─ Kit::Error (tree) # typed exceptions mapped from HTTP status
|
|
25
|
+
├─ Kit::Objects::* # Data value objects, `.from(hash)` constructors
|
|
26
|
+
└─ Kit::Resources::* # one class per resource group; client.account.get
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Facts pinned from the OpenAPI spec (`developers.kit.com/api-reference/v4.json`,
|
|
30
|
+
OpenAPI 3.0.3, "Kit API 4.0", host `https://api.kit.com`, 52 paths): API-key
|
|
31
|
+
header `X-Kit-Api-Key`; OAuth authorize/token at `/v4/oauth/*`, scopes read/write;
|
|
32
|
+
rate limits 120/60s (key) and 600/60s (OAuth); cursor pagination (`after`/`before`
|
|
33
|
+
+ `per_page`, response carries a `pagination` object).
|
|
34
|
+
|
|
35
|
+
## Phases
|
|
36
|
+
|
|
37
|
+
- **P0 — foundations (this).** Gem skeleton on the clean `Kit` namespace, gates
|
|
38
|
+
(rspec + rubocop + steep) green in CI (Ruby 3.2–3.4), and a walking-skeleton
|
|
39
|
+
vertical slice: `GET /v4/account` end to end (auth → request → typed error →
|
|
40
|
+
`Data` object) with full spec coverage.
|
|
41
|
+
- **P1 — core, by hand.** Flesh out the transport: cursor auto-pagination
|
|
42
|
+
(lazy Enumerator), 429 rate-limit-aware retry with backoff, the full OAuth
|
|
43
|
+
authorization-code grant + refresh + PKCE, and instrumentation hooks.
|
|
44
|
+
- **P2 — resources, spec-driven + local models.** One class + objects + specs per
|
|
45
|
+
resource group (subscribers, tags, custom fields, forms, sequences, broadcasts,
|
|
46
|
+
purchases, webhooks, email templates, segments, snippets), generated against
|
|
47
|
+
the OpenAPI spec via `forge`, gated by `rspec`/`steep`.
|
|
48
|
+
- **P3 — quality.** Contract tests against the OpenAPI spec, edge cases
|
|
49
|
+
(pagination tail, nulls, large payloads), coverage floor.
|
|
50
|
+
- **P4 — DX & release.** YARD docs, examples, signed gem, release automation.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
module Auth
|
|
5
|
+
# API-key authentication: sends the key in the `X-Kit-Api-Key` header.
|
|
6
|
+
#
|
|
7
|
+
# Simplest way to reach your own account. Rate limit is 120 requests / 60s.
|
|
8
|
+
# Note: Kit's bulk and purchase-creation endpoints require OAuth, not a key.
|
|
9
|
+
class ApiKey
|
|
10
|
+
HEADER = "X-Kit-Api-Key"
|
|
11
|
+
|
|
12
|
+
def initialize(key)
|
|
13
|
+
raise ConfigurationError, "API key cannot be blank" if key.nil? || key.empty?
|
|
14
|
+
|
|
15
|
+
@key = key
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Adds the auth header to an outgoing request's header hash.
|
|
19
|
+
def headers
|
|
20
|
+
{ HEADER => @key }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
module Auth
|
|
5
|
+
# OAuth 2.0 bearer-token authentication: sends `Authorization: Bearer <token>`.
|
|
6
|
+
#
|
|
7
|
+
# Rate limit is 600 requests / 60s, and OAuth is required for the bulk and
|
|
8
|
+
# purchase-creation endpoints.
|
|
9
|
+
#
|
|
10
|
+
# P0 scope: carry an already-obtained access token. The authorization-code
|
|
11
|
+
# grant (authorize/token URLs at /v4/oauth/*), refresh, and PKCE helper land
|
|
12
|
+
# in P1 — this class is the seam they plug into.
|
|
13
|
+
class OAuth
|
|
14
|
+
AUTHORIZE_URL = "https://api.kit.com/v4/oauth/authorize"
|
|
15
|
+
TOKEN_URL = "https://api.kit.com/v4/oauth/token"
|
|
16
|
+
|
|
17
|
+
def initialize(access_token)
|
|
18
|
+
raise ConfigurationError, "OAuth access token cannot be blank" if access_token.nil? || access_token.empty?
|
|
19
|
+
|
|
20
|
+
@access_token = access_token
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def headers
|
|
24
|
+
{ "Authorization" => "Bearer #{@access_token}" }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/kit/client.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
# The entry point. Construct with one credential, then reach resources:
|
|
5
|
+
#
|
|
6
|
+
# client = Kit::Client.new(api_key: ENV.fetch("KIT_API_KEY"))
|
|
7
|
+
# client.account.get.account.plan_type
|
|
8
|
+
#
|
|
9
|
+
# client = Kit::Client.new(access_token: oauth_token) # OAuth
|
|
10
|
+
#
|
|
11
|
+
# A client is thread-safe to share: it holds immutable config and a stateless
|
|
12
|
+
# connection, and resource accessors are memoized per client.
|
|
13
|
+
class Client
|
|
14
|
+
attr_reader :config
|
|
15
|
+
|
|
16
|
+
def initialize(api_key: nil, access_token: nil, **options)
|
|
17
|
+
@config = Configuration.new(api_key: api_key, access_token: access_token, **options)
|
|
18
|
+
@connection = Connection.new(@config)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def account
|
|
22
|
+
@account ||= Resources::Account.new(@connection)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
# Immutable per-client configuration. Exactly one credential (api_key or
|
|
5
|
+
# access_token) must be supplied; the matching auth strategy is selected here.
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_reader :auth, :base_url, :open_timeout, :read_timeout
|
|
8
|
+
|
|
9
|
+
def initialize(api_key: nil, access_token: nil, base_url: DEFAULT_BASE_URL,
|
|
10
|
+
open_timeout: 10, read_timeout: 30)
|
|
11
|
+
@auth = build_auth(api_key, access_token)
|
|
12
|
+
@base_url = base_url
|
|
13
|
+
@open_timeout = open_timeout
|
|
14
|
+
@read_timeout = read_timeout
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def build_auth(api_key, access_token)
|
|
20
|
+
raise ConfigurationError, "supply either api_key or access_token, not both" if api_key && access_token
|
|
21
|
+
|
|
22
|
+
return Auth::ApiKey.new(api_key) if api_key
|
|
23
|
+
return Auth::OAuth.new(access_token) if access_token
|
|
24
|
+
|
|
25
|
+
raise ConfigurationError, "an api_key or access_token is required"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "http"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Kit
|
|
7
|
+
# The transport layer: builds requests with http.rb, injects auth and JSON
|
|
8
|
+
# headers, parses responses, and maps non-2xx statuses onto the typed error
|
|
9
|
+
# hierarchy. Resources talk to the API only through this.
|
|
10
|
+
class Connection
|
|
11
|
+
JSON_TYPE = "application/json"
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Issues a request and returns the parsed JSON body (a Hash) on success.
|
|
18
|
+
#
|
|
19
|
+
# @param method [Symbol] :get, :post, :put, :delete
|
|
20
|
+
# @param path [String] e.g. "/v4/account" (leading slash, no host)
|
|
21
|
+
# @param params [Hash] query string params
|
|
22
|
+
# @param body [Hash, nil] JSON request body
|
|
23
|
+
def request(method, path, params: {}, body: nil)
|
|
24
|
+
response = client.request(
|
|
25
|
+
method,
|
|
26
|
+
"#{@config.base_url}#{path}",
|
|
27
|
+
params: params,
|
|
28
|
+
json: body
|
|
29
|
+
)
|
|
30
|
+
handle(response)
|
|
31
|
+
rescue HTTP::Error => e
|
|
32
|
+
raise Error, "HTTP transport error: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def client
|
|
38
|
+
HTTP
|
|
39
|
+
.headers(default_headers)
|
|
40
|
+
.timeout(connect: @config.open_timeout, read: @config.read_timeout)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def default_headers
|
|
44
|
+
{
|
|
45
|
+
"Accept" => JSON_TYPE,
|
|
46
|
+
"Content-Type" => JSON_TYPE,
|
|
47
|
+
"User-Agent" => "kit-rb/#{Kit::VERSION}"
|
|
48
|
+
}.merge(@config.auth.headers)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def handle(response)
|
|
52
|
+
status = response.status.to_i
|
|
53
|
+
parsed = parse(response)
|
|
54
|
+
return parsed if (200..299).cover?(status)
|
|
55
|
+
|
|
56
|
+
raise error_for(status, parsed, response)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse(response)
|
|
60
|
+
raw = response.body.to_s
|
|
61
|
+
return nil if raw.empty?
|
|
62
|
+
|
|
63
|
+
JSON.parse(raw)
|
|
64
|
+
rescue JSON::ParserError
|
|
65
|
+
raw
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def error_for(status, body, response)
|
|
69
|
+
klass = Error.class_for(status)
|
|
70
|
+
if klass == RateLimitError
|
|
71
|
+
klass.new(status: status, body: body, response: response,
|
|
72
|
+
retry_after: response.headers["Retry-After"]&.to_i)
|
|
73
|
+
else
|
|
74
|
+
klass.new(status: status, body: body, response: response)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/kit/errors.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
# Base class for every error the gem raises. Rescue `Kit::Error` to catch all.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised for configuration problems detected before any request is made
|
|
8
|
+
# (e.g. no credentials supplied).
|
|
9
|
+
class ConfigurationError < Error; end
|
|
10
|
+
|
|
11
|
+
# Base for every error that carries an HTTP response. `status` is the code,
|
|
12
|
+
# `body` the parsed JSON body (or the raw string when it wasn't JSON), and
|
|
13
|
+
# `errors` the `errors` array Kit returns on validation failures.
|
|
14
|
+
class APIError < Error
|
|
15
|
+
attr_reader :status, :body, :errors, :response
|
|
16
|
+
|
|
17
|
+
def initialize(message = nil, status:, body: nil, response: nil)
|
|
18
|
+
@status = status
|
|
19
|
+
@body = body
|
|
20
|
+
@response = response
|
|
21
|
+
@errors = body.is_a?(Hash) ? Array(body["errors"]) : []
|
|
22
|
+
super(message || default_message)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def default_message
|
|
28
|
+
base = "Kit API request failed with status #{status}"
|
|
29
|
+
@errors.empty? ? base : "#{base}: #{@errors.join(", ")}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 401 — missing or invalid credentials.
|
|
34
|
+
class AuthenticationError < APIError; end
|
|
35
|
+
# 403 — authenticated but not permitted (e.g. an API-key-only call needing OAuth).
|
|
36
|
+
class AuthorizationError < APIError; end
|
|
37
|
+
# 404 — no such resource.
|
|
38
|
+
class NotFoundError < APIError; end
|
|
39
|
+
# 422 — the request was well-formed but semantically invalid.
|
|
40
|
+
class UnprocessableEntityError < APIError; end
|
|
41
|
+
|
|
42
|
+
# 429 — rate limited. `retry_after` is the seconds to wait, when Kit sends it.
|
|
43
|
+
class RateLimitError < APIError
|
|
44
|
+
attr_reader :retry_after
|
|
45
|
+
|
|
46
|
+
def initialize(message = nil, status:, body: nil, response: nil, retry_after: nil)
|
|
47
|
+
@retry_after = retry_after
|
|
48
|
+
super(message, status: status, body: body, response: response)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 5xx — a Kit-side failure.
|
|
53
|
+
class ServerError < APIError; end
|
|
54
|
+
|
|
55
|
+
# Maps an HTTP status to the most specific error class above.
|
|
56
|
+
class Error
|
|
57
|
+
def self.class_for(status)
|
|
58
|
+
case status
|
|
59
|
+
when 401 then AuthenticationError
|
|
60
|
+
when 403 then AuthorizationError
|
|
61
|
+
when 404 then NotFoundError
|
|
62
|
+
when 422 then UnprocessableEntityError
|
|
63
|
+
when 429 then RateLimitError
|
|
64
|
+
when 500..599 then ServerError
|
|
65
|
+
else APIError
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
module Objects
|
|
5
|
+
# The user half of GET /v4/account.
|
|
6
|
+
User = Data.define(:email, :id) do
|
|
7
|
+
def self.from(hash)
|
|
8
|
+
new(email: hash["email"], id: hash["id"])
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The account half of GET /v4/account. Extra fields Kit adds later are
|
|
13
|
+
# ignored rather than crashing the client (forward-compatible).
|
|
14
|
+
Account = Data.define(:id, :name, :plan_type, :primary_email_address, :created_at) do
|
|
15
|
+
def self.from(hash)
|
|
16
|
+
new(
|
|
17
|
+
id: hash["id"],
|
|
18
|
+
name: hash["name"],
|
|
19
|
+
plan_type: hash["plan_type"],
|
|
20
|
+
primary_email_address: hash["primary_email_address"],
|
|
21
|
+
created_at: hash["created_at"]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The whole GET /v4/account response: { user:, account: }.
|
|
27
|
+
AccountInfo = Data.define(:user, :account) do
|
|
28
|
+
def self.from(hash)
|
|
29
|
+
new(
|
|
30
|
+
user: User.from(hash.fetch("user", {})),
|
|
31
|
+
account: Account.from(hash.fetch("account", {}))
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
module Resources
|
|
5
|
+
# The /v4/account endpoints. P0 covers the current-account read; colors,
|
|
6
|
+
# creator_profile, email_stats, and growth_stats follow in P1/P2.
|
|
7
|
+
class Account < Base
|
|
8
|
+
# GET /v4/account — current account and user info.
|
|
9
|
+
#
|
|
10
|
+
# @return [Kit::Objects::AccountInfo]
|
|
11
|
+
def get
|
|
12
|
+
Objects::AccountInfo.from(super("/v4/account"))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kit
|
|
4
|
+
module Resources
|
|
5
|
+
# Shared base for every resource group. Holds the connection and exposes
|
|
6
|
+
# thin verb helpers so resource classes read as `get("/v4/account")`.
|
|
7
|
+
class Base
|
|
8
|
+
def initialize(connection)
|
|
9
|
+
@connection = connection
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def get(path, params: {})
|
|
15
|
+
@connection.request(:get, path, params: params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def post(path, body: nil, params: {})
|
|
19
|
+
@connection.request(:post, path, params: params, body: body)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def put(path, body: nil, params: {})
|
|
23
|
+
@connection.request(:put, path, params: params, body: body)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def delete(path, params: {})
|
|
27
|
+
@connection.request(:delete, path, params: params)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/kit/version.rb
ADDED
data/lib/kit-rb.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# kit-rb — a Ruby client for the Kit (formerly ConvertKit) API v4.
|
|
4
|
+
#
|
|
5
|
+
# The gem is named `kit-rb`; the public namespace is the clean `Kit`
|
|
6
|
+
# (like redis-rb → Redis). Require it and talk to the API through a client:
|
|
7
|
+
#
|
|
8
|
+
# client = Kit::Client.new(api_key: ENV.fetch("KIT_API_KEY"))
|
|
9
|
+
# client.account.get.plan_type # => "creator_pro"
|
|
10
|
+
module Kit
|
|
11
|
+
# Base URL for the v4 REST API. Overridable per-client for tests/mocks.
|
|
12
|
+
DEFAULT_BASE_URL = "https://api.kit.com"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require "kit/version"
|
|
16
|
+
require "kit/errors"
|
|
17
|
+
require "kit/configuration"
|
|
18
|
+
require "kit/auth/api_key"
|
|
19
|
+
require "kit/auth/oauth"
|
|
20
|
+
require "kit/connection"
|
|
21
|
+
require "kit/objects/account"
|
|
22
|
+
require "kit/resources/base"
|
|
23
|
+
require "kit/resources/account"
|
|
24
|
+
require "kit/client"
|
data/sig/kit-rb.rbs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# RBS type signatures for the public surface of kit-rb (P0: account slice).
|
|
2
|
+
# Expanded per-resource as coverage grows in P1/P2.
|
|
3
|
+
|
|
4
|
+
module Kit
|
|
5
|
+
VERSION: String
|
|
6
|
+
DEFAULT_BASE_URL: String
|
|
7
|
+
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
def self.class_for: (Integer status) -> singleton(APIError)
|
|
10
|
+
end
|
|
11
|
+
class ConfigurationError < Error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class APIError < Error
|
|
15
|
+
attr_reader status: Integer
|
|
16
|
+
attr_reader body: untyped
|
|
17
|
+
attr_reader errors: Array[untyped]
|
|
18
|
+
attr_reader response: untyped
|
|
19
|
+
def initialize: (?String? message, status: Integer, ?body: untyped, ?response: untyped) -> void
|
|
20
|
+
end
|
|
21
|
+
class AuthenticationError < APIError
|
|
22
|
+
end
|
|
23
|
+
class AuthorizationError < APIError
|
|
24
|
+
end
|
|
25
|
+
class NotFoundError < APIError
|
|
26
|
+
end
|
|
27
|
+
class UnprocessableEntityError < APIError
|
|
28
|
+
end
|
|
29
|
+
class RateLimitError < APIError
|
|
30
|
+
attr_reader retry_after: Integer?
|
|
31
|
+
def initialize: (?String? message, status: Integer, ?body: untyped, ?response: untyped, ?retry_after: Integer?) -> void
|
|
32
|
+
end
|
|
33
|
+
class ServerError < APIError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module Auth
|
|
37
|
+
class ApiKey
|
|
38
|
+
HEADER: String
|
|
39
|
+
def initialize: (String key) -> void
|
|
40
|
+
def headers: () -> Hash[String, String]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class OAuth
|
|
44
|
+
AUTHORIZE_URL: String
|
|
45
|
+
TOKEN_URL: String
|
|
46
|
+
def initialize: (String access_token) -> void
|
|
47
|
+
def headers: () -> Hash[String, String]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Configuration
|
|
52
|
+
attr_reader auth: (Auth::ApiKey | Auth::OAuth)
|
|
53
|
+
attr_reader base_url: String
|
|
54
|
+
attr_reader open_timeout: Integer
|
|
55
|
+
attr_reader read_timeout: Integer
|
|
56
|
+
def initialize: (?api_key: String?, ?access_token: String?, ?base_url: String, ?open_timeout: Integer, ?read_timeout: Integer) -> void
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Connection
|
|
60
|
+
JSON_TYPE: String
|
|
61
|
+
def initialize: (Configuration config) -> void
|
|
62
|
+
def request: (Symbol method, String path, ?params: Hash[untyped, untyped], ?body: Hash[untyped, untyped]?) -> untyped
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module Objects
|
|
66
|
+
class User
|
|
67
|
+
attr_reader email: String?
|
|
68
|
+
attr_reader id: Integer?
|
|
69
|
+
def self.from: (Hash[String, untyped] hash) -> User
|
|
70
|
+
end
|
|
71
|
+
class Account
|
|
72
|
+
attr_reader id: Integer?
|
|
73
|
+
attr_reader name: String?
|
|
74
|
+
attr_reader plan_type: String?
|
|
75
|
+
attr_reader primary_email_address: String?
|
|
76
|
+
attr_reader created_at: String?
|
|
77
|
+
def self.from: (Hash[String, untyped] hash) -> Account
|
|
78
|
+
end
|
|
79
|
+
class AccountInfo
|
|
80
|
+
attr_reader user: User
|
|
81
|
+
attr_reader account: Account
|
|
82
|
+
def self.from: (Hash[String, untyped] hash) -> AccountInfo
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
module Resources
|
|
87
|
+
class Base
|
|
88
|
+
def initialize: (Connection connection) -> void
|
|
89
|
+
end
|
|
90
|
+
class Account < Base
|
|
91
|
+
def get: () -> Objects::AccountInfo
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class Client
|
|
96
|
+
attr_reader config: Configuration
|
|
97
|
+
def initialize: (?api_key: String?, ?access_token: String?, **untyped options) -> void
|
|
98
|
+
def account: () -> Resources::Account
|
|
99
|
+
end
|
|
100
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kit-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Lawrence Lin
|
|
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: http
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.2'
|
|
26
|
+
description: kit-rb wraps the Kit v4 REST API with API-key and OAuth 2.0 auth, cursor
|
|
27
|
+
pagination, typed errors, and immutable value objects.
|
|
28
|
+
email:
|
|
29
|
+
- deduce@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- Steepfile
|
|
39
|
+
- docs/DESIGN.md
|
|
40
|
+
- lib/kit-rb.rb
|
|
41
|
+
- lib/kit/auth/api_key.rb
|
|
42
|
+
- lib/kit/auth/oauth.rb
|
|
43
|
+
- lib/kit/client.rb
|
|
44
|
+
- lib/kit/configuration.rb
|
|
45
|
+
- lib/kit/connection.rb
|
|
46
|
+
- lib/kit/errors.rb
|
|
47
|
+
- lib/kit/objects/account.rb
|
|
48
|
+
- lib/kit/resources/account.rb
|
|
49
|
+
- lib/kit/resources/base.rb
|
|
50
|
+
- lib/kit/version.rb
|
|
51
|
+
- sig/kit-rb.rbs
|
|
52
|
+
homepage: https://github.com/solcreek/kit-rb
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://github.com/solcreek/kit-rb
|
|
57
|
+
source_code_uri: https://github.com/solcreek/kit-rb
|
|
58
|
+
changelog_uri: https://github.com/solcreek/kit-rb/blob/main/CHANGELOG.md
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 3.2.0
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 3.6.9
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: A modern, fully-typed Ruby client for the Kit (ConvertKit) API v4.
|
|
77
|
+
test_files: []
|