wcp-library 1.2.9__py3-none-any.whl → 1.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,95 +1,49 @@
1
1
  import logging
2
2
 
3
- import requests
4
- from yarl import URL
5
-
6
- from wcp_library.credentials import MissingCredentialsError
3
+ from wcp_library.credentials.credential_manager_asynchronous import AsyncCredentialManager
4
+ from wcp_library.credentials.credential_manager_synchronous import CredentialManager
7
5
 
8
6
  logger = logging.getLogger(__name__)
9
7
 
10
8
 
11
- class PostgresCredentialManager:
9
+ class PostgresCredentialManager(CredentialManager):
12
10
  def __init__(self, passwordState_api_key: str):
13
- self.password_url = URL("https://vault.wcap.ca/api/passwords/")
14
- self.api_key = passwordState_api_key
15
- self.headers = {"APIKey": self.api_key, 'Reason': 'Python Script Access'}
16
- self._password_list_id = 210
17
-
18
- def _get_credentials(self) -> dict:
19
- """
20
- Get all credentials from the password list
21
-
22
- :return: Dictionary of credentials
23
- """
11
+ super().__init__(passwordState_api_key, 210)
24
12
 
25
- logger.debug("Getting credentials from PasswordState")
26
- url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
27
- passwords = requests.get(str(url), headers=self.headers).json()
28
-
29
- if not passwords:
30
- raise MissingCredentialsError("No credentials found in this Password List")
31
-
32
- password_dict = {}
33
- for password in passwords:
34
- password_info = {'PasswordID': password['PasswordID'], 'UserName': password['UserName'], 'Password': password['Password']}
35
- for field in password['GenericFieldInfo']:
36
- password_info[field['DisplayName']] = field['Value'].lower() if field['DisplayName'].lower() == 'username' else field['Value']
37
- password_dict[password['UserName'].lower()] = password_info
38
- logger.debug("Credentials retrieved")
39
- return password_dict
40
-
41
- def get_credentials(self, username: str) -> dict:
42
- """
43
- Get the credentials for a specific username
44
-
45
- :param username:
46
- :return: Dictionary of credentials
47
- """
48
-
49
- logger.debug(f"Getting credentials for {username}")
50
- credentials = self._get_credentials()
51
-
52
- try:
53
- return_credential = credentials[username.lower()]
54
- except KeyError:
55
- raise MissingCredentialsError(f"Credentials for {username} not found in this Password List")
56
- logger.debug(f"Credentials for {username} retrieved")
57
- return return_credential
58
-
59
- def update_credential(self, credentials_dict: dict) -> bool:
13
+ def new_credentials(self, credentials_dict: dict) -> bool:
60
14
  """
61
- Update the credentials for a specific username
15
+ Create a new credential entry
62
16
 
63
17
  Credentials dictionary must have the following keys:
64
- - PasswordID
65
18
  - UserName
66
19
  - Password
67
-
68
- The dictionary should be obtained from the get_credentials method and modified accordingly
20
+ - Host
21
+ - Port
22
+ - Database
69
23
 
70
24
  :param credentials_dict:
71
25
  :return: True if successful, False otherwise
72
26
  """
73
27
 
74
- logger.debug(f"Updating credentials for {credentials_dict['UserName']}")
75
- url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
76
- passwords = requests.get(str(url), headers=self.headers).json()
28
+ data = {
29
+ "PasswordListID": self._password_list_id,
30
+ "Title": credentials_dict['UserName'].upper() if "Title" not in credentials_dict else credentials_dict['Title'].upper(),
31
+ "Notes": credentials_dict['Notes'] if 'Notes' in credentials_dict else None,
32
+ "UserName": credentials_dict['UserName'].lower(),
33
+ "Password": credentials_dict['Password'],
34
+ "GenericField1": credentials_dict['Host'],
35
+ "GenericField2": credentials_dict['Port'],
36
+ "GenericField3": credentials_dict['Database']
37
+ }
77
38
 
