ohmydata 0.3.2__tar.gz → 0.3.3__tar.gz

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 (131) hide show
  1. {ohmydata-0.3.2 → ohmydata-0.3.3}/CHANGELOG.md +7 -0
  2. {ohmydata-0.3.2 → ohmydata-0.3.3}/PKG-INFO +38 -1
  3. {ohmydata-0.3.2 → ohmydata-0.3.3}/README.md +37 -0
  4. {ohmydata-0.3.2 → ohmydata-0.3.3}/pyproject.toml +1 -1
  5. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/__init__.py +1 -1
  6. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/__init__.py +3 -0
  7. ohmydata-0.3.3/src/ohmydata/providers/sec/submissions.py +143 -0
  8. {ohmydata-0.3.2 → ohmydata-0.3.3}/uv.lock +1 -1
  9. {ohmydata-0.3.2 → ohmydata-0.3.3}/.gitignore +0 -0
  10. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/adapters/__init__.py +0 -0
  11. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/adapters/polars.py +0 -0
  12. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/cli.py +0 -0
  13. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/__init__.py +0 -0
  14. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/_vintage_lock.py +0 -0
  15. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/availability.py +0 -0
  16. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/errors.py +0 -0
  17. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/facts.py +0 -0
  18. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/instruments.py +0 -0
  19. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/policy.py +0 -0
  20. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/provenance.py +0 -0
  21. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/rate_limit.py +0 -0
  22. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/snapshot.py +0 -0
  23. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/specs.py +0 -0
  24. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/core/vintage.py +0 -0
  25. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/__init__.py +0 -0
  26. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_accounting_models.py +0 -0
  27. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_dated_invalidation_models.py +0 -0
  28. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_document_bundle_codec.py +0 -0
  29. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_document_financial_contract.py +0 -0
  30. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_document_source_links.py +0 -0
  31. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_discovery_models.py +0 -0
  32. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_execution_io.py +0 -0
  33. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_execution_models.py +0 -0
  34. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_execution_receipts.py +0 -0
  35. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_ledger_io.py +0 -0
  36. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_recompute_codec.py +0 -0
  37. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_work_codec.py +0 -0
  38. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_work_models.py +0 -0
  39. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_event_work_transitions.py +0 -0
  40. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_live_unit_corroboration.py +0 -0
  41. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_metric_arithmetic.py +0 -0
  42. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_metric_common.py +0 -0
  43. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_metric_evaluation.py +0 -0
  44. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_metric_graph_models.py +0 -0
  45. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_metric_inputs.py +0 -0
  46. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_observed_financial_bundle_codec.py +0 -0
  47. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_observed_financial_bundle_graph.py +0 -0
  48. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_observed_financial_output.py +0 -0
  49. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_observed_financial_receipts.py +0 -0
  50. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_observed_xbrl_units.py +0 -0
  51. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_pit_bundle_findings.py +0 -0
  52. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_pit_projection.py +0 -0
  53. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_pit_query.py +0 -0
  54. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_statement_parser.py +0 -0
  55. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/_structural_admission.py +0 -0
  56. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/artifacts.py +0 -0
  57. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/batch.py +0 -0
  58. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/cli.py +0 -0
  59. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/core_dataset.py +0 -0
  60. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/dated_invalidation.py +0 -0
  61. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/document_accounting.py +0 -0
  62. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/document_financial_bundle.py +0 -0
  63. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/document_financial_replay.py +0 -0
  64. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/document_financials.py +0 -0
  65. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/document_source.py +0 -0
  66. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/edgar.py +0 -0
  67. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/edgartools_adapter.py +0 -0
  68. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/embedded_document_source.py +0 -0
  69. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/endpoints.py +0 -0
  70. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/errors.py +0 -0
  71. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/event_dependencies.py +0 -0
  72. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/event_discovery.py +0 -0
  73. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/event_execution.py +0 -0
  74. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/event_ledger.py +0 -0
  75. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/event_recompute.py +0 -0
  76. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/event_work.py +0 -0
  77. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/financials.py +0 -0
  78. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/financials_dataset.py +0 -0
  79. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/http.py +0 -0
  80. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/known_by_ttm.py +0 -0
  81. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/metric_graph.py +0 -0
  82. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/nport.py +0 -0
  83. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/observed_accounting.py +0 -0
  84. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/observed_financial_bundle.py +0 -0
  85. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/observed_financial_replay.py +0 -0
  86. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/observed_xbrl_financials.py +0 -0
  87. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/observed_xbrl_package.py +0 -0
  88. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/pit.py +0 -0
  89. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/pit_bundle.py +0 -0
  90. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/qualification.py +0 -0
  91. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/qualification_dataset.py +0 -0
  92. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/quality_findings.py +0 -0
  93. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/quarter_binding.py +0 -0
  94. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/quarter_ttm.py +0 -0
  95. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/sgml_financials.py +0 -0
  96. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/structural_quality.py +0 -0
  97. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/unit_evidence.py +0 -0
  98. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/xbrl_package.py +0 -0
  99. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/sec/xbrl_package_financials.py +0 -0
  100. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/__init__.py +0 -0
  101. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/client.py +0 -0
  102. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/endpoints.py +0 -0
  103. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/errors.py +0 -0
  104. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/observations/__init__.py +0 -0
  105. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/observations/capture.py +0 -0
  106. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/observations/serialization.py +0 -0
  107. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/recipes/__init__.py +0 -0
  108. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/recipes/etf_adjusted_bars.py +0 -0
  109. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/recipes/etf_index_mapping.py +0 -0
  110. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/recipes/etf_pcf_history.py +0 -0
  111. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/recipes/index_weight_vintage.py +0 -0
  112. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/recipes/lookthrough_bundle.py +0 -0
  113. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/recipes/shared.py +0 -0
  114. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/recipes/weighted_dividend_yield.py +0 -0
  115. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/vintage_artifacts.py +0 -0
  116. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/vintage_canonical.py +0 -0
  117. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/vintage_gates.py +0 -0
  118. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/vintage_plane.py +0 -0
  119. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/tushare/vintage_producer.py +0 -0
  120. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/yfinance/__init__.py +0 -0
  121. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/yfinance/_fundamentals_parsers.py +0 -0
  122. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/yfinance/_fundamentals_periods.py +0 -0
  123. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/yfinance/client.py +0 -0
  124. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/yfinance/endpoints.py +0 -0
  125. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/yfinance/errors.py +0 -0
  126. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/yfinance/fundamentals.py +0 -0
  127. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/providers/yfinance/quality.py +0 -0
  128. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/tools/__init__.py +0 -0
  129. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/tools/cli.py +0 -0
  130. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/tools/drift_audit.py +0 -0
  131. {ohmydata-0.3.2 → ohmydata-0.3.3}/src/ohmydata/tools/universes.py +0 -0
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.3 — 2026-09-23
6
+
7
+ - Add `fetch_sec_submissions_closure` to fetch and retain a complete SEC
8
+ Submissions root and its advertised history pages using the injected SEC HTTP
9
+ client. It returns snapshot-backed discovery sources and fails explicitly on
10
+ missing pages, malformed schemas, conflicting accession metadata or bounds.
11
+
5
12
  ## 0.3.2 — 2026-09-13
