sec_api 1.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +54 -0
  3. data/.devcontainer/README.md +178 -0
  4. data/.devcontainer/devcontainer.json +46 -0
  5. data/.devcontainer/docker-compose.yml +28 -0
  6. data/.devcontainer/post-create.sh +51 -0
  7. data/.devcontainer/post-start.sh +44 -0
  8. data/.rspec +3 -0
  9. data/.standard.yml +3 -0
  10. data/CHANGELOG.md +5 -0
  11. data/CLAUDE.md +0 -0
  12. data/LICENSE.txt +21 -0
  13. data/MIGRATION.md +274 -0
  14. data/README.md +370 -0
  15. data/Rakefile +10 -0
  16. data/config/secapi.yml.example +57 -0
  17. data/docs/development-guide.md +291 -0
  18. data/docs/enumerator_pattern_design.md +483 -0
  19. data/docs/examples/README.md +58 -0
  20. data/docs/examples/backfill_filings.rb +419 -0
  21. data/docs/examples/instrumentation.rb +583 -0
  22. data/docs/examples/query_builder.rb +308 -0
  23. data/docs/examples/streaming_notifications.rb +491 -0
  24. data/docs/index.md +244 -0
  25. data/docs/migration-guide-v1.md +1091 -0
  26. data/docs/pre-review-checklist.md +145 -0
  27. data/docs/project-overview.md +90 -0
  28. data/docs/project-scan-report.json +60 -0
  29. data/docs/source-tree-analysis.md +190 -0
  30. data/lib/sec_api/callback_helper.rb +49 -0
  31. data/lib/sec_api/client.rb +606 -0
  32. data/lib/sec_api/collections/filings.rb +267 -0
  33. data/lib/sec_api/collections/fulltext_results.rb +86 -0
  34. data/lib/sec_api/config.rb +590 -0
  35. data/lib/sec_api/deep_freezable.rb +42 -0
  36. data/lib/sec_api/errors/authentication_error.rb +24 -0
  37. data/lib/sec_api/errors/configuration_error.rb +5 -0
  38. data/lib/sec_api/errors/error.rb +75 -0
  39. data/lib/sec_api/errors/network_error.rb +26 -0
  40. data/lib/sec_api/errors/not_found_error.rb +23 -0
  41. data/lib/sec_api/errors/pagination_error.rb +28 -0
  42. data/lib/sec_api/errors/permanent_error.rb +29 -0
  43. data/lib/sec_api/errors/rate_limit_error.rb +57 -0
  44. data/lib/sec_api/errors/reconnection_error.rb +34 -0
  45. data/lib/sec_api/errors/server_error.rb +25 -0
  46. data/lib/sec_api/errors/transient_error.rb +28 -0
  47. data/lib/sec_api/errors/validation_error.rb +23 -0
  48. data/lib/sec_api/extractor.rb +122 -0
  49. data/lib/sec_api/filing_journey.rb +477 -0
  50. data/lib/sec_api/mapping.rb +125 -0
  51. data/lib/sec_api/metrics_collector.rb +411 -0
  52. data/lib/sec_api/middleware/error_handler.rb +250 -0
  53. data/lib/sec_api/middleware/instrumentation.rb +186 -0
  54. data/lib/sec_api/middleware/rate_limiter.rb +541 -0
  55. data/lib/sec_api/objects/data_file.rb +34 -0
  56. data/lib/sec_api/objects/document_format_file.rb +45 -0
  57. data/lib/sec_api/objects/entity.rb +92 -0
  58. data/lib/sec_api/objects/extracted_data.rb +118 -0
  59. data/lib/sec_api/objects/fact.rb +147 -0
  60. data/lib/sec_api/objects/filing.rb +197 -0
  61. data/lib/sec_api/objects/fulltext_result.rb +66 -0
  62. data/lib/sec_api/objects/period.rb +96 -0
  63. data/lib/sec_api/objects/stream_filing.rb +194 -0
  64. data/lib/sec_api/objects/xbrl_data.rb +356 -0
  65. data/lib/sec_api/query.rb +423 -0
  66. data/lib/sec_api/rate_limit_state.rb +130 -0
  67. data/lib/sec_api/rate_limit_tracker.rb +154 -0
  68. data/lib/sec_api/stream.rb +841 -0
  69. data/lib/sec_api/structured_logger.rb +199 -0
  70. data/lib/sec_api/types.rb +32 -0
  71. data/lib/sec_api/version.rb +42 -0
  72. data/lib/sec_api/xbrl.rb +220 -0
  73. data/lib/sec_api.rb +137 -0
  74. data/sig/sec_api.rbs +4 -0
  75. metadata +217 -0
