pydpm_xl 0.1.39rc32__py3-none-any.whl → 0.2.0__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.
Files changed (123) hide show
  1. py_dpm/__init__.py +1 -1
  2. py_dpm/api/__init__.py +58 -189
  3. py_dpm/api/dpm/__init__.py +20 -0
  4. py_dpm/api/{data_dictionary.py → dpm/data_dictionary.py} +903 -984
  5. py_dpm/api/dpm/explorer.py +236 -0
  6. py_dpm/api/dpm/hierarchical_queries.py +142 -0
  7. py_dpm/api/{migration.py → dpm/migration.py} +16 -19
  8. py_dpm/api/{operation_scopes.py → dpm/operation_scopes.py} +319 -267
  9. py_dpm/api/dpm_xl/__init__.py +25 -0
  10. py_dpm/api/{ast_generator.py → dpm_xl/ast_generator.py} +3 -3
  11. py_dpm/api/{complete_ast.py → dpm_xl/complete_ast.py} +191 -167
  12. py_dpm/api/dpm_xl/semantic.py +354 -0
  13. py_dpm/api/{syntax.py → dpm_xl/syntax.py} +6 -5
  14. py_dpm/api/explorer.py +4 -0
  15. py_dpm/api/semantic.py +30 -306
  16. py_dpm/cli/__init__.py +9 -0
  17. py_dpm/{client.py → cli/main.py} +8 -8
  18. py_dpm/dpm/__init__.py +11 -0
  19. py_dpm/{models.py → dpm/models.py} +112 -88
  20. py_dpm/dpm/queries/base.py +100 -0
  21. py_dpm/dpm/queries/basic_objects.py +33 -0
  22. py_dpm/dpm/queries/explorer_queries.py +352 -0
  23. py_dpm/dpm/queries/filters.py +139 -0
  24. py_dpm/dpm/queries/glossary.py +45 -0
  25. py_dpm/dpm/queries/hierarchical_queries.py +838 -0
  26. py_dpm/dpm/queries/tables.py +133 -0
  27. py_dpm/dpm/utils.py +356 -0
  28. py_dpm/dpm_xl/__init__.py +8 -0
  29. py_dpm/dpm_xl/ast/__init__.py +14 -0
  30. py_dpm/{AST/ASTConstructor.py → dpm_xl/ast/constructor.py} +6 -6
  31. py_dpm/{AST/MLGeneration.py → dpm_xl/ast/ml_generation.py} +137 -87
  32. py_dpm/{AST/ModuleAnalyzer.py → dpm_xl/ast/module_analyzer.py} +7 -7
  33. py_dpm/{AST/ModuleDependencies.py → dpm_xl/ast/module_dependencies.py} +56 -41
  34. py_dpm/{AST/ASTObjects.py → dpm_xl/ast/nodes.py} +1 -1
  35. py_dpm/{AST/check_operands.py → dpm_xl/ast/operands.py} +16 -13
  36. py_dpm/{AST/ASTTemplate.py → dpm_xl/ast/template.py} +2 -2
  37. py_dpm/{AST/WhereClauseChecker.py → dpm_xl/ast/where_clause.py} +2 -2
  38. py_dpm/dpm_xl/grammar/__init__.py +18 -0
  39. py_dpm/dpm_xl/operators/__init__.py +19 -0
  40. py_dpm/{Operators/AggregateOperators.py → dpm_xl/operators/aggregate.py} +7 -7
  41. py_dpm/{Operators/NumericOperators.py → dpm_xl/operators/arithmetic.py} +6 -6
  42. py_dpm/{Operators/Operator.py → dpm_xl/operators/base.py} +5 -5
  43. py_dpm/{Operators/BooleanOperators.py → dpm_xl/operators/boolean.py} +5 -5
  44. py_dpm/{Operators/ClauseOperators.py → dpm_xl/operators/clause.py} +8 -8
  45. py_dpm/{Operators/ComparisonOperators.py → dpm_xl/operators/comparison.py} +5 -5
  46. py_dpm/{Operators/ConditionalOperators.py → dpm_xl/operators/conditional.py} +7 -7
  47. py_dpm/{Operators/StringOperators.py → dpm_xl/operators/string.py} +5 -5
  48. py_dpm/{Operators/TimeOperators.py → dpm_xl/operators/time.py} +6 -6
  49. py_dpm/{semantics/SemanticAnalyzer.py → dpm_xl/semantic_analyzer.py} +168 -68
  50. py_dpm/{semantics/Symbols.py → dpm_xl/symbols.py} +3 -3
  51. py_dpm/dpm_xl/types/__init__.py +13 -0
  52. py_dpm/{DataTypes/TypePromotion.py → dpm_xl/types/promotion.py} +2 -2
  53. py_dpm/{DataTypes/ScalarTypes.py → dpm_xl/types/scalar.py} +2 -2
  54. py_dpm/dpm_xl/utils/__init__.py +14 -0
  55. py_dpm/{data_handlers.py → dpm_xl/utils/data_handlers.py} +2 -2
  56. py_dpm/{Utils → dpm_xl/utils}/operands_mapping.py +1 -1
  57. py_dpm/{Utils → dpm_xl/utils}/operator_mapping.py +8 -8
  58. py_dpm/{OperationScopes/OperationScopeService.py → dpm_xl/utils/scopes_calculator.py} +148 -58
  59. py_dpm/{Utils/ast_serialization.py → dpm_xl/utils/serialization.py} +2 -2
  60. py_dpm/dpm_xl/validation/__init__.py +12 -0
  61. py_dpm/{Utils/ValidationsGenerationUtils.py → dpm_xl/validation/generation_utils.py} +2 -3
  62. py_dpm/{ValidationsGeneration/PropertiesConstraintsProcessor.py → dpm_xl/validation/property_constraints.py} +56 -21
  63. py_dpm/{ValidationsGeneration/auxiliary_functions.py → dpm_xl/validation/utils.py} +2 -2
  64. py_dpm/{ValidationsGeneration/VariantsProcessor.py → dpm_xl/validation/variants.py} +149 -55
  65. py_dpm/exceptions/__init__.py +23 -0
  66. py_dpm/{Exceptions → exceptions}/exceptions.py +7 -2
  67. pydpm_xl-0.2.0.dist-info/METADATA +278 -0
  68. pydpm_xl-0.2.0.dist-info/RECORD +88 -0
  69. pydpm_xl-0.2.0.dist-info/entry_points.txt +2 -0
  70. py_dpm/Exceptions/__init__.py +0 -0
  71. py_dpm/OperationScopes/__init__.py +0 -0
  72. py_dpm/Operators/__init__.py +0 -0
  73. py_dpm/Utils/__init__.py +0 -0
  74. py_dpm/Utils/utils.py +0 -2
  75. py_dpm/ValidationsGeneration/Utils.py +0 -364
  76. py_dpm/ValidationsGeneration/__init__.py +0 -0
  77. py_dpm/api/data_dictionary_validation.py +0 -614
  78. py_dpm/db_utils.py +0 -221
  79. py_dpm/grammar/__init__.py +0 -0
  80. py_dpm/grammar/dist/__init__.py +0 -0
  81. py_dpm/grammar/dpm_xlLexer.g4 +0 -437
  82. py_dpm/grammar/dpm_xlParser.g4 +0 -263
  83. py_dpm/semantics/DAG/DAGAnalyzer.py +0 -158
  84. py_dpm/semantics/DAG/__init__.py +0 -0
  85. py_dpm/semantics/__init__.py +0 -0
  86. py_dpm/views/data_types.sql +0 -12
  87. py_dpm/views/datapoints.sql +0 -65
  88. py_dpm/views/hierarchy_operand_reference.sql +0 -11
  89. py_dpm/views/hierarchy_preconditions.sql +0 -13
  90. py_dpm/views/hierarchy_variables.sql +0 -26
  91. py_dpm/views/hierarchy_variables_context.sql +0 -14
  92. py_dpm/views/key_components.sql +0 -18
  93. py_dpm/views/module_from_table.sql +0 -11
  94. py_dpm/views/open_keys.sql +0 -13
  95. py_dpm/views/operation_info.sql +0 -27
  96. py_dpm/views/operation_list.sql +0 -18
  97. py_dpm/views/operations_versions_from_module_version.sql +0 -30
  98. py_dpm/views/precondition_info.sql +0 -17
  99. py_dpm/views/report_type_operand_reference_info.sql +0 -18
  100. py_dpm/views/subcategory_info.sql +0 -17
  101. py_dpm/views/table_info.sql +0 -19
  102. pydpm_xl-0.1.39rc32.dist-info/METADATA +0 -53
  103. pydpm_xl-0.1.39rc32.dist-info/RECORD +0 -96
  104. pydpm_xl-0.1.39rc32.dist-info/entry_points.txt +0 -2
  105. /py_dpm/{AST → cli/commands}/__init__.py +0 -0
  106. /py_dpm/{migration.py → dpm/migration.py} +0 -0
  107. /py_dpm/{AST/ASTVisitor.py → dpm_xl/ast/visitor.py} +0 -0
  108. /py_dpm/{DataTypes → dpm_xl/grammar/generated}/__init__.py +0 -0
  109. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.interp +0 -0
  110. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.py +0 -0
  111. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.tokens +0 -0
  112. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.interp +0 -0
  113. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.py +0 -0
  114. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.tokens +0 -0
  115. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParserListener.py +0 -0
  116. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParserVisitor.py +0 -0
  117. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/listeners.py +0 -0
  118. /py_dpm/{DataTypes/TimeClasses.py → dpm_xl/types/time.py} +0 -0
  119. /py_dpm/{Utils → dpm_xl/utils}/tokens.py +0 -0
  120. /py_dpm/{Exceptions → exceptions}/messages.py +0 -0
  121. {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.0.dist-info}/WHEEL +0 -0
  122. {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.0.dist-info}/licenses/LICENSE +0 -0
  123. {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,26 @@
1
- from typing import List, Optional, Any
1
+ from typing import List, Optional, Dict, Any
2
2
  from dataclasses import dataclass, field
3
3
  from datetime import date
4
4
 
5
- from sqlalchemy import and_, or_
6
5
  from antlr4 import CommonTokenStream, InputStream
7
- from py_dpm.grammar.dist.dpm_xlLexer import dpm_xlLexer
8
- from py_dpm.grammar.dist.dpm_xlParser import dpm_xlParser
9
- from py_dpm.grammar.dist.listeners import DPMErrorListener
10
- from py_dpm.AST.ASTConstructor import ASTVisitor
11
- from py_dpm.AST.check_operands import OperandsChecking
12
- from py_dpm.AST.ASTObjects import VarID, PreconditionItem
13
- from py_dpm.OperationScopes.OperationScopeService import OperationScopeService
14
- from py_dpm.models import ModuleVersion, OperationScope, OperationScopeComposition, TableVersion, HeaderVersion, TableVersionHeader, Framework, Module
15
- from py_dpm.db_utils import get_session, get_engine
16
- from py_dpm.Exceptions.exceptions import SemanticError
6
+ from py_dpm.dpm_xl.grammar.generated.dpm_xlLexer import dpm_xlLexer
7
+ from py_dpm.dpm_xl.grammar.generated.dpm_xlParser import dpm_xlParser
8
+ from py_dpm.dpm_xl.grammar.generated.listeners import DPMErrorListener
9
+ from py_dpm.dpm_xl.ast.constructor import ASTVisitor
10
+ from py_dpm.dpm_xl.ast.operands import OperandsChecking
11
+ from py_dpm.dpm_xl.ast.nodes import PreconditionItem
12
+ from py_dpm.dpm_xl.utils.scopes_calculator import OperationScopeService
13
+ from py_dpm.dpm.models import (
14
+ ModuleVersion,
15
+ OperationScope,
16
+ TableVersion,
17
+ HeaderVersion,
18
+ TableVersionHeader,
19
+ Framework,
20
+ Module,
21
+ )
22
+ from py_dpm.dpm.utils import get_session, get_engine
23
+ from py_dpm.exceptions.exceptions import SemanticError
17
24
 
18
25
 
19
26
  @dataclass
@@ -32,6 +39,7 @@ class OperationScopeResult:
32
39
  release_id (Optional[int]): Release ID used for filtering
33
40
  expression (Optional[str]): Original expression if calculated from expression
34
41
  """
42
+
35
43
  existing_scopes: List[OperationScope] = field(default_factory=list)
36
44
  new_scopes: List[OperationScope] = field(default_factory=list)
37
45
  total_scopes: int = 0
@@ -43,77 +51,6 @@ class OperationScopeResult:
43
51
  expression: Optional[str] = None
44
52
 
45
53
 
46
- @dataclass
47
- class ModuleVersionInfo:
48
- """
49
- Module version information with metadata.
50
-
51
- Attributes:
52
- module_vid (int): Module version ID
53
- code (str): Module code
54
- name (str): Module name
55
- description (str): Module description
56
- version_number (str): Version number
57
- from_reference_date (Optional[date]): Start date of validity
58
- to_reference_date (Optional[date]): End date of validity
59
- """
60
- module_vid: int
61
- code: str
62
- name: str
63
- description: str
64
- version_number: str
65
- from_reference_date: Optional[date] = None
66
- to_reference_date: Optional[date] = None
67
-
68
-
69
- @dataclass
70
- class TableVersionInfo:
71
- """
72
- Table version information with metadata.
73
-
74
- Attributes:
75
- table_vid (int): Table version ID
76
- code (str): Table code
77
- name (str): Table name
78
- description (str): Table description
79
- module_vid (Optional[int]): Module version ID
80
- module_code (Optional[str]): Module code
81
- module_name (Optional[str]): Module name
82
- module_version (Optional[str]): Module version number
83
- """
84
- table_vid: int
85
- code: str
86
- name: str
87
- description: str
88
- module_vid: Optional[int] = None
89
- module_code: Optional[str] = None
90
- module_name: Optional[str] = None
91
- module_version: Optional[str] = None
92
-
93
-
94
- @dataclass
95
- class HeaderVersionInfo:
96
- """
97
- Header version information with metadata.
98
-
99
- Attributes:
100
- header_vid (int): Header version ID
101
- code (str): Header code
102
- label (str): Header label/name
103
- header_type (str): Type of header (row/column/sheet)
104
- table_vid (Optional[int]): Associated table version ID (if queried with table context)
105
- table_code (Optional[str]): Associated table code
106
- table_name (Optional[str]): Associated table name
107
- """
108
- header_vid: int
109
- code: str
110
- label: str
111
- header_type: str
112
- table_vid: Optional[int] = None
113
- table_code: Optional[str] = None
114
- table_name: Optional[str] = None
115
-
116
-
117
54
  @dataclass
118
55
  class FrameworkInfo:
119
56
  """
@@ -125,6 +62,7 @@ class FrameworkInfo:
125
62
  name (str): Framework name
126
63
  description (str): Framework description
127
64
  """
65
+
128
66
  framework_id: int
129
67
  code: str
130
68
  name: str
@@ -144,12 +82,13 @@ class OperationScopeDetailedInfo:
144
82
  from_submission_date (Optional[date]): Start date for submission
145
83
  module_versions (List[ModuleVersionInfo]): List of modules with metadata
146
84
  """
85
+
147
86
  operation_scope_id: int
148
87
  operation_vid: int
149
88
  is_active: int
150
89
  severity: str
151
90
  from_submission_date: Optional[date]
152
- module_versions: List[ModuleVersionInfo] = field(default_factory=list)
91
+ module_versions: List[Dict[str, Any]] = field(default_factory=list)
153
92
 
154
93
 
155
94
  class OperationScopesAPI:
@@ -160,7 +99,9 @@ class OperationScopesAPI:
160
99
  in a DPM-XL operation based on table references and precondition items.
161
100
  """
162
101
 
163
- def __init__(self, database_path: Optional[str] = None, connection_url: Optional[str] = None):
102
+ def __init__(
103
+ self, database_path: Optional[str] = None, connection_url: Optional[str] = None
104
+ ):
164
105
  """
165
106
  Initialize the Operation Scopes API.
166
107
 
@@ -175,7 +116,7 @@ class OperationScopesAPI:
175
116
  if connection_url:
176
117
  # Create isolated engine and session for the provided connection URL
177
118
  from sqlalchemy.orm import sessionmaker
178
- from py_dpm.db_utils import create_engine_from_url
119
+ from py_dpm.dpm.utils import create_engine_from_url
179
120
 
180
121
  # Create engine for the connection URL (supports SQLite, PostgreSQL, MySQL, etc.)
181
122
  self.engine = create_engine_from_url(connection_url)
@@ -212,7 +153,7 @@ class OperationScopesAPI:
212
153
  expression: str,
213
154
  operation_version_id: Optional[int] = None,
214
155
  release_id: Optional[int] = None,
215
- read_only: bool = False
156
+ read_only: bool = False,
216
157
  ) -> OperationScopeResult:
