vtlengine 1.2.1rc1__tar.gz → 1.2.2__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.

Potentially problematic release.


This version of vtlengine might be problematic. Click here for more details.

Files changed (63) hide show
  1. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/PKG-INFO +5 -4
  2. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/pyproject.toml +7 -7
  3. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTConstructorModules/Terminals.py +1 -5
  4. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/__init__.py +3 -3
  5. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/DataTypes/TimeHandling.py +8 -7
  6. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Exceptions/messages.py +7 -0
  7. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Interpreter/__init__.py +60 -16
  8. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Aggregation.py +4 -0
  9. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Conditional.py +51 -34
  10. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Validation.py +33 -5
  11. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/__init__.py +1 -1
  12. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/files/parser/__init__.py +10 -0
  13. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/files/parser/_time_checking.py +5 -0
  14. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/LICENSE.md +0 -0
  15. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/README.md +0 -0
  16. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/API/_InternalApi.py +0 -0
  17. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/API/__init__.py +0 -0
  18. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/API/data/schema/json_schema_2.1.json +0 -0
  19. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTComment.py +0 -0
  20. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTConstructor.py +0 -0
  21. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTConstructorModules/Expr.py +0 -0
  22. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTConstructorModules/ExprComponents.py +0 -0
  23. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTConstructorModules/__init__.py +0 -0
  24. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTDataExchange.py +0 -0
  25. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTEncoders.py +0 -0
  26. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTString.py +0 -0
  27. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTTemplate.py +0 -0
  28. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/ASTVisitor.py +0 -0
  29. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/DAG/__init__.py +0 -0
  30. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/DAG/_words.py +0 -0
  31. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/Grammar/Vtl.g4 +0 -0
  32. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/Grammar/VtlTokens.g4 +0 -0
  33. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/Grammar/__init__.py +0 -0
  34. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/Grammar/lexer.py +0 -0
  35. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/Grammar/parser.py +0 -0
  36. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/Grammar/tokens.py +0 -0
  37. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/AST/VtlVisitor.py +0 -0
  38. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/DataTypes/__init__.py +0 -0
  39. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Exceptions/__init__.py +0 -0
  40. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Model/__init__.py +0 -0
  41. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Analytic.py +0 -0
  42. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Assignment.py +0 -0
  43. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Boolean.py +0 -0
  44. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/CastOperator.py +0 -0
  45. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Clause.py +0 -0
  46. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Comparison.py +0 -0
  47. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/General.py +0 -0
  48. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/HROperators.py +0 -0
  49. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Join.py +0 -0
  50. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Numeric.py +0 -0
  51. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/RoleSetter.py +0 -0
  52. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Set.py +0 -0
  53. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/String.py +0 -0
  54. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/Time.py +0 -0
  55. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Operators/__init__.py +0 -0
  56. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Utils/__Virtual_Assets.py +0 -0
  57. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/Utils/__init__.py +0 -0
  58. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/__extras_check.py +0 -0
  59. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/files/__init__.py +0 -0
  60. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/files/output/__init__.py +0 -0
  61. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/files/output/_time_period_representation.py +0 -0
  62. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/files/parser/_rfc_dialect.py +0 -0
  63. {vtlengine-1.2.1rc1 → vtlengine-1.2.2}/src/vtlengine/py.typed +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: vtlengine
3
- Version: 1.2.1rc1
3
+ Version: 1.2.2
4
4
  Summary: Run and Validate VTL Scripts
5
- License: AGPL-3.0
5
+ License-Expression: AGPL-3.0
6
+ License-File: LICENSE.md
6
7
  Keywords: vtl,sdmx,vtlengine,Validation and Transformation Language
7
8
  Author: MeaningfulData
8
9
  Author-email: info@meaningfuldata.eu
@@ -25,7 +26,7 @@ Requires-Dist: networkx (>=2.8,<3.0)
25
26
  Requires-Dist: numpy (>=1.23.2,<2) ; python_version < "3.13"
