tagreader 6.0.3__tar.gz → 6.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: tagreader
3
- Version: 6.0.3
3
+ Version: 6.1.0
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
@@ -15,27 +15,23 @@ Classifier: Natural Language :: English
15
15
  Classifier: Operating System :: MacOS
16
16
  Classifier: Operating System :: Microsoft :: Windows
17
17
  Classifier: Operating System :: POSIX :: Linux
18
- Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
19
  Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
- Classifier: Programming Language :: Python :: 3.13
23
- Classifier: Programming Language :: Python :: 3.8
24
- Classifier: Programming Language :: Python :: 3.9
25
22
  Classifier: Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator
26
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
24
  Classifier: Topic :: Utilities
28
- Provides-Extra: notebooks
29
25
  Requires-Dist: certifi (>=2024.12.14,<2026.0.0)
30
- Requires-Dist: diskcache (>=5.6.1,<6.0.0)
31
- Requires-Dist: matplotlib (>=3.7.5,<4.0.0) ; extra == "notebooks"
32
- Requires-Dist: msal-bearer (>=1.3.0,<2.0.0)
33
- Requires-Dist: notebook (>=7.2.2,<8.0.0) ; extra == "notebooks"
26
+ Requires-Dist: diskcache (>=5.6.1)
27
+ Requires-Dist: msal-bearer (>=1.3.3)
34
28
  Requires-Dist: pandas (>=1)
35
- Requires-Dist: pycryptodome (>=3.20.0,<4.0.0)
36
- Requires-Dist: requests (>=2,<3)
37
- Requires-Dist: requests-kerberos (>=0,<1)
29
+ Requires-Dist: platformdirs (>=4.3.7)
30
+ Requires-Dist: pycryptodome (>=3.20.0)
31
+ Requires-Dist: requests (>=2.32.5)
32
+ Requires-Dist: requests-kerberos (>=0)
38
33
  Requires-Dist: requests-ntlm (>=1.1,<=2.0)
34
+ Requires-Dist: urllib3 (>=2.5.0)
39
35
  Project-URL: Homepage, https://github.com/equinor/tagreader-python
40
36
  Project-URL: Repository, https://github.com/equinor/tagreader-python
41
37
  Description-Content-Type: text/markdown
@@ -1,14 +1,15 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "tagreader"
3
- version = "6.0.3"
3
+ version = "6.1.0"
4
4
  description = "Tagreader is a Python package for reading trend data from the OSIsoft PI and Aspen Infoplus.21 IMS systems."
5
- authors = ["Einar S. Idsø <eiids@equinor.com>", "Morten Dæhli Aslesen <masl@equinor.com"]
5
+ authors = [
6
+ { name = "Einar S. Idsø", email = "eiids@equinor.com" },
7
+ { name = "Morten Dæhli Aslesen", email = "masl@equinor.com" }
8
+ ]
6
9
  license = "MIT"
7
10
  readme = "README.md"
8
- packages = [{include = "tagreader"}]
9
11
  keywords=["Aspen InfoPlus.21", "OSIsoft PI"]
