data-manipulation 0.35__tar.gz → 0.37__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.35 → data_manipulation-0.37}/LICENSE +4 -4
- {data_manipulation-0.35 → data_manipulation-0.37}/PKG-INFO +12 -1
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/__init__.py +8 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/_version.py +3 -3
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/base.py +40 -122
- data_manipulation-0.37/data_manipulation/boto3_.py +82 -0
- data_manipulation-0.37/data_manipulation/cryptography_.py +91 -0
- data_manipulation-0.37/data_manipulation/django_.py +123 -0
- data_manipulation-0.37/data_manipulation/kerberos_.py +43 -0
- data_manipulation-0.37/data_manipulation/openldap_.py +27 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/pandas_.py +49 -5
- data_manipulation-0.37/data_manipulation/prometheus_.py +26 -0
- data_manipulation-0.37/data_manipulation/smtplib_.py +70 -0
- data_manipulation-0.37/data_manipulation/sqlalchemy_.py +99 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/PKG-INFO +12 -1
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/SOURCES.txt +7 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/setup.py +1 -0
- data_manipulation-0.35/data_manipulation/django_.py +0 -33
- {data_manipulation-0.35 → data_manipulation-0.37}/MANIFEST.in +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/README.md +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/beautifulsoup_.py +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/flask_.py +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/geopandas_.py +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/mysql_connector_python_.py +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/psycopg2_.py +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/pyspark_.py +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/dependency_links.txt +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/requires.txt +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/top_level.txt +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/setup.cfg +0 -0
- {data_manipulation-0.35 → data_manipulation-0.37}/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.37
|
|
4
4
|
Summary: Powerful data manipulation
|
|
5
5
|
Home-page: https://github.com/shawnngtq/data-manipulation
|
|
6
6
|
Author: Shawn Ng
|
|
@@ -15,9 +15,20 @@ Classifier: Programming Language :: Python
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
19
|
Classifier: Programming Language :: Cython
|
|
19
20
|
Classifier: Topic :: Scientific/Engineering
|
|
20
21
|
License-File: LICENSE
|
|
22
|
+
Requires-Dist: beautifulsoup4
|
|
23
|
+
Requires-Dist: django
|
|
24
|
+
Requires-Dist: lxml
|
|
25
|
+
Requires-Dist: matplotlib
|
|
26
|
+
Requires-Dist: mysql-connector-python
|
|
27
|
+
Requires-Dist: pandas
|
|
28
|
+
Requires-Dist: psycopg2
|
|
29
|
+
Requires-Dist: pyspark
|
|
30
|
+
Requires-Dist: tqdm
|
|
31
|
+
Requires-Dist: xlsxwriter
|
|
21
32
|
|
|
22
33
|
|
|
23
34
|
Data Manipulation is a Python package providing powerful utility functions.
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
from data_manipulation import (
|
|
2
2
|
base,
|
|
3
3
|
beautifulsoup_,
|
|
4
|
+
boto3_,
|
|
5
|
+
cryptography_,
|
|
4
6
|
django_,
|
|
7
|
+
flask_,
|
|
5
8
|
geopandas_,
|
|
9
|
+
kerberos_,
|
|
6
10
|
mysql_connector_python_,
|
|
11
|
+
openldap_,
|
|
7
12
|
pandas_,
|
|
13
|
+
prometheus_,
|
|
8
14
|
psycopg2_,
|
|
9
15
|
pyspark_,
|
|
16
|
+
smtplib_,
|
|
17
|
+
sqlalchemy_,
|
|
10
18
|
)
|
|
11
19
|
|
|
12
20
|
from . import _version
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "
|
|
11
|
+
"date": "2023-12-30T10:16:23+0800",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.
|
|
14
|
+
"full-revisionid": "6f824ae7f46ddf5b2f60190bacfc2e4118d7f80f",
|
|
15
|
+
"version": "0.37"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -97,6 +97,24 @@ def get_none_variation() -> list:
|
|
|
97
97
|
return variations
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def get_country_name_variation() -> dict:
|
|
101
|
+
variations = {
|
|
102
|
+
"AFRICA": "BW",
|
|
103
|
+
"BOSNIA": "BA",
|
|
104
|
+
"CZECH REPUBLIC": "CZ",
|
|
105
|
+
"MACEDONIA": "MK",
|
|
106
|
+
"REPUBLIC OF CHINA": "CN",
|
|
107
|
+
"REPUBLIC OF KOREA": "KR",
|
|
108
|
+
"RUSSIAN FEDERATION": "RU",
|
|
109
|
+
"SLAVONIC": "SK",
|
|
110
|
+
"SLOVAK REPUBLIC": "SK",
|
|
111
|
+
"TURKEY": "TR",
|
|
112
|
+
"TURKIYE": "TR",
|
|
113
|
+
"UNITED STATES": "US",
|
|
114
|
+
}
|
|
115
|
+
return variations
|
|
116
|
+
|
|
117
|
+
|
|
100
118
|
def list_tuple_without_none(list_tuple: list | tuple) -> list | tuple:
|
|
101
119
|
"""
|
|
102
120
|
Return the given list / tuple without None variation
|
|
@@ -258,128 +276,6 @@ def delete_list_indices(list_: list, indices: list) -> None:
|
|
|
258
276
|
del list_[index]
|
|
259
277
|
|
|
260
278
|
|
|
261
|
-
# DECRYPT / ENCRYPT
|
|
262
|
-
def encrypt_fernet_file(keypath: str, filepath: str) -> str:
|
|
263
|
-
"""
|
|
264
|
-
Encrypt file
|
|
265
|
-
|
|
266
|
-
Parameters
|
|
267
|
-
----------
|
|
268
|
-
keypath : str
|
|
269
|
-
keypath
|
|
270
|
-
filepath : str
|
|
271
|
-
File path
|
|
272
|
-
|
|
273
|
-
Returns
|
|
274
|
-
-------
|
|
275
|
-
str
|
|
276
|
-
Encrypted data
|
|
277
|
-
"""
|
|
278
|
-
from cryptography.fernet import Fernet
|
|
279
|
-
|
|
280
|
-
if isinstance(keypath, str) and isinstance(filepath, str):
|
|
281
|
-
fernet = Fernet(open(keypath, "rb").read())
|
|
282
|
-
data = open(filepath, "rb").read()
|
|
283
|
-
encrypt_data = fernet.encrypt(data)
|
|
284
|
-
return encrypt_data
|
|
285
|
-
else:
|
|
286
|
-
raise TypeError("Wrong datatype(s)")
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def decrypt_fernet_data(keypath: str, filepath: str) -> str:
|
|
290
|
-
"""
|
|
291
|
-
Decrypt file
|
|
292
|
-
|
|
293
|
-
Parameters
|
|
294
|
-
----------
|
|
295
|
-
keypath : str
|
|
296
|
-
keypath
|
|
297
|
-
filepath : str
|
|
298
|
-
File path
|
|
299
|
-
|
|
300
|
-
Returns
|
|
301
|
-
-------
|
|
302
|
-
str
|
|
303
|
-
Decrypted data
|
|
304
|
-
"""
|
|
305
|
-
from cryptography.fernet import Fernet
|
|
306
|
-
|
|
307
|
-
if isinstance(keypath, str) and isinstance(filepath, str):
|
|
308
|
-
fernet = Fernet(open(keypath, "rb").read())
|
|
309
|
-
data = open(filepath, "rb").read()
|
|
310
|
-
decrypt_data = fernet.decrypt(data)
|
|
311
|
-
return decrypt_data
|
|
312
|
-
else:
|
|
313
|
-
raise TypeError("Wrong datatype(s)")
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# EMAIL
|
|
317
|
-
def send_email(
|
|
318
|
-
logname: str,
|
|
319
|
-
message_subject: str,
|
|
320
|
-
message_sender: str,
|
|
321
|
-
message_receiver: str,
|
|
322
|
-
html: str,
|
|
323
|
-
smtp_address: str,
|
|
324
|
-
) -> None:
|
|
325
|
-
"""
|
|
326
|
-
Send html email
|
|
327
|
-
|
|
328
|
-
Parameters
|
|
329
|
-
----------
|
|
330
|
-
logname : str
|
|
331
|
-
Log
|
|
332
|
-
message_subject : str
|
|
333
|
-
Message subject
|
|
334
|
-
message_sender : str
|
|
335
|
-
Sender email
|
|
336
|
-
message_receiver : str
|
|
337
|
-
Receiver email
|
|
338
|
-
html : str
|
|
339
|
-
HTML string
|
|
340
|
-
smtp_address: str
|
|
341
|
-
SMTP address
|
|
342
|
-
"""
|
|
343
|
-
import logging
|
|
344
|
-
import smtplib
|
|
345
|
-
from email.mime.multipart import MIMEMultipart
|
|
346
|
-
from email.mime.text import MIMEText
|
|
347
|
-
|
|
348
|
-
if all(
|
|
349
|
-
isinstance(i, str)
|
|
350
|
-
for i in [
|
|
351
|
-
logname,
|
|
352
|
-
message_subject,
|
|
353
|
-
message_sender,
|
|
354
|
-
message_receiver,
|
|
355
|
-
html,
|
|
356
|
-
smtp_address,
|
|
357
|
-
]
|
|
358
|
-
):
|
|
359
|
-
logging.basicConfig(
|
|
360
|
-
filename=logname,
|
|
361
|
-
level=logging.DEBUG,
|
|
362
|
-
format="%(asctime)s %(message)s",
|
|
363
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
message = MIMEMultipart("alternative")
|
|
367
|
-
message["Subject"] = message_subject
|
|
368
|
-
message["From"] = message_sender
|
|
369
|
-
message["To"] = message_receiver
|
|
370
|
-
html = MIMEText(html, "html")
|
|
371
|
-
message.attach(html)
|
|
372
|
-
try:
|
|
373
|
-
server = smtplib.SMTP(smtp_address)
|
|
374
|
-
server.sendmail(message_sender, message_receiver, message.as_string())
|
|
375
|
-
server.quit()
|
|
376
|
-
logging.info("Email sent")
|
|
377
|
-
except Exception as e:
|
|
378
|
-
logging.error(f"Email not send: {str(e)}")
|
|
379
|
-
else:
|
|
380
|
-
raise TypeError("Wrong datatype(s)")
|
|
381
|
-
|
|
382
|
-
|
|
383
279
|
# FILESYSTEM
|
|
384
280
|
def get_path_files(path: str, keywords: list) -> list:
|
|
385
281
|
"""
|
|
@@ -468,6 +364,28 @@ def list_to_file(filepath: str, list_: list, newline: bool = True) -> None:
|
|
|
468
364
|
f.close()
|
|
469
365
|
|
|
470
366
|
|
|
367
|
+
# URLLIB
|
|
368
|
+
def create_encode_url(url: str, query_params: dict = {}) -> str:
|
|
369
|
+
"""
|
|
370
|
+
Create encoded url
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
url : str
|
|
375
|
+
base url
|
|
376
|
+
query_params : dict, optional
|
|
377
|
+
dictionary of url query parameters, by default {}
|
|
378
|
+
|
|
379
|
+
Returns
|
|
380
|
+
-------
|
|
381
|
+
str
|
|
382
|
+
encoded url
|
|
383
|
+
"""
|
|
384
|
+
from urllib.parse import urlencode
|
|
385
|
+
|
|
386
|
+
return f"{url}{urlencode(query_params)}"
|
|
387
|
+
|
|
388
|
+
|
|
471
389
|
# SYSTEM
|
|
472
390
|
def parse_ps_aux(ps_aux_commands: str) -> List[list]:
|
|
473
391
|
"""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
def send_aws_ses_email(
|
|
2
|
+
sender: str,
|
|
3
|
+
recipient: list,
|
|
4
|
+
subject: str,
|
|
5
|
+
body_text: str,
|
|
6
|
+
body_type: str,
|
|
7
|
+
ses_client,
|
|
8
|
+
attachment: str = None,
|
|
9
|
+
):
|
|
10
|
+
"""
|
|
11
|
+
Send AWS SES email
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
sender : str
|
|
16
|
+
sender email
|
|
17
|
+
recipient : list
|
|
18
|
+
list of recipient emails
|
|
19
|
+
subject : str
|
|
20
|
+
email subject
|
|
21
|
+
body_text : str
|
|
22
|
+
email body
|
|
23
|
+
body_type : str
|
|
24
|
+
email body type
|
|
25
|
+
ses_client : _type_
|
|
26
|
+
aws ses client
|
|
27
|
+
attachment : str, optional
|
|
28
|
+
attachment path, by default None
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
str
|
|
33
|
+
aws ses client email response or none
|
|
34
|
+
"""
|
|
35
|
+
import os
|
|
36
|
+
from email.mime.application import MIMEApplication
|
|
37
|
+
from email.mime.multipart import MIMEMultipart
|
|
38
|
+
from email.mime.text import MIMEText
|
|
39
|
+
|
|
40
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
|
41
|
+
|
|
42
|
+
msg = MIMEMultipart()
|
|
43
|
+
msg["Subject"] = subject
|
|
44
|
+
msg["From"] = sender
|
|
45
|
+
msg["To"] = ", ".join(recipient)
|
|
46
|
+
|
|
47
|
+
part = MIMEText(body_text, body_type)
|
|
48
|
+
msg.attach(part)
|
|
49
|
+
|
|
50
|
+
if attachment:
|
|
51
|
+
try:
|
|
52
|
+
with open(attachment, "rb") as f:
|
|
53
|
+
part = MIMEApplication(f.read())
|
|
54
|
+
part.add_header(
|
|
55
|
+
"Content-Disposition",
|
|
56
|
+
"attachment",
|
|
57
|
+
filename=os.path.basename(attachment),
|
|
58
|
+
)
|
|
59
|
+
msg.attach(part)
|
|
60
|
+
except FileNotFoundError:
|
|
61
|
+
print(f"{attachment} not found")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
response = ses_client.send_raw_email(
|
|
66
|
+
Source=sender,
|
|
67
|
+
Destinations=recipient,
|
|
68
|
+
RawMessage={
|
|
69
|
+
"Data": msg.as_string(),
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
except (BotoCoreError, ClientError) as error:
|
|
73
|
+
print(f"Error: {error}")
|
|
74
|
+
return
|
|
75
|
+
else:
|
|
76
|
+
return response
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
import doctest
|
|
81
|
+
|
|
82
|
+
doctest.testmod()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from cryptography.fernet import Fernet
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_fernet_key(output_directory: str, output_filename: str) -> bytes:
|
|
7
|
+
"""
|
|
8
|
+
Generate cryptography fernet key
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
output_directory : str
|
|
13
|
+
Linux directory
|
|
14
|
+
output_filename : str
|
|
15
|
+
Linux filename
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
bytes
|
|
20
|
+
Fernet key in bytes
|
|
21
|
+
"""
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
key = None
|
|
25
|
+
if os.path.isdir(output_directory):
|
|
26
|
+
try:
|
|
27
|
+
key = Fernet.generate_key()
|
|
28
|
+
filepath = os.path.join(output_directory, output_filename)
|
|
29
|
+
f = open(filepath, "wb")
|
|
30
|
+
f.write(key)
|
|
31
|
+
f.close()
|
|
32
|
+
logging.info(f"generate_fernet_key: True")
|
|
33
|
+
except Exception as e:
|
|
34
|
+
logging.error(f"generate_fernet_key: False ({e})")
|
|
35
|
+
return key
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def encrypt_fernet_file(keypath: str, filepath: str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Encrypt file
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
keypath : str
|
|
45
|
+
keypath
|
|
46
|
+
filepath : str
|
|
47
|
+
File path
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
str
|
|
52
|
+
Encrypted data
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(keypath, str) and isinstance(filepath, str):
|
|
55
|
+
fernet = Fernet(open(keypath, "rb").read())
|
|
56
|
+
data = open(filepath, "rb").read()
|
|
57
|
+
encrypt_data = fernet.encrypt(data)
|
|
58
|
+
return encrypt_data
|
|
59
|
+
else:
|
|
60
|
+
raise TypeError("Wrong datatype(s)")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def decrypt_fernet_data(keypath: str, filepath: str) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Decrypt file
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
keypath : str
|
|
70
|
+
keypath
|
|
71
|
+
filepath : str
|
|
72
|
+
File path
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
str
|
|
77
|
+
Decrypted data
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(keypath, str) and isinstance(filepath, str):
|
|
80
|
+
fernet = Fernet(open(keypath, "rb").read())
|
|
81
|
+
data = open(filepath, "rb").read()
|
|
82
|
+
decrypt_data = fernet.decrypt(data)
|
|
83
|
+
return decrypt_data
|
|
84
|
+
else:
|
|
85
|
+
raise TypeError("Wrong datatype(s)")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
import doctest
|
|
90
|
+
|
|
91
|
+
doctest.testmod()
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
def django_validate_email(email: str) -> str | None:
|
|
46
|
+
"""
|
|
47
|
+
Django validate email
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
email : str
|
|
52
|
+
email to check
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
str | None
|
|
57
|
+
email or none
|
|
58
|
+
"""
|
|
59
|
+
from django.core.validators import validate_email
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
validate_email(email)
|
|
63
|
+
return email
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def django_validate_url(url: str) -> str | None:
|
|
69
|
+
"""
|
|
70
|
+
Django validate url
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
url : str
|
|
75
|
+
url to validate
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
str | None
|
|
80
|
+
url or None
|
|
81
|
+
"""
|
|
82
|
+
from django.core.validators import URLValidator
|
|
83
|
+
|
|
84
|
+
validator = URLValidator()
|
|
85
|
+
try:
|
|
86
|
+
validator(url)
|
|
87
|
+
return url
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def django_validate_phone(phone: str, region=None) -> str | None:
|
|
93
|
+
"""
|
|
94
|
+
Django validate phone
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
phone : str
|
|
99
|
+
phone to check
|
|
100
|
+
region : _type_, optional
|
|
101
|
+
phone region, by default None
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
str | None
|
|
106
|
+
parsed phone or none
|
|
107
|
+
"""
|
|
108
|
+
from phonenumber_field.phonenumber import PhoneNumber
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
return PhoneNumber.from_string(phone).as_e164
|
|
112
|
+
except:
|
|
113
|
+
try:
|
|
114
|
+
return PhoneNumber.from_string(phone, region=region).as_e164
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print(e)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
import doctest
|
|
122
|
+
|
|
123
|
+
doctest.testmod()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def keytab_valid(
|
|
6
|
+
keytab_filepath: str,
|
|
7
|
+
principal_name: str,
|
|
8
|
+
) -> subprocess.CompletedProcess:
|
|
9
|
+
"""
|
|
10
|
+
Check keytab validity
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
keytab_filepath : str
|
|
15
|
+
Keytab linux filepath
|
|
16
|
+
principal_name : str
|
|
17
|
+
Keytab principal name
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
subprocess.CompletedProcess
|
|
22
|
+
Subprocess object
|
|
23
|
+
"""
|
|
24
|
+
output = subprocess.run(
|
|
25
|
+
f"kinit -kt {keytab_filepath} {principal_name}",
|
|
26
|
+
capture_output=True,
|
|
27
|
+
shell=True,
|
|
28
|
+
text=True,
|
|
29
|
+
)
|
|
30
|
+
try:
|
|
31
|
+
if output.returncode == 0:
|
|
32
|
+
logging.info("keytab_valid: True")
|
|
33
|
+
else:
|
|
34
|
+
logging.info("keytab_valid: False")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logging.error(f"keytab_valid: False ({e})")
|
|
37
|
+
return output
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
import doctest
|
|
42
|
+
|
|
43
|
+
doctest.testmod()
|
|
@@ -0,0 +1,27 @@
|
|
|
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()
|
|
@@ -364,15 +364,56 @@ def split_left_merged_dataframe(dataframe, dataframe2, columns):
|
|
|
364
364
|
return df_both, df_left
|
|
365
365
|
|
|
366
366
|
|
|
367
|
+
## AGGREGATE
|
|
368
|
+
def aggregate_set_without_none(column: pd.Series, nested_set: bool = False) -> set:
|
|
369
|
+
"""
|
|
370
|
+
Use in pandas groupby agg, create a set without none based on input column
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
column : pd.Series
|
|
375
|
+
columns to set
|
|
376
|
+
nested_set : bool, optional
|
|
377
|
+
nested set within set, by default False
|
|
378
|
+
|
|
379
|
+
Returns
|
|
380
|
+
-------
|
|
381
|
+
set
|
|
382
|
+
set without None
|
|
383
|
+
"""
|
|
384
|
+
if nested_set:
|
|
385
|
+
output = set()
|
|
386
|
+
for value in column:
|
|
387
|
+
if isinstance(value, set):
|
|
388
|
+
output.update(value)
|
|
389
|
+
# todo, what if there is valid list / dict / tuple / complex object?
|
|
390
|
+
elif value:
|
|
391
|
+
output.add(value)
|
|
392
|
+
return output
|
|
393
|
+
else:
|
|
394
|
+
return {value for value in column if value is not None}
|
|
395
|
+
|
|
396
|
+
|
|
367
397
|
# DATA STRUCTURE
|
|
368
|
-
def clean_none(
|
|
398
|
+
def clean_none(
|
|
399
|
+
dataframe: pd.DataFrame,
|
|
400
|
+
nan_to_none: bool = True,
|
|
401
|
+
clean_variation: bool = True,
|
|
402
|
+
none_variations: list = [],
|
|
403
|
+
) -> pd.DataFrame:
|
|
369
404
|
"""
|
|
370
405
|
Return a dataframe from given dataframe with standardized None. Deprecated as of pandas 1.3.0.
|
|
371
406
|
|
|
372
407
|
Parameters
|
|
373
408
|
----------
|
|
374
|
-
dataframe :
|
|
375
|
-
|
|
409
|
+
dataframe : pd.DataFrame
|
|
410
|
+
_description_
|
|
411
|
+
nan_to_none : bool, optional
|
|
412
|
+
replace np.NaN with None, by default True
|
|
413
|
+
clean_variation : bool, optional
|
|
414
|
+
clean custom variables, by default True
|
|
415
|
+
none_variations : list, optional
|
|
416
|
+
list of none variations, by default []
|
|
376
417
|
|
|
377
418
|
Examples
|
|
378
419
|
--------
|
|
@@ -410,12 +451,15 @@ def clean_none(dataframe, clean_variation=True, non_variations=[]):
|
|
|
410
451
|
|
|
411
452
|
Returns
|
|
412
453
|
-------
|
|
413
|
-
|
|
454
|
+
pd.DataFrame
|
|
455
|
+
_description_
|
|
414
456
|
"""
|
|
415
457
|
df = dataframe.copy()
|
|
416
458
|
df = df.replace(r"^\s*$", np.nan, regex=True)
|
|
459
|
+
if nan_to_none:
|
|
460
|
+
df = df.replace(np.NaN, None, inplace=True)
|
|
417
461
|
if clean_variation:
|
|
418
|
-
df = df.replace(
|
|
462
|
+
df = df.replace(none_variations, np.nan)
|
|
419
463
|
df = df.where(pd.notnull(df), None)
|
|
420
464
|
return df
|
|
421
465
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# import logging
|
|
2
|
+
# import subprocess
|
|
3
|
+
|
|
4
|
+
# def ldapsearch(
|
|
5
|
+
# search_base: str,
|
|
6
|
+
# ldap_uri: str,
|
|
7
|
+
# bind_dn: str,
|
|
8
|
+
# password: str,
|
|
9
|
+
# filter: str,
|
|
10
|
+
# ) -> subprocess.CompletedProcess:
|
|
11
|
+
# cmd = (
|
|
12
|
+
# "ldapsearch -b {search_base} -H {ldap_uri} -D {bind_dn} -w {password} {filter}"
|
|
13
|
+
# )
|
|
14
|
+
# output = subprocess.run(
|
|
15
|
+
# cmd,
|
|
16
|
+
# capture_output=True,
|
|
17
|
+
# shell=True,
|
|
18
|
+
# text=True,
|
|
19
|
+
# )
|
|
20
|
+
# return output
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# if __name__ == "__main__":
|
|
24
|
+
# import doctest
|
|
25
|
+
|
|
26
|
+
# doctest.testmod()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
def send_email(
|
|
2
|
+
logname: str,
|
|
3
|
+
message_subject: str,
|
|
4
|
+
message_sender: str,
|
|
5
|
+
message_receiver: str,
|
|
6
|
+
html: str,
|
|
7
|
+
smtp_address: str,
|
|
8
|
+
) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Send html email
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
logname : str
|
|
15
|
+
Log
|
|
16
|
+
message_subject : str
|
|
17
|
+
Message subject
|
|
18
|
+
message_sender : str
|
|
19
|
+
Sender email
|
|
20
|
+
message_receiver : str
|
|
21
|
+
Receiver email
|
|
22
|
+
html : str
|
|
23
|
+
HTML string
|
|
24
|
+
smtp_address: str
|
|
25
|
+
SMTP address
|
|
26
|
+
"""
|
|
27
|
+
import logging
|
|
28
|
+
import smtplib
|
|
29
|
+
from email.mime.multipart import MIMEMultipart
|
|
30
|
+
from email.mime.text import MIMEText
|
|
31
|
+
|
|
32
|
+
if all(
|
|
33
|
+
isinstance(i, str)
|
|
34
|
+
for i in [
|
|
35
|
+
logname,
|
|
36
|
+
message_subject,
|
|
37
|
+
message_sender,
|
|
38
|
+
message_receiver,
|
|
39
|
+
html,
|
|
40
|
+
smtp_address,
|
|
41
|
+
]
|
|
42
|
+
):
|
|
43
|
+
logging.basicConfig(
|
|
44
|
+
filename=logname,
|
|
45
|
+
level=logging.DEBUG,
|
|
46
|
+
format="%(asctime)s %(message)s",
|
|
47
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
message = MIMEMultipart("alternative")
|
|
51
|
+
message["Subject"] = message_subject
|
|
52
|
+
message["From"] = message_sender
|
|
53
|
+
message["To"] = message_receiver
|
|
54
|
+
html = MIMEText(html, "html")
|
|
55
|
+
message.attach(html)
|
|
56
|
+
try:
|
|
57
|
+
server = smtplib.SMTP(smtp_address)
|
|
58
|
+
server.sendmail(message_sender, message_receiver, message.as_string())
|
|
59
|
+
server.quit()
|
|
60
|
+
logging.info("Email sent")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logging.error(f"Email not send: {str(e)}")
|
|
63
|
+
else:
|
|
64
|
+
raise TypeError("Wrong datatype(s)")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
import doctest
|
|
69
|
+
|
|
70
|
+
doctest.testmod()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import sqlalchemy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_sqlalchemy_url(
|
|
7
|
+
drivername: str,
|
|
8
|
+
host: str,
|
|
9
|
+
dbname: str,
|
|
10
|
+
user: str,
|
|
11
|
+
password: str,
|
|
12
|
+
port=3306,
|
|
13
|
+
) -> sqlalchemy.engine.url.URL:
|
|
14
|
+
"""
|
|
15
|
+
Create sqlalchemy url
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
drivername : str
|
|
20
|
+
firebird+kinterbasdb, mssql+pyodbc, mysql+mysqlconnector, mysql+pymysql, oracle+cx_oracle, postgresql+psycopg2, sapdb+pysapdb, sqlite3, teradata+pytds
|
|
21
|
+
host : str
|
|
22
|
+
Database host address
|
|
23
|
+
dbname : str
|
|
24
|
+
Database name
|
|
25
|
+
user : str
|
|
26
|
+
User used to authenticate
|
|
27
|
+
password : str
|
|
28
|
+
Password used to authenticate
|
|
29
|
+
port : int, optional
|
|
30
|
+
Connection port number, by default 3306
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
sqlalchemy.engine.url.URL
|
|
35
|
+
url object
|
|
36
|
+
"""
|
|
37
|
+
url = sqlalchemy.engine.url.URL.create(
|
|
38
|
+
drivername=drivername,
|
|
39
|
+
username=user,
|
|
40
|
+
password=password,
|
|
41
|
+
host=host,
|
|
42
|
+
port=port,
|
|
43
|
+
database=dbname,
|
|
44
|
+
)
|
|
45
|
+
return url
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_sqlalchemy_engine(
|
|
49
|
+
drivername: str,
|
|
50
|
+
host: str,
|
|
51
|
+
dbname: str,
|
|
52
|
+
user: str,
|
|
53
|
+
password: str,
|
|
54
|
+
port=3306,
|
|
55
|
+
) -> sqlalchemy.engine.base.Engine:
|
|
56
|
+
"""
|
|
57
|
+
Create SQLalchemy engine
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
drivername : str
|
|
62
|
+
firebird+kinterbasdb, mssql+pyodbc, mysql+mysqlconnector, mysql+pymysql, oracle+cx_oracle, postgresql+psycopg2, sapdb+pysapdb, sqlite3, teradata+pytds
|
|
63
|
+
host : str
|
|
64
|
+
Database host address
|
|
65
|
+
dbname : str
|
|
66
|
+
Database name
|
|
67
|
+
user : str
|
|
68
|
+
User used to authenticate
|
|
69
|
+
password : str
|
|
70
|
+
Password used to authenticate
|
|
71
|
+
port : int, optional
|
|
72
|
+
Connection port number, by default 3306
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
sqlalchemy.engine.base.Engine
|
|
77
|
+
engine object
|
|
78
|
+
"""
|
|
79
|
+
url = create_sqlalchemy_url(
|
|
80
|
+
drivername=drivername,
|
|
81
|
+
host=host,
|
|
82
|
+
dbname=dbname,
|
|
83
|
+
user=user,
|
|
84
|
+
password=password,
|
|
85
|
+
port=port,
|
|
86
|
+
)
|
|
87
|
+
engine = sqlalchemy.create_engine(url)
|
|
88
|
+
try:
|
|
89
|
+
engine.connect()
|
|
90
|
+
logging.info("create_sqlalchemy_engine: True")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logging.error(f"create_sqlalchemy_engine: False ({e})")
|
|
93
|
+
return engine
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
import doctest
|
|
98
|
+
|
|
99
|
+
doctest.testmod()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: data-manipulation
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.37
|
|
4
4
|
Summary: Powerful data manipulation
|
|
5
5
|
Home-page: https://github.com/shawnngtq/data-manipulation
|
|
6
6
|
Author: Shawn Ng
|
|
@@ -15,9 +15,20 @@ Classifier: Programming Language :: Python
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
19
|
Classifier: Programming Language :: Cython
|
|
19
20
|
Classifier: Topic :: Scientific/Engineering
|
|
20
21
|
License-File: LICENSE
|
|
22
|
+
Requires-Dist: beautifulsoup4
|
|
23
|
+
Requires-Dist: django
|
|
24
|
+
Requires-Dist: lxml
|
|
25
|
+
Requires-Dist: matplotlib
|
|
26
|
+
Requires-Dist: mysql-connector-python
|
|
27
|
+
Requires-Dist: pandas
|
|
28
|
+
Requires-Dist: psycopg2
|
|
29
|
+
Requires-Dist: pyspark
|
|
30
|
+
Requires-Dist: tqdm
|
|
31
|
+
Requires-Dist: xlsxwriter
|
|
21
32
|
|
|
22
33
|
|
|
23
34
|
Data Manipulation is a Python package providing powerful utility functions.
|
|
@@ -8,13 +8,20 @@ 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
|
|
12
|
+
data_manipulation/cryptography_.py
|
|
11
13
|
data_manipulation/django_.py
|
|
12
14
|
data_manipulation/flask_.py
|
|
13
15
|
data_manipulation/geopandas_.py
|
|
16
|
+
data_manipulation/kerberos_.py
|
|
14
17
|
data_manipulation/mysql_connector_python_.py
|
|
18
|
+
data_manipulation/openldap_.py
|
|
15
19
|
data_manipulation/pandas_.py
|
|
20
|
+
data_manipulation/prometheus_.py
|
|
16
21
|
data_manipulation/psycopg2_.py
|
|
17
22
|
data_manipulation/pyspark_.py
|
|
23
|
+
data_manipulation/smtplib_.py
|
|
24
|
+
data_manipulation/sqlalchemy_.py
|
|
18
25
|
data_manipulation.egg-info/PKG-INFO
|
|
19
26
|
data_manipulation.egg-info/SOURCES.txt
|
|
20
27
|
data_manipulation.egg-info/dependency_links.txt
|
|
@@ -16,6 +16,7 @@ CLASSIFIERS = [
|
|
|
16
16
|
"Programming Language :: Python :: 3.9",
|
|
17
17
|
"Programming Language :: Python :: 3.10",
|
|
18
18
|
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
19
20
|
"Programming Language :: Cython",
|
|
20
21
|
"Topic :: Scientific/Engineering",
|
|
21
22
|
]
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
def init_django(project_name: str) -> None:
|
|
2
|
-
"""
|
|
3
|
-
Setup Django in Jupyter. Reference from https://gist.github.com/codingforentrepreneurs/76e570d759f83d690bf36a8a8fa4cfbe
|
|
4
|
-
|
|
5
|
-
Parameters
|
|
6
|
-
----------
|
|
7
|
-
project_name : str
|
|
8
|
-
Django project name
|
|
9
|
-
"""
|
|
10
|
-
import os
|
|
11
|
-
import sys
|
|
12
|
-
|
|
13
|
-
import django
|
|
14
|
-
|
|
15
|
-
present_working_dir = os.getenv("PWD")
|
|
16
|
-
os.chdir(present_working_dir)
|
|
17
|
-
|
|
18
|
-
project_name = project_name or os.environ.get("DJANGO_PROJECT") or None
|
|
19
|
-
if not project_name:
|
|
20
|
-
raise Exception(
|
|
21
|
-
"""Set an environment variable: `DJANGO_PROJECT=your_project_name` or call `init_django(your_project_name)`"""
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
sys.path.insert(0, present_working_dir)
|
|
25
|
-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{project_name}.settings")
|
|
26
|
-
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
|
27
|
-
django.setup()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if __name__ == "__main__":
|
|
31
|
-
import doctest
|
|
32
|
-
|
|
33
|
-
doctest.testmod()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/mysql_connector_python_.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|