ransacklib 1.1.0.dev4__tar.gz → 1.1.0.dev5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {ransacklib-1.1.0.dev4/ransacklib.egg-info → ransacklib-1.1.0.dev5}/PKG-INFO +1 -1
  2. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/pyproject.toml +2 -1
  3. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/transformer.py +16 -11
  4. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5/ransacklib.egg-info}/PKG-INFO +1 -1
  5. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/tests/test_transformer.py +182 -2
  6. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/LICENSE +0 -0
  7. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/README.rst +0 -0
  8. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/__init__.py +0 -0
  9. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/exceptions.py +0 -0
  10. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/function.py +0 -0
  11. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/operator.py +0 -0
  12. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/parser.py +0 -0
  13. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/py.typed +0 -0
  14. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/SOURCES.txt +0 -0
  15. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/dependency_links.txt +0 -0
  16. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/requires.txt +0 -0
  17. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/top_level.txt +0 -0
  18. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/setup.cfg +0 -0
  19. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/tests/test_operator.py +0 -0
  20. {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/tests/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ransacklib
3
- Version: 1.1.0.dev4
3
+ Version: 1.1.0.dev5
4
4
  Summary: A modern, extensible language for manipulation with structured data
5
5
  Author-email: "Rajmund H. Hruška" <rajmund.hruska@cesnet.cz>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ransacklib"
7
- version = "1.1.0.dev4"
7
+ version = "1.1.0.dev5"
8
8
  description = "A modern, extensible language for manipulation with structured data"
9
9
  license = "MIT"
10
10
  readme = "README.rst"
@@ -83,4 +83,5 @@ select = [
83
83
 
84
84
  ignore = [
85
85
  "PLR0913", # too-many-arguments
86
+ "DTZ001", # call-datetime-without-tzinfo
86
87
  ]
@@ -828,19 +828,23 @@ class SQLInterpreter(Interpreter):
828
828
  self.data = {}
829
829
  return result, params
830
830
 
831
- def number(self, token: TokenWrapper) -> tuple[str, list[Any], type[int | float]]:
831
+ def number(self, token: TokenWrapper) -> tuple[str, list, type[int | float]]:
832
832
  return ("%s", [token.real_value], type(token.real_value))
833
833
 
834
- def datetime(self, token: TokenWrapper) -> tuple[str, list[Any], type[datetime]]:
835
- return ("%s", [token.real_value], type(token.real_value))
834
+ def datetime(self, token: TokenWrapper) -> tuple[str, list, type[datetime]]:
835
+ if token.real_value.tzinfo is not None:
836
+ cast = "TIMESTAMP WITH TIME ZONE"
837
+ else:
838
+ cast = "TIMESTAMP"
839
+ return (f"%s::{cast}", [token.real_value], type(token.real_value))
836
840
 
837
- def timedelta_(self, token: TokenWrapper) -> tuple[str, list[Any], type[timedelta]]:
841
+ def timedelta_(self, token: TokenWrapper) -> tuple[str, list, type[timedelta]]:
838
842
  return ("%s", [token.real_value], type(token.real_value))
839
843
 
840
- def string_(self, token: TokenWrapper) -> tuple[str, list[Any], type[str]]:
844
+ def string_(self, token: TokenWrapper) -> tuple[str, list, type[str]]:
841
845
  return ("%s", [token.real_value], type(token.real_value))
842
846
 
843
- def ip(self, token: TokenWrapper) -> tuple[str, list[Any], type[IP]]:
847
+ def ip(self, token: TokenWrapper) -> tuple[str, list, type[IP]]:
844
848
  match token.real_value:
845
849
  case IP4() | IP6():
846
850
  cast = "ipaddress"
@@ -858,8 +862,6 @@ class SQLInterpreter(Interpreter):
858
862
  l_sql, l_params, l_type = self.visit(l_tree)
859
863
  r_sql, r_params, r_type = self.visit(r_tree)
860
864
 
861
- params = l_params + r_params
862
-
863
865
  if issubclass(l_type, int) and issubclass(r_type, int):
864
866
  range_type = "int4range"
865
867
  elif issubclass(l_type, float) and issubclass(r_type, float):
@@ -872,7 +874,7 @@ class SQLInterpreter(Interpreter):
872
874
  elif issubclass(l_type, (IP4, IP6)) and issubclass(r_type, (IP4, IP6)):
873
875
  return (
874
876
  f"iprange({l_sql}, {r_sql})",
875
- params,
877
+ l_params + r_params,
876
878
  IP4Range if issubclass(l_type, IP4) else IP6Range,
877
879
  )
878
880
  else:
@@ -892,7 +894,7 @@ class SQLInterpreter(Interpreter):
892
894
  f"THEN {range_type}({l_sql}, {r_sql}, '[]') "
893
895
  f"ELSE {range_type}({r_sql}, {l_sql}, '[]') END"
894
896
  ),
895
- params,
897
+ l_params + r_params + l_params + r_params + r_params + l_params,
896
898
  tuple,
897
899
  )
