leafobject 0.0.13__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Tom Chen (tomchen.org)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: leafobject
3
+ Version: 0.0.13
4
+ Summary: LEAF Object Package
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: leaf,influx,json,package
8
+ Author: Jasper Koehorst
9
+ Author-email: jasper.koehorst@wur.nl
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
23
+ Classifier: Programming Language :: Python :: 3 :: Only
24
+ Classifier: Topic :: Software Development :: Build Tools
25
+ Provides-Extra: dev
26
+ Project-URL: Documentation, https://gitlab.com/leaf-framework/leaf-object
27
+ Project-URL: Homepage, https://gitlab.com/leaf-framework/leaf-object
28
+ Project-URL: Repository, https://gitlab.com/leaf-framework/leaf-object
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Leaf Point
32
+
33
+ ## Description
34
+
35
+ This module enables the creation of a LeafPoint object that can be transformed into either a JSON or LineProtocol format.
36
+
37
+ ### Features
38
+
39
+ #### InfluxDB Line Protocol Format
40
+
41
+ The InfluxDB Line Protocol is a text-based format designed for the efficient writing of time-series data into InfluxDB. It organizes the data points with timestamps, measurement names, fields (key-value pairs representing the data), and tags (optional key-value pairs used to store metadata that describes the data). The format is highly optimized for time-series data, enabling quick parsing and writing. A typical line in this format looks like this:
42
+
43
+ > measurementName,tagKey=tagValue fieldKey="fieldValue" 1465839830100400200
44
+
45
+ - measurementName: Identifies the measurement.
46
+ - tagKey=tagValue: Zero or more tag sets separated by commas. Tags are optional but recommended for indexing.
47
+ - fieldKey="fieldValue": At least one field set, with multiple fields separated by commas. Fields are the actual data points.
48
+ - 1465839830100400200: An optional timestamp for the data point. If not specified, the server's current time is used.
49
+
50
+ ### JSON Format
51
+
52
+ The JSON format offers a more flexible way to represent data.
53
+
54
+ ```json
55
+ {
56
+ "measurement": "measurement",
57
+ "tags": {"tag1": "value1"},
58
+ "fields": {"field1": 1, "field2": 2},
59
+ "timestamp": 1609455600,
60
+ }
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ```python
66
+
67
+ from leafobject.leafpoint import LeafPoint
68
+
69
+ leaf_point = LeafPoint()
70
+ leaf_point.set_measurement("measurement")
71
+ leaf_point.set_tags({"tag1": "value1"})
72
+ leaf_point.set_fields({"field1": 1, "field2": 2})
73
+ leaf_point.set_timestamp(datetime.datetime(2021, 1, 1))\
74
+
75
+ print(leaf_point.to_json())
76
+
77
+ # {
78
+ # "measurement": "measurement",
79
+ # "tags": {"tag1": "value1"},
80
+ # "fields": {"field1": 1, "field2": 2},
81
+ # "timestamp": 1609455600,
82
+ # }
83
+
84
+ print(leaf_point.to_line_protocol())
85
+
86
+ # "measurement,tag1=value1 field1=1,field2=2 1609455600"
87
+ ```
88
+
89
+ All functinoalities of the LeafPoint object are listed below:
90
+
91
+ ```python
92
+ import leafobject
93
+ x = leafobject.LeafPoint()
94
+ x.
95
+ x.add_field(..., ...)
96
+ x.from_json(...)
97
+ x.remove_field(...)
98
+ x.set_measurement(...)
99
+ x.to_line_protocol()
100
+ x.add_tag(..., ...)
101
+ x.remove_tag(...)
102
+ x.set_tags(...)
103
+ x.validate()
104
+ x.parse_line_protocol(...)
105
+ x.set_fields(...)
106
+ x.set_timestamp(...)
107
+ x.to_json()
108
+ ```
109
+
110
+ ## Migrating from influxobject
111
+
112
+ `influxobject` is now `leafobject` and `InfluxPoint` is now `LeafPoint`. `InfluxPoint` still works as a deprecated alias:
113
+
114
+ ```python
115
+ from leafobject import LeafPoint # was: from influxobject.influxpoint import InfluxPoint
116
+ ```
117
+
118
+ ## Installation
119
+
120
+ To install the package use the pip package manager
121
+
122
+ ```bash
123
+ pip install leafobject
124
+ ```
125
+
126
+ ## Development
127
+
128
+ Tox is used as the test runner for this project. To run the entire tox suite use the following command:
129
+
130
+ ```bash
131
+ tox
132
+ ```
133
+
134
+ To only run the tests under python 3.9
135
+
136
+ ```bash
137
+ tox -e py39
138
+ ```
139
+
140
+ Build
141
+
142
+ ```bash
143
+ python setup.py sdist
144
+ ```
145
+
146
+ Publish
147
+
148
+ ```bash
149
+ twine upload dist/*
150
+ ```
151
+
@@ -0,0 +1,120 @@
1
+ # Leaf Point
2
+
3
+ ## Description
4
+
5
+ This module enables the creation of a LeafPoint object that can be transformed into either a JSON or LineProtocol format.
6
+
7
+ ### Features
8
+
9
+ #### InfluxDB Line Protocol Format
10
+
11
+ The InfluxDB Line Protocol is a text-based format designed for the efficient writing of time-series data into InfluxDB. It organizes the data points with timestamps, measurement names, fields (key-value pairs representing the data), and tags (optional key-value pairs used to store metadata that describes the data). The format is highly optimized for time-series data, enabling quick parsing and writing. A typical line in this format looks like this:
12
+
13
+ > measurementName,tagKey=tagValue fieldKey="fieldValue" 1465839830100400200
14
+
15
+ - measurementName: Identifies the measurement.
16
+ - tagKey=tagValue: Zero or more tag sets separated by commas. Tags are optional but recommended for indexing.
17
+ - fieldKey="fieldValue": At least one field set, with multiple fields separated by commas. Fields are the actual data points.
18
+ - 1465839830100400200: An optional timestamp for the data point. If not specified, the server's current time is used.
19
+
20
+ ### JSON Format
21
+
22
+ The JSON format offers a more flexible way to represent data.
23
+
24
+ ```json
25
+ {
26
+ "measurement": "measurement",
27
+ "tags": {"tag1": "value1"},
28
+ "fields": {"field1": 1, "field2": 2},
29
+ "timestamp": 1609455600,
30
+ }
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+
37
+ from leafobject.leafpoint import LeafPoint
38
+
39
+ leaf_point = LeafPoint()
40
+ leaf_point.set_measurement("measurement")
41
+ leaf_point.set_tags({"tag1": "value1"})
42
+ leaf_point.set_fields({"field1": 1, "field2": 2})
43
+ leaf_point.set_timestamp(datetime.datetime(2021, 1, 1))\
44
+
45
+ print(leaf_point.to_json())
46
+
47
+ # {
48
+ # "measurement": "measurement",
49
+ # "tags": {"tag1": "value1"},
50
+ # "fields": {"field1": 1, "field2": 2},
51
+ # "timestamp": 1609455600,
52
+ # }
53
+
54
+ print(leaf_point.to_line_protocol())
55
+
56
+ # "measurement,tag1=value1 field1=1,field2=2 1609455600"
57
+ ```
58
+
59
+ All functinoalities of the LeafPoint object are listed below:
60
+
61
+ ```python
62
+ import leafobject
63
+ x = leafobject.LeafPoint()
64
+ x.
65
+ x.add_field(..., ...)
66
+ x.from_json(...)
67
+ x.remove_field(...)
68
+ x.set_measurement(...)
69
+ x.to_line_protocol()
70
+ x.add_tag(..., ...)
71
+ x.remove_tag(...)
72
+ x.set_tags(...)
73
+ x.validate()
74
+ x.parse_line_protocol(...)
75
+ x.set_fields(...)
76
+ x.set_timestamp(...)
77
+ x.to_json()
78
+ ```
79
+
80
+ ## Migrating from influxobject
81
+
82
+ `influxobject` is now `leafobject` and `InfluxPoint` is now `LeafPoint`. `InfluxPoint` still works as a deprecated alias:
83
+
84
+ ```python
85
+ from leafobject import LeafPoint # was: from influxobject.influxpoint import InfluxPoint
86
+ ```
87
+
88
+ ## Installation
89
+
90
+ To install the package use the pip package manager
91
+
92
+ ```bash
93
+ pip install leafobject
94
+ ```
95
+
96
+ ## Development
97
+
98
+ Tox is used as the test runner for this project. To run the entire tox suite use the following command:
99
+
100
+ ```bash
101
+ tox
102
+ ```
103
+
104
+ To only run the tests under python 3.9
105
+
106
+ ```bash
107
+ tox -e py39
108
+ ```
109
+
110
+ Build
111
+
112
+ ```bash
113
+ python setup.py sdist
114
+ ```
115
+
116
+ Publish
117
+
118
+ ```bash
119
+ twine upload dist/*
120
+ ```
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["poetry-core"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "leafobject"
7
+ version = "0.0.13"
8
+ description = "LEAF Object Package"
9
+ authors = [
10
+ "Jasper Koehorst <jasper.koehorst@wur.nl>",
11
+ "Brett Metcalfe"
12
+ ]
13
+ license = "MIT"
14
+ readme = "README.md"
15
+ homepage = "https://gitlab.com/leaf-framework/leaf-object"
16
+ repository = "https://gitlab.com/leaf-framework/leaf-object"
17
+ documentation = "https://gitlab.com/leaf-framework/leaf-object"
18
+ keywords = ["leaf", "influx", "json", "package"]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "Topic :: Software Development :: Build Tools",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent"
27
+ ]
28
+ packages = [
29
+ { include = "leafobject", from = "src" }
30
+ ]
31
+
32
+ [tool.poetry.dependencies]
33
+ python = "^3.9"
34
+
35
+ [tool.poetry.group.dev.dependencies]
36
+ check-manifest = { version = "*", optional = true }
37
+ pytest = "^8.4.1"
38
+ pytest-xdist = "^3.8.0"
39
+ ruff = "^0.9"
40
+ pre-commit = "^4.0"
41
+
42
+ [tool.poetry.extras]
43
+ dev = ["check-manifest"]
44
+
45
+ [tool.ruff]
46
+ target-version = "py39"
47
+ line-length = 88
48
+ src = ["src", "tests"]
49
+
50
+ [tool.ruff.lint]
51
+ select = [
52
+ "E", # pycodestyle errors
53
+ "W", # pycodestyle warnings
54
+ "F", # Pyflakes
55
+ "I", # isort
56
+ "UP", # pyupgrade
57
+ "B", # flake8-bugbear
58
+ "SIM", # flake8-simplify
59
+ ]
60
+ ignore = []
61
+
62
+ [tool.ruff.format]
63
+ quote-style = "double"
64
+ indent-style = "space"
@@ -0,0 +1,7 @@
1
+ __version__ = "0.0.3"
2
+
3
+ # from .module1 import *
4
+
5
+ from .leafpoint import InfluxPoint, LeafPoint, convert_to_seconds
6
+
7
+ __all__ = ["LeafPoint", "InfluxPoint", "convert_to_seconds"]
@@ -0,0 +1,5 @@
1
+ """influxpoint.py: Deprecated module, use leafobject.leafpoint instead."""
2
+
3
+ from .leafpoint import InfluxPoint, LeafPoint, convert_to_seconds
4
+
5
+ __all__ = ["InfluxPoint", "LeafPoint", "convert_to_seconds"]
@@ -0,0 +1,37 @@
1
+ """leafobject_tools.py"""
2
+ # ----------------------------
3
+ # PACKAGES
4
+ # ----------------------------
5
+
6
+
7
+ # ----------------------------
8
+ # VARIABLES
9
+ # ----------------------------
10
+
11
+
12
+ # ----------------------------
13
+ # FUNCTIONS
14
+ # ----------------------------
15
+ def celsius_to_fahrenheit(temperature_in_celsius) -> float:
16
+ """celsius_to_fahrenheit: Convert celsius to fahrenheit"""
17
+ return (temperature_in_celsius * 9 / 5) + 32
18
+
19
+
20
+ def celsius_to_kelvin(temperature_in_celsius) -> float:
21
+ """celsius_to_kelvin: Convert celsius to kelvin"""
22
+ return temperature_in_celsius + 273.15
23
+
24
+
25
+ def fahrenheit_to_celsius(temperature_in_fahrenheit) -> float:
26
+ """fahrenheit_to_celsius: Convert fahrenheit to celsius"""
27
+ return (temperature_in_fahrenheit - 32) * 5 / 9
28
+
29
+
30
+ def fahrenheit_to_kelvin(temperature_in_fahrenheit) -> float:
31
+ """fahrenheit_to_celsius: Convert fahrenheit to celsius"""
32
+ return ((temperature_in_fahrenheit - 32) * 5 / 9) + 273.15
33
+
34
+
35
+ # ----------------------------
36
+ # CLASSES
37
+ # ----------------------------
@@ -0,0 +1,528 @@
1
+ """leafpoint.py
2
+
3
+
4
+
5
+ REQUIREMENTS:
6
+ import logging
7
+ from typing import Dict, Optional, Union
8
+ from datetime import datetime
9
+ import re
10
+
11
+
12
+
13
+ """
14
+
15
+ # ----------------------------
16
+ # PACKAGES
17
+ # ----------------------------
18
+ import logging
19
+ import re
20
+ import warnings
21
+ from datetime import datetime
22
+ from typing import Optional, Union
23
+
24
+ # ----------------------------
25
+ # VARIABLES
26
+ # ----------------------------
27
+
28
+
29
+ # ----------------------------
30
+ # FUNCTIONS
31
+ # ----------------------------
32
+
33
+
34
+ def convert_to_seconds(timestamp: int) -> float:
35
+ """convert_to_seconds: Convert timestamp to seconds
36
+
37
+ INPUT:
38
+ timestamp = original timestamp, an int
39
+
40
+ OUTPUT:
41
+ timestamp = converted timestamp, a float
42
+
43
+
44
+ """
45
+ length = len(str(timestamp))
46
+ if length >= 19: # nanoseconds
47
+ logging.warning("Timestamp is converted to seconds, precision is lost.")
48
+ return timestamp / 1_000_000_000
49
+ elif length >= 16: # microseconds
50
+ logging.warning("Timestamp is converted to seconds, precision is lost.")
51
+ return timestamp / 1_000_000
52
+ elif length >= 13: # milliseconds
53
+ logging.warning("Timestamp is converted to seconds, precision is lost.")
54
+ return timestamp / 1_000
55
+ elif length >= 10: # seconds
56
+ return timestamp
57
+ else:
58
+ raise ValueError("Timestamp value is too short to be valid")
59
+
60
+
61
+ # ----------------------------
62
+ # CLASSES
63
+ # ----------------------------
64
+
65
+
66
+ class LeafPoint:
67
+ def __init__(self) -> None:
68
+ """init: Initialise class
69
+
70
+ INPUT:
71
+ None
72
+
73
+ OUTPUT:
74
+ None, initialise self.measurement
75
+ self.tags
76
+ self.fields
77
+ self.timestamp
78
+
79
+
80
+ """
81
+ self.measurement: Optional[str] = None
82
+ self.tags: dict[str, str] = {}
83
+ self.fields: dict[str, Union[int, float, str]] = {}
84
+ self.timestamp: Optional[datetime] = datetime.now()
85
+
86
+ def set_measurement(self, measurement: str) -> None:
87
+ """set_measurement: Set measurement
88
+
89
+ INPUT:
90
+ measurement = measurement value to set, of type str
91
+
92
+ OUTPUT:
93
+ None, sets self.measurement or raises TypeError
94
+
95
+
96
+ """
97
+ if not isinstance(measurement, str):
98
+ raise TypeError(
99
+ f"Measurement must be of type str, not {type(measurement).__name__}"
100
+ )
101
+ self.measurement = measurement
102
+
103
+ def set_tags(self, tags: dict[str, str]) -> None:
104
+ """set_tags: Set tags"""
105
+ for k, v in tags.items():
106
+ if not isinstance(v, str):
107
+ raise TypeError(
108
+ f"Value for '{k}' must be of type str, not {type(v).__name__}"
109
+ )
110
+ self.tags = tags
111
+
112
+ def set_fields(self, fields: dict[str, Union[int, float, str]]) -> None:
113
+ """set_fields: Set fields"""
114
+ for k, v in fields.items():
115
+ if not isinstance(v, (int, float, str)):
116
+ raise TypeError(
117
+ f"Value for '{k}' must be int, float, or str, "
118
+ f"not {type(v).__name__}"
119
+ )
120
+ self.fields = fields
121
+
122
+ def set_timestamp(self, timestamp: datetime) -> None:
123
+ """set_timestamp: Set timestamp"""
124
+ if not isinstance(timestamp, datetime):
125
+ if isinstance(timestamp, int):
126
+ timestamp = datetime.fromtimestamp(convert_to_seconds(timestamp))
127
+ else:
128
+ raise ValueError(
129
+ "Timestamp must be either numeric (seconds) or a datetime object"
130
+ )
131
+ self.timestamp = timestamp
132
+
133
+ def to_line_protocol(self) -> str:
134
+ """set_timestamp: Set timestamp"""
135
+ self.validate()
136
+ tags = ",".join([f"{k}={v}" for k, v in self.tags.items()])
137
+ fields = ",".join([f"{k}={v}" for k, v in self.fields.items()])
138
+ return f"{self.measurement},{tags} {fields} {int(self.timestamp.timestamp())}"
139
+
140
+ def __str__(self) -> str:
141
+ """str: Get line protocol"""
142
+ return self.to_line_protocol()
143
+
144
+ # ----------------------------
145
+ # Add or remove tags
146
+ # ----------------------------
147
+ def add_tag(self, key: str, value: str) -> None:
148
+ """add_tag: Add tag"""
149
+ if not isinstance(value, (int, float, str)):
150
+ raise TypeError(
151
+ f"Value for '{key}' must be int, float, or str, "
152
+ f"not {type(value).__name__}"
153
+ )
154
+ self.tags[key] = value
155
+
156
+ def remove_tag(self, key: str) -> None:
157
+ """remove_tag: Remove tag"""
158
+ del self.tags[key]
159
+
160
+ # ----------------------------
161
+ # Add or remove fields
162
+ # ----------------------------
163
+ def add_field(self, key: str, value: Union[int, float, str]) -> None:
164
+ """add_field: Add field"""
165
+ if not isinstance(value, (int, float, str)):
166
+ raise TypeError(
167
+ f"Value for '{key}' must be int, float, or str, "
168
+ f"not {type(value).__name__}"
169
+ )
170
+ self.fields[key] = value
171
+
172
+ def remove_field(self, key: str) -> None:
173
+ """remove_field: Remove field timestamp"""
174
+ del self.fields[key]
175
+
176
+ # ----------------------------
177
+ # Add specific tags if needed
178
+ # ----------------------------
179
+ def set_entity_tag(self, entity: str) -> None:
180
+ """set_entity_tag: Set entity tag
181
+
182
+ Add an entity tag to the LeafPoint for example a weather station,
183
+ bioreactor, storage instance.
184
+
185
+ INPUT:
186
+ entity: The entity to add as a tag.
187
+
188
+ OUTPUT:
189
+ None, raises TypeError: If the entity is not a string.
190
+
191
+ """
192
+ if not isinstance(entity, str):
193
+ raise TypeError(f"Entity must be of type str, not {type(entity).__name__}")
194
+ self.add_tag("entity", entity)
195
+
196
+ def get_entity_tag(self) -> Optional[str]:
197
+ """
198
+ Get the entity tag from the LeafPoint.
199
+ :return: The entity tag if it exists, otherwise None.
200
+ """
201
+ if not self.tags:
202
+ return None
203
+ if "entity" not in self.tags:
204
+ logging.warning("Entity tag is not set, returning None")
205
+ return None
206
+ return self.tags.get("entity")
207
+
208
+ def set_value(self, value: Union[int, float]) -> None:
209
+ """set_value: Set the value field for the LeafPoint.
210
+
211
+ This is a convenience method to set a primary value field in the LeafPoint.
212
+ :param value: The value to set in the fields.
213
+ :raises TypeError: If the value is not of type int, float.
214
+ """
215
+ if not isinstance(value, (int, float)):
216
+ raise TypeError(
217
+ f"Value must be of type int, float, not {type(value).__name__}"
218
+ )
219
+ self.add_field("value", value)
220
+
221
+ def get_value(self) -> Optional[Union[int, float]]:
222
+ """
223
+ Get the value field from the LeafPoint.
224
+ :return: The value field if it exists, otherwise None.
225
+ """
226
+ if not self.fields:
227
+ return None
228
+ if "value" not in self.fields:
229
+ logging.warning("Value field is not set, returning None")
230
+ return None
231
+ return self.fields.get("value")
232
+
233
+ def set_metric(self, metric: str) -> None:
234
+ """
235
+ Set the metric tag for the LeafPoint to identify the type of data.
236
+ Useful for categorizing the data, such as temperature, humidity, etc.
237
+ :param metric: The metric to set as a tag.
238
+ :raises TypeError: If the metric is not a string.
239
+ """
240
+ if not isinstance(metric, str):
241
+ raise TypeError(f"Metric must be of type str, not {type(metric).__name__}")
242
+ self.add_tag("metric", metric)
243
+
244
+ def get_metric(self) -> Optional[str]:
245
+ """
246
+ Get the metric tag from the LeafPoint.
247
+ :return: The metric tag if it exists, otherwise None.
248
+ """
249
+ if not self.tags:
250
+ return None
251
+ if "metric" not in self.tags:
252
+ logging.warning("Metric tag is not set, returning None")
253
+ return None
254
+ return self.tags.get("metric")
255
+
256
+ def set_unit(self, unit: str) -> None:
257
+ """
258
+ Set the unit tag for the LeafPoint to specify the unit of measurement.
259
+ :param unit: The unit to set as a tag.
260
+ :raises TypeError: If the unit is not a string.
261
+ """
262
+ if not isinstance(unit, str):
263
+ raise TypeError(f"Unit must be of type str, not {type(unit).__name__}")
264
+ self.add_tag("unit", unit)
265
+
266
+ def get_unit(self) -> Optional[str]:
267
+ """get_unit: Get the unit tag from the LeafPoint.
268
+
269
+ INPUT:
270
+ None
271
+
272
+ OUTPUT:
273
+ :return: The unit tag if it exists, otherwise None.
274
+
275
+ """
276
+ if not self.tags:
277
+ return None
278
+ if "unit" not in self.tags:
279
+ logging.warning("Unit tag is not set, returning None")
280
+ return None
281
+ return self.tags.get("unit")
282
+
283
+ def set_organisation(self, organisation: str) -> None:
284
+ """set_organisation: Set the organisation tag for the LeafPoint to specify the
285
+ organisation.
286
+
287
+ :param organisation: The organisation to set as a tag.
288
+ :raises TypeError: If the organisation is not a string.
289
+ """
290
+ if not isinstance(organisation, str):
291
+ raise TypeError(
292
+ f"Organisation must be of type str, not {type(organisation).__name__}"
293
+ )
294
+ self.add_tag("organisation", organisation)
295
+
296
+ def get_organisation(self) -> Optional[str]:
297
+ """get_organisation: Get the organisation tag from the LeafPoint.
298
+
299
+ INPUT:
300
+ None
301
+
302
+ OUTPUT:
303
+ :return: The organisation tag if it exists, otherwise None.
304
+
305
+ """
306
+ if not self.tags:
307
+ return None
308
+ if "organisation" not in self.tags:
309
+ logging.warning("Organisation tag is not set, returning None")
310
+ return None
311
+ return self.tags.get("organisation")
312
+
313
+ def set_institute(self, institute: str) -> None:
314
+ """set_institute: Deprecated alias for set_organisation.
315
+
316
+ :param institute: The organisation to set as a tag.
317
+ :raises TypeError: If the institute is not a string.
318
+ """
319
+ warnings.warn(
320
+ "set_institute is deprecated, use set_organisation instead",
321
+ DeprecationWarning,
322
+ stacklevel=2,
323
+ )
324
+ self.set_organisation(institute)
325
+
326
+ def get_institute(self) -> Optional[str]:
327
+ """get_institute: Deprecated alias for get_organisation.
328
+
329
+ INPUT:
330
+ None
331
+
332
+ OUTPUT:
333
+ :return: The organisation tag if it exists, otherwise None.
334
+
335
+ """
336
+ warnings.warn(
337
+ "get_institute is deprecated, use get_organisation instead",
338
+ DeprecationWarning,
339
+ stacklevel=2,
340
+ )
341
+ return self.get_organisation()
342
+
343
+ def set_department(self, department: str) -> None:
344
+ """set_department: Set the department tag for the LeafPoint to specify the
345
+ department.
346
+
347
+ :param department: The department to set as a tag.
348
+ :raises TypeError: If the department is not a string.
349
+ """
350
+ if not isinstance(department, str):
351
+ raise TypeError(
352
+ f"Department must be of type str, not {type(department).__name__}"
353
+ )
354
+ self.add_tag("department", department)
355
+
356
+ def get_department(self) -> Optional[str]:
357
+ """get_department: Get the department tag from the LeafPoint.
358
+
359
+ INPUT:
360
+ None
361
+
362
+ OUTPUT:
363
+ :return: The department tag if it exists, otherwise None.
364
+
365
+ """
366
+ if not self.tags:
367
+ return None
368
+ if "department" not in self.tags:
369
+ logging.warning("Department tag is not set, returning None")
370
+ return None
371
+ return self.tags.get("department")
372
+
373
+ def validate(self) -> None:
374
+ """validate: Validate the line protocol
375
+
376
+ INPUT:
377
+ None
378
+
379
+ OUTPUT:
380
+ None, raise valueerror if line is missing
381
+ measurement, timestamp, fields or
382
+ tags
383
+
384
+ """
385
+ errors = []
386
+
387
+ # -------------------------
388
+ # CHECK
389
+ if self.measurement is None:
390
+ errors.append("Measurement is not set")
391
+
392
+ if self.timestamp is None:
393
+ errors.append("Timestamp is not set")
394
+
395
+ if not self.fields:
396
+ errors.append("Fields are not set")
397
+
398
+ if not self.tags:
399
+ errors.append("Tags are not set")
400
+
401
+ # -------------------------
402
+ # IF errors are not empty
403
+ if errors:
404
+ raise ValueError(", ".join(errors))
405
+
406
+ # ----------------------------
407
+ # to and from json
408
+ # ----------------------------
409
+ def to_json(
410
+ self,
411
+ ) -> dict[str, Union[str, dict[str, str], dict[str, Union[int, float, str]], str]]:
412
+ """to_json: Convert point to json
413
+
414
+ INPUT:
415
+ None
416
+
417
+ OUTPUT:
418
+ json object with keys: measurement, tags, fields, and timestamp
419
+
420
+ """
421
+ # ----------------------
422
+ # CHECK
423
+ self.validate()
424
+
425
+ # ----------------------
426
+ # IF validation successful return json dict
427
+ return {
428
+ "measurement": self.measurement,
429
+ "tags": self.tags,
430
+ "fields": self.fields,
431
+ "timestamp": self.timestamp.timestamp() if self.timestamp else None,
432
+ }
433
+
434
+ def from_json(
435
+ self,
436
+ json: dict[
437
+ str, Union[str, dict[str, str], dict[str, Union[int, float, str]], str]
438
+ ],
439
+ ) -> None:
440
+ """from_json: Set class variables
441
+
442
+ INPUT:
443
+ json = dictionary of line protocol
444
+
445
+ OUTPUT:
446
+ None, set the various class variables
447
+ from a json dict
448
+
449
+ """
450
+ measurement = json.get("measurement")
451
+ tags = json.get("tags", {})
452
+ fields = json.get("fields", {})
453
+ timestamp = (
454
+ datetime.fromtimestamp(json.get("timestamp"))
455
+ if json.get("timestamp")
456
+ else None
457
+ )
458
+
459
+ self.measurement = measurement
460
+ self.tags = tags
461
+ self.fields = fields
462
+ self.timestamp = timestamp
463
+
464
+ def parse_line_protocol(self, line_protocol: str) -> None:
465
+ """parse_line_protocol: Set class variables from a line protocol
466
+
467
+ INPUT:
468
+ line_protocol = line protocol to parse
469
+
470
+ OUTPUT:
471
+ None, set the various class variables
472
+ from a line protocol
473
+
474
+ """
475
+ # -----------------------------------
476
+ # Regex split on white space only that
477
+ # are not escaped with \
478
+ measurement_tags, fields, epoch = re.split(r"(?<!\\)\s", line_protocol)
479
+ self.set_timestamp(datetime.fromtimestamp(convert_to_seconds(int(epoch))))
480
+
481
+ # -----------------------------------
482
+ # Split the measurement from the tags
483
+ measurement, tags = re.split(r"(?<!\\),", measurement_tags, maxsplit=1)
484
+ self.set_measurement(measurement)
485
+
486
+ # -----------------------------------
487
+ # For each tag split...
488
+ tags = re.split(r"(?<!\\),", tags)
489
+
490
+ # -----------------------------------
491
+ # ...turn it into a dictionary
492
+ for tag in tags:
493
+ key, value = re.split(r"(?<!\\)=", tag)
494
+ self.add_tag(key, value)
495
+
496
+ # -----------------------------------
497
+ # For each field split...
498
+ fields = re.split(r"(?<!\\),", fields)
499
+
500
+ # -----------------------------------
501
+ # ...turn it into a dictionary
502
+ for field in fields:
503
+ key, value = re.split(r"(?<!\\)=", field)
504
+ self.add_field(key, value)
505
+
506
+ # -----------------------------------
507
+ # Map field value to int or float if possible
508
+ for k, v in self.fields.items():
509
+ if v.isdigit():
510
+ self.fields[k] = int(v)
511
+ elif v.replace(".", "", 1).isdigit():
512
+ self.fields[k] = float(v)
513
+
514
+ # -----------------------------------
515
+ # Convert a int timestamp to a datetime object
516
+ # open('test.txt', 'w').write(f"{epoch} + {datetime.fromtimestamp(int(epoch))}")
517
+ self.validate()
518
+
519
+
520
+ class InfluxPoint(LeafPoint):
521
+ # Deprecated alias kept so existing InfluxPoint code keeps working.
522
+ def __init__(self) -> None:
523
+ warnings.warn(
524
+ "InfluxPoint is deprecated, use LeafPoint instead",
525
+ DeprecationWarning,
526
+ stacklevel=2,
527
+ )
528
+ super().__init__()
@@ -0,0 +1,22 @@
1
+ # Example PyPI (Python Package Index) Package
2
+
3
+
4
+ class LeafObject:
5
+ def __init__(self, n):
6
+ self.value = n
7
+
8
+ def val(self):
9
+ return self.value
10
+
11
+ def add(self, n2):
12
+ self.value += n2.val()
13
+
14
+ def __add__(self, n2):
15
+ return self.__class__(self.value + n2.val())
16
+
17
+ def __str__(self):
18
+ return str(self.val())
19
+
20
+ @classmethod
21
+ def addall(cls, number_obj_iter):
22
+ cls(sum(n.val() for n in number_obj_iter))