toolify 0.2.0__tar.gz → 1.0.0__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 (36) hide show
  1. {toolify-0.2.0 → toolify-1.0.0}/LICENSE +2 -2
  2. toolify-1.0.0/PKG-INFO +100 -0
  3. toolify-1.0.0/README.md +61 -0
  4. toolify-1.0.0/pyproject.toml +68 -0
  5. toolify-1.0.0/setup.py +3 -0
  6. toolify-1.0.0/tests/test_ai.py +223 -0
  7. toolify-1.0.0/tests/test_tools.py +103 -0
  8. toolify-1.0.0/tests/test_youtube.py +190 -0
  9. toolify-1.0.0/toolify/__init__.py +35 -0
  10. toolify-1.0.0/toolify/ai/__init__.py +15 -0
  11. toolify-1.0.0/toolify/ai/ai.py +15 -0
  12. toolify-1.0.0/toolify/ai/huggingface.py +439 -0
  13. toolify-1.0.0/toolify/audio/__init__.py +15 -0
  14. toolify-1.0.0/toolify/audio/audio.py +199 -0
  15. toolify-1.0.0/toolify/plots/__init__.py +7 -0
  16. toolify-1.0.0/toolify/plots/plots.py +75 -0
  17. toolify-1.0.0/toolify/tools/__init__.py +12 -0
  18. toolify-1.0.0/toolify/tools/constants.py +178 -0
  19. toolify-1.0.0/toolify/tools/tools.py +369 -0
  20. toolify-1.0.0/toolify/youtube/__init__.py +21 -0
  21. toolify-1.0.0/toolify/youtube/youtube.py +453 -0
  22. toolify-1.0.0/toolify.egg-info/PKG-INFO +100 -0
  23. toolify-1.0.0/toolify.egg-info/SOURCES.txt +25 -0
  24. toolify-1.0.0/toolify.egg-info/requires.txt +20 -0
  25. toolify-0.2.0/PKG-INFO +0 -107
  26. toolify-0.2.0/README.md +0 -74
  27. toolify-0.2.0/setup.py +0 -33
  28. toolify-0.2.0/toolify/__init__.py +0 -1
  29. toolify-0.2.0/toolify/tools-old.py +0 -234
  30. toolify-0.2.0/toolify/tools.py +0 -292
  31. toolify-0.2.0/toolify.egg-info/PKG-INFO +0 -107
  32. toolify-0.2.0/toolify.egg-info/SOURCES.txt +0 -11
  33. toolify-0.2.0/toolify.egg-info/requires.txt +0 -2
  34. {toolify-0.2.0 → toolify-1.0.0}/setup.cfg +0 -0
  35. {toolify-0.2.0 → toolify-1.0.0}/toolify.egg-info/dependency_links.txt +0 -0
  36. {toolify-0.2.0 → toolify-1.0.0}/toolify.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 Amr Abdelsamea
