diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 71965b5..0dedd69 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -115,14 +115,14 @@ jobs: const body = `## ⚠️ Parameter Sync Check Failed - The \`generate()\` methods are missing parameters that exist in \`create()\` methods. + The \`generate()\` methods are missing parameters that exist in \`create()\` methods, or their param types/defaults differ from \`create()\`. \`\`\` ${output} \`\`\` - **How to fix:** Add the missing parameters to the \`generate()\` method signature and forward them to \`self.create()\`. + **How to fix:** Run \`python codemod/check_generate_create_sync.py --fix\` and commit the result. `; // Check if we already commented diff --git a/codemod/check_generate_create_sync.py b/codemod/check_generate_create_sync.py index cf59f8c..588d960 100644 --- a/codemod/check_generate_create_sync.py +++ b/codemod/check_generate_create_sync.py @@ -7,6 +7,8 @@ - Checks both sync and async client classes within each file - Compares parameters between the two methods for each client type - Reports any parameters in create() that are missing from generate() +- Reports shared parameters whose type/default in generate() drifted from create() + (e.g. create() made a param optional or added new Literal values) - Ignores parameters specific to generate() (wait_for_completion, download_outputs, etc.) - Ignores 'assets' parameter (handled differently in generate vs create) - Ensures trailing params are always last in correct order @@ -21,14 +23,15 @@ EXIT CODES: 0 - All generate methods are in sync with create methods - 1 - Some generate methods are missing parameters from create + 1 - Some generate methods are missing parameters from create, or have drifted definitions IMPLEMENTATION OVERVIEW: 1. extract_method_params: Extracts parameter names from a method 2. extract_param_definitions: Extracts full parameter definitions (with types) 3. extract_all_generate_params: Categorizes params as regular vs trailing -4. check_file: Checks for parameter mismatches -5. fix_generate_method: Adds missing params to generate() and forwards to create() +4. check_file: Checks for missing params and drifted param definitions +5. fix_generate_method: Adds missing params to generate() and forwards to create(), + and replaces drifted param definitions with the ones from create() - _update_generate_signature: Updates method signature with correct param order - _update_create_call: Updates self.create() call to include new params """ @@ -212,9 +215,39 @@ def find_client_files_with_generate(base_dir: str) -> List[str]: return sorted(result) -def check_file( - file_path: str, verbose: bool = True -) -> Tuple[bool, List[Tuple[str, List[str]]]]: +def _normalize_definition(definition: str) -> str: + """Normalize a param definition so formatting differences don't count as drift.""" + compact = re.sub(r"\s+", "", definition).rstrip(",") + return re.sub(r",([\]\)])", r"\1", compact) + + +def find_drifted_params( + content: str, class_name: str, param_names: Set[str] +) -> List[str]: + """ + Find params present in both generate() and create() whose definitions differ. + + Returns: + Sorted list of parameter names whose type hint or default drifted + """ + generate_defs = extract_param_definitions( + content, "generate", class_name, param_names + ) + create_defs = extract_param_definitions(content, "create", class_name, param_names) + return sorted( + name + for name in param_names + if name in generate_defs + and name in create_defs + and _normalize_definition(generate_defs[name]) + != _normalize_definition(create_defs[name]) + ) + + +Issue = Tuple[str, List[str], List[str]] + + +def check_file(file_path: str, verbose: bool = True) -> Tuple[bool, List[Issue]]: """ Check a single client file for parameter mismatches in both sync and async clients. @@ -224,7 +257,7 @@ def check_file( Returns: Tuple of (is_in_sync, list_of_issues) - where each issue is (client_type, missing_params) + where each issue is (client_type, missing_params, drifted_params) """ with open(file_path, "r") as f: content = f.read() @@ -248,9 +281,14 @@ def check_file( generate_params - GENERATE_ONLY_PARAMS - IGNORED_PARAMS ) missing_in_generate = create_params_to_check - generate_params_to_check + drifted = find_drifted_params( + content, + class_name, + create_params_to_check & generate_params_to_check, + ) - if missing_in_generate: - issues.append((client_type, sorted(missing_in_generate))) + if missing_in_generate or drifted: + issues.append((client_type, sorted(missing_in_generate), drifted)) return len(issues) == 0, issues @@ -349,10 +387,14 @@ def _categorize_param(param_text: str, result: dict) -> None: def fix_generate_method( - content: str, class_name: str, missing_params: List[str] + content: str, + class_name: str, + missing_params: List[str], + drifted_params: Optional[List[str]] = None, ) -> str: """ Add missing parameters to generate() method and forward them to create() call. + Drifted parameters keep their position but take create()'s definition. Ensures trailing params are always last in correct order: 1. wait_for_completion @@ -364,10 +406,13 @@ def fix_generate_method( content: File content class_name: Name of the client class (e.g., "AsyncAiGifGeneratorClient") missing_params: List of parameter names to add + drifted_params: List of parameter names whose definition to replace Returns: Updated file content with fixed generate() method """ + drifted_params = drifted_params or [] + # Extract the class content class_pattern = rf"class\s+{class_name}\s*[:\(].*?(?=\nclass\s|\Z)" class_match = re.search(class_pattern, content, re.DOTALL) @@ -379,7 +424,7 @@ def fix_generate_method( # Get full parameter definitions from create method param_defs = extract_param_definitions( - class_content, "create", None, set(missing_params) + class_content, "create", None, set(missing_params) | set(drifted_params) ) if not param_defs: return content @@ -395,7 +440,11 @@ def fix_generate_method( ) # Update the self.create() call to include new parameters - updated_class_content = _update_create_call(updated_class_content, missing_params) + # (drifted params are already forwarded) + if missing_params: + updated_class_content = _update_create_call( + updated_class_content, missing_params + ) # Replace in full content return ( @@ -543,13 +592,13 @@ def _update_create_call(class_content: str, missing_params: List[str]) -> str: ) -def fix_file(file_path: str, issues: List[Tuple[str, List[str]]]) -> bool: +def fix_file(file_path: str, issues: List[Issue]) -> bool: """ Fix parameter mismatches in a client file. Args: file_path: Path to the client file - issues: List of (client_type, missing_params) tuples + issues: List of (client_type, missing_params, drifted_params) tuples Returns: True if file was modified, False otherwise @@ -560,11 +609,12 @@ def fix_file(file_path: str, issues: List[Tuple[str, List[str]]]) -> bool: original_content = content sync_class, async_class = extract_class_names(content) - for client_type, missing_params in issues: - if client_type == "sync" and sync_class: - content = fix_generate_method(content, sync_class, missing_params) - elif client_type == "async" and async_class: - content = fix_generate_method(content, async_class, missing_params) + for client_type, missing_params, drifted_params in issues: + class_name = sync_class if client_type == "sync" else async_class + if class_name: + content = fix_generate_method( + content, class_name, missing_params, drifted_params + ) if content != original_content: with open(file_path, "w") as f: @@ -574,6 +624,18 @@ def fix_file(file_path: str, issues: List[Tuple[str, List[str]]]) -> bool: return False +def _print_issues(issues: List[Issue], indent: str) -> None: + for client_type, missing_params, drifted_params in issues: + if missing_params: + print( + f"{indent}{client_type} client - Missing in generate(): {', '.join(missing_params)}" + ) + if drifted_params: + print( + f"{indent}{client_type} client - Definition differs from create(): {', '.join(drifted_params)}" + ) + + def main(): parser = argparse.ArgumentParser( description="Check that generate() and create() methods have matching parameters" @@ -614,10 +676,7 @@ def main(): if verbose: print(f"MISMATCH: {resource_name}") print(f" File: {file_path}") - for client_type, missing_params in client_issues: - print( - f" {client_type} client - Missing in generate(): {', '.join(missing_params)}" - ) + _print_issues(client_issues, " ") print() elif verbose and not args.quiet: print(f"OK: {resource_name}") @@ -638,10 +697,7 @@ def main(): fixed_count = 0 for resource_name, file_path, client_issues in issues: print(f" Fixing {resource_name}...") - for client_type, missing_params in client_issues: - print( - f" {client_type} client - Adding: {', '.join(missing_params)}" - ) + _print_issues(client_issues, " ") if fix_file(file_path, client_issues): fixed_count += 1 @@ -660,10 +716,7 @@ def main(): if not is_in_sync: all_fixed = False print(f" {resource_name}: Still has issues") - for client_type, missing_params in remaining_issues: - print( - f" {client_type} client - Still missing: {', '.join(missing_params)}" - ) + _print_issues(remaining_issues, " ") else: print(f" {resource_name}: ✓ All fixed") @@ -676,10 +729,7 @@ def main(): else: for resource_name, file_path, client_issues in issues: print(f" {resource_name}:") - for client_type, missing_params in client_issues: - print( - f" {client_type} client - Missing: {', '.join(missing_params)}" - ) + _print_issues(client_issues, " ") print(f" File: {file_path}") print() diff --git a/magic_hour/resources/v1/ai_image_editor/client.py b/magic_hour/resources/v1/ai_image_editor/client.py index 7de3fc4..c90e152 100644 --- a/magic_hour/resources/v1/ai_image_editor/client.py +++ b/magic_hour/resources/v1/ai_image_editor/client.py @@ -45,11 +45,18 @@ def generate( typing.Optional[ typing_extensions.Literal[ "default", + "flux-2-klein", + "gpt-image-2", + "gpt-image-2.5-flare", + "krea-2", "nano-banana", + "nano-banana-2", + "nano-banana-2-lite", "nano-banana-pro", "qwen-edit", "seedream-v4", "seedream-v4.5", + "seedream-v5-pro", ] ], type_utils.NotGiven, @@ -58,7 +65,9 @@ def generate( typing.Optional[str], type_utils.NotGiven ] = type_utils.NOT_GIVEN, resolution: typing.Union[ - typing.Optional[typing_extensions.Literal["2k", "4k", "auto"]], + typing.Optional[ + typing_extensions.Literal["1k", "2k", "4k", "640px", "auto"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, wait_for_completion: bool = True, @@ -341,17 +350,26 @@ async def generate( typing.Optional[ typing_extensions.Literal[ "default", + "flux-2-klein", + "gpt-image-2", + "gpt-image-2.5-flare", + "krea-2", "nano-banana", + "nano-banana-2", + "nano-banana-2-lite", "nano-banana-pro", "qwen-edit", "seedream-v4", "seedream-v4.5", + "seedream-v5-pro", ] ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, resolution: typing.Union[ - typing.Optional[typing_extensions.Literal["2k", "4k", "auto"]], + typing.Optional[ + typing_extensions.Literal["1k", "2k", "4k", "640px", "auto"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, wait_for_completion: bool = True, diff --git a/magic_hour/resources/v1/ai_image_generator/README.md b/magic_hour/resources/v1/ai_image_generator/README.md index 1214c0f..f9574bd 100644 --- a/magic_hour/resources/v1/ai_image_generator/README.md +++ b/magic_hour/resources/v1/ai_image_generator/README.md @@ -56,7 +56,7 @@ from os import getenv client = AsyncClient(token=getenv("API_TOKEN")) res = await client.v1.ai_image_generator.generate( image_count=1, - orientation="landscape", + aspect_ratio="16:9", style={"prompt": "Cool image", "tool": "ai-anime-generator"}, name="Ai Image image", wait_for_completion=True, diff --git a/magic_hour/resources/v1/ai_image_generator/client.py b/magic_hour/resources/v1/ai_image_generator/client.py index 1d9db3b..d1138f5 100644 --- a/magic_hour/resources/v1/ai_image_generator/client.py +++ b/magic_hour/resources/v1/ai_image_generator/client.py @@ -37,9 +37,18 @@ def generate( typing.Optional[ typing_extensions.Literal[ "default", + "flux-2-klein", "flux-schnell", + "gpt-image-2", + "gpt-image-2.5-flare", + "krea-2", + "nano-banana", + "nano-banana-2", + "nano-banana-2-lite", "nano-banana-pro", "seedream", + "seedream-v4", + "seedream-v5-pro", "z-image-turbo", ] ], @@ -55,7 +64,9 @@ def generate( type_utils.NotGiven, ] = type_utils.NOT_GIVEN, resolution: typing.Union[ - typing.Optional[typing_extensions.Literal["2k", "4k", "auto"]], + typing.Optional[ + typing_extensions.Literal["1k", "2k", "4k", "640px", "auto"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, wait_for_completion: bool = True, @@ -311,7 +322,12 @@ async def generate( self, *, image_count: int, - orientation: typing_extensions.Literal["landscape", "portrait", "square"], + orientation: typing.Union[ + typing.Optional[ + typing_extensions.Literal["landscape", "portrait", "square"] + ], + type_utils.NotGiven, + ] = type_utils.NOT_GIVEN, style: params.V1AiImageGeneratorCreateBodyStyle, name: typing.Union[ typing.Optional[str], type_utils.NotGiven @@ -324,16 +340,27 @@ async def generate( typing.Optional[ typing_extensions.Literal[ "default", + "flux-2-klein", "flux-schnell", + "gpt-image-2", + "gpt-image-2.5-flare", + "krea-2", + "nano-banana", + "nano-banana-2", + "nano-banana-2-lite", "nano-banana-pro", "seedream", + "seedream-v4", + "seedream-v5-pro", "z-image-turbo", ] ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, resolution: typing.Union[ - typing.Optional[typing_extensions.Literal["2k", "4k", "auto"]], + typing.Optional[ + typing_extensions.Literal["1k", "2k", "4k", "640px", "auto"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, wait_for_completion: bool = True, diff --git a/magic_hour/resources/v1/ai_image_upscaler/client.py b/magic_hour/resources/v1/ai_image_upscaler/client.py index 68949f2..a03aa5e 100644 --- a/magic_hour/resources/v1/ai_image_upscaler/client.py +++ b/magic_hour/resources/v1/ai_image_upscaler/client.py @@ -29,7 +29,10 @@ def generate( *, assets: params.V1AiImageUpscalerGenerateBodyAssets, scale_factor: float, - style: params.V1AiImageUpscalerCreateBodyStyle, + style: typing.Union[ + typing.Optional[params.V1AiImageUpscalerCreateBodyStyle], + type_utils.NotGiven, + ] = type_utils.NOT_GIVEN, name: typing.Union[ typing.Optional[str], type_utils.NotGiven ] = type_utils.NOT_GIVEN, @@ -168,7 +171,10 @@ async def generate( *, assets: params.V1AiImageUpscalerGenerateBodyAssets, scale_factor: float, - style: params.V1AiImageUpscalerCreateBodyStyle, + style: typing.Union[ + typing.Optional[params.V1AiImageUpscalerCreateBodyStyle], + type_utils.NotGiven, + ] = type_utils.NOT_GIVEN, name: typing.Union[ typing.Optional[str], type_utils.NotGiven ] = type_utils.NOT_GIVEN, diff --git a/magic_hour/resources/v1/ai_video_editor/client.py b/magic_hour/resources/v1/ai_video_editor/client.py index dd85eb8..40b9e4c 100644 --- a/magic_hour/resources/v1/ai_video_editor/client.py +++ b/magic_hour/resources/v1/ai_video_editor/client.py @@ -32,7 +32,9 @@ def generate( end_seconds: float, style: params.V1AiVideoEditorCreateBodyStyle, model: typing.Union[ - typing.Optional[typing_extensions.Literal["gemini-omni", "ltx-2.3"]], + typing.Optional[ + typing_extensions.Literal["gemini-omni", "gemini-omni-1.1", "ltx-2.3"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, name: typing.Union[ @@ -224,7 +226,9 @@ async def generate( end_seconds: float, style: params.V1AiVideoEditorCreateBodyStyle, model: typing.Union[ - typing.Optional[typing_extensions.Literal["gemini-omni", "ltx-2.3"]], + typing.Optional[ + typing_extensions.Literal["gemini-omni", "gemini-omni-1.1", "ltx-2.3"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, name: typing.Union[ diff --git a/magic_hour/resources/v1/image_to_video/client.py b/magic_hour/resources/v1/image_to_video/client.py index 2d2a16e..8951fee 100644 --- a/magic_hour/resources/v1/image_to_video/client.py +++ b/magic_hour/resources/v1/image_to_video/client.py @@ -37,12 +37,27 @@ def generate( typing.Optional[ typing_extensions.Literal[ "default", + "gemini-omni-1.1", "kling-1.6", + "kling-2.5", "kling-2.5-audio", + "kling-2.6", + "kling-3.0", + "ltx-2", + "ltx-2.3", + "ltx-2.5", + "minimax-h3", "seedance", + "seedance-1.5", + "seedance-2.0", + "seedance-2.0-mini", + "seedance-2.5", "sora-2", "veo3.1", "veo3.1-audio", + "veo3.1-lite", + "wan-2.2", + "wan-3.0", ] ], type_utils.NotGiven, @@ -51,7 +66,9 @@ def generate( typing.Optional[str], type_utils.NotGiven ] = type_utils.NOT_GIVEN, resolution: typing.Union[ - typing.Optional[typing_extensions.Literal["1080p", "480p", "720p"]], + typing.Optional[ + typing_extensions.Literal["1080p", "360p", "480p", "4k", "720p"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, style: typing.Union[ @@ -366,7 +383,9 @@ async def generate( typing.Optional[str], type_utils.NotGiven ] = type_utils.NOT_GIVEN, resolution: typing.Union[ - typing.Optional[typing_extensions.Literal["1080p", "480p", "720p"]], + typing.Optional[ + typing_extensions.Literal["1080p", "360p", "480p", "4k", "720p"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, style: typing.Union[ @@ -379,12 +398,27 @@ async def generate( typing.Optional[ typing_extensions.Literal[ "default", + "gemini-omni-1.1", "kling-1.6", + "kling-2.5", "kling-2.5-audio", + "kling-2.6", + "kling-3.0", + "ltx-2", + "ltx-2.3", + "ltx-2.5", + "minimax-h3", "seedance", + "seedance-1.5", + "seedance-2.0", + "seedance-2.0-mini", + "seedance-2.5", "sora-2", "veo3.1", "veo3.1-audio", + "veo3.1-lite", + "wan-2.2", + "wan-3.0", ] ], type_utils.NotGiven, diff --git a/magic_hour/resources/v1/text_to_video/README.md b/magic_hour/resources/v1/text_to_video/README.md index 585e6c5..dbfdc89 100644 --- a/magic_hour/resources/v1/text_to_video/README.md +++ b/magic_hour/resources/v1/text_to_video/README.md @@ -52,7 +52,7 @@ from os import getenv client = AsyncClient(token=getenv("API_TOKEN")) res = await client.v1.text_to_video.generate( end_seconds=5.0, - orientation="landscape", + aspect_ratio="16:9", style={"prompt": "a dog running"}, name="Text To Video video", resolution="720p", diff --git a/magic_hour/resources/v1/text_to_video/client.py b/magic_hour/resources/v1/text_to_video/client.py index a6c5a81..c54c674 100644 --- a/magic_hour/resources/v1/text_to_video/client.py +++ b/magic_hour/resources/v1/text_to_video/client.py @@ -37,12 +37,27 @@ def generate( typing.Optional[ typing_extensions.Literal[ "default", + "gemini-omni-1.1", "kling-1.6", + "kling-2.5", "kling-2.5-audio", + "kling-2.6", + "kling-3.0", + "ltx-2", + "ltx-2.3", + "ltx-2.5", + "minimax-h3", "seedance", + "seedance-1.5", + "seedance-2.0", + "seedance-2.0-mini", + "seedance-2.5", "sora-2", "veo3.1", "veo3.1-audio", + "veo3.1-lite", + "wan-2.2", + "wan-3.0", ] ], type_utils.NotGiven, @@ -57,7 +72,9 @@ def generate( type_utils.NotGiven, ] = type_utils.NOT_GIVEN, resolution: typing.Union[ - typing.Optional[typing_extensions.Literal["1080p", "480p", "720p"]], + typing.Optional[ + typing_extensions.Literal["1080p", "360p", "480p", "4k", "720p"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, audio: typing.Union[ @@ -360,13 +377,20 @@ async def generate( self, *, end_seconds: float, - orientation: typing_extensions.Literal["landscape", "portrait", "square"], + orientation: typing.Union[ + typing.Optional[ + typing_extensions.Literal["landscape", "portrait", "square"] + ], + type_utils.NotGiven, + ] = type_utils.NOT_GIVEN, style: params.V1TextToVideoCreateBodyStyle, name: typing.Union[ typing.Optional[str], type_utils.NotGiven ] = type_utils.NOT_GIVEN, resolution: typing.Union[ - typing.Optional[typing_extensions.Literal["1080p", "480p", "720p"]], + typing.Optional[ + typing_extensions.Literal["1080p", "360p", "480p", "4k", "720p"] + ], type_utils.NotGiven, ] = type_utils.NOT_GIVEN, aspect_ratio: typing.Union[ @@ -377,12 +401,27 @@ async def generate( typing.Optional[ typing_extensions.Literal[ "default", + "gemini-omni-1.1", "kling-1.6", + "kling-2.5", "kling-2.5-audio", + "kling-2.6", + "kling-3.0", + "ltx-2", + "ltx-2.3", + "ltx-2.5", + "minimax-h3", "seedance", + "seedance-1.5", + "seedance-2.0", + "seedance-2.0-mini", + "seedance-2.5", "sora-2", "veo3.1", "veo3.1-audio", + "veo3.1-lite", + "wan-2.2", + "wan-3.0", ] ], type_utils.NotGiven, diff --git a/pyproject.toml b/pyproject.toml index 91c5fc5..5545643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "magic_hour" -version = "0.82.0" +version = "0.82.1" description = "Python SDK for Magic Hour API" readme = "README.md" authors = []