bank_of_thailand 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c8ed6b63a147c446647a9a14d22496586de29fcb34841bb87eb1486a7307118
4
- data.tar.gz: a5929c13399366bf2715621922c73c6565c8a98b2f6d5d172ce8520e3006c0d6
3
+ metadata.gz: e4e88cba8f1640985c4d18be3853c7072429a27df7f7d28c9bd490c13321826b
4
+ data.tar.gz: fe6da0cb6c0dd0a3f7f6a782f196bfd9ac969605b44f0f8b83a89af73afa641d
5
5
  SHA512:
6
- metadata.gz: 2f18242c5e071d0e42f3123bf1a796427a693603bb8c6297c9ae98ef01585515ffe5cf9e6fbf332c3454e3a3ad28c678f3347ae09b58dd7d62407b137f932ac8
7
- data.tar.gz: e237535be4e3f98f0953ac74b06e8bdde61f91e7ee8a70fd7665f5faeb46fa7ede942d552d3a2d4dc2e64ad85e7e99b9f4f09b99f6f9a2789ba971430caef405
6
+ metadata.gz: 5816fa3b3f30eb7c86bff63ec7e67c95b16730ec73dd570c4187a2cbbaaed5edeccd77150b08d5d18613df959f2c1f56c7a06b3920b7f47be27c2d138953e209
7
+ data.tar.gz: c8e516160e94ca7e361b4088894e76e6e2a686c63fa68c06fff1189ebf1e76c886dee72fbe8656361bace652c9fd8b7fa3e04c1fb66ee59de8570b57d9fd9918
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-11-04
4
+
5
+ ### Added
6
+ - **Response wrapper class** with comprehensive data analysis utilities
7
+ - Basic statistics: `count`, `first`, `last`, `min`, `max`, `sum`, `average`/`mean`
8
+ - Date range analysis: `date_range`, `period_days`, `complete?`, `missing_dates`
9
+ - Rate change calculations: `change`, `daily_changes`, `volatility`, `trend`
10
+ - CSV export: `to_csv` method for easy data export
11
+ - Backward compatibility: `[]` and `dig` methods for hash-like access
12
+ - Enhanced test coverage (117 examples, 100% pass rate)
13
+ - Updated README with Response class documentation
14
+
15
+ ### Changed
16
+ - All API methods now return `Response` objects instead of raw hashes
17
+ - Improved error handling for different response formats (arrays, hashes, edge cases)
18
+
3
19
  ## [0.1.0] - 2025-11-03
4
20
 
5
21
  ### Added
data/README.md CHANGED
@@ -9,9 +9,10 @@ A Ruby gem for accessing the Bank of Thailand's public API services. This gem pr
9
9
 
10
10
  - **Token-based Authentication** - Secure API access using BOT's authorization system
11
11
  - **11 API Resources** - Complete coverage of all documented BOT API products
12
+ - **Smart Response Objects** - Built-in statistics, CSV export, and data analysis
12
13
  - **Type Safety** - Comprehensive error handling with custom exception classes
13
14
  - **Flexible Configuration** - Global or instance-level configuration options
14
- - **Full Test Coverage** - 66 examples with 100% pass rate
15
+ - **Full Test Coverage** - 117 examples with 100% pass rate
15
16
  - **YARD Documentation** - Complete API documentation for all endpoints
16
17
 
17
18
  ## Installation
@@ -77,15 +78,7 @@ client = BankOfThailand::Client.new do |config|
77
78
  end
78
79
  ```
79
80
 
80
- ### Configuration Options
81
-
82
- | Option | Type | Default | Description |
83
- |--------|------|---------|-------------|
84
- | `api_token` | String | `nil` | **Required.** Your BOT API token from the portal |
85
- | `base_url` | String | `https://gateway.api.bot.or.th` | Base gateway URL (rarely needs changing) |
86
- | `timeout` | Integer | `30` | Request timeout in seconds |
87
- | `max_retries` | Integer | `3` | Number of retry attempts for failed requests |
88
- | `logger` | Logger | `nil` | Optional logger for debugging |
81
+ **Available Options:** `api_token` (required), `base_url`, `timeout`, `max_retries`, `logger`
89
82
 
90
83
  ## Usage
91
84
 
@@ -116,6 +109,42 @@ client.financial_holidays # Financial Institutions' Holidays
116
109
  client.search_series # Search Stat APIs
