winipedia-utils 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

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.

Potentially problematic release.


This version of winipedia-utils might be problematic. Click here for more details.

Files changed (90) hide show
  1. winipedia_utils/concurrent/concurrent.py +245 -245
  2. winipedia_utils/concurrent/multiprocessing.py +130 -130
  3. winipedia_utils/concurrent/multithreading.py +93 -93
  4. winipedia_utils/consts.py +21 -23
  5. winipedia_utils/data/__init__.py +1 -1
  6. winipedia_utils/data/dataframe/__init__.py +1 -1
  7. winipedia_utils/data/dataframe/cleaning.py +378 -378
  8. winipedia_utils/data/structures/__init__.py +1 -1
  9. winipedia_utils/data/structures/dicts.py +16 -16
  10. winipedia_utils/git/__init__.py +1 -1
  11. winipedia_utils/git/gitignore/__init__.py +1 -1
  12. winipedia_utils/git/gitignore/gitignore.py +136 -136
  13. winipedia_utils/git/pre_commit/__init__.py +1 -1
  14. winipedia_utils/git/pre_commit/config.py +70 -70
  15. winipedia_utils/git/pre_commit/hooks.py +109 -109
  16. winipedia_utils/git/pre_commit/run_hooks.py +49 -49
  17. winipedia_utils/iterating/__init__.py +1 -1
  18. winipedia_utils/iterating/iterate.py +29 -29
  19. winipedia_utils/logging/ansi.py +6 -6
  20. winipedia_utils/logging/config.py +64 -64
  21. winipedia_utils/logging/logger.py +26 -26
  22. winipedia_utils/modules/class_.py +119 -119
  23. winipedia_utils/modules/function.py +101 -101
  24. winipedia_utils/modules/module.py +379 -379
  25. winipedia_utils/modules/package.py +390 -390
  26. winipedia_utils/oop/mixins/meta.py +333 -333
  27. winipedia_utils/oop/mixins/mixin.py +37 -37
  28. winipedia_utils/os/__init__.py +1 -1
  29. winipedia_utils/os/os.py +63 -63
  30. winipedia_utils/projects/__init__.py +1 -1
  31. winipedia_utils/projects/poetry/__init__.py +1 -1
  32. winipedia_utils/projects/poetry/config.py +91 -91
  33. winipedia_utils/projects/poetry/poetry.py +31 -31
  34. winipedia_utils/projects/project.py +48 -48
  35. winipedia_utils/resources/__init__.py +1 -1
  36. winipedia_utils/resources/svgs/__init__.py +1 -1
  37. winipedia_utils/resources/svgs/download_arrow.svg +2 -2
  38. winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +5 -5
  39. winipedia_utils/resources/svgs/fullscreen_icon.svg +2 -2
  40. winipedia_utils/resources/svgs/menu_icon.svg +3 -3
  41. winipedia_utils/resources/svgs/pause_icon.svg +3 -3
  42. winipedia_utils/resources/svgs/play_icon.svg +16 -16
  43. winipedia_utils/resources/svgs/plus_icon.svg +23 -23
  44. winipedia_utils/resources/svgs/svg.py +15 -15
  45. winipedia_utils/security/__init__.py +1 -1
  46. winipedia_utils/security/cryptography.py +29 -29
  47. winipedia_utils/security/keyring.py +70 -70
  48. winipedia_utils/setup.py +47 -47
  49. winipedia_utils/testing/assertions.py +23 -23
  50. winipedia_utils/testing/convention.py +177 -177
  51. winipedia_utils/testing/create_tests.py +291 -291
  52. winipedia_utils/testing/fixtures.py +28 -28
  53. winipedia_utils/testing/tests/base/fixtures/__init__.py +1 -1
  54. winipedia_utils/testing/tests/base/fixtures/fixture.py +6 -6
  55. winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +33 -33
  56. winipedia_utils/testing/tests/base/fixtures/scopes/function.py +7 -7
  57. winipedia_utils/testing/tests/base/fixtures/scopes/module.py +31 -31
  58. winipedia_utils/testing/tests/base/fixtures/scopes/package.py +7 -7
  59. winipedia_utils/testing/tests/base/fixtures/scopes/session.py +312 -312
  60. winipedia_utils/testing/tests/base/utils/utils.py +82 -82
  61. winipedia_utils/testing/tests/conftest.py +32 -32
  62. winipedia_utils/text/string.py +126 -126
  63. {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.1.dist-info}/METADATA +1 -4
  64. winipedia_utils-0.2.1.dist-info/RECORD +80 -0
  65. {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.1.dist-info}/licenses/LICENSE +21 -21
  66. winipedia_utils/django/__init__.py +0 -24
  67. winipedia_utils/django/bulk.py +0 -538
  68. winipedia_utils/django/command.py +0 -334
  69. winipedia_utils/django/database.py +0 -289
  70. winipedia_utils/pyside/__init__.py +0 -1
  71. winipedia_utils/pyside/core/__init__.py +0 -1
  72. winipedia_utils/pyside/core/py_qiodevice.py +0 -476
  73. winipedia_utils/pyside/ui/__init__.py +0 -1
  74. winipedia_utils/pyside/ui/base/__init__.py +0 -1
  75. winipedia_utils/pyside/ui/base/base.py +0 -180
  76. winipedia_utils/pyside/ui/pages/__init__.py +0 -1
  77. winipedia_utils/pyside/ui/pages/base/__init__.py +0 -1
  78. winipedia_utils/pyside/ui/pages/base/base.py +0 -92
  79. winipedia_utils/pyside/ui/pages/browser.py +0 -26
  80. winipedia_utils/pyside/ui/pages/player.py +0 -85
  81. winipedia_utils/pyside/ui/widgets/__init__.py +0 -1
  82. winipedia_utils/pyside/ui/widgets/browser.py +0 -243
  83. winipedia_utils/pyside/ui/widgets/clickable_widget.py +0 -57
  84. winipedia_utils/pyside/ui/widgets/media_player.py +0 -430
  85. winipedia_utils/pyside/ui/widgets/notification.py +0 -78
  86. winipedia_utils/pyside/ui/windows/__init__.py +0 -1
  87. winipedia_utils/pyside/ui/windows/base/__init__.py +0 -1
  88. winipedia_utils/pyside/ui/windows/base/base.py +0 -49
  89. winipedia_utils-0.2.0.dist-info/RECORD +0 -103
  90. {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.1.dist-info}/WHEEL +0 -0
