tagmapper 0.2.2__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.
- tagmapper-0.2.2/PKG-INFO +45 -0
- tagmapper-0.2.2/README.md +25 -0
- tagmapper-0.2.2/pyproject.toml +25 -0
- tagmapper-0.2.2/tagmapper/__init__.py +5 -0
- tagmapper-0.2.2/tagmapper/attribute.py +29 -0
- tagmapper-0.2.2/tagmapper/connector.py +205 -0
- tagmapper-0.2.2/tagmapper/mapped_object.py +7 -0
- tagmapper-0.2.2/tagmapper/separator.py +73 -0
- tagmapper-0.2.2/tagmapper/well.py +67 -0
tagmapper-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tagmapper
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Python wrapper for sql tag mapping database
|
|
5
|
+
Author: Åsmund Våge Fannemel
|
|
6
|
+
Author-email: 34712686+asmfstatoil@users.noreply.github.com
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: azure-identity (>=1.17.1,<2.0.0)
|
|
14
|
+
Requires-Dist: msal-bearer (>0.2.1,<2.0.0)
|
|
15
|
+
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
16
|
+
Requires-Dist: pyodbc (>=5.1.0,<6.0.0)
|
|
17
|
+
Requires-Dist: sqlalchemy (>=2.0.28,<3.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# tagmapper-sdk
|
|
21
|
+
Prototype python package to get IMS-tag mappings for data models for separators and wells.
|
|
22
|
+
|
|
23
|
+
Authentication is done using Azure credentials and bearer tokens.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Use
|
|
27
|
+
See (examples/demo_separator.py)[demo]. Or try the following simple code.
|
|
28
|
+
```
|
|
29
|
+
from tagmapper import Well
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
w = Well("NO 30/6-E-2")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Installing
|
|
37
|
+
Install from github using pip.
|
|
38
|
+
``
|
|
39
|
+
pip install git+https://github.com/equinor/tagmapper-sdk.git
|
|
40
|
+
``
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Developing
|
|
44
|
+
Clone repo and run ``poetry install``. Tests are run using ``poetry run pytest``.
|
|
45
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# tagmapper-sdk
|
|
2
|
+
Prototype python package to get IMS-tag mappings for data models for separators and wells.
|
|
3
|
+
|
|
4
|
+
Authentication is done using Azure credentials and bearer tokens.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
See (examples/demo_separator.py)[demo]. Or try the following simple code.
|
|
9
|
+
```
|
|
10
|
+
from tagmapper import Well
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
w = Well("NO 30/6-E-2")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Installing
|
|
18
|
+
Install from github using pip.
|
|
19
|
+
``
|
|
20
|
+
pip install git+https://github.com/equinor/tagmapper-sdk.git
|
|
21
|
+
``
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Developing
|
|
25
|
+
Clone repo and run ``poetry install``. Tests are run using ``poetry run pytest``.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "tagmapper"
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "Python wrapper for sql tag mapping database"
|
|
5
|
+
authors = ["Åsmund Våge Fannemel <34712686+asmfstatoil@users.noreply.github.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.9,<4.0"
|
|
10
|
+
msal-bearer = ">0.2.1,<2.0.0"
|
|
11
|
+
# urllib3 = [
|
|
12
|
+
# { version = "<2.0.0", markers = "sys_platform=='linux'" }, # FIX for use with RHEL7 environments
|
|
13
|
+
# ]
|
|
14
|
+
pyodbc = "^5.1.0"
|
|
15
|
+
pandas = "^2.2.2"
|
|
16
|
+
sqlalchemy = "^2.0.28"
|
|
17
|
+
azure-identity = "^1.17.1"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
|
+
pytest = "^7.0.0"
|
|
21
|
+
black = "^24.1.1"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["poetry-core"]
|
|
25
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class Attribute:
|
|
2
|
+
"""
|
|
3
|
+
Attribute class
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, data):
|
|
7
|
+
if not isinstance(data, dict):
|
|
8
|
+
raise ValueError("Input data must be a dict")
|
|
9
|
+
|
|
10
|
+
self.name = ""
|
|
11
|
+
if "attribute_name" in data.keys():
|
|
12
|
+
self.name = data["attribute_name"]
|
|
13
|
+
elif "Attribute_Name" in data.keys():
|
|
14
|
+
self.name = data["Attribute_Name"]
|
|
15
|
+
|
|
16
|
+
self.tag = ""
|
|
17
|
+
if "TAG_ID" in data.keys():
|
|
18
|
+
self.tag = data["TAG_ID"]
|
|
19
|
+
elif "Tag_Id" in data.keys():
|
|
20
|
+
self.tag = data["Tag_Id"]
|
|
21
|
+
|
|
22
|
+
self.source = ""
|
|
23
|
+
if "source" in data.keys():
|
|
24
|
+
self.source = data["source"]
|
|
25
|
+
elif "Attribute_Source_Name" in data.keys():
|
|
26
|
+
self.source = data["Attribute_Source_Name"]
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
return f"{self.name} : {self.tag}"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from azure.identity import DefaultAzureCredential
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from sqlalchemy import URL, Connection, Engine, create_engine
|
|
6
|
+
from sqlalchemy import text as sql_text
|
|
7
|
+
|
|
8
|
+
from msal_bearer.BearerAuth import BearerAuth, get_login_name
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import pyodbc
|
|
12
|
+
|
|
13
|
+
_engine = None
|
|
14
|
+
_token = ""
|
|
15
|
+
_conn_string = ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_token(token: str) -> None:
|
|
19
|
+
"""Setter for global property token.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
token (str): Token to set.
|
|
23
|
+
"""
|
|
24
|
+
global _token
|
|
25
|
+
_token = token
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_token() -> str:
|
|
29
|
+
"""Getter for token. Will first see if a global token has been set, then try to get a token using app registration, then last try to get via azure authentication.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
str: _description_
|
|
33
|
+
"""
|
|
34
|
+
if _token:
|
|
35
|
+
return _token
|
|
36
|
+
|
|
37
|
+
az_token = get_app_token()
|
|
38
|
+
if az_token:
|
|
39
|
+
return az_token
|
|
40
|
+
|
|
41
|
+
return get_az_token()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def reset_engine() -> None:
|
|
45
|
+
"""Reset cached Engine"""
|
|
46
|
+
global _engine
|
|
47
|
+
|
|
48
|
+
if _engine is not None:
|
|
49
|
+
_engine.dispose()
|
|
50
|
+
_engine = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_engine(conn_string="", token="", reset=False) -> Engine:
|
|
54
|
+
"""Getter of cached Engine. Will create one if not existing.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
conn_string (str, optional): Connection string for odbc connection. Defaults to "" to support just getting cached engine.
|
|
58
|
+
token (str, optional): Token string. Defaults to "" to support just getting cached engine.
|
|
59
|
+
reset (bool, optional): Set true to reset engine, i.e., not get cached engine. Defaults to False.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def get_token_struct(token: str) -> bytes:
|
|
63
|
+
"""Convert token string to token byte struct for use in connection string
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
token (str): Token as string
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
(bytes): Token as bytes
|
|
70
|
+
"""
|
|
71
|
+
tokenb = bytes(token, "UTF-8")
|
|
72
|
+
exptoken = b""
|
|
73
|
+
for i in tokenb:
|
|
74
|
+
exptoken += bytes({i})
|
|
75
|
+
exptoken += bytes(1)
|
|
76
|
+
|
|
77
|
+
tokenstruct = struct.pack("=i", len(exptoken)) + exptoken
|
|
78
|
+
|
|
79
|
+
return tokenstruct
|
|
80
|
+
|
|
81
|
+
global _engine
|
|
82
|
+
|
|
83
|
+
if not conn_string == _conn_string:
|
|
84
|
+
reset = True
|
|
85
|
+
|
|
86
|
+
if token == "":
|
|
87
|
+
token = get_token()
|
|
88
|
+
|
|
89
|
+
if reset:
|
|
90
|
+
reset_engine()
|
|
91
|
+
|
|
92
|
+
if _engine is None and isinstance(conn_string, str) and len(conn_string) > 0:
|
|
93
|
+
SQL_COPT_SS_ACCESS_TOKEN = 1256
|
|
94
|
+
_engine = create_engine(
|
|
95
|
+
URL.create("mssql+pyodbc", query={"odbc_connect": conn_string}),
|
|
96
|
+
connect_args={
|
|
97
|
+
"attrs_before": {SQL_COPT_SS_ACCESS_TOKEN: get_token_struct(token)}
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return _engine
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_sql_driver() -> str:
|
|
105
|
+
"""Get name of ODBC SQL driver
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: Raised if required ODBC driver is not installed.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
str: ODBC driver name
|
|
112
|
+
"""
|
|
113
|
+
drivers = pyodbc.drivers()
|
|
114
|
+
|
|
115
|
+
for driver in drivers:
|
|
116
|
+
if "18" in driver and "SQL Server" in driver:
|
|
117
|
+
return driver
|
|
118
|
+
|
|
119
|
+
for driver in drivers:
|
|
120
|
+
if "17" in driver and "SQL Server" in driver:
|
|
121
|
+
return driver
|
|
122
|
+
|
|
123
|
+
raise ValueError("ODBC driver 17 or 18 for SQL server is required.")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_connection_string(
|
|
127
|
+
server: str, database: str, driver: str = get_sql_driver()
|
|
128
|
+
) -> str:
|
|
129
|
+
"""Build database connection string
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
server (str): Server url
|
|
133
|
+
database (str): Database name
|
|
134
|
+
driver (str): ODBC driver name. Defaults to get_sql_driver().
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
str: Database connection string
|
|
138
|
+
"""
|
|
139
|
+
return f"DRIVER={driver};SERVER={server};DATABASE={database};"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_az_token() -> str:
|
|
143
|
+
"""Getter for token uzing azure authentication.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
str: Token from azure authentication
|
|
147
|
+
"""
|
|
148
|
+
credential = DefaultAzureCredential()
|
|
149
|
+
databaseToken = credential.get_token("https://database.windows.net/")
|
|
150
|
+
return databaseToken[0]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_app_token() -> str:
|
|
154
|
+
"""Getter for token using app registration authentication.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
str: Token from app registration
|
|
158
|
+
"""
|
|
159
|
+
# SHORTNAME@equinor.com -- short name shall be capitalized
|
|
160
|
+
username = get_login_name().upper() + "@equinor.com"
|
|
161
|
+
tenantID = "3aa4a235-b6e2-48d5-9195-7fcf05b459b0"
|
|
162
|
+
clientID = "5850cfaf-0427-4e96-9813-a7874c8324ae"
|
|
163
|
+
scope = ["https://database.windows.net/.default"]
|
|
164
|
+
auth = BearerAuth.get_auth(
|
|
165
|
+
tenantID=tenantID, clientID=clientID, scopes=scope, username=username
|
|
166
|
+
)
|
|
167
|
+
return auth.token
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_connection(database: str = "Lh_Gold", token: str = get_token()) -> Connection:
|
|
171
|
+
"""Get Connection object to database.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
database (str, optional): Name of database. Defaults to "Lh_Gold".
|
|
175
|
+
token (str, optional): Token string. Defaults to get_token().
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Connection: Connection object to database
|
|
179
|
+
"""
|
|
180
|
+
server = "gwrkioxcw3kuremvp7hqlnczwa-bjb35lhdq4oubeecgujfxwyxcu.datawarehouse.fabric.microsoft.com"
|
|
181
|
+
|
|
182
|
+
return get_engine(
|
|
183
|
+
get_connection_string(server=server, database=database), token
|
|
184
|
+
).connect()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def query(
|
|
188
|
+
sql: str,
|
|
189
|
+
connection: Optional[Connection] = None,
|
|
190
|
+
params: Optional[dict] = None,
|
|
191
|
+
) -> pd.DataFrame:
|
|
192
|
+
"""Query SQL database using pd.read_sql
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
sql (str): SQL query for database
|
|
196
|
+
connection (Optional[Connection], optional): Database Connection object. Defaults to None, which resolves to get_connection().
|
|
197
|
+
params (Optional[dict], optional): SQL parameters. Defaults to None.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
pd.DataFrame: Result from pd.read_sql
|
|
201
|
+
"""
|
|
202
|
+
if connection is None:
|
|
203
|
+
connection = get_connection()
|
|
204
|
+
|
|
205
|
+
return pd.read_sql(sql_text(sql), connection, params=params)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from tagmapper.attribute import Attribute
|
|
5
|
+
from tagmapper.connector import query
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Separator:
|
|
9
|
+
"""
|
|
10
|
+
Separator class
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
_sep_attributes = pd.DataFrame()
|
|
14
|
+
|
|
15
|
+
def __init__(self, usi):
|
|
16
|
+
if isinstance(usi, str):
|
|
17
|
+
# assume data is USI
|
|
18
|
+
data = Separator.get_sep_attributes(usi)
|
|
19
|
+
elif isinstance(usi, pd.DataFrame):
|
|
20
|
+
data = usi
|
|
21
|
+
|
|
22
|
+
if not isinstance(data, pd.DataFrame):
|
|
23
|
+
raise ValueError("Input data must be a dataframe")
|
|
24
|
+
|
|
25
|
+
if data.empty:
|
|
26
|
+
raise ValueError("Input data can not be empty")
|
|
27
|
+
|
|
28
|
+
self.inst_code = data["STID_CODE"].iloc[0]
|
|
29
|
+
self.object_name = data["OBJECT_NAME"].iloc[0]
|
|
30
|
+
self.object_code = data["PDM.OBJECT_CODE"].iloc[0]
|
|
31
|
+
self.usi = data["unique_separator_identifier"].iloc[0]
|
|
32
|
+
|
|
33
|
+
self.attributes = []
|
|
34
|
+
for _, r in data.iterrows():
|
|
35
|
+
self.attributes.append(Attribute(r.to_dict()))
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_all_separators(cls) -> List["Separator"]:
|
|
39
|
+
usi = Separator.get_separator_names()
|
|
40
|
+
sep = []
|
|
41
|
+
|
|
42
|
+
for u in usi:
|
|
43
|
+
sep.append(Separator(Separator.get_sep_attributes(u)))
|
|
44
|
+
|
|
45
|
+
return sep
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_separator(cls, inst_code: str, tag_no: str) -> "Separator":
|
|
49
|
+
return Separator(Separator.get_sep_attributes(f"{inst_code}-{tag_no}"))
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_sep_attributes(cls, usi: str = "") -> pd.DataFrame:
|
|
53
|
+
if cls._sep_attributes.empty:
|
|
54
|
+
cls._sep_attributes = query(
|
|
55
|
+
"select * from [dbo].[separator_attribute_mapping]"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if usi:
|
|
59
|
+
ind = cls._sep_attributes["unique_separator_identifier"] == usi
|
|
60
|
+
return cls._sep_attributes.loc[ind, :]
|
|
61
|
+
else:
|
|
62
|
+
return cls._sep_attributes
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_separator_names() -> List[str]:
|
|
66
|
+
d = Separator.get_sep_attributes()
|
|
67
|
+
usi = list(d["unique_separator_identifier"].unique())
|
|
68
|
+
usi.sort()
|
|
69
|
+
return usi
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def get_usi() -> List[str]:
|
|
73
|
+
return Separator.get_separator_names()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from tagmapper.attribute import Attribute
|
|
5
|
+
from tagmapper.connector import query
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Well:
|
|
9
|
+
"""
|
|
10
|
+
Well class
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
_well_attributes = pd.DataFrame()
|
|
14
|
+
|
|
15
|
+
def __init__(self, uwi):
|
|
16
|
+
if isinstance(uwi, str):
|
|
17
|
+
# assume data is UWI
|
|
18
|
+
data = Well.get_well_attributes(uwi)
|
|
19
|
+
elif isinstance(uwi, pd.DataFrame):
|
|
20
|
+
data = uwi
|
|
21
|
+
|
|
22
|
+
if not isinstance(data, pd.DataFrame):
|
|
23
|
+
raise ValueError("Input data must be a dataframe")
|
|
24
|
+
|
|
25
|
+
if data.empty:
|
|
26
|
+
raise ValueError("Input data can not be empty")
|
|
27
|
+
|
|
28
|
+
# self.inst_code = data["STID_CODE"].iloc[0]
|
|
29
|
+
# self.object_name = data["OBJECT_NAME"].iloc[0]
|
|
30
|
+
# self.object_code = data["PDM.OBJECT_CODE"].iloc[0]
|
|
31
|
+
self.uwi = data["Pdm_Well_UWI"].iloc[0]
|
|
32
|
+
|
|
33
|
+
self.attributes = []
|
|
34
|
+
for _, r in data.iterrows():
|
|
35
|
+
self.attributes.append(Attribute(r.to_dict()))
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_all_wells(cls):
|
|
39
|
+
uwi = Well.get_uwis()
|
|
40
|
+
well = []
|
|
41
|
+
|
|
42
|
+
for u in uwi:
|
|
43
|
+
well.append(Well(Well.get_well_attributes(u)))
|
|
44
|
+
|
|
45
|
+
return well
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_well(cls, inst_code: str, tag_no: str):
|
|
49
|
+
return Well(Well.get_well_attributes(f"{inst_code}-{tag_no}"))
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_well_attributes(cls, uwi: str = ""):
|
|
53
|
+
if cls._well_attributes.empty:
|
|
54
|
+
cls._well_attributes = query("select * from [dbo].[mapped_well_attributes]")
|
|
55
|
+
|
|
56
|
+
if uwi:
|
|
57
|
+
ind = cls._well_attributes["Pdm_Well_UWI"] == uwi
|
|
58
|
+
return cls._well_attributes.loc[ind, :]
|
|
59
|
+
else:
|
|
60
|
+
return cls._well_attributes
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def get_uwis() -> List[str]:
|
|
64
|
+
d = Well.get_well_attributes()
|
|
65
|
+
uwi = list(d["Pdm_Well_UWI"].unique())
|
|
66
|
+
uwi.sort()
|
|
67
|
+
return uwi
|