217
158
  """
218
159
  Calculate operation scopes from a DPM-XL expression.
@@ -259,19 +200,26 @@ class OperationScopesAPI:
259
200
  has_error=True,
260
201
  error_message="Syntax errors detected in expression",
261
202
  expression=expression,
262
- release_id=release_id
203
+ release_id=release_id,
263
204
  )
264
205
 
265
206
  # Generate AST
266
207
  ast = self.visitor.visit(parse_tree)
267
208
 
268
209
  # Perform operands checking to get data
269
- oc = OperandsChecking(session=self.session, expression=expression, ast=ast, release_id=release_id)
210
+ oc = OperandsChecking(
211
+ session=self.session,
212
+ expression=expression,
213
+ ast=ast,
214
+ release_id=release_id,
215
+ )
270
216
 
271
217
  # Extract table VIDs, precondition items, and table codes from AST
272
218
  # Always extract table codes for cross-version scope calculation
273
219
  # (release_id will be determined later if None)
274
- table_vids, precondition_items, table_codes = self._extract_vids_from_ast(ast, oc.data, extract_codes=True)
220
+ table_vids, precondition_items, table_codes = self._extract_vids_from_ast(
221
+ ast, oc.data, extract_codes=True
222
+ )
275
223
 
276
224
  # Calculate scopes using the low-level API
277
225
  return self.calculate_scopes(
@@ -281,7 +229,7 @@ class OperationScopesAPI:
281
229
  release_id=release_id,
282
230
  expression=expression,
283
231
  table_codes=table_codes,
284
- read_only=read_only
232
+ read_only=read_only,
285
233
  )
286
234
 
287
235
  except SemanticError as e:
@@ -289,17 +237,19 @@ class OperationScopesAPI:
289
237
  has_error=True,
290
238
  error_message=str(e),
291
239
  expression=expression,
292
- release_id=release_id
240
+ release_id=release_id,
293
241
  )
294
242
  except Exception as e:
295
243
  return OperationScopeResult(
296
244
  has_error=True,
297
245
  error_message=f"Unexpected error: {str(e)}",
298
246
  expression=expression,
299
- release_id=release_id
247
+ release_id=release_id,
300
248
  )
301
249
 
302
- def _extract_vids_from_ast(self, ast, data, extract_codes=False) -> tuple[List[int], List[str], List[str]]:
250
+ def _extract_vids_from_ast(
251
+ self, ast, data, extract_codes=False
252
+ ) -> tuple[List[int], List[str], List[str]]:
303
253
  """
