tagreader 6.0.1__tar.gz → 6.0.3__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,13 +1,12 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: tagreader
3
- Version: 6.0.1
3
+ Version: 6.0.3
4
4
  Summary: Tagreader is a Python package for reading trend data from the OSIsoft PI and Aspen Infoplus.21 IMS systems.
5
- Home-page: https://github.com/equinor/tagreader-python
6
5
  License: MIT
7
6
  Keywords: Aspen InfoPlus.21,OSIsoft PI
8
7
  Author: Einar S. Idsø
9
8
  Author-email: eiids@equinor.com
10
- Requires-Python: >=3.8,<4.0
9
+ Requires-Python: >=3.9.2,<4.0.0
11
10
  Classifier: Development Status :: 5 - Production/Stable
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: Intended Audience :: Science/Research
@@ -17,25 +16,27 @@ Classifier: Operating System :: MacOS
17
16
  Classifier: Operating System :: Microsoft :: Windows
18
17
  Classifier: Operating System :: POSIX :: Linux
19
18
  Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.8
21
- Classifier: Programming Language :: Python :: 3.9
22
19
  Classifier: Programming Language :: Python :: 3.10
23
20
  Classifier: Programming Language :: Python :: 3.11
24
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
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.7.4,<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
- Requires-Dist: matplotlib (>=3.7.0,<4.0.0) ; extra == "notebooks"
32
- Requires-Dist: msal-bearer (>=0.2.1,<1.2.0)
33
- Requires-Dist: notebook (>=7.2.0,<8.0.0) ; extra == "notebooks"
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"
34
34
  Requires-Dist: pandas (>=1)
35
35
  Requires-Dist: pycryptodome (>=3.20.0,<4.0.0)
36
36
  Requires-Dist: requests (>=2,<3)
37
37
  Requires-Dist: requests-kerberos (>=0,<1)
38
38
  Requires-Dist: requests-ntlm (>=1.1,<=2.0)
39
+ Project-URL: Homepage, https://github.com/equinor/tagreader-python
39
40
  Project-URL: Repository, https://github.com/equinor/tagreader-python
40
41
  Description-Content-Type: text/markdown
41
42
 
