tagreader 6.0.3__tar.gz → 6.0.4__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.
- {tagreader-6.0.3 → tagreader-6.0.4}/PKG-INFO +2 -1
- {tagreader-6.0.3 → tagreader-6.0.4}/pyproject.toml +2 -1
- {tagreader-6.0.3 → tagreader-6.0.4}/tagreader/clients.py +0 -81
- {tagreader-6.0.3 → tagreader-6.0.4}/tagreader/utils.py +61 -74
- {tagreader-6.0.3 → tagreader-6.0.4}/tagreader/web_handlers.py +1 -32
- {tagreader-6.0.3 → tagreader-6.0.4}/LICENSE +0 -0
- {tagreader-6.0.3 → tagreader-6.0.4}/README.md +0 -0
- {tagreader-6.0.3 → tagreader-6.0.4}/tagreader/__init__.py +0 -0
- {tagreader-6.0.3 → tagreader-6.0.4}/tagreader/__version__.py +0 -0
- {tagreader-6.0.3 → tagreader-6.0.4}/tagreader/cache.py +0 -0
- {tagreader-6.0.3 → tagreader-6.0.4}/tagreader/logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: tagreader
|
|
3
|
-
Version: 6.0.
|
|
3
|
+
Version: 6.0.4
|
|
4
4
|
Summary: Tagreader is a Python package for reading trend data from the OSIsoft PI and Aspen Infoplus.21 IMS systems.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: Aspen InfoPlus.21,OSIsoft PI
|
|
@@ -32,6 +32,7 @@ Requires-Dist: matplotlib (>=3.7.5,<4.0.0) ; extra == "notebooks"
|
|
|
32
32
|
Requires-Dist: msal-bearer (>=1.3.0,<2.0.0)
|
|
33
33
|
Requires-Dist: notebook (>=7.2.2,<8.0.0) ; extra == "notebooks"
|
|
34
34
|
Requires-Dist: pandas (>=1)
|
|
35
|
+
Requires-Dist: platformdirs (>=4.3.7,<5.0.0)
|
|
35
36
|
Requires-Dist: pycryptodome (>=3.20.0,<4.0.0)
|
|
36
37
|
Requires-Dist: requests (>=2,<3)
|
|
37
38
|
Requires-Dist: requests-kerberos (>=0,<1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "tagreader"
|
|
3
|
-
version = "6.0.
|
|
3
|
+
version = "6.0.4"
|
|
4
4
|
description = "Tagreader is a Python package for reading trend data from the OSIsoft PI and Aspen Infoplus.21 IMS systems."
|
|
5
5
|
authors = ["Einar S. Idsø <eiids@equinor.com>", "Morten Dæhli Aslesen <masl@equinor.com"]
|
|
6
6
|
license = "MIT"
|
|
@@ -39,6 +39,7 @@ matplotlib = { version = "^3.7.5", optional = true }
|
|
|
39
39
|
diskcache = "^5.6.1"
|
|
40
40
|
pycryptodome = "^3.20.0"
|
|
41
41
|
requests-ntlm = ">=1.1,<=2.0"
|
|
42
|
+
platformdirs = "^4.3.7"
|
|
42
43
|
|
|
43
44
|
[tool.poetry.group.dev.dependencies]
|
|
44
45
|
pre-commit = "^3"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from datetime import datetime, timedelta, timezone, tzinfo
|
|
3
2
|
from itertools import groupby
|
|
4
3
|
from operator import itemgetter
|
|
@@ -16,9 +15,6 @@ from tagreader.utils import (
|
|
|
16
15
|
ReaderType,
|
|
17
16
|
convert_to_pydatetime,
|
|
18
17
|
ensure_datetime_with_tz,
|
|
19
|
-
find_registry_key,
|
|
20
|
-
find_registry_key_from_name,
|
|
21
|
-
is_windows,
|
|
22
18
|
)
|
|
23
19
|
from tagreader.web_handlers import (
|
|
24
20
|
AspenHandlerWeb,
|
|
@@ -119,83 +115,6 @@ def get_next_timeslice(
|
|
|
119
115
|
return start, calc_end
|
|
120
116
|
|
|
121
117
|
|
|
122
|
-
def get_server_address_aspen(datasource: str) -> Optional[Tuple[str, int]]:
|
|
123
|
-
"""Data sources are listed under
|
|
124
|
-
HKEY_CURRENT_USER\\Software\\AspenTech\\ADSA\\Caches\\AspenADSA\\username.
|
|
125
|
-
For each data source there are multiple keys with Host entries. We start by
|
|
126
|
-
identifying the correct key to use by locating the UUID for Aspen SQLplus
|
|
127
|
-
services located under Aspen SQLplus service component. Then we find the
|
|
128
|
-
host and port based on the path above and the UUID.
|
|
129
|
-
"""
|
|
130
|
-
|
|
131
|
-
# todo: is obsolete after removing ODBC
|
|
132
|
-
|
|
133
|
-
if not is_windows():
|
|
134
|
-
return None
|
|
135
|
-
import winreg
|
|
136
|
-
|
|
137
|
-
regkey_clsid = winreg.OpenKey(
|
|
138
|
-
winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Classes\Wow6432Node\CLSID"
|
|
139
|
-
)
|
|
140
|
-
regkey, _ = find_registry_key_from_name(
|
|
141
|
-
regkey_clsid, "Aspen SQLplus service component"
|
|
142
|
-
)
|
|
143
|
-
regkey_implemented_categories = winreg.OpenKeyEx(regkey, "Implemented Categories")
|
|
144
|
-
|
|
145
|
-
_, aspen_uuid = find_registry_key_from_name(
|
|
146
|
-
regkey_implemented_categories, "Aspen SQLplus services"
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
reg_adsa = winreg.OpenKey(
|
|
150
|
-
winreg.HKEY_CURRENT_USER,
|
|
151
|
-
r"Software\AspenTech\ADSA\Caches\AspenADSA\\" + os.getlogin(),
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
try:
|
|
155
|
-
reg_site_key = winreg.OpenKey(reg_adsa, datasource + "\\" + aspen_uuid)
|
|
156
|
-
host = winreg.QueryValueEx(reg_site_key, "Host")[0]
|
|
157
|
-
port = int(winreg.QueryValueEx(reg_site_key, "Port")[0])
|
|
158
|
-
return host, port
|
|
159
|
-
except FileNotFoundError:
|
|
160
|
-
return None
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def get_server_address_pi(datasource: str) -> Optional[Tuple[str, int]]:
|
|
164
|
-
"""
|
|
165
|
-
PI data sources are listed under
|
|
166
|
-
HKEY_LOCAL_MACHINE\\Software\\Wow6432Node\\PISystem\\PI-SDK\\x.x\\ServerHandles or
|
|
167
|
-
\\Software\\PISystem\\PI-SDK\\x.x\\ServerHandles.
|
|
168
|
-
|
|
169
|
-
:param datasource: Name of data source
|
|
170
|
-
:return: host, port
|
|
171
|
-
:type: tuple(string, int)
|
|
172
|
-
"""
|
|
173
|
-
# todo: is obsolete after removing ODBC
|
|
174
|
-
|
|
175
|
-
if not is_windows():
|
|
176
|
-
return None
|
|
177
|
-
import winreg
|
|
178
|
-
|
|
179
|
-
try:
|
|
180
|
-
reg_key = winreg.OpenKey(
|
|
181
|
-
winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\PISystem\PI-SDK"
|
|
182
|
-
)
|
|
183
|
-
reg_key_handles = find_registry_key(reg_key, "ServerHandles")
|
|
184
|
-
reg_site_key = find_registry_key(reg_key_handles, datasource)
|
|
185
|
-
if reg_site_key is None:
|
|
186
|
-
reg_key = winreg.OpenKey(
|
|
187
|
-
winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\PISystem\PI-SDK"
|
|
188
|
-
)
|
|
189
|
-
reg_key_handles = find_registry_key(reg_key, "ServerHandles")
|
|
190
|
-
reg_site_key = find_registry_key(reg_key_handles, datasource)
|
|
191
|
-
if reg_site_key is not None:
|
|
192
|
-
host = winreg.QueryValueEx(reg_site_key, "path")[0]
|
|
193
|
-
port = int(winreg.QueryValueEx(reg_site_key, "port")[0])
|
|
194
|
-
return host, port
|
|
195
|
-
except FileNotFoundError:
|
|
196
|
-
return None
|
|
197
|
-
|
|
198
|
-
|
|
199
118
|
def get_handler(
|
|
200
119
|
imstype: Optional[IMSType],
|
|
201
120
|
datasource: str,
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import enum
|
|
2
|
+
import hashlib
|
|
2
3
|
import logging
|
|
3
4
|
import platform
|
|
4
|
-
import
|
|
5
|
+
import ssl
|
|
5
6
|
from datetime import datetime, tzinfo
|
|
6
7
|
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
7
9
|
from typing import Union
|
|
8
10
|
|
|
11
|
+
import certifi
|
|
9
12
|
import pandas as pd
|
|
10
13
|
import pytz
|
|
14
|
+
import requests
|
|
15
|
+
from platformdirs import user_data_dir
|
|
11
16
|
|
|
12
17
|
from tagreader.logger import logger
|
|
13
18
|
|
|
@@ -27,47 +32,11 @@ def is_linux() -> bool:
|
|
|
27
32
|
if is_windows():
|
|
28
33
|
import winreg
|
|
29
34
|
|
|
30
|
-
|
|
31
35
|
if is_mac():
|
|
32
36
|
import socket
|
|
33
37
|
import subprocess
|
|
34
38
|
|
|
35
39
|
|
|
36
|
-
def find_registry_key(base_key, search_key_name: str):
|
|
37
|
-
search_key_name = search_key_name.lower()
|
|
38
|
-
if base_key is not None:
|
|
39
|
-
num_keys, _, _ = winreg.QueryInfoKey(base_key)
|
|
40
|
-
for i in range(0, num_keys):
|
|
41
|
-
key_name = winreg.EnumKey(base_key, i)
|
|
42
|
-
if key_name.lower() == search_key_name:
|
|
43
|
-
return winreg.OpenKey(base_key, key_name)
|
|
44
|
-
else:
|
|
45
|
-
key = find_registry_key(
|
|
46
|
-
winreg.OpenKey(base_key, key_name), search_key_name
|
|
47
|
-
)
|
|
48
|
-
if key is not None:
|
|
49
|
-
return key
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def find_registry_key_from_name(base_key, search_key_name: str):
|
|
54
|
-
search_key_name = search_key_name.lower()
|
|
55
|
-
num_keys, _, _ = winreg.QueryInfoKey(base_key)
|
|
56
|
-
key = key_string = None
|
|
57
|
-
for i in range(0, num_keys):
|
|
58
|
-
try:
|
|
59
|
-
key_string = winreg.EnumKey(base_key, i)
|
|
60
|
-
key = winreg.OpenKey(base_key, key_string)
|
|
61
|
-
_, num_vals, _ = winreg.QueryInfoKey(key)
|
|
62
|
-
if num_vals > 0:
|
|
63
|
-
(_, key_name, _) = winreg.EnumValue(key, 0)
|
|
64
|
-
if str(key_name).lower() == search_key_name:
|
|
65
|
-
break
|
|
66
|
-
except Exception as err:
|
|
67
|
-
logging.error("{}: {}".format(i, err))
|
|
68
|
-
return key, key_string
|
|
69
|
-
|
|
70
|
-
|
|
71
40
|
def convert_to_pydatetime(date_stamp: Union[datetime, str, pd.Timestamp]) -> datetime:
|
|
72
41
|
if isinstance(date_stamp, datetime):
|
|
73
42
|
return date_stamp
|
|
@@ -138,64 +107,82 @@ def add_equinor_root_certificate() -> bool:
|
|
|
138
107
|
running on Equinor host.
|
|
139
108
|
|
|
140
109
|
NB! This needs to be repeated after updating the cacert module.
|
|
141
|
-
NB! Will not download certificates.
|
|
142
110
|
|
|
143
111
|
Returns:
|
|
144
112
|
bool: True if function completes successfully
|
|
145
113
|
"""
|
|
146
|
-
|
|
147
|
-
import ssl
|
|
114
|
+
certificate = find_local_equinor_root_certificate()
|
|
148
115
|
|
|
149
|
-
|
|
116
|
+
# If certificate is not found locally, we download it from the Equinor server
|
|
117
|
+
if certificate == "":
|
|
118
|
+
logger.debug(
|
|
119
|
+
"Unable to locate Equinor Root CA certificate on this host. Downloading from Equinor server."
|
|
120
|
+
)
|
|
121
|
+
response = requests.get("http://pki.equinor.com/aia/ecpr.crt")
|
|
150
122
|
|
|
151
|
-
|
|
152
|
-
|
|
123
|
+
if response.status_code != 200:
|
|
124
|
+
logger.error(
|
|
125
|
+
"Unable to find Equinor Root CA certificate locally and on Equinor server."
|
|
126
|
+
)
|
|
127
|
+
return False
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
der = None
|
|
129
|
+
certificate = response.text.replace("\r", "")
|
|
156
130
|
|
|
157
|
-
|
|
131
|
+
# Write result to user data so we can read the cert from there next time
|
|
132
|
+
filepath = Path(user_data_dir("tagreader")) / "equinor_root_ca.crt"
|
|
133
|
+
try:
|
|
134
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
filepath.write_text(certificate)
|
|
136
|
+
logger.debug("Equinor Root CA certificate written to cache")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.debug(f"Failed to write Equinor Root CA certificate to cache: {e}")
|
|
139
|
+
|
|
140
|
+
if certificate in certifi.contents():
|
|
141
|
+
logger.debug("Equinor Root Certificate already exists in certifi store")
|
|
158
142
|
return True
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
|
|
144
|
+
ca_file = certifi.where()
|
|
145
|
+
with open(ca_file, "ab") as f:
|
|
146
|
+
f.write(bytes(certificate, "ascii"))
|
|
147
|
+
logger.debug("Equinor Root Certificate added to certifi store")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def find_local_equinor_root_certificate() -> str:
|
|
151
|
+
equinor_root_pem_hash = "5A206332CE73CED1D44C8A99C4C43B7CEE03DF5F"
|
|
152
|
+
ca_search = "Equinor Root CA"
|
|
153
|
+
|
|
154
|
+
if is_windows():
|
|
155
|
+
logger.debug("Checking for Equinor Root CA in Windows certificate store")
|
|
161
156
|
for cert in ssl.enum_certificates("CA"):
|
|
162
|
-
|
|
157
|
+
found_cert = cert[0]
|
|
163
158
|
# deepcode ignore InsecureHash: <Only hashes to compare with known hash>
|
|
164
|
-
if hashlib.sha1(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
break
|
|
159
|
+
if hashlib.sha1(found_cert).hexdigest().upper() == equinor_root_pem_hash:
|
|
160
|
+
return ssl.DER_cert_to_PEM_cert(found_cert)
|
|
161
|
+
|
|
168
162
|
elif is_mac():
|
|
163
|
+
logger.debug("Checking for Equinor Root CA in MacOS certificate store")
|
|
169
164
|
macos_ca_certs = subprocess.run(
|
|
170
165
|
["security", "find-certificate", "-a", "-c", ca_search, "-Z"],
|
|
171
166
|
stdout=subprocess.PIPE,
|
|
172
167
|
).stdout
|
|
173
168
|
|
|
174
|
-
if
|
|
169
|
+
if equinor_root_pem_hash in str(macos_ca_certs).upper():
|
|
175
170
|
c = get_macos_equinor_certificates()
|
|
176
171
|
for cert in c:
|
|
177
172
|
# deepcode ignore InsecureHash: <Only hashes to compare with known hash>
|
|
178
|
-
if hashlib.sha1(cert).hexdigest().upper() ==
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
else:
|
|
190
|
-
logger.debug("Writing certificate to certifi store.")
|
|
191
|
-
ca_file = certifi.where()
|
|
192
|
-
with open(ca_file, "ab") as f:
|
|
193
|
-
f.write(bytes(pem, "ascii"))
|
|
194
|
-
logger.debug("Completed")
|
|
195
|
-
else:
|
|
196
|
-
warnings.warn("Unable to locate root certificate on this host.")
|
|
173
|
+
if hashlib.sha1(cert).hexdigest().upper() == equinor_root_pem_hash:
|
|
174
|
+
return ssl.DER_cert_to_PEM_cert(cert)
|
|
175
|
+
|
|
176
|
+
# If the certificate is not found in the local cert store, look in the tagreader cache
|
|
177
|
+
filepath = Path(user_data_dir("tagreader")) / "equinor_root_ca.crt"
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
if filepath.exists():
|
|
181
|
+
return filepath.read_text()
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.debug(f"Failed to read Equinor Root CA certificate from cache: {e}")
|
|
197
184
|
|
|
198
|
-
return
|
|
185
|
+
return ""
|
|
199
186
|
|
|
200
187
|
|
|
201
188
|
def get_macos_equinor_certificates():
|
|
@@ -19,7 +19,7 @@ from urllib3.exceptions import InsecureRequestWarning
|
|
|
19
19
|
|
|
20
20
|
from tagreader.cache import BucketCache, SmartCache
|
|
21
21
|
from tagreader.logger import logger
|
|
22
|
-
from tagreader.utils import
|
|
22
|
+
from tagreader.utils import ReaderType, is_mac, is_windows, urljoin
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class MD4:
|
|
@@ -258,11 +258,6 @@ class AspenHandlerWeb(BaseHandlerWeb):
|
|
|
258
258
|
self._max_rows = options.get("max_rows", 100000)
|
|
259
259
|
self._connection_string = "" # Used for raw SQL queries
|
|
260
260
|
|
|
261
|
-
@staticmethod
|
|
262
|
-
def generate_connection_string(host, *_, **__):
|
|
263
|
-
# todo: is obsolete after removing ODBC
|
|
264
|
-
raise NotImplementedError
|
|
265
|
-
|
|
266
261
|
@staticmethod
|
|
267
262
|
def generate_search_query(
|
|
268
263
|
tag: Optional[str],
|
|
@@ -670,27 +665,6 @@ class AspenHandlerWeb(BaseHandlerWeb):
|
|
|
670
665
|
connection_string += f"<![CDATA[{query}]]></SQL>"
|
|
671
666
|
return connection_string
|
|
672
667
|
|
|
673
|
-
def initialize_connection_string(
|
|
674
|
-
# todo: is obsolete after removing ODBC
|
|
675
|
-
self,
|
|
676
|
-
host: Optional[str] = None,
|
|
677
|
-
port: int = 10014,
|
|
678
|
-
connection_string: Optional[str] = None,
|
|
679
|
-
):
|
|
680
|
-
# todo: is obsolete after removing ODBC
|
|
681
|
-
if connection_string:
|
|
682
|
-
self._connection_string = connection_string
|
|
683
|
-
else:
|
|
684
|
-
if host is None:
|
|
685
|
-
from tagreader.clients import get_server_address_aspen
|
|
686
|
-
|
|
687
|
-
host = get_server_address_aspen(self.datasource)
|
|
688
|
-
self._connection_string = (
|
|
689
|
-
f"DRIVER=AspenTech SQLPlus;HOST={host};"
|
|
690
|
-
f"PORT={port};CHARINT=N;CHARFLOAT=N;"
|
|
691
|
-
"CHARTIME=N;CONVERTERRORS=N"
|
|
692
|
-
)
|
|
693
|
-
|
|
694
668
|
def query_sql(self, query: str, parse: bool = True) -> Union[str, pd.DataFrame]:
|
|
695
669
|
url = urljoin(self.base_url, "SQL")
|
|
696
670
|
if self._connection_string is None:
|
|
@@ -758,11 +732,6 @@ class PIHandlerWeb(BaseHandlerWeb):
|
|
|
758
732
|
def _time_to_UTC_string(time: datetime) -> str:
|
|
759
733
|
return time.astimezone(pytz.UTC).strftime("%d-%b-%y %H:%M:%S")
|
|
760
734
|
|
|
761
|
-
@staticmethod
|
|
762
|
-
def generate_connection_string(host, *_, **__):
|
|
763
|
-
# todo: is obsolete after removing ODBC
|
|
764
|
-
raise NotImplementedError
|
|
765
|
-
|
|
766
735
|
@staticmethod
|
|
767
736
|
def escape(s: str) -> str:
|
|
768
737
|
# https://techsupport.osisoft.com/Documentation/PI-Web-API/help/topics/search-queries.html
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|