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.
- egauge/ctid/__init__.py +7 -0
- egauge/ctid/bit_stuffer.py +65 -0
- egauge/ctid/ctid.py +967 -0
- egauge/ctid/encoder.py +436 -0
- egauge/ctid/intel_hex_encoder.py +98 -0
- egauge/ctid/waveform.py +299 -0
- egauge/examples/data/test-ctid-decoder.raw +0 -0
- egauge/examples/test_capture.py +77 -0
- egauge/examples/test_common.py +26 -0
- egauge/examples/test_ctid.py +89 -0
- egauge/examples/test_ctid_decoder.py +93 -0
- egauge/examples/test_local.py +201 -0
- egauge/examples/test_register.py +104 -0
- egauge/loggers.py +72 -0
- egauge/pyside/__init__.py +0 -0
- egauge/pyside/ansi2html.py +112 -0
- egauge/pyside/terminal.py +295 -0
- egauge/webapi/__init__.py +34 -0
- egauge/webapi/auth.py +364 -0
- egauge/webapi/cloud/__init__.py +30 -0
- egauge/webapi/cloud/credentials.py +86 -0
- egauge/webapi/cloud/credentials_dialog.py +58 -0
- egauge/webapi/cloud/gui/credentials_dialog.py +100 -0
- egauge/webapi/cloud/serial_number.py +276 -0
- egauge/webapi/device/__init__.py +38 -0
- egauge/webapi/device/capture.py +453 -0
- egauge/webapi/device/ctid_info.py +553 -0
- egauge/webapi/device/device.py +349 -0
- egauge/webapi/device/local.py +268 -0
- egauge/webapi/device/physical_quantity.py +439 -0
- egauge/webapi/device/physical_units.py +473 -0
- egauge/webapi/device/register.py +338 -0
- egauge/webapi/device/register_row.py +145 -0
- egauge/webapi/device/register_type.py +851 -0
- egauge/webapi/device/slop.py +334 -0
- egauge/webapi/device/virtual_register.py +353 -0
- egauge/webapi/error.py +34 -0
- egauge/webapi/json_api.py +332 -0
- egauge_python-0.9.8.dist-info/METADATA +148 -0
- egauge_python-0.9.8.dist-info/RECORD +44 -0
- egauge_python-0.9.8.dist-info/WHEEL +5 -0
- egauge_python-0.9.8.dist-info/entry_points.txt +2 -0
- egauge_python-0.9.8.dist-info/licenses/LICENSE +22 -0
- 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
|