tagreader 6.0.2__tar.gz → 6.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: tagreader
3
- Version: 6.0.2
3
+ Version: 6.0.4
4
4
  Summary: Tagreader is a Python package for reading trend data from the OSIsoft PI and Aspen Infoplus.21 IMS systems.
5
5
  License: MIT
6
6
  Keywords: Aspen InfoPlus.21,OSIsoft PI
7
7
  Author: Einar S. Idsø
8
8
  Author-email: eiids@equinor.com
9
- Requires-Python: >=3.8,<4.0
9
+ Requires-Python: >=3.9.2,<4.0.0
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Intended Audience :: Science/Research
@@ -16,22 +16,23 @@ Classifier: Operating System :: MacOS
16
16
  Classifier: Operating System :: Microsoft :: Windows
17
17
  Classifier: Operating System :: POSIX :: Linux
18
18
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.8
20
- Classifier: Programming Language :: Python :: 3.9
21
19
  Classifier: Programming Language :: Python :: 3.10
22
20
  Classifier: Programming Language :: Python :: 3.11
23
21
  Classifier: Programming Language :: Python :: 3.12
24
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.8
24
+ Classifier: Programming Language :: Python :: 3.9
25
25
  Classifier: Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator
26
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
27
  Classifier: Topic :: Utilities
28
28
  Provides-Extra: notebooks
29
- Requires-Dist: certifi (>=2024.12.14,<2025.0.0)
29
+ Requires-Dist: certifi (>=2024.12.14,<2026.0.0)
30
30
  Requires-Dist: diskcache (>=5.6.1,<6.0.0)
31
31
  Requires-Dist: matplotlib (>=3.7.5,<4.0.0) ; extra == "notebooks"
32
- Requires-Dist: msal-bearer (>=0.2.1,<1.2.0)
32
+ Requires-Dist: msal-bearer (>=1.3.0,<2.0.0)
33
33
  Requires-Dist: notebook (>=7.2.2,<8.0.0) ; extra == "notebooks"
34
34
  Requires-Dist: pandas (>=1)
35
+ Requires-Dist: platformdirs (>=4.3.7,<5.0.0)
35
36
  Requires-Dist: pycryptodome (>=3.20.0,<4.0.0)
36
37
  Requires-Dist: requests (>=2,<3)
37
38
  Requires-Dist: requests-kerberos (>=0,<1)