304
254
  Extract table VIDs, table codes, and precondition items from OperandsChecking data.
305
255
 
@@ -323,12 +273,12 @@ class OperationScopesAPI:
323
273
  precondition_items = []
324
274
 
325
275
  # Extract unique table VIDs from the data DataFrame
326
- if 'table_vid' in data.columns:
327
- table_vids = data['table_vid'].dropna().unique().astype(int).tolist()
276
+ if "table_vid" in data.columns:
277
+ table_vids = data["table_vid"].dropna().unique().astype(int).tolist()
328
278
 
329
279
  # If requested, also extract table codes for cross-version scope calculation
330
280
  if extract_codes and table_vids:
331
- from py_dpm.models import TableVersion
281
+ from py_dpm.dpm.models import TableVersion
332
282
 
333
283
  # Get table codes for the VIDs
334
284
  table_codes_query = (
@@ -350,15 +300,19 @@ class OperationScopesAPI:
350
300
  precondition_items.append(precondition_code)
351
301
 
352
302
  # Recursively process child nodes
353
- if hasattr(node, '__dict__'):
303
+ if hasattr(node, "__dict__"):
354
304
  for attr_value in vars(node).values():
355
- if hasattr(attr_value, '__class__') and hasattr(attr_value.__class__, '__module__'):
356
- if 'ASTObjects' in attr_value.__class__.__module__:
305
+ if hasattr(attr_value, "__class__") and hasattr(
306
+ attr_value.__class__, "__module__"
307
+ ):
308
+ if "ASTObjects" in attr_value.__class__.__module__:
357
309
  walk_ast(attr_value)
358
310
  elif isinstance(attr_value, list):
359
311
  for item in attr_value:
360
- if hasattr(item, '__class__') and hasattr(item.__class__, '__module__'):
361
- if 'ASTObjects' in item.__class__.__module__:
312
+ if hasattr(item, "__class__") and hasattr(
313
+ item.__class__, "__module__"
314
+ ):
315
+ if "ASTObjects" in item.__class__.__module__:
362
316
  walk_ast(item)
363
317
 
364
318
  walk_ast(ast)
@@ -372,7 +326,7 @@ class OperationScopesAPI:
372
326
  release_id: Optional[int] = None,
373
327
  expression: Optional[str] = None,
374
328
  table_codes: Optional[List[str]] = None,
375
- read_only: bool = False
329
+ read_only: bool = False,
376
330
  ) -> OperationScopeResult:
377
331
  """
378
332
  Calculate operation scopes from table VIDs and precondition items.
@@ -412,8 +366,7 @@ class OperationScopesAPI:
412
366
 
413
367
  # Create service and calculate scopes
414
368
  service = OperationScopeService(
415
- operation_version_id=temp_operation_version_id,
416
- session=self.session
369
+ operation_version_id=temp_operation_version_id, session=self.session
417
370
  )
418
371
 
419
372
  # Use no_autoflush when not persisting to avoid premature flush attempts
@@ -422,14 +375,13 @@ class OperationScopesAPI:
422
375
  tables_vids=tables_vids,
423
376
  precondition_items=precondition_items,
424
377
  release_id=release_id,
425
- table_codes=table_codes
378
+ table_codes=table_codes,
426
379
  )
427
380
 
428
381
  # Analyze results
429
382
  all_scopes = existing_scopes + new_scopes
430
383
  is_cross_module = any(
431
- len(scope.operation_scope_compositions) > 1
432
- for scope in all_scopes
384
+ len(scope.operation_scope_compositions) > 1 for scope in all_scopes
433
385
  )
434
386
 
435
387
  # Collect unique module versions
@@ -454,7 +406,7 @@ class OperationScopesAPI:
454
406
  has_error=False,
455
407
  error_message=None,
456
408
  release_id=release_id,
457
- expression=expression
409
+ expression=expression,
458
410
  )
459
411
 
460
412
  except SemanticError as e:
@@ -463,7 +415,7 @@ class OperationScopesAPI:
463
415
  has_error=True,
464
416
  error_message=str(e),
465
417
  release_id=release_id,
466
- expression=expression
418
+ expression=expression,
467
419
  )
468
420
  except Exception as e:
469
421
  self.session.rollback()
@@ -471,7 +423,7 @@ class OperationScopesAPI:
471
423
  has_error=True,
472
424
  error_message=f"Unexpected error: {str(e)}",
473
425
  release_id=release_id,
474
- expression=expression
426
+ expression=expression,
475
427
  )
476
428
 
477
429
  def get_existing_scopes(self, operation_version_id: int) -> List[OperationScope]:
@@ -539,7 +491,9 @@ class OperationScopesAPI:
539
491
  except Exception:
540
492
  return False
541
493
 
542
- def get_scopes_with_metadata(self, operation_version_id: int) -> List[OperationScopeDetailedInfo]:
494
+ def get_scopes_with_metadata(
495
+ self, operation_version_id: int
496
+ ) -> List[OperationScopeDetailedInfo]:
543
497
  """
544
498
  Get operation scopes with detailed module metadata.
545
499
 
@@ -571,31 +525,33 @@ class OperationScopesAPI:
571
525
  .first()
572
526
  )
573
527
  if module:
574
- module_infos.append(ModuleVersionInfo(
575
- module_vid=module.modulevid,
576
- code=module.code or "",
577
- name=module.name or "",
578
- description=module.description or "",
579
- version_number=module.versionnumber or "",
580
- from_reference_date=module.fromreferencedate,
581
- to_reference_date=module.toreferencedate
582
- ))
583
-
584
- result.append(OperationScopeDetailedInfo(
585
- operation_scope_id=scope.operationscopeid,
586
- operation_vid=scope.operationvid,
587
- is_active=scope.isactive,
588
- severity=scope.severity or "",
589
- from_submission_date=scope.fromsubmissiondate,
590
- module_versions=module_infos
591
- ))
528
+ module_infos.append(
529
+ {
530
+ "module_vid": module.modulevid,
531
+ "code": module.code or "",
532
+ "name": module.name or "",
533
+ "description": module.description or "",
534
+ "version_number": module.versionnumber or "",
535
+ "from_reference_date": module.fromreferencedate,
536
+ "to_reference_date": module.toreferencedate,
537
+ }
538
+ )
539
+
540
+ result.append(
541
+ OperationScopeDetailedInfo(
542
+ operation_scope_id=scope.operationscopeid,
543
+ operation_vid=scope.operationvid,
544
+ is_active=scope.isactive,
545
+ severity=scope.severity or "",
546
+ from_submission_date=scope.fromsubmissiondate,
547
+ module_versions=module_infos,
548
+ )
549
+ )
592
550
 
593
551
  return result
594
552
 
595
553
  def get_scopes_with_metadata_from_expression(
596
- self,
597
- expression: str,
598
- release_id: Optional[int] = None
554
+ self, expression: str, release_id: Optional[int] = None
599
555
  ) -> List[OperationScopeDetailedInfo]:
600
556
  """
601
557
  Calculate operation scopes from expression and return with detailed metadata.
@@ -620,9 +576,7 @@ class OperationScopesAPI:
620
576
  """
621
577
  # Calculate scopes in read-only mode
