attio 0.1.1 → 0.1.3

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.
data/lib/attio/logger.rb CHANGED
@@ -1,7 +1,17 @@
1
- require 'logger'
2
- require 'json'
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "json"
3
5
 
4
6
  module Attio
7
+ # Enhanced logger for structured logging with JSON formatting
8
+ #
9
+ # This logger extends Ruby's standard Logger to provide structured
10
+ # logging with contextual information and JSON output.
11
+ #
12
+ # @example Basic usage
13
+ # logger = Attio::Logger.new(STDOUT)
14
+ # logger.info("API request", method: "GET", path: "/users")
5
15
  class Logger < ::Logger
6
16
  def initialize(logdev, level: ::Logger::INFO, formatter: nil)
7
17
  super(logdev)
@@ -25,23 +35,21 @@ module Attio
25
35
  super(format_message(message, context))
26
36
  end
27
37
 
28
- private
29
-
30
- def format_message(message, context)
38
+ private def format_message(message, context)
31
39
  return message if context.empty?
32
-
40
+
33
41
  {
34
42
  message: message,
35
- **context
43
+ **context,
36
44
  }
37
45
  end
38
46
 
39
- def default_formatter
47
+ private def default_formatter
40
48
  proc do |severity, datetime, progname, msg|
41
49
  data = {
42
50
  timestamp: datetime.iso8601,
43
51
  level: severity,
44
- progname: progname
52
+ progname: progname,
45
53
  }
46
54
 
47
55
  if msg.is_a?(Hash)
@@ -55,6 +63,12 @@ module Attio
55
63
  end
56
64
  end
57
65
 
66
+ # Specialized logger for API request/response logging
67
+ #
68
+ # This class provides sanitized logging of HTTP requests and responses,
69
+ # automatically redacting sensitive information like API keys.
70
+ #
71
+ # @api private
58
72
  class RequestLogger
59
73
  attr_reader :logger, :log_level
60
74
 
@@ -67,39 +81,35 @@ module Attio
67
81
  return unless logger
68
82
 
69
83
  logger.send(log_level, "API Request",
70
- method: method.to_s.upcase,
71
- url: url,
72
- headers: sanitize_headers(headers),
73
- body: sanitize_body(body)
74
- )
84
+ method: method.to_s.upcase,
85
+ url: url,
86
+ headers: sanitize_headers(headers),
87
+ body: sanitize_body(body))
75
88
  end
76
89
 
77
90
  def log_response(response, duration)
78
91
  return unless logger
79
92
 
80
93
  logger.send(log_level, "API Response",
81
- status: response.code,
82
- duration_ms: (duration * 1000).round(2),
83
- headers: response.headers,
84
- body_size: response.body&.bytesize
85
- )
94
+ status: response.code,
95
+ duration_ms: (duration * 1000).round(2),
96
+ headers: response.headers,
97
+ body_size: response.body&.bytesize)
86
98
  end
87
99
 
88
- private
89
-
90
- def sanitize_headers(headers)
100
+ private def sanitize_headers(headers)
91
101
  headers.transform_values do |value|
92
- if value.include?('Bearer')
93
- value.gsub(/Bearer\s+[\w\-]+/, 'Bearer [REDACTED]')
102
+ if value.include?("Bearer")
103
+ value.gsub(/Bearer\s+[\w\-]+/, "Bearer [REDACTED]")
94
104
  else
95
105
  value
96
106
  end
97
107
  end
98
108
  end
99
109
 
100
- def sanitize_body(body)
110
+ private def sanitize_body(body)
101
111
  return nil unless body
102
-
112
+
103
113
  if body.is_a?(String) && body.length > 1000
104
114
  "#{body[0..1000]}... (truncated)"
105
115
  else
@@ -107,4 +117,4 @@ module Attio
107
117
  end
108
118
  end
109
119
  end
110
- end
120
+ end
@@ -1,5 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
2
4
  module Resources
5
+ # API resource for managing Attio attributes
6
+ #
7
+ # Attributes define custom fields that can be added to objects
8
+ # and records in your Attio workspace.
9
+ #
10
+ # @example Listing attributes
11
+ # client.attributes.list(object: "people")
3
12
  class Attributes < Base
4
13
  def list(object:, **params)
5
14
  validate_object!(object)
