generic_time_series_objects 0.1.1__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 @@
1
+ /target
@@ -0,0 +1,8 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="MarkdownSettings">
4
+ <option name="showProblemsInCodeBlocks" value="false" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.13" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/rust-time-series-objects.iml" filepath="$PROJECT_DIR$/.idea/rust-time-series-objects.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="EMPTY_MODULE" version="4">
3
+ <component name="FacetManager">
4
+ <facet type="Python" name="Python facet">
5
+ <configuration sdkName="Python 3.13" />
6
+ </facet>
7
+ </component>
8
+ <component name="NewModuleRootManager">
9
+ <content url="file://$MODULE_DIR$">
10
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
11
+ <excludeFolder url="file://$MODULE_DIR$/target" />
12
+ </content>
13
+ <orderEntry type="inheritedJdk" />
14
+ <orderEntry type="sourceFolder" forTests="false" />
15
+ <orderEntry type="library" name="Python 3.13 interpreter library" level="application" />
16
+ </component>
17
+ </module>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,3 @@
1
+ [pypi]
2
+ username = __token__
3
+ password = pypi-AgEIcHlwaS5vcmcCJDljM2Q5MmQ4LTNjMTEtNDNmMS1hNWRhLTc0OGQ5NGY4ZTA3MQACKlszLCI3OTVlNTRjMy1hNzZmLTQ0OTEtODA4NS0xMWRmMTM0MzNlMmYiXQAABiCvQEudVPmfG4FJND575iuRP6SEApezPp1WkBfFy11_sA
@@ -0,0 +1,172 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
+
11
+ [[package]]
12
+ name = "generic_time_series_objects"
13
+ version = "0.1.0"
14
+ dependencies = [
15
+ "pyo3",
16
+ ]
17
+
18
+ [[package]]
19
+ name = "heck"
20
+ version = "0.5.0"
21
+ source = "registry+https://github.com/rust-lang/crates.io-index"
22
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
23
+
24
+ [[package]]
25
+ name = "indoc"
26
+ version = "2.0.7"
27
+ source = "registry+https://github.com/rust-lang/crates.io-index"
28
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
29
+ dependencies = [
30
+ "rustversion",
31
+ ]
32
+
33
+ [[package]]
34
+ name = "libc"
35
+ version = "0.2.177"
36
+ source = "registry+https://github.com/rust-lang/crates.io-index"
37
+ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
38
+
39
+ [[package]]
40
+ name = "memoffset"
41
+ version = "0.9.1"
42
+ source = "registry+https://github.com/rust-lang/crates.io-index"
43
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
44
+ dependencies = [
45
+ "autocfg",
46
+ ]
47
+
48
+ [[package]]
49
+ name = "once_cell"
50
+ version = "1.21.3"
51
+ source = "registry+https://github.com/rust-lang/crates.io-index"
52
+ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
53
+
54
+ [[package]]
55
+ name = "portable-atomic"
56
+ version = "1.11.1"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
59
+
60
+ [[package]]
61
+ name = "proc-macro2"
62
+ version = "1.0.103"
63
+ source = "registry+https://github.com/rust-lang/crates.io-index"
64
+ checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
65
+ dependencies = [
66
+ "unicode-ident",
67
+ ]
68
+
69
+ [[package]]
70
+ name = "pyo3"
71
+ version = "0.27.2"
72
+ source = "registry+https://github.com/rust-lang/crates.io-index"
73
+ checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
74
+ dependencies = [
75
+ "indoc",
76
+ "libc",
77
+ "memoffset",
78
+ "once_cell",
79
+ "portable-atomic",
80
+ "pyo3-build-config",
81
+ "pyo3-ffi",
82
+ "pyo3-macros",
83
+ "unindent",
84
+ ]
85
+
86
+ [[package]]
87
+ name = "pyo3-build-config"
88
+ version = "0.27.2"
89
+ source = "registry+https://github.com/rust-lang/crates.io-index"
90
+ checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
91
+ dependencies = [
92
+ "target-lexicon",
93
+ ]
94
+
95
+ [[package]]
96
+ name = "pyo3-ffi"
97
+ version = "0.27.2"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
100
+ dependencies = [
101
+ "libc",
102
+ "pyo3-build-config",
103
+ ]
104
+
105
+ [[package]]
106
+ name = "pyo3-macros"
107
+ version = "0.27.2"
108
+ source = "registry+https://github.com/rust-lang/crates.io-index"
109
+ checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
110
+ dependencies = [
111
+ "proc-macro2",
112
+ "pyo3-macros-backend",
113
+ "quote",
114
+ "syn",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "pyo3-macros-backend"
119
+ version = "0.27.2"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
122
+ dependencies = [
123
+ "heck",
124
+ "proc-macro2",
125
+ "pyo3-build-config",
126
+ "quote",
127
+ "syn",
128
+ ]
129
+
130
+ [[package]]
131
+ name = "quote"
132
+ version = "1.0.41"
133
+ source = "registry+https://github.com/rust-lang/crates.io-index"
134
+ checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
135
+ dependencies = [
136
+ "proc-macro2",
137
+ ]
138
+
139
+ [[package]]
140
+ name = "rustversion"
141
+ version = "1.0.22"
142
+ source = "registry+https://github.com/rust-lang/crates.io-index"
143
+ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
144
+
145
+ [[package]]
146
+ name = "syn"
147
+ version = "2.0.108"
148
+ source = "registry+https://github.com/rust-lang/crates.io-index"
149
+ checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
150
+ dependencies = [
151
+ "proc-macro2",
152
+ "quote",
153
+ "unicode-ident",
154
+ ]
155
+
156
+ [[package]]
157
+ name = "target-lexicon"
158
+ version = "0.13.3"
159
+ source = "registry+https://github.com/rust-lang/crates.io-index"
160
+ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
161
+
162
+ [[package]]
163
+ name = "unicode-ident"
164
+ version = "1.0.20"
165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
166
+ checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
167
+
168
+ [[package]]
169
+ name = "unindent"
170
+ version = "0.2.4"
171
+ source = "registry+https://github.com/rust-lang/crates.io-index"
172
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "generic_time_series_objects"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ readme = "README.md"
6
+
7
+ [lib]
8
+ name = "generic_time_series_objects"
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+
13
+ [dependencies.pyo3]
14
+ version = "0.27.0"
15
+ features = ["abi3-py38"]
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: generic_time_series_objects
3
+ Version: 0.1.1
4
+ Requires-Dist: pytest>=8.4.2
5
+ Summary: Generic Time Series Objects for Python
6
+ Requires-Python: >=3.13
@@ -0,0 +1,257 @@
1
+ # [Generic Time Series Objects](https://pypi.org/project/generic-time-series-objects/)
2
+ Store Python objects in a time series to capture evolving data over time. Built to be highly
3
+ generic and capable of storing any python class, even custom, against a timestamp (integer)
4
+ value. This project is built in rust, with [pyo3](https://github.com/PyO3/pyo3) bindings, and
5
+ compiled using [maturin](https://github.com/PyO3/maturin). Tests are written in python
6
+ (with pytest).
7
+
8
+ > [!NOTE]
9
+ >
10
+ > Rust code is compiled using `maturin develop --uv` (or `maturin develop --uv --release`). <br>
11
+ > Test cases are then run using `pytest .\python\test_ts.py`.
12
+
13
+ > [!IMPORTANT]
14
+ >
15
+ > Work in progress:
16
+ > - [ ] Time Series Data BaseClass to manage methods as timeseries that can change through time.
17
+ > - [ ] Add further tests to `test_ts.py`
18
+ > - [ ] Proper set up of `test_ts_data_class.py`
19
+ > - [ ] Migrate the Time Series Data BaseClass into `lib.rs`
20
+
21
+ ## TimeSeriesObject Interface
22
+ Methods to interact with the TimeSeriesObject Class.
23
+ ### Dunder Methods
24
+ #### __new\_\_
25
+ Creates the object with no arguments. <br>
26
+ _Arguments_
27
+ * **`self`** (`TimeSeriesObject`): The object itself.
28
+
29
+ _Output/Exceptions_
30
+ * (`TimeSeriesObject`): Returns the created object.
31
+
32
+ _Example:_
33
+ ```python
34
+ from generic_time_series_objects import TimeSeriesObject
35
+
36
+ obj = TimeSeriesObject()
37
+ ```
38
+ #### __repr\_\_
39
+ Representation of the object. <br>
40
+ _Arguments_
41
+ * **`self`** (`TimeSeriesObject`): The object itself.
42
+
43
+ _Output/Exceptions_
44
+ * (`str`): Returns a string with the object name and a list of timestamps.
45
+
46
+ _Example:_
47
+ ```python
48
+ from generic_time_series_objects import TimeSeriesObject
49
+
50
+ obj = TimeSeriesObject()
51
+ print(obj) # prints "TimeSeriesObject(timestamps=[])"
52
+ ```
53
+ #### __len\_\_
54
+ Returns the number of data points currently stored in the time series object. <br>
55
+ _Arguments_
56
+ * **`self`** (`TimeSeriesObject`): The object itself.
57
+
58
+ _Output/Exceptions_
59
+ * (`int`): Number of data points inserted into the object.
60
+
61
+ _Example:_
62
+ ```python
63
+ from generic_time_series_objects import TimeSeriesObject
64
+
65
+ obj = TimeSeriesObject()
66
+ print(len(obj)) # prints 0
67
+ ```
68
+ #### __bool\_\_
69
+ Returns a boolean for if the object contains data points or not. <br>
70
+ _Arguments_
71
+ * **`self`** (`TimeSeriesObject`): The object itself.
72
+
73
+ _Output/Exceptions_
74
+ * (`bool`): False if there are no data points, otherwise True.
75
+
76
+ _Example:_
77
+ ```python
78
+ from generic_time_series_objects import TimeSeriesObject
79
+
80
+ obj = TimeSeriesObject()
81
+ print(bool(obj)) # prints False
82
+ ```
83
+ ### Mutating Data
84
+ Methods return None for success and raises Exception if failed to perform operation.
85
+ #### insert
86
+ Inserts a Python object at a given timestamp. <br>
87
+ _Arguments_
88
+ * **`self`** (`TimeSeriesObject`): The object itself.
89
+ * **`ts`** (`int`): Timestamp of the data point.
90
+ * **`value`** (`Any`): The Python object to be stored.
91
+ * **`overwrite`** (`bool`): Defaults to `False`. Determines what to do if a provided timestamp already exists, if
92
+ `overwrite=False`, raises Exception otherwise overwrites the existing data point.
93
+
94
+ _Output/Exceptions_
95
+ * (`None`): Successfully inserted data point at timestamp.
96
+ * **`ValueError`** (`Exception`): Timestamp provided already has existing data point and overwrite is set to False.
97
+
98
+ _Example:_
99
+ ```python
100
+ from generic_time_series_objects import TimeSeriesObject
101
+
102
+ obj = TimeSeriesObject()
103
+ obj.insert(1, {1, 2, 3}) # overwrite defaults to False
104
+ # obj.insert(1, {1, 2, 3}) # !raises ValueError
105
+ obj.insert(1, {1, 2, 3}, overwrite=True)
106
+ ```
107
+ #### update
108
+ Updates the point at a given timestamp. <br>
109
+ _Arguments_
110
+ * **`self`** (`TimeSeriesObject`): The object itself.
111
+ * **`ts`** (`int`): Timestamp of the point we want to update.
112
+ * **`value`** (`Any`): The Python object we want to update with.
113
+
114
+ _Output/Exceptions_
115
+ * (`None`): Successfully inserted Python object at timestamp.
116
+ * **`ValueError`** (`Exception`): TimeSeriesObject is empty and could not update.
117
+ * **`IndexError`** (`Exception`): Provided timestamp does not exist within TimeSeriesObject.
118
+
119
+ _Example:_
120
+ ```python
121
+ from generic_time_series_objects import TimeSeriesObject
122
+
123
+ obj = TimeSeriesObject()
124
+ # obj.update(2, {1, 2, 3, 4}) # !raises ValueError
125
+ obj.insert(1, {1, 2, 3})
126
+ obj.update(1, {1, 2, 3, 4})
127
+ # obj.update(2, {1, 2, 3, 4}) # !raises IndexError
128
+ ```
129
+ #### delete
130
+ Deletes the data point at a given timestamp. <br>
131
+ _Arguments_
132
+ * **`self`** (`TimeSeriesObject`): The object itself.
133
+ * **`ts`** (`int`): Timestamp of the point we want to delete.
134
+
135
+ _Output/Exceptions_
136
+ * (`None`): Successfully deleted data point.
137
+ * **`ValueError`** (`Exception`): TimeSeriesObject is empty and could not delete.
138
+ * **`IndexError`** (`Exception`): Provided timestamp does not exist within TimeSeriesObject.
139
+
140
+ _Example:_
141
+ ```python
142
+ from generic_time_series_objects import TimeSeriesObject
143
+
144
+ obj = TimeSeriesObject()
145
+ # obj.delete(2) # !raises ValueError
146
+ obj.insert(1, {1, 2, 3})
147
+ # obj.delete(2) # !raises IndexError
148
+ obj.delete(1)
149
+ ```
150
+ ### Retrieving Data Points
151
+ Methods return a tuple of the timestamp and Python object for success and None if nothing is found.
152
+ #### point
153
+ Fetches the data point on or before a certain timestamp.<br>
154
+ _Arguments_
155
+ * **`self`** (`TimeSeriesObject`): The object itself.
156
+ * **`ts`** (`int`): Timestamp on or before the time we want to retrieve data for.
157
+
158
+ _Output/Exceptions_
159
+ * (`tuple[int, Any]`): The data point, as a tuple of timestamp and Python object, that was retrieved.
160
+ * **`None`** (`NoneType`): Nothing was found, in this case timestamp provided was before the minimum timestamp in the
161
+ TimeSeriesObject.
162
+
163
+ _Example:_
164
+ ```python
165
+ from generic_time_series_objects import TimeSeriesObject
166
+
167
+ obj = TimeSeriesObject()
168
+ obj.insert(2, {1, 2, 3})
169
+ obj.insert(10, {1, 2, 3, 4})
170
+ print(obj.point(10)) # prints {1, 2, 3, 4}
171
+ print(obj.point(5)) # prints {1, 2, 3}
172
+ print(obj.point(1)) # prints None
173
+ ```
174
+ #### point_on
175
+ Fetches the data point exactly on a certain timestamp.<br>
176
+ _Arguments_
177
+ * **`self`** (`TimeSeriesObject`): The object itself.
178
+ * **`ts`** (`int`): Timestamp exactly equal to the time we want to retrieve data for.
179
+
180
+ _Output/Exceptions_
181
+ * (`tuple[int, Any]`): The data point, as a tuple of timestamp and Python object, that was retrieved.
182
+ * **`None`** (`NoneType`): Nothing was found at provided timestamp.
183
+
184
+ _Example:_
185
+ ```python
186
+ from generic_time_series_objects import TimeSeriesObject
187
+
188
+ obj = TimeSeriesObject()
189
+ obj.insert(2, {1, 2, 3})
190
+ obj.insert(10, {1, 2, 3, 4})
191
+ print(obj.point_on(10)) # prints {1, 2, 3, 4}
192
+ print(obj.point_on(5)) # prints None
193
+ print(obj.point_on(2)) # prints {1, 2, 3}
194
+ ```
195
+
196
+ #### points_between
197
+ Fetches all data points between the two provided timestamps, inclusive of start and exclusive of end [start_ts, end_ts).
198
+ <br>
199
+ _Arguments_
200
+ * **`self`** (`TimeSeriesObject`): The object itself.
201
+ * **`start_ts`** (`int`): Start timestamp to filter for, inclusive.
202
+ * **`end_ts`** (`int`): End timestamp to filter for, exclusive.
203
+
204
+ _Output/Exceptions_
205
+ * (`list[tuple[int, Any]]`): List of points between the starting and ending timestamp.
206
+
207
+ _Example:_
208
+ ```python
209
+ from generic_time_series_objects import TimeSeriesObject
210
+
211
+ obj = TimeSeriesObject()
212
+ obj.insert(2, {1})
213
+ obj.insert(5, {1, 2})
214
+ obj.insert(10, {1, 2, 3})
215
+ print(obj.points_between(1, 100)) # prints [(2, {1}), (5, {1, 2}), (10, {1, 2, 3})]
216
+ print(obj.points_between(1, 10)) # prints [(2, {1}), (5, {1, 2})]
217
+ print(obj.points_between(1, 1)) # prints []
218
+
219
+ ```
220
+ ### Transforming Data Type
221
+ Methods return the data type named in the method as the outer return type.
222
+ #### as_dict
223
+ Transforms all data points in the TimeSeriesObject to a mapping between the timestamp and the Python object.<br>
224
+ _Arguments_
225
+ * **`self`** (`TimeSeriesObject`): The object itself.
226
+
227
+ _Output/Exceptions_
228
+ * (`dict[int, Any]`): All data points in the form of a dictionary mapping timestamp to Python
229
+ object.
230
+
231
+ _Example:_
232
+ ```python
233
+ from generic_time_series_objects import TimeSeriesObject
234
+
235
+ obj = TimeSeriesObject()
236
+ print(obj.as_dict()) # prints {}
237
+ obj.insert(1, ['hello'])
238
+ print(obj.as_dict()) # prints {1: ['hello']}
239
+ ```
240
+ #### as_list
241
+ Transforms all data points in the TimeSeriesObject to a list of tuples containing the timestamp and the Python object.<br>
242
+ _Arguments_
243
+ * **`self`** (`TimeSeriesObject`): The object itself.
244
+
245
+ _Output/Exceptions_
246
+ * (`list[tuple[int, Any]]`): All data points in the form of a list of tuples with each tuple
247
+ containing a timestamp and the Python object.
248
+
249
+ _Example:_
250
+ ```python
251
+ from generic_time_series_objects import TimeSeriesObject
252
+
253
+ obj = TimeSeriesObject()
254
+ print(obj.as_list()) # prints []
255
+ obj.insert(1, ['hello'])
256
+ print(obj.as_list()) # prints [(1, ['hello'])]
257
+ ```
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "generic_time_series_objects"
3
+ version = "0.1.1"
4
+ description = "Generic Time Series Objects for Python"
5
+ requires-python = ">=3.13"
6
+ dependencies = [
7
+ "pytest>=8.4.2",
8
+ ]
9
+
10
+ [build-system]
11
+ requires = ["maturin>=1.0,<2.0"]
12
+ build-backend = "maturin"
13
+
14
+ [tool.maturin]
15
+ features = ["pyo3/extension-module"]
16
+ python-source = "python"
@@ -0,0 +1,17 @@
1
+ from .generic_time_series_objects import TimeSeriesObject
2
+ from .time_series_data_baseclass import TimeSeriesDataBaseclass, time_series_data
3
+
4
+
5
+ classes = [
6
+ "TimeSeriesObject",
7
+ "TimeSeriesDataBaseclass",
8
+ ]
9
+
10
+ decorators = [
11
+ "time_series_data",
12
+ ]
13
+
14
+ __all__ = [
15
+ *classes,
16
+ *decorators,
17
+ ]
@@ -0,0 +1,8 @@
1
+ from generic_time_series_objects import time_series_data
2
+
3
+
4
+ @time_series_data
5
+ def test_func1(a):
6
+ return a + 1
7
+
8
+ print(test_func1(3))
@@ -0,0 +1,49 @@
1
+ import datetime
2
+ from abc import ABC
3
+ from functools import wraps
4
+ from typing import Any, Callable
5
+
6
+ from generic_time_series_objects import TimeSeriesObject
7
+
8
+
9
+ TS_DATA_FLAG = 'is_time_series_data'
10
+
11
+
12
+ class TimeSeriesDataBaseclass(ABC):
13
+ def __init__(self):
14
+ self.timestamp: int = -1
15
+ self.data: dict[str, TimeSeriesObject] = {}
16
+ ts_data_flag = TS_DATA_FLAG
17
+ for method_name in dir(self):
18
+ method = getattr(self, method_name)
19
+ if not getattr(method, ts_data_flag, False):
20
+ continue
21
+ self.data[method_name] = TimeSeriesObject()
22
+
23
+ def set_date(self, new_date: datetime.datetime) -> None:
24
+ self.timestamp = int(new_date.timestamp())
25
+
26
+ def reset_data(self) -> None:
27
+ self.timestamp = -1
28
+
29
+ def update(self, new_data: dict[str, Any], overwrite: bool=False) -> None:
30
+ if self.timestamp == -1:
31
+ raise ValueError("`set_date` before attempting to `update`")
32
+ for method_name, data_point in new_data.items():
33
+ self.data[method_name].insert(self.timestamp, data_point, overwrite)
34
+
35
+
36
+ def time_series_data(fn: Callable) -> Callable:
37
+ fn.is_time_series_data = True
38
+ @wraps(fn)
39
+ def wrapper(self, *args, **kwargs) -> Any:
40
+ if self.timestamp == -1:
41
+ return fn(self, *args, **kwargs)
42
+
43
+ data_point = self.data[fn.__name__].point(self.timestamp)
44
+ if not data_point:
45
+ return fn(self, *args, **kwargs)
46
+
47
+ return data_point[1]
48
+
49
+ return wrapper
@@ -0,0 +1,131 @@
1
+ import pytest
2
+
3
+ from generic_time_series_objects import TimeSeriesObject
4
+
5
+
6
+ TEST_INIT_POINTS = [
7
+ [1, 2, 3, 4, 5, 10],
8
+ [-1.0, -7.5, -100.1, -50, -1000],
9
+ [3, -1, -50.5, "X", ["OOPS"], {1, 2, 3, 4}, {"TEST": "TEST1"}, lambda x: x, ("1", True), None],
10
+ range(1, 1_000_000),
11
+ ]
12
+
13
+ TEST_POINT_DATA = [
14
+ ([1, 5, 10, 15], [10, 20, 30, 40]),
15
+ ]
16
+
17
+
18
+ @pytest.mark.parametrize("test_points", TEST_INIT_POINTS)
19
+ def test_initialise_ts_object(test_points: list):
20
+ obj = TimeSeriesObject()
21
+ assert obj is not None
22
+ for i, val in enumerate(test_points):
23
+ obj.insert(i, val)
24
+
25
+ for i, val in enumerate(test_points):
26
+ point_time, point_value = obj.point(i)
27
+ assert point_time == i
28
+ assert point_value == val
29
+ assert len(obj) == len(test_points)
30
+
31
+ assert obj.as_dict() == {i: val for i, val in enumerate(test_points)}
32
+ assert obj.as_list() == [(i, val) for i, val in enumerate(test_points)]
33
+ with pytest.raises(AttributeError):
34
+ obj.get_insertion_index(3)
35
+ with pytest.raises(AttributeError):
36
+ obj.is_empty()
37
+
38
+
39
+ def test_empty_object():
40
+ obj = TimeSeriesObject()
41
+ assert obj is not None
42
+ assert obj.as_dict() == {}
43
+ assert obj.as_list() == []
44
+ assert obj.point(0) is None
45
+ assert obj.points_between(0, 1) == []
46
+ with pytest.raises(ValueError):
47
+ obj.update(100, None)
48
+ with pytest.raises(ValueError):
49
+ obj.delete(100)
50
+
51
+
52
+ @pytest.mark.parametrize(("test_ts", "test_points"), TEST_POINT_DATA)
53
+ def test_ts_object_point(test_ts: list, test_points: list):
54
+ obj = TimeSeriesObject()
55
+ for ts, val in zip(test_ts, test_points):
56
+ obj.insert(ts, val)
57
+
58
+ loop_test_ts = test_ts.copy()
59
+ loop_test_points = test_points.copy()
60
+ for i in range(0, max(test_ts)+1):
61
+ if i < test_ts[0]:
62
+ assert obj.point(i) is None
63
+ continue
64
+ point_time, point_value = obj.point(i)
65
+ if i == loop_test_ts[1]:
66
+ loop_test_ts = loop_test_ts[1:]
67
+ loop_test_points = loop_test_points[1:]
68
+ assert point_time >= loop_test_ts[0]
69
+ assert point_value == loop_test_points[0]
70
+
71
+ assert obj.point(100000) == (test_ts[-1], test_points[-1])
72
+
73
+
74
+ @pytest.mark.parametrize(("test_ts", "test_points"), TEST_POINT_DATA)
75
+ def test_ts_object_point_on(test_ts: list, test_points: list):
76
+ obj = TimeSeriesObject()
77
+ for ts, val in zip(test_ts, test_points):
78
+ obj.insert(ts, val)
79
+
80
+ for i in range(0, max(test_ts)+1):
81
+ if i not in test_ts:
82
+ assert obj.point_on(i) is None
83
+ continue
84
+ point_time, point_value = obj.point_on(i)
85
+ assert point_time in test_ts
86
+ assert point_value in test_points
87
+
88
+ assert obj.point_on(100000) is None
89
+
90
+
91
+ @pytest.mark.parametrize(("test_ts", "test_points"), TEST_POINT_DATA)
92
+ def test_ts_points_between(test_ts: list, test_points: list):
93
+ obj = TimeSeriesObject()
94
+ for ts, val in zip(test_ts, test_points):
95
+ obj.insert(ts, val)
96
+ assert [x for _, x in obj.points_between(0, 1_000_000)] == [10, 20, 30, 40]
97
+ assert [x for _, x in obj.points_between(1, 16)] == [10, 20, 30, 40]
98
+ assert [x for _, x in obj.points_between(1, 15)] == [10, 20, 30]
99
+ assert [x for _, x in obj.points_between(1, 14)] == [10, 20, 30]
100
+ assert [x for _, x in obj.points_between(2, 12)] == [20, 30]
101
+ assert [x for _, x in obj.points_between(2, 11)] == [20, 30]
102
+ assert [x for _, x in obj.points_between(1, 10)] == [10, 20]
103
+ assert [x for _, x in obj.points_between(0, 0)] == []
104
+
105
+
106
+ @pytest.mark.parametrize(("test_ts", "test_points"), TEST_POINT_DATA)
107
+ def test_update_delete_from_object(test_ts: list, test_points: list):
108
+ obj = TimeSeriesObject()
109
+ for ts, val in zip(test_ts, test_points):
110
+ obj.insert(ts, val)
111
+ # cannot add same thing twice
112
+ with pytest.raises(ValueError):
113
+ obj.insert(test_ts[0], test_points[0])
114
+
115
+ obj.insert(test_ts[0], test_points[0], overwrite=True)
116
+
117
+ # check index must exist for update
118
+ with pytest.raises(IndexError):
119
+ obj.update(0, None)
120
+
121
+ obj.update(test_ts[0], None)
122
+
123
+ # check can delete
124
+ with pytest.raises(IndexError):
125
+ obj.delete(0)
126
+
127
+ for ts in test_ts:
128
+ obj.delete(ts)
129
+
130
+ with pytest.raises(ValueError):
131
+ obj.delete(test_ts[0])
@@ -0,0 +1,43 @@
1
+ # import pytest
2
+ import datetime
3
+
4
+ from generic_time_series_objects import TimeSeriesDataBaseclass, time_series_data
5
+
6
+
7
+ class TestClass(TimeSeriesDataBaseclass):
8
+
9
+ def __init__(self, a, b, c):
10
+ super().__init__()
11
+ self.a = a
12
+ self.b = b
13
+ self.c = c
14
+
15
+ @time_series_data
16
+ def A(self):
17
+ return self.a
18
+
19
+ @time_series_data
20
+ def B(self):
21
+ return self.b
22
+
23
+ @time_series_data
24
+ def C(self):
25
+ return self.c
26
+
27
+ @time_series_data
28
+ def D(self):
29
+ return 1
30
+
31
+ def E(self):
32
+ return 0
33
+
34
+ test_obj = TestClass(2, 3, 4)
35
+ print(test_obj.A(), test_obj.B(), test_obj.C(), test_obj.D())
36
+ test_obj.set_date(datetime.datetime(2025, 12, 13))
37
+ test_obj.update({"A": 9, "B": 99, "C": 999})
38
+ print(test_obj.A(), test_obj.B(), test_obj.C(), test_obj.D())
39
+ test_obj.reset_data()
40
+ print(test_obj.A(), test_obj.B(), test_obj.C(), test_obj.D())
41
+ test_obj.set_date(datetime.datetime(2025, 12, 14))
42
+ print(test_obj.A(), test_obj.B(), test_obj.C(), test_obj.D())
43
+ print(test_obj.data)
@@ -0,0 +1,168 @@
1
+ use std::collections::HashMap;
2
+
3
+ use pyo3::exceptions::{PyValueError, PyIndexError};
4
+ use pyo3::prelude::*;
5
+
6
+
7
+ #[pyclass]
8
+ struct TimeSeriesObject {
9
+ timestamps: Vec<i32>,
10
+ values: Vec<Py<PyAny>>,
11
+ }
12
+
13
+
14
+ impl TimeSeriesObject {
15
+ fn get_insertion_index(&self, ts: i32) -> usize {
16
+ self.timestamps.binary_search(&ts).unwrap_or_else(|idx| idx)
17
+ }
18
+
19
+ fn is_empty(&self) -> bool {
20
+ self.timestamps.is_empty()
21
+ }
22
+ }
23
+
24
+
25
+ #[pymethods]
26
+ impl TimeSeriesObject {
27
+
28
+ #[new]
29
+ fn new() -> Self {
30
+ TimeSeriesObject {timestamps: Vec::new(), values: Vec::new()}
31
+ }
32
+
33
+ fn __len__(&self) -> usize {
34
+ self.timestamps.len()
35
+ }
36
+
37
+ fn __repr__(&self) -> String {
38
+ let timestamps = &self.timestamps;
39
+ format!("TimeSeriesObject(timestamps={timestamps:?})")
40
+ }
41
+
42
+ fn __bool__(&self) -> bool {
43
+ !self.is_empty()
44
+ }
45
+
46
+ #[pyo3(signature = (ts, value, overwrite = false))]
47
+ fn insert(&mut self, ts: i32, value: Py<PyAny>, overwrite: bool) -> PyResult<()> {
48
+ if self.is_empty() || (ts > self.timestamps[self.timestamps.len()-1]) {
49
+ self.timestamps.push(ts);
50
+ self.values.push(value);
51
+ return Ok(())
52
+ }
53
+ let idx = self.get_insertion_index(ts);
54
+
55
+ let current_ts_at_idx = self.timestamps[idx];
56
+
57
+ if (current_ts_at_idx == ts) & overwrite{
58
+ self.values[idx] = value;
59
+ return Ok(())
60
+ }
61
+
62
+ if (current_ts_at_idx == ts) & !overwrite{
63
+ return Err(PyValueError::new_err("Timestamp already exists in TimeSeriesObject, set overwrite=True to overwrite this value"))
64
+ }
65
+
66
+ self.timestamps.insert(idx, ts);
67
+ self.values.insert(idx, value);
68
+ Ok(())
69
+ }
70
+
71
+ fn delete(&mut self, ts: i32) -> PyResult<()> {
72
+ if self.is_empty() {
73
+ return Err(PyValueError::new_err("TimeSeriesObject is empty"))
74
+ }
75
+ let delete_idx = self.get_insertion_index(ts);
76
+ if self.timestamps[delete_idx] != ts {
77
+ return Err(PyIndexError::new_err("Timestamp does not exist in TimeSeriesObject"))
78
+ }
79
+ self.timestamps.remove(delete_idx);
80
+ self.values.remove(delete_idx);
81
+ Ok(())
82
+ }
83
+
84
+ fn update(&mut self, ts: i32, value: Py<PyAny>) -> PyResult<()> {
85
+ if self.is_empty() {
86
+ return Err(PyValueError::new_err("TimeSeriesObject is empty"))
87
+ }
88
+ let update_idx = self.get_insertion_index(ts);
89
+ if self.timestamps[update_idx] != ts {
90
+ return Err(PyIndexError::new_err("Timestamp does not exist in TimeSeriesObject"))
91
+ }
92
+ self.values[update_idx] = value;
93
+ Ok(())
94
+ }
95
+
96
+ fn point(&self, ts: i32) -> Option<(&i32, &Py<PyAny>)> {
97
+ if self.is_empty() {
98
+ return None
99
+ }
100
+
101
+ let idx = self.get_insertion_index(ts);
102
+ if idx == 0 && self.timestamps[idx] == ts {
103
+ return Some((&self.timestamps[idx], &self.values[idx]))
104
+ }
105
+ if idx == 0 && self.timestamps[idx] != ts{
106
+ return None
107
+ }
108
+
109
+ if (idx == self.__len__()) || self.timestamps[idx] != ts {
110
+ Some((&self.timestamps[idx-1], &self.values[idx-1]))
111
+ } else if self.timestamps[idx] == ts{
112
+ Some((&self.timestamps[idx], &self.values[idx]))
113
+ } else {
114
+ None
115
+ }
116
+ }
117
+
118
+ fn point_on(&self, ts: i32) -> Option<(&i32, &Py<PyAny>)> {
119
+ if self.is_empty() {
120
+ return None
121
+ }
122
+ let idx = self.get_insertion_index(ts);
123
+ if (idx != self.__len__()) && self.timestamps[idx] == ts {
124
+ Some((&self.timestamps[idx], &self.values[idx]))
125
+ } else {
126
+ None
127
+ }
128
+ }
129
+
130
+ fn points_between(&self, start_ts: i32, end_ts: i32) -> Vec<(&i32,&Py<PyAny>)> {
131
+ let start_idx = self.get_insertion_index(start_ts);
132
+ let end_idx = self.get_insertion_index(end_ts);
133
+ let mut return_vec = Vec::new();
134
+ for idx in start_idx..end_idx {
135
+ let point_ts = &self.timestamps[idx];
136
+ let value_ts = &self.values[idx];
137
+ return_vec.push((point_ts, value_ts))
138
+ }
139
+ return_vec
140
+ }
141
+
142
+ fn as_dict(&self) -> HashMap<&i32, &Py<PyAny>> {
143
+ let mut return_dict = HashMap::new();
144
+ for idx in 0..self.__len__() {
145
+ let point_ts = &self.timestamps[idx];
146
+ let value_ts = &self.values[idx];
147
+ return_dict.insert(point_ts, value_ts);
148
+ }
149
+ return_dict
150
+ }
151
+
152
+ fn as_list(&self) -> Vec<(&i32, &Py<PyAny>)> {
153
+ let mut return_list = Vec::new();
154
+ for idx in 0..self.__len__() {
155
+ let point_ts = &self.timestamps[idx];
156
+ let value_ts = &self.values[idx];
157
+ return_list.push((point_ts, value_ts));
158
+ }
159
+ return_list
160
+ }
161
+ }
162
+
163
+
164
+ #[pymodule]
165
+ fn generic_time_series_objects(m: &Bound<'_, PyModule>) -> PyResult<()> {
166
+ m.add_class::<TimeSeriesObject>()?;
167
+ Ok(())
168
+ }
@@ -0,0 +1,75 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "generic-time-series-objects"
16
+ version = "0.1.1"
17
+ source = { editable = "." }
18
+ dependencies = [
19
+ { name = "pytest" },
20
+ ]
21
+
22
+ [package.metadata]
23
+ requires-dist = [{ name = "pytest", specifier = ">=8.4.2" }]
24
+
25
+ [[package]]
26
+ name = "iniconfig"
27
+ version = "2.3.0"
28
+ source = { registry = "https://pypi.org/simple" }
29
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
30
+ wheels = [
31
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
32
+ ]
33
+
34
+ [[package]]
35
+ name = "packaging"
36
+ version = "25.0"
37
+ source = { registry = "https://pypi.org/simple" }
38
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
39
+ wheels = [
40
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
41
+ ]
42
+
43
+ [[package]]
44
+ name = "pluggy"
45
+ version = "1.6.0"
46
+ source = { registry = "https://pypi.org/simple" }
47
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
48
+ wheels = [
49
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
50
+ ]
51
+
52
+ [[package]]
53
+ name = "pygments"
54
+ version = "2.19.2"
55
+ source = { registry = "https://pypi.org/simple" }
56
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
57
+ wheels = [
58
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
59
+ ]
60
+
61
+ [[package]]
62
+ name = "pytest"
63
+ version = "8.4.2"
64
+ source = { registry = "https://pypi.org/simple" }
65
+ dependencies = [
66
+ { name = "colorama", marker = "sys_platform == 'win32'" },
67
+ { name = "iniconfig" },
68
+ { name = "packaging" },
69
+ { name = "pluggy" },
70
+ { name = "pygments" },
71
+ ]
72
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
75
+ ]