6
13
 
7
14
  - Narrow OMD SEC v2 live XBRL compatibility fix:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ohmydata
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Financial and alternative market data ingestion SDK.
5
5
  Author: OMD contributors
6
6
  Requires-Python: <3.13,>=3.11
@@ -728,6 +728,43 @@ It preserves acceptance metadata, emits filing events and exact 8-K item 2.02
728
728
  events, and fails on missing pages or conflicting facts. Acceptance is not proof
729
729
  of the website's first publication time.
730
730
 
731
+ `fetch_sec_submissions_closure` obtains that complete root and every advertised
732
+ history page with an injected `SecHttpClient` and `SnapshotStore`. The client
733
+ requires a caller supplied SEC User-Agent and applies its request spacing and
734
+ retry policy for transient connection and HTTP status failures. A response body
735
+ read failure is classified as transient; the caller retries the full closure.
736
+ The fetcher validates bounded JSON, response length, CIK, page names,
737
+ columns and duplicate accession metadata, then retains exact response bytes as
738
+ append-only observations. A failed later page leaves earlier observations
739
+ available for inspection but returns no complete closure. A new call fetches a
740
+ fresh root and all pages it advertises; callers choose their own schedule and
741
+ reconciliation windows. Up to 16 history pages, 8 MiB per page, 64 MiB total and
742
+ 100,000 filing rows are supported; exceeding a bound fails explicitly.
743
+
744
+ ```python
745
+ from ohmydata.providers.sec import (
746
+ SecHttpClient,
747
+ fetch_sec_submissions_closure,
748
+ discover_sec_filing_events,
749
+ )
750
+
751
+ closure = fetch_sec_submissions_closure(snapshot_store, SecHttpClient(sec_user_agent), "0000000001")
752
+ batch = discover_sec_filing_events(
753
+ snapshot_store,
754
+ closure.root_source,
755
+ closure.historical_sources,
756
+ policy=discovery_policy,
757
+ prior_cursor=prior_cursor,
758
+ )
759
+ ```
760
+
761
+ The closure is retained source evidence. Event discovery does not infer whether
762
+ an amendment changes financial values. Existing retained document and SGML
763
+ financial production paths take an explicit accession, while the live
764
+ `SecFinancialsClient` still selects filings by form and date. This fetcher does
765
+ not acquire the filing artifacts needed for exact-accession financial production.
766
+ Event ledger/work processing remains a separate step.
767
+
731
768
  `SecEventDiscoveryLedger` atomically stores discovered events and their cursor in