@@ -12,15 +21,13 @@ module Attio
12
21
  request(:get, "objects/#{object}/attributes/#{id_or_slug}")
13
22
  end
14
23
 
15
- private
16
-
17
- def validate_object!(object)
24
+ private def validate_object!(object)
18
25
  raise ArgumentError, "Object type is required" if object.nil? || object.to_s.strip.empty?
19
26
  end
20
27
 
21
- def validate_id_or_slug!(id_or_slug)
28
+ private def validate_id_or_slug!(id_or_slug)
22
29
  raise ArgumentError, "Attribute ID or slug is required" if id_or_slug.nil? || id_or_slug.to_s.strip.empty?
23
30
  end
24
31
  end
25
32
  end
26
- end
33
+ end
@@ -1,39 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all API resource classes.
4
+ #
5
+ # This class provides common functionality and request handling
6
+ # for all Attio API resource implementations.
7
+ #
8
+ # @api private
9
+ # @author Ernest Sim
10
+ # @since 1.0.0
1
11
  module Attio
2
12
  module Resources
3
- # Base class for all API resource classes.
4
- #
5
- # This class provides common functionality and request handling
6
- # for all Attio API resource implementations.
7
- #
8
- # @api private
9
- # @author Ernest Sim
10
- # @since 1.0.0
11
13
  class Base
12
14
  # @return [Client] The API client instance
13
15
  attr_reader :client
14
16
 
15
17
  # Initialize a new resource instance.
16
- #
18
+ #
17
19
  # @param client [Client] The API client instance
18
20
  # @raise [ArgumentError] if client is nil
19
21
  def initialize(client)
20
22
  raise ArgumentError, "Client is required" unless client
23
+
21
24
  @client = client
22
25
  end
23
26
 
24
- private
25
-
26
- # Make an HTTP request to the API.
27
- #
28
- # @param method [Symbol] The HTTP method (:get, :post, :patch, :put, :delete)
29
- # @param path [String] The API endpoint path
30
- # @param params [Hash] Request parameters (default: {})
31
- #
32
- # @return [Hash] The API response
33
- # @raise [ArgumentError] if method is unsupported
34
- #
35
- # @api private
36
- def request(method, path, params = {})
27
+ private def request(method, path, params = {})
37
28
  case method
38
29
  when :get
39
30
  client.connection.get(path, params)
@@ -49,7 +40,6 @@ module Attio
49
40
  raise ArgumentError, "Unsupported HTTP method: #{method}"
50
41
  end
51
42
  end
52
-
53
43
  end
54
44
  end
55
- end
45
+ end
@@ -1,5 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
2
4
  module Resources
5
+ # API resource for managing Attio lists and list entries
6
+ #
7
+ # Lists are custom collections for organizing records in your workspace.
8
+ #
9
+ # @example Listing all lists
10
+ # client.lists.list
11
+ #
12
+ # @example Adding an entry to a list
13
+ # client.lists.create_entry(id: "list_id", data: { record_id: "rec_123" })
3
14
  class Lists < Base
4
15
  def list(**params)
5
16
  request(:get, "lists", params)
@@ -33,24 +44,22 @@ module Attio
33
44
  request(:delete, "lists/#{list_id}/entries/#{entry_id}")
34
45
  end
35
46
 
36
- private
37
-
38
- def validate_id!(id)
47
+ private def validate_id!(id)
39
48
  raise ArgumentError, "List ID is required" if id.nil? || id.to_s.strip.empty?
40
49
  end
41
50
 
42
- def validate_list_id!(list_id)
51
+ private def validate_list_id!(list_id)
43
52
  raise ArgumentError, "List ID is required" if list_id.nil? || list_id.to_s.strip.empty?
44
53
  end
45
54
 
46
- def validate_entry_id!(entry_id)
55
+ private def validate_entry_id!(entry_id)
47
56
  raise ArgumentError, "Entry ID is required" if entry_id.nil? || entry_id.to_s.strip.empty?
48
57
  end
49
58
 
50
- def validate_data!(data)
59
+ private def validate_data!(data)
51
60
  raise ArgumentError, "Data is required" if data.nil?
52
61
  raise ArgumentError, "Data must be a hash" unless data.is_a?(Hash)
53
62
  end
54
63
  end
55
64
  end
56
- end
65
+ end
@@ -1,5 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
2
4
  module Resources
