1+ #!/usr/bin/env python3
2+ """Generate Direct API methods from OpenAPI specification."""
3+
4+ import re
5+ from pathlib import Path
6+ from typing import Any , Dict , List , Optional , Set
7+
8+ import yaml
9+
10+
11+ def to_snake_case (name : str ) -> str :
12+ """Convert string to snake_case."""
13+ # Handle common patterns
14+ name = name .replace ("-" , "_" )
15+ # Insert underscore before uppercase letters
16+ name = re .sub (r"(?<!^)(?=[A-Z])" , "_" , name ).lower ()
17+ return name
18+
19+
20+ def get_python_type (schema : Dict [str , Any ]) -> str :
21+ """Convert OpenAPI schema type to Python type hint."""
22+ if not schema :
23+ return "Any"
24+
25+ type_mapping = {
26+ "string" : "str" ,
27+ "integer" : "int" ,
28+ "number" : "float" ,
29+ "boolean" : "bool" ,
30+ "array" : "List[Any]" ,
31+ "object" : "Dict[str, Any]" ,
32+ }
33+
34+ schema_type = schema .get ("type" , "string" )
35+ return type_mapping .get (schema_type , "Any" )
36+
37+
38+ def create_manual_tools () -> List [Dict [str , Any ]]:
39+ """Create tool definitions based on the specification documentation.
40+
41+ Since the Nutrient API uses a build endpoint with actions rather than
42+ individual tool endpoints, we'll create convenience methods that wrap
43+ the build API.
44+ """
45+ tools = [
46+ {
47+ "tool_name" : "convert-to-pdf" ,
48+ "method_name" : "convert_to_pdf" ,
49+ "summary" : "Convert a document to PDF" ,
50+ "description" : "Convert various document formats (DOCX, XLSX, PPTX, etc.) to PDF." ,
51+ "parameters" : {},
52+ },
53+ {
54+ "tool_name" : "convert-to-pdfa" ,
55+ "method_name" : "convert_to_pdfa" ,
56+ "summary" : "Convert a document to PDF/A" ,
57+ "description" : "Convert documents to PDF/A format for long-term archiving." ,
58+ "parameters" : {
59+ "conformance_level" : {
60+ "type" : "str" ,
61+ "required" : False ,
62+ "description" : "PDF/A conformance level (e.g., '2b', '3b')" ,
63+ "default" : "2b" ,
64+ },
65+ },
66+ },
67+ {
68+ "tool_name" : "ocr-pdf" ,
69+ "method_name" : "ocr_pdf" ,
70+ "summary" : "Perform OCR on a PDF" ,
71+ "description" : "Apply optical character recognition to make scanned PDFs searchable." ,
72+ "parameters" : {
73+ "language" : {
74+ "type" : "str" ,
75+ "required" : False ,
76+ "description" : "OCR language code (e.g., 'en', 'de', 'fr')" ,
77+ "default" : "en" ,
78+ },
79+ },
80+ },
81+ {
82+ "tool_name" : "rotate-pages" ,
83+ "method_name" : "rotate_pages" ,
84+ "summary" : "Rotate PDF pages" ,
85+ "description" : "Rotate pages in a PDF document." ,
86+ "parameters" : {
87+ "degrees" : {
88+ "type" : "int" ,
89+ "required" : True ,
90+ "description" : "Rotation angle in degrees (90, 180, 270)" ,
91+ },
92+ "page_indexes" : {
93+ "type" : "List[int]" ,
94+ "required" : False ,
95+ "description" : "List of page indexes to rotate (0-based). If not specified, all pages are rotated." ,
96+ },
97+ },
98+ },
99+ {
100+ "tool_name" : "flatten-annotations" ,
101+ "method_name" : "flatten_annotations" ,
102+ "summary" : "Flatten PDF annotations" ,
103+ "description" : "Flatten annotations and form fields in a PDF." ,
104+ "parameters" : {},
105+ },
106+ {
107+ "tool_name" : "watermark-pdf" ,
108+ "method_name" : "watermark_pdf" ,
109+ "summary" : "Add watermark to PDF" ,
110+ "description" : "Add text or image watermark to PDF pages." ,
111+ "parameters" : {
112+ "text" : {
113+ "type" : "str" ,
114+ "required" : False ,
115+ "description" : "Watermark text" ,
116+ },
117+ "image_url" : {
118+ "type" : "str" ,
119+ "required" : False ,
120+ "description" : "URL of watermark image" ,
121+ },
122+ "opacity" : {
123+ "type" : "float" ,
124+ "required" : False ,
125+ "description" : "Watermark opacity (0.0 to 1.0)" ,
126+ "default" : 0.5 ,
127+ },
128+ "position" : {
129+ "type" : "str" ,
130+ "required" : False ,
131+ "description" : "Watermark position" ,
132+ "default" : "center" ,
133+ },
134+ },
135+ },
136+ {
137+ "tool_name" : "sign-pdf" ,
138+ "method_name" : "sign_pdf" ,
139+ "summary" : "Digitally sign a PDF" ,
140+ "description" : "Add a digital signature to a PDF document." ,
141+ "parameters" : {
142+ "certificate_file" : {
143+ "type" : "FileInput" ,
144+ "required" : True ,
145+ "description" : "Digital certificate file (P12/PFX format)" ,
146+ },
147+ "certificate_password" : {
148+ "type" : "str" ,
149+ "required" : True ,
150+ "description" : "Certificate password" ,
151+ },
152+ "reason" : {
153+ "type" : "str" ,
154+ "required" : False ,
155+ "description" : "Reason for signing" ,
156+ },
157+ "location" : {
158+ "type" : "str" ,
159+ "required" : False ,
160+ "description" : "Location of signing" ,
161+ },
162+ },
163+ },
164+ {
165+ "tool_name" : "redact-pdf" ,
166+ "method_name" : "redact_pdf" ,
167+ "summary" : "Redact sensitive information from PDF" ,
168+ "description" : "Use AI to automatically redact sensitive information from a PDF." ,
169+ "parameters" : {
170+ "types" : {
171+ "type" : "List[str]" ,
172+ "required" : False ,
173+ "description" : "Types of information to redact (e.g., 'email', 'phone', 'ssn')" ,
174+ },
175+ },
176+ },
177+ {
178+ "tool_name" : "export-pdf-to-office" ,
179+ "method_name" : "export_pdf_to_office" ,
180+ "summary" : "Export PDF to Office format" ,
181+ "description" : "Convert PDF to Microsoft Office formats (DOCX, XLSX, PPTX)." ,
182+ "parameters" : {
183+ "format" : {
184+ "type" : "str" ,
185+ "required" : True ,
186+ "description" : "Output format ('docx', 'xlsx', 'pptx')" ,
187+ },
188+ },
189+ },
190+ {
191+ "tool_name" : "export-pdf-to-images" ,
192+ "method_name" : "export_pdf_to_images" ,
193+ "summary" : "Export PDF pages as images" ,
194+ "description" : "Convert PDF pages to image files." ,
195+ "parameters" : {
196+ "format" : {
197+ "type" : "str" ,
198+ "required" : False ,
199+ "description" : "Image format ('png', 'jpeg', 'webp')" ,
200+ "default" : "png" ,
201+ },
202+ "dpi" : {
203+ "type" : "int" ,
204+ "required" : False ,
205+ "description" : "Image resolution in DPI" ,
206+ "default" : 150 ,
207+ },
208+ "page_indexes" : {
209+ "type" : "List[int]" ,
210+ "required" : False ,
211+ "description" : "List of page indexes to export (0-based)" ,
212+ },
213+ },
214+ },
215+ ]
216+
217+ return tools
218+
219+
220+ def generate_method_code (tool_info : Dict [str , Any ]) -> str :
221+ """Generate Python method code for a tool."""
222+ method_name = tool_info ["method_name" ]
223+ tool_name = tool_info ["tool_name" ]
224+ summary = tool_info ["summary" ]
225+ description = tool_info ["description" ]
226+ parameters = tool_info ["parameters" ]
227+
228+ # Build parameter list
229+ param_list = ["self" , "input_file: FileInput" ]
230+ param_docs = []
231+
232+ # Add required parameters first
233+ for param_name , param_info in parameters .items ():
234+ if param_info ["required" ]:
235+ param_type = param_info ["type" ]
236+ # Handle imports for complex types
237+ if param_type == "FileInput" :
238+ param_type = "'FileInput'" # Forward reference
239+ param_list .append (f"{ param_name } : { param_type } " )
240+ param_docs .append (f" { param_name } : { param_info ['description' ]} " )
241+
242+ # Always add output_path
243+ param_list .append ("output_path: Optional[str] = None" )
244+
245+ # Add optional parameters
246+ for param_name , param_info in parameters .items ():
247+ if not param_info ["required" ]:
248+ param_type = param_info ["type" ]
249+ # Handle List types
250+ if param_type .startswith ("List[" ):
251+ base_type = param_type
252+ else :
253+ base_type = param_type
254+
255+ default = param_info .get ("default" )
256+ if default is None :
257+ param_list .append (f"{ param_name } : Optional[{ base_type } ] = None" )
258+ else :
259+ if isinstance (default , str ):
260+ param_list .append (f'{ param_name } : { base_type } = "{ default } "' )
261+ else :
262+ param_list .append (f"{ param_name } : { base_type } = { default } " )
263+ param_docs .append (f" { param_name } : { param_info ['description' ]} " )
264+
265+ # Build method signature
266+ if len (param_list ) > 3 : # Multiple parameters
267+ params_str = ",\n " .join (param_list )
268+ method_signature = f" def { method_name } (\n { params_str } ,\n ) -> Optional[bytes]:"
269+ else :
270+ params_str = ", " .join (param_list )
271+ method_signature = f" def { method_name } ({ params_str } ) -> Optional[bytes]:"
272+
273+ # Build docstring
274+ docstring_lines = [f' """{ summary } ' ]
275+ if description and description != summary :
276+ docstring_lines .append ("" )
277+ docstring_lines .append (f" { description } " )
278+
279+ docstring_lines .extend ([
280+ "" ,
281+ " Args:" ,
282+ " input_file: Input file (path, bytes, or file-like object)." ,
283+ ])
284+
285+ if param_docs :
286+ docstring_lines .extend (param_docs )
287+
288+ docstring_lines .extend ([
289+ " output_path: Optional path to save the output file." ,
290+ "" ,
291+ " Returns:" ,
292+ " Processed file as bytes, or None if output_path is provided." ,
293+ "" ,
294+ " Raises:" ,
295+ " AuthenticationError: If API key is missing or invalid." ,
296+ " APIError: For other API errors." ,
297+ ' """' ,
298+ ])
299+
300+ # Build method body
301+ method_body = []
302+
303+ # Collect kwargs
304+ kwargs_params = [
305+ f"{ name } ={ name } "
306+ for name in parameters .keys ()
307+ ]
308+
309+ if kwargs_params :
310+ kwargs_str = ", " .join (kwargs_params )
311+ method_body .append (f' return self._process_file("{ tool_name } ", input_file, output_path, { kwargs_str } )' )
312+ else :
313+ method_body .append (f' return self._process_file("{ tool_name } ", input_file, output_path)' )
314+
315+ # Combine all parts
316+ return "\n " .join ([
317+ method_signature ,
318+ "\n " .join (docstring_lines ),
319+ "\n " .join (method_body ),
320+ ])
321+
322+
323+ def generate_api_methods (spec_path : Path , output_path : Path ) -> None :
324+ """Generate API methods from OpenAPI specification."""
325+ # For Nutrient API, we'll use manually defined tools since they use
326+ # a build endpoint with actions rather than individual endpoints
327+ tools = create_manual_tools ()
328+
329+ # Sort tools by method name
330+ tools .sort (key = lambda t : t ["method_name" ])
331+
332+ # Generate code
333+ code_lines = [
334+ '"""Direct API methods for individual document processing tools.' ,
335+ '' ,
336+ 'This file provides convenient methods that wrap the Nutrient Build API' ,
337+ 'for common document processing operations.' ,
338+ '"""' ,
339+ '' ,
340+ 'from typing import List, Optional' ,
341+ '' ,
342+ 'from nutrient.file_handler import FileInput' ,
343+ '' ,
344+ '' ,
345+ 'class DirectAPIMixin:' ,
346+ ' """Mixin class containing Direct API methods.' ,
347+ ' ' ,
348+ ' These methods provide a simplified interface to common document' ,
349+ ' processing operations. They internally use the Build API.' ,
350+ ' """' ,
351+ '' ,
352+ ]
353+
354+ # Add methods
355+ for tool in tools :
356+ code_lines .append (generate_method_code (tool ))
357+ code_lines .append ("" ) # Empty line between methods
358+
359+ # Write to file
360+ output_path .write_text ("\n " .join (code_lines ))
361+ print (f"Generated { len (tools )} API methods in { output_path } " )
362+
363+
364+ if __name__ == "__main__" :
365+ spec_path = Path ("openapi_spec.yml" )
366+ output_path = Path ("src/nutrient/api/direct.py" )
367+
368+ generate_api_methods (spec_path , output_path )
0 commit comments