data-manipulation 0.53__tar.gz → 0.54__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 (80) hide show
  1. {data_manipulation-0.53 → data_manipulation-0.54}/CHANGELOG.md +28 -17
  2. {data_manipulation-0.53 → data_manipulation-0.54}/CODE_OF_CONDUCT.md +9 -6
  3. {data_manipulation-0.53 → data_manipulation-0.54}/PKG-INFO +7 -9
  4. {data_manipulation-0.53 → data_manipulation-0.54}/README.md +1 -1
  5. {data_manipulation-0.53 → data_manipulation-0.54}/RELEASE.md +21 -4
  6. data_manipulation-0.54/conftest.py +24 -0
  7. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/__init__.py +0 -2
  8. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/_version.py +3 -3
  9. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/base.py +29 -90
  10. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/beautifulsoup_.py +18 -17
  11. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/boto3_.py +18 -18
  12. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/cryptography_.py +12 -17
  13. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/kerberos_.py +4 -7
  14. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/mysql_connector_python_.py +28 -26
  15. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/openldap_.py +6 -10
  16. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/pandas_.py +71 -228
  17. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/postgres_.py +16 -48
  18. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/pyspark_.py +27 -95
  19. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/smtplib_.py +14 -11
  20. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/sqlalchemy_.py +21 -36
  21. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation.egg-info/PKG-INFO +7 -9
  22. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation.egg-info/SOURCES.txt +28 -12
  23. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation.egg-info/requires.txt +4 -2
  24. data_manipulation-0.54/data_manipulation.egg-info/scm_file_list.json +60 -0
  25. data_manipulation-0.54/data_manipulation.egg-info/scm_version.json +8 -0
  26. data_manipulation-0.54/docs/api/base.md +3 -0
  27. data_manipulation-0.54/docs/api/beautifulsoup_.md +3 -0
  28. data_manipulation-0.54/docs/api/boto3_.md +3 -0
  29. data_manipulation-0.54/docs/api/cryptography_.md +3 -0
  30. data_manipulation-0.54/docs/api/kerberos_.md +3 -0
  31. data_manipulation-0.54/docs/api/mysql_connector_python_.md +3 -0
  32. data_manipulation-0.54/docs/api/pandas_.md +3 -0
  33. data_manipulation-0.54/docs/api/polars_.md +3 -0
  34. data_manipulation-0.54/docs/api/postgres_.md +3 -0
  35. data_manipulation-0.54/docs/api/pyspark_.md +3 -0
  36. data_manipulation-0.54/docs/api/smtplib_.md +3 -0
  37. data_manipulation-0.54/docs/api/sqlalchemy_.md +3 -0
  38. {data_manipulation-0.53 → data_manipulation-0.54}/docs/changelog.md +28 -17
  39. {data_manipulation-0.53 → data_manipulation-0.54}/docs/index.md +1 -2
  40. data_manipulation-0.54/mkdocs.yml +36 -0
  41. data_manipulation-0.54/pyproject.toml +95 -0
  42. {data_manipulation-0.53 → data_manipulation-0.54}/setup.sh +4 -0
  43. data_manipulation-0.54/tests/conftest.py +36 -0
  44. data_manipulation-0.54/tests/test_base.py +124 -0
  45. data_manipulation-0.54/tests/test_beautifulsoup.py +40 -0
  46. data_manipulation-0.54/tests/test_boto3.py +22 -0
  47. data_manipulation-0.54/tests/test_cryptography.py +43 -0
  48. data_manipulation-0.54/tests/test_kerberos.py +26 -0
  49. data_manipulation-0.54/tests/test_mysql.py +27 -0
  50. data_manipulation-0.54/tests/test_openldap.py +29 -0
  51. data_manipulation-0.54/tests/test_pandas.py +128 -0
  52. data_manipulation-0.54/tests/test_postgres.py +32 -0
  53. data_manipulation-0.54/tests/test_pyspark.py +64 -0
  54. data_manipulation-0.54/tests/test_smtplib.py +35 -0
  55. data_manipulation-0.54/tests/test_sqlalchemy.py +43 -0
  56. data_manipulation-0.53/Makefile +0 -144
  57. data_manipulation-0.53/data_manipulation/django_.py +0 -169
  58. data_manipulation-0.53/data_manipulation/geopandas_.py +0 -84
  59. data_manipulation-0.53/docs/api.md +0 -59
  60. data_manipulation-0.53/docs/commands.md +0 -10
  61. data_manipulation-0.53/mkdocs.yml +0 -25
  62. data_manipulation-0.53/models/.gitkeep +0 -0
  63. data_manipulation-0.53/notebooks/.gitkeep +0 -0
  64. data_manipulation-0.53/pyproject.toml +0 -55
  65. data_manipulation-0.53/references/.gitkeep +0 -0
  66. data_manipulation-0.53/reports/.gitkeep +0 -0
  67. data_manipulation-0.53/reports/figures/.gitkeep +0 -0
  68. data_manipulation-0.53/test_environment.py +0 -26
  69. data_manipulation-0.53/tox.ini +0 -3
  70. {data_manipulation-0.53 → data_manipulation-0.54}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  71. {data_manipulation-0.53 → data_manipulation-0.54}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  72. {data_manipulation-0.53 → data_manipulation-0.54}/.gitignore +0 -0
  73. {data_manipulation-0.53 → data_manipulation-0.54}/CONTRIBUTING.md +0 -0
  74. {data_manipulation-0.53 → data_manipulation-0.54}/LICENSE +0 -0
  75. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation/polars_.py +0 -0
  76. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation.egg-info/dependency_links.txt +0 -0
  77. {data_manipulation-0.53 → data_manipulation-0.54}/data_manipulation.egg-info/top_level.txt +0 -0
  78. {data_manipulation-0.53 → data_manipulation-0.54}/docs/getting-started.md +0 -0
  79. {data_manipulation-0.53 → data_manipulation-0.54}/docs/hooks.py +0 -0
  80. {data_manipulation-0.53 → data_manipulation-0.54}/setup.cfg +0 -0