5
+ # API resource for managing Attio objects
6
+ #
7
+ # Objects define the schema and structure for different types of
8
+ # records in your Attio workspace (e.g., people, companies).
9
+ #
10
+ # @example Listing all objects
11
+ # client.objects.list
3
12
  class Objects < Base
4
13
  def list(**params)
5
14
  request(:get, "objects", params)
@@ -10,11 +19,9 @@ module Attio
10
19
  request(:get, "objects/#{id_or_slug}")
11
20
  end
12
21
 
13
- private
14
-
15
- def validate_id_or_slug!(id_or_slug)
22
+ private def validate_id_or_slug!(id_or_slug)
16
23
  raise ArgumentError, "Object ID or slug is required" if id_or_slug.nil? || id_or_slug.to_s.strip.empty?
17
24
  end
18
25
  end
19
26
  end
20
- end
27
+ end
@@ -1,41 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Handles all record-related API operations.
4
+ #
5
+ # Records are the main data entities in Attio, representing things like
6
+ # people, companies, deals, etc. This class provides methods to create,
7
+ # read, update, delete, and query records.
8
+ #
9
+ # @example List records
10
+ # records = client.records.list(object: 'people', filters: { name: 'John' })
11
+ #
12
+ # @example Create a record
13
+ # record = client.records.create(
14
+ # object: 'people',
15
+ # data: { name: 'Jane Doe', email: 'jane@example.com' }
16
+ # )
17
+ #
18
+ # @author Ernest Sim
19
+ # @since 1.0.0
1
20
  module Attio
2
21
  module Resources
3
- # Handles all record-related API operations.
4
- #
5
- # Records are the main data entities in Attio, representing things like
6
- # people, companies, deals, etc. This class provides methods to create,
7
- # read, update, delete, and query records.
8
- #
9
- # @example List records
10
- # records = client.records.list(object: 'people', filters: { name: 'John' })
11
- #
12
- # @example Create a record
13
- # record = client.records.create(
14
- # object: 'people',
15
- # data: { name: 'Jane Doe', email: 'jane@example.com' }
16
- # )
17
- #
18
- # @author Ernest Sim
19
- # @since 1.0.0
20
22
  class Records < Base
21
23
  # Query and list records for a specific object type.
22
- #
24
+ #
23
25
  # This method allows you to retrieve records with optional filtering,
24
26
  # sorting, and pagination parameters.
25
- #
27
+ #
26
28
  # @param object [String] The object type to query (e.g., 'people', 'companies')
27
29
  # @param params [Hash] Query parameters including filters, sorts, and pagination
28
30
  # @option params [Hash] :filters Filtering criteria
29
31
  # @option params [Array] :sorts Sorting options
30
32
  # @option params [Integer] :limit Number of records to return
31
33
  # @option params [String] :cursor Pagination cursor for next page
32
- #
34
+ #
33
35
  # @return [Hash] API response containing records and pagination info
34
36
  # @raise [ArgumentError] if object is nil or empty
35
- #
37
+ #
36
38
  # @example Basic listing
37
39
  # records = client.records.list(object: 'people')
38
- #
40
+ #
39
41
  # @example With filters
40
42
  # records = client.records.list(
41
43
  # object: 'people',
@@ -48,13 +50,13 @@ module Attio
48
50
  end
49
51
 
50
52
  # Retrieve a specific record by ID.
51
- #
53
+ #
52
54
  # @param object [String] The object type (e.g., 'people', 'companies')
53
55
  # @param id [String] The record ID
54
- #
56
+ #
55
57
  # @return [Hash] The record data
56
58
  # @raise [ArgumentError] if object or id is nil or empty
57
- #
59
+ #
58
60
  # @example
59
61
  # record = client.records.get(object: 'people', id: 'abc123')
60
62
  def get(object:, id:)
@@ -64,13 +66,13 @@ module Attio
64
66
  end
65
67
 
66
68
  # Create a new record.
67
- #
69
+ #
68
70
  # @param object [String] The object type to create the record in
69
71
  # @param data [Hash] The record data to create
70
- #
72
+ #
71
73
  # @return [Hash] The created record data
72
74
  # @raise [ArgumentError] if object is nil/empty or data is invalid
73
- #
75
+ #
74
76
  # @example Create a person
75
77
  # record = client.records.create(
76
78
  # object: 'people',
@@ -87,14 +89,14 @@ module Attio
87
89
  end
