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,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)