3
+ Copyright (c) 2025 Amr Abdelsamea
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
toolify-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: toolify
3
+ Version: 1.0.0
4
+ Summary: Utilities for terminal output, Arabic text, audio, plotting, Hugging Face, and YouTube.
5
+ Author-email: Amr Abdelsamea <amr.abdelsamee33@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Amr-abdelsamee/toolify
8
+ Project-URL: Source, https://github.com/Amr-abdelsamee/toolify
9
+ Project-URL: Documentation, https://amr-abdelsamee.github.io/toolify/
10
+ Project-URL: Issues, https://github.com/Amr-abdelsamee/toolify/issues
11
+ Project-URL: Changelog, https://github.com/Amr-abdelsamee/toolify/blob/main/CHANGELOG.md
12
+ Keywords: tools,arabic,audio,plotting,youtube,huggingface
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: python-bidi==0.6.6
21
+ Requires-Dist: arabic-reshaper==3.0.0
22
+ Requires-Dist: youtube-transcript-api==1.1.0
23
+ Requires-Dist: yt-dlp==2025.10.22
24
+ Requires-Dist: tqdm==4.67.1
25
+ Requires-Dist: matplotlib==3.11.0
26
+ Requires-Dist: librosa==0.11.0
27
+ Requires-Dist: numpy==2.4.6
28
+ Requires-Dist: soundfile==0.14.0
29
+ Requires-Dist: huggingface-hub==1.21.0
30
+ Requires-Dist: hf-transfer==0.1.9
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest; extra == "dev"
33
+ Requires-Dist: build; extra == "dev"
34
+ Requires-Dist: twine; extra == "dev"
35
+ Provides-Extra: docs
36
+ Requires-Dist: mkdocs-material; extra == "docs"
37
+ Requires-Dist: mkdocstrings[python]; extra == "docs"
38
+ Dynamic: license-file
39
+
40
+ # Toolify
41
+
42
+ [![PyPI](https://img.shields.io/pypi/v/toolify)](https://pypi.org/project/toolify/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/toolify)](https://pypi.org/project/toolify/)
44
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://amr-abdelsamee.github.io/toolify/)
45
+ [![License](https://img.shields.io/pypi/l/toolify)](https://github.com/Amr-abdelsamee/toolify/blob/main/LICENSE)
46
+
47
+ Toolify is a Python utility library for terminal output, Arabic text handling,
48
+ logging, plotting, audio inspection, Hugging Face downloads, and YouTube media
49
+ downloads.
50
+
51
+ ## Features
52
+
53
+ | Module | Purpose |
54
+ | --- | --- |
55
+ | `toolify.tools` | Colored output, Arabic text, tables, logging, and confirmation |
56
+ | `toolify.plots` | Line-plot generation |
57
+ | `toolify.audio` | Silence detection, spectrograms, and duration helpers |
58
+ | `toolify.ai` | Hugging Face repository inspection and downloads |
59
+ | `toolify.youtube` | YouTube video, transcript, and playlist utilities |
60
+
61
+ ## Installation
62
+
63
+ Install the latest release from PyPI:
64
+
65
+ ```bash
66
+ pip install toolify
67
+ ```
68
+
69
+ Python 3.11 or newer is required. FFmpeg is also required when YouTube video
70
+ and audio streams need to be merged.
71
+
72
+
73
+ ## Documentation
74
+
75
+ The complete guides and API reference are available in the
76
+ [Toolify documentation](https://amr-abdelsamee.github.io/toolify/).
77
+
78
+ - [Installation](https://amr-abdelsamee.github.io/toolify/getting-started/installation/)
79
+ - [Quick start](https://amr-abdelsamee.github.io/toolify/getting-started/quickstart/)
80
+ - [User guides](https://amr-abdelsamee.github.io/toolify/guides/tools/)
81
+ - [API reference](https://amr-abdelsamee.github.io/toolify/reference/tools/)
82
+ - [Changelog](https://github.com/Amr-abdelsamee/toolify/blob/main/CHANGELOG.md)
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ git clone https://github.com/Amr-abdelsamee/toolify.git
88
+ cd toolify
89
+ pip install -e ".[dev,docs]"
90
+ python -m pytest -m "not integration"
91
+ mkdocs serve
92
+ ```
93
+
94
+ See the [contributing guide](https://amr-abdelsamee.github.io/toolify/development/contributing/)
95
+ and [release guide](https://amr-abdelsamee.github.io/toolify/development/releasing/).
96
+
97
+ ## License
98
+
99
+ Toolify is licensed under the
100
+ [MIT License](https://github.com/Amr-abdelsamee/toolify/blob/main/LICENSE).
@@ -0,0 +1,61 @@
1
+ # Toolify
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/toolify)](https://pypi.org/project/toolify/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/toolify)](https://pypi.org/project/toolify/)
5
+ [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://amr-abdelsamee.github.io/toolify/)
6
+ [![License](https://img.shields.io/pypi/l/toolify)](https://github.com/Amr-abdelsamee/toolify/blob/main/LICENSE)
7
+
8
+ Toolify is a Python utility library for terminal output, Arabic text handling,
9
+ logging, plotting, audio inspection, Hugging Face downloads, and YouTube media
10
+ downloads.
11
+
12
+ ## Features
13
+
14
+ | Module | Purpose |
15
+ | --- | --- |
16
+ | `toolify.tools` | Colored output, Arabic text, tables, logging, and confirmation |
17
+ | `toolify.plots` | Line-plot generation |
18
+ | `toolify.audio` | Silence detection, spectrograms, and duration helpers |
19
+ | `toolify.ai` | Hugging Face repository inspection and downloads |
20
+ | `toolify.youtube` | YouTube video, transcript, and playlist utilities |
21
+
22
+ ## Installation
23
+
24
+ Install the latest release from PyPI:
25
+
26
+ ```bash
27
+ pip install toolify
28
+ ```
29
+
30
+ Python 3.11 or newer is required. FFmpeg is also required when YouTube video
31
+ and audio streams need to be merged.
32
+
33
+
34
+ ## Documentation
35
+
36
+ The complete guides and API reference are available in the
37
+ [Toolify documentation](https://amr-abdelsamee.github.io/toolify/).
38
+
39
+ - [Installation](https://amr-abdelsamee.github.io/toolify/getting-started/installation/)
40
+ - [Quick start](https://amr-abdelsamee.github.io/toolify/getting-started/quickstart/)
41
+ - [User guides](https://amr-abdelsamee.github.io/toolify/guides/tools/)
42
+ - [API reference](https://amr-abdelsamee.github.io/toolify/reference/tools/)
43
+ - [Changelog](https://github.com/Amr-abdelsamee/toolify/blob/main/CHANGELOG.md)
44
+
45
+ ## Development
46
+
47
+ ```bash
48
+ git clone https://github.com/Amr-abdelsamee/toolify.git
49
+ cd toolify
50
+ pip install -e ".[dev,docs]"
51
+ python -m pytest -m "not integration"
52
+ mkdocs serve
53
+ ```
54
+
55
+ See the [contributing guide](https://amr-abdelsamee.github.io/toolify/development/contributing/)
56
+ and [release guide](https://amr-abdelsamee.github.io/toolify/development/releasing/).
57
+
58
+ ## License
59
+
60
+ Toolify is licensed under the
61
+ [MIT License](https://github.com/Amr-abdelsamee/toolify/blob/main/LICENSE).
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "toolify"
7
+ version = "1.0.0"
8
+ description = "Utilities for terminal output, Arabic text, audio, plotting, Hugging Face, and YouTube."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+
13
+ authors = [
14
+ { name = "Amr Abdelsamea", email = "amr.abdelsamee33@gmail.com" }
15
+ ]
16
+
17
+ keywords = [
18
+ "tools",
19
+ "arabic",
20
+ "audio",
21
+ "plotting",
22
+ "youtube",
23
+ "huggingface",
24
+ ]
25
+
26
+ classifiers = [
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Operating System :: OS Independent",
31
+ ]
32
+
33
+ dependencies = [
34
+ "python-bidi==0.6.6",
35
+ "arabic-reshaper==3.0.0",
36
+ "youtube-transcript-api==1.1.0",
37
+ "yt-dlp==2025.10.22",
38
+ "tqdm==4.67.1",
39
+ "matplotlib==3.11.0",
40
+ "librosa==0.11.0",
41
+ "numpy==2.4.6",
42
+ "soundfile==0.14.0",
43
+ "huggingface-hub==1.21.0",
44
+ "hf-transfer==0.1.9",
45
+ ]
46
+
47
+ [project.optional-dependencies]
48
+ dev = [
49
+ "pytest",
50
+ "build",
51
+ "twine",
52
+ ]
53
+
54
+ docs = [
55
+ "mkdocs-material",
56
+ "mkdocstrings[python]",
57
+ ]
58
+
59
+ [project.urls]
60
+ Homepage = "https://github.com/Amr-abdelsamee/toolify"
61
+ Source = "https://github.com/Amr-abdelsamee/toolify"
62
+ Documentation = "https://amr-abdelsamee.github.io/toolify/"
63
+ Issues = "https://github.com/Amr-abdelsamee/toolify/issues"
64
+ Changelog = "https://github.com/Amr-abdelsamee/toolify/blob/main/CHANGELOG.md"
65
+
66
+ [tool.setuptools.packages.find]
67
+ include = ["toolify*"]
68
+ exclude = ["tests*", "docs*"]
toolify-1.0.0/setup.py ADDED
@@ -0,0 +1,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()
@@ -0,0 +1,223 @@
1
+ import os
2
+ from pathlib import Path
3
+ from types import SimpleNamespace
4
+
5
+ import pytest
6
+
7
+ import toolify.ai.huggingface as hf
8
+ from toolify.ai import (
9
+ get_hf_model_size,
10
+ get_hf_dataset_size,
11
+ download_hf_model,
12
+ download_hf_dataset,
13
+ )
14
+
15
+
16
+ GB = 1024**3
17
+
18
+
19
+ def test_ai_public_imports():
20
+ assert callable(get_hf_model_size)
21
+ assert callable(get_hf_dataset_size)
22
+ assert callable(download_hf_model)
23
+ assert callable(download_hf_dataset)
24
+
25
+
26
+ def test_get_hf_model_size_falls_back_when_safetensors_size_is_none(monkeypatch):
27
+ """This test catches the real bug:
28
+ safetensors metadata exists but total size cannot be extracted.
29
+ The fallback must reset total_bytes to 0, not keep it as None.
30
+ """
31
+
32
+ class FakeHfApi:
33
+ def __init__(self, token=None):
34
+ self.token = token
35
+
36
+ def get_safetensors_metadata(self, repo_id, revision=None):
37
+ # No total_size, no metadata["total_size"], no files_metadata sizes.
38
+ return SimpleNamespace()
39
+
40
+ def model_info(self, repo_id, revision=None, files_metadata=True):
41
+ return SimpleNamespace(
42
+ siblings=[
43
+ SimpleNamespace(size=1 * GB),
44
+ SimpleNamespace(size=2 * GB),
45
+ SimpleNamespace(size=None),
46
+ ]
47
+ )
48
+
49
+ monkeypatch.setattr(
50
+ hf,
51
+ "_require_huggingface_hub",
52
+ lambda: (FakeHfApi, None),
53
+ )
54
+
55
+ size = hf.get_hf_model_size("org/test-model", verbose=False)
56
+
57
+ assert size == 3.0
58
+
59
+
60
+ def test_get_hf_model_size_uses_safetensors_when_valid(monkeypatch):
61
+ class FakeHfApi:
62
+ def __init__(self, token=None):
63
+ self.token = token
64
+
65
+ def get_safetensors_metadata(self, repo_id, revision=None):
66
+ return SimpleNamespace(total_size=4 * GB)
67
+
68
+ def model_info(self, *args, **kwargs):
69
+ raise AssertionError("model_info should not be called")
70
+
71
+ monkeypatch.setattr(
72
+ hf,
73
+ "_require_huggingface_hub",
74
+ lambda: (FakeHfApi, None),
75
+ )
76
+
77
+ size = hf.get_hf_model_size("org/test-model", verbose=False)
78
+
79
+ assert size == 4.0
80
+
81
+
82
+ def test_get_hf_dataset_size(monkeypatch):
83
+ class FakeHfApi:
84
+ def __init__(self, token=None):
85
+ self.token = token
86
+
87
+ def dataset_info(self, repo_id, revision=None, expand=None):
88
+ assert expand == ["usedStorage"]
89
+ return SimpleNamespace(usedStorage=5 * GB)
90
+
91
+ monkeypatch.setattr(
92
+ hf,
93
+ "_require_huggingface_hub",
94
+ lambda: (FakeHfApi, None),
95
+ )
96
+
97
+ size = hf.get_hf_dataset_size("org/test-dataset", verbose=False)
98
+
99
+ assert size == 5.0
100
+
101
+
102
+ def test_download_hf_model_uses_local_dir(monkeypatch, tmp_path):
103
+ calls = {}
104
+
105
+ class FakeHfApi:
106
+ pass
107
+
108
+ def fake_snapshot_download(**kwargs):
109
+ calls.update(kwargs)
110
+
111
+ local_dir = Path(kwargs["local_dir"])
112
+ local_dir.mkdir(parents=True, exist_ok=True)
113
+ (local_dir / "README.md").write_text("fake model", encoding="utf-8")
114
+
115
+ return str(local_dir)
116
+
117
+ monkeypatch.setattr(
118
+ hf,
119
+ "_require_huggingface_hub",
120
+ lambda: (FakeHfApi, fake_snapshot_download),
121
+ )
122
+
123
+ target_dir = tmp_path / "custom_model"
124
+
125
+ path = hf.download_hf_model(
126
+ "org/test-model",
127
+ local_dir=target_dir,
128
+ show_size=False,
129
+ verbose=False,
130
+ )
131
+
132
+ assert path == target_dir
133
+ assert path.exists()
134
+ assert (path / "README.md").exists()
135
+ assert calls["repo_id"] == "org/test-model"
136
+ assert calls["repo_type"] == "model"
137
+ assert Path(calls["local_dir"]) == target_dir
138
+
139
+
140
+ def test_download_hf_dataset_uses_base_dir(monkeypatch, tmp_path):
141
+ calls = {}
142
+
143
+ class FakeHfApi:
144
+ pass
145
+
146
+ def fake_snapshot_download(**kwargs):
147
+ calls.update(kwargs)
148
+
149
+ local_dir = Path(kwargs["local_dir"])
150
+ local_dir.mkdir(parents=True, exist_ok=True)
151
+ (local_dir / "README.md").write_text("fake dataset", encoding="utf-8")
152
+
153
+ return str(local_dir)
154
+
155
+ monkeypatch.setattr(
156
+ hf,
157
+ "_require_huggingface_hub",
158
+ lambda: (FakeHfApi, fake_snapshot_download),
159
+ )
160
+
161
+ path = hf.download_hf_dataset(
162
+ "org/test-dataset",
163
+ base_dir=tmp_path / "datasets",
164
+ show_size=False,
165
+ verbose=False,
166
+ )
167
+
168
+ expected = tmp_path / "datasets" / "org_test-dataset"
169
+
170
+ assert path == expected
171
+ assert path.exists()
172
+ assert (path / "README.md").exists()
173
+ assert calls["repo_id"] == "org/test-dataset"
174
+ assert calls["repo_type"] == "dataset"
175
+ assert Path(calls["local_dir"]) == expected
176
+
177
+
178
+ def test_download_hf_repo_rejects_invalid_repo_type(tmp_path):
179
+ with pytest.raises(ValueError):
180
+ hf.download_hf_repo(
181
+ "org/test-repo",
182
+ repo_type="invalid", # type: ignore[arg-type]
183
+ base_dir=tmp_path,
184
+ verbose=False,
185
+ )
186
+
187
+
188
+ @pytest.mark.integration
189
+ def test_integration_get_real_hf_model_size():
190
+ """Real Hugging Face test.
191
+
192
+ Run only when:
193
+ RUN_HF_INTEGRATION=1 python -m pytest tests/test_ai.py -m integration -v
194
+ """
195
+ if os.getenv("RUN_HF_INTEGRATION") != "1":
196
+ pytest.skip("Set RUN_HF_INTEGRATION=1 to run Hugging Face integration tests.")
197
+
198
+ size = hf.get_hf_model_size(
199
+ "Qwen/Qwen3-Reranker-0.6B",
200
+ verbose=True,
201
+ raise_on_error=True,
202
+ )
203
+
204
+ assert size is not None
205
+ assert size > 0
206
+
207
+
208
+ @pytest.mark.integration
209
+ def test_integration_download_model_readme_only(tmp_path):
210
+ """Downloads only tiny files from a real model repo."""
211
+ if os.getenv("RUN_HF_INTEGRATION") != "1":
212
+ pytest.skip("Set RUN_HF_INTEGRATION=1 to run Hugging Face integration tests.")
213
+
214
+ path = hf.download_hf_model(
215
+ "Qwen/Qwen3-Reranker-0.6B",
216
+ local_dir=tmp_path / "qwen_reranker_test",
217
+ allow_patterns=["README.md", "config.json"],
218
+ show_size=False,
219
+ verbose=True,
220
+ )
221
+
222
+ assert path.exists()
223
+ assert any(path.iterdir())
@@ -0,0 +1,103 @@
1
+ import os
2
+ import logging
3
+ import pytest
4
+ from datetime import datetime
5
+
6
+ from toolify.tools import (
7
+ pct,
8
+ print_table,
9
+ setup_logger,
10
+ strip_tashkeel,
11
+ confirm,
12
+ )
13
+
14
+
15
+ def test_strip_tashkeel_removes_arabic_diacritics():
16
+ text = "مُحَمَّدٌ"
17
+ result = strip_tashkeel(text)
18
+
19
+ assert result == "محمد"
20
+
21
+
22
+ def test_strip_tashkeel_removes_special_symbols_by_default():
23
+ text = "ا^ل>س<لام؞"
24
+ result = strip_tashkeel(text)
25
+
26
+ assert result == "السلام"
27
+
28
+
29
+ def test_strip_tashkeel_can_keep_special_symbols():
30
+ text = "ا^ل>س<لام؞"
31
+ result = strip_tashkeel(text, remove_special_symbols=False)
32
+
33
+ assert result == "ا^ل>س<لام؞"
34
+
35
+
36
+ def test_pct_prints_plain_text_when_ec_false(capsys):
37
+ pct("Hello Toolify", ec=False)
38
+
39
+ captured = capsys.readouterr()
40
+
41
+ assert captured.out == "Hello Toolify\n"
42
+
43
+
44
+ def test_pct_prints_with_emoji_when_ec_false(capsys):
45
+ pct("Done", ec=False, emoji="success", end_emoji="fire")
46
+
47
+ captured = capsys.readouterr()
48
+
49
+ assert captured.out == "✅ Done 🔥\n"
50
+
51
+
52
+ def test_pct_custom_end(capsys):
53
+ pct("Hello", ec=False, end="")
54
+
55
+ captured = capsys.readouterr()
56
+
57
+ assert captured.out == "Hello"
58
+
59
+
60
+ def test_print_table_outputs_headers_and_rows(capsys):
61
+ headers = ["Name", "Score"]
62
+ rows = [
63
+ ["Ali", 95],
64
+ ["Sara", 88],
65
+ ]
66
+
67
+ print_table(headers, rows)
68
+
69
+ captured = capsys.readouterr()
70
+
71
+ assert "Name" in captured.out
72
+ assert "Score" in captured.out
73
+ assert "Ali" in captured.out
74
+ assert "Sara" in captured.out
75
+
76
+
77
+ def test_setup_logger_creates_log_file(tmp_path):
78
+ date_str = datetime.now().strftime("%Y_%m_%d")
79
+ log_file = tmp_path /f"test.log"
80
+ logger = setup_logger(__name__, str(log_file))
81
+ log_file = tmp_path /f"test__{date_str}.log"
82
+ logger.info("Hello logger")
83
+
84
+ assert isinstance(logger, logging.Logger)
85
+ assert log_file.exists()
86
+
87
+ content = log_file.read_text(encoding="utf-8")
88
+ assert "Hello logger" in content
89
+
90
+
91
+ def test_confirm_returns_none_when_user_confirms(monkeypatch):
92
+ monkeypatch.setattr("builtins.input", lambda _: "yes")
93
+
94
+ result = confirm(message="Continue? ")
95
+
96
+ assert result is None
97
+
98
+
99
+ def test_confirm_exits_when_user_declines(monkeypatch):
100
+ monkeypatch.setattr("builtins.input", lambda _: "no")
101
+
102
+ with pytest.raises(SystemExit):
103
+ confirm(message="Continue? ")