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,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