88
90
 
89
91
  # Update an existing record.
90
- #
92
+ #
91
93
  # @param object [String] The object type
92
94
  # @param id [String] The record ID to update
93
95
  # @param data [Hash] The updated record data
94
- #
96
+ #
95
97
  # @return [Hash] The updated record data
96
98
  # @raise [ArgumentError] if object, id, or data is invalid
97
- #
99
+ #
98
100
  # @example Update a person's name
99
101
  # record = client.records.update(
100
102
  # object: 'people',
@@ -109,13 +111,13 @@ module Attio
109
111
  end
110
112
 
111
113
  # Delete a record.
112
- #
114
+ #
113
115
  # @param object [String] The object type
114
116
  # @param id [String] The record ID to delete
115
- #
117
+ #
116
118
  # @return [Hash] Deletion confirmation
117
119
  # @raise [ArgumentError] if object or id is nil or empty
118
- #
120
+ #
119
121
  # @example
120
122
  # client.records.delete(object: 'people', id: 'abc123')
121
123
  def delete(object:, id:)
@@ -124,35 +126,28 @@ module Attio
124
126
  request(:delete, "objects/#{object}/records/#{id}")
125
127
  end
126
128
 
127
- private
128
-
129
- # Validates that the object parameter is present and not empty.
130
- #
131
- # @param object [String, nil] The object type to validate
132
- # @raise [ArgumentError] if object is nil or empty
133
- # @api private
134
- def validate_object!(object)
129
+ private def validate_object!(object)
135
130
  raise ArgumentError, "Object type is required" if object.nil? || object.to_s.strip.empty?
136
131
  end
137
132
 
138
133
  # Validates that the ID parameter is present and not empty.
139
- #
134
+ #
140
135
  # @param id [String, nil] The record ID to validate
141
136
  # @raise [ArgumentError] if id is nil or empty
142
137
  # @api private
143
- def validate_id!(id)
138
+ private def validate_id!(id)
144
139
  raise ArgumentError, "Record ID is required" if id.nil? || id.to_s.strip.empty?
145
140
  end
146
141
 
147
142
  # Validates that the data parameter is present and is a hash.
148
- #
143
+ #
149
144
  # @param data [Hash, nil] The data to validate
150
145
  # @raise [ArgumentError] if data is nil or not a hash
151
146
  # @api private
152
- def validate_data!(data)
147
+ private def validate_data!(data)
153
148
  raise ArgumentError, "Data is required" if data.nil?
154
149
  raise ArgumentError, "Data must be a hash" unless data.is_a?(Hash)
155
150
  end
156
151
  end
157
152
  end
158
- end
153
+ end
@@ -1,5 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
2
4
  module Resources
5
+ # API resource for managing workspace users
6
+ #
7
+ # Users are people who have access to your Attio workspace.
8
+ #
9
+ # @example Listing all users
10
+ # client.users.list
3
11
  class Users < Base
4
12
  def list(**params)
5
13
  request(:get, "users", params)
@@ -10,11 +18,9 @@ module Attio
10
18
  request(:get, "users/#{id}")
11
19
  end
12
20
 
13
- private
14
-
15
- def validate_id!(id)
21
+ private def validate_id!(id)
16
22
  raise ArgumentError, "User ID is required" if id.nil? || id.to_s.strip.empty?
17
23
  end
18
24
  end
19
25
  end
20
- end
26
+ end
@@ -1,5 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
2
4
  module Resources
5
+ # API resource for managing workspace information
6
+ #
7
+ # Workspaces are the top-level organizational unit in Attio.
8
+ #
9
+ # @example Getting workspace information
10
+ # client.workspaces.get
3
11
  class Workspaces < Base
4
12
  def get
5
13
  request(:get, "workspace")
@@ -10,4 +18,4 @@ module Attio
10
18
  end
11
19
  end
12
20
  end
13
- end
21
+ end
@@ -1,4 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
4
+ # Handles automatic retry logic for failed API requests
5
+ #
6
+ # This class implements exponential backoff retry strategy for
7
+ # transient errors like timeouts and rate limits.
8
+ #
9
+ # @example Using the retry handler
10
+ # handler = RetryHandler.new(max_retries: 5)
11
+ # handler.with_retry { api_client.get("/endpoint") }
2
12
  class RetryHandler
3
13
  DEFAULT_MAX_RETRIES = 3