622
578
  scope_result = self.calculate_scopes_from_expression(
623
- expression=expression,
624
- release_id=release_id,
625
- read_only=True
579
+ expression=expression, release_id=release_id, read_only=True
626
580
  )
627
581
 
628
582
  if scope_result.has_error:
@@ -641,28 +595,34 @@ class OperationScopesAPI:
641
595
  .first()
642
596
  )
643
597
  if module:
644
- module_infos.append(ModuleVersionInfo(
645
- module_vid=module.modulevid,
646
- code=module.code or "",
647
- name=module.name or "",
648
- description=module.description or "",
649
- version_number=module.versionnumber or "",
650
- from_reference_date=module.fromreferencedate,
651
- to_reference_date=module.toreferencedate
652
- ))
653
-
654
- result.append(OperationScopeDetailedInfo(
655
- operation_scope_id=scope.operationscopeid,
656
- operation_vid=scope.operationvid,
657
- is_active=scope.isactive,
658
- severity=scope.severity or "",
659
- from_submission_date=scope.fromsubmissiondate,
660
- module_versions=module_infos
661
- ))
598
+ module_infos.append(
599
+ {
600
+ "module_vid": module.modulevid,
601
+ "code": module.code or "",
602
+ "name": module.name or "",
603
+ "description": module.description or "",
604
+ "version_number": module.versionnumber or "",
605
+ "from_reference_date": module.fromreferencedate,
606
+ "to_reference_date": module.toreferencedate,
607
+ }
608
+ )
609
+
610
+ result.append(
611
+ OperationScopeDetailedInfo(
612
+ operation_scope_id=scope.operationscopeid,
613
+ operation_vid=scope.operationvid,
614
+ is_active=scope.isactive,
615
+ severity=scope.severity or "",
616
+ from_submission_date=scope.fromsubmissiondate,
617
+ module_versions=module_infos,
618
+ )
619
+ )
662
620
 
663
621
  return result
664
622
 
665
- def get_tables_with_metadata(self, operation_version_id: int) -> List[TableVersionInfo]:
623
+ def get_tables_with_metadata(
624
+ self, operation_version_id: int
625
+ ) -> List[Dict[str, Any]]:
666
626
  """
667
627
  Get all tables involved in operation scopes with metadata.
668
628
 
@@ -670,14 +630,14 @@ class OperationScopesAPI:
670
630
  operation_version_id (int): Operation version ID
671
631
 
672
632
  Returns:
673
- List[TableVersionInfo]: List of unique tables with metadata
633
+ List[Dict[str, Any]]: List of unique tables with metadata
674
634
 
675
635
  Example:
676
636
  >>> from py_dpm.api import OperationScopesAPI
677
637
  >>> api = OperationScopesAPI()
678
638
  >>> tables = api.get_tables_with_metadata(operation_version_id=1)
679
639
  >>> for table in tables:
680
- ... print(f"{table.code}: {table.name}")
640
+ ... print(f"{table['code']}: {table['name']}")
681
641
  """
682
642
  scopes = self.get_existing_scopes(operation_version_id)
683
643
 
@@ -691,7 +651,7 @@ class OperationScopesAPI:
691
651
  return []
692
652
 
693
653
  # Query tables from these modules
694
- from py_dpm.models import ModuleVersionComposition
654
+ from py_dpm.dpm.models import ModuleVersionComposition
695
655
 
696
656
  tables_query = (
697
657
  self.session.query(
@@ -699,35 +659,47 @@ class OperationScopesAPI:
699
659
  ModuleVersionComposition.modulevid,
700
660
  ModuleVersion.code,
701
661
  ModuleVersion.name,
702
- ModuleVersion.versionnumber
662
+ ModuleVersion.versionnumber,
663
+ )
664
+ .join(
665
+ ModuleVersionComposition,
666
+ ModuleVersionComposition.tablevid == TableVersion.tablevid,
667
+ )
668
+ .join(
669
+ ModuleVersion,
670
+ ModuleVersion.modulevid == ModuleVersionComposition.modulevid,
703
671
  )
704
- .join(ModuleVersionComposition, ModuleVersionComposition.tablevid == TableVersion.tablevid)
705
- .join(ModuleVersion, ModuleVersion.modulevid == ModuleVersionComposition.modulevid)
706
672
  .filter(ModuleVersionComposition.modulevid.in_(module_vids))
707
673
  .distinct()
708
674
  .order_by(TableVersion.code)
709
675
  )
710
676
 
711
677
  result = []
712
- for table, module_vid, module_code, module_name, module_version in tables_query.all():
713
- result.append(TableVersionInfo(
714
- table_vid=table.tablevid,
715
- code=table.code or "",
716
- name=table.name or "",
717
- description=table.description or "",
718
- module_vid=module_vid,
719
- module_code=module_code or "",
720
- module_name=module_name or "",
721
- module_version=module_version or ""
722
- ))
678
+ for (
679
+ table,
680
+ module_vid,
681
+ module_code,
682
+ module_name,
683
+ module_version,
684
+ ) in tables_query.all():
685
+ result.append(
686
+ {
687
+ "table_vid": table.tablevid,
688
+ "code": table.code or "",
689
+ "name": table.name or "",
690
+ "description": table.description or "",
691
+ "module_vid": module_vid,
692
+ "module_code": module_code or "",
693
+ "module_name": module_name or "",
694
+ "module_version": module_version or "",
695
+ }
696
+ )
723
697
 
724
698
  return result
725
699
 
