eba-xbridge 1.5.0rc5__py3-none-any.whl → 1.5.0rc7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eba-xbridge
3
- Version: 1.5.0rc5
3
+ Version: 1.5.0rc7
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=S6yAyrnTJ19Trbh-OrGVDIwi5lh88C2pCxDncQyX1VY,68
1
+ xbridge/__init__.py,sha256=XzUbSgyZfflnN8d9Wx5SXWCa0-1hE_4R0hUjO4uld7g,68
2
2
  xbridge/__main__.py,sha256=trtFEv7TRJgrLL84leIapPvgC_iVTj05qLHRRS1Olts,2219
3
3
  xbridge/api.py,sha256=NCBz7VRJWE3gID6ndgL4Awoxw0w1yMIIf_OTLRuZyyQ,1559
4
- xbridge/converter.py,sha256=vWlIaqQ2eA8Y5b1jqnqTIcOxeubJEu1itggMrFxYnS8,24077
5
- xbridge/instance.py,sha256=GA5D2p3IobCp5W4-x1dibgOQFuy_W4R8LmZnQ4H900g,32292
4
+ xbridge/converter.py,sha256=wLBm2QjxmGh_PdTDHExnoQw2WXixYlPpns1ViB4yG_I,24665
5
+ xbridge/exceptions.py,sha256=v219QGAjcO4--7wS0bBRtm2TdccX9RhExV7gAhnEfgE,1285
6
+ xbridge/instance.py,sha256=mBK8qvghSwvqPdFq3BkhpOyRNmAMLFsaDIfH6acfHzk,33736
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.0rc5.dist-info/METADATA,sha256=88P20ZbVDK8cMGG0-rs1W7OihOdBrmOxujMvoeB3RQM,10430
387
- eba_xbridge-1.5.0rc5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
388
- eba_xbridge-1.5.0rc5.dist-info/entry_points.txt,sha256=FATct4icSewM04cegjhybtm7xcQWhaSahL-DTtuFdZw,49
389
- eba_xbridge-1.5.0rc5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
390
- eba_xbridge-1.5.0rc5.dist-info/RECORD,,
387
+ eba_xbridge-1.5.0rc7.dist-info/METADATA,sha256=zKESjF-Gw_YU6Inj1dYZJnPAB7bhVOIaHkUZh2NOVSc,12053
388
+ eba_xbridge-1.5.0rc7.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
389
+ eba_xbridge-1.5.0rc7.dist-info/entry_points.txt,sha256=FATct4icSewM04cegjhybtm7xcQWhaSahL-DTtuFdZw,49
390
+ eba_xbridge-1.5.0rc7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
391
+ eba_xbridge-1.5.0rc7.dist-info/RECORD,,
xbridge/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  Init file for eba-xbridge library
3
3
  """
4
4
 
5
- __version__ = "1.5.0rc5"
5
+ __version__ = "1.5.0rc7"
xbridge/converter.py CHANGED
@@ -15,6 +15,11 @@ from zipfile import ZipFile
15
15
 
16
16
  import pandas as pd
17
17
 
18
+ from xbridge.exceptions import (
19
+ DecimalValueError,
20
+ FilingIndicatorValueError,
21
+ FilingIndicatorWarning,
22
+ )
18
23
  from xbridge.instance import CsvInstance, Instance, XmlInstance
19
24
  from xbridge.modules import Module, Table
20
25
 
@@ -376,31 +381,23 @@ class Converter:
376
381
 
377
382
  data_type = row["data_type"][1:]
378
383
  decimals = row["decimals"]
384
+ normalized_decimals = self._normalize_decimals_value(decimals)
379
385
 
380
386
  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
- )
387
+ self._decimals_parameters[data_type] = normalized_decimals
384
388
  else:
385
- # If new value is a special value, skip it (prefer numeric values)
386
- if decimals in {"INF", "#none"}:
387
- pass
388
- # If new value is numeric
389
- else:
390
- try:
391
- decimals = int(decimals)
392
- except ValueError:
393
- raise ValueError(
394
- f"Invalid decimals value: {decimals}, "
395
- "should be integer, 'INF' or '#none'"
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
389
+ # Skip special values when we already have an entry,
390
+ # as numeric values take precedence.
391
+ if normalized_decimals in {"INF", "#none"}:
392
+ continue
393
+
394
+ existing_value = self._decimals_parameters[data_type]
395
+ if existing_value in {"INF", "#none"} or (
396
+ isinstance(existing_value, int)
397
+ and isinstance(normalized_decimals, int)
398
+ and normalized_decimals < existing_value
399
+ ):
400
+ self._decimals_parameters[data_type] = normalized_decimals
404
401
 
405
402
  drop_columns = merge_cols + ["data_type", "decimals"]
406
403
  else:
@@ -422,6 +419,23 @@ class Converter:
422
419
 
423
420
  return table_df
424
421
 
422
+ def _normalize_decimals_value(self, decimals: Any) -> Union[int, str]:
423
+ """Return a validated decimals value or raise a DecimalValueError."""
424
+ candidate = decimals
425
+ if isinstance(candidate, str):
426
+ candidate = candidate.strip()
427
+
428
+ if candidate in {"INF", "#none"}:
429
+ return candidate
430
+
431
+ try:
432
+ return int(candidate)
433
+ except (TypeError, ValueError) as exc:
434
+ raise DecimalValueError(
435
+ f"Invalid decimals value: {decimals}, should be integer, 'INF' or '#none'",
436
+ offending_value=decimals,
437
+ ) from exc
438
+
425
439
  def _convert_tables(
426
440
  self,
427
441
  temp_dir_path: Path,
@@ -503,7 +517,7 @@ class Converter:
503
517
  """Validate that no facts are orphaned (belong only to non-reported tables).