732
769
  immutable generations. On POSIX filesystems, appenders cooperate through a writer
733
770
  lock; `load()` replays the retained source observations and reconstructs the
@@ -702,6 +702,43 @@ It preserves acceptance metadata, emits filing events and exact 8-K item 2.02
702
702
  events, and fails on missing pages or conflicting facts. Acceptance is not proof
703
703
  of the website's first publication time.
704
704
 
705
+ `fetch_sec_submissions_closure` obtains that complete root and every advertised
706
+ history page with an injected `SecHttpClient` and `SnapshotStore`. The client
707
+ requires a caller supplied SEC User-Agent and applies its request spacing and
708
+ retry policy for transient connection and HTTP status failures. A response body
709
+ read failure is classified as transient; the caller retries the full closure.
710
+ The fetcher validates bounded JSON, response length, CIK, page names,
711
+ columns and duplicate accession metadata, then retains exact response bytes as
712
+ append-only observations. A failed later page leaves earlier observations
713
+ available for inspection but returns no complete closure. A new call fetches a
714
+ fresh root and all pages it advertises; callers choose their own schedule and
715
+ reconciliation windows. Up to 16 history pages, 8 MiB per page, 64 MiB total and
716
+ 100,000 filing rows are supported; exceeding a bound fails explicitly.
717
+
718
+ ```python
719
+ from ohmydata.providers.sec import (
720
+ SecHttpClient,
721
+ fetch_sec_submissions_closure,
722
+ discover_sec_filing_events,
723
+ )
724
+
725
+ closure = fetch_sec_submissions_closure(snapshot_store, SecHttpClient(sec_user_agent), "0000000001")
726
+ batch = discover_sec_filing_events(
727
+ snapshot_store,
728
+ closure.root_source,
729
+ closure.historical_sources,
730
+ policy=discovery_policy,
731
+ prior_cursor=prior_cursor,
732
+ )
733
+ ```
734
+
735
+ The closure is retained source evidence. Event discovery does not infer whether
736
+ an amendment changes financial values. Existing retained document and SGML
737
+ financial production paths take an explicit accession, while the live
738
+ `SecFinancialsClient` still selects filings by form and date. This fetcher does
739
+ not acquire the filing artifacts needed for exact-accession financial production.
740
+ Event ledger/work processing remains a separate step.
741
+
705
742
  `SecEventDiscoveryLedger` atomically stores discovered events and their cursor in