726
700
  def get_tables_with_metadata_from_expression(
727
- self,
728
- expression: str,
729
- release_id: Optional[int] = None
730
- ) -> List[TableVersionInfo]:
701
+ self, expression: str, release_id: Optional[int] = None
702
+ ) -> List[Dict[str, Any]]:
731
703
  """
732
704
  Get tables from expression with metadata.
733
705
 
@@ -739,7 +711,7 @@ class OperationScopesAPI:
739
711
  release_id (Optional[int]): Specific release ID to filter modules
740
712
 
741
713
  Returns:
742
- List[TableVersionInfo]: List of tables referenced in the expression with metadata
714
+ List[Dict[str, Any]]: List of tables referenced in the expression with metadata
743
715
 
744
716
  Example:
745
717
  >>> from py_dpm.api import OperationScopesAPI
@@ -767,16 +739,23 @@ class OperationScopesAPI:
767
739
  ast = self.visitor.visit(parse_tree)
768
740
 
769
741
  # Perform operands checking to get data
770
- oc = OperandsChecking(session=self.session, expression=expression, ast=ast, release_id=release_id)
742
+ oc = OperandsChecking(
743
+ session=self.session,
744
+ expression=expression,
745
+ ast=ast,
746
+ release_id=release_id,
747
+ )
771
748
 
772
749
  # Extract table VIDs referenced in the expression
773
- table_vids, _, _ = self._extract_vids_from_ast(ast, oc.data, extract_codes=False)
750
+ table_vids, _, _ = self._extract_vids_from_ast(
751
+ ast, oc.data, extract_codes=False
752
+ )
774
753
 
775
754
  if not table_vids:
776
755
  return []
777
756
 
778
757
  # Query only the specific tables referenced in the expression
779
- from py_dpm.models import ModuleVersionComposition
758
+ from py_dpm.dpm.models import ModuleVersionComposition
780
759
 
781
760
  tables_query = (
782
761
  self.session.query(
@@ -784,27 +763,41 @@ class OperationScopesAPI:
784
763
  ModuleVersionComposition.modulevid,
785
764
  ModuleVersion.code,
786
765
  ModuleVersion.name,
787
- ModuleVersion.versionnumber
766
+ ModuleVersion.versionnumber,
767
+ )
768
+ .join(
769
+ ModuleVersionComposition,
770
+ ModuleVersionComposition.tablevid == TableVersion.tablevid,
771
+ )
772
+ .join(
773
+ ModuleVersion,
774
+ ModuleVersion.modulevid == ModuleVersionComposition.modulevid,
788
775
  )
789
- .join(ModuleVersionComposition, ModuleVersionComposition.tablevid == TableVersion.tablevid)
790
- .join(ModuleVersion, ModuleVersion.modulevid == ModuleVersionComposition.modulevid)
791
776
  .filter(TableVersion.tablevid.in_(table_vids))
792
777
  .distinct()
793
778
  .order_by(TableVersion.code)
794
779
  )
795
780
 
796
781
  result = []
797
- for table, module_vid, module_code, module_name, module_version in tables_query.all():
798
- result.append(TableVersionInfo(
799
- table_vid=table.tablevid,
800
- code=table.code or "",
801
- name=table.name or "",
802
- description=table.description or "",
803
- module_vid=module_vid,
804
- module_code=module_code or "",
805
- module_name=module_name or "",
806
- module_version=module_version or ""
807
- ))
782
+ for (
783
+ table,
784
+ module_vid,
785
+ module_code,
786
+ module_name,
787
+ module_version,
788
+ ) in tables_query.all():
789
+ result.append(
790
+ {
791
+ "table_vid": table.tablevid,
792
+ "code": table.code or "",
793
+ "name": table.name or "",
794
+ "description": table.description or "",
795
+ "module_vid": module_vid,
796
+ "module_code": module_code or "",
797
+ "module_name": module_name or "",
798
+ "module_version": module_version or "",
799
+ }
800
+ )
808
801
 
809
802
  return result
810
803
 
@@ -814,10 +807,8 @@ class OperationScopesAPI:
814
807
  return []
815
808
 
816
809
  def get_headers_with_metadata(
817
- self,
818
- operation_version_id: int,
819
- table_vid: Optional[int] = None
820
- ) -> List[HeaderVersionInfo]:
810
+ self, operation_version_id: int, table_vid: Optional[int] = None
811
+ ) -> List[Dict[str, Any]]:
821
812
  """
822
813
  Get headers from tables in operation scopes with metadata.
823
814
 
@@ -826,7 +817,7 @@ class OperationScopesAPI:
826
817
  table_vid (Optional[int]): Filter by specific table VID. If None, returns all headers.
827
818
 
828
819
  Returns:
829
- List[HeaderVersionInfo]: List of headers with metadata
820
+ List[Dict[str, Any]]: List of headers with metadata
830
821
 
831
822
  Example:
832
823
  >>> from py_dpm.api import OperationScopesAPI
@@ -848,7 +839,7 @@ class OperationScopesAPI:
848
839
  return []
849
840
 
850
841
  # Get table VIDs from modules
851
- from py_dpm.models import ModuleVersionComposition, Header
842
+ from py_dpm.dpm.models import ModuleVersionComposition, Header
852
843
 
853
844
  table_vids_query = (
854
845
  self.session.query(ModuleVersionComposition.tablevid)
@@ -858,7 +849,9 @@ class OperationScopesAPI:
858
849
 
859
850
  if table_vid is not None:
860
851
  # Filter by specific table
861
- table_vids_query = table_vids_query.filter(ModuleVersionComposition.tablevid == table_vid)
852
+ table_vids_query = table_vids_query.filter(
853
+ ModuleVersionComposition.tablevid == table_vid
854
+ )
862
855
 
863
856
  table_vids = [row[0] for row in table_vids_query.all()]
864
857
 
@@ -872,9 +865,12 @@ class OperationScopesAPI:
872
865
  TableVersionHeader.tablevid,
873
866
  Header.direction,
874
867
  TableVersion.code,
875
- TableVersion.name
868
+ TableVersion.name,
869
+ )
870
+ .join(
871
+ TableVersionHeader,
872
+ TableVersionHeader.headervid == HeaderVersion.headervid,
876
873
  )
877
- .join(TableVersionHeader, TableVersionHeader.headervid == HeaderVersion.headervid)
878
874
  .join(Header, Header.headerid == TableVersionHeader.headerid)
879
875
  .join(TableVersion, TableVersion.tablevid == TableVersionHeader.tablevid)
880
876
  .filter(TableVersionHeader.tablevid.in_(table_vids))
@@ -883,20 +879,30 @@ class OperationScopesAPI:
883
879
  )
884
880
 
885
881
  result = []
886
- for header_version, table_vid_val, direction, table_code, table_name in headers_query.all():
882
+ for (
883
+ header_version,
884
+ table_vid_val,
885
+ direction,
886
+ table_code,
887
+ table_name,
888
+ ) in headers_query.all():
887
889
  # Map direction to readable type (DPM uses X=Row, Y=Column, Z=Sheet)
888
- header_type_map = {'X': 'Row', 'Y': 'Column', 'Z': 'Sheet'}
890
+ header_type_map = {"X": "Row", "Y": "Column", "Z": "Sheet"}
889
891
  header_type = header_type_map.get(direction, direction or "Unknown")
890
892
 
891
- result.append(HeaderVersionInfo(
892
- header_vid=header_version.headervid,
893
- code=header_version.code or "",
894
- label=header_version.label or "",
895
- header_type=header_type,
896
- table_vid=table_vid_val,
897
- table_code=table_code or "",
898
- table_name=table_name or ""
899
- ))
893
+ result.append(
894
+ result.append(
895
+ {
896
+ "header_vid": header_version.headervid,
897
+ "code": header_version.code or "",
898
+ "label": header_version.label or "",
899
+ "header_type": header_type,
900
+ "table_vid": table_vid_val,
901
+ "table_code": table_code or "",
902
+ "table_name": table_name or "",
903
+ }
904
+ )
905
+ )
900
906
 
