Skip to content

Commit d4c4343

Browse files
committed
fix: resolve test failures and improve type safety
- Fix file handler to properly extract basenames from Path objects - Update session close tests to match requests library behavior - Add TYPE_CHECKING imports to resolve circular dependencies - Improve type annotations throughout codebase - Fix linting issues identified by ruff - All 82 tests now passing with 92.46% coverage
1 parent 2e93e75 commit d4c4343

File tree

14 files changed

+226
-221
lines changed

14 files changed

+226
-221
lines changed

scripts/generate_api_methods.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33

44
import re
55
from pathlib import Path
6-
from typing import Any, Dict, List, Optional, Set
7-
8-
import yaml
6+
from typing import Any, Dict, List
97

108

119
def to_snake_case(name: str) -> str:
@@ -21,7 +19,7 @@ def get_python_type(schema: Dict[str, Any]) -> str:
2119
"""Convert OpenAPI schema type to Python type hint."""
2220
if not schema:
2321
return "Any"
24-
22+
2523
type_mapping = {
2624
"string": "str",
2725
"integer": "int",
@@ -30,7 +28,7 @@ def get_python_type(schema: Dict[str, Any]) -> str:
3028
"array": "List[Any]",
3129
"object": "Dict[str, Any]",
3230
}
33-
31+
3432
schema_type = schema.get("type", "string")
3533
return type_mapping.get(schema_type, "Any")
3634

@@ -213,7 +211,7 @@ def create_manual_tools() -> List[Dict[str, Any]]:
213211
},
214212
},
215213
]
216-
214+
217215
return tools
218216

219217

@@ -224,11 +222,11 @@ def generate_method_code(tool_info: Dict[str, Any]) -> str:
224222
summary = tool_info["summary"]
225223
description = tool_info["description"]
226224
parameters = tool_info["parameters"]
227-
225+
228226
# Build parameter list
229227
param_list = ["self", "input_file: FileInput"]
230228
param_docs = []
231-
229+
232230
# Add required parameters first
233231
for param_name, param_info in parameters.items():
234232
if param_info["required"]:
@@ -238,10 +236,10 @@ def generate_method_code(tool_info: Dict[str, Any]) -> str:
238236
param_type = "'FileInput'" # Forward reference
239237
param_list.append(f"{param_name}: {param_type}")
240238
param_docs.append(f" {param_name}: {param_info['description']}")
241-
239+
242240
# Always add output_path
243241
param_list.append("output_path: Optional[str] = None")
244-
242+
245243
# Add optional parameters
246244
for param_name, param_info in parameters.items():
247245
if not param_info["required"]:
@@ -251,7 +249,7 @@ def generate_method_code(tool_info: Dict[str, Any]) -> str:
251249
base_type = param_type
252250
else:
253251
base_type = param_type
254-
252+
255253
default = param_info.get("default")
256254
if default is None:
257255
param_list.append(f"{param_name}: Optional[{base_type}] = None")
@@ -261,30 +259,30 @@ def generate_method_code(tool_info: Dict[str, Any]) -> str:
261259
else:
262260
param_list.append(f"{param_name}: {base_type} = {default}")
263261
param_docs.append(f" {param_name}: {param_info['description']}")
264-
262+
265263
# Build method signature
266264
if len(param_list) > 3: # Multiple parameters
267265
params_str = ",\n ".join(param_list)
268266
method_signature = f" def {method_name}(\n {params_str},\n ) -> Optional[bytes]:"
269267
else:
270268
params_str = ", ".join(param_list)
271269
method_signature = f" def {method_name}({params_str}) -> Optional[bytes]:"
272-
270+
273271
# Build docstring
274272
docstring_lines = [f' """{summary}']
275273
if description and description != summary:
276274
docstring_lines.append("")
277275
docstring_lines.append(f" {description}")
278-
276+
279277
docstring_lines.extend([
280278
"",
281279
" Args:",
282280
" input_file: Input file (path, bytes, or file-like object).",
283281
])
284-
282+
285283
if param_docs:
286284
docstring_lines.extend(param_docs)
287-
285+
288286
docstring_lines.extend([
289287
" output_path: Optional path to save the output file.",
290288
"",
@@ -296,22 +294,22 @@ def generate_method_code(tool_info: Dict[str, Any]) -> str:
296294
" APIError: For other API errors.",
297295
' """',
298296
])
299-
297+
300298
# Build method body
301299
method_body = []
302-
300+
303301
# Collect kwargs
304302
kwargs_params = [
305-
f"{name}={name}"
303+
f"{name}={name}"
306304
for name in parameters.keys()
307305
]
308-
306+
309307
if kwargs_params:
310308
kwargs_str = ", ".join(kwargs_params)
311309
method_body.append(f' return self._process_file("{tool_name}", input_file, output_path, {kwargs_str})')
312310
else:
313311
method_body.append(f' return self._process_file("{tool_name}", input_file, output_path)')
314-
312+
315313
# Combine all parts
316314
return "\n".join([
317315
method_signature,
@@ -325,10 +323,10 @@ def generate_api_methods(spec_path: Path, output_path: Path) -> None:
325323
# For Nutrient API, we'll use manually defined tools since they use
326324
# a build endpoint with actions rather than individual endpoints
327325
tools = create_manual_tools()
328-
326+
329327
# Sort tools by method name
330328
tools.sort(key=lambda t: t["method_name"])
331-
329+
332330
# Generate code
333331
code_lines = [
334332
'"""Direct API methods for individual document processing tools.',
@@ -350,12 +348,12 @@ def generate_api_methods(spec_path: Path, output_path: Path) -> None:
350348
' """',
351349
'',
352350
]
353-
351+
354352
# Add methods
355353
for tool in tools:
356354
code_lines.append(generate_method_code(tool))
357355
code_lines.append("") # Empty line between methods
358-
356+
359357
# Write to file
360358
output_path.write_text("\n".join(code_lines))
361359
print(f"Generated {len(tools)} API methods in {output_path}")
@@ -364,5 +362,5 @@ def generate_api_methods(spec_path: Path, output_path: Path) -> None:
364362
if __name__ == "__main__":
365363
spec_path = Path("openapi_spec.yml")
366364
output_path = Path("src/nutrient/api/direct.py")
367-
368-
generate_api_methods(spec_path, output_path)
365+
366+
generate_api_methods(spec_path, output_path)

src/nutrient/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515

1616
__version__ = "0.1.0"
1717
__all__ = [
18+
"APIError",
19+
"AuthenticationError",
20+
"FileProcessingError",
1821
"NutrientClient",
1922
"NutrientError",
20-
"AuthenticationError",
21-
"APIError",
22-
"ValidationError",
2323
"TimeoutError",
24-
"FileProcessingError",
25-
]
24+
"ValidationError",
25+
]

src/nutrient/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""API module for Nutrient DWS client."""
1+
"""API module for Nutrient DWS client."""

src/nutrient/api/direct.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
for common document processing operations.
55
"""
66

7-
from typing import List, Optional
7+
from typing import TYPE_CHECKING, Any, List, Optional
88

99
from nutrient.file_handler import FileInput
1010

11+
if TYPE_CHECKING:
12+
from nutrient.client import NutrientClient
13+
1114

1215
class DirectAPIMixin:
1316
"""Mixin class containing Direct API methods.
@@ -16,6 +19,16 @@ class DirectAPIMixin:
1619
processing operations. They internally use the Build API.
1720
"""
1821

22+
def _process_file(
23+
self: "NutrientClient",
24+
tool: str,
25+
input_file: FileInput,
26+
output_path: Optional[str] = None,
27+
**options: Any,
28+
) -> Optional[bytes]:
29+
"""Process file method that will be provided by NutrientClient."""
30+
raise NotImplementedError("This method is provided by NutrientClient")
31+
1932
def convert_to_pdf(self, input_file: FileInput, output_path: Optional[str] = None) -> Optional[bytes]:
2033
"""Convert a document to PDF
2134

src/nutrient/builder.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Builder API implementation for multi-step workflows."""
22

3-
import json
4-
from typing import Any, Dict, List, Optional, Union
3+
from typing import Any, Dict, List, Optional
54

65
from nutrient.file_handler import FileInput, prepare_file_for_upload, save_file_output
76

@@ -20,7 +19,7 @@ class BuildAPIWrapper:
2019
... .execute(output_path="processed.pdf")
2120
"""
2221

23-
def __init__(self, client, input_file: FileInput) -> None:
22+
def __init__(self, client: Any, input_file: FileInput) -> None:
2423
"""Initialize builder with client and input file.
2524
2625
Args:
@@ -83,24 +82,24 @@ def execute(self, output_path: Optional[str] = None) -> Optional[bytes]:
8382
"""
8483
# Prepare the build instructions
8584
instructions = self._build_instructions()
86-
85+
8786
# Prepare file for upload
8887
file_field, file_data = prepare_file_for_upload(self._input_file)
8988
files = {file_field: file_data}
90-
89+
9190
# Make API request
9291
result = self._client._http_client.post(
9392
"/build",
9493
files=files,
9594
json_data=instructions,
9695
)
97-
96+
9897
# Handle output
9998
if output_path:
10099
save_file_output(result, output_path)
101100
return None
102101
else:
103-
return result
102+
return result # type: ignore[no-any-return]
104103

105104
def _build_instructions(self) -> Dict[str, Any]:
106105
"""Build the instructions payload for the API.
@@ -115,11 +114,11 @@ def _build_instructions(self) -> Dict[str, Any]:
115114
],
116115
"actions": self._actions,
117116
}
118-
117+
119118
# Add output options if specified
120119
if self._output_options:
121120
instructions["output"] = self._output_options
122-
121+
123122
return instructions
124123