898
900
 
@@ -1004,7 +1006,7 @@ class SQLInterpreter(Interpreter):
1004
1006
  )
1005
1007
 
1006
1008
  @v_args(tree=True)
1007
- def list(self, data):
1009
+ def list(self, data: Tree) -> tuple[str, list, Any]:
1008
1010
  parts = []
1009
1011
  params = []
1010
1012
 
@@ -1015,4 +1017,7 @@ class SQLInterpreter(Interpreter):
1015
1017
  parts.append(sql)
1016
1018
  params.extend(p)
1017
1019
 
1020
+ if not parts:
1021
+ return "'{}'", [], list
1022
+
1018
1023
  return f"ARRAY[{', '.join(parts)}]", params, list
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ransacklib
3
- Version: 1.1.0.dev4
3
+ Version: 1.1.0.dev5
4
4
  Summary: A modern, extensible language for manipulation with structured data
5
5
  Author-email: "Rajmund H. Hruška" <rajmund.hruska@cesnet.cz>
6
6
  License-Expression: MIT
@@ -1,7 +1,7 @@
1
- from datetime import datetime, timedelta
1
+ from datetime import datetime, timedelta, timezone
2
2
 
3
3
  import pytest
4
- from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range
4
+ from ipranges import IP4, IP6, IP4Net, IP4Range, IP6Net, IP6Range, from_str
5
5
  from lark import Token
6
6
 
7
7
  from ransack.exceptions import RansackError, ShapeError
@@ -9,6 +9,7 @@ from ransack.parser import Parser
9
9
  from ransack.transformer import (
10
10
  ExpressionTransformer,
11
11
  Filter,
12
+ SQLInterpreter,
12
13
  TokenWrapper,
13
14
  get_values,
14
15
  )
@@ -29,6 +30,11 @@ def expr_transformer():
29
30
  return ExpressionTransformer()
30
31
 
31
32
 
