tagreader 6.1.0__tar.gz → 6.2.1__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.1.0 → tagreader-6.2.1}/PKG-INFO +8 -6
- {tagreader-6.1.0 → tagreader-6.2.1}/README.md +4 -3
- {tagreader-6.1.0 → tagreader-6.2.1}/pyproject.toml +1 -1
- {tagreader-6.1.0 → tagreader-6.2.1}/tagreader/cache.py +2 -3
- {tagreader-6.1.0 → tagreader-6.2.1}/tagreader/clients.py +37 -17
- {tagreader-6.1.0 → tagreader-6.2.1}/tagreader/utils.py +21 -8
- {tagreader-6.1.0 → tagreader-6.2.1}/tagreader/web_handlers.py +19 -14
- {tagreader-6.1.0 → tagreader-6.2.1}/LICENSE +0 -0
- {tagreader-6.1.0 → tagreader-6.2.1}/tagreader/__init__.py +0 -0
- {tagreader-6.1.0 → tagreader-6.2.1}/tagreader/__version__.py +0 -0
- {tagreader-6.1.0 → tagreader-6.2.1}/tagreader/logger.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: tagreader
|
|
3
|
-
Version: 6.1
|
|
3
|
+
Version: 6.2.1
|
|
4
4
|
Summary: Tagreader is a Python package for reading trend data from the OSIsoft PI and Aspen Infoplus.21 IMS systems.
|
|
5
|
-
License: MIT
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: Aspen InfoPlus.21,OSIsoft PI
|
|
7
8
|
Author: Einar S. Idsø
|
|
8
9
|
Author-email: eiids@equinor.com
|
|
@@ -36,6 +37,10 @@ Project-URL: Homepage, https://github.com/equinor/tagreader-python
|
|
|
36
37
|
Project-URL: Repository, https://github.com/equinor/tagreader-python
|
|
37
38
|
Description-Content-Type: text/markdown
|
|
38
39
|
|
|
40
|
+
⚠️ **Important:** **We are phasing out TagReader, and the code will no longer be maintained.**
|
|
41
|
+
|
|
42
|
+
For employees in Equinor we recommend using Omnia Timeseries
|
|
43
|
+
|
|
39
44
|
# tagreader-python <!-- omit in toc -->
|
|
40
45
|
|
|
41
46
|