901
907
  return result
902
908
 
@@ -904,8 +910,8 @@ class OperationScopesAPI:
904
910
  self,
905
911
  expression: str,
906
912
  table_vid: Optional[int] = None,
907
- release_id: Optional[int] = None
908
- ) -> List[HeaderVersionInfo]:
913
+ release_id: Optional[int] = None,
914
+ ) -> List[Dict[str, Any]]:
909
915
  """
910
916
  Get headers from expression with metadata.
911
917
 
@@ -919,7 +925,7 @@ class OperationScopesAPI:
919
925
  release_id (Optional[int]): Specific release ID to filter modules
920
926
 
921
927
  Returns:
922
- List[HeaderVersionInfo]: List of headers referenced in the expression with metadata
928
+ List[Dict[str, Any]]: List of headers referenced in the expression with metadata
923
929
 
924
930
  Example:
925
931
  >>> from py_dpm.api import OperationScopesAPI
@@ -948,10 +954,17 @@ class OperationScopesAPI:
948
954
  ast = self.visitor.visit(parse_tree)
949
955
 
950
956
  # Perform operands checking to get data
951
- oc = OperandsChecking(session=self.session, expression=expression, ast=ast, release_id=release_id)
957
+ oc = OperandsChecking(
958
+ session=self.session,
959
+ expression=expression,
960
+ ast=ast,
961
+ release_id=release_id,
962
+ )
952
963
 
953
964
  # Extract table VIDs referenced in the expression
954
- table_vids, _, _ = self._extract_vids_from_ast(ast, oc.data, extract_codes=False)
965
+ table_vids, _, _ = self._extract_vids_from_ast(
966
+ ast, oc.data, extract_codes=False
967
+ )
955
968
 
956
969
  if not table_vids:
957
970
  return []
@@ -968,9 +981,9 @@ class OperationScopesAPI:
968
981
  # are USED in the expression syntax (r=Row, c=Column, s=Sheet), which may differ
969
982
  # from the Header.direction field in the database (some tables may be transposed).
970
983
  # We return headers based on their USAGE in the expression, not their catalog definition.
971
- row_codes = set(oc.data['row_code'].dropna().unique().tolist())
972
- column_codes = set(oc.data['column_code'].dropna().unique().tolist())
973
- sheet_codes = set(oc.data['sheet_code'].dropna().unique().tolist())
984
+ row_codes = set(oc.data["row_code"].dropna().unique().tolist())
985
+ column_codes = set(oc.data["column_code"].dropna().unique().tolist())
986
+ sheet_codes = set(oc.data["sheet_code"].dropna().unique().tolist())
974
987
 
975
988
  # Create mapping: code -> usage dimension(s) in the expression
976
989
  # The same code might be used in multiple dimensions
@@ -978,24 +991,26 @@ class OperationScopesAPI:
978
991
  for code in row_codes:
979
992
  if code not in code_usage:
980
993
  code_usage[code] = set()
981
- code_usage[code].add('Row')
994
+ code_usage[code].add("Row")
982
995
  for code in column_codes:
983
996
  if code not in code_usage:
984
997
  code_usage[code] = set()
985
- code_usage[code].add('Column')
998
+ code_usage[code].add("Column")
986
999
  for code in sheet_codes:
987
1000
  if code not in code_usage:
988
1001
  code_usage[code] = set()
989
- code_usage[code].add('Sheet')
1002
+ code_usage[code].add("Sheet")
990
1003
 
991
1004
  if not code_usage:
992
1005
  return []
993
1006
 
994
1007
  all_header_codes = set(code_usage.keys())
995
1008
 
1009
+ # Query headers - get all headers with matching codes
996
1010
  # Query headers - get all headers with matching codes
997
1011
  # Note: We don't filter by Header.direction because tables may be transposed
998
- from py_dpm.models import Header
1012
+ from py_dpm.dpm.models import Header
1013
+ from sqlalchemy import and_
999
1014
 
1000
1015
  headers_query = (
1001
1016
  self.session.query(
@@ -1003,15 +1018,20 @@ class OperationScopesAPI:
1003
1018
  TableVersionHeader.tablevid,
1004
1019
  Header.direction,
1005
1020
  TableVersion.code,
1006
- TableVersion.name
1021
+ TableVersion.name,
1022
+ )
1023
+ .join(
1024
+ TableVersionHeader,
1025
+ TableVersionHeader.headervid == HeaderVersion.headervid,
1007
1026
  )
1008
- .join(TableVersionHeader, TableVersionHeader.headervid == HeaderVersion.headervid)
1009
1027
  .join(Header, Header.headerid == HeaderVersion.headerid)
1010
- .join(TableVersion, TableVersion.tablevid == TableVersionHeader.tablevid)
1028
+ .join(
1029
+ TableVersion, TableVersion.tablevid == TableVersionHeader.tablevid
1030
+ )
1011
1031
  .filter(
1012
1032
  and_(
1013
1033
  TableVersionHeader.tablevid.in_(table_vids),
1014
- HeaderVersion.code.in_(all_header_codes)
1034
+ HeaderVersion.code.in_(all_header_codes),
1015
1035
  )
1016
1036
  )
1017
1037
  .distinct()
@@ -1020,7 +1040,13 @@ class OperationScopesAPI:
1020
1040
  result = []
1021
1041
  seen = set() # Track (code, usage_type, table_vid) to avoid duplicates
1022
1042
 
1023
- for header_version, table_vid_val, direction, table_code, table_name in headers_query.all():
1043
+ for (
1044
+ header_version,
1045
+ table_vid_val,
1046
+ direction,
1047
+ table_code,
1048
+ table_name,
1049
+ ) in headers_query.all():
1024
1050
  code = header_version.code or ""
1025
1051
 
1026
1052
  # For each usage dimension of this code in the expression
@@ -1033,15 +1059,18 @@ class OperationScopesAPI:
1033
1059
  seen.add(key)
1034
1060
 
1035
1061
  # Return header with usage type from expression, not catalog direction
1036
- result.append(HeaderVersionInfo(
1037
- header_vid=header_version.headervid,
1038
- code=code,
1039
- label=header_version.label or "",
1040
- header_type=usage_type, # Usage in expression: Row, Column, or Sheet
1041
- table_vid=table_vid_val,
1042
- table_code=table_code or "",
1043
- table_name=table_name or ""
1044
- ))
1062
+ # Return header with usage type from expression, not catalog direction
1063
+ result.append(
1064
+ {
1065
+ "header_vid": header_version.headervid,
1066
+ "code": code,
1067
+ "label": header_version.label or "",
1068
+ "header_type": usage_type, # Usage in expression: Row, Column, or Sheet
1069
+ "table_vid": table_vid_val,
1070
+ "table_code": table_code or "",
1071
+ "table_name": table_name or "",
1072
+ }
1073
+ )
1045
1074
  break # Only add each header once per code (we'll get multiple if used in multiple dims)
1046
1075
 
1047
1076
  return result
@@ -1052,8 +1081,7 @@ class OperationScopesAPI:
1052
1081
  return []
1053
1082
 
1054
1083
  def get_frameworks_with_metadata(
1055
- self,
1056
- operation_version_id: int
1084
+ self, operation_version_id: int
1057
1085
  ) -> List[FrameworkInfo]:
1058
1086
  """