117
110
  ```
118
111
 
112
+ ### Working with Responses
113
+
114
+ All API calls return a `Response` object with built-in data analysis features:
115
+
116
+ ```ruby
117
+ rates = client.exchange_rate.daily(
118
+ start_period: "2025-01-01",
119
+ end_period: "2025-01-31"
120
+ )
121
+
122
+ # Access data
123
+ rates.data # Array of data points
124
+ rates.count # Number of records
125
+ rates.first # First record
126
+ rates.last # Last record
127
+
128
+ # Quick statistics
129
+ rates.average("value") # Average rate
130
+ rates.min("value") # Minimum rate
131
+ rates.max("value") # Maximum rate
132
+
133
+ # Analyze changes
134
+ rates.change("value") # Overall change with percentage
135
+ rates.volatility("value") # Daily volatility
136
+ rates.trend("value") # :up, :down, or :flat
137
+
138
+ # Export to CSV
139
+ rates.to_csv("rates.csv") # Save to file
140
+ csv_string = rates.to_csv # Get CSV string
141
+
142
+ # Check completeness
143
+ rates.date_range # ["2025-01-01", "2025-01-31"]
144
+ rates.complete? # All dates present?
145
+ rates.missing_dates # Array of missing dates
146
+ ```
147
+
119
148
  ### Exchange Rates
120
149
 
121
150
  #### Weighted-average Interbank Exchange Rate (THB/USD)
@@ -177,12 +206,6 @@ implied = client.implied_rate.rates(
177
206
 
178
207
  ### Securities & Markets
179
208
 
180
- ```ruby
181
- # Debt securities auction results
182
- ```
183
-
184
- ### Securities & Markets
185
-
186
209
  ```ruby
187
210
  # Debt securities auction results
