cvec 0.1.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.
- cvec-0.1.0/LICENSE +21 -0
- cvec-0.1.0/PKG-INFO +165 -0
- cvec-0.1.0/README.md +148 -0
- cvec-0.1.0/pyproject.toml +27 -0
- cvec-0.1.0/src/cvec/__init__.py +5 -0
- cvec-0.1.0/src/cvec/cvec.py +242 -0
- cvec-0.1.0/src/cvec/metric.py +31 -0
- cvec-0.1.0/src/cvec/py.typed +1 -0
- cvec-0.1.0/src/cvec/span.py +42 -0
cvec-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 CVector Energy
|
|
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.
|
cvec-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cvec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDK for CVector Energy
|
|
5
|
+
Author: CVector
|
|
6
|
+
Author-email: support@cvector.energy
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
14
|
+
Requires-Dist: psycopg (>=3.1.0,<4.0.0)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# CVec Client Library
|
|
18
|
+
|
|
19
|
+
The "cvec" package is the Python SDK for CVector Energy.
|
|
20
|
+
|
|
21
|
+
# Getting Started
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Assuming that you have a supported version of Python installed, you can first create a venv with:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then, activate the venv:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
. .venv/bin/activate
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then, you can install cvec from PyPI with:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
pip install cvec
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Using cvec
|
|
44
|
+
|
|
45
|
+
Import the cvec package. We will also use the datetime module.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
import cvec
|
|
49
|
+
from datetime import datetime
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Construct the CVec client. The host, tenant, and api_key can be given through parameters to the constructor or from the environment variables CVEC_HOST, CVEC_TENANT, and CVEC_API_KEY:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
cvec = cvec.CVec()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Spans
|
|
59
|
+
|
|
60
|
+
A span is a period of interest, such as an experiment, a baseline recording session, or an alarm. The initial state of a Span is implicitly defined by a period where a given metric has a constant value.
|
|
61
|
+
|
|
62
|
+
The newest span for a metric does not have an end time, since it has not ended yet (or has not ended by the finish of the queried period).
|
|
63
|
+
|
|
64
|
+
To get the spans on `my_tag_name` since 2025-05-14 10am, run:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
for span in cvec.get_spans("mygroup/myedge/mode", start_at=datetime(2025, 5, 14, 10, 0, 0)):
|
|
68
|
+
print("%s\t%s" % (span.value, span.raw_start_at))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The output will be like:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
offline 2025-05-19 16:28:02.130000+00:00
|
|
75
|
+
starting 2025-05-19 16:28:01.107000+00:00
|
|
76
|
+
running 2025-05-19 15:29:28.795000+00:00
|
|
77
|
+
stopping 2025-05-19 15:29:27.788000+00:00
|
|
78
|
+
offline 2025-05-19 14:14:43.752000+00:00
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Metrics
|
|
82
|
+
|
|
83
|
+
A metric is a named set of time-series data points pertaining to a particular resource (for example, the value reported by a sensor). Metrics can have numeric or string values. Boolean values are mapped to 0 and 1. The get_metrics function returns a list of metric metadata.
|
|
84
|
+
|
|
85
|
+
To get all of the metrics that changed value at 10am on 2025-05-14, run:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
for item in cvec.get_metrics(start_at=datetime(2025, 5, 14, 10, 0, 0), end_at=datetime(2025, 5, 14, 11, 0, 0)):
|
|
89
|
+
print(item.name)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Example output:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
mygroup/myedge/compressor01/status
|
|
96
|
+
mygroup/myedge/compressor01/interlocks/emergency_stop
|
|
97
|
+
mygroup/myedge/compressor01/stage1/pressure_out/psig
|
|
98
|
+
mygroup/myedge/compressor01/stage1/temp_out/c
|
|
99
|
+
mygroup/myedge/compressor01/stage2/pressure_out/psig
|
|
100
|
+
mygroup/myedge/compressor01/stage2/temp_out/c
|
|
101
|
+
mygroup/myedge/compressor01/motor/current/a
|
|
102
|
+
mygroup/myedge/compressor01/motor/power_kw
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Metric Data
|
|
106
|
+
|
|
107
|
+
The main content for a metric is a set of points where the metric value changed. These are returned as a Pandas Dataframe with columns for name, time, value_double, value_string.
|
|
108
|
+
|
|
109
|
+
To get all of the value changes for all metrics at 10am on 2025-05-14, run:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
cvec.get_metric_data(start_at=datetime(2025, 5, 14, 10, 0, 0), end_at=datetime(2025, 5, 14, 11, 0, 0))
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Example output:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
name time value_double value_string
|
|
119
|
+
0 mygroup/myedge/mode 2025-05-14 10:10:41.949000+00:00 24.900000 starting
|
|
120
|
+
1 mygroup/myedge/compressor01/interlocks/emergency_stop 2025-05-14 10:27:24.899000+00:00 0.0000000 None
|
|
121
|
+
2 mygroup/myedge/compressor01/stage1/pressure_out/psig 2025-05-14 10:43:38.282000+00:00 123.50000 None
|
|
122
|
+
3 mygroup/myedge/compressor01/stage1/temp_out/c 2025-05-14 10:10:41.948000+00:00 24.900000 None
|
|
123
|
+
4 mygroup/myedge/compressor01/motor/current/a 2025-05-14 10:27:24.897000+00:00 12.000000 None
|
|
124
|
+
... ... ... ... ...
|
|
125
|
+
46253 mygroup/myedge/compressor01/stage1/temp_out/c 2025-05-14 10:59:55.725000+00:00 25.300000 None
|
|
126
|
+
46254 mygroup/myedge/compressor01/stage2/pressure_out/psig 2025-05-14 10:59:56.736000+00:00 250.00000 None
|
|
127
|
+
46255 mygroup/myedge/compressor01/stage2/temp_out/c 2025-05-14 10:59:57.746000+00:00 12.700000 None
|
|
128
|
+
46256 mygroup/myedge/compressor01/motor/current/a 2025-05-14 10:59:58.752000+00:00 11.300000 None
|
|
129
|
+
46257 mygroup/myedge/compressor01/motor/power_kw 2025-05-14 10:59:59.760000+00:00 523.40000 None
|
|
130
|
+
|
|
131
|
+
[46257 rows x 4 columns]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
# CVec Class
|
|
135
|
+
|
|
136
|
+
The SDK provides an API client class named `CVec` with the following functions.
|
|
137
|
+
|
|
138
|
+
## `__init__(?host, ?tenant, ?api_key, ?default_start_at, ?default_end_at)`
|
|
139
|
+
|
|
140
|
+
Setup the SDK with the given host and API Key. The host and API key are loaded from environment variables CVEC_HOST, CVEC_TENANT, CVEC_API_KEY, if they are not given as arguments to the constructor. The `default_start_at` and `default_end_at` can provide a default query time interval for API methods.
|
|
141
|
+
|
|
142
|
+
## `get_spans(name, ?start_at, ?end_at, ?limit)`
|
|
143
|
+
|
|
144
|
+
Return time spans for a metric. Spans are generated from value changes that occur after `start_at` (if specified) and before `end_at` (if specified).
|
|
145
|
+
If `start_at` is `None` (e.g., not provided as an argument and no class default `default_start_at` is set), the query for value changes is unbounded at the start. Similarly, if `end_at` is `None`, the query is unbounded at the end.
|
|
146
|
+
|
|
147
|
+
Each `Span` object in the returned list represents a period where the metric's value is constant and has the following attributes:
|
|
148
|
+
- `value`: The metric's value during the span.
|
|
149
|
+
- `name`: The name of the metric.
|
|
150
|
+
- `raw_start_at`: The timestamp of the value change that initiated this span's value. This will be greater than or equal to the query's `start_at` if one was specified.
|
|
151
|
+
- `raw_end_at`: The timestamp marking the end of this span's constant value. For the newest span, the value is `None`. For other spans, it's the raw_start_at of the immediately newer data point, which is next span in the list.
|
|
152
|
+
- `id`: Currently `None`. In a future version of the SDK, this will be the span's unique identifier.
|
|
153
|
+
- `metadata`: Currently `None`. In a future version, this can be used to store annotations or other metadata related to the span.
|
|
154
|
+
|
|
155
|
+
Returns a list of `Span` objects, sorted in descending chronological order (newest span first).
|
|
156
|
+
If no relevant value changes are found, an empty list is returned.
|
|
157
|
+
|
|
158
|
+
## `get_metric_data(?names, ?start_at, ?end_at)`
|
|
159
|
+
|
|
160
|
+
Return all data-points within a given [`start_at`, `end_at`) interval, optionally selecting a given list of metric names. The return value is a Pandas DataFrame with four columns: name, time, value_double, value_string. One row is returned for each metric value transition.
|
|
161
|
+
|
|
162
|
+
## `get_metrics(?start_at, ?end_at)`
|
|
163
|
+
|
|
164
|
+
Return a list of metrics that had at least one transition in the given [`start_at`, `end_at`) interval. All metrics are returned if no `start_at` and `end_at` are given.
|
|
165
|
+
|
cvec-0.1.0/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# CVec Client Library
|
|
2
|
+
|
|
3
|
+
The "cvec" package is the Python SDK for CVector Energy.
|
|
4
|
+
|
|
5
|
+
# Getting Started
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Assuming that you have a supported version of Python installed, you can first create a venv with:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
python -m venv .venv
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then, activate the venv:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
. .venv/bin/activate
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then, you can install cvec from PyPI with:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install cvec
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Using cvec
|
|
28
|
+
|
|
29
|
+
Import the cvec package. We will also use the datetime module.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
import cvec
|
|
33
|
+
from datetime import datetime
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Construct the CVec client. The host, tenant, and api_key can be given through parameters to the constructor or from the environment variables CVEC_HOST, CVEC_TENANT, and CVEC_API_KEY:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
cvec = cvec.CVec()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Spans
|
|
43
|
+
|
|
44
|
+
A span is a period of interest, such as an experiment, a baseline recording session, or an alarm. The initial state of a Span is implicitly defined by a period where a given metric has a constant value.
|
|
45
|
+
|
|
46
|
+
The newest span for a metric does not have an end time, since it has not ended yet (or has not ended by the finish of the queried period).
|
|
47
|
+
|
|
48
|
+
To get the spans on `my_tag_name` since 2025-05-14 10am, run:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
for span in cvec.get_spans("mygroup/myedge/mode", start_at=datetime(2025, 5, 14, 10, 0, 0)):
|
|
52
|
+
print("%s\t%s" % (span.value, span.raw_start_at))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The output will be like:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
offline 2025-05-19 16:28:02.130000+00:00
|
|
59
|
+
starting 2025-05-19 16:28:01.107000+00:00
|
|
60
|
+
running 2025-05-19 15:29:28.795000+00:00
|
|
61
|
+
stopping 2025-05-19 15:29:27.788000+00:00
|
|
62
|
+
offline 2025-05-19 14:14:43.752000+00:00
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Metrics
|
|
66
|
+
|
|
67
|
+
A metric is a named set of time-series data points pertaining to a particular resource (for example, the value reported by a sensor). Metrics can have numeric or string values. Boolean values are mapped to 0 and 1. The get_metrics function returns a list of metric metadata.
|
|
68
|
+
|
|
69
|
+
To get all of the metrics that changed value at 10am on 2025-05-14, run:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
for item in cvec.get_metrics(start_at=datetime(2025, 5, 14, 10, 0, 0), end_at=datetime(2025, 5, 14, 11, 0, 0)):
|
|
73
|
+
print(item.name)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Example output:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
mygroup/myedge/compressor01/status
|
|
80
|
+
mygroup/myedge/compressor01/interlocks/emergency_stop
|
|
81
|
+
mygroup/myedge/compressor01/stage1/pressure_out/psig
|
|
82
|
+
mygroup/myedge/compressor01/stage1/temp_out/c
|
|
83
|
+
mygroup/myedge/compressor01/stage2/pressure_out/psig
|
|
84
|
+
mygroup/myedge/compressor01/stage2/temp_out/c
|
|
85
|
+
mygroup/myedge/compressor01/motor/current/a
|
|
86
|
+
mygroup/myedge/compressor01/motor/power_kw
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Metric Data
|
|
90
|
+
|
|
91
|
+
The main content for a metric is a set of points where the metric value changed. These are returned as a Pandas Dataframe with columns for name, time, value_double, value_string.
|
|
92
|
+
|
|
93
|
+
To get all of the value changes for all metrics at 10am on 2025-05-14, run:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
cvec.get_metric_data(start_at=datetime(2025, 5, 14, 10, 0, 0), end_at=datetime(2025, 5, 14, 11, 0, 0))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Example output:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
name time value_double value_string
|
|
103
|
+
0 mygroup/myedge/mode 2025-05-14 10:10:41.949000+00:00 24.900000 starting
|
|
104
|
+
1 mygroup/myedge/compressor01/interlocks/emergency_stop 2025-05-14 10:27:24.899000+00:00 0.0000000 None
|
|
105
|
+
2 mygroup/myedge/compressor01/stage1/pressure_out/psig 2025-05-14 10:43:38.282000+00:00 123.50000 None
|
|
106
|
+
3 mygroup/myedge/compressor01/stage1/temp_out/c 2025-05-14 10:10:41.948000+00:00 24.900000 None
|
|
107
|
+
4 mygroup/myedge/compressor01/motor/current/a 2025-05-14 10:27:24.897000+00:00 12.000000 None
|
|
108
|
+
... ... ... ... ...
|
|
109
|
+
46253 mygroup/myedge/compressor01/stage1/temp_out/c 2025-05-14 10:59:55.725000+00:00 25.300000 None
|
|
110
|
+
46254 mygroup/myedge/compressor01/stage2/pressure_out/psig 2025-05-14 10:59:56.736000+00:00 250.00000 None
|
|
111
|
+
46255 mygroup/myedge/compressor01/stage2/temp_out/c 2025-05-14 10:59:57.746000+00:00 12.700000 None
|
|
112
|
+
46256 mygroup/myedge/compressor01/motor/current/a 2025-05-14 10:59:58.752000+00:00 11.300000 None
|
|
113
|
+
46257 mygroup/myedge/compressor01/motor/power_kw 2025-05-14 10:59:59.760000+00:00 523.40000 None
|
|
114
|
+
|
|
115
|
+
[46257 rows x 4 columns]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
# CVec Class
|
|
119
|
+
|
|
120
|
+
The SDK provides an API client class named `CVec` with the following functions.
|
|
121
|
+
|
|
122
|
+
## `__init__(?host, ?tenant, ?api_key, ?default_start_at, ?default_end_at)`
|
|
123
|
+
|
|
124
|
+
Setup the SDK with the given host and API Key. The host and API key are loaded from environment variables CVEC_HOST, CVEC_TENANT, CVEC_API_KEY, if they are not given as arguments to the constructor. The `default_start_at` and `default_end_at` can provide a default query time interval for API methods.
|
|
125
|
+
|
|
126
|
+
## `get_spans(name, ?start_at, ?end_at, ?limit)`
|
|
127
|
+
|
|
128
|
+
Return time spans for a metric. Spans are generated from value changes that occur after `start_at` (if specified) and before `end_at` (if specified).
|
|
129
|
+
If `start_at` is `None` (e.g., not provided as an argument and no class default `default_start_at` is set), the query for value changes is unbounded at the start. Similarly, if `end_at` is `None`, the query is unbounded at the end.
|
|
130
|
+
|
|
131
|
+
Each `Span` object in the returned list represents a period where the metric's value is constant and has the following attributes:
|
|
132
|
+
- `value`: The metric's value during the span.
|
|
133
|
+
- `name`: The name of the metric.
|
|
134
|
+
- `raw_start_at`: The timestamp of the value change that initiated this span's value. This will be greater than or equal to the query's `start_at` if one was specified.
|
|
135
|
+
- `raw_end_at`: The timestamp marking the end of this span's constant value. For the newest span, the value is `None`. For other spans, it's the raw_start_at of the immediately newer data point, which is next span in the list.
|
|
136
|
+
- `id`: Currently `None`. In a future version of the SDK, this will be the span's unique identifier.
|
|
137
|
+
- `metadata`: Currently `None`. In a future version, this can be used to store annotations or other metadata related to the span.
|
|
138
|
+
|
|
139
|
+
Returns a list of `Span` objects, sorted in descending chronological order (newest span first).
|
|
140
|
+
If no relevant value changes are found, an empty list is returned.
|
|
141
|
+
|
|
142
|
+
## `get_metric_data(?names, ?start_at, ?end_at)`
|
|
143
|
+
|
|
144
|
+
Return all data-points within a given [`start_at`, `end_at`) interval, optionally selecting a given list of metric names. The return value is a Pandas DataFrame with four columns: name, time, value_double, value_string. One row is returned for each metric value transition.
|
|
145
|
+
|
|
146
|
+
## `get_metrics(?start_at, ?end_at)`
|
|
147
|
+
|
|
148
|
+
Return a list of metrics that had at least one transition in the given [`start_at`, `end_at`) interval. All metrics are returned if no `start_at` and `end_at` are given.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cvec"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SDK for CVector Energy"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "CVector",email = "support@cvector.energy"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pandas (>=2.2.3,<3.0.0)",
|
|
12
|
+
"psycopg (>=3.1.0,<4.0.0)" # Assuming a recent version of psycopg3
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.poetry]
|
|
16
|
+
packages = [{include = "cvec", from = "src"}]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
|
+
pytest = "^8.3.5"
|
|
21
|
+
mypy = "^1.15.0"
|
|
22
|
+
pandas-stubs = "^2.2.3.250308"
|
|
23
|
+
ruff = "^0.11.10"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
27
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, List, Optional
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import psycopg
|
|
7
|
+
|
|
8
|
+
from .span import Span
|
|
9
|
+
from .metric import Metric
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CVec:
|
|
13
|
+
"""
|
|
14
|
+
CVec API Client
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
host: Optional[str]
|
|
18
|
+
tenant: Optional[str]
|
|
19
|
+
api_key: Optional[str]
|
|
20
|
+
default_start_at: Optional[datetime]
|
|
21
|
+
default_end_at: Optional[datetime]
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
host: Optional[str] = None,
|
|
26
|
+
tenant: Optional[str] = None,
|
|
27
|
+
api_key: Optional[str] = None,
|
|
28
|
+
default_start_at: Optional[datetime] = None,
|
|
29
|
+
default_end_at: Optional[datetime] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Setup the SDK with the given host and API Key.
|
|
33
|
+
The host and API key are loaded from environment variables CVEC_HOST,
|
|
34
|
+
CVEC_TENANT, CVEC_API_KEY, if they are not given as arguments to the constructor.
|
|
35
|
+
The default_start_at and default_end_at can provide a default query time interval for API methods.
|
|
36
|
+
"""
|
|
37
|
+
self.host = host or os.environ.get("CVEC_HOST")
|
|
38
|
+
self.tenant = tenant or os.environ.get("CVEC_TENANT")
|
|
39
|
+
self.api_key = api_key or os.environ.get("CVEC_API_KEY")
|
|
40
|
+
self.default_start_at = default_start_at
|
|
41
|
+
self.default_end_at = default_end_at
|
|
42
|
+
|
|
43
|
+
if not self.host:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"CVEC_HOST must be set either as an argument or environment variable"
|
|
46
|
+
)
|
|
47
|
+
if not self.tenant:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"CVEC_TENANT must be set either as an argument or environment variable"
|
|
50
|
+
)
|
|
51
|
+
if not self.api_key:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"CVEC_API_KEY must be set either as an argument or environment variable"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _get_db_connection(self) -> psycopg.Connection:
|
|
57
|
+
"""Helper method to establish a database connection."""
|
|
58
|
+
return psycopg.connect(
|
|
59
|
+
user=self.tenant,
|
|
60
|
+
password=self.api_key,
|
|
61
|
+
host=self.host,
|
|
62
|
+
dbname=self.tenant,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def get_spans(
|
|
66
|
+
self,
|
|
67
|
+
name: str,
|
|
68
|
+
start_at: Optional[datetime] = None,
|
|
69
|
+
end_at: Optional[datetime] = None,
|
|
70
|
+
limit: Optional[int] = None,
|
|
71
|
+
) -> List[Span]:
|
|
72
|
+
"""
|
|
73
|
+
Return time spans for a metric. Spans are generated from value changes
|
|
74
|
+
that occur after `start_at` (if specified) and before `end_at` (if specified).
|
|
75
|
+
If `start_at` is `None` (e.g., not provided via argument or class default),
|
|
76
|
+
the query is unbounded at the start. If `end_at` is `None`, it's unbounded at the end.
|
|
77
|
+
|
|
78
|
+
Each span represents a period where the metric's value is constant.
|
|
79
|
+
- `value`: The metric's value during the span.
|
|
80
|
+
- `name`: The name of the metric.
|
|
81
|
+
- `raw_start_at`: The timestamp of the value change that initiated this span's value.
|
|
82
|
+
This will be >= `_start_at` if `_start_at` was specified.
|
|
83
|
+
- `raw_end_at`: The timestamp marking the end of this span's constant value.
|
|
84
|
+
For the newest span, the value is `None`. For other spans, it's the raw_start_at of the immediately newer data point, which is next span in the list.
|
|
85
|
+
- `id`: Currently `None`.
|
|
86
|
+
- `metadata`: Currently `None`.
|
|
87
|
+
|
|
88
|
+
Returns a list of Span objects, sorted in descending chronological order (newest span first).
|
|
89
|
+
Each Span object has attributes corresponding to the fields listed above.
|
|
90
|
+
If no relevant value changes are found, an empty list is returned.
|
|
91
|
+
The `limit` parameter restricts the number of spans returned.
|
|
92
|
+
"""
|
|
93
|
+
_start_at = start_at or self.default_start_at
|
|
94
|
+
_end_at = end_at or self.default_end_at
|
|
95
|
+
|
|
96
|
+
with self._get_db_connection() as conn:
|
|
97
|
+
with conn.cursor() as cur:
|
|
98
|
+
query_params = {
|
|
99
|
+
"metric": name,
|
|
100
|
+
"start_at": _start_at,
|
|
101
|
+
"end_at": _end_at,
|
|
102
|
+
# Fetch up to 'limit' points. If limit is None, then the `LIMIT NULL` clause
|
|
103
|
+
# has no effect (in PostgreSQL).
|
|
104
|
+
"limit": limit,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
combined_query = """
|
|
108
|
+
SELECT
|
|
109
|
+
time,
|
|
110
|
+
value_double,
|
|
111
|
+
value_string
|
|
112
|
+
FROM metric_data
|
|
113
|
+
WHERE metric = %(metric)s
|
|
114
|
+
AND (time >= %(start_at)s OR %(start_at)s IS NULL)
|
|
115
|
+
AND (time < %(end_at)s OR %(end_at)s IS NULL)
|
|
116
|
+
ORDER BY time DESC
|
|
117
|
+
LIMIT %(limit)s
|
|
118
|
+
"""
|
|
119
|
+
cur.execute(combined_query, query_params)
|
|
120
|
+
db_rows = cur.fetchall()
|
|
121
|
+
spans = []
|
|
122
|
+
|
|
123
|
+
# None indicates that the end time is not known; the span extends beyond
|
|
124
|
+
# the query period.
|
|
125
|
+
raw_end_at = None
|
|
126
|
+
for time, value_double, value_string in db_rows:
|
|
127
|
+
raw_start_at = time
|
|
128
|
+
value = value_double if value_double is not None else value_string
|
|
129
|
+
spans.append(
|
|
130
|
+
Span(
|
|
131
|
+
id=None,
|
|
132
|
+
name=name,
|
|
133
|
+
value=value,
|
|
134
|
+
raw_start_at=raw_start_at,
|
|
135
|
+
raw_end_at=raw_end_at,
|
|
136
|
+
metadata=None,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
raw_end_at = raw_start_at
|
|
140
|
+
|
|
141
|
+
return spans
|
|
142
|
+
|
|
143
|
+
def get_metric_data(
|
|
144
|
+
self,
|
|
145
|
+
names: Optional[List[str]] = None,
|
|
146
|
+
start_at: Optional[datetime] = None,
|
|
147
|
+
end_at: Optional[datetime] = None,
|
|
148
|
+
) -> pd.DataFrame:
|
|
149
|
+
"""
|
|
150
|
+
Return all data-points within a given [start_at, end_at) interval,
|
|
151
|
+
optionally selecting a given list of metric names.
|
|
152
|
+
The return value is a Pandas DataFrame with four columns: name, time, value_double, value_string.
|
|
153
|
+
One row is returned for each metric value transition.
|
|
154
|
+
"""
|
|
155
|
+
_start_at = start_at or self.default_start_at
|
|
156
|
+
_end_at = end_at or self.default_end_at
|
|
157
|
+
|
|
158
|
+
params = {
|
|
159
|
+
"start_at": _start_at,
|
|
160
|
+
"end_at": _end_at,
|
|
161
|
+
"tag_names_is_null": names is None,
|
|
162
|
+
# Pass an empty tuple if names is None or empty, otherwise the tuple of names.
|
|
163
|
+
# ANY(%(empty_tuple)s) will correctly result in no matches if names is empty.
|
|
164
|
+
# If names is None, the tag_names_is_null condition handles it.
|
|
165
|
+
"tag_names_list": names if names else [],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
sql_query = """
|
|
169
|
+
SELECT metric AS name, time, value_double, value_string
|
|
170
|
+
FROM metric_data
|
|
171
|
+
WHERE (time >= %(start_at)s OR %(start_at)s IS NULL)
|
|
172
|
+
AND (time < %(end_at)s OR %(end_at)s IS NULL)
|
|
173
|
+
AND (%(tag_names_is_null)s IS TRUE OR metric = ANY(%(tag_names_list)s))
|
|
174
|
+
ORDER BY name, time ASC
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
with self._get_db_connection() as conn:
|
|
178
|
+
with conn.cursor() as cur:
|
|
179
|
+
cur.execute(sql_query, params)
|
|
180
|
+
rows = cur.fetchall()
|
|
181
|
+
|
|
182
|
+
if not rows:
|
|
183
|
+
return pd.DataFrame(
|
|
184
|
+
columns=["name", "time", "value_double", "value_string"]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Create DataFrame from fetched rows
|
|
188
|
+
df = pd.DataFrame(
|
|
189
|
+
rows, columns=["name", "time", "value_double", "value_string"]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Return the DataFrame with the required columns
|
|
193
|
+
return df[["name", "time", "value_double", "value_string"]]
|
|
194
|
+
|
|
195
|
+
def get_metrics(
|
|
196
|
+
self, start_at: Optional[datetime] = None, end_at: Optional[datetime] = None
|
|
197
|
+
) -> List[Metric]:
|
|
198
|
+
"""
|
|
199
|
+
Return a list of metrics that had at least one transition in the given [start_at, end_at) interval.
|
|
200
|
+
All metrics are returned if no start_at and end_at are given.
|
|
201
|
+
"""
|
|
202
|
+
sql_query: str
|
|
203
|
+
params: Optional[dict[str, Any]]
|
|
204
|
+
|
|
205
|
+
if start_at is None and end_at is None:
|
|
206
|
+
# No time interval specified by arguments, return all tags
|
|
207
|
+
sql_query = """
|
|
208
|
+
SELECT id, normalized_name AS name, birth_at, death_at
|
|
209
|
+
FROM tag_names
|
|
210
|
+
ORDER BY name ASC;
|
|
211
|
+
"""
|
|
212
|
+
params = None
|
|
213
|
+
else:
|
|
214
|
+
# Time interval specified, find tags with transitions in the interval
|
|
215
|
+
_start_at = start_at or self.default_start_at
|
|
216
|
+
_end_at = end_at or self.default_end_at
|
|
217
|
+
|
|
218
|
+
params = {"start_at_param": _start_at, "end_at_param": _end_at}
|
|
219
|
+
sql_query = f"""
|
|
220
|
+
SELECT DISTINCT metric_id AS id, metric AS name, birth_at, death_at
|
|
221
|
+
FROM {self.tenant}.metric_data
|
|
222
|
+
WHERE (time >= %(start_at_param)s OR %(start_at_param)s IS NULL)
|
|
223
|
+
AND (time < %(end_at_param)s OR %(end_at_param)s IS NULL)
|
|
224
|
+
ORDER BY name ASC;
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
with self._get_db_connection() as conn:
|
|
228
|
+
with conn.cursor() as cur:
|
|
229
|
+
cur.execute(sql_query, params)
|
|
230
|
+
rows = cur.fetchall()
|
|
231
|
+
|
|
232
|
+
# Format rows into list of Metric objects
|
|
233
|
+
metrics_list = [
|
|
234
|
+
Metric(
|
|
235
|
+
id=row[0],
|
|
236
|
+
name=row[1],
|
|
237
|
+
birth_at=row[2],
|
|
238
|
+
death_at=row[3],
|
|
239
|
+
)
|
|
240
|
+
for row in rows
|
|
241
|
+
]
|
|
242
|
+
return metrics_list
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Metric:
|
|
6
|
+
"""
|
|
7
|
+
Represents metadata for a metric.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
id: int
|
|
11
|
+
name: str
|
|
12
|
+
birth_at: Optional[datetime]
|
|
13
|
+
death_at: Optional[datetime]
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
id: int,
|
|
18
|
+
name: str,
|
|
19
|
+
birth_at: Optional[datetime],
|
|
20
|
+
death_at: Optional[datetime],
|
|
21
|
+
):
|
|
22
|
+
self.id = id
|
|
23
|
+
self.name = name
|
|
24
|
+
self.birth_at = birth_at
|
|
25
|
+
self.death_at = death_at
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
f"Metric(id={self.id!r}, name={self.name!r}, "
|
|
30
|
+
f"birth_at={self.birth_at!r}, death_at={self.death_at!r})"
|
|
31
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Optional, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Span:
|
|
6
|
+
"""
|
|
7
|
+
Represents a time span where a metric has a constant value.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
id: Optional[Any]
|
|
11
|
+
name: str
|
|
12
|
+
value: Optional[Union[float, str]]
|
|
13
|
+
raw_start_at: datetime
|
|
14
|
+
raw_end_at: Optional[datetime]
|
|
15
|
+
metadata: Optional[Any]
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
id: Optional[Any],
|
|
20
|
+
name: str,
|
|
21
|
+
value: Optional[Union[float, str]],
|
|
22
|
+
raw_start_at: datetime,
|
|
23
|
+
raw_end_at: Optional[datetime],
|
|
24
|
+
metadata: Optional[Any],
|
|
25
|
+
):
|
|
26
|
+
self.id = id
|
|
27
|
+
self.name = name
|
|
28
|
+
self.value = value
|
|
29
|
+
self.raw_start_at = raw_start_at
|
|
30
|
+
self.raw_end_at = raw_end_at
|
|
31
|
+
self.metadata = metadata
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
raw_start_at_repr = (
|
|
35
|
+
self.raw_start_at.isoformat() if self.raw_start_at else "None"
|
|
36
|
+
)
|
|
37
|
+
raw_end_at_repr = self.raw_end_at.isoformat() if self.raw_end_at else "None"
|
|
38
|
+
return (
|
|
39
|
+
f"Span(id={self.id!r}, name={self.name!r}, value={self.value!r}, "
|
|
40
|
+
f"raw_start_at={raw_start_at_repr}, raw_end_at={raw_end_at_repr}, "
|
|
41
|
+
f"metadata={self.metadata!r})"
|
|
42
|
+
)
|