egauge-python 0.9.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. egauge/ctid/__init__.py +7 -0
  2. egauge/ctid/bit_stuffer.py +65 -0
  3. egauge/ctid/ctid.py +967 -0
  4. egauge/ctid/encoder.py +436 -0
  5. egauge/ctid/intel_hex_encoder.py +98 -0
  6. egauge/ctid/waveform.py +299 -0
  7. egauge/examples/data/test-ctid-decoder.raw +0 -0
  8. egauge/examples/test_capture.py +77 -0
  9. egauge/examples/test_common.py +26 -0
  10. egauge/examples/test_ctid.py +89 -0
  11. egauge/examples/test_ctid_decoder.py +93 -0
  12. egauge/examples/test_local.py +201 -0
  13. egauge/examples/test_register.py +104 -0
  14. egauge/loggers.py +72 -0
  15. egauge/pyside/__init__.py +0 -0
  16. egauge/pyside/ansi2html.py +112 -0
  17. egauge/pyside/terminal.py +295 -0
  18. egauge/webapi/__init__.py +34 -0
  19. egauge/webapi/auth.py +364 -0
  20. egauge/webapi/cloud/__init__.py +30 -0
  21. egauge/webapi/cloud/credentials.py +86 -0
  22. egauge/webapi/cloud/credentials_dialog.py +58 -0
  23. egauge/webapi/cloud/gui/credentials_dialog.py +100 -0
  24. egauge/webapi/cloud/serial_number.py +276 -0
  25. egauge/webapi/device/__init__.py +38 -0
  26. egauge/webapi/device/capture.py +453 -0
  27. egauge/webapi/device/ctid_info.py +553 -0
  28. egauge/webapi/device/device.py +349 -0
  29. egauge/webapi/device/local.py +268 -0
  30. egauge/webapi/device/physical_quantity.py +439 -0
  31. egauge/webapi/device/physical_units.py +473 -0
  32. egauge/webapi/device/register.py +338 -0
  33. egauge/webapi/device/register_row.py +145 -0
  34. egauge/webapi/device/register_type.py +851 -0
  35. egauge/webapi/device/slop.py +334 -0
  36. egauge/webapi/device/virtual_register.py +353 -0
  37. egauge/webapi/error.py +34 -0
  38. egauge/webapi/json_api.py +332 -0
  39. egauge_python-0.9.8.dist-info/METADATA +148 -0
  40. egauge_python-0.9.8.dist-info/RECORD +44 -0
  41. egauge_python-0.9.8.dist-info/WHEEL +5 -0
  42. egauge_python-0.9.8.dist-info/entry_points.txt +2 -0
  43. egauge_python-0.9.8.dist-info/licenses/LICENSE +22 -0
  44. egauge_python-0.9.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,30 @@