26
27
  Requires-Dist: numpy (>=2.1.0) ; python_version >= "3.13"
27
28
  Requires-Dist: pandas (>=2.1.4,<3.0)
28
- Requires-Dist: pysdmx[xml] (>=1.4.0rc1,<2.0)
29
+ Requires-Dist: pysdmx[xml] (>=1.5.2,<2.0)
29
30
  Requires-Dist: s3fs (>=2022.11.0,<2023.0) ; extra == "all"
30
31
  Requires-Dist: s3fs (>=2022.11.0,<2023.0) ; extra == "s3"
31
32
  Requires-Dist: sqlglot (>=22.2.0,<23.0)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vtlengine"
3
- version = "1.2.1rc1"
3
+ version = "1.2.2"
4
4
  description = "Run and Validate VTL Scripts"
5
5
  license = "AGPL-3.0"
6
6
  readme = "README.md"
@@ -25,7 +25,7 @@ keywords = ['vtl', 'sdmx', 'vtlengine', 'Validation and Transformation Language'
25
25
  dependencies = [
26
26
  # PyPi dependencies
27
27
  "duckdb>=1.1,<1.2",
28
- "pysdmx[xml]>=1.4.0rc1,<2.0",
28
+ "pysdmx[xml]>=1.5.2,<2.0",
29
29
  # APT-supported dependencies
30
30
  "jsonschema>=3.2.0,<5.0",
31
31
  "sqlglot>=22.2.0,<23.0",
@@ -52,13 +52,13 @@ python = ">=3.9,<4.0"
52
52
 
53
53
  [tool.poetry.group.dev.dependencies]
54
54
  pytest = "^8.4"
55
- pytest-cov = "^6.2.1"
55
+ pytest-cov = "^7.0.0"
56
56
  pytest-xdist = "^3.8.0"
57
57
  line-profiler-pycharm = "^1.2.0"
58
- mypy = "1.17.1"
59
- pandas-stubs = "2.2.2.240807"
60
- ruff = "^0.12.7"
61
- types-jsonschema = "4.25.0.20250720"
58
+ mypy = "1.18.2"
59
+ pandas-stubs = "^2.2.2"
60
+ ruff = "^0.14.0"
61
+ types-jsonschema = "^4.25.1"
62
62
 
63
63
  [tool.poetry.group.docs.dependencies]
64
64
  sphinx = "^7.4.7"
@@ -622,11 +622,7 @@ class Terminals(VtlVisitor):
622
622
  erLevel: ERRORLEVEL constant;
623
623
  """
624
624
  ctx_list = list(ctx.getChildren())
625
-
626
- try:
627
- return int(self.visitConstant(ctx_list[1]).value)
628
- except Exception:
629
- raise Exception(f"Error level must be an integer, line {ctx_list[1].start.line}")
625
+ return self.visitConstant(ctx_list[1]).value
630
626
 
631
627
  def visitSignature(self, ctx: Parser.SignatureContext, kind="ComponentID"):
632
628
  """
@@ -443,7 +443,7 @@ class Validation(AST):
443
443
  op: str
444
444
  validation: str
445
445
  error_code: Optional[str]
446
- error_level: Optional[int]
446
+ error_level: Optional[Union[int, str]]
447
447
  imbalance: Optional[AST]
448
448
  invalid: bool
449
449
 
@@ -590,7 +590,7 @@ class HRule(AST):
590
590
  name: Optional[str]
591
591
  rule: HRBinOp
592
592
  erCode: Optional[str]
593
- erLevel: Optional[int]
593
+ erLevel: Optional[Union[int, str]]
594
594
 
595
595
  __eq__ = AST.ast_equality
596
596
 
@@ -604,7 +604,7 @@ class DPRule(AST):
604
604
  name: Optional[str]
605
605
  rule: HRBinOp
606
606
  erCode: Optional[str]
607
- erLevel: Optional[int]
607
+ erLevel: Optional[Union[int, str]]
608
608
 
609
609
  __eq__ = AST.ast_equality
610
610
 
@@ -7,6 +7,7 @@ from typing import Any, Dict, Optional, Union
7
7
 
8
8
  import pandas as pd
9
9
 
10
+ from vtlengine.AST.Grammar.tokens import GT, GTE, LT, LTE
10
11
  from vtlengine.Exceptions import SemanticError
11
12
 
12
13
  PERIOD_IND_MAPPING = {"A": 6, "S": 5, "Q": 4, "M": 3, "W": 2, "D": 1}
@@ -180,7 +181,7 @@ class TimePeriodHandler:
180
181
 
181
182
  @staticmethod
182
183
  def _check_year(year: int) -> None:
183
- if year < 1900 or year > 9999:
184
+ if year < 0 or year > 9999:
184
185
  raise SemanticError("2-1-19-10", year=year)
185
186
  # raise ValueError(f'Invalid year {year}, must be between 1900 and 9999.')
186
187
 
@@ -407,22 +408,22 @@ class TimeIntervalHandler:
407
408
  return py_op(self.length, other.length)
408
409
 
409
410
  def __eq__(self, other: Any) -> Optional[bool]: # type: ignore[override]
410
- return self._meta_comparison(other, operator.eq)
411
+ return str(self) == str(other) if other is not None else None
411
412
 
412
413
  def __ne__(self, other: Any) -> Optional[bool]: # type: ignore[override]
413
- return self._meta_comparison(other, operator.ne)
414
+ return str(self) != str(other) if other is not None else None
414
415
 
415
416
  def __lt__(self, other: Any) -> Optional[bool]:
416
- return self._meta_comparison(other, operator.lt)
417
+ raise SemanticError("2-1-19-17", op=LT, type="Time")
417
418
 
418
419
  def __le__(self, other: Any) -> Optional[bool]:
419
- return self._meta_comparison(other, operator.le)
420
+ raise SemanticError("2-1-19-17", op=LTE, type="Time")
420
421
 
421
422
  def __gt__(self, other: Any) -> Optional[bool]:
422
- return self._meta_comparison(other, operator.gt)
423
+ raise SemanticError("2-1-19-17", op=GT, type="Time")
423
424
 
424
425
  def __ge__(self, other: Any) -> Optional[bool]:
425
- return self._meta_comparison(other, operator.ge)
426
+ raise SemanticError("2-1-19-17", op=GTE, type="Time")
426
427
 
427
428
  @classmethod
428
429
  def from_time_period(cls, value: TimePeriodHandler) -> "TimeIntervalHandler":
@@ -44,6 +44,8 @@ centralised_messages = {
44
44
  "0-1-1-12": "On Dataset {name} loading: not possible to cast column {column} to {type}.",
45
45
  "0-1-1-13": "Invalid key on {field} field: {key}{closest_key}.",
46
46
  "0-1-1-14": "Empty datasets {dataset1} and {dataset2} shape missmatch.",
47
+ "0-1-1-15": "On Dataset {name} loading: Duplicated identifiers are not allowed, "
48
+ "found on row {row_index}",
47
49
  "0-1-0-1": " Trying to redefine input datasets {dataset}.", # Semantic Error
48
50
  # ------------Operators-------------
49
51
  # General Semantic errors
@@ -51,6 +53,7 @@ centralised_messages = {
51
53
  "1-1-1-2": "Invalid implicit cast from {type_1} and {type_2} to {type_check}.",
52
54
  "1-1-1-3": "At op {op}: {entity} {name} cannot be promoted to {target_type}.",
53
55
  "1-1-1-4": "At op {op}: Operation not allowed for multimeasure datasets.",
56
+ "1-1-1-5": "At op {op}: Invalid type {type}.",
54
57
  "1-1-1-8": "At op {op}: Invalid Dataset {name}, no measures defined.",
55
58
  "1-1-1-9": "At op {op}: Invalid Dataset {name}, all measures must have the same type: {type}.",
56
59
  "1-1-1-10": "Component {comp_name} not found in Dataset {dataset_name}.",
@@ -240,6 +243,10 @@ centralised_messages = {
240
243
  "measure.",
241
244
  "2-1-19-15": "{op} can only be applied according to the iso 8601 format mask",
242
245
  "2-1-19-16": "{op} can only be positive numbers",
246
+ "2-1-19-17": "At op {op}: Time operators comparison are only support "
247
+ "= and <> comparison operations",
248
+ "2-1-19-18": "At op {op}: Time operators do not support < and > comparison operations, "
249
+ "so its not possible to use get the max or min between two time operators",
243
250
  # ----------- Interpreter Common ------
244
251
  "2-3-1": "{comp_type} {comp_name} not found.",
245
252
  "2-3-2": "{op_type} cannot be used with {node_op} operators.",
@@ -151,6 +151,8 @@ class InterpreterAnalyzer(ASTTemplate):
151
151
  dprs: Optional[Dict[str, Optional[Dict[str, Any]]]] = None
152
152
  udos: Optional[Dict[str, Optional[Dict[str, Any]]]] = None
153
153
  hrs: Optional[Dict[str, Optional[Dict[str, Any]]]] = None
154
+ is_from_case_then: bool = False
155
+ signature_values: Optional[Dict[str, Any]] = None
154
156
 
155
157
  # **********************************
156
158
  # * *
@@ -1078,15 +1080,43 @@ class InterpreterAnalyzer(ASTTemplate):
1078
1080
 
1079
1081
  if self.condition_stack is None:
1080
1082
  self.condition_stack = []
1083
+ if self.then_condition_dataset is None:
1084
+ self.then_condition_dataset = []
1085
+ if self.else_condition_dataset is None:
1086
+ self.else_condition_dataset = []
1081
1087
 
1082
- while node.cases:
1083
- case = node.cases.pop(0)
1088
+ for case in node.cases:
1084
1089
  self.is_from_condition = True
1085
- conditions.append(self.visit(case.condition))
1090
+ cond = self.visit(case.condition)
1086
1091
  self.is_from_condition = False
1087
- thenOps.append(self.visit(case.thenOp))
1088
1092
 
1089
- return Case.analyze(conditions, thenOps, self.visit(node.elseOp))
1093
+ conditions.append(cond)
1094
+ if isinstance(cond, Scalar):
1095
+ then_result = self.visit(case.thenOp)
1096
+ thenOps.append(then_result)
1097
+ continue
1098
+
1099
+ self.generate_then_else_datasets(copy(cond))
1100
+
1101
+ self.condition_stack.append(THEN_ELSE["then"])
1102
+ self.is_from_if = True
1103
+ self.is_from_case_then = True
1104
+
1105
+ then_result = self.visit(case.thenOp)
1106
+ thenOps.append(then_result)
1107
+
1108
+ self.is_from_case_then = False
1109
+ self.is_from_if = False
1110
+ if len(self.condition_stack) > 0:
1111
+ self.condition_stack.pop()
1112
+ if len(self.then_condition_dataset) > 0:
1113
+ self.then_condition_dataset.pop()
1114
+ if len(self.else_condition_dataset) > 0:
1115
+ self.else_condition_dataset.pop()
1116
+
1117
+ elseOp = self.visit(node.elseOp)
1118
+
1119
+ return Case.analyze(conditions, thenOps, elseOp)
1090
1120
 
1091
1121
  def visit_RenameNode(self, node: AST.RenameNode) -> Any:
1092
1122
  if self.udo_params is not None:
@@ -1575,11 +1605,10 @@ class InterpreterAnalyzer(ASTTemplate):
1575
1605
  if self.else_condition_dataset is None:
1576
1606
  self.else_condition_dataset = []
1577
1607
  if isinstance(condition, Dataset):
1578
- if (
1579
- len(condition.get_measures()) != 1
1580
- or condition.get_measures()[0].data_type != BASIC_TYPES[bool]
1581
- ):
1582
- raise ValueError("Only one boolean measure is allowed on condition dataset")
1608
+ if len(condition.get_measures()) != 1:
1609
+ raise SemanticError("1-1-1-4", op="condition")
1610
+ if condition.get_measures()[0].data_type != BASIC_TYPES[bool]:
1611
+ raise SemanticError("2-1-9-5", op="condition", name=condition.name)
1583
1612
  name = condition.get_measures_names()[0]
1584
1613
  if condition.data is None or condition.data.empty:
1585
1614
  data = None
@@ -1589,7 +1618,7 @@ class InterpreterAnalyzer(ASTTemplate):
1589
1618
 
1590
1619
  else:
1591
1620
  if condition.data_type != BASIC_TYPES[bool]:
1592
- raise ValueError("Only boolean scalars are allowed on data component condition")
1621
+ raise SemanticError("2-1-9-4", op="condition", name=condition.name)
1593
1622
  name = condition.name
1594
1623
  data = None if condition.data is None else condition.data
1595
1624
 
@@ -1667,11 +1696,18 @@ class InterpreterAnalyzer(ASTTemplate):
1667
1696
  ):
1668
1697
  return left_operand, right_operand
1669
1698
 
1670
- merge_dataset = (
1671
- self.then_condition_dataset.pop()
1672
- if self.condition_stack.pop() == THEN_ELSE["then"]
1673
- else (self.else_condition_dataset.pop())
1674
- )
1699
+ if self.is_from_case_then:
1700
+ merge_dataset = (
1701
+ self.then_condition_dataset[-1]
1702
+ if self.condition_stack[-1] == THEN_ELSE["then"]
1703
+ else self.else_condition_dataset[-1]
1704
+ )
1705
+ else:
1706
+ merge_dataset = (
1707
+ self.then_condition_dataset.pop()
1708
+ if self.condition_stack.pop() == THEN_ELSE["then"]
1709
+ else (self.else_condition_dataset.pop())
1710
+ )
1675
1711
 
1676
1712
  merge_index = merge_dataset.data[merge_dataset.get_measures_names()[0]].to_list()
1677
1713
  ids = merge_dataset.get_identifiers_names()
@@ -1826,6 +1862,8 @@ class InterpreterAnalyzer(ASTTemplate):
1826
1862
  raise SemanticError("2-3-10", comp_type="User Defined Operators")
1827
1863
  elif node.op not in self.udos:
1828
1864
  raise SemanticError("1-3-5", node_op=node.op, op_type="User Defined Operator")
1865
+ if self.signature_values is None:
1866
+ self.signature_values = {}
1829
1867
 
1830
1868
  operator = self.udos[node.op]
1831
1869
  signature_values = {}
@@ -1919,6 +1957,12 @@ class InterpreterAnalyzer(ASTTemplate):
1919
1957
  self.udo_params = []
1920
1958
 
1921
1959
  # Adding parameters to the stack
1960
+ for k, v in signature_values.items():
1961
+ if hasattr(v, "name"):
1962
+ v = v.name # type: ignore[assignment]
1963
+ if v in self.signature_values:
1964
+ signature_values[k] = self.signature_values[v] # type: ignore[index]
1965
+ self.signature_values.update(signature_values)
1922
1966
  self.udo_params.append(signature_values)
1923
1967
 
1924
1968
  # Calling the UDO AST, we use deepcopy to avoid changing the original UDO AST
@@ -259,6 +259,10 @@ class Aggregation(Operator.Unary):
259
259
  result_df = result_df[grouping_keys + measure_names]
260
260
  if cls.op == COUNT:
261
261
  result_df = result_df.dropna(subset=measure_names, how="any")
262
+ if cls.op in [MAX, MIN]:
263
+ for measure in operand.get_measures():
264
+ if measure.data_type == TimeInterval:
265
+ raise SemanticError("2-1-19-18", op=cls.op)
262
266
  cls._handle_data_types(result_df, operand.get_measures(), "input")
263
267
  result_df = cls._agg_func(result_df, grouping_keys, measure_names, having_expr)
264
268
 
@@ -316,6 +316,7 @@ class Case(Operator):
316
316
  cls, conditions: List[Any], thenOps: List[Any], elseOp: Any
317
317
  ) -> Union[Scalar, DataComponent, Dataset]:
318
318
  result = cls.validate(conditions, thenOps, elseOp)
319
+
319
320
  for condition in conditions:
320
321
  if isinstance(condition, Dataset) and condition.data is not None:
321
322
  condition.data.fillna(False, inplace=True)
@@ -344,57 +345,73 @@ class Case(Operator):
344
345
  result.value = thenOps[i].value
345
346
 
346
347
  if isinstance(result, DataComponent):
347
- result.data = pd.Series(None, index=conditions[0].data.index)
348
+ full_index = conditions[0].data.index
349
+ result.data = pd.Series(None, index=full_index)
348
350
 
349
351
  for i, condition in enumerate(conditions):
350
- value = thenOps[i].value if isinstance(thenOps[i], Scalar) else thenOps[i].data
351
- result.data = np.where(
352
- condition.data.notna(),
353
- np.where(condition.data, value, result.data),
354
- result.data,
355
- )
356
-
357
- condition_mask_else = ~np.any([condition.data for condition in conditions], axis=0)
358
- else_value = elseOp.value if isinstance(elseOp, Scalar) else elseOp.data
359
- result.data = pd.Series(
360
- np.where(condition_mask_else, else_value, result.data),
361
- index=conditions[0].data.index,
352
+ if isinstance(thenOps[i], Scalar):
353
+ value_series = pd.Series(thenOps[i].value, index=full_index)
354
+ else:
355
+ value_series = thenOps[i].data.reindex(full_index)
356
+ cond_series = condition.data.reindex(full_index)
357
+ cond_mask = cond_series.notna() & cond_series == True
358
+ result_data = result.data.copy()
359
+ result_data[cond_mask] = value_series[cond_mask]
360
+ result.data = result_data
361
+
362
+ conditions_stack = [c.data.reindex(full_index).fillna(False) for c in conditions]
363
+ else_cond_mask = (
364
+ ~np.logical_or.reduce(conditions_stack)
365
+ if conditions_stack
366
+ else pd.Series(True, index=full_index)
362
367
  )
368
+ if isinstance(elseOp, Scalar):
369
+ else_series = pd.Series(elseOp.value, index=full_index)
370
+ else:
371
+ else_series = elseOp.data.reindex(full_index)
372
+ result.data[else_cond_mask] = else_series[else_cond_mask]
363
373
 
364
- if isinstance(result, Dataset):
374
+ elif isinstance(result, Dataset):
365
375
  identifiers = result.get_identifiers_names()
366
376
  columns = [col for col in result.get_components_names() if col not in identifiers]
367
377
  result.data = (
368
378
  conditions[0].data[identifiers]
369
379
  if conditions[0].data is not None
370
380
  else pd.DataFrame(columns=identifiers)
371
- )
381
+ ).copy()
372
382
 
383
+ full_index = result.data.index
373
384
  for i in range(len(conditions)):
374
385
  condition = conditions[i]
375
386
  bool_col = next(x.name for x in condition.get_measures() if x.data_type == Boolean)
376
- condition_mask = condition.data[bool_col]
387
+ cond_mask = condition.data[bool_col].reindex(full_index).astype(bool)
377
388
 
378
- result.data.loc[condition_mask, columns] = (
379
- thenOps[i].value
380
- if isinstance(thenOps[i], Scalar)
381
- else thenOps[i].data.loc[condition_mask, columns]
382
- )
383
-
384
- condition_mask_else = ~np.logical_or.reduce(
385
- [
386
- condition.data[
387
- next(x.name for x in condition.get_measures() if x.data_type == Boolean)
388
- ].astype(bool)
389
- for condition in conditions
390
- ]
389
+ if isinstance(thenOps[i], Scalar):
390
+ for col in columns:
391
+ result.data.loc[cond_mask, col] = thenOps[i].value
392
+ else:
393
+ cond_df = thenOps[i].data.reindex(full_index)
394
+ result.data.loc[cond_mask, columns] = cond_df.loc[cond_mask, columns]
395
+
396
+ then_cond_masks = [
397
+ c.data[next(x.name for x in c.get_measures() if x.data_type == Boolean)]
398
+ .reindex(full_index)
399
+ .fillna(False)
400
+ .astype(bool)
401
+ for c in conditions
402
+ ]
403
+ else_cond_mask = (
404
+ ~np.logical_or.reduce(then_cond_masks)
405
+ if then_cond_masks
406
+ else pd.Series(True, index=full_index)
391
407
  )
392
408
 
393
- result.data.loc[condition_mask_else, columns] = ( # type: ignore[index, unused-ignore]
394
- elseOp.value
395
- if isinstance(elseOp, Scalar)
396
- else elseOp.data.loc[condition_mask_else, columns]
397
- )
409
+ if isinstance(elseOp, Scalar):
410
+ for col in columns:
411
+ result.data.loc[else_cond_mask, col] = elseOp.value
412
+ else:
413
+ else_df = elseOp.data.reindex(full_index)
414
+ result.data.loc[else_cond_mask, columns] = else_df.loc[else_cond_mask, columns]
398
415
 
399
416
  return result
400
417
 
@@ -1,5 +1,5 @@
1
1
  from copy import copy
2
- from typing import Any, Dict, Optional
2
+ from typing import Any, Dict, Optional, Union
3
3
 
4
4
  import pandas as pd
5
5
 
@@ -27,7 +27,7 @@ class Check(Operator):
27
27
  validation_element: Dataset,
28
28
  imbalance_element: Optional[Dataset],
29
29
  error_code: Optional[str],
30
- error_level: Optional[int],
30
+ error_level: Optional[Union[int, str]],
31
31
  invalid: bool,
32
32
  ) -> Dataset:
33
33
  dataset_name = VirtualCounter._new_ds_name()
@@ -36,6 +36,13 @@ class Check(Operator):
36
36
  measure = validation_element.get_measures()[0]
37
37
  if measure.data_type != Boolean:
38
38
  raise SemanticError("1-1-10-1", op=cls.op, op_type="validation", me_type="Boolean")
39
+ error_level_type = None
40
+ if error_level is None or isinstance(error_level, int):
41
+ error_level_type = Integer
42
+ elif isinstance(error_level, str):
43
+ error_level_type = String # type: ignore[assignment]
44
+ else:
45
+ error_level_type = String
39
46
 
40
47
  imbalance_measure = None
41
48
  if imbalance_element is not None:
@@ -69,8 +76,12 @@ class Check(Operator):
69
76
  result_components["errorcode"] = Component(
70
77
  name="errorcode", data_type=String, role=Role.MEASURE, nullable=True
71
78
  )
79
+
72
80
  result_components["errorlevel"] = Component(
73
- name="errorlevel", data_type=Integer, role=Role.MEASURE, nullable=True
81
+ name="errorlevel",
82
+ data_type=error_level_type, # type: ignore[arg-type]
83
+ role=Role.MEASURE,
84
+ nullable=True,
74
85
  )
75
86
 
76
87
  return Dataset(name=dataset_name, components=result_components, data=None)
@@ -81,7 +92,7 @@ class Check(Operator):
81
92
  validation_element: Dataset,
82
93
  imbalance_element: Optional[Dataset],
83
94
  error_code: Optional[str],
84
- error_level: Optional[int],
95
+ error_level: Optional[Union[int, str]],
85
96
  invalid: bool,
86
97
  ) -> Dataset:
87
98
  result = cls.validate(
@@ -128,6 +139,20 @@ class Validation(Operator):
128
139
 
129
140
  @classmethod
130
141
  def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: str) -> Dataset:
142
+ error_level_type = None
143
+ error_levels = [
144
+ rule_data.get("errorlevel")
145
+ for rule_data in rule_info.values()
146
+ if "errorlevel" in rule_data
147
+ ]
148
+ non_null_levels = [el for el in error_levels if el is not None]
149
+
150
+ if len(non_null_levels) == 0 or all(isinstance(el, int) for el in non_null_levels):
151
+ error_level_type = Number
152
+ elif all(isinstance(el, str) for el in non_null_levels):
153
+ error_level_type = String # type: ignore[assignment]
154
+ else:
155
+ error_level_type = String # type: ignore[assignment]
131
156
  dataset_name = VirtualCounter._new_ds_name()
132
157
  result_components = {comp.name: comp for comp in dataset_element.get_identifiers()}
133
158
  result_components["ruleid"] = Component(
@@ -154,7 +179,10 @@ class Validation(Operator):
154
179
  name="errorcode", data_type=String, role=Role.MEASURE, nullable=True
155
180
  )
156
181
  result_components["errorlevel"] = Component(
157
- name="errorlevel", data_type=Number, role=Role.MEASURE, nullable=True
182
+ name="errorlevel",
183
+ data_type=error_level_type, # type: ignore[arg-type]
184
+ role=Role.MEASURE,
185
+ nullable=True,
158
186
  )
159
187
 
160
188
  return Dataset(name=dataset_name, components=result_components, data=None)
@@ -2,4 +2,4 @@ from vtlengine.API import generate_sdmx, prettify, run, run_sdmx, semantic_analy
2
2
 
3
3
  __all__ = ["semantic_analysis", "run", "generate_sdmx", "run_sdmx", "prettify"]
4
4
 
5
- __version__ = "1.2.1rc1"
5
+ __version__ = "1.2.2"
@@ -203,9 +203,19 @@ def _validate_pandas(
203
203
  str_comp = SCALAR_TYPES_CLASS_REVERSE[comp.data_type] if comp else "Null"
204
204
  raise SemanticError("0-1-1-12", name=dataset_name, column=comp_name, type=str_comp)
205
205
 
206
+ if id_names:
207
+ check_identifiers_duplicity(data, id_names, dataset_name)
208
+
206
209
  return data
207
210
 
208
211
 
212
+ def check_identifiers_duplicity(data: pd.DataFrame, identifiers: List[str], name: str) -> None:
213
+ dup_id_row = data.duplicated(subset=identifiers, keep=False)
214
+ if dup_id_row.any():
215
+ row_index = int(dup_id_row.idxmax()) + 1
216
+ raise SemanticError("0-1-1-15", name=name, row_index=row_index)
217
+
218
+
209
219
  def load_datapoints(
210
220
  components: Dict[str, Component],
211
221
  dataset_name: str,
@@ -95,6 +95,11 @@ def check_time_period(value: str) -> str:
95
95
  if isinstance(value, int):
96
96
  value = str(value)
97
97
  value = value.replace(" ", "")
98
+
99
+ match = re.fullmatch(r"^(\d{4})-(\d{2})$", value)
100
+ if match:
101
+ value = f"{match.group(1)}-M{match.group(2)}"
102
+
98
103
  period_result = re.fullmatch(period_pattern, value)
99
104
  if period_result is not None:
100
105
  result = TimePeriodHandler(value)
File without changes
File without changes