@@ -1,17 +1,17 @@
1
- <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
- <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3
- <svg width="800px" height="800px" viewBox="-3 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
4
-
5
- <title>play</title>
6
- <desc>Created with Sketch Beta.</desc>
7
- <defs>
8
-
9
- </defs>
10
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
11
- <g id="Icon-Set-Filled" sketch:type="MSLayerGroup" transform="translate(-419.000000, -571.000000)" fill="#000000">
12
- <path d="M440.415,583.554 L421.418,571.311 C420.291,570.704 419,570.767 419,572.946 L419,597.054 C419,599.046 420.385,599.36 421.418,598.689 L440.415,586.446 C441.197,585.647 441.197,584.353 440.415,583.554" id="play" sketch:type="MSShapeGroup">
13
-
14
- </path>
15
- </g>
16
- </g>
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3
+ <svg width="800px" height="800px" viewBox="-3 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
4
+
5
+ <title>play</title>
6
+ <desc>Created with Sketch Beta.</desc>
7
+ <defs>
8
+
9
+ </defs>
10
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
11
+ <g id="Icon-Set-Filled" sketch:type="MSLayerGroup" transform="translate(-419.000000, -571.000000)" fill="#000000">
12
+ <path d="M440.415,583.554 L421.418,571.311 C420.291,570.704 419,570.767 419,572.946 L419,597.054 C419,599.046 420.385,599.36 421.418,598.689 L440.415,586.446 C441.197,585.647 441.197,584.353 440.415,583.554" id="play" sketch:type="MSShapeGroup">
13
+
14
+ </path>
15
+ </g>
16
+ </g>
17
17
  </svg>
