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
@@ -0,0 +1,838 @@
1
+ from typing import Optional
2
+
3
+ from sqlalchemy import func, literal, and_, or_, join
4
+ from sqlalchemy.orm import aliased
5
+
6
+ from py_dpm.dpm.models import (
7
+ Framework,
8
+ Module,
9
+ ModuleVersionComposition,
10
+ Table,
11
+ ModuleVersion,
12
+ TableVersion,
13
+ Header,
14
+ HeaderVersion,
15
+ TableVersionHeader,
16
+ Cell,
17
+ TableVersionCell,
18
+ VariableVersion,
19
+ Property,
20
+ DataType,
21
+ Item,
22
+ ItemCategory,
23
+ ContextComposition,
24
+ SubCategoryVersion,
25
+ SubCategoryItem,
26
+ )
27
+ from py_dpm.dpm.queries.filters import (
28
+ filter_by_release,
29
+ filter_by_date,
30
+ filter_active_only,
31
+ filter_item_version,
32
+ )
33
+
34
+
35
+ class HierarchicalQuery:
36
+ """
37
+ Queries that return hierarchical dictionaries for collections
38
+ and similar objects
39
+ """
40
+
41
+ @staticmethod
42
+ def get_module_version(
43
+ session,
44
+ module_code: str,
45
+ release_id: Optional[int] = None,
46
+ date: Optional[str] = None,
47
+ release_code: Optional[str] = None,
48
+ ) -> dict:
49
+
50
+ if sum(bool(x) for x in [release_id, date, release_code]) > 1:
51
+ raise ValueError(
52
+ "Specify a maximum of one of release_id, release_code or date."
53
+ )
54
+
55
+ q = session.query(ModuleVersion).filter(ModuleVersion.code == module_code)
56
+
57
+ if date:
58
+ q = filter_by_date(
59
+ q,
60
+ date,
61
+ ModuleVersion.fromreferencedate,
62
+ ModuleVersion.toreferencedate,
63
+ )
64
+ elif release_id:
65
+ q = filter_by_release(
66
+ q,
67
+ start_col=ModuleVersion.startreleaseid,
68
+ end_col=ModuleVersion.endreleaseid,
69
+ release_id=release_id,
70
+ )
71
+ elif release_code:
72
+ q = filter_by_release(
73
+ q,
74
+ start_col=ModuleVersion.startreleaseid,
75
+ end_col=ModuleVersion.endreleaseid,
76
+ release_code=release_code,
77
+ )
78
+ else:
79
+ q = filter_active_only(q, end_col=ModuleVersion.endreleaseid)
80
+
81
+ query_result = q.all()
82
+
83
+ if len(query_result) != 1:
84
+ raise ValueError(
85
+ f"Should return 1 record, but returned {len(query_result)}"
86
+ )
87
+ result = query_result[0].to_dict()
88
+
89
+ table_versions = query_result[0].table_versions
90
+ result["table_versions"] = [tv.to_dict() for tv in table_versions]
91
+
92
+ return result
93
+
94
+ @staticmethod
95
+ def get_all_frameworks(
96
+ session,
97
+ release_id: Optional[int] = None,
98
+ date: Optional[str] = None,
99
+ release_code: Optional[str] = None,
100
+ ) -> list[dict]:
101
+
102
+ if sum(bool(x) for x in [release_id, date, release_code]) > 1:
103
+ raise ValueError(
104
+ "Specify a maximum of one of release_id, release_code or date."
105
+ )
106
+
107
+ q = (
108
+ session.query(
109
+ # Framework
110
+ Framework.frameworkid,
111
+ Framework.code.label("framework_code"),
112
+ Framework.name.label("framework_name"),
113
+ Framework.description.label("framework_description"),
114
+ # ModuleVersion
115
+ ModuleVersion.modulevid,
116
+ ModuleVersion.moduleid,
117
+ ModuleVersion.startreleaseid.label("module_version_startreleaseid"),
118
+ ModuleVersion.endreleaseid.label("module_version_endreleaseid"),
119
+ ModuleVersion.code.label("module_version_code"),
120
+ ModuleVersion.name.label("module_version_name"),
121
+ ModuleVersion.description.label("module_version_description"),
122
+ ModuleVersion.versionnumber,
123
+ ModuleVersion.fromreferencedate,
124
+ ModuleVersion.toreferencedate,
125
+ # TableVersion
126
+ TableVersion.tablevid,
127
+ TableVersion.code.label("table_version_code"),
128
+ TableVersion.name.label("table_version_name"),
129
+ TableVersion.description.label("table_version_description"),
130
+ TableVersion.tableid.label("table_version_tableid"),
131
+ TableVersion.abstracttableid,
132
+ TableVersion.startreleaseid.label("table_version_startreleaseid"),
133
+ TableVersion.endreleaseid.label("table_version_endreleaseid"),
134
+ # Table
135
+ Table.tableid.label("table_tableid"),
136
+ Table.isabstract,
137
+ Table.hasopencolumns,
138
+ Table.hasopenrows,
139
+ Table.hasopensheets,
140
+ Table.isnormalised,
141
+ Table.isflat,
142
+ )
143
+ .join(Module, Framework.modules)
144
+ .join(ModuleVersion, Module.module_versions)
145
+ .join(
146
+ ModuleVersionComposition,
147
+ ModuleVersion.module_version_compositions,
148
+ )
149
+ .join(TableVersion, ModuleVersionComposition.table_version)
150
+ .join(Table, TableVersion.table)
151
+ )
152
+
153
+ if date:
154
+ q = filter_by_date(
155
+ q,
156
+ date,
157
+ ModuleVersion.fromreferencedate,
158
+ ModuleVersion.toreferencedate,
159
+ )
160
+ elif release_id:
161
+ q = filter_by_release(
162
+ q,
163
+ start_col=ModuleVersion.startreleaseid,
164
+ end_col=ModuleVersion.endreleaseid,
165
+ release_id=release_id,
166
+ )
167
+ elif release_code:
168
+ q = filter_by_release(
169
+ q,
170
+ start_col=ModuleVersion.startreleaseid,
171
+ end_col=ModuleVersion.endreleaseid,
172
+ release_code=release_code,
173
+ )
174
+ else:
175
+ q = filter_active_only(q, end_col=ModuleVersion.endreleaseid)
176
+
177
+ # Execute query and return list of dictionaries
178
+ query_result = [dict(row._mapping) for row in q.all()]
179
+
180
+ frameworks = {}
181
+
182
+ for row in query_result:
183
+ fw_id = row["frameworkid"]
184
+ if fw_id not in frameworks:
185
+ frameworks[fw_id] = {
186
+ "frameworkid": row["frameworkid"],
187
+ "code": row["framework_code"],
188
+ "name": row["framework_name"],
189
+ "description": row["framework_description"],
190
+ "module_versions": {},
191
+ }
192
+
193
+ fw = frameworks[fw_id]
194
+
195
+ mod_vid = row["modulevid"]
196
+ if mod_vid not in fw["module_versions"]:
197
+ fw["module_versions"][mod_vid] = {
198
+ "modulevid": row["modulevid"],
199
+ "moduleid": row["moduleid"],
200
+ "startreleaseid": row["module_version_startreleaseid"],
201
+ "endreleaseid": row["module_version_endreleaseid"],
202
+ "code": row["module_version_code"],
203
+ "name": row["module_version_name"],
204
+ "description": row["module_version_description"],
205
+ "versionnumber": row["versionnumber"],
206
+ "fromreferencedate": row["fromreferencedate"],
207
+ "toreferencedate": row["toreferencedate"],
208
+ "table_versions": [],
209
+ }
210
+
211
+ mod = fw["module_versions"][mod_vid]
212
+
213
+ # Flatten TableVersion and Table info
214
+ table_ver = {
215
+ "tablevid": row["tablevid"],
216
+ "code": row["table_version_code"],
217
+ "name": row["table_version_name"],
218
+ "description": row["table_version_description"],
219
+ "tableid": row["table_version_tableid"],
220
+ "abstracttableid": row["abstracttableid"],
221
+ "startreleaseid": row["table_version_startreleaseid"],
222
+ "endreleaseid": row["table_version_endreleaseid"],
223
+ "isabstract": row["isabstract"],
224
+ "hasopencolumns": row["hasopencolumns"],
225
+ "hasopenrows": row["hasopenrows"],
226
+ "hasopensheets": row["hasopensheets"],
227
+ "isnormalised": row["isnormalised"],
228
+ "isflat": row["isflat"],
229
+ }
230
+ mod["table_versions"].append(table_ver)
231
+
232
+ # Convert dicts back to lists
233
+ final_result = []
234
+ for fw in frameworks.values():
235
+ fw["module_versions"] = list(fw["module_versions"].values())
236
+ final_result.append(fw)
237
+
238
+ return final_result
239
+
240
+ @staticmethod
241
+ def get_table_details(
242
+ session,
243
+ table_code: str,
244
+ release_id: Optional[int] = None,
245
+ date: Optional[str] = None,
246
+ release_code: Optional[str] = None,
247
+ ) -> dict:
248
+ # Input Validation: Mutually exclusive release params
249
+ if sum(bool(x) for x in [release_id, release_code, date]) > 1:
250
+ raise ValueError(
251
+ "Specify a maximum of one of release_id, release_code or date."
252
+ )
253
+
254
+ # Determine the relevant table version using the same filter helpers
255
+ # as other hierarchical queries.
256
+ q_tv = (
257
+ session.query(TableVersion)
258
+ .join(
259
+ ModuleVersionComposition,
260
+ ModuleVersionComposition.tablevid == TableVersion.tablevid,
261
+ )
262
+ .join(
263
+ ModuleVersion,
264
+ ModuleVersion.modulevid == ModuleVersionComposition.modulevid,
265
+ )
266
+ .filter(TableVersion.code == table_code)
267
+ )
268
+
269
+ if date:
270
+ q_tv = filter_by_date(
271
+ q_tv,
272
+ date,
273
+ ModuleVersion.fromreferencedate,
274
+ ModuleVersion.toreferencedate,
275
+ )
276
+ elif release_id or release_code:
277
+ q_tv = filter_by_release(
278
+ q_tv,
279
+ start_col=ModuleVersion.startreleaseid,
280
+ end_col=ModuleVersion.endreleaseid,
281
+ release_id=release_id,
282
+ release_code=release_code,
283
+ )
284
+ else:
285
+ q_tv = filter_active_only(q_tv, end_col=ModuleVersion.endreleaseid)
286
+
287
+ table_version = q_tv.order_by(
288
+ ModuleVersion.startreleaseid.desc()
289
+ ).first()
290
+
291
+ # If no table version matches the filters, the table does not exist
292
+ # (for the requested context).
293
+ if not table_version:
294
+ raise ValueError(f"Table {table_code} was not found.")
295
+
296
+ # If the underlying table is not abstract, return details for this
297
+ # single table version as before.
298
+ table_obj = table_version.table
299
+ if not table_obj or not table_obj.isabstract:
300
+ header_results, cell_results = HierarchicalQuery._fetch_header_and_cells(
301
+ session, table_version.tablevid
302
+ )
303
+ if not header_results:
304
+ return {}
305
+ return HierarchicalQuery._transform_to_dpm_format(
306
+ header_results, cell_results
307
+ )
308
+
309
+ # If the table is abstract, consolidate details for all table versions
310
+ # that reference this table as their abstract table, applying the same
311
+ # release/date filters.
312
+ q_child = (
313
+ session.query(TableVersion)
314
+ .join(
315
+ ModuleVersionComposition,
316
+ ModuleVersionComposition.tablevid == TableVersion.tablevid,
317
+ )
318
+ .join(
319
+ ModuleVersion,
320
+ ModuleVersion.modulevid == ModuleVersionComposition.modulevid,
321
+ )
322
+ .filter(TableVersion.abstracttableid == table_obj.tableid)
323
+ )
324
+
325
+ if date:
326
+ q_child = filter_by_date(
327
+ q_child,
328
+ date,
329
+ ModuleVersion.fromreferencedate,
330
+ ModuleVersion.toreferencedate,
331
+ )
332
+ elif release_id or release_code:
333
+ q_child = filter_by_release(
334
+ q_child,
335
+ start_col=ModuleVersion.startreleaseid,
336
+ end_col=ModuleVersion.endreleaseid,
337
+ release_id=release_id,
338
+ release_code=release_code,
339
+ )
340
+ else:
341
+ q_child = filter_active_only(q_child, end_col=ModuleVersion.endreleaseid)
342
+
343
+ child_versions = q_child.all()
344
+
345
+ # If there are no child versions, fall back to the abstract table
346
+ # version itself.
347
+ if not child_versions:
348
+ header_results, cell_results = HierarchicalQuery._fetch_header_and_cells(
349
+ session, table_version.tablevid
350
+ )
351
+ if not header_results:
352
+ return {}
353
+ result = HierarchicalQuery._transform_to_dpm_format(
354
+ header_results, cell_results
355
+ )
356
+ result["tableCode"] = table_version.code
357
+ result["tableTitle"] = table_version.name
358
+ result["tableVid"] = table_version.tablevid
359
+ return result
360
+
361
+ # Collect headers and cells from all matching child table versions.
362
+ all_headers = []
363
+ all_cells = []
364
+ for child_tv in child_versions:
365
+ child_headers, child_cells = HierarchicalQuery._fetch_header_and_cells(
366
+ session, child_tv.tablevid
367
+ )
368
+ all_headers.extend(child_headers)
369
+ all_cells.extend(child_cells)
370
+
371
+ if not all_headers:
372
+ return {}
373
+
374
+ result = HierarchicalQuery._transform_to_dpm_format(all_headers, all_cells)
375
+
376
+ # Represent the consolidated view as belonging to the originally
377
+ # requested (abstract) table version.
378
+ result["tableCode"] = table_version.code
379
+ result["tableTitle"] = table_version.name
380
+ result["tableVid"] = table_version.tablevid
381
+ return result
382
+
383
+ @staticmethod
384
+ def get_table_modelling(
385
+ session,
386
+ table_code: str,
387
+ release_id: Optional[int] = None,
388
+ date: Optional[str] = None,
389
+ release_code: Optional[str] = None,
390
+ ) -> dict:
391
+ """
392
+ Return modelling metadata for a table, based on the context
393
+ composition associated with each header.
394
+
395
+ The selection logic for the relevant table version mirrors
396
+ ``get_table_details``: table code plus optional release/date filters.
397
+ """
398
+ # Input Validation: Mutually exclusive release params
399
+ if sum(bool(x) for x in [release_id, release_code, date]) > 1:
400
+ raise ValueError(
401
+ "Specify a maximum of one of release_id, release_code or date."
402
+ )
403
+
404
+ # Resolve the relevant table version using the same pattern as
405
+ # get_table_details.
406
+ q_tv = (
407
+ session.query(TableVersion)
408
+ .join(
409
+ ModuleVersionComposition,
410
+ ModuleVersionComposition.tablevid == TableVersion.tablevid,
411
+ )
412
+ .join(
413
+ ModuleVersion,
414
+ ModuleVersion.modulevid == ModuleVersionComposition.modulevid,
415
+ )
416
+ .filter(TableVersion.code == table_code)
417
+ )
418
+
419
+ if date:
420
+ q_tv = filter_by_date(
421
+ q_tv,
422
+ date,
423
+ ModuleVersion.fromreferencedate,
424
+ ModuleVersion.toreferencedate,
425
+ )
426
+ elif release_id or release_code:
427
+ q_tv = filter_by_release(
428
+ q_tv,
429
+ start_col=ModuleVersion.startreleaseid,
430
+ end_col=ModuleVersion.endreleaseid,
431
+ release_id=release_id,
432
+ release_code=release_code,
433
+ )
434
+ else:
435
+ q_tv = filter_active_only(q_tv, end_col=ModuleVersion.endreleaseid)
436
+
437
+ table_version = q_tv.order_by(
438
+ ModuleVersion.startreleaseid.desc()
439
+ ).first()
440
+
441
+ if not table_version:
442
+ raise ValueError(f"Table {table_code} was not found.")
443
+
444
+ # Aliases for Item and ItemCategory used multiple times in the query
445
+ iccp = aliased(ItemCategory) # context property category
446
+ icci = aliased(ItemCategory) # context item category
447
+ icmp = aliased(ItemCategory) # main property category
448
+ icp = aliased(Item) # context property item
449
+ ici = aliased(Item) # context item
450
+ mpi = aliased(Item) # main property item
451
+
452
+ # Pre-build joined table expressions so that the SQL more closely
453
+ # matches:
454
+ # LEFT JOIN (ItemCategory_X JOIN Item_Y ON ...) ON ...
455
+ context_property_join = join(iccp, icp, iccp.itemid == icp.itemid)
456
+ context_item_join = join(icci, ici, icci.itemid == ici.itemid)
457
+ main_property_join = join(icmp, mpi, icmp.itemid == mpi.itemid)
458
+
459
+ # ORM translation of the desired SQL query with left joins so that
460
+ # headers without a context (or without a main property) are still
461
+ # returned.
462
+ q = (
463
+ session.query(
464
+ HeaderVersion.headerid.label("header_id"),
465
+ icmp.signature.label("main_property_code"),
466
+ mpi.name.label("main_property_name"),
467
+ iccp.signature.label("context_property_code"),
468
+ icp.name.label("context_property_name"),
469
+ icci.signature.label("context_item_code"),
470
+ ici.name.label("context_item_name"),
471
+ )
472
+ .select_from(TableVersion)
473
+ .join(
474
+ TableVersionHeader,
475
+ TableVersionHeader.tablevid == TableVersion.tablevid,
476
+ )
477
+ .join(
478
+ HeaderVersion,
479
+ TableVersionHeader.headervid == HeaderVersion.headervid,
480
+ )
481
+ # Context is optional, so use LEFT JOIN
482
+ .outerjoin(
483
+ ContextComposition,
484
+ HeaderVersion.contextid == ContextComposition.contextid,
485
+ )
486
+ # Context property (optional, versioned)
487
+ .outerjoin(
488
+ context_property_join,
489
+ and_(
490
+ ContextComposition.propertyid == iccp.itemid,
491
+ filter_item_version(
492
+ TableVersion.startreleaseid,
493
+ iccp.startreleaseid,
494
+ iccp.endreleaseid,
495
+ ),
496
+ ),
497
+ )
498
+ # Context item (optional, versioned)
499
+ .outerjoin(
500
+ context_item_join,
501
+ and_(
502
+ ContextComposition.itemid == icci.itemid,
503
+ filter_item_version(
504
+ TableVersion.startreleaseid,
505
+ icci.startreleaseid,
506
+ icci.endreleaseid,
507
+ ),
508
+ ),
509
+ )
510
+ # Main property (optional, versioned)
511
+ .outerjoin(
512
+ main_property_join,
513
+ and_(
514
+ HeaderVersion.propertyid == icmp.itemid,
515
+ filter_item_version(
516
+ TableVersion.startreleaseid,
517
+ icmp.startreleaseid,
518
+ icmp.endreleaseid,
519
+ ),
520
+ ),
521
+ )
522
+ .filter(TableVersion.tablevid == table_version.tablevid)
523
+ )
524
+
525
+ modelling: dict[int, list[dict]] = {}
526
+ for row in q.all():
527
+ header_id = row.header_id
528
+ if header_id not in modelling:
529
+ modelling[header_id] = []
530
+
531
+ # Main property pair (if present)
532
+ if row.main_property_code is not None or row.main_property_name is not None:
533
+ modelling[header_id].append(
534
+ {
535
+ "main_property_code": row.main_property_code,
536
+ "main_property_name": row.main_property_name,
537
+ }
538
+ )
539
+
540
+ # Context pair: property and item metadata in a single object
541
+ if (
542
+ row.context_property_code is not None
543
+ or row.context_property_name is not None
544
+ or row.context_item_code is not None
545
+ or row.context_item_name is not None
546
+ ):
547
+ modelling[header_id].append(
548
+ {
549
+ "context_property_code": row.context_property_code,
550
+ "context_property_name": row.context_property_name,
551
+ "context_item_code": row.context_item_code,
552
+ "context_item_name": row.context_item_name,
553
+ }
554
+ )
555
+
556
+ return modelling
557
+
558
+ @staticmethod
559
+ def _fetch_header_and_cells(session, table_vid):
560
+ # Get the table version and module version info for release filtering
561
+ tv_info = (
562
+ session.query(
563
+ TableVersion.tablevid,
564
+ TableVersion.startreleaseid,
565
+ ModuleVersion.startreleaseid.label("mv_startreleaseid"),
566
+ )
567
+ .join(
568
+ ModuleVersionComposition,
569
+ ModuleVersionComposition.tablevid == TableVersion.tablevid,
570
+ )
571
+ .join(
572
+ ModuleVersion,
573
+ ModuleVersion.modulevid == ModuleVersionComposition.modulevid,
574
+ )
575
+ .filter(TableVersion.tablevid == table_vid)
576
+ .first()
577
+ )
578
+
579
+ if not tv_info:
580
+ return [], []
581
+
582
+ release_id = tv_info.mv_startreleaseid
583
+
584
+ # Aliases for the multiple ItemCategory and Item joins
585
+ ic_prop = aliased(ItemCategory) # For property code
586
+ ic_enum = aliased(ItemCategory) # For enumeration items
587
+ item_prop = aliased(Item) # For property name
588
+ item_enum = aliased(Item) # For enumeration item names
589
+
590
+ # Headers query with property codes and enumeration items
591
+ header_query = (
592
+ session.query(
593
+ TableVersion.tablevid.label("table_vid"),
594
+ TableVersion.code.label("table_code"),
595
+ TableVersion.name.label("table_name"),
596
+ TableVersionHeader.headerid.label("header_id"),
597
+ TableVersionHeader.parentheaderid.label("parent_header_id"),
598
+ TableVersionHeader.parentfirst.label("parent_first"),
599
+ TableVersionHeader.order.label("order"),
600
+ TableVersionHeader.isabstract.label("is_abstract"),
601
+ HeaderVersion.code.label("header_code"),
602
+ HeaderVersion.label.label("label"),
603
+ Header.direction.label("direction"),
604
+ Header.iskey.label("is_key"),
605
+ ic_prop.code.label("property_code"),
606
+ item_prop.name.label("property_name"),
607
+ DataType.name.label("data_type_name"),
608
+ ic_enum.signature.label("item_signature"),
609
+ item_enum.name.label("item_label"),
610
+ )
611
+ .join(
612
+ TableVersionHeader,
613
+ TableVersionHeader.tablevid == TableVersion.tablevid,
614
+ )
615
+ .join(
616
+ HeaderVersion,
617
+ TableVersionHeader.headervid == HeaderVersion.headervid,
618
+ )
619
+ .join(Header, HeaderVersion.headerid == Header.headerid)
620
+ .outerjoin(
621
+ ic_prop,
622
+ and_(
623
+ HeaderVersion.propertyid == ic_prop.itemid,
624
+ filter_item_version(
625
+ release_id,
626
+ ic_prop.startreleaseid,
627
+ ic_prop.endreleaseid,
628
+ ),
629
+ ),
630
+ )
631
+ # Property and its Item (for name) do not require an ItemCategory row
632
+ .outerjoin(Property, HeaderVersion.propertyid == Property.propertyid)
633
+ .outerjoin(item_prop, Property.propertyid == item_prop.itemid)
634
+ .outerjoin(DataType, Property.datatypeid == DataType.datatypeid)
635
+ .outerjoin(
636
+ SubCategoryVersion,
637
+ HeaderVersion.subcategoryvid == SubCategoryVersion.subcategoryvid,
638
+ )
639
+ .outerjoin(
640
+ SubCategoryItem,
641
+ SubCategoryVersion.subcategoryvid
642
+ == SubCategoryItem.subcategoryvid,
643
+ )
644
+ .outerjoin(
645
+ ic_enum,
646
+ and_(
647
+ SubCategoryItem.itemid == ic_enum.itemid,
648
+ filter_item_version(
649
+ release_id,
650
+ ic_enum.startreleaseid,
651
+ ic_enum.endreleaseid,
652
+ ),
653
+ ),
654
+ )
655
+ .outerjoin(item_enum, ic_enum.itemid == item_enum.itemid)
656
+ .filter(TableVersion.tablevid == table_vid)
657
+ .order_by(TableVersionHeader.order)
658
+ )
659
+
660
+ header_results = header_query.all()
661
+
662
+ if not header_results:
663
+ return [], []
664
+
665
+ # Cells query: ORM-based, returning cell-level metadata for the
666
+ # selected table version.
667
+ hv_col = aliased(HeaderVersion)
668
+ hv_row = aliased(HeaderVersion)
669
+ hv_sheet = aliased(HeaderVersion)
670
+
671
+ # Aliases for ItemCategory and Property/DataType for each axis
672
+ ic_col = aliased(ItemCategory)
673
+ ic_row = aliased(ItemCategory)
674
+ ic_sheet = aliased(ItemCategory)
675
+ prop_col = aliased(Property)
676
+ prop_row = aliased(Property)
677
+ prop_sheet = aliased(Property)
678
+ dt_col = aliased(DataType)
679
+ dt_row = aliased(DataType)
680
+ dt_sheet = aliased(DataType)
681
+
682
+ cell_query = (
683
+ session.query(
684
+ hv_col.code.label("column_code"),
685
+ hv_row.code.label("row_code"),
686
+ hv_sheet.code.label("sheet_code"),
687
+ VariableVersion.variableid.label("variable_id"),
688
+ VariableVersion.variablevid.label("variable_vid"),
689
+ TableVersionCell.isnullable.label("cell_is_nullable"),
690
+ TableVersionCell.isexcluded.label("cell_is_excluded"),
691
+ TableVersionCell.isvoid.label("cell_is_void"),
692
+ TableVersionCell.sign.label("cell_sign"),
693
+ func.coalesce(
694
+ ic_col.code, ic_row.code, ic_sheet.code
695
+ ).label("property_code"),
696
+ func.coalesce(
697
+ dt_col.name, dt_row.name, dt_sheet.name
698
+ ).label("data_type_name"),
699
+ )
700
+ .select_from(TableVersionCell)
701
+ .join(
702
+ TableVersion,
703
+ TableVersion.tablevid == TableVersionCell.tablevid,
704
+ )
705
+ .join(Cell, TableVersionCell.cellid == Cell.cellid)
706
+ .outerjoin(hv_col, Cell.columnid == hv_col.headerid)
707
+ .outerjoin(hv_row, Cell.rowid == hv_row.headerid)
708
+ .outerjoin(hv_sheet, Cell.sheetid == hv_sheet.headerid)
709
+ .join(
710
+ VariableVersion,
711
+ VariableVersion.variablevid == TableVersionCell.variablevid,
712
+ )
713
+ # Column axis ItemCategory, Property, DataType
714
+ .outerjoin(
715
+ ic_col,
716
+ and_(
717
+ hv_col.propertyid == ic_col.itemid,
718
+ filter_item_version(
719
+ release_id,
720
+ ic_col.startreleaseid,
721
+ ic_col.endreleaseid,
722
+ ),
723
+ ic_col.isdefaultitem != 0,
724
+ ),
725
+ )
726
+ .outerjoin(prop_col, hv_col.propertyid == prop_col.propertyid)
727
+ .outerjoin(dt_col, prop_col.datatypeid == dt_col.datatypeid)
728
+ # Row axis ItemCategory, Property, DataType
729
+ .outerjoin(
730
+ ic_row,
731
+ and_(
732
+ hv_row.propertyid == ic_row.itemid,
733
+ filter_item_version(
734
+ release_id,
735
+ ic_row.startreleaseid,
736
+ ic_row.endreleaseid,
737
+ ),
738
+ ic_row.isdefaultitem != 0,
739
+ ),
740
+ )
741
+ .outerjoin(prop_row, hv_row.propertyid == prop_row.propertyid)
742
+ .outerjoin(dt_row, prop_row.datatypeid == dt_row.datatypeid)
743
+ # Sheet axis ItemCategory, Property, DataType
744
+ .outerjoin(
745
+ ic_sheet,
746
+ and_(
747
+ hv_sheet.propertyid == ic_sheet.itemid,
748
+ filter_item_version(
749
+ release_id,
750
+ ic_sheet.startreleaseid,
751
+ ic_sheet.endreleaseid,
752
+ ),
753
+ ic_sheet.isdefaultitem != 0,
754
+ ),
755
+ )
756
+ .outerjoin(prop_sheet, hv_sheet.propertyid == prop_sheet.propertyid)
757
+ .outerjoin(dt_sheet, prop_sheet.datatypeid == dt_sheet.datatypeid)
758
+ .filter(TableVersionCell.tablevid == table_vid)
759
+ .distinct()
760
+ )
761
+
762
+ cell_results = cell_query.all()
763
+
764
+ return header_results, cell_results
765
+
766
+ @staticmethod
767
+ def _transform_to_dpm_format(header_rows, cell_rows) -> dict:
768
+ if not header_rows:
769
+ return {}
770
+
771
+ first_row = header_rows[0]
772
+
773
+ # Group header rows by header_id and aggregate enumeration items
774
+ headers_dict = {}
775
+ for row in header_rows:
776
+ header_id = row.header_id
777
+
778
+ if header_id not in headers_dict:
779
+ headers_dict[header_id] = {
780
+ "id": row.header_id,
781
+ "parentId": row.parent_header_id,
782
+ "code": row.header_code,
783
+ "label": row.label,
784
+ "direction": row.direction,
785
+ "order": row.order,
786
+ "isAbstract": row.is_abstract,
787
+ "isKey": row.is_key,
788
+ "propertyCode": getattr(row, "property_code", None),
789
+ "propertyName": row.property_name,
790
+ "dataTypeName": row.data_type_name,
791
+ "items": [],
792
+ }
793
+
794
+ # Aggregate enumeration items
795
+ if (
796
+ hasattr(row, "item_signature")
797
+ and row.item_signature
798
+ and hasattr(row, "item_label")
799
+ and row.item_label
800
+ ):
801
+ item_str = f"{row.item_signature} - {row.item_label}"
802
+ if item_str not in headers_dict[header_id]["items"]:
803
+ headers_dict[header_id]["items"].append(item_str)
804
+
805
+ # Convert to list and sort by order
806
+ headers = sorted(headers_dict.values(), key=lambda x: x["order"])
807
+
808
+ cells = []
809
+ for row in cell_rows:
810
+ cells.append(
811
+ {
812
+ "column_code": row.column_code,
813
+ "row_code": row.row_code,
814
+ "sheet_code": row.sheet_code,
815
+ "variable_id": row.variable_id,
816
+ "variable_vid": row.variable_vid,
817
+ "cell_is_nullable": row.cell_is_nullable,
818
+ "cell_is_excluded": row.cell_is_excluded,
819
+ "cell_is_void": row.cell_is_void,
820
+ "cell_sign": row.cell_sign,
821
+ "property_code": getattr(row, "property_code", None),
822
+ "data_type_name": row.data_type_name,
823
+ }
824
+ )
825
+
826
+ return {
827
+ "tableCode": first_row.table_code,
828
+ "tableTitle": first_row.table_name, # Assuming Name -> Title mapping
829
+ "tableVid": first_row.table_vid,
830
+ "headers": headers,
831
+ "data": {},
832
+ "metadata": {
833
+ "version": "1.0",
834
+ "source": "database",
835
+ "recordCount": len(headers),
836
+ },
837
+ "cells": cells,
838
+ }