188
211
  results = client.debt_securities.auction_results(
@@ -223,32 +246,22 @@ series = client.search_series.search(keyword: "government debt")
223
246
 
224
247
  ## Error Handling
225
248
 
249
+ The gem provides specific exception classes for different error scenarios:
250
+
226
251
  ```ruby
227
252
  begin
228
- client.exchange_rate.daily
253
+ rates = client.exchange_rate.daily(start_period: "2025-01-01", end_period: "2025-01-31")
254
+ rescue BankOfThailand::AuthenticationError
255
+ puts "Invalid API token"
229
256
  rescue BankOfThailand::RateLimitError => e
230
- retry_after = e.retry_after
231
- rescue BankOfThailand::APIError => e
232
- puts "API error: #{e.message}"
257
+ sleep e.retry_after
258
+ retry
259
+ rescue BankOfThailand::RequestError => e
260
+ puts "Request failed: #{e.message}"
233
261
  end
234
262
  ```
235
263
 
236
- ### Exception Hierarchy
237
-
238
- ```
239
- BankOfThailand::Error (StandardError)
240
- ├── ConfigurationError
241
- └── RequestError
242
- ├── AuthenticationError
243
- │ └── InvalidTokenError
244
- ├── NotFoundError
245
- ├── RateLimitError
246
- └── ServerError
247
- ```
248
-
249
- ## Rate Limits
250
-
251
- Rate limits vary by API product. The gem automatically handles rate limiting errors and provides retry information via `RateLimitError#retry_after`.
264
+ **Exception Types:** `ConfigurationError`, `AuthenticationError`, `NotFoundError`, `RateLimitError`, `ServerError`
252
265
 
253
266
  ## Development
254
267
 
@@ -149,12 +149,13 @@ module BankOfThailand
149
149
 
150
150
  # Handle HTTP response
151
151
  # @param response [Faraday::Response] HTTP response
152
- # @return [Hash] Parsed response body
152
+ # @return [Response] Wrapped response object
153
153
  # @raise [RequestError] if response indicates an error
154
154
  def handle_response(response)
155
155
  case response.status
156
156
  when 200..299
157
- parse_json(response.body)
157
+ json = parse_json(response.body)
158
+ Response.new(json)
158
159
  when 401
159
160
  raise AuthenticationError.new("Authentication failed. Check your API token.", response)
160
161
  when 403
@@ -176,11 +177,13 @@ module BankOfThailand
176
177
 
177
178
  # Parse JSON response
178
179
  # @param body [String] Response body
179
- # @return [Hash] Parsed JSON
180
+ # @return [Hash, Array] Parsed JSON
180
181
  def parse_json(body)
181
182
  return {} if body.nil? || body.empty?
182
183
 
183
- JSON.parse(body)
184
+ result = JSON.parse(body)
185
+ # Ensure we return a Hash or Array, not a primitive
186
+ result.is_a?(Hash) || result.is_a?(Array) ? result : {}
184
187
  rescue JSON::ParserError => e
185
188
  raise RequestError, "Invalid JSON response: #{e.message}"
186
189
  end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "date"
5
+
6
+ module BankOfThailand
7
+ # Wrapper for API responses with convenience methods
8
+ class Response
9
+ # @return [Hash] the raw API response
10
+ attr_reader :raw
11
+
12
+ # @return [Array<Hash>] the extracted data array
13
+ attr_reader :data
14
+
15
+ # Initialize a new Response
16
+ # @param raw_response [Hash] the raw API response
17
+ def initialize(raw_response)
18
+ @raw = raw_response
19
+ @data = extract_data(raw_response)
20
+ end
21
+
22
+ # Allow hash-like access for backward compatibility
23
+ # @param key [String, Symbol] the key to access
24
+ # @return [Object] the value at the key
25
+ def [](key)
26
+ return nil unless raw.is_a?(Hash)
27
+
28
+ raw[key.to_s]
29
+ end
30
+
31
+ # Allow hash-like dig for backward compatibility
32
+ # @param keys [Array] the keys to dig through
33
+ # @return [Object] the value at the nested keys
34
+ def dig(*keys)
35
+ return nil unless raw.is_a?(Hash)
36
+
37
+ raw.dig(*keys.map(&:to_s))
38
+ end
39
+
40
+ # Count of data points
41
+ # @return [Integer] number of data points
42
+ def count
43
+ data.size
44
+ end
45
+
46
+ # First data point
47
+ # @return [Hash, nil] first data point or nil if empty
48
+ def first
49
+ data.first
50
+ end
51
+
52
+ # Last data point
53
+ # @return [Hash, nil] last data point or nil if empty
54
+ def last
55
+ data.last
56
+ end
57
+
58
+ # Extract numeric values for a column
59
+ # @param column [String] the column name
60
+ # @return [Array<Float>] array of numeric values
61
+ def values_for(column)
62
+ data.select { |row| row.is_a?(Hash) }.map { |row| row[column]&.to_f }.compact
63
+ end
64
+
65
+ # Minimum value for a column
66
+ # @param column [String] the column name
67
+ # @return [Float, nil] minimum value or nil if no data
68
+ def min(column)
69
+ values_for(column).min
70
+ end
71
+
72
+ # Maximum value for a column
73
+ # @param column [String] the column name
74
+ # @return [Float, nil] maximum value or nil if no data
75
+ def max(column)
76
+ values_for(column).max
77
+ end
78
+
79
+ # Sum of values for a column
80
+ # @param column [String] the column name
81
+ # @return [Float] sum of values
82
+ def sum(column)
83
+ values_for(column).sum
84
+ end
85
+
86
+ # Average value for a column
87
+ # @param column [String] the column name
88
+ # @return [Float] average value or 0 if no data
89
+ def average(column)
90
+ vals = values_for(column)
91
+ return 0.0 if vals.empty?
92
+
93
+ vals.sum / vals.size.to_f
94
+ end
95
+
96
+ alias mean average
97
+
98
+ # Date range covered by the data
99
+ # @return [Array<String>, nil] [start_date, end_date] or nil if no dates
100
+ def date_range
101
+ dates = data.select { |row| row.is_a?(Hash) }.map { |row| row["period"] || row["date"] }.compact
102
+ return nil if dates.empty?
103
+
104
+ dates.minmax
105
+ end
106
+
107
+ # Number of days in the period
108
+ # @return [Integer] number of days
109
+ def period_days
110
+ range = date_range
111
+ return 0 unless range
112
+
113
+ (Date.parse(range[1]) - Date.parse(range[0])).to_i + 1
114
+ rescue Date::Error
115
+ 0
116
+ end
117
+
118
+ # Check if data is complete for the date range
119
+ # @return [Boolean] true if all expected days are present
120
+ def complete?
121
+ expected_days = period_days
122
+ return true if expected_days.zero?
123
+
124
+ count >= expected_days
125
+ end
126
+
127
+ # Find missing dates in the range
128
+ # @return [Array<Date>] array of missing dates
129
+ def missing_dates
130
+ return [] unless date_range
131
+
132
+ start_date = Date.parse(date_range[0])
133
+ end_date = Date.parse(date_range[1])
134
+ actual_dates = data.select { |row| row.is_a?(Hash) }.map { |row| Date.parse(row["period"] || row["date"]) }
135
+
136
+ (start_date..end_date).reject { |date| actual_dates.include?(date) }
137
+ rescue Date::Error
138
+ []
139
+ end
140
+
141
+ # Calculate change metrics for a column
142
+ # @param column [String] the column name (default: "value")
143
+ # @return [Hash, nil] hash with :absolute, :percentage, :first_value, :last_value
144
+ def change(column = "value")
145
+ vals = values_for(column)
146
+ return nil if vals.size < 2
147
+
148
+ first_val = vals.first
149
+ last_val = vals.last
150
+
151
+ {
152
+ absolute: last_val - first_val,
153
+ percentage: first_val.zero? ? 0.0 : ((last_val - first_val) / first_val * 100).round(4),
154
+ first_value: first_val,
155
+ last_value: last_val
156
+ }
157
+ end
158
+
159
+ # Calculate daily changes for a column
160
+ # @param column [String] the column name (default: "value")
161
+ # @return [Array<Hash>] array of change hashes with :absolute and :percentage
162
+ def daily_changes(column = "value")
163
+ vals = values_for(column)
164
+ return [] if vals.size < 2
165
+
166
+ vals.each_cons(2).map do |prev, curr|
167
+ {
168
+ absolute: curr - prev,
169
+ percentage: prev.zero? ? 0.0 : ((curr - prev) / prev * 100).round(4)
170
+ }
171
+ end
172
+ end
173
+
174
+ # Calculate volatility (standard deviation of daily percentage changes)
175
+ # @param column [String] the column name (default: "value")
176
+ # @return [Float] volatility as standard deviation
177
+ def volatility(column = "value")
178
+ changes = daily_changes(column).map { |c| c[:percentage] }
179
+ return 0.0 if changes.empty?
180
+
181
+ mean = changes.sum / changes.size.to_f
182
+ variance = changes.sum { |x| (x - mean)**2 } / changes.size.to_f
183
+ Math.sqrt(variance).round(4)
184
+ end
185
+
186
+ # Determine trend direction
187
+ # @param column [String] the column name (default: "value")
188
+ # @return [Symbol] :up, :down, or :flat
189
+ def trend(column = "value")
190
+ change_data = change(column)
191
+ return :flat unless change_data
192
+
193
+ pct = change_data[:percentage]
194
+
195
+ if pct > 1
196
+ :up
197
+ elsif pct < -1
198
+ :down
199
+ else
200
+ :flat
201
+ end
202
+ end
203
+
204
+ # Export data to CSV
205
+ # @param filename [String, nil] optional filename to save CSV
206
+ # @return [String] CSV string or filename if saved
207
+ def to_csv(filename = nil)
208
+ csv_data = CSV.generate do |csv|
209
+ csv << extract_headers
210
+ extract_rows.each { |row| csv << row }
211
+ end
212
+
213
+ if filename
214
+ File.write(filename, csv_data)
215
+ filename
216
+ else
217
+ csv_data
218
+ end
219
+ end
220
+
221
+ private
222
+
223
+ # Extract data array from API response
224
+ # @param response [Hash, Array, Object] raw API response
225
+ # @return [Array<Hash>] extracted data array
226
+ def extract_data(response)
227
+ # Handle array responses (e.g., financial holidays)
228
+ return response if response.is_a?(Array)
229
+
230
+ # Handle non-hash/non-array responses
231
+ return [] unless response.is_a?(Hash)
232
+
233
+ # Handle standard BOT API format with result.data
234
+ # Only dig if result exists and is a Hash
235
+ result_data = response["result"]
236
+ return [] unless result_data.is_a?(Hash)
237
+
238
+ result_data["data"] || []
239
+ end
240
+
241
+ # Extract CSV headers from data
242
+ # @return [Array<String>] array of header names
243
+ def extract_headers
244
+ return [] if data.empty?
245
+
246
+ first_row = data.first
247
+ case first_row
248
+ when Hash
249
+ first_row.keys
250
+ when Array
251
+ (1..first_row.size).map { |i| "column_#{i}" }
252
+ else
253
+ ["value"]
254
+ end
255
+ end
256
+
257
+ # Extract CSV rows from data
258
+ # @return [Array<Array>] array of row arrays
259
+ def extract_rows
260
+ data.map do |row|
261
+ case row
262
+ when Hash
263
+ row.values
264
+ when Array
265
+ row
266
+ else
267
+ [row]
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BankOfThailand
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "bank_of_thailand/version"
4
4
  require_relative "bank_of_thailand/errors"
5
5
  require_relative "bank_of_thailand/configuration"
6
+ require_relative "bank_of_thailand/response"
6
7
  require_relative "bank_of_thailand/client"
7
8
  require_relative "bank_of_thailand/resource"
8
9
  require_relative "bank_of_thailand/resources/exchange_rate"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bank_of_thailand
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chayut Orapinpatipat
@@ -178,6 +178,7 @@ files:
178
178
  - lib/bank_of_thailand/resources/search_series.rb
179
179
  - lib/bank_of_thailand/resources/statistics.rb
180
180
  - lib/bank_of_thailand/resources/swap_point.rb
181
+ - lib/bank_of_thailand/response.rb
181
182
  - lib/bank_of_thailand/version.rb
182
183
  - sig/bank_of_thailand.rbs
183
184
  homepage: https://github.com/chayuto/bank_of_thailand