|
|
@@ -85,6 +90,3 @@ For more details, see the [Tagreader Docs](https://equinor.github.io/tagreader-p
|
|
|
85
90
|
## Documentation
|
|
86
91
|
The full documentation can be found in [Tagreader Docs](https://equinor.github.io/tagreader-python/)
|
|
87
92
|
|
|
88
|
-
## Contribute
|
|
89
|
-
To starting contributing, please see [Tagreader Docs - Contribute](https://equinor.github.io/tagreader-python/docs/contribute/overview)
|
|
90
|
-
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
⚠️ **Important:** **We are phasing out TagReader, and the code will no longer be maintained.**
|
|
2
|
+
|
|
3
|
+
For employees in Equinor we recommend using Omnia Timeseries
|
|
4
|
+
|
|
1
5
|
# tagreader-python <!-- omit in toc -->
|
|
2
6
|
|
|
3
7
|

|
|
@@ -46,6 +50,3 @@ For more details, see the [Tagreader Docs](https://equinor.github.io/tagreader-p
|
|
|
46
50
|
|
|
47
51
|
## Documentation
|
|
48
52
|
The full documentation can be found in [Tagreader Docs](https://equinor.github.io/tagreader-python/)
|
|
49
|
-
|
|
50
|
-
## Contribute
|
|
51
|
-
To starting contributing, please see [Tagreader Docs - Contribute](https://equinor.github.io/tagreader-python/docs/contribute/overview)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tagreader"
|
|
3
|
-
version = "6.1
|
|
3
|
+
version = "6.2.1"
|
|
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 = [
|
|
6
6
|
{ name = "Einar S. Idsø", email = "eiids@equinor.com" },
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import Dict, List, Optional, Tuple, Union, cast
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
|
-
import pytz
|
|
7
6
|
from diskcache import Cache
|
|
8
7
|
|
|
9
8
|
from tagreader.logger import logger
|
|
@@ -21,7 +20,7 @@ def safe_tagname(tagname: str) -> str:
|
|
|
21
20
|
def timestamp_to_epoch(timestamp: datetime) -> int:
|
|
22
21
|
origin = datetime(1970, 1, 1)
|
|
23
22
|
if timestamp.tzinfo is not None:
|
|
24
|
-
timestamp = timestamp.astimezone(
|
|
23
|
+
timestamp = timestamp.astimezone(timezone.utc).replace(tzinfo=None)
|
|
25
24
|
return (timestamp - origin) // timedelta(seconds=1)
|
|
26
25
|
|
|
27
26
|
|
|
@@ -2,11 +2,11 @@ from datetime import datetime, timedelta, timezone, tzinfo
|
|
|
2
2
|
from itertools import groupby
|
|
3
3
|
from operator import itemgetter
|
|
4
4
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
5
|
-
from
|
|
5
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
|
-
import
|
|
9
|
+
import requests
|
|
10
10
|
|
|
11
11
|
from tagreader.cache import BucketCache, SmartCache
|
|
12
12
|
from tagreader.logger import logger
|
|
@@ -21,11 +21,12 @@ from tagreader.web_handlers import (
|
|
|
21
21
|
PIHandlerWeb,
|
|
22
22
|
get_auth_aspen,
|
|
23
23
|
get_auth_pi,
|
|
24
|
+
get_url_aspen,
|
|
24
25
|
list_aspenone_sources,
|
|
25
26
|
list_piwebapi_sources,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
|
-
NONE_START_TIME = datetime(1970, 1, 1, tzinfo=
|
|
29
|
+
NONE_START_TIME = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def list_sources(
|
|
@@ -49,8 +50,6 @@ def list_sources(
|
|
|
49
50
|
auth = get_auth_pi()
|
|
50
51
|
return list_piwebapi_sources(url=url, auth=auth, verify_ssl=verify_ssl)
|
|
51
52
|
elif imstype == IMSType.ASPENONE:
|
|
52
|
-
if auth is None:
|
|
53
|
-
auth = get_auth_aspen()
|
|
54
53
|
return list_aspenone_sources(url=url, auth=auth, verify_ssl=verify_ssl)
|
|
55
54
|
elif imstype in [IMSType.PI, IMSType.ASPEN, IMSType.IP21]:
|
|
56
55
|
raise ValueError(
|
|
@@ -121,24 +120,41 @@ def get_handler(
|
|
|
121
120
|
url: Optional[str],
|
|
122
121
|
options: Dict[str, Union[int, float, str]],
|
|
123
122
|
verify_ssl: Optional[Union[bool, str]],
|
|
124
|
-
auth: Optional[Any],
|
|
123
|
+
auth: Optional[Any] = None,
|
|
125
124
|
cache: Optional[Union[SmartCache, BucketCache]] = None,
|
|
126
125
|
):
|
|
127
126
|
if imstype is None:
|
|
127
|
+
orig_auth = auth
|
|
128
|
+
orig_url = url
|
|
128
129
|
try:
|
|
129
|
-
|
|
130
|
+
aspen_source = list_aspenone_sources(
|
|
130
131
|
url=None, auth=None, verify_ssl=verify_ssl
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
)
|
|
133
|
+
except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError):
|
|
134
|
+
# Try again using app registration auth
|
|
135
|
+
try:
|
|
136
|
+
auth = get_auth_aspen(False)
|
|
137
|
+
url = get_url_aspen(False)
|
|
138
|
+
aspen_source = list_aspenone_sources(
|
|
139
|
+
auth=auth, url=url, verify_ssl=verify_ssl
|
|
140
|
+
)
|
|
141
|
+
except requests.exceptions.HTTPError as e:
|
|
142
|
+
logger.debug(f"Could not list Aspenone sources: {e}")
|
|
143
|
+
aspen_source = []
|
|
144
|
+
|
|
145
|
+
if datasource in aspen_source:
|
|
146
|
+
imstype = IMSType.ASPENONE
|
|
147
|
+
else:
|
|
148
|
+
auth = orig_auth
|
|
149
|
+
url = orig_url
|
|
150
|
+
|
|
135
151
|
if imstype is None:
|
|
136
152
|
try:
|
|
137
153
|
if datasource in list_piwebapi_sources(
|
|
138
154
|
url=None, auth=None, verify_ssl=verify_ssl
|
|
139
155
|
):
|
|
140
156
|
imstype = IMSType.PIWEBAPI
|
|
141
|
-
except HTTPError as e:
|
|
157
|
+
except requests.exceptions.HTTPError as e:
|
|
142
158
|
logger.debug(f"Could not list PI sources: {e}")
|
|
143
159
|
|
|
144
160
|
if imstype == IMSType.PIWEBAPI:
|
|
@@ -175,7 +191,7 @@ class IMSClient:
|
|
|
175
191
|
self,
|
|
176
192
|
datasource: str,
|
|
177
193
|
imstype: Optional[Union[str, IMSType]] = None,
|
|
178
|
-
tz: Union[tzinfo, str] =
|
|
194
|
+
tz: Optional[Union[tzinfo, str]] = None,
|
|
179
195
|
url: Optional[str] = None,
|
|
180
196
|
handler_options: Dict[str, Union[int, float, str]] = {}, # noqa:
|
|
181
197
|
verify_ssl: Optional[Union[bool, str]] = True,
|
|
@@ -191,11 +207,15 @@ class IMSClient:
|
|
|
191
207
|
f" We suggest to use the tagreader.IMSType enumerator when initiating a client."
|
|
192
208
|
)
|
|
193
209
|
|
|
210
|
+
if tz is None or tz == "":
|
|
211
|
+
tz = ZoneInfo("Europe/Oslo")
|
|
194
212
|
if isinstance(tz, str):
|
|
195
|
-
|
|
196
|
-
self.tz =
|
|
197
|
-
|
|
198
|
-
raise ValueError(
|
|
213
|
+
try:
|
|
214
|
+
self.tz = ZoneInfo(tz)
|
|
215
|
+
except ZoneInfoNotFoundError:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"Invalid timezone string. Given tz was {tz}"
|
|
218
|
+
) from None
|
|
199
219
|
elif isinstance(tz, tzinfo):
|
|
200
220
|
self.tz = tz
|
|
201
221
|
else:
|
|
@@ -3,14 +3,14 @@ import hashlib
|
|
|
3
3
|
import platform
|
|
4
4
|
import socket
|
|
5
5
|
import ssl
|
|
6
|
-
from datetime import datetime, tzinfo
|
|
6
|
+
from datetime import datetime, timezone, tzinfo
|
|
7
7
|
from enum import Enum
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Union
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
10
11
|
|
|
11
12
|
import certifi
|
|
12
13
|
import pandas as pd
|
|
13
|
-
import pytz
|
|
14
14
|
import requests
|
|
15
15
|
from platformdirs import user_data_dir
|
|
16
16
|
|
|
@@ -50,12 +50,24 @@ def convert_to_pydatetime(date_stamp: Union[datetime, str, pd.Timestamp]) -> dat
|
|
|
50
50
|
|
|
51
51
|
def ensure_datetime_with_tz(
|
|
52
52
|
date_stamp: Union[datetime, str, pd.Timestamp],
|
|
53
|
-
tz: tzinfo =
|
|
53
|
+
tz: Optional[tzinfo] = None,
|
|
54
54
|
) -> datetime:
|
|
55
|
+
if tz is None:
|
|
56
|
+
try:
|
|
57
|
+
tz = ZoneInfo("Europe/Oslo")
|
|
58
|
+
except ZoneInfoNotFoundError:
|
|
59
|
+
logger.warning(
|
|
60
|
+
"Unable to load timezone 'Europe/Oslo'. Falling back to UTC."
|
|
61
|
+
)
|
|
62
|
+
tz = timezone.utc
|
|
55
63
|
date_stamp = convert_to_pydatetime(date_stamp)
|
|
56
64
|
|
|
57
65
|
if not date_stamp.tzinfo:
|
|
58
|
-
|
|
66
|
+
localize = getattr(tz, "localize", None)
|
|
67
|
+
if callable(localize):
|
|
68
|
+
date_stamp = localize(date_stamp)
|
|
69
|
+
else:
|
|
70
|
+
date_stamp = date_stamp.replace(tzinfo=tz)
|
|
59
71
|
|
|
60
72
|
return date_stamp
|
|
61
73
|
|
|
@@ -117,7 +129,7 @@ def add_equinor_root_certificate() -> bool:
|
|
|
117
129
|
logger.debug(
|
|
118
130
|
"Unable to locate Equinor Root CA certificate on this host. Downloading from Equinor server."
|
|
119
131
|
)
|
|
120
|
-
response = requests.get("http://pki.equinor.com/aia/ecpr.crt")
|
|
132
|
+
response = requests.get("http://pki.equinor.com/aia/ecpr.crt", timeout=20)
|
|
121
133
|
|
|
122
134
|
if response.status_code != 200:
|
|
123
135
|
logger.error(
|
|
@@ -230,7 +242,8 @@ def is_equinor() -> bool:
|
|
|
230
242
|
if hostname.lower().startswith("eqdev") or hostname.lower().startswith("eqpc"):
|
|
231
243
|
return True
|
|
232
244
|
with winreg.OpenKey( # type: ignore
|
|
233
|
-
winreg.HKEY_LOCAL_MACHINE,
|
|
245
|
+
winreg.HKEY_LOCAL_MACHINE,
|
|
246
|
+
r"SYSTEM\ControlSet001\Services\Tcpip\Parameters", # type: ignore
|
|
234
247
|
) as key:
|
|
235
248
|
domain = winreg.QueryValueEx(key, "Domain") # type: ignore
|
|
236
249
|
if "statoil" in domain[0] or "equinor" in domain[0]:
|
|
@@ -282,7 +295,7 @@ def is_equinor() -> bool:
|
|
|
282
295
|
return True
|
|
283
296
|
|
|
284
297
|
s = subprocess.run(
|
|
285
|
-
["security", "find-certificate", "-a", "-
|
|
298
|
+
["security", "find-certificate", "-a", "-cclient.statoil.net"],
|
|
286
299
|
stdout=subprocess.PIPE,
|
|
287
300
|
).stdout
|
|
288
301
|
|
|
@@ -3,14 +3,13 @@ import json
|
|
|
3
3
|
import re
|
|
4
4
|
import urllib.parse
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from datetime import datetime, timedelta
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
7
|
from hashlib import new as hashlib_new_method
|
|
8
8
|
from json.decoder import JSONDecodeError
|
|
9
9
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import pandas as pd
|
|
13
|
-
import pytz
|
|
14
13
|
import requests
|
|
15
14
|
import urllib3
|
|
16
15
|
from Crypto.Hash import MD4 as _MD4
|
|
@@ -107,7 +106,7 @@ def list_aspenone_sources(
|
|
|
107
106
|
url_ = urljoin(url, "DataSources")
|
|
108
107
|
params = {"service": "ProcessData", "allQuotes": 1}
|
|
109
108
|
|
|
110
|
-
res = requests.get(url_, params=params, auth=auth, verify=verify_ssl)
|
|
109
|
+
res = requests.get(url_, params=params, auth=auth, verify=verify_ssl, timeout=300)
|
|
111
110
|
res.raise_for_status()
|
|
112
111
|
try:
|
|
113
112
|
source_list = [r["n"] for r in res.json()["data"] if r["t"] == "IP21"]
|
|
@@ -115,6 +114,8 @@ def list_aspenone_sources(
|
|
|
115
114
|
except JSONDecodeError as e:
|
|
116
115
|
logger.error(f"Could not decode JSON response: {e}")
|
|
117
116
|
|
|
117
|
+
return []
|
|
118
|
+
|
|
118
119
|
|
|
119
120
|
def list_piwebapi_sources(
|
|
120
121
|
url: Optional[str] = None,
|
|
@@ -134,7 +135,7 @@ def list_piwebapi_sources(
|
|
|
134
135
|
urllib3.disable_warnings(InsecureRequestWarning)
|
|
135
136
|
|
|
136
137
|
url_ = urljoin(url, "dataservers")
|
|
137
|
-
res = requests.get(url_, auth=auth, verify=verify_ssl)
|
|
138
|
+
res = requests.get(url_, auth=auth, verify=verify_ssl, timeout=300)
|
|
138
139
|
|
|
139
140
|
res.raise_for_status()
|
|
140
141
|
try:
|
|
@@ -143,6 +144,8 @@ def list_piwebapi_sources(
|
|
|
143
144
|
except JSONDecodeError as e:
|
|
144
145
|
logger.error(f"Could not decode JSON response: {e}")
|
|
145
146
|
|
|
147
|
+
return []
|
|
148
|
+
|
|
146
149
|
|
|
147
150
|
def get_piwebapi_source_to_webid_dict(
|
|
148
151
|
url: Optional[str] = None,
|
|
@@ -162,7 +165,7 @@ def get_piwebapi_source_to_webid_dict(
|
|
|
162
165
|
urllib3.disable_warnings(InsecureRequestWarning)
|
|
163
166
|
|
|
164
167
|
url_ = urljoin(url, "dataservers")
|
|
165
|
-
res = requests.get(url_, auth=auth, verify=verify_ssl)
|
|
168
|
+
res = requests.get(url_, auth=auth, verify=verify_ssl, timeout=300)
|
|
166
169
|
|
|
167
170
|
res.raise_for_status()
|
|
168
171
|
try:
|
|
@@ -170,6 +173,8 @@ def get_piwebapi_source_to_webid_dict(
|
|
|
170
173
|
except JSONDecodeError as e:
|
|
171
174
|
logger.error(f"Could not decode JSON response: {e}")
|
|
172
175
|
|
|
176
|
+
return []
|
|
177
|
+
|
|
173
178
|
|
|
174
179
|
class BaseHandlerWeb(ABC):
|
|
175
180
|
def __init__(
|
|
@@ -316,12 +321,12 @@ class AspenHandlerWeb(BaseHandlerWeb):
|
|
|
316
321
|
else:
|
|
317
322
|
query = '<Q f="d" allQuotes="1">'
|
|
318
323
|
|
|
319
|
-
query += "<Tag
|
|
324
|
+
query += f"<Tag><N><![CDATA[{tagname}]]></N>"
|
|
320
325
|
|
|
321
326
|
if mapname:
|
|
322
327
|
query += f"<M><![CDATA[{mapname}]]></M>"
|
|
323
328
|
|
|
324
|
-
query += f"<D><![CDATA[{self.datasource}]]></D
|
|
329
|
+
query += f"<D><![CDATA[{self.datasource}]]></D><F><![CDATA[VAL]]></F>"
|
|
325
330
|
|
|
326
331
|
if read_type == ReaderType.SNAPSHOT:
|
|
327
332
|
query += "<VS>1</VS>"
|
|
@@ -437,7 +442,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
|
|
|
437
442
|
return ret
|
|
438
443
|
|
|
439
444
|
def _get_default_mapname(self, tagname: str):
|
|
440
|
-
|
|
445
|
+
tagname, _ = self.split_tagmap(tagname)
|
|
441
446
|
all_maps = self._get_maps(tagname)
|
|
442
447
|
for k, v in all_maps.items():
|
|
443
448
|
if v:
|
|
@@ -729,7 +734,7 @@ class PIHandlerWeb(BaseHandlerWeb):
|
|
|
729
734
|
|
|
730
735
|
@staticmethod
|
|
731
736
|
def _time_to_UTC_string(time: datetime) -> str:
|
|
732
|
-
return time.astimezone(
|
|
737
|
+
return time.astimezone(timezone.utc).strftime("%d-%b-%y %H:%M:%S")
|
|
733
738
|
|
|
734
739
|
@staticmethod
|
|
735
740
|
def escape(s: str) -> str:
|
|
@@ -843,10 +848,10 @@ class PIHandlerWeb(BaseHandlerWeb):
|
|
|
843
848
|
if self._is_summary(read_type):
|
|
844
849
|
params["selectedFields"] = "Links;Items.Value.Timestamp;Items.Value.Value"
|
|
845
850
|
if get_status:
|
|
846
|
-
params[
|
|
847
|
-
"
|
|
848
|
-
|
|
849
|
-
";Items.Value.Substituted"
|
|
851
|
+
params[
|
|
852
|
+
"selectedFields"
|
|
853
|
+
] += (
|
|
854
|
+
";Items.Value.Good;Items.Value.Questionable;Items.Value.Substituted"
|
|
850
855
|
)
|
|
851
856
|
elif read_type in [ReaderType.INT, ReaderType.RAW, ReaderType.SHAPEPRESERVING]:
|
|
852
857
|
params["selectedFields"] = "Links;Items.Timestamp;Items.Value"
|
|
@@ -994,7 +999,7 @@ class PIHandlerWeb(BaseHandlerWeb):
|
|
|
994
999
|
if not web_id:
|
|
995
1000
|
return pd.DataFrame()
|
|
996
1001
|
|
|
997
|
-
|
|
1002
|
+
url, params = self.generate_read_query(
|
|
998
1003
|
tag=web_id,
|
|
999
1004
|
start=start,
|
|
1000
1005
|
end=end,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|