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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module SecApi
|
|
7
|
+
module Middleware
|
|
8
|
+
# Faraday middleware that provides instrumentation callbacks for request/response lifecycle.
|
|
9
|
+
#
|
|
10
|
+
# This middleware captures request timing and invokes configurable callbacks for:
|
|
11
|
+
# - on_request: Before the request is sent (for logging, tracing)
|
|
12
|
+
# - on_response: After the response is received (for metrics, latency tracking)
|
|
13
|
+
# - on_error: When request ultimately fails after all retries exhausted (for error tracking)
|
|
14
|
+
#
|
|
15
|
+
# Position in middleware stack: FIRST (before Retry, RateLimiter, ErrorHandler)
|
|
16
|
+
# This ensures all requests are instrumented, including retried requests.
|
|
17
|
+
# Being first also allows capturing exceptions after all retries are exhausted.
|
|
18
|
+
#
|
|
19
|
+
# @example Basic usage with config callbacks
|
|
20
|
+
# config = SecApi::Config.new(
|
|
21
|
+
# api_key: "...",
|
|
22
|
+
# on_request: ->(request_id:, method:, url:, headers:) { log_request(request_id) },
|
|
23
|
+
# on_response: ->(request_id:, status:, duration_ms:, url:, method:) { track_metrics(duration_ms) }
|
|
24
|
+
# )
|
|
25
|
+
# client = SecApi::Client.new(config)
|
|
26
|
+
#
|
|
27
|
+
# @example Using external request_id for distributed tracing
|
|
28
|
+
# # Create custom middleware to inject trace ID from your APM system
|
|
29
|
+
# class TraceIdMiddleware < Faraday::Middleware
|
|
30
|
+
# def call(env)
|
|
31
|
+
# # Use existing trace ID from Datadog, New Relic, OpenTelemetry, etc.
|
|
32
|
+
# # Falls back to SecureRandom.uuid if no trace ID is available
|
|
33
|
+
# env[:request_id] = Datadog.tracer.active_span&.span_id ||
|
|
34
|
+
# RequestStore[:request_id] ||
|
|
35
|
+
# SecureRandom.uuid
|
|
36
|
+
# @app.call(env)
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # Register BEFORE sec_api Instrumentation middleware
|
|
41
|
+
# Faraday.new do |conn|
|
|
42
|
+
# conn.use TraceIdMiddleware # Sets env[:request_id]
|
|
43
|
+
# conn.use SecApi::Middleware::Instrumentation # Preserves via ||=
|
|
44
|
+
# # ... rest of stack
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# @example Correlating errors with APM spans
|
|
48
|
+
# SecApi.configure do |config|
|
|
49
|
+
# config.on_error = ->(request_id:, error:, **) {
|
|
50
|
+
# if span = Datadog.tracer.active_span
|
|
51
|
+
# span.set_tag('sec_api.request_id', request_id)
|
|
52
|
+
# span.set_error(error)
|
|
53
|
+
# end
|
|
54
|
+
# }
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# @note Authorization headers are automatically sanitized from on_request callbacks
|
|
58
|
+
# to prevent API key leakage in logs.
|
|
59
|
+
#
|
|
60
|
+
# @note External request_id: If you pre-set env[:request_id] via upstream middleware,
|
|
61
|
+
# this middleware will preserve it (uses ||= operator). This enables distributed
|
|
62
|
+
# tracing integration with Datadog, New Relic, OpenTelemetry, and Rails request IDs.
|
|
63
|
+
#
|
|
64
|
+
class Instrumentation < Faraday::Middleware
|
|
65
|
+
include SecApi::CallbackHelper
|
|
66
|
+
|
|
67
|
+
# Initializes the instrumentation middleware.
|
|
68
|
+
#
|
|
69
|
+
# @param app [Faraday::Middleware] The next middleware in the stack
|
|
70
|
+
# @param options [Hash] Configuration options
|
|
71
|
+
# @option options [SecApi::Config] :config The config object containing callbacks
|
|
72
|
+
def initialize(app, options = {})
|
|
73
|
+
super(app)
|
|
74
|
+
@config = options[:config]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Processes the request and invokes instrumentation callbacks.
|
|
78
|
+
#
|
|
79
|
+
# @param env [Faraday::Env] The request environment
|
|
80
|
+
# @return [Faraday::Response] The response from downstream middleware
|
|
81
|
+
def call(env)
|
|
82
|
+
# Generate request_id if not already set (allows upstream middleware to set it)
|
|
83
|
+
env[:request_id] ||= SecureRandom.uuid
|
|
84
|
+
|
|
85
|
+
# Capture start time using monotonic clock for accurate duration.
|
|
86
|
+
# Why monotonic? Time.now can jump backward (NTP sync, DST) causing negative durations.
|
|
87
|
+
# CLOCK_MONOTONIC is guaranteed to increase, essential for accurate latency metrics.
|
|
88
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
89
|
+
|
|
90
|
+
# Invoke on_request callback BEFORE request is sent
|
|
91
|
+
invoke_on_request(env)
|
|
92
|
+
|
|
93
|
+
# Execute the request through downstream middleware
|
|
94
|
+
@app.call(env).on_complete do |response_env|
|
|
95
|
+
# Calculate duration in milliseconds
|
|
96
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
97
|
+
duration_ms = ((end_time - start_time) * 1000).round
|
|
98
|
+
|
|
99
|
+
# Store duration in env for potential use by other middleware
|
|
100
|
+
response_env[:duration_ms] = duration_ms
|
|
101
|
+
|
|
102
|
+
# Invoke on_response callback AFTER response is received
|
|
103
|
+
invoke_on_response(response_env, duration_ms)
|
|
104
|
+
end
|
|
105
|
+
rescue SecApi::Error => e
|
|
106
|
+
# Invoke on_error callback for errors that escape after all retries exhausted.
|
|
107
|
+
# This catches both TransientError (NetworkError, ServerError, RateLimitError)
|
|
108
|
+
# and PermanentError (AuthenticationError, NotFoundError, ValidationError).
|
|
109
|
+
# PermanentError on_error is also invoked by ErrorHandler for immediate failures,
|
|
110
|
+
# but we invoke here too for consistency (both paths call on_error exactly once).
|
|
111
|
+
# Note: ErrorHandler only invokes on_error for PermanentError, not TransientError.
|
|
112
|
+
invoke_on_error(env, e)
|
|
113
|
+
raise
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# Invokes the on_request callback if configured.
|
|
119
|
+
#
|
|
120
|
+
# @param env [Faraday::Env] The request environment
|
|
121
|
+
# @return [void]
|
|
122
|
+
def invoke_on_request(env)
|
|
123
|
+
return unless @config&.on_request
|
|
124
|
+
|
|
125
|
+
@config.on_request.call(
|
|
126
|
+
request_id: env[:request_id],
|
|
127
|
+
method: env[:method],
|
|
128
|
+
url: env[:url].to_s,
|
|
129
|
+
headers: sanitize_headers(env[:request_headers])
|
|
130
|
+
)
|
|
131
|
+
rescue => e
|
|
132
|
+
log_callback_error("on_request", e)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Invokes the on_response callback if configured.
|
|
136
|
+
#
|
|
137
|
+
# @param env [Faraday::Env] The response environment
|
|
138
|
+
# @param duration_ms [Integer] Request duration in milliseconds
|
|
139
|
+
# @return [void]
|
|
140
|
+
def invoke_on_response(env, duration_ms)
|
|
141
|
+
return unless @config&.on_response
|
|
142
|
+
|
|
143
|
+
@config.on_response.call(
|
|
144
|
+
request_id: env[:request_id],
|
|
145
|
+
status: env[:status],
|
|
146
|
+
duration_ms: duration_ms,
|
|
147
|
+
url: env[:url].to_s,
|
|
148
|
+
method: env[:method]
|
|
149
|
+
)
|
|
150
|
+
rescue => e
|
|
151
|
+
log_callback_error("on_response", e)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Invokes the on_error callback if configured.
|
|
155
|
+
# Called when a request ultimately fails (after all retries exhausted).
|
|
156
|
+
#
|
|
157
|
+
# @param env [Faraday::Env] The request environment
|
|
158
|
+
# @param error [SecApi::Error] The error that caused the failure
|
|
159
|
+
# @return [void]
|
|
160
|
+
def invoke_on_error(env, error)
|
|
161
|
+
return unless @config&.on_error
|
|
162
|
+
|
|
163
|
+
@config.on_error.call(
|
|
164
|
+
request_id: env[:request_id],
|
|
165
|
+
error: error,
|
|
166
|
+
url: env[:url].to_s,
|
|
167
|
+
method: env[:method]
|
|
168
|
+
)
|
|
169
|
+
rescue => e
|
|
170
|
+
log_callback_error("on_error", e)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Removes sensitive headers (Authorization) from the headers hash.
|
|
174
|
+
#
|
|
175
|
+
# @param headers [Hash, nil] Request headers
|
|
176
|
+
# @return [Hash] Headers with sensitive values removed
|
|
177
|
+
def sanitize_headers(headers)
|
|
178
|
+
return {} unless headers
|
|
179
|
+
|
|
180
|
+
headers.reject { |k, _| k.to_s.downcase == "authorization" }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# log_callback_error is provided by CallbackHelper module
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|