706
743
  immutable generations. On POSIX filesystems, appenders cooperate through a writer
707
744
  lock; `load()` replays the retained source observations and reconstructs the
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ohmydata"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Financial and alternative market data ingestion SDK."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.13"
@@ -1,5 +1,5 @@
1
1
  """Provisional offline-only Oh My Data package scaffold."""
2
2
 
3
- __version__ = "0.3.2"
3
+ __version__ = "0.3.3"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -266,6 +266,7 @@ from .sgml_financials import (
266
266
  produce_sec_financials_from_sgml,
267
267
  )
268
268
  from .structural_quality import SecStructuralQualityReport, evaluate_sec_structural_quality
269
+ from .submissions import SecSubmissionsClosure, fetch_sec_submissions_closure
269
270
  from .unit_evidence import SecFinancialUnitEvidence
270
271
  from .xbrl_package import (
271
272
  SecXbrlPackage,
@@ -419,6 +420,7 @@ __all__ = [
419
420
  "SecStatementParseError",
420
421
  "SecStatementRow",
421
422
  "SecStructuralQualityReport",
423
+ "SecSubmissionsClosure",
422
424
  "SecTargetMetricInputs",
423
425
  "SecTransportEvidence",
424
426
  "SecUnavailableResult",
@@ -449,6 +451,7 @@ __all__ = [
449
451
  "evaluate_sec_structural_quality",
450
452
  "execute_sec_metric_recompute",
451
453
  "extract_from_artifact",
454
+ "fetch_sec_submissions_closure",
452
455
  "historical_basenames",
453
456
  "historical_submission_url",
454
457
  "latest_completed_quarter",
@@ -0,0 +1,143 @@
1
+ """Retain a complete SEC Submissions root and its advertised history pages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ from typing import Any
10
+
11
+ from ...core import RequestSpec, SnapshotMode, SnapshotStore
12
+ from ._event_discovery_models import SecDiscoverySource
13
+ from .edgar import historical_basenames, historical_submission_url
14
+ from .errors import (
15
+ PermanentProviderError,
16
+ ResourceLimitError,
17
+ SchemaMismatchError,
18
+ TransientProviderError,
19
+ )
20
+ from .event_discovery import _strict_json, _submission_rows
21
+ from .http import SecHttpClient, validate_sec_url
22
+
23
+ _SOURCE_LIMIT = 8 * 1024**2
24
+ _TOTAL_LIMIT = 64 * 1024**2
25
+ _ROW_LIMIT = 100_000
26
+ _SERIALIZATION = "sec-submissions-json-v1"
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class SecSubmissionsClosure:
31
+ """A retained root and every history page named by that exact root."""
32
+
33
+ cik: str
34
+ root_source: SecDiscoverySource
35
+ historical_sources: tuple[SecDiscoverySource, ...]
36
+
37
+
38
+ def _exact_url(expected: str, actual: str) -> str:
39
+ if validate_sec_url(actual) != validate_sec_url(expected):
40
+ raise PermanentProviderError("SEC submissions redirect changed source URL")
41
+ return actual
42
+
43
+
44
+ def _fetch(client: SecHttpClient, url: str, remaining: int) -> bytes:
45
+ if remaining <= 0:
46
+ raise ResourceLimitError("aggregate submissions bytes exceeded")
47
+ response = client.open(
48
+ url,
49
+ accept="application/json",
50
+ max_bytes=min(_SOURCE_LIMIT, remaining),
51
+ redirect_validator=lambda actual: _exact_url(url, actual),
52
+ )
53
+ try:
54
+ _exact_url(url, response.url)
55
+ try:
56
+ body: bytes = response.body.read()
57
+ except (TimeoutError, ConnectionError, OSError) as exc:
58
+ raise TransientProviderError("SEC submissions response read failed") from exc
59
+ if type(body) is not bytes or len(body) > min(_SOURCE_LIMIT, remaining):
60
+ raise ResourceLimitError("submissions response bytes exceeded")
61
+ declared_length = response.headers.get("Content-Length")
62
+ if declared_length is not None and len(body) != int(declared_length):
63
+ raise TransientProviderError("SEC submissions response length mismatch")
64
+ return body
65
+ finally:
66
+ response.body.close()
67
+
68
+
69
+ def fetch_sec_submissions_closure(
70
+ store: SnapshotStore,
71
+ client: SecHttpClient,
72
+ cik: str,
73
+ *,
74
+ utc_now: Callable[[], datetime] = lambda: datetime.now(UTC),
75
+ ) -> SecSubmissionsClosure:
76
+ """Fetch and append immutable observations for one complete advertised closure.
77
+
78
+ Each HTTP GET uses the injected SEC client's limiter and classified retries.
79
+ Failed later pages leave earlier observations retained, but no closure is returned.
80
+ A subsequent invocation fetches a fresh root and all pages it advertises.
81
+ """
82
+ if type(cik) is not str or re.fullmatch(r"[0-9]{10}", cik) is None:
83
+ raise ValueError("CIK must be ten digits")
84
+ if not isinstance(store, SnapshotStore) or not isinstance(client, SecHttpClient):
85
+ raise TypeError("invalid submissions fetch dependencies")
86
+
87
+ root_url = f"https://data.sec.gov/submissions/CIK{cik}.json"
88
+ root_bytes = _fetch(client, root_url, _TOTAL_LIMIT)
89
+ root = _strict_json(root_bytes)
90
+ filings = root.get("filings")
91
+ if not isinstance(filings, dict) or not isinstance(filings.get("files"), list):
92
+ raise SchemaMismatchError("historical submissions references missing")
93
+ names = historical_basenames(root, cik)
94
+ observed = len(root_bytes)
95
+ rows: dict[str, dict[str, object]] = {}
96
+ row_count = 0
97
+
98
+ def validate_rows(payload: dict[str, Any], *, child: bool) -> None:
99
+ nonlocal row_count
100
+ for row in _submission_rows(payload, cik, child=child):
101
+ row_count += 1
102
+ if row_count > _ROW_LIMIT:
103
+ raise ResourceLimitError("aggregate filing row limit exceeded")
104
+ accession = row["accessionNumber"]
105
+ if (
106
+ not isinstance(accession, str)
107
+ or re.fullmatch(r"[0-9]{10}-[0-9]{2}-[0-9]{6}", accession) is None
108
+ ):
109
+ raise SchemaMismatchError("invalid accession")
110
+ old = rows.setdefault(accession, row)
111
+ if old != row:
112
+ raise SchemaMismatchError("conflicting submission metadata")
113
+
114
+ validate_rows(root, child=False)
115
+
116
+ def retain(
117
+ endpoint: str, parameters: dict[str, str], url: str, body: bytes
118
+ ) -> SecDiscoverySource:
119
+ fetched_at = utc_now()
120
+ observation = store.observe(
121
+ RequestSpec("sec", endpoint, parameters),
122
+ body,
123
+ fetched_at,
124
+ _SERIALIZATION,
125
+ SnapshotMode.APPEND,
126
+ )
127
+ return SecDiscoverySource(url, observation)
128
+
129
+ root_source = retain("edgar_submissions", {"cik": cik}, root_url, root_bytes)
130
+ children: list[SecDiscoverySource] = []
131
+ for name in names:
132
+ url = historical_submission_url(cik, name)
133
+ body = _fetch(client, url, _TOTAL_LIMIT - observed)
134
+ observed += len(body)
135
+ child = _strict_json(body)
136
+ validate_rows(child, child=True)
137
+ children.append(
138
+ retain("edgar_submissions_history", {"cik": cik, "basename": name}, url, body)
139
+ )
140
+ return SecSubmissionsClosure(cik, root_source, tuple(children))
141
+
142
+
143
+ __all__ = ["SecSubmissionsClosure", "fetch_sec_submissions_closure"]
@@ -491,7 +491,7 @@ wheels = [
491
491
 
492
492
  [[package]]
493
493
  name = "ohmydata"
494
- version = "0.3.2"
494
+ version = "0.3.3"
495
495
  source = { editable = "." }
496
496
 
497
497
  [package.optional-dependencies]
File without changes
File without changes