ohmydata 0.2.3__tar.gz → 0.2.4__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.
- {ohmydata-0.2.3 → ohmydata-0.2.4}/CHANGELOG.md +15 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/PKG-INFO +1 -1
- {ohmydata-0.2.3 → ohmydata-0.2.4}/pyproject.toml +1 -1
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/__init__.py +1 -1
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/__init__.py +2 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/_statement_parser.py +169 -57
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/edgartools_adapter.py +20 -1
- {ohmydata-0.2.3 → ohmydata-0.2.4}/uv.lock +1 -1
- {ohmydata-0.2.3 → ohmydata-0.2.4}/.gitignore +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/README.md +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/adapters/__init__.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/adapters/polars.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/cli.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/__init__.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/_vintage_lock.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/availability.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/errors.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/facts.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/policy.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/provenance.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/rate_limit.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/snapshot.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/specs.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/core/vintage.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/__init__.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/artifacts.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/batch.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/cli.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/core_dataset.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/edgar.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/endpoints.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/errors.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/financials.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/financials_dataset.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/http.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/nport.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/qualification.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/sec/qualification_dataset.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/__init__.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/client.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/endpoints.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/errors.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/observations/__init__.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/observations/capture.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/observations/serialization.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/__init__.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/etf_adjusted_bars.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/etf_index_mapping.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/etf_pcf_history.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/index_weight_vintage.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/lookthrough_bundle.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/shared.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/weighted_dividend_yield.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/vintage_artifacts.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/vintage_canonical.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/vintage_gates.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/vintage_plane.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/vintage_producer.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/yfinance/__init__.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/yfinance/_fundamentals_parsers.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/yfinance/_fundamentals_periods.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/yfinance/client.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/yfinance/endpoints.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/yfinance/errors.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/yfinance/fundamentals.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/yfinance/quality.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/tools/__init__.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/tools/cli.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/tools/drift_audit.py +0 -0
- {ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/tools/universes.py +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.4 — 2026-09-09
|
|
4
|
+
|
|
5
|
+
- Fix the SEC native-fact adapter for edgartools 5.56.0: `XBRL.facts` is a
|
|
6
|
+
`FactsView`, while original `Fact` objects belong to `XBRL.parser.facts`.
|
|
7
|
+
Regression fixtures now use actual XBRL/FactsView/Statement objects.
|
|
8
|
+
- A selected filing with zero rows and parsing failures now raises
|
|
9
|
+
`SecFinancialsParseError`; its `vintage` retains coverage/accession and its
|
|
10
|
+
cause retains the first failure. Missing/empty filings without parser failures
|
|
11
|
+
and explicitly flagged partial coverage retain their existing behavior.
|
|
12
|
+
See [the financials contract](docs/financial-period-integrity.md).
|
|
13
|
+
- Native SEC duplicate facts now use exact Decimal precision intervals and retain
|
|
14
|
+
the highest precision original fact per context, while rejecting true value or
|
|
15
|
+
unit conflicts, mixed known/unknown precision, and arithmetic inputs beyond
|
|
16
|
+
the bounded 10,000-digit guard.
|
|
17
|
+
|
|
3
18
|
## 0.2.3 — 2026-09-09
|
|
4
19
|
|
|
5
20
|
- Repair SEC financial-period extraction and filing eligibility in the OMD adapter
|
|
@@ -36,6 +36,7 @@ from .edgar import (
|
|
|
36
36
|
)
|
|
37
37
|
from .edgartools_adapter import (
|
|
38
38
|
SecFinancialsClient,
|
|
39
|
+
SecFinancialsParseError,
|
|
39
40
|
SecStatementParseError,
|
|
40
41
|
ensure_edgar_available,
|
|
41
42
|
parse_statement_rows,
|
|
@@ -90,6 +91,7 @@ __all__ = [
|
|
|
90
91
|
"SecEmptyPolicy",
|
|
91
92
|
"SecEquityEtfUniverse",
|
|
92
93
|
"SecFinancialsClient",
|
|
94
|
+
"SecFinancialsParseError",
|
|
93
95
|
"SecFinancialsRequest",
|
|
94
96
|
"SecFundHoldingVintage",
|
|
95
97
|
"SecFundSelector",
|
|
@@ -5,9 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
7
|
from collections.abc import Mapping
|
|
8
|
-
from dataclasses import replace
|
|
9
8
|
from datetime import date
|
|
10
9
|
from decimal import Decimal, InvalidOperation
|
|
10
|
+
from fractions import Fraction
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from .financials import SecStatementRow, StatementType
|
|
@@ -16,6 +16,7 @@ _DISPLAY_PERIOD = re.compile(r"^(\d{4}-\d{2}-\d{2})(?:\s+\((FY|Q[1-4]|YTD)\))?$"
|
|
|
16
16
|
_STRUCTURED_PERIOD = re.compile(
|
|
17
17
|
r"^(?:(instant)_(\d{4}-\d{2}-\d{2})|(duration)_(\d{4}-\d{2}-\d{2})_(\d{4}-\d{2}-\d{2}))$"
|
|
18
18
|
)
|
|
19
|
+
_MAX_FACT_ARITHMETIC_DIGITS = 10_000
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class SecStatementParseError(ValueError):
|
|
@@ -92,11 +93,122 @@ def _dim_key(dimensions: Mapping[str, Any]) -> str:
|
|
|
92
93
|
return json.dumps(dict(dimensions), sort_keys=True, separators=(",", ":"))
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
def _fact_precision(fact: Any) -> int | str | None:
|
|
97
|
+
"""Return finite precision, None for missing, and ``INF`` for infinity."""
|
|
98
|
+
decimals = fact.decimals
|
|
99
|
+
if decimals is None or type(decimals).__name__ in {"NAType", "NaTType"}:
|
|
100
|
+
return None
|
|
101
|
+
if isinstance(decimals, str) and decimals.strip() == "INF":
|
|
102
|
+
return "INF"
|
|
103
|
+
if isinstance(decimals, bool):
|
|
104
|
+
raise SecStatementParseError("invalid fact precision")
|
|
105
|
+
if isinstance(decimals, int):
|
|
106
|
+
return decimals
|
|
107
|
+
if isinstance(decimals, str) and re.fullmatch(r"[+-]?\d+", decimals.strip()):
|
|
108
|
+
return int(decimals.strip())
|
|
109
|
+
raise SecStatementParseError("invalid fact precision")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _fact_interval(value: Decimal, precision: int | str | None) -> tuple[Fraction, Fraction]:
|
|
113
|
+
value_tuple = value.as_tuple()
|
|
114
|
+
if not isinstance(value_tuple.exponent, int):
|
|
115
|
+
raise SecStatementParseError("non-finite statement fact")
|
|
116
|
+
if (
|
|
117
|
+
abs(value_tuple.exponent) > _MAX_FACT_ARITHMETIC_DIGITS
|
|
118
|
+
or len(value_tuple.digits) > _MAX_FACT_ARITHMETIC_DIGITS
|
|
119
|
+
):
|
|
120
|
+
raise SecStatementParseError("statement fact exceeds arithmetic bounds")
|
|
121
|
+
if isinstance(precision, int) and abs(precision) > _MAX_FACT_ARITHMETIC_DIGITS:
|
|
122
|
+
raise SecStatementParseError("statement fact exceeds arithmetic bounds")
|
|
123
|
+
exact = Fraction(value)
|
|
124
|
+
if precision is None or precision == "INF":
|
|
125
|
+
return exact, exact
|
|
126
|
+
if not isinstance(precision, int):
|
|
127
|
+
raise SecStatementParseError("invalid fact precision")
|
|
128
|
+
half_unit = Fraction(10 ** (-precision), 2) if precision < 0 else Fraction(1, 2 * 10**precision)
|
|
129
|
+
return exact - half_unit, exact + half_unit
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _select_native_facts(facts: list[Any]) -> list[Any]:
|
|
133
|
+
"""Validate one duplicate group and select its best fact per context."""
|
|
134
|
+
parsed: list[tuple[Any, Decimal, int | str | None]] = []
|
|
135
|
+
unit_refs = set()
|
|
136
|
+
intervals = []
|
|
137
|
+
missing_precision = False
|
|
138
|
+
values_by_precision: dict[int | str, set[Decimal]] = {}
|
|
139
|
+
best_by_context: dict[str, tuple[tuple[int, int], tuple[str, str], Any]] = {}
|
|
140
|
+
|
|
141
|
+
def rank(precision: int | str | None) -> tuple[int, int]:
|
|
142
|
+
if precision is None:
|
|
143
|
+
return (0, 0)
|
|
144
|
+
if precision == "INF":
|
|
145
|
+
return (2, 0)
|
|
146
|
+
if not isinstance(precision, int):
|
|
147
|
+
raise SecStatementParseError("invalid fact precision")
|
|
148
|
+
return (1, precision)
|
|
149
|
+
|
|
150
|
+
for fact in facts:
|
|
151
|
+
value = _number(fact.value)
|
|
152
|
+
precision = _fact_precision(fact)
|
|
153
|
+
missing_precision |= precision is None
|
|
154
|
+
unit_refs.add(fact.unit_ref)
|
|
155
|
+
if value is None:
|
|
156
|
+
continue
|
|
157
|
+
parsed.append((fact, value, precision))
|
|
158
|
+
intervals.append(_fact_interval(value, precision))
|
|
159
|
+
if precision is not None:
|
|
160
|
+
values_by_precision.setdefault(precision, set()).add(value)
|
|
161
|
+
context = fact.context_ref
|
|
162
|
+
tie_break = (str(fact.value), str(fact.decimals))
|
|
163
|
+
candidate = (rank(precision), tie_break, fact)
|
|
164
|
+
current = best_by_context.get(context)
|
|
165
|
+
if (
|
|
166
|
+
current is None
|
|
167
|
+
or candidate[0] > current[0]
|
|
168
|
+
or (candidate[0] == current[0] and candidate[1] < current[1])
|
|
169
|
+
):
|
|
170
|
+
best_by_context[context] = candidate
|
|
171
|
+
if len(unit_refs) != 1:
|
|
172
|
+
raise SecStatementParseError("conflicting native facts for period and dimensions")
|
|
173
|
+
if not parsed:
|
|
174
|
+
return []
|
|
175
|
+
if len(parsed) != len(facts):
|
|
176
|
+
raise SecStatementParseError("non-numeric statement fact")
|
|
177
|
+
# Every interval must share one common point; pairwise or adjacent checks
|
|
178
|
+
# can incorrectly accept a chain of individually overlapping intervals.
|
|
179
|
+
lower = max(interval[0] for interval in intervals)
|
|
180
|
+
upper = min(interval[1] for interval in intervals)
|
|
181
|
+
if missing_precision and any(precision is not None for _, _, precision in parsed):
|
|
182
|
+
raise SecStatementParseError("conflicting native facts for period and dimensions")
|
|
183
|
+
if missing_precision and len({value for _, value, _ in parsed}) != 1:
|
|
184
|
+
raise SecStatementParseError("conflicting native facts for period and dimensions")
|
|
185
|
+
for values in values_by_precision.values():
|
|
186
|
+
if len(values) != 1:
|
|
187
|
+
raise SecStatementParseError("conflicting native facts for period and dimensions")
|
|
188
|
+
if lower > upper:
|
|
189
|
+
raise SecStatementParseError("conflicting native facts for period and dimensions")
|
|
190
|
+
selected: list[Any] = []
|
|
191
|
+
for context_ref in sorted(best_by_context):
|
|
192
|
+
selected.append(best_by_context[context_ref][2])
|
|
193
|
+
return selected
|
|
194
|
+
|
|
195
|
+
|
|
95
196
|
def _native_index(xbrl: Any, concepts: set[str]) -> dict[tuple[str, str, str], list[Any]]:
|
|
96
197
|
"""One pass over facts, avoiding a full source scan for each row or period."""
|
|
198
|
+
# Edgar 5.56 XBRL.facts is FactsView, an enriched query interface whose
|
|
199
|
+
# get_facts() may rewrite concept identifiers. The raw Fact objects are
|
|
200
|
+
# owned by XBRLParser.facts (also exposed upstream as XBRL._facts).
|
|
201
|
+
# Keep this version-specific boundary explicit: never use display values
|
|
202
|
+
# or the enriched view as a fallback for unavailable native facts.
|
|
203
|
+
try:
|
|
204
|
+
native_facts = xbrl.parser.facts
|
|
205
|
+
except AttributeError as exc:
|
|
206
|
+
raise SecStatementParseError("native parser fact mapping unavailable") from exc
|
|
207
|
+
if not isinstance(native_facts, Mapping):
|
|
208
|
+
raise SecStatementParseError("native parser facts must be a mapping")
|
|
97
209
|
result: dict[tuple[str, str, str], list[Any]] = {}
|
|
98
210
|
seen: set[int] = set()
|
|
99
|
-
for fact in
|
|
211
|
+
for fact in native_facts.values():
|
|
100
212
|
if id(fact) in seen:
|
|
101
213
|
continue
|
|
102
214
|
seen.add(id(fact))
|
|
@@ -156,70 +268,70 @@ def _structured_rows(
|
|
|
156
268
|
unit_ref = (item.get("units") or {}).get(key)
|
|
157
269
|
native_decimals = (item.get("decimals") or {}).get(key)
|
|
158
270
|
context_ref = None
|
|
159
|
-
context_refs: list[str | None] = [None]
|
|
160
271
|
if index is not None:
|
|
161
272
|
facts = index.get((concept.replace(":", "_"), key, _dim_key(dims)), [])
|
|
162
273
|
if not facts:
|
|
163
274
|
raise SecStatementParseError("statement value has no native fact context")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
275
|
+
selected_facts = _select_native_facts(facts)
|
|
276
|
+
else:
|
|
277
|
+
selected_facts = [None]
|
|
278
|
+
for selected_fact in selected_facts:
|
|
279
|
+
selected_raw = raw
|
|
280
|
+
selected_unit_ref = unit_ref
|
|
281
|
+
selected_decimals = native_decimals
|
|
282
|
+
if selected_fact is not None:
|
|
283
|
+
selected_raw = selected_fact.value
|
|
284
|
+
selected_unit_ref = selected_fact.unit_ref
|
|
285
|
+
selected_decimals = selected_fact.decimals
|
|
286
|
+
context_ref = selected_fact.context_ref
|
|
287
|
+
value = _number(selected_raw)
|
|
288
|
+
if value is None:
|
|
289
|
+
continue
|
|
290
|
+
unit = _text(selected_unit_ref) or _text(item.get("unit"))
|
|
291
|
+
if xbrl is not None and selected_unit_ref:
|
|
292
|
+
definition = xbrl.units.get(selected_unit_ref)
|
|
293
|
+
if definition is None:
|
|
294
|
+
unit = None
|
|
295
|
+
elif isinstance(definition, dict):
|
|
296
|
+
unit = _text(definition.get("measure")) or json.dumps(
|
|
297
|
+
definition, sort_keys=True
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
raise SecStatementParseError("invalid native unit definition")
|
|
301
|
+
decimals = None
|
|
302
|
+
if selected_decimals is not None and str(selected_decimals).strip() != "INF":
|
|
303
|
+
try:
|
|
304
|
+
decimals = int(str(selected_decimals).strip())
|
|
305
|
+
except (TypeError, ValueError) as exc:
|
|
306
|
+
raise SecStatementParseError("invalid fact precision") from exc
|
|
307
|
+
row = SecStatementRow(
|
|
308
|
+
statement_type=kind,
|
|
309
|
+
standard_concept=_text(item.get("standard_concept")) or concept,
|
|
310
|
+
concept=concept,
|
|
311
|
+
label=_text(item.get("label")) or "",
|
|
312
|
+
value=value,
|
|
313
|
+
value_native=str(selected_raw),
|
|
314
|
+
unit=unit,
|
|
315
|
+
unit_ref=_text(selected_unit_ref),
|
|
316
|
+
decimals=decimals,
|
|
317
|
+
decimals_native=_text(selected_decimals),
|
|
318
|
+
period_start=start,
|
|
319
|
+
period_end=end,
|
|
320
|
+
period_type=period_type,
|
|
321
|
+
period_key=str(key),
|
|
322
|
+
context_ref=context_ref,
|
|
323
|
+
dimension=_dim_key(dims) if dims else None,
|
|
324
|
+
is_point_in_time=period_type == "instant",
|
|
325
|
+
period_source="xbrl-context" if index is not None else "structured-period-key",
|
|
326
|
+
)
|
|
327
|
+
identity = concept, str(key), _dim_key(dims), context_ref
|
|
216
328
|
if identity in seen:
|
|
217
329
|
previous = seen[identity]
|
|
218
330
|
if (previous.value, previous.unit) != (row.value, row.unit):
|
|
219
331
|
raise SecStatementParseError("conflicting duplicate statement facts")
|
|
220
332
|
continue
|
|
221
|
-
seen[identity] =
|
|
222
|
-
rows.append(
|
|
333
|
+
seen[identity] = row
|
|
334
|
+
rows.append(row)
|
|
223
335
|
return rows
|
|
224
336
|
|
|
225
337
|
|
|
@@ -17,6 +17,18 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
_EASTERN_TZ = ZoneInfo("America/New_York")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class SecFinancialsParseError(SecStatementParseError):
|
|
21
|
+
"""A selected filing produced no financial rows because parsing failed.
|
|
22
|
+
|
|
23
|
+
``vintage`` retains accession and coverage flags; ``__cause__`` retains
|
|
24
|
+
the first parser failure. Neither is replaced by a different filing.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, vintage: SecCompanyFinancialVintage) -> None:
|
|
28
|
+
super().__init__("selected SEC filing produced no financial rows because parsing failed")
|
|
29
|
+
self.vintage = vintage
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
def ensure_edgar_available() -> None:
|
|
21
33
|
"""Require the optional Edgar financials dependency."""
|
|
22
34
|
try:
|
|
@@ -179,14 +191,16 @@ class SecFinancialsClient:
|
|
|
179
191
|
|
|
180
192
|
# Parse the report object (TenK, TenQ, etc.)
|
|
181
193
|
quality_flags: list[str] = []
|
|
194
|
+
parse_failures: list[Exception] = []
|
|
182
195
|
if accepted_at is None:
|
|
183
196
|
quality_flags.append("ACCEPTED_AT_MISSING")
|
|
184
197
|
try:
|
|
185
198
|
report: Any = filing.obj()
|
|
186
199
|
except (AttributeError, KeyError, ValueError, TypeError, OSError) as err:
|
|
187
|
-
logger.debug("Could not parse filing obj: %s", err)
|
|
200
|
+
logger.debug("Could not parse filing obj: %s", type(err).__name__)
|
|
188
201
|
report = None
|
|
189
202
|
quality_flags.append("FILING_PARSE_FAILED")
|
|
203
|
+
parse_failures.append(err)
|
|
190
204
|
|
|
191
205
|
try:
|
|
192
206
|
fin: Any = getattr(report, "financials", None) if report is not None else None
|
|
@@ -201,6 +215,7 @@ class SecFinancialsClient:
|
|
|
201
215
|
logger.debug("Could not access filing financials: %s", type(err).__name__)
|
|
202
216
|
fin = None
|
|
203
217
|
quality_flags.append("FINANCIALS_PARSE_FAILED")
|
|
218
|
+
parse_failures.append(err)
|
|
204
219
|
if fin is None:
|
|
205
220
|
quality_flags.append("NO_FINANCIALS_OBJECT")
|
|
206
221
|
|
|
@@ -229,6 +244,7 @@ class SecFinancialsClient:
|
|
|
229
244
|
"Could not extract %s: %s", statement_type, type(err).__name__
|
|
230
245
|
)
|
|
231
246
|
quality_flags.append(f"{flag}_PARSE_FAILED")
|
|
247
|
+
parse_failures.append(err)
|
|
232
248
|
|
|
233
249
|
if not rows:
|
|
234
250
|
quality_flags.append("NO_FINANCIAL_STATEMENTS")
|
|
@@ -260,6 +276,8 @@ class SecFinancialsClient:
|
|
|
260
276
|
quality_flags=tuple(quality_flags),
|
|
261
277
|
rows=tuple(rows),
|
|
262
278
|
)
|
|
279
|
+
if not rows and parse_failures:
|
|
280
|
+
raise SecFinancialsParseError(vintage) from parse_failures[0]
|
|
263
281
|
vintages.append(vintage)
|
|
264
282
|
|
|
265
283
|
return vintages
|
|
@@ -267,6 +285,7 @@ class SecFinancialsClient:
|
|
|
267
285
|
|
|
268
286
|
__all__ = [
|
|
269
287
|
"SecFinancialsClient",
|
|
288
|
+
"SecFinancialsParseError",
|
|
270
289
|
"SecStatementParseError",
|
|
271
290
|
"ensure_edgar_available",
|
|
272
291
|
"parse_statement_rows",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/observations/serialization.py
RENAMED
|
File without changes
|
|
File without changes
|
{ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/etf_adjusted_bars.py
RENAMED
|
File without changes
|
{ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/etf_index_mapping.py
RENAMED
|
File without changes
|
|
File without changes
|
{ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/index_weight_vintage.py
RENAMED
|
File without changes
|
{ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/lookthrough_bundle.py
RENAMED
|
File without changes
|
|
File without changes
|
{ohmydata-0.2.3 → ohmydata-0.2.4}/src/ohmydata/providers/tushare/recipes/weighted_dividend_yield.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|