504
518
 
505
519
  Raises:
506
- ValueError: If facts exist that belong only to tables with filed=false
520
+ FilingIndicatorValueError: If facts exist that belong only to tables with filed=false
507
521
  """
508
522
  if self.instance.instance_df is None or self.instance.instance_df.empty:
509
523
  return
@@ -551,13 +565,17 @@ class Converter:
551
565
  "Either set filed=true for the relevant tables "
552
566
  "or remove these facts from the XML."
553
567
  )
554
- raise ValueError(error_msg)
568
+ raise FilingIndicatorValueError(error_msg, orphaned_per_table)
555
569
  error_msg += (
556
570
  "\nThese facts will be excluded from the output. "
557
571
  "Consider setting filed=true for the relevant tables "
558
572
  "or removing these facts from the XML."
559
573
  )
560
- warnings.warn(error_msg)
574
+ warnings.warn(
575
+ error_msg,
576
+ category=FilingIndicatorWarning,
577
+ stacklevel=2,
578
+ )
561
579
 
562
580
  def _convert_parameters(self, temp_dir_path: Path) -> None:
563
581
  # Workaround;
xbridge/exceptions.py ADDED
@@ -0,0 +1,41 @@
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 FilingIndicatorValueError(ValueError):
25
+ """Raised when filing indicator validation fails."""
26
+
27
+ def __init__(self, error_message: str, offending_value: Optional[Any] = None) -> None:
28
+ super().__init__(error_message)
29
+ self.offending_value = offending_value
30
+
31
+
32
+ class XbridgeWarning(Warning):
33
+ """Base warning for the xbridge library."""
34
+
35
+
36
+ class IdentifierPrefixWarning(XbridgeWarning):
37
+ """Unknown identifier prefix; defaulting to 'rs'."""
38
+
39
+
40
+ class FilingIndicatorWarning(XbridgeWarning):
41
+ """Facts orphaned by filing indicators; some are excluded."""
xbridge/instance.py CHANGED
@@ -13,6 +13,12 @@ from zipfile import ZipFile
13
13
  import pandas as pd
14
14
  from lxml import etree
15
15
 
16
+ from xbridge.exceptions import (
17
+ FilingIndicatorValueError,
18
+ IdentifierPrefixWarning,
19
+ SchemaRefValueError,
20
+ )
21
+
16
22
  # Cache namespace → CSV prefix derivations to avoid repeated string work during parse
17
23
  _namespace_prefix_cache: Dict[str, str] = {}
18
24
 
@@ -110,9 +116,7 @@ def _normalize_namespaced_value(
110
116
  return value
111
117
 
112
118
 
113
- def _normalize_metric_value(
114
- value: Optional[str], nsmap: Dict[Optional[str], str]
115
- ) -> Optional[str]:
119
+ def _normalize_metric_value(value: Optional[str], nsmap: Dict[Optional[str], str]) -> Optional[str]:
116
120
  """
117
121
  Normalize a metric namespaced value to the CSV prefix convention.
118
122
  For metrics, we preserve version suffixes (e.g., eba_met_3.5, eba_met_4.0).