@@ -48,25 +49,17 @@ Description-Content-Type: text/markdown
48
49
  [![Downloads](https://pepy.tech/badge/tagreader)](https://pepy.tech/project/tagreader)
49
50
 
50
51
  Tagreader is a Python package for reading timeseries data from the OSIsoft PI and Aspen Infoplus.21
51
- Information Manufacturing Systems (IMS) systems. It is intended to be easy to use, and present as similar interfaces
52
- as possible to the backend historians.
52
+ Information Management Systems (IMS). It is intended to be easy to use, and present as similar interfaces
53
+ as possible to the backend plant historians.
53
54
 
54
55
  ## Installation
55
56
  You can install tagreader directly into your project from pypi by using pip
56
- or another package manager. The only requirement is Python version 3.8 or above.
57
+ or another package manager. Supports Python version 3.9.2 and above.
57
58
 
58
59
  ```shell
59
60
  pip install tagreader
60
61
  ```
61
62
 
62
- The following are required and will be installed:
63
-
64
- * pandas
65
- * requests
66
- * requests-kerberos
67
- * certifi
68
- * diskcache
69
-
70
63
  ## Usage
71
64
  Tagreader is easy to use for both Equinor internal IMS services, and non-internal usage. For non-internal usage
72
65
  you simply need to provide the corresponding IMS service URLs and IMSType.
@@ -6,25 +6,17 @@
6
6
  [![Downloads](https://pepy.tech/badge/tagreader)](https://pepy.tech/project/tagreader)
7
7
 
8
8
  Tagreader is a Python package for reading timeseries data from the OSIsoft PI and Aspen Infoplus.21
9
- Information Manufacturing Systems (IMS) systems. It is intended to be easy to use, and present as similar interfaces
10
- as possible to the backend historians.
9
+ Information Management Systems (IMS). It is intended to be easy to use, and present as similar interfaces
10
+ as possible to the backend plant historians.
11
11
 
12
12
  ## Installation
13
13
  You can install tagreader directly into your project from pypi by using pip
14
- or another package manager. The only requirement is Python version 3.8 or above.
14
+ or another package manager. Supports Python version 3.9.2 and above.
15
15
 
16
16
  ```shell
17
17
  pip install tagreader
18
18
  ```
19
19
 
20
- The following are required and will be installed:
21
-
22
- * pandas
23
- * requests
24
- * requests-kerberos
25
- * certifi
26
- * diskcache
27
-
28
20
  ## Usage
29
21
  Tagreader is easy to use for both Equinor internal IMS services, and non-internal usage. For non-internal usage
30
22
  you simply need to provide the corresponding IMS service URLs and IMSType.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "tagreader"
3
- version = "6.0.2"
3
+ version = "6.0.4"
4
4
  description = "Tagreader is a Python package for reading trend data from the OSIsoft PI and Aspen Infoplus.21 IMS systems."
5
5
  authors = ["Einar S. Idsø <eiids@equinor.com>", "Morten Dæhli Aslesen <masl@equinor.com"]
6
6
  license = "MIT"
@@ -28,18 +28,18 @@ classifiers=[
28
28
  ]
29
29
 
30
30
  [tool.poetry.dependencies]
31
- python = "^3.8"
31
+ python = "^3.9.2"
32
32
  pandas = ">=1"
33
- certifi = "^2024.12.14"
33
+ certifi = ">=2024.12.14,<2026.0.0"
34
34
  requests = "^2"
35
35
  requests-kerberos = "^0"
36
- msal-bearer = ">=0.2.1,<1.2.0"
36
+ msal-bearer = "^1.3.0"
37
37
  notebook = { version = "^7.2.2", optional = true }
38
38
  matplotlib = { version = "^3.7.5", optional = true }
39
39
  diskcache = "^5.6.1"
40
40
  pycryptodome = "^3.20.0"
41
41
  requests-ntlm = ">=1.1,<=2.0"
42
-
42
+ platformdirs = "^4.3.7"
43
43
 
44
44
  [tool.poetry.group.dev.dependencies]
45
45
  pre-commit = "^3"
@@ -1,5 +1,4 @@
1
- import os
2
- from datetime import datetime, timedelta, tzinfo
1
+ from datetime import datetime, timedelta, timezone, tzinfo
3
2
  from itertools import groupby
4
3
  from operator import itemgetter
5
4
  from typing import Any, Dict, List, Optional, Tuple, Union
@@ -16,9 +15,6 @@ from tagreader.utils import (
16
15
  ReaderType,
17
16
  convert_to_pydatetime,
18
17
  ensure_datetime_with_tz,
19
- find_registry_key,
20
- find_registry_key_from_name,
21
- is_windows,
22
18
  )
23
19
  from tagreader.web_handlers import (
24
20
  AspenHandlerWeb,
@@ -119,80 +115,6 @@ def get_next_timeslice(
119
115
  return start, calc_end
120
116
 
121
117
 
122
- def get_server_address_aspen(datasource: str) -> Optional[Tuple[str, int]]:
123
- """Data sources are listed under
124
- HKEY_CURRENT_USER\\Software\\AspenTech\\ADSA\\Caches\\AspenADSA\\username.
125
- For each data source there are multiple keys with Host entries. We start by
126
- identifying the correct key to use by locating the UUID for Aspen SQLplus
127
- services located under Aspen SQLplus service component. Then we find the
128
- host and port based on the path above and the UUID.
129
- """
130
-
131
- if not is_windows():
132
- return None
133
- import winreg
134
-
135
- regkey_clsid = winreg.OpenKey(
136
- winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Classes\Wow6432Node\CLSID"
137
- )
138
- regkey, _ = find_registry_key_from_name(
139
- regkey_clsid, "Aspen SQLplus service component"
140
- )
141
- regkey_implemented_categories = winreg.OpenKeyEx(regkey, "Implemented Categories")
142
-
143
- _, aspen_uuid = find_registry_key_from_name(
144
- regkey_implemented_categories, "Aspen SQLplus services"
145
- )
146
-
147
- reg_adsa = winreg.OpenKey(
148
- winreg.HKEY_CURRENT_USER,
149
- r"Software\AspenTech\ADSA\Caches\AspenADSA\\" + os.getlogin(),
150
- )
151
-
152
- try:
153
- reg_site_key = winreg.OpenKey(reg_adsa, datasource + "\\" + aspen_uuid)
154
- host = winreg.QueryValueEx(reg_site_key, "Host")[0]
155
- port = int(winreg.QueryValueEx(reg_site_key, "Port")[0])
156
- return host, port
157
- except FileNotFoundError:
158
- return None
159
-
160
-
161
- def get_server_address_pi(datasource: str) -> Optional[Tuple[str, int]]:
162
- """
163
- PI data sources are listed under
164
- HKEY_LOCAL_MACHINE\\Software\\Wow6432Node\\PISystem\\PI-SDK\\x.x\\ServerHandles or
165
- \\Software\\PISystem\\PI-SDK\\x.x\\ServerHandles.
166
-
167
- :param datasource: Name of data source
168
- :return: host, port
169
- :type: tuple(string, int)
170
- """
171
-
172
- if not is_windows():
173
- return None
174
- import winreg
175
-
176
- try:
177
- reg_key = winreg.OpenKey(
178
- winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\PISystem\PI-SDK"
179
- )
180
- reg_key_handles = find_registry_key(reg_key, "ServerHandles")
181
- reg_site_key = find_registry_key(reg_key_handles, datasource)
182
- if reg_site_key is None:
183
- reg_key = winreg.OpenKey(
184
- winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\PISystem\PI-SDK"
185
- )
186
- reg_key_handles = find_registry_key(reg_key, "ServerHandles")
187
- reg_site_key = find_registry_key(reg_key_handles, datasource)
188
- if reg_site_key is not None:
189
- host = winreg.QueryValueEx(reg_site_key, "path")[0]
190
- port = int(winreg.QueryValueEx(reg_site_key, "port")[0])
191
- return host, port
192
- except FileNotFoundError:
193
- return None
194
-
195
-
196
118
  def get_handler(
197
119
  imstype: Optional[IMSType],
198
120
  datasource: str,
@@ -557,7 +479,7 @@ class IMSClient:
557
479
  except ValueError:
558
480
  start = convert_to_pydatetime(start)
559
481
  if end is None:
560
- end = datetime.utcnow()
482
+ end = datetime.now(timezone.utc)
561
483
  elif isinstance(end, (str, pd.Timestamp)):
562
484
  end = convert_to_pydatetime(end)
563
485
 
@@ -1,13 +1,18 @@
1
1
  import enum
2
+ import hashlib
2
3
  import logging
3
4
  import platform
4
- import warnings
5
+ import ssl
5
6
  from datetime import datetime, tzinfo
6
7
  from enum import Enum
8
+ from pathlib import Path
7
9
  from typing import Union
8
10
 
11
+ import certifi
9
12
  import pandas as pd
10
13
  import pytz
14
+ import requests
15
+ from platformdirs import user_data_dir
11
16
 
12
17
  from tagreader.logger import logger
13
18
 
@@ -27,47 +32,11 @@ def is_linux() -> bool:
27
32
  if is_windows():
28
33
  import winreg
29
34
 
30
-
31
35
  if is_mac():
32
36
  import socket
33
37
  import subprocess
34
38
 
35
39
 
36
- def find_registry_key(base_key, search_key_name: str):
37
- search_key_name = search_key_name.lower()
38
- if base_key is not None:
39
- num_keys, _, _ = winreg.QueryInfoKey(base_key)
40
- for i in range(0, num_keys):
41
- key_name = winreg.EnumKey(base_key, i)
42
- if key_name.lower() == search_key_name:
43
- return winreg.OpenKey(base_key, key_name)
44
- else:
45
- key = find_registry_key(
46
- winreg.OpenKey(base_key, key_name), search_key_name
47
- )
48
- if key is not None:
49
- return key
50
- return None
51
-
52
-
53
- def find_registry_key_from_name(base_key, search_key_name: str):
54
- search_key_name = search_key_name.lower()
55
- num_keys, _, _ = winreg.QueryInfoKey(base_key)
56
- key = key_string = None
57
- for i in range(0, num_keys):
58
- try:
59
- key_string = winreg.EnumKey(base_key, i)
60
- key = winreg.OpenKey(base_key, key_string)
61
- _, num_vals, _ = winreg.QueryInfoKey(key)
62
- if num_vals > 0:
63
- (_, key_name, _) = winreg.EnumValue(key, 0)
64
- if str(key_name).lower() == search_key_name:
65
- break
66
- except Exception as err:
67
- logging.error("{}: {}".format(i, err))
68
- return key, key_string
69
-
70
-
71
40
  def convert_to_pydatetime(date_stamp: Union[datetime, str, pd.Timestamp]) -> datetime:
72
41
  if isinstance(date_stamp, datetime):
73
42
  return date_stamp
@@ -138,64 +107,82 @@ def add_equinor_root_certificate() -> bool:
138
107
  running on Equinor host.
139
108
 
140
109
  NB! This needs to be repeated after updating the cacert module.
141
- NB! Will not download certificates.
142
110
 
143
111
  Returns:
144
112
  bool: True if function completes successfully
145
113
  """
146
- import hashlib
147
- import ssl
114
+ certificate = find_local_equinor_root_certificate()
148
115
 
149
- import certifi
116
+ # If certificate is not found locally, we download it from the Equinor server
117
+ if certificate == "":
118
+ logger.debug(
119
+ "Unable to locate Equinor Root CA certificate on this host. Downloading from Equinor server."
120
+ )
121
+ response = requests.get("http://pki.equinor.com/aia/ecpr.crt")
150
122
 
151
- EQUINOR_root_PEM_HASH = "5A206332CE73CED1D44C8A99C4C43B7CEE03DF5F"
152
- ca_search = "Equinor Root CA"
123
+ if response.status_code != 200:
124
+ logger.error(
125
+ "Unable to find Equinor Root CA certificate locally and on Equinor server."
126
+ )
127
+ return False
153
128
 
154
- found = False
155
- der = None
129
+ certificate = response.text.replace("\r", "")
156
130
 
157
- if is_linux():
131
+ # Write result to user data so we can read the cert from there next time
132
+ filepath = Path(user_data_dir("tagreader")) / "equinor_root_ca.crt"
133
+ try:
134
+ filepath.parent.mkdir(parents=True, exist_ok=True)
135
+ filepath.write_text(certificate)
136
+ logger.debug("Equinor Root CA certificate written to cache")
137
+ except Exception as e:
138
+ logger.debug(f"Failed to write Equinor Root CA certificate to cache: {e}")
139
+
140
+ if certificate in certifi.contents():
141
+ logger.debug("Equinor Root Certificate already exists in certifi store")
158
142
  return True
159
- elif is_windows():
160
- logger.debug("Scanning CA certificate in Windows cert store", end="")
143
+
144
+ ca_file = certifi.where()
145
+ with open(ca_file, "ab") as f:
146
+ f.write(bytes(certificate, "ascii"))
147
+ logger.debug("Equinor Root Certificate added to certifi store")
148
+
149
+
150
+ def find_local_equinor_root_certificate() -> str:
151
+ equinor_root_pem_hash = "5A206332CE73CED1D44C8A99C4C43B7CEE03DF5F"
152
+ ca_search = "Equinor Root CA"
153
+
154
+ if is_windows():
155
+ logger.debug("Checking for Equinor Root CA in Windows certificate store")
161
156
  for cert in ssl.enum_certificates("CA"):
162
- der = cert[0]
157
+ found_cert = cert[0]
163
158
  # 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
159
+ if hashlib.sha1(found_cert).hexdigest().upper() == equinor_root_pem_hash:
160
+ return ssl.DER_cert_to_PEM_cert(found_cert)
161
+
168
162
  elif is_mac():
163
+ logger.debug("Checking for Equinor Root CA in MacOS certificate store")
169
164
  macos_ca_certs = subprocess.run(
170
165
  ["security", "find-certificate", "-a", "-c", ca_search, "-Z"],
171
166
  stdout=subprocess.PIPE,
172
167
  ).stdout
173
168
 
174
- if EQUINOR_root_PEM_HASH in str(macos_ca_certs).upper():
169
+ if equinor_root_pem_hash in str(macos_ca_certs).upper():
175
170
  c = get_macos_equinor_certificates()
176
171
  for cert in c:
177
172
  # deepcode ignore InsecureHash: <Only hashes to compare with known hash>
178
- if hashlib.sha1(cert).hexdigest().upper() == 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.")
173
+ if hashlib.sha1(cert).hexdigest().upper() == equinor_root_pem_hash:
174
+ return ssl.DER_cert_to_PEM_cert(cert)
175
+
176
+ # If the certificate is not found in the local cert store, look in the tagreader cache
177
+ filepath = Path(user_data_dir("tagreader")) / "equinor_root_ca.crt"
178
+
179
+ try:
180
+ if filepath.exists():
181
+ return filepath.read_text()
182
+ except Exception as e:
183
+ logger.debug(f"Failed to read Equinor Root CA certificate from cache: {e}")
197
184
 
198
- return found
185
+ return ""
199
186
 
200
187
 
201
188
  def get_macos_equinor_certificates():
@@ -19,7 +19,7 @@ from urllib3.exceptions import InsecureRequestWarning
19
19
 
20
20
  from tagreader.cache import BucketCache, SmartCache
21
21
  from tagreader.logger import logger
22
- from tagreader.utils import 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:
@@ -72,7 +72,7 @@ def get_auth_aspen(use_internal: bool = True):
72
72
  if use_internal:
73
73
  return HTTPKerberosAuth(mutual_authentication=OPTIONAL)
74
74
 
75
- from msal_bearer.BearerAuth import BearerAuth
75
+ from msal_bearer import BearerAuth
76
76
 
77
77
  tenantID = "3aa4a235-b6e2-48d5-9195-7fcf05b459b0"
78
78
  clientID = "7adaaa99-897f-428c-8a5f-4053db565b32"
@@ -258,10 +258,6 @@ class AspenHandlerWeb(BaseHandlerWeb):
258
258
  self._max_rows = options.get("max_rows", 100000)
259
259
  self._connection_string = "" # Used for raw SQL queries
260
260
 
261
- @staticmethod
262
- def generate_connection_string(host, *_, **__):
263
- raise NotImplementedError
264
-
265
261
  @staticmethod
266
262
  def generate_search_query(
267
263
  tag: Optional[str],
@@ -669,25 +665,6 @@ class AspenHandlerWeb(BaseHandlerWeb):
669
665
  connection_string += f"<![CDATA[{query}]]></SQL>"
670
666
  return connection_string
671
667
 
672
- def initialize_connection_string(
673
- self,
674
- host: Optional[str] = None,
675
- port: int = 10014,
676
- connection_string: Optional[str] = None,
677
- ):
678
- if connection_string:
679
- self._connection_string = connection_string
680
- else:
681
- if host is None:
682
- from tagreader.clients import get_server_address_aspen
683
-
684
- host = get_server_address_aspen(self.datasource)
685
- self._connection_string = (
686
- f"DRIVER=AspenTech SQLPlus;HOST={host};"
687
- f"PORT={port};CHARINT=N;CHARFLOAT=N;"
688
- "CHARTIME=N;CONVERTERRORS=N"
689
- )
690
-
691
668
  def query_sql(self, query: str, parse: bool = True) -> Union[str, pd.DataFrame]:
692
669
  url = urljoin(self.base_url, "SQL")
693
670
  if self._connection_string is None:
@@ -712,16 +689,17 @@ class AspenHandlerWeb(BaseHandlerWeb):
712
689
  parsed_dict = res.json()["data"][0]
713
690
 
714
691
  cols = []
715
- for i in parsed_dict["cols"]:
716
- cols.append(i["n"])
692
+ if "cols" in parsed_dict.keys():
693
+ for i in parsed_dict["cols"]:
694
+ cols.append(i["n"])
717
695
 
718
696
  rows = []
719
-
720
- for i in parsed_dict["rows"]:
721
- element = []
722
- for j in i["fld"]:
723
- element.append(j["v"])
724
- rows.append(element)
697
+ if "rows" in parsed_dict.keys():
698
+ for i in parsed_dict["rows"]:
699
+ element = []
700
+ for j in i["fld"]:
701
+ element.append(j["v"])
702
+ rows.append(element)
725
703
  return pd.DataFrame(data=rows, columns=cols)
726
704
  return res.text
727
705
 
@@ -754,10 +732,6 @@ class PIHandlerWeb(BaseHandlerWeb):
754
732
  def _time_to_UTC_string(time: datetime) -> str:
755
733
  return time.astimezone(pytz.UTC).strftime("%d-%b-%y %H:%M:%S")
756
734
 
757
- @staticmethod
758
- def generate_connection_string(host, *_, **__):
759
- raise NotImplementedError
760
-
761
735
  @staticmethod
762
736
  def escape(s: str) -> str:
763
737
  # https://techsupport.osisoft.com/Documentation/PI-Web-API/help/topics/search-queries.html
File without changes
File without changes
File without changes