tagreader 6.2.1__tar.gz → 6.3.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.
@@ -1,13 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tagreader
3
- Version: 6.2.1
3
+ Version: 6.3.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-Expression: MIT
6
- License-File: LICENSE
7
5
  Keywords: Aspen InfoPlus.21,OSIsoft PI
8
- Author: Einar S. Idsø
9
- Author-email: eiids@equinor.com
10
- Requires-Python: >=3.9.2,<4.0.0
6
+ Author: Einar S. Idsø, Morten Dæhli Aslesen
7
+ Author-email: Einar S. Idsø <eiids@equinor.com>, Morten Dæhli Aslesen <masl@equinor.com>
8
+ License-Expression: MIT
11
9
  Classifier: Development Status :: 5 - Production/Stable
12
10
  Classifier: Intended Audience :: Developers
13
11
  Classifier: Intended Audience :: Science/Research
@@ -23,70 +21,68 @@ Classifier: Programming Language :: Python :: 3.12
23
21
  Classifier: Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator
24
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
23
  Classifier: Topic :: Utilities
26
- Requires-Dist: certifi (>=2024.12.14,<2026.0.0)
27
- Requires-Dist: diskcache (>=5.6.1)
28
- Requires-Dist: msal-bearer (>=1.3.3)
29
- Requires-Dist: pandas (>=1)
30
- Requires-Dist: platformdirs (>=4.3.7)
31
- Requires-Dist: pycryptodome (>=3.20.0)
32
- Requires-Dist: requests (>=2.32.5)
33
- Requires-Dist: requests-kerberos (>=0)
34
- Requires-Dist: requests-ntlm (>=1.1,<=2.0)
35
- Requires-Dist: urllib3 (>=2.5.0)
36
- Project-URL: Homepage, https://github.com/equinor/tagreader-python
37
- Project-URL: Repository, https://github.com/equinor/tagreader-python
24
+ Requires-Dist: pandas>=1.4.0,<4
25
+ Requires-Dist: certifi>=2026.10.23,<2027.0.0
26
+ Requires-Dist: requests>=2.32.5
27
+ Requires-Dist: requests-kerberos>=0
28
+ Requires-Dist: msal-bearer>=1.6.0
29
+ Requires-Dist: diskcache>=5.6.1
30
+ Requires-Dist: pycryptodome>=4.1.0
31
+ Requires-Dist: requests-ntlm>=1.1,<=2.0
32
+ Requires-Dist: platformdirs>=4.4.0
33
+ Requires-Dist: urllib3>=2.5.0
34
+ Maintainer: Åsmund Våge Fannemel
35
+ Maintainer-email: Åsmund Våge Fannemel <asmf@equinor.com>
36
+ Requires-Python: >=3.9.6, <4.0.0
37
+ Project-URL: homepage, https://github.com/equinor/tagreader-python
38
+ Project-URL: repository, https://github.com/equinor/tagreader-python
38
39
  Description-Content-Type: text/markdown