10
- homepage = "https://github.com/equinor/tagreader-python"
11
- repository = "https://github.com/equinor/tagreader-python"
12
+ requires-python = ">=3.9.2,<4.0.0"
12
13
  classifiers=[
13
14
  "Development Status :: 5 - Production/Stable",
14
15
  "Intended Audience :: Developers",
@@ -18,35 +19,39 @@ classifiers=[
18
19
  "Operating System :: MacOS",
19
20
  "Operating System :: Microsoft :: Windows",
20
21
  "Operating System :: POSIX :: Linux",
21
- "Programming Language :: Python :: 3.8",
22
22
  "Programming Language :: Python :: 3.9",
23
23
  "Programming Language :: Python :: 3.10",
24
24
  "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
25
26
  "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator",
26
27
  "Topic :: Software Development :: Libraries :: Python Modules",
27
28
  "Topic :: Utilities",
28
29
  ]
30
+ dependencies = [
31
+ "pandas>=1",
32
+ "certifi>=2024.12.14,<2026.0.0",
33
+ "requests>=2.32.5",
34
+ "requests-kerberos>=0",
35
+ "msal-bearer>=1.3.3",
36
+ "diskcache>=5.6.1",
37
+ "pycryptodome>=3.20.0",
38
+ "requests-ntlm>=1.1,<=2.0",
39
+ "platformdirs>=4.3.7",
40
+ "urllib3>=2.5.0" # To avoid security issue https://www.cve.org/CVERecord?id=CVE-2025-50182
41
+ ]
29
42
 
30
- [tool.poetry.dependencies]
31
- python = "^3.9.2"
32
- pandas = ">=1"
33
- certifi = ">=2024.12.14,<2026.0.0"
34
- requests = "^2"
35
- requests-kerberos = "^0"
36
- msal-bearer = "^1.3.0"
37
- notebook = { version = "^7.2.2", optional = true }
38
- matplotlib = { version = "^3.7.5", optional = true }
39
- diskcache = "^5.6.1"
40
- pycryptodome = "^3.20.0"
41
- requests-ntlm = ">=1.1,<=2.0"
43
+ [tool.poetry.group.test.dependencies]
44
+ pytest = ">=7,<9"
45
+ # pytest-cov = ">=6.0.0"
42
46
 
43
47
  [tool.poetry.group.dev.dependencies]
44
- pre-commit = "^3"
45
- pytest = ">=7,<9"
48
+ # "black>=24.1.1",
49
+ pre-commit = ">=3.0"
46
50
 
47
- [tool.poetry.extras]
48
- notebooks = ["notebook", "matplotlib"]
51
+ [project.urls]
52
+ homepage = "https://github.com/equinor/tagreader-python"
53
+ repository = "https://github.com/equinor/tagreader-python"
49
54
 
50
55
  [build-system]
51
- requires = ["poetry-core"]
56
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
52
57
  build-backend = "poetry.core.masonry.api"
@@ -4,11 +4,18 @@ from tagreader.utils import (
4
4
  ReaderType,
5
5
  add_equinor_root_certificate,
6
6
  is_equinor,
7
- is_mac,
8
- is_windows,
9
7
  )
10
8
 
11
9
  if is_equinor():
12
10
  add_equinor_root_certificate()
13
11
 
14
12
  from tagreader.__version__ import version as __version__
13
+
14
+ __all__ = [
15
+ "IMSClient",
16
+ "list_sources",
17
+ "IMSType",
18
+ "ReaderType",
19
+ "add_equinor_root_certificate",
20
+ "__version__",
21
+ ]
@@ -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,
@@ -36,7 +32,7 @@ def list_sources(
36
32
  imstype: Union[IMSType, str],
37
33
  url: Optional[str] = None,
38
34
  auth: Optional[Any] = None,
39
- verifySSL: bool = True,
35
+ verify_ssl: Optional[Union[bool, str]] = True,
40
36
  ) -> List[str]:
41
37
  if isinstance(imstype, str):
42
38
  try:
@@ -51,11 +47,11 @@ def list_sources(
51
47
  if imstype == IMSType.PIWEBAPI:
52
48
  if auth is None:
53
49
  auth = get_auth_pi()
54
- return list_piwebapi_sources(url=url, auth=auth, verify_ssl=verifySSL)
50
+ return list_piwebapi_sources(url=url, auth=auth, verify_ssl=verify_ssl)
55
51
  elif imstype == IMSType.ASPENONE:
56
52
  if auth is None:
57
53
  auth = get_auth_aspen()
58
- return list_aspenone_sources(url=url, auth=auth, verify_ssl=verifySSL)
54
+ return list_aspenone_sources(url=url, auth=auth, verify_ssl=verify_ssl)
59
55
  elif imstype in [IMSType.PI, IMSType.ASPEN, IMSType.IP21]:
60
56
  raise ValueError(
61
57
  f"ODBC clients are no longer supported. Given ims client type: {imstype}."
@@ -119,96 +115,19 @@ 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,
202
121
  url: Optional[str],
203
122
  options: Dict[str, Union[int, float, str]],
204
- verifySSL: Optional[bool],
123
+ verify_ssl: Optional[Union[bool, str]],
205
124
  auth: Optional[Any],
206
125
  cache: Optional[Union[SmartCache, BucketCache]] = None,
207
126
  ):
208
127
  if imstype is None:
209
128
  try:
210
129
  if datasource in list_aspenone_sources(
211
- url=None, auth=None, verify_ssl=verifySSL
130
+ url=None, auth=None, verify_ssl=verify_ssl
212
131
  ):
213
132
  imstype = IMSType.ASPENONE
214
133
  except HTTPError as e:
@@ -216,7 +135,7 @@ def get_handler(
216
135
  if imstype is None:
217
136
  try:
218
137
  if datasource in list_piwebapi_sources(
219
- url=None, auth=None, verify_ssl=verifySSL
138
+ url=None, auth=None, verify_ssl=verify_ssl
220
139
  ):
221
140
  imstype = IMSType.PIWEBAPI
222
141
  except HTTPError as e:
@@ -227,7 +146,7 @@ def get_handler(
227
146
  url=url,
228
147
  datasource=datasource,
229
148
  options=options,
230
- verify_ssl=verifySSL,
149
+ verify_ssl=verify_ssl,
231
150
  auth=auth,
232
151
  cache=cache,
233
152
  )
@@ -237,7 +156,7 @@ def get_handler(
237
156
  datasource=datasource,
238
157
  url=url,
239
158
  options=options,
240
- verify_ssl=verifySSL,
159
+ verify_ssl=verify_ssl,
241
160
  auth=auth,
242
161
  )
243
162
  elif imstype in [IMSType.PI, IMSType.ASPEN, IMSType.IP21]:
@@ -259,7 +178,7 @@ class IMSClient:
259
178
  tz: Union[tzinfo, str] = pytz.timezone("Europe/Oslo"),
260
179
  url: Optional[str] = None,
261
180
  handler_options: Dict[str, Union[int, float, str]] = {}, # noqa:
262
- verifySSL: bool = True,
181
+ verify_ssl: Optional[Union[bool, str]] = True,
263
182
  auth: Optional[Any] = None,
264
183
  cache: Optional[Union[SmartCache, BucketCache]] = None,
265
184
  ):
@@ -290,7 +209,7 @@ class IMSClient:
290
209
  datasource=datasource,
291
210
  url=url,
292
211
  options=handler_options,
293
- verifySSL=verifySSL,
212
+ verify_ssl=verify_ssl,
294
213
  auth=auth,
295
214
  cache=self.cache,
296
215
  )
@@ -493,7 +412,7 @@ class IMSClient:
493
412
  ):