@@ -4,29 +4,40 @@
4
4
 
5
5
  ### Removed
6
6
 
7
- - **`pandas_.clean_none()`** function has been removed.
7
+ - **`django_` module** - removed entirely (breaking change). The Django helpers
8
+ (`init_django`, `get_django_countries_dict`, `django_validate_email`,
9
+ `django_validate_url`, `django_validate_phone`) now live in the
10
+ [`django-util`](https://github.com/shawnngtq/django-util) package:
11
+ `django_util.utility` (`init_django`, `get_django_countries_dict`) and
12
+ `django_util.validators` (the `django_validate_*` functions). `data_manipulation`
13
+ no longer depends on Django. This warrants a major version bump on release.
14
+
15
+ - **`pandas_.clean_none()`** - function has been removed.
8
16
  Migrate to pandas built-ins:
17
+
9
18
  ```python
10
19
  df = df.replace(r"^\s*$", np.nan, regex=True)
11
20
  df = df.where(pd.notnull(df), None)
12
21
  ```
13
22
 
14
- - **`pandas_.config_pandas_display()`** function has been removed.
23
+ - **`pandas_.config_pandas_display()`** - function has been removed.
15
24
  Set display options directly:
25
+
16
26
  ```python
17
27
  pd.set_option("display.max_columns", 500)
18
28
  pd.set_option("display.max_colwidth", 500)
19
29
  pd.set_option("display.expand_frame_repr", True)
20
30
  ```
21
31
 
22
- - **`prometheus_` module** was an empty placeholder; removed from
32
+ - **`prometheus_` module** - was an empty placeholder; removed from
23
33
  the public API and from `__init__` imports.
24
34
 
25
35
  ### Changed
26
36
 
27
- - **`base.get_none_variation()`** now returns a `frozenset` of 7
37
+ - **`base.get_none_variation()`** - now returns a `frozenset` of 7
28
38
  lowercase canonical strings instead of a 72-item list of case
29
39
  permutations. Use `.casefold()` for membership checks:
40
+
30
41
  ```python
31
42
  # before
32
43
  if value in get_none_variation():
@@ -36,46 +47,46 @@
36
47
  ...
37
48
  ```
38
49
 
39
- - **`pyspark_.group_count()`** the `percent` column is now computed
50
+ - **`pyspark_.group_count()`** - the `percent` column is now computed
40
51
  as a native Spark column expression instead of a Python UDF.
41
52
  Behaviour is identical; performance improves on large datasets.
42
53
 
43
- - **`pyspark_.column_into_list()`** raises `ValueError` (with the
54
+ - **`pyspark_.column_into_list()`** - raises `ValueError` (with the
44
55
  list of available columns) when the requested column does not exist.
45
56
  Previously returned `None` silently.
46
57
 
47
- - **`pandas_.print_dataframe_overview()`** now returns a
58
+ - **`pandas_.print_dataframe_overview()`** - now returns a
48
59
  `Dict[str, Dict[str, Any]]` of per-column stats (`unique`,
49
60
  `null_count`, `value_counts`). Still prints to stdout as before;
50
61
  existing call sites are unaffected.
51
62
 
52
- - **`pandas_.compare_dataframes()`** now returns a comparison
63
+ - **`pandas_.compare_dataframes()`** - now returns a comparison
53
64
  summary dict (`same_length`, `df1_length`, `df2_length`, `columns`).
54
65
  Still prints to stdout as before; existing call sites are unaffected.
55
66
 
56
- - **`pyspark_` module** all `print()` calls replaced with
67
+ - **`pyspark_` module** - all `print()` calls replaced with
57
68
  `logger.info()` / `logger.debug()`. Output is now routed through
58
69
  the standard logging pipeline and can be silenced or redirected.
59
70
 
60
- - **`sqlalchemy_.create_sqlalchemy_engine()`** duplicate
71
+ - **`sqlalchemy_.create_sqlalchemy_engine()`** - duplicate
61
72
  `if/elif` timeout branches (both setting the same value) consolidated
62
73
  into a single condition.
63
74
 
64
- - **`django_.django_validate_phone()`** now has an explicit
75
+ - **`django_.django_validate_phone()`** - now has an explicit
65
76
  `return None` on failure instead of a bare `return`.
66
77
 
67
- ### Fixed (previous pass reference)
78
+ ### Fixed (previous pass - reference)
68
79
 
69
- - `pyspark_.columns_statistics()` always-true condition in empty-column
80
+ - `pyspark_.columns_statistics()` - always-true condition in empty-column
70
81
  detection (third clause was truthy for any non-empty string).
71
- - `pandas_.dtypes_dictionary()` `.iteritems()` removed in pandas 2.0;
82
+ - `pandas_.dtypes_dictionary()` - `.iteritems()` removed in pandas 2.0;
72
83
  replaced with `.items()`.
73
84
  - Bare `except:` replaced with `except Exception:` in `pandas_` and
74
85
  `django_`.
75
- - `base.string_boolean_to_int()` removed (used `distutils`, dropped
86
+ - `base.string_boolean_to_int()` - removed (used `distutils`, dropped
76
87
  in Python 3.12).
77
- - `flask_.py` deleted (was entirely commented-out dead code).
78
- - `beautifulsoup_.preprocess()` fixed mixed tabs/spaces and invalid
88
+ - `flask_.py` - deleted (was entirely commented-out dead code).
89
+ - `beautifulsoup_.preprocess()` - fixed mixed tabs/spaces and invalid
79
90
  `\s` escape sequences.
80
91
  - All modules now guard third-party imports with `try/except`; the
81
92
  package imports cleanly even when optional dependencies are missing.
@@ -23,13 +23,16 @@ include:
23
23
  Examples of unacceptable behavior by participants include:
24
24
 
25
25
  * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
26
+
27
+ advances
27
28
  * Trolling, insulting/derogatory comments, and personal or political attacks
28
29
  * Public or private harassment
29
30
  * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
+
32
+ address, without explicit permission
31
33
  * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
34
+
35
+ professional setting
33
36
 
34
37
  ## Our Responsibilities
35
38
 
@@ -55,7 +58,7 @@ further defined and clarified by project maintainers.
55
58
  ## Enforcement
56
59
 
57
60
  Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at Shawn.coding.acc@gmail.com. All
61
+ reported by contacting the project team at <Shawn.coding.acc@gmail.com>. All
59
62
  complaints will be reviewed and investigated and will result in a response that
60
63
  is deemed necessary and appropriate to the circumstances. The project team is
61
64
  obligated to maintain confidentiality with regard to the reporter of an incident.
@@ -68,9 +71,9 @@ members of the project's leadership.
68
71
  ## Attribution
69
72
 
70
73
  This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
74
+ available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
72
75
 
73
76
  [homepage]: https://www.contributor-covenant.org
74
77
 
75
78
  For answers to common questions about this code of conduct, see
76
- https://www.contributor-covenant.org/faq
79
+ <https://www.contributor-covenant.org/faq>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data_manipulation
3
- Version: 0.53
3
+ Version: 0.54
4
4
  Summary: Powerful data manipulation
5
5
  Author-email: Shawn Ng <shawn.coding.acc@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -10,21 +10,16 @@ Classifier: Environment :: Console
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Intended Audience :: Science/Research
12
12
  Classifier: Programming Language :: Python
13
- Classifier: Programming Language :: Python :: 3.9
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.14
17
14
  Classifier: Programming Language :: Cython
18
15
  Classifier: Topic :: Scientific/Engineering
19
- Requires-Python: >=3.9
16
+ Requires-Python: >=3.14
20
17
  Description-Content-Type: text/markdown
21
18
  License-File: LICENSE
22
19
  Requires-Dist: beautifulsoup4
23
20
  Requires-Dist: boto3
24
21
  Requires-Dist: cryptography
25
- Requires-Dist: django
26
22
  Requires-Dist: flask
27
- Requires-Dist: geopandas
28
23
  Requires-Dist: ipython
29
24
  Requires-Dist: loguru
30
25
  Requires-Dist: mysql-connector-python
@@ -34,6 +29,9 @@ Requires-Dist: psycopg
34
29
  Requires-Dist: pyspark
35
30
  Requires-Dist: python-dotenv
36
31
  Requires-Dist: sqlalchemy
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest; extra == "test"
34
+ Requires-Dist: pytest-mock; extra == "test"
37
35
  Dynamic: license-file
38
36
 
39
37
  # Data Manipulation
@@ -56,7 +54,7 @@ Data Manipulation is a Python package providing powerful utility functions. It c
56
54
 
57
55
  ## Where to get it
58
56
 
59
- The source code is currently hosted on GitHub at: https://github.com/shawnngtq/data-manipulation
57
+ The source code is currently hosted on GitHub at: <https://github.com/shawnngtq/data-manipulation>
60
58
 
61
59
  ```bash
62
60
  pip install data-manipulation
@@ -18,7 +18,7 @@ Data Manipulation is a Python package providing powerful utility functions. It c
18
18
 
19
19
  ## Where to get it
20
20
 
21
- The source code is currently hosted on GitHub at: https://github.com/shawnngtq/data-manipulation
21
+ The source code is currently hosted on GitHub at: <https://github.com/shawnngtq/data-manipulation>
22
22
 
23
23
  ```bash
24
24
  pip install data-manipulation
@@ -34,20 +34,37 @@ The tag must point at the exact commit being released.
34
34
 
35
35
  ## Publish Documentation
36
36
 
37
- Build the MkDocs documentation:
37
+ Both commands require an environment that provides `mkdocs`, `mkdocs-material`, and
38
+ `mkdocstrings-python` (for example a virtualenv, conda env, or pixi shell). Activate that
39
+ environment before running them.
40
+
41
+ Build the MkDocs documentation (local preview only):
38
42
 
39
43
  ```bash
40
- mkdocs build
44
+ ./setup.sh create_update_docs
41
45
  ```
42
46
 
43
- Publish the generated HTML to GitHub Pages:
47
+ Deploy to GitHub Pages:
44
48
 
45
49
  ```bash
46
- mkdocs gh-deploy
50
+ ./setup.sh deploy_docs
47
51
  ```
48
52
 
49
53
  This publishes the contents of `site/` to the `gh-pages` branch.
50
54
 
55
+ **Note - documentation and PyPI releases are independent.**
56
+
57
+ - **Tags are only for PyPI releases.** `setuptools-scm` derives the package
58
+ version from git tags. Create a tag only when the shipped `data_manipulation/`
59
+ package changes; tooling- or docs-only changes need no tag.
60
+ - **Docs deploy without a tag.** `deploy_docs` publishes current `master` to the
61
+ separate `gh-pages` branch, independent of any tag or PyPI release.
62
+ - **Commit and push doc/source changes to `master` before building.** The site
63
+ renders the working tree, and `create_update_docs` writes only to the
64
+ gitignored `site/` directory - no tracked files change.
65
+ - **When cutting a release, build docs after tagging** so the published site
66
+ matches the released commit.
67
+
51
68
  ## Publish To PyPI
52
69
 
53
70
  Build and validate the distribution artifacts:
@@ -0,0 +1,24 @@
1
+ """Repo-root pytest configuration.
2
+
3
+ ``--doctest-modules`` (see ``[tool.pytest.ini_options]``) collects doctests from
4
+ every ``data_manipulation/*.py``. We only want to *execute* examples that are
5
+ robustly runnable, so ignore the rest here. Their ``Examples:`` blocks still
6
+ render in the mkdocs API reference — they are just not auto-verified.
7
+
8
+ Auto-verified modules (not ignored): base.py, beautifulsoup_.py, sqlalchemy_.py.
9
+ """
10
+
11
+ collect_ignore = [
12
+ "data_manipulation/__init__.py",
13
+ "data_manipulation/_version.py",
14
+ "data_manipulation/pandas_.py",
15
+ "data_manipulation/pyspark_.py",
16
+ "data_manipulation/boto3_.py",
17
+ "data_manipulation/kerberos_.py",
18
+ "data_manipulation/openldap_.py",
19
+ "data_manipulation/cryptography_.py",
20
+ "data_manipulation/mysql_connector_python_.py",
21
+ "data_manipulation/postgres_.py",
22
+ "data_manipulation/smtplib_.py",
23
+ "data_manipulation/polars_.py",
24
+ ]
@@ -3,8 +3,6 @@ from data_manipulation import (
3
3
  beautifulsoup_,
4
4
  boto3_,
5
5
  cryptography_,
6
- django_,
7
- geopandas_,
8
6
  kerberos_,
9
7
  mysql_connector_python_,
10
8
  openldap_,
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.53'
22
- __version_tuple__ = version_tuple = (0, 53)
21
+ __version__ = version = '0.54'
22
+ __version_tuple__ = version_tuple = (0, 54)
23
23
 
24
- __commit_id__ = commit_id = 'g2446c213a'
24
+ __commit_id__ = commit_id = 'g7826f58d8'
@@ -4,13 +4,14 @@ import re
4
4
  import subprocess
5
5
  from functools import wraps
6
6
  from pathlib import Path
7
- from typing import Any, Dict, List, Optional, Tuple, Union
7
+ from typing import Any
8
8
  from urllib.parse import urlencode, urlparse, urlunparse
9
9
 
10
10
  try:
11
11
  from loguru import logger
12
12
  except ImportError:
13
13
  import logging
14
+
14
15
  logger = logging.getLogger(__name__)
15
16
 
16
17
 
@@ -67,14 +68,14 @@ def clean_string(
67
68
  return " ".join(string.strip().upper().split())
68
69
 
69
70
 
70
- def get_string_case_combination(str_: str) -> List[str]:
71
+ def get_string_case_combination(str_: str) -> list[str]:
71
72
  """Generates all possible case combinations of a string.
72
73
 
73
74
  Args:
74
75
  str_ (str): Input string to generate combinations for.
75
76
 
76
77
  Returns:
77
- List[str]: List of all possible case combinations.
78
+ list[str]: List of all possible case combinations.
78
79
 
79
80
  Examples:
80
81
  >>> get_string_case_combination("abc")
@@ -83,7 +84,12 @@ def get_string_case_combination(str_: str) -> List[str]:
83
84
  if not str_:
84
85
  raise ValueError("Input string cannot be empty or None")
85
86
 
86
- return list(map("".join, itertools.product(*zip(str_.upper(), str_.lower()))))
87
+ return list(
88
+ map(
89
+ "".join,
90
+ itertools.product(*zip(str_.upper(), str_.lower(), strict=True)),
91
+ )
92
+ )
87
93
 
88
94
 
89
95
  def get_none_variation() -> frozenset:
@@ -150,14 +156,16 @@ def get_country_code_variation() -> dict:
150
156
  return variations
151
157
 
152
158
 
153
- def list_tuple_without_none(list_tuple: Union[List, Tuple]) -> Union[List, Tuple]:
159
+ def list_tuple_without_none(
160
+ list_tuple: list | tuple,
161
+ ) -> list | tuple:
154
162
  """Removes None variations from a list or tuple.
155
163
 
156
164
  Args:
157
165
  list_tuple: Input list or tuple to clean.
158
166
 
159
167
  Returns:
160
- Union[List, Tuple]: Cleaned list or tuple.
168
+ list | tuple: Cleaned list or tuple.
161
169
 
162
170
  Raises:
163
171
  TypeError: If input is not a list or tuple.
@@ -185,55 +193,9 @@ def list_tuple_without_none(list_tuple: Union[List, Tuple]) -> Union[List, Tuple
185
193
  return tuple(item for item in list_tuple if _keep(item))
186
194
 
187
195
 
188
-
189
- def string_dlt_to_dlt(dlt_str_rep: str) -> Union[Dict, List, Tuple]:
190
- """Converts string representation of data structures to actual Python objects.
191
-
192
- Args:
193
- dlt_str_rep (str): String representation of dictionary/list/tuple.
194
-
195
- Returns:
196
- Union[Dict, List, Tuple]: Converted Python data structure.
197
-
198
- Examples:
199
- >>> string_dlt_to_dlt("[1, 2, 3]")
200
- [1, 2, 3]
201
- >>> string_dlt_to_dlt("{'a': 1, 'b': 2}")
202
- {'a': 1, 'b': 2}
203
- >>> string_dlt_to_dlt("('1', '2', '3')")
204
- ('1', '2', '3')
205
- """
206
- try:
207
- from ast import literal_eval
208
-
209
- return literal_eval(dlt_str_rep)
210
- except (ValueError, SyntaxError) as e:
211
- raise ValueError(f"Invalid data structure string: {dlt_str_rep}") from e
212
-
213
-
214
- def string_str_to_str(string_str_rep: str) -> str:
215
- """Converts string representation to a clean string by removing quotes.
216
-
217
- Args:
218
- string_str_rep (str): String representation to clean.
219
-
220
- Returns:
221
- str: Cleaned string with outer quotes removed.
222
-
223
- Examples:
224
- >>> string_str_to_str("'test'")
225
- 'test'
226
- >>> string_str_to_str('"test"')
227
- 'test'
228
- """
229
- if not string_str_rep:
230
- raise ValueError("Input string cannot be empty or None")
231
- return string_str_rep.strip("'\"")
232
-
233
-
234
196
  def delete_list_indices(
235
197
  list_: list,
236
- indices: List[int],
198
+ indices: list[int],
237
199
  ) -> None:
238
200
  """Deletes multiple indices from a list in-place.
239
201
 
@@ -253,9 +215,9 @@ def delete_list_indices(
253
215
 
254
216
  # FILESYSTEM
255
217
  def get_path_files(
256
- path: Union[str, Path],
257
- keywords: List[str],
258
- ) -> List[str]:
218
+ path: str | Path,
219
+ keywords: list[str],
220
+ ) -> list[str]:
259
221
  """Returns sorted list of files from given path that contain specified keywords.
260
222
 
261
223
  Args:
@@ -263,10 +225,10 @@ def get_path_files(
263
225
  keywords (list): List of keywords to match in filenames.
264
226
 
265
227
  Returns:
266
- List[str]: Sorted list of matching filenames.
228
+ list[str]: Sorted list of matching filenames.
267
229
 
268
230
  Examples:
269
- >>> get_path_files("test_base_folder", ["py"])
231
+ >>> get_path_files("test_base_folder", ["py"]) # doctest: +SKIP
270
232
  ['test1.py', 'test2.py', 'test3.py', 'test4.py', 'test5.py']
271
233
  """
272
234
  path = Path(path)
@@ -281,7 +243,7 @@ def get_path_files(
281
243
 
282
244
 
283
245
  def remove_path_file(
284
- path: Union[str, Path],
246
+ path: str | Path,
285
247
  keyword: str,
286
248
  n: int = 2,
287
249
  ) -> None:
@@ -293,7 +255,7 @@ def remove_path_file(
293
255
  n (int, optional): Number of newest files to keep. Defaults to 2.
294
256
 
295
257
  Examples:
296
- >>> remove_path_file("test_base_folder", ".py")
258
+ >>> remove_path_file("test_base_folder", ".py") # doctest: +SKIP
297
259
  """
298
260
  if n < 0:
299
261
  raise ValueError("n must be non-negative")
@@ -311,8 +273,8 @@ def remove_path_file(
311
273
 
312
274
 
313
275
  def list_to_file(
314
- filepath: Union[str, Path],
315
- list_: List,
276
+ filepath: str | Path,
277
+ list_: list,
316
278
  newline: bool = True,
317
279
  ) -> None:
318
280
  """Writes list contents to a file.
@@ -323,7 +285,7 @@ def list_to_file(
323
285
  newline (bool, optional): Whether to add newline after each item. Defaults to True.
324
286
 
325
287
  Examples:
326
- >>> list_to_file("test.txt", [1, 2, 3])
288
+ >>> list_to_file("test.txt", [1, 2, 3]) # doctest: +SKIP
327
289
  """
328
290
  filepath = Path(filepath)
329
291
  try:
@@ -333,7 +295,7 @@ def list_to_file(
333
295
  if newline:
334
296
  f.write("\n")
335
297
  logger.info(f"Successfully wrote to file: {filepath}")
336
- except IOError as e:
298
+ except OSError as e:
337
299
  logger.error(f"Failed to write to file {filepath}: {e}")
338
300
  raise
339
301
 
@@ -341,7 +303,7 @@ def list_to_file(
341
303
  # URLLIB
342
304
  def create_encode_url(
343
305
  url: str,
344
- query_params: Optional[Dict[str, Any]] = None,
306
+ query_params: dict[str, Any] | None = None,
345
307
  ) -> str:
346
308
  """Creates an encoded URL with query parameters.
347
309
 
@@ -378,14 +340,14 @@ def create_encode_url(
378
340
 
379
341
 
380
342
  # SYSTEM
381
- def parse_ps_aux(ps_aux_commands: str) -> List[List[str]]:
343
+ def parse_ps_aux(ps_aux_commands: str) -> list[list[str]]:
382
344
  """Parses Linux ps aux command output into a list of records.
383
345
 
384
346
  Args:
385
347
  ps_aux_commands (str): Linux ps aux command string.
386
348
 
387
349
  Returns:
388
- List[List[str]]: List of process records.
350
+ list[list[str]]: List of process records.
389
351
 
390
352
  Examples:
391
353
  >>> # parse_ps_aux("ps aux | egrep -i '%cpu|anaconda3' | head")
@@ -413,26 +375,3 @@ def parse_ps_aux(ps_aux_commands: str) -> List[List[str]]:
413
375
  except subprocess.SubprocessError as e:
414
376
  logger.error(f"Failed to execute command: {e}")
415
377
  raise
416
-
417
-
418
- if __name__ == "__main__":
419
- import doctest
420
-
421
- # Setup test environment
422
- test_folder = Path("test_base_folder")
423
- try:
424
- test_folder.mkdir(exist_ok=True)
425
- for i in range(1, 6):
426
- (test_folder / f"test{i}.py").touch()
427
-
428
- doctest.testmod()
429
- finally:
430
- # Cleanup
431
- if test_folder.exists():
432
- for file in test_folder.iterdir():
433
- file.unlink()
434
- test_folder.rmdir()
435
-
436
- test_file = Path("test.txt")
437
- if test_file.exists():
438
- test_file.unlink()
@@ -1,10 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING
3
5
 
4
6
  try:
5
7
  from loguru import logger
6
8
  except ImportError:
7
9
  import logging
10
+
8
11
  logger = logging.getLogger(__name__)
9
12
 
10
13
  if TYPE_CHECKING:
@@ -17,14 +20,14 @@ DEFAULT_HEADERS = {
17
20
  }
18
21
 
19
22
 
20
- def preprocess(html: str) -> Optional[str]:
23
+ def preprocess(html: str) -> str | None:
21
24
  """Removes whitespaces and newline characters from HTML string.
22
25
 
23
26
  Args:
24
27
  html (str): HTML string to be cleaned.
25
28
 
26
29
  Returns:
27
- Optional[str]: Cleaned HTML string with normalized whitespace.
30
+ str | None: Cleaned HTML string with normalized whitespace.
28
31
 
29
32
  Examples:
30
33
  >>> a = "<html> <p> Something </p> </html> "
@@ -48,8 +51,8 @@ def build_soup(
48
51
  features: str = "lxml",
49
52
  to_preprocess: bool = True,
50
53
  timeout: int = DEFAULT_TIMEOUT,
51
- headers: Optional[dict] = None,
52
- ) -> "Optional[BeautifulSoup]":
54
+ headers: dict | None = None,
55
+ ) -> BeautifulSoup | None:
53
56
  """Creates a BeautifulSoup object from a given URL.
54
57
 
55
58
  Args:
@@ -57,14 +60,14 @@ def build_soup(
57
60
  features (str, optional): Parser to use. Defaults to "lxml".
58
61
  to_preprocess (bool, optional): Whether to preprocess the HTML. Defaults to True.
59
62
  timeout (int, optional): Request timeout in seconds. Defaults to 10.
60
- headers (Optional[dict], optional): Custom headers for the request. Defaults to None.
63
+ headers (dict | None, optional): Custom headers for the request. Defaults to None.
61
64
 
62
65
  Returns:
63
- Optional[BeautifulSoup]: Parsed BeautifulSoup object, or None if request fails.
66
+ BeautifulSoup | None: Parsed BeautifulSoup object, or None if request fails.
64
67
 
65
68
  Examples:
66
- >>> a = build_soup("https://google.com")
67
- >>> type(a)
69
+ >>> a = build_soup("https://google.com") # doctest: +SKIP
70
+ >>> type(a) # doctest: +SKIP
68
71
  <class 'bs4.BeautifulSoup'>
69
72
 
70
73
  Note:
@@ -83,18 +86,16 @@ def build_soup(
83
86
 
84
87
  try:
85
88
  with requests.Session() as session:
86
- response = session.get(url, headers=request_headers, timeout=timeout)
89
+ response = session.get(
90
+ url, headers=request_headers, timeout=timeout
91
+ )
87
92
  response.raise_for_status()
88
93
 
89
- html_content = preprocess(response.text) if to_preprocess else response.text
94
+ html_content = (
95
+ preprocess(response.text) if to_preprocess else response.text
96
+ )
90
97
  return BeautifulSoup(html_content, features=features)
91
98
 
92
99
  except requests.RequestException as e:
93
100
  logger.error(f"Failed to fetch URL {url}: {str(e)}")
94
101
  return None
95
-
96
-
97
- if __name__ == "__main__":
98
- import doctest
99
-
100
- doctest.testmod()