dadascribe 0.1.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.
@@ -0,0 +1,46 @@
1
+ name: Publish Python Package to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ release-build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.x"
18
+ - name: Install build dependencies
19
+ run: |
20
+ python -m pip install --upgrade pip
21
+ python -m pip install build
22
+ - name: Build release distributions
23
+ run: python -m build
24
+ - name: Upload distributions
25
+ uses: actions/upload-artifact@v4
26
+ with:
27
+ name: release-dists
28
+ path: dist/
29
+
30
+ pypi-publish:
31
+ runs-on: ubuntu-latest
32
+ needs:
33
+ - release-build
34
+ permissions:
35
+ id-token: write
36
+ environment:
37
+ name: pypi
38
+ url: https://pypi.org/p/dadascribe
39
+ steps:
40
+ - name: Retrieve release distributions
41
+ uses: actions/download-artifact@v4
42
+ with:
43
+ name: release-dists
44
+ path: dist/
45
+ - name: Publish to PyPI
46
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ .env
2
+ *__pycache__
3
+ *.DS_Store
4
+
5
+ dist/
6
+
7
+ examples/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PatzEdi, Fabrizio Ferrari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: dadascribe
3
+ Version: 0.1.0
4
+ Summary: Python wrapper for the DaDaScribe API
5
+ Project-URL: Homepage, https://api.dadascribe.com/
6
+ Project-URL: Repository, https://github.com/PatzEdi/dadascribe-api-python/
7
+ Project-URL: Bug Tracker, https://github.com/PatzEdi/dadascribe-api-python/issues
8
+ Author-email: PatzEdi <patzedigithub@gmail.com>, Fabrizio Ferrari <fabriziovsm@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: diarization,transcriptions,whisper
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.13
16
+ Requires-Dist: requests
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Python wrapper for the DaDaScribe API
20
+
21
+ The official DaDaScribe API wrapper in Python.
22
+
23
+ ![DaDaScribe Logo](https://www.dadascribe.com/images/DaDaScribeLogo.svg)
24
+
25
+ ## CLI Interface
26
+ The wrapper offers a CLI interface.
27
+
28
+ To start a job, for example:
29
+ ```bash
30
+ $ dadascribe --source "link/to/youtube/or/path/to/file"
31
+ {
32
+ "status": "ok",
33
+ "id": "a7wuTaPrebOqheE0",
34
+ "count": 1
35
+ }
36
+ ```
37
+
38
+ Check a job status:
39
+ ```bash
40
+ $ dadascribe --status a7wuTaPrebOqheE0
41
+ ```
42
+
43
+ Easily download results of a completed job:
44
+ ```bash
45
+ $ dadadascribe --download a7wuTaPrebOqheE0
46
+ ```
47
+
48
+ ## Python API
49
+ The wrapper offers a Python API.
50
+
51
+ ```python
52
+ from dadascribe import ScribeAPIWrapper
53
+ api_key = ... # e.g. from an environment variable, as str
54
+ w = ScribeAPIWrapper(api_key)
55
+
56
+ # Example transcription:
57
+ trsc_result = w.transcribe(
58
+ source="youtube/link/or/path/to/file",
59
+ source_language="en",
60
+ destination_language="es,it",
61
+ )
62
+ ```
63
+
64
+
65
+ ## Development
66
+ ```bash
67
+ pip3 install -e .
68
+ ```
69
+
70
+ Make sure to run the tests as well via the run_tests.sh script:
71
+ ```bash
72
+ chmod +x run_tests.sh
73
+ ./run_tests.sh
74
+ ```
@@ -0,0 +1,56 @@
1
+ # Python wrapper for the DaDaScribe API
2
+
3
+ The official DaDaScribe API wrapper in Python.
4
+
5
+ ![DaDaScribe Logo](https://www.dadascribe.com/images/DaDaScribeLogo.svg)
6
+
7
+ ## CLI Interface
8
+ The wrapper offers a CLI interface.
9
+
10
+ To start a job, for example:
11
+ ```bash
12
+ $ dadascribe --source "link/to/youtube/or/path/to/file"
13
+ {
14
+ "status": "ok",
15
+ "id": "a7wuTaPrebOqheE0",
16
+ "count": 1
17
+ }
18
+ ```
19
+
20
+ Check a job status:
21
+ ```bash
22
+ $ dadascribe --status a7wuTaPrebOqheE0
23
+ ```
24
+
25
+ Easily download results of a completed job:
26
+ ```bash
27
+ $ dadadascribe --download a7wuTaPrebOqheE0
28
+ ```
29
+
30
+ ## Python API
31
+ The wrapper offers a Python API.
32
+
33
+ ```python
34
+ from dadascribe import ScribeAPIWrapper
35
+ api_key = ... # e.g. from an environment variable, as str
36
+ w = ScribeAPIWrapper(api_key)
37
+
38
+ # Example transcription:
39
+ trsc_result = w.transcribe(
40
+ source="youtube/link/or/path/to/file",
41
+ source_language="en",
42
+ destination_language="es,it",
43
+ )
44
+ ```
45
+
46
+
47
+ ## Development
48
+ ```bash
49
+ pip3 install -e .
50
+ ```
51
+
52
+ Make sure to run the tests as well via the run_tests.sh script:
53
+ ```bash
54
+ chmod +x run_tests.sh
55
+ ./run_tests.sh
56
+ ```
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dadascribe"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "PatzEdi", email = "patzedigithub@gmail.com" },
10
+ { name = "Fabrizio Ferrari", email = "fabriziovsm@gmail.com" }
11
+ ]
12
+ description = "Python wrapper for the DaDaScribe API"
13
+ readme = "README.md"
14
+ keywords = ["transcriptions", "diarization", "whisper"]
15
+ requires-python = ">=3.13"
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+ license="MIT"
22
+ license-files=["LICENSE"]
23
+
24
+ dependencies = ["requests"]
25
+
26
+ [project.scripts]
27
+ dadascribe = "dadascribe.cli:main"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/dadascribe"]
31
+
32
+ [project.urls]
33
+ "Homepage" = "https://api.dadascribe.com/"
34
+ "Repository" = "https://github.com/PatzEdi/dadascribe-api-python/"
35
+ "Bug Tracker" = "https://github.com/PatzEdi/dadascribe-api-python/issues"
@@ -0,0 +1,2 @@
1
+
2
+ python -m unittest discover -s tests -v
@@ -0,0 +1,2 @@
1
+
2
+ from .wrapper import ScribeAPIWrapper
@@ -0,0 +1,127 @@
1
+
2
+ import argparse
3
+ import os
4
+ import sys
5
+ import json
6
+
7
+ from .internal_globals import ENV_API_NAME
8
+ from .wrapper import ScribeAPIWrapper, DownloadError
9
+ from .request_utils import InternalRequestError, InvalidFileError
10
+
11
+
12
+ def _check_api_key_presence(args) -> str:
13
+ api_key = args.api_key or os.getenv(ENV_API_NAME)
14
+ if not api_key:
15
+ print(
16
+ "Error: API key required. Provide --api-key " \
17
+ "or set DADASCRIBE_API_KEY env var.",
18
+ file=sys.stderr,
19
+ )
20
+ sys.exit(2)
21
+
22
+ return api_key
23
+
24
+
25
+ def parse_args() -> argparse.Namespace:
26
+ p = argparse.ArgumentParser(
27
+ description="Interact with the DaDaScribe API through the CLI."
28
+ )
29
+ p.add_argument(
30
+ "--api-key",
31
+ help="Dadascribe API key (or set DADASCRIBE_API_KEY env var).",
32
+ )
33
+ p.add_argument(
34
+ "--source",
35
+ default="https://www.youtube.com/watch?v=VIDEO_ID",
36
+ help="Source media URL (e.g. YouTube link) OR file path.",
37
+ )
38
+ p.add_argument(
39
+ "--source-language", default="en", help="Source language (e.g. 'en')."
40
+ )
41
+ p.add_argument(
42
+ "--destination-language",
43
+ default="",
44
+ help="Comma-separated destination languages (e.g. 'it,fr').",
45
+ )
46
+ p.add_argument(
47
+ "--diarization",
48
+ default="",
49
+ help="Comma-separated diarization labels (e.g. 'Alice,Bob').",
50
+ )
51
+ p.add_argument(
52
+ "--timeout", type=int, default=60, help="Request timeout in seconds."
53
+ )
54
+ p.add_argument(
55
+ "--dump-response",
56
+ help="File to save JSON response (optional). "
57
+ "If omitted, prints to stdout.",
58
+ )
59
+
60
+ p.add_argument(
61
+ "--status-id",
62
+ help="Check status for a previously submitted job ID (e.g. " \
63
+ "a1B2c3D4e5F6g7H8). If provided, transcribe will not run.",
64
+ )
65
+
66
+ # For downloading transcription files:
67
+
68
+ p.add_argument(
69
+ "--download",
70
+ help="Download generated files given a job ID " \
71
+ "& an optional output dir path" \
72
+ )
73
+
74
+ p.add_argument(
75
+ "--download-output-dir",
76
+ default=None,
77
+ help="Directory to save downloaded files (optional). " \
78
+ "If omitted, saves to current directory."
79
+ )
80
+
81
+ return p.parse_args()
82
+
83
+
84
+ def _handle_args(args: argparse.Namespace, api_key: str) -> None:
85
+ wrapper = ScribeAPIWrapper(api_key=api_key, req_timeout=args.timeout)
86
+ try:
87
+ if args.status_id:
88
+ # If a status ID is provided, perform a status
89
+ # check and ignore other transcribe options.
90
+ result = wrapper.retrieve_status(id=args.status_id)
91
+ elif args.download:
92
+ result = wrapper.download_transcription_output(
93
+ id=args.download,
94
+ output_dir=args.download_output_dir or os.getcwd(),
95
+ )
96
+ else:
97
+ result = wrapper.transcribe(
98
+ source=args.source,
99
+ source_language=args.source_language,
100
+ destination_language=args.destination_language,
101
+ diarization=args.diarization,
102
+ )
103
+ except (InternalRequestError, InvalidFileError, DownloadError) as e:
104
+ if isinstance(e, DownloadError) or isinstance(e, InvalidFileError):
105
+ print(f"Error: {e.message}", file=sys.stderr)
106
+ elif isinstance(e, InternalRequestError):
107
+ print(f"Error during request: {e.resp_body}", file=sys.stderr)
108
+
109
+ sys.exit(1)
110
+
111
+ if args.dump_response:
112
+ with open(args.dump_response, "w", encoding="utf-8") as f:
113
+ json.dump(result, f, ensure_ascii=False, indent=2)
114
+ print(f"Saved response to {args.dump_response}")
115
+ else:
116
+ # Pretty-print JSON or text
117
+ if isinstance(result, (dict, list)):
118
+ print(json.dumps(result, ensure_ascii=False, indent=2))
119
+ elif result is not None:
120
+ print(result)
121
+
122
+
123
+
124
+ def main() -> None:
125
+ parsed_args = parse_args()
126
+ api_key = _check_api_key_presence(parsed_args)
127
+ _handle_args(parsed_args, api_key)
@@ -0,0 +1,4 @@
1
+
2
+ # Stores important global constants used across the package.
3
+
4
+ ENV_API_NAME: str = "DADASCRIBE_API_KEY"
@@ -0,0 +1,173 @@
1
+ # Stores global constants & utils related to requests.
2
+ from enum import StrEnum
3
+ from io import BufferedReader
4
+ from typing import Optional, Any
5
+ from urllib.parse import urlparse
6
+ from pathlib import Path
7
+ from os import PathLike
8
+
9
+ import requests
10
+ import json
11
+
12
+ BASE_API_URL: str = "https://api.dadascribe.com/v1/"
13
+
14
+
15
+ class EndPoints(StrEnum):
16
+ """Common endpoint names for the API."""
17
+
18
+ STATUS = "status"
19
+ TRANSCRIBE = "transcribe"
20
+
21
+
22
+ class PayLoadKeys(StrEnum):
23
+ """Common payload keys for the API."""
24
+
25
+ ID = "id"
26
+ SOURCE = "source"
27
+ SOURCE_LANGUAGE = "source-language"
28
+ DEST_LANGUAGE = "destination-language"
29
+ DIARIZATION = "diarization"
30
+
31
+
32
+ class ResponseKeys(StrEnum):
33
+ """Common response keys for the API."""
34
+
35
+ STATUS = "status"
36
+ URLS = "urls"
37
+
38
+
39
+ class Status(StrEnum):
40
+ """Common status values for the API."""
41
+
42
+ COMPLETE = "complete"
43
+ PROCESSING = "processing"
44
+ ERROR = "error"
45
+
46
+
47
+ def extract_response_content(resp: requests.Response) -> str:
48
+ """Helper to extract the response content as JSON or raw text."""
49
+ try:
50
+ return resp.json()
51
+ except ValueError:
52
+ return resp.text
53
+
54
+
55
+ class InternalRequestError(Exception):
56
+ """Raised when a request fails with a non-200 status code.
57
+ Contains the response object for debugging and other useful
58
+ information about the failure."""
59
+
60
+ def __init__(self, response: requests.Response):
61
+ self._response = response
62
+ self._message = (
63
+ f"Request failed: {response.status_code} {response.reason}"
64
+ )
65
+
66
+ super().__init__(self.message)
67
+
68
+ @property
69
+ def response(self) -> requests.Response:
70
+ return self._response
71
+
72
+ @property
73
+ def message(self) -> str:
74
+ return self._message
75
+
76
+ @property
77
+ def resp_body(self) -> str:
78
+ return extract_response_content(self._response)
79
+
80
+
81
+ class InvalidFileError(Exception):
82
+ """Raised when an inputted file is not valid."""
83
+
84
+ def __init__(self, message: str):
85
+ self._message = message
86
+ super().__init__(self._message)
87
+
88
+ @property
89
+ def message(self) -> str:
90
+ return self._message
91
+
92
+
93
+ class RequestUtils:
94
+ """Utility class for making HTTP requests,
95
+ handling common request logic."""
96
+
97
+ def exec_request(
98
+ self,
99
+ api_key: str,
100
+ url: str,
101
+ payload: Optional[dict],
102
+ timeout: int = 0,
103
+ get: bool = False,
104
+ file: Optional[PathLike] | Optional[str] = None
105
+ ) -> str | None:
106
+ """Executes a POST/GET request to the given URL with the given headers
107
+ and payload, and returns the response as JSON or raw text."""
108
+ req_fn = requests.get if get else requests.post
109
+ if file is not None:
110
+ headers = self.construct_headers(api_key, include_content=False)
111
+ payload, file_obj = self.construct_file_payload(file, payload)
112
+ resp = req_fn(
113
+ url,
114
+ headers=headers,
115
+ files=payload, timeout=timeout
116
+ )
117
+ file_obj.close()
118
+ else:
119
+ headers = self.construct_headers(api_key)
120
+ resp = req_fn(
121
+ url,
122
+ headers=headers,
123
+ json=payload, timeout=timeout
124
+ )
125
+
126
+ try:
127
+ resp.raise_for_status()
128
+ except requests.HTTPError:
129
+ raise InternalRequestError(resp)
130
+
131
+ # Return JSON if possible, otherwise raw text
132
+ return extract_response_content(resp)
133
+
134
+
135
+ def construct_headers(
136
+ self,
137
+ api_key: str,
138
+ include_content: bool = True
139
+ ) -> dict:
140
+ """Contructs a commonly used headers dict with the given API key."""
141
+ headers = {
142
+ "Authorization": f"Bearer {api_key}",
143
+ }
144
+ if include_content:
145
+ headers["Content-Type"] = "application/json"
146
+ return headers
147
+
148
+ def is_link(self, object: Any) -> bool:
149
+ """Returns whether a given string is a link or not."""
150
+ try:
151
+ stringed = str(object)
152
+ result = urlparse(stringed)
153
+ return all([result.scheme, result.netloc])
154
+ except ValueError:
155
+ return False
156
+
157
+
158
+ def construct_file_payload(
159
+ self,
160
+ file: PathLike | str,
161
+ params: Optional[dict] = None
162
+ ) -> tuple[dict, BufferedReader]:
163
+ """Constructs a file payload dict from the given file path."""
164
+ file = Path(file)
165
+ if not file.is_file():
166
+ raise InvalidFileError(f"File not found or is not a file: {file}")
167
+
168
+ file_obj = open(file, "rb")
169
+ file_payload = {
170
+ "file": (file.name, file_obj),
171
+ "data": (None, json.dumps(params or {}), "application/json"),
172
+ }
173
+ return file_payload, file_obj
@@ -0,0 +1,116 @@
1
+ import os
2
+ from os import PathLike
3
+ from typing import Any, Optional
4
+
5
+ from .request_utils import (
6
+ BASE_API_URL,
7
+ EndPoints,
8
+ PayLoadKeys,
9
+ RequestUtils,
10
+ ResponseKeys,
11
+ Status,
12
+ )
13
+
14
+
15
+ class DownloadError(Exception):
16
+ """Raised when download fails."""
17
+
18
+ def __init__(self, message: str):
19
+ self.message = message
20
+ super().__init__(self.message)
21
+
22
+
23
+ class ScribeAPIWrapper:
24
+ def __init__(self, api_key: str, req_timeout: int = 60):
25
+ self._request_utils = RequestUtils()
26
+ self._api_key = api_key
27
+ self._req_timeout = req_timeout
28
+
29
+ def transcribe(
30
+ self,
31
+ source: str | list[str] | PathLike,
32
+ source_language: str,
33
+ destination_language: str,
34
+ diarization: Optional[str] = None,
35
+ ) -> Any:
36
+ """Send a POST request to the Dadascribe /v1/transcribe
37
+ endpoint and return the parsed response.
38
+
39
+ Raises InvalidRequestError on failing requests.
40
+ """
41
+ url = BASE_API_URL + EndPoints.TRANSCRIBE
42
+ payload: dict[str, Any] = {
43
+ PayLoadKeys.SOURCE_LANGUAGE: source_language,
44
+ PayLoadKeys.DEST_LANGUAGE: destination_language,
45
+ }
46
+ if diarization is not None:
47
+ payload[PayLoadKeys.DIARIZATION] = diarization
48
+ file_source = None
49
+ if isinstance(source, list) or self._request_utils.is_link(source):
50
+ print("Source detected as LINK or LIST of LINKS.")
51
+ payload[PayLoadKeys.SOURCE] = source
52
+ else:
53
+ print("Source detected as FILE.")
54
+ # Construct files data:
55
+ file_source = source
56
+
57
+ return self._api_request(url, payload=payload, file=file_source)
58
+
59
+ def retrieve_status(self, id: str) -> Any:
60
+ """Send a POST request to the Dadascribe /v1/status endpoint
61
+ and return the parsed response.
62
+
63
+ Raises InvalidRequestError on failing requests.
64
+ """
65
+ url = BASE_API_URL + EndPoints.STATUS
66
+ payload = {PayLoadKeys.ID: id}
67
+
68
+ return self._api_request(url, payload=payload)
69
+
70
+ def download_transcription_output(
71
+ self,
72
+ id: str,
73
+ output_dir: str | PathLike,
74
+ ) -> None:
75
+ """Download the transcription output for the given job ID
76
+ to the specified directory. If no directory is specified,
77
+ saves to the current directory.
78
+ """
79
+ if not os.path.exists(output_dir):
80
+ raise DownloadError(
81
+ f'Output directory "{output_dir}" does not exist.'
82
+ )
83
+
84
+ status_info = self.retrieve_status(id)
85
+ if status_info[ResponseKeys.STATUS] != Status.COMPLETE:
86
+ raise DownloadError(
87
+ "Job is not completed yet. Cannot download output."
88
+ )
89
+
90
+ for file_url in status_info.get(ResponseKeys.URLS, []):
91
+ file_name = os.path.basename(file_url)
92
+ file_content = self._api_request(file_url, get=True)
93
+ with open(os.path.join(output_dir, file_name), "w") as f:
94
+ f.write(file_content)
95
+
96
+ def _api_request(
97
+ self,
98
+ url: str,
99
+ payload: Optional[dict] = None,
100
+ get: bool = False,
101
+ file: Optional[Any] = None,
102
+ ) -> Any:
103
+ """Send a POST request to the given URL with the given payload
104
+ and return the parsed response. Higher level version of exec_request
105
+ found in RequestUtils.
106
+
107
+ Raises InvalidRequestError on failing requests.
108
+ """
109
+ return self._request_utils.exec_request(
110
+ self._api_key,
111
+ url,
112
+ payload=payload,
113
+ timeout=self._req_timeout,
114
+ get=get,
115
+ file=file,
116
+ )
@@ -0,0 +1,14 @@
1
+
2
+ import unittest
3
+ from dadascribe.internal_globals import ENV_API_NAME
4
+
5
+ # Mostly just checking that when we modify these, we
6
+ # really want to modify them.
7
+
8
+ class TestInternalGlobals(unittest.TestCase):
9
+ def test_env_api_name(self):
10
+ self.assertEqual(ENV_API_NAME, "DADASCRIBE_API_KEY")
11
+
12
+
13
+ if __name__ == "__main__":
14
+ unittest.main()
@@ -0,0 +1,175 @@
1
+ import unittest
2
+ from unittest.mock import patch, mock_open
3
+
4
+ import requests
5
+ from src.dadascribe.request_utils import (
6
+ InternalRequestError,
7
+ InvalidFileError,
8
+ RequestUtils,
9
+ extract_response_content,
10
+ )
11
+
12
+
13
+ class TestRequestUtils(unittest.TestCase):
14
+ def setUp(self):
15
+ self._request_utils = RequestUtils()
16
+
17
+ def test_extract_response_content(self):
18
+ resp = requests.Response()
19
+ resp._content = b'{"key": "value"}'
20
+ result = extract_response_content(resp)
21
+ self.assertIsInstance(result, dict)
22
+ self.assertEqual(result, {"key": "value"})
23
+
24
+ # Now with a list:
25
+ resp._content = b'["response", "content", "as", "list"]'
26
+ result = extract_response_content(resp)
27
+ self.assertIsInstance(result, list)
28
+ self.assertEqual(result, ["response", "content", "as", "list"])
29
+
30
+ # Now with a string:
31
+ resp._content = b"response as a string"
32
+ result = extract_response_content(resp)
33
+ self.assertIsInstance(result, str)
34
+ self.assertEqual(result, "response as a string")
35
+
36
+ def test_construct_headers(self):
37
+ """Makes sure that the headers we construct
38
+ are of the expected form. This is more of a confirmation
39
+ that we want to change this header format in the code."""
40
+ headers = self._request_utils.construct_headers("dummy_api_key")
41
+ self.assertIsInstance(headers, dict)
42
+ self.assertEqual(
43
+ headers,
44
+ {
45
+ "Authorization": "Bearer dummy_api_key",
46
+ "Content-Type": "application/json",
47
+ },
48
+ )
49
+
50
+ @patch("requests.post", return_value=requests.Response())
51
+ def test_exec_request_calls_post_correctly(self, mock_post):
52
+ resp = mock_post.return_value
53
+ resp._content = b'{"dummy": "response"}'
54
+ resp.status_code = 200
55
+ self._request_utils.exec_request(
56
+ "dummy_api_key", "https://dummy.url", {}
57
+ )
58
+ mock_post.assert_called_once()
59
+ mock_post.assert_called_with(
60
+ "https://dummy.url",
61
+ headers={
62
+ "Authorization": "Bearer dummy_api_key",
63
+ "Content-Type": "application/json",
64
+ },
65
+ json={},
66
+ timeout=0,
67
+ )
68
+
69
+ @patch("requests.post", return_value=requests.Response())
70
+ def test_exec_request_raises_internal_request_error_on_status_raised(
71
+ self, mock_post
72
+ ):
73
+ resp = mock_post.return_value
74
+ resp.status_code = 500
75
+ resp.reason = "Bad Request"
76
+ with self.assertRaises(InternalRequestError) as context:
77
+ self._request_utils.exec_request(
78
+ "dummy_api_key", "https://dummy.url", {}
79
+ )
80
+ self.assertEqual(
81
+ str(context.exception), "Request failed: 500 Bad Request"
82
+ )
83
+
84
+ @patch("requests.post", return_value=requests.Response())
85
+ def test_exec_request_returns_formatted_extraction_content_output(
86
+ self, mock_post
87
+ ):
88
+ resp = mock_post.return_value
89
+ resp._content = b'["response", "content", "as", "list"]'
90
+ resp.status_code = 200
91
+ result = self._request_utils.exec_request(
92
+ "dummy_api_key", "https://dummy.url", {}
93
+ )
94
+ self.assertIsInstance(result, list)
95
+ self.assertEqual(result, ["response", "content", "as", "list"])
96
+
97
+ def test_is_link(self):
98
+ """Test is_link correctly identifies URLs and non-URLs."""
99
+ # Valid URLs
100
+ self.assertTrue(self._request_utils.is_link("https://example.com"))
101
+ self.assertTrue(self._request_utils.is_link("http://example.com"))
102
+ self.assertTrue(
103
+ self._request_utils.is_link("https://example.com/path/to/file.mp3")
104
+ )
105
+ self.assertTrue(self._request_utils.is_link("ftp://example.com"))
106
+
107
+ # Not valid URLs (no scheme or no netloc)
108
+ self.assertFalse(self._request_utils.is_link("example.com"))
109
+ self.assertFalse(self._request_utils.is_link("/path/to/file.mp3"))
110
+ self.assertFalse(self._request_utils.is_link("file.mp3"))
111
+ self.assertFalse(self._request_utils.is_link("just_a_string"))
112
+ self.assertFalse(self._request_utils.is_link(""))
113
+
114
+ @patch(
115
+ "builtins.open",
116
+ new_callable=mock_open,
117
+ read_data=b"file content",
118
+ )
119
+ @patch("pathlib.Path.is_file", return_value=True)
120
+ @patch("pathlib.Path.exists", return_value=True)
121
+ @patch("requests.post", return_value=requests.Response())
122
+ def test_exec_request_with_file(
123
+ self, mock_post, mock_exists, mock_is_file, mock_open_file
124
+ ):
125
+ """Test that exec_request correctly handles file uploads."""
126
+ resp = mock_post.return_value
127
+ resp._content = b'{"id": "test_id"}'
128
+ resp.status_code = 200
129
+
130
+ result = self._request_utils.exec_request(
131
+ "dummy_api_key",
132
+ "https://dummy.url",
133
+ {"key": "value"},
134
+ file="/path/to/test.mp3",
135
+ )
136
+
137
+ mock_post.assert_called_once()
138
+ call_args = mock_post.call_args
139
+
140
+ # Verify headers don't include Content-Type (multipart/form-data is automatic)
141
+ self.assertEqual(
142
+ call_args.kwargs["headers"],
143
+ {"Authorization": "Bearer dummy_api_key"},
144
+ )
145
+
146
+ # Verify files parameter is present
147
+ self.assertIn("files", call_args.kwargs)
148
+ self.assertEqual(result, {"id": "test_id"})
149
+
150
+ @patch("pathlib.Path.is_file", return_value=False)
151
+ def test_construct_file_payload_raises_on_non_file(
152
+ self, mock_is_file
153
+ ):
154
+ """Test that construct_file_payload raises InvalidFileError for non-files."""
155
+ with self.assertRaises(InvalidFileError) as context:
156
+ self._request_utils.construct_file_payload("/path/to/directory")
157
+ self.assertIn(
158
+ "File not found or is not a file", str(context.exception)
159
+ )
160
+
161
+ def test_construct_headers_with_include_content_false(self):
162
+ """Test construct_headers without Content-Type for file uploads."""
163
+ headers = self._request_utils.construct_headers(
164
+ "dummy_api_key", include_content=False
165
+ )
166
+ self.assertIsInstance(headers, dict)
167
+ self.assertEqual(
168
+ headers,
169
+ {"Authorization": "Bearer dummy_api_key"},
170
+ )
171
+ self.assertNotIn("Content-Type", headers)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ unittest.main()
@@ -0,0 +1,203 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, mock_open, patch
3
+
4
+ from src.dadascribe.request_utils import (
5
+ BASE_API_URL,
6
+ EndPoints,
7
+ PayLoadKeys,
8
+ ResponseKeys,
9
+ Status,
10
+ )
11
+ from src.dadascribe.wrapper import DownloadError, ScribeAPIWrapper
12
+
13
+
14
+ class TestWrapper(unittest.TestCase):
15
+ def setUp(self):
16
+ self._wrapper = ScribeAPIWrapper(api_key="")
17
+
18
+ @patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
19
+ def test_transcribe_calls_api_request_correctly(self, mock_api_request):
20
+ """Checks that the transcribe function calls _api_request
21
+ with the correct arguments, such as the payload."""
22
+ self._wrapper.transcribe(
23
+ source="https://example_source_url.com",
24
+ source_language="en",
25
+ destination_language="it",
26
+ )
27
+ mock_api_request.assert_called_once()
28
+ mock_api_request.assert_called_with(
29
+ BASE_API_URL + EndPoints.TRANSCRIBE,
30
+ payload={
31
+ PayLoadKeys.SOURCE_LANGUAGE: "en",
32
+ PayLoadKeys.DEST_LANGUAGE: "it",
33
+ PayLoadKeys.SOURCE: "https://example_source_url.com",
34
+ },
35
+ file=None,
36
+ )
37
+
38
+ mock_api_request.reset_mock()
39
+ # Make sure it works out with diarization as well:
40
+ # Note: "example_source_url" without a protocol is treated as a file
41
+ self._wrapper.transcribe(
42
+ source="example_source_url",
43
+ source_language="en",
44
+ destination_language="it",
45
+ diarization="speaker1,speaker2",
46
+ )
47
+ mock_api_request.assert_called_once()
48
+ mock_api_request.assert_called_with(
49
+ BASE_API_URL + EndPoints.TRANSCRIBE,
50
+ payload={
51
+ PayLoadKeys.SOURCE_LANGUAGE: "en",
52
+ PayLoadKeys.DEST_LANGUAGE: "it",
53
+ PayLoadKeys.DIARIZATION: "speaker1,speaker2",
54
+ },
55
+ file="example_source_url",
56
+ )
57
+
58
+ # As a list for the source:
59
+ mock_api_request.reset_mock()
60
+ self._wrapper.transcribe(
61
+ source=["example_source_url", "example_source_url2"],
62
+ source_language="en",
63
+ destination_language="it",
64
+ diarization="speaker1,speaker2",
65
+ )
66
+ mock_api_request.assert_called_once()
67
+ mock_api_request.assert_called_with(
68
+ BASE_API_URL + EndPoints.TRANSCRIBE,
69
+ payload={
70
+ PayLoadKeys.SOURCE: [
71
+ "example_source_url",
72
+ "example_source_url2",
73
+ ],
74
+ PayLoadKeys.SOURCE_LANGUAGE: "en",
75
+ PayLoadKeys.DEST_LANGUAGE: "it",
76
+ PayLoadKeys.DIARIZATION: "speaker1,speaker2",
77
+ },
78
+ file=None,
79
+ )
80
+
81
+ @patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
82
+ def test_transcribe_with_file_path(self, mock_api_request):
83
+ """Test that transcribe correctly handles file paths."""
84
+ # Test with a file path (no protocol scheme)
85
+ self._wrapper.transcribe(
86
+ source="/path/to/audio.mp3",
87
+ source_language="en",
88
+ destination_language="fr",
89
+ )
90
+ mock_api_request.assert_called_once()
91
+ mock_api_request.assert_called_with(
92
+ BASE_API_URL + EndPoints.TRANSCRIBE,
93
+ payload={
94
+ PayLoadKeys.SOURCE_LANGUAGE: "en",
95
+ PayLoadKeys.DEST_LANGUAGE: "fr",
96
+ },
97
+ file="/path/to/audio.mp3",
98
+ )
99
+
100
+ @patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
101
+ def test_transcribe_with_http_url(self, mock_api_request):
102
+ """Test that transcribe correctly handles HTTP URLs."""
103
+ self._wrapper.transcribe(
104
+ source="http://example.com/audio.mp3",
105
+ source_language="en",
106
+ destination_language="es",
107
+ )
108
+ mock_api_request.assert_called_once()
109
+ mock_api_request.assert_called_with(
110
+ BASE_API_URL + EndPoints.TRANSCRIBE,
111
+ payload={
112
+ PayLoadKeys.SOURCE_LANGUAGE: "en",
113
+ PayLoadKeys.DEST_LANGUAGE: "es",
114
+ PayLoadKeys.SOURCE: "http://example.com/audio.mp3",
115
+ },
116
+ file=None,
117
+ )
118
+
119
+ @patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
120
+ def test_transcribe_with_relative_file_path(self, mock_api_request):
121
+ """Test that transcribe correctly handles relative file paths."""
122
+ self._wrapper.transcribe(
123
+ source="audio_files/recording.wav",
124
+ source_language="de",
125
+ destination_language="en",
126
+ )
127
+ mock_api_request.assert_called_once()
128
+ mock_api_request.assert_called_with(
129
+ BASE_API_URL + EndPoints.TRANSCRIBE,
130
+ payload={
131
+ PayLoadKeys.SOURCE_LANGUAGE: "de",
132
+ PayLoadKeys.DEST_LANGUAGE: "en",
133
+ },
134
+ file="audio_files/recording.wav",
135
+ )
136
+
137
+ @patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
138
+ def test_transcribe_with_list_of_urls(self, mock_api_request):
139
+ """Test that transcribe handles a list of URLs correctly."""
140
+ self._wrapper.transcribe(
141
+ source=[
142
+ "https://example.com/audio1.mp3",
143
+ "https://example.com/audio2.mp3",
144
+ ],
145
+ source_language="en",
146
+ destination_language="es",
147
+ )
148
+ mock_api_request.assert_called_once()
149
+ mock_api_request.assert_called_with(
150
+ BASE_API_URL + EndPoints.TRANSCRIBE,
151
+ payload={
152
+ PayLoadKeys.SOURCE: [
153
+ "https://example.com/audio1.mp3",
154
+ "https://example.com/audio2.mp3",
155
+ ],
156
+ PayLoadKeys.SOURCE_LANGUAGE: "en",
157
+ PayLoadKeys.DEST_LANGUAGE: "es",
158
+ },
159
+ file=None,
160
+ )
161
+
162
+ @patch("src.dadascribe.wrapper.ScribeAPIWrapper._api_request")
163
+ def test_retrieve_status_calls_api_request_correctly(
164
+ self, mock_api_request
165
+ ):
166
+ """Checks that the retrieve_transcription function calls _api_request
167
+ with the correct arguments, such as the payload."""
168
+ self._wrapper.retrieve_status(id="example_id")
169
+ mock_api_request.assert_called_once()
170
+ mock_api_request.assert_called_with(
171
+ BASE_API_URL + EndPoints.STATUS,
172
+ payload={
173
+ PayLoadKeys.ID: "example_id",
174
+ },
175
+ )
176
+
177
+ @patch("os.path.exists")
178
+ def test_download_trsc_raises_err_when_output_dir_dne(self, mock_exists):
179
+ """When the output dir does not exist, should raise DownloadError."""
180
+ mock_exists.return_value = False
181
+ with self.assertRaises(DownloadError):
182
+ self._wrapper.download_transcription_output(
183
+ id="example_id", output_dir="example_path"
184
+ )
185
+
186
+ @patch("os.path.exists")
187
+ @patch("src.dadascribe.wrapper.ScribeAPIWrapper.retrieve_status")
188
+ def test_download_trsc_raises_err_when_status_not_complete(
189
+ self, mock_retrieve_status, mock_exists
190
+ ):
191
+ """When the output dir does not exist, should raise DownloadError."""
192
+ mock_exists.return_value = True
193
+ mock_retrieve_status.return_value = {
194
+ ResponseKeys.STATUS: Status.PROCESSING
195
+ }
196
+ with self.assertRaises(DownloadError):
197
+ self._wrapper.download_transcription_output(
198
+ id="example_id", output_dir="example_path"
199
+ )
200
+
201
+
202
+ if __name__ == "__main__":
203
+ unittest.main()