1
+ #
2
+ # Copyright (c) 2020, 2024 eGauge Systems LLC
3
+ # 1644 Conestoga St, Suite 2
4
+ # Boulder, CO 80301
5
+ # voice: 720-545-9767
6
+ # email: davidm@egauge.net
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # MIT License
11
+ #
12
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ # of this software and associated documentation files (the "Software"), to deal
14
+ # in the Software without restriction, including without limitation the rights
15
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ # copies of the Software, and to permit persons to whom the Software is
17
+ # furnished to do so, subject to the following conditions:
18
+ #
19
+ # The above copyright notice and this permission notice shall be included in
20
+ # all copies or substantial portions of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ # THE SOFTWARE.
29
+ #
30
+ from .serial_number import * # NOQA
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright (c) 2021-2022, 2024 eGauge Systems LLC
4
+ # 1644 Conestoga St, Suite 2
5
+ # Boulder, CO 80301
6
+ # voice: 720-545-9767
7
+ # email: dave@egauge.net
8
+ #
9
+ # All rights reserved.
10
+ #
11
+ # This code is the property of eGauge Systems LLC and may not be
12
+ # copied, modified, or disclosed without any prior and written
13
+ # permission from eGauge Systems LLC.
14
+ #
15
+ """This module provides a credentials manager which can ask for
16
+ credentials (username, password, second factor) either via a PySide
17
+ dialog or by prompting the user via stdout and stdin (i.e., the
18
+ terminal).
19
+
20
+ """
21
+
22
+ import getpass
23
+
24
+ CredentialsDialog = None
25
+
26
+ try:
27
+ from .credentials_dialog import CredentialsDialog
28
+
29
+ have_pyside = True
30
+ except (ImportError, ModuleNotFoundError):
31
+ have_pyside = False
32
+
33
+
34
+ class LoginCanceled(Exception):
35
+ """Raised when the user cancels a login request."""
36
+
37
+
38
+ class CredentialsManager:
39
+ def __init__(self, gui_parent=None):
40
+ """Create a credentials manager. The task of this manager is mainly
41
+ to track if a previous login failed. The user of this object
42
+ should set the previous_login_failed member to False after the
43
+ credentials have been used successfully.
44
+
45
+ If GUI_PARENT is not None, it must be the QT5 (PySide6) parent
46
+ window to use for the dialog. If it is None, the credentials
47
+ will be requested via standard I/O (getpass).
48
+
49
+ """
50
+ self.parent = gui_parent if have_pyside else None
51
+ self.previous_login_failed = False
52
+
53
+ def ask(self):
54
+ """Ask for the username, password, and optional token for an eGauge
55
+ cloud API account (eGuard account). Returns a tuple
56
+ containing a username and password or raises LoginCanceled if
57
+ the user presses the "Cancel" button.
58
+
59
+ """
60
+ if self.parent and CredentialsDialog is not None:
61
+ dialog = CredentialsDialog(self.parent, self.previous_login_failed)
62
+ dialog.exec()
63
+ if not dialog.accepted:
64
+ raise LoginCanceled()
65
+ self.previous_login_failed = True
66
+ pwd = "" if dialog.password is None else dialog.password
67
+ if dialog.token is not None:
68
+ pwd += dialog.token
69
+ return (dialog.username, pwd)
70
+
71
+ fail_msg = ""
72
+ if self.previous_login_failed:
73
+ fail_msg = "Login failed. "
74
+ print(fail_msg + "Please enter eGuard credentials.")
75
+ try:
76
+ usr = input("Username: ")
77
+ pwd = getpass.getpass(prompt="Password[+token]: ")
78
+ except (KeyboardInterrupt, EOFError) as e:
79
+ raise LoginCanceled from e
80
+ self.previous_login_failed = True
81
+ return [usr, pwd]
82
+
83
+
84
+ # Alias for backwards-compatibility. Please use CredentialsManager in
85
+ # new code.
86
+ Credentials_Manager = CredentialsManager
@@ -0,0 +1,58 @@
1
+ #
2
+ # Copyright (c) 2021-2022, 2024 eGauge Systems LLC
3
+ # 1644 Conestoga St, Suite 2
4
+ # Boulder, CO 80301
5
+ # voice: 720-545-9767
6
+ # email: dave@egauge.net
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # This code is the property of eGauge Systems LLC and may not be
11
+ # copied, modified, or disclosed without any prior and written
12
+ # permission from eGauge Systems LLC.
13
+ #
14
+ from PySide6.QtWidgets import QDialog
15
+
16
+ from .gui.credentials_dialog import Ui_Credentials_Dialog
17
+
18
+ # pyright: reportIncompatibleVariableOverride = false
19
+ #
20
+ # Unfortunately, we need the above because PySide automatically
21
+ # defines a Signal called `accepted` but the users of this dialog
22
+ # expected that property to be a bool indicating whether or not the
23
+ # user clicked the "OK" button.
24
+ #
25
+
26
+
27
+ class CredentialsDialog(QDialog, Ui_Credentials_Dialog):
28
+ @property
29
+ def accepted(self) -> bool:
30
+ return self.user_accepted
31
+
32
+ def __init__(self, parent, failed):
33
+ self.username = None
34
+ self.user_accepted = False
35
+ self.password = None
36
+ self.token = ""
37
+ super().__init__(parent)
38
+ self.setupUi(self)
39
+ if failed:
40
+ prompt = "Login failed. " + self.prompt_label.text()
41
+ self.prompt_label.setText(prompt)
42
+ self.username_lineEdit.setFocus()
43
+
44
+ def exec(self) -> int:
45
+ self.user_accepted = False
46
+ return super().exec()
47
+
48
+ def accept(self):
49
+ super().accept()
50
+ self.user_accepted = True
51
+ self.username = self.username_lineEdit.text()
52
+ self.password = self.password_lineEdit.text()
53
+ self.token = self.token_lineEdit.text()
54
+
55
+
56
+ # Alias for backwards-compatibility. Please use CredentialsDialog in
57
+ # new code.
58
+ Credentials_Dialog = CredentialsDialog
@@ -0,0 +1,100 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ################################################################################
4
+ ## Form generated from reading UI file 'credentials_dialog.ui'
5
+ ##
6
+ ## Created by: Qt User Interface Compiler version 6.7.2
7
+ ##
8
+ ## WARNING! All changes made in this file will be lost when recompiling UI file!
9
+ ################################################################################
10
+
11
+ from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
12
+ QMetaObject, QObject, QPoint, QRect,
13
+ QSize, QTime, QUrl, Qt)
14
+ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
15
+ QFont, QFontDatabase, QGradient, QIcon,
16
+ QImage, QKeySequence, QLinearGradient, QPainter,
17
+ QPalette, QPixmap, QRadialGradient, QTransform)
18
+ from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
19
+ QGridLayout, QLabel, QLineEdit, QSizePolicy,
20
+ QVBoxLayout, QWidget)
21
+
22
+ class Ui_Credentials_Dialog(object):
23
+ def setupUi(self, Credentials_Dialog):
24
+ if not Credentials_Dialog.objectName():
25
+ Credentials_Dialog.setObjectName(u"Credentials_Dialog")
26
+ Credentials_Dialog.resize(327, 194)
27
+ self.verticalLayout = QVBoxLayout(Credentials_Dialog)
28
+ self.verticalLayout.setObjectName(u"verticalLayout")
29
+ self.prompt_label = QLabel(Credentials_Dialog)
30
+ self.prompt_label.setObjectName(u"prompt_label")
31
+ self.prompt_label.setWordWrap(True)
32
+
33
+ self.verticalLayout.addWidget(self.prompt_label)
34
+
35
+ self.gridLayout = QGridLayout()
36
+ self.gridLayout.setObjectName(u"gridLayout")
37
+ self.label_3 = QLabel(Credentials_Dialog)
38
+ self.label_3.setObjectName(u"label_3")
39
+
40
+ self.gridLayout.addWidget(self.label_3, 0, 0, 1, 1)
41
+
42
+ self.username_lineEdit = QLineEdit(Credentials_Dialog)
43
+ self.username_lineEdit.setObjectName(u"username_lineEdit")
44
+
45
+ self.gridLayout.addWidget(self.username_lineEdit, 0, 1, 1, 1)
46
+
47
+ self.label = QLabel(Credentials_Dialog)
48
+ self.label.setObjectName(u"label")
49
+
50
+ self.gridLayout.addWidget(self.label, 1, 0, 1, 1)
51
+
52
+ self.password_lineEdit = QLineEdit(Credentials_Dialog)
53
+ self.password_lineEdit.setObjectName(u"password_lineEdit")
54
+ self.password_lineEdit.setInputMask(u"")
55
+ self.password_lineEdit.setText(u"")
56
+ self.password_lineEdit.setEchoMode(QLineEdit.Password)
57
+
58
+ self.gridLayout.addWidget(self.password_lineEdit, 1, 1, 1, 1)
59
+
60
+ self.label_2 = QLabel(Credentials_Dialog)
61
+ self.label_2.setObjectName(u"label_2")
62
+
63
+ self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1)
64
+
65
+ self.token_lineEdit = QLineEdit(Credentials_Dialog)
66
+ self.token_lineEdit.setObjectName(u"token_lineEdit")
67
+ self.token_lineEdit.setInputMask(u"")
68
+ self.token_lineEdit.setText(u"")
69
+ self.token_lineEdit.setEchoMode(QLineEdit.Password)
70
+
71
+ self.gridLayout.addWidget(self.token_lineEdit, 2, 1, 1, 1)
72
+
73
+
74
+ self.verticalLayout.addLayout(self.gridLayout)
75
+
76
+ self.buttonBox = QDialogButtonBox(Credentials_Dialog)
77
+ self.buttonBox.setObjectName(u"buttonBox")
78
+ self.buttonBox.setOrientation(Qt.Horizontal)
79
+ self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
80
+
81
+ self.verticalLayout.addWidget(self.buttonBox)
82
+
83
+ QWidget.setTabOrder(self.username_lineEdit, self.password_lineEdit)
84
+ QWidget.setTabOrder(self.password_lineEdit, self.token_lineEdit)
85
+
86
+ self.retranslateUi(Credentials_Dialog)
87
+ self.buttonBox.accepted.connect(Credentials_Dialog.accept)
88
+ self.buttonBox.rejected.connect(Credentials_Dialog.reject)
89
+
90
+ QMetaObject.connectSlotsByName(Credentials_Dialog)
91
+ # setupUi
92
+
93
+ def retranslateUi(self, Credentials_Dialog):
94
+ Credentials_Dialog.setWindowTitle(QCoreApplication.translate("Credentials_Dialog", u"eGuard Login", None))
95
+ self.prompt_label.setText(QCoreApplication.translate("Credentials_Dialog", u"Please enter your eGuard credentials. If required, include the current 2FA token.", None))
96
+ self.label_3.setText(QCoreApplication.translate("Credentials_Dialog", u"Username", None))
97
+ self.label.setText(QCoreApplication.translate("Credentials_Dialog", u"Password", None))
98
+ self.label_2.setText(QCoreApplication.translate("Credentials_Dialog", u"2FA token", None))
99
+ # retranslateUi
100
+
@@ -0,0 +1,276 @@
1
+ #
2
+ # Copyright (c) 2020, 2024-2025 eGauge Systems LLC
3
+ # 1644 Conestoga St, Suite 2
4
+ # Boulder, CO 80301
5
+ # voice: 720-545-9767
6
+ # email: davidm@egauge.net
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # MIT License
11
+ #
12
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ # of this software and associated documentation files (the "Software"), to deal
14
+ # in the Software without restriction, including without limitation the rights
15
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ # copies of the Software, and to permit persons to whom the Software is
17
+ # furnished to do so, subject to the following conditions:
18
+ #
19
+ # The above copyright notice and this permission notice shall be included in
20
+ # all copies or substantial portions of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ # THE SOFTWARE.
29
+ #
30
+ """This module provides access to the eGauge Serial Number web
31
+ service.
32
+
33
+ """
34
+
35
+ import urllib.parse
36
+
37
+ import requests
38
+
39
+ from egauge.loggers import ModuleLogger
40
+
41
+ from .. import json_api
42
+ from ..auth import TokenAuth
43
+ from ..error import Error
44
+ from ..json_api import JSONObject, JSONValue
45
+
46
+ log = ModuleLogger.get(__name__)
47
+
48
+
49
+ class SerialNumberError(Error):
50
+ """Raised for Serial Number API errors."""
51
+
52
+
53
+ class SerialNumber:
54
+ def __init__(self, auth: TokenAuth | None = None):
55
+ """Create an object providing access to the serial-number
56
+ service.
57
+
58
+ Keyword arguments:
59
+
60
+ auth -- An authentication object which can provide the
61
+ credentials required to access the serial-number service.
62
+
63
+ """
64
+ self.api_uri = "https://api.egauge.net/v1/serial-numbers/"
65
+ self.auth = auth
66
+
67
+ def _get(self, resource: str, **kwargs) -> JSONValue | requests.Response:
68
+ """Issue an HTTP GET request for a serial-number resource
69
+ and return the reply.
70
+
71
+ Required arguments:
72
+
73
+ resource -- The URI of the resource to get.
74
+
75
+ Keyword arguments are passed on to requests.get().
76
+
77
+ """
78
+ reply = json_api.get(self.api_uri + resource, auth=self.auth, **kwargs)
79
+ log.debug("get[%s] = %s", resource, reply)
80
+ return reply
81
+
82
+ def _post(
83
+ self, resource: str, json_data, **kwargs
84
+ ) -> JSONValue | requests.Response:
85
+ """Issue an HTTP POST request to serial-number resource and
86
+ return the reply.
87
+
88
+ Required arguments:
89
+
90
+ resource -- The URI of the resource to post to.
91
+
92
+ Keyword arguments are passed on to requests.get().
93
+
94
+ """
95
+ reply = json_api.post(
96
+ self.api_uri + resource, json_data, auth=self.auth, **kwargs
97
+ )
98
+ log.debug("post[%s] = %s", resource, reply)
99
+ return reply
100
+
101
+ def allocate(self, model_name: str, serial: int | None = None) -> int:
102
+ """Allocate the next available serial-number for a model and
103
+ return it.
104
+
105
+ Required arguments:
106
+
107
+ model_name -- The model name for which to allocate a serial
108
+ number. Typically, this should be prefixed by the
109
+ manufacturer's name to ensure uniqueness. For example,
110
+ for eGauge model ETN100, the model name would be
111
+ "eGauge-ETN100". Once allocated, a serial-number cannot
112
+ be freed again, so care should be taken to use all
113
+ allocated numbers.
114
+
115
+ Keyword arguments:
116
+
117
+ serial -- If specified, allocate that specific serial-number,
118
+ if it is avaiable, or fail otherwise. Depending on model
119
+ name, the serial-number API service may reject attempts to
120
+ allocate specific serial numbers.
121
+
122
+ On error, exception SerialNumberError is raised.
123
+
124
+ """
125
+ data: JSONObject = {"name": model_name}
126
+ if serial is not None:
127
+ data["serial"] = serial
128
+ reply = self._post("models/allocate/", json_data=data)
129
+ if not isinstance(reply, dict):
130
+ raise SerialNumberError(
131
+ f"Unexpected response allocating SN for {model_name}.", reply
132
+ )
133
+ if "serial" not in reply:
134
+ log.error(
135
+ "Failed to allocate SN: model=%s, reply=%s.", model_name, reply
136
+ )
137
+ if "errors" in reply:
138
+ raise SerialNumberError(
139
+ "Error during SN allocation.", model_name, reply["errors"]
140
+ )
141
+ raise SerialNumberError("SN allocation failed.", model_name)
142
+ sn = reply["serial"]
143
+ if not isinstance(sn, int):
144
+ raise SerialNumberError(f'SN "{sn}" is not an integer.')
145
+ return sn
146
+
147
+ def get_models(self) -> list[dict]:
148
+ """Get a list of all model names registered in the database.
149
+ Each object in the list has members `id` (internal database id
150
+ of the model), `name` (the model name), and `max_sn` (the
151
+ maximum serial-number).
152
+
153
+ On error, exception SerialNumberError is raised.
154
+
155
+ """
156
+
157
+ # For better or worse, this end point returns a list on
158
+ # success but a dictionary in failure:
159
+
160
+ reply = self._get("models/")
161
+
162
+ if isinstance(reply, dict):
163
+ err = reply.get("detail")
164
+ if err:
165
+ raise SerialNumberError(f"Error fetching SN models: {err}")
166
+
167
+ if not isinstance(reply, list):
168
+ raise SerialNumberError(
169
+ "Unexpected response from SN models.", reply
170
+ )
171
+ return reply
172
+
173
+ def create_model(self, model_name: str, max_sn: int) -> bool:
174
+ """Create a new model.
175
+
176
+ Returns True if the model was created successfully, False
177
+ otherwise.
178
+
179
+ Required arguments:
180
+
181
+ model_name -- The name of the model to create.
182
+
183
+ max_sn -- The maximum serial number that may be allocated.
184
+
185
+ """
186
+ data = {"name": model_name, "max_sn": max_sn}
187
+ reply = self._post("models/", data)
188
+ if not isinstance(reply, dict) or "name" not in reply:
189
+ return False
190
+ return reply["name"] == model_name
191
+
192
+ def get_devices(
193
+ self, model_name: str | None = None, dev_filter: str | None = None
194
+ ) -> list[dict]:
195
+ """Get a list of devices.
196
+
197
+ Keyword arguments:
198
+
199
+ model_name -- If specified, only devices with that model name
200
+ are returned.
201
+
202
+ dev_filter -- If specified, only devices matching the filter
203
+ are returned.
204
+
205
+ """
206
+ resource = "devices/"
207
+ if model_name is not None:
208
+ quoted_model = urllib.parse.quote(model_name, safe="")
209
+ resource += quoted_model + "/"
210
+
211
+ if dev_filter is not None:
212
+ resource += "?" + dev_filter
213
+
214
+ reply = self._get(resource)
215
+ if not isinstance(reply, list):
216
+ raise SerialNumberError(
217
+ "Failed to get metadata.", model_name, dev_filter
218
+ )
219
+ return reply
220
+
221
+ def get_metadata(self, model_name: str, sn: int) -> dict:
222
+ """Get the JSON-blob metadata for a device.
223
+
224
+ On error, exception SerialNumberError is raised.
225
+
226
+ Required arguments:
227
+
228
+ model_name -- The model name of the device.
229
+
230
+ sn -- The serial number of the device.
231
+
232
+ """
233
+ quoted_model = urllib.parse.quote(model_name, safe="")
234
+ resource = f"devices/{quoted_model}/{sn}/"
235
+ reply = self._get(resource)
236
+ if not isinstance(reply, dict):
237
+ raise SerialNumberError(
238
+ "Failed to get serial number record.", model_name, sn
239
+ )
240
+
241
+ metadata = reply.get("metadata")
242
+ if not isinstance(metadata, dict):
243
+ log.warning("no metadata exists for SN %s.", sn)
244
+ return {}
245
+
246
+ return metadata
247
+
248
+ def set_metadata(self, model_name: str, sn: int, meta: dict):
249
+ """Set the metadata for a device.
250
+
251
+ Using methods get_metadata() and set_metadata() to update
252
+ portions of the metadata is not atomic. Higher-level
253
+ synchronization (such as a ResourceLock) can be used to ensure
254
+ atomicity of such updates.
255
+
256
+ Required arguments:
257
+
258
+ model_name -- The model name of the device.
259
+
260
+ sn -- The serial number of the device.
261
+
262
+ meta -- The meta data to associated with the device. This
263
+ must be serializable with json.dumps().
264
+
265
+ On error, exception SerialNumberError is raised.
266
+
267
+ """
268
+ quoted_model = urllib.parse.quote(model_name, safe="")
269
+ resource = "devices/%s/%s/" % (quoted_model, sn)
270
+ reply = self._post(resource, json_data={"metadata": meta})
271
+ if not isinstance(reply, dict):
272
+ raise SerialNumberError("Failed to set metadata.", model_name, sn)
273
+ if "errors" in reply:
274
+ raise SerialNumberError(
275
+ "Failed to save metadata.", model_name, sn, reply["errors"]
276
+ )
@@ -0,0 +1,38 @@
1
+ #
2
+ # Copyright (c) 2020 eGauge Systems LLC
3
+ # 1644 Conestoga St, Suite 2
4
+ # Boulder, CO 80301
5
+ # voice: 720-545-9767
6
+ # email: davidm@egauge.net
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # MIT License
11
+ #
12
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ # of this software and associated documentation files (the "Software"), to deal
14
+ # in the Software without restriction, including without limitation the rights
15
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ # copies of the Software, and to permit persons to whom the Software is
17
+ # furnished to do so, subject to the following conditions:
18
+ #
19
+ # The above copyright notice and this permission notice shall be included in
20
+ # all copies or substantial portions of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ # THE SOFTWARE.
29
+ #
30
+ from .capture import * # NOQA
31
+ from .ctid_info import * # NOQA
32
+ from .device import * # NOQA
33
+ from .local import * # NOQA
34
+ from .physical_quantity import * # NOQA
35
+ from .physical_units import * # NOQA
36
+ from .register import * # NOQA
37
+ from .register_row import * # NOQA
38
+ from .virtual_register import * # NOQA