39
40
 
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
-
44
- # tagreader-python <!-- omit in toc -->
45
-
46
- ![GitHub Build Status](https://github.com/equinor/tagreader-python/workflows/Test/badge.svg)
47
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tagreader)
48
- ![PyPI](https://img.shields.io/pypi/v/tagreader)
49
- [![Downloads](https://pepy.tech/badge/tagreader)](https://pepy.tech/project/tagreader)
50
-
51
- Tagreader is a Python package for reading timeseries data from the OSIsoft PI and Aspen Infoplus.21
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.
54
-
55
- ## Installation
56
- You can install tagreader directly into your project from pypi by using pip
57
- or another package manager. Supports Python version 3.9.2 and above.
58
-
59
- ```shell
60
- pip install tagreader
61
- ```
62
-
63
- ## Usage
64
- Tagreader is easy to use for both Equinor internal IMS services, and non-internal usage. For non-internal usage
65
- you simply need to provide the corresponding IMS service URLs and IMSType.
66
- See [data source](https://equinor.github.io/tagreader-python/docs/about/usage/data-source) for details.
67
-
68
- ### Usage example
69
- ```python
70
- import tagreader
71
- c = tagreader.IMSClient("mysource", "aspenone")
72
- print(c.search("tag*"))
73
- df = c.read_tags(["tag1", "tag2"], "18.06.2020 08:00:00", "18.06.2020 09:00:00", 60)
74
- ```
75
-
76
- Note, you can add a timeout argument to the search method in order to avoid long-running search queries.
77
-
78
- ### Jupyter Notebook Quickstart
79
- Jupyter Notebook examples can be found in /examples. In order to run these examples, you need to install the
80
- optional dependencies.
81
-
82
- ```shell
83
- pip install tagreader[notebooks]
84
- ```
85
-
86
- The quickstart Jupyter Notebook can be found [here](https://github.com/equinor/tagreader-python/blob/main/examples/quickstart.ipynb)
87
-
88
- For more details, see the [Tagreader Docs](https://equinor.github.io/tagreader-python/).
89
-
90
- ## Documentation
91
- The full documentation can be found in [Tagreader Docs](https://equinor.github.io/tagreader-python/)
92
-
41
+ # tagreader-python <!-- omit in toc -->
42
+
43
+ [![GitHub Build Status](https://github.com/equinor/tagreader-python/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/equinor/tagreader-python/actions/workflows/build.yml)
44
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tagreader)
45
+ ![PyPI](https://img.shields.io/pypi/v/tagreader)
46
+ [![Downloads](https://pepy.tech/badge/tagreader)](https://pepy.tech/project/tagreader)
47
+
48
+ Tagreader is a Python package for reading timeseries data from the OSIsoft PI and Aspen Infoplus.21
49
+ Information Management Systems (IMS). It is intended to be easy to use, and present as similar interfaces
50
+ as possible to the backend plant historians.
51
+
52
+ ## Installation
53
+ You can install tagreader directly into your project from pypi by using pip
54
+ or another package manager. Supports Python version 3.9.2 and above.
55
+
56
+ ```shell
57
+ pip install tagreader
58
+ ```
59
+
60
+ ## Usage
61
+ Tagreader is easy to use for both Equinor internal IMS services, and non-internal usage. For non-internal usage
62
+ you simply need to provide the corresponding IMS service URLs and IMSType.
63
+ See [data source](https://equinor.github.io/tagreader-python/docs/about/usage/data-source) for details.
64
+
65
+ ### Usage example
66
+ ```python
67
+ import tagreader
68
+ c = tagreader.IMSClient("mysource", "aspenone")
69
+ print(c.search("tag*"))
70
+ df = c.read_tags(["tag1", "tag2"], "18.06.2020 08:00:00", "18.06.2020 09:00:00", 60)
71
+ ```
72
+
73
+ Note, you can add a timeout argument to the search method in order to avoid long-running search queries.
74
+
75
+ ### Jupyter Notebook Quickstart
76
+ Jupyter Notebook examples can be found in /examples. In order to run these examples, you need to install the
77
+ optional dependencies.
78
+
79
+ ```shell
80
+ pip install tagreader[notebooks]
81
+ ```
82
+
83
+ The quickstart Jupyter Notebook can be found [here](https://github.com/equinor/tagreader-python/blob/main/examples/quickstart.ipynb)
84
+
85
+ For more details, see the [Tagreader Docs](https://equinor.github.io/tagreader-python/).
86
+
87
+ ## Documentation
88
+ The full documentation can be found in [Tagreader Docs](https://equinor.github.io/tagreader-python/)
@@ -1,10 +1,6 @@
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
-
5
1
  # tagreader-python <!-- omit in toc -->
6
2
 
7
- ![GitHub Build Status](https://github.com/equinor/tagreader-python/workflows/Test/badge.svg)
3
+ [![GitHub Build Status](https://github.com/equinor/tagreader-python/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/equinor/tagreader-python/actions/workflows/build.yml)
8
4
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tagreader)
9
5
  ![PyPI](https://img.shields.io/pypi/v/tagreader)
10
6
  [![Downloads](https://pepy.tech/badge/tagreader)](https://pepy.tech/project/tagreader)
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "tagreader"
3
+ version = "6.3.1"
4
+ description = "Tagreader is a Python package for reading trend data from the OSIsoft PI and Aspen Infoplus.21 IMS systems."
5
+ requires-python = ">=3.9.6,<4.0.0"
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ keywords = [
9
+ "Aspen InfoPlus.21",
10
+ "OSIsoft PI",
11
+ ]
12
+ classifiers = [
13
+ "Development Status :: 5 - Production/Stable",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Natural Language :: English",
18
+ "Operating System :: MacOS",
19
+ "Operating System :: Microsoft :: Windows",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator",
26
+ "Topic :: Software Development :: Libraries :: Python Modules",
27
+ "Topic :: Utilities",
28
+ ]
29
+ dependencies = [
30
+ "pandas>=1.4.0,<4",
31
+ "certifi>=2026.10.23,<2027.0.0",
32
+ "requests>=2.32.5",
33
+ "requests-kerberos>=0",
34
+ "msal-bearer>=1.6.0",
35
+ "diskcache>=5.6.1",
36
+ "pycryptodome>=4.1.0",
37
+ "requests-ntlm>=1.1,<=2.0",
38
+ "platformdirs>=4.4.0",
39
+ "urllib3>=2.5.0",
40
+ ]
41
+
42
+ [[project.authors]]
43
+ name = "Einar S. Idsø"
44
+ email = "eiids@equinor.com"
45
+
46
+ [[project.authors]]
47
+ name = "Morten Dæhli Aslesen"
48
+ email = "masl@equinor.com"
49
+
50
+ [[project.maintainers]]
51
+ name = "Åsmund Våge Fannemel"
52
+ email = "asmf@equinor.com"
53
+
54
+ [project.urls]
55
+ homepage = "https://github.com/equinor/tagreader-python"
56
+ repository = "https://github.com/equinor/tagreader-python"
57
+
58
+ [dependency-groups]
59
+ dev = [
60
+ "pre-commit>=3.0",
61
+ "pytest>=7,<9",
62
+ ]
63
+
64
+ [tool.uv]
65
+ default-groups = "all"
66
+
67
+ [build-system]
68
+ requires = ["uv_build>=0.13.5,<0.14.0"]
69
+ build-backend = "uv_build"
@@ -1,16 +1,22 @@
1
1
  [project]
2
2
  name = "tagreader"
3
- version = "6.2.1"
3
+ version = "6.3.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" },
7
- { name = "Morten Dæhli Aslesen", email = "masl@equinor.com" }
7
+ { name = "Morten Dæhli Aslesen", email = "masl@equinor.com" },
8
8
  ]
9
- license = "MIT"
9
+ maintainers = [
10
+ { name = "Åsmund Våge Fannemel", email = "asmf@equinor.com" },
11
+ ]
12
+ requires-python = ">=3.9.6,<4.0.0"
10
13
  readme = "README.md"
11
- keywords=["Aspen InfoPlus.21", "OSIsoft PI"]
12
- requires-python = ">=3.9.2,<4.0.0"
13
- classifiers=[
14
+ license = "MIT"
15
+ keywords = [
16
+ "Aspen InfoPlus.21",
17
+ "OSIsoft PI",
18
+ ]
19
+ classifiers = [
14
20
  "Development Status :: 5 - Production/Stable",
15
21
  "Intended Audience :: Developers",
16
22
  "Intended Audience :: Science/Research",
@@ -28,30 +34,28 @@ classifiers=[
28
34
  "Topic :: Utilities",
29
35
  ]
30
36
  dependencies = [
31
- "pandas>=1",
32
- "certifi>=2024.12.14,<2026.0.0",
37
+ "pandas>=1.4.0,<4",
38
+ "certifi>=2026.10.23,<2027.0.0",
33
39
  "requests>=2.32.5",
34
40
  "requests-kerberos>=0",
35
- "msal-bearer>=1.3.3",
41
+ "msal-bearer>=1.6.0",
36
42
  "diskcache>=5.6.1",
37
- "pycryptodome>=3.20.0",
43
+ "pycryptodome>=4.1.0",
38
44
  "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
45
+ "platformdirs>=4.4.0",
46
+ "urllib3>=2.5.0",
41
47
  ]
42
48
 
43
- [tool.poetry.group.test.dependencies]
44
- pytest = ">=7,<9"
45
- # pytest-cov = ">=6.0.0"
46
-
47
- [tool.poetry.group.dev.dependencies]
48
- # "black>=24.1.1",
49
- pre-commit = ">=3.0"
50
-
51
49
  [project.urls]
52
50
  homepage = "https://github.com/equinor/tagreader-python"
53
51
  repository = "https://github.com/equinor/tagreader-python"
54
52
 
53
+ [dependency-groups]
54
+ dev = ["pre-commit>=3.0", "pytest>=7,<9"]
55
+
56
+ [tool.uv]
57
+ default-groups = "all"
58
+
55
59
  [build-system]
56
- requires = ["poetry-core>=2.0.0,<3.0.0"]
57
- build-backend = "poetry.core.masonry.api"
60
+ requires = ["uv_build>=0.13.5,<0.14.0"]
61
+ build-backend = "uv_build"
@@ -3,5 +3,5 @@ from importlib.metadata import PackageNotFoundError, distribution
3
3
  try:
4
4
  version = distribution("tagreader").version
5
5
  except PackageNotFoundError:
6
- # This will happen if you run tagreader without installing it as a package. E.g. poetry install --no-root.
6
+ # This will happen if you run tagreader without installing it as a package. E.g. uv sync --no-install-project.
7
7
  version = "0.0.0"
@@ -159,11 +159,11 @@ def get_handler(
159
159
 
160
160
  if imstype == IMSType.PIWEBAPI:
161
161
  return PIHandlerWeb(
162
- url=url,
163
162
  datasource=datasource,
164
- options=options,
165
- verify_ssl=verify_ssl,
163
+ url=url,
166
164
  auth=auth,
165
+ verify_ssl=verify_ssl,
166
+ options=options,
167
167
  cache=cache,
168
168
  )
169
169
 
@@ -365,7 +365,7 @@ class IMSClient:
365
365
  end=end,
366
366
  )
367
367
  frames.append(df)
368
- if len(df) < self.handler._max_rows:
368
+ if len(df) < self.handler.max_rows:
369
369
  break
370
370
  start = df.index[-1]
371
371
 
@@ -86,17 +86,11 @@ def get_url_aspen(use_internal: bool = True) -> str:
86
86
  return r"https://ewepwapa1pep04-statoilsrm.msappproxy.net/ProcessExplorer/ProcessData/AtProcessDataREST.dll"
87
87
 
88
88
 
89
- def list_aspenone_sources(
90
- url: Optional[str] = None,
91
- auth: Optional[Any] = None,
92
- verify_ssl: Optional[Union[bool, str]] = True,
89
+ def _fetch_aspenone_sources(
90
+ url: str,
91
+ auth: Any,
92
+ verify_ssl: Optional[Union[bool, str]],
93
93
  ) -> List[str]:
94
- if url is None:
95
- url = get_url_aspen()
96
-
97
- if auth is None:
98
- auth = get_auth_aspen()
99
-
100
94
  if verify_ssl is None:
101
95
  verify_ssl = get_verify_ssl()
102
96
 
@@ -117,17 +111,35 @@ def list_aspenone_sources(
117
111
  return []
118
112
 
119
113
 
120
- def list_piwebapi_sources(
114
+ # Keyed on url only so default-auth calls share the cache regardless of auth or verify_ssl.
115
+ _aspenone_sources_cache: Dict[str, List[str]] = {}
116
+
117
+
118
+ def list_aspenone_sources(
121
119
  url: Optional[str] = None,
122
120
  auth: Optional[Any] = None,
123
121
  verify_ssl: Optional[Union[bool, str]] = True,
124
122
  ) -> List[str]:
125
123
  if url is None:
126
- url = get_url_pi()
124
+ url = get_url_aspen()
127
125
 
128
- if auth is None:
129
- auth = get_auth_pi()
126
+ if auth is not None:
127
+ return _fetch_aspenone_sources(url, auth, verify_ssl)
130
128
 
129
+ if url not in _aspenone_sources_cache:
130
+ url_sources = _fetch_aspenone_sources(url, get_auth_aspen(), verify_ssl)
131
+ if len(url_sources) > 0:
132
+ _aspenone_sources_cache[url] = url_sources
133
+ else:
134
+ return []
135
+ return list(_aspenone_sources_cache[url])
136
+
137
+
138
+ def _fetch_piwebapi_sources(
139
+ url: str,
140
+ auth: Any,
141
+ verify_ssl: Optional[Union[bool, str]],
142
+ ) -> List[str]:
131
143
  if verify_ssl is None:
132
144
  verify_ssl = get_verify_ssl()
133
145
 
@@ -147,6 +159,28 @@ def list_piwebapi_sources(
147
159
  return []
148
160
 
149
161
 
162
+ # Keyed on url only so default-auth calls share the cache regardless of auth or verify_ssl.
163
+ _piwebapi_sources_cache: Dict[str, List[str]] = {}
164
+
165
+
166
+ def list_piwebapi_sources(
167
+ url: Optional[str] = None,
168
+ auth: Optional[Any] = None,
169
+ verify_ssl: Optional[Union[bool, str]] = True,
170
+ ) -> List[str]:
171
+ if url is None:
172
+ url = get_url_pi()
173
+
174
+ if auth is not None:
175
+ return _fetch_piwebapi_sources(url, auth, verify_ssl)
176
+
177
+ if url not in _piwebapi_sources_cache:
178
+ url_sources = _fetch_piwebapi_sources(url, get_auth_pi(), verify_ssl)
179
+ if len(url_sources) > 0:
180
+ _piwebapi_sources_cache[url] = url_sources
181
+ return list(_piwebapi_sources_cache[url])
182
+
183
+
150
184
  def get_piwebapi_source_to_webid_dict(
151
185
  url: Optional[str] = None,
152
186
  auth: Optional[Any] = None,
@@ -186,6 +220,7 @@ class BaseHandlerWeb(ABC):
186
220
  ):
187
221
  self.datasource = datasource
188
222
  self.base_url = url
223
+ self.max_rows = 10000
189
224
  self.session = requests.Session()
190
225
  self.auth = auth
191
226
  self.session.auth = auth if auth is not None else get_auth_aspen()
@@ -193,20 +228,66 @@ class BaseHandlerWeb(ABC):
193
228
  urllib3.disable_warnings(InsecureRequestWarning)
194
229
  self.session.verify = verify_ssl if verify_ssl is not None else get_verify_ssl()
195
230
 
231
+ @property
232
+ def datasource(self) -> str:
233
+ return self._datasource
234
+
235
+ @datasource.setter
236
+ def datasource(self, value: Optional[str]) -> None:
237
+ self._datasource = value if value is not None else ""
238
+
239
+ @property
240
+ def base_url(self) -> str:
241
+ return self._base_url
242
+
243
+ @base_url.setter
244
+ def base_url(self, value: Optional[str]) -> None:
245
+ self._base_url = value if value is not None else ""
246
+
247
+ @property
248
+ def max_rows(self) -> int:
249
+ return self._max_rows
250
+
251
+ @max_rows.setter
252
+ def max_rows(self, value: int) -> None:
253
+ self._max_rows = value
254
+
196
255
  def fetch(
197
256
  self,
198
257
  url,
199
- params: Optional[Union[str, Dict[str, str]]] = None,
258
+ params: Optional[Union[bytes, str, Dict[str, str]]] = None,
200
259
  timeout: Optional[int] = None,
260
+ use_post: bool = False,
201
261
  ) -> Dict:
202
- res = self.session.get(
203
- url,
204
- params=params,
205
- timeout=(
206
- None,
207
- timeout,
208
- ),
209
- ) # Noqa. Read timeout, No connect timeout.
262
+ if use_post:
263
+ headers = {}
264
+ if isinstance(params, dict):
265
+ params = urllib.parse.urlencode(
266
+ params, safe="*", quote_via=urllib.parse.quote
267
+ ).encode("utf-8")
268
+ headers["Content-Type"] = (
269
+ "application/x-www-form-urlencoded; charset=utf-8"
270
+ )
271
+ elif isinstance(params, str):
272
+ params = params.encode("utf-8")
273
+ headers["Content-Type"] = "text/xml; charset=utf-8"
274
+
275
+ res = self.session.post(
276
+ url,
277
+ data=params,
278
+ headers=headers,
279
+ timeout=(None, timeout),
280
+ )
281
+ else:
282
+ res = self.session.get(
283
+ url,
284
+ params=params,
285
+ timeout=(
286
+ None,
287
+ timeout,
288
+ ),
289
+ ) # Noqa. Read timeout, No connect timeout.
290
+
210
291
  res.raise_for_status()
211
292
 
212
293
  if len(res.text) == 0:
@@ -228,15 +309,18 @@ class BaseHandlerWeb(ABC):
228
309
  return json.loads(txt)
229
310
 
230
311
  def connect(self):
312
+ if self.datasource == "":
313
+ raise ValueError("Datasource must be specified")
314
+
231
315
  try:
232
316
  self.verify_connection(self.datasource)
233
317
  except requests.ConnectionError:
234
318
  raise ConnectionError(
235
- f"Not able to connect to {self.base_url}. Check network connection."
319
+ f"Not able to connect to api at {self.base_url}. Check network connection."
236
320
  ) from None
237
321
 
238
322
  @abstractmethod
239
- def verify_connection(self, datasource: str): ...
323
+ def verify_connection(self, datasource: str) -> bool: ...
240
324
 
241
325
 
242
326
  class AspenHandlerWeb(BaseHandlerWeb):
@@ -248,7 +332,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
248
332
  verify_ssl: Optional[Union[bool, str]] = True,
249
333
  options: Dict[str, Any] = dict(),
250
334
  ):
251
- if url is None:
335
+ if url is None or url == "":
252
336
  url = get_url_aspen()
253
337
  if auth is None:
254
338
  auth = get_auth_aspen()
@@ -259,7 +343,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
259
343
  auth=auth,
260
344
  verify_ssl=verify_ssl,
261
345
  )
262
- self._max_rows = options.get("max_rows", 100000)
346
+ self.max_rows = int(options.get("max_rows", 100000))
263
347
  self._connection_string = "" # Used for raw SQL queries
264
348
 
265
349
  @staticmethod
@@ -271,6 +355,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
271
355
  ) -> Dict[str, Any]:
272
356
  if not datasource:
273
357
  raise ValueError("Data source is required argument")
358
+
274
359
  # Aspen Web API expects single space instead of consecutive spaces.
275
360
  tag = " ".join(tag.split())
276
361
  params = {"datasource": datasource, "tag": tag, "max": max, "getTrendable": 0}
@@ -338,7 +423,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
338
423
  f"<RT>{rt}</RT>"
339
424
  )
340
425
  if read_type in [ReaderType.RAW, ReaderType.SHAPEPRESERVING]:
341
- query += f"<X>{self._max_rows}</X>"
426
+ query += f"<X>{self.max_rows}</X>"
342
427
  if read_type not in [ReaderType.INT, ReaderType.SNAPSHOT]:
343
428
  query += f"<O>{outsiders}</O>"
344
429
  if read_type not in [ReaderType.RAW]:
@@ -372,7 +457,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
372
457
 
373
458
  return query
374
459
 
375
- def verify_connection(self, datasource: str):
460
+ def verify_connection(self, datasource: str) -> bool:
376
461
  """Connects to the URL and verifies that the provided data source exists.
377
462
 
378
463
  :param datasource: Data source to look for
@@ -429,7 +514,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
429
514
  def _get_maps(self, tagname: str):
430
515
  params = self.generate_get_map_query(tagname)
431
516
  url = urljoin(self.base_url, "TagInfo")
432
- data = self.fetch(url, params=params)
517
+ data = self.fetch(url, params=params, use_post=not tagname.isascii())
433
518
 
434
519
  if "tags" not in data["data"]:
435
520
  return {}
@@ -457,18 +542,12 @@ class AspenHandlerWeb(BaseHandlerWeb):
457
542
  ) -> Union[List[Tuple[str, str]], List[str]]:
458
543
  ret = []
459
544
 
460
- if tag is None and desc is None:
461
- raise ValueError("Tag is a required argument")
462
- # return ret
463
- elif tag is None and len(desc) == 0:
464
- raise ValueError("Tag is a required argument")
465
- # return ret
466
- elif desc is None and len(tag) == 0:
467
- raise ValueError("Tag is a required argument")
468
- # return ret
545
+ # Currently not possible to search only using description.
546
+ if not tag:
547
+ raise ValueError("Input tag is a required argument")
469
548
 
470
- if tag is None:
471
- raise ValueError("Tag is a required argument")
549
+ if not tag and not desc:
550
+ raise ValueError("Either tag or desc must be provided.")
472
551
 
473
552
  tag = tag.replace("%", "*") if isinstance(tag, str) else None
474
553
  # Prepare for regex
@@ -518,7 +597,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
518
597
  def _get_tag_unit(self, tag: str):
519
598
  query = self.generate_get_unit_query(tag)
520
599
  url = urljoin(self.base_url, "TagInfo")
521
- data = self.fetch(url, params=query)
600
+ data = self.fetch(url, params=query, use_post=not tag.isascii())
522
601
  # try:
523
602
  attr_data = data["data"]["tags"][0]["attrData"]
524
603
  # except KeyError as e:
@@ -553,7 +632,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
553
632
  query = self.generate_get_description_query(tag)
554
633
  url = urljoin(self.base_url, "TagInfo")
555
634
  try:
556
- data = self.fetch(url, params=query)
635
+ data = self.fetch(url, params=query, use_post=not tag.isascii())
557
636
  desc = data["data"]["tags"][0]["attrData"][0]["samples"][0]["v"]
558
637
  # except KeyError:
559
638
  # desc = ""
@@ -570,6 +649,7 @@ class AspenHandlerWeb(BaseHandlerWeb):
570
649
  read_type: ReaderType,
571
650
  metadata: Optional[Dict[str, str]],
572
651
  get_status: bool = False,
652
+ use_post: bool = False,
573
653
  ):
574
654
  if read_type not in [
575
655
  ReaderType.INT,
@@ -597,9 +677,13 @@ class AspenHandlerWeb(BaseHandlerWeb):
597
677
  # so we need to limit the range. Note -1 because INT normally includes
598
678
  # both start and end time.
599
679
  if read_type == ReaderType.INT:
600
- end = min(end, start + sample_time * (self._max_rows - 1))
680
+ end = min(end, start + sample_time * (self.max_rows - 1))
601
681
 
602
682
  tag_name, map_name = self.split_tagmap(tag)
683
+ if tag_name is None or len(tag_name) == 0:
684
+ raise ValueError(f"Input tag: {tag} returns no tag_name. Invalid input.")
685
+ if map_name is None:
686
+ map_name = ""
603
687
 
604
688
  params = self.generate_read_query(
605
689
  tagname=tag_name,
@@ -611,26 +695,40 @@ class AspenHandlerWeb(BaseHandlerWeb):
611
695
  metadata={},
612
696
  )
613
697
 
614
- data = self.fetch(url, params=params)
698
+ # Consider use_post = use_post or not tag.isascii()
699
+ data = self.fetch(url, params=params, use_post=use_post)
615
700
 
616
701
  if len(data) == 0: # Normally for timestamps in future
617
702
  return pd.DataFrame(columns=[tag])
618
703
 
619
704
  if "er" in data["data"][0]["samples"][0]:
705
+ if not use_post and data["data"][0]["samples"][0]["er"] == 5:
706
+ return self.read_tag(
707
+ tag=tag,
708
+ start=start,
709
+ end=end,
710
+ sample_time=sample_time,
711
+ read_type=read_type,
712
+ metadata=metadata,
713
+ get_status=get_status,
714
+ use_post=True,
715
+ )
716
+
620
717
  logger.warning(
621
718
  f"API error for {tag}: {data['data'][0]['samples'][0]['es']} params: {params}"
622
719
  )
720
+
623
721
  return pd.DataFrame(columns=[tag])
624
722
  if get_status:
625
723
  # The "l" field maps 1:1 to ODBC status field values 0, 1, 2, 4, 5, 6
626
724
  df = (
627
- pd.DataFrame.from_dict(data["data"][0]["samples"])
725
+ pd.DataFrame(data["data"][0]["samples"])
628
726
  .drop(labels=["s", "V"], axis="columns")
629
727
  .rename(columns={"t": "Timestamp", "v": "Value", "l": "Status"})
630
728
  )
631
729
  else:
632
730
  df = (
633
- pd.DataFrame.from_dict(data["data"][0]["samples"])
731
+ pd.DataFrame(data["data"][0]["samples"])
634
732
  .drop(labels=["l", "s", "V"], axis="columns")
635
733
  .rename(columns={"t": "Timestamp", "v": "Value"})
636
734
  )
@@ -645,12 +743,17 @@ class AspenHandlerWeb(BaseHandlerWeb):
645
743
 
646
744
  @staticmethod
647
745
  def generate_sql_query(
648
- connection_string: Optional[str],
649
- datasource: Optional[str],
650
- query: Optional[str],
746
+ query: str,
747
+ connection_string: Optional[str] = None,
748
+ datasource: Optional[str] = None,
651
749
  max_rows: int = 100000,
652
- ):
653
- if connection_string is not None:
750
+ ) -> str:
751
+ if not query:
752
+ raise ValueError("Query must not be empty")
753
+
754
+ if not connection_string and not datasource:
755
+ raise ValueError("Either connection_string or datasource must be provided")
756
+ if connection_string:
654
757
  connection_string = (
655
758
  f'<SQL c="{connection_string}" m="{max_rows}" to="30" s="1">'
656
759
  )
@@ -660,31 +763,20 @@ class AspenHandlerWeb(BaseHandlerWeb):
660
763
  'dso="CHARINT=N;CHARFLOAT=N;CHARTIME=N;CONVERTERRORS=N" '
661
764
  f'm="{max_rows}" to="30" s="1">'
662
765
  )
663
- query = query.replace("\t", " ").replace(
664
- "\n", " "
665
- ) # Replace new lines and tabs that are typical in formatted SQL queries with spaces.
666
-
766
+ # Replace new lines and tabs that are typical in formatted SQL queries with spaces.
767
+ query = query.replace("\t", " ").replace("\n", " ")
667
768
  # Need a solution to LIKE comments. These have a % symbol in the query that does not seem to pass through the request
668
-
669
769
  connection_string += f"<![CDATA[{query}]]></SQL>"
670
770
  return connection_string
671
771
 
672
772
  def query_sql(self, query: str, parse: bool = True) -> Union[str, pd.DataFrame]:
673
773
  url = urljoin(self.base_url, "SQL")
674
- if self._connection_string is None:
675
- params = self.generate_sql_query(
676
- datasource=self.datasource,
677
- query=query,
678
- max_rows=self._max_rows,
679
- connection_string=None,
680
- )
681
- else:
682
- params = self.generate_sql_query(
683
- connection_string=self._connection_string,
684
- query=query,
685
- max_rows=self._max_rows,
686
- datasource=None,
687
- )
774
+ params = self.generate_sql_query(
775
+ query=query,
776
+ connection_string=self._connection_string,
777
+ datasource=self.datasource,
778
+ max_rows=self.max_rows,
779
+ )
688
780
 
689
781
  res = self.session.get(url, params=params)
690
782
  res.raise_for_status()
@@ -711,15 +803,14 @@ class AspenHandlerWeb(BaseHandlerWeb):
711
803
  class PIHandlerWeb(BaseHandlerWeb):
712
804
  def __init__(
713
805
  self,
714
- url: Optional[str],
715
806
  datasource: Optional[str],
807
+ url: Optional[str],
716
808
  auth: Optional[Any],
717
809
  verify_ssl: Optional[Union[bool, str]],
718
810
  options: Dict[str, Union[int, float, str]],
719
811
  cache: Optional[Union[SmartCache, BucketCache]],
720
812
  ):
721
- self._max_rows = options.get("max_rows", 10000)
722
- if url is None:
813
+ if url is None or url == "":
723
814
  url = get_url_pi()
724
815
  if auth is None:
725
816
  auth = get_auth_pi()
@@ -729,7 +820,7 @@ class PIHandlerWeb(BaseHandlerWeb):
729
820
  auth=auth,
730
821
  verify_ssl=verify_ssl,
731
822
  )
732
- self._max_rows = options.get("max_rows", 10000)
823
+ self.max_rows = int(options.get("max_rows", 10000))
733
824
  self.web_id_cache = cache
734
825
 
735
826
  @staticmethod
@@ -779,7 +870,7 @@ class PIHandlerWeb(BaseHandlerWeb):
779
870
  query = " AND ".join(q)
780
871
  params = {"query": f"{query}"}
781
872
 
782
- if datasource is not None:
873
+ if isinstance(datasource, str) and len(datasource) > 0:
783
874
  params["dataserverwebid"] = (
784
875
  f"{get_piwebapi_source_to_webid_dict(auth=auth)[datasource]}"
785
876
  )
@@ -806,6 +897,9 @@ class PIHandlerWeb(BaseHandlerWeb):
806
897
  ]:
807
898
  raise NotImplementedError
808
899
 
900
+ if not tag:
901
+ raise ValueError("Tag must be provided.")
902
+
809
903
  web_id = tag
810
904
 
811
905
  seconds = 0
@@ -818,7 +912,6 @@ class PIHandlerWeb(BaseHandlerWeb):
818
912
  ReaderType.SNAPSHOT: "value",
819
913
  ReaderType.SHAPEPRESERVING: "plot",
820
914
  }.get(read_type, "summary")
821
-
822
915
  url = f"streams/{web_id}/{get_action}"
823
916
  params = {}
824
917
 
@@ -865,7 +958,7 @@ class PIHandlerWeb(BaseHandlerWeb):
865
958
  params["selectedFields"] += ";Good;Questionable;Substituted"
866
959
 
867
960
  if read_type == ReaderType.RAW:
868
- params["maxCount"] = self._max_rows
961
+ params["maxCount"] = self.max_rows
869
962
 
870
963
  return url, params
871
964
 
@@ -1012,7 +1105,7 @@ class PIHandlerWeb(BaseHandlerWeb):
1012
1105
  data = self.fetch(url, params=params)
1013
1106
 
1014
1107
  if read_type == ReaderType.SNAPSHOT:
1015
- df = pd.DataFrame.from_dict([data]) # noqa
1108
+ df = pd.DataFrame([data])
1016
1109
  else:
1017
1110
  # Summary (aggregated) data and DigitalSets return Value as dict
1018
1111
  df = pd.json_normalize(data=data, record_path="Items")
tagreader-6.2.1/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2020 Equinor
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.