tinyflux 1.2.0__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.0/tinyflux.egg-info → tinyflux-1.3.0}/PKG-INFO +11 -10
- {tinyflux-1.2.0 → tinyflux-1.3.0}/README.md +7 -7
- {tinyflux-1.2.0 → tinyflux-1.3.0}/setup.cfg +3 -2
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/test_index.py +17 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/test_measurement.py +2 -2
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/test_point.py +112 -5
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/test_queries.py +78 -2
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/test_storages.py +117 -4
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/test_tinyflux.py +177 -5
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/database.py +46 -7
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/index.py +21 -10
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/point.py +79 -9
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/queries.py +59 -8
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/storages.py +3 -1
- tinyflux-1.3.0/tinyflux/version.py +3 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0/tinyflux.egg-info}/PKG-INFO +11 -10
- tinyflux-1.2.0/tinyflux/version.py +0 -3
- {tinyflux-1.2.0 → tinyflux-1.3.0}/LICENSE +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/MANIFEST.in +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/pyproject.toml +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/__init__.py +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/conftest.py +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tests/test_utils.py +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/__init__.py +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/decorators.py +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/measurement.py +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/py.typed +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux/utils.py +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux.egg-info/SOURCES.txt +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux.egg-info/dependency_links.txt +0 -0
- {tinyflux-1.2.0 → tinyflux-1.3.0}/tinyflux.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinyflux
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: The Tiny Time-Series Database Optimized for Your Happiness
|
|
5
5
|
Home-page: https://github.com/citrusvanilla/tinyflux
|
|
6
6
|
Author: Justin Fung
|
|
@@ -17,15 +17,16 @@ Classifier: Topic :: Database
|
|
|
17
17
|
Classifier: Topic :: Database :: Database Engines/Servers
|
|
18
18
|
Classifier: Topic :: Utilities
|
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
25
26
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
27
|
Classifier: Operating System :: OS Independent
|
|
27
28
|
Classifier: Typing :: Typed
|
|
28
|
-
Requires-Python: >=3.
|
|
29
|
+
Requires-Python: >=3.10
|
|
29
30
|
Description-Content-Type: text/markdown
|
|
30
31
|
License-File: LICENSE
|
|
31
32
|
Dynamic: license-file
|
|
@@ -37,7 +38,7 @@ Dynamic: license-file
|
|
|
37
38
|
|
|
38
39
|
TinyFlux is the tiny time series database optimized for your happiness 😎
|
|
39
40
|
|
|
40
|
-
TinyFlux is the time series version of [TinyDB](https://tinydb.readthedocs.io/en/latest/index.html) that is written in Python and has no external dependencies. It consistently writes atomically to file at over 6,000 writes per second and supports greater than 100 queries per second for time-based range queries on datasets up to 100,000 points. It's a perfect companion for small analytics workflows and apps, as well as at-home IOT data stores. TinyFlux has 100% test coverage, over
|
|
41
|
+
TinyFlux is the time series version of [TinyDB](https://tinydb.readthedocs.io/en/latest/index.html) that is written in Python and has no external dependencies. It consistently writes atomically to file at over 6,000 writes per second and supports greater than 100 queries per second for time-based range queries on datasets up to 100,000 points. It's a perfect companion for small analytics workflows and apps, as well as at-home IOT data stores. TinyFlux has 100% test coverage, over 200,000 downloads, and no open issues.
|
|
41
42
|
|
|
42
43
|
[](https://tinyflux.readthedocs.io/en/latest/?badge=latest)
|
|
43
44
|
[](https://pypi.org/project/tinyflux/)
|
|
@@ -59,6 +60,8 @@ Install from PyPI:
|
|
|
59
60
|
pip install tinyflux
|
|
60
61
|
```
|
|
61
62
|
|
|
63
|
+
TinyFlux supports CPython 3.10 through 3.15.
|
|
64
|
+
|
|
62
65
|
## Quick Start
|
|
63
66
|
|
|
64
67
|
```python
|
|
@@ -238,13 +241,11 @@ TinyFlux automatic indexing provides:
|
|
|
238
241
|
|
|
239
242
|
## TinyFlux Across the Internet
|
|
240
243
|
|
|
241
|
-
|
|
242
|
-
- **System Monitoring**: Track server metrics and application performance
|
|
243
|
-
- **Financial Data**: Store and query stock prices, trading volumes
|
|
244
|
-
- **Scientific Research**: Log experimental measurements and observations
|
|
245
|
-
- **Web Analytics**: Time-stamped user interaction data
|
|
244
|
+
Articles, tutorials, and other instances of TinyFlux in the wild:
|
|
246
245
|
|
|
247
|
-
|
|
246
|
+
- ["Introducing TinyFlux: The Tiny Time Series Database for Python-based IoT & Analytics Applications"](https://citrusvanilla.medium.com/introducing-tinyflux-the-tiny-time-series-database-for-python-based-iot-analytics-applications-c3ef3c3bedf): A Medium.com article announcing the release of TinyFlux
|
|
247
|
+
- ["Storing Time Series Data in Python Using TinyFluxDB"](http://www.steves-internet-guide.com/storing-time-series-data-python-using-tinyflux-db/): A tutorial from [Steve's Internet Guide](http://www.steves-internet-guide.com/), a portal for learning MQTT and IoT development for Python
|
|
248
|
+
- ["KaiCode 2024 Shortlist"](https://www.kaicode.org/2024.html): TinyFlux came in 10th place out of 412 entrants in the 7th edition of this open-source festival, a festival dedicated to recognizing projects with high-quality open-source principles
|
|
248
249
|
|
|
249
250
|
## Contributing
|
|
250
251
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
TinyFlux is the tiny time series database optimized for your happiness 😎
|
|
7
7
|
|
|
8
|
-
TinyFlux is the time series version of [TinyDB](https://tinydb.readthedocs.io/en/latest/index.html) that is written in Python and has no external dependencies. It consistently writes atomically to file at over 6,000 writes per second and supports greater than 100 queries per second for time-based range queries on datasets up to 100,000 points. It's a perfect companion for small analytics workflows and apps, as well as at-home IOT data stores. TinyFlux has 100% test coverage, over
|
|
8
|
+
TinyFlux is the time series version of [TinyDB](https://tinydb.readthedocs.io/en/latest/index.html) that is written in Python and has no external dependencies. It consistently writes atomically to file at over 6,000 writes per second and supports greater than 100 queries per second for time-based range queries on datasets up to 100,000 points. It's a perfect companion for small analytics workflows and apps, as well as at-home IOT data stores. TinyFlux has 100% test coverage, over 200,000 downloads, and no open issues.
|
|
9
9
|
|
|
10
10
|
[](https://tinyflux.readthedocs.io/en/latest/?badge=latest)
|
|
11
11
|
[](https://pypi.org/project/tinyflux/)
|
|
@@ -27,6 +27,8 @@ Install from PyPI:
|
|
|
27
27
|
pip install tinyflux
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
TinyFlux supports CPython 3.10 through 3.15.
|
|
31
|
+
|
|
30
32
|
## Quick Start
|
|
31
33
|
|
|
32
34
|
```python
|
|
@@ -206,13 +208,11 @@ TinyFlux automatic indexing provides:
|
|
|
206
208
|
|
|
207
209
|
## TinyFlux Across the Internet
|
|
208
210
|
|
|
209
|
-
|
|
210
|
-
- **System Monitoring**: Track server metrics and application performance
|
|
211
|
-
- **Financial Data**: Store and query stock prices, trading volumes
|
|
212
|
-
- **Scientific Research**: Log experimental measurements and observations
|
|
213
|
-
- **Web Analytics**: Time-stamped user interaction data
|
|
211
|
+
Articles, tutorials, and other instances of TinyFlux in the wild:
|
|
214
212
|
|
|
215
|
-
|
|
213
|
+
- ["Introducing TinyFlux: The Tiny Time Series Database for Python-based IoT & Analytics Applications"](https://citrusvanilla.medium.com/introducing-tinyflux-the-tiny-time-series-database-for-python-based-iot-analytics-applications-c3ef3c3bedf): A Medium.com article announcing the release of TinyFlux
|
|
214
|
+
- ["Storing Time Series Data in Python Using TinyFluxDB"](http://www.steves-internet-guide.com/storing-time-series-data-python-using-tinyflux-db/): A tutorial from [Steve's Internet Guide](http://www.steves-internet-guide.com/), a portal for learning MQTT and IoT development for Python
|
|
215
|
+
- ["KaiCode 2024 Shortlist"](https://www.kaicode.org/2024.html): TinyFlux came in 10th place out of 412 entrants in the 7th edition of this open-source festival, a festival dedicated to recognizing projects with high-quality open-source principles
|
|
216
216
|
|
|
217
217
|
## Contributing
|
|
218
218
|
|
|
@@ -30,18 +30,19 @@ classifiers =
|
|
|
30
30
|
Topic :: Database :: Database Engines/Servers
|
|
31
31
|
Topic :: Utilities
|
|
32
32
|
Programming Language :: Python :: 3
|
|
33
|
-
Programming Language :: Python :: 3.9
|
|
34
33
|
Programming Language :: Python :: 3.10
|
|
35
34
|
Programming Language :: Python :: 3.11
|
|
36
35
|
Programming Language :: Python :: 3.12
|
|
37
36
|
Programming Language :: Python :: 3.13
|
|
37
|
+
Programming Language :: Python :: 3.14
|
|
38
|
+
Programming Language :: Python :: 3.15
|
|
38
39
|
Programming Language :: Python :: Implementation :: CPython
|
|
39
40
|
Operating System :: OS Independent
|
|
40
41
|
Typing :: Typed
|
|
41
42
|
|
|
42
43
|
[options]
|
|
43
44
|
packages = tinyflux
|
|
44
|
-
python_requires = >=3.
|
|
45
|
+
python_requires = >=3.10
|
|
45
46
|
|
|
46
47
|
[options.package_data]
|
|
47
48
|
tinyflux = py.typed
|
|
@@ -350,6 +350,23 @@ def test_search_field_query():
|
|
|
350
350
|
assert rst.items == set({})
|
|
351
351
|
|
|
352
352
|
|
|
353
|
+
def test_search_field_query_with_map():
|
|
354
|
+
"""Test Index search applies map functions to field values."""
|
|
355
|
+
index = Index()
|
|
356
|
+
index._insert_fields(0, {"temp": 70.0})
|
|
357
|
+
|
|
358
|
+
rst = index.search(FieldQuery().temp.map(lambda x: x * 2) == 140.0)
|
|
359
|
+
assert rst.items == {0}
|
|
360
|
+
|
|
361
|
+
rst = index.search(
|
|
362
|
+
FieldQuery().map(lambda fields: fields["temp"] * 2) == 140.0
|
|
363
|
+
)
|
|
364
|
+
assert rst.items == {0}
|
|
365
|
+
|
|
366
|
+
rst = index.search(FieldQuery().temp.map(lambda _: 1 / 0) == 1)
|
|
367
|
+
assert rst.items == set({})
|
|
368
|
+
|
|
369
|
+
|
|
353
370
|
def test_search_compound_query_not():
|
|
354
371
|
"""Test search_query of Index on compound 'not' queries."""
|
|
355
372
|
# Some timestamps.
|
|
@@ -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]
|
|
@@ -352,7 +352,7 @@ def test_connect_to_existing_csv(tmpdir, csv_storage_with_counters):
|
|
|
352
352
|
|
|
353
353
|
# Mock CSV store. Insert points out of order.
|
|
354
354
|
path = os.path.join(tmpdir, "test.csv")
|
|
355
|
-
with open(path, "w") as f:
|
|
355
|
+
with open(path, "w", newline="") as f:
|
|
356
356
|
w = csv.writer(f)
|
|
357
357
|
w.writerow(p2._serialize_to_list())
|
|
358
358
|
w.writerow(p1._serialize_to_list())
|
|
@@ -478,7 +478,7 @@ def test_multiple_appends(
|
|
|
478
478
|
|
|
479
479
|
# Mock CSV store. Insert points out of order.
|
|
480
480
|
path = os.path.join(tmpdir, "test.csv")
|
|
481
|
-
with open(path, "w") as f:
|
|
481
|
+
with open(path, "w", newline="") as f:
|
|
482
482
|
w = csv.writer(f)
|
|
483
483
|
w.writerow(p2._serialize_to_list())
|
|
484
484
|
w.writerow(p1._serialize_to_list())
|
|
@@ -514,7 +514,7 @@ def test_multiple_reads(
|
|
|
514
514
|
path = os.path.join(tmpdir, "test.csv")
|
|
515
515
|
t = datetime.now(timezone.utc)
|
|
516
516
|
|
|
517
|
-
with open(path, "w") as f:
|
|
517
|
+
with open(path, "w", newline="") as f:
|
|
518
518
|
w = csv.writer(f)
|
|
519
519
|
for _ in range(10):
|
|
520
520
|
w.writerow(Point(time=t)._serialize_to_list())
|
|
@@ -533,7 +533,7 @@ def test_multiple_reads(
|
|
|
533
533
|
|
|
534
534
|
# Mock CSV store. Insert points out-of-order.
|
|
535
535
|
path = os.path.join(tmpdir, "test.csv")
|
|
536
|
-
with open(path, "w") as f:
|
|
536
|
+
with open(path, "w", newline="") as f:
|
|
537
537
|
w = csv.writer(f)
|
|
538
538
|
for _ in range(10):
|
|
539
539
|
w.writerow(Point(time=t)._serialize_to_list())
|
|
@@ -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()
|