tinyflux 0.2.6__tar.gz → 0.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-0.2.6/tinyflux.egg-info → tinyflux-0.3.0}/PKG-INFO +19 -3
- {tinyflux-0.2.6 → tinyflux-0.3.0}/README.rst +18 -2
- {tinyflux-0.2.6 → tinyflux-0.3.0}/pyproject.toml +1 -1
- {tinyflux-0.2.6 → tinyflux-0.3.0}/setup.cfg +1 -1
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_point.py +76 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_queries.py +5 -15
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_storages.py +27 -1
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_tinyflux.py +63 -9
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/database.py +31 -21
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/point.py +62 -18
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/queries.py +1 -3
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/storages.py +8 -4
- {tinyflux-0.2.6 → tinyflux-0.3.0/tinyflux.egg-info}/PKG-INFO +19 -3
- tinyflux-0.3.0/version.txt +1 -0
- tinyflux-0.2.6/version.txt +0 -1
- {tinyflux-0.2.6 → tinyflux-0.3.0}/LICENSE +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/MANIFEST.in +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/__init__.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/conftest.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_index.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_measurement.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_utils.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/__init__.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/index.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/measurement.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/py.typed +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/utils.py +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux.egg-info/SOURCES.txt +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux.egg-info/dependency_links.txt +0 -0
- {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinyflux
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.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
|
|
@@ -48,6 +48,17 @@ Quick Links
|
|
|
48
48
|
- `Contributing`_
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
Recent Updates
|
|
52
|
+
**************
|
|
53
|
+
|
|
54
|
+
v0.3.0 (2023-3-21)
|
|
55
|
+
==================
|
|
56
|
+
|
|
57
|
+
* Tag and field keys can be compacted when using CSVStorage, saving potentially many bytes per Point (resolves issue #32).
|
|
58
|
+
* Fixed bug that causes tag values of '' to be serialized as "_none" (resolves issue #33).
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
51
62
|
Installation
|
|
52
63
|
************
|
|
53
64
|
|
|
@@ -110,7 +121,7 @@ Writing to TinyFlux
|
|
|
110
121
|
... tags={"room": "bedroom"},
|
|
111
122
|
... fields={"temp": 72.0}
|
|
112
123
|
... )
|
|
113
|
-
>>> db.insert(p)
|
|
124
|
+
>>> db.insert(p, compact_key_prefixes=True)
|
|
114
125
|
|
|
115
126
|
|
|
116
127
|
Querying TinyFlux
|
|
@@ -147,6 +158,11 @@ The `examples <https://github.com/citrusvanilla/tinyflux/tree/master/examples>`_
|
|
|
147
158
|
2. `Local Analytics Workflow with a TinyFlux Database <https://github.com/citrusvanilla/tinyflux/blob/master/examples/2_analytics_workflow.ipynb>`_
|
|
148
159
|
3. `TinyFlux as a MQTT Datastore for IoT Devices <https://github.com/citrusvanilla/tinyflux/blob/master/examples/3_iot_datastore_with_mqtt.py>`_
|
|
149
160
|
|
|
161
|
+
Tips
|
|
162
|
+
****
|
|
163
|
+
|
|
164
|
+
Checkout some tips for working with TinyFlux `here <https://tinyflux.readthedocs.io/en/latest/tips.html>`_.
|
|
165
|
+
|
|
150
166
|
|
|
151
167
|
TinyFlux Across the Internet
|
|
152
168
|
****************************
|
|
@@ -162,7 +178,7 @@ Contributing
|
|
|
162
178
|
|
|
163
179
|
New ideas, new developer tools, improvements, and bugfixes are always welcome. Follow these guidelines before getting started:
|
|
164
180
|
|
|
165
|
-
1. Make sure to read `Getting Started <https://tinyflux.readthedocs.io/en/latest/getting-started.html>`_ and the `Contributing <https://tinyflux.readthedocs.io/en/latest/contributing-
|
|
181
|
+
1. Make sure to read `Getting Started <https://tinyflux.readthedocs.io/en/latest/getting-started.html>`_ and the `Contributing Tooling and Conventions <https://tinyflux.readthedocs.io/en/latest/contributing-tooling.html>`_ section of the documentation.
|
|
166
182
|
2. Check GitHub for `existing open issues <https://github.com/citrusvanilla/tinyflux/issues>`_, `open a new issue <https://github.com/citrusvanilla/tinyflux/issues/new>`_ or `start a new discussion <https://github.com/citrusvanilla/tinyflux/discussions/new>`_.
|
|
167
183
|
3. To get started on a pull request, fork the repository on GitHub, create a new branch, and make updates.
|
|
168
184
|
4. Write unit tests, ensure the code is 100% covered, update documentation where necessary, and format and style the code correctly.
|
|
@@ -16,6 +16,17 @@ Quick Links
|
|
|
16
16
|
- `Contributing`_
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
Recent Updates
|
|
20
|
+
**************
|
|
21
|
+
|
|
22
|
+
v0.3.0 (2023-3-21)
|
|
23
|
+
==================
|
|
24
|
+
|
|
25
|
+
* Tag and field keys can be compacted when using CSVStorage, saving potentially many bytes per Point (resolves issue #32).
|
|
26
|
+
* Fixed bug that causes tag values of '' to be serialized as "_none" (resolves issue #33).
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
19
30
|
Installation
|
|
20
31
|
************
|
|
21
32
|
|
|
@@ -78,7 +89,7 @@ Writing to TinyFlux
|
|
|
78
89
|
... tags={"room": "bedroom"},
|
|
79
90
|
... fields={"temp": 72.0}
|
|
80
91
|
... )
|
|
81
|
-
>>> db.insert(p)
|
|
92
|
+
>>> db.insert(p, compact_key_prefixes=True)
|
|
82
93
|
|
|
83
94
|
|
|
84
95
|
Querying TinyFlux
|
|
@@ -115,6 +126,11 @@ The `examples <https://github.com/citrusvanilla/tinyflux/tree/master/examples>`_
|
|
|
115
126
|
2. `Local Analytics Workflow with a TinyFlux Database <https://github.com/citrusvanilla/tinyflux/blob/master/examples/2_analytics_workflow.ipynb>`_
|
|
116
127
|
3. `TinyFlux as a MQTT Datastore for IoT Devices <https://github.com/citrusvanilla/tinyflux/blob/master/examples/3_iot_datastore_with_mqtt.py>`_
|
|
117
128
|
|
|
129
|
+
Tips
|
|
130
|
+
****
|
|
131
|
+
|
|
132
|
+
Checkout some tips for working with TinyFlux `here <https://tinyflux.readthedocs.io/en/latest/tips.html>`_.
|
|
133
|
+
|
|
118
134
|
|
|
119
135
|
TinyFlux Across the Internet
|
|
120
136
|
****************************
|
|
@@ -130,7 +146,7 @@ Contributing
|
|
|
130
146
|
|
|
131
147
|
New ideas, new developer tools, improvements, and bugfixes are always welcome. Follow these guidelines before getting started:
|
|
132
148
|
|
|
133
|
-
1. Make sure to read `Getting Started <https://tinyflux.readthedocs.io/en/latest/getting-started.html>`_ and the `Contributing <https://tinyflux.readthedocs.io/en/latest/contributing-
|
|
149
|
+
1. Make sure to read `Getting Started <https://tinyflux.readthedocs.io/en/latest/getting-started.html>`_ and the `Contributing Tooling and Conventions <https://tinyflux.readthedocs.io/en/latest/contributing-tooling.html>`_ section of the documentation.
|
|
134
150
|
2. Check GitHub for `existing open issues <https://github.com/citrusvanilla/tinyflux/issues>`_, `open a new issue <https://github.com/citrusvanilla/tinyflux/issues/new>`_ or `start a new discussion <https://github.com/citrusvanilla/tinyflux/discussions/new>`_.
|
|
135
151
|
3. To get started on a pull request, fork the repository on GitHub, create a new branch, and make updates.
|
|
136
152
|
4. Write unit tests, ensure the code is 100% covered, update documentation where necessary, and format and style the code correctly.
|
|
@@ -180,6 +180,12 @@ def test_tags():
|
|
|
180
180
|
{
|
|
181
181
|
"key1": "value1",
|
|
182
182
|
},
|
|
183
|
+
{
|
|
184
|
+
"key1": "",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"key1": None,
|
|
188
|
+
},
|
|
183
189
|
{"key2": "value2", "key3": "value3"},
|
|
184
190
|
]
|
|
185
191
|
|
|
@@ -414,3 +420,73 @@ def test_serialize_zero_values():
|
|
|
414
420
|
and new_p.fields["b"] == 0.0
|
|
415
421
|
and new_p.fields["c"] is None
|
|
416
422
|
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_serialize_none_values():
|
|
426
|
+
"""Test serializing/deserializing None values."""
|
|
427
|
+
p = Point(fields={"a": None}, tags={"a": None})
|
|
428
|
+
s = p._serialize_to_list()
|
|
429
|
+
assert s[3] == p._none_str and s[5] == p._none_str
|
|
430
|
+
|
|
431
|
+
new_p = Point()._deserialize_from_list(s)
|
|
432
|
+
|
|
433
|
+
assert p == new_p
|
|
434
|
+
|
|
435
|
+
assert new_p.fields["a"] is None and new_p.tags["a"] is None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_serialize_empty_strings():
|
|
439
|
+
"""Test serializing/deserializing empty string tag values."""
|
|
440
|
+
p = Point(tags={"a": ""})
|
|
441
|
+
s = p._serialize_to_list()
|
|
442
|
+
assert s[3] == ""
|
|
443
|
+
|
|
444
|
+
new_p = Point()._deserialize_from_list(s)
|
|
445
|
+
|
|
446
|
+
assert p == new_p
|
|
447
|
+
|
|
448
|
+
assert new_p.tags["a"] == ""
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def test_compact_tag_keys():
|
|
452
|
+
"""Test compact tag keys in CSV Storage."""
|
|
453
|
+
p = Point(fields={"a": 0, "b": 0.0, "c": None})
|
|
454
|
+
s = p._serialize_to_list(compact_key_prefixes=True)
|
|
455
|
+
s1 = p._serialize_to_list(compact_key_prefixes=False)
|
|
456
|
+
|
|
457
|
+
assert all(s[i].startswith(p._compact_field_key_prefix) for i in (2, 4, 6))
|
|
458
|
+
|
|
459
|
+
new_p = Point()._deserialize_from_list(s)
|
|
460
|
+
new_p1 = Point()._deserialize_from_list(s1)
|
|
461
|
+
|
|
462
|
+
assert p == new_p == new_p1
|
|
463
|
+
|
|
464
|
+
assert all(i in new_p.fields for i in ("a", "b", "c"))
|
|
465
|
+
|
|
466
|
+
assert (
|
|
467
|
+
new_p.fields["a"] == 0
|
|
468
|
+
and new_p.fields["b"] == 0.0
|
|
469
|
+
and new_p.fields["c"] is None
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_compact_field_keys():
|
|
474
|
+
"""Test compact tag keys in CSV Storage."""
|
|
475
|
+
p = Point(tags={"a": "aa", "b": "bb", "c": None})
|
|
476
|
+
s = p._serialize_to_list(compact_key_prefixes=True)
|
|
477
|
+
s1 = p._serialize_to_list(compact_key_prefixes=False)
|
|
478
|
+
|
|
479
|
+
assert all(s[i].startswith(p._compact_tag_key_prefix) for i in (2, 4, 6))
|
|
480
|
+
|
|
481
|
+
new_p = Point()._deserialize_from_list(s)
|
|
482
|
+
new_p1 = Point()._deserialize_from_list(s1)
|
|
483
|
+
|
|
484
|
+
assert p == new_p == new_p1
|
|
485
|
+
|
|
486
|
+
assert all(i in new_p.tags for i in ("a", "b", "c"))
|
|
487
|
+
|
|
488
|
+
assert (
|
|
489
|
+
new_p.tags["a"] == "aa"
|
|
490
|
+
and new_p.tags["b"] == "bb"
|
|
491
|
+
and new_p.tags["c"] is None
|
|
492
|
+
)
|
|
@@ -59,19 +59,11 @@ def test_repr():
|
|
|
59
59
|
c5 = q2 | q4
|
|
60
60
|
c6 = ~q3
|
|
61
61
|
|
|
62
|
-
rc1 = (
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
rc2 = (
|
|
66
|
-
f"CompoundQuery({c2.operator.__name__}, 'SimpleQuery', 'SimpleQuery')"
|
|
67
|
-
)
|
|
62
|
+
rc1 = f"CompoundQuery({c1.operator.__name__}, 'SimpleQuery', 'SimpleQuery')"
|
|
63
|
+
rc2 = f"CompoundQuery({c2.operator.__name__}, 'SimpleQuery', 'SimpleQuery')"
|
|
68
64
|
rc3 = f"CompoundQuery({c3.operator.__name__}, 'SimpleQuery')"
|
|
69
|
-
rc4 = (
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
rc5 = (
|
|
73
|
-
f"CompoundQuery({c5.operator.__name__}, 'SimpleQuery', 'SimpleQuery')"
|
|
74
|
-
)
|
|
65
|
+
rc4 = f"CompoundQuery({c4.operator.__name__}, 'SimpleQuery', 'SimpleQuery')"
|
|
66
|
+
rc5 = f"CompoundQuery({c5.operator.__name__}, 'SimpleQuery', 'SimpleQuery')"
|
|
75
67
|
rc6 = "CompoundQuery(not_, 'SimpleQuery')"
|
|
76
68
|
|
|
77
69
|
assert repr(c1) == rc1
|
|
@@ -854,9 +846,7 @@ def test_basequery():
|
|
|
854
846
|
):
|
|
855
847
|
q & (TagQuery().a == "b")
|
|
856
848
|
|
|
857
|
-
with pytest.raises(
|
|
858
|
-
RuntimeError, match="Cannot logical-OR an empty query."
|
|
859
|
-
):
|
|
849
|
+
with pytest.raises(RuntimeError, match="Cannot logical-OR an empty query."):
|
|
860
850
|
q | (TagQuery().a == "b")
|
|
861
851
|
|
|
862
852
|
with pytest.raises(
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import random
|
|
6
6
|
import re
|
|
7
7
|
import tempfile
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
10
11
|
|
|
@@ -238,7 +239,9 @@ def test_subclassing_storage():
|
|
|
238
239
|
"""Deserialize storage."""
|
|
239
240
|
...
|
|
240
241
|
|
|
241
|
-
def _serialize_point(
|
|
242
|
+
def _serialize_point(
|
|
243
|
+
self, point: Point, *args: Any, **kwargs: Any
|
|
244
|
+
) -> None:
|
|
242
245
|
"""Serialize Point."""
|
|
243
246
|
...
|
|
244
247
|
|
|
@@ -639,3 +642,26 @@ def test_temporary_storage(tmpdir):
|
|
|
639
642
|
# Exception should be thrown if temp storage not initialized.
|
|
640
643
|
with pytest.raises(IOError):
|
|
641
644
|
storage.append([], temporary=True)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def test_compact_key_prefixes(tmpdir):
|
|
648
|
+
"""Test compact keys option."""
|
|
649
|
+
# Memory.
|
|
650
|
+
m = MemoryStorage()
|
|
651
|
+
p = Point(tags={"a": "aa"}, fields={"a": 1})
|
|
652
|
+
assert (
|
|
653
|
+
m._serialize_point(p, compact_key_prefixes=True)
|
|
654
|
+
== m._serialize_point(p, compact_key_prefixes=False)
|
|
655
|
+
== m._serialize_point(p)
|
|
656
|
+
== p
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
# CSV.
|
|
660
|
+
path = os.path.join(tmpdir, "test.csv")
|
|
661
|
+
m = CSVStorage(path)
|
|
662
|
+
assert m._serialize_point(
|
|
663
|
+
p, compact_key_prefixes=True
|
|
664
|
+
) == p._serialize_to_list(compact_key_prefixes=True)
|
|
665
|
+
assert m._serialize_point(
|
|
666
|
+
p, compact_key_prefixes=False
|
|
667
|
+
) == p._serialize_to_list(compact_key_prefixes=False)
|
|
@@ -897,9 +897,9 @@ def test_update():
|
|
|
897
897
|
db = TinyFlux(storage=MemoryStorage, auto_index=False)
|
|
898
898
|
|
|
899
899
|
# Insert some points.
|
|
900
|
-
t1, t2 = datetime.now(timezone.utc), datetime.now(
|
|
901
|
-
|
|
902
|
-
)
|
|
900
|
+
t1, t2 = datetime.now(timezone.utc), datetime.now(timezone.utc) + timedelta(
|
|
901
|
+
days=1
|
|
902
|
+
)
|
|
903
903
|
|
|
904
904
|
p1 = Point(
|
|
905
905
|
time=t1,
|
|
@@ -1027,13 +1027,9 @@ def test_update():
|
|
|
1027
1027
|
with pytest.raises(
|
|
1028
1028
|
ValueError, match="Measurement must update to a string."
|
|
1029
1029
|
):
|
|
1030
|
-
db.update(
|
|
1031
|
-
FieldQuery().fk2 == 2, measurement=lambda _: {"golden bears"}
|
|
1032
|
-
)
|
|
1030
|
+
db.update(FieldQuery().fk2 == 2, measurement=lambda _: {"golden bears"})
|
|
1033
1031
|
|
|
1034
|
-
with pytest.raises(
|
|
1035
|
-
ValueError, match="Tags must update to a valid TagSet."
|
|
1036
|
-
):
|
|
1032
|
+
with pytest.raises(ValueError, match="Tags must update to a valid TagSet."):
|
|
1037
1033
|
db.update(FieldQuery().fk2 == 2, tags=lambda _: {"golden bears"})
|
|
1038
1034
|
|
|
1039
1035
|
with pytest.raises(
|
|
@@ -1174,3 +1170,61 @@ def test_open_unindexed_storage_with_autoindex_OFF(tmpdir):
|
|
|
1174
1170
|
db.all()
|
|
1175
1171
|
assert not db._index.valid
|
|
1176
1172
|
assert db.index.empty
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def test_insert_compact_keys(tmpdir):
|
|
1176
|
+
"""Test insert method."""
|
|
1177
|
+
# Memory storage.
|
|
1178
|
+
db = TinyFlux(storage=MemoryStorage)
|
|
1179
|
+
p = Point(tags={"a": "aa"}, fields={"a": 1})
|
|
1180
|
+
p1 = Point(tags={"a": "aa"}, fields={"a": 1})
|
|
1181
|
+
p2 = Point(tags={"a": "aa"}, fields={"a": 1})
|
|
1182
|
+
|
|
1183
|
+
# Insert into empty db.
|
|
1184
|
+
assert db.insert(p, compact_key_prefixes=True) == 1
|
|
1185
|
+
assert db.index.valid and not db.index.empty
|
|
1186
|
+
assert len(db.index) == 1
|
|
1187
|
+
assert len(db) == 1
|
|
1188
|
+
|
|
1189
|
+
assert db.all()[0] == p
|
|
1190
|
+
|
|
1191
|
+
assert db.insert_multiple([p1, p2], compact_key_prefixes=True) == 2
|
|
1192
|
+
assert db.index.valid and not db.index.empty
|
|
1193
|
+
assert len(db.index) == 3
|
|
1194
|
+
assert len(db) == 3
|
|
1195
|
+
|
|
1196
|
+
assert db.all() == [p, p1, p2]
|
|
1197
|
+
|
|
1198
|
+
# CSVStorage
|
|
1199
|
+
path = os.path.join(tmpdir, "test.csv")
|
|
1200
|
+
|
|
1201
|
+
db = TinyFlux(path)
|
|
1202
|
+
assert db.index.valid
|
|
1203
|
+
assert db.index.empty
|
|
1204
|
+
|
|
1205
|
+
assert db.insert(p, compact_key_prefixes=True) == 1
|
|
1206
|
+
assert db.index.valid and not db.index.empty
|
|
1207
|
+
assert len(db.index) == 1
|
|
1208
|
+
assert len(db) == 1
|
|
1209
|
+
|
|
1210
|
+
f = open(path, "r+")
|
|
1211
|
+
rows = list(csv.reader(f))
|
|
1212
|
+
|
|
1213
|
+
assert rows[0][2].startswith(p._compact_tag_key_prefix)
|
|
1214
|
+
assert rows[0][4].startswith(p._compact_field_key_prefix)
|
|
1215
|
+
|
|
1216
|
+
assert db.all()[0] == p
|
|
1217
|
+
|
|
1218
|
+
assert db.insert_multiple([p1, p2], compact_key_prefixes=True) == 2
|
|
1219
|
+
assert db.index.valid and not db.index.empty
|
|
1220
|
+
assert len(db.index) == 3
|
|
1221
|
+
assert len(db) == 3
|
|
1222
|
+
|
|
1223
|
+
f = open(path, "r+")
|
|
1224
|
+
rows = list(csv.reader(f))
|
|
1225
|
+
|
|
1226
|
+
for row in rows:
|
|
1227
|
+
assert row[2].startswith(p._compact_tag_key_prefix)
|
|
1228
|
+
assert row[4].startswith(p._compact_field_key_prefix)
|
|
1229
|
+
|
|
1230
|
+
assert db.all() == [p, p1, p2]
|
|
@@ -153,9 +153,7 @@ class TinyFlux:
|
|
|
153
153
|
_measurements: Dict[str, Measurement]
|
|
154
154
|
_open: bool
|
|
155
155
|
|
|
156
|
-
def __init__(
|
|
157
|
-
self: Any, *args: Any, auto_index: bool = True, **kwargs: Any
|
|
158
|
-
):
|
|
156
|
+
def __init__(self: Any, *args: Any, auto_index: bool = True, **kwargs: Any):
|
|
159
157
|
"""Initialize a new instance of TinyFlux.
|
|
160
158
|
|
|
161
159
|
If 'auto_index' is set to True, an index will be built in-memory for
|
|
@@ -281,9 +279,7 @@ class TinyFlux:
|
|
|
281
279
|
return
|
|
282
280
|
|
|
283
281
|
@read_op
|
|
284
|
-
def contains(
|
|
285
|
-
self, query: Query, measurement: Optional[str] = None
|
|
286
|
-
) -> bool:
|
|
282
|
+
def contains(self, query: Query, measurement: Optional[str] = None) -> bool:
|
|
287
283
|
"""Check whether the database contains a point matching a query.
|
|
288
284
|
|
|
289
285
|
Defines a function that iterates over storage items and submits it to
|
|
@@ -674,12 +670,18 @@ class TinyFlux:
|
|
|
674
670
|
return rst
|
|
675
671
|
|
|
676
672
|
@append_op
|
|
677
|
-
def insert(
|
|
673
|
+
def insert(
|
|
674
|
+
self,
|
|
675
|
+
point: Point,
|
|
676
|
+
measurement: Optional[str] = None,
|
|
677
|
+
compact_key_prefixes: bool = False,
|
|
678
|
+
) -> int:
|
|
678
679
|
"""Insert a Point into the database.
|
|
679
680
|
|
|
680
681
|
Args:
|
|
681
682
|
point: A Point object.
|
|
682
683
|
measurement: An optional measurement to filter by.
|
|
684
|
+
compact_key_prefixes: Use compact key prefixes in relevant storages.
|
|
683
685
|
|
|
684
686
|
Returns:
|
|
685
687
|
1 if success.
|
|
@@ -688,17 +690,21 @@ class TinyFlux:
|
|
|
688
690
|
OSError if storage cannot be appendex to.
|
|
689
691
|
TypeError if point is not a Point instance.
|
|
690
692
|
"""
|
|
691
|
-
return self._insert_helper([point], measurement)
|
|
693
|
+
return self._insert_helper([point], measurement, compact_key_prefixes)
|
|
692
694
|
|
|
693
695
|
@append_op
|
|
694
696
|
def insert_multiple(
|
|
695
|
-
self,
|
|
697
|
+
self,
|
|
698
|
+
points: Iterable[Any],
|
|
699
|
+
measurement: Optional[str] = None,
|
|
700
|
+
compact_key_prefixes: bool = False,
|
|
696
701
|
) -> int:
|
|
697
702
|
"""Insert Points into the database.
|
|
698
703
|
|
|
699
704
|
Args:
|
|
700
705
|
points: An iterable of Point objects.
|
|
701
706
|
measurement: An optional measurement to insert Points into.
|
|
707
|
+
compact_key_prefixes: Use compact key prefixes in relevant storages.
|
|
702
708
|
|
|
703
709
|
Returns:
|
|
704
710
|
The count of inserted points.
|
|
@@ -707,7 +713,7 @@ class TinyFlux:
|
|
|
707
713
|
OSError if storage cannot be appendex to.
|
|
708
714
|
TypeError if point is not a Point instance.
|
|
709
715
|
"""
|
|
710
|
-
return self._insert_helper(points, measurement)
|
|
716
|
+
return self._insert_helper(points, measurement, compact_key_prefixes)
|
|
711
717
|
|
|
712
718
|
def measurement(self, name: str, **kwargs: Any) -> Measurement:
|
|
713
719
|
"""Return a reference to a measurement in this database.
|
|
@@ -905,9 +911,7 @@ class TinyFlux:
|
|
|
905
911
|
raise ValueError("'keys' must be a string or iterable of strings.")
|
|
906
912
|
|
|
907
913
|
keys: List[Any] = (
|
|
908
|
-
[select_keys]
|
|
909
|
-
if isinstance(select_keys, str)
|
|
910
|
-
else list(select_keys)
|
|
914
|
+
[select_keys] if isinstance(select_keys, str) else list(select_keys)
|
|
911
915
|
)
|
|
912
916
|
|
|
913
917
|
# Validate bad keys.
|
|
@@ -1067,9 +1071,7 @@ class TinyFlux:
|
|
|
1067
1071
|
datetime, Callable[[Optional[datetime]], datetime], None
|
|
1068
1072
|
] = None,
|
|
1069
1073
|
measurement: Union[str, Callable[[Optional[str]], str], None] = None,
|
|
1070
|
-
tags: Union[
|
|
1071
|
-
TagSet, Callable[[Mapping[Any, Any]], TagSet], None
|
|
1072
|
-
] = None,
|
|
1074
|
+
tags: Union[TagSet, Callable[[Mapping[Any, Any]], TagSet], None] = None,
|
|
1073
1075
|
fields: Union[
|
|
1074
1076
|
FieldSet, Callable[[Mapping[Any, Any]], FieldSet], None
|
|
1075
1077
|
] = None,
|
|
@@ -1172,9 +1174,7 @@ class TinyFlux:
|
|
|
1172
1174
|
try:
|
|
1173
1175
|
point.measurement = measurement(point.measurement)
|
|
1174
1176
|
except ValueError:
|
|
1175
|
-
raise ValueError(
|
|
1176
|
-
"Measurement must update to a string."
|
|
1177
|
-
)
|
|
1177
|
+
raise ValueError("Measurement must update to a string.")
|
|
1178
1178
|
else:
|
|
1179
1179
|
point.measurement = measurement
|
|
1180
1180
|
|
|
@@ -1204,13 +1204,17 @@ class TinyFlux:
|
|
|
1204
1204
|
return perform_update
|
|
1205
1205
|
|
|
1206
1206
|
def _insert_helper(
|
|
1207
|
-
self,
|
|
1207
|
+
self,
|
|
1208
|
+
points: Iterable[Any],
|
|
1209
|
+
measurement: Optional[str],
|
|
1210
|
+
compact_key_prefixes: bool = False,
|
|
1208
1211
|
) -> int:
|
|
1209
1212
|
"""Insert point helper.
|
|
1210
1213
|
|
|
1211
1214
|
Args:
|
|
1212
1215
|
updater: Update function.
|
|
1213
1216
|
measurement: Optional measurement to insert into.
|
|
1217
|
+
compact_key_prefixes: Use compact key prefixes in relevant storages.
|
|
1214
1218
|
|
|
1215
1219
|
Returns:
|
|
1216
1220
|
Count of number of updates made.
|
|
@@ -1233,7 +1237,13 @@ class TinyFlux:
|
|
|
1233
1237
|
point.time = t
|
|
1234
1238
|
|
|
1235
1239
|
# Insert the points into storage.
|
|
1236
|
-
self._storage.append(
|
|
1240
|
+
self._storage.append(
|
|
1241
|
+
[
|
|
1242
|
+
self._storage._serialize_point(
|
|
1243
|
+
point, compact_key_prefixes=compact_key_prefixes
|
|
1244
|
+
)
|
|
1245
|
+
]
|
|
1246
|
+
)
|
|
1237
1247
|
|
|
1238
1248
|
# Check index.
|
|
1239
1249
|
if self._auto_index and self._index.valid:
|
|
@@ -47,7 +47,7 @@ def validate_tags(tags: Any) -> None:
|
|
|
47
47
|
|
|
48
48
|
# Check values.
|
|
49
49
|
if not all(i is None or isinstance(i, str) for i in tags.values()):
|
|
50
|
-
raise ValueError("Tag set must contain only string values.")
|
|
50
|
+
raise ValueError("Tag set must contain only string values or None.")
|
|
51
51
|
|
|
52
52
|
return
|
|
53
53
|
|
|
@@ -74,7 +74,9 @@ def validate_fields(fields: Any) -> None:
|
|
|
74
74
|
continue
|
|
75
75
|
|
|
76
76
|
if isinstance(i, bool) or not isinstance(i, (int, float)):
|
|
77
|
-
raise ValueError(
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"Field set must contain only numeric values or None."
|
|
79
|
+
)
|
|
78
80
|
|
|
79
81
|
return
|
|
80
82
|
|
|
@@ -99,6 +101,11 @@ class Point:
|
|
|
99
101
|
_valid_kwargs = set(["time", "measurement", "tags", "fields"])
|
|
100
102
|
__slots__ = ("_time", "_measurement", "_tags", "_fields")
|
|
101
103
|
|
|
104
|
+
_default_tag_key_prefix = "_tag_"
|
|
105
|
+
_default_field_key_prefix = "_field_"
|
|
106
|
+
_compact_tag_key_prefix = "t_"
|
|
107
|
+
_compact_field_key_prefix = "f_"
|
|
108
|
+
|
|
102
109
|
_time: Optional[datetime]
|
|
103
110
|
_measurement: str
|
|
104
111
|
_tags: TagSet
|
|
@@ -205,18 +212,14 @@ class Point:
|
|
|
205
212
|
repr_str = "Point("
|
|
206
213
|
|
|
207
214
|
# Add time.
|
|
208
|
-
repr_str += (
|
|
209
|
-
f"time={self._time.isoformat() if self._time else 'None'}, "
|
|
210
|
-
)
|
|
215
|
+
repr_str += f"time={self._time.isoformat() if self._time else 'None'}, "
|
|
211
216
|
|
|
212
217
|
# Add measurement.
|
|
213
218
|
repr_str += f"measurement={self._measurement}"
|
|
214
219
|
|
|
215
220
|
# Add tags.
|
|
216
221
|
if self._tags:
|
|
217
|
-
tags_str = "; ".join(
|
|
218
|
-
f"{k}:{str(v)}" for k, v in self._tags.items()
|
|
219
|
-
)
|
|
222
|
+
tags_str = "; ".join(f"{k}:{str(v)}" for k, v in self._tags.items())
|
|
220
223
|
repr_str += f", tags={tags_str}"
|
|
221
224
|
|
|
222
225
|
# Add fields.
|
|
@@ -239,10 +242,6 @@ class Point:
|
|
|
239
242
|
|
|
240
243
|
Returns:
|
|
241
244
|
A Point object.
|
|
242
|
-
|
|
243
|
-
Raises:
|
|
244
|
-
ValueError: Deserializing encounters a bad type.
|
|
245
|
-
RuntimeError: Deserializing encounters an unexpected column.
|
|
246
245
|
"""
|
|
247
246
|
p_time = datetime.fromisoformat(row[0]).replace(tzinfo=timezone.utc)
|
|
248
247
|
p_measurement = row[1]
|
|
@@ -254,14 +253,31 @@ class Point:
|
|
|
254
253
|
i = 2
|
|
255
254
|
|
|
256
255
|
# Check for tag key/values.
|
|
257
|
-
while i < row_len
|
|
258
|
-
|
|
256
|
+
while i < row_len:
|
|
257
|
+
# Default tag key prefix is "_tag_" (most-used case).
|
|
258
|
+
if row[i][1] == "t":
|
|
259
|
+
t_key = row[i][len(self._default_tag_key_prefix) :]
|
|
260
|
+
# Compact tag key prefix is "t_".
|
|
261
|
+
elif row[i][0] == "t":
|
|
262
|
+
t_key = row[i][len(self._compact_tag_key_prefix) :]
|
|
263
|
+
# Otherwise, its a field -> continue.
|
|
264
|
+
else:
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
t_value = None if row[i + 1] == self._none_str else str(row[i + 1])
|
|
259
268
|
p_tags[t_key] = t_value
|
|
260
269
|
i += 2
|
|
261
270
|
|
|
262
271
|
# Check for field key/values.
|
|
263
272
|
while i < row_len:
|
|
264
|
-
|
|
273
|
+
# Default field key prefix is "_field_" (most-used case).
|
|
274
|
+
if row[i][1] == "f":
|
|
275
|
+
f_key = row[i][len(self._default_field_key_prefix) :]
|
|
276
|
+
# Compact field key prefix is "f_".
|
|
277
|
+
else:
|
|
278
|
+
f_key = row[i][len(self._compact_field_key_prefix) :]
|
|
279
|
+
|
|
280
|
+
f_value = row[i + 1]
|
|
265
281
|
|
|
266
282
|
# Value is an integer.
|
|
267
283
|
if f_value.isdigit() or (
|
|
@@ -288,27 +304,55 @@ class Point:
|
|
|
288
304
|
|
|
289
305
|
return self
|
|
290
306
|
|
|
291
|
-
def _serialize_to_list(
|
|
307
|
+
def _serialize_to_list(
|
|
308
|
+
self, compact_key_prefixes: bool = False
|
|
309
|
+
) -> Sequence[str]:
|
|
292
310
|
"""Serialize a Point to a tuple of strings.
|
|
293
311
|
|
|
312
|
+
Args:
|
|
313
|
+
compact_key_prefixes: Use compact key prefixes.
|
|
314
|
+
|
|
294
315
|
Returns:
|
|
295
316
|
A well-formed tuple of strings, representing a Point.
|
|
296
317
|
|
|
297
318
|
Usage:
|
|
298
319
|
>>> sp = Point()._serialize_to_list()
|
|
299
320
|
"""
|
|
321
|
+
# Time.
|
|
300
322
|
t = (
|
|
301
323
|
self._time.replace(tzinfo=None).isoformat()
|
|
302
324
|
if self._time
|
|
303
325
|
else self._none_str
|
|
304
326
|
)
|
|
327
|
+
|
|
328
|
+
# Measurement.
|
|
305
329
|
m = str(self._measurement or self._none_str)
|
|
330
|
+
|
|
331
|
+
# Tags.
|
|
332
|
+
tag_key_prefix = (
|
|
333
|
+
self._compact_tag_key_prefix
|
|
334
|
+
if compact_key_prefixes
|
|
335
|
+
else self._default_tag_key_prefix
|
|
336
|
+
)
|
|
306
337
|
tags = (
|
|
307
|
-
(
|
|
338
|
+
(
|
|
339
|
+
f"{tag_key_prefix}{k}",
|
|
340
|
+
self._none_str if v is None else str(v),
|
|
341
|
+
)
|
|
308
342
|
for k, v in self._tags.items()
|
|
309
343
|
)
|
|
344
|
+
|
|
345
|
+
# Fields.
|
|
346
|
+
field_key_prefix = (
|
|
347
|
+
self._compact_field_key_prefix
|
|
348
|
+
if compact_key_prefixes
|
|
349
|
+
else self._default_field_key_prefix
|
|
350
|
+
)
|
|
310
351
|
fields = (
|
|
311
|
-
(
|
|
352
|
+
(
|
|
353
|
+
f"{field_key_prefix}{k}",
|
|
354
|
+
self._none_str if v is None else str(float(v)),
|
|
355
|
+
)
|
|
312
356
|
for k, v in self._fields.items()
|
|
313
357
|
)
|
|
314
358
|
|
|
@@ -453,9 +453,7 @@ class BaseQuery:
|
|
|
453
453
|
and rhs
|
|
454
454
|
and not isinstance(rhs, str)
|
|
455
455
|
):
|
|
456
|
-
raise TypeError(
|
|
457
|
-
"MeasurementQuery comparison value must be string."
|
|
458
|
-
)
|
|
456
|
+
raise TypeError("MeasurementQuery comparison value must be string.")
|
|
459
457
|
|
|
460
458
|
# Validation for tags.
|
|
461
459
|
if self._point_attr == "_tags" and rhs and not isinstance(rhs, str):
|
|
@@ -144,7 +144,7 @@ class Storage(ABC): # pragma: no cover
|
|
|
144
144
|
...
|
|
145
145
|
|
|
146
146
|
@abstractmethod
|
|
147
|
-
def _serialize_point(self, point: Point) -> Any:
|
|
147
|
+
def _serialize_point(self, point: Point, *args: Any, **kwargs: Any) -> Any:
|
|
148
148
|
"""Serialize a point to an item for storage."""
|
|
149
149
|
...
|
|
150
150
|
|
|
@@ -372,10 +372,12 @@ class CSVStorage(Storage):
|
|
|
372
372
|
return
|
|
373
373
|
|
|
374
374
|
def _serialize_point(
|
|
375
|
-
self, point: Point
|
|
375
|
+
self, point: Point, *args: Any, **kwargs: Any
|
|
376
376
|
) -> Sequence[Union[str, float, int]]:
|
|
377
377
|
"""Serialize a point to an item for storage."""
|
|
378
|
-
return point._serialize_to_list(
|
|
378
|
+
return point._serialize_to_list(
|
|
379
|
+
compact_key_prefixes=kwargs.pop("compact_key_prefixes", False)
|
|
380
|
+
)
|
|
379
381
|
|
|
380
382
|
def _swap_temp_with_primary(self) -> None:
|
|
381
383
|
"""Swap primary data store with temporary data store."""
|
|
@@ -525,7 +527,9 @@ class MemoryStorage(Storage):
|
|
|
525
527
|
"""Initialize temporary storage."""
|
|
526
528
|
self._temp_memory = []
|
|
527
529
|
|
|
528
|
-
def _serialize_point(
|
|
530
|
+
def _serialize_point(
|
|
531
|
+
self, point: Point, *args: Any, **kwargs: Any
|
|
532
|
+
) -> MemStorageItem:
|
|
529
533
|
"""Serialize a point to an item for storage."""
|
|
530
534
|
return point
|
|
531
535
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinyflux
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.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
|
|
@@ -48,6 +48,17 @@ Quick Links
|
|
|
48
48
|
- `Contributing`_
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
Recent Updates
|
|
52
|
+
**************
|
|
53
|
+
|
|
54
|
+
v0.3.0 (2023-3-21)
|
|
55
|
+
==================
|
|
56
|
+
|
|
57
|
+
* Tag and field keys can be compacted when using CSVStorage, saving potentially many bytes per Point (resolves issue #32).
|
|
58
|
+
* Fixed bug that causes tag values of '' to be serialized as "_none" (resolves issue #33).
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
51
62
|
Installation
|
|
52
63
|
************
|
|
53
64
|
|
|
@@ -110,7 +121,7 @@ Writing to TinyFlux
|
|
|
110
121
|
... tags={"room": "bedroom"},
|
|
111
122
|
... fields={"temp": 72.0}
|
|
112
123
|
... )
|
|
113
|
-
>>> db.insert(p)
|
|
124
|
+
>>> db.insert(p, compact_key_prefixes=True)
|
|
114
125
|
|
|
115
126
|
|
|
116
127
|
Querying TinyFlux
|
|
@@ -147,6 +158,11 @@ The `examples <https://github.com/citrusvanilla/tinyflux/tree/master/examples>`_
|
|
|
147
158
|
2. `Local Analytics Workflow with a TinyFlux Database <https://github.com/citrusvanilla/tinyflux/blob/master/examples/2_analytics_workflow.ipynb>`_
|
|
148
159
|
3. `TinyFlux as a MQTT Datastore for IoT Devices <https://github.com/citrusvanilla/tinyflux/blob/master/examples/3_iot_datastore_with_mqtt.py>`_
|
|
149
160
|
|
|
161
|
+
Tips
|
|
162
|
+
****
|
|
163
|
+
|
|
164
|
+
Checkout some tips for working with TinyFlux `here <https://tinyflux.readthedocs.io/en/latest/tips.html>`_.
|
|
165
|
+
|
|
150
166
|
|
|
151
167
|
TinyFlux Across the Internet
|
|
152
168
|
****************************
|
|
@@ -162,7 +178,7 @@ Contributing
|
|
|
162
178
|
|
|
163
179
|
New ideas, new developer tools, improvements, and bugfixes are always welcome. Follow these guidelines before getting started:
|
|
164
180
|
|
|
165
|
-
1. Make sure to read `Getting Started <https://tinyflux.readthedocs.io/en/latest/getting-started.html>`_ and the `Contributing <https://tinyflux.readthedocs.io/en/latest/contributing-
|
|
181
|
+
1. Make sure to read `Getting Started <https://tinyflux.readthedocs.io/en/latest/getting-started.html>`_ and the `Contributing Tooling and Conventions <https://tinyflux.readthedocs.io/en/latest/contributing-tooling.html>`_ section of the documentation.
|
|
166
182
|
2. Check GitHub for `existing open issues <https://github.com/citrusvanilla/tinyflux/issues>`_, `open a new issue <https://github.com/citrusvanilla/tinyflux/issues/new>`_ or `start a new discussion <https://github.com/citrusvanilla/tinyflux/discussions/new>`_.
|
|
167
183
|
3. To get started on a pull request, fork the repository on GitHub, create a new branch, and make updates.
|
|
168
184
|
4. Write unit tests, ensure the code is 100% covered, update documentation where necessary, and format and style the code correctly.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v0.3.0
|
tinyflux-0.2.6/version.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
v0.2.6
|
|
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
|