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.
- {data_manipulation-0.36 → data_manipulation-0.39}/LICENSE +4 -4
- {data_manipulation-0.36 → data_manipulation-0.39}/PKG-INFO +12 -1
- {data_manipulation-0.36 → data_manipulation-0.39}/README.md +1 -10
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/__init__.py +4 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/_version.py +3 -3
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/base.py +28 -1
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/beautifulsoup_.py +5 -5
- data_manipulation-0.39/data_manipulation/boto3_.py +91 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/cryptography_.py +5 -2
- data_manipulation-0.39/data_manipulation/django_.py +129 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/kerberos_.py +6 -3
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/mysql_connector_python_.py +7 -4
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/openldap_.py +0 -1
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/pandas_.py +55 -7
- data_manipulation-0.39/data_manipulation/prometheus_.py +0 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/psycopg2_.py +7 -4
- data_manipulation-0.39/data_manipulation/psycopg_.py +119 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/pyspark_.py +0 -3
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/sqlalchemy_.py +7 -4
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/PKG-INFO +13 -2
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/SOURCES.txt +2 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/requires.txt +5 -4
- {data_manipulation-0.36 → data_manipulation-0.39}/setup.py +5 -4
- data_manipulation-0.36/data_manipulation/django_.py +0 -48
- data_manipulation-0.36/data_manipulation/prometheus_.py +0 -27
- {data_manipulation-0.36 → data_manipulation-0.39}/MANIFEST.in +0 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/flask_.py +0 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/geopandas_.py +0 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/smtplib_.py +0 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/dependency_links.txt +0 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/top_level.txt +0 -0
- {data_manipulation-0.36 → data_manipulation-0.39}/setup.cfg +0 -0
- {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
|
|
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
|
-
|
|
9
|
+
- Redistributions of source code must retain the above copyright notice, this
|
|
10
10
|
list of conditions and the following disclaimer.
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
11
|
+
"date": "2024-05-26T15:36:56+0800",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
logger.info(f"generate_fernet_key: True")
|
|
33
36
|
except Exception as e:
|
|
34
|
-
|
|
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
|
-
|
|
35
|
+
logger.info("keytab_valid: True")
|
|
33
36
|
else:
|
|
34
|
-
|
|
37
|
+
logger.info("keytab_valid: False")
|
|
35
38
|
except Exception as e:
|
|
36
|
-
|
|
39
|
+
logger.error(f"keytab_valid: False ({e})")
|
|
37
40
|
return output
|
|
38
41
|
|
|
39
42
|
|
{data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation/mysql_connector_python_.py
RENAMED
|
@@ -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
|
-
|
|
49
|
+
logger.info("MySQL database connected ...")
|
|
47
50
|
except mysql.connector.errors.Error as e:
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
+
logger.error(f"{e}")
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
if __name__ == "__main__":
|
|
@@ -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
|
-
|
|
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(
|
|
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 :
|
|
375
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
729
|
+
logger.error(f"Unable to get value_counts of {column} ...\n")
|
|
682
730
|
|
|
683
731
|
|
|
684
732
|
def series_count(series):
|
|
File without changes
|
|
@@ -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
|
-
|
|
45
|
+
logger.info("PostgreSQL database connected ...")
|
|
43
46
|
except psycopg2.OperationalError as e:
|
|
44
|
-
|
|
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
|
-
|
|
76
|
+
logger.info("PostgreSQL committed ...")
|
|
74
77
|
except psycopg2.OperationalError as e:
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
logger.info("create_sqlalchemy_engine: True")
|
|
91
94
|
except Exception as e:
|
|
92
|
-
|
|
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:
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -23,15 +23,16 @@ CLASSIFIERS = [
|
|
|
23
23
|
|
|
24
24
|
DEPENDENCIES = [
|
|
25
25
|
"beautifulsoup4",
|
|
26
|
+
"boto3",
|
|
26
27
|
"django",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"flask",
|
|
29
|
+
"geopandas",
|
|
29
30
|
"mysql-connector-python",
|
|
30
31
|
"pandas",
|
|
32
|
+
"psycopy",
|
|
31
33
|
"psycopg2",
|
|
32
34
|
"pyspark",
|
|
33
|
-
"
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_manipulation-0.36 → data_manipulation-0.39}/data_manipulation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|