attio 0.1.1 ā 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 +4 -4
- data/.github/workflows/ci.yml +39 -15
- data/.github/workflows/coverage.yml +67 -0
- data/.github/workflows/pr_checks.yml +25 -7
- data/.github/workflows/release.yml +27 -13
- data/.github/workflows/tests.yml +67 -0
- data/.rubocop.yml +362 -90
- data/CHANGELOG.md +49 -1
- data/CONCEPTS.md +428 -0
- data/CONTRIBUTING.md +4 -4
- data/Gemfile +8 -5
- data/Gemfile.lock +4 -4
- data/README.md +164 -2
- data/Rakefile +8 -6
- data/attio.gemspec +6 -7
- data/danger/Dangerfile +22 -34
- data/docs/example.rb +30 -29
- data/examples/advanced_filtering.rb +178 -0
- data/examples/basic_usage.rb +110 -0
- data/examples/collaboration_example.rb +173 -0
- data/examples/full_workflow.rb +348 -0
- data/examples/notes_and_tasks.rb +200 -0
- data/lib/attio/client.rb +67 -29
- data/lib/attio/connection_pool.rb +26 -14
- data/lib/attio/errors.rb +4 -2
- data/lib/attio/http_client.rb +70 -41
- data/lib/attio/logger.rb +37 -27
- data/lib/attio/resources/attributes.rb +12 -5
- data/lib/attio/resources/base.rb +66 -27
- data/lib/attio/resources/comments.rb +147 -0
- data/lib/attio/resources/lists.rb +21 -24
- data/lib/attio/resources/notes.rb +110 -0
- data/lib/attio/resources/objects.rb +11 -4
- data/lib/attio/resources/records.rb +49 -67
- data/lib/attio/resources/tasks.rb +131 -0
- data/lib/attio/resources/threads.rb +154 -0
- data/lib/attio/resources/users.rb +10 -4
- data/lib/attio/resources/workspaces.rb +9 -1
- data/lib/attio/retry_handler.rb +19 -11
- data/lib/attio/version.rb +3 -1
- data/lib/attio.rb +15 -9
- metadata +13 -18
- data/run_tests.rb +0 -52
- data/test_basic.rb +0 -51
- data/test_typhoeus.rb +0 -31
@@ -0,0 +1,200 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "attio"
|
6
|
+
require "date"
|
7
|
+
|
8
|
+
# Example demonstrating Notes and Tasks functionality
|
9
|
+
#
|
10
|
+
# This example shows how to:
|
11
|
+
# - Create and manage notes on records
|
12
|
+
# - Create and manage tasks
|
13
|
+
# - Work with task assignments and due dates
|
14
|
+
# - Track task completion
|
15
|
+
|
16
|
+
client = Attio.client(api_key: ENV.fetch("ATTIO_API_KEY"))
|
17
|
+
|
18
|
+
puts "š Attio Notes and Tasks Example"
|
19
|
+
puts "=" * 50
|
20
|
+
|
21
|
+
begin
|
22
|
+
# First, find or create a person to work with
|
23
|
+
puts "\nš¤ Setting up test person..."
|
24
|
+
test_person = client.records.create(
|
25
|
+
object: "people",
|
26
|
+
data: {
|
27
|
+
name: "Jane Smith",
|
28
|
+
email: "jane.smith@example.com",
|
29
|
+
title: "Product Manager",
|
30
|
+
}
|
31
|
+
)
|
32
|
+
person_id = test_person["data"]["id"]
|
33
|
+
puts " Created test person: Jane Smith (#{person_id})"
|
34
|
+
|
35
|
+
# Working with Notes
|
36
|
+
puts "\nš Creating Notes:"
|
37
|
+
|
38
|
+
# Create a meeting note
|
39
|
+
meeting_note = client.notes.create(
|
40
|
+
parent_object: "people",
|
41
|
+
parent_record_id: person_id,
|
42
|
+
title: "Initial Meeting - Product Requirements",
|
43
|
+
content: <<~MARKDOWN
|
44
|
+
## Meeting Summary
|
45
|
+
**Date:** #{Date.today}
|
46
|
+
**Attendees:** Jane Smith, Development Team
|
47
|
+
|
48
|
+
### Discussion Points:
|
49
|
+
1. **Q1 Product Roadmap**
|
50
|
+
- Feature A: User authentication improvements
|
51
|
+
- Feature B: New dashboard design
|
52
|
+
- Feature C: API v2 development
|
53
|
+
|
54
|
+
2. **Timeline**
|
55
|
+
- Sprint 1: Jan 15-29
|
56
|
+
- Sprint 2: Jan 30-Feb 12
|
57
|
+
- Release: Feb 15
|
58
|
+
|
59
|
+
3. **Action Items**
|
60
|
+
- [ ] Create detailed specs for Feature A
|
61
|
+
- [ ] Schedule design review for Feature B
|
62
|
+
- [ ] Allocate resources for API development
|
63
|
+
|
64
|
+
### Next Steps:
|
65
|
+
Follow-up meeting scheduled for next week.
|
66
|
+
MARKDOWN
|
67
|
+
)
|
68
|
+
puts " ā
Created meeting note: #{meeting_note['data']['title']}"
|
69
|
+
|
70
|
+
# Create a follow-up note
|
71
|
+
followup_note = client.notes.create(
|
72
|
+
parent_object: "people",
|
73
|
+
parent_record_id: person_id,
|
74
|
+
title: "Follow-up: Action Items",
|
75
|
+
content: "Confirmed timeline and resource allocation. Jane will provide detailed specs by EOW."
|
76
|
+
)
|
77
|
+
puts " ā
Created follow-up note"
|
78
|
+
|
79
|
+
# List all notes for the person
|
80
|
+
notes = client.notes.list(
|
81
|
+
parent_object: "people",
|
82
|
+
parent_record_id: person_id
|
83
|
+
)
|
84
|
+
puts "\n š Notes for Jane Smith:"
|
85
|
+
notes["data"].each do |note|
|
86
|
+
puts " - #{note['title']} (created: #{note['created_at']})"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Working with Tasks
|
90
|
+
puts "\nā
Creating Tasks:"
|
91
|
+
|
92
|
+
# Create tasks based on action items
|
93
|
+
task1 = client.tasks.create(
|
94
|
+
parent_object: "people",
|
95
|
+
parent_record_id: person_id,
|
96
|
+
title: "Create detailed specs for Feature A",
|
97
|
+
description: "Write comprehensive specifications for the user authentication improvements",
|
98
|
+
due_date: (Date.today + 7).iso8601,
|
99
|
+
priority: 1,
|
100
|
+
status: "pending"
|
101
|
+
)
|
102
|
+
puts " ā
Created task: #{task1['data']['title']}"
|
103
|
+
|
104
|
+
task2 = client.tasks.create(
|
105
|
+
parent_object: "people",
|
106
|
+
parent_record_id: person_id,
|
107
|
+
title: "Schedule design review for Feature B",
|
108
|
+
description: "Coordinate with design team and schedule review meeting",
|
109
|
+
due_date: (Date.today + 3).iso8601,
|
110
|
+
priority: 2,
|
111
|
+
status: "pending"
|
112
|
+
)
|
113
|
+
puts " ā
Created task: #{task2['data']['title']}"
|
114
|
+
|
115
|
+
task3 = client.tasks.create(
|
116
|
+
parent_object: "people",
|
117
|
+
parent_record_id: person_id,
|
118
|
+
title: "Allocate resources for API development",
|
119
|
+
description: "Determine team members and timeline for API v2",
|
120
|
+
due_date: (Date.today + 10).iso8601,
|
121
|
+
priority: 3,
|
122
|
+
status: "pending"
|
123
|
+
)
|
124
|
+
puts " ā
Created task: #{task3['data']['title']}"
|
125
|
+
|
126
|
+
# List pending tasks
|
127
|
+
puts "\n š Pending Tasks:"
|
128
|
+
pending_tasks = client.tasks.list(
|
129
|
+
status: "pending",
|
130
|
+
parent_object: "people",
|
131
|
+
parent_record_id: person_id
|
132
|
+
)
|
133
|
+
pending_tasks["data"].each do |task|
|
134
|
+
puts " - #{task['title']} (due: #{task['due_date']})"
|
135
|
+
end
|
136
|
+
|
137
|
+
# Complete a task
|
138
|
+
puts "\n ā
Completing task..."
|
139
|
+
completed_task = client.tasks.complete(
|
140
|
+
id: task2["data"]["id"],
|
141
|
+
completed_at: DateTime.now.iso8601
|
142
|
+
)
|
143
|
+
puts " Task completed: #{completed_task['data']['title']}"
|
144
|
+
|
145
|
+
# Update a task
|
146
|
+
puts "\n š Updating task..."
|
147
|
+
updated_task = client.tasks.update(
|
148
|
+
id: task1["data"]["id"],
|
149
|
+
description: "Updated: Include security requirements in the specifications",
|
150
|
+
priority: 1
|
151
|
+
)
|
152
|
+
puts " Task updated: #{updated_task['data']['title']}"
|
153
|
+
|
154
|
+
# Get task details
|
155
|
+
puts "\n š Task Details:"
|
156
|
+
task_detail = client.tasks.get(id: task1["data"]["id"])
|
157
|
+
puts " Title: #{task_detail['data']['title']}"
|
158
|
+
puts " Description: #{task_detail['data']['description']}"
|
159
|
+
puts " Due Date: #{task_detail['data']['due_date']}"
|
160
|
+
puts " Status: #{task_detail['data']['status']}"
|
161
|
+
|
162
|
+
# Update a note
|
163
|
+
puts "\n š Updating note..."
|
164
|
+
client.notes.update(
|
165
|
+
id: followup_note["data"]["id"],
|
166
|
+
content: "Updated: Specs delivered. Design review scheduled for Friday."
|
167
|
+
)
|
168
|
+
puts " Note updated successfully"
|
169
|
+
|
170
|
+
# Cleanup
|
171
|
+
puts "\nš§¹ Cleaning up..."
|
172
|
+
|
173
|
+
# Delete tasks
|
174
|
+
[task1, task2, task3].each do |task|
|
175
|
+
client.tasks.delete(id: task["data"]["id"])
|
176
|
+
end
|
177
|
+
puts " ā
Deleted tasks"
|
178
|
+
|
179
|
+
# Delete notes
|
180
|
+
[meeting_note, followup_note].each do |note|
|
181
|
+
client.notes.delete(id: note["data"]["id"])
|
182
|
+
end
|
183
|
+
puts " ā
Deleted notes"
|
184
|
+
|
185
|
+
# Delete test person
|
186
|
+
client.records.delete(object: "people", id: person_id)
|
187
|
+
puts " ā
Deleted test person"
|
188
|
+
rescue Attio::Error => e
|
189
|
+
puts "ā Error: #{e.message}"
|
190
|
+
# Cleanup on error
|
191
|
+
if defined?(person_id)
|
192
|
+
begin
|
193
|
+
client.records.delete(object: "people", id: person_id)
|
194
|
+
rescue StandardError
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
puts "\n⨠Example completed!"
|
data/lib/attio/client.rb
CHANGED
@@ -1,51 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The main client class for interacting with the Attio API.
|
4
|
+
#
|
5
|
+
# This class provides access to all Attio API resources and handles
|
6
|
+
# authentication, connection management, and request routing.
|
7
|
+
#
|
8
|
+
# @example Basic client creation
|
9
|
+
# client = Attio::Client.new(api_key: 'your-api-key')
|
10
|
+
#
|
11
|
+
# @example Custom timeout
|
12
|
+
# client = Attio::Client.new(api_key: 'your-api-key', timeout: 60)
|
13
|
+
#
|
14
|
+
# @author Ernest Sim
|
15
|
+
# @since 1.0.0
|
1
16
|
module Attio
|
2
|
-
# The main client class for interacting with the Attio API.
|
3
|
-
#
|
4
|
-
# This class provides access to all Attio API resources and handles
|
5
|
-
# authentication, connection management, and request routing.
|
6
|
-
#
|
7
|
-
# @example Basic client creation
|
8
|
-
# client = Attio::Client.new(api_key: 'your-api-key')
|
9
|
-
#
|
10
|
-
# @example Custom timeout
|
11
|
-
# client = Attio::Client.new(api_key: 'your-api-key', timeout: 60)
|
12
|
-
#
|
13
|
-
# @author Ernest Sim
|
14
|
-
# @since 1.0.0
|
15
17
|
class Client
|
16
18
|
# The base URL for the Attio API v2
|
17
|
-
API_BASE_URL = "https://api.attio.com/v2"
|
18
|
-
|
19
|
+
API_BASE_URL = "https://api.attio.com/v2"
|
20
|
+
|
19
21
|
# Default request timeout in seconds
|
20
22
|
DEFAULT_TIMEOUT = 30
|
21
23
|
|
22
24
|
# @return [String] The API key used for authentication
|
23
25
|
attr_reader :api_key
|
24
|
-
|
26
|
+
|
25
27
|
# @return [Integer] The request timeout in seconds
|
26
28
|
attr_reader :timeout
|
27
29
|
|
28
30
|
# Initialize a new Attio API client.
|
29
|
-
#
|
31
|
+
#
|
30
32
|
# @param api_key [String] Your Attio API key (required)
|
31
33
|
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
32
34
|
# @raise [ArgumentError] if api_key is nil or empty
|
33
|
-
#
|
35
|
+
#
|
34
36
|
# @example
|
35
37
|
# client = Attio::Client.new(api_key: 'sk-...your-key...')
|
36
38
|
def initialize(api_key:, timeout: DEFAULT_TIMEOUT)
|
37
39
|
raise ArgumentError, "API key is required" if api_key.nil? || api_key.empty?
|
38
|
-
|
40
|
+
|
39
41
|
@api_key = api_key
|
40
42
|
@timeout = timeout
|
41
43
|
end
|
42
44
|
|
43
45
|
# Returns the HTTP connection instance for making API requests.
|
44
|
-
#
|
46
|
+
#
|
45
47
|
# This method creates and configures the HTTP client with proper
|
46
48
|
# authentication headers and settings. The connection is cached
|
47
49
|
# for subsequent requests.
|
48
|
-
#
|
50
|
+
#
|
49
51
|
# @return [HttpClient] The configured HTTP client instance
|
50
52
|
# @api private
|
51
53
|
def connection
|
@@ -55,14 +57,14 @@ module Attio
|
|
55
57
|
"Authorization" => "Bearer #{api_key}",
|
56
58
|
"Accept" => "application/json",
|
57
59
|
"Content-Type" => "application/json",
|
58
|
-
"User-Agent" => "Attio Ruby Client/#{VERSION}"
|
60
|
+
"User-Agent" => "Attio Ruby Client/#{VERSION}",
|
59
61
|
},
|
60
62
|
timeout: timeout
|
61
63
|
)
|
62
64
|
end
|
63
65
|
|
64
66
|
# Access to the Records API resource.
|
65
|
-
#
|
67
|
+
#
|
66
68
|
# @return [Resources::Records] Records resource instance
|
67
69
|
# @example
|
68
70
|
# records = client.records.list(object: 'people')
|
@@ -71,7 +73,7 @@ module Attio
|
|
71
73
|
end
|
72
74
|
|
73
75
|
# Access to the Objects API resource.
|
74
|
-
#
|
76
|
+
#
|
75
77
|
# @return [Resources::Objects] Objects resource instance
|
76
78
|
# @example
|
77
79
|
# objects = client.objects.list
|
@@ -80,7 +82,7 @@ module Attio
|
|
80
82
|
end
|
81
83
|
|
82
84
|
# Access to the Lists API resource.
|
83
|
-
#
|
85
|
+
#
|
84
86
|
# @return [Resources::Lists] Lists resource instance
|
85
87
|
# @example
|
86
88
|
# lists = client.lists.list
|
@@ -89,7 +91,7 @@ module Attio
|
|
89
91
|
end
|
90
92
|
|
91
93
|
# Access to the Workspaces API resource.
|
92
|
-
#
|
94
|
+
#
|
93
95
|
# @return [Resources::Workspaces] Workspaces resource instance
|
94
96
|
# @example
|
95
97
|
# workspaces = client.workspaces.list
|
@@ -98,7 +100,7 @@ module Attio
|
|
98
100
|
end
|
99
101
|
|
100
102
|
# Access to the Attributes API resource.
|
101
|
-
#
|
103
|
+
#
|
102
104
|
# @return [Resources::Attributes] Attributes resource instance
|
103
105
|
# @example
|
104
106
|
# attributes = client.attributes.list
|
@@ -107,12 +109,48 @@ module Attio
|
|
107
109
|
end
|
108
110
|
|
109
111
|
# Access to the Users API resource.
|
110
|
-
#
|
112
|
+
#
|
111
113
|
# @return [Resources::Users] Users resource instance
|
112
114
|
# @example
|
113
115
|
# users = client.users.list
|
114
116
|
def users
|
115
117
|
@users ||= Resources::Users.new(self)
|
116
118
|
end
|
119
|
+
|
120
|
+
# Access to the Notes API resource.
|
121
|
+
#
|
122
|
+
# @return [Resources::Notes] Notes resource instance
|
123
|
+
# @example
|
124
|
+
# notes = client.notes.list(parent_object: 'people', parent_record_id: 'rec_123')
|
125
|
+
def notes
|
126
|
+
@notes ||= Resources::Notes.new(self)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Access to the Tasks API resource.
|
130
|
+
#
|
131
|
+
# @return [Resources::Tasks] Tasks resource instance
|
132
|
+
# @example
|
133
|
+
# tasks = client.tasks.list(status: 'pending')
|
134
|
+
def tasks
|
135
|
+
@tasks ||= Resources::Tasks.new(self)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Access to the Comments API resource.
|
139
|
+
#
|
140
|
+
# @return [Resources::Comments] Comments resource instance
|
141
|
+
# @example
|
142
|
+
# comments = client.comments.list(thread_id: 'thread_123')
|
143
|
+
def comments
|
144
|
+
@comments ||= Resources::Comments.new(self)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Access to the Threads API resource.
|
148
|
+
#
|
149
|
+
# @return [Resources::Threads] Threads resource instance
|
150
|
+
# @example
|
151
|
+
# threads = client.threads.list(parent_object: 'companies', parent_record_id: 'rec_456')
|
152
|
+
def threads
|
153
|
+
@threads ||= Resources::Threads.new(self)
|
154
|
+
end
|
117
155
|
end
|
118
|
-
end
|
156
|
+
end
|
@@ -1,6 +1,18 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Attio
|
4
|
+
# Thread-safe connection pool for managing HTTP connections
|
5
|
+
#
|
6
|
+
# This class provides a pool of connections that can be shared
|
7
|
+
# across threads for improved performance and resource management.
|
8
|
+
#
|
9
|
+
# @example Creating a connection pool
|
10
|
+
# pool = ConnectionPool.new(size: 10) { HttpClient.new }
|
11
|
+
#
|
12
|
+
# @example Using a connection from the pool
|
13
|
+
# pool.with_connection do |conn|
|
14
|
+
# conn.get("/endpoint")
|
15
|
+
# end
|
4
16
|
class ConnectionPool
|
5
17
|
DEFAULT_POOL_SIZE = 5
|
6
18
|
DEFAULT_TIMEOUT = 5 # seconds to wait for connection
|
@@ -14,7 +26,7 @@ module Attio
|
|
14
26
|
@key = :"#{object_id}_connection"
|
15
27
|
@block = block
|
16
28
|
@mutex = Mutex.new
|
17
|
-
|
29
|
+
|
18
30
|
size.times { @available << create_connection }
|
19
31
|
end
|
20
32
|
|
@@ -29,14 +41,12 @@ module Attio
|
|
29
41
|
|
30
42
|
def checkout
|
31
43
|
deadline = Time.now + timeout
|
32
|
-
|
44
|
+
|
33
45
|
loop do
|
34
|
-
return @available.pop(true) if @available.size
|
35
|
-
|
36
|
-
if Time.now >= deadline
|
37
|
-
|
38
|
-
end
|
39
|
-
|
46
|
+
return @available.pop(true) if @available.size.positive?
|
47
|
+
|
48
|
+
raise TimeoutError, "Couldn't acquire connection within #{timeout} seconds" if Time.now >= deadline
|
49
|
+
|
40
50
|
sleep(0.01)
|
41
51
|
end
|
42
52
|
rescue ThreadError
|
@@ -52,18 +62,20 @@ module Attio
|
|
52
62
|
def shutdown
|
53
63
|
@mutex.synchronize do
|
54
64
|
@available.close
|
55
|
-
while connection =
|
65
|
+
while (connection = begin
|
66
|
+
@available.pop(true)
|
67
|
+
rescue StandardError
|
68
|
+
nil
|
69
|
+
end)
|
56
70
|
connection.close if connection.respond_to?(:close)
|
57
71
|
end
|
58
72
|
end
|
59
73
|
end
|
60
74
|
|
61
|
-
private
|
62
|
-
|
63
|
-
def create_connection
|
75
|
+
private def create_connection
|
64
76
|
@block.call
|
65
77
|
end
|
66
78
|
|
67
79
|
class TimeoutError < StandardError; end
|
68
80
|
end
|
69
|
-
end
|
81
|
+
end
|
data/lib/attio/errors.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Attio
|
2
4
|
class Error < StandardError; end
|
3
|
-
|
5
|
+
|
4
6
|
class AuthenticationError < Error; end
|
5
7
|
class NotFoundError < Error; end
|
6
8
|
class ValidationError < Error; end
|
7
9
|
class RateLimitError < Error; end
|
8
10
|
class ServerError < Error; end
|
9
|
-
end
|
11
|
+
end
|
data/lib/attio/http_client.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "typhoeus"
|
4
|
+
require "json"
|
3
5
|
|
4
6
|
module Attio
|
7
|
+
# HTTP client for making API requests to Attio
|
8
|
+
#
|
9
|
+
# This class handles the low-level HTTP communication with the Attio API,
|
10
|
+
# including request execution, response parsing, and error handling.
|
11
|
+
#
|
12
|
+
# @api private
|
5
13
|
class HttpClient
|
6
14
|
DEFAULT_TIMEOUT = 30
|
7
15
|
|
@@ -13,8 +21,12 @@ module Attio
|
|
13
21
|
@timeout = timeout
|
14
22
|
end
|
15
23
|
|
16
|
-
def get(path, params =
|
17
|
-
|
24
|
+
def get(path, params = nil)
|
25
|
+
if params && !params.empty?
|
26
|
+
execute_request(:get, path, params: params)
|
27
|
+
else
|
28
|
+
execute_request(:get, path)
|
29
|
+
end
|
18
30
|
end
|
19
31
|
|
20
32
|
def post(path, body = {})
|
@@ -29,20 +41,22 @@ module Attio
|
|
29
41
|
execute_request(:put, path, body: body.to_json)
|
30
42
|
end
|
31
43
|
|
32
|
-
def delete(path)
|
33
|
-
|
44
|
+
def delete(path, params = nil)
|
45
|
+
if params
|
46
|
+
execute_request(:delete, path, body: params.to_json)
|
47
|
+
else
|
48
|
+
execute_request(:delete, path)
|
49
|
+
end
|
34
50
|
end
|
35
51
|
|
36
|
-
private
|
37
|
-
|
38
|
-
def execute_request(method, path, options = {})
|
52
|
+
private def execute_request(method, path, options = {})
|
39
53
|
url = "#{base_url}/#{path}"
|
40
|
-
|
54
|
+
|
41
55
|
request_options = {
|
42
56
|
method: method,
|
43
|
-
headers: headers.merge(
|
57
|
+
headers: headers.merge("Content-Type" => "application/json"),
|
44
58
|
timeout: timeout,
|
45
|
-
connecttimeout: timeout
|
59
|
+
connecttimeout: timeout,
|
46
60
|
}.merge(options)
|
47
61
|
|
48
62
|
request = Typhoeus::Request.new(url, request_options)
|
@@ -51,42 +65,57 @@ module Attio
|
|
51
65
|
handle_response(response)
|
52
66
|
end
|
53
67
|
|
54
|
-
def handle_response(response)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
else
|
76
|
-
raise Error, "Request failed with status #{response.code}: #{parse_error_message(response)}"
|
77
|
-
end
|
68
|
+
private def handle_response(response)
|
69
|
+
return handle_connection_error(response) if response.code == 0
|
70
|
+
return parse_json(response.body) if (200..299).cover?(response.code)
|
71
|
+
|
72
|
+
handle_error_response(response)
|
73
|
+
end
|
74
|
+
|
75
|
+
private def handle_connection_error(response)
|
76
|
+
raise TimeoutError, "Request timed out" if response.timed_out?
|
77
|
+
|
78
|
+
raise ConnectionError, "Connection failed: #{response.return_message}"
|
79
|
+
end
|
80
|
+
|
81
|
+
private def handle_error_response(response)
|
82
|
+
error_class = error_class_for_status(response.code)
|
83
|
+
message = parse_error_message(response)
|
84
|
+
|
85
|
+
# Add status code to message for generic errors
|
86
|
+
message = "Request failed with status #{response.code}: #{message}" if error_class == Error
|
87
|
+
|
88
|
+
raise error_class, message
|
78
89
|
end
|
79
90
|
|
80
|
-
def
|
91
|
+
private def error_class_for_status(status)
|
92
|
+
error_map = {
|
93
|
+
401 => AuthenticationError,
|
94
|
+
404 => NotFoundError,
|
95
|
+
422 => ValidationError,
|
96
|
+
429 => RateLimitError,
|
97
|
+
}
|
98
|
+
return error_map[status] if error_map.key?(status)
|
99
|
+
return ServerError if (500..599).cover?(status)
|
100
|
+
|
101
|
+
Error
|
102
|
+
end
|
103
|
+
|
104
|
+
private def parse_json(body)
|
81
105
|
return {} if body.nil? || body.empty?
|
106
|
+
|
82
107
|
JSON.parse(body)
|
83
108
|
rescue JSON::ParserError => e
|
84
109
|
raise Error, "Invalid JSON response: #{e.message}"
|
85
110
|
end
|
86
111
|
|
87
|
-
def parse_error_message(response)
|
88
|
-
body =
|
89
|
-
|
112
|
+
private def parse_error_message(response)
|
113
|
+
body = begin
|
114
|
+
parse_json(response.body)
|
115
|
+
rescue StandardError
|
116
|
+
response.body
|
117
|
+
end
|
118
|
+
|
90
119
|
if body.is_a?(Hash)
|
91
120
|
body["error"] || body["message"] || body.to_s
|
92
121
|
else
|
@@ -97,4 +126,4 @@ module Attio
|
|
97
126
|
class TimeoutError < Error; end
|
98
127
|
class ConnectionError < Error; end
|
99
128
|
end
|
100
|
-
end
|
129
|
+
end
|