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.
- {ransacklib-1.1.0.dev4/ransacklib.egg-info → ransacklib-1.1.0.dev5}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/pyproject.toml +2 -1
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/transformer.py +16 -11
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5/ransacklib.egg-info}/PKG-INFO +1 -1
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/tests/test_transformer.py +182 -2
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/LICENSE +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/README.rst +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/__init__.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/exceptions.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/function.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/operator.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/parser.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransack/py.typed +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/SOURCES.txt +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/dependency_links.txt +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/requires.txt +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/ransacklib.egg-info/top_level.txt +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/setup.cfg +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/tests/test_operator.py +0 -0
- {ransacklib-1.1.0.dev4 → ransacklib-1.1.0.dev5}/tests/test_parser.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ransacklib"
|
|
7
|
-
version = "1.1.0.
|
|
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
|
|
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
|
|
835
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|