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.
@@ -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,5 @@
1
+ from .attribute import Attribute
2
+ from .separator import Separator
3
+ from .well import Well
4
+
5
+ __all__ = ["Attribute", "Separator", "Well"]
@@ -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,7 @@
1
+ class Mapped_Object:
2
+ def __str__(self):
3
+ s = f"{self.inst_code} - {self.object_name}"
4
+ for att in self.attributes:
5
+ s = f"{s}\n{str(att)}"
6
+
7
+ return s
@@ -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