port-ocean 0.28.11__py3-none-any.whl → 0.28.14__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.

Potentially problematic release.


This version of port-ocean might be problematic. Click here for more details.

@@ -50,7 +50,7 @@ class TestJQEntityProcessor:
50
50
  raw_entity_mappings = {"foo": ".foo"}
51
51
  selector_query = '.foo == "bar"'
52
52
  result = await mocked_processor._get_mapped_entity(
53
- data, raw_entity_mappings, selector_query
53
+ data, raw_entity_mappings, None, selector_query
54
54
  )
55
55
  assert result.entity == {"foo": "bar"}
56
56
  assert result.did_entity_pass_selector is True
@@ -357,3 +357,934 @@ class TestJQEntityProcessor:
357
357
  "{'blueprint': '.bar', 'identifier': '.foo'} (null, missing, or misconfigured)"
358
358
  in logs_captured
359
359
  )
360
+
361
+ async def test_build_raw_entity_mappings_string_values(
362
+ self, mocked_processor: JQEntityProcessor
363
+ ) -> None:
364
+ """Test _build_raw_entity_mappings with string values that evaluate to different InputEvaluationResult types"""
365
+ raw_entity_mappings = {
366
+ "identifier": ".item.id", # SINGLE - contains pattern
367
+ "title": ".item.name", # SINGLE - contains pattern
368
+ "blueprint": ".item.type", # SINGLE - contains pattern
369
+ "icon": ".item.icon", # SINGLE - contains pattern
370
+ "team": ".item.team", # SINGLE - contains pattern
371
+ "properties": {
372
+ "status": ".item.status", # SINGLE - contains pattern
373
+ "description": ".item.desc", # SINGLE - contains pattern
374
+ "external_ref": ".external.ref", # ALL - contains dots but not pattern
375
+ "static_value": '"static"', # NONE - no pattern, no dots
376
+ },
377
+ "relations": {
378
+ "owner": ".item.owner", # SINGLE - contains pattern
379
+ "parent": ".item.parent", # SINGLE - contains pattern
380
+ "external_relation": ".external.relation", # ALL - contains dots but not pattern
381
+ "null_value": "null", # NONE - nullary expression
382
+ },
383
+ }
384
+ items_to_parse_name = "item"
385
+
386
+ single, all_items, none = mocked_processor._build_raw_entity_mappings(
387
+ raw_entity_mappings, items_to_parse_name
388
+ )
389
+
390
+ # SINGLE mappings should contain all fields that reference .item
391
+ expected_single = {
392
+ "identifier": ".item.id",
393
+ "title": ".item.name",
394
+ "blueprint": ".item.type",
395
+ "icon": ".item.icon",
396
+ "team": ".item.team",
397
+ "properties": {
398
+ "status": ".item.status",
399
+ "description": ".item.desc",
400
+ },
401
+ "relations": {
402
+ "owner": ".item.owner",
403
+ "parent": ".item.parent",
404
+ },
405
+ }
406
+ assert single == expected_single
407
+
408
+ # ALL mappings should contain fields that reference other patterns
409
+ expected_all = {
410
+ "properties": {
411
+ "external_ref": ".external.ref",
412
+ },
413
+ "relations": {
414
+ "external_relation": ".external.relation",
415
+ },
416
+ }
417
+ assert all_items == expected_all
418
+
419
+ # NONE mappings should contain nullary expressions
420
+ expected_none = {
421
+ "properties": {
422
+ "static_value": '"static"',
423
+ },
424
+ "relations": {
425
+ "null_value": "null",
426
+ },
427
+ }
428
+ assert none == expected_none
429
+
430
+ async def test_group_string_mapping_value(
431
+ self, mocked_processor: JQEntityProcessor
432
+ ) -> None:
433
+ """Test group_string_mapping_value function with various string values"""
434
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
435
+ InputClassifyingResult,
436
+ )
437
+
438
+ # Test with different input evaluation results
439
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
440
+ InputClassifyingResult.SINGLE: {},
441
+ InputClassifyingResult.ALL: {},
442
+ InputClassifyingResult.NONE: {},
443
+ }
444
+
445
+ # Test SINGLE evaluation (contains pattern)
446
+ mocked_processor.group_string_mapping_value(
447
+ "item", mappings, "identifier", ".item.id"
448
+ )
449
+ assert mappings[InputClassifyingResult.SINGLE]["identifier"] == ".item.id"
450
+ assert (
451
+ InputClassifyingResult.ALL not in mappings
452
+ or not mappings[InputClassifyingResult.ALL]
453
+ )
454
+ assert (
455
+ InputClassifyingResult.NONE not in mappings
456
+ or not mappings[InputClassifyingResult.NONE]
457
+ )
458
+
459
+ # Test ALL evaluation (contains dots but not pattern)
460
+ mappings = {
461
+ InputClassifyingResult.SINGLE: {},
462
+ InputClassifyingResult.ALL: {},
463
+ InputClassifyingResult.NONE: {},
464
+ }
465
+ mocked_processor.group_string_mapping_value(
466
+ "item", mappings, "external_ref", ".external.ref"
467
+ )
468
+ assert mappings[InputClassifyingResult.ALL]["external_ref"] == ".external.ref"
469
+ assert (
470
+ InputClassifyingResult.SINGLE not in mappings
471
+ or not mappings[InputClassifyingResult.SINGLE]
472
+ )
473
+ assert (
474
+ InputClassifyingResult.NONE not in mappings
475
+ or not mappings[InputClassifyingResult.NONE]
476
+ )
477
+
478
+ # Test NONE evaluation (nullary expression)
479
+ mappings = {
480
+ InputClassifyingResult.SINGLE: {},
481
+ InputClassifyingResult.ALL: {},
482
+ InputClassifyingResult.NONE: {},
483
+ }
484
+ mocked_processor.group_string_mapping_value(
485
+ "item", mappings, "static_value", '"static"'
486
+ )
487
+ assert mappings[InputClassifyingResult.NONE]["static_value"] == '"static"'
488
+ assert (
489
+ InputClassifyingResult.SINGLE not in mappings
490
+ or not mappings[InputClassifyingResult.SINGLE]
491
+ )
492
+ assert (
493
+ InputClassifyingResult.ALL not in mappings
494
+ or not mappings[InputClassifyingResult.ALL]
495
+ )
496
+
497
+ async def test_group_complex_mapping_value_properties(
498
+ self, mocked_processor: JQEntityProcessor
499
+ ) -> None:
500
+ """Test group_complex_mapping_value with properties dictionary"""
501
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
502
+ InputClassifyingResult,
503
+ )
504
+
505
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
506
+ InputClassifyingResult.SINGLE: {},
507
+ InputClassifyingResult.ALL: {},
508
+ InputClassifyingResult.NONE: {},
509
+ }
510
+
511
+ # Test properties with mixed string values
512
+ properties = {
513
+ "name": ".item.name", # SINGLE
514
+ "description": ".item.desc", # SINGLE
515
+ "external_ref": ".external.ref", # ALL
516
+ "static_value": '"static"', # NONE
517
+ }
518
+
519
+ mocked_processor.group_complex_mapping_value(
520
+ "item", mappings, "properties", properties
521
+ )
522
+
523
+ expected_single = {
524
+ "name": ".item.name",
525
+ "description": ".item.desc",
526
+ }
527
+ expected_all = {
528
+ "external_ref": ".external.ref",
529
+ }
530
+ expected_none = {
531
+ "static_value": '"static"',
532
+ }
533
+
534
+ assert mappings[InputClassifyingResult.SINGLE]["properties"] == expected_single
535
+ assert mappings[InputClassifyingResult.ALL]["properties"] == expected_all
536
+ assert mappings[InputClassifyingResult.NONE]["properties"] == expected_none
537
+
538
+ async def test_build_raw_entity_mappings_edge_cases(
539
+ self, mocked_processor: JQEntityProcessor
540
+ ) -> None:
541
+ """Test _build_raw_entity_mappings with edge cases including patterns in the middle of expressions"""
542
+ raw_entity_mappings: dict[str, Any] = {
543
+ "identifier": ".item.id", # Normal case - SINGLE
544
+ "title": "", # Empty string - NONE
545
+ "blueprint": " ", # Whitespace only - NONE
546
+ "icon": ".", # Just a dot - ALL
547
+ "team": ".item", # Just the pattern - SINGLE
548
+ "properties": {
549
+ "multiple_patterns": ".item.field.item", # Multiple occurrences - SINGLE
550
+ "pattern_at_end": ".field.item", # Pattern at end - ALL (doesn't start with .item)
551
+ "pattern_at_start": ".item.field", # Pattern at start - SINGLE
552
+ "pattern_in_middle": ".body.somefield.item", # Pattern in middle - ALL (doesn't start with .item)
553
+ "pattern_in_middle_with_dots": ".data.items.item.field", # Pattern in middle with dots - ALL
554
+ "case_sensitive": ".ITEM.field", # Case sensitive (should not match) - ALL
555
+ "special_chars": ".item.field[0]", # Special characters - SINGLE
556
+ "quoted_pattern": '".item.field"', # Quoted pattern - NONE
557
+ "field_with_null_name": ".is_null", # Field with null name - ALL
558
+ "empty_string": "", # Empty string - NONE
559
+ "item_in_string": 'select(.data.string == ".item")', # Item referenced in string only - ALL
560
+ "function_with_pattern": "map(.item.field)", # Function with pattern - SINGLE
561
+ "function_with_middle_pattern": "map(.body.item.field)", # Function with middle pattern - ALL
562
+ "select_with_pattern": 'select(.item.status == "active")', # Select with pattern - SINGLE
563
+ "select_with_middle_pattern": 'select(.data.item.status == "active")', # Select with middle pattern - ALL
564
+ "pipe_with_pattern": ".[] | .item.field", # Pipe with pattern - SINGLE
565
+ "pipe_with_middle_pattern": ".[] | .body.item.field", # Pipe with middle pattern - ALL
566
+ "array_with_pattern": "[.item.id, .item.name]", # Array with pattern - SINGLE
567
+ "array_with_middle_pattern": "[.data.item.id, .body.item.name]", # Array with middle pattern - ALL
568
+ "object_with_pattern": "{id: .item.id, name: .item.name}", # Object with pattern - SINGLE
569
+ "object_with_middle_pattern": "{id: .data.item.id, name: .body.item.name}", # Object with middle pattern - ALL
570
+ "nested_with_pattern": ".data.items[] | .item.field", # Nested with pattern - SINGLE
571
+ "nested_with_middle_pattern": ".data.items[] | .body.item.field", # Nested with middle pattern - ALL
572
+ "conditional_with_pattern": "if .item.exists then .item.value else null end", # Conditional with pattern - SINGLE
573
+ "conditional_with_middle_pattern": "if .data.item.exists then .body.item.value else null end", # Conditional with middle pattern - ALL
574
+ "string_plus_string": '"abc" + "def"', # String plus string - NONE
575
+ "number_plus_number": "42 + 10", # Number plus number - NONE
576
+ },
577
+ "relations": {
578
+ "normal_relation": ".item.owner", # Normal case - SINGLE
579
+ "middle_pattern_relation": ".data.item.owner", # Middle pattern - ALL
580
+ "external_relation": ".external.ref", # External reference - ALL
581
+ "nullary_relation": "null", # Nullary expression - NONE
582
+ },
583
+ }
584
+ items_to_parse_name = "item"
585
+
586
+ single, all_items, none = mocked_processor._build_raw_entity_mappings(
587
+ raw_entity_mappings, items_to_parse_name
588
+ )
589
+
590
+ # SINGLE mappings - only those that start with the exact pattern
591
+ expected_single = {
592
+ "identifier": ".item.id",
593
+ "team": ".item",
594
+ "properties": {
595
+ "multiple_patterns": ".item.field.item",
596
+ "pattern_at_start": ".item.field",
597
+ "special_chars": ".item.field[0]",
598
+ "function_with_pattern": "map(.item.field)",
599
+ "select_with_pattern": 'select(.item.status == "active")',
600
+ "pipe_with_pattern": ".[] | .item.field",
601
+ "array_with_pattern": "[.item.id, .item.name]",
602
+ "object_with_pattern": "{id: .item.id, name: .item.name}",
603
+ "nested_with_pattern": ".data.items[] | .item.field",
604
+ "conditional_with_pattern": "if .item.exists then .item.value else null end",
605
+ },
606
+ "relations": {
607
+ "normal_relation": ".item.owner",
608
+ },
609
+ }
610
+ assert single == expected_single
611
+
612
+ # ALL mappings - those with dots but not starting with the pattern
613
+ expected_all = {
614
+ "icon": ".",
615
+ "properties": {
616
+ "pattern_at_end": ".field.item",
617
+ "pattern_in_middle": ".body.somefield.item",
618
+ "pattern_in_middle_with_dots": ".data.items.item.field",
619
+ "case_sensitive": ".ITEM.field",
620
+ "function_with_middle_pattern": "map(.body.item.field)",
621
+ "select_with_middle_pattern": 'select(.data.item.status == "active")',
622
+ "item_in_string": 'select(.data.string == ".item")',
623
+ "pipe_with_middle_pattern": ".[] | .body.item.field",
624
+ "array_with_middle_pattern": "[.data.item.id, .body.item.name]",
625
+ "object_with_middle_pattern": "{id: .data.item.id, name: .body.item.name}",
626
+ "nested_with_middle_pattern": ".data.items[] | .body.item.field",
627
+ "conditional_with_middle_pattern": "if .data.item.exists then .body.item.value else null end",
628
+ "field_with_null_name": ".is_null",
629
+ },
630
+ "relations": {
631
+ "middle_pattern_relation": ".data.item.owner",
632
+ "external_relation": ".external.ref",
633
+ },
634
+ }
635
+ assert all_items == expected_all
636
+
637
+ # NONE mappings - nullary expressions
638
+ expected_none = {
639
+ "title": "",
640
+ "blueprint": " ",
641
+ "properties": {
642
+ "quoted_pattern": '".item.field"',
643
+ "empty_string": "",
644
+ "string_plus_string": '"abc" + "def"',
645
+ "number_plus_number": "42 + 10",
646
+ },
647
+ "relations": {
648
+ "nullary_relation": "null",
649
+ },
650
+ }
651
+ assert none == expected_none
652
+
653
+ async def test_build_raw_entity_mappings_complex_jq_expressions(
654
+ self, mocked_processor: JQEntityProcessor
655
+ ) -> None:
656
+ """Test _build_raw_entity_mappings with complex JQ expressions that contain the pattern but don't start with it"""
657
+ raw_entity_mappings: dict[str, Any] = {
658
+ "identifier": ".item.id", # Simple case - SINGLE
659
+ "title": ".item.name", # Simple case - SINGLE
660
+ "blueprint": ".item.type", # Simple case - SINGLE
661
+ "icon": ".item.icon", # Simple case - SINGLE
662
+ "team": ".item.team", # Simple case - SINGLE
663
+ "properties": {
664
+ # JQ expressions with functions that contain .item
665
+ "mapped_property": "map(.item.field)", # Contains .item but starts with map - SINGLE
666
+ "selected_property": 'select(.item.status == "active")', # Contains .item but starts with select - SINGLE
667
+ "filtered_property": '.[] | select(.item.type == "service")', # Contains .item in pipe - SINGLE
668
+ "array_literal": "[.item.id, .item.name]", # Contains .item in array - SINGLE
669
+ "object_literal": "{id: .item.id, name: .item.name}", # Contains .item in object - SINGLE
670
+ "nested_access": ".data.items[] | .item.field", # Contains .item in nested access - SINGLE
671
+ "conditional": "if .item.exists then .item.value else null end", # Contains .item in conditional - SINGLE
672
+ "function_call": "length(.item.array)", # Contains .item in function call - SINGLE
673
+ "range_expression": "range(.item.start; .item.end)", # Contains .item in range - SINGLE
674
+ "reduce_expression": "reduce .item.items[] as $item (0; . + $item.value)", # Contains .item in reduce - SINGLE
675
+ "group_by": "group_by(.item.category)", # Contains .item in group_by - SINGLE
676
+ "sort_by": "sort_by(.item.priority)", # Contains .item in sort_by - SINGLE
677
+ "unique_by": "unique_by(.item.id)", # Contains .item in unique_by - SINGLE
678
+ "flatten": "flatten(.item.nested)", # Contains .item in flatten - SINGLE
679
+ "transpose": "transpose(.item.matrix)", # Contains .item in transpose - SINGLE
680
+ "combinations": "combinations(.item.items)", # Contains .item in combinations - SINGLE
681
+ "permutations": "permutations(.item.items)", # Contains .item in permutations - SINGLE
682
+ "bsearch": "bsearch(.item.target)", # Contains .item in bsearch - SINGLE
683
+ "while_loop": "while(.item.condition; .item.update)", # Contains .item in while - SINGLE
684
+ "until_loop": "until(.item.condition; .item.update)", # Contains .item in until - SINGLE
685
+ "recurse": "recurse(.item.children)", # Contains .item in recurse - SINGLE
686
+ "paths": "paths(.item.structure)", # Contains .item in paths - SINGLE
687
+ "leaf_paths": "leaf_paths(.item.tree)", # Contains .item in leaf_paths - SINGLE
688
+ "keys": "keys(.item.object)", # Contains .item in keys - SINGLE
689
+ "values": "values(.item.object)", # Contains .item in values - SINGLE
690
+ "to_entries": "to_entries(.item.object)", # Contains .item in to_entries - SINGLE
691
+ "from_entries": "from_entries(.item.array)", # Contains .item in from_entries - SINGLE
692
+ "with_entries": "with_entries(.item.transformation)", # Contains .item in with_entries - SINGLE
693
+ "del": "del(.item.field)", # Contains .item in del - SINGLE
694
+ "delpaths": "delpaths(.item.paths)", # Contains .item in delpaths - SINGLE
695
+ "walk": "walk(.item.transformation)", # Contains .item in walk - SINGLE
696
+ "limit": "limit(.item.count; .item.items)", # Contains .item in limit - SINGLE
697
+ "first": "first(.item.items)", # Contains .item in first - SINGLE
698
+ "last": "last(.item.items)", # Contains .item in last - SINGLE
699
+ "nth": "nth(.item.index; .item.items)", # Contains .item in nth - SINGLE
700
+ "input": "input(.item.stream)", # Contains .item in input - SINGLE
701
+ "inputs": "inputs(.item.streams)", # Contains .item in inputs - SINGLE
702
+ "foreach": "foreach(.item.items) as $item (0; . + $item.value)", # Contains .item in foreach - SINGLE
703
+ "explode": "explode(.item.string)", # Contains .item in explode - SINGLE
704
+ "implode": "implode(.item.codes)", # Contains .item in implode - SINGLE
705
+ "split": "split(.item.delimiter; .item.string)", # Contains .item in split - SINGLE
706
+ "join": "join(.item.delimiter; .item.array)", # Contains .item in join - SINGLE
707
+ "add": "add(.item.numbers)", # Contains .item in add - SINGLE
708
+ "has": "has(.item.key; .item.object)", # Contains .item in has - SINGLE
709
+ "in": "in(.item.value; .item.array)", # Contains .item in in - SINGLE
710
+ "index": "index(.item.value; .item.array)", # Contains .item in index - SINGLE
711
+ "indices": "indices(.item.value; .item.array)", # Contains .item in indices - SINGLE
712
+ "contains": "contains(.item.value; .item.array)", # Contains .item in contains - SINGLE
713
+ "startswith": "startswith(.item.prefix; .item.string)", # Contains .item in startswith - SINGLE
714
+ "endswith": "endswith(.item.suffix; .item.string)", # Contains .item in endswith - SINGLE
715
+ "ltrimstr": "ltrimstr(.item.prefix; .item.string)", # Contains .item in ltrimstr - SINGLE
716
+ "rtrimstr": "rtrimstr(.item.suffix; .item.string)", # Contains .item in rtrimstr - SINGLE
717
+ "sub": "sub(.item.pattern; .item.replacement; .item.string)", # Contains .item in sub - SINGLE
718
+ "gsub": "gsub(.item.pattern; .item.replacement; .item.string)", # Contains .item in gsub - SINGLE
719
+ "test": "test(.item.pattern; .item.string)", # Contains .item in test - SINGLE
720
+ "match": "match(.item.pattern; .item.string)", # Contains .item in match - SINGLE
721
+ "capture": "capture(.item.pattern; .item.string)", # Contains .item in capture - SINGLE
722
+ "scan": "scan(.item.pattern; .item.string)", # Contains .item in scan - SINGLE
723
+ "split_on": "split_on(.item.delimiter; .item.string)", # Contains .item in split_on - SINGLE
724
+ "join_on": "join_on(.item.delimiter; .item.array)", # Contains .item in join_on - SINGLE
725
+ "tonumber": "tonumber(.item.string)", # Contains .item in tonumber - SINGLE
726
+ "tostring": "tostring(.item.number)", # Contains .item in tostring - SINGLE
727
+ "type": "type(.item.value)", # Contains .item in type - SINGLE
728
+ "isnan": "isnan(.item.number)", # Contains .item in isnan - SINGLE
729
+ "isinfinite": "isinfinite(.item.number)", # Contains .item in isinfinite - SINGLE
730
+ "isfinite": "isfinite(.item.number)", # Contains .item in isfinite - SINGLE
731
+ "isnormal": "isnormal(.item.number)", # Contains .item in isnormal - SINGLE
732
+ "floor": "floor(.item.number)", # Contains .item in floor - SINGLE
733
+ "ceil": "ceil(.item.number)", # Contains .item in ceil - SINGLE
734
+ "round": "round(.item.number)", # Contains .item in round - SINGLE
735
+ "sqrt": "sqrt(.item.number)", # Contains .item in sqrt - SINGLE
736
+ "sin": "sin(.item.angle)", # Contains .item in sin - SINGLE
737
+ "cos": "cos(.item.angle)", # Contains .item in cos - SINGLE
738
+ "tan": "tan(.item.angle)", # Contains .item in tan - SINGLE
739
+ "asin": "asin(.item.value)", # Contains .item in asin - SINGLE
740
+ "acos": "acos(.item.value)", # Contains .item in acos - SINGLE
741
+ "atan": "atan(.item.value)", # Contains .item in atan - SINGLE
742
+ "atan2": "atan2(.item.y; .item.x)", # Contains .item in atan2 - SINGLE
743
+ "log": "log(.item.number)", # Contains .item in log - SINGLE
744
+ "log10": "log10(.item.number)", # Contains .item in log10 - SINGLE
745
+ "log2": "log2(.item.number)", # Contains .item in log2 - SINGLE
746
+ "exp": "exp(.item.number)", # Contains .item in exp - SINGLE
747
+ "exp10": "exp10(.item.number)", # Contains .item in exp10 - SINGLE
748
+ "exp2": "exp2(.item.number)", # Contains .item in exp2 - SINGLE
749
+ "pow": "pow(.item.base; .item.exponent)", # Contains .item in pow - SINGLE
750
+ "fma": "fma(.item.x; .item.y; .item.z)", # Contains .item in fma - SINGLE
751
+ "fmod": "fmod(.item.x; .item.y)", # Contains .item in fmod - SINGLE
752
+ "remainder": "remainder(.item.x; .item.y)", # Contains .item in remainder - SINGLE
753
+ "drem": "drem(.item.x; .item.y)", # Contains .item in drem - SINGLE
754
+ "fabs": "fabs(.item.number)", # Contains .item in fabs - SINGLE
755
+ "fmax": "fmax(.item.x; .item.y)", # Contains .item in fmax - SINGLE
756
+ "fmin": "fmin(.item.x; .item.y)", # Contains .item in fmin - SINGLE
757
+ "fdim": "fdim(.item.x; .item.y)", # Contains .item in fdim - SINGLE
758
+ # Expressions that don't contain .item (should go to ALL or NONE)
759
+ "external_map": "map(.external.field)", # Doesn't contain .item - ALL
760
+ "external_select": 'select(.external.status == "active")', # Doesn't contain .item - ALL
761
+ "external_array": "[.external.id, .external.name]", # Doesn't contain .item - ALL
762
+ "static_value": '"static"', # Static value - NONE
763
+ "nullary_expression": "null", # Nullary expression - NONE
764
+ "boolean_expression": "true", # Boolean expression - NONE
765
+ "number_expression": "42", # Number expression - NONE
766
+ "string_expression": '"hello"', # String expression - NONE
767
+ "array_expression": "[1,2,3]", # Array expression - NONE
768
+ "object_expression": '{"key": "value"}', # Object expression - NONE
769
+ },
770
+ "relations": {
771
+ "mapped_relation": "map(.item.relation)", # Contains .item - SINGLE
772
+ "selected_relation": 'select(.item.relation == "active")', # Contains .item - SINGLE
773
+ "external_relation": "map(.external.relation)", # Doesn't contain .item - ALL
774
+ "static_relation": '"static"', # Static value - NONE
775
+ },
776
+ }
777
+ items_to_parse_name = "item"
778
+
779
+ single, all_items, none = mocked_processor._build_raw_entity_mappings(
780
+ raw_entity_mappings, items_to_parse_name
781
+ )
782
+
783
+ # SINGLE mappings - all expressions that contain .item
784
+ expected_single = {
785
+ "identifier": ".item.id",
786
+ "title": ".item.name",
787
+ "blueprint": ".item.type",
788
+ "icon": ".item.icon",
789
+ "team": ".item.team",
790
+ "properties": {
791
+ "mapped_property": "map(.item.field)",
792
+ "selected_property": 'select(.item.status == "active")',
793
+ "filtered_property": '.[] | select(.item.type == "service")',
794
+ "array_literal": "[.item.id, .item.name]",
795
+ "object_literal": "{id: .item.id, name: .item.name}",
796
+ "nested_access": ".data.items[] | .item.field",
797
+ "conditional": "if .item.exists then .item.value else null end",
798
+ "function_call": "length(.item.array)",
799
+ "range_expression": "range(.item.start; .item.end)",
800
+ "reduce_expression": "reduce .item.items[] as $item (0; . + $item.value)",
801
+ "group_by": "group_by(.item.category)",
802
+ "sort_by": "sort_by(.item.priority)",
803
+ "unique_by": "unique_by(.item.id)",
804
+ "flatten": "flatten(.item.nested)",
805
+ "transpose": "transpose(.item.matrix)",
806
+ "combinations": "combinations(.item.items)",
807
+ "permutations": "permutations(.item.items)",
808
+ "bsearch": "bsearch(.item.target)",
809
+ "while_loop": "while(.item.condition; .item.update)",
810
+ "until_loop": "until(.item.condition; .item.update)",
811
+ "recurse": "recurse(.item.children)",
812
+ "paths": "paths(.item.structure)",
813
+ "leaf_paths": "leaf_paths(.item.tree)",
814
+ "keys": "keys(.item.object)",
815
+ "values": "values(.item.object)",
816
+ "to_entries": "to_entries(.item.object)",
817
+ "from_entries": "from_entries(.item.array)",
818
+ "with_entries": "with_entries(.item.transformation)",
819
+ "del": "del(.item.field)",
820
+ "delpaths": "delpaths(.item.paths)",
821
+ "walk": "walk(.item.transformation)",
822
+ "limit": "limit(.item.count; .item.items)",
823
+ "first": "first(.item.items)",
824
+ "last": "last(.item.items)",
825
+ "nth": "nth(.item.index; .item.items)",
826
+ "input": "input(.item.stream)",
827
+ "inputs": "inputs(.item.streams)",
828
+ "foreach": "foreach(.item.items) as $item (0; . + $item.value)",
829
+ "explode": "explode(.item.string)",
830
+ "implode": "implode(.item.codes)",
831
+ "split": "split(.item.delimiter; .item.string)",
832
+ "join": "join(.item.delimiter; .item.array)",
833
+ "add": "add(.item.numbers)",
834
+ "has": "has(.item.key; .item.object)",
835
+ "in": "in(.item.value; .item.array)",
836
+ "index": "index(.item.value; .item.array)",
837
+ "indices": "indices(.item.value; .item.array)",
838
+ "contains": "contains(.item.value; .item.array)",
839
+ "startswith": "startswith(.item.prefix; .item.string)",
840
+ "endswith": "endswith(.item.suffix; .item.string)",
841
+ "ltrimstr": "ltrimstr(.item.prefix; .item.string)",
842
+ "rtrimstr": "rtrimstr(.item.suffix; .item.string)",
843
+ "sub": "sub(.item.pattern; .item.replacement; .item.string)",
844
+ "gsub": "gsub(.item.pattern; .item.replacement; .item.string)",
845
+ "test": "test(.item.pattern; .item.string)",
846
+ "match": "match(.item.pattern; .item.string)",
847
+ "capture": "capture(.item.pattern; .item.string)",
848
+ "scan": "scan(.item.pattern; .item.string)",
849
+ "split_on": "split_on(.item.delimiter; .item.string)",
850
+ "join_on": "join_on(.item.delimiter; .item.array)",
851
+ "tonumber": "tonumber(.item.string)",
852
+ "tostring": "tostring(.item.number)",
853
+ "type": "type(.item.value)",
854
+ "isnan": "isnan(.item.number)",
855
+ "isinfinite": "isinfinite(.item.number)",
856
+ "isfinite": "isfinite(.item.number)",
857
+ "isnormal": "isnormal(.item.number)",
858
+ "floor": "floor(.item.number)",
859
+ "ceil": "ceil(.item.number)",
860
+ "round": "round(.item.number)",
861
+ "sqrt": "sqrt(.item.number)",
862
+ "sin": "sin(.item.angle)",
863
+ "cos": "cos(.item.angle)",
864
+ "tan": "tan(.item.angle)",
865
+ "asin": "asin(.item.value)",
866
+ "acos": "acos(.item.value)",
867
+ "atan": "atan(.item.value)",
868
+ "atan2": "atan2(.item.y; .item.x)",
869
+ "log": "log(.item.number)",
870
+ "log10": "log10(.item.number)",
871
+ "log2": "log2(.item.number)",
872
+ "exp": "exp(.item.number)",
873
+ "exp10": "exp10(.item.number)",
874
+ "exp2": "exp2(.item.number)",
875
+ "pow": "pow(.item.base; .item.exponent)",
876
+ "fma": "fma(.item.x; .item.y; .item.z)",
877
+ "fmod": "fmod(.item.x; .item.y)",
878
+ "remainder": "remainder(.item.x; .item.y)",
879
+ "drem": "drem(.item.x; .item.y)",
880
+ "fabs": "fabs(.item.number)",
881
+ "fmax": "fmax(.item.x; .item.y)",
882
+ "fmin": "fmin(.item.x; .item.y)",
883
+ "fdim": "fdim(.item.x; .item.y)",
884
+ },
885
+ "relations": {
886
+ "mapped_relation": "map(.item.relation)",
887
+ "selected_relation": 'select(.item.relation == "active")',
888
+ },
889
+ }
890
+ assert single == expected_single
891
+
892
+ # ALL mappings - expressions with dots but not containing .item
893
+ expected_all = {
894
+ "properties": {
895
+ "external_map": "map(.external.field)",
896
+ "external_select": 'select(.external.status == "active")',
897
+ "external_array": "[.external.id, .external.name]",
898
+ },
899
+ "relations": {
900
+ "external_relation": "map(.external.relation)",
901
+ },
902
+ }
903
+ assert all_items == expected_all
904
+
905
+ # NONE mappings - nullary expressions and static values
906
+ expected_none = {
907
+ "properties": {
908
+ "static_value": '"static"',
909
+ "nullary_expression": "null",
910
+ "boolean_expression": "true",
911
+ "number_expression": "42",
912
+ "string_expression": '"hello"',
913
+ "array_expression": "[1,2,3]",
914
+ "object_expression": '{"key": "value"}',
915
+ },
916
+ "relations": {
917
+ "static_relation": '"static"',
918
+ },
919
+ }
920
+ assert none == expected_none
921
+
922
+ async def test_group_complex_mapping_value_relations(
923
+ self, mocked_processor: JQEntityProcessor
924
+ ) -> None:
925
+ """Test group_complex_mapping_value with relations dictionary"""
926
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
927
+ InputClassifyingResult,
928
+ )
929
+
930
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
931
+ InputClassifyingResult.SINGLE: {},
932
+ InputClassifyingResult.ALL: {},
933
+ InputClassifyingResult.NONE: {},
934
+ }
935
+
936
+ # Test relations with mixed string and IngestSearchQuery values
937
+ relations = {
938
+ "owner": ".item.owner", # String - SINGLE
939
+ "parent": { # IngestSearchQuery - SINGLE
940
+ "combinator": "and",
941
+ "rules": [
942
+ {
943
+ "property": "parent",
944
+ "operator": "equals",
945
+ "value": ".item.parent",
946
+ }
947
+ ],
948
+ },
949
+ "external_relation": { # IngestSearchQuery - ALL
950
+ "combinator": "and",
951
+ "rules": [
952
+ {
953
+ "property": "external",
954
+ "operator": "equals",
955
+ "value": ".external.ref",
956
+ }
957
+ ],
958
+ },
959
+ "static_relation": '"static"', # String - NONE
960
+ }
961
+
962
+ mocked_processor.group_complex_mapping_value(
963
+ "item", mappings, "relations", relations
964
+ )
965
+
966
+ expected_single = {
967
+ "owner": ".item.owner",
968
+ "parent": {
969
+ "combinator": "and",
970
+ "rules": [
971
+ {
972
+ "property": "parent",
973
+ "operator": "equals",
974
+ "value": ".item.parent",
975
+ }
976
+ ],
977
+ },
978
+ }
979
+ expected_all = {
980
+ "external_relation": {
981
+ "combinator": "and",
982
+ "rules": [
983
+ {
984
+ "property": "external",
985
+ "operator": "equals",
986
+ "value": ".external.ref",
987
+ }
988
+ ],
989
+ }
990
+ }
991
+ expected_none = {
992
+ "static_relation": '"static"',
993
+ }
994
+
995
+ assert mappings[InputClassifyingResult.SINGLE]["relations"] == expected_single
996
+ assert mappings[InputClassifyingResult.ALL]["relations"] == expected_all
997
+ assert mappings[InputClassifyingResult.NONE]["relations"] == expected_none
998
+
999
+ async def test_group_complex_mapping_value_identifier_ingest_search_query(
1000
+ self, mocked_processor: JQEntityProcessor
1001
+ ) -> None:
1002
+ """Test group_complex_mapping_value with identifier IngestSearchQuery"""
1003
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
1004
+ InputClassifyingResult,
1005
+ )
1006
+
1007
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
1008
+ InputClassifyingResult.SINGLE: {},
1009
+ InputClassifyingResult.ALL: {},
1010
+ InputClassifyingResult.NONE: {},
1011
+ }
1012
+
1013
+ # Test identifier IngestSearchQuery that matches pattern
1014
+ identifier_query = {
1015
+ "combinator": "and",
1016
+ "rules": [{"property": "id", "operator": "equals", "value": ".item.id"}],
1017
+ }
1018
+
1019
+ mocked_processor.group_complex_mapping_value(
1020
+ "item", mappings, "identifier", identifier_query
1021
+ )
1022
+
1023
+ expected_single = {
1024
+ "combinator": "and",
1025
+ "rules": [{"property": "id", "operator": "equals", "value": ".item.id"}],
1026
+ }
1027
+
1028
+ assert mappings[InputClassifyingResult.SINGLE]["identifier"] == expected_single
1029
+ assert (
1030
+ InputClassifyingResult.ALL not in mappings
1031
+ or not mappings[InputClassifyingResult.ALL]
1032
+ )
1033
+ assert (
1034
+ InputClassifyingResult.NONE not in mappings
1035
+ or not mappings[InputClassifyingResult.NONE]
1036
+ )
1037
+
1038
+ async def test_group_complex_mapping_value_team_ingest_search_query(
1039
+ self, mocked_processor: JQEntityProcessor
1040
+ ) -> None:
1041
+ """Test group_complex_mapping_value with team IngestSearchQuery"""
1042
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
1043
+ InputClassifyingResult,
1044
+ )
1045
+
1046
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
1047
+ InputClassifyingResult.SINGLE: {},
1048
+ InputClassifyingResult.ALL: {},
1049
+ InputClassifyingResult.NONE: {},
1050
+ }
1051
+
1052
+ # Test team IngestSearchQuery that doesn't match pattern
1053
+ team_query = {
1054
+ "combinator": "and",
1055
+ "rules": [
1056
+ {"property": "team", "operator": "equals", "value": ".data.team"}
1057
+ ],
1058
+ }
1059
+
1060
+ mocked_processor.group_complex_mapping_value(
1061
+ "item", mappings, "team", team_query
1062
+ )
1063
+
1064
+ expected_all = {
1065
+ "combinator": "and",
1066
+ "rules": [
1067
+ {"property": "team", "operator": "equals", "value": ".data.team"}
1068
+ ],
1069
+ }
1070
+
1071
+ assert mappings[InputClassifyingResult.ALL]["team"] == expected_all
1072
+ assert (
1073
+ InputClassifyingResult.SINGLE not in mappings
1074
+ or not mappings[InputClassifyingResult.SINGLE]
1075
+ )
1076
+ assert (
1077
+ InputClassifyingResult.NONE not in mappings
1078
+ or not mappings[InputClassifyingResult.NONE]
1079
+ )
1080
+
1081
+ async def test_group_complex_mapping_value_nested_ingest_search_query(
1082
+ self, mocked_processor: JQEntityProcessor
1083
+ ) -> None:
1084
+ """Test group_complex_mapping_value with nested IngestSearchQuery"""
1085
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
1086
+ InputClassifyingResult,
1087
+ )
1088
+
1089
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
1090
+ InputClassifyingResult.SINGLE: {},
1091
+ InputClassifyingResult.ALL: {},
1092
+ InputClassifyingResult.NONE: {},
1093
+ }
1094
+
1095
+ # Test nested IngestSearchQuery with mixed rules
1096
+ nested_query = {
1097
+ "combinator": "and",
1098
+ "rules": [
1099
+ {
1100
+ "property": "field",
1101
+ "operator": "equals",
1102
+ "value": ".item.field", # SINGLE - contains pattern
1103
+ },
1104
+ {
1105
+ "combinator": "or",
1106
+ "rules": [
1107
+ {
1108
+ "property": "external",
1109
+ "operator": "equals",
1110
+ "value": ".external.ref", # ALL - doesn't contain pattern
1111
+ }
1112
+ ],
1113
+ },
1114
+ ],
1115
+ }
1116
+
1117
+ mocked_processor.group_complex_mapping_value(
1118
+ "item", mappings, "identifier", nested_query
1119
+ )
1120
+
1121
+ # Should go to SINGLE because it contains at least one rule with the pattern
1122
+ expected_single = {
1123
+ "combinator": "and",
1124
+ "rules": [
1125
+ {"property": "field", "operator": "equals", "value": ".item.field"},
1126
+ {
1127
+ "combinator": "or",
1128
+ "rules": [
1129
+ {
1130
+ "property": "external",
1131
+ "operator": "equals",
1132
+ "value": ".external.ref",
1133
+ }
1134
+ ],
1135
+ },
1136
+ ],
1137
+ }
1138
+
1139
+ assert mappings[InputClassifyingResult.SINGLE]["identifier"] == expected_single
1140
+ assert (
1141
+ InputClassifyingResult.ALL not in mappings
1142
+ or not mappings[InputClassifyingResult.ALL]
1143
+ )
1144
+ assert (
1145
+ InputClassifyingResult.NONE not in mappings
1146
+ or not mappings[InputClassifyingResult.NONE]
1147
+ )
1148
+
1149
+ async def test_group_complex_mapping_value_invalid_ingest_search_query(
1150
+ self, mocked_processor: JQEntityProcessor
1151
+ ) -> None:
1152
+ """Test group_complex_mapping_value with invalid IngestSearchQuery structures"""
1153
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
1154
+ InputClassifyingResult,
1155
+ )
1156
+
1157
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
1158
+ InputClassifyingResult.SINGLE: {},
1159
+ InputClassifyingResult.ALL: {},
1160
+ InputClassifyingResult.NONE: {},
1161
+ }
1162
+
1163
+ # Test invalid IngestSearchQuery (no rules field)
1164
+ invalid_query = {
1165
+ "combinator": "and"
1166
+ # Missing rules field
1167
+ }
1168
+
1169
+ mocked_processor.group_complex_mapping_value(
1170
+ ".item", mappings, "identifier", invalid_query
1171
+ )
1172
+
1173
+ # Should go to ALL since it doesn't match the pattern
1174
+ expected_all = {"combinator": "and"}
1175
+
1176
+ assert mappings[InputClassifyingResult.ALL]["identifier"] == expected_all
1177
+ assert (
1178
+ InputClassifyingResult.SINGLE not in mappings
1179
+ or not mappings[InputClassifyingResult.SINGLE]
1180
+ )
1181
+ assert (
1182
+ InputClassifyingResult.NONE not in mappings
1183
+ or not mappings[InputClassifyingResult.NONE]
1184
+ )
1185
+
1186
+ async def test_group_complex_mapping_value_empty_dict(
1187
+ self, mocked_processor: JQEntityProcessor
1188
+ ) -> None:
1189
+ """Test group_complex_mapping_value with empty dictionary"""
1190
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
1191
+ InputClassifyingResult,
1192
+ )
1193
+
1194
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
1195
+ InputClassifyingResult.SINGLE: {},
1196
+ InputClassifyingResult.ALL: {},
1197
+ InputClassifyingResult.NONE: {},
1198
+ }
1199
+
1200
+ # Test empty properties dictionary
1201
+ empty_properties: dict[str, Any] = {}
1202
+
1203
+ mocked_processor.group_complex_mapping_value(
1204
+ ".item", mappings, "properties", empty_properties
1205
+ )
1206
+
1207
+ # Should not add anything to any mapping
1208
+ assert (
1209
+ InputClassifyingResult.SINGLE not in mappings
1210
+ or not mappings[InputClassifyingResult.SINGLE]
1211
+ )
1212
+ assert (
1213
+ InputClassifyingResult.ALL not in mappings
1214
+ or not mappings[InputClassifyingResult.ALL]
1215
+ )
1216
+ assert (
1217
+ InputClassifyingResult.NONE not in mappings
1218
+ or not mappings[InputClassifyingResult.NONE]
1219
+ )
1220
+
1221
+ async def test_group_complex_mapping_value_mixed_content(
1222
+ self, mocked_processor: JQEntityProcessor
1223
+ ) -> None:
1224
+ """Test group_complex_mapping_value with mixed string and IngestSearchQuery content"""
1225
+ from port_ocean.core.handlers.entity_processor.jq_input_evaluator import (
1226
+ InputClassifyingResult,
1227
+ )
1228
+
1229
+ mappings: dict[InputClassifyingResult, dict[str, Any]] = {
1230
+ InputClassifyingResult.SINGLE: {},
1231
+ InputClassifyingResult.ALL: {},
1232
+ InputClassifyingResult.NONE: {},
1233
+ }
1234
+
1235
+ # Test properties with mixed content
1236
+ mixed_properties = {
1237
+ "string_single": ".item.name", # String - SINGLE
1238
+ "string_all": ".external.ref", # String - ALL
1239
+ "string_none": '"static"', # String - NONE
1240
+ "query_single": { # IngestSearchQuery - SINGLE
1241
+ "combinator": "and",
1242
+ "rules": [
1243
+ {"property": "field", "operator": "equals", "value": ".item.field"}
1244
+ ],
1245
+ },
1246
+ "query_all": { # IngestSearchQuery - ALL
1247
+ "combinator": "and",
1248
+ "rules": [
1249
+ {
1250
+ "property": "external",
1251
+ "operator": "equals",
1252
+ "value": ".external.field",
1253
+ }
1254
+ ],
1255
+ },
1256
+ }
1257
+
1258
+ mocked_processor.group_complex_mapping_value(
1259
+ "item", mappings, "properties", mixed_properties
1260
+ )
1261
+
1262
+ expected_single = {
1263
+ "string_single": ".item.name",
1264
+ "query_single": {
1265
+ "combinator": "and",
1266
+ "rules": [
1267
+ {"property": "field", "operator": "equals", "value": ".item.field"}
1268
+ ],
1269
+ },
1270
+ }
1271
+ expected_all = {
1272
+ "string_all": ".external.ref",
1273
+ "query_all": {
1274
+ "combinator": "and",
1275
+ "rules": [
1276
+ {
1277
+ "property": "external",
1278
+ "operator": "equals",
1279
+ "value": ".external.field",
1280
+ }
1281
+ ],
1282
+ },
1283
+ }
1284
+ expected_none = {
1285
+ "string_none": '"static"',
1286
+ }
1287
+
1288
+ assert mappings[InputClassifyingResult.SINGLE]["properties"] == expected_single
1289
+ assert mappings[InputClassifyingResult.ALL]["properties"] == expected_all
1290
+ assert mappings[InputClassifyingResult.NONE]["properties"] == expected_none