125124
def _map_tool_to_action(self, tool: str, options: Dict[str, Any]) -> Dict[str, Any]:
@@ -143,22 +142,22 @@ def _map_tool_to_action(self, tool: str, options: Dict[str, Any]) -> Dict[str, A
143142
"create-redactions": "createRedactions",
144143
"apply-redactions": "applyRedactions",
145144
}
146-
145+
147146
action_type = tool_mapping.get(tool, tool)
148-
147+
149148
# Build action dictionary
150149
action = {"type": action_type}
151-
150+
152151
# Handle special cases for different action types
153152
if action_type == "rotate":
154153
action["rotateBy"] = options.get("degrees", 0)
155154
if "page_indexes" in options:
156155
action["pageIndexes"] = options["page_indexes"]
157-
156+
158157
elif action_type == "ocr":
159158
if "language" in options:
160159
action["language"] = options["language"]
161-
160+
162161
elif action_type == "watermark":
163162
if "text" in options:
164163
action["text"] = options["text"]
@@ -168,11 +167,11 @@ def _map_tool_to_action(self, tool: str, options: Dict[str, Any]) -> Dict[str, A
168167
action["opacity"] = options["opacity"]
169168
if "position" in options:
170169
action["position"] = options["position"]
171-
170+
172171
else:
173172
# For other actions, pass options directly
174173
action.update(options)
175-
174+
176175
return action
177176

178177
def __str__(self) -> str:
@@ -187,4 +186,4 @@ def __repr__(self) -> str:
187186
f"input_file={self._input_file!r}, "
188187
f"actions={self._actions!r}, "
189188
f"output_options={self._output_options!r})"
190-
)
189+
)

0 commit comments

Comments
 (0)