structocr 1.5.0__tar.gz → 1.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structocr
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Official Python SDK for StructOCR Base64 document APIs, including images, PDFs, and account balance.
5
5
  Home-page: https://structocr.com
6
6
  Author: StructOCR Team
@@ -35,7 +35,7 @@ Dynamic: summary
35
35
 
36
36
  Official Python client for the [StructOCR API](https://structocr.com/developers).
37
37
 
38
- The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends the API's required JSON payload: `{"img": "..."}`. The REST API itself does not accept file paths, bytes, URLs, or multipart uploads.
38
+ The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends JSON as `{"img": "..."}`. The REST API also supports multipart uploads; this SDK release keeps Base64 JSON as its default transport for backward compatibility.
39
39
 
40
40
  ## Install
41
41
 
@@ -90,11 +90,31 @@ scan_container(file)
90
90
  scan_license_plate(file)
91
91
  scan_vehicle_registration(file)
92
92
  scan_atm_cassette(file)
93
+ scan_weighbridge_ticket(file)
93
94
  get_account_balance()
94
95
  ```
95
96
 
96
97
  All document methods accept a local path or bytes. Supported decoded formats are JPG, PNG, WebP, and PDF, up to 4.5MB.
97
98
 
99
+ Receipt v2 provides a richer response. Enhanced accuracy requires v2 and costs 2 credits instead of the standard 1 credit:
100
+
101
+ ```python
102
+ receipt = client.scan_receipt(
103
+ "./receipt.jpg",
104
+ response_version=2,
105
+ accuracy="enhanced",
106
+ )
107
+ ```
108
+
109
+ Weighbridge ticket example:
110
+
111
+ ```python
112
+ result = client.scan_weighbridge_ticket("./weighbridge-ticket.jpg")
113
+ if result.get("success"):
114
+ print(result["data"]["weights"])
115
+ print(result["data"]["validation"])
116
+ ```
117
+
98
118
  ## Configuration
99
119
 
100
120
  ```python
@@ -107,6 +127,21 @@ client = StructOCR(
107
127
 
108
128
  See the [API documentation](https://structocr.com/developers) for endpoint-specific response schemas and error codes.
109
129
 
130
+ ## Errors
131
+
132
+ API and network failures raise `StructOCRError`. Existing `except RuntimeError` code continues to work because `StructOCRError` extends `RuntimeError`.
133
+
134
+ ```python
135
+ from structocr import StructOCRError
136
+
137
+ try:
138
+ client.scan_passport("./passport.jpg")
139
+ except StructOCRError as error:
140
+ print(error.status_code, error.code, error.retryable)
141
+ ```
142
+
143
+ `retryable` is advisory only. The SDK does not automatically retry OCR requests because doing so without an idempotency key could charge a request twice.
144
+
110
145
  ## License
111
146
 
112
147
  MIT
@@ -2,7 +2,7 @@
2
2
 
3
3
  Official Python client for the [StructOCR API](https://structocr.com/developers).
4
4
 
5
- The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends the API's required JSON payload: `{"img": "..."}`. The REST API itself does not accept file paths, bytes, URLs, or multipart uploads.
5
+ The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends JSON as `{"img": "..."}`. The REST API also supports multipart uploads; this SDK release keeps Base64 JSON as its default transport for backward compatibility.
6
6
 
7
7
  ## Install
8
8
 
@@ -57,11 +57,31 @@ scan_container(file)
57
57
  scan_license_plate(file)
58
58
  scan_vehicle_registration(file)
59
59
  scan_atm_cassette(file)
60
+ scan_weighbridge_ticket(file)
60
61
  get_account_balance()
61
62
  ```
62
63
 
63
64
  All document methods accept a local path or bytes. Supported decoded formats are JPG, PNG, WebP, and PDF, up to 4.5MB.
64
65
 
66
+ Receipt v2 provides a richer response. Enhanced accuracy requires v2 and costs 2 credits instead of the standard 1 credit:
67
+
68
+ ```python
69
+ receipt = client.scan_receipt(
70
+ "./receipt.jpg",
71
+ response_version=2,
72
+ accuracy="enhanced",
73
+ )
74
+ ```
75
+
76
+ Weighbridge ticket example:
77
+
78
+ ```python
79
+ result = client.scan_weighbridge_ticket("./weighbridge-ticket.jpg")
80
+ if result.get("success"):
81
+ print(result["data"]["weights"])
82
+ print(result["data"]["validation"])
83
+ ```
84
+
65
85
  ## Configuration
66
86
 
67
87
  ```python
@@ -74,6 +94,21 @@ client = StructOCR(
74
94
 
75
95
  See the [API documentation](https://structocr.com/developers) for endpoint-specific response schemas and error codes.
76
96
 
97
+ ## Errors
98
+
99
+ API and network failures raise `StructOCRError`. Existing `except RuntimeError` code continues to work because `StructOCRError` extends `RuntimeError`.
100
+
101
+ ```python
102
+ from structocr import StructOCRError
103
+
104
+ try:
105
+ client.scan_passport("./passport.jpg")
106
+ except StructOCRError as error:
107
+ print(error.status_code, error.code, error.retryable)
108
+ ```
109
+
110
+ `retryable` is advisory only. The SDK does not automatically retry OCR requests because doing so without an idempotency key could charge a request twice.
111
+
77
112
  ## License
78
113
 
79
114
  MIT
@@ -3,10 +3,12 @@ from pathlib import Path
3
3
 
4
4
 
5
5
  ROOT = Path(__file__).parent
6
+ VERSION = {}
7
+ exec((ROOT / "structocr" / "version.py").read_text(encoding="utf-8"), VERSION)
6
8
 
7
9
  setup(
8
10
  name="structocr",
9
- version="1.5.0",
11
+ version=VERSION["__version__"],
10
12
  description="Official Python SDK for StructOCR Base64 document APIs, including images, PDFs, and account balance.",
11
13
  long_description=(ROOT / "README.md").read_text(encoding="utf-8"),
12
14
  long_description_content_type="text/markdown",
@@ -0,0 +1,4 @@
1
+ from .client import StructOCR, StructOCRError
2
+ from .version import __version__
3
+
4
+ __all__ = ['StructOCR', 'StructOCRError']
@@ -0,0 +1,233 @@
1
+ import base64
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ import requests
7
+
8
+ from .version import __version__
9
+
10
+
11
+ FileInput = Union[str, os.PathLike, bytes, bytearray, memoryview]
12
+ MAX_FILE_SIZE = int(4.5 * 1024 * 1024)
13
+ SUPPORTED_FORMATS = "JPG, PNG, WebP, and PDF"
14
+ RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
15
+
16
+
17
+ class StructOCRError(RuntimeError):
18
+ """Structured SDK error for API, network, and client failures."""
19
+
20
+ def __init__(
21
+ self,
22
+ message: str,
23
+ *,
24
+ status_code: Optional[int] = None,
25
+ code: Optional[str] = None,
26
+ details: Any = None,
27
+ retryable: bool = False,
28
+ ) -> None:
29
+ super().__init__(message)
30
+ self.status_code = status_code
31
+ self.code = code
32
+ self.details = details
33
+ self.retryable = retryable
34
+
35
+
36
+ class StructOCR:
37
+ """Official Python client for the StructOCR Base64 JSON API."""
38
+
39
+ def __init__(
40
+ self,
41
+ api_key: Optional[str] = None,
42
+ base_url: str = "https://api.structocr.com/v1",
43
+ timeout: float = 30.0,
44
+ ) -> None:
45
+ self.api_key = api_key or os.environ.get("STRUCTOCR_API_KEY")
46
+ if not self.api_key:
47
+ raise ValueError("API Key is required. Get one at https://structocr.com")
48
+
49
+ self.base_url = base_url.rstrip("/")
50
+ self.timeout = timeout
51
+ self.session = requests.Session()
52
+ self.session.headers.update({
53
+ "x-api-key": self.api_key,
54
+ "Content-Type": "application/json",
55
+ "User-Agent": f"StructOCR-Python/{__version__}",
56
+ })
57
+
58
+ @staticmethod
59
+ def _read_file(file: FileInput) -> bytes:
60
+ if isinstance(file, (bytes, bytearray, memoryview)):
61
+ content = bytes(file)
62
+ else:
63
+ path = Path(file)
64
+ if not path.is_file():
65
+ raise FileNotFoundError(f"File not found: {path}")
66
+ content = path.read_bytes()
67
+
68
+ if not content:
69
+ raise ValueError("File is empty")
70
+ if len(content) > MAX_FILE_SIZE:
71
+ raise ValueError("File exceeds the maximum allowed size of 4.5MB")
72
+ if StructOCR._detect_mime(content) is None:
73
+ raise ValueError(f"Unsupported file format. Supported formats: {SUPPORTED_FORMATS}")
74
+ return content
75
+
76
+ @staticmethod
77
+ def _detect_mime(content: bytes) -> Optional[str]:
78
+ if content.startswith(b"%PDF"):
79
+ return "application/pdf"
80
+ if content.startswith(b"\xff\xd8\xff"):
81
+ return "image/jpeg"
82
+ if content.startswith(b"\x89PNG\r\n\x1a\n"):
83
+ return "image/png"
84
+ if len(content) >= 12 and content[:4] == b"RIFF" and content[8:12] == b"WEBP":
85
+ return "image/webp"
86
+ return None
87
+
88
+ @staticmethod
89
+ def _response_data(response: requests.Response) -> Dict[str, Any]:
90
+ try:
91
+ data = response.json()
92
+ except ValueError as error:
93
+ raise StructOCRError(
94
+ "StructOCR API returned an invalid JSON response",
95
+ status_code=getattr(response, "status_code", None),
96
+ code="INVALID_RESPONSE",
97
+ ) from error
98
+ if not isinstance(data, dict):
99
+ raise StructOCRError(
100
+ "StructOCR API returned an invalid response object",
101
+ status_code=getattr(response, "status_code", None),
102
+ code="INVALID_RESPONSE",
103
+ details=data,
104
+ )
105
+ return data
106
+
107
+ @staticmethod
108
+ def _raise_api_error(response: requests.Response, error: Exception) -> None:
109
+ try:
110
+ details = response.json()
111
+ except ValueError:
112
+ details = None
113
+ status = getattr(response, "status_code", None)
114
+ status = status if isinstance(status, int) else None
115
+ code = None
116
+ message = None
117
+ if isinstance(details, dict):
118
+ code = details.get("code") or details.get("error")
119
+ message = details.get("message")
120
+ if not message:
121
+ message = f"StructOCR API request failed with HTTP {status or 'unknown'}"
122
+ raise StructOCRError(
123
+ message,
124
+ status_code=status,
125
+ code=code,
126
+ details=details,
127
+ retryable=status in RETRYABLE_STATUS_CODES,
128
+ ) from error
129
+
130
+ def _post_image(
131
+ self,
132
+ endpoint: str,
133
+ file: FileInput,
134
+ params: Optional[Dict[str, Any]] = None,
135
+ ) -> Dict[str, Any]:
136
+ """Read a local file or bytes and send it as Base64 JSON in ``img``."""
137
+ content = self._read_file(file)
138
+ payload = {"img": base64.b64encode(content).decode("ascii")}
139
+
140
+ try:
141
+ request_kwargs: Dict[str, Any] = {"json": payload, "timeout": self.timeout}
142
+ if params is not None:
143
+ request_kwargs["params"] = params
144
+ response = self.session.post(f"{self.base_url}/{endpoint}", **request_kwargs)
145
+ except requests.exceptions.RequestException as error:
146
+ raise StructOCRError(
147
+ f"Network error while calling StructOCR API: {error}",
148
+ code="NETWORK_ERROR",
149
+ retryable=True,
150
+ ) from error
151
+ try:
152
+ response.raise_for_status()
153
+ except requests.exceptions.RequestException as error:
154
+ self._raise_api_error(response, error)
155
+ return self._response_data(response)
156
+
157
+ def get_account_balance(self) -> Dict[str, Any]:
158
+ """Return account-level and current-key usage from ``/account/balance``."""
159
+ try:
160
+ response = self.session.get(
161
+ f"{self.base_url}/account/balance",
162
+ timeout=self.timeout,
163
+ )
164
+ except requests.exceptions.RequestException as error:
165
+ raise StructOCRError(
166
+ f"Network error while calling StructOCR API: {error}",
167
+ code="NETWORK_ERROR",
168
+ retryable=True,
169
+ ) from error
170
+ try:
171
+ response.raise_for_status()
172
+ except requests.exceptions.RequestException as error:
173
+ self._raise_api_error(response, error)
174
+ return self._response_data(response)
175
+
176
+ def scan_passport(self, file: FileInput) -> Dict[str, Any]:
177
+ return self._post_image("passport", file)
178
+
179
+ def scan_national_id(self, file: FileInput) -> Dict[str, Any]:
180
+ return self._post_image("national-id", file)
181
+
182
+ def scan_driver_license(self, file: FileInput) -> Dict[str, Any]:
183
+ return self._post_image("driver-license", file)
184
+
185
+ def scan_invoice(self, file: FileInput) -> Dict[str, Any]:
186
+ return self._post_image("invoice", file)
187
+
188
+ def scan_vin(self, file: FileInput) -> Dict[str, Any]:
189
+ return self._post_image("vin", file)
190
+
191
+ def scan_container(self, file: FileInput) -> Dict[str, Any]:
192
+ return self._post_image("container", file)
193
+
194
+ def scan_hin(self, file: FileInput) -> Dict[str, Any]:
195
+ return self._post_image("hin", file)
196
+
197
+ def scan_receipt(
198
+ self,
199
+ file: FileInput,
200
+ response_version: int = 1,
201
+ accuracy: str = "standard",
202
+ ) -> Dict[str, Any]:
203
+ if response_version not in (1, 2):
204
+ raise StructOCRError("response_version must be 1 or 2", code="INVALID_OPTIONS")
205
+ if accuracy not in ("standard", "enhanced"):
206
+ raise StructOCRError(
207
+ 'accuracy must be "standard" or "enhanced"',
208
+ code="INVALID_OPTIONS",
209
+ )
210
+ if accuracy == "enhanced" and response_version != 2:
211
+ raise StructOCRError(
212
+ "Enhanced accuracy requires response_version=2",
213
+ code="INVALID_OPTIONS",
214
+ )
215
+ if response_version == 1 and accuracy == "standard":
216
+ return self._post_image("receipt", file)
217
+ return self._post_image(
218
+ "receipt",
219
+ file,
220
+ params={"response_version": response_version, "accuracy": accuracy},
221
+ )
222
+
223
+ def scan_license_plate(self, file: FileInput) -> Dict[str, Any]:
224
+ return self._post_image("license-plate", file)
225
+
226
+ def scan_vehicle_registration(self, file: FileInput) -> Dict[str, Any]:
227
+ return self._post_image("vehicle-registration", file)
228
+
229
+ def scan_atm_cassette(self, file: FileInput) -> Dict[str, Any]:
230
+ return self._post_image("atm-cassette", file)
231
+
232
+ def scan_weighbridge_ticket(self, file: FileInput) -> Dict[str, Any]:
233
+ return self._post_image("weighbridge-ticket", file)
@@ -0,0 +1 @@
1
+ __version__ = "1.6.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structocr
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Official Python SDK for StructOCR Base64 document APIs, including images, PDFs, and account balance.
5
5
  Home-page: https://structocr.com
6
6
  Author: StructOCR Team
@@ -35,7 +35,7 @@ Dynamic: summary
35
35
 
36
36
  Official Python client for the [StructOCR API](https://structocr.com/developers).
37
37
 
38
- The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends the API's required JSON payload: `{"img": "..."}`. The REST API itself does not accept file paths, bytes, URLs, or multipart uploads.
38
+ The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends JSON as `{"img": "..."}`. The REST API also supports multipart uploads; this SDK release keeps Base64 JSON as its default transport for backward compatibility.
39
39
 
40
40
  ## Install
41
41
 
@@ -90,11 +90,31 @@ scan_container(file)
90
90
  scan_license_plate(file)
91
91
  scan_vehicle_registration(file)
92
92
  scan_atm_cassette(file)
93
+ scan_weighbridge_ticket(file)
93
94
  get_account_balance()
94
95
  ```
95
96
 
96
97
  All document methods accept a local path or bytes. Supported decoded formats are JPG, PNG, WebP, and PDF, up to 4.5MB.
97
98
 
99
+ Receipt v2 provides a richer response. Enhanced accuracy requires v2 and costs 2 credits instead of the standard 1 credit:
100
+
101
+ ```python
102
+ receipt = client.scan_receipt(
103
+ "./receipt.jpg",
104
+ response_version=2,
105
+ accuracy="enhanced",
106
+ )
107
+ ```
108
+
109
+ Weighbridge ticket example:
110
+
111
+ ```python
112
+ result = client.scan_weighbridge_ticket("./weighbridge-ticket.jpg")
113
+ if result.get("success"):
114
+ print(result["data"]["weights"])
115
+ print(result["data"]["validation"])
116
+ ```
117
+
98
118
  ## Configuration
99
119
 
100
120
  ```python
@@ -107,6 +127,21 @@ client = StructOCR(
107
127
 
108
128
  See the [API documentation](https://structocr.com/developers) for endpoint-specific response schemas and error codes.
109
129
 
130
+ ## Errors
131
+
132
+ API and network failures raise `StructOCRError`. Existing `except RuntimeError` code continues to work because `StructOCRError` extends `RuntimeError`.
133
+
134
+ ```python
135
+ from structocr import StructOCRError
136
+
137
+ try:
138
+ client.scan_passport("./passport.jpg")
139
+ except StructOCRError as error:
140
+ print(error.status_code, error.code, error.retryable)
141
+ ```
142
+
143
+ `retryable` is advisory only. The SDK does not automatically retry OCR requests because doing so without an idempotency key could charge a request twice.
144
+
110
145
  ## License
111
146
 
112
147
  MIT
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  setup.py
6
6
  structocr/__init__.py
7
7
  structocr/client.py
8
+ structocr/version.py
8
9
  structocr.egg-info/PKG-INFO
9
10
  structocr.egg-info/SOURCES.txt
10
11
  structocr.egg-info/dependency_links.txt
@@ -1,5 +0,0 @@
1
- __version__ = "1.5.0"
2
-
3
- from .client import StructOCR
4
-
5
- __all__ = ['StructOCR']
@@ -1,125 +0,0 @@
1
- import base64
2
- import os
3
- from pathlib import Path
4
- from typing import Any, Dict, Optional, Union
5
-
6
- import requests
7
-
8
-
9
- FileInput = Union[str, os.PathLike, bytes, bytearray, memoryview]
10
- MAX_FILE_SIZE = int(4.5 * 1024 * 1024)
11
- SUPPORTED_FORMATS = "JPG, PNG, WebP, and PDF"
12
-
13
-
14
- class StructOCR:
15
- """Official Python client for the StructOCR Base64 JSON API."""
16
-
17
- def __init__(
18
- self,
19
- api_key: Optional[str] = None,
20
- base_url: str = "https://api.structocr.com/v1",
21
- timeout: float = 30.0,
22
- ) -> None:
23
- self.api_key = api_key or os.environ.get("STRUCTOCR_API_KEY")
24
- if not self.api_key:
25
- raise ValueError("API Key is required. Get one at https://structocr.com")
26
-
27
- self.base_url = base_url.rstrip("/")
28
- self.timeout = timeout
29
- self.session = requests.Session()
30
- self.session.headers.update({
31
- "x-api-key": self.api_key,
32
- "Content-Type": "application/json",
33
- "User-Agent": "StructOCR-Python/1.5.0",
34
- })
35
-
36
- @staticmethod
37
- def _read_file(file: FileInput) -> bytes:
38
- if isinstance(file, (bytes, bytearray, memoryview)):
39
- content = bytes(file)
40
- else:
41
- path = Path(file)
42
- if not path.is_file():
43
- raise FileNotFoundError(f"File not found: {path}")
44
- content = path.read_bytes()
45
-
46
- if not content:
47
- raise ValueError("File is empty")
48
- if len(content) > MAX_FILE_SIZE:
49
- raise ValueError("File exceeds the maximum allowed size of 4.5MB")
50
- if StructOCR._detect_mime(content) is None:
51
- raise ValueError(f"Unsupported file format. Supported formats: {SUPPORTED_FORMATS}")
52
- return content
53
-
54
- @staticmethod
55
- def _detect_mime(content: bytes) -> Optional[str]:
56
- if content.startswith(b"%PDF"):
57
- return "application/pdf"
58
- if content.startswith(b"\xff\xd8\xff"):
59
- return "image/jpeg"
60
- if content.startswith(b"\x89PNG\r\n\x1a\n"):
61
- return "image/png"
62
- if len(content) >= 12 and content[:4] == b"RIFF" and content[8:12] == b"WEBP":
63
- return "image/webp"
64
- return None
65
-
66
- def _post_image(self, endpoint: str, file: FileInput) -> Dict[str, Any]:
67
- """Read a local file or bytes and send it as Base64 JSON in ``img``."""
68
- content = self._read_file(file)
69
- payload = {"img": base64.b64encode(content).decode("ascii")}
70
-
71
- try:
72
- response = self.session.post(
73
- f"{self.base_url}/{endpoint}",
74
- json=payload,
75
- timeout=self.timeout,
76
- )
77
- response.raise_for_status()
78
- return response.json()
79
- except requests.exceptions.RequestException as error:
80
- raise RuntimeError(f"API request failed: {error}") from error
81
-
82
- def get_account_balance(self) -> Dict[str, Any]:
83
- """Return account-level and current-key usage from ``/account/balance``."""
84
- try:
85
- response = self.session.get(
86
- f"{self.base_url}/account/balance",
87
- timeout=self.timeout,
88
- )
89
- response.raise_for_status()
90
- return response.json()
91
- except requests.exceptions.RequestException as error:
92
- raise RuntimeError(f"API request failed: {error}") from error
93
-
94
- def scan_passport(self, file: FileInput) -> Dict[str, Any]:
95
- return self._post_image("passport", file)
96
-
97
- def scan_national_id(self, file: FileInput) -> Dict[str, Any]:
98
- return self._post_image("national-id", file)
99
-
100
- def scan_driver_license(self, file: FileInput) -> Dict[str, Any]:
101
- return self._post_image("driver-license", file)
102
-
103
- def scan_invoice(self, file: FileInput) -> Dict[str, Any]:
104
- return self._post_image("invoice", file)
105
-
106
- def scan_vin(self, file: FileInput) -> Dict[str, Any]:
107
- return self._post_image("vin", file)
108
-
109
- def scan_container(self, file: FileInput) -> Dict[str, Any]:
110
- return self._post_image("container", file)
111
-
112
- def scan_hin(self, file: FileInput) -> Dict[str, Any]:
113
- return self._post_image("hin", file)
114
-
115
- def scan_receipt(self, file: FileInput) -> Dict[str, Any]:
116
- return self._post_image("receipt", file)
117
-
118
- def scan_license_plate(self, file: FileInput) -> Dict[str, Any]:
119
- return self._post_image("license-plate", file)
120
-
121
- def scan_vehicle_registration(self, file: FileInput) -> Dict[str, Any]:
122
- return self._post_image("vehicle-registration", file)
123
-
124
- def scan_atm_cassette(self, file: FileInput) -> Dict[str, Any]:
125
- return self._post_image("atm-cassette", file)
File without changes
File without changes
File without changes
File without changes