pydpm_xl 0.2.2__py3-none-any.whl → 0.2.4__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 +23 -51
- py_dpm/api/dpm/__init__.py +2 -2
- py_dpm/api/dpm/instance.py +111 -0
- py_dpm/api/dpm_xl/__init__.py +10 -2
- py_dpm/api/dpm_xl/ast_generator.py +690 -10
- py_dpm/api/dpm_xl/complete_ast.py +54 -565
- py_dpm/api/{dpm → dpm_xl}/operation_scopes.py +2 -2
- py_dpm/cli/main.py +1 -1
- py_dpm/dpm/models.py +5 -1
- py_dpm/instance/__init__.py +0 -0
- py_dpm/instance/instance.py +304 -0
- {pydpm_xl-0.2.2.dist-info → pydpm_xl-0.2.4.dist-info}/METADATA +1 -1
- {pydpm_xl-0.2.2.dist-info → pydpm_xl-0.2.4.dist-info}/RECORD +18 -22
- py_dpm/api/explorer.py +0 -4
- py_dpm/api/semantic.py +0 -56
- py_dpm/dpm_xl/validation/__init__.py +0 -12
- py_dpm/dpm_xl/validation/generation_utils.py +0 -428
- py_dpm/dpm_xl/validation/property_constraints.py +0 -225
- py_dpm/dpm_xl/validation/utils.py +0 -98
- py_dpm/dpm_xl/validation/variants.py +0 -359
- {pydpm_xl-0.2.2.dist-info → pydpm_xl-0.2.4.dist-info}/WHEEL +0 -0
- {pydpm_xl-0.2.2.dist-info → pydpm_xl-0.2.4.dist-info}/entry_points.txt +0 -0
- {pydpm_xl-0.2.2.dist-info → pydpm_xl-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {pydpm_xl-0.2.2.dist-info → pydpm_xl-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -7,15 +7,37 @@ without exposing internal complexity or version compatibility issues.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from typing import Dict, Any, Optional, List, Union
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
import json
|
|
12
|
+
from datetime import datetime
|
|
11
13
|
from py_dpm.api.dpm_xl.syntax import SyntaxAPI
|
|
12
14
|
from py_dpm.api.dpm_xl.semantic import SemanticAPI
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
class ASTGeneratorAPI:
|
|
16
19
|
"""
|
|
17
20
|
Simplified AST Generator for external packages.
|
|
18
21
|
|
|
22
|
+
Provides three levels of AST generation:
|
|
23
|
+
|
|
24
|
+
1. **Basic AST** (parse_expression):
|
|
25
|
+
- Syntax parsing only, no database required
|
|
26
|
+
- Returns: Clean AST dictionary with version compatibility normalization
|
|
27
|
+
- Use for: Syntax validation, basic AST analysis
|
|
28
|
+
|
|
29
|
+
2. **Complete AST** (generate_complete_ast):
|
|
30
|
+
- Requires database connection
|
|
31
|
+
- Performs full semantic validation and operand checking
|
|
32
|
+
- Returns: AST with data fields populated (datapoint IDs, operand references)
|
|
33
|
+
- Use for: AST analysis with complete metadata, matching json_scripts/*.json format
|
|
34
|
+
|
|
35
|
+
3. **Enriched AST** (generate_enriched_ast):
|
|
36
|
+
- Requires database connection
|
|
37
|
+
- Extends complete AST with framework structure for execution engines
|
|
38
|
+
- Returns: Engine-ready AST with operations, variables, tables, preconditions sections
|
|
39
|
+
- Use for: Business rule execution engines, validation frameworks
|
|
40
|
+
|
|
19
41
|
Handles all internal complexity including:
|
|
20
42
|
- Version compatibility
|
|
21
43
|
- Context processing
|
|
@@ -49,18 +71,30 @@ class ASTGenerator:
|
|
|
49
71
|
|
|
50
72
|
def parse_expression(self, expression: str) -> Dict[str, Any]:
|
|
51
73
|
"""
|
|
52
|
-
Parse DPM-XL expression into clean AST format.
|
|
74
|
+
Parse DPM-XL expression into clean AST format (Level 1 - Basic AST).
|
|
75
|
+
|
|
76
|
+
Performs syntax parsing only, no database required. Returns a clean AST dictionary
|
|
77
|
+
with version compatibility normalization applied.
|
|
78
|
+
|
|
79
|
+
**What you get:**
|
|
80
|
+
- Clean AST structure (syntax tree)
|
|
81
|
+
- Context information (if WITH clause present)
|
|
82
|
+
- Version compatibility normalization
|
|
83
|
+
|
|
84
|
+
**What you DON'T get:**
|
|
85
|
+
- Data fields (datapoint IDs, operand references) - use generate_complete_ast()
|
|
86
|
+
- Framework structure - use generate_enriched_ast()
|
|
53
87
|
|
|
54
88
|
Args:
|
|
55
89
|
expression: DPM-XL expression string
|
|
56
90
|
|
|
57
91
|
Returns:
|
|
58
92
|
Dictionary containing:
|
|
59
|
-
- success:
|
|
60
|
-
- ast: AST dictionary
|
|
61
|
-
- context: Context information (if WITH clause present)
|
|
62
|
-
- error: Error message (if failed)
|
|
63
|
-
- metadata: Additional information
|
|
93
|
+
- success (bool): Whether parsing succeeded
|
|
94
|
+
- ast (dict): Clean AST dictionary
|
|
95
|
+
- context (dict): Context information (if WITH clause present)
|
|
96
|
+
- error (str): Error message (if failed)
|
|
97
|
+
- metadata (dict): Additional information (expression type, compatibility mode)
|
|
64
98
|
"""
|
|
65
99
|
try:
|
|
66
100
|
# Parse with syntax API
|
|
@@ -179,6 +213,260 @@ class ASTGenerator:
|
|
|
179
213
|
result['analysis'] = analysis
|
|
180
214
|
return result
|
|
181
215
|
|
|
216
|
+
# ============================================================================
|
|
217
|
+
# Complete AST Generation (requires database)
|
|
218
|
+
# ============================================================================
|
|
219
|
+
|
|
220
|
+
def generate_complete_ast(
|
|
221
|
+
self,
|
|
222
|
+
expression: str,
|
|
223
|
+
release_id: Optional[int] = None,
|
|
224
|
+
) -> Dict[str, Any]:
|
|
225
|
+
"""
|
|
226
|
+
Generate complete AST with all data fields populated (Level 2).
|
|
227
|
+
|
|
228
|
+
This method performs full semantic validation and operand checking using the database,
|
|
229
|
+
populating datapoint IDs and operand references in the AST. The result matches the
|
|
230
|
+
format found in json_scripts/*.json files.
|
|
231
|
+
|
|
232
|
+
**What you get:**
|
|
233
|
+
- Pure AST with data fields (datapoint IDs, operand references)
|
|
234
|
+
- Semantic validation results
|
|
235
|
+
- Context information
|
|
236
|
+
|
|
237
|
+
**What you DON'T get:**
|
|
238
|
+
- Framework structure (operations, variables, tables, preconditions)
|
|
239
|
+
- For that, use generate_enriched_ast() instead
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
expression: DPM-XL expression string
|
|
243
|
+
release_id: Optional release ID to filter database lookups by specific release.
|
|
244
|
+
If None, uses all available data (release-agnostic).
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
dict with keys:
|
|
248
|
+
- success (bool): Whether generation succeeded
|
|
249
|
+
- ast (dict): Complete AST with data fields
|
|
250
|
+
- context (dict): Context information (table, rows, columns, etc.)
|
|
251
|
+
- error (str): Error message if failed
|
|
252
|
+
- data_populated (bool): Whether data fields were populated
|
|
253
|
+
- semantic_result: Semantic validation result object
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
from py_dpm.dpm.utils import get_engine
|
|
257
|
+
from py_dpm.dpm_xl.utils.serialization import ASTToJSONVisitor
|
|
258
|
+
|
|
259
|
+
# Initialize database connection if explicitly provided, to surface connection errors early
|
|
260
|
+
try:
|
|
261
|
+
get_engine(database_path=self.database_path, connection_url=self.connection_url)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
return {
|
|
264
|
+
"success": False,
|
|
265
|
+
"ast": None,
|
|
266
|
+
"context": None,
|
|
267
|
+
"error": f"Database connection failed: {e}",
|
|
268
|
+
"data_populated": False,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Create or reuse semantic API for validation
|
|
272
|
+
if not self.semantic_api:
|
|
273
|
+
self.semantic_api = SemanticAPI(
|
|
274
|
+
database_path=self.database_path,
|
|
275
|
+
connection_url=self.connection_url
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
semantic_result = self.semantic_api.validate_expression(
|
|
279
|
+
expression, release_id=release_id
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# If semantic validation failed, return structured error
|
|
283
|
+
if not semantic_result.is_valid:
|
|
284
|
+
return {
|
|
285
|
+
"success": False,
|
|
286
|
+
"ast": None,
|
|
287
|
+
"context": None,
|
|
288
|
+
"error": semantic_result.error_message,
|
|
289
|
+
"data_populated": False,
|
|
290
|
+
"semantic_result": semantic_result,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
ast_root = getattr(self.semantic_api, "ast", None)
|
|
294
|
+
|
|
295
|
+
if ast_root is None:
|
|
296
|
+
return {
|
|
297
|
+
"success": False,
|
|
298
|
+
"ast": None,
|
|
299
|
+
"context": None,
|
|
300
|
+
"error": "Semantic validation did not generate AST",
|
|
301
|
+
"data_populated": False,
|
|
302
|
+
"semantic_result": semantic_result,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Extract components
|
|
306
|
+
actual_ast, context = self._extract_complete_components(ast_root)
|
|
307
|
+
|
|
308
|
+
# Convert to JSON using the ASTToJSONVisitor
|
|
309
|
+
visitor = ASTToJSONVisitor(context)
|
|
310
|
+
ast_dict = visitor.visit(actual_ast)
|
|
311
|
+
|
|
312
|
+
# Check if data fields were populated
|
|
313
|
+
data_populated = self._check_data_fields_populated(ast_dict)
|
|
314
|
+
|
|
315
|
+
# Serialize context
|
|
316
|
+
context_dict = self._serialize_context(context)
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
"success": True,
|
|
320
|
+
"ast": ast_dict,
|
|
321
|
+
"context": context_dict,
|
|
322
|
+
"error": None,
|
|
323
|
+
"data_populated": data_populated,
|
|
324
|
+
"semantic_result": semantic_result,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
return {
|
|
329
|
+
"success": False,
|
|
330
|
+
"ast": None,
|
|
331
|
+
"context": None,
|
|
332
|
+
"error": f"API error: {str(e)}",
|
|
333
|
+
"data_populated": False,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
def generate_complete_batch(
|
|
337
|
+
self,
|
|
338
|
+
expressions: List[str],
|
|
339
|
+
release_id: Optional[int] = None,
|
|
340
|
+
) -> List[Dict[str, Any]]:
|
|
341
|
+
"""
|
|
342
|
+
Generate complete ASTs for multiple expressions.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
expressions: List of DPM-XL expression strings
|
|
346
|
+
release_id: Optional release ID to filter database lookups by specific release.
|
|
347
|
+
If None, uses all available data (release-agnostic).
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
list: List of result dictionaries (same format as generate_complete_ast)
|
|
351
|
+
"""
|
|
352
|
+
results = []
|
|
353
|
+
for i, expr in enumerate(expressions):
|
|
354
|
+
result = self.generate_complete_ast(expr, release_id=release_id)
|
|
355
|
+
result["batch_index"] = i
|
|
356
|
+
results.append(result)
|
|
357
|
+
return results
|
|
358
|
+
|
|
359
|
+
# ============================================================================
|
|
360
|
+
# Enriched AST Generation (requires database)
|
|
361
|
+
# ============================================================================
|
|
362
|
+
|
|
363
|
+
def generate_enriched_ast(
|
|
364
|
+
self,
|
|
365
|
+
expression: str,
|
|
366
|
+
dpm_version: Optional[str] = None,
|
|
367
|
+
operation_code: Optional[str] = None,
|
|
368
|
+
table_context: Optional[Dict[str, Any]] = None,
|
|
369
|
+
precondition: Optional[str] = None,
|
|
370
|
+
release_id: Optional[int] = None,
|
|
371
|
+
output_path: Optional[Union[str, Path]] = None,
|
|
372
|
+
) -> Dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Generate enriched, engine-ready AST with framework structure (Level 3).
|
|
375
|
+
|
|
376
|
+
This extends generate_complete_ast() by wrapping the complete AST in an engine-ready
|
|
377
|
+
framework structure with operations, variables, tables, and preconditions sections.
|
|
378
|
+
This is the format required by business rule execution engines.
|
|
379
|
+
|
|
380
|
+
**What you get:**
|
|
381
|
+
- Everything from generate_complete_ast() PLUS:
|
|
382
|
+
- Framework structure: operations, variables, tables, preconditions
|
|
383
|
+
- Module metadata: version, release info, dates
|
|
384
|
+
- Dependency information
|
|
385
|
+
- Coordinates (x/y/z) added to data entries
|
|
386
|
+
|
|
387
|
+
**Typical use case:**
|
|
388
|
+
- Feeding AST to business rule execution engines
|
|
389
|
+
- Validation framework integration
|
|
390
|
+
- Production rule processing
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
expression: DPM-XL expression string
|
|
394
|
+
dpm_version: DPM version code (e.g., "4.0", "4.1", "4.2")
|
|
395
|
+
operation_code: Optional operation code (defaults to "default_code")
|
|
396
|
+
table_context: Optional table context dict with keys: 'table', 'columns', 'rows', 'sheets', 'default', 'interval'
|
|
397
|
+
precondition: Optional precondition variable reference (e.g., {v_F_44_04})
|
|
398
|
+
release_id: Optional release ID to filter database lookups by specific release.
|
|
399
|
+
If None, uses all available data (release-agnostic).
|
|
400
|
+
output_path: Optional path (string or Path) to save the enriched_ast as JSON file.
|
|
401
|
+
If provided, the enriched_ast will be automatically saved to this location.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
dict: {
|
|
405
|
+
'success': bool,
|
|
406
|
+
'enriched_ast': dict, # Engine-ready AST with framework structure
|
|
407
|
+
'error': str # Error message if failed
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
Example:
|
|
411
|
+
>>> generator = ASTGeneratorAPI(database_path="data.db")
|
|
412
|
+
>>> result = generator.generate_enriched_ast(
|
|
413
|
+
... "{tF_01.00, r0010, c0010}",
|
|
414
|
+
... dpm_version="4.2",
|
|
415
|
+
... operation_code="my_validation"
|
|
416
|
+
... )
|
|
417
|
+
>>> # result['enriched_ast'] contains framework structure ready for engines
|
|
418
|
+
>>>
|
|
419
|
+
>>> # Or save directly to a file:
|
|
420
|
+
>>> result = generator.generate_enriched_ast(
|
|
421
|
+
... "{tF_01.00, r0010, c0010}",
|
|
422
|
+
... dpm_version="4.2",
|
|
423
|
+
... operation_code="my_validation",
|
|
424
|
+
... output_path="./output/enriched_ast.json"
|
|
425
|
+
... )
|
|
426
|
+
>>> # The enriched_ast is automatically saved to the specified path
|
|
427
|
+
"""
|
|
428
|
+
try:
|
|
429
|
+
# Generate complete AST first
|
|
430
|
+
complete_result = self.generate_complete_ast(expression, release_id=release_id)
|
|
431
|
+
|
|
432
|
+
if not complete_result["success"]:
|
|
433
|
+
return {
|
|
434
|
+
"success": False,
|
|
435
|
+
"enriched_ast": None,
|
|
436
|
+
"error": f"Failed to generate complete AST: {complete_result['error']}",
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
complete_ast = complete_result["ast"]
|
|
440
|
+
context = complete_result.get("context") or table_context
|
|
441
|
+
|
|
442
|
+
# Enrich with framework structure
|
|
443
|
+
enriched_ast = self._enrich_ast_with_metadata(
|
|
444
|
+
ast_dict=complete_ast,
|
|
445
|
+
expression=expression,
|
|
446
|
+
context=context,
|
|
447
|
+
dpm_version=dpm_version,
|
|
448
|
+
operation_code=operation_code,
|
|
449
|
+
precondition=precondition,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Save to file if output_path is provided
|
|
453
|
+
if output_path is not None:
|
|
454
|
+
path = Path(output_path) if isinstance(output_path, str) else output_path
|
|
455
|
+
# Create parent directories if they don't exist
|
|
456
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
457
|
+
# Save enriched_ast as JSON
|
|
458
|
+
with open(path, "w") as f:
|
|
459
|
+
json.dump(enriched_ast, f, indent=4)
|
|
460
|
+
|
|
461
|
+
return {"success": True, "enriched_ast": enriched_ast, "error": None}
|
|
462
|
+
|
|
463
|
+
except Exception as e:
|
|
464
|
+
return {
|
|
465
|
+
"success": False,
|
|
466
|
+
"enriched_ast": None,
|
|
467
|
+
"error": f"Enrichment error: {str(e)}",
|
|
468
|
+
}
|
|
469
|
+
|
|
182
470
|
# Internal helper methods
|
|
183
471
|
|
|
184
472
|
def _extract_components(self, raw_ast):
|
|
@@ -393,6 +681,398 @@ class ASTGenerator:
|
|
|
393
681
|
score += self._calculate_complexity(item)
|
|
394
682
|
return score
|
|
395
683
|
|
|
684
|
+
# ============================================================================
|
|
685
|
+
# Helper methods for complete and enriched AST generation
|
|
686
|
+
# ============================================================================
|
|
687
|
+
|
|
688
|
+
def _extract_complete_components(self, ast_obj):
|
|
689
|
+
"""Extract context and expression from complete AST object."""
|
|
690
|
+
if hasattr(ast_obj, "children") and len(ast_obj.children) > 0:
|
|
691
|
+
child = ast_obj.children[0]
|
|
692
|
+
if hasattr(child, "expression"):
|
|
693
|
+
return child.expression, child.partial_selection
|
|
694
|
+
else:
|
|
695
|
+
return child, None
|
|
696
|
+
return ast_obj, None
|
|
697
|
+
|
|
698
|
+
def _check_data_fields_populated(self, ast_dict):
|
|
699
|
+
"""Check if any VarID nodes have data fields populated."""
|
|
700
|
+
if not isinstance(ast_dict, dict):
|
|
701
|
+
return False
|
|
702
|
+
|
|
703
|
+
if ast_dict.get("class_name") == "VarID" and "data" in ast_dict:
|
|
704
|
+
return True
|
|
705
|
+
|
|
706
|
+
# Recursively check nested structures
|
|
707
|
+
for value in ast_dict.values():
|
|
708
|
+
if isinstance(value, dict):
|
|
709
|
+
if self._check_data_fields_populated(value):
|
|
710
|
+
return True
|
|
711
|
+
elif isinstance(value, list):
|
|
712
|
+
for item in value:
|
|
713
|
+
if isinstance(item, dict) and self._check_data_fields_populated(item):
|
|
714
|
+
return True
|
|
715
|
+
|
|
716
|
+
return False
|
|
717
|
+
|
|
718
|
+
def _enrich_ast_with_metadata(
|
|
719
|
+
self,
|
|
720
|
+
ast_dict: Dict[str, Any],
|
|
721
|
+
expression: str,
|
|
722
|
+
context: Optional[Dict[str, Any]],
|
|
723
|
+
dpm_version: Optional[str] = None,
|
|
724
|
+
operation_code: Optional[str] = None,
|
|
725
|
+
precondition: Optional[str] = None,
|
|
726
|
+
) -> Dict[str, Any]:
|
|
727
|
+
"""
|
|
728
|
+
Add framework structure (operations, variables, tables, preconditions) to complete AST.
|
|
729
|
+
|
|
730
|
+
This creates the engine-ready format with all metadata sections.
|
|
731
|
+
"""
|
|
732
|
+
from py_dpm.dpm.utils import get_engine
|
|
733
|
+
import copy
|
|
734
|
+
|
|
735
|
+
# Initialize database connection
|
|
736
|
+
engine = get_engine(database_path=self.database_path, connection_url=self.connection_url)
|
|
737
|
+
|
|
738
|
+
# Generate operation code if not provided
|
|
739
|
+
if not operation_code:
|
|
740
|
+
operation_code = "default_code"
|
|
741
|
+
|
|
742
|
+
# Get current date for framework structure
|
|
743
|
+
current_date = datetime.now().strftime("%Y-%m-%d")
|
|
744
|
+
|
|
745
|
+
# Query database for release information
|
|
746
|
+
release_info = self._get_release_info(dpm_version, engine)
|
|
747
|
+
|
|
748
|
+
# Build module info
|
|
749
|
+
module_info = {
|
|
750
|
+
"module_code": "default",
|
|
751
|
+
"module_version": "1.0.0",
|
|
752
|
+
"framework_code": "default",
|
|
753
|
+
"dpm_release": {
|
|
754
|
+
"release": release_info["release"],
|
|
755
|
+
"publication_date": release_info["publication_date"],
|
|
756
|
+
},
|
|
757
|
+
"dates": {"from": "2001-01-01", "to": None},
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
# Add coordinates to AST data entries
|
|
761
|
+
ast_with_coords = self._add_coordinates_to_ast(ast_dict, context)
|
|
762
|
+
|
|
763
|
+
# Build operations section
|
|
764
|
+
operations = {
|
|
765
|
+
operation_code: {
|
|
766
|
+
"version_id": hash(expression) % 10000,
|
|
767
|
+
"code": operation_code,
|
|
768
|
+
"expression": expression,
|
|
769
|
+
"root_operator_id": 24, # Default for now
|
|
770
|
+
"ast": ast_with_coords,
|
|
771
|
+
"from_submission_date": current_date,
|
|
772
|
+
"severity": "Error",
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
# Build variables section by extracting from the complete AST
|
|
777
|
+
all_variables, variables_by_table = self._extract_variables_from_ast(ast_with_coords)
|
|
778
|
+
|
|
779
|
+
variables = all_variables
|
|
780
|
+
tables = {}
|
|
781
|
+
|
|
782
|
+
# Build tables with their specific variables
|
|
783
|
+
for table_code, table_variables in variables_by_table.items():
|
|
784
|
+
tables[table_code] = {"variables": table_variables, "open_keys": {}}
|
|
785
|
+
|
|
786
|
+
# Build preconditions
|
|
787
|
+
preconditions = {}
|
|
788
|
+
precondition_variables = {}
|
|
789
|
+
|
|
790
|
+
if precondition or (context and "table" in context):
|
|
791
|
+
preconditions, precondition_variables = self._build_preconditions(
|
|
792
|
+
precondition=precondition,
|
|
793
|
+
context=context,
|
|
794
|
+
operation_code=operation_code,
|
|
795
|
+
engine=engine,
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
# Build dependency information
|
|
799
|
+
dependency_info = {
|
|
800
|
+
"intra_instance_validations": [operation_code],
|
|
801
|
+
"cross_instance_dependencies": [],
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
# Build dependency modules
|
|
805
|
+
dependency_modules = {}
|
|
806
|
+
|
|
807
|
+
# Build complete structure
|
|
808
|
+
namespace = "default_module"
|
|
809
|
+
|
|
810
|
+
return {
|
|
811
|
+
namespace: {
|
|
812
|
+
**module_info,
|
|
813
|
+
"operations": operations,
|
|
814
|
+
"variables": variables,
|
|
815
|
+
"tables": tables,
|
|
816
|
+
"preconditions": preconditions,
|
|
817
|
+
"precondition_variables": precondition_variables,
|
|
818
|
+
"dependency_information": dependency_info,
|
|
819
|
+
"dependency_modules": dependency_modules,
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
def _get_release_info(self, dpm_version: Optional[str], engine) -> Dict[str, Any]:
|
|
824
|
+
"""Get release information from database using SQLAlchemy."""
|
|
825
|
+
from py_dpm.dpm.models import Release
|
|
826
|
+
from sqlalchemy.orm import sessionmaker
|
|
827
|
+
|
|
828
|
+
Session = sessionmaker(bind=engine)
|
|
829
|
+
session = Session()
|
|
830
|
+
|
|
831
|
+
try:
|
|
832
|
+
if dpm_version:
|
|
833
|
+
# Query for specific version
|
|
834
|
+
version_float = float(dpm_version)
|
|
835
|
+
release = (
|
|
836
|
+
session.query(Release)
|
|
837
|
+
.filter(Release.code == str(version_float))
|
|
838
|
+
.first()
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
if release:
|
|
842
|
+
return {
|
|
843
|
+
"release": str(release.code) if release.code else dpm_version,
|
|
844
|
+
"publication_date": (
|
|
845
|
+
release.date.strftime("%Y-%m-%d")
|
|
846
|
+
if release.date
|
|
847
|
+
else "2001-01-01"
|
|
848
|
+
),
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
# Fallback: get latest released version
|
|
852
|
+
release = (
|
|
853
|
+
session.query(Release)
|
|
854
|
+
.filter(Release.status == "released")
|
|
855
|
+
.order_by(Release.code.desc())
|
|
856
|
+
.first()
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
if release:
|
|
860
|
+
return {
|
|
861
|
+
"release": str(release.code) if release.code else "4.1",
|
|
862
|
+
"publication_date": (
|
|
863
|
+
release.date.strftime("%Y-%m-%d") if release.date else "2001-01-01"
|
|
864
|
+
),
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
# Final fallback
|
|
868
|
+
return {"release": "4.1", "publication_date": "2001-01-01"}
|
|
869
|
+
|
|
870
|
+
except Exception:
|
|
871
|
+
# Fallback on any error
|
|
872
|
+
return {"release": "4.1", "publication_date": "2001-01-01"}
|
|
873
|
+
finally:
|
|
874
|
+
session.close()
|
|
875
|
+
|
|
876
|
+
def _get_table_info(self, table_code: str, engine) -> Optional[Dict[str, Any]]:
|
|
877
|
+
"""Get table information from database using SQLAlchemy."""
|
|
878
|
+
from py_dpm.dpm.models import TableVersion
|
|
879
|
+
from sqlalchemy.orm import sessionmaker
|
|
880
|
+
import re
|
|
881
|
+
|
|
882
|
+
Session = sessionmaker(bind=engine)
|
|
883
|
+
session = Session()
|
|
884
|
+
|
|
885
|
+
try:
|
|
886
|
+
# Try exact match first
|
|
887
|
+
table = (
|
|
888
|
+
session.query(TableVersion).filter(TableVersion.code == table_code).first()
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
if table:
|
|
892
|
+
return {"table_vid": table.tablevid, "code": table.code}
|
|
893
|
+
|
|
894
|
+
# Handle precondition parser format: F_25_01 -> F_25.01
|
|
895
|
+
if re.match(r"^[A-Z]_\d+_\d+", table_code):
|
|
896
|
+
parts = table_code.split("_", 2)
|
|
897
|
+
if len(parts) >= 3:
|
|
898
|
+
table_code_with_dot = f"{parts[0]}_{parts[1]}.{parts[2]}"
|
|
899
|
+
table = (
|
|
900
|
+
session.query(TableVersion)
|
|
901
|
+
.filter(TableVersion.code == table_code_with_dot)
|
|
902
|
+
.first()
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
if table:
|
|
906
|
+
return {"table_vid": table.tablevid, "code": table.code}
|
|
907
|
+
|
|
908
|
+
# Try LIKE pattern as last resort (handles sub-tables like F_25.01.a)
|
|
909
|
+
table = (
|
|
910
|
+
session.query(TableVersion)
|
|
911
|
+
.filter(TableVersion.code.like(f"{table_code}%"))
|
|
912
|
+
.order_by(TableVersion.code)
|
|
913
|
+
.first()
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
if table:
|
|
917
|
+
return {"table_vid": table.tablevid, "code": table.code}
|
|
918
|
+
|
|
919
|
+
return None
|
|
920
|
+
|
|
921
|
+
except Exception:
|
|
922
|
+
return None
|
|
923
|
+
finally:
|
|
924
|
+
session.close()
|
|
925
|
+
|
|
926
|
+
def _build_preconditions(
|
|
927
|
+
self,
|
|
928
|
+
precondition: Optional[str],
|
|
929
|
+
context: Optional[Dict[str, Any]],
|
|
930
|
+
operation_code: str,
|
|
931
|
+
engine,
|
|
932
|
+
) -> tuple:
|
|
933
|
+
"""Build preconditions and precondition_variables sections."""
|
|
934
|
+
import re
|
|
935
|
+
|
|
936
|
+
preconditions = {}
|
|
937
|
+
precondition_variables = {}
|
|
938
|
+
|
|
939
|
+
# Extract table code from precondition or context
|
|
940
|
+
table_code = None
|
|
941
|
+
|
|
942
|
+
if precondition:
|
|
943
|
+
# Extract variable code from precondition reference like {v_F_44_04}
|
|
944
|
+
match = re.match(r"\{v_([^}]+)\}", precondition)
|
|
945
|
+
if match:
|
|
946
|
+
table_code = match.group(1)
|
|
947
|
+
elif context and "table" in context:
|
|
948
|
+
table_code = context["table"]
|
|
949
|
+
|
|
950
|
+
if table_code:
|
|
951
|
+
# Query database for actual variable ID and version
|
|
952
|
+
table_info = self._get_table_info(table_code, engine)
|
|
953
|
+
|
|
954
|
+
if table_info:
|
|
955
|
+
precondition_var_id = table_info["table_vid"]
|
|
956
|
+
version_id = table_info["table_vid"]
|
|
957
|
+
precondition_code = f"p_{precondition_var_id}"
|
|
958
|
+
|
|
959
|
+
preconditions[precondition_code] = {
|
|
960
|
+
"ast": {
|
|
961
|
+
"class_name": "PreconditionItem",
|
|
962
|
+
"variable_id": precondition_var_id,
|
|
963
|
+
"variable_code": table_code,
|
|
964
|
+
},
|
|
965
|
+
"affected_operations": [operation_code],
|
|
966
|
+
"version_id": version_id,
|
|
967
|
+
"code": precondition_code,
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
precondition_variables[str(precondition_var_id)] = "b"
|
|
971
|
+
|
|
972
|
+
return preconditions, precondition_variables
|
|
973
|
+
|
|
974
|
+
def _extract_variables_from_ast(self, ast_dict: Dict[str, Any]) -> tuple:
|
|
975
|
+
"""
|
|
976
|
+
Extract variables from complete AST by table.
|
|
977
|
+
|
|
978
|
+
Returns:
|
|
979
|
+
tuple: (all_variables_dict, variables_by_table_dict)
|
|
980
|
+
"""
|
|
981
|
+
variables_by_table = {}
|
|
982
|
+
all_variables = {}
|
|
983
|
+
|
|
984
|
+
def extract_from_node(node):
|
|
985
|
+
if isinstance(node, dict):
|
|
986
|
+
# Check if this is a VarID node with data
|
|
987
|
+
if node.get("class_name") == "VarID" and "data" in node:
|
|
988
|
+
table = node.get("table")
|
|
989
|
+
if table:
|
|
990
|
+
if table not in variables_by_table:
|
|
991
|
+
variables_by_table[table] = {}
|
|
992
|
+
|
|
993
|
+
# Extract variable IDs and data types from AST data array
|
|
994
|
+
for data_item in node["data"]:
|
|
995
|
+
if "datapoint" in data_item:
|
|
996
|
+
var_id = str(int(data_item["datapoint"]))
|
|
997
|
+
data_type = data_item.get("data_type", "e")
|
|
998
|
+
variables_by_table[table][var_id] = data_type
|
|
999
|
+
all_variables[var_id] = data_type
|
|
1000
|
+
|
|
1001
|
+
# Recursively process nested nodes
|
|
1002
|
+
for value in node.values():
|
|
1003
|
+
if isinstance(value, (dict, list)):
|
|
1004
|
+
extract_from_node(value)
|
|
1005
|
+
elif isinstance(node, list):
|
|
1006
|
+
for item in node:
|
|
1007
|
+
extract_from_node(item)
|
|
1008
|
+
|
|
1009
|
+
extract_from_node(ast_dict)
|
|
1010
|
+
return all_variables, variables_by_table
|
|
1011
|
+
|
|
1012
|
+
def _add_coordinates_to_ast(
|
|
1013
|
+
self, ast_dict: Dict[str, Any], context: Optional[Dict[str, Any]]
|
|
1014
|
+
) -> Dict[str, Any]:
|
|
1015
|
+
"""Add x/y/z coordinates to data entries in AST."""
|
|
1016
|
+
import copy
|
|
1017
|
+
|
|
1018
|
+
def add_coords_to_node(node):
|
|
1019
|
+
if isinstance(node, dict):
|
|
1020
|
+
# Handle VarID nodes with data arrays
|
|
1021
|
+
if node.get("class_name") == "VarID" and "data" in node:
|
|
1022
|
+
# Get column information from context
|
|
1023
|
+
cols = []
|
|
1024
|
+
if context and "columns" in context and context["columns"]:
|
|
1025
|
+
cols = context["columns"]
|
|
1026
|
+
|
|
1027
|
+
# Group data entries by row to assign coordinates correctly
|
|
1028
|
+
entries_by_row = {}
|
|
1029
|
+
for data_entry in node["data"]:
|
|
1030
|
+
row_code = data_entry.get("row", "")
|
|
1031
|
+
if row_code not in entries_by_row:
|
|
1032
|
+
entries_by_row[row_code] = []
|
|
1033
|
+
entries_by_row[row_code].append(data_entry)
|
|
1034
|
+
|
|
1035
|
+
# Assign coordinates based on column order and row grouping
|
|
1036
|
+
rows = list(entries_by_row.keys())
|
|
1037
|
+
for x_index, row_code in enumerate(rows, 1):
|
|
1038
|
+
for data_entry in entries_by_row[row_code]:
|
|
1039
|
+
column_code = data_entry.get("column", "")
|
|
1040
|
+
|
|
1041
|
+
# Find y coordinate based on column position in context
|
|
1042
|
+
y_index = 1 # default
|
|
1043
|
+
if cols and column_code in cols:
|
|
1044
|
+
y_index = cols.index(column_code) + 1
|
|
1045
|
+
elif cols:
|
|
1046
|
+
# Fallback to order in data
|
|
1047
|
+
row_columns = [
|
|
1048
|
+
entry.get("column", "")
|
|
1049
|
+
for entry in entries_by_row[row_code]
|
|
1050
|
+
]
|
|
1051
|
+
if column_code in row_columns:
|
|
1052
|
+
y_index = row_columns.index(column_code) + 1
|
|
1053
|
+
|
|
1054
|
+
# Always add y coordinate
|
|
1055
|
+
data_entry["y"] = y_index
|
|
1056
|
+
|
|
1057
|
+
# Add x coordinate only if there are multiple rows
|
|
1058
|
+
if len(rows) > 1:
|
|
1059
|
+
data_entry["x"] = x_index
|
|
1060
|
+
|
|
1061
|
+
# TODO: Add z coordinate for sheets when needed
|
|
1062
|
+
|
|
1063
|
+
# Recursively process child nodes
|
|
1064
|
+
for key, value in node.items():
|
|
1065
|
+
if isinstance(value, (dict, list)):
|
|
1066
|
+
add_coords_to_node(value)
|
|
1067
|
+
elif isinstance(node, list):
|
|
1068
|
+
for item in node:
|
|
1069
|
+
add_coords_to_node(item)
|
|
1070
|
+
|
|
1071
|
+
# Create a deep copy to avoid modifying the original
|
|
1072
|
+
result = copy.deepcopy(ast_dict)
|
|
1073
|
+
add_coords_to_node(result)
|
|
1074
|
+
return result
|
|
1075
|
+
|
|
396
1076
|
|
|
397
1077
|
# Convenience functions for simple usage
|
|
398
1078
|
|
|
@@ -407,7 +1087,7 @@ def parse_expression(expression: str, compatibility_mode: str = "auto") -> Dict[
|
|
|
407
1087
|
Returns:
|
|
408
1088
|
Parse result dictionary
|
|
409
1089
|
"""
|
|
410
|
-
generator =
|
|
1090
|
+
generator = ASTGeneratorAPI(compatibility_mode=compatibility_mode)
|
|
411
1091
|
return generator.parse_expression(expression)
|
|
412
1092
|
|
|
413
1093
|
|
|
@@ -421,7 +1101,7 @@ def validate_expression(expression: str) -> bool:
|
|
|
421
1101
|
Returns:
|
|
422
1102
|
True if valid, False otherwise
|
|
423
1103
|
"""
|
|
424
|
-
generator =
|
|
1104
|
+
generator = ASTGeneratorAPI()
|
|
425
1105
|
result = generator.validate_expression(expression)
|
|
426
1106
|
return result['valid']
|
|
427
1107
|
|
|
@@ -437,5 +1117,5 @@ def parse_batch(expressions: List[str], compatibility_mode: str = "auto") -> Lis
|
|
|
437
1117
|
Returns:
|
|
438
1118
|
List of parse results
|
|
439
1119
|
"""
|
|
440
|
-
generator =
|
|
1120
|
+
generator = ASTGeneratorAPI(compatibility_mode=compatibility_mode)
|
|
441
1121
|
return generator.parse_batch(expressions)
|