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