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.
- {tagreader-6.0.3 → tagreader-6.1.0}/PKG-INFO +9 -13
- {tagreader-6.0.3 → tagreader-6.1.0}/pyproject.toml +29 -24
- {tagreader-6.0.3 → tagreader-6.1.0}/tagreader/__init__.py +9 -2
- {tagreader-6.0.3 → tagreader-6.1.0}/tagreader/clients.py +11 -92
- tagreader-6.1.0/tagreader/utils.py +306 -0
- {tagreader-6.0.3 → tagreader-6.1.0}/tagreader/web_handlers.py +12 -44
- tagreader-6.0.3/tagreader/utils.py +0 -270
- {tagreader-6.0.3 → tagreader-6.1.0}/LICENSE +0 -0
- {tagreader-6.0.3 → tagreader-6.1.0}/README.md +0 -0
- {tagreader-6.0.3 → tagreader-6.1.0}/tagreader/__version__.py +0 -0
- {tagreader-6.0.3 → tagreader-6.1.0}/tagreader/cache.py +0 -0
- {tagreader-6.0.3 → tagreader-6.1.0}/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.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
|
|
31
|
-
Requires-Dist:
|
|
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:
|
|
36
|
-
Requires-Dist:
|
|
37
|
-
Requires-Dist: requests
|
|
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
|
-
[
|
|
1
|
+
[project]
|
|
2
2
|
name = "tagreader"
|
|
3
|
-
version = "6.0
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
45
|
-
|
|
48
|
+
# "black>=24.1.1",
|
|
49
|
+
pre-commit = ">=3.0"
|
|
46
50
|
|
|
47
|
-
[
|
|
48
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
File without changes
|