tinyflux 1.2.1__tar.gz → 1.3.0__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.
- {tinyflux-1.2.1/tinyflux.egg-info → tinyflux-1.3.0}/PKG-INFO +1 -1
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/test_measurement.py +2 -2
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/test_point.py +112 -5
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/test_queries.py +78 -2
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/test_storages.py +113 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/test_tinyflux.py +159 -2
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/database.py +46 -7
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/index.py +1 -1
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/point.py +79 -9
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/queries.py +54 -8
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/storages.py +3 -1
- tinyflux-1.3.0/tinyflux/version.py +3 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0/tinyflux.egg-info}/PKG-INFO +1 -1
- tinyflux-1.2.1/tinyflux/version.py +0 -3
- {tinyflux-1.2.1 → tinyflux-1.3.0}/LICENSE +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/MANIFEST.in +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/README.md +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/pyproject.toml +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/setup.cfg +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/__init__.py +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/conftest.py +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/test_index.py +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tests/test_utils.py +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/__init__.py +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/decorators.py +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/measurement.py +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/py.typed +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux/utils.py +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux.egg-info/SOURCES.txt +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux.egg-info/dependency_links.txt +0 -0
- {tinyflux-1.2.1 → tinyflux-1.3.0}/tinyflux.egg-info/top_level.txt +0 -0
|
@@ -918,9 +918,9 @@ def test_update():
|
|
|
918
918
|
m1.update(TagQuery().noop(), tags={"a": 1})
|
|
919
919
|
|
|
920
920
|
with pytest.raises(
|
|
921
|
-
ValueError, match="Field set must contain only numeric
|
|
921
|
+
ValueError, match="Field set must contain only numeric, string, or bool"
|
|
922
922
|
):
|
|
923
|
-
m1.update(TagQuery().noop(), fields={"a": "a"})
|
|
923
|
+
m1.update(TagQuery().noop(), fields={"a": ["a"]})
|
|
924
924
|
|
|
925
925
|
# Valid index, no index results.
|
|
926
926
|
db.reindex()
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timezone, timedelta
|
|
4
4
|
import pytest
|
|
5
|
-
from tinyflux.point import
|
|
5
|
+
from tinyflux.point import (
|
|
6
|
+
Point,
|
|
7
|
+
_serialize_field_value,
|
|
8
|
+
validate_tags,
|
|
9
|
+
validate_fields,
|
|
10
|
+
)
|
|
6
11
|
|
|
7
12
|
|
|
8
13
|
@pytest.fixture
|
|
@@ -100,8 +105,13 @@ def test_args_and_kwargs_invalid(args, kwargs, exception_type):
|
|
|
100
105
|
),
|
|
101
106
|
(
|
|
102
107
|
validate_fields,
|
|
103
|
-
[3, {1: "A"}, {"a": "A"}],
|
|
104
|
-
[
|
|
108
|
+
[3, {1: "A"}, {"a": ["A"]}, {"a": b"A"}],
|
|
109
|
+
[
|
|
110
|
+
{"a": 1},
|
|
111
|
+
{"temp": 70.5},
|
|
112
|
+
{"count": 10, "avg": 25.3},
|
|
113
|
+
{"s": "A", "b": True, "n": None},
|
|
114
|
+
],
|
|
105
115
|
),
|
|
106
116
|
],
|
|
107
117
|
)
|
|
@@ -199,8 +209,8 @@ def invalid_field_values(test_times):
|
|
|
199
209
|
{test_time: "all good"},
|
|
200
210
|
{tuple((1, 2)): "ok"},
|
|
201
211
|
{"key": {"a": "b"}},
|
|
202
|
-
{"a":
|
|
203
|
-
# Note: {"a": 123}
|
|
212
|
+
{"a": ["b"]},
|
|
213
|
+
# Note: {"a": 123}, {"a": "b"}, and {"a": True} are valid for fields.
|
|
204
214
|
]
|
|
205
215
|
|
|
206
216
|
|
|
@@ -533,3 +543,100 @@ def test_compact_keys(point_data, prefix_attr, key_indices, expected_values):
|
|
|
533
543
|
# Check that values match expected
|
|
534
544
|
for key, expected_value in expected_values.items():
|
|
535
545
|
assert getattr(new_p_compact, data_attr)[key] == expected_value
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@pytest.mark.parametrize(
|
|
549
|
+
"value,serialized",
|
|
550
|
+
[
|
|
551
|
+
(True, "true"),
|
|
552
|
+
(False, "false"),
|
|
553
|
+
("abc", '"abc"'),
|
|
554
|
+
("", '""'),
|
|
555
|
+
("true", '"true"'),
|
|
556
|
+
("1.5", '"1.5"'),
|
|
557
|
+
("_none", '"_none"'),
|
|
558
|
+
("a,b", '"a,b"'),
|
|
559
|
+
('say "hi"', '"say \\"hi\\""'),
|
|
560
|
+
("back\\slash", '"back\\\\slash"'),
|
|
561
|
+
("line1\nline2\r\n", '"line1\\nline2\\r\\n"'),
|
|
562
|
+
("\\n", '"\\\\n"'),
|
|
563
|
+
("\u00e9\u4e2d", '"\u00e9\u4e2d"'),
|
|
564
|
+
(1, "1.0"),
|
|
565
|
+
(-2.5, "-2.5"),
|
|
566
|
+
(None, "_none"),
|
|
567
|
+
],
|
|
568
|
+
)
|
|
569
|
+
def test_serialize_string_and_bool_fields(value, serialized):
|
|
570
|
+
"""Test the serialized form of, and round trip for, each field type."""
|
|
571
|
+
p = Point(
|
|
572
|
+
time=datetime(2022, 1, 1, tzinfo=timezone.utc), fields={"k": value}
|
|
573
|
+
)
|
|
574
|
+
row = p._serialize_to_list()
|
|
575
|
+
|
|
576
|
+
assert row[-2:] == ("_field_k", serialized)
|
|
577
|
+
assert not any("\n" in i or "\r" in i for i in row)
|
|
578
|
+
|
|
579
|
+
result = Point()._deserialize_from_list(row)
|
|
580
|
+
|
|
581
|
+
# Numbers always come back as floats, everything else as it went in.
|
|
582
|
+
expected = float(value) if type(value) is int else value
|
|
583
|
+
assert result.fields == {"k": expected}
|
|
584
|
+
assert type(result.fields["k"]) is type(expected)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def test_serialize_numeric_field_subclasses():
|
|
588
|
+
"""Test numeric subclasses keep the same serialized form as numbers."""
|
|
589
|
+
|
|
590
|
+
class MyInt(int):
|
|
591
|
+
pass
|
|
592
|
+
|
|
593
|
+
class MyFloat(float):
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
p = Point(
|
|
597
|
+
time=datetime(2022, 1, 1, tzinfo=timezone.utc),
|
|
598
|
+
fields={"i": MyInt(1), "f": MyFloat(2.5)},
|
|
599
|
+
)
|
|
600
|
+
row = p._serialize_to_list()
|
|
601
|
+
|
|
602
|
+
assert row[-4:] == ("_field_i", "1.0", "_field_f", "2.5")
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def test_serialize_unsupported_field_value():
|
|
606
|
+
"""Test the serializer rejects unsupported values defensively."""
|
|
607
|
+
with pytest.raises(TypeError, match="Unsupported field value type: list"):
|
|
608
|
+
_serialize_field_value([], "_none")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_deserialize_legacy_and_unknown_field_values():
|
|
612
|
+
"""Test that values written by earlier releases read back unchanged."""
|
|
613
|
+
row = [
|
|
614
|
+
"2022-01-01T00:00:00",
|
|
615
|
+
"m",
|
|
616
|
+
"_tag_t",
|
|
617
|
+
"true",
|
|
618
|
+
"_field_a",
|
|
619
|
+
"1.0",
|
|
620
|
+
"_field_b",
|
|
621
|
+
"-3",
|
|
622
|
+
"_field_c",
|
|
623
|
+
"2.5e-05",
|
|
624
|
+
"_field_d",
|
|
625
|
+
"_none",
|
|
626
|
+
"_field_e",
|
|
627
|
+
"nan",
|
|
628
|
+
"_field_f",
|
|
629
|
+
"garbage",
|
|
630
|
+
"_field_g",
|
|
631
|
+
'"unterminated',
|
|
632
|
+
]
|
|
633
|
+
p = Point()._deserialize_from_list(row)
|
|
634
|
+
|
|
635
|
+
assert p.tags == {"t": "true"}
|
|
636
|
+
assert p.fields["a"] == 1.0 and isinstance(p.fields["a"], float)
|
|
637
|
+
assert p.fields["b"] == -3 and isinstance(p.fields["b"], int)
|
|
638
|
+
assert p.fields["c"] == 2.5e-05
|
|
639
|
+
assert p.fields["d"] is None
|
|
640
|
+
assert p.fields["e"] != p.fields["e"]
|
|
641
|
+
assert p.fields["f"] is None
|
|
642
|
+
assert p.fields["g"] is None
|
|
@@ -625,6 +625,27 @@ def test_hash():
|
|
|
625
625
|
assert not (~(q1 & q5)).is_hashable()
|
|
626
626
|
|
|
627
627
|
|
|
628
|
+
def test_bool_numeric_field_query_hash_identity():
|
|
629
|
+
"""Test bool field queries are distinct from numeric field queries."""
|
|
630
|
+
q_true = FieldQuery().a == True # noqa: E712
|
|
631
|
+
q_one = FieldQuery().a == 1
|
|
632
|
+
q_one_float = FieldQuery().a == 1.0
|
|
633
|
+
q_false = FieldQuery().a == False # noqa: E712
|
|
634
|
+
q_zero = FieldQuery().a == 0
|
|
635
|
+
q_zero_float = FieldQuery().a == 0.0
|
|
636
|
+
|
|
637
|
+
assert q_true != q_one
|
|
638
|
+
assert q_false != q_zero
|
|
639
|
+
assert q_one == q_one_float
|
|
640
|
+
assert q_zero == q_zero_float
|
|
641
|
+
|
|
642
|
+
assert q_true._hash[-1] != True # noqa: E712
|
|
643
|
+
assert len({q_true, q_one, q_one_float, q_false, q_zero, q_zero_float}) == 4
|
|
644
|
+
|
|
645
|
+
assert (q_true & q_one) != (q_one & q_one)
|
|
646
|
+
assert (q_false | q_zero) != (q_zero | q_zero)
|
|
647
|
+
|
|
648
|
+
|
|
628
649
|
def test_empty_query_evaluation():
|
|
629
650
|
"""Test calling an empty query raises Exception."""
|
|
630
651
|
with pytest.raises(TypeError):
|
|
@@ -870,6 +891,61 @@ def test_bad_queries():
|
|
|
870
891
|
with pytest.raises(TypeError):
|
|
871
892
|
TagQuery().city == ["a", "b"]
|
|
872
893
|
|
|
873
|
-
# Test a
|
|
894
|
+
# Test a right hand side that is not numeric, string, or bool.
|
|
874
895
|
with pytest.raises(TypeError):
|
|
875
|
-
FieldQuery().a == "a"
|
|
896
|
+
FieldQuery().a == ["a"]
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def test_string_and_bool_field_queries():
|
|
900
|
+
"""Test queries against string and bool field values."""
|
|
901
|
+
s = Point(fields={"a": "abc"})
|
|
902
|
+
t = Point(fields={"a": True})
|
|
903
|
+
f = Point(fields={"a": False})
|
|
904
|
+
n = Point(fields={"a": 1})
|
|
905
|
+
z = Point(fields={"a": 0.0})
|
|
906
|
+
none = Point(fields={"a": None})
|
|
907
|
+
missing = Point(fields={"b": "abc"})
|
|
908
|
+
points = [s, t, f, n, z, none, missing]
|
|
909
|
+
|
|
910
|
+
yes, no = True, False
|
|
911
|
+
|
|
912
|
+
def matches(q):
|
|
913
|
+
return [p for p in points if q(p)]
|
|
914
|
+
|
|
915
|
+
# Equality is typed.
|
|
916
|
+
assert matches(FieldQuery().a == "abc") == [s]
|
|
917
|
+
assert matches(FieldQuery().a == "ABC") == []
|
|
918
|
+
assert matches(FieldQuery().a == "") == []
|
|
919
|
+
assert matches(FieldQuery().a == yes) == [t]
|
|
920
|
+
assert matches(FieldQuery().a == no) == [f]
|
|
921
|
+
assert matches(FieldQuery().a == 1) == [n]
|
|
922
|
+
assert matches(FieldQuery().a == 0) == [z]
|
|
923
|
+
assert matches(FieldQuery().a == "1") == []
|
|
924
|
+
|
|
925
|
+
# Inequality is typed too: a missing field is never a match.
|
|
926
|
+
assert matches(FieldQuery().a != "abc") == [t, f, n, z, none]
|
|
927
|
+
assert matches(FieldQuery().a != yes) == [s, f, n, z, none]
|
|
928
|
+
assert matches(FieldQuery().a != 1) == [s, t, f, z, none]
|
|
929
|
+
|
|
930
|
+
# Numeric comparisons skip strings and bools instead of raising.
|
|
931
|
+
assert matches(FieldQuery().a > 0) == [n]
|
|
932
|
+
assert matches(FieldQuery().a >= 0) == [n, z]
|
|
933
|
+
assert matches(FieldQuery().a < 1) == [z]
|
|
934
|
+
assert matches(FieldQuery().a <= 1) == [n, z]
|
|
935
|
+
|
|
936
|
+
# String comparisons are lexicographic and skip numbers and bools.
|
|
937
|
+
assert matches(FieldQuery().a > "abb") == [s]
|
|
938
|
+
assert matches(FieldQuery().a < "abd") == [s]
|
|
939
|
+
assert matches(FieldQuery().a >= "abc") == [s]
|
|
940
|
+
assert matches(FieldQuery().a <= "abc") == [s]
|
|
941
|
+
|
|
942
|
+
# Bools only order against bools.
|
|
943
|
+
assert matches(FieldQuery().a > no) == [t]
|
|
944
|
+
assert matches(FieldQuery().a <= yes) == [t, f]
|
|
945
|
+
|
|
946
|
+
# Compound queries work as for any other field value.
|
|
947
|
+
assert matches((FieldQuery().a == "abc") | (FieldQuery().a == yes)) == [
|
|
948
|
+
s,
|
|
949
|
+
t,
|
|
950
|
+
]
|
|
951
|
+
assert matches(~(FieldQuery().a == "abc")) == [t, f, n, z, none, missing]
|
|
@@ -667,3 +667,116 @@ def test_compact_key_prefixes(tmpdir):
|
|
|
667
667
|
assert m._serialize_point(
|
|
668
668
|
p, compact_key_prefixes=False
|
|
669
669
|
) == p._serialize_to_list(compact_key_prefixes=False)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
STRING_AND_BOOL_FIELDS = {
|
|
673
|
+
"s": "plain",
|
|
674
|
+
"empty": "",
|
|
675
|
+
"t": True,
|
|
676
|
+
"f": False,
|
|
677
|
+
"str_true": "true",
|
|
678
|
+
"str_num": "1.5",
|
|
679
|
+
"str_none": "_none",
|
|
680
|
+
"comma": "a,b",
|
|
681
|
+
"quote": 'she said "hi", then left',
|
|
682
|
+
"backslash": "C:\\temp\\new",
|
|
683
|
+
"newline": "line1\nline2\r\nline3\r",
|
|
684
|
+
"unicode": "caf\u00e9 \u4e2d\u6587",
|
|
685
|
+
"n": 1.5,
|
|
686
|
+
"i": 3,
|
|
687
|
+
"none": None,
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@pytest.mark.parametrize("compact_key_prefixes", [False, True])
|
|
692
|
+
def test_string_and_bool_fields_round_trip(tmpdir, compact_key_prefixes):
|
|
693
|
+
"""Test writing, closing, reopening, and reading string/bool fields."""
|
|
694
|
+
path = os.path.join(tmpdir, "test.csv")
|
|
695
|
+
t = datetime(2022, 1, 1, tzinfo=timezone.utc)
|
|
696
|
+
points = [
|
|
697
|
+
Point(time=t, measurement="m", fields=STRING_AND_BOOL_FIELDS),
|
|
698
|
+
Point(
|
|
699
|
+
time=t + timedelta(seconds=1),
|
|
700
|
+
measurement="m",
|
|
701
|
+
tags={"tag": "true"},
|
|
702
|
+
fields={"s": "second", "t": False},
|
|
703
|
+
),
|
|
704
|
+
]
|
|
705
|
+
|
|
706
|
+
db = TinyFlux(path, encoding="utf-8")
|
|
707
|
+
db.insert_multiple(points, compact_key_prefixes=compact_key_prefixes)
|
|
708
|
+
db.close()
|
|
709
|
+
|
|
710
|
+
# Each point is still exactly one line in the file.
|
|
711
|
+
with open(path, encoding="utf-8", newline="") as f:
|
|
712
|
+
assert len(f.read().split("\n")) == len(points) + 1
|
|
713
|
+
|
|
714
|
+
db = TinyFlux(path, encoding="utf-8")
|
|
715
|
+
assert len(db) == 2
|
|
716
|
+
result = db.all()
|
|
717
|
+
assert result == points
|
|
718
|
+
|
|
719
|
+
# Types survive, in particular bool vs. int vs. str.
|
|
720
|
+
first = result[0].fields
|
|
721
|
+
for key, value in STRING_AND_BOOL_FIELDS.items():
|
|
722
|
+
assert first[key] == value
|
|
723
|
+
assert type(first[key]) is (float if key == "i" else type(value))
|
|
724
|
+
|
|
725
|
+
# The index built on reopen sees the typed values.
|
|
726
|
+
assert db.get_field_values("s") == ["plain", "second"]
|
|
727
|
+
assert db.get_field_values("t") == [True, False]
|
|
728
|
+
db.close()
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def test_string_and_bool_fields_memory_storage():
|
|
732
|
+
"""Test string and bool fields in memory storage."""
|
|
733
|
+
db = TinyFlux(storage=MemoryStorage)
|
|
734
|
+
p = Point(fields=STRING_AND_BOOL_FIELDS)
|
|
735
|
+
db.insert(p)
|
|
736
|
+
|
|
737
|
+
assert db.all() == [p]
|
|
738
|
+
assert db.get_field_values("quote") == [STRING_AND_BOOL_FIELDS["quote"]]
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def test_numeric_only_files_from_earlier_releases_read_identically(tmpdir):
|
|
742
|
+
"""Test that a file written before string/bool support reads unchanged."""
|
|
743
|
+
path = os.path.join(tmpdir, "legacy.csv")
|
|
744
|
+
|
|
745
|
+
with open(path, "w", newline="") as f:
|
|
746
|
+
f.write(
|
|
747
|
+
"2022-01-01T00:00:00,m,_tag_a,b,_field_x,1.0,_field_y,-2.5,"
|
|
748
|
+
"_field_z,_none\r\n"
|
|
749
|
+
"2022-01-01T00:00:01,m,t_a,c,f_x,3.0,f_y,1e-07\r\n"
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
db = TinyFlux(path)
|
|
753
|
+
assert db.all() == [
|
|
754
|
+
Point(
|
|
755
|
+
time=datetime(2022, 1, 1, tzinfo=timezone.utc),
|
|
756
|
+
measurement="m",
|
|
757
|
+
tags={"a": "b"},
|
|
758
|
+
fields={"x": 1.0, "y": -2.5, "z": None},
|
|
759
|
+
),
|
|
760
|
+
Point(
|
|
761
|
+
time=datetime(2022, 1, 1, 0, 0, 1, tzinfo=timezone.utc),
|
|
762
|
+
measurement="m",
|
|
763
|
+
tags={"a": "c"},
|
|
764
|
+
fields={"x": 3.0, "y": 1e-07},
|
|
765
|
+
),
|
|
766
|
+
]
|
|
767
|
+
|
|
768
|
+
# New string/bool points can be appended to an old file.
|
|
769
|
+
db.insert(
|
|
770
|
+
Point(
|
|
771
|
+
time=datetime(2022, 1, 1, 0, 0, 2, tzinfo=timezone.utc),
|
|
772
|
+
measurement="m",
|
|
773
|
+
fields={"x": "on", "y": True},
|
|
774
|
+
)
|
|
775
|
+
)
|
|
776
|
+
db.close()
|
|
777
|
+
|
|
778
|
+
db = TinyFlux(path)
|
|
779
|
+
assert len(db) == 3
|
|
780
|
+
assert db.all()[0].fields == {"x": 1.0, "y": -2.5, "z": None}
|
|
781
|
+
assert db.all()[2].fields == {"x": "on", "y": True}
|
|
782
|
+
db.close()
|
|
@@ -857,6 +857,95 @@ def test_search_field_map_index_storage_parity():
|
|
|
857
857
|
assert db_index.search(query) == [point]
|
|
858
858
|
|
|
859
859
|
|
|
860
|
+
def test_negated_field_query_index_storage_read_parity():
|
|
861
|
+
"""Test negated FieldQuery reads match with and without the index."""
|
|
862
|
+
t = datetime(2022, 1, 1, tzinfo=timezone.utc)
|
|
863
|
+
points = [
|
|
864
|
+
Point(time=t, measurement="m", fields={"a": 1}),
|
|
865
|
+
Point(time=t + timedelta(seconds=1), measurement="m", fields={"a": 2}),
|
|
866
|
+
Point(time=t + timedelta(seconds=2), measurement="m", fields={"b": 3}),
|
|
867
|
+
Point(time=t + timedelta(seconds=3), measurement="n", fields={"a": 1}),
|
|
868
|
+
Point(time=t + timedelta(seconds=4), measurement="n", fields={"b": 4}),
|
|
869
|
+
Point(
|
|
870
|
+
time=t + timedelta(seconds=5),
|
|
871
|
+
measurement="full",
|
|
872
|
+
fields={"a": 9},
|
|
873
|
+
),
|
|
874
|
+
]
|
|
875
|
+
|
|
876
|
+
db_storage = TinyFlux(storage=MemoryStorage, auto_index=False)
|
|
877
|
+
db_storage.insert_multiple(points)
|
|
878
|
+
|
|
879
|
+
db_index = TinyFlux(storage=MemoryStorage, auto_index=True)
|
|
880
|
+
db_index.insert_multiple(points)
|
|
881
|
+
|
|
882
|
+
queries = [
|
|
883
|
+
~FieldQuery().a.exists(),
|
|
884
|
+
~(FieldQuery().a == 1),
|
|
885
|
+
(MeasurementQuery() == "m") & ~(FieldQuery().a == 1),
|
|
886
|
+
]
|
|
887
|
+
|
|
888
|
+
for query in queries:
|
|
889
|
+
for measurement in (None, "m", "full"):
|
|
890
|
+
assert db_index.search(query, measurement=measurement) == (
|
|
891
|
+
db_storage.search(query, measurement=measurement)
|
|
892
|
+
)
|
|
893
|
+
assert db_index.count(query, measurement=measurement) == (
|
|
894
|
+
db_storage.count(query, measurement=measurement)
|
|
895
|
+
)
|
|
896
|
+
assert db_index.contains(query, measurement=measurement) == (
|
|
897
|
+
db_storage.contains(query, measurement=measurement)
|
|
898
|
+
)
|
|
899
|
+
assert db_index.get(query, measurement=measurement) == (
|
|
900
|
+
db_storage.get(query, measurement=measurement)
|
|
901
|
+
)
|
|
902
|
+
assert db_index.select(
|
|
903
|
+
"fields.a", query, measurement=measurement
|
|
904
|
+
) == (db_storage.select("fields.a", query, measurement=measurement))
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def test_negated_field_query_index_storage_write_parity():
|
|
908
|
+
"""Test negated FieldQuery writes match with and without the index."""
|
|
909
|
+
t = datetime(2022, 1, 1, tzinfo=timezone.utc)
|
|
910
|
+
query = (MeasurementQuery() == "m") & ~FieldQuery().a.exists()
|
|
911
|
+
|
|
912
|
+
def make_points():
|
|
913
|
+
return [
|
|
914
|
+
Point(time=t, measurement="m", fields={"a": 1}),
|
|
915
|
+
Point(
|
|
916
|
+
time=t + timedelta(seconds=1),
|
|
917
|
+
measurement="m",
|
|
918
|
+
fields={"a": 2},
|
|
919
|
+
),
|
|
920
|
+
Point(
|
|
921
|
+
time=t + timedelta(seconds=2),
|
|
922
|
+
measurement="m",
|
|
923
|
+
fields={"b": 3},
|
|
924
|
+
),
|
|
925
|
+
Point(
|
|
926
|
+
time=t + timedelta(seconds=3),
|
|
927
|
+
measurement="n",
|
|
928
|
+
fields={"b": 4},
|
|
929
|
+
),
|
|
930
|
+
]
|
|
931
|
+
|
|
932
|
+
db_storage = TinyFlux(storage=MemoryStorage, auto_index=False)
|
|
933
|
+
db_storage.insert_multiple(make_points())
|
|
934
|
+
|
|
935
|
+
db_index = TinyFlux(storage=MemoryStorage, auto_index=True)
|
|
936
|
+
db_index.insert_multiple(make_points())
|
|
937
|
+
|
|
938
|
+
assert db_index.update(query, fields={"updated": True}) == 1
|
|
939
|
+
assert db_storage.update(query, fields={"updated": True}) == 1
|
|
940
|
+
assert db_index.all() == db_storage.all()
|
|
941
|
+
|
|
942
|
+
assert db_index.remove(query) == 1
|
|
943
|
+
assert db_storage.remove(query) == 1
|
|
944
|
+
assert db_index.remove(FieldQuery().updated == True) == 0 # noqa: E712
|
|
945
|
+
assert db_storage.remove(FieldQuery().updated == True) == 0 # noqa: E712
|
|
946
|
+
assert db_index.all() == db_storage.all()
|
|
947
|
+
|
|
948
|
+
|
|
860
949
|
def test_select():
|
|
861
950
|
"""Test select method."""
|
|
862
951
|
db = TinyFlux(storage=MemoryStorage, auto_index=False)
|
|
@@ -960,9 +1049,9 @@ def test_update():
|
|
|
960
1049
|
db.update(TagQuery().noop(), tags={"a": 1})
|
|
961
1050
|
|
|
962
1051
|
with pytest.raises(
|
|
963
|
-
ValueError, match="Field set must contain only numeric
|
|
1052
|
+
ValueError, match="Field set must contain only numeric, string, or bool"
|
|
964
1053
|
):
|
|
965
|
-
db.update(TagQuery().noop(), fields={"a": "a"})
|
|
1054
|
+
db.update(TagQuery().noop(), fields={"a": ["a"]})
|
|
966
1055
|
|
|
967
1056
|
# Missing updates.
|
|
968
1057
|
with pytest.raises(ValueError):
|
|
@@ -1639,3 +1728,71 @@ def test_all_get_methods_parity_storage_vs_index():
|
|
|
1639
1728
|
assert sorted(
|
|
1640
1729
|
db_test.get_tag_values(["device"], "measurement2")["device"]
|
|
1641
1730
|
) == ["C", "D"]
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
def test_string_and_bool_fields_mixed_types(tmpdir):
|
|
1734
|
+
"""Test search and update with mixed field types under one field key."""
|
|
1735
|
+
path = os.path.join(tmpdir, "test.csv")
|
|
1736
|
+
t = datetime(2022, 1, 1, tzinfo=timezone.utc)
|
|
1737
|
+
values = ["on", True, 1, 2.5, "true", False, None, "1"]
|
|
1738
|
+
points = [
|
|
1739
|
+
Point(time=t + timedelta(seconds=i), fields={"state": v})
|
|
1740
|
+
for i, v in enumerate(values)
|
|
1741
|
+
]
|
|
1742
|
+
|
|
1743
|
+
db = TinyFlux(path)
|
|
1744
|
+
db.insert_multiple(points)
|
|
1745
|
+
|
|
1746
|
+
def check(db):
|
|
1747
|
+
q = FieldQuery().state
|
|
1748
|
+
assert db.search(q == "on") == [points[0]]
|
|
1749
|
+
assert db.search(q == "true") == [points[4]]
|
|
1750
|
+
assert db.search(q == True) == [points[1]] # noqa: E712
|
|
1751
|
+
assert db.search(q == False) == [points[5]] # noqa: E712
|
|
1752
|
+
assert db.search(q == 1) == [points[2]]
|
|
1753
|
+
assert db.search(q == "1") == [points[7]]
|
|
1754
|
+
assert db.search(q >= 1) == [points[2], points[3]]
|
|
1755
|
+
assert db.search(q > "a") == [points[0], points[4]]
|
|
1756
|
+
assert db.search(q != "on") == [
|
|
1757
|
+
points[i] for i in (1, 2, 3, 4, 5, 6, 7)
|
|
1758
|
+
]
|
|
1759
|
+
assert db.count(q == "on") == 1
|
|
1760
|
+
assert db.get_field_values("state") == values
|
|
1761
|
+
|
|
1762
|
+
# Index is valid.
|
|
1763
|
+
check(db)
|
|
1764
|
+
|
|
1765
|
+
# No index.
|
|
1766
|
+
db._index.invalidate()
|
|
1767
|
+
check(db)
|
|
1768
|
+
|
|
1769
|
+
# Reopened from disk.
|
|
1770
|
+
db.close()
|
|
1771
|
+
db = TinyFlux(path)
|
|
1772
|
+
check(db)
|
|
1773
|
+
db.reindex()
|
|
1774
|
+
check(db)
|
|
1775
|
+
|
|
1776
|
+
# Update from one type to another.
|
|
1777
|
+
assert db.update(FieldQuery().state == "on", fields={"state": False}) == 1
|
|
1778
|
+
assert db.search(FieldQuery().state == "on") == []
|
|
1779
|
+
assert db.get_field_values("state")[0] is False
|
|
1780
|
+
db.close()
|
|
1781
|
+
|
|
1782
|
+
db = TinyFlux(path)
|
|
1783
|
+
assert db.get_field_values("state")[0] is False
|
|
1784
|
+
db.close()
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
def test_string_and_bool_fields_in_measurement_and_select():
|
|
1788
|
+
"""Test string/bool fields through the Measurement and select APIs."""
|
|
1789
|
+
db = TinyFlux(storage=MemoryStorage)
|
|
1790
|
+
m = db.measurement("m")
|
|
1791
|
+
m.insert(Point(fields={"s": "x", "b": True}))
|
|
1792
|
+
m.insert(Point(fields={"s": "y", "b": False}))
|
|
1793
|
+
|
|
1794
|
+
assert m.get_field_values("s") == ["x", "y"]
|
|
1795
|
+
assert m.get_field_values("b") == [True, False]
|
|
1796
|
+
assert m.count(FieldQuery().b == True) == 1 # noqa: E712
|
|
1797
|
+
assert m.select("fields.s", FieldQuery().s == "y") == ["y"]
|
|
1798
|
+
assert db.select("fields.b", FieldQuery().s >= "x") == [True, False]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
|
+
import operator
|
|
5
6
|
from typing import (
|
|
6
7
|
Any,
|
|
7
8
|
Callable,
|
|
@@ -36,6 +37,24 @@ from .queries import (
|
|
|
36
37
|
from .storages import CSVStorage, Storage
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
def _query_contains_negated_field_query(query: Query) -> bool:
|
|
41
|
+
"""Return whether a query needs storage evaluation for field negation."""
|
|
42
|
+
if not isinstance(query, CompoundQuery):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
query.operator == operator.not_
|
|
47
|
+
and isinstance(query.query1, SimpleQuery)
|
|
48
|
+
and query.query1.point_attr == "_fields"
|
|
49
|
+
):
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
return _query_contains_negated_field_query(query.query1) or (
|
|
53
|
+
query.query2 is not None
|
|
54
|
+
and _query_contains_negated_field_query(query.query2)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
39
58
|
class TinyFlux:
|
|
40
59
|
"""The TinyFlux class containing the interface for the TinyFlux package.
|
|
41
60
|
|
|
@@ -220,8 +239,12 @@ class TinyFlux:
|
|
|
220
239
|
Returns:
|
|
221
240
|
True if point found, else False.
|
|
222
241
|
"""
|
|
242
|
+
use_index = (
|
|
243
|
+
self._index.valid and not _query_contains_negated_field_query(query)
|
|
244
|
+
)
|
|
245
|
+
|
|
223
246
|
# If the index is valid, check it.
|
|
224
|
-
if
|
|
247
|
+
if use_index:
|
|
225
248
|
if measurement:
|
|
226
249
|
mq = MeasurementQuery() == measurement
|
|
227
250
|
index_rst = self._index.search(mq & query)
|
|
@@ -261,8 +284,12 @@ class TinyFlux:
|
|
|
261
284
|
Returns:
|
|
262
285
|
A count of matching points in the measurement.
|
|
263
286
|
"""
|
|
287
|
+
use_index = (
|
|
288
|
+
self._index.valid and not _query_contains_negated_field_query(query)
|
|
289
|
+
)
|
|
290
|
+
|
|
264
291
|
# If the index is valid, check it.
|
|
265
|
-
if
|
|
292
|
+
if use_index:
|
|
266
293
|
if measurement:
|
|
267
294
|
mq = MeasurementQuery() == measurement
|
|
268
295
|
index_rst = self._index.search(mq & query)
|
|
@@ -327,7 +354,9 @@ class TinyFlux:
|
|
|
327
354
|
Returns:
|
|
328
355
|
First found Point or None.
|
|
329
356
|
"""
|
|
330
|
-
use_index =
|
|
357
|
+
use_index = (
|
|
358
|
+
self._index.valid and not _query_contains_negated_field_query(query)
|
|
359
|
+
)
|
|
331
360
|
|
|
332
361
|
# If we are auto-indexing and the index is valid, check it.
|
|
333
362
|
if use_index:
|
|
@@ -757,7 +786,9 @@ class TinyFlux:
|
|
|
757
786
|
if not isinstance(query, (SimpleQuery, CompoundQuery)):
|
|
758
787
|
raise ValueError("query must be a TinyFlux Query.")
|
|
759
788
|
|
|
760
|
-
use_index =
|
|
789
|
+
use_index = (
|
|
790
|
+
self._index.valid and not _query_contains_negated_field_query(query)
|
|
791
|
+
)
|
|
761
792
|
|
|
762
793
|
# If we are auto-indexing and the index is valid, check it.
|
|
763
794
|
if use_index:
|
|
@@ -870,7 +901,9 @@ class TinyFlux:
|
|
|
870
901
|
else:
|
|
871
902
|
raise ValueError(f"Invalid key `{key}`.")
|
|
872
903
|
|
|
873
|
-
use_index =
|
|
904
|
+
use_index = (
|
|
905
|
+
self._index.valid and not _query_contains_negated_field_query(query)
|
|
906
|
+
)
|
|
874
907
|
|
|
875
908
|
results = []
|
|
876
909
|
|
|
@@ -1337,7 +1370,9 @@ class TinyFlux:
|
|
|
1337
1370
|
Raises:
|
|
1338
1371
|
OSError if storage cannot be written to.
|
|
1339
1372
|
"""
|
|
1340
|
-
use_index =
|
|
1373
|
+
use_index = (
|
|
1374
|
+
self._index.valid and not _query_contains_negated_field_query(query)
|
|
1375
|
+
)
|
|
1341
1376
|
|
|
1342
1377
|
# If we are auto-indexing and the index is valid, check it.
|
|
1343
1378
|
if use_index:
|
|
@@ -1492,7 +1527,11 @@ class TinyFlux:
|
|
|
1492
1527
|
unset_tags=unset_tags,
|
|
1493
1528
|
)
|
|
1494
1529
|
|
|
1495
|
-
use_index =
|
|
1530
|
+
use_index = (
|
|
1531
|
+
not update_all
|
|
1532
|
+
and self._index.valid
|
|
1533
|
+
and not _query_contains_negated_field_query(query)
|
|
1534
|
+
)
|
|
1496
1535
|
|
|
1497
1536
|
# If we are auto-indexing and the index is valid, check it.
|
|
1498
1537
|
if use_index:
|
|
@@ -110,7 +110,7 @@ class Index:
|
|
|
110
110
|
|
|
111
111
|
_num_items: int
|
|
112
112
|
_tags: Dict[str, Dict[Union[None, str], List[int]]]
|
|
113
|
-
_fields: Dict[str, List[Tuple[int,
|
|
113
|
+
_fields: Dict[str, List[Tuple[int, FieldValue]]]
|
|
114
114
|
_measurements: Dict[str, List[int]]
|
|
115
115
|
_timestamps: List[float]
|
|
116
116
|
_valid: bool
|
|
@@ -6,8 +6,8 @@ deserialized from Storage. SimpleQuery act upon individual Points.
|
|
|
6
6
|
|
|
7
7
|
A Point is comprised of a timestamp, a measurement, fields, and tags.
|
|
8
8
|
|
|
9
|
-
Fields contains string/numeric key-values, while tags
|
|
10
|
-
string/string key-values. This is enforced upon Point instantiation.
|
|
9
|
+
Fields contains string/(numeric, string, or bool) key-values, while tags
|
|
10
|
+
contain string/string key-values. This is enforced upon Point instantiation.
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
13
|
|
|
@@ -22,10 +22,11 @@ Usage:
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
from datetime import datetime, timezone
|
|
25
|
-
|
|
25
|
+
import re
|
|
26
|
+
from typing import Any, Dict, Mapping, Optional, Sequence, Union, cast
|
|
26
27
|
|
|
27
28
|
TagSet = Dict[str, Optional[str]]
|
|
28
|
-
FieldValue = Union[int, float, None]
|
|
29
|
+
FieldValue = Union[int, float, str, bool, None]
|
|
29
30
|
FieldSet = Dict[str, FieldValue]
|
|
30
31
|
|
|
31
32
|
|
|
@@ -73,14 +74,83 @@ def validate_fields(fields: Any) -> None:
|
|
|
73
74
|
if i is None:
|
|
74
75
|
continue
|
|
75
76
|
|
|
76
|
-
if
|
|
77
|
+
if not isinstance(i, (int, float, str, bool)):
|
|
77
78
|
raise ValueError(
|
|
78
|
-
"Field set must contain only numeric
|
|
79
|
+
"Field set must contain only numeric, string, or bool values "
|
|
80
|
+
"or None."
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
return
|
|
82
84
|
|
|
83
85
|
|
|
86
|
+
_FIELD_STR_ESCAPES = {"\\": "\\\\", '"': '\\"', "\n": "\\n", "\r": "\\r"}
|
|
87
|
+
_FIELD_STR_UNESCAPES = {"\\": "\\", '"': '"', "n": "\n", "r": "\r"}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _serialize_field_value(value: FieldValue, none_str: str) -> str:
|
|
91
|
+
"""Serialize a field value to a string.
|
|
92
|
+
|
|
93
|
+
Numbers are written as floats, bools as "true"/"false", and strings in
|
|
94
|
+
double quotes with backslash, double quote, and line break characters
|
|
95
|
+
escaped so that a serialized value never spans multiple lines.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
value: The field value.
|
|
99
|
+
none_str: The string that represents None.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The serialized field value.
|
|
103
|
+
"""
|
|
104
|
+
value_type = type(value)
|
|
105
|
+
|
|
106
|
+
if value_type is float:
|
|
107
|
+
return str(value)
|
|
108
|
+
|
|
109
|
+
if value_type is int:
|
|
110
|
+
return str(float(cast(int, value)))
|
|
111
|
+
|
|
112
|
+
if value is None:
|
|
113
|
+
return none_str
|
|
114
|
+
|
|
115
|
+
if isinstance(value, bool):
|
|
116
|
+
return "true" if value else "false"
|
|
117
|
+
|
|
118
|
+
if isinstance(value, str):
|
|
119
|
+
escaped = "".join(_FIELD_STR_ESCAPES.get(c, c) for c in value)
|
|
120
|
+
return f'"{escaped}"'
|
|
121
|
+
|
|
122
|
+
if isinstance(value, (int, float)):
|
|
123
|
+
return str(float(value))
|
|
124
|
+
|
|
125
|
+
raise TypeError(f"Unsupported field value type: {type(value).__name__}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _deserialize_non_numeric_field_value(value: str) -> FieldValue:
|
|
129
|
+
"""Deserialize a field value that is not a number.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
value: The serialized field value.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
A bool for "true"/"false", a str for a double-quoted value, and None
|
|
136
|
+
for anything else.
|
|
137
|
+
"""
|
|
138
|
+
if value == "true":
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
if value == "false":
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
|
|
145
|
+
return re.sub(
|
|
146
|
+
r"\\(.)",
|
|
147
|
+
lambda m: _FIELD_STR_UNESCAPES.get(m.group(1), m.group(0)),
|
|
148
|
+
value[1:-1],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
84
154
|
class Point:
|
|
85
155
|
"""Define the Point class.
|
|
86
156
|
|
|
@@ -291,9 +361,9 @@ class Point:
|
|
|
291
361
|
try:
|
|
292
362
|
p_fields[f_key] = float(f_value)
|
|
293
363
|
|
|
294
|
-
# Value is None.
|
|
364
|
+
# Value is a bool, a string, or None.
|
|
295
365
|
except Exception:
|
|
296
|
-
p_fields[f_key] =
|
|
366
|
+
p_fields[f_key] = _deserialize_non_numeric_field_value(f_value)
|
|
297
367
|
|
|
298
368
|
i += 2
|
|
299
369
|
|
|
@@ -351,7 +421,7 @@ class Point:
|
|
|
351
421
|
fields = (
|
|
352
422
|
(
|
|
353
423
|
f"{field_key_prefix}{k}",
|
|
354
|
-
self._none_str
|
|
424
|
+
_serialize_field_value(v, self._none_str),
|
|
355
425
|
)
|
|
356
426
|
for k, v in self._fields.items()
|
|
357
427
|
)
|
|
@@ -32,6 +32,42 @@ from .point import Point
|
|
|
32
32
|
|
|
33
33
|
Query = Union["SimpleQuery", "CompoundQuery"]
|
|
34
34
|
|
|
35
|
+
_NE = operator.ne
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _HashableRHS:
|
|
39
|
+
"""A hash key for RHS values that preserves bool identity."""
|
|
40
|
+
|
|
41
|
+
__slots__ = ("_kind", "_value", "_display_value")
|
|
42
|
+
|
|
43
|
+
def __init__(self, kind: Any, value: Any, display_value: Any) -> None:
|
|
44
|
+
self._kind = kind
|
|
45
|
+
self._value = value
|
|
46
|
+
self._display_value = display_value
|
|
47
|
+
|
|
48
|
+
def __hash__(self) -> int:
|
|
49
|
+
return hash((self._kind, self._value))
|
|
50
|
+
|
|
51
|
+
def __eq__(self, other: object) -> bool:
|
|
52
|
+
if not isinstance(other, _HashableRHS):
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
return bool((self._kind, self._value) == (other._kind, other._value))
|
|
56
|
+
|
|
57
|
+
def __repr__(self) -> str:
|
|
58
|
+
return repr(self._display_value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _hashable_rhs(rhs: Any) -> Any:
|
|
62
|
+
"""Return a hash key for RHS values that preserves bool identity."""
|
|
63
|
+
if isinstance(rhs, bool):
|
|
64
|
+
return _HashableRHS(bool, rhs, rhs)
|
|
65
|
+
|
|
66
|
+
if isinstance(rhs, (int, float)):
|
|
67
|
+
return _HashableRHS("number", float(rhs), rhs)
|
|
68
|
+
|
|
69
|
+
return _HashableRHS(type(rhs), rhs, rhs)
|
|
70
|
+
|
|
35
71
|
|
|
36
72
|
class CompoundQuery:
|
|
37
73
|
"""A container class for simple and/or compound queries and an operator.
|
|
@@ -467,15 +503,25 @@ class BaseQuery:
|
|
|
467
503
|
if (
|
|
468
504
|
self._point_attr == "_fields"
|
|
469
505
|
and rhs
|
|
470
|
-
and not isinstance(rhs, (int, float))
|
|
506
|
+
and not isinstance(rhs, (int, float, str))
|
|
471
507
|
):
|
|
472
|
-
raise TypeError(
|
|
508
|
+
raise TypeError(
|
|
509
|
+
"FieldQuery comparison value must be numeric, string, or bool."
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
is_field_query = self._point_attr == "_fields"
|
|
513
|
+
rhs_is_bool = isinstance(rhs, bool)
|
|
473
514
|
|
|
474
515
|
def test(x: Any) -> bool:
|
|
475
516
|
"""The test function from an operator and right-hand side."""
|
|
476
517
|
if not test_against_rhs:
|
|
477
518
|
return operator(x, *args) if args else operator(x)
|
|
478
519
|
|
|
520
|
+
# A bool field is not a number, so a bool is only ever equal to
|
|
521
|
+
# another bool and never orders against a non-bool.
|
|
522
|
+
if is_field_query and isinstance(x, bool) != rhs_is_bool:
|
|
523
|
+
return operator is _NE
|
|
524
|
+
|
|
479
525
|
# Wrap this in a try/except block.
|
|
480
526
|
# Some operators do not work against None types.
|
|
481
527
|
# They should evaluate to False.
|
|
@@ -532,7 +578,7 @@ class BaseQuery:
|
|
|
532
578
|
test_against_rhs=True,
|
|
533
579
|
rhs=rhs,
|
|
534
580
|
args=None,
|
|
535
|
-
hashval=(self._point_attr, "==", self._path, rhs),
|
|
581
|
+
hashval=(self._point_attr, "==", self._path, _hashable_rhs(rhs)),
|
|
536
582
|
)
|
|
537
583
|
|
|
538
584
|
def __ne__(self, rhs: Any) -> SimpleQuery: # type: ignore
|
|
@@ -551,7 +597,7 @@ class BaseQuery:
|
|
|
551
597
|
test_against_rhs=True,
|
|
552
598
|
rhs=rhs,
|
|
553
599
|
args=None,
|
|
554
|
-
hashval=(self._point_attr, "!=", self._path, rhs),
|
|
600
|
+
hashval=(self._point_attr, "!=", self._path, _hashable_rhs(rhs)),
|
|
555
601
|
)
|
|
556
602
|
|
|
557
603
|
def __lt__(self, rhs: Any) -> SimpleQuery:
|
|
@@ -568,7 +614,7 @@ class BaseQuery:
|
|
|
568
614
|
test_against_rhs=True,
|
|
569
615
|
rhs=rhs,
|
|
570
616
|
args=None,
|
|
571
|
-
hashval=(self._point_attr, "<", self._path, rhs),
|
|
617
|
+
hashval=(self._point_attr, "<", self._path, _hashable_rhs(rhs)),
|
|
572
618
|
)
|
|
573
619
|
|
|
574
620
|
def __le__(self, rhs: Any) -> SimpleQuery:
|
|
@@ -585,7 +631,7 @@ class BaseQuery:
|
|
|
585
631
|
test_against_rhs=True,
|
|
586
632
|
rhs=rhs,
|
|
587
633
|
args=None,
|
|
588
|
-
hashval=(self._point_attr, "<=", self._path, rhs),
|
|
634
|
+
hashval=(self._point_attr, "<=", self._path, _hashable_rhs(rhs)),
|
|
589
635
|
)
|
|
590
636
|
|
|
591
637
|
def __gt__(self, rhs: Any) -> SimpleQuery:
|
|
@@ -602,7 +648,7 @@ class BaseQuery:
|
|
|
602
648
|
test_against_rhs=True,
|
|
603
649
|
rhs=rhs,
|
|
604
650
|
args=None,
|
|
605
|
-
hashval=(self._point_attr, ">", self._path, rhs),
|
|
651
|
+
hashval=(self._point_attr, ">", self._path, _hashable_rhs(rhs)),
|
|
606
652
|
)
|
|
607
653
|
|
|
608
654
|
def __ge__(self, rhs: Any) -> SimpleQuery:
|
|
@@ -619,7 +665,7 @@ class BaseQuery:
|
|
|
619
665
|
test_against_rhs=True,
|
|
620
666
|
rhs=rhs,
|
|
621
667
|
args=None,
|
|
622
|
-
hashval=(self._point_attr, ">=", self._path, rhs),
|
|
668
|
+
hashval=(self._point_attr, ">=", self._path, _hashable_rhs(rhs)),
|
|
623
669
|
)
|
|
624
670
|
|
|
625
671
|
def __and__(self, other: Any) -> None:
|
|
@@ -368,7 +368,9 @@ class CSVStorage(Storage):
|
|
|
368
368
|
|
|
369
369
|
def _init_temp_storage(self) -> None:
|
|
370
370
|
"""Initialize temporary storage."""
|
|
371
|
-
self._temp_handle = NamedTemporaryFile(
|
|
371
|
+
self._temp_handle = NamedTemporaryFile(
|
|
372
|
+
"w+t", encoding=self._encoding, newline="", delete=False
|
|
373
|
+
)
|
|
372
374
|
|
|
373
375
|
return
|
|
374
376
|
|
|
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
|
|
File without changes
|
|
File without changes
|