494
413
  start = start_time
495
414
  end = stop_time
496
- logger.warn(
415
+ logger.warning(
497
416
  (
498
417
  "This function has been renamed to read() and is deprecated. "
499
418
  "Please call 'read()' instead"
@@ -0,0 +1,306 @@
1
+ import enum
2
+ import hashlib
3
+ import platform
4
+ import socket
5
+ import ssl
6
+ from datetime import datetime, tzinfo
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Union
10
+
11
+ import certifi
12
+ import pandas as pd
13
+ import pytz
14
+ import requests
15
+ from platformdirs import user_data_dir
16
+
17
+ from tagreader.logger import logger
18
+
19
+
20
+ def is_windows() -> bool:
21
+ return platform.system() == "Windows"
22
+
23
+
24
+ def is_mac() -> bool:
25
+ return platform.system() == "Darwin"
26
+
27
+
28
+ def is_linux() -> bool:
29
+ return platform.system() == "Linux"
30
+
31
+
32
+ if is_windows():
33
+ import winreg
34
+
35
+ if is_mac():
36
+ import subprocess
37
+
38
+
39
+ def convert_to_pydatetime(date_stamp: Union[datetime, str, pd.Timestamp]) -> datetime:
40
+ if isinstance(date_stamp, datetime):
41
+ return date_stamp
42
+ elif isinstance(date_stamp, pd.Timestamp):
43
+ return date_stamp.to_pydatetime()
44
+ else:
45
+ try:
46
+ return pd.to_datetime(str(date_stamp), format="ISO8601").to_pydatetime()
47
+ except ValueError:
48
+ return pd.to_datetime(str(date_stamp), dayfirst=True).to_pydatetime()
49
+
50
+
51
+ def ensure_datetime_with_tz(
52
+ date_stamp: Union[datetime, str, pd.Timestamp],
53
+ tz: tzinfo = pytz.timezone("Europe/Oslo"),
54
+ ) -> datetime:
55
+ date_stamp = convert_to_pydatetime(date_stamp)
56
+
57
+ if not date_stamp.tzinfo:
58
+ date_stamp = tz.localize(date_stamp)
59
+
60
+ return date_stamp
61
+
62
+
63
+ def urljoin(*args) -> str:
64
+ """
65
+ Joins components of URL. Ensures slashes are inserted or removed where
66
+ needed, and does not strip trailing slash of last element.
67
+
68
+ Returns:
69
+ str -- Generated URL
70
+ """
71
+ trailing_slash = "/" if args[-1].endswith("/") else ""
72
+ return "/".join(map(lambda x: str(x).strip("/"), args)) + trailing_slash
73
+
74
+
75
+ class ReaderType(enum.IntEnum):
76
+ """Enumerates available types of data to read.
77
+
78
+ For members with more than one name per value, the first member (the
79
+ original) needs to be untouched since it may be used as back-reference
80
+ (specifically for cache hierarchies).
81
+ """
82
+
83
+ RAW = SAMPLED = ACTUAL = enum.auto() # Raw sampled data
84
+ SHAPEPRESERVING = BESTFIT = enum.auto() # Minimum data points for preserving shape
85
+ INT = INTERPOLATE = INTERPOLATED = enum.auto() # Interpolated data
86
+ MIN = MINIMUM = enum.auto() # Min value
87
+ MAX = MAXIMUM = enum.auto() # Max value
88
+ AVG = AVERAGE = AVERAGED = enum.auto() # Averaged data
89
+ VAR = VARIANCE = enum.auto() # Variance of data
90
+ STD = STDDEV = enum.auto() # Standard deviation of data
91
+ RNG = RANGE = enum.auto() # Range of data
92
+ COUNT = enum.auto() # Number of data points
93
+ GOOD = enum.auto() # Number of good data points
94
+ BAD = NOTGOOD = enum.auto() # Number of not good data points
95
+ TOTAL = enum.auto() # Number of total data
96
+ SUM = enum.auto() # Sum of data
97
+ SNAPSHOT = FINAL = LAST = enum.auto() # Last sampled value
98
+
99
+
100
+ def add_equinor_root_certificate() -> bool:
101
+ """
102
+ This is a utility function for Equinor employees on Equinor managed machines.
103
+
104
+ The function searches for the Equinor Root certificate in the
105
+ cert store and imports it to the cacert bundle. Does nothing if not
106
+ running on Equinor host.
107
+
108
+ NB! This needs to be repeated after updating the cacert module.
109
+
110
+ Returns:
111
+ bool: True if function completes successfully
112
+ """
113
+ certificate = find_local_equinor_root_certificate()
114
+
115
+ # If certificate is not found locally, we download it from the Equinor server
116
+ if certificate == "":
117
+ logger.debug(
118
+ "Unable to locate Equinor Root CA certificate on this host. Downloading from Equinor server."
119
+ )
120
+ response = requests.get("http://pki.equinor.com/aia/ecpr.crt")
121
+
122
+ if response.status_code != 200:
123
+ logger.error(
124
+ "Unable to find Equinor Root CA certificate locally and on Equinor server."
125
+ )
126
+ return False
127
+
128
+ certificate = response.text.replace("\r", "")
129
+
130
+ # Write result to user data so we can read the cert from there next time
131
+ filepath = Path(user_data_dir("tagreader")) / "equinor_root_ca.crt"
132
+ try:
133
+ filepath.parent.mkdir(parents=True, exist_ok=True)
134
+ filepath.write_text(certificate)
135
+ logger.debug("Equinor Root CA certificate written to cache")
136
+ except Exception as e:
137
+ logger.debug(f"Failed to write Equinor Root CA certificate to cache: {e}")
138
+
139
+ if certificate in certifi.contents():
140
+ logger.debug("Equinor Root Certificate already exists in certifi store")
141
+ return True
142
+
143
+ ca_file = certifi.where()
144
+ with open(ca_file, "ab") as f:
145
+ f.write(bytes(certificate, "ascii"))
146
+ logger.debug("Equinor Root Certificate added to certifi store")
147
+ return True
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")
156
+ for cert in ssl.enum_certificates("CA"): # type: ignore
157
+ found_cert = cert[0]
158
+ # deepcode ignore InsecureHash: <Only hashes to compare with known hash>
159
+ if hashlib.sha1(found_cert).hexdigest().upper() == equinor_root_pem_hash:
160
+ return ssl.DER_cert_to_PEM_cert(found_cert)
161
+
162
+ elif is_mac():
163
+ logger.debug("Checking for Equinor Root CA in MacOS certificate store")
164
+ macos_ca_certs = subprocess.run(
165
+ ["security", "find-certificate", "-a", "-c", ca_search, "-Z"],
166
+ stdout=subprocess.PIPE,
167
+ ).stdout
168
+
169
+ if equinor_root_pem_hash in str(macos_ca_certs).upper():
170
+ c = get_macos_equinor_certificates()
171
+ for cert in c:
172
+ # deepcode ignore InsecureHash: <Only hashes to compare with known hash>
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}")
184
+
185
+ return ""
186
+
187
+
188
+ def get_macos_equinor_certificates():
189
+ import ssl
190
+ import tempfile
191
+
192
+ if not is_mac():
193
+ raise OSError("Function only works on MacOS")
194
+
195
+ ca_search = "Equinor Root CA"
196
+
197
+ ctx = ssl.create_default_context()
198
+ macos_ca_certs = subprocess.run(
199
+ ["security", "find-certificate", "-a", "-c", ca_search, "-p"],
200
+ stdout=subprocess.PIPE,
201
+ ).stdout
202
+ with tempfile.NamedTemporaryFile("w+b", delete=False) as tmp_file:
203
+ tmp_file.write(macos_ca_certs)
204
+
205
+ ctx.load_verify_locations(tmp_file.name)
206
+
207
+ return ctx.get_ca_certs(binary_form=True)
208
+
209
+
210
+ def is_equinor() -> bool:
211
+ """Determines whether code is running on an Equinor host
212
+
213
+ If Windows host:
214
+ Finds host's domain in Windows Registry at
215
+ HKLM\\SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Domain
216
+ or check if hostname starts with eqdev or eqpc
217
+ If mac os host:
218
+ Finds statoil.net as AD hostname in certificates or
219
+ Finds statoil.net, client.statoil.net or equinor.com in dns search domains
220
+ If Linux host:
221
+ Checks whether statoil.no is dns search domains
222
+
223
+ Returns:
224
+ bool: True if Equinor
225
+ """
226
+
227
+ hostname = socket.gethostname()
228
+
229
+ if is_windows():
230
+ if hostname.lower().startswith("eqdev") or hostname.lower().startswith("eqpc"):
231
+ return True
232
+ with winreg.OpenKey( # type: ignore
233
+ winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\ControlSet001\Services\Tcpip\Parameters" # type: ignore
234
+ ) as key:
235
+ domain = winreg.QueryValueEx(key, "Domain") # type: ignore
236
+ if "statoil" in domain[0] or "equinor" in domain[0]:
237
+ return True
238
+ elif is_linux() or is_mac():
239
+ with open("/etc/resolv.conf", "r") as f:
240
+ if any(
241
+ domain in f.read()
242
+ for domain in ["client.statoil.net", "statoil.net", "equinor.com"]
243
+ ):
244
+ return True
245
+
246
+ if is_mac():
247
+
248
+ def get_mac_dns_search_list():
249
+ """
250
+ Retrieves the DNS search list configured on macOS.
251
+ """
252
+ try:
253
+ # Execute the scutil command to get DNS configuration
254
+ result = subprocess.run(
255
+ ["scutil", "--dns"], capture_output=True, text=True, check=True
256
+ )
257
+ output = result.stdout
258
+
259
+ search_list = []
260
+ # Parse the output to find the search domains
261
+ for line in output.splitlines():
262
+ if "search domain" in line:
263
+ # Extract the domain from the line
264
+ parts = line.split(":")
265
+ if len(parts) > 1:
266
+ domain = parts[1].strip()
267
+ # Remove any leading/trailing quotes if present
268
+ domain = domain.strip('"')
269
+ search_list.append(domain)
270
+ return search_list
271
+ except subprocess.CalledProcessError as e:
272
+ print(f"Error executing scutil: {e}")
273
+ return []
274
+ except Exception as e:
275
+ print(f"An unexpected error occurred: {e}")
276
+ return []
277
+
278
+ if any(
279
+ domain in get_mac_dns_search_list()
280
+ for domain in ["client.statoil.net", "statoil.net", "equinor.com"]
281
+ ):
282
+ return True
283
+
284
+ s = subprocess.run(
285
+ ["security", "find-certificate", "-a", "-c" "client.statoil.net"],
286
+ stdout=subprocess.PIPE,
287
+ ).stdout
288
+
289
+ # deepcode ignore IdenticalBranches: Not an error. First test is just more precise.
290
+ if hostname + ".client.statoil.net" in str(s):
291
+ return True
292
+ elif "client.statoil.net" in hostname and hostname in str(s):
293
+ return True
294
+ else:
295
+ raise OSError(
296
+ f"Unsupported system: {platform.system()}. Please report this as an issue."
297
+ )
298
+ return False
299
+
300
+
301
+ class IMSType(str, Enum):
302
+ PIWEBAPI = "piwebapi"
303
+ ASPENONE = "aspenone"
304
+ PI = "pi"
305
+ ASPEN = "aspen"
306
+ IP21 = "ip21"
@@ -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 IMSType, ReaderType, is_mac, is_windows, urljoin
22
+ from tagreader.utils import ReaderType, is_mac, is_windows, urljoin
23
23
 
24
24
 
25
25
  class MD4:
@@ -42,12 +42,7 @@ def patched_hashlib_new(name, data=b"", usedforsecurity=True):
42
42
  if name.lower() == "md4":
43
43
  return MD4(data)
44
44
  else:
45
- # Try / Catch easier than detecting python version
46
- try:
47
- return hashlib_new_method(name, data=data, usedforsecurity=usedforsecurity)
48
- except TypeError:
49
- # Required for python 3.8
50
- return hashlib_new_method(name, data=data)
45
+ return hashlib_new_method(name, data=data, usedforsecurity=usedforsecurity)
51
46
 
52
47
 
53
48
  # Monkey-patch md4 in hashlib.new due to missing support for md4 in later releases of Python:
@@ -95,7 +90,7 @@ def get_url_aspen(use_internal: bool = True) -> str:
95
90
  def list_aspenone_sources(
96
91
  url: Optional[str] = None,
97
92
  auth: Optional[Any] = None,
98
- verify_ssl: Optional[bool] = True,
93
+ verify_ssl: Optional[Union[bool, str]] = True,
99
94
  ) -> List[str]:
100
95
  if url is None:
101
96
  url = get_url_aspen()
@@ -124,7 +119,7 @@ def list_aspenone_sources(
124
119
  def list_piwebapi_sources(
125
120
  url: Optional[str] = None,
126
121
  auth: Optional[Any] = None,
127
- verify_ssl: Optional[bool] = True,
122
+ verify_ssl: Optional[Union[bool, str]] = True,
128
123
  ) -> List[str]:
129
124
  if url is None:
130
125
  url = get_url_pi()
@@ -152,7 +147,7 @@ def list_piwebapi_sources(
152
147
  def get_piwebapi_source_to_webid_dict(
153
148
  url: Optional[str] = None,
154
149
  auth: Optional[Any] = None,
155
- verify_ssl: Optional[bool] = True,
150
+ verify_ssl: Optional[Union[bool, str]] = True,
156
151
  ) -> List[str]:
157
152
  if url is None:
158
153
  url = get_url_pi()
@@ -182,7 +177,7 @@ class BaseHandlerWeb(ABC):
182
177
  datasource: Optional[str],
183
178
  url: Optional[str],
184
179
  auth: Optional[Any],
185
- verify_ssl: Optional[bool],
180
+ verify_ssl: Optional[Union[bool, str]],
186
181
  ):
187
182
  self.datasource = datasource
188
183
  self.base_url = url
@@ -213,6 +208,10 @@ class BaseHandlerWeb(ABC):
213
208
  logger.warning(f"No data found for {url} {params}")
214
209
  return {}
215
210
 
211
+ if res.text.startswith("XML Error"):
212
+ logger.warning(f"Invalid data returned for {url} {params}")
213
+ return {}
214
+
216
215
  try:
217
216
  return res.json()
218
217
  except JSONDecodeError:
@@ -241,7 +240,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
241
240
  datasource: Optional[str],
242
241
  url: Optional[str] = None,
243
242
  auth: Optional[Any] = None,
244
- verify_ssl: Optional[bool] = True,
243
+ verify_ssl: Optional[Union[bool, str]] = True,
245
244
  options: Dict[str, Any] = dict(),
246
245
  ):
247
246
  if url is None:
@@ -258,11 +257,6 @@ class AspenHandlerWeb(BaseHandlerWeb):
258
257
  self._max_rows = options.get("max_rows", 100000)
259
258
  self._connection_string = "" # Used for raw SQL queries
260
259
 
261
- @staticmethod
262
- def generate_connection_string(host, *_, **__):
263
- # todo: is obsolete after removing ODBC
264
- raise NotImplementedError
265
-
266
260
  @staticmethod
267
261
  def generate_search_query(
268
262
  tag: Optional[str],
@@ -670,27 +664,6 @@ class AspenHandlerWeb(BaseHandlerWeb):
670
664
  connection_string += f"<![CDATA[{query}]]></SQL>"
671
665
  return connection_string
672
666
 
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
667
  def query_sql(self, query: str, parse: bool = True) -> Union[str, pd.DataFrame]:
695
668
  url = urljoin(self.base_url, "SQL")
696
669
  if self._connection_string is None:
@@ -736,7 +709,7 @@ class PIHandlerWeb(BaseHandlerWeb):
736
709
  url: Optional[str],
737
710
  datasource: Optional[str],
738
711
  auth: Optional[Any],
739
- verify_ssl: bool,
712
+ verify_ssl: Optional[Union[bool, str]],
740
713
  options: Dict[str, Union[int, float, str]],
741
714
  cache: Optional[Union[SmartCache, BucketCache]],
742
715
  ):
@@ -758,11 +731,6 @@ class PIHandlerWeb(BaseHandlerWeb):
758
731
  def _time_to_UTC_string(time: datetime) -> str:
759
732
  return time.astimezone(pytz.UTC).strftime("%d-%b-%y %H:%M:%S")
760
733
 
761
- @staticmethod
762
- def generate_connection_string(host, *_, **__):
763
- # todo: is obsolete after removing ODBC
764
- raise NotImplementedError
765
-
766
734
  @staticmethod
767
735
  def escape(s: str) -> str:
768
736
  # https://techsupport.osisoft.com/Documentation/PI-Web-API/help/topics/search-queries.html
@@ -1,270 +0,0 @@
1
- import enum
2
- import logging
3
- import platform
4
- import warnings
5
- from datetime import datetime, tzinfo
6
- from enum import Enum
7
- from typing import Union
8
-
9
- import pandas as pd
10
- import pytz
11
-
12
- from tagreader.logger import logger
13
-
14
-
15
- def is_windows() -> bool:
16
- return platform.system() == "Windows"
17
-
18
-
19
- def is_mac() -> bool:
20
- return platform.system() == "Darwin"
21
-
22
-
23
- def is_linux() -> bool:
24
- return platform.system() == "Linux"
25
-
26
-
27
- if is_windows():
28
- import winreg
29
-
30
-
31
- if is_mac():
32
- import socket
33
- import subprocess
34
-
35
-
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
- def convert_to_pydatetime(date_stamp: Union[datetime, str, pd.Timestamp]) -> datetime:
72
- if isinstance(date_stamp, datetime):
73
- return date_stamp
74
- elif isinstance(date_stamp, pd.Timestamp):
75
- return date_stamp.to_pydatetime()
76
- else:
77
- try:
78
- return pd.to_datetime(str(date_stamp), format="ISO8601").to_pydatetime()
79
- except ValueError:
80
- return pd.to_datetime(str(date_stamp), dayfirst=True).to_pydatetime()
81
-
82
-
83
- def ensure_datetime_with_tz(
84
- date_stamp: Union[datetime, str, pd.Timestamp],
85
- tz: tzinfo = pytz.timezone("Europe/Oslo"),
86
- ) -> datetime:
87
- date_stamp = convert_to_pydatetime(date_stamp)
88
-
89
- if not date_stamp.tzinfo:
90
- date_stamp = tz.localize(date_stamp)
91
-
92
- return date_stamp
93
-
94
-
95
- def urljoin(*args) -> str:
96
- """
97
- Joins components of URL. Ensures slashes are inserted or removed where
98
- needed, and does not strip trailing slash of last element.
99
-
100
- Returns:
101
- str -- Generated URL
102
- """
103
- trailing_slash = "/" if args[-1].endswith("/") else ""
104
- return "/".join(map(lambda x: str(x).strip("/"), args)) + trailing_slash
105
-
106
-
107
- class ReaderType(enum.IntEnum):
108
- """Enumerates available types of data to read.
109
-
110
- For members with more than one name per value, the first member (the
111
- original) needs to be untouched since it may be used as back-reference
112
- (specifically for cache hierarchies).
113
- """
114
-
115
- RAW = SAMPLED = ACTUAL = enum.auto() # Raw sampled data
116
- SHAPEPRESERVING = BESTFIT = enum.auto() # Minimum data points for preserving shape
117
- INT = INTERPOLATE = INTERPOLATED = enum.auto() # Interpolated data
118
- MIN = MINIMUM = enum.auto() # Min value
119
- MAX = MAXIMUM = enum.auto() # Max value
120
- AVG = AVERAGE = AVERAGED = enum.auto() # Averaged data
121
- VAR = VARIANCE = enum.auto() # Variance of data
122
- STD = STDDEV = enum.auto() # Standard deviation of data
123
- RNG = RANGE = enum.auto() # Range of data
124
- COUNT = enum.auto() # Number of data points
125
- GOOD = enum.auto() # Number of good data points
126
- BAD = NOTGOOD = enum.auto() # Number of not good data points
127
- TOTAL = enum.auto() # Number of total data
128
- SUM = enum.auto() # Sum of data
129
- SNAPSHOT = FINAL = LAST = enum.auto() # Last sampled value
130
-
131
-
132
- def add_equinor_root_certificate() -> bool:
133
- """
134
- This is a utility function for Equinor employees on Equinor managed machines.
135
-
136
- The function searches for the Equinor Root certificate in the
137
- cert store and imports it to the cacert bundle. Does nothing if not
138
- running on Equinor host.
139
-
140
- NB! This needs to be repeated after updating the cacert module.
141
- NB! Will not download certificates.
142
-
143
- Returns:
144
- bool: True if function completes successfully
145
- """
146
- import hashlib
147
- import ssl
148
-
149
- import certifi
150
-
151
- EQUINOR_root_PEM_HASH = "5A206332CE73CED1D44C8A99C4C43B7CEE03DF5F"
152
- ca_search = "Equinor Root CA"
153
-
154
- found = False
155
- der = None
156
-
157
- if is_linux():
158
- return True
159
- elif is_windows():
160
- logger.debug("Scanning CA certificate in Windows cert store", end="")
161
- for cert in ssl.enum_certificates("CA"):
162
- der = cert[0]
163
- # deepcode ignore InsecureHash: <Only hashes to compare with known hash>
164
- if hashlib.sha1(der).hexdigest().upper() == EQUINOR_root_PEM_HASH:
165
- found = True
166
- logger.debug("CA certificate found!")
167
- break
168
- elif is_mac():
169
- macos_ca_certs = subprocess.run(
170
- ["security", "find-certificate", "-a", "-c", ca_search, "-Z"],
171
- stdout=subprocess.PIPE,
172
- ).stdout
173
-
174
- if EQUINOR_root_PEM_HASH in str(macos_ca_certs).upper():
175
- c = get_macos_equinor_certificates()
176
- for cert in c:
177
- # deepcode ignore InsecureHash: <Only hashes to compare with known hash>
178
- if hashlib.sha1(cert).hexdigest().upper() == EQUINOR_root_PEM_HASH:
179
- der = cert
180
- found = True
181
- break
182
-
183
- if found and der:
184
- pem = ssl.DER_cert_to_PEM_cert(der)
185
- if pem in certifi.contents():
186
- logger.debug(
187
- "CA Certificate already exists in certifi store. Nothing to do."
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.")
197
-
198
- return found
199
-
200
-
201
- def get_macos_equinor_certificates():
202
- import ssl
203
- import tempfile
204
-
205
- ca_search = "Equinor Root CA"
206
-
207
- ctx = ssl.create_default_context()
208
- macos_ca_certs = subprocess.run(
209
- ["security", "find-certificate", "-a", "-c", ca_search, "-p"],
210
- stdout=subprocess.PIPE,
211
- ).stdout
212
- with tempfile.NamedTemporaryFile("w+b", delete=False) as tmp_file:
213
- tmp_file.write(macos_ca_certs)
214
-
215
- ctx.load_verify_locations(tmp_file.name)
216
-
217
- return ctx.get_ca_certs(binary_form=True)
218
-
219
-
220
- def is_equinor() -> bool:
221
- """Determines whether code is running on an Equinor host
222
-
223
- If Windows host:
224
- Finds host's domain in Windows Registry at
225
- HKLM\\SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Domain
226
- If mac os host:
227
- Finds statoil.net as AD hostname in certificates
228
- If Linux host:
229
- Checks whether statoil.no is search domain
230
-
231
- Returns:
232
- bool: True if Equinor
233
- """
234
- if is_windows():
235
- with winreg.OpenKey(
236
- winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\ControlSet001\Services\Tcpip\Parameters"
237
- ) as key:
238
- domain = winreg.QueryValueEx(key, "Domain")
239
- if "statoil" in domain[0]:
240
- return True
241
- elif is_mac():
242
- s = subprocess.run(
243
- ["security", "find-certificate", "-a", "-c" "client.statoil.net"],
244
- stdout=subprocess.PIPE,
245
- ).stdout
246
-
247
- host = socket.gethostname()
248
-
249
- # deepcode ignore IdenticalBranches: Not an error. First test is just more precise.
250
- if host + ".client.statoil.net" in str(s):
251
- return True
252
- elif "client.statoil.net" in host and host in str(s):
253
- return True
254
- elif is_linux():
255
- with open("/etc/resolv.conf", "r") as f:
256
- if "statoil.no" in f.read():
257
- return True
258
- else:
259
- raise OSError(
260
- f"Unsupported system: {platform.system()}. Please report this as an issue."
261
- )
262
- return False
263
-
264
-
265
- class IMSType(str, Enum):
266
- PIWEBAPI = "piwebapi"
267
- ASPENONE = "aspenone"
268
- PI = "pi"
269
- ASPEN = "aspen"
270
- IP21 = "ip21"
File without changes
File without changes
File without changes
File without changes