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.
- checksums.yaml +7 -0
- data/.devcontainer/Dockerfile +54 -0
- data/.devcontainer/README.md +178 -0
- data/.devcontainer/devcontainer.json +46 -0
- data/.devcontainer/docker-compose.yml +28 -0
- data/.devcontainer/post-create.sh +51 -0
- data/.devcontainer/post-start.sh +44 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +0 -0
- data/LICENSE.txt +21 -0
- data/MIGRATION.md +274 -0
- data/README.md +370 -0
- data/Rakefile +10 -0
- data/config/secapi.yml.example +57 -0
- data/docs/development-guide.md +291 -0
- data/docs/enumerator_pattern_design.md +483 -0
- data/docs/examples/README.md +58 -0
- data/docs/examples/backfill_filings.rb +419 -0
- data/docs/examples/instrumentation.rb +583 -0
- data/docs/examples/query_builder.rb +308 -0
- data/docs/examples/streaming_notifications.rb +491 -0
- data/docs/index.md +244 -0
- data/docs/migration-guide-v1.md +1091 -0
- data/docs/pre-review-checklist.md +145 -0
- data/docs/project-overview.md +90 -0
- data/docs/project-scan-report.json +60 -0
- data/docs/source-tree-analysis.md +190 -0
- data/lib/sec_api/callback_helper.rb +49 -0
- data/lib/sec_api/client.rb +606 -0
- data/lib/sec_api/collections/filings.rb +267 -0
- data/lib/sec_api/collections/fulltext_results.rb +86 -0
- data/lib/sec_api/config.rb +590 -0
- data/lib/sec_api/deep_freezable.rb +42 -0
- data/lib/sec_api/errors/authentication_error.rb +24 -0
- data/lib/sec_api/errors/configuration_error.rb +5 -0
- data/lib/sec_api/errors/error.rb +75 -0
- data/lib/sec_api/errors/network_error.rb +26 -0
- data/lib/sec_api/errors/not_found_error.rb +23 -0
- data/lib/sec_api/errors/pagination_error.rb +28 -0
- data/lib/sec_api/errors/permanent_error.rb +29 -0
- data/lib/sec_api/errors/rate_limit_error.rb +57 -0
- data/lib/sec_api/errors/reconnection_error.rb +34 -0
- data/lib/sec_api/errors/server_error.rb +25 -0
- data/lib/sec_api/errors/transient_error.rb +28 -0
- data/lib/sec_api/errors/validation_error.rb +23 -0
- data/lib/sec_api/extractor.rb +122 -0
- data/lib/sec_api/filing_journey.rb +477 -0
- data/lib/sec_api/mapping.rb +125 -0
- data/lib/sec_api/metrics_collector.rb +411 -0
- data/lib/sec_api/middleware/error_handler.rb +250 -0
- data/lib/sec_api/middleware/instrumentation.rb +186 -0
- data/lib/sec_api/middleware/rate_limiter.rb +541 -0
- data/lib/sec_api/objects/data_file.rb +34 -0
- data/lib/sec_api/objects/document_format_file.rb +45 -0
- data/lib/sec_api/objects/entity.rb +92 -0
- data/lib/sec_api/objects/extracted_data.rb +118 -0
- data/lib/sec_api/objects/fact.rb +147 -0
- data/lib/sec_api/objects/filing.rb +197 -0
- data/lib/sec_api/objects/fulltext_result.rb +66 -0
- data/lib/sec_api/objects/period.rb +96 -0
- data/lib/sec_api/objects/stream_filing.rb +194 -0
- data/lib/sec_api/objects/xbrl_data.rb +356 -0
- data/lib/sec_api/query.rb +423 -0
- data/lib/sec_api/rate_limit_state.rb +130 -0
- data/lib/sec_api/rate_limit_tracker.rb +154 -0
- data/lib/sec_api/stream.rb +841 -0
- data/lib/sec_api/structured_logger.rb +199 -0
- data/lib/sec_api/types.rb +32 -0
- data/lib/sec_api/version.rb +42 -0
- data/lib/sec_api/xbrl.rb +220 -0
- data/lib/sec_api.rb +137 -0
- data/sig/sec_api.rbs +4 -0
- 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
|
+
[](https://badge.fury.io/rb/sec_api)
|
|
4
|
+
[](https://github.com/ljuti/sec_api/actions/workflows/main.yml)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](LICENSE.txt)
|
|
7
|
+
[](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,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
|