33
+ @pytest.fixture
34
+ def sql():
35
+ return SQLInterpreter()
36
+
37
+
32
38
  sample_data = {
33
39
  "Category": ["Intrusion.UserCompromise"],
34
40
  "ConnCount": 1,
@@ -505,3 +511,177 @@ class TestFilter:
505
511
  exp_tree = expr_transformer.transform(parser.parse_only("my_var == 14"))
506
512
  res = filter_.transform(exp_tree)
507
513
  assert res
514
+
515
+
516
+ class TestSQLInterpreter:
517
+ def parse(self, parser, sql, expression):
518
+ tree = parser.parse(expression)
519
+ data = {
520
+ "a_int": ("a", int),
521
+ "b_int": ("b", int),
522
+ "c_int": ("c", int),
523
+ "d_int": ("d", int),
524
+ "Description": ("description", str),
525
+ }
526
+ return sql.to_sql(tree, data)
527
+
528
+ @pytest.mark.parametrize(
529
+ ("expr", "expected_sql", "expected_params"),
530
+ [
531
+ # Numbers.
532
+ ("5", "%s", [5]),
533
+ ("-12", "-%s", [12]),
534
+ ("23.22", "%s", [23.22]),
535
+ ("-0.224", "-%s", [0.224]),
536
+ ("13e19", "%s", [1.3e20]),
537
+ # Datetime.
538
+ ("2025-01-01", "%s::TIMESTAMP", [datetime(2025, 1, 1, 0, 0)]),
539
+ (
540
+ "2025-01-01T22:14:24.142+02:00",
541
+ "%s::TIMESTAMP WITH TIME ZONE",
542
+ [
543
+ datetime(
544
+ 2025,
545
+ 1,
546
+ 1,
547
+ 22,
548
+ 14,
549
+ 24,
550
+ 142000,
551
+ tzinfo=timezone(timedelta(seconds=7200)),
552
+ )
553
+ ],
554
+ ),
555
+ # Timedelta.
556
+ ("01:23:34", "%s", [timedelta(seconds=5014)]),
557
+ ("-10D23:59:59", "-%s", [timedelta(days=10, seconds=86399)]),
558
+ ("99:59:59", "%s", [timedelta(days=4, seconds=14399)]),
559
+ # Strings.
560
+ ("''", "%s", [""]),
561
+ ('"string"', "%s", ["string"]),
562
+ ('"\';DROP TABLE USERS"', "%s", ["';DROP TABLE USERS"]),
563
+ # IPs.
564
+ ("192.168.0.0", "%s::ipaddress", [from_str("192.168.0.0")]),
565
+ ("192.168.0.0/16", "%s::iprange", [from_str("192.168.0.0/16")]),
566
+ (
567
+ "192.168.0.10-192.168.0.50",
568
+ "%s::iprange",
569
+ [from_str("192.168.0.10-192.168.0.50")],
570
+ ),
571
+ ("fd00::1", "%s::ipaddress", [from_str("fd00::1")]),
572
+ ("fd00::/8", "%s::iprange", [from_str("fd00::/8")]),
573
+ ("fd00::1-fd00::10", "%s::iprange", [from_str("fd00::1-fd00::10")]),
574
+ # List.
575
+ ("[1, 2, 3]", "ARRAY[%s, %s, %s]", [1, 2, 3]),
576
+ ("[01:23:34]", "ARRAY[%s]", [timedelta(seconds=5014)]),
577
+ ("[]", "'{}'", []),
578
+ # Ranges.
579
+ (
580
+ "100..1",
581
+ (
582
+ "CASE WHEN %s < %s "
583
+ "THEN int4range(%s, %s, '[]') "
584
+ "ELSE int4range(%s, %s, '[]') END"
585
+ ),
586
+ [100, 1, 100, 1, 1, 100],
587
+ ),
588
+ (
589
+ "100.12..1.14",
590
+ (
591
+ "CASE WHEN %s < %s "
592
+ "THEN numrange(%s, %s, '[]') "
593
+ "ELSE numrange(%s, %s, '[]') END"
594
+ ),
595
+ [100.12, 1.14, 100.12, 1.14, 1.14, 100.12],
596
+ ),
597
+ (
598
+ "2024-01-01T00:00:00+00:00..2024-12-31T00:00:00Z",
599
+ (
600
+ "CASE WHEN %s::TIMESTAMP WITH TIME ZONE "
601
+ "< %s::TIMESTAMP WITH TIME ZONE THEN "
602
+ "tstzrange(%s::TIMESTAMP WITH TIME ZONE, "
603
+ "%s::TIMESTAMP WITH TIME ZONE, '[]') ELSE "
604
+ "tstzrange(%s::TIMESTAMP WITH TIME ZONE, "
605
+ "%s::TIMESTAMP WITH TIME ZONE, '[]') END"
606
+ ),
607
+ [
608
+ datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc),
609
+ datetime(2024, 12, 31, 0, 0, tzinfo=timezone.utc),
610
+ datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc),
611
+ datetime(2024, 12, 31, 0, 0, tzinfo=timezone.utc),
612
+ datetime(2024, 12, 31, 0, 0, tzinfo=timezone.utc),
613
+ datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc),
614
+ ],
615
+ ),
616
+ (
617
+ "2024-01-01T00:00:00..2024-12-31T00:00:00",
618
+ (
619
+ "CASE WHEN %s::TIMESTAMP "
620
+ "< %s::TIMESTAMP THEN "
621
+ "tsrange(%s::TIMESTAMP, "
622
+ "%s::TIMESTAMP, '[]') ELSE "
623
+ "tsrange(%s::TIMESTAMP, "
624
+ "%s::TIMESTAMP, '[]') END"
625
+ ),
626
+ [
627
+ datetime(
628
+ 2024,
629
+ 1,
630
+ 1,
631
+ 0,
632
+ 0,
633
+ ),
634
+ datetime(2024, 12, 31, 0, 0),
635
+ datetime(2024, 1, 1, 0, 0),
636
+ datetime(2024, 12, 31, 0, 0),
637
+ datetime(2024, 12, 31, 0, 0),
638
+ datetime(2024, 1, 1, 0, 0),
639
+ ],
640
+ ),
641
+ (
642
+ "192.168.0.10..192.168.0.50",
643
+ "iprange(%s::ipaddress, %s::ipaddress)",
644
+ [from_str("192.168.0.10"), from_str("192.168.0.50")],
645
+ ),
646
+ (
647
+ "fd00::1..fd00::10",
648
+ "iprange(%s::ipaddress, %s::ipaddress)",
649
+ [from_str("fd00::1"), from_str("fd00::10")],
650
+ ),
651
+ # Test variable access.
652
+ ("a_int", '"a"', []),
653
+ (
654
+ "a_int .. b_int",
655
+ (
656
+ 'CASE WHEN "a" < "b" '
657
+ 'THEN int4range("a", "b", \'[]\') '
658
+ 'ELSE int4range("b", "a", \'[]\') END'
659
+ ),
660
+ [],
661
+ ),
662
+ ],
663
+ )
664
+ def test_parse_sql(self, parser, sql, expr, expected_sql, expected_params):
665
+ sql_str, params = self.parse(parser, sql, expr)
666
+
667
+ assert sql_str == expected_sql
668
+ assert params == expected_params
669
+
670
+ def to_sql_and_expect_error(self, parser, sql, expression):
671
+ tree = parser.parse(expression)
672
+ with pytest.raises(RansackError):
673
+ sql.to_sql(tree)
674
+
675
+ @pytest.mark.parametrize(
676
+ "expression",
677
+ [
678
+ "2000-12-24..192.168.0.50", # Unsupported type of range.
679
+ "not_there", # Unknown column.
680
+ ],
681
+ )
682
+ def test_invalid_cases(self, parser, sql, expression):
683
+ self.to_sql_and_expect_error(
684
+ parser,
685
+ sql,
686
+ expression,
687
+ )
File without changes