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.
Files changed (30) hide show
  1. {tinyflux-0.2.6/tinyflux.egg-info → tinyflux-0.3.0}/PKG-INFO +19 -3
  2. {tinyflux-0.2.6 → tinyflux-0.3.0}/README.rst +18 -2
  3. {tinyflux-0.2.6 → tinyflux-0.3.0}/pyproject.toml +1 -1
  4. {tinyflux-0.2.6 → tinyflux-0.3.0}/setup.cfg +1 -1
  5. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_point.py +76 -0
  6. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_queries.py +5 -15
  7. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_storages.py +27 -1
  8. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_tinyflux.py +63 -9
  9. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/database.py +31 -21
  10. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/point.py +62 -18
  11. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/queries.py +1 -3
  12. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/storages.py +8 -4
  13. {tinyflux-0.2.6 → tinyflux-0.3.0/tinyflux.egg-info}/PKG-INFO +19 -3
  14. tinyflux-0.3.0/version.txt +1 -0
  15. tinyflux-0.2.6/version.txt +0 -1
  16. {tinyflux-0.2.6 → tinyflux-0.3.0}/LICENSE +0 -0
  17. {tinyflux-0.2.6 → tinyflux-0.3.0}/MANIFEST.in +0 -0
  18. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/__init__.py +0 -0
  19. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/conftest.py +0 -0
  20. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_index.py +0 -0
  21. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_measurement.py +0 -0
  22. {tinyflux-0.2.6 → tinyflux-0.3.0}/tests/test_utils.py +0 -0
  23. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/__init__.py +0 -0
  24. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/index.py +0 -0
  25. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/measurement.py +0 -0
  26. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/py.typed +0 -0
  27. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux/utils.py +0 -0
  28. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux.egg-info/SOURCES.txt +0 -0
  29. {tinyflux-0.2.6 → tinyflux-0.3.0}/tinyflux.egg-info/dependency_links.txt +0 -0
  30. {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.2.6
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-philosophy.html>`_ section of the documentation.
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-philosophy.html>`_ section of the documentation.
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.
@@ -1,5 +1,5 @@
1
1
  [tool.black]
2
- line-length = 79
2
+ line-length = 80
3
3
  exclude = '(docs|\.ipynb$)'
4
4
 
5
5
  [build-system]
@@ -1,5 +1,5 @@
1
1
  [flake8]
2
- max-line-length = 79
2
+ max-line-length = 80
3
3
  exclude =
4
4
  .git,
5
5
  __pycache__,
@@ -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
- f"CompoundQuery({c1.operator.__name__}, 'SimpleQuery', 'SimpleQuery')"
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
- f"CompoundQuery({c4.operator.__name__}, 'SimpleQuery', 'SimpleQuery')"
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(self, Point) -> None:
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
- timezone.utc
902
- ) + timedelta(days=1)
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(self, point: Point, measurement: Optional[str] = None) -> int:
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, points: Iterable[Any], measurement: Optional[str] = None
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, points: Iterable[Any], measurement: Optional[str]
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([self._storage._serialize_point(point)])
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("Field set must contain only numeric values.")
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 and row[i][1] == "t":
258
- t_key, t_value = row[i][5:], row[i + 1]
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
- f_key, f_value = row[i][7:], row[i + 1]
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(self) -> Sequence[str]:
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
- (f"_tag_{k}", str(v) or self._none_str)
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
- (f"_field_{k}", self._none_str if v is None else str(float(v)))
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(self, point: Point) -> MemStorageItem:
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.2.6
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-philosophy.html>`_ section of the documentation.
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
@@ -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