@@ -181,6 +185,7 @@ class Instance:
181
185
  self._contexts: Optional[Dict[str, Context]] = None
182
186
  self._facts: Optional[List[Fact]] = None
183
187
  self._facts_list_dict: Optional[List[Dict[str, Any]]] = None
188
+ self._df: Optional[pd.DataFrame] = None
184
189
  self._table_files: Optional[set[Path]] = None
185
190
  self._root_folder: Optional[str] = None
186
191
  self._report_file: Optional[Path] = None
@@ -270,7 +275,7 @@ class Instance:
270
275
  """
271
276
  if self.facts_list_dict is None:
272
277
  return
273
- df = pd.DataFrame.from_dict(self.facts_list_dict) # type: ignore[call-overload]
278
+ df = pd.DataFrame(self.facts_list_dict)
274
279
  df_columns = list(df.columns)
275
280
  ##Workaround
276
281
  # Dropping period an entity columns because in current EBA architecture,
@@ -296,7 +301,9 @@ class Instance:
296
301
  (
297
302
  f"{self._identifier_prefix} is not a known identifier prefix. "
298
303
  "Default 'rs' will be used."
299
- )
304
+ ),
305
+ category=IdentifierPrefixWarning,
306
+ stacklevel=2,
300
307
  )
301
308
  return "rs"
302
309
 
@@ -338,6 +345,10 @@ class Instance:
338
345
  self.get_filing_indicators()
339
346
  except etree.XMLSyntaxError:
340
347
  raise ValueError("Invalid XML format")
348
+ except SchemaRefValueError:
349
+ raise # Let SchemaRefValueError propagate as-is
350
+ except FilingIndicatorValueError:
351
+ raise # Let FilingIndicatorValueError propagate as-is
341
352
  except Exception as e:
342
353
  raise ValueError(f"Error parsing instance: {str(e)}")
343
354
 
@@ -354,16 +365,17 @@ class Instance:
354
365
  raise AttributeError("XML root not loaded.")
355
366
 
356
367
  contexts: Dict[str, Context] = {}
368
+ namespaces: Dict[str, str] = {key or "": value for key, value in self.namespaces.items()}
357
369
  for context in self.root.findall(
358
370
  "{http://www.xbrl.org/2003/instance}context",
359
- self.namespaces, # type: ignore[arg-type]
371
+ namespaces,
360
372
  ):
361
373
  context_object = Context(context)
362
374
  contexts[context_object.id] = context_object
363
375
 
364
376
  self._contexts = contexts
365
377
 
366
- first_ctx = self.root.find("{http://www.xbrl.org/2003/instance}context", self.namespaces) # type: ignore[arg-type]
378
+ first_ctx = self.root.find("{http://www.xbrl.org/2003/instance}context", namespaces)
367
379
  if first_ctx is not None:
368
380
  entity_elem = first_ctx.find("{http://www.xbrl.org/2003/instance}entity")
369
381
  if entity_elem is not None:
@@ -406,7 +418,8 @@ class Instance:
406
418
  href_attr = "{http://www.w3.org/1999/xlink}href"
407
419
  if href_attr not in child.attrib:
408
420
  continue
409
- value: str = child.attrib[href_attr] # type: ignore[assignment]
421
+ raw_value = child.attrib[href_attr]
422
+ value = str(raw_value)
410
423
  schema_refs.append(value)
411
424
 
412
425
  # Validate that only one schemaRef exists
@@ -414,11 +427,14 @@ class Instance:
414
427
  return # No schema reference found, module_ref will remain None
415
428
 
416
429
  if len(schema_refs) > 1:
417
- raise ValueError(
418
- f"Multiple schemaRef elements found in the XBRL instance. "
419
- f"Only one schemaRef is expected, but {len(schema_refs)} "
420
- f"were found: {schema_refs}. "
421
- f"This may indicate an invalid XBRL-XML file."
430
+ raise SchemaRefValueError(
431
+ (
432
+ "Multiple schemaRef elements found in the XBRL instance. "
433
+ f"Only one schemaRef is expected, but {len(schema_refs)} "
434
+ f"were found: {schema_refs}. "
435
+ "This may indicate an invalid XBRL-XML file."
436
+ ),
437
+ offending_value=schema_refs,
422
438
  )
423
439
 
424
440
  # Process the single schema reference
@@ -427,25 +443,34 @@ class Instance:
427
443
 
428
444
  # Validate href format and extract module code
429
445
  if "/mod/" not in value:
430
- raise ValueError(
431
- f"Invalid href format in schemaRef. Expected href to contain '/mod/' "
432
- f"but got: '{value}'. Please verify the XBRL-XML file contains a "
433
- f"valid schema reference."
446
+ raise SchemaRefValueError(
447
+ (
448
+ "Invalid href format in schemaRef. Expected href to contain '/mod/' "
449
+ f"but got: '{value}'. Please verify the XBRL-XML file contains a "
450
+ "valid schema reference."
451
+ ),
452
+ offending_value=value,
434
453
  )
435
454
 
436
455
  split_parts = value.split("/mod/")
437
456
  if len(split_parts) < 2:
438
- raise ValueError(
439
- f"Invalid href format in schemaRef. Could not extract module name from: '{value}'. "
440
- f"Expected format: 'http://.../mod/[module_name].xsd'"
457
+ raise SchemaRefValueError(
458
+ (
459
+ "Invalid href format in schemaRef. Could not extract module name "
460
+ f"from: '{value}'. Expected format: 'http://.../mod/[module_name].xsd'"
461
+ ),
462
+ offending_value=value,
441
463
  )
442
464
 
443
465
  module_part = split_parts[1]
444
466
  if ".xsd" not in module_part:
445
- raise ValueError(
446
- f"Invalid href format in schemaRef. Expected href to end with '.xsd' "
447
- f"but got: '{value}'. Please verify the XBRL-XML file contains a valid "
448
- f"schema reference."
467
+ raise SchemaRefValueError(
468
+ (
469
+ "Invalid href format in schemaRef. Expected href to end with '.xsd' "
470
+ f"but got: '{value}'. Please verify the XBRL-XML file contains a valid "
471
+ "schema reference."
472
+ ),
473
+ offending_value=value,
449
474
  )
450
475
 
451
476
  xsd_split = module_part.split(".xsd")
@@ -485,7 +510,8 @@ class Instance:
485
510
 
486
511
  units: Dict[str, str] = {}
487
512
  for unit in self.root.findall("{http://www.xbrl.org/2003/instance}unit"):
488
- unit_name: str = unit.attrib["id"] # type: ignore[assignment]
513
+ unit_id = unit.attrib["id"]
514
+ unit_name = str(unit_id)
489
515
  measure = unit.find("{http://www.xbrl.org/2003/instance}measure")
490
516
  if measure is None or measure.text is None:
491
517
  continue
@@ -633,6 +659,10 @@ class XmlInstance(Instance):
633
659
  self.get_filing_indicators()
634
660
  except etree.XMLSyntaxError:
635
661
  raise ValueError("Invalid XML format")
662
+ except SchemaRefValueError:
663
+ raise # Let SchemaRefValueError propagate as-is
664
+ except FilingIndicatorValueError:
665
+ raise # Let FilingIndicatorValueError propagate as-is
636
666
  except Exception as e:
637
667
  raise ValueError(f"Error parsing instance: {str(e)}")
638
668
 
@@ -739,7 +769,7 @@ class Context:
739
769
 
740
770
  def parse(self) -> None:
741
771
  """Parses the XML node with the :obj:`Context <xbridge.xml_instance.Context>`."""
742
- self._id = self.context_xml.attrib["id"] # type: ignore[assignment]
772
+ self._id = str(self.context_xml.attrib["id"])
743
773
 
744
774
  entity_elem = self.context_xml.find("{http://www.xbrl.org/2003/instance}entity")
745
775
  if entity_elem is not None:
@@ -838,11 +868,21 @@ class FilingIndicator:
838
868
  self.parse()
839
869
 
840
870
  def parse(self) -> None:
841
- """Parse the XML node with the filing indicator."""
871
+ """Parse the XML node with the filing indicator.
872
+
873
+ Raises:
874
+ FilingIndicatorValueError: If the filed attribute is not "true" or "false"
875
+ """
842
876
  value = self.filing_indicator_xml.attrib.get(
843
877
  "{http://www.eurofiling.info/xbrl/ext/filing-indicators}filed"
844
878
  )
845
879
  if value:
880
+ if value not in ("true", "false"):
881
+ raise FilingIndicatorValueError(
882
+ f"Invalid filing indicator value: '{value}'. "
883
+ f"The 'filed' attribute must be either 'true' or 'false'.",
884
+ offending_value=value,
885
+ )
846
886
  self.value = value == "true"
847
887
  else:
848
888
  self.value = True