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.
Files changed (31) hide show
  1. {data_manipulation-0.35 → data_manipulation-0.37}/LICENSE +4 -4
  2. {data_manipulation-0.35 → data_manipulation-0.37}/PKG-INFO +12 -1
  3. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/__init__.py +8 -0
  4. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/_version.py +3 -3
  5. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/base.py +40 -122
  6. data_manipulation-0.37/data_manipulation/boto3_.py +82 -0
  7. data_manipulation-0.37/data_manipulation/cryptography_.py +91 -0
  8. data_manipulation-0.37/data_manipulation/django_.py +123 -0
  9. data_manipulation-0.37/data_manipulation/kerberos_.py +43 -0
  10. data_manipulation-0.37/data_manipulation/openldap_.py +27 -0
  11. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/pandas_.py +49 -5
  12. data_manipulation-0.37/data_manipulation/prometheus_.py +26 -0
  13. data_manipulation-0.37/data_manipulation/smtplib_.py +70 -0
  14. data_manipulation-0.37/data_manipulation/sqlalchemy_.py +99 -0
  15. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/PKG-INFO +12 -1
  16. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/SOURCES.txt +7 -0
  17. {data_manipulation-0.35 → data_manipulation-0.37}/setup.py +1 -0
  18. data_manipulation-0.35/data_manipulation/django_.py +0 -33
  19. {data_manipulation-0.35 → data_manipulation-0.37}/MANIFEST.in +0 -0
  20. {data_manipulation-0.35 → data_manipulation-0.37}/README.md +0 -0
  21. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/beautifulsoup_.py +0 -0
  22. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/flask_.py +0 -0
  23. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/geopandas_.py +0 -0
  24. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/mysql_connector_python_.py +0 -0
  25. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/psycopg2_.py +0 -0
  26. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation/pyspark_.py +0 -0
  27. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/dependency_links.txt +0 -0
  28. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/requires.txt +0 -0
  29. {data_manipulation-0.35 → data_manipulation-0.37}/data_manipulation.egg-info/top_level.txt +0 -0
  30. {data_manipulation-0.35 → data_manipulation-0.37}/setup.cfg +0 -0
  31. {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 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.35
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": "2022-10-31T08:26:32+0800",
11
+ "date": "2023-12-30T10:16:23+0800",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "7f2c736e77551bc9101dbc486004a1ffd46bebc5",
15
- "version": "0.35"
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(dataframe, clean_variation=True, non_variations=[]):
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 : pandas.DataFrame
375
- Dataframe
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
- df : pandas.DataFrame
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(non_variations, np.nan)
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.35
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()