data/MIGRATION.md ADDED
@@ -0,0 +1,274 @@
1
+ # Migration Guide: v0.1.0 → v1.0.0
2
+
3
+ This guide documents breaking changes when upgrading from `sec_api` v0.1.0 to v1.0.0 and provides migration examples.
4
+
5
+ ## Overview of Breaking Changes
6
+
7
+ v1.0.0 standardizes all API responses to return **strongly-typed, immutable objects** instead of raw hashes. This ensures thread safety, improves IDE support, and provides a consistent API surface.
8
+
9
+ **Affected endpoints:**
10
+ - Mapping endpoints (ticker, cik, cusip, name)
11
+ - Extractor endpoint (extract)
12
+ - FulltextResults collection (now Enumerable)
13
+
14
+ ## Breaking Change #1: Mapping Methods Return Entity Objects
15
+
16
+ ### What Changed
17
+
18
+ All mapping methods now return `SecApi::Entity` objects instead of raw hashes.
19
+
20
+ **Affected methods:**
21
+ - `client.mapping.ticker(symbol)`
22
+ - `client.mapping.cik(cik_number)`
23
+ - `client.mapping.cusip(cusip_id)`
24
+ - `client.mapping.name(company_name)`
25
+
26
+ ### Migration Example
27
+
28
+ **v0.1.0 - Returns Hash:**
29
+ ```ruby
30
+ client = SecApi::Client.new(api_key: "your_key")
31
+ entity_data = client.mapping.ticker("AAPL")
32
+
33
+ # Hash access with string keys
34
+ cik = entity_data["cik"] # => "0000320193"
35
+ ticker = entity_data["ticker"] # => "AAPL"
36
+ name = entity_data["name"] # => "Apple Inc."
37
+ ```
38
+
39
+ **v1.0.0 - Returns Entity Object:**
40
+ ```ruby
41
+ client = SecApi::Client.new(api_key: "your_key")
42
+ entity = client.mapping.ticker("AAPL")
43
+
44
+ # Method access (typed attributes)
45
+ cik = entity.cik # => "0000320193"
46
+ ticker = entity.ticker # => "AAPL"
47
+ name = entity.name # => "Apple Inc."
48
+
49
+ # Hash access NO LONGER WORKS
50
+ entity["cik"] # => NoMethodError: undefined method `[]' for SecApi::Entity
51
+ ```
52
+
53
+ ### Migration Steps
54
+
55
+ 1. **Replace hash bracket notation with method calls:**
56
+ ```ruby
57
+ # BEFORE
58
+ entity_data["cik"]
59
+
60
+ # AFTER
61
+ entity.cik
62
+ ```
63
+
64
+ 2. **Update type checks if any:**
65
+ ```ruby
66
+ # BEFORE
67
+ if entity_data.is_a?(Hash)
68
+
69
+ # AFTER
70
+ if entity.is_a?(SecApi::Entity)
71
+ ```
72
+
73
+ 3. **Immutability note:** Entity objects are frozen and cannot be modified:
74
+ ```ruby
75
+ entity.cik = "new_value" # => FrozenError: can't modify frozen SecApi::Entity
76
+ ```
77
+
78
+ ## Breaking Change #2: Extractor Returns ExtractedData Objects
79
+
80
+ ### What Changed
81
+
82
+ The `extract` method now returns `SecApi::ExtractedData` objects instead of raw hashes.
83
+
84
+ **Affected methods:**
85
+ - `client.extractor.extract(filing_url)`
86
+
87
+ ### Migration Example
88
+
89
+ **v0.1.0 - Returns Hash:**
90
+ ```ruby
91
+ client = SecApi::Client.new(api_key: "your_key")
92
+ filing_url = "https://www.sec.gov/Archives/edgar/data/320193/..."
93
+
94
+ extracted = client.extractor.extract(filing_url)
95
+
96
+ # Hash access
97
+ text = extracted["text"]
98
+ sections = extracted["sections"]
99
+ metadata = extracted["metadata"]
100
+ ```
101
+
102
+ **v1.0.0 - Returns ExtractedData Object:**
103
+ ```ruby
104
+ client = SecApi::Client.new(api_key: "your_key")
105
+ filing_url = "https://www.sec.gov/Archives/edgar/data/320193/..."
106
+
107
+ extracted = client.extractor.extract(filing_url)
108
+
109
+ # Method access (typed attributes)
110
+ text = extracted.text # => "Full extracted text..."
111
+ sections = extracted.sections # => { risk_factors: "...", financials: "..." }
112
+ metadata = extracted.metadata # => { source_url: "...", form_type: "10-K" }
113
+
114
+ # Hash access NO LONGER WORKS
115
+ extracted["text"] # => NoMethodError: undefined method `[]' for SecApi::ExtractedData
116
+ ```
117
+
118
+ ### Migration Steps
119
+
120
+ 1. **Replace hash bracket notation with method calls:**
121
+ ```ruby
122
+ # BEFORE
123
+ extracted["text"]
124
+ extracted["sections"]["risk_factors"]
125
+
126
+ # AFTER
127
+ extracted.text
128
+ extracted.sections[:risk_factors] # Note: sections keys are symbols in v1.0.0
129
+ ```
130
+
131
+ 2. **Update type checks:**
132
+ ```ruby
133
+ # BEFORE
134
+ if extracted.is_a?(Hash)
135
+
136
+ # AFTER
137
+ if extracted.is_a?(SecApi::ExtractedData)
138
+ ```
139
+
140
+ 3. **Handle optional attributes:**
141
+ ```ruby
142
+ # All attributes are optional (may be nil)
143
+ if extracted.text
144
+ process_text(extracted.text)
145
+ end
146
+
147
+ if extracted.sections
148
+ risk_factors = extracted.sections[:risk_factors]
149
+ end
150
+ ```
151
+
152
+ ## Breaking Change #3: FulltextResults is Enumerable
153
+
154
+ ### What Changed
155
+
156
+ `SecApi::Collections::FulltextResults` now includes `Enumerable`, allowing direct iteration without calling `.fulltext_results` first.
157
+
158
+ ### Migration Example
159
+
160
+ **v0.1.0 - Not Enumerable:**
161
+ ```ruby
162
+ results = client.query.fulltext("merger acquisition")
163
+
164
+ # Must use .fulltext_results accessor
165
+ results.fulltext_results.each { |r| puts r.ticker }
166
+ results.fulltext_results.map(&:ticker)
167
+ results.fulltext_results.select { |r| r.form_type == "8-K" }
168
+ ```
169
+
170
+ **v1.0.0 - Enumerable:**
171
+ ```ruby
172
+ results = client.query.fulltext("merger acquisition")
173
+
174
+ # Direct iteration (Enumerable)
175
+ results.each { |r| puts r.ticker }
176
+ results.map(&:ticker)
177
+ results.select { |r| r.form_type == "8-K" }
178
+
179
+ # .fulltext_results accessor still works for backward compatibility
180
+ results.fulltext_results.each { |r| puts r.ticker }
181
+ ```
182
+
183
+ ### Migration Steps
184
+
185
+ 1. **Simplify iteration (optional):**
186
+ ```ruby
187
+ # BEFORE (still works)
188
+ results.fulltext_results.each { |r| ... }
189
+
190
+ # AFTER (cleaner)
191
+ results.each { |r| ... }
192
+ ```
193
+
194
+ 2. **Use Enumerable methods directly:**
195
+ ```ruby
196
+ # BEFORE
197
+ results.fulltext_results.count
198
+ results.fulltext_results.first(10)
199
+
200
+ # AFTER
201
+ results.count
202
+ results.first(10)
203
+ ```
204
+
205
+ ## Thread Safety Improvements
206
+
207
+ All response objects in v1.0.0 are **immutable and thread-safe**:
208
+
209
+ ```ruby
210
+ # Safe for concurrent usage (Sidekiq, background jobs, etc.)
211
+ entity = client.mapping.ticker("AAPL")
212
+
213
+ threads = 10.times.map do
214
+ Thread.new do
215
+ 100.times { puts entity.cik }
216
+ end
217
+ end
218
+
219
+ threads.each(&:join) # No race conditions
220
+ ```
221
+
222
+ ## IDE Autocomplete Benefits
223
+
224
+ With typed objects, your IDE can now provide accurate autocomplete:
225
+
226
+ ```ruby
227
+ entity = client.mapping.ticker("AAPL")
228
+ entity. # IDE shows: cik, ticker, name, exchange, sic, etc.
229
+
230
+ extracted = client.extractor.extract(filing_url)
231
+ extracted. # IDE shows: text, sections, metadata
232
+ ```
233
+
234
+ ## Quick Reference: API Changes
235
+
236
+ | Endpoint | v0.1.0 Return Type | v1.0.0 Return Type | Migration |
237
+ |----------|-------------------|-------------------|-----------|
238
+ | `mapping.ticker()` | `Hash` | `Entity` | Use `.cik` instead of `["cik"]` |
239
+ | `mapping.cik()` | `Hash` | `Entity` | Use `.ticker` instead of `["ticker"]` |
240
+ | `mapping.cusip()` | `Hash` | `Entity` | Use `.name` instead of `["name"]` |
241
+ | `mapping.name()` | `Hash` | `Entity` | Use `.cik` instead of `["cik"]` |
242
+ | `extractor.extract()` | `Hash` | `ExtractedData` | Use `.text` instead of `["text"]` |
243
+ | `query.fulltext()` | `FulltextResults` (not Enumerable) | `FulltextResults` (Enumerable) | Use `.each` directly |
244
+
245
+ ## Testing Your Migration
246
+
247
+ After migrating, verify your code works:
248
+
249
+ ```ruby
250
+ # Test mapping endpoints
251
+ entity = client.mapping.ticker("AAPL")
252
+ raise unless entity.is_a?(SecApi::Entity)
253
+ raise unless entity.cik == "0000320193"
254
+
255
+ # Test extractor endpoint
256
+ extracted = client.extractor.extract(filing_url)
257
+ raise unless extracted.is_a?(SecApi::ExtractedData)
258
+ raise unless extracted.text.is_a?(String) || extracted.text.nil?
259
+
260
+ # Test collections
261
+ results = client.query.fulltext("acquisition")
262
+ raise unless results.respond_to?(:each)
263
+ raise unless results.first.is_a?(SecApi::FulltextResult)
264
+ ```
265
+
266
+ ## Need Help?
267
+
268
+ - **GitHub Issues:** https://github.com/your-org/sec_api/issues
269
+ - **Documentation:** See README.md and inline YARD docs
270
+ - **Breaking Changes:** This migration guide documents all breaking changes
271
+
272
+ ## Summary
273
+
274
+ v1.0.0 brings production-grade thread safety and a consistent, typed API surface. While hash access is no longer supported, the migration is straightforward: replace `["key"]` with `.key` for all mapping and extractor responses.
data/README.md ADDED
@@ -0,0 +1,370 @@
1
+ # sec_api
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/sec_api.svg)](https://badge.fury.io/rb/sec_api)
4
+ [![CI](https://github.com/ljuti/sec_api/actions/workflows/main.yml/badge.svg)](https://github.com/ljuti/sec_api/actions/workflows/main.yml)
5
+ [![Ruby](https://img.shields.io/badge/ruby-3.1%2B-ruby.svg)](https://www.ruby-lang.org)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
7
+ [![Documentation](https://img.shields.io/badge/docs-YARD-blue.svg)](https://rubydoc.info/gems/sec_api)
8
+
9
+ Production-grade Ruby client for accessing SEC EDGAR filings through the [sec-api.io](https://sec-api.io) API. Query, search, and extract structured financial data from 18+ million SEC filings with automatic retry, rate limiting, and comprehensive error handling.
10
+
11
+ ## Features
12
+
13
+ - **Query Builder DSL** - Fluent, chainable interface for searching filings by ticker, CIK, form type, date range, and full-text keywords
14
+ - **Automatic Pagination** - Memory-efficient lazy enumeration through large result sets with `auto_paginate`
15
+ - **Entity Mapping** - Resolve tickers, CIKs, CUSIPs, and company names to entity records
16
+ - **XBRL Extraction** - Extract structured financial data from US GAAP and IFRS filings
17
+ - **Real-Time Streaming** - WebSocket notifications for new filings with <2 minute latency
18
+ - **Intelligent Rate Limiting** - Proactive throttling and request queueing to maximize throughput
19
+ - **Production Error Handling** - TransientError/PermanentError hierarchy with automatic retry
20
+ - **Observability Hooks** - Instrumentation callbacks for logging, metrics, and distributed tracing
21
+
22
+ ## Installation
23
+
24
+ Add to your Gemfile:
25
+
26
+ ```ruby
27
+ gem 'sec_api'
28
+ ```
29
+
30
+ Or install directly:
31
+
32
+ ```bash
33
+ gem install sec_api
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Configuration
39
+
40
+ Set your API key via environment variable:
41
+
42
+ ```bash
43
+ export SECAPI_API_KEY=your_api_key_here
44
+ ```
45
+
46
+ Get your API key from [sec-api.io](https://sec-api.io).
47
+
48
+ Alternatively, create `config/secapi.yml`:
49
+
50
+ ```yaml
51
+ api_key: <%= ENV['SECAPI_API_KEY'] %>
52
+ ```
53
+
54
+ ### Basic Usage
55
+
56
+ ```ruby
57
+ require 'sec_api'
58
+
59
+ # Initialize client (auto-loads configuration)
60
+ client = SecApi::Client.new
61
+
62
+ # Query filings by ticker
63
+ filings = client.query.ticker("AAPL").form_type("10-K").search
64
+ filings.each do |filing|
65
+ puts "#{filing.form_type} filed on #{filing.filed_at}"
66
+ end
67
+ ```
68
+
69
+ ## Usage Examples
70
+
71
+ ### Query Builder
72
+
73
+ ```ruby
74
+ # Simple ticker query
75
+ filings = client.query
76
+ .ticker("AAPL")
77
+ .form_type("10-K")
78
+ .search
79
+
80
+ # Multiple tickers and form types with date range
81
+ filings = client.query
82
+ .ticker("AAPL", "TSLA", "GOOGL")
83
+ .form_type("10-K", "10-Q", "8-K")
84
+ .date_range(from: "2020-01-01", to: Date.today)
85
+ .limit(100)
86
+ .search
87
+
88
+ # Full-text search
89
+ filings = client.query
90
+ .ticker("META")
91
+ .search_text("artificial intelligence")
92
+ .search
93
+ ```
94
+
95
+ ### Automatic Pagination
96
+
97
+ ```ruby
98
+ # Manual pagination
99
+ filings = client.query.ticker("AAPL").search
100
+ next_page = filings.fetch_next_page if filings.has_more?
101
+
102
+ # Automatic pagination for backfills (memory-efficient)
103
+ client.query
104
+ .ticker("AAPL")
105
+ .date_range(from: "2015-01-01", to: Date.today)
106
+ .auto_paginate
107
+ .each do |filing|
108
+ # Process thousands of filings with constant memory usage
109
+ process_filing(filing)
110
+ end
111
+ ```
112
+
113
+ ### Entity Mapping
114
+
115
+ ```ruby
116
+ # Ticker to entity
117
+ entity = client.mapping.ticker("AAPL")
118
+ puts "CIK: #{entity.cik}, Name: #{entity.name}"
119
+
120
+ # CIK to entity
121
+ entity = client.mapping.cik("0000320193")
122
+
123
+ # CUSIP lookup
124
+ entity = client.mapping.cusip("037833100")
125
+ ```
126
+
127
+ ### XBRL Data Extraction
128
+
129
+ ```ruby
130
+ # Extract XBRL data from a filing
131
+ xbrl_data = client.xbrl.to_json(filing.xbrl_url)
132
+
133
+ # Access financial data by US GAAP element names
134
+ revenue = xbrl_data.statements_of_income["RevenueFromContractWithCustomerExcludingAssessedTax"]
135
+ assets = xbrl_data.balance_sheets["Assets"]
136
+
137
+ # Discover available elements
138
+ xbrl_data.element_names # => ["Assets", "Revenue", ...]
139
+ xbrl_data.taxonomy_hint # => :us_gaap or :ifrs
140
+ ```
141
+
142
+ ### Real-Time Streaming
143
+
144
+ ```ruby
145
+ # Subscribe to filtered filings
146
+ client.stream.subscribe(
147
+ tickers: ["AAPL", "TSLA"],
148
+ form_types: ["10-K", "8-K"]
149
+ ) do |filing|
150
+ puts "New filing: #{filing.ticker} - #{filing.form_type}"
151
+ ProcessFilingJob.perform_async(filing.accession_no)
152
+ end
153
+ ```
154
+
155
+ ### Error Handling
156
+
157
+ ```ruby
158
+ begin
159
+ filings = client.query.ticker("AAPL").search
160
+ rescue SecApi::RateLimitError => e
161
+ # Automatically retried with exponential backoff
162
+ # Only raised after max retries exhausted
163
+ puts "Rate limited: retry after #{e.retry_after}s"
164
+ rescue SecApi::AuthenticationError => e
165
+ # Permanent error - fix API key
166
+ puts "Auth failed: #{e.message}"
167
+ rescue SecApi::TransientError => e
168
+ # Network or server error - safe to retry
169
+ retry
170
+ rescue SecApi::PermanentError => e
171
+ # Don't retry - fix the request
172
+ puts "Permanent error: #{e.message}"
173
+ end
174
+ ```
175
+
176
+ ### Observability
177
+
178
+ ```ruby
179
+ # Configure instrumentation callbacks
180
+ config = SecApi::Config.new(
181
+ api_key: ENV.fetch("SECAPI_API_KEY"),
182
+
183
+ on_request: ->(request_id:, method:, url:, headers:) {
184
+ Rails.logger.info("SEC API Request", request_id: request_id, url: url)
185
+ },
186
+
187
+ on_response: ->(request_id:, status:, duration_ms:, url:, method:) {
188
+ StatsD.histogram("sec_api.request.duration_ms", duration_ms)
189
+ },
190
+
191
+ on_error: ->(request_id:, error:, url:, method:) {
192
+ Bugsnag.notify(error)
193
+ }
194
+ )
195
+
196
+ client = SecApi::Client.new(config)
197
+
198
+ # Or use automatic structured logging
199
+ client = SecApi::Client.new(
200
+ api_key: ENV.fetch("SECAPI_API_KEY"),
201
+ logger: Rails.logger,
202
+ default_logging: true
203
+ )
204
+ ```
205
+
206
+ ## Architecture
207
+
208
+ ### Client Proxy Pattern
209
+
210
+ ```ruby
211
+ SecApi::Client
212
+ ├── .query # Query API proxy (fluent search builder)
213
+ ├── .mapping # Mapping API proxy (ticker/CIK resolution)
214
+ ├── .extractor # Extractor API proxy (document extraction)
215
+ ├── .xbrl # XBRL API proxy (financial data)
216
+ └── .stream # Stream API proxy (WebSocket notifications)
217
+ ```
218
+
219
+ ### Exception Hierarchy
220
+
221
+ ```
222
+ SecApi::Error (base)
223
+ ├── TransientError (automatic retry)
224
+ │ ├── RateLimitError (429)
225
+ │ ├── ServerError (5xx)
226
+ │ └── NetworkError
227
+ └── PermanentError (fail immediately)
228
+ ├── AuthenticationError (401/403)
229
+ ├── NotFoundError (404)
230
+ ├── ValidationError (400/422)
231
+ └── ConfigurationError
232
+ ```
233
+
234
+ ### Middleware Stack
235
+
236
+ ```
237
+ Request → Instrumentation → Retry → RateLimiter → ErrorHandler → Adapter → sec-api.io
238
+ ```
239
+
240
+ ## Configuration Options
241
+
242
+ All options can be set via YAML or environment variables:
243
+
244
+ | Option | Env Variable | Default | Description |
245
+ |--------|--------------|---------|-------------|
246
+ | `api_key` | `SECAPI_API_KEY` | _(required)_ | Your sec-api.io API key |
247
+ | `base_url` | `SECAPI_BASE_URL` | `https://api.sec-api.io` | API base URL |
248
+ | `retry_max_attempts` | `SECAPI_RETRY_MAX_ATTEMPTS` | `5` | Maximum retry attempts |
249
+ | `retry_initial_delay` | `SECAPI_RETRY_INITIAL_DELAY` | `1.0` | Initial retry delay (seconds) |
250
+ | `retry_max_delay` | `SECAPI_RETRY_MAX_DELAY` | `60` | Maximum retry delay (seconds) |
251
+ | `request_timeout` | `SECAPI_REQUEST_TIMEOUT` | `30` | HTTP request timeout (seconds) |
252
+ | `rate_limit_threshold` | `SECAPI_RATE_LIMIT_THRESHOLD` | `0.1` | Throttle when <10% quota remains |
253
+ | `default_logging` | - | `false` | Enable automatic structured logging |
254
+ | `metrics_backend` | - | `nil` | StatsD-compatible metrics backend |
255
+
256
+ ## Requirements
257
+
258
+ - **Ruby:** 3.1.0 or higher
259
+ - **Dependencies:**
260
+ - `faraday` - HTTP client
261
+ - `faraday-retry` - Automatic retry middleware
262
+ - `anyway_config` - Configuration management
263
+ - `dry-struct` - Immutable value objects
264
+ - `faye-websocket` - WebSocket client for streaming
265
+ - `eventmachine` - Event-driven I/O
266
+
267
+ ## Documentation
268
+
269
+ ### API Reference
270
+
271
+ Generate YARD documentation:
272
+
273
+ ```bash
274
+ bundle exec yard doc
275
+ open doc/index.html
276
+ ```
277
+
278
+ ### Usage Examples
279
+
280
+ See working examples in `docs/examples/`:
281
+
282
+ | File | Description |
283
+ |------|-------------|
284
+ | [query_builder.rb](docs/examples/query_builder.rb) | Query by ticker, CIK, form type, date range |
285
+ | [backfill_filings.rb](docs/examples/backfill_filings.rb) | Multi-year backfill with auto-pagination |
286
+ | [streaming_notifications.rb](docs/examples/streaming_notifications.rb) | Real-time WebSocket notifications |
287
+ | [instrumentation.rb](docs/examples/instrumentation.rb) | Logging, metrics, and observability |
288
+
289
+ ### Migration Guide
290
+
291
+ Upgrading from v0.1.0? See the [Migration Guide](docs/migration-guide-v1.md) for breaking changes and upgrade instructions.
292
+
293
+ ### Architecture Documentation
294
+
295
+ - [Product Requirements](_bmad-output/planning-artifacts/prd.md) - Complete requirements
296
+ - [Architecture](_bmad-output/planning-artifacts/architecture.md) - Technical decisions
297
+ - [Epics & Stories](_bmad-output/planning-artifacts/epics.md) - Implementation roadmap
298
+
299
+ ## Development
300
+
301
+ ### Setup
302
+
303
+ ```bash
304
+ git clone https://github.com/ljuti/sec_api.git
305
+ cd sec_api
306
+ bin/setup
307
+ ```
308
+
309
+ ### Testing
310
+
311
+ ```bash
312
+ bundle exec rspec # Run tests
313
+ bundle exec standardrb # Run linter
314
+ bundle exec rake # Run both
315
+ ```
316
+
317
+ ### Interactive Console
318
+
319
+ ```bash
320
+ bin/console
321
+ ```
322
+
323
+ ## Roadmap
324
+
325
+ ### v0.1.0
326
+ - ✅ Basic query, search, mapping, extractor endpoints
327
+ - ✅ Configuration via anyway_config
328
+ - ✅ Immutable value objects (Dry::Struct)
329
+
330
+ ### v1.0.0
331
+ - ✅ Production-grade error handling with TransientError/PermanentError
332
+ - ✅ Fluent query builder DSL
333
+ - ✅ Automatic pagination with lazy enumeration
334
+ - ✅ XBRL extraction with taxonomy detection
335
+ - ✅ Real-time streaming API (WebSocket)
336
+ - ✅ Intelligent rate limiting with proactive throttling
337
+ - ✅ Observability hooks (instrumentation callbacks)
338
+ - ✅ Structured logging and metrics integration
339
+ - ✅ 100% YARD documentation coverage
340
+ - ✅ Migration guide from v0.1.0
341
+
342
+ ## Contributing
343
+
344
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ljuti/sec_api.
345
+
346
+ 1. Fork the repository
347
+ 2. Create your feature branch (`git checkout -b feature/my-feature`)
348
+ 3. Write tests for your changes
349
+ 4. Ensure tests pass (`bundle exec rspec && bundle exec standardrb`)
350
+ 5. Commit your changes (`git commit -am 'Add new feature'`)
351
+ 6. Push to the branch (`git push origin feature/my-feature`)
352
+ 7. Create a Pull Request
353
+
354
+ ## License
355
+
356
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
357
+
358
+ ## Support
359
+
360
+ - **GitHub Issues:** https://github.com/ljuti/sec_api/issues
361
+ - **Author:** Lauri Jutila
362
+ - **Email:** git@laurijutila.com
363
+
364
+ ## Acknowledgments
365
+
366
+ This gem interacts with the [sec-api.io](https://sec-api.io) API. You'll need an API key from sec-api.io to use this gem.
367
+
368
+ ---
369
+
370
+ **Status:** v1.0.0 released
data/Rakefile ADDED
@@ -0,0 +1,10 @@
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 "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,57 @@
1
+ # SecAPI Configuration Example
2
+ # Copy this file to config/secapi.yml or config/secapi.local.yml
3
+ # and configure your API credentials
4
+
5
+ # REQUIRED: Your SEC API key
6
+ # Get your API key from https://sec-api.io
7
+ # Can also be set via SECAPI_API_KEY environment variable
8
+ api_key: <%= ENV.fetch('SECAPI_API_KEY', 'your_api_key_here') %>
9
+
10
+ # OPTIONAL: Base URL for SEC API endpoints
11
+ # Default: https://api.sec-api.io
12
+ # Override for testing environments or sandbox APIs
13
+ # Can also be set via SECAPI_BASE_URL environment variable
14
+ # base_url: https://api.sec-api.io
15
+
16
+ # OPTIONAL: Retry Configuration
17
+ # Maximum number of retry attempts for failed requests
18
+ # Default: 5
19
+ # retry_max_attempts: 5
20
+
21
+ # Initial delay in seconds before first retry
22
+ # Default: 1.0
23
+ # retry_initial_delay: 1.0
24
+
25
+ # Maximum delay in seconds between retries (exponential backoff cap)
26
+ # Default: 60.0
27
+ # retry_max_delay: 60.0
28
+
29
+ # OPTIONAL: Timeout Configuration
30
+ # Request timeout in seconds
31
+ # Default: 30
32
+ # request_timeout: 30
33
+
34
+ # OPTIONAL: Rate Limiting Configuration
35
+ # Threshold (0.0-1.0) for proactive throttling
36
+ # Default: 0.1 (throttle when less than 10% quota remaining)
37
+ # rate_limit_threshold: 0.1
38
+
39
+ # PRODUCTION DEPLOYMENT EXAMPLE
40
+ # Using environment variables (recommended for production):
41
+ #
42
+ # api_key: <%= ENV.fetch('SECAPI_API_KEY') %>
43
+ # base_url: <%= ENV.fetch('SECAPI_BASE_URL', 'https://api.sec-api.io') %>
44
+ # retry_max_attempts: <%= ENV.fetch('SECAPI_RETRY_MAX_ATTEMPTS', 5).to_i %>
45
+ # request_timeout: <%= ENV.fetch('SECAPI_REQUEST_TIMEOUT', 30).to_i %>
46
+
47
+ # TESTING ENVIRONMENT EXAMPLE
48
+ # Override base_url to point to test server:
49
+ #
50
+ # base_url: https://test.sec-api.example.com
51
+ # api_key: test_api_key_12345
52
+
53
+ # LOCAL DEVELOPMENT
54
+ # For local overrides without committing credentials:
55
+ # 1. Copy this file to config/secapi.local.yml
56
+ # 2. Add config/secapi.local.yml to .gitignore (already done)
57
+ # 3. Configure your local API key in secapi.local.yml