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.
- py_dpm/__init__.py +1 -1
- py_dpm/api/__init__.py +58 -189
- py_dpm/api/dpm/__init__.py +20 -0
- py_dpm/api/{data_dictionary.py → dpm/data_dictionary.py} +903 -984
- py_dpm/api/dpm/explorer.py +236 -0
- py_dpm/api/dpm/hierarchical_queries.py +142 -0
- py_dpm/api/{migration.py → dpm/migration.py} +16 -19
- py_dpm/api/{operation_scopes.py → dpm/operation_scopes.py} +319 -267
- py_dpm/api/dpm_xl/__init__.py +25 -0
- py_dpm/api/{ast_generator.py → dpm_xl/ast_generator.py} +3 -3
- py_dpm/api/{complete_ast.py → dpm_xl/complete_ast.py} +191 -167
- py_dpm/api/dpm_xl/semantic.py +354 -0
- py_dpm/api/{syntax.py → dpm_xl/syntax.py} +6 -5
- py_dpm/api/explorer.py +4 -0
- py_dpm/api/semantic.py +30 -306
- py_dpm/cli/__init__.py +9 -0
- py_dpm/{client.py → cli/main.py} +8 -8
- py_dpm/dpm/__init__.py +11 -0
- py_dpm/{models.py → dpm/models.py} +112 -88
- py_dpm/dpm/queries/base.py +100 -0
- py_dpm/dpm/queries/basic_objects.py +33 -0
- py_dpm/dpm/queries/explorer_queries.py +352 -0
- py_dpm/dpm/queries/filters.py +139 -0
- py_dpm/dpm/queries/glossary.py +45 -0
- py_dpm/dpm/queries/hierarchical_queries.py +838 -0
- py_dpm/dpm/queries/tables.py +133 -0
- py_dpm/dpm/utils.py +356 -0
- py_dpm/dpm_xl/__init__.py +8 -0
- py_dpm/dpm_xl/ast/__init__.py +14 -0
- py_dpm/{AST/ASTConstructor.py → dpm_xl/ast/constructor.py} +6 -6
- py_dpm/{AST/MLGeneration.py → dpm_xl/ast/ml_generation.py} +137 -87
- py_dpm/{AST/ModuleAnalyzer.py → dpm_xl/ast/module_analyzer.py} +7 -7
- py_dpm/{AST/ModuleDependencies.py → dpm_xl/ast/module_dependencies.py} +56 -41
- py_dpm/{AST/ASTObjects.py → dpm_xl/ast/nodes.py} +1 -1
- py_dpm/{AST/check_operands.py → dpm_xl/ast/operands.py} +16 -13
- py_dpm/{AST/ASTTemplate.py → dpm_xl/ast/template.py} +2 -2
- py_dpm/{AST/WhereClauseChecker.py → dpm_xl/ast/where_clause.py} +2 -2
- py_dpm/dpm_xl/grammar/__init__.py +18 -0
- py_dpm/dpm_xl/operators/__init__.py +19 -0
- py_dpm/{Operators/AggregateOperators.py → dpm_xl/operators/aggregate.py} +7 -7
- py_dpm/{Operators/NumericOperators.py → dpm_xl/operators/arithmetic.py} +6 -6
- py_dpm/{Operators/Operator.py → dpm_xl/operators/base.py} +5 -5
- py_dpm/{Operators/BooleanOperators.py → dpm_xl/operators/boolean.py} +5 -5
- py_dpm/{Operators/ClauseOperators.py → dpm_xl/operators/clause.py} +8 -8
- py_dpm/{Operators/ComparisonOperators.py → dpm_xl/operators/comparison.py} +5 -5
- py_dpm/{Operators/ConditionalOperators.py → dpm_xl/operators/conditional.py} +7 -7
- py_dpm/{Operators/StringOperators.py → dpm_xl/operators/string.py} +5 -5
- py_dpm/{Operators/TimeOperators.py → dpm_xl/operators/time.py} +6 -6
- py_dpm/{semantics/SemanticAnalyzer.py → dpm_xl/semantic_analyzer.py} +168 -68
- py_dpm/{semantics/Symbols.py → dpm_xl/symbols.py} +3 -3
- py_dpm/dpm_xl/types/__init__.py +13 -0
- py_dpm/{DataTypes/TypePromotion.py → dpm_xl/types/promotion.py} +2 -2
- py_dpm/{DataTypes/ScalarTypes.py → dpm_xl/types/scalar.py} +2 -2
- py_dpm/dpm_xl/utils/__init__.py +14 -0
- py_dpm/{data_handlers.py → dpm_xl/utils/data_handlers.py} +2 -2
- py_dpm/{Utils → dpm_xl/utils}/operands_mapping.py +1 -1
- py_dpm/{Utils → dpm_xl/utils}/operator_mapping.py +8 -8
- py_dpm/{OperationScopes/OperationScopeService.py → dpm_xl/utils/scopes_calculator.py} +148 -58
- py_dpm/{Utils/ast_serialization.py → dpm_xl/utils/serialization.py} +2 -2
- py_dpm/dpm_xl/validation/__init__.py +12 -0
- py_dpm/{Utils/ValidationsGenerationUtils.py → dpm_xl/validation/generation_utils.py} +2 -3
- py_dpm/{ValidationsGeneration/PropertiesConstraintsProcessor.py → dpm_xl/validation/property_constraints.py} +56 -21
- py_dpm/{ValidationsGeneration/auxiliary_functions.py → dpm_xl/validation/utils.py} +2 -2
- py_dpm/{ValidationsGeneration/VariantsProcessor.py → dpm_xl/validation/variants.py} +149 -55
- py_dpm/exceptions/__init__.py +23 -0
- py_dpm/{Exceptions → exceptions}/exceptions.py +7 -2
- pydpm_xl-0.2.0.dist-info/METADATA +278 -0
- pydpm_xl-0.2.0.dist-info/RECORD +88 -0
- pydpm_xl-0.2.0.dist-info/entry_points.txt +2 -0
- py_dpm/Exceptions/__init__.py +0 -0
- py_dpm/OperationScopes/__init__.py +0 -0
- py_dpm/Operators/__init__.py +0 -0
- py_dpm/Utils/__init__.py +0 -0
- py_dpm/Utils/utils.py +0 -2
- py_dpm/ValidationsGeneration/Utils.py +0 -364
- py_dpm/ValidationsGeneration/__init__.py +0 -0
- py_dpm/api/data_dictionary_validation.py +0 -614
- py_dpm/db_utils.py +0 -221
- py_dpm/grammar/__init__.py +0 -0
- py_dpm/grammar/dist/__init__.py +0 -0
- py_dpm/grammar/dpm_xlLexer.g4 +0 -437
- py_dpm/grammar/dpm_xlParser.g4 +0 -263
- py_dpm/semantics/DAG/DAGAnalyzer.py +0 -158
- py_dpm/semantics/DAG/__init__.py +0 -0
- py_dpm/semantics/__init__.py +0 -0
- py_dpm/views/data_types.sql +0 -12
- py_dpm/views/datapoints.sql +0 -65
- py_dpm/views/hierarchy_operand_reference.sql +0 -11
- py_dpm/views/hierarchy_preconditions.sql +0 -13
- py_dpm/views/hierarchy_variables.sql +0 -26
- py_dpm/views/hierarchy_variables_context.sql +0 -14
- py_dpm/views/key_components.sql +0 -18
- py_dpm/views/module_from_table.sql +0 -11
- py_dpm/views/open_keys.sql +0 -13
- py_dpm/views/operation_info.sql +0 -27
- py_dpm/views/operation_list.sql +0 -18
- py_dpm/views/operations_versions_from_module_version.sql +0 -30
- py_dpm/views/precondition_info.sql +0 -17
- py_dpm/views/report_type_operand_reference_info.sql +0 -18
- py_dpm/views/subcategory_info.sql +0 -17
- py_dpm/views/table_info.sql +0 -19
- pydpm_xl-0.1.39rc32.dist-info/METADATA +0 -53
- pydpm_xl-0.1.39rc32.dist-info/RECORD +0 -96
- pydpm_xl-0.1.39rc32.dist-info/entry_points.txt +0 -2
- /py_dpm/{AST → cli/commands}/__init__.py +0 -0
- /py_dpm/{migration.py → dpm/migration.py} +0 -0
- /py_dpm/{AST/ASTVisitor.py → dpm_xl/ast/visitor.py} +0 -0
- /py_dpm/{DataTypes → dpm_xl/grammar/generated}/__init__.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.interp +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.tokens +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.interp +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.tokens +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParserListener.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParserVisitor.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/listeners.py +0 -0
- /py_dpm/{DataTypes/TimeClasses.py → dpm_xl/types/time.py} +0 -0
- /py_dpm/{Utils → dpm_xl/utils}/tokens.py +0 -0
- /py_dpm/{Exceptions → exceptions}/messages.py +0 -0
- {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.0.dist-info}/WHEEL +0 -0
- {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Any
|
|
2
|
+
|
|
3
|
+
from sqlalchemy.orm import Session, aliased
|
|
4
|
+
|
|
5
|
+
from py_dpm.dpm.models import (
|
|
6
|
+
VariableVersion,
|
|
7
|
+
TableVersionCell,
|
|
8
|
+
TableVersion,
|
|
9
|
+
ModuleVersionComposition,
|
|
10
|
+
ModuleVersion,
|
|
11
|
+
Module,
|
|
12
|
+
Framework,
|
|
13
|
+
Release,
|
|
14
|
+
Cell,
|
|
15
|
+
HeaderVersion,
|
|
16
|
+
)
|
|
17
|
+
from py_dpm.dpm.queries.filters import (
|
|
18
|
+
filter_by_release,
|
|
19
|
+
filter_by_date,
|
|
20
|
+
filter_active_only,
|
|
21
|
+
filter_item_version,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExplorerQuery:
|
|
26
|
+
"""
|
|
27
|
+
Queries used by the Explorer API for inverse lookups, such as
|
|
28
|
+
"where is this variable used?".
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def get_variable_usage(
|
|
33
|
+
session: Session,
|
|
34
|
+
variable_id: Optional[int] = None,
|
|
35
|
+
variable_vid: Optional[int] = None,
|
|
36
|
+
release_id: Optional[int] = None,
|
|
37
|
+
date: Optional[str] = None,
|
|
38
|
+
release_code: Optional[str] = None,
|
|
39
|
+
) -> List[Dict[str, Any]]:
|
|
40
|
+
"""
|
|
41
|
+
Return all table cells and module versions in which a given variable
|
|
42
|
+
(by id or vid) is used.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
session: SQLAlchemy session
|
|
46
|
+
variable_id: VariableID to filter on (mutually exclusive with variable_vid)
|
|
47
|
+
variable_vid: VariableVID to filter on (mutually exclusive with variable_id)
|
|
48
|
+
release_id: Optional release id, mutually exclusive with date/release_code
|
|
49
|
+
date: Optional reference date (YYYY-MM-DD), mutually exclusive with release args
|
|
50
|
+
release_code: Optional release code, mutually exclusive with release_id/date
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of dictionaries with cell and module/table metadata.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Exactly one of variable_id / variable_vid must be provided
|
|
57
|
+
if (variable_id is None) == (variable_vid is None):
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"Specify exactly one of variable_id or variable_vid."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Release/date arguments follow the same rules as hierarchical queries
|
|
63
|
+
if sum(bool(x) for x in [release_id, date, release_code]) > 1:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"Specify a maximum of one of release_id, release_code or date."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Build SQLAlchemy ORM query mirroring:
|
|
69
|
+
# FROM VariableVersion vv
|
|
70
|
+
# JOIN TableVersionCell tvc ON tvc.VariableVID = vv.VariableVID
|
|
71
|
+
# JOIN TableVersion tv ON tv.TableVID = tvc.TableVID
|
|
72
|
+
# JOIN ModuleVersionComposition mvc ON mvc.TableVID = tv.TableVID
|
|
73
|
+
# JOIN ModuleVersion mv ON mv.ModuleVID = mvc.ModuleVID
|
|
74
|
+
q = (
|
|
75
|
+
session.query(
|
|
76
|
+
TableVersionCell.cellcode.label("cell_code"),
|
|
77
|
+
TableVersionCell.sign.label("cell_sign"),
|
|
78
|
+
TableVersion.code.label("table_code"),
|
|
79
|
+
TableVersion.name.label("table_name"),
|
|
80
|
+
ModuleVersion.code.label("module_code"),
|
|
81
|
+
ModuleVersion.name.label("module_name"),
|
|
82
|
+
ModuleVersion.versionnumber.label("module_version_number"),
|
|
83
|
+
ModuleVersion.startreleaseid.label("module_startreleaseid"),
|
|
84
|
+
ModuleVersion.endreleaseid.label("module_endreleaseid"),
|
|
85
|
+
ModuleVersion.fromreferencedate.label("module_fromreferencedate"),
|
|
86
|
+
ModuleVersion.toreferencedate.label("module_toreferencedate"),
|
|
87
|
+
)
|
|
88
|
+
.select_from(VariableVersion)
|
|
89
|
+
.join(
|
|
90
|
+
TableVersionCell,
|
|
91
|
+
TableVersionCell.variablevid == VariableVersion.variablevid,
|
|
92
|
+
)
|
|
93
|
+
.join(TableVersion, TableVersion.tablevid == TableVersionCell.tablevid)
|
|
94
|
+
.join(
|
|
95
|
+
ModuleVersionComposition,
|
|
96
|
+
ModuleVersionComposition.tablevid == TableVersion.tablevid,
|
|
97
|
+
)
|
|
98
|
+
.join(
|
|
99
|
+
ModuleVersion,
|
|
100
|
+
ModuleVersion.modulevid == ModuleVersionComposition.modulevid,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Filter by the chosen variable identifier
|
|
105
|
+
if variable_vid is not None:
|
|
106
|
+
q = q.filter(VariableVersion.variablevid == variable_vid)
|
|
107
|
+
else:
|
|
108
|
+
q = q.filter(VariableVersion.variableid == variable_id)
|
|
109
|
+
|
|
110
|
+
# Apply release/date filtering on ModuleVersion.
|
|
111
|
+
# If no release arguments are provided, return all results without
|
|
112
|
+
# restricting to "active only".
|
|
113
|
+
if date:
|
|
114
|
+
q = filter_by_date(
|
|
115
|
+
q,
|
|
116
|
+
date,
|
|
117
|
+
ModuleVersion.fromreferencedate,
|
|
118
|
+
ModuleVersion.toreferencedate,
|
|
119
|
+
)
|
|
120
|
+
elif release_id or release_code:
|
|
121
|
+
q = filter_by_release(
|
|
122
|
+
q,
|
|
123
|
+
start_col=ModuleVersion.startreleaseid,
|
|
124
|
+
end_col=ModuleVersion.endreleaseid,
|
|
125
|
+
release_id=release_id,
|
|
126
|
+
release_code=release_code,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
results = q.all()
|
|
130
|
+
return [dict(row._mapping) for row in results]
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def get_module_url(
|
|
134
|
+
session: Session,
|
|
135
|
+
module_code: str,
|
|
136
|
+
date: Optional[str] = None,
|
|
137
|
+
release_id: Optional[int] = None,
|
|
138
|
+
release_code: Optional[str] = None,
|
|
139
|
+
) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Resolve the EBA taxonomy URL for a given module code.
|
|
142
|
+
|
|
143
|
+
The URL format is:
|
|
144
|
+
http://www.eba.europa.eu/eu/fr/xbrl/crr/fws/{framework_code}/{release_code}/mod/{module_code}.json
|
|
145
|
+
|
|
146
|
+
Exactly one of date, release_id or release_code may be provided.
|
|
147
|
+
If none are provided, the currently active module version is used
|
|
148
|
+
(based on ModuleVersion.endreleaseid being NULL).
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
if sum(bool(x) for x in [release_id, date, release_code]) > 1:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"Specify a maximum of one of release_id, release_code or date."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Base query to resolve framework and module version metadata
|
|
157
|
+
q = (
|
|
158
|
+
session.query(
|
|
159
|
+
Framework.code.label("framework_code"),
|
|
160
|
+
ModuleVersion.code.label("module_code"),
|
|
161
|
+
ModuleVersion.startreleaseid.label("module_startreleaseid"),
|
|
162
|
+
ModuleVersion.endreleaseid.label("module_endreleaseid"),
|
|
163
|
+
ModuleVersion.fromreferencedate.label("module_fromreferencedate"),
|
|
164
|
+
ModuleVersion.toreferencedate.label("module_toreferencedate"),
|
|
165
|
+
)
|
|
166
|
+
.select_from(ModuleVersion)
|
|
167
|
+
.join(Module, ModuleVersion.moduleid == Module.moduleid)
|
|
168
|
+
.join(Framework, Module.frameworkid == Framework.frameworkid)
|
|
169
|
+
.filter(ModuleVersion.code == module_code)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Apply release/date filtering mirroring HierarchicalQuery.get_module_version
|
|
173
|
+
if date:
|
|
174
|
+
q = filter_by_date(
|
|
175
|
+
q,
|
|
176
|
+
date,
|
|
177
|
+
ModuleVersion.fromreferencedate,
|
|
178
|
+
ModuleVersion.toreferencedate,
|
|
179
|
+
)
|
|
180
|
+
elif release_id or release_code:
|
|
181
|
+
q = filter_by_release(
|
|
182
|
+
q,
|
|
183
|
+
start_col=ModuleVersion.startreleaseid,
|
|
184
|
+
end_col=ModuleVersion.endreleaseid,
|
|
185
|
+
release_id=release_id,
|
|
186
|
+
release_code=release_code,
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
# Default to currently active module versions
|
|
190
|
+
q = filter_active_only(q, end_col=ModuleVersion.endreleaseid)
|
|
191
|
+
|
|
192
|
+
rows = q.all()
|
|
193
|
+
|
|
194
|
+
if len(rows) != 1:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"Should return 1 record, but returned {len(rows)}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
row = rows[0]
|
|
200
|
+
framework_code = row.framework_code
|
|
201
|
+
resolved_module_code = row.module_code
|
|
202
|
+
|
|
203
|
+
# Determine which release_code to embed in the URL
|
|
204
|
+
if release_code is not None:
|
|
205
|
+
effective_release_code = release_code
|
|
206
|
+
elif release_id is not None:
|
|
207
|
+
release_row = (
|
|
208
|
+
session.query(Release.code)
|
|
209
|
+
.filter(Release.releaseid == release_id)
|
|
210
|
+
.first()
|
|
211
|
+
)
|
|
212
|
+
if not release_row:
|
|
213
|
+
raise ValueError(f"Release with id {release_id} was not found.")
|
|
214
|
+
effective_release_code = release_row.code
|
|
215
|
+
else:
|
|
216
|
+
# For date-based or default queries, use the module version's
|
|
217
|
+
# starting release to derive the release code.
|
|
218
|
+
start_release_id = row.module_startreleaseid
|
|
219
|
+
release_row = (
|
|
220
|
+
session.query(Release.code)
|
|
221
|
+
.filter(Release.releaseid == start_release_id)
|
|
222
|
+
.first()
|
|
223
|
+
)
|
|
224
|
+
if not release_row:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Release with id {start_release_id} was not found."
|
|
227
|
+
)
|
|
228
|
+
effective_release_code = release_row.code
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
"http://www.eba.europa.eu/eu/fr/xbrl/crr/fws/"
|
|
232
|
+
f"{framework_code.lower()}/{effective_release_code}/mod/{resolved_module_code.lower()}.json"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def get_variable_from_cell_address(
|
|
237
|
+
session: Session,
|
|
238
|
+
table_code: str,
|
|
239
|
+
row_code: Optional[str] = None,
|
|
240
|
+
column_code: Optional[str] = None,
|
|
241
|
+
sheet_code: Optional[str] = None,
|
|
242
|
+
release_id: Optional[int] = None,
|
|
243
|
+
release_code: Optional[str] = None,
|
|
244
|
+
date: Optional[str] = None,
|
|
245
|
+
) -> List[Dict[str, Any]]:
|
|
246
|
+
"""
|
|
247
|
+
Resolve variables from a cell address (table / row / column / sheet).
|
|
248
|
+
|
|
249
|
+
The query mirrors the provided SQL, but uses SQLAlchemy ORM and the
|
|
250
|
+
standard release/date filtering helpers. Row, column and sheet codes
|
|
251
|
+
are optional and are only applied when not None.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
if sum(bool(x) for x in [release_id, release_code, date]) > 1:
|
|
255
|
+
raise ValueError(
|
|
256
|
+
"Specify a maximum of one of release_id, release_code or date."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Base query: link variables to cells and table versions
|
|
260
|
+
q = (
|
|
261
|
+
session.query(
|
|
262
|
+
VariableVersion.variableid.label("variable_id"),
|
|
263
|
+
VariableVersion.variablevid.label("variable_vid")
|
|
264
|
+
)
|
|
265
|
+
.select_from(VariableVersion)
|
|
266
|
+
.join(
|
|
267
|
+
TableVersionCell,
|
|
268
|
+
TableVersionCell.variablevid == VariableVersion.variablevid,
|
|
269
|
+
)
|
|
270
|
+
.join(TableVersion, TableVersion.tablevid == TableVersionCell.tablevid)
|
|
271
|
+
.join(Cell, Cell.cellid == TableVersionCell.cellid)
|
|
272
|
+
.join(
|
|
273
|
+
ModuleVersionComposition,
|
|
274
|
+
ModuleVersionComposition.tablevid == TableVersion.tablevid,
|
|
275
|
+
)
|
|
276
|
+
.join(
|
|
277
|
+
ModuleVersion,
|
|
278
|
+
ModuleVersion.modulevid == ModuleVersionComposition.modulevid,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Aliases for the three header axes
|
|
283
|
+
hv_row = aliased(HeaderVersion, name="hv_row")
|
|
284
|
+
hv_col = aliased(HeaderVersion, name="hv_col")
|
|
285
|
+
hv_sheet = aliased(HeaderVersion, name="hv_sheet")
|
|
286
|
+
|
|
287
|
+
q = q.add_columns(
|
|
288
|
+
hv_row.code.label("row_code"),
|
|
289
|
+
hv_col.code.label("column_code"),
|
|
290
|
+
hv_sheet.code.label("sheet_code"),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
q = q.outerjoin(
|
|
294
|
+
hv_row,
|
|
295
|
+
(Cell.rowid == hv_row.headerid)
|
|
296
|
+
& filter_item_version(
|
|
297
|
+
TableVersion.startreleaseid,
|
|
298
|
+
hv_row.startreleaseid,
|
|
299
|
+
hv_row.endreleaseid,
|
|
300
|
+
),
|
|
301
|
+
).outerjoin(
|
|
302
|
+
hv_col,
|
|
303
|
+
(Cell.columnid == hv_col.headerid)
|
|
304
|
+
& filter_item_version(
|
|
305
|
+
TableVersion.startreleaseid,
|
|
306
|
+
hv_col.startreleaseid,
|
|
307
|
+
hv_col.endreleaseid,
|
|
308
|
+
),
|
|
309
|
+
).outerjoin(
|
|
310
|
+
hv_sheet,
|
|
311
|
+
(Cell.sheetid == hv_sheet.headerid)
|
|
312
|
+
& filter_item_version(
|
|
313
|
+
TableVersion.startreleaseid,
|
|
314
|
+
hv_sheet.startreleaseid,
|
|
315
|
+
hv_sheet.endreleaseid,
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Mandatory table filter
|
|
320
|
+
q = q.filter(TableVersion.code == table_code)
|
|
321
|
+
|
|
322
|
+
# Optional axis filters
|
|
323
|
+
if row_code is not None:
|
|
324
|
+
q = q.filter(hv_row.code == row_code)
|
|
325
|
+
if column_code is not None:
|
|
326
|
+
q = q.filter(hv_col.code == column_code)
|
|
327
|
+
if sheet_code is not None:
|
|
328
|
+
q = q.filter(hv_sheet.code == sheet_code)
|
|
329
|
+
|
|
330
|
+
# Apply standard release/date filtering on ModuleVersion.
|
|
331
|
+
# For this method, if no release argument is provided, we default
|
|
332
|
+
# to filtering active-only module versions.
|
|
333
|
+
if date:
|
|
334
|
+
q = filter_by_date(
|
|
335
|
+
q,
|
|
336
|
+
date,
|
|
337
|
+
ModuleVersion.fromreferencedate,
|
|
338
|
+
ModuleVersion.toreferencedate,
|
|
339
|
+
)
|
|
340
|
+
elif release_id or release_code:
|
|
341
|
+
q = filter_by_release(
|
|
342
|
+
q,
|
|
343
|
+
start_col=ModuleVersion.startreleaseid,
|
|
344
|
+
end_col=ModuleVersion.endreleaseid,
|
|
345
|
+
release_id=release_id,
|
|
346
|
+
release_code=release_code,
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
q = filter_active_only(q, end_col=ModuleVersion.endreleaseid)
|
|
350
|
+
|
|
351
|
+
results = q.all()
|
|
352
|
+
return [dict(row._mapping) for row in results]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from sqlalchemy import or_, and_
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def filter_by_date(query, date, start_col, end_col):
|
|
7
|
+
"""
|
|
8
|
+
Filter a query by a date range.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
query: SQLAlchemy Query object
|
|
12
|
+
date: Date string (YYYY-MM-DD) or date object
|
|
13
|
+
start_col: Column representing start date
|
|
14
|
+
end_col: Column representing end date
|
|
15
|
+
"""
|
|
16
|
+
if not date:
|
|
17
|
+
return query
|
|
18
|
+
|
|
19
|
+
if isinstance(date, str):
|
|
20
|
+
target_date = datetime.strptime(date, "%Y-%m-%d").date()
|
|
21
|
+
else:
|
|
22
|
+
target_date = date
|
|
23
|
+
|
|
24
|
+
from sqlalchemy import cast, Date
|
|
25
|
+
|
|
26
|
+
# Check dialect to apply CAST only for Postgres where type mismatch occurs
|
|
27
|
+
is_postgres = False
|
|
28
|
+
if hasattr(query, "session") and query.session:
|
|
29
|
+
bind = query.session.get_bind()
|
|
30
|
+
if bind.dialect.name == "postgresql":
|
|
31
|
+
is_postgres = True
|
|
32
|
+
|
|
33
|
+
if is_postgres:
|
|
34
|
+
start_expr = cast(start_col, Date)
|
|
35
|
+
end_expr = cast(end_col, Date)
|
|
36
|
+
else:
|
|
37
|
+
start_expr = start_col
|
|
38
|
+
end_expr = end_col
|
|
39
|
+
|
|
40
|
+
return query.filter(
|
|
41
|
+
and_(
|
|
42
|
+
start_expr <= target_date,
|
|
43
|
+
or_(end_col.is_(None), end_expr > target_date),
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def filter_by_release(
|
|
49
|
+
query,
|
|
50
|
+
start_col,
|
|
51
|
+
end_col,
|
|
52
|
+
release_id: Optional[int] = None,
|
|
53
|
+
release_code: Optional[str] = None,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Filter a query by DPM release versioning logic.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
query: SQLAlchemy Query object
|
|
60
|
+
release_id: The release ID to filter for. If None, no filtering is applied (or returns all? Usually active).
|
|
61
|
+
Wait, if release_id is None, usually implies 'latest' or 'active' or 'all'?
|
|
62
|
+
Looking at existing code:
|
|
63
|
+
If release_id IS None:
|
|
64
|
+
query.filter(or_(end_release.is_(None), end_release > release_id)) <-- This fails if release_id is None
|
|
65
|
+
|
|
66
|
+
Let's check `data_dictionary.helper`:
|
|
67
|
+
If `release_id` passed as None to `get_available_tables`:
|
|
68
|
+
It just executes `query.all()` without filtering (lines 93-100 only run `if release_id is not None`).
|
|
69
|
+
|
|
70
|
+
HOWEVER, in `ItemCategory` access:
|
|
71
|
+
`else: query.filter(ItemCategory.endreleaseid.is_(None))`
|
|
72
|
+
|
|
73
|
+
So there is inconsistency.
|
|
74
|
+
Reference: `data_dictionary.py` L93.
|
|
75
|
+
|
|
76
|
+
Standard Logic adopted here:
|
|
77
|
+
If release_id provided:
|
|
78
|
+
start <= release_id AND (end is NULL OR end > release_id)
|
|
79
|
+
If release_id IS None:
|
|
80
|
+
Return query unmodified (fetch all history? or active? Caller decides by not calling this or passing optional arg)
|
|
81
|
+
"""
|
|
82
|
+
if release_id is not None and release_code is not None:
|
|
83
|
+
raise ValueError("Specify a maximum of one of release_id or release_code.")
|
|
84
|
+
|
|
85
|
+
if release_id is None and release_code is None:
|
|
86
|
+
return query
|
|
87
|
+
elif release_id:
|
|
88
|
+
return query.filter(
|
|
89
|
+
and_(start_col <= release_id, or_(end_col.is_(None), end_col > release_id))
|
|
90
|
+
)
|
|
91
|
+
elif release_code:
|
|
92
|
+
# Resolve release_code to release_id using the session from the query
|
|
93
|
+
if hasattr(query, "session") and query.session:
|
|
94
|
+
from py_dpm.dpm.queries.basic_objects import ReleaseQuery
|
|
95
|
+
|
|
96
|
+
release_q = ReleaseQuery.get_release_by_code(query.session, release_code)
|
|
97
|
+
results = release_q.to_dict()
|
|
98
|
+
if results:
|
|
99
|
+
release_id = results[0]["releaseid"]
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(f"Release code '{release_code}' not found.")
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError("Query has no session, cannot resolve release_code.")
|
|
104
|
+
print(release_id)
|
|
105
|
+
|
|
106
|
+
return query.filter(
|
|
107
|
+
and_(start_col <= release_id, or_(end_col.is_(None), end_col > release_id))
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def filter_active_only(query, end_col):
|
|
112
|
+
"""Filter for currently active records (end_release is None)."""
|
|
113
|
+
return query.filter(end_col.is_(None))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def filter_item_version(ref_start_col, item_start_col, item_end_col):
|
|
117
|
+
"""
|
|
118
|
+
Build a version-range condition for joining versioned items (such as
|
|
119
|
+
ItemCategory) against a reference start-release column.
|
|
120
|
+
|
|
121
|
+
The pattern is:
|
|
122
|
+
ref_start_col >= item_start_col
|
|
123
|
+
AND (item_end_col IS NULL OR ref_start_col < item_end_col)
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
ref_start_col: Column representing the reference start release
|
|
127
|
+
(e.g. TableVersion.startreleaseid).
|
|
128
|
+
item_start_col: Item's start-release column
|
|
129
|
+
(e.g. ItemCategory.startreleaseid).
|
|
130
|
+
item_end_col: Item's end-release column
|
|
131
|
+
(e.g. ItemCategory.endreleaseid).
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
SQLAlchemy boolean expression combining the above conditions.
|
|
135
|
+
"""
|
|
136
|
+
return and_(
|
|
137
|
+
ref_start_col >= item_start_col,
|
|
138
|
+
or_(ref_start_col < item_end_col, item_end_col.is_(None)),
|
|
139
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
from sqlalchemy import distinct, or_
|
|
3
|
+
from py_dpm.dpm.models import ItemCategory
|
|
4
|
+
from py_dpm.dpm.queries.base import BaseQuery
|
|
5
|
+
from py_dpm.dpm.queries.filters import filter_by_release, filter_active_only
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ItemQuery:
|
|
9
|
+
"""
|
|
10
|
+
Queries related to Items and Categories.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_all_item_signatures(session, release_id: Optional[int] = None) -> BaseQuery:
|
|
15
|
+
"""Get all item signatures."""
|
|
16
|
+
q = session.query(distinct(ItemCategory.signature).label("signature")).filter(
|
|
17
|
+
ItemCategory.signature.isnot(None)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if release_id is not None:
|
|
21
|
+
q = filter_by_release(
|
|
22
|
+
q, release_id, ItemCategory.startreleaseid, ItemCategory.endreleaseid
|
|
23
|
+
)
|
|
24
|
+
else:
|
|
25
|
+
q = filter_active_only(q, ItemCategory.endreleaseid)
|
|
26
|
+
|
|
27
|
+
q = q.order_by(ItemCategory.signature)
|
|
28
|
+
return BaseQuery(session, q)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def get_item_categories(session, release_id: Optional[int] = None) -> BaseQuery:
|
|
32
|
+
"""Get item categories (code, signature)."""
|
|
33
|
+
q = session.query(ItemCategory.code, ItemCategory.signature).filter(
|
|
34
|
+
ItemCategory.code.isnot(None), ItemCategory.signature.isnot(None)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if release_id is not None:
|
|
38
|
+
q = filter_by_release(
|
|
39
|
+
q, release_id, ItemCategory.startreleaseid, ItemCategory.endreleaseid
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
q = q.order_by(ItemCategory.code, ItemCategory.signature)
|
|
45
|
+
return BaseQuery(session, q)
|