@@ -1,24 +1,24 @@
1
- <?xml version="1.0" ?>
2
-
3
- <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
4
- <svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
5
-
6
- <title/>
7
-
8
- <g id="Complete">
9
-
10
- <g data-name="add" id="add-2">
11
-
12
- <g>
13
-
14
- <line fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="12" x2="12" y1="19" y2="5"/>
15
-
16
- <line fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="5" x2="19" y1="12" y2="12"/>
17
-
18
- </g>
19
-
20
- </g>
21
-
22
- </g>
23
-
1
+ <?xml version="1.0" ?>
2
+
3
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
4
+ <svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
5
+
6
+ <title/>
7
+
8
+ <g id="Complete">
9
+
10
+ <g data-name="add" id="add-2">
11
+
12
+ <g>
13
+
14
+ <line fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="12" x2="12" y1="19" y2="5"/>
15
+
16
+ <line fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" x1="5" x2="19" y1="12" y2="12"/>
17
+
18
+ </g>
19
+
20
+ </g>
21
+
22
+ </g>
23
+
24
24
  </svg>
@@ -1,15 +1,15 @@
1
- """utils for svgs."""
2
-
3
- from importlib.resources import as_file, files
4
- from pathlib import Path
5
- from types import ModuleType
6
-
7
- from winipedia_utils.resources import svgs
8
-
9
-
10
- def get_svg_path(svg_name: str, package: ModuleType | None = None) -> Path:
11
- """Get the path to a svg."""
12
- package = package or svgs
13
- svg_path = files(package) / f"{svg_name}.svg"
14
- with as_file(svg_path) as path:
15
- return path
1
+ """utils for svgs."""
2
+
3
+ from importlib.resources import as_file, files
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+ from winipedia_utils.resources import svgs
8
+
9
+
10
+ def get_svg_path(svg_name: str, package: ModuleType | None = None) -> Path:
11
+ """Get the path to a svg."""
12
+ package = package or svgs
13
+ svg_path = files(package) / f"{svg_name}.svg"
14
+ with as_file(svg_path) as path:
15
+ return path
@@ -1 +1 @@
1
- """__init__ module for winipedia_utils.security."""
1
+ """__init__ module for winipedia_utils.security."""
@@ -1,29 +1,29 @@
1
- """Cryptography utilities for secure data handling.
2
-
3
- This module provides utility functions for working with cryptography,
4
- including encryption and decryption using Fernet and AESGCM.
5
- These utilities help with secure data handling.
6
- """
7
-
8
- import os
9
-
10
- from cryptography.hazmat.primitives.ciphers.aead import AESGCM
11
-
12
- IV_LEN = 12
13
-
14
-
15
- def encrypt_with_aes_gcm(
16
- aes_gcm: AESGCM, data: bytes, aad: bytes | None = None
17
- ) -> bytes:
18
- """Encrypt data using AESGCM."""
19
- iv = os.urandom(IV_LEN)
20
- encrypted = aes_gcm.encrypt(iv, data, aad)
21
- return iv + encrypted
22
-
23
-
24
- def decrypt_with_aes_gcm(
25
- aes_gcm: AESGCM, data: bytes, aad: bytes | None = None
26
- ) -> bytes:
27
- """Decrypt data using AESGCM."""
28
- iv, encrypted = data[:IV_LEN], data[IV_LEN:]
29
- return aes_gcm.decrypt(iv, encrypted, aad)
1
+ """Cryptography utilities for secure data handling.
2
+
3
+ This module provides utility functions for working with cryptography,
4
+ including encryption and decryption using Fernet and AESGCM.
5
+ These utilities help with secure data handling.
6
+ """
7
+
8
+ import os
9
+
10
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
11
+
12
+ IV_LEN = 12
13
+
14
+
15
+ def encrypt_with_aes_gcm(
16
+ aes_gcm: AESGCM, data: bytes, aad: bytes | None = None
17
+ ) -> bytes:
18
+ """Encrypt data using AESGCM."""
19
+ iv = os.urandom(IV_LEN)
20
+ encrypted = aes_gcm.encrypt(iv, data, aad)
21
+ return iv + encrypted
22
+
23
+
24
+ def decrypt_with_aes_gcm(
25
+ aes_gcm: AESGCM, data: bytes, aad: bytes | None = None
26
+ ) -> bytes:
27
+ """Decrypt data using AESGCM."""
28
+ iv, encrypted = data[:IV_LEN], data[IV_LEN:]
29
+ return aes_gcm.decrypt(iv, encrypted, aad)
@@ -1,70 +1,70 @@
1
- """Keyring utilities for secure storage and retrieval of secrets.
2
-
3
- This module provides utility functions for working with keyring,
4
- including getting and creating secrets and fernets.
5
- These utilities help with secure storage and retrieval of secrets.
6
- """
7
-
8
- from base64 import b64decode, b64encode
9
- from collections.abc import Callable
10
-
11
- import keyring
12
- from cryptography.fernet import Fernet
13
- from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
-
15
-
16
- def get_or_create_fernet(service_name: str, username: str) -> tuple[Fernet, bytes]:
17
- """Get the app secret using keyring.
18
-
19
- If it does not exist, create it with a Fernet.
20
- """
21
- return get_or_create_key(
22
- service_name, username, Fernet, lambda: Fernet.generate_key()
23
- )
24
-
25
-
26
- def get_or_create_aes_gcm(service_name: str, username: str) -> tuple[AESGCM, bytes]:
27
- """Get the app secret using keyring.
28
-
29
- If it does not exist, create it with a AESGCM.
30
- """
31
- return get_or_create_key(
32
- service_name, username, AESGCM, lambda: AESGCM.generate_key(bit_length=256)
33
- )
34
-
35
-
36
- def get_or_create_key[T](
37
- service_name: str,
38
- username: str,
39
- key_class: Callable[[bytes], T],
40
- generate_key_func: Callable[..., bytes],
41
- ) -> tuple[T, bytes]:
42
- """Get the app secret using keyring.
43
-
44
- If it does not exist, create it with the generate_func.
45
- """
46
- key = get_key_as_str(service_name, username, key_class)
47
- if key is None:
48
- binary_key = generate_key_func()
49
- key = b64encode(binary_key).decode("ascii")
50
- modified_service_name = make_service_name(service_name, key_class)
51
- keyring.set_password(modified_service_name, username, key)
52
-
53
- binary_key = b64decode(key)
54
- return key_class(binary_key), binary_key
55
-
56
-
57
- def get_key_as_str[T](
58
- service_name: str, username: str, key_class: Callable[[bytes], T]
59
- ) -> str | None:
60
- """Get the app secret using keyring.
61
-
62
- If it does not exist, create it with the generate_func.
63
- """
64
- service_name = make_service_name(service_name, key_class)
65
- return keyring.get_password(service_name, username)
66
-
67
-
68
- def make_service_name[T](service_name: str, key_class: Callable[[bytes], T]) -> str:
69
- """Make a service name from a service name and a key class."""
70
- return f"{service_name}_{key_class.__name__}"
1
+ """Keyring utilities for secure storage and retrieval of secrets.
2
+
3
+ This module provides utility functions for working with keyring,
4
+ including getting and creating secrets and fernets.
5
+ These utilities help with secure storage and retrieval of secrets.
6
+ """
7
+
8
+ from base64 import b64decode, b64encode
9
+ from collections.abc import Callable
10
+
11
+ import keyring
12
+ from cryptography.fernet import Fernet
13
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
+
15
+
16
+ def get_or_create_fernet(service_name: str, username: str) -> tuple[Fernet, bytes]:
17
+ """Get the app secret using keyring.
18
+
19
+ If it does not exist, create it with a Fernet.
20
+ """
21
+ return get_or_create_key(
22
+ service_name, username, Fernet, lambda: Fernet.generate_key()
23
+ )
24
+
25
+
26
+ def get_or_create_aes_gcm(service_name: str, username: str) -> tuple[AESGCM, bytes]:
27
+ """Get the app secret using keyring.
28
+
29
+ If it does not exist, create it with a AESGCM.
30
+ """
31
+ return get_or_create_key(
32
+ service_name, username, AESGCM, lambda: AESGCM.generate_key(bit_length=256)
33
+ )
34
+
35
+
36
+ def get_or_create_key[T](
37
+ service_name: str,
38
+ username: str,
39
+ key_class: Callable[[bytes], T],
40
+ generate_key_func: Callable[..., bytes],
41
+ ) -> tuple[T, bytes]:
42
+ """Get the app secret using keyring.
43
+
44
+ If it does not exist, create it with the generate_func.
45
+ """
46
+ key = get_key_as_str(service_name, username, key_class)
47
+ if key is None:
48
+ binary_key = generate_key_func()
49
+ key = b64encode(binary_key).decode("ascii")
50
+ modified_service_name = make_service_name(service_name, key_class)
51
+ keyring.set_password(modified_service_name, username, key)
52
+
53
+ binary_key = b64decode(key)
54
+ return key_class(binary_key), binary_key
55
+
56
+
57
+ def get_key_as_str[T](
58
+ service_name: str, username: str, key_class: Callable[[bytes], T]
59
+ ) -> str | None:
60
+ """Get the app secret using keyring.
61
+
62
+ If it does not exist, create it with the generate_func.
63
+ """
64
+ service_name = make_service_name(service_name, key_class)
65
+ return keyring.get_password(service_name, username)
66
+
67
+
68
+ def make_service_name[T](service_name: str, key_class: Callable[[bytes], T]) -> str:
69
+ """Make a service name from a service name and a key class."""
70
+ return f"{service_name}_{key_class.__name__}"
winipedia_utils/setup.py CHANGED
@@ -1,47 +1,47 @@
1
- """A script that can be called after you installed the package.
2
-
3
- This script calls create tests, creates the pre-commit config, and
4
- creates the pyproject.toml file and some other things to set up a project.
5
- This package assumes you are using poetry and pre-commit.
6
- This script is intended to be called once at the beginning of a project.
7
- """
8
-
9
- from winipedia_utils.git.gitignore.gitignore import _add_package_patterns_to_gitignore
10
- from winipedia_utils.git.pre_commit.config import (
11
- _add_package_hook_to_pre_commit_config,
12
- _pre_commit_install,
13
- )
14
- from winipedia_utils.git.pre_commit.run_hooks import _run_all_hooks
15
- from winipedia_utils.logging.logger import get_logger
16
- from winipedia_utils.projects.poetry.config import (
17
- _add_configurations_to_pyproject_toml,
18
- )
19
- from winipedia_utils.projects.poetry.poetry import (
20
- _install_dev_dependencies,
21
- )
22
- from winipedia_utils.projects.project import _create_project_root
23
-
24
- logger = get_logger(__name__)
25
-
26
-
27
- def _setup() -> None:
28
- """Set up the project."""
29
- # install winipedia_utils dev dependencies as dev
30
- _install_dev_dependencies()
31
- # create pre-commit config
32
- _add_package_hook_to_pre_commit_config()
33
- # install pre-commit
34
- _pre_commit_install()
35
- # add patterns to .gitignore
36
- _add_package_patterns_to_gitignore()
37
- # add tool.* configurations to pyproject.toml
38
- _add_configurations_to_pyproject_toml()
39
- # create the project root
40
- _create_project_root()
41
- # run pre-commit once, create tests is included here
42
- _run_all_hooks()
43
- logger.info("Setup complete!")
44
-
45
-
46
- if __name__ == "__main__":
47
- _setup()
1
+ """A script that can be called after you installed the package.
2
+
3
+ This script calls create tests, creates the pre-commit config, and
4
+ creates the pyproject.toml file and some other things to set up a project.
5
+ This package assumes you are using poetry and pre-commit.
6
+ This script is intended to be called once at the beginning of a project.
7
+ """
8
+
9
+ from winipedia_utils.git.gitignore.gitignore import _add_package_patterns_to_gitignore
10
+ from winipedia_utils.git.pre_commit.config import (
11
+ _add_package_hook_to_pre_commit_config,
12
+ _pre_commit_install,
13
+ )
14
+ from winipedia_utils.git.pre_commit.run_hooks import _run_all_hooks
15
+ from winipedia_utils.logging.logger import get_logger
16
+ from winipedia_utils.projects.poetry.config import (
17
+ _add_configurations_to_pyproject_toml,
18
+ )
19
+ from winipedia_utils.projects.poetry.poetry import (
20
+ _install_dev_dependencies,
21
+ )
22
+ from winipedia_utils.projects.project import _create_project_root
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ def _setup() -> None:
28
+ """Set up the project."""
29
+ # install winipedia_utils dev dependencies as dev
30
+ _install_dev_dependencies()
31
+ # create pre-commit config
32
+ _add_package_hook_to_pre_commit_config()
33
+ # install pre-commit
34
+ _pre_commit_install()
35
+ # add patterns to .gitignore
36
+ _add_package_patterns_to_gitignore()
37
+ # add tool.* configurations to pyproject.toml
38
+ _add_configurations_to_pyproject_toml()
39
+ # create the project root
40
+ _create_project_root()
41
+ # run pre-commit once, create tests is included here
42
+ _run_all_hooks()
43
+ logger.info("Setup complete!")
44
+
45
+
46
+ if __name__ == "__main__":
47
+ _setup()
@@ -1,23 +1,23 @@
1
- """Testing assertion utilities for enhanced test validation.
2
-
3
- This module provides custom assertion functions that extend Python's built-in
4
- assert statement with additional features like improved error messages and
5
- specialized validation logic for common testing scenarios.
6
- """
7
-
8
-
9
- def assert_with_msg(expr: bool, msg: str) -> None: # noqa: FBT001
10
- """Assert that an expression is true with a custom error message.
11
-
12
- A thin wrapper around Python's built-in assert statement that makes it
13
- easier to provide meaningful error messages when assertions fail.
14
-
15
- Args:
16
- expr: The expression to evaluate for truthiness
17
- msg: The error message to display if the assertion fails
18
-
19
- Raises:
20
- AssertionError: If the expression evaluates to False
21
-
22
- """
23
- assert expr, msg # noqa: S101 # nosec: B101
1
+ """Testing assertion utilities for enhanced test validation.
2
+
3
+ This module provides custom assertion functions that extend Python's built-in
4
+ assert statement with additional features like improved error messages and
5
+ specialized validation logic for common testing scenarios.
6
+ """
7
+
8
+
9
+ def assert_with_msg(expr: bool, msg: str) -> None: # noqa: FBT001
10
+ """Assert that an expression is true with a custom error message.
11
+
12
+ A thin wrapper around Python's built-in assert statement that makes it
13
+ easier to provide meaningful error messages when assertions fail.
14
+
15
+ Args:
16
+ expr: The expression to evaluate for truthiness
17
+ msg: The error message to display if the assertion fails
18
+
19
+ Raises:
20
+ AssertionError: If the expression evaluates to False
21
+
22
+ """
23
+ assert expr, msg # noqa: S101 # nosec: B101