data-manipulation 0.36__tar.gz → 0.39__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 (33) hide show
  1. {data_manipulation-0.36 → data_manipulation-0.39}/LICENSE +4 -4
  2. {data_manipulation-0.36 → data_manipulation-0.39}/PKG-INFO +12 -1
  3. {data_manipulation-0.36 → data_manipulation-0.39}/README.md +1 -10
  4. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/__init__.py +4 -0
  5. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/_version.py +3 -3
  6. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/base.py +28 -1
  7. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/beautifulsoup_.py +5 -5
  8. data_manipulation-0.39/data_manipulation/boto3_.py +91 -0
  9. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/cryptography_.py +5 -2
  10. data_manipulation-0.39/data_manipulation/django_.py +129 -0
  11. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/kerberos_.py +6 -3
  12. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/mysql_connector_python_.py +7 -4
  13. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/openldap_.py +0 -1
  14. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/pandas_.py +55 -7
  15. data_manipulation-0.39/data_manipulation/prometheus_.py +0 -0
  16. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/psycopg2_.py +7 -4
  17. data_manipulation-0.39/data_manipulation/psycopg_.py +119 -0
  18. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/pyspark_.py +0 -3
  19. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/sqlalchemy_.py +7 -4
  20. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/PKG-INFO +13 -2
  21. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/SOURCES.txt +2 -0
  22. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/requires.txt +5 -4
  23. {data_manipulation-0.36 → data_manipulation-0.39}/setup.py +5 -4
  24. data_manipulation-0.36/data_manipulation/django_.py +0 -48
  25. data_manipulation-0.36/data_manipulation/prometheus_.py +0 -27
  26. {data_manipulation-0.36 → data_manipulation-0.39}/MANIFEST.in +0 -0
  27. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/flask_.py +0 -0
  28. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/geopandas_.py +0 -0
  29. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/smtplib_.py +0 -0
  30. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/dependency_links.txt +0 -0
  31. {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/top_level.txt +0 -0
  32. {data_manipulation-0.36 → data_manipulation-0.39}/setup.cfg +0 -0
  33. {data_manipulation-0.36 → data_manipulation-0.39}/versioneer.py +0 -0
@@ -1,19 +1,19 @@
1
1
  BSD 3-Clause License
2
2
 
3
- Copyright (c) 2020, Ng Teng Qing (shawnngtq)
3
+ Copyright (c) 2020, Shawn Ng (shawnngtq)
4
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without modification,
7
7
  are permitted provided that the following conditions are met:
8
8
 
9
- * Redistributions of source code must retain the above copyright notice, this
9
+ - Redistributions of source code must retain the above copyright notice, this
10
10
  list of conditions and the following disclaimer.
11
11
 
12
- * Redistributions in binary form must reproduce the above copyright notice, this
12
+ - Redistributions in binary form must reproduce the above copyright notice, this
13
13
  list of conditions and the following disclaimer in the documentation and/or
14
14
  other materials provided with the distribution.
15
15
 
16
- * Neither the name of data_manipulation nor the names of its
16
+ - Neither the name of data_manipulation nor the names of its
17
17
  contributors may be used to endorse or promote products derived from this
18
18
  software without specific prior written permission.
19
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: data_manipulation
3
- Version: 0.36
3
+ Version: 0.39
4
4
  Summary: Powerful data manipulation
5
5
  Home-page: https://github.com/shawnngtq/data-manipulation
6
6
  Author: Shawn Ng
@@ -19,6 +19,17 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Cython
20
20
  Classifier: Topic :: Scientific/Engineering
21
21
  License-File: LICENSE
22
+ Requires-Dist: beautifulsoup4
23
+ Requires-Dist: boto3
24
+ Requires-Dist: django
25
+ Requires-Dist: flask
26
+ Requires-Dist: geopandas
27
+ Requires-Dist: mysql-connector-python
28
+ Requires-Dist: pandas
29
+ Requires-Dist: psycopy
30
+ Requires-Dist: psycopg2
31
+ Requires-Dist: pyspark
32
+ Requires-Dist: sqlalchemy
22
33
 
23
34
 
24
35
  Data Manipulation is a Python package providing powerful utility functions.
@@ -25,16 +25,7 @@ pip install data-manipulation
25
25
 
26
26
  ## Dependencies
27
27
 
28
- The dependencies will be installed automatically along with this package
29
-
30
- - beautifulsoup4
31
- - django
32
- - numpy
33
- - pandas
34
- - ipython
35
- - requests
36
- - psycopg2
37
- - pyspark
28
+ The dependencies will be installed automatically along with this package, in `setup.py`
38
29
 
39
30
  ## License
40
31
 
@@ -1,14 +1,18 @@
1
1
  from data_manipulation import (
2
2
  base,
3
3
  beautifulsoup_,
4
+ boto3_,
4
5
  cryptography_,
5
6
  django_,
7
+ flask_,
6
8
  geopandas_,
7
9
  kerberos_,
8
10
  mysql_connector_python_,
9
11
  openldap_,
10
12
  pandas_,
13
+ prometheus_,
11
14
  psycopg2_,
15
+ psycopg_,
12
16
  pyspark_,
13
17
  smtplib_,
14
18
  sqlalchemy_,
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2023-10-03T09:27:54+0800",
11
+ "date": "2024-05-26T15:36:56+0800",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "263a1314653b0a2f32b9e1eea3efbf78ad990ca2",
15
- "version": "0.36"
14
+ "full-revisionid": "2c9d748a4051bd9c0d34c3dc58adb3d06d8b0a5b",
15
+ "version": "0.39"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -1,6 +1,10 @@
1
+ import logging
1
2
  import subprocess
2
3
  from typing import List
3
4
 
5
+ logging.basicConfig(level=logging.INFO)
6
+ logger = logging.getLogger(__file__)
7
+
4
8
 
5
9
  # DATA STRUCTURE
6
10
  def clean_string(
@@ -109,6 +113,7 @@ def get_country_name_variation() -> dict:
109
113
  "SLAVONIC": "SK",
110
114
  "SLOVAK REPUBLIC": "SK",
111
115
  "TURKEY": "TR",
116
+ "TURKIYE": "TR",
112
117
  "UNITED STATES": "US",
113
118
  }
114
119
  return variations
@@ -335,7 +340,7 @@ def remove_path_file(path: str, keyword: str, n: int = 2) -> None:
335
340
  to_delete = get_path_files(path=path, keywords=[keyword])[:-n]
336
341
  for file in to_delete:
337
342
  os.remove(f"{path}/{file}")
338
- print(f"{path}/{file} deleted ...")
343
+ logger.info(f"{path}/{file} deleted ...")
339
344
 
340
345
 
341
346
  def list_to_file(filepath: str, list_: list, newline: bool = True) -> None:
@@ -363,6 +368,28 @@ def list_to_file(filepath: str, list_: list, newline: bool = True) -> None:
363
368
  f.close()
364
369
 
365
370
 
371
+ # URLLIB
372
+ def create_encode_url(url: str, query_params: dict = {}) -> str:
373
+ """
374
+ Create encoded url
375
+
376
+ Parameters
377
+ ----------
378
+ url : str
379
+ base url
380
+ query_params : dict, optional
381
+ dictionary of url query parameters, by default {}
382
+
383
+ Returns
384
+ -------
385
+ str
386
+ encoded url
387
+ """
388
+ from urllib.parse import urlencode
389
+
390
+ return f"{url}{urlencode(query_params)}"
391
+
392
+
366
393
  # SYSTEM
367
394
  def parse_ps_aux(ps_aux_commands: str) -> List[list]:
368
395
  """
@@ -1,6 +1,8 @@
1
+ import logging
1
2
  from typing import Optional
2
3
 
3
- from bs4 import BeautifulSoup
4
+ logging.basicConfig(level=logging.INFO)
5
+ logger = logging.getLogger(__file__)
4
6
 
5
7
 
6
8
  def preprocess(html: str) -> Optional[str]:
@@ -37,9 +39,7 @@ def preprocess(html: str) -> Optional[str]:
37
39
  return html
38
40
 
39
41
 
40
- def build_soup(
41
- url: str, features: str = "lxml", to_preprocess: str = True
42
- ) -> Optional[BeautifulSoup]:
42
+ def build_soup(url: str, features: str = "lxml", to_preprocess: str = True):
43
43
  """
44
44
  Return Beautifulsoup object from given url
45
45
 
@@ -74,7 +74,7 @@ def build_soup(
74
74
  soup = BeautifulSoup(response.text, features=features)
75
75
  return soup
76
76
  else:
77
- print(f"response status code: {response.status_code}")
77
+ logger.error(f"response status code: {response.status_code}")
78
78
 
79
79
 
80
80
  if __name__ == "__main__":
@@ -0,0 +1,91 @@
1
+ import logging
2
+
3
+ logging.basicConfig(level=logging.INFO)
4
+ logger = logging.getLogger(__file__)
5
+
6
+
7
+ def send_aws_ses_email(
8
+ sender: str,
9
+ recipient: list,
10
+ subject: str,
11
+ body_text: str,
12
+ body_type: str,
13
+ ses_client,
14
+ attachment: str = None,
15
+ ):
16
+ """
17
+ Send AWS SES email
18
+
19
+ Parameters
20
+ ----------
21
+ sender : str
22
+ sender email
23
+ recipient : list
24
+ list of recipient emails
25
+ subject : str
26
+ email subject
27
+ body_text : str
28
+ email body
29
+ body_type : str
30
+ email body type
31
+ ses_client : _type_
32
+ aws ses client
33
+ attachment : str, optional
34
+ attachment path, by default None
35
+
36
+ Returns
37
+ -------
38
+ dict
39
+ aws ses client email response or none
40
+
41
+ Reference
42
+ ---------
43
+ - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses/client/send_raw_email.html
44
+ """
45
+ import os
46
+ from email.mime.application import MIMEApplication
47
+ from email.mime.multipart import MIMEMultipart
48
+ from email.mime.text import MIMEText
49
+
50
+ from botocore.exceptions import BotoCoreError, ClientError
51
+
52
+ msg = MIMEMultipart()
53
+ msg["Subject"] = subject
54
+ msg["From"] = sender
55
+ msg["To"] = ", ".join(recipient)
56
+
57
+ part = MIMEText(body_text, body_type)
58
+ msg.attach(part)
59
+
60
+ if attachment:
61
+ try:
62
+ with open(attachment, "rb") as f:
63
+ part = MIMEApplication(f.read())
64
+ part.add_header(
65
+ "Content-Disposition",
66
+ "attachment",
67
+ filename=os.path.basename(attachment),
68
+ )
69
+ msg.attach(part)
70
+ except FileNotFoundError:
71
+ logger.error(f"{attachment} not found")
72
+ return
73
+
74
+ try:
75
+ response = ses_client.send_raw_email(
76
+ Source=sender,
77
+ Destinations=recipient,
78
+ RawMessage={
79
+ "Data": msg.as_string(),
80
+ },
81
+ )
82
+ return response
83
+ except (BotoCoreError, ClientError) as error:
84
+ logger.error(f"Error: {error}")
85
+ return
86
+
87
+
88
+ if __name__ == "__main__":
89
+ import doctest
90
+
91
+ doctest.testmod()
@@ -2,6 +2,9 @@ import logging
2
2
 
3
3
  from cryptography.fernet import Fernet
4
4
 
5
+ logging.basicConfig(level=logging.INFO)
6
+ logger = logging.getLogger(__file__)
7
+
5
8
 
6
9
  def generate_fernet_key(output_directory: str, output_filename: str) -> bytes:
7
10
  """
@@ -29,9 +32,9 @@ def generate_fernet_key(output_directory: str, output_filename: str) -> bytes:
29
32
  f = open(filepath, "wb")
30
33
  f.write(key)
31
34
  f.close()
32
- logging.info(f"generate_fernet_key: True")
35
+ logger.info(f"generate_fernet_key: True")
33
36
  except Exception as e:
34
- logging.error(f"generate_fernet_key: False ({e})")
37
+ logger.error(f"generate_fernet_key: False ({e})")
35
38
  return key
36
39
 
37
40
 
@@ -0,0 +1,129 @@
1
+ import logging
2
+
3
+ logging.basicConfig(level=logging.INFO)
4
+ logger = logging.getLogger(__file__)
5
+
6
+
7
+ def init_django(django_dir: str, project_name: str) -> None:
8
+ """
9
+ Setup Django in Jupyter. Reference from https://gist.github.com/codingforentrepreneurs/76e570d759f83d690bf36a8a8fa4cfbe
10
+
11
+ Parameters
12
+ ----------
13
+ django_dir : str
14
+ Django project location
15
+ project_name : str
16
+ Django project name
17
+ """
18
+ import os
19
+ import sys
20
+
21
+ import django
22
+
23
+ # django_dir = os.getenv("PWD")
24
+ # os.chdir(django_dir)
25
+
26
+ project_name = project_name or os.environ.get("DJANGO_PROJECT") or None
27
+ if not project_name:
28
+ raise Exception(
29
+ """Set an environment variable: `DJANGO_PROJECT=your_project_name` or call `init_django(your_project_name)`"""
30
+ )
31
+
32
+ sys.path.insert(0, django_dir)
33
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{project_name}.settings")
34
+ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
35
+ django.setup()
36
+
37
+
38
+ def get_django_countries_dict():
39
+ """
40
+ Get django_countries package's dictionaries
41
+ code_name - "SG": "SINGAPORE"
42
+ name_code - "SINGAPORE": "SG"
43
+ """
44
+ from django_countries import countries
45
+
46
+ code_name = {k: v.upper() for k, v in dict(countries).items()}
47
+ name_code = {v: k for k, v in code_name.items()}
48
+ return code_name, name_code
49
+
50
+
51
+ def django_validate_email(email: str) -> str | None:
52
+ """
53
+ Django validate email
54
+
55
+ Parameters
56
+ ----------
57
+ email : str
58
+ email to check
59
+
60
+ Returns
61
+ -------
62
+ str | None
63
+ email or none
64
+ """
65
+ from django.core.validators import validate_email
66
+
67
+ try:
68
+ validate_email(email)
69
+ return email
70
+ except Exception as e:
71
+ return
72
+
73
+
74
+ def django_validate_url(url: str) -> str | None:
75
+ """
76
+ Django validate url
77
+
78
+ Parameters
79
+ ----------
80
+ url : str
81
+ url to validate
82
+
83
+ Returns
84
+ -------
85
+ str | None
86
+ url or None
87
+ """
88
+ from django.core.validators import URLValidator
89
+
90
+ validator = URLValidator()
91
+ try:
92
+ validator(url)
93
+ return url
94
+ except Exception as e:
95
+ return
96
+
97
+
98
+ def django_validate_phone(phone: str, region=None) -> str | None:
99
+ """
100
+ Django validate phone
101
+
102
+ Parameters
103
+ ----------
104
+ phone : str
105
+ phone to check
106
+ region : _type_, optional
107
+ phone region, by default None
108
+
109
+ Returns
110
+ -------
111
+ str | None
112
+ parsed phone or none
113
+ """
114
+ from phonenumber_field.phonenumber import PhoneNumber
115
+
116
+ try:
117
+ return PhoneNumber.from_string(phone).as_e164
118
+ except:
119
+ try:
120
+ return PhoneNumber.from_string(phone, region=region).as_e164
121
+ except Exception as e:
122
+ logger.error(e)
123
+ return
124
+
125
+
126
+ if __name__ == "__main__":
127
+ import doctest
128
+
129
+ doctest.testmod()
@@ -1,6 +1,9 @@
1
1
  import logging
2
2
  import subprocess
3
3
 
4
+ logging.basicConfig(level=logging.INFO)
5
+ logger = logging.getLogger(__file__)
6
+
4
7
 
5
8
  def keytab_valid(
6
9
  keytab_filepath: str,
@@ -29,11 +32,11 @@ def keytab_valid(
29
32
  )
30
33
  try:
31
34
  if output.returncode == 0:
32
- logging.info("keytab_valid: True")
35
+ logger.info("keytab_valid: True")
33
36
  else:
34
- logging.info("keytab_valid: False")
37
+ logger.info("keytab_valid: False")
35
38
  except Exception as e:
36
- logging.error(f"keytab_valid: False ({e})")
39
+ logger.error(f"keytab_valid: False ({e})")
37
40
  return output
38
41
 
39
42
 
@@ -3,6 +3,9 @@ from typing import Optional, Union
3
3
 
4
4
  import mysql.connector
5
5
 
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__file__)
8
+
6
9
 
7
10
  def create_connection(
8
11
  host: str,
@@ -43,9 +46,9 @@ def create_connection(
43
46
  password=password,
44
47
  port=port,
45
48
  )
46
- logging.info("MySQL database connected ...")
49
+ logger.info("MySQL database connected ...")
47
50
  except mysql.connector.errors.Error as e:
48
- logging.error(f"{e}")
51
+ logger.error(f"{e}")
49
52
  return connection
50
53
 
51
54
 
@@ -79,12 +82,12 @@ def execute_query(
79
82
  cursor.execute(sql_query, data)
80
83
  if commit:
81
84
  connection.commit()
82
- logging.info("MySQL committed ...")
85
+ logger.info("MySQL committed ...")
83
86
  id = cursor.lastrowid
84
87
  cursor.close()
85
88
  return id
86
89
  except mysql.connector.errors.Error as e:
87
- logging.error(f"{e}")
90
+ logger.error(f"{e}")
88
91
 
89
92
 
90
93
  if __name__ == "__main__":
@@ -1,4 +1,3 @@
1
- import logging
2
1
  import subprocess
3
2
 
4
3
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import subprocess
2
3
  from itertools import combinations
3
4
 
@@ -5,6 +6,9 @@ import numpy as np
5
6
  import pandas as pd
6
7
  from IPython.display import display
7
8
 
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__file__)
11
+
8
12
 
9
13
  # CONFIG
10
14
  def config_pandas_display():
@@ -226,7 +230,7 @@ def compare_dataframes(dataframe, dataframe2):
226
230
  try:
227
231
  pd_df2 = dataframe2[dataframe2[column].notnull()]
228
232
  except KeyError:
229
- print(f"df2 {column}: does not exist")
233
+ logger.error(f"df2 {column}: does not exist")
230
234
 
231
235
  if pd_df2 is not None:
232
236
  print(f"df2 non-null {column}: {len(pd_df2)}")
@@ -364,15 +368,56 @@ def split_left_merged_dataframe(dataframe, dataframe2, columns):
364
368
  return df_both, df_left
365
369
 
366
370
 
371
+ ## AGGREGATE
372
+ def aggregate_set_without_none(column: pd.Series, nested_set: bool = False) -> set:
373
+ """
374
+ Use in pandas groupby agg, create a set without none based on input column
375
+
376
+ Parameters
377
+ ----------
378
+ column : pd.Series
379
+ columns to set
380
+ nested_set : bool, optional
381
+ nested set within set, by default False
382
+
383
+ Returns
384
+ -------
385
+ set
386
+ set without None
387
+ """
388
+ if nested_set:
389
+ output = set()
390
+ for value in column:
391
+ if isinstance(value, set):
392
+ output.update(value)
393
+ # todo, what if there is valid list / dict / tuple / complex object?
394
+ elif value:
395
+ output.add(value)
396
+ return output
397
+ else:
398
+ return {value for value in column if value is not None}
399
+
400
+
367
401
  # DATA STRUCTURE
368
- def clean_none(dataframe, clean_variation=True, non_variations=[]):
402
+ def clean_none(
403
+ dataframe: pd.DataFrame,
404
+ nan_to_none: bool = True,
405
+ clean_variation: bool = True,
406
+ none_variations: list = [],
407
+ ) -> pd.DataFrame:
369
408
  """
370
409
  Return a dataframe from given dataframe with standardized None. Deprecated as of pandas 1.3.0.
371
410
 
372
411
  Parameters
373
412
  ----------
374
- dataframe : pandas.DataFrame
375
- Dataframe
413
+ dataframe : pd.DataFrame
414
+ _description_
415
+ nan_to_none : bool, optional
416
+ replace np.NaN with None, by default True
417
+ clean_variation : bool, optional
418
+ clean custom variables, by default True
419
+ none_variations : list, optional
420
+ list of none variations, by default []
376
421
 
377
422
  Examples
378
423
  --------
@@ -410,12 +455,15 @@ def clean_none(dataframe, clean_variation=True, non_variations=[]):
410
455
 
411
456
  Returns
412
457
  -------
413
- df : pandas.DataFrame
458
+ pd.DataFrame
459
+ _description_
414
460
  """
415
461
  df = dataframe.copy()
416
462
  df = df.replace(r"^\s*$", np.nan, regex=True)
463
+ if nan_to_none:
464
+ df = df.replace(np.NaN, None, inplace=True)
417
465
  if clean_variation:
418
- df = df.replace(non_variations, np.nan)
466
+ df = df.replace(none_variations, np.nan)
419
467
  df = df.where(pd.notnull(df), None)
420
468
  return df
421
469
 
@@ -678,7 +726,7 @@ def print_dataframe_overview(dataframe, stats=False):
678
726
  print(f"Maximum: {max(dataframe[column])}")
679
727
  display(series_count(dataframe[column]).head(20))
680
728
  except:
681
- print(f"Unable to get value_counts of {column} ...\n")
729
+ logger.error(f"Unable to get value_counts of {column} ...\n")
682
730
 
683
731
 
684
732
  def series_count(series):
@@ -4,6 +4,9 @@ from typing import Optional, Union
4
4
  import pandas as pd
5
5
  import psycopg2
6
6
 
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__file__)
9
+
7
10
 
8
11
  def create_connection(
9
12
  host: str,
@@ -39,9 +42,9 @@ def create_connection(
39
42
  f"host={host} port={port} dbname={dbname} user={user} password={password}"
40
43
  )
41
44
  connection = psycopg2.connect(connection_string)
42
- logging.info("PostgreSQL database connected ...")
45
+ logger.info("PostgreSQL database connected ...")
43
46
  except psycopg2.OperationalError as e:
44
- logging.error(f"{e}")
47
+ logger.error(f"{e}")
45
48
  return connection
46
49
 
47
50
 
@@ -70,9 +73,9 @@ def execute_sql(
70
73
  cursor.execute(sql_query, data)
71
74
  if commit:
72
75
  connection.commit()
73
- logging.info("PostgreSQL committed ...")
76
+ logger.info("PostgreSQL committed ...")
74
77
  except psycopg2.OperationalError as e:
75
- logging.error(f"{e}")
78
+ logger.error(f"{e}")
76
79
 
77
80
 
78
81
  def query_to_pandas(
@@ -0,0 +1,119 @@
1
+ import logging
2
+ from typing import Optional, Union
3
+
4
+ import pandas as pd
5
+ import psycopg
6
+
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__file__)
9
+
10
+
11
+ def create_connection(
12
+ host: str,
13
+ dbname: str,
14
+ user: str,
15
+ password: str,
16
+ port=5432,
17
+ ) -> psycopg.Connection:
18
+ """
19
+ Return psycopg connection (https://www.psycopg.org/)
20
+
21
+ Parameters
22
+ ----------
23
+ host : str
24
+ Database host address
25
+ dbname : str
26
+ Database name
27
+ user : str
28
+ User used to authenticate
29
+ password : str
30
+ Password used to authenticate
31
+ port : int, optional
32
+ Port number, by default 5432
33
+
34
+ Returns
35
+ -------
36
+ psycopg.Connection
37
+ Connection object
38
+ """
39
+ connection = None
40
+ try:
41
+ connection_string = (
42
+ f"host={host} port={port} dbname={dbname} user={user} password={password}"
43
+ )
44
+ connection = psycopg.connect(connection_string)
45
+ logger.info("PostgreSQL database connected ...")
46
+ except psycopg.OperationalError as e:
47
+ logger.error(f"{e}")
48
+ return connection
49
+
50
+
51
+ def execute_sql(
52
+ connection: psycopg.Connection,
53
+ sql_query: str,
54
+ data: Union[dict, tuple],
55
+ commit=True,
56
+ ) -> None:
57
+ """
58
+ Execute and commit PostgreSQL query
59
+
60
+ Parameters
61
+ ----------
62
+ connection : psycopg.Connection
63
+ _description_
64
+ sql_query : str
65
+ SQL query
66
+ data : Union[dict, tuple]
67
+ Data
68
+ commit : bool, optional
69
+ Make database change persistent, by default True
70
+
71
+ Reference
72
+ ---------
73
+ When you insert data key-value with dict type, error
74
+ ProgrammingError: cannot adapt type 'dict' using placeholder '%s' (format: AUTO)
75
+ """
76
+ cursor = connection.cursor()
77
+ try:
78
+ cursor.execute(sql_query, data)
79
+ if commit:
80
+ connection.commit()
81
+ logger.info("PostgreSQL committed ...")
82
+ except psycopg.OperationalError as e:
83
+ logger.error(f"{e}")
84
+
85
+
86
+ def query_to_pandas(
87
+ connection: psycopg.Connection,
88
+ sql_query: str,
89
+ ) -> pd.DataFrame:
90
+ """
91
+ Return Pandas dataframe from result of given SQL query
92
+
93
+ Parameters
94
+ ----------
95
+ connection : psycopg.Connection
96
+ _description_
97
+ sql_query : str
98
+ SQL query
99
+
100
+ Examples
101
+ --------
102
+ >>> sql_query = '''SELECT * FROM users;'''
103
+
104
+ Returns
105
+ -------
106
+ pd.DataFrame
107
+ _description_
108
+ """
109
+
110
+ return pd.read_sql(
111
+ sql=sql_query,
112
+ con=connection,
113
+ )
114
+
115
+
116
+ if __name__ == "__main__":
117
+ import doctest
118
+
119
+ doctest.testmod()
@@ -2,8 +2,6 @@ import pandas as pd
2
2
  import pyspark
3
3
  import pyspark.sql.functions as F
4
4
 
5
- spark = pyspark.sql.SparkSession.builder.master("local").getOrCreate()
6
-
7
5
 
8
6
  # CONFIG
9
7
  def config_spark_local(autoset=True):
@@ -84,7 +82,6 @@ def config_spark_local(autoset=True):
84
82
  print(f"spark_default_parallelism: {spark_default_parallelism}")
85
83
 
86
84
  if autoset:
87
- global spark
88
85
  spark = (
89
86
  pyspark.sql.SparkSession.builder.master("local")
90
87
  .config("spark.executor.cores", str(spark_executor_cores))
@@ -2,6 +2,9 @@ import logging
2
2
 
3
3
  import sqlalchemy
4
4
 
5
+ logging.basicConfig(level=logging.INFO)
6
+ logger = logging.getLogger(__file__)
7
+
5
8
 
6
9
  def create_sqlalchemy_url(
7
10
  drivername: str,
@@ -45,7 +48,7 @@ def create_sqlalchemy_url(
45
48
  return url
46
49
 
47
50
 
48
- def check_connection_valid(
51
+ def create_sqlalchemy_engine(
49
52
  drivername: str,
50
53
  host: str,
51
54
  dbname: str,
@@ -54,7 +57,7 @@ def check_connection_valid(
54
57
  port=3306,
55
58
  ) -> sqlalchemy.engine.base.Engine:
56
59
  """
57
- Check mysql connection validity
60
+ Create SQLalchemy engine
58
61
 
59
62
  Parameters
60
63
  ----------
@@ -87,9 +90,9 @@ def check_connection_valid(
87
90
  engine = sqlalchemy.create_engine(url)
88
91
  try:
89
92
  engine.connect()
90
- logging.info("check_connection_valid: True")
93
+ logger.info("create_sqlalchemy_engine: True")
91
94
  except Exception as e:
92
- logging.error(f"check_connection_valid: False ({e})")
95
+ logger.error(f"create_sqlalchemy_engine: False ({e})")
93
96
  return engine
94
97
 
95
98
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: data-manipulation
3
- Version: 0.36
2
+ Name: data_manipulation
3
+ Version: 0.39
4
4
  Summary: Powerful data manipulation
5
5
  Home-page: https://github.com/shawnngtq/data-manipulation
6
6
  Author: Shawn Ng
@@ -19,6 +19,17 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Cython
20
20
  Classifier: Topic :: Scientific/Engineering
21
21
  License-File: LICENSE
22
+ Requires-Dist: beautifulsoup4
23
+ Requires-Dist: boto3
24
+ Requires-Dist: django
25
+ Requires-Dist: flask
26
+ Requires-Dist: geopandas
27
+ Requires-Dist: mysql-connector-python
28
+ Requires-Dist: pandas
29
+ Requires-Dist: psycopy
30
+ Requires-Dist: psycopg2
31
+ Requires-Dist: pyspark
32
+ Requires-Dist: sqlalchemy
22
33
 
23
34
 
24
35
  Data Manipulation is a Python package providing powerful utility functions.
@@ -8,6 +8,7 @@ data_manipulation/__init__.py
8
8
  data_manipulation/_version.py
9
9
  data_manipulation/base.py
10
10
  data_manipulation/beautifulsoup_.py
11
+ data_manipulation/boto3_.py
11
12
  data_manipulation/cryptography_.py
12
13
  data_manipulation/django_.py
13
14
  data_manipulation/flask_.py
@@ -18,6 +19,7 @@ data_manipulation/openldap_.py
18
19
  data_manipulation/pandas_.py
19
20
  data_manipulation/prometheus_.py
20
21
  data_manipulation/psycopg2_.py
22
+ data_manipulation/psycopg_.py
21
23
  data_manipulation/pyspark_.py
22
24
  data_manipulation/smtplib_.py
23
25
  data_manipulation/sqlalchemy_.py
@@ -1,10 +1,11 @@
1
1
  beautifulsoup4
2
+ boto3
2
3
  django
3
- lxml
4
- matplotlib
4
+ flask
5
+ geopandas
5
6
  mysql-connector-python
6
7
  pandas
8
+ psycopy
7
9
  psycopg2
8
10
  pyspark
9
- tqdm
10
- xlsxwriter
11
+ sqlalchemy
@@ -23,15 +23,16 @@ CLASSIFIERS = [
23
23
 
24
24
  DEPENDENCIES = [
25
25
  "beautifulsoup4",
26
+ "boto3",
26
27
  "django",
27
- "lxml",
28
- "matplotlib",
28
+ "flask",
29
+ "geopandas",
29
30
  "mysql-connector-python",
30
31
  "pandas",
32
+ "psycopy",
31
33
  "psycopg2",
32
34
  "pyspark",
33
- "tqdm",
34
- "xlsxwriter",
35
+ "sqlalchemy",
35
36
  ]
36
37
 
37
38
  setup(
@@ -1,48 +0,0 @@
1
- def init_django(django_dir: str, project_name: str) -> None:
2
- """
3
- Setup Django in Jupyter. Reference from https://gist.github.com/codingforentrepreneurs/76e570d759f83d690bf36a8a8fa4cfbe
4
-
5
- Parameters
6
- ----------
7
- django_dir : str
8
- Django project location
9
- project_name : str
10
- Django project name
11
- """
12
- import os
13
- import sys
14
-
15
- import django
16
-
17
- # django_dir = os.getenv("PWD")
18
- # os.chdir(django_dir)
19
-
20
- project_name = project_name or os.environ.get("DJANGO_PROJECT") or None
21
- if not project_name:
22
- raise Exception(
23
- """Set an environment variable: `DJANGO_PROJECT=your_project_name` or call `init_django(your_project_name)`"""
24
- )
25
-
26
- sys.path.insert(0, django_dir)
27
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{project_name}.settings")
28
- os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
29
- django.setup()
30
-
31
-
32
- def get_django_countries_dict():
33
- """
34
- Get django_countries package's dictionaries
35
- code_name - "SG": "SINGAPORE"
36
- name_code - "SINGAPORE": "SG"
37
- """
38
- from django_countries import countries
39
-
40
- code_name = {k: v.upper() for k, v in dict(countries).items()}
41
- name_code = {v: k for k, v in code_name.items()}
42
- return code_name, name_code
43
-
44
-
45
- if __name__ == "__main__":
46
- import doctest
47
-
48
- doctest.testmod()
@@ -1,27 +0,0 @@
1
- import logging
2
- import subprocess
3
-
4
-
5
- def ldapsearch(
6
- search_base: str,
7
- ldap_uri: str,
8
- bind_dn: str,
9
- password: str,
10
- filter: str,
11
- ) -> subprocess.CompletedProcess:
12
- cmd = (
13
- "ldapsearch -b {search_base} -H {ldap_uri} -D {bind_dn} -w {password} {filter}"
14
- )
15
- output = subprocess.run(
16
- cmd,
17
- capture_output=True,
18
- shell=True,
19
- text=True,
20
- )
21
- return output
22
-
23
-
24
- if __name__ == "__main__":
25
- import doctest
26
-
27
- doctest.testmod()