4
14
  DEFAULT_RETRY_DELAY = 1 # seconds
@@ -7,12 +17,12 @@ module Attio
7
17
  HttpClient::TimeoutError,
8
18
  HttpClient::ConnectionError,
9
19
  ServerError,
10
- RateLimitError
20
+ RateLimitError,
11
21
  ].freeze
12
22
 
13
23
  attr_reader :max_retries, :retry_delay, :backoff_factor, :logger
14
24
 
15
- def initialize(max_retries: DEFAULT_MAX_RETRIES,
25
+ def initialize(max_retries: DEFAULT_MAX_RETRIES,
16
26
  retry_delay: DEFAULT_RETRY_DELAY,
17
27
  backoff_factor: DEFAULT_BACKOFF_FACTOR,
18
28
  logger: nil)
@@ -22,7 +32,7 @@ module Attio
22
32
  @logger = logger
23
33
  end
24
34
 
25
- def with_retry(&block)
35
+ def with_retry
26
36
  retries = 0
27
37
  delay = retry_delay
28
38
 
@@ -30,7 +40,7 @@ module Attio
30
40
  yield
31
41
  rescue *RETRIABLE_ERRORS => e
32
42
  retries += 1
33
-
43
+
34
44
  if retries <= max_retries
35
45
  log_retry(e, retries, delay)
36
46
  sleep(delay)
@@ -43,11 +53,9 @@ module Attio
43
53
  end
44
54
  end
45
55
 
46
- private
47
-
48
- def log_retry(error, attempt, delay)
56
+ private def log_retry(error, attempt, delay)
49
57
  return unless logger
50
-
58
+
51
59
  logger.warn(
52
60
  "Retry attempt #{attempt}/#{max_retries}",
53
61
  error: error.class.name,
@@ -56,9 +64,9 @@ module Attio
56
64
  )
57
65
  end
58
66
 
59
- def log_failure(error, attempts)
67
+ private def log_failure(error, attempts)
60
68
  return unless logger
61
-
69
+
62
70
  logger.error(
63
71
  "Max retries exceeded",
64
72
  error: error.class.name,
@@ -67,4 +75,4 @@ module Attio
67
75
  )
68
76
  end
69
77
  end
70
- end
78
+ end
data/lib/attio/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
2
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
3
5
  end
data/lib/attio.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "typhoeus"
2
4
 
3
5
  require "attio/version"
@@ -14,32 +16,32 @@ require "attio/resources/attributes"
14
16
  require "attio/resources/users"
15
17
 
16
18
  # The main Attio module provides access to the Attio API client.
17
- #
19
+ #
18
20
  # This is the primary entry point for interacting with the Attio API.
19
- #
21
+ #
20
22
  # @example Basic usage
21
23
  # client = Attio.client(api_key: 'your-api-key')
22
- #
24
+ #
23
25
  # @example Working with records
24
26
  # # List records for a specific object type
25
27
  # records = client.records.list(object: 'people', filters: { name: 'John' })
26
- #
28
+ #
27
29
  # # Create a new record
28
30
  # new_record = client.records.create(
29
31
  # object: 'people',
30
32
  # data: { name: 'Jane Doe', email: 'jane@example.com' }
31
33
  # )
32
- #
34
+ #
33
35
  # # Get a specific record
34
36
  # record = client.records.get(object: 'people', id: 'record-id')
35
- #
37
+ #
36
38
  # # Update a record
37
39
  # updated = client.records.update(
38
40
  # object: 'people',
39
41
  # id: 'record-id',
40
42
  # data: { name: 'Jane Smith' }
41
43
  # )
42
- #
44
+ #
43
45
  # # Delete a record
44
46
  # client.records.delete(object: 'people', id: 'record-id')
45
47
  #
@@ -47,11 +49,11 @@ require "attio/resources/users"
47
49
  # @since 1.0.0
48
50
  module Attio
49
51
  # Creates a new Attio API client instance.
50
- #
52
+ #
51
53
  # @param api_key [String] Your Attio API key
52
54
  # @return [Client] A new client instance configured with the provided API key
53
55
  # @raise [ArgumentError] if api_key is nil or empty
54
- #
56
+ #
55
57
  # @example Create a client
56
58
  # client = Attio.client(api_key: 'your-api-key-here')
57
59
  def self.client(api_key:)