mbu-dev-shared-components 2.4.8__tar.gz → 3.0.0__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.
Files changed (64) hide show
  1. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/PKG-INFO +1 -1
  2. mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/database/__init__.py +4 -0
  3. mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/database/connection.py +49 -0
  4. mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/database/constants.py +54 -0
  5. mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/database/logging.py +139 -0
  6. mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/database/utility.py +109 -0
  7. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/document.py +0 -8
  8. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components.egg-info/PKG-INFO +1 -1
  9. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components.egg-info/SOURCES.txt +3 -1
  10. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/pyproject.toml +1 -5
  11. mbu_dev_shared_components-3.0.0/tests/test_database.py +319 -0
  12. mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/database/constants.py +0 -131
  13. mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/database/logging.py +0 -86
  14. mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/database/utility.py +0 -44
  15. mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/utils/__init__.py +0 -0
  16. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/LICENSE +0 -0
  17. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/README.md +0 -0
  18. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/database → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/getorganized}/__init__.py +0 -0
  19. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/getorganized/auth.py +0 -0
  20. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/getorganized/cases.py +0 -0
  21. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/getorganized/contacts.py +0 -0
  22. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/getorganized/documents.py +0 -0
  23. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/getorganized/objects.py +0 -0
  24. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/getorganized → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/google}/__init__.py +0 -0
  25. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/google → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/google/api}/__init__.py +0 -0
  26. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/google/api/auth.py +0 -0
  27. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/google/api → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/google/workspace}/__init__.py +0 -0
  28. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/google/workspace/alerts.py +0 -0
  29. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/google/workspace → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/msoffice365}/__init__.py +0 -0
  30. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/msoffice365 → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/msoffice365/excel}/__init__.py +0 -0
  31. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/msoffice365/excel/excel_reader.py +0 -0
  32. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/msoffice365/excel → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/msoffice365/sharepoint_api}/__init__.py +0 -0
  33. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/msoffice365/sharepoint_api/files.py +0 -0
  34. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/msoffice365/sharepoint_api → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/os2forms}/__init__.py +0 -0
  35. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/os2forms/documents.py +0 -0
  36. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/os2forms/forms.py +0 -0
  37. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/os2forms → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/romexis}/__init__.py +0 -0
  38. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/romexis/db_handler.py +0 -0
  39. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/romexis/helper_functions.py +0 -0
  40. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/romexis → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/sap}/__init__.py +0 -0
  41. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/sap/create_invoice.py +0 -0
  42. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/__init__.py +0 -0
  43. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/__init__.py +0 -0
  44. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/app_handler.py +0 -0
  45. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/appointment.py +0 -0
  46. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/base_ui.py +0 -0
  47. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/clinic.py +0 -0
  48. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/edi_portal.py +0 -0
  49. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/event.py +0 -0
  50. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/exceptions.py +0 -0
  51. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/handler_base.py +0 -0
  52. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/journal_note.py +0 -0
  53. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/application/patient.py +0 -0
  54. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/database/__init__.py +0 -0
  55. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/solteqtand/database/db_handler.py +0 -0
  56. {mbu_dev_shared_components-2.4.8/mbu_dev_shared_components/sap → mbu_dev_shared_components-3.0.0/mbu_dev_shared_components/utils}/__init__.py +0 -0
  57. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/utils/db_stored_procedure_executor.py +0 -0
  58. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/utils/fernet_encryptor.py +0 -0
  59. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/utils/file_handler.py +0 -0
  60. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components/utils/json_handler.py +0 -0
  61. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components.egg-info/dependency_links.txt +0 -0
  62. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components.egg-info/requires.txt +0 -0
  63. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/mbu_dev_shared_components.egg-info/top_level.txt +0 -0
  64. {mbu_dev_shared_components-2.4.8 → mbu_dev_shared_components-3.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbu_dev_shared_components
3
- Version: 2.4.8
3
+ Version: 3.0.0
4
4
  Summary: Shared components to use in RPA projects
5
5
  Author-email: MBU <rpa@mbu.aarhus.dk>
6
6
  License-Expression: MIT
@@ -0,0 +1,4 @@
1
+ # mbu_dev_shared_components/database/__init__.py
2
+ from .connection import RPAConnection
3
+
4
+ __all__ = ["RPAConnection"]
@@ -0,0 +1,49 @@
1
+ """Handles the RPA connection"""
2
+
3
+ from .constants import Constants
4
+ from .utility import Utility
5
+ from .logging import Log
6
+
7
+
8
+ class RPAConnection(
9
+ Constants,
10
+ Utility,
11
+ Log,
12
+ ):
13
+ """Class for running database related """
14
+ def __init__(self, db_env: str = "PROD", commit: bool | str = False):
15
+ Constants.__init__(self)
16
+ Utility.__init__(self)
17
+ Log.__init__(self)
18
+ self.db_env = db_env
19
+ self.commit = commit if isinstance(commit, bool) else commit == "True"
20
+ self.conn = None
21
+ self.cursor = None
22
+
23
+ def __enter__(self):
24
+ self.conn = self.connect_to_db(autocommit=False, db_env=self.db_env)
25
+ self.cursor = self.conn.cursor()
26
+
27
+ return self
28
+
29
+ def __exit__(self, exc_type, exc_val, exc_tb):
30
+ if self.commit:
31
+ print("Commiting transaction...")
32
+ self.conn.commit()
33
+ else:
34
+ print("Rolling back transaction....")
35
+ self.conn.rollback()
36
+ print("Closing conection...")
37
+ self.close()
38
+ print("Connection closed.")
39
+
40
+ def rollback(self):
41
+ """Rollback transaction on connection if autocommit is not enabled"""
42
+ if self.autocommit:
43
+ raise RuntimeError("Cannot rollback: autocommit is enabled.")
44
+ self.conn.rollback()
45
+
46
+ def close(self):
47
+ """Closes connection"""
48
+ self.cursor.close()
49
+ self.conn.close()
@@ -0,0 +1,54 @@
1
+ """This module handles generating and fetching constants and credentials from the database"""
2
+
3
+ from datetime import datetime
4
+
5
+ from mbu_dev_shared_components.utils.fernet_encryptor import Encryptor
6
+
7
+
8
+ class Constants:
9
+ """Base class for adding and collection constants and credentials"""
10
+
11
+ def add_constant(self, constant_name: str, value: str, changed_at: datetime = datetime.now()):
12
+ query = """
13
+ INSERT INTO [RPA].[rpa].[Constants] ([name], [value], [changed_at])
14
+ VALUES (?, ?, ?)
15
+ """
16
+ self.execute_query(query, [constant_name, value, changed_at])
17
+
18
+ def get_constant(self, constant_name: str) -> dict:
19
+ query = """
20
+ SELECT name, value FROM [RPA].[rpa].[Constants] WHERE name = ?
21
+ """
22
+ res = self.execute_query(query, [constant_name])
23
+ if res:
24
+ name, value = res[0]
25
+ return {"constant_name": name, "value": value}
26
+ raise ValueError(f"No constant found with name: {constant_name}")
27
+
28
+ def add_credential(self, credential_name: str, username: str, password: str,
29
+ changed_at: datetime = datetime.now()):
30
+ encryptor = Encryptor()
31
+ encrypted_password = encryptor.encrypt(password)
32
+ query = """
33
+ INSERT INTO [RPA].[rpa].[Credentials] ([name], [username], [password], [changed_at])
34
+ VALUES (?, ?, ?, ?)
35
+ """
36
+ self.execute_query(query, [credential_name, username, encrypted_password, changed_at])
37
+
38
+ def get_credential(self, credential_name: str) -> dict:
39
+ encryptor = Encryptor()
40
+ query = """
41
+ SELECT username, CAST(password AS varbinary(max))
42
+ FROM [RPA].[rpa].[Credentials]
43
+ WHERE name = ?
44
+ """
45
+ res = self.execute_query(query, [credential_name])
46
+ if res:
47
+ username, encrypted_password = res[0]
48
+ decrypted_password = encryptor.decrypt(encrypted_password)
49
+ return {
50
+ "username": username,
51
+ "decrypted_password": decrypted_password,
52
+ "encrypted_password": encrypted_password
53
+ }
54
+ raise ValueError(f"No credential found with name {credential_name}")
@@ -0,0 +1,139 @@
1
+ """This module handles logging in the RPA database"""
2
+
3
+ from datetime import datetime
4
+ import time
5
+ import socket
6
+
7
+
8
+ class Log:
9
+ """Base class for handling logging"""
10
+ def log_event(
11
+ self,
12
+ log_db: str,
13
+ level: str,
14
+ message: str,
15
+ context: str,
16
+ ):
17
+ """Logs the inputted parameters in """
18
+ created_at = datetime.now()
19
+ query = f"""
20
+ INSERT INTO RPA.{log_db}
21
+ ([level]
22
+ ,[message]
23
+ ,[created_at]
24
+ ,[context])
25
+ VALUES
26
+ (?
27
+ ,?
28
+ ,?
29
+ ,?)
30
+ """
31
+
32
+ params = [level, message, created_at, context]
33
+ self.execute_query(query=query, params=params)
34
+
35
+ def _get_log_event(
36
+ self,
37
+ log_db: str,
38
+ level: str,
39
+ message: str,
40
+ context: str,
41
+ ):
42
+ query = f"""
43
+ SELECT
44
+ ([level]
45
+ ,[message]
46
+ ,[created_at]
47
+ ,[context])
48
+ FROM
49
+ RPA.{log_db}
50
+ WHERE
51
+ level={level},
52
+ message={message},
53
+ context={context}
54
+ """
55
+
56
+ res = self.execute_query(query=query)
57
+ return res
58
+
59
+ def get_latest_log(
60
+ self,
61
+ log_db: str,
62
+ ):
63
+ """Retrieve latest log message from database"""
64
+ query = f"""
65
+ SELECT TOP (1)
66
+ [level]
67
+ ,[message]
68
+ ,[created_at]
69
+ ,[context]
70
+ FROM
71
+ RPA.{log_db}
72
+ ORDER BY
73
+ created_at desc
74
+ """
75
+
76
+ res = self.execute_query(query=query)
77
+ return res
78
+
79
+ def _send_heartbeat(
80
+ self,
81
+ servicename,
82
+ status,
83
+ details
84
+ ):
85
+ """Function to send heartbeat to database"""
86
+ hostname = socket.gethostname()
87
+ params = {
88
+ "ServiceName": (str, servicename),
89
+ "Status": (str, status),
90
+ "HostName": (str, hostname),
91
+ "Details": (str, details)
92
+ }
93
+ result = self.execute_stored_procedure(
94
+ stored_procedure='rpa.sp_UpdateHeartbeat',
95
+ params=params)
96
+ if result["success"] is not True:
97
+ print(result["error_message"])
98
+
99
+ def log_heartbeat(
100
+ self,
101
+ stop: str | bool,
102
+ servicename: str,
103
+ heartbeat_interval: int,
104
+ details: str = "",
105
+ ):
106
+ """Function to log heartbeat"""
107
+ # Update connection such that it autocommits
108
+ self.conn.autocommit = True # Sets class attribute
109
+ if isinstance(stop, str):
110
+ stop = stop == "True"
111
+ if not isinstance(heartbeat_interval, int):
112
+ heartbeat_interval = int(heartbeat_interval)
113
+ while not stop:
114
+ status = "RUNNING"
115
+ self._send_heartbeat(
116
+ servicename,
117
+ status,
118
+ details,
119
+ )
120
+ time.sleep(heartbeat_interval)
121
+ status = "STOPPED"
122
+ self._send_heartbeat(
123
+ servicename,
124
+ status,
125
+ details,
126
+ )
127
+
128
+ def get_heartbeat(self, service_name: str):
129
+ """Get hearbeats """
130
+ query = f"""
131
+ SELECT
132
+ *
133
+ FROM
134
+ [RPA].[rpa].[ServiceHeartbeat]
135
+ WHERE
136
+ ServiceName = '{service_name}'
137
+ """
138
+ res = self.execute_query(query)
139
+ return res
@@ -0,0 +1,109 @@
1
+ """This module handles general database connection and calls"""
2
+
3
+ import os
4
+ import json
5
+ from typing import Dict, Union, Tuple, Any
6
+ from dateutil import parser
7
+ import pyodbc
8
+
9
+
10
+ class Utility:
11
+ """Base class handling general utilities"""
12
+ def connect_to_db(self, autocommit=True, db_env="PROD") -> pyodbc.Connection:
13
+ """Establish connection to sql database
14
+
15
+ Returns:
16
+ rpa_conn (pyodbc.Connection): The connection object to the SQL database.
17
+ """
18
+ connection_env = self.fetch_env(db_env)
19
+ rpa_conn_string = os.getenv(connection_env)
20
+ rpa_conn = pyodbc.connect(rpa_conn_string, autocommit=autocommit)
21
+ return rpa_conn
22
+
23
+ def execute_query(self, query: str, params: list = None) -> pyodbc.Cursor:
24
+ """Execute SQL query with pyodbc"""
25
+ params = [] if not params else params
26
+ is_select = query.strip().upper().startswith('SELECT')
27
+ try:
28
+ res = self.cursor.execute(query, params)
29
+ if is_select:
30
+ res = self.cursor.fetchall()
31
+ if len(res) == 0:
32
+ print("No results from query")
33
+ return None
34
+ return res
35
+ else:
36
+ return None
37
+ except pyodbc.Error as e:
38
+ print(e)
39
+ print(query)
40
+ raise e
41
+
42
+ def fetch_env(self, db_env):
43
+ """Get env variable based on context, PROD or TEST"""
44
+ if db_env.upper() == "PROD":
45
+ connection_env = "DbConnectionString"
46
+ return connection_env
47
+ if db_env.upper() == "TEST":
48
+ connection_env = "DbConnectionStringTest"
49
+ return connection_env
50
+
51
+ raise ValueError(f"arg db_env is {db_env.upper()} but should be 'PROD' or 'TEST'")
52
+
53
+ def execute_stored_procedure(self, stored_procedure: str, params: Dict[str, Tuple[type, Any]] | None = None) -> Dict[str, Union[bool, str, Any]]:
54
+ """
55
+ Executes a stored procedure with the given parameters.
56
+
57
+ Args:
58
+ connection_string (str): The connection string to connect to the database.
59
+ stored_procedure (str): The name of the stored procedure to execute.
60
+ params (Dict[str, Tuple[type, Any]], optional): A dictionary of parameters to pass to the stored procedure.
61
+ Each value should be a tuple of (type, actual_value).
62
+
63
+ Returns:
64
+ Dict[str, Union[bool, str, Any]]: A dictionary containing the success status, an error message (if any),
65
+ number of affected rows, and additional data.
66
+ """
67
+ result = {
68
+ "success": False,
69
+ "error_message": None,
70
+ }
71
+
72
+ type_mapping = {
73
+ "str": str,
74
+ "int": int,
75
+ "float": float,
76
+ "datetime": parser.isoparse,
77
+ "json": lambda x: json.dumps(x, ensure_ascii=False)
78
+ }
79
+
80
+ try:
81
+ if params:
82
+ param_placeholders = ', '.join([f"@{key} = ?" for key in params.keys()])
83
+ param_values = []
84
+
85
+ for key, value in params.items():
86
+ if isinstance(value, tuple) and len(value) == 2:
87
+ value_type, actual_value = value
88
+ if value_type in type_mapping:
89
+ param_values.append(type_mapping[value_type](actual_value))
90
+ else:
91
+ param_values.append(actual_value)
92
+ else:
93
+ raise ValueError("Each parameter value must be a tuple of (type, actual_value).")
94
+
95
+ sql = f"EXEC {stored_procedure} {param_placeholders}"
96
+ rows_updated = self.cursor.execute(sql, tuple(param_values))
97
+ else:
98
+ sql = f"EXEC {stored_procedure}"
99
+ rows_updated = self.cursor.execute(sql)
100
+ result["success"] = True
101
+ result["rows_updated"] = rows_updated.rowcount
102
+ except pyodbc.Error as e:
103
+ result["error_message"] = f"Database error: {str(e)}"
104
+ except ValueError as e:
105
+ result["error_message"] = f"Value error: {str(e)}"
106
+ except Exception as e:
107
+ result["error_message"] = f"An unexpected error occurred: {str(e)}"
108
+
109
+ return result
@@ -126,14 +126,6 @@ class DocumentHandler(HandlerBase):
126
126
  first_booking = controls[0]
127
127
  first_booking.RightClick(simulateMove=False, waitTime=0)
128
128
 
129
- # list_bookings_group = self.wait_for_control(
130
- # auto.GroupControl,
131
- # {'AutomationId': 'GroupBoxView'},
132
- # search_depth=13,
133
- # )
134
- # group_bookings_list = list_bookings_group.GetChildren()[0].GetChildren()[1]
135
- # group_bookings_list.RightClick(simulateMove=False, waitTime=0)
136
-
137
129
  pop_up_right_click_menu = self.wait_for_control(
138
130
  auto.MenuControl,
139
131
  {'Name': 'Kontekst'},
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbu_dev_shared_components
3
- Version: 2.4.8
3
+ Version: 3.0.0
4
4
  Summary: Shared components to use in RPA projects
5
5
  Author-email: MBU <rpa@mbu.aarhus.dk>
6
6
  License-Expression: MIT
@@ -7,6 +7,7 @@ mbu_dev_shared_components.egg-info/dependency_links.txt
7
7
  mbu_dev_shared_components.egg-info/requires.txt
8
8
  mbu_dev_shared_components.egg-info/top_level.txt
9
9
  mbu_dev_shared_components/database/__init__.py
10
+ mbu_dev_shared_components/database/connection.py
10
11
  mbu_dev_shared_components/database/constants.py
11
12
  mbu_dev_shared_components/database/logging.py
12
13
  mbu_dev_shared_components/database/utility.py
@@ -53,4 +54,5 @@ mbu_dev_shared_components/utils/__init__.py
53
54
  mbu_dev_shared_components/utils/db_stored_procedure_executor.py
54
55
  mbu_dev_shared_components/utils/fernet_encryptor.py
55
56
  mbu_dev_shared_components/utils/file_handler.py
56
- mbu_dev_shared_components/utils/json_handler.py
57
+ mbu_dev_shared_components/utils/json_handler.py
58
+ tests/test_database.py
@@ -2,13 +2,9 @@
2
2
  requires = ["setuptools>=65.0"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
- #[tool.setuptools]
6
- #package-dir = { "" = "." }
7
- #packages = ["mbu_dev_shared_components"]
8
-
9
5
  [project]
10
6
  name = "mbu_dev_shared_components"
11
- version = "2.4.8" # Specify the version manually here
7
+ version = "3.0.0" # Specify the version manually here
12
8
  authors = [
13
9
  { name="MBU", email="rpa@mbu.aarhus.dk" },
14
10
  ]
@@ -0,0 +1,319 @@
1
+ """
2
+ Module to test RPAConnection functionalities in the Solteq Tand system
3
+
4
+ Tested functionalities:
5
+
6
+ - Establish database connection:
7
+ Function:
8
+ RPAConnection.__enter__
9
+ Assertion:
10
+ Connection and cursor are not None
11
+ Dependencies:
12
+ None
13
+
14
+ - Add and retrieve constant:
15
+ Function:
16
+ RPAConnection.add_constant, RPAConnection.get_constant
17
+ Assertion:
18
+ Constant is correctly inserted and retrieved
19
+ Dependencies:
20
+ test_connection
21
+
22
+ - Add and retrieve credential:
23
+ Function:
24
+ RPAConnection.add_credential, RPAConnection.get_credential
25
+ Assertion:
26
+ Credential is correctly inserted and retrieved
27
+ Dependencies:
28
+ test_connection
29
+
30
+ - Rollback functionality:
31
+ Function:
32
+ RPAConnection.__exit__
33
+ Assertion:
34
+ Constant added without commit is not persisted
35
+ Dependencies:
36
+ test_connection, test_add_get_constant
37
+
38
+ - Log event:
39
+ Function:
40
+ RPAConnection.log_event, RPAConnection.get_latest_log
41
+ Assertion:
42
+ Log entry is correctly inserted and retrieved
43
+ Dependencies:
44
+ None
45
+
46
+ - Heartbeat logging (stop as string):
47
+ Function:
48
+ RPAConnection.log_heartbeat, RPAConnection.get_heartbeat
49
+ Assertion:
50
+ Heartbeat status is "STOPPED"
51
+ Dependencies:
52
+ None
53
+
54
+ - Heartbeat logging (stop as boolean):
55
+ Function:
56
+ RPAConnection.log_heartbeat, RPAConnection.get_heartbeat
57
+ Assertion:
58
+ Heartbeat status is "STOPPED"
59
+ Dependencies:
60
+ None
61
+
62
+ - Run heartbeat process:
63
+ Function:
64
+ External subprocess running heartbeat_worker.py
65
+ Assertion:
66
+ Heartbeat status is "RUNNING" and updates over time
67
+ Dependencies:
68
+ None
69
+
70
+ Requirements:
71
+ - Database environment variable `TEST` must be configured
72
+ - pyodbc must be installed and accessible
73
+ - mbu_dev_shared_components must be available in PYTHONPATH
74
+ - `heartbeat_worker.py` must exist in the `tests/` directory and be executable
75
+ - `.venv/Scripts/python` must point to the correct Python interpreter
76
+ Further description:
77
+ Each test uses the RPAConnection context manager to ensure proper handling of database transactions.
78
+ COMMIT is set to False to test rollback behavior and avoid persistent changes to the test database.
79
+ Heartbeat and logging tests validate time-sensitive operations with a threshold of 0.5 seconds.
80
+ The heartbeat process test (`test_run_heartbeat`) runs a parallel subprocess to simulate real-time heartbeat updates.
81
+ """
82
+
83
+ from datetime import datetime, timedelta
84
+ import time
85
+ from uuid import uuid4
86
+ import socket
87
+ import subprocess
88
+
89
+ import pyodbc
90
+ import pytest
91
+ from mbu_dev_shared_components.database.connection import RPAConnection
92
+
93
+ # Global test configuration
94
+ DB_ENV = "TEST"
95
+ COMMIT = False
96
+ THRES_SEC = 0.5
97
+
98
+
99
+ @pytest.mark.dependency()
100
+ def test_connection():
101
+ """Test that RPAConnection successfully establishes a database connection
102
+
103
+ Verifies that the `conn` and `cursor` attributes are initialized and not None
104
+ after creating an instance of RPAConnection. Checks that
105
+ """
106
+ with RPAConnection(db_env=DB_ENV, commit=COMMIT) as rpa_connection:
107
+ # Assert connection is established
108
+ assert rpa_connection.conn is not None
109
+ assert rpa_connection.cursor is not None
110
+
111
+ # Assert connection is closed
112
+ with pytest.raises(pyodbc.ProgrammingError, match="Attempt to use a closed cursor."):
113
+ rpa_connection.get_constant("test_uuid")
114
+
115
+
116
+ @pytest.mark.dependency(depends=["test_connection"])
117
+ def test_add_get_constant():
118
+ """
119
+ Adds a test constant, rolls back the transaction.
120
+ """
121
+ test_constant_name = f"pytest_constant_{uuid4()}"
122
+ test_value = "temporary_value"
123
+
124
+ with RPAConnection(db_env=DB_ENV, commit=COMMIT) as rpa_connection:
125
+ # Add constant (should be rolled back)
126
+ rpa_connection.add_constant(test_constant_name, test_value, datetime.now())
127
+
128
+ assert rpa_connection.cursor.rowcount == 1
129
+
130
+ # Check that constant is added (will be rolled back after function)
131
+ test_const = rpa_connection.get_constant(test_constant_name)
132
+
133
+ assert test_const
134
+ assert test_const["constant_name"] == test_constant_name
135
+ assert test_const["value"] == test_value
136
+
137
+
138
+ @pytest.mark.dependency(depends=["test_connection"])
139
+ def test_add_get_credential():
140
+ """
141
+ Adds a test credential, rolls back the transaction.
142
+ """
143
+ test_credential_name = f"pytest_constant_{uuid4()}"
144
+ test_username = "test_user"
145
+ test_password = "test_password"
146
+
147
+ with RPAConnection(db_env=DB_ENV, commit=COMMIT) as rpa_connection:
148
+ # Add constant (should be rolled back)
149
+ rpa_connection.add_credential(test_credential_name, test_username, test_password, datetime.now())
150
+
151
+ # Check that constant is added (will be rolled back after function)
152
+ test_const = rpa_connection.get_credential(test_credential_name)
153
+
154
+ assert test_const
155
+ assert test_const["username"] == test_username
156
+ assert test_const["decrypted_password"] == test_password
157
+
158
+
159
+ @pytest.mark.dependency(depends=["test_connection", "test_add_get_constant"])
160
+ def test_rollback():
161
+ """
162
+ Test that rollback undoes the insertion of a constant through the context manager.
163
+ Asserts that a constant added in a scope without commiting to the db cannot be accessed outside the scope
164
+ """
165
+ test_constant_name = f"pytest_rollback_same_conn_{uuid4()}"
166
+ test_value = "temporary_value"
167
+
168
+ with RPAConnection(db_env=DB_ENV, commit=COMMIT) as rpa_connection:
169
+ # Add constant
170
+ rpa_connection.add_constant(test_constant_name, test_value, datetime.now())
171
+
172
+ with RPAConnection(db_env=DB_ENV, commit=COMMIT) as rpa_connection:
173
+ # Try to retrieve the constant in new connection
174
+ with pytest.raises(ValueError, match=f"No constant found with name: {test_constant_name}"):
175
+ rpa_connection.get_constant(test_constant_name)
176
+
177
+
178
+ def test_log():
179
+ """Test log functionality """
180
+
181
+ # Variables for test log row
182
+ log_db = "journalizing.Journalize_log"
183
+ level = "INFO"
184
+ message = "test_log"
185
+ context = "pytest"
186
+
187
+ with RPAConnection(db_env=DB_ENV, commit=COMMIT) as rpa_connection:
188
+ now = datetime.now()
189
+ # Attempt insertion of log_event
190
+ rpa_connection.log_event(
191
+ log_db=log_db,
192
+ level=level,
193
+ message=message,
194
+ context=context
195
+ )
196
+ # Assert that one row is inserted
197
+ assert rpa_connection.cursor.rowcount == 1
198
+
199
+ # Get latest log
200
+ # pylint: disable-next=W0212
201
+ log_row = rpa_connection.get_latest_log(
202
+ log_db=log_db
203
+ )[0]
204
+
205
+ # Assert values of inserted element
206
+ assert log_row[0] == level
207
+ assert log_row[1] == message
208
+ assert abs(log_row[2]-now) < timedelta(seconds=THRES_SEC) # Latest log was within 0.1 second of start of function
209
+ assert log_row[3] == context
210
+
211
+
212
+ def test_stop_heartbeat_str():
213
+ """Test stopping heartbeat functionality"""
214
+
215
+ servicename = "pytest"
216
+ heartbeat_interval = 1.0
217
+ details = "pytest testing heartbeat functionality"
218
+ stop = "True"
219
+ with RPAConnection(db_env=DB_ENV, commit=COMMIT) as rpa_connection:
220
+ now = datetime.now()
221
+ rpa_connection.log_heartbeat(
222
+ stop=stop,
223
+ servicename=servicename,
224
+ heartbeat_interval=heartbeat_interval,
225
+ details=details
226
+ )
227
+
228
+ heartbeat = rpa_connection.get_heartbeat(service_name=servicename)[0]
229
+
230
+ assert heartbeat[0] == servicename
231
+ assert abs(heartbeat[1]-now) < timedelta(seconds=THRES_SEC)
232
+ assert heartbeat[2] == "STOPPED"
233
+ assert heartbeat[3] == socket.gethostname()
234
+
235
+
236
+ def test_stop_heartbeat_bool():
237
+ """Test stopping heartbeat functionality"""
238
+
239
+ servicename = "pytest"
240
+ heartbeat_interval = 1.0
241
+ details = "pytest testing heartbeat functionality"
242
+ stop = True
243
+ with RPAConnection(db_env=DB_ENV, commit=COMMIT) as rpa_connection:
244
+ now = datetime.now()
245
+ rpa_connection.log_heartbeat(
246
+ stop=stop,
247
+ servicename=servicename,
248
+ heartbeat_interval=heartbeat_interval,
249
+ details=details
250
+ )
251
+
252
+ heartbeat = rpa_connection.get_heartbeat(service_name=servicename)[0]
253
+
254
+ assert heartbeat[0] == servicename
255
+ assert abs(heartbeat[1]-now) < timedelta(seconds=THRES_SEC)
256
+ assert heartbeat[2] == "STOPPED"
257
+ assert heartbeat[3] == socket.gethostname()
258
+
259
+
260
+ def test_run_heartbeat():
261
+ """Test running heartbeat functionality
262
+ Uses subprocess to run the heartbeat process in parallel and allows to stop it after some time
263
+ Since we are using a stored procedure, we cannot roll back the transaction, so we have to accept the table being affected by the test
264
+ Effectively we insert one row to the heartbeat table for the 'pytest' service
265
+ """
266
+ servicename = "pytest"
267
+ heartbeat_interval = 2.0
268
+ details = "pytest testing heartbeat functionality"
269
+ stop = False
270
+ now = datetime.now()
271
+ heartbeat_process = subprocess.Popen(
272
+ [
273
+ ".venv/Scripts/python",
274
+ "tests/heartbeat_worker.py",
275
+ DB_ENV,
276
+ str(stop),
277
+ servicename,
278
+ str(heartbeat_interval),
279
+ details
280
+ ]
281
+ )
282
+
283
+ time.sleep(heartbeat_interval)
284
+
285
+ with RPAConnection(db_env="TEST", commit=False) as rpa_connection:
286
+ heartbeat = rpa_connection.get_heartbeat(servicename)[0]
287
+
288
+ # Assert heartbeat is running and recent
289
+ assert heartbeat[0] == servicename
290
+ assert abs(heartbeat[1]-now) < timedelta(seconds=THRES_SEC)
291
+ assert heartbeat[2] == "RUNNING"
292
+ assert heartbeat[3] == socket.gethostname()
293
+
294
+ # Test that heartbeat is updated by heartbeat interval time
295
+ for i in range(5):
296
+ print(f"Running assertion loop {i+1}")
297
+ prev_heartbeat_time = heartbeat[1]
298
+ time.sleep(heartbeat_interval)
299
+ # Get new heartbeat and assert that it is newer than previous hearbeat
300
+ heartbeat = rpa_connection.get_heartbeat(servicename)[0]
301
+ assert heartbeat[1] > prev_heartbeat_time, f"Heartbeat not updated on iteration {i+1}"
302
+
303
+ print("Should have finished assertion loop ")
304
+
305
+ heartbeat_process.terminate()
306
+
307
+ print("Should have terminated heartbeat process")
308
+
309
+ with RPAConnection(db_env="TEST", commit=True) as rpa_connection:
310
+ rpa_connection.log_heartbeat(
311
+ stop=True,
312
+ servicename=servicename,
313
+ heartbeat_interval=2,
314
+ details="Stop send from pytest"
315
+ )
316
+
317
+
318
+ if __name__ == '__main__':
319
+ pytest.main([__file__])
@@ -1,131 +0,0 @@
1
- """This module handles generating and fetching constants and credentials from the database"""
2
-
3
- import os
4
- import pyodbc
5
- from datetime import datetime
6
-
7
- from mbu_dev_shared_components.utils.fernet_encryptor import Encryptor
8
- from mbu_dev_shared_components.database.utility import connect_to_db, execute_query
9
-
10
-
11
- def add_credential(
12
- credential_name: str,
13
- username: str,
14
- password: str,
15
- changed_at: datetime = datetime.now(),
16
- db_env: str = "PROD"
17
- ):
18
- encryptor = Encryptor()
19
- encrypted_password = encryptor.encrypt(password)
20
-
21
- rpa_conn = connect_to_db(db_env=db_env)
22
- cursor = rpa_conn.cursor()
23
-
24
- query = """
25
- INSERT INTO [RPA].[rpa].[Credentials]
26
- ([name]
27
- ,[username]
28
- ,[password]
29
- ,[changed_at])
30
- VALUES
31
- (?
32
- ,?
33
- ,?
34
- ,?)
35
- """
36
- params = [credential_name, username, encrypted_password, changed_at]
37
- execute_query(query=query, cursor=cursor, params=params)
38
-
39
-
40
- def get_credential(
41
- credential_name: str,
42
- db_env: str = "PROD"
43
- ) -> dict:
44
-
45
- rpa_conn = connect_to_db(db_env=db_env)
46
- cursor = rpa_conn.cursor()
47
- encryptor = Encryptor()
48
-
49
- query = """
50
- SELECT
51
- Username
52
- ,cast(Password as varbinary(max))
53
- FROM [RPA].[rpa].[Credentials]
54
- WHERE
55
- name = ?
56
- """
57
-
58
- params = [credential_name]
59
-
60
- res = execute_query(query=query, cursor=cursor, params=params)
61
- if res is not None:
62
- res = res[0]
63
- username = res[0]
64
- encrypted_password = res[1]
65
-
66
- decrypted_password = encryptor.decrypt(encrypted_password)
67
-
68
- return {
69
- "username": username,
70
- "decrypted_password": decrypted_password,
71
- "encrypted_password": encrypted_password
72
- }
73
- else:
74
- print(f"No credential found with name {credential_name}")
75
-
76
- def get_constant(
77
- constant_name: str,
78
- db_env: str = "PROD"
79
- ) -> tuple:
80
-
81
- rpa_conn = connect_to_db(db_env=db_env)
82
- cursor = rpa_conn.cursor()
83
- encryptor = Encryptor()
84
-
85
- query = """
86
- SELECT
87
- name
88
- ,value
89
- FROM [RPA].[rpa].[Constants]
90
- WHERE
91
- name = ?
92
- """
93
-
94
- params = [constant_name]
95
-
96
- res = execute_query(query=query, cursor=cursor, params=params)
97
- if res is not None:
98
-
99
- returned_constant = res[0]
100
- constant_name = returned_constant[0]
101
- value = returned_constant[1]
102
-
103
- return {"constant_name": constant_name, "value": value}
104
- else:
105
- print(f"No constant found with name: {constant_name}")
106
-
107
-
108
- def add_constant(
109
- constant_name: str,
110
- value: str,
111
- changed_at: datetime = datetime.now(),
112
- db_env: str = "PROD"
113
- ):
114
- query = """
115
- INSERT INTO [RPA].[rpa].[Constants]
116
- ([name]
117
- ,[value]
118
- ,[changed_at])
119
- VALUES
120
- (?
121
- ,?
122
- ,?)
123
- """
124
-
125
- rpa_conn = connect_to_db(db_env=db_env)
126
- cursor = rpa_conn.cursor()
127
-
128
- params = [constant_name, value, changed_at]
129
- execute_query(query=query, cursor=cursor, params=params)
130
-
131
-
@@ -1,86 +0,0 @@
1
- """This module handles logging in the RPA database"""
2
-
3
- from datetime import datetime
4
- import time
5
- import os
6
- import socket
7
-
8
- from mbu_dev_shared_components.database.utility import execute_query, connect_to_db, fetch_env
9
- from mbu_dev_shared_components.utils.db_stored_procedure_executor import execute_stored_procedure
10
-
11
- def log_event(
12
- log_db: str,
13
- level: str,
14
- message: str,
15
- context: str,
16
- db_env="TEST"
17
- ):
18
- created_at = datetime.now()
19
- """Logs the inputted parameters in """
20
- query = f"""
21
- INSERT INTO RPA.{log_db}
22
- ([level]
23
- ,[message]
24
- ,[created_at]
25
- ,[context])
26
- VALUES
27
- (?
28
- ,?
29
- ,?
30
- ,?)
31
- """
32
- rpa_conn = connect_to_db(db_env=db_env)
33
- cursor = rpa_conn.cursor()
34
-
35
- params = [level, message, created_at, context]
36
- execute_query(query=query, cursor=cursor, params=params)
37
-
38
- def _send_heartbeat(
39
- servicename,
40
- status,
41
- details,
42
- db_env,
43
- ):
44
- conn_env = fetch_env(db_env=db_env)
45
- conn_str = os.getenv(conn_env)
46
- hostname = socket.gethostname()
47
- params = {
48
- "ServiceName": (str, servicename),
49
- "Status": (str, status),
50
- "HostName": (str, hostname),
51
- "Details": (str, details)
52
- }
53
- result = execute_stored_procedure(
54
- connection_string=conn_str,
55
- stored_procedure='rpa.sp_UpdateHeartbeat',
56
- params=params)
57
- if result["success"] is not True:
58
- print(result["error_message"])
59
-
60
- def log_heartbeat(
61
- stop: str|bool,
62
- servicename: str,
63
- heartbeat_interval: int,
64
- details: str = "",
65
- db_env: str = "PROD"
66
- ):
67
- if isinstance(stop,str):
68
- stop = stop == "True"
69
- if not isinstance(heartbeat_interval, int):
70
- heartbeat_interval = int(heartbeat_interval)
71
- while not stop:
72
- status = "RUNNING"
73
- _send_heartbeat(
74
- servicename,
75
- status,
76
- details,
77
- db_env,
78
- )
79
- time.sleep(heartbeat_interval)
80
- status = "STOPPED"
81
- _send_heartbeat(
82
- servicename,
83
- status,
84
- details,
85
- db_env,
86
- )
@@ -1,44 +0,0 @@
1
- """This module handles general database connection and calls"""
2
-
3
- import os
4
- import pyodbc
5
- from datetime import datetime
6
-
7
- def connect_to_db(autocommit=True, db_env="PROD") -> pyodbc.Connection:
8
- """Establish connection to sql database
9
-
10
- Returns:
11
- rpa_conn (pyodbc.Connection): The connection object to the SQL database.
12
- """
13
- connection_env = fetch_env(db_env)
14
- rpa_conn_string = os.getenv(connection_env)
15
- rpa_conn = pyodbc.connect(rpa_conn_string, autocommit=autocommit)
16
- return rpa_conn
17
-
18
- def execute_query(query: str, cursor: pyodbc.Cursor, params: list) -> pyodbc.Cursor:
19
- is_select = query.strip().upper().startswith('SELECT')
20
- try:
21
- res = cursor.execute(query, params)
22
- if is_select:
23
- res = cursor.fetchall()
24
- if len(res) == 0:
25
- print("No results from query")
26
- return None
27
- return res
28
- else:
29
- return None
30
- except pyodbc.Error as e:
31
- print(e)
32
- finally:
33
- cursor.close()
34
-
35
-
36
- def fetch_env(db_env):
37
- if db_env.upper() == "PROD":
38
- connection_env = "DbConnectionString"
39
- return connection_env
40
- if db_env.upper() == "TEST":
41
- connection_env = "DbConnectionStringTest"
42
- return connection_env
43
-
44
- raise ValueError(f"arg db_env is {db_env.upper()} but should be 'PROD' or 'TEST'")