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.
- {tagreader-6.2.1 → tagreader-6.3.1}/PKG-INFO +67 -71
- {tagreader-6.2.1 → tagreader-6.3.1}/README.md +1 -5
- tagreader-6.3.1/pyproject.toml +69 -0
- tagreader-6.2.1/pyproject.toml → tagreader-6.3.1/pyproject.toml.orig +26 -22
- {tagreader-6.2.1 → tagreader-6.3.1/src}/tagreader/__version__.py +1 -1
- {tagreader-6.2.1 → tagreader-6.3.1/src}/tagreader/clients.py +4 -4
- {tagreader-6.2.1 → tagreader-6.3.1/src}/tagreader/web_handlers.py +172 -79
- tagreader-6.2.1/LICENSE +0 -21
- {tagreader-6.2.1 → tagreader-6.3.1/src}/tagreader/__init__.py +0 -0
- {tagreader-6.2.1 → tagreader-6.3.1/src}/tagreader/cache.py +0 -0
- {tagreader-6.2.1 → tagreader-6.3.1/src}/tagreader/logger.py +0 -0
- {tagreader-6.2.1 → tagreader-6.3.1/src}/tagreader/utils.py +0 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tagreader
|
|
3
|
-
Version: 6.
|
|
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.
|
|
9
|
-
Author-email: eiids@equinor.com
|
|
10
|
-
|
|
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:
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
Requires-Dist:
|
|
33
|
-
Requires-Dist: requests-
|
|
34
|
-
Requires-Dist:
|
|
35
|
-
Requires-Dist: urllib3
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
[](https://github.com/equinor/tagreader-python/actions/workflows/build.yml)
|
|
44
|
+

|
|
45
|
+

|
|
46
|
+
[](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
|
-
](https://github.com/equinor/tagreader-python/actions/workflows/build.yml)
|
|
8
4
|

|
|
9
5
|

|
|
10
6
|
[](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.
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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>=
|
|
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.
|
|
41
|
+
"msal-bearer>=1.6.0",
|
|
36
42
|
"diskcache>=5.6.1",
|
|
37
|
-
"pycryptodome>=
|
|
43
|
+
"pycryptodome>=4.1.0",
|
|
38
44
|
"requests-ntlm>=1.1,<=2.0",
|
|
39
|
-
"platformdirs>=4.
|
|
40
|
-
"urllib3>=2.5.0"
|
|
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 = ["
|
|
57
|
-
build-backend = "
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
90
|
-
url:
|
|
91
|
-
auth:
|
|
92
|
-
verify_ssl: Optional[Union[bool, str]]
|
|
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
|
-
|
|
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 =
|
|
124
|
+
url = get_url_aspen()
|
|
127
125
|
|
|
128
|
-
if auth is None:
|
|
129
|
-
auth
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
params
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
471
|
-
raise ValueError("
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
746
|
+
query: str,
|
|
747
|
+
connection_string: Optional[str] = None,
|
|
748
|
+
datasource: Optional[str] = None,
|
|
651
749
|
max_rows: int = 100000,
|
|
652
|
-
):
|
|
653
|
-
if
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|