eba-xbridge 1.5.0rc5__py3-none-any.whl → 1.5.0rc6__py3-none-any.whl
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.
- {eba_xbridge-1.5.0rc5.dist-info → eba_xbridge-1.5.0rc6.dist-info}/METADATA +50 -1
- {eba_xbridge-1.5.0rc5.dist-info → eba_xbridge-1.5.0rc6.dist-info}/RECORD +9 -8
- xbridge/__init__.py +1 -1
- xbridge/converter.py +37 -23
- xbridge/exceptions.py +33 -0
- xbridge/instance.py +44 -26
- {eba_xbridge-1.5.0rc5.dist-info → eba_xbridge-1.5.0rc6.dist-info}/WHEEL +0 -0
- {eba_xbridge-1.5.0rc5.dist-info → eba_xbridge-1.5.0rc6.dist-info}/entry_points.txt +0 -0
- {eba_xbridge-1.5.0rc5.dist-info → eba_xbridge-1.5.0rc6.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eba-xbridge
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.0rc6
|
|
4
4
|
Summary: XBRL-XML to XBRL-CSV converter for EBA Taxonomy (version 4.1)
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -154,6 +154,55 @@ Customize the conversion with additional parameters:
|
|
|
154
154
|
strict_validation=False, # Emit warnings instead of errors for orphaned facts
|
|
155
155
|
)
|
|
156
156
|
|
|
157
|
+
Python API - Handling Warnings
|
|
158
|
+
------------------------------
|
|
159
|
+
|
|
160
|
+
XBridge emits structured warnings that can be filtered or turned into errors from your code.
|
|
161
|
+
The most common ones are:
|
|
162
|
+
|
|
163
|
+
* ``IdentifierPrefixWarning``: Unknown entity identifier prefix; XBridge falls back to ``rs``.
|
|
164
|
+
* ``FilingIndicatorWarning``: Filing indicator inconsistencies; some facts are excluded.
|
|
165
|
+
|
|
166
|
+
To capture these warnings when using ``convert_instance``:
|
|
167
|
+
|
|
168
|
+
.. code-block:: python
|
|
169
|
+
|
|
170
|
+
import warnings
|
|
171
|
+
from xbridge.api import convert_instance
|
|
172
|
+
from xbridge.exceptions import XbridgeWarning, FilingIndicatorWarning
|
|
173
|
+
|
|
174
|
+
input_path = "path/to/instance.xbrl"
|
|
175
|
+
output_path = "path/to/output"
|
|
176
|
+
|
|
177
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
178
|
+
# Ensure all xbridge warnings are captured
|
|
179
|
+
warnings.simplefilter("always", XbridgeWarning)
|
|
180
|
+
|
|
181
|
+
zip_path = convert_instance(
|
|
182
|
+
instance_path=input_path,
|
|
183
|
+
output_path=output_path,
|
|
184
|
+
validate_filing_indicators=True,
|
|
185
|
+
strict_validation=False, # Warnings instead of errors for orphaned facts
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
filing_warnings = [
|
|
189
|
+
w for w in caught if issubclass(w.category, FilingIndicatorWarning)
|
|
190
|
+
]
|
|
191
|
+
for w in filing_warnings:
|
|
192
|
+
print(f"Filing indicator warning: {w.message}")
|
|
193
|
+
|
|
194
|
+
To treat all XBridge warnings as errors:
|
|
195
|
+
|
|
196
|
+
.. code-block:: python
|
|
197
|
+
|
|
198
|
+
import warnings
|
|
199
|
+
from xbridge.api import convert_instance
|
|
200
|
+
from xbridge.exceptions import XbridgeWarning
|
|
201
|
+
|
|
202
|
+
with warnings.catch_warnings():
|
|
203
|
+
warnings.simplefilter("error", XbridgeWarning)
|
|
204
|
+
convert_instance("path/to/instance.xbrl", "path/to/output")
|
|
205
|
+
|
|
157
206
|
Loading an Instance
|
|
158
207
|
-------------------
|
|
159
208
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
xbridge/__init__.py,sha256=
|
|
1
|
+
xbridge/__init__.py,sha256=OJUYuCUHGkgWxo9tS6dntxUteSpYkfqo5Weaad1X56M,68
|
|
2
2
|
xbridge/__main__.py,sha256=trtFEv7TRJgrLL84leIapPvgC_iVTj05qLHRRS1Olts,2219
|
|
3
3
|
xbridge/api.py,sha256=NCBz7VRJWE3gID6ndgL4Awoxw0w1yMIIf_OTLRuZyyQ,1559
|
|
4
|
-
xbridge/converter.py,sha256=
|
|
5
|
-
xbridge/
|
|
4
|
+
xbridge/converter.py,sha256=BgmJY06yuB9aLVhcf8OvVZphbQYP3w66RKi3ViZ3LDA,24571
|
|
5
|
+
xbridge/exceptions.py,sha256=C9fjO8nUGVL376qiyQ1vAozafCWP4OfIbuujpXtNPeQ,1002
|
|
6
|
+
xbridge/instance.py,sha256=8cCGKxgtmdP9-1c2Nz4TdhsXw5dtRrykwp9rAXD4-Ew,32863
|
|
6
7
|
xbridge/modules/ae_ae_4.2.json,sha256=AdFvwZqX0KVP3jF1iHeQc5QSnSMvvT3GvoA2G1AgXis,460165
|
|
7
8
|
xbridge/modules/ae_con_cir-680-2014_2017-04-04.json,sha256=4n0t9dKJNU8Nb5QHpssrDs8ZLwzI-Mw75ax-ar9pLu0,363273
|
|
8
9
|
xbridge/modules/ae_con_cir-680-2014_2018-03-31.json,sha256=aVWeLLs20p39kQQUthUzqrxBGKTycqhgX9WLk1rVlNw,363538
|
|
@@ -383,8 +384,8 @@ xbridge/modules/sepa_ipr_pay_4.2.json,sha256=JLJvR02LOAJy6SWPRuhV1TT02oXQhsG83FB
|
|
|
383
384
|
xbridge/modules.py,sha256=bTvBXtp3w4Gad2DpEQE7Hb-UfuUQLlRl8gywRstQtpU,22399
|
|
384
385
|
xbridge/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
385
386
|
xbridge/taxonomy_loader.py,sha256=K0lnJVryvkKsaoK3fMis-L2JpmwLO6z3Ruq3yj9FxDY,9317
|
|
386
|
-
eba_xbridge-1.5.
|
|
387
|
-
eba_xbridge-1.5.
|
|
388
|
-
eba_xbridge-1.5.
|
|
389
|
-
eba_xbridge-1.5.
|
|
390
|
-
eba_xbridge-1.5.
|
|
387
|
+
eba_xbridge-1.5.0rc6.dist-info/METADATA,sha256=1lyxX7bu8C3L0XVF1yEjEUMSRiyIgEL-ncJFr41uzw8,12053
|
|
388
|
+
eba_xbridge-1.5.0rc6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
389
|
+
eba_xbridge-1.5.0rc6.dist-info/entry_points.txt,sha256=FATct4icSewM04cegjhybtm7xcQWhaSahL-DTtuFdZw,49
|
|
390
|
+
eba_xbridge-1.5.0rc6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
391
|
+
eba_xbridge-1.5.0rc6.dist-info/RECORD,,
|
xbridge/__init__.py
CHANGED
xbridge/converter.py
CHANGED
|
@@ -15,6 +15,7 @@ from zipfile import ZipFile
|
|
|
15
15
|
|
|
16
16
|
import pandas as pd
|
|
17
17
|
|
|
18
|
+
from xbridge.exceptions import DecimalValueError, FilingIndicatorWarning
|
|
18
19
|
from xbridge.instance import CsvInstance, Instance, XmlInstance
|
|
19
20
|
from xbridge.modules import Module, Table
|
|
20
21
|
|
|
@@ -376,31 +377,23 @@ class Converter:
|
|
|
376
377
|
|
|
377
378
|
data_type = row["data_type"][1:]
|
|
378
379
|
decimals = row["decimals"]
|
|
380
|
+
normalized_decimals = self._normalize_decimals_value(decimals)
|
|
379
381
|
|
|
380
382
|
if data_type not in self._decimals_parameters:
|
|
381
|
-
self._decimals_parameters[data_type] =
|
|
382
|
-
int(decimals) if decimals not in {"INF", "#none"} else decimals
|
|
383
|
-
)
|
|
383
|
+
self._decimals_parameters[data_type] = normalized_decimals
|
|
384
384
|
else:
|
|
385
|
-
#
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
# If existing value is special, replace with numeric
|
|
399
|
-
if (
|
|
400
|
-
self._decimals_parameters[data_type] in {"INF", "#none"}
|
|
401
|
-
or decimals < self._decimals_parameters[data_type]
|
|
402
|
-
):
|
|
403
|
-
self._decimals_parameters[data_type] = decimals
|
|
385
|
+
# Skip special values when we already have an entry,
|
|
386
|
+
# as numeric values take precedence.
|
|
387
|
+
if normalized_decimals in {"INF", "#none"}:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
existing_value = self._decimals_parameters[data_type]
|
|
391
|
+
if existing_value in {"INF", "#none"} or (
|
|
392
|
+
isinstance(existing_value, int)
|
|
393
|
+
and isinstance(normalized_decimals, int)
|
|
394
|
+
and normalized_decimals < existing_value
|
|
395
|
+
):
|
|
396
|
+
self._decimals_parameters[data_type] = normalized_decimals
|
|
404
397
|
|
|
405
398
|
drop_columns = merge_cols + ["data_type", "decimals"]
|
|
406
399
|
else:
|
|
@@ -422,6 +415,23 @@ class Converter:
|
|
|
422
415
|
|
|
423
416
|
return table_df
|
|
424
417
|
|
|
418
|
+
def _normalize_decimals_value(self, decimals: Any) -> Union[int, str]:
|
|
419
|
+
"""Return a validated decimals value or raise a DecimalValueError."""
|
|
420
|
+
candidate = decimals
|
|
421
|
+
if isinstance(candidate, str):
|
|
422
|
+
candidate = candidate.strip()
|
|
423
|
+
|
|
424
|
+
if candidate in {"INF", "#none"}:
|
|
425
|
+
return candidate
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
return int(candidate)
|
|
429
|
+
except (TypeError, ValueError) as exc:
|
|
430
|
+
raise DecimalValueError(
|
|
431
|
+
f"Invalid decimals value: {decimals}, should be integer, 'INF' or '#none'",
|
|
432
|
+
offending_value=decimals,
|
|
433
|
+
) from exc
|
|
434
|
+
|
|
425
435
|
def _convert_tables(
|
|
426
436
|
self,
|
|
427
437
|
temp_dir_path: Path,
|
|
@@ -557,7 +567,11 @@ class Converter:
|
|
|
557
567
|
"Consider setting filed=true for the relevant tables "
|
|
558
568
|
"or removing these facts from the XML."
|
|
559
569
|
)
|
|
560
|
-
warnings.warn(
|
|
570
|
+
warnings.warn(
|
|
571
|
+
error_msg,
|
|
572
|
+
category=FilingIndicatorWarning,
|
|
573
|
+
stacklevel=2,
|
|
574
|
+
)
|
|
561
575
|
|
|
562
576
|
def _convert_parameters(self, temp_dir_path: Path) -> None:
|
|
563
577
|
# Workaround;
|
xbridge/exceptions.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Custom exception types for the xbridge package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SchemaRefValueError(ValueError):
|
|
9
|
+
"""Raised when schemaRef validation fails in an XBRL instance."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, error_message: str, offending_value: Optional[Any] = None) -> None:
|
|
12
|
+
super().__init__(error_message)
|
|
13
|
+
self.offending_value = offending_value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DecimalValueError(ValueError):
|
|
17
|
+
"""Raised when decimals metadata contains unsupported values."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, error_message: str, offending_value: Optional[Any] = None) -> None:
|
|
20
|
+
super().__init__(error_message)
|
|
21
|
+
self.offending_value = offending_value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class XbridgeWarning(Warning):
|
|
25
|
+
"""Base warning for the xbridge library."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IdentifierPrefixWarning(XbridgeWarning):
|
|
29
|
+
"""Unknown identifier prefix; defaulting to 'rs'."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FilingIndicatorWarning(XbridgeWarning):
|
|
33
|
+
"""Facts orphaned by filing indicators; some are excluded."""
|
xbridge/instance.py
CHANGED
|
@@ -13,6 +13,8 @@ from zipfile import ZipFile
|
|
|
13
13
|
import pandas as pd
|
|
14
14
|
from lxml import etree
|
|
15
15
|
|
|
16
|
+
from xbridge.exceptions import IdentifierPrefixWarning, SchemaRefValueError
|
|
17
|
+
|
|
16
18
|
# Cache namespace → CSV prefix derivations to avoid repeated string work during parse
|
|
17
19
|
_namespace_prefix_cache: Dict[str, str] = {}
|
|
18
20
|
|
|
@@ -110,9 +112,7 @@ def _normalize_namespaced_value(
|
|
|
110
112
|
return value
|
|
111
113
|
|
|
112
114
|
|
|
113
|
-
def _normalize_metric_value(
|
|
114
|
-
value: Optional[str], nsmap: Dict[Optional[str], str]
|
|
115
|
-
) -> Optional[str]:
|
|
115
|
+
def _normalize_metric_value(value: Optional[str], nsmap: Dict[Optional[str], str]) -> Optional[str]:
|
|
116
116
|
"""
|
|
117
117
|
Normalize a metric namespaced value to the CSV prefix convention.
|
|
118
118
|
For metrics, we preserve version suffixes (e.g., eba_met_3.5, eba_met_4.0).
|
|
@@ -181,6 +181,7 @@ class Instance:
|
|
|
181
181
|
self._contexts: Optional[Dict[str, Context]] = None
|
|
182
182
|
self._facts: Optional[List[Fact]] = None
|
|
183
183
|
self._facts_list_dict: Optional[List[Dict[str, Any]]] = None
|
|
184
|
+
self._df: Optional[pd.DataFrame] = None
|
|
184
185
|
self._table_files: Optional[set[Path]] = None
|
|
185
186
|
self._root_folder: Optional[str] = None
|
|
186
187
|
self._report_file: Optional[Path] = None
|
|
@@ -270,7 +271,7 @@ class Instance:
|
|
|
270
271
|
"""
|
|
271
272
|
if self.facts_list_dict is None:
|
|
272
273
|
return
|
|
273
|
-
df = pd.DataFrame
|
|
274
|
+
df = pd.DataFrame(self.facts_list_dict)
|
|
274
275
|
df_columns = list(df.columns)
|
|
275
276
|
##Workaround
|
|
276
277
|
# Dropping period an entity columns because in current EBA architecture,
|
|
@@ -296,7 +297,9 @@ class Instance:
|
|
|
296
297
|
(
|
|
297
298
|
f"{self._identifier_prefix} is not a known identifier prefix. "
|
|
298
299
|
"Default 'rs' will be used."
|
|
299
|
-
)
|
|
300
|
+
),
|
|
301
|
+
category=IdentifierPrefixWarning,
|
|
302
|
+
stacklevel=2,
|
|
300
303
|
)
|
|
301
304
|
return "rs"
|
|
302
305
|
|
|
@@ -354,16 +357,17 @@ class Instance:
|
|
|
354
357
|
raise AttributeError("XML root not loaded.")
|
|
355
358
|
|
|
356
359
|
contexts: Dict[str, Context] = {}
|
|
360
|
+
namespaces: Dict[str, str] = {key or "": value for key, value in self.namespaces.items()}
|
|
357
361
|
for context in self.root.findall(
|
|
358
362
|
"{http://www.xbrl.org/2003/instance}context",
|
|
359
|
-
|
|
363
|
+
namespaces,
|
|
360
364
|
):
|
|
361
365
|
context_object = Context(context)
|
|
362
366
|
contexts[context_object.id] = context_object
|
|
363
367
|
|
|
364
368
|
self._contexts = contexts
|
|
365
369
|
|
|
366
|
-
first_ctx = self.root.find("{http://www.xbrl.org/2003/instance}context",
|
|
370
|
+
first_ctx = self.root.find("{http://www.xbrl.org/2003/instance}context", namespaces)
|
|
367
371
|
if first_ctx is not None:
|
|
368
372
|
entity_elem = first_ctx.find("{http://www.xbrl.org/2003/instance}entity")
|
|
369
373
|
if entity_elem is not None:
|
|
@@ -406,7 +410,8 @@ class Instance:
|
|
|
406
410
|
href_attr = "{http://www.w3.org/1999/xlink}href"
|
|
407
411
|
if href_attr not in child.attrib:
|
|
408
412
|
continue
|
|
409
|
-
|
|
413
|
+
raw_value = child.attrib[href_attr]
|
|
414
|
+
value = str(raw_value)
|
|
410
415
|
schema_refs.append(value)
|
|
411
416
|
|
|
412
417
|
# Validate that only one schemaRef exists
|
|
@@ -414,11 +419,14 @@ class Instance:
|
|
|
414
419
|
return # No schema reference found, module_ref will remain None
|
|
415
420
|
|
|
416
421
|
if len(schema_refs) > 1:
|
|
417
|
-
raise
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
+
raise SchemaRefValueError(
|
|
423
|
+
(
|
|
424
|
+
"Multiple schemaRef elements found in the XBRL instance. "
|
|
425
|
+
f"Only one schemaRef is expected, but {len(schema_refs)} "
|
|
426
|
+
f"were found: {schema_refs}. "
|
|
427
|
+
"This may indicate an invalid XBRL-XML file."
|
|
428
|
+
),
|
|
429
|
+
offending_value=schema_refs,
|
|
422
430
|
)
|
|
423
431
|
|
|
424
432
|
# Process the single schema reference
|
|
@@ -427,25 +435,34 @@ class Instance:
|
|
|
427
435
|
|
|
428
436
|
# Validate href format and extract module code
|
|
429
437
|
if "/mod/" not in value:
|
|
430
|
-
raise
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
438
|
+
raise SchemaRefValueError(
|
|
439
|
+
(
|
|
440
|
+
"Invalid href format in schemaRef. Expected href to contain '/mod/' "
|
|
441
|
+
f"but got: '{value}'. Please verify the XBRL-XML file contains a "
|
|
442
|
+
"valid schema reference."
|
|
443
|
+
),
|
|
444
|
+
offending_value=value,
|
|
434
445
|
)
|
|
435
446
|
|
|
436
447
|
split_parts = value.split("/mod/")
|
|
437
448
|
if len(split_parts) < 2:
|
|
438
|
-
raise
|
|
439
|
-
|
|
440
|
-
|
|
449
|
+
raise SchemaRefValueError(
|
|
450
|
+
(
|
|
451
|
+
"Invalid href format in schemaRef. Could not extract module name "
|
|
452
|
+
f"from: '{value}'. Expected format: 'http://.../mod/[module_name].xsd'"
|
|
453
|
+
),
|
|
454
|
+
offending_value=value,
|
|
441
455
|
)
|
|
442
456
|
|
|
443
457
|
module_part = split_parts[1]
|
|
444
458
|
if ".xsd" not in module_part:
|
|
445
|
-
raise
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
459
|
+
raise SchemaRefValueError(
|
|
460
|
+
(
|
|
461
|
+
"Invalid href format in schemaRef. Expected href to end with '.xsd' "
|
|
462
|
+
f"but got: '{value}'. Please verify the XBRL-XML file contains a valid "
|
|
463
|
+
"schema reference."
|
|
464
|
+
),
|
|
465
|
+
offending_value=value,
|
|
449
466
|
)
|
|
450
467
|
|
|
451
468
|
xsd_split = module_part.split(".xsd")
|
|
@@ -485,7 +502,8 @@ class Instance:
|
|
|
485
502
|
|
|
486
503
|
units: Dict[str, str] = {}
|
|
487
504
|
for unit in self.root.findall("{http://www.xbrl.org/2003/instance}unit"):
|
|
488
|
-
|
|
505
|
+
unit_id = unit.attrib["id"]
|
|
506
|
+
unit_name = str(unit_id)
|
|
489
507
|
measure = unit.find("{http://www.xbrl.org/2003/instance}measure")
|
|
490
508
|
if measure is None or measure.text is None:
|
|
491
509
|
continue
|
|
@@ -739,7 +757,7 @@ class Context:
|
|
|
739
757
|
|
|
740
758
|
def parse(self) -> None:
|
|
741
759
|
"""Parses the XML node with the :obj:`Context <xbridge.xml_instance.Context>`."""
|
|
742
|
-
self._id = self.context_xml.attrib["id"]
|
|
760
|
+
self._id = str(self.context_xml.attrib["id"])
|
|
743
761
|
|
|
744
762
|
entity_elem = self.context_xml.find("{http://www.xbrl.org/2003/instance}entity")
|
|
745
763
|
if entity_elem is not None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|