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.
- leafobject-0.0.13/LICENSE +21 -0
- leafobject-0.0.13/PKG-INFO +151 -0
- leafobject-0.0.13/README.md +120 -0
- leafobject-0.0.13/pyproject.toml +64 -0
- leafobject-0.0.13/src/leafobject/__init__.py +7 -0
- leafobject-0.0.13/src/leafobject/influxpoint.py +5 -0
- leafobject-0.0.13/src/leafobject/leafobject_tools.py +37 -0
- leafobject-0.0.13/src/leafobject/leafpoint.py +528 -0
- leafobject-0.0.13/src/leafobject/module1.py +22 -0
|
@@ -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,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))
|