wcp-library 1.2.8__py3-none-any.whl → 1.3.1__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.
@@ -4,9 +4,9 @@ from typing import Optional
4
4
  import numpy as np
5
5
  import pandas as pd
6
6
  from psycopg.sql import SQL
7
- from psycopg_pool import ConnectionPool
7
+ from psycopg_pool import AsyncConnectionPool, ConnectionPool
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
 
@@ -37,6 +37,35 @@ def _connect_warehouse(username: str, password: str, hostname: str, port: int, d
37
37
  return session_pool
38
38
 
39
39
 
40
+ async def _async_connect_warehouse(username: str, password: str, hostname: str, port: int, database: str, min_connections: int,
41
+ max_connections: int) -> AsyncConnectionPool:
42
+ """
43
+ Create Warehouse Connection
44
+
45
+ :param username: username
46
+ :param password: password
47
+ :param hostname: hostname
48
+ :param port: port
49
+ :param database: database
50
+ :param min_connections:
51
+ :param max_connections:
52
+ :return: session_pool
53
+ """
54
+
55
+ url = f"postgres://{username}:{password}@{hostname}:{port}/{database}"
56
+
57
+ session_pool = AsyncConnectionPool(
58
+ conninfo=url,
59
+ min_size=min_connections,
60
+ max_size=max_connections,
61
+ )
62
+ await session_pool.open()
63
+ return session_pool
64
+
65
+
66
+ """~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"""
67
+
68
+
40
69
  class PostgresConnection(object):
41
70
  """
42
71
  SQL Connection Class
@@ -186,7 +215,10 @@ class PostgresConnection(object):
186
215
  param_list = []
187
216
  for column in match_cols:
188
217
  param_list.append(f"{column} = %({column})s")
189
- params = ' AND '.join(param_list)
218
+ if len(param_list) > 1:
219
+ params = ' AND '.join(param_list)
220
+ else:
221
+ params = param_list[0]
190
222
 
191
223
  main_dict = df.to_dict('records')
192
224
  query = """DELETE FROM {} WHERE {}""".format(outputTableName, params)
@@ -249,3 +281,214 @@ class PostgresConnection(object):
249
281
  """
250
282
 
251
283
  self._session_pool.close()
284
+
285
+
286
+ class AsyncPostgresConnection(object):
287
+ """
288
+ SQL Connection Class
289
+
290
+ :return: None
291
+ """
292
+
293
+ def __init__(self, min_connections: int = 2, max_connections: int = 5):
294
+ self._username: Optional[str] = None
295
+ self._password: Optional[str] = None
296
+ self._hostname: Optional[str] = None
297
+ self._port: Optional[int] = None
298
+ self._database: Optional[str] = None
299
+ self._session_pool: Optional[AsyncConnectionPool] = None
300
+
301
+ self.min_connections = min_connections
302
+ self.max_connections = max_connections
303
+
304
+ self._retry_count = 0
305
+ self.retry_limit = 50
306
+ self.retry_error_codes = ['08001', '08004']
307
+
308
+ @async_retry
309
+ async def _connect(self) -> None:
310
+ """
311
+ Connect to the warehouse
312
+
313
+ :return: None
314
+ """
315
+
316
+ self._session_pool = await _connect_warehouse(self._username, self._password, self._hostname, self._port,
317
+ self._database, self.min_connections, self.max_connections)
318
+
319
+ async def set_user(self, credentials_dict: dict) -> None:
320
+ """
321
+ Set the user credentials and connect
322
+
323
+ :param credentials_dict: dictionary of connection details
324
+ :return: None
325
+ """
326
+
327
+ self._username: Optional[str] = credentials_dict['UserName']
328
+ self._password: Optional[str] = credentials_dict['Password']
329
+ self._hostname: Optional[str] = credentials_dict['Host']
330
+ self._port: Optional[int] = int(credentials_dict['Port'])
331
+ self._database: Optional[str] = credentials_dict['Database']
332
+
333
+ await self._connect()
334
+
335
+ async def close_connection(self) -> None:
336
+ """
337
+ Close the connection
338
+
339
+ :return: None
340
+ """
341
+
342
+ await self._session_pool.close()
343
+
344
+ @async_retry
345
+ async def execute(self, query: SQL | str) -> None:
346
+ """
347
+ Execute the query
348
+
349
+ :param query: query
350
+ :return: None
351
+ """
352
+
353
+ async with self._session_pool.connection() as connection:
354
+ await connection.execute(query)
355
+
356
+ @async_retry
357
+ async def safe_execute(self, query: SQL | str, packed_values: dict) -> None:
358
+ """
359
+ Execute the query without SQL Injection possibility, to be used with external facing projects.
360
+
361
+ :param query: query
362
+ :param packed_values: dictionary of values
363
+ :return: None
364
+ """
365
+
366
+ async with self._session_pool.connection() as connection:
367
+ await connection.execute(query, packed_values)
368
+
369
+ @async_retry
370
+ async def execute_multiple(self, queries: list[list[SQL | str, dict]]) -> None:
371
+ """
372
+ Execute multiple queries
373
+
374
+ :param queries: list of queries
375
+ :return: None
376
+ """
377
+
378
+ async with self._session_pool.connection() as connection:
379
+ for item in queries:
380
+ query = item[0]
381
+ packed_values = item[1]
382
+ if packed_values:
383
+ await connection.execute(query, packed_values)
384
+ else:
385
+ await connection.execute(query)
386
+
387
+ @async_retry
388
+ async def execute_many(self, query: SQL | str, dictionary: list[dict]) -> None:
389
+ """
390
+ Execute many queries
391
+
392
+ :param query: query
393
+ :param dictionary: dictionary of values
394
+ :return: None
395
+ """
396
+
397
+ async with self._session_pool.connection() as connection:
398
+ await connection.executemany(query, dictionary)
399
+
400
+ @async_retry
401
+ async def fetch_data(self, query: SQL | str, packed_data=None):
402
+ """
403
+ Fetch the data from the query
404
+
405
+ :param query: query
406
+ :param packed_data: packed data
407
+ :return: rows
408
+ """
409
+
410
+ async with self._session_pool.connection() as connection:
411
+ cursor = connection.cursor()
412
+ if packed_data:
413
+ await cursor.execute(query, packed_data)
414
+ else:
415
+ await cursor.execute(query)
416
+ rows = await cursor.fetchall()
417
+ return rows
418
+
419
+ @async_retry
420
+ async def remove_matching_data(self, dfObj: pd.DataFrame, outputTableName: str, match_cols: list) -> None:
421
+ """
422
+ Remove matching data from the warehouse
423
+
424
+ :param dfObj: DataFrame
425
+ :param outputTableName: output table name
426
+ :param match_cols: list of columns
427
+ :return: None
428
+ """
429
+
430
+ df = dfObj[match_cols]
431
+ param_list = []
432
+ for column in match_cols:
433
+ param_list.append(f"{column} = %({column})s")
434
+ if len(param_list) > 1:
435
+ params = ' AND '.join(param_list)
436
+ else:
437
+ params = param_list[0]
438
+
439
+ main_dict = df.to_dict('records')
440
+ query = """DELETE FROM {} WHERE {}""".format(outputTableName, params)
441
+ await self.execute_many(query, main_dict)
442
+
443
+ @async_retry
444
+ async def export_DF_to_warehouse(self, dfObj: pd.DataFrame, outputTableName: str, columns: list, remove_nan=False) -> None:
445
+ """
446
+ Export the DataFrame to the warehouse
447
+
448
+ :param dfObj: DataFrame
449
+ :param outputTableName: output table name
450
+ :param columns: list of columns
451
+ :param remove_nan: remove NaN values
452
+ :return: None
453
+ """
454
+
455
+ col = ', '.join(columns)
456
+ param_list = []
457
+ for column in columns:
458
+ param_list.append(f"%({column})s")
459
+ params = ', '.join(param_list)
460
+
461
+ if remove_nan:
462
+ dfObj = dfObj.replace({np.nan: None})
463
+ main_dict = dfObj.to_dict('records')
464
+ for record in main_dict:
465
+ for key in record:
466
+ if record[key] == '':
467
+ record[key] = None
468
+
469
+ query = """INSERT INTO {} ({}) VALUES ({})""".format(outputTableName, col, params)
470
+ await self.execute_many(query, main_dict)
471
+
472
+ @async_retry
473
+ async def truncate_table(self, tableName: str) -> None:
474
+ """
475
+ Truncate the table
476
+
477
+ :param tableName: table name
478
+ :return: None
479
+ """
480
+
481
+ truncateQuery = """TRUNCATE TABLE {}""".format(tableName)
482
+ await self.execute(truncateQuery)
483
+
484
+ @async_retry
485
+ async def empty_table(self, tableName: str) -> None:
486
+ """
487
+ Empty the table
488
+
489
+ :param tableName: table name
490
+ :return: None
491
+ """
492
+
493
+ deleteQuery = """DELETE FROM {}""".format(tableName)
494
+ await self.execute(deleteQuery)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wcp-library
3
- Version: 1.2.8
3
+ Version: 1.3.1
4
4
  Summary: Common utilites for internal development at WCP
5
5
  Home-page: https://github.com/Whitecap-DNA/WCP-Library
6
6
  Author: Mitch-Petersen
@@ -0,0 +1,20 @@
1
+ wcp_library/__init__.py,sha256=hwLbcu00uI6L_xjXO9-I0YcODl2WtIOkdNLoDcXv7zk,591
2
+ wcp_library/credentials/__init__.py,sha256=HRmg7mqcATeclIz3lZQjSR4nmK6aY6MK9-QXEYZoFrw,1857
3
+ wcp_library/credentials/credential_manager_asynchronous.py,sha256=oTuo-TJpOWU52eaW6fhQL6QZ3zZvz_1WSpuSeA9thco,5728
4
+ wcp_library/credentials/credential_manager_synchronous.py,sha256=ouPNLt20FvocuMoFx49mbDL7-Moj_WrlpR7k97mx-i4,5512
5
+ wcp_library/credentials/ftp.py,sha256=O4oSPtCCv_0w6sLITFnY9EpN6-K2XxeibbGB0VnTHJ4,2589
6
+ wcp_library/credentials/oracle.py,sha256=m0WtmSyUdKUfsz1SPkRgc7A080rK6cq7jVoQ0YcWJ50,2867
7
+ wcp_library/credentials/postgres.py,sha256=tCCWdc10lgdu6zbU-Hv0ZxGw9rn6ZvDvFkMZqm9qfBo,2571
8
+ wcp_library/emailing.py,sha256=xqNly6Tmj-pvwl5bdys3gauZFDd4SuWCQYzGFNemv2Q,2496
9
+ wcp_library/ftp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ wcp_library/ftp/ftp.py,sha256=EpyW0J2QIGxP8zVGD4VarA0hi4C2XAPDPF-0j2sRdpI,4350
11
+ wcp_library/ftp/sftp.py,sha256=hykXGLGdxe7DYAxFdTwjPjTEOYuIpSMyK3NOiTQNUK0,4176
12
+ wcp_library/informatica.py,sha256=IXZtk_9X1XLbOEwFrsyOwTgajQKvtXgANBHmuTOP3Kk,4064
13
+ wcp_library/logging.py,sha256=e6gG7HFgUrMajUZs4Gs0atFfOJJmdmxN0GerfynNWlY,2061
14
+ wcp_library/selenium_helper.py,sha256=rlphTXsUgnbaXZknY5nfQqxFhnc7UmrpzhV3hQ-cv7k,2509
15
+ wcp_library/sql/__init__.py,sha256=s2psmwkq_ZU23iGWvXjJrLu0hD1fB6CDv6RHcK7y828,1917
16
+ wcp_library/sql/oracle.py,sha256=TGiTC5L5UcM5QcHFajgn43NI8HygOGIEAtLmLbVFp2I,15772
17
+ wcp_library/sql/postgres.py,sha256=ybk7WudcuT3_SmdkWrl01Z8rmiishHqvB1SCI9nng7g,14392
18
+ wcp_library-1.3.1.dist-info/METADATA,sha256=jBdqrTJokOY93ioq_ZLIfykk6_V6Mdvx0qFQAaWxaWE,1513
19
+ wcp_library-1.3.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
20
+ wcp_library-1.3.1.dist-info/RECORD,,
@@ -1,49 +0,0 @@
1
- import secrets
2
- import string
3
-
4
-
5
- class MissingCredentialsError(KeyError):
6
- pass
7
-
8
-
9
- async def generate_password(length: int=12, use_nums: bool=True, use_special: bool=True, special_chars_override: list=None, force_num: bool=True, force_spec: bool=True) -> str:
10
- """
11
- Function to generate a random password
12
-
13
- :param length:
14
- :param use_nums: Allows the use of numbers
15
- :param use_special: Allows the use of special characters
16
- :param special_chars_override: List of special characters to use
17
- :param force_num: Requires the password to contain at least one number
18
- :param force_spec: Requires the password to contain at least one special character
19
- :return: Password
20
- """
21
-
22
- letters = string.ascii_letters
23
- digits = string.digits
24
- if special_chars_override:
25
- special_chars = special_chars_override
26
- else:
27
- special_chars = string.punctuation
28
-
29
- alphabet = letters
30
- if use_nums:
31
- alphabet += digits
32
- if use_special:
33
- alphabet += special_chars
34
-
35
- pwd = ''
36
- for i in range(length):
37
- pwd += ''.join(secrets.choice(alphabet))
38
-
39
- if (use_nums and force_num) and (use_special and force_spec):
40
- while pwd[0].isdigit() or not any(char.isdigit() for char in pwd) or not any(char in pwd for char in special_chars):
41
- pwd = await generate_password(length, use_nums, use_special, special_chars_override, force_num, force_spec)
42
- elif use_nums and force_num:
43
- while pwd[0].isdigit() or not any(char.isdigit() for char in pwd):
44
- pwd = await generate_password(length, use_nums, use_special, special_chars_override, force_num, force_spec)
45
- elif use_special and force_spec:
46
- while not any(char in pwd for char in special_chars):
47
- pwd = await generate_password(length, use_nums, use_special, special_chars_override, force_num, force_spec)
48
-
49
- return pwd
File without changes
@@ -1,130 +0,0 @@
1
- import logging
2
-
3
- import aiohttp
4
- from yarl import URL
5
-
6
- from wcp_library.async_credentials import MissingCredentialsError
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
-
11
- class AsyncPostgresCredentialManager:
12
- 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 = 207
17
-
18
- async def _get_credentials(self) -> dict:
19
- """
20
- Get all credentials from the password list
21
-
22
- :return:
23
- """
24
-
25
- logger.debug("Getting credentials from PasswordState")
26
- url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
27
- async with aiohttp.ClientSession() as session:
28
- async with session.get(str(url), headers=self.headers) as response:
29
- passwords = await response.json()
30
-
31
- if not passwords:
32
- raise MissingCredentialsError("No credentials found in this Password List")
33
-
34
- password_dict = {}
35
- for password in passwords:
36
- password_info = {'PasswordID': password['PasswordID'], 'UserName': password['UserName'], 'Password': password['Password']}
37
- for field in password['GenericFieldInfo']:
38
- password_info[field['DisplayName']] = field['Value'].lower() if field['DisplayName'].lower() == 'username' else field['Value']
39
- password_dict[password['UserName'].lower()] = password_info
40
- logger.debug("Credentials retrieved")
41
- return password_dict
42
-
43
- async def get_credentials(self, username: str) -> dict:
44
- """
45
- Get the credentials for a specific username
46
-
47
- :param username:
48
- :return:
49
- """
50
-
51
- logger.debug(f"Getting credentials for {username}")
52
- credentials = await self._get_credentials()
53
-
54
- try:
55
- return_credential = credentials[username.lower()]
56
- except KeyError:
57
- raise MissingCredentialsError(f"Credentials for {username} not found in this Password List")
58
- logger.debug(f"Credentials for {username} retrieved")
59
- return return_credential
60
-
61
- async def update_credential(self, credentials_dict: dict) -> bool:
62
- """
63
- Update username and password in PasswordState
64
-
65
- Credentials dictionary must have the following keys:
66
- - PasswordID
67
- - UserName
68
- - Password
69
-
70
- The dictionary can be obtained from the get_credentials method
71
-
72
- :param credentials_dict:
73
- :return:
74
- """
75
-
76
- logger.debug(f"Updating credentials for {credentials_dict['UserName']}")
77
- url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
78
- async with aiohttp.ClientSession() as session:
79
- async with session.get(str(url), headers=self.headers) as response:
80
- passwords = await response.json()
81
-
82
- relevant_credential_entry = [x for x in passwords if x['UserName'] == credentials_dict['UserName']][0]
83
- for field in relevant_credential_entry['GenericFieldInfo']:
84
- if field['DisplayName'] in credentials_dict:
85
- credentials_dict[field['GenericFieldID']] = credentials_dict[field['DisplayName']]
86
- credentials_dict.pop(field['DisplayName'])
87
-
88
- async with aiohttp.ClientSession() as session:
89
- async with session.put(str(self.password_url), json=credentials_dict, headers=self.headers) as response:
90
- if response.status == 200:
91
- logger.debug(f"Credentials for {credentials_dict['UserName']} updated")
92
- return True
93
- else:
94
- logger.error(f"Failed to update credentials for {credentials_dict['UserName']}")
95
- return False
96
-
97
- async def new_credentials(self, credentials_dict: dict) -> bool:
98
- """
99
- Create a new credential entry
100
-
101
- Credentials dictionary must have the following keys:
102
- - UserName
103
- - Password
104
- - Host
105
- - Port
106
- - Database
107
-
108
- :param credentials_dict:
109
- :return:
110
- """
111
-
112
- data = {
113
- "PasswordListID": self._password_list_id,
114
- "Title": credentials_dict['UserName'].upper() if "Title" not in credentials_dict else credentials_dict['Title'].upper(),
115
- "Notes": credentials_dict['Notes'] if 'Notes' in credentials_dict else None,
116
- "UserName": credentials_dict['UserName'].lower(),
117
- "Password": credentials_dict['Password'],
118
- "GenericField1": credentials_dict['Host'],
119
- "GenericField2": credentials_dict['Port'],
120
- "GenericField3": credentials_dict['Database']
121
- }
122
-
123
- async with aiohttp.ClientSession() as session:
124
- async with session.post(str(self.password_url), json=data, headers=self.headers) as response:
125
- if response.status == 201:
126
- logger.debug(f"New credentials for {credentials_dict['UserName']} created")
127
- return True
128
- else:
129
- logger.error(f"Failed to create new credentials for {credentials_dict['UserName']}")
130
- return False
@@ -1,35 +0,0 @@
1
- import asyncio
2
- import logging
3
- from functools import wraps
4
-
5
- import oracledb
6
- import psycopg
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
-
11
- def retry(f: callable) -> callable:
12
- """
13
- Decorator to retry a function
14
-
15
- :param f: function
16
- :return: function
17
- """
18
-
19
- @wraps(f)
20
- async def wrapper(self, *args, **kwargs):
21
- self._retry_count = 0
22
- while True:
23
- try:
24
- return await f(self, *args, **kwargs)
25
- except (oracledb.OperationalError, psycopg.OperationalError) as e:
26
- error_obj, = e.args
27
- if error_obj.full_code in self.retry_error_codes and self._retry_count < self.retry_limit:
28
- self._retry_count += 1
29
- logger.debug(f"{self._db_service} connection error")
30
- logger.debug(error_obj.message)
31
- logger.info("Waiting 5 minutes before retrying Oracle connection")
32
- await asyncio.sleep(300)
33
- else:
34
- raise e
35
- return wrapper