@@ -47,27 +48,19 @@ Description-Content-Type: text/markdown
47
48
  [![Downloads](https://pepy.tech/badge/tagreader)](https://pepy.tech/project/tagreader)
48
49
 
49
50
  Tagreader is a Python package for reading timeseries data from the OSIsoft PI and Aspen Infoplus.21
50
- Information Manufacturing Systems (IMS) systems. It is intended to be easy to use, and present as similar interfaces
51
- as possible to the backend historians.
51
+ Information Management Systems (IMS). It is intended to be easy to use, and present as similar interfaces
52
+ as possible to the backend plant historians.
52
53
 
53
54
  ## Installation
54
55
  You can install tagreader directly into your project from pypi by using pip
55
- or another package manager. The only requirement is Python version 3.8 or above.
56
+ or another package manager. Supports Python version 3.9.2 and above.
56
57
 
57
- ```shell"
58
+ ```shell
58
59
  pip install tagreader
59
60
  ```
60
61
 
61
- The following are required and will be installed:
62
-
63
- * pandas
64
- * requests
65
- * requests-kerberos
66
- * certifi
67
- * diskcache
68
-
69
62
  ## Usage
70
- Tagreader easy to use for both Equinor internal IMS services, and non-internal usage. For non-internal usage
63
+ Tagreader is easy to use for both Equinor internal IMS services, and non-internal usage. For non-internal usage
71
64
  you simply need to provide the corresponding IMS service URLs and IMSType.
72
65
  See [data source](https://equinor.github.io/tagreader-python/docs/about/usage/data-source) for details.
73
66
 
@@ -6,27 +6,19 @@
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
- ```shell"
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
- Tagreader easy to use for both Equinor internal IMS services, and non-internal usage. For non-internal usage
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.
31
23
  See [data source](https://equinor.github.io/tagreader-python/docs/about/usage/data-source) for details.
32
24
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "tagreader"
3
- version = "6.0.1"
3
+ version = "6.0.3"
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,19 +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.7.4"
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"
37
- notebook = { version = "^7.2.0", optional = true }
38
- matplotlib = { version = "^3.7.0", optional = true }
36
+ msal-bearer = "^1.3.0"
37
+ notebook = { version = "^7.2.2", optional = true }
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
 
43
-
44
43
  [tool.poetry.group.dev.dependencies]
45
44
  pre-commit = "^3"
46
45
  pytest = ">=7,<9"
@@ -1,5 +1,5 @@
1
1
  import os
2
- from datetime import datetime, timedelta, tzinfo
2
+ from datetime import datetime, timedelta, timezone, tzinfo
3
3
  from itertools import groupby
4
4
  from operator import itemgetter
5
5
  from typing import Any, Dict, List, Optional, Tuple, Union
@@ -128,6 +128,8 @@ def get_server_address_aspen(datasource: str) -> Optional[Tuple[str, int]]:
128
128
  host and port based on the path above and the UUID.
129
129
  """
130
130
 
131
+ # todo: is obsolete after removing ODBC
132
+
131
133
  if not is_windows():
132
134
  return None
133
135
  import winreg
@@ -168,6 +170,7 @@ def get_server_address_pi(datasource: str) -> Optional[Tuple[str, int]]:
168
170
  :return: host, port
169
171
  :type: tuple(string, int)
170
172
  """
173
+ # todo: is obsolete after removing ODBC
171
174
 
172
175
  if not is_windows():
173
176
  return None
@@ -442,15 +445,20 @@ class IMSClient:
442
445
  tags = [tags]
443
446
  units = {}
444
447
  for tag in tags:
445
- if self.cache is not None:
446
- r = self.cache.get_metadata(key=tag, properties="unit")
447
- if r is not None and "unit" in r:
448
- units[tag] = r["unit"]
449
- if tag not in units:
450
- unit = self.handler._get_tag_unit(tag)
451
- if self.cache is not None and unit is not None:
452
- self.cache.put_metadata(key=tag, value={"unit": unit})
453
- units[tag] = unit
448
+ try:
449
+ if self.cache is not None:
450
+ r = self.cache.get_metadata(key=tag, properties="unit")
451
+ if r is not None and "unit" in r:
452
+ units[tag] = r["unit"]
453
+ if tag not in units:
454
+ unit = self.handler._get_tag_unit(tag)
455
+ if self.cache is not None and unit is not None:
456
+ self.cache.put_metadata(key=tag, value={"unit": unit})
457
+ units[tag] = unit
458
+ except Exception:
459
+ if self.search(tag) == []: # check for nonexisting string
460
+ logger.warning(f"Tag not found: {tag}")
461
+ continue
454
462
  return units
455
463
 
456
464
  def get_descriptions(self, tags: Union[str, List[str]]) -> Dict[str, str]:
@@ -458,15 +466,20 @@ class IMSClient:
458
466
  tags = [tags]
459
467
  descriptions = {}
460
468
  for tag in tags:
461
- if self.cache is not None:
462
- r = self.cache.get_metadata(key=tag, properties="description")
463
- if r is not None and "description" in r:
464
- descriptions[tag] = r["description"]
465
- if tag not in descriptions:
466
- desc = self.handler._get_tag_description(tag)
467
- if self.cache is not None and desc is not None:
468
- self.cache.put_metadata(key=tag, value={"description": desc})
469
- descriptions[tag] = desc
469
+ try:
470
+ if self.cache is not None:
471
+ r = self.cache.get_metadata(key=tag, properties="description")
472
+ if r is not None and "description" in r:
473
+ descriptions[tag] = r["description"]
474
+ if tag not in descriptions:
475
+ desc = self.handler._get_tag_description(tag)
476
+ if self.cache is not None and desc is not None:
477
+ self.cache.put_metadata(key=tag, value={"description": desc})
478
+ descriptions[tag] = desc
479
+ except Exception:
480
+ if self.search(tag) == []: # check for nonexisting string
481
+ logger.warning(f"Tag not found: {tag}")
482
+ continue
470
483
  return descriptions
471
484
 
472
485
  def read_tags(
@@ -547,7 +560,7 @@ class IMSClient:
547
560
  except ValueError:
548
561
  start = convert_to_pydatetime(start)
549
562
  if end is None:
550
- end = datetime.utcnow()
563
+ end = datetime.now(timezone.utc)
551
564
  elif isinstance(end, (str, pd.Timestamp)):
552
565
  end = convert_to_pydatetime(end)
553
566
 
@@ -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"
@@ -187,6 +187,7 @@ class BaseHandlerWeb(ABC):
187
187
  self.datasource = datasource
188
188
  self.base_url = url
189
189
  self.session = requests.Session()
190
+ self.auth = auth
190
191
  self.session.auth = auth if auth is not None else get_auth_aspen()
191
192
  if verify_ssl is False:
192
193
  urllib3.disable_warnings(InsecureRequestWarning)
@@ -222,14 +223,6 @@ class BaseHandlerWeb(ABC):
222
223
  txt = res.text.replace('"v":nan', '"v":NaN').replace('"v":-nan', '"v":NaN')
223
224
  return json.loads(txt)
224
225
 
225
- def fetch_text(
226
- self, url, params: Optional[Union[str, Dict[str, str]]] = None
227
- ) -> str:
228
- res = self.session.get(url, params=params)
229
- res.raise_for_status()
230
-
231
- return res.text
232
-
233
226
  def connect(self):
234
227
  try:
235
228
  self.verify_connection(self.datasource)
@@ -267,6 +260,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
267
260
 
268
261
  @staticmethod
269
262
  def generate_connection_string(host, *_, **__):
263
+ # todo: is obsolete after removing ODBC
270
264
  raise NotImplementedError
271
265
 
272
266
  @staticmethod
@@ -503,7 +497,10 @@ class AspenHandlerWeb(BaseHandlerWeb):
503
497
  if not desc and not return_desc:
504
498
  ret.append(tagname)
505
499
  else:
506
- description = self._get_tag_description(tagname)
500
+ try:
501
+ description = self._get_tag_description(tagname)
502
+ except KeyError:
503
+ description = ""
507
504
  ret.append((tagname, description))
508
505
 
509
506
  if not desc:
@@ -523,11 +520,11 @@ class AspenHandlerWeb(BaseHandlerWeb):
523
520
  query = self.generate_get_unit_query(tag)
524
521
  url = urljoin(self.base_url, "TagInfo")
525
522
  data = self.fetch(url, params=query)
526
- try:
527
- attr_data = data["data"]["tags"][0]["attrData"]
528
- except KeyError as e:
529
- logger.error(f"Error. I got this: {data}")
530
- raise e
523
+ # try:
524
+ attr_data = data["data"]["tags"][0]["attrData"]
525
+ # except KeyError as e:
526
+ # logger.error(f"Error. I got this: {data}")
527
+ # raise e
531
528
  unit = ""
532
529
  for a in attr_data:
533
530
  if a["g"] == "Units":
@@ -559,8 +556,8 @@ class AspenHandlerWeb(BaseHandlerWeb):
559
556
  try:
560
557
  data = self.fetch(url, params=query)
561
558
  desc = data["data"]["tags"][0]["attrData"][0]["samples"][0]["v"]
562
- except KeyError:
563
- desc = ""
559
+ # except KeyError:
560
+ # desc = ""
564
561
  except JSONDecodeError:
565
562
  desc = ""
566
563
  return desc
@@ -664,16 +661,23 @@ class AspenHandlerWeb(BaseHandlerWeb):
664
661
  'dso="CHARINT=N;CHARFLOAT=N;CHARTIME=N;CONVERTERRORS=N" '
665
662
  f'm="{max_rows}" to="30" s="1">'
666
663
  )
664
+ query = query.replace("\t", " ").replace(
665
+ "\n", " "
666
+ ) # Replace new lines and tabs that are typical in formatted SQL queries with spaces.
667
+
668
+ # Need a solution to LIKE comments. These have a % symbol in the query that does not seem to pass through the request
667
669
 
668
670
  connection_string += f"<![CDATA[{query}]]></SQL>"
669
671
  return connection_string
670
672
 
671
673
  def initialize_connection_string(
674
+ # todo: is obsolete after removing ODBC
672
675
  self,
673
676
  host: Optional[str] = None,
674
677
  port: int = 10014,
675
678
  connection_string: Optional[str] = None,
676
679
  ):
680
+ # todo: is obsolete after removing ODBC
677
681
  if connection_string:
678
682
  self._connection_string = connection_string
679
683
  else:
@@ -703,13 +707,27 @@ class AspenHandlerWeb(BaseHandlerWeb):
703
707
  max_rows=self._max_rows,
704
708
  datasource=None,
705
709
  )
706
- res_text = self.fetch_text(url, params=params)
707
- # For now just return result as text regardless of value of parse
710
+
711
+ res = self.session.get(url, params=params)
712
+ res.raise_for_status()
713
+
708
714
  if parse:
709
- raise NotImplementedError(
710
- "Use parse=False to receive and handle text result instead"
711
- )
712
- return res_text
715
+ parsed_dict = res.json()["data"][0]
716
+
717
+ cols = []
718
+ if "cols" in parsed_dict.keys():
719
+ for i in parsed_dict["cols"]:
720
+ cols.append(i["n"])
721
+
722
+ rows = []
723
+ if "rows" in parsed_dict.keys():
724
+ for i in parsed_dict["rows"]:
725
+ element = []
726
+ for j in i["fld"]:
727
+ element.append(j["v"])
728
+ rows.append(element)
729
+ return pd.DataFrame(data=rows, columns=cols)
730
+ return res.text
713
731
 
714
732
 
715
733
  class PIHandlerWeb(BaseHandlerWeb):
@@ -742,6 +760,7 @@ class PIHandlerWeb(BaseHandlerWeb):
742
760
 
743
761
  @staticmethod
744
762
  def generate_connection_string(host, *_, **__):
763
+ # todo: is obsolete after removing ODBC
745
764
  raise NotImplementedError
746
765
 
747
766
  @staticmethod
@@ -777,6 +796,7 @@ class PIHandlerWeb(BaseHandlerWeb):
777
796
  tag: Optional[str],
778
797
  desc: Optional[str],
779
798
  datasource: Optional[str],
799
+ auth: Optional[Any] = None,
780
800
  ) -> Dict[str, str]:
781
801
  q = []
782
802
  if tag is not None:
@@ -788,7 +808,7 @@ class PIHandlerWeb(BaseHandlerWeb):
788
808
 
789
809
  if datasource is not None:
790
810
  params["dataserverwebid"] = (
791
- f"{get_piwebapi_source_to_webid_dict()[datasource]}"
811
+ f"{get_piwebapi_source_to_webid_dict(auth=auth)[datasource]}"
792
812
  )
793
813
 
794
814
  return params
@@ -900,7 +920,7 @@ class PIHandlerWeb(BaseHandlerWeb):
900
920
  return_desc: bool = True,
901
921
  ) -> Union[List[Tuple[str, str]], List[str]]:
902
922
  params = self.generate_search_params(
903
- tag=tag, desc=desc, datasource=self.datasource
923
+ tag=tag, desc=desc, datasource=self.datasource, auth=self.auth
904
924
  )
905
925
  url = urljoin(self.base_url, "points", "search")
906
926
  done = False
@@ -956,7 +976,7 @@ class PIHandlerWeb(BaseHandlerWeb):
956
976
  return self.web_id_cache[tag]
957
977
 
958
978
  params = self.generate_search_params(
959
- tag=tag, datasource=self.datasource, desc=None
979
+ tag=tag, datasource=self.datasource, desc=None, auth=self.auth
960
980
  )
961
981
  url = urljoin(self.base_url, "points", "search")
962
982
  data = self.fetch(url, params=params)
File without changes
File without changes
File without changes
File without changes