1059
1087
  Get frameworks from operation scopes with metadata.
@@ -1094,19 +1122,19 @@ class OperationScopesAPI:
1094
1122
 
1095
1123
  result = []
1096
1124
  for framework in frameworks_query.all():
1097
- result.append(FrameworkInfo(
1098
- framework_id=framework.frameworkid,
1099
- code=framework.code or "",
1100
- name=framework.name or "",
1101
- description=framework.description or ""
1102
- ))
1125
+ result.append(
1126
+ FrameworkInfo(
1127
+ framework_id=framework.frameworkid,
1128
+ code=framework.code or "",
1129
+ name=framework.name or "",
1130
+ description=framework.description or "",
1131
+ )
1132
+ )
1103
1133
 
1104
1134
  return result
1105
1135
 
1106
1136
  def get_frameworks_with_metadata_from_expression(
1107
- self,
1108
- expression: str,
1109
- release_id: Optional[int] = None
1137
+ self, expression: str, release_id: Optional[int] = None
1110
1138
  ) -> List[FrameworkInfo]:
1111
1139
  """
1112
1140
  Get frameworks from expression with metadata.
@@ -1127,9 +1155,7 @@ class OperationScopesAPI:
1127
1155
  """
1128
1156
  # Calculate scopes in read-only mode
1129
1157
  scope_result = self.calculate_scopes_from_expression(
1130
- expression=expression,
1131
- release_id=release_id,
1132
- read_only=True
1158
+ expression=expression, release_id=release_id, read_only=True
1133
1159
  )
1134
1160
 
1135
1161
  if scope_result.has_error:
@@ -1153,22 +1179,46 @@ class OperationScopesAPI:
1153
1179
 
1154
1180
  result = []
1155
1181
  for framework in frameworks_query.all():
1156
- result.append(FrameworkInfo(
1157
- framework_id=framework.frameworkid,
1158
- code=framework.code or "",
1159
- name=framework.name or "",
1160
- description=framework.description or ""
1161
- ))
1182
+ result.append(
1183
+ FrameworkInfo(
1184
+ framework_id=framework.frameworkid,
1185
+ code=framework.code or "",
1186
+ name=framework.name or "",
1187
+ description=framework.description or "",
1188
+ )
1189
+ )
1162
1190
 
1163
1191
  return result
1164
1192
 
1165
1193
  def __del__(self):
1166
1194
  """Clean up resources."""
1167
- if hasattr(self, 'session'):
1195
+ if hasattr(self, "session"):
1168
1196
  self.session.close()
1169
- if hasattr(self, 'engine') and self.engine is not None:
1197
+ if hasattr(self, "engine") and self.engine is not None:
1170
1198
  self.engine.dispose()
1171
1199
 
1200
+ def close(self):
1201
+ """
1202
+ Explicitly close the underlying SQLAlchemy session and dispose any private engine.
1203
+ """
1204
+ try:
1205
+ if hasattr(self, "session") and self.session:
1206
+ self.session.close()
1207
+ except Exception:
1208
+ pass
1209
+
1210
+ try:
1211
+ if hasattr(self, "engine") and self.engine is not None:
1212
+ self.engine.dispose()
1213
+ except Exception:
1214
+ pass
1215
+
1216
+ def __enter__(self):
1217
+ return self
1218
+
1219
+ def __exit__(self, exc_type, exc_val, exc_tb):
1220
+ self.close()
1221
+
1172
1222
 
1173
1223
  # Convenience functions for direct usage
1174
1224
  def calculate_scopes_from_expression(
@@ -1177,7 +1227,7 @@ def calculate_scopes_from_expression(
1177
1227
  release_id: Optional[int] = None,
1178
1228
  database_path: Optional[str] = None,
1179
1229
  connection_url: Optional[str] = None,
1180
- read_only: bool = True
1230
+ read_only: bool = True,
1181
1231
  ) -> OperationScopeResult:
1182
1232
  """
1183
1233
  Convenience function to calculate operation scopes from expression.
@@ -1194,7 +1244,7 @@ def calculate_scopes_from_expression(
1194
1244
  OperationScopeResult: Result containing existing and new scopes
1195
1245
 
1196
1246
  Example:
1197
- >>> from py_dpm.api.operation_scopes import calculate_scopes_from_expression
1247
+ >>> from py_dpm.api.dpm.operation_scopes import calculate_scopes_from_expression
1198
1248
  >>> result = calculate_scopes_from_expression(
1199
1249
  ... "{tC_01.00, r0100, c0010}",
1200
1250
  ... release_id=4,
@@ -1203,13 +1253,15 @@ def calculate_scopes_from_expression(
1203
1253
  >>> print(f"Total scopes: {result.total_scopes}")
1204
1254
  """
1205
1255
  api = OperationScopesAPI(database_path=database_path, connection_url=connection_url)
1206
- return api.calculate_scopes_from_expression(expression, operation_version_id, release_id, read_only=read_only)
1256
+ return api.calculate_scopes_from_expression(
1257
+ expression, operation_version_id, release_id, read_only=read_only
1258
+ )
1207
1259
 
1208
1260
 
1209
1261
  def get_existing_scopes(
1210
1262
  operation_version_id: int,
1211
1263
  database_path: Optional[str] = None,
1212
- connection_url: Optional[str] = None
1264
+ connection_url: Optional[str] = None,
1213
1265
  ) -> List[OperationScope]:
1214
1266
  """
1215
1267
  Convenience function to get existing scopes for an operation.
@@ -1223,7 +1275,7 @@ def get_existing_scopes(
1223
1275
  List[OperationScope]: List of existing scopes
1224
1276
 
1225
1277
  Example:
1226
- >>> from py_dpm.api.operation_scopes import get_existing_scopes
1278
+ >>> from py_dpm.api.dpm.operation_scopes import get_existing_scopes
1227
1279
  >>> scopes = get_existing_scopes(operation_version_id=1, database_path="./database.db")
1228
1280
  """
1229
1281
  api = OperationScopesAPI(database_path=database_path, connection_url=connection_url)