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
@@ -0,0 +1,1091 @@
1
+ # Migration Guide: v0.1.0 to v1.0.0
2
+
3
+ > **Version:** v1.0.0
4
+ > **Upgrade From:** v0.1.0
5
+ > **Last Updated:** 2026-01-13
6
+
7
+ This guide covers all breaking changes and new features when upgrading from sec_api v0.1.0 to v1.0.0. Follow this guide to ensure a smooth migration.
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Executive Summary](#executive-summary)
12
+ 2. [Quick Migration Checklist](#quick-migration-checklist)
13
+ 3. [Breaking Changes](#breaking-changes)
14
+ - [Response Type Changes](#response-type-changes)
15
+ - [Exception Hierarchy Changes](#exception-hierarchy-changes)
16
+ - [Query Builder DSL](#query-builder-dsl)
17
+ - [XBRL Endpoint Availability](#xbrl-endpoint-availability)
18
+ 4. [New Features in v1.0.0](#new-features-in-v100)
19
+ - [Rate Limiting Intelligence](#rate-limiting-intelligence)
20
+ - [Retry Middleware with Exponential Backoff](#retry-middleware-with-exponential-backoff)
21
+ - [WebSocket Streaming API](#websocket-streaming-api)
22
+ - [Observability Hooks](#observability-hooks)
23
+ - [Structured Logging](#structured-logging)
24
+ - [Metrics Exposure](#metrics-exposure)
25
+ - [Filing Journey Tracking](#filing-journey-tracking)
26
+ - [Automatic Pagination](#automatic-pagination)
27
+ 5. [Configuration Changes](#configuration-changes)
28
+ 6. [Deprecations](#deprecations)
29
+ 7. [Troubleshooting](#troubleshooting)
30
+
31
+ ---
32
+
33
+ ## Executive Summary
34
+
35
+ **sec_api v1.0.0** is a production-grade release with significant improvements over v0.1.0:
36
+
37
+ - **Type Safety:** All API responses now return immutable, thread-safe Dry::Struct value objects instead of raw hashes
38
+ - **Error Handling:** Typed exception hierarchy with automatic retry for transient failures
39
+ - **Query Builder:** Fluent DSL replaces raw query string construction
40
+ - **Observability:** Built-in instrumentation, structured logging, and metrics support
41
+ - **Real-Time:** WebSocket streaming API for filing notifications
42
+ - **Resilience:** Intelligent rate limiting and exponential backoff retry logic
43
+
44
+ **Why Upgrade?**
45
+
46
+ - 95%+ automatic recovery from transient API failures
47
+ - Thread-safe for concurrent usage (Sidekiq, background jobs)
48
+ - Zero-tolerance error handling prevents silent data loss
49
+ - Production monitoring integration via metrics and callbacks
50
+
51
+ ---
52
+
53
+ ## Quick Migration Checklist
54
+
55
+ Use this checklist to verify your migration is complete:
56
+
57
+ ### Required Changes
58
+
59
+ - [ ] **Update gem version** in Gemfile: `gem "sec_api", "~> 1.0"`
60
+ - [ ] **Run `bundle update sec_api`** to install v1.0.0
61
+ - [ ] **Replace hash access with method calls** in all API response handling:
62
+ - `result["ticker"]` → `result.ticker`
63
+ - `result["formType"]` → `result.form_type`
64
+ - `result["filedAt"]` → `result.filed_at`
65
+ - [ ] **Update exception handling** to use new typed exceptions:
66
+ - Generic `rescue => e` → specific `rescue SecApi::RateLimitError`
67
+ - Add handling for `TransientError` vs `PermanentError`
68
+ - [ ] **Migrate raw Lucene queries** to Query Builder DSL:
69
+ - `search(query: 'ticker:AAPL')` → `.ticker("AAPL").search`
70
+ - [ ] **Update date range calls** to use keyword arguments:
71
+ - `.date_range("2020-01-01", "2023-12-31")` → `.date_range(from: "2020-01-01", to: "2023-12-31")`
72
+
73
+ ### Recommended Additions
74
+
75
+ - [ ] **Configure retry settings** if defaults don't suit your use case:
76
+ ```ruby
77
+ config = SecApi::Config.new(
78
+ api_key: ENV["SEC_API_KEY"],
79
+ retry_max_attempts: 5,
80
+ retry_initial_delay: 1.0
81
+ )
82
+ ```
83
+ - [ ] **Set up structured logging** for production monitoring:
84
+ ```ruby
85
+ config = SecApi::Config.new(
86
+ api_key: ENV["SEC_API_KEY"],
87
+ logger: Rails.logger,
88
+ default_logging: true
89
+ )
90
+ ```
91
+ - [ ] **Add error tracking callback** for alerting:
92
+ ```ruby
93
+ on_error: ->(request_id:, error:, url:, method:) {
94
+ Bugsnag.notify(error)
95
+ }
96
+ ```
97
+ - [ ] **Configure metrics backend** if using StatsD/Datadog:
98
+ ```ruby
99
+ metrics_backend: StatsD.new('localhost', 8125)
100
+ ```
101
+
102
+ ### Testing Your Migration
103
+
104
+ - [ ] **Run your test suite** - existing tests should pass with response type updates
105
+ - [ ] **Verify error handling** - test with invalid API key, network failures
106
+ - [ ] **Check query results** - ensure typed objects work with your code
107
+ - [ ] **Test XBRL extraction** - verify `client.xbrl.to_json` works
108
+
109
+ ### Optional: New Features to Enable
110
+
111
+ - [ ] **WebSocket streaming** for real-time filing notifications
112
+ - [ ] **Auto-pagination** with `.auto_paginate` for large result sets
113
+ - [ ] **Filing journey tracking** for end-to-end observability
114
+
115
+ ---
116
+
117
+ ## Breaking Changes
118
+
119
+ ### Response Type Changes
120
+
121
+ **Impact:** All API methods now return typed value objects instead of raw hashes.
122
+
123
+ All API responses are now wrapped in immutable [Dry::Struct](https://dry-rb.org/gems/dry-struct/) value objects. This provides type safety, thread safety for concurrent access, and a clean method-based interface.
124
+
125
+ #### Query Results: Filings Collection
126
+
127
+ **v0.1.0 (OLD):**
128
+ ```ruby
129
+ result = client.query.search(query: 'ticker:AAPL AND formType:"10-K"')
130
+ result.each do |hash|
131
+ puts hash["accessionNo"] # Hash access with string keys
132
+ puts hash["formType"]
133
+ puts hash["filedAt"]
134
+ end
135
+ ```
136
+
137
+ **v1.0.0 (NEW):**
138
+ ```ruby
139
+ result = client.query.ticker("AAPL").form_type("10-K").search
140
+ result.each do |filing|
141
+ puts filing.accession_no # Method calls (snake_case)
142
+ puts filing.form_type
143
+ puts filing.filed_at # Returns Date object
144
+ end
145
+ ```
146
+
147
+ **Key Changes:**
148
+ - `result` is now a `SecApi::Collections::Filings` object (includes Enumerable)
149
+ - Each item is a `SecApi::Objects::Filing` struct
150
+ - Use method calls instead of hash access: `filing.ticker` not `hash["ticker"]`
151
+ - Date fields return Ruby `Date` objects instead of strings
152
+ - All objects are frozen and thread-safe
153
+
154
+ #### Filing Object Attributes
155
+
156
+ | v0.1.0 Hash Key | v1.0.0 Method | Type |
157
+ |-----------------|---------------|------|
158
+ | `hash["ticker"]` | `filing.ticker` | String |
159
+ | `hash["cik"]` | `filing.cik` | String |
160
+ | `hash["formType"]` | `filing.form_type` | String |
161
+ | `hash["filedAt"]` | `filing.filed_at` | Date |
162
+ | `hash["accessionNo"]` | `filing.accession_no` | String |
163
+ | `hash["companyName"]` | `filing.company_name` | String |
164
+ | `hash["linkToHtml"]` | `filing.html_url` | String |
165
+ | `hash["linkToTxt"]` | `filing.txt_url` | String |
166
+ | `hash["entities"]` | `filing.entities` | Array<Entity> |
167
+ | `hash["documentFormatFiles"]` | `filing.documents` | Array<DocumentFormatFile> |
168
+ | `hash["dataFiles"]` | `filing.data_files` | Array<DataFile> |
169
+
170
+ #### Mapping Results: Entity Object
171
+
172
+ **v0.1.0 (OLD):**
173
+ ```ruby
174
+ result = client.mapping.resolve_ticker("AAPL")
175
+ puts result["cik"]
176
+ puts result["companyName"]
177
+ ```
178
+
179
+ **v1.0.0 (NEW):**
180
+ ```ruby
181
+ entity = client.mapping.ticker("AAPL")
182
+ puts entity.cik # => "0000320193"
183
+ puts entity.name # => "Apple Inc."
184
+ puts entity.ticker # => "AAPL"
185
+ puts entity.exchange # => "NASDAQ"
186
+ ```
187
+
188
+ **Key Changes:**
189
+ - Returns `SecApi::Objects::Entity` struct
190
+ - Method names use snake_case: `company_name` → `name`
191
+ - All attributes accessible via methods
192
+
193
+ #### Extractor Results: ExtractedData Object
194
+
195
+ **v0.1.0 (OLD):**
196
+ ```ruby
197
+ result = client.extractor.extract(url, section: "risk_factors")
198
+ puts result["text"]
199
+ ```
200
+
201
+ **v1.0.0 (NEW):**
202
+ ```ruby
203
+ extracted = client.extractor.extract(url, section: "risk_factors")
204
+ puts extracted.text
205
+ puts extracted.sections[:risk_factors] # Section access
206
+ puts extracted.risk_factors # Dynamic method access
207
+ ```
208
+
209
+ **Key Changes:**
210
+ - Returns `SecApi::ExtractedData` struct
211
+ - Sections accessible via hash or dynamic methods
212
+ - Thread-safe with deep-frozen nested structures
213
+
214
+ #### Collection Classes
215
+
216
+ v1.0.0 introduces collection classes that wrap arrays of results:
217
+
218
+ | Endpoint | Collection Class | Item Class |
219
+ |----------|------------------|------------|
220
+ | `client.query.search` | `SecApi::Collections::Filings` | `SecApi::Objects::Filing` |
221
+ | `client.query.fulltext(...)` | `SecApi::Collections::FulltextResults` | `SecApi::Objects::FulltextResult` |
222
+
223
+ **Collection Features:**
224
+ - Implements `Enumerable` for `each`, `map`, `select`, etc.
225
+ - `count` returns total API results (not just current page)
226
+ - `has_more?` checks for additional pages
227
+ - `fetch_next_page` retrieves next page
228
+ - `auto_paginate` returns lazy enumerator for all results
229
+
230
+ ```ruby
231
+ filings = client.query.ticker("AAPL").search
232
+
233
+ # Enumerable methods
234
+ filings.each { |f| puts f.ticker }
235
+ filings.map(&:form_type)
236
+ filings.select { |f| f.form_type == "10-K" }
237
+
238
+ # Pagination metadata
239
+ filings.count # Total across all pages
240
+ filings.has_more? # More pages available?
241
+
242
+ # Fetch all pages lazily
243
+ filings.auto_paginate.each { |f| process(f) }
244
+ ```
245
+
246
+ ### Exception Hierarchy Changes
247
+
248
+ **Impact:** Generic exceptions replaced with typed exception classes that enable automatic retry and precise error handling.
249
+
250
+ v1.0.0 introduces a structured exception hierarchy that distinguishes between retryable (transient) and non-retryable (permanent) errors. The retry middleware automatically retries transient errors, while permanent errors fail immediately.
251
+
252
+ #### Exception Hierarchy Tree
253
+
254
+ ```
255
+ SecApi::Error (base class)
256
+ ├── SecApi::ConfigurationError # Invalid/missing configuration
257
+
258
+ ├── SecApi::TransientError # Auto-retry eligible (temporary failures)
259
+ │ ├── SecApi::RateLimitError # 429 Too Many Requests
260
+ │ ├── SecApi::NetworkError # Timeouts, connection failures, SSL errors
261
+ │ └── SecApi::ServerError # 500-504 Server errors
262
+
263
+ └── SecApi::PermanentError # Fail fast (no retry)
264
+ ├── SecApi::AuthenticationError # 401, 403 Invalid/unauthorized API key
265
+ ├── SecApi::NotFoundError # 404 Resource not found
266
+ └── SecApi::ValidationError # 400, 422 Invalid request parameters
267
+ ```
268
+
269
+ #### Before/After: Rescue Blocks
270
+
271
+ **v0.1.0 (OLD):**
272
+ ```ruby
273
+ begin
274
+ client.query.search(query: "ticker:AAPL")
275
+ rescue => e
276
+ # All errors caught generically - can't distinguish error types
277
+ puts e.message
278
+ # Can't tell if retry might help
279
+ # No request ID for correlation
280
+ end
281
+ ```
282
+
283
+ **v1.0.0 (NEW):**
284
+ ```ruby
285
+ begin
286
+ client.query.ticker("AAPL").search
287
+ rescue SecApi::RateLimitError => e
288
+ # Auto-retry exhausted (5 retries by default)
289
+ logger.warn("Rate limit hit: #{e.message}")
290
+ logger.info("Retry after: #{e.retry_after}s") if e.retry_after
291
+
292
+ rescue SecApi::AuthenticationError => e
293
+ # Permanent: Invalid API key, no retry
294
+ logger.error("Auth failed: #{e.message}")
295
+ notify_developer("Check API key configuration")
296
+
297
+ rescue SecApi::TransientError => e
298
+ # Catch-all for retryable errors (network, server)
299
+ logger.error("Transient failure after retries: #{e.message}")
300
+ schedule_retry_later
301
+
302
+ rescue SecApi::PermanentError => e
303
+ # Catch-all for non-retryable errors (not found, validation)
304
+ logger.error("Permanent failure: #{e.message}")
305
+
306
+ rescue SecApi::ConfigurationError => e
307
+ # Missing API key or invalid configuration
308
+ logger.error("Configuration error: #{e.message}")
309
+ end
310
+ ```
311
+
312
+ #### Automatic Retry Behavior
313
+
314
+ **TransientError subclasses are automatically retried:**
315
+ - Default: 5 retry attempts
316
+ - Exponential backoff with jitter
317
+ - Honors `Retry-After` header when present
318
+
319
+ ```ruby
320
+ # These errors are auto-retried before being raised:
321
+ SecApi::RateLimitError # 429 responses
322
+ SecApi::NetworkError # Timeouts, connection failures
323
+ SecApi::ServerError # 500, 502, 503, 504 responses
324
+ ```
325
+
326
+ **PermanentError subclasses fail immediately:**
327
+ ```ruby
328
+ # These errors are NOT retried:
329
+ SecApi::AuthenticationError # 401, 403 - fix API key
330
+ SecApi::NotFoundError # 404 - resource doesn't exist
331
+ SecApi::ValidationError # 400, 422 - fix request parameters
332
+ ```
333
+
334
+ #### Request Correlation IDs
335
+
336
+ All errors include a `request_id` for tracing through logs and monitoring:
337
+
338
+ ```ruby
339
+ begin
340
+ client.query.ticker("AAPL").search
341
+ rescue SecApi::Error => e
342
+ # Error message includes request_id prefix:
343
+ # "[abc123-def456] Rate limit exceeded (429 Too Many Requests)"
344
+ puts e.message
345
+
346
+ # Access request_id directly for logging/monitoring:
347
+ logger.error("Request failed",
348
+ request_id: e.request_id,
349
+ error_class: e.class.name,
350
+ message: e.message
351
+ )
352
+
353
+ # Send to error tracking service
354
+ Bugsnag.notify(e, metadata: { request_id: e.request_id })
355
+ end
356
+ ```
357
+
358
+ #### RateLimitError Additional Context
359
+
360
+ `RateLimitError` includes retry timing information:
361
+
362
+ ```ruby
363
+ rescue SecApi::RateLimitError => e
364
+ e.retry_after # Seconds to wait (from Retry-After header)
365
+ e.reset_at # Time when rate limit resets (from X-RateLimit-Reset header)
366
+
367
+ if e.reset_at
368
+ wait_time = e.reset_at - Time.now
369
+ sleep(wait_time) if wait_time.positive?
370
+ end
371
+ end
372
+ ```
373
+
374
+ #### Pattern: Comprehensive Error Handling
375
+
376
+ ```ruby
377
+ def fetch_filings(ticker)
378
+ client.query.ticker(ticker).search
379
+ rescue SecApi::ConfigurationError => e
380
+ # Fail fast - cannot proceed without valid configuration
381
+ raise
382
+
383
+ rescue SecApi::AuthenticationError => e
384
+ # Fail fast - invalid API key
385
+ Rails.logger.error("SEC API auth failed", request_id: e.request_id)
386
+ raise
387
+
388
+ rescue SecApi::RateLimitError => e
389
+ # Rate limit exhausted after retries - queue for later
390
+ SecFilingJob.perform_in(e.retry_after || 60, ticker)
391
+ nil
392
+
393
+ rescue SecApi::TransientError => e
394
+ # Network/server issues after retries - queue for retry
395
+ SecFilingJob.perform_in(30, ticker)
396
+ nil
397
+
398
+ rescue SecApi::PermanentError => e
399
+ # Not found or validation error - log and continue
400
+ Rails.logger.warn("Permanent error for #{ticker}", error: e.message)
401
+ nil
402
+ end
403
+ ```
404
+
405
+ ### Query Builder DSL
406
+
407
+ **Impact:** Raw Lucene query strings replaced with fluent, chainable builder methods.
408
+
409
+ v1.0.0 introduces an ActiveRecord-style query builder that provides type-safe, discoverable filtering methods. Each method returns `self` for chaining, with `.search` as the terminal method.
410
+
411
+ #### Before/After: Query Syntax
412
+
413
+ **v0.1.0 (OLD):**
414
+ ```ruby
415
+ # Raw Lucene query string construction
416
+ result = client.query.search(
417
+ query: 'ticker:AAPL AND formType:"10-K"',
418
+ from: "0",
419
+ size: "10"
420
+ )
421
+
422
+ # Error-prone string interpolation
423
+ ticker = "AAPL"
424
+ form = "10-K"
425
+ result = client.query.search(
426
+ query: "ticker:#{ticker} AND formType:\"#{form}\""
427
+ )
428
+ ```
429
+
430
+ **v1.0.0 (NEW):**
431
+ ```ruby
432
+ # Fluent, chainable methods
433
+ result = client.query
434
+ .ticker("AAPL")
435
+ .form_type("10-K")
436
+ .limit(10)
437
+ .search
438
+
439
+ # Type-safe, no string interpolation
440
+ ticker = "AAPL"
441
+ form = "10-K"
442
+ result = client.query
443
+ .ticker(ticker)
444
+ .form_type(form)
445
+ .search
446
+ ```
447
+
448
+ #### Builder Methods Reference
449
+
450
+ | Method | Description | Example |
451
+ |--------|-------------|---------|
452
+ | `.ticker(*tickers)` | Filter by stock ticker(s) | `.ticker("AAPL")` or `.ticker("AAPL", "TSLA")` |
453
+ | `.cik(cik_number)` | Filter by CIK (leading zeros stripped) | `.cik("0000320193")` → `cik:320193` |
454
+ | `.form_type(*types)` | Filter by SEC form type(s) | `.form_type("10-K", "10-Q")` |
455
+ | `.date_range(from:, to:)` | Filter by filing date range | `.date_range(from: "2020-01-01", to: "2023-12-31")` |
456
+ | `.search_text(keywords)` | Full-text search in content | `.search_text("merger acquisition")` |
457
+ | `.limit(count)` | Limit results (default: 50) | `.limit(100)` |
458
+
459
+ #### Terminal Methods
460
+
461
+ | Method | Description | Returns |
462
+ |--------|-------------|---------|
463
+ | `.search` | Execute query, return first page | `SecApi::Collections::Filings` |
464
+ | `.auto_paginate` | Execute query with lazy pagination | `Enumerator::Lazy` |
465
+
466
+ #### Date Range Accepts Multiple Types
467
+
468
+ ```ruby
469
+ # ISO 8601 strings
470
+ .date_range(from: "2020-01-01", to: "2023-12-31")
471
+
472
+ # Ruby Date objects
473
+ .date_range(from: Date.new(2020, 1, 1), to: Date.today)
474
+
475
+ # Time objects (including ActiveSupport::TimeWithZone)
476
+ .date_range(from: 1.year.ago, to: Time.now)
477
+ ```
478
+
479
+ #### Multiple Value Support
480
+
481
+ ```ruby
482
+ # Multiple tickers (OR logic)
483
+ client.query.ticker("AAPL", "TSLA", "MSFT").search
484
+ # → ticker:(AAPL, TSLA, MSFT)
485
+
486
+ # Multiple form types (OR logic)
487
+ client.query.form_type("10-K", "10-Q", "8-K").search
488
+ # → formType:("10-K" OR "10-Q" OR "8-K")
489
+ ```
490
+
491
+ #### International Filing Support
492
+
493
+ International SEC forms work identically to domestic forms:
494
+
495
+ ```ruby
496
+ # Form 20-F: Foreign private issuer annual reports
497
+ client.query.ticker("NMR").form_type("20-F").search
498
+
499
+ # Form 40-F: Canadian issuer annual reports (MJDS)
500
+ client.query.ticker("ABX").form_type("40-F").search
501
+
502
+ # Form 6-K: Foreign private issuer current reports
503
+ client.query.ticker("NMR").form_type("6-K").search
504
+
505
+ # Mix domestic and international
506
+ client.query.form_type("10-K", "20-F", "40-F").search
507
+ ```
508
+
509
+ #### Complex Query Examples
510
+
511
+ ```ruby
512
+ # Multi-year backfill for specific company
513
+ client.query
514
+ .ticker("AAPL")
515
+ .form_type("10-K", "10-Q")
516
+ .date_range(from: "2018-01-01", to: Date.today)
517
+ .auto_paginate
518
+ .each { |filing| process(filing) }
519
+
520
+ # Search for M&A announcements
521
+ client.query
522
+ .form_type("8-K")
523
+ .search_text("merger acquisition")
524
+ .date_range(from: "2023-01-01", to: Date.today)
525
+ .limit(100)
526
+ .search
527
+
528
+ # Debug: Inspect generated Lucene query
529
+ query = client.query.ticker("AAPL").form_type("10-K")
530
+ puts query.to_lucene
531
+ # → "ticker:AAPL AND formType:\"10-K\""
532
+ ```
533
+
534
+ #### Backward Compatibility
535
+
536
+ Raw Lucene queries still work but are deprecated:
537
+
538
+ ```ruby
539
+ # Still works (deprecated)
540
+ client.query.search('ticker:AAPL AND formType:"10-K"')
541
+
542
+ # Recommended
543
+ client.query.ticker("AAPL").form_type("10-K").search
544
+ ```
545
+
546
+ ### XBRL Endpoint Availability
547
+
548
+ **Impact:** The XBRL proxy was orphaned in v0.1.0 (not accessible via `client.xbrl`). Now fully wired and functional.
549
+
550
+ #### What Changed
551
+
552
+ In v0.1.0, the `SecApi::Xbrl` class existed but was never wired to the `Client` class. Calling `client.xbrl` would fail with `NoMethodError`. v1.0.0 fixes this by:
553
+
554
+ 1. Wiring `client.xbrl` to return a cached `Xbrl` proxy instance
555
+ 2. Returning immutable `XbrlData` objects instead of raw hashes
556
+ 3. Supporting multiple input formats (URL, accession number, Filing object)
557
+
558
+ #### Using the XBRL Endpoint
559
+
560
+ ```ruby
561
+ client = SecApi::Client.new(api_key: "your_api_key")
562
+
563
+ # Extract XBRL data using SEC filing URL
564
+ xbrl = client.xbrl.to_json(
565
+ "https://www.sec.gov/Archives/edgar/data/320193/000032019323000106/aapl-20230930.htm"
566
+ )
567
+
568
+ # Extract using accession number
569
+ xbrl = client.xbrl.to_json(accession_no: "0000320193-23-000106")
570
+
571
+ # Extract from Filing object (convenient)
572
+ filing = client.query.ticker("AAPL").form_type("10-K").search.first
573
+ xbrl = client.xbrl.to_json(filing)
574
+ ```
575
+
576
+ #### XbrlData Object Structure
577
+
578
+ ```ruby
579
+ xbrl = client.xbrl.to_json(filing)
580
+
581
+ # Access financial statement sections
582
+ xbrl.statements_of_income # Income statement elements
583
+ xbrl.balance_sheets # Balance sheet elements
584
+ xbrl.statements_of_cash_flows # Cash flow statement elements
585
+ xbrl.cover_page # Document and entity information (DEI)
586
+
587
+ # Each section is a Hash: element_name => Array<Fact>
588
+ revenue_facts = xbrl.statements_of_income["RevenueFromContractWithCustomerExcludingAssessedTax"]
589
+ revenue_facts.first.value # Raw string value
590
+ revenue_facts.first.to_numeric # => 394328000000.0
591
+ revenue_facts.first.period # Period object with start_date, end_date
592
+ ```
593
+
594
+ #### Discovering Available Elements
595
+
596
+ Element names vary by taxonomy (US GAAP vs IFRS). Use helper methods:
597
+
598
+ ```ruby
599
+ # Get all available element names
600
+ xbrl.element_names
601
+ # => ["Assets", "CashAndCashEquivalents", "Revenue", ...]
602
+
603
+ # Search for specific elements
604
+ xbrl.element_names.grep(/Revenue/)
605
+ # => ["RevenueFromContractWithCustomerExcludingAssessedTax"]
606
+
607
+ # Detect taxonomy (heuristic)
608
+ xbrl.taxonomy_hint # => :us_gaap, :ifrs, or :unknown
609
+
610
+ # Validate data presence
611
+ xbrl.valid? # => true if any financial statement section is present
612
+ ```
613
+
614
+ #### US GAAP vs IFRS Element Names
615
+
616
+ | Concept | US GAAP Element | IFRS Element |
617
+ |---------|-----------------|--------------|
618
+ | Revenue | `RevenueFromContractWithCustomerExcludingAssessedTax` | `Revenue` |
619
+ | Net Income | `NetIncomeLoss` | `ProfitLoss` |
620
+ | Cost of Sales | `CostOfGoodsAndServicesSold` | `CostOfSales` |
621
+ | Equity | `StockholdersEquity` | `Equity` |
622
+ | Operating Cash | `NetCashProvidedByUsedInOperatingActivities` | `CashFlowsFromUsedInOperatingActivities` |
623
+
624
+ ```ruby
625
+ # Pattern for handling both taxonomies
626
+ case xbrl.taxonomy_hint
627
+ when :us_gaap
628
+ revenue = xbrl.statements_of_income["RevenueFromContractWithCustomerExcludingAssessedTax"]
629
+ when :ifrs
630
+ revenue = xbrl.statements_of_income["Revenue"]
631
+ else
632
+ # Fall back to discovery
633
+ revenue_key = xbrl.element_names.find { |n| n.include?("Revenue") }
634
+ revenue = xbrl.statements_of_income[revenue_key]
635
+ end
636
+ ```
637
+
638
+ ---
639
+
640
+ ## New Features in v1.0.0
641
+
642
+ ### Rate Limiting Intelligence
643
+
644
+ v1.0.0 includes intelligent rate limit handling:
645
+
646
+ - **Proactive throttling:** Automatically sleeps when remaining quota drops below threshold (default: 10%)
647
+ - **Request queueing:** Queues requests when rate limit is exhausted (remaining = 0)
648
+ - **Header tracking:** Parses `X-RateLimit-*` headers from every response
649
+
650
+ ```ruby
651
+ config = SecApi::Config.new(
652
+ api_key: "...",
653
+ rate_limit_threshold: 0.2, # Throttle at 20% remaining
654
+
655
+ # Callbacks for monitoring
656
+ on_throttle: ->(info) {
657
+ puts "Throttling for #{info[:delay]}s, #{info[:remaining]} requests remaining"
658
+ },
659
+ on_queue: ->(info) {
660
+ puts "Request queued, #{info[:queue_size]} waiting"
661
+ },
662
+ on_rate_limit: ->(info) {
663
+ puts "429 received, retry in #{info[:retry_after]}s"
664
+ }
665
+ )
666
+ ```
667
+
668
+ ### Retry Middleware with Exponential Backoff
669
+
670
+ Automatic retry for transient failures with exponential backoff:
671
+
672
+ ```ruby
673
+ config = SecApi::Config.new(
674
+ api_key: "...",
675
+ retry_max_attempts: 5, # Default: 5 retries
676
+ retry_initial_delay: 1.0, # Start with 1 second
677
+ retry_max_delay: 60.0, # Cap at 60 seconds
678
+ retry_backoff_factor: 2, # 1s, 2s, 4s, 8s, 16s...
679
+
680
+ on_retry: ->(info) {
681
+ puts "Retry #{info[:attempt]}/#{info[:max_attempts]}: #{info[:error_class]}"
682
+ }
683
+ )
684
+ ```
685
+
686
+ **Retry-eligible errors:** `RateLimitError`, `NetworkError`, `ServerError` (all `TransientError` subclasses)
687
+
688
+ ### WebSocket Streaming API
689
+
690
+ Real-time SEC filing notifications via WebSocket:
691
+
692
+ ```ruby
693
+ client = SecApi::Client.new
694
+
695
+ # Subscribe to all filings
696
+ client.stream.subscribe do |filing|
697
+ puts "#{filing.ticker}: #{filing.form_type} at #{filing.filed_at}"
698
+ end
699
+
700
+ # Filter by tickers and/or form types
701
+ client.stream.subscribe(tickers: ["AAPL"], form_types: ["10-K", "8-K"]) do |filing|
702
+ ProcessFilingJob.perform_async(filing.accession_no)
703
+ end
704
+
705
+ # Check connection status
706
+ client.stream.connected?
707
+
708
+ # Close connection
709
+ client.stream.close
710
+ ```
711
+
712
+ **Stream features:**
713
+ - Client-side filtering (tickers, form_types)
714
+ - Auto-reconnect with exponential backoff (configurable)
715
+ - Latency tracking (`filing.latency_ms`, `filing.latency_seconds`)
716
+ - Best-effort delivery (use Query API to backfill gaps)
717
+
718
+ ### Observability Hooks
719
+
720
+ Instrumentation callbacks for monitoring and APM integration:
721
+
722
+ ```ruby
723
+ config = SecApi::Config.new(
724
+ api_key: "...",
725
+
726
+ # Request lifecycle callbacks
727
+ on_request: ->(request_id:, method:, url:, headers:) {
728
+ Rails.logger.info("SEC API request", request_id: request_id, method: method)
729
+ },
730
+
731
+ on_response: ->(request_id:, status:, duration_ms:, url:, method:) {
732
+ StatsD.histogram("sec_api.duration_ms", duration_ms)
733
+ },
734
+
735
+ on_retry: ->(request_id:, attempt:, max_attempts:, error_class:, error_message:, will_retry_in:) {
736
+ StatsD.increment("sec_api.retries")
737
+ },
738
+
739
+ on_error: ->(request_id:, error:, url:, method:) {
740
+ Bugsnag.notify(error, request_id: request_id)
741
+ },
742
+
743
+ # Stream-specific callbacks
744
+ on_filing: ->(filing:, latency_ms:, received_at:) {
745
+ StatsD.histogram("sec_api.stream.latency_ms", latency_ms)
746
+ },
747
+
748
+ on_reconnect: ->(attempt_count:, downtime_seconds:) {
749
+ StatsD.increment("sec_api.stream.reconnected")
750
+ }
751
+ )
752
+ ```
753
+
754
+ ### Structured Logging
755
+
756
+ JSON-formatted logs for log aggregation tools (ELK, Datadog, Splunk):
757
+
758
+ ```ruby
759
+ config = SecApi::Config.new(
760
+ api_key: "...",
761
+ logger: Rails.logger,
762
+ log_level: :info,
763
+ default_logging: true # Enable automatic structured logging
764
+ )
765
+
766
+ # Log output format (JSON):
767
+ # {"event":"secapi.request.start","request_id":"abc-123","method":"GET","url":"https://...","timestamp":"2024-01-15T10:30:00.123Z"}
768
+ # {"event":"secapi.request.complete","request_id":"abc-123","status":200,"duration_ms":150,...}
769
+ # {"event":"secapi.request.retry","request_id":"abc-123","attempt":1,...}
770
+ # {"event":"secapi.request.error","request_id":"abc-123","error_class":"SecApi::ServerError",...}
771
+ ```
772
+
773
+ Or use `SecApi::StructuredLogger` directly:
774
+
775
+ ```ruby
776
+ SecApi::StructuredLogger.log_request(logger, :info, request_id: id, method: :get, url: url)
777
+ SecApi::StructuredLogger.log_response(logger, :info, request_id: id, status: 200, duration_ms: 150, ...)
778
+ ```
779
+
780
+ ### Metrics Exposure
781
+
782
+ Automatic metrics collection via StatsD-compatible backends:
783
+
784
+ ```ruby
785
+ require 'statsd-ruby'
786
+ statsd = StatsD.new('localhost', 8125)
787
+
788
+ config = SecApi::Config.new(
789
+ api_key: "...",
790
+ metrics_backend: statsd # Or Datadog::Statsd.new(...)
791
+ )
792
+
793
+ # Metrics automatically collected:
794
+ # sec_api.requests.total (counter, tags: method, status)
795
+ # sec_api.requests.duration_ms (histogram)
796
+ # sec_api.retries.total (counter, tags: error_class, attempt)
797
+ # sec_api.errors.total (counter, tags: error_class)
798
+ # sec_api.rate_limit.throttle (counter)
799
+ # sec_api.rate_limit.queue (gauge)
800
+ ```
801
+
802
+ ### Filing Journey Tracking
803
+
804
+ Track filing lifecycle from detection through processing:
805
+
806
+ ```ruby
807
+ # In your stream handler:
808
+ client.stream.subscribe do |filing|
809
+ detected_at = Time.now
810
+
811
+ # Stage 1: Detection
812
+ SecApi::FilingJourney.log_detected(logger, :info,
813
+ accession_no: filing.accession_no,
814
+ ticker: filing.ticker,
815
+ form_type: filing.form_type
816
+ )
817
+
818
+ # Stage 2: Query for metadata
819
+ full_filing = client.query.ticker(filing.ticker).limit(1).search.first
820
+ SecApi::FilingJourney.log_queried(logger, :info,
821
+ accession_no: filing.accession_no,
822
+ found: !full_filing.nil?
823
+ )
824
+
825
+ # Stage 3: XBRL extraction
826
+ xbrl = client.xbrl.to_json(filing)
827
+ SecApi::FilingJourney.log_extracted(logger, :info,
828
+ accession_no: filing.accession_no,
829
+ facts_count: xbrl.element_names.size
830
+ )
831
+
832
+ # Stage 4: Processing complete
833
+ total_ms = SecApi::FilingJourney.calculate_duration_ms(detected_at)
834
+ SecApi::FilingJourney.log_processed(logger, :info,
835
+ accession_no: filing.accession_no,
836
+ success: true,
837
+ total_duration_ms: total_ms
838
+ )
839
+ end
840
+
841
+ # Query logs by accession_no for complete journey:
842
+ # ELK: accession_no:"0000320193-24-000001" AND event:secapi.filing.journey.*
843
+ # Datadog: @accession_no:0000320193-24-000001 @event:secapi.filing.journey.*
844
+ ```
845
+
846
+ ### Automatic Pagination
847
+
848
+ Lazy enumeration through all search results:
849
+
850
+ ```ruby
851
+ # Auto-paginate through all results (lazy evaluation)
852
+ client.query
853
+ .ticker("AAPL")
854
+ .form_type("10-K", "10-Q")
855
+ .date_range(from: "2018-01-01", to: Date.today)
856
+ .auto_paginate
857
+ .each { |filing| process(filing) }
858
+
859
+ # With Enumerable methods (also lazy)
860
+ client.query.ticker("AAPL").auto_paginate
861
+ .select { |f| f.form_type == "10-K" }
862
+ .take(100)
863
+ .each { |f| process(f) }
864
+
865
+ # Manual pagination still available
866
+ filings = client.query.ticker("AAPL").search
867
+ while filings.has_more?
868
+ filings.each { |f| process(f) }
869
+ filings = filings.fetch_next_page
870
+ end
871
+ ```
872
+
873
+ ---
874
+
875
+ ## Configuration Changes
876
+
877
+ ### New Configuration Options
878
+
879
+ v1.0.0 adds many new configuration options. All are optional with sensible defaults.
880
+
881
+ | Option | Type | Default | Description |
882
+ |--------|------|---------|-------------|
883
+ | `retry_max_attempts` | Integer | 5 | Maximum retry attempts for transient errors |
884
+ | `retry_initial_delay` | Float | 1.0 | Initial retry delay (seconds) |
885
+ | `retry_max_delay` | Float | 60.0 | Maximum retry delay (seconds) |
886
+ | `retry_backoff_factor` | Integer | 2 | Exponential backoff multiplier |
887
+ | `request_timeout` | Integer | 30 | HTTP request timeout (seconds) |
888
+ | `rate_limit_threshold` | Float | 0.1 | Throttle threshold (0.0-1.0) |
889
+ | `queue_wait_warning_threshold` | Integer | 300 | Warn if queue wait exceeds (seconds) |
890
+ | `logger` | Logger | nil | Logger instance for structured logging |
891
+ | `log_level` | Symbol | :info | Log level (:debug, :info, :warn, :error) |
892
+ | `default_logging` | Boolean | false | Enable automatic structured logging |
893
+ | `metrics_backend` | Object | nil | StatsD-compatible metrics backend |
894
+
895
+ **Stream-specific options:**
896
+
897
+ | Option | Type | Default | Description |
898
+ |--------|------|---------|-------------|
899
+ | `stream_max_reconnect_attempts` | Integer | 10 | Max reconnection attempts |
900
+ | `stream_initial_reconnect_delay` | Float | 1.0 | Initial reconnect delay (seconds) |
901
+ | `stream_max_reconnect_delay` | Float | 60.0 | Max reconnect delay (seconds) |
902
+ | `stream_backoff_multiplier` | Integer | 2 | Reconnect backoff multiplier |
903
+ | `stream_latency_warning_threshold` | Float | 120.0 | Warn if latency exceeds (seconds) |
904
+
905
+ **Callback options:**
906
+
907
+ | Option | Invoked When |
908
+ |--------|--------------|
909
+ | `on_request` | Before each REST API request |
910
+ | `on_response` | After each REST API response |
911
+ | `on_retry` | Before each retry attempt |
912
+ | `on_error` | On final failure (all retries exhausted) |
913
+ | `on_throttle` | When proactive throttling occurs |
914
+ | `on_rate_limit` | When 429 response received |
915
+ | `on_queue` | When request queued (rate limit exhausted) |
916
+ | `on_dequeue` | When request exits queue |
917
+ | `on_excessive_wait` | When queue wait exceeds threshold |
918
+ | `on_callback_error` | When stream callback raises exception |
919
+ | `on_reconnect` | When stream reconnection succeeds |
920
+ | `on_filing` | When filing received via stream |
921
+
922
+ ### Configuration Sources
923
+
924
+ Configuration can be provided via (in order of precedence):
925
+
926
+ 1. **Constructor arguments** (highest priority)
927
+ 2. **YAML file:** `config/secapi.yml`
928
+ 3. **Environment variables** (lowest priority)
929
+
930
+ ```ruby
931
+ # Constructor (highest priority)
932
+ config = SecApi::Config.new(api_key: "from_constructor")
933
+
934
+ # YAML file (config/secapi.yml)
935
+ # secapi:
936
+ # api_key: "from_yaml"
937
+ # retry_max_attempts: 3
938
+
939
+ # Environment variables (SECAPI_ prefix)
940
+ # SECAPI_API_KEY=from_env
941
+ # SECAPI_RETRY_MAX_ATTEMPTS=3
942
+ ```
943
+
944
+ ### Minimal Configuration
945
+
946
+ ```ruby
947
+ # v0.1.0 style (still works)
948
+ client = SecApi::Client.new(api_key: ENV["SEC_API_KEY"])
949
+
950
+ # v1.0.0 style (recommended)
951
+ config = SecApi::Config.new(api_key: ENV["SEC_API_KEY"])
952
+ client = SecApi::Client.new(config: config)
953
+
954
+ # Or use environment variable directly
955
+ # Set SECAPI_API_KEY in your environment
956
+ client = SecApi::Client.new
957
+ ```
958
+
959
+ ### Production-Ready Configuration
960
+
961
+ ```ruby
962
+ config = SecApi::Config.new(
963
+ api_key: ENV["SEC_API_KEY"],
964
+
965
+ # Retry settings
966
+ retry_max_attempts: 5,
967
+ retry_initial_delay: 1.0,
968
+ retry_max_delay: 60.0,
969
+ retry_backoff_factor: 2,
970
+
971
+ # Rate limiting
972
+ rate_limit_threshold: 0.1,
973
+
974
+ # Logging
975
+ logger: Rails.logger,
976
+ log_level: :info,
977
+ default_logging: true,
978
+
979
+ # Metrics (optional)
980
+ metrics_backend: StatsD.new('localhost', 8125),
981
+
982
+ # Error tracking
983
+ on_error: ->(request_id:, error:, url:, method:) {
984
+ Bugsnag.notify(error, request_id: request_id)
985
+ }
986
+ )
987
+
988
+ client = SecApi::Client.new(config: config)
989
+ ```
990
+
991
+ ---
992
+
993
+ ## Deprecations
994
+
995
+ ### Deprecated in v1.0.0
996
+
997
+ | Deprecated | Replacement | Notes |
998
+ |------------|-------------|-------|
999
+ | Raw Lucene query strings | Query Builder DSL | Still works, will be removed in v2.0 |
1000
+ | `client.query.search(query: "...")` | `client.query.ticker(...).search` | Fluent DSL is preferred |
1001
+
1002
+ ---
1003
+
1004
+ ## Troubleshooting
1005
+
1006
+ ### Common Migration Issues
1007
+
1008
+ #### 1. NoMethodError on API responses
1009
+
1010
+ **Problem:** `undefined method 'ticker' for {"ticker"=>"AAPL"...}:Hash`
1011
+
1012
+ **Cause:** Code expects hash access but v1.0.0 returns typed objects.
1013
+
1014
+ **Fix:** Replace hash access with method calls:
1015
+ ```ruby
1016
+ # Old
1017
+ result["ticker"]
1018
+ result["formType"]
1019
+
1020
+ # New
1021
+ result.ticker
1022
+ result.form_type
1023
+ ```
1024
+
1025
+ #### 2. TypeError when accessing nested data
1026
+
1027
+ **Problem:** `TypeError: can't convert String to Integer`
1028
+
1029
+ **Cause:** Trying to use string keys on typed objects.
1030
+
1031
+ **Fix:** Use dot notation for all attributes:
1032
+ ```ruby
1033
+ # Old
1034
+ filing["entities"][0]["name"]
1035
+
1036
+ # New
1037
+ filing.entities.first.name
1038
+ ```
1039
+
1040
+ #### 3. Exception handling not working
1041
+
1042
+ **Problem:** `rescue => e` catches nothing when expected.
1043
+
1044
+ **Cause:** Catching wrong exception type.
1045
+
1046
+ **Fix:** Use the new exception hierarchy:
1047
+ ```ruby
1048
+ # Catch specific errors
1049
+ rescue SecApi::RateLimitError => e
1050
+ rescue SecApi::TransientError => e # Catch-all for retryable
1051
+ rescue SecApi::PermanentError => e # Catch-all for non-retryable
1052
+ rescue SecApi::Error => e # Catch any SecApi error
1053
+ ```
1054
+
1055
+ #### 4. Query builder returns no results
1056
+
1057
+ **Problem:** Query returns empty results when it shouldn't.
1058
+
1059
+ **Cause:** Using wrong method syntax.
1060
+
1061
+ **Fix:** Check method signatures:
1062
+ ```ruby
1063
+ # Wrong - dates as positional args
1064
+ .date_range("2020-01-01", "2023-12-31")
1065
+
1066
+ # Correct - dates as keyword args
1067
+ .date_range(from: "2020-01-01", to: "2023-12-31")
1068
+ ```
1069
+
1070
+ #### 5. XBRL extraction fails
1071
+
1072
+ **Problem:** `ValidationError: XBRL data validation failed`
1073
+
1074
+ **Cause:** Filing may not have XBRL data, or URL is invalid.
1075
+
1076
+ **Fix:** Verify the filing has XBRL data:
1077
+ ```ruby
1078
+ # Check filing has XBRL
1079
+ if filing.respond_to?(:xbrl_url) && !filing.xbrl_url.to_s.empty?
1080
+ xbrl = client.xbrl.to_json(filing)
1081
+ end
1082
+
1083
+ # Or use accession number
1084
+ xbrl = client.xbrl.to_json(accession_no: filing.accession_no)
1085
+ ```
1086
+
1087
+ ### Getting Help
1088
+
1089
+ - **GitHub Issues:** https://github.com/ljuti/sec_api/issues
1090
+ - **sec-api.io Documentation:** https://sec-api.io/docs
1091
+