78
- relevant_credential_entry = [x for x in passwords if x['UserName'] == credentials_dict['UserName']][0]
79
- for field in relevant_credential_entry['GenericFieldInfo']:
80
- if field['DisplayName'] in credentials_dict:
81
- credentials_dict[field['GenericFieldID']] = credentials_dict[field['DisplayName']]
82
- credentials_dict.pop(field['DisplayName'])
39
+ return self._publish_new_password(data)
83
40
 
84
- response = requests.put(str(self.password_url), json=credentials_dict, headers=self.headers)
85
- if response.status_code == 200:
86
- logger.debug(f"Credentials for {credentials_dict['UserName']} updated")
87
- return True
88
- else:
89
- logger.error(f"Failed to update credentials for {credentials_dict['UserName']}")
90
- return False
91
41
 
92
- def new_credentials(self, credentials_dict: dict) -> bool:
42
+ class AsyncPostgresCredentialManager(AsyncCredentialManager):
43
+ def __init__(self, passwordState_api_key: str):
44
+ super().__init__(passwordState_api_key, 210)
45
+
46
+ async def new_credentials(self, credentials_dict: dict) -> bool:
93
47
  """
94
48
  Create a new credential entry
95
49
 
@@ -101,7 +55,7 @@ class PostgresCredentialManager:
101
55
  - Database
102
56
 
103
57
  :param credentials_dict:
104
- :return: True if successful, False otherwise
58
+ :return:
105
59
  """
106
60
 
107
61
  data = {
@@ -115,10 +69,4 @@ class PostgresCredentialManager:
115
69
  "GenericField3": credentials_dict['Database']
116
70
  }
117
71
 
118
- response = requests.post(str(self.password_url), json=data, headers=self.headers)
119
- if response.status_code == 201:
120
- logger.debug(f"New credentials for {credentials_dict['UserName']} created")
121
- return True
122
- else:
123
- logger.error(f"Failed to create new credentials for {credentials_dict['UserName']}")
124
- return False
72
+ return await self._publish_new_password(data)
@@ -32,4 +32,31 @@ def retry(f: callable) -> callable:
32
32
  sleep(300)
33
33
  else:
34
34
  raise e
35
- return wrapper
35
+ return wrapper
36
+
37
+
38
+ def async_retry(f: callable) -> callable:
39
+ """
40
+ Decorator to retry a function
41
+
42
+ :param f: function
43
+ :return: function
44
+ """
45
+
46
+ @wraps(f)
47
+ async def wrapper(self, *args, **kwargs):
48
+ self._retry_count = 0
49
+ while True:
50
+ try:
51
+ return await f(self, *args, **kwargs)
52
+ except (oracledb.OperationalError, psycopg.OperationalError) as e:
53
+ error_obj, = e.args
54
+ if error_obj.full_code in self.retry_error_codes and self._retry_count < self.retry_limit:
55
+ self._retry_count += 1
56
+ logger.debug(f"{self._db_service} connection error")
57
+ logger.debug(error_obj.message)
58
+ logger.info("Waiting 5 minutes before retrying Oracle connection")
59
+ await asyncio.sleep(300)
60
+ else:
61
+ raise e
62
+ return wrapper
wcp_library/sql/oracle.py CHANGED
@@ -4,9 +4,9 @@ from typing import Optional
4
4
  import numpy as np
5
5
  import pandas as pd
6
6
  import oracledb
7
- from oracledb import ConnectionPool
7
+ from oracledb import ConnectionPool, AsyncConnectionPool
8
8
 
9
- from wcp_library.sql import retry
9
+ from wcp_library.sql import retry, async_retry
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
@@ -38,6 +38,36 @@ def _connect_warehouse(username: str, password: str, hostname: str, port: int, d
38
38
  return session_pool
39
39
 
40
40
 
41
+ async def _async_connect_warehouse(username: str, password: str, hostname: str, port: int, database: str,
42
+ min_connections: int, max_connections: int) -> AsyncConnectionPool:
43
+ """
44
+ Create Warehouse Connection
45
+
46
+ :param username: username
47
+ :param password: password
48
+ :param hostname: hostname
49
+ :param port: port
50
+ :param database: database
51
+ :param min_connections:
52
+ :param max_connections:
53
+ :return: session_pool
54
+ """
55
+
56
+ dsn = oracledb.makedsn(hostname, port, sid=database)
57
+ session_pool = oracledb.create_pool_async(
58
+ user=username,
59
+ password=password,
60
+ dsn=dsn,
61
+ min=min_connections,
62
+ max=max_connections,
63
+ increment=1
64
+ )
65
+ return session_pool
66
+
67
+
68
+ """~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"""
69
+
70
+
41
71
  class OracleConnection(object):
42
72
  """
43
73
  SQL Connection Class
@@ -272,3 +302,226 @@ class OracleConnection(object):
272
302
  """
273
303
 
274
304
  self._session_pool.close()
305
+
306
+
307
+ class AsyncOracleConnection(object):
308
+ """
309
+ SQL Connection Class
310
+
311
+ :return: None
312
+ """
313
+
314
+ def __init__(self, min_connections: int = 2, max_connections: int = 5):
315
+ self._db_service: str = "Oracle"
316
+ self._username: Optional[str] = None
317
+ self._password: Optional[str] = None
318
+ self._hostname: Optional[str] = None
319
+ self._port: Optional[int] = None
320
+ self._database: Optional[str] = None
321
+ self._sid: Optional[str] = None
322
+ self._session_pool: Optional[AsyncConnectionPool] = None
323
+
324
+ self.min_connections = min_connections
325
+ self.max_connections = max_connections
326
+
327
+ self._retry_count = 0
328
+ self.retry_limit = 50
329
+ self.retry_error_codes = ['ORA-01033', 'DPY-6005', 'DPY-4011']
330
+
331
+ @async_retry
332
+ async def _connect(self) -> None:
333
+ """
334
+ Connect to the warehouse
335
+
336
+ :return: None
337
+ """
338
+
339
+ sid_or_service = self._database if self._database else self._sid
340
+
341
+ self._session_pool = await _async_connect_warehouse(self._username, self._password, self._hostname, self._port,
342
+ sid_or_service, self.min_connections, self.max_connections)
343
+
344
+ async def set_user(self, credentials_dict: dict) -> None:
345
+ """
346
+ Set the user credentials and connect
347
+
348
+ :param credentials_dict: dictionary of connection details
349
+ :return: None
350
+ """
351
+
352
+ if not ([credentials_dict['Service'] or credentials_dict['SID']]):
353
+ raise ValueError("Either Service or SID must be provided")
354
+
355
+ self._username: Optional[str] = credentials_dict['UserName']
356
+ self._password: Optional[str] = credentials_dict['Password']
357
+ self._hostname: Optional[str] = credentials_dict['Host']
358
+ self._port: Optional[int] = int(credentials_dict['Port'])
359
+ self._database: Optional[str] = credentials_dict['Service'] if 'Service' in credentials_dict else None
360
+ self._sid: Optional[str] = credentials_dict['SID'] if 'SID' in credentials_dict else None
361
+
362
+ await self._connect()
363
+
364
+ async def close_connection(self) -> None:
365
+ """
366
+ Close the connection
367
+
368
+ :return: None
369
+ """
370
+
371
+ await self._session_pool.close()
372
+
373
+ @async_retry
374
+ async def execute(self, query: str) -> None:
375
+ """
376
+ Execute the query
377
+
378
+ :param query: query
379
+ :return: None
380
+ """
381
+
382
+ async with self._session_pool.acquire() as connection:
383
+ with connection.cursor() as cursor:
384
+ await cursor.execute(query)
385
+ await connection.commit()
386
+
387
+ @async_retry
388
+ async def safe_execute(self, query: str, packed_values: dict) -> None:
389
+ """
390
+ Execute the query without SQL Injection possibility, to be used with external facing projects.
391
+
392
+ :param query: query
393
+ :param packed_values: dictionary of values
394
+ :return: None
395
+ """
396
+
397
+ async with self._session_pool.acquire() as connection:
398
+ with connection.cursor() as cursor:
399
+ await cursor.execute(query, packed_values)
400
+ await connection.commit()
401
+
402
+ @async_retry
403
+ async def execute_multiple(self, queries: list[list[str, dict]]) -> None:
404
+ """
405
+ Execute multiple queries
406
+
407
+ :param queries: list of queries
408
+ :return: None
409
+ """
410
+
411
+ async with self._session_pool.acquire() as connection:
412
+ with connection.cursor() as cursor:
413
+ for item in queries:
414
+ query = item[0]
415
+ packed_values = item[1]
416
+ if packed_values:
417
+ await cursor.execute(query, packed_values)
418
+ else:
419
+ await cursor.execute(query)
420
+ await connection.commit()
421
+
422
+ @async_retry
423
+ async def execute_many(self, query: str, dictionary: list[dict]) -> None:
424
+ """
425
+ Execute many queries
426
+
427
+ :param query: query
428
+ :param dictionary: dictionary of values
429
+ :return: None
430
+ """
431
+
432
+ async with self._session_pool.acquire() as connection:
433
+ with connection.cursor() as cursor:
434
+ await cursor.executemany(query, dictionary)
435
+ await connection.commit()
436
+
437
+ @async_retry
438
+ async def fetch_data(self, query: str, packed_data=None):
439
+ """
440
+ Fetch the data from the query
441
+
442
+ :param query: query
443
+ :param packed_data: packed data
444
+ :return: rows
445
+ """
446
+
447
+ async with self._session_pool.acquire() as connection:
448
+ with connection.cursor() as cursor:
449
+ if packed_data:
450
+ await cursor.execute(query, packed_data)
451
+ else:
452
+ await cursor.execute(query)
453
+ rows = await cursor.fetchall()
454
+ return rows
455
+
456
+ @async_retry
457
+ async def remove_matching_data(self, dfObj: pd.DataFrame, outputTableName: str, match_cols: list) -> None:
458
+ """
459
+ Remove matching data from the warehouse
460
+
461
+ :param dfObj: DataFrame
462
+ :param outputTableName: output table name
463
+ :param match_cols: list of columns
464
+ :return: None
465
+ """
466
+
467
+ df = dfObj[match_cols]
468
+ param_list = []
469
+ for column in match_cols:
470
+ param_list.append(f"{column} = :{column}")
471
+ if len(param_list) > 1:
472
+ params = ' AND '.join(param_list)
473
+ else:
474
+ params = param_list[0]
475
+
476
+ main_dict = df.to_dict('records')
477
+ query = f"""DELETE FROM {outputTableName} WHERE {params}"""
478
+ await self.execute_many(query, main_dict)
479
+
480
+ @async_retry
481
+ async def export_DF_to_warehouse(self, dfObj: pd.DataFrame, outputTableName: str, columns: list, remove_nan=False) -> None:
482
+ """
483
+ Export the DataFrame to the warehouse
484
+
485
+ :param dfObj: DataFrame
486
+ :param outputTableName: output table name
487
+ :param columns: list of columns
488
+ :param remove_nan: remove NaN values
489
+ :return: None
490
+ """
491
+
492
+ col = ', '.join(columns)
493
+ bindList = []
494
+ for column in columns:
495
+ bindList.append(':' + column)
496
+ bind = ', '.join(bindList)
497
+
498
+ if remove_nan:
499
+ dfObj = dfObj.replace({np.nan: None})
500
+ main_dict = dfObj.to_dict('records')
501
+
502
+ query = """INSERT INTO {} ({}) VALUES ({})""".format(outputTableName, col, bind)
503
+ await self.execute_many(query, main_dict)
504
+
505
+ @async_retry
506
+ async def truncate_table(self, tableName: str) -> None:
507
+ """
508
+ Truncate the table
509
+
510
+ :param tableName: table name
511
+ :return: None
512
+ """
513
+
514
+ truncateQuery = """TRUNCATE TABLE {}""".format(tableName)
515
+ await self.execute(truncateQuery)
516
+
517
+ @async_retry
518
+ async def empty_table(self, tableName: str) -> None:
519
+ """
520
+ Empty the table
521
+
522
+ :param tableName: table name
523
+ :return: None
524
+ """
525
+
526
+ deleteQuery = """DELETE FROM {}""".format(tableName)
527
+ await self.execute(deleteQuery)