Skip to content

Conversation

@dgarros
Copy link
Contributor

@dgarros dgarros commented Sep 14, 2025

Fixes #545

This PR adds a new command to generate Pydantic model that matches the format of a given GraphQL query
For each graphql query, a python file with the same name will be created with the corresponding Pydantic models.

infrahubctl graphql generate-return-types

The current implementation support fragments that are stored within the same query file

In order to generate the return types, the graphql schema needs to exist locally so this PR also adds a command to fetch the graphql schema from the infrahub instance.

infrahubctl graphql export-schema

Implementation

Internally, the command is leveraging the library ariadne-codegen to generate the Pydantic models. ariadne-codegen provide a plugin system that makes it very easy to control the format of the generated file, currently we are doing some small adjustment but the system is very powerful.
The main drawback if using ``ariadne-codegen` is that by default the system has been designed to generate not just the return types but the full client so a wrapper was required to only generate the types.

Summary by CodeRabbit

  • New Features

    • Adds a graphql CLI group with commands to export a schema and generate Pydantic return types; SDK exposes GraphQL helpers and schema fetch APIs.
  • Documentation

    • CLI docs for graphql commands and a Python guide on generating Pydantic models from GraphQL queries; minor typo fix.
  • Refactor

    • Improved GraphQL rendering and query builder surface; new public constants and plugins to produce type-safe models.
  • Tests

    • New unit tests and fixtures for renderers, plugins, and codegen flow.
  • Chores

    • Adds optional codegen dependency and removes a branded-term mapping.

@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

Walkthrough

Adds an infrahubctl CLI group graphql with export-schema and generate-return-types commands and registers it. Introduces a GraphQL package (constants, plugin, query, renderers, utils, init) and schema retrieval methods on the SDK schema client. Adds documentation and a changelog entry, tests and fixtures for plugin/renderer behavior, and makes ariadne-codegen an optional dependency in pyproject extras. Removes a Vale branded-terms mapping for "GraphQL".

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The removal of the GraphQL mapping in the Vale style configuration (.vale/styles/Infrahub/branded-terms-case-swap.yml) is unrelated to the pull request’s goal of adding GraphQL CLI commands and Pydantic model generation. Move the Vale style update to a separate pull request or provide justification in this PR to maintain focus on the GraphQL command and model generation functionality.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly describes adding a new CLI command that generates Pydantic models from GraphQL queries and reflects the primary change introduced by this pull request.
Linked Issues Check ✅ Passed The changes implement the core requirements of issue #545 by providing infrahubctl graphql generate-return-types and export-schema commands, leveraging ariadne-codegen for Pydantic model generation, and including relevant tests and documentation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dga-20250913-graphql-return-types

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Sep 14, 2025

Codecov Report

❌ Patch coverage is 50.00000% with 114 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
infrahub_sdk/ctl/graphql.py 35.78% 61 Missing ⚠️
infrahub_sdk/graphql/plugin.py 50.98% 24 Missing and 1 partial ⚠️
infrahub_sdk/graphql/utils.py 20.00% 16 Missing ⚠️
infrahub_sdk/schema/__init__.py 14.28% 12 Missing ⚠️
@@             Coverage Diff             @@
##           develop     #546      +/-   ##
===========================================
- Coverage    76.35%   75.37%   -0.99%     
===========================================
  Files          102      108       +6     
  Lines        10086     9298     -788     
  Branches      2081     1847     -234     
===========================================
- Hits          7701     7008     -693     
+ Misses        1831     1802      -29     
+ Partials       554      488      -66     
Flag Coverage Δ
integration-tests 34.83% <28.50%> (-1.38%) ⬇️
python-3.10 48.44% <39.91%> (-1.81%) ⬇️
python-3.11 48.41% <39.91%> (-1.81%) ⬇️
python-3.12 48.41% <39.91%> (-1.81%) ⬇️
python-3.13 48.39% <39.91%> (-1.84%) ⬇️
python-3.9 47.07% <39.91%> (-1.63%) ⬇️
python-filler-3.12 24.37% <10.08%> (+0.28%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
infrahub_sdk/ctl/cli_commands.py 70.85% <100.00%> (-0.01%) ⬇️
infrahub_sdk/graphql/__init__.py 100.00% <100.00%> (ø)
infrahub_sdk/graphql/constants.py 100.00% <100.00%> (ø)
infrahub_sdk/graphql/query.py 100.00% <100.00%> (ø)
infrahub_sdk/graphql/renderers.py 98.68% <100.00%> (ø)
infrahub_sdk/schema/__init__.py 66.36% <14.28%> (-2.29%) ⬇️
infrahub_sdk/graphql/utils.py 20.00% <20.00%> (ø)
infrahub_sdk/graphql/plugin.py 50.98% <50.98%> (ø)
infrahub_sdk/ctl/graphql.py 35.78% <35.78%> (ø)

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Sep 14, 2025

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: d490fd8
Status: ✅  Deploy successful!
Preview URL: https://20553454.infrahub-sdk-python.pages.dev
Branch Preview URL: https://dga-20250913-graphql-return.infrahub-sdk-python.pages.dev

View logs

@dgarros dgarros force-pushed the dga-20250913-graphql-return-types branch from 9f31369 to 6754510 Compare September 21, 2025 13:12
@dgarros dgarros force-pushed the dga-20250913-graphql-return-types branch from 6754510 to 911ca81 Compare September 30, 2025 13:17
@github-actions github-actions bot added the type/documentation Improvements or additions to documentation label Sep 30, 2025
@dgarros dgarros marked this pull request as ready for review September 30, 2025 14:29
@dgarros dgarros requested review from a team September 30, 2025 14:29
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (9)
tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (1)

24-33: Consider the verbosity of wrapper models for simple fields.

The separate BaseModel classes for name, description, and status (each containing only value: Optional[str]) create a deeply nested structure. While this may accurately reflect the GraphQL schema, it adds verbosity.

If these models are auto-generated from the GraphQL schema (as suggested by the fixture location), this is acceptable. However, if manually maintained, consider whether the GraphQL schema could be simplified.

pyproject.toml (1)

49-49: Approve ariadne-codegen dependency

Version ^0.15.2 is available on PyPI and has no known security advisories; consider updating to ^0.15.3 for the latest patch fixes, though 0.15.2 is acceptable.

docs/docs/python-sdk/guides/python-typing.mdx (1)

130-151: Consider enhancing the example workflow with complete code samples.

The workflow steps are clear, but the example would benefit from showing a complete .gql file example and a more detailed usage example with error handling.

Consider adding a concrete .gql file example in step 1:

# queries/get_devices.gql
query GetDevices {
  InfraDevice {
    edges {
      node {
        id
        name {
          value
        }
        status {
          value
        }
      }
    }
  }
}

And enhancing step 3 with error handling:

from .queries.get_devices import GetDevicesQuery
from pydantic import ValidationError

try:
    response = await client.execute_graphql(query=MY_QUERY)
    data = GetDevicesQuery(**response)
    # Access type-safe fields
    for edge in data.infra_device.edges:
        print(f"Device: {edge.node.name.value}")
except ValidationError as e:
    print(f"Invalid response structure: {e}")
tests/unit/sdk/graphql/conftest.py (1)

6-13: Consider consolidating duplicate enum definitions.

The MyStrEnum and MyIntEnum classes are duplicated in both this conftest file and tests/unit/sdk/graphql/test_renderer.py (lines 8-15). To follow DRY principles, consider keeping these enum definitions only in conftest.py and removing them from test_renderer.py since fixtures defined here are already shared across test modules.

Apply this approach:

  1. Keep the enum definitions here in conftest.py
  2. Remove lines 8-15 from test_renderer.py
  3. Import the enums in test_renderer.py if needed: from .conftest import MyStrEnum, MyIntEnum
infrahub_sdk/ctl/graphql.py (4)

38-54: Consider handling non-file, non-directory paths explicitly.

If query_path exists but is neither a regular file nor a directory (e.g., a broken symlink or a special file), the function will return an empty list from glob() without any indication to the user. This may lead to confusion when no files are found.

Apply this diff to add an explicit check:

     if not query_path.is_dir() and query_path.is_file():
         return [query_path]
+    
+    if not query_path.is_dir():
+        raise ValueError(f"{query_path} is neither a file nor a directory")
 
     return list(query_path.glob("**/*.gql"))

57-74: Consider catching parse exceptions for better error messages.

parse(queries_str) on line 66 can raise GraphQLSyntaxError if the query file contains invalid GraphQL syntax. Currently, this exception propagates to the caller. While the caller in generate_return_types does catch generic ValueError, GraphQLSyntaxError is not a subclass of ValueError and will not be caught, potentially causing an unhandled exception.

Apply this diff to catch and re-raise as ValueError for consistent error handling:

     queries_str = queries_path.read_text(encoding="utf-8")
-    queries_ast = parse(queries_str)
+    try:
+        queries_ast = parse(queries_str)
+    except Exception as exc:
+        raise ValueError(f"Failed to parse GraphQL query: {exc}") from exc
     validation_errors = validate(

77-88: Ensure parent directory exists before writing generated files.

Line 87 writes to file_path without verifying that the parent directory exists. If the directory structure has been modified or deleted between discovery and generation, this will raise a FileNotFoundError.

Apply this diff to ensure the directory exists:

     for file_name, module in package._result_types_files.items():
         file_path = directory / file_name
+        file_path.parent.mkdir(parents=True, exist_ok=True)
 
         insert_fragments_inline(module, fragment)

Note on private attribute access:

This function accesses private attributes (_result_types_files, _add_comments_to_code, _generated_files) from PackageGenerator. While this is necessary for the plugin architecture and wrapper functionality, it creates tight coupling with ariadne-codegen internals. Consider adding a comment documenting this dependency or, if ariadne-codegen provides public APIs for these operations in the future, migrating to those.


112-118: Consider clarifying default query path behavior.

The query parameter defaults to Path.cwd() (line 115), meaning the command will search the current working directory for .gql files if no path is provided. This might be unexpected for users who expect an explicit path argument. Consider either:

  1. Making the parameter required (query: Path = typer.Argument(..., help=...)), or
  2. Adding a note in the help text that it defaults to the current directory.

Apply this diff to clarify the default behavior:

     query: Path = typer.Argument(Path.cwd(), help="Location of the GraphQL query file(s)."),
+    # Or make it required:
+    # query: Path = typer.Argument(..., help="Location of the GraphQL query file(s)."),

Update the help text:

-    query: Path = typer.Argument(Path.cwd(), help="Location of the GraphQL query file(s)."),
+    query: Path = typer.Argument(Path.cwd(), help="Location of the GraphQL query file(s). Defaults to current directory."),
infrahub_sdk/graphql/utils.py (1)

24-30: Refactor to avoid modifying list during iteration.

Lines 26-28 remove an item from module.body while iterating over it. Although the immediate return prevents issues in practice, this pattern is fragile and will silently skip subsequent fragment imports if multiple exist.

Apply this diff to handle all fragment imports safely:

 def remove_fragment_import(module: ast.Module) -> ast.Module:
     """Remove the fragment import from the module."""
-    for item in module.body:
-        if isinstance(item, ast.ImportFrom) and item.module == "fragments":
-            module.body.remove(item)
-            return module
-    return module
+    module.body = [
+        item for item in module.body
+        if not (isinstance(item, ast.ImportFrom) and item.module == "fragments")
+    ]
+    return module

Alternatively, if only the first fragment import should be removed (and multiples are invalid), document this explicitly in the docstring.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9ccf10 and 89779f4.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • .vale/styles/Infrahub/branded-terms-case-swap.yml (0 hunks)
  • changelog/+gql-command.added.md (1 hunks)
  • docs/docs/infrahubctl/infrahubctl-graphql.mdx (1 hunks)
  • docs/docs/python-sdk/guides/python-typing.mdx (1 hunks)
  • infrahub_sdk/ctl/cli_commands.py (2 hunks)
  • infrahub_sdk/ctl/graphql.py (1 hunks)
  • infrahub_sdk/graphql/__init__.py (1 hunks)
  • infrahub_sdk/graphql/constants.py (1 hunks)
  • infrahub_sdk/graphql/plugin.py (1 hunks)
  • infrahub_sdk/graphql/query.py (1 hunks)
  • infrahub_sdk/graphql/renderers.py (3 hunks)
  • infrahub_sdk/graphql/utils.py (1 hunks)
  • pyproject.toml (5 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python01.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python02.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (1 hunks)
  • tests/unit/sdk/graphql/conftest.py (1 hunks)
  • tests/unit/sdk/graphql/test_plugin.py (1 hunks)
  • tests/unit/sdk/graphql/test_query.py (1 hunks)
  • tests/unit/sdk/graphql/test_renderer.py (1 hunks)
💤 Files with no reviewable changes (1)
  • .vale/styles/Infrahub/branded-terms-case-swap.yml
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

When implementing Infrahub checks, subclass InfrahubCheck and override validate(data); do not implement or rely on a check() method

Files:

  • tests/fixtures/unit/test_graphql_plugin/python02.py
  • tests/fixtures/unit/test_graphql_plugin/python01.py
  • tests/unit/sdk/graphql/conftest.py
  • infrahub_sdk/graphql/__init__.py
  • infrahub_sdk/graphql/query.py
  • tests/unit/sdk/graphql/test_renderer.py
  • infrahub_sdk/graphql/plugin.py
  • infrahub_sdk/graphql/renderers.py
  • infrahub_sdk/ctl/graphql.py
  • infrahub_sdk/graphql/constants.py
  • infrahub_sdk/graphql/utils.py
  • tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py
  • tests/unit/sdk/graphql/test_plugin.py
  • infrahub_sdk/ctl/cli_commands.py
  • tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py
  • tests/unit/sdk/graphql/test_query.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use the custom pytest plugin (infrahub_sdk.pytest_plugin) fixtures for clients, configuration, and Infrahub-specific testing

Files:

  • tests/fixtures/unit/test_graphql_plugin/python02.py
  • tests/fixtures/unit/test_graphql_plugin/python01.py
  • tests/unit/sdk/graphql/conftest.py
  • tests/unit/sdk/graphql/test_renderer.py
  • tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py
  • tests/unit/sdk/graphql/test_plugin.py
  • tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py
  • tests/unit/sdk/graphql/test_query.py
tests/unit/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Place and write unit tests under tests/unit/ (isolated component tests)

Files:

  • tests/unit/sdk/graphql/conftest.py
  • tests/unit/sdk/graphql/test_renderer.py
  • tests/unit/sdk/graphql/test_plugin.py
  • tests/unit/sdk/graphql/test_query.py
infrahub_sdk/ctl/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

infrahub_sdk/ctl/**/*.py: Build CLI commands with Typer
Organize and keep all CLI commands within infrahub_sdk/ctl/

Files:

  • infrahub_sdk/ctl/graphql.py
  • infrahub_sdk/ctl/cli_commands.py
docs/**/*.{md,mdx}

📄 CodeRabbit inference engine (CLAUDE.md)

docs/**/*.{md,mdx}: Follow the Diataxis framework and classify docs as Tutorials, How-to guides, Explanation, or Reference
Structure How-to guides with required frontmatter and sections: introduction, prerequisites, step-by-step steps, validation, related resources
Structure Topics (Explanation) docs with introduction, concepts & definitions, background & context, architecture & design, connections, further reading
Use a professional, concise, informative tone with consistent structure across documents
Use proper language tags on all code blocks
Include both async and sync examples where applicable using the Tabs component
Add validation steps to guides to confirm success and expected outputs
Use callouts for warnings, tips, and important notes
Define new terms on first use and use domain-relevant terminology consistent with Infrahub’s model/UI
Conform to markdown style rules in .markdownlint.yaml and Vale styles in .vale/styles/

Files:

  • docs/docs/python-sdk/guides/python-typing.mdx
  • docs/docs/infrahubctl/infrahubctl-graphql.mdx
🧠 Learnings (1)
📚 Learning: 2025-08-24T13:35:12.998Z
Learnt from: CR
PR: opsmill/infrahub-sdk-python#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T13:35:12.998Z
Learning: Applies to infrahub_sdk/ctl/**/*.py : Build CLI commands with Typer

Applied to files:

  • infrahub_sdk/ctl/cli_commands.py
🧬 Code graph analysis (11)
tests/fixtures/unit/test_graphql_plugin/python02.py (1)
tests/fixtures/unit/test_graphql_plugin/python01.py (2)
  • CreateDevice (8-9)
  • CreateDeviceInfraDeviceUpsert (12-14)
tests/fixtures/unit/test_graphql_plugin/python01.py (2)
tests/fixtures/unit/test_graphql_plugin/python02.py (2)
  • CreateDevice (6-7)
  • CreateDeviceInfraDeviceUpsert (10-12)
tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (2)
  • CreateDevice (9-10)
  • CreateDeviceInfraDeviceUpsert (13-15)
tests/unit/sdk/graphql/conftest.py (1)
infrahub_sdk/protocols_base.py (1)
  • Enum (111-112)
infrahub_sdk/graphql/__init__.py (2)
infrahub_sdk/graphql/query.py (2)
  • Mutation (46-77)
  • Query (31-43)
infrahub_sdk/graphql/renderers.py (3)
  • render_input_block (152-212)
  • render_query_block (83-149)
  • render_variables_to_string (69-80)
infrahub_sdk/graphql/query.py (1)
infrahub_sdk/graphql/renderers.py (3)
  • render_input_block (152-212)
  • render_query_block (83-149)
  • render_variables_to_string (69-80)
tests/unit/sdk/graphql/test_renderer.py (1)
infrahub_sdk/graphql/renderers.py (2)
  • render_input_block (152-212)
  • render_query_block (83-149)
infrahub_sdk/ctl/graphql.py (4)
infrahub_sdk/async_typer.py (2)
  • AsyncTyper (11-31)
  • command (29-31)
infrahub_sdk/ctl/client.py (1)
  • initialize_client (10-25)
infrahub_sdk/ctl/utils.py (1)
  • catch_exception (78-106)
infrahub_sdk/graphql/utils.py (2)
  • insert_fragments_inline (13-21)
  • remove_fragment_import (24-30)
tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (1)
tests/fixtures/unit/test_graphql_plugin/python02.py (2)
  • CreateDevice (6-7)
  • CreateDeviceInfraDeviceUpsert (10-12)
tests/unit/sdk/graphql/test_plugin.py (1)
infrahub_sdk/graphql/plugin.py (4)
  • FutureAnnotationPlugin (12-28)
  • generate_result_types_module (23-28)
  • generate_result_types_module (56-62)
  • generate_result_types_module (81-86)
tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (3)
tests/fixtures/unit/test_graphql_plugin/python01.py (2)
  • CreateDevice (8-9)
  • CreateDeviceInfraDeviceUpsert (12-14)
tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (2)
  • CreateDevice (8-9)
  • CreateDeviceInfraDeviceUpsert (12-14)
tests/fixtures/unit/test_graphql_plugin/python02.py (2)
  • CreateDevice (6-7)
  • CreateDeviceInfraDeviceUpsert (10-12)
tests/unit/sdk/graphql/test_query.py (1)
infrahub_sdk/graphql/query.py (2)
  • Mutation (46-77)
  • Query (31-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: unit-tests (3.9)
  • GitHub Check: unit-tests (3.13)
  • GitHub Check: unit-tests (3.12)
  • GitHub Check: unit-tests (3.11)
  • GitHub Check: integration-tests-latest-infrahub
  • GitHub Check: unit-tests (3.10)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (35)
pyproject.toml (2)

73-73: LGTM! ariadne-codegen properly added to extras.

The dependency is correctly included in both ctl and all extras, making it available for CLI GraphQL codegen functionality.

Also applies to: 86-86


135-135: LGTM! Test fixture exclusions are appropriate.

Excluding the generated test fixtures from Ruff and suppressing FA100 (future annotations) for these files makes sense, as they represent expected output from the codegen process and don't need to follow all project conventions.

Also applies to: 281-283

tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (4)

1-6: LGTM! Imports are correct for Pydantic v2 with forward references.

The use of from __future__ import annotations enables forward reference strings without quotes, which is appropriate for the nested model structure.


8-9: LGTM! Field alias matches GraphQL convention.

The alias "InfraDeviceUpsert" correctly maps the Pythonic snake_case field name to the GraphQL PascalCase field name.


36-38: LGTM! model_rebuild() calls are necessary for forward references.

In Pydantic v2, when using forward references (even with from __future__ import annotations), calling model_rebuild() after all referenced models are defined ensures proper field resolution.


1-39: No changes needed for python01 fixtures The identical before/after fixtures correctly test idempotency of the FutureAnnotationPlugin.

tests/fixtures/unit/test_graphql_plugin/python01.py (1)

1-39: LGTM with caveat! Model structure is well-formed.

The Pydantic models correctly represent a nested GraphQL response structure with:

  • Proper use of from __future__ import annotations
  • Appropriate Field aliases for GraphQL naming conventions
  • Correct model_rebuild() calls for forward reference resolution

However, this file appears identical to python01_after_annotation.py. See the verification request in that file's review regarding the intended relationship between these fixtures.

changelog/+gql-command.added.md (1)

1-1: LGTM! Clear and concise changelog entry.

The changelog entry accurately describes the new feature and follows the expected format.

infrahub_sdk/graphql/constants.py (1)

1-1: LGTM! Well-structured type mapping constant.

The mapping correctly defines Python-to-GraphQL non-null type conversions for common scalar types. Centralizing this in a constants module improves maintainability.

infrahub_sdk/ctl/cli_commands.py (2)

29-29: LGTM! Import follows established pattern.

The import of graphql_app is consistent with other CLI subcommand imports in this file.


67-67: LGTM! CLI registration follows established pattern.

The registration of the graphql command group follows the same pattern as other CLI subcommands in this file. As per coding guidelines, CLI commands are properly organized using Typer in infrahub_sdk/ctl/.

Based on learnings

docs/docs/python-sdk/guides/python-typing.mdx (1)

105-128: LGTM! Clear introduction and rationale for Pydantic model generation.

The section provides a strong rationale for generating Pydantic models from GraphQL queries, covering type safety, IDE support, documentation, and validation. The command examples are clear and include both current directory and specific file usage patterns.

docs/docs/infrahubctl/infrahubctl-graphql.mdx (2)

1-21: LGTM! Clear command group overview.

The main command group documentation follows the standard CLI reference format with proper usage block, options, and subcommand list.


22-36: LGTM! Clear export-schema documentation.

The export-schema command documentation is complete and follows the standard reference format with usage, options, and defaults clearly specified.

tests/unit/sdk/graphql/test_plugin.py (3)

14-36: LGTM! Fixture setup is clean and properly typed.

The fixtures correctly load test resources using get_fixtures_dir() and appropriate encoding. Return type annotations are present.


38-48: LGTM! Test correctly verifies idempotency of the plugin.

The test appropriately checks that the plugin doesn't duplicate the __future__ import when already present. The operation_definition=None argument aligns with the plugin's signature (the parameter is intentionally unused in the implementation).


51-62: LGTM! Test correctly verifies annotation insertion.

The test appropriately validates that the plugin inserts the __future__ import when it's missing. Together with the first test, this provides good coverage of both plugin branches.

infrahub_sdk/graphql/__init__.py (1)

1-12: LGTM! Clean public API organization.

The module properly exposes the GraphQL utilities through explicit imports and __all__ declaration, making the public interface clear.

tests/unit/sdk/graphql/conftest.py (1)

16-113: LGTM! Well-structured test fixtures.

The fixtures provide comprehensive test data covering various GraphQL scenarios (no filters, aliases, fragments, different filter types). The naming is clear and the data structures properly represent the different test cases.

infrahub_sdk/graphql/renderers.py (5)

9-9: LGTM! Good separation of concerns.

Moving VARIABLE_TYPE_MAPPING to a constants module improves code organization.


12-66: LGTM! Comprehensive value conversion with excellent documentation.

The convert_to_graphql_as_string function properly handles various Python types (None, str, bool, Enum, list, BaseModel) and converts them to GraphQL string representations. The docstring provides clear examples and the implementation correctly uses json.dumps() for string escaping.


69-80: LGTM! Clean variable rendering implementation.

The function properly maps Python types to GraphQL variable type strings using the imported VARIABLE_TYPE_MAPPING.


83-149: LGTM! Well-documented query block renderer.

The render_query_block function correctly handles:

  • Special GraphQL keys (@filters, @alias)
  • Nested structures with proper indentation
  • Recursive rendering for complex queries

The comprehensive docstring with examples makes the function's behavior clear.


152-212: LGTM! Robust input block renderer.

The render_input_block function properly handles:

  • Nested dictionaries with recursive rendering
  • Lists with mixed item types (dicts and primitives)
  • Proper indentation at all nesting levels

The docstring clearly documents the differences from render_query_block (no special keys like @filters or @alias).

tests/unit/sdk/graphql/test_renderer.py (1)

104-245: LGTM! Comprehensive test coverage.

The test functions thoroughly validate:

  • Query block rendering with and without filters/aliases/fragments
  • Input block rendering with nested structures and lists
  • Different indentation levels (offset=2, indentation=2)
  • Line-by-line assertion of expected output

The tests provide excellent coverage of the rendering functions.

infrahub_sdk/graphql/query.py (3)

8-28: LGTM! Clean base class design.

BaseGraphQLQuery provides a solid foundation with:

  • Type-safe attributes (query_type, indentation)
  • Flexible constructor accepting query, variables, and name
  • Reusable render_first_line() method that properly formats the query opening with variables

31-43: LGTM! Clean Query class implementation.

The Query class properly:

  • Sets query_type = "query"
  • Delegates rendering to render_query_block with appropriate parameters
  • Supports optional convert_enum parameter
  • Returns properly formatted GraphQL query string

46-77: LGTM! Well-structured Mutation class.

The Mutation class correctly:

  • Extends BaseGraphQLQuery with mutation-specific attributes (mutation, input_data)
  • Renders both input block (mutation arguments) and query block (return fields)
  • Uses proper indentation and formatting
  • Supports convert_enum parameter throughout
infrahub_sdk/graphql/plugin.py (2)

31-62: LGTM! Type hint normalization is correctly implemented.

The recursive traversal of Subscript nodes correctly handles nested List annotations (e.g., List[List[X]]list[list[X]]) and applies the transformation to all annotated assignments within class definitions. The integration with FutureAnnotationPlugin.insert_future_annotation ensures from __future__ import annotations is present before normalizing type hints.


65-86: LGTM! BaseModel import replacement is correctly implemented.

The plugin correctly locates the from base_model import BaseModel import generated by ariadne-codegen and replaces it with from pydantic import BaseModel. The error handling for missing imports is appropriate, and the AST transformation is accurate.

tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (1)

1-20: LGTM! Fixture correctly demonstrates Pydantic v2 forward reference handling.

This test fixture appropriately uses from __future__ import annotations with string-quoted forward references and model_rebuild() calls to verify the FutureAnnotationPlugin behavior. The pattern aligns with Pydantic v2 best practices for resolving forward references.

tests/fixtures/unit/test_graphql_plugin/python02.py (1)

1-17: LGTM! Fixture correctly demonstrates standard forward reference pattern.

This test fixture uses standard string-quoted forward references without from __future__ import annotations, with appropriate model_rebuild() calls. This provides the baseline for testing FutureAnnotationPlugin transformations.

tests/unit/sdk/graphql/test_query.py (2)

3-3: LGTM! Import path updated to reflect module reorganization.

The updated import from infrahub_sdk.graphql.query aligns with the PR's GraphQL module refactoring. The Query and Mutation classes maintain the same API, ensuring test compatibility.

Based on coding guidelines.


16-291: LGTM! Comprehensive test coverage for Query and Mutation rendering.

The test suite thoroughly covers various scenarios: no variables, empty filters, filters with variables, enum handling, and mutation rendering. Tests follow pytest best practices with clear naming and explicit assertions.

Based on coding guidelines.

infrahub_sdk/graphql/utils.py (1)

4-10: LGTM! Clean sentinel pattern for class lookup.

The function correctly identifies the first class definition and returns a clear sentinel value (-1) when none exists. The implementation is straightforward and well-documented.

Comment on lines 8 to 15
class MyStrEnum(str, Enum):
VALUE1 = "value1"
VALUE2 = "value2"


class MyIntEnum(int, Enum):
VALUE1 = 12
VALUE2 = 24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate enum definitions.

These enum classes are already defined in tests/unit/sdk/graphql/conftest.py (lines 6-13). Remove the duplicates here and import from conftest if needed.

-class MyStrEnum(str, Enum):
-    VALUE1 = "value1"
-    VALUE2 = "value2"
-
-
-class MyIntEnum(int, Enum):
-    VALUE1 = 12
-    VALUE2 = 24

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In tests/unit/sdk/graphql/test_renderer.py around lines 8 to 15, duplicate enum
classes MyStrEnum and MyIntEnum are defined but already exist in
tests/unit/sdk/graphql/conftest.py (lines 6-13); remove these class definitions
from this file and instead import the enums from conftest (e.g., add from
.conftest import MyStrEnum, MyIntEnum at top) or rely on pytest
fixtures/namespace as appropriate so tests use the single shared definitions.

Comment on lines 18 to 102
@pytest.fixture
def query_data_no_filter():
data = {
"device": {
"name": {"value": None},
"description": {"value": None},
"interfaces": {"name": {"value": None}},
}
}

return data


@pytest.fixture
def query_data_alias():
data = {
"device": {
"name": {"@alias": "new_name", "value": None},
"description": {"value": {"@alias": "myvalue"}},
"interfaces": {"@alias": "myinterfaces", "name": {"value": None}},
}
}

return data


@pytest.fixture
def query_data_fragment():
data = {
"device": {
"name": {"value": None},
"...on Builtin": {
"description": {"value": None},
"interfaces": {"name": {"value": None}},
},
}
}

return data


@pytest.fixture
def query_data_empty_filter():
data = {
"device": {
"@filters": {},
"name": {"value": None},
"description": {"value": None},
"interfaces": {"name": {"value": None}},
}
}

return data


@pytest.fixture
def query_data_filters_01():
data = {
"device": {
"@filters": {"name__value": "$name"},
"name": {"value": None},
"description": {"value": None},
"interfaces": {
"@filters": {"enabled__value": "$enabled"},
"name": {"value": None},
},
}
}
return data


@pytest.fixture
def query_data_filters_02():
data = {
"device": {
"@filters": {"name__value": "myname", "integer__value": 44, "enumstr__value": MyStrEnum.VALUE2},
"name": {"value": None},
"interfaces": {
"@filters": {"enabled__value": True, "enumint__value": MyIntEnum.VALUE1},
"name": {"value": None},
},
}
}
return data

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate fixture definitions.

All these fixtures (query_data_no_filter, query_data_alias, query_data_fragment, query_data_empty_filter, query_data_filters_01, query_data_filters_02, input_data_01) are already defined in tests/unit/sdk/graphql/conftest.py (lines 16-113). Pytest automatically discovers and makes conftest fixtures available to test modules, so these duplicates should be removed.

-@pytest.fixture
-def query_data_no_filter():
-    data = {
-        "device": {
-            "name": {"value": None},
-            "description": {"value": None},
-            "interfaces": {"name": {"value": None}},
-        }
-    }
-
-    return data
-
-
-@pytest.fixture
-def query_data_alias():
-    data = {
-        "device": {
-            "name": {"@alias": "new_name", "value": None},
-            "description": {"value": {"@alias": "myvalue"}},
-            "interfaces": {"@alias": "myinterfaces", "name": {"value": None}},
-        }
-    }
-
-    return data
-
-
-@pytest.fixture
-def query_data_fragment():
-    data = {
-        "device": {
-            "name": {"value": None},
-            "...on Builtin": {
-                "description": {"value": None},
-                "interfaces": {"name": {"value": None}},
-            },
-        }
-    }
-
-    return data
-
-
-@pytest.fixture
-def query_data_empty_filter():
-    data = {
-        "device": {
-            "@filters": {},
-            "name": {"value": None},
-            "description": {"value": None},
-            "interfaces": {"name": {"value": None}},
-        }
-    }
-
-    return data
-
-
-@pytest.fixture
-def query_data_filters_01():
-    data = {
-        "device": {
-            "@filters": {"name__value": "$name"},
-            "name": {"value": None},
-            "description": {"value": None},
-            "interfaces": {
-                "@filters": {"enabled__value": "$enabled"},
-                "name": {"value": None},
-            },
-        }
-    }
-    return data
-
-
-@pytest.fixture
-def query_data_filters_02():
-    data = {
-        "device": {
-            "@filters": {"name__value": "myname", "integer__value": 44, "enumstr__value": MyStrEnum.VALUE2},
-            "name": {"value": None},
-            "interfaces": {
-                "@filters": {"enabled__value": True, "enumint__value": MyIntEnum.VALUE1},
-                "name": {"value": None},
-            },
-        }
-    }
-    return data
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture
def query_data_no_filter():
data = {
"device": {
"name": {"value": None},
"description": {"value": None},
"interfaces": {"name": {"value": None}},
}
}
return data
@pytest.fixture
def query_data_alias():
data = {
"device": {
"name": {"@alias": "new_name", "value": None},
"description": {"value": {"@alias": "myvalue"}},
"interfaces": {"@alias": "myinterfaces", "name": {"value": None}},
}
}
return data
@pytest.fixture
def query_data_fragment():
data = {
"device": {
"name": {"value": None},
"...on Builtin": {
"description": {"value": None},
"interfaces": {"name": {"value": None}},
},
}
}
return data
@pytest.fixture
def query_data_empty_filter():
data = {
"device": {
"@filters": {},
"name": {"value": None},
"description": {"value": None},
"interfaces": {"name": {"value": None}},
}
}
return data
@pytest.fixture
def query_data_filters_01():
data = {
"device": {
"@filters": {"name__value": "$name"},
"name": {"value": None},
"description": {"value": None},
"interfaces": {
"@filters": {"enabled__value": "$enabled"},
"name": {"value": None},
},
}
}
return data
@pytest.fixture
def query_data_filters_02():
data = {
"device": {
"@filters": {"name__value": "myname", "integer__value": 44, "enumstr__value": MyStrEnum.VALUE2},
"name": {"value": None},
"interfaces": {
"@filters": {"enabled__value": True, "enumint__value": MyIntEnum.VALUE1},
"name": {"value": None},
},
}
}
return data
🤖 Prompt for AI Agents
In tests/unit/sdk/graphql/test_renderer.py around lines 18 to 102, there are
duplicate pytest fixture definitions (query_data_no_filter, query_data_alias,
query_data_fragment, query_data_empty_filter, query_data_filters_01,
query_data_filters_02, and input_data_01) that are already provided by
tests/unit/sdk/graphql/conftest.py; remove these fixture definitions from this
test file so the tests use the conftest-provided fixtures instead, ensuring no
local redefinitions remain and that any references in this file rely on the
conftest fixtures.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
infrahub_sdk/ctl/graphql.py (2)

98-110: HTTP response validation is still missing.

Line 107-108 still writes response.text directly to the schema file without validating the HTTP status code. If the server returns an error response (e.g., 404, 500), the error body will be written to the schema file, corrupting it and causing downstream failures in generate_return_types.

Apply this diff to validate the response:

     client = initialize_client()
     response = await client._get(url=f"{client.address}/schema.graphql")
+    response.raise_for_status()
     destination.write_text(response.text)
     console.print(f"[green]Schema exported to {destination}")

Note: Line 107 uses the private method client._get(). If a public method becomes available, prefer that to avoid coupling to internal implementation details.


141-173: Inconsistent logging and partial generation issues remain unresolved.

Two issues persist in this loop:

  1. Inconsistent logging: Line 146 still uses print() while lines 167 and 173 use console.print(), creating inconsistent output formatting.

  2. Partial generation after parsing error: Lines 163-173 catch ParsingError from add_operation and continue processing. Lines 168-173 proceed to generate fragments and write files even when operations failed to parse, resulting in incomplete or incorrect generated types. Success messages are still printed despite the error.

Apply this diff to fix both issues:

             except ValueError as exc:
-                print(f"Error generating result types for {gql_file}: {exc}")
+                console.print(f"[red]Error generating result types for {gql_file}: {exc}")
                 continue
             queries = filter_operations_definitions(definitions)
             fragments = filter_fragments_definitions(definitions)
 
             package_generator = get_package_generator(
                 schema=graphql_schema,
                 fragments=fragments,
                 settings=ClientSettings(
                     schema_path=str(schema),
                     target_package_name=directory.name,
                     queries_path=str(directory),
                     include_comments=CommentsStrategy.NONE,
                 ),
                 plugin_manager=plugin_manager,
             )
 
+            parsing_failed = False
             try:
                 for query_operation in queries:
                     package_generator.add_operation(query_operation)
             except ParsingError as exc:
                 console.print(f"[red]Unable to process {gql_file.name}: {exc}")
+                parsing_failed = True
+            
+            if parsing_failed:
+                continue
+            
             module_fragment = package_generator.fragments_generator.generate()
 
             generate_result_types(directory=directory, package=package_generator, fragment=module_fragment)
 
             for file_name in package_generator._result_types_files.keys():
                 console.print(f"[green]Generated {file_name} in {directory}")
🧹 Nitpick comments (1)
infrahub_sdk/ctl/graphql.py (1)

77-89: Consider using public APIs instead of private attributes.

This function accesses multiple private attributes and methods from PackageGenerator (_result_types_files, _add_comments_to_code, _generated_files). While this may be necessary given the current ariadne-codegen API, it creates tight coupling and risks breakage if the library changes its internals.

If public APIs are available or can be requested from the library maintainers, prefer those. Otherwise, document this dependency and consider wrapping this logic in a way that isolates the private-attribute access.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89779f4 and 448eac4.

📒 Files selected for processing (2)
  • docs/docs/infrahubctl/infrahubctl-graphql.mdx (1 hunks)
  • infrahub_sdk/ctl/graphql.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/infrahubctl/infrahubctl-graphql.mdx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

When implementing Infrahub checks, subclass InfrahubCheck and override validate(data); do not implement or rely on a check() method

Files:

  • infrahub_sdk/ctl/graphql.py
infrahub_sdk/ctl/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

infrahub_sdk/ctl/**/*.py: Build CLI commands with Typer
Organize and keep all CLI commands within infrahub_sdk/ctl/

Files:

  • infrahub_sdk/ctl/graphql.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
PR: opsmill/infrahub-sdk-python#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T13:35:12.998Z
Learning: Applies to infrahub_sdk/ctl/**/*.py : Organize and keep all CLI commands within infrahub_sdk/ctl/
🧬 Code graph analysis (1)
infrahub_sdk/ctl/graphql.py (4)
infrahub_sdk/async_typer.py (2)
  • AsyncTyper (11-31)
  • command (29-31)
infrahub_sdk/ctl/client.py (1)
  • initialize_client (10-25)
infrahub_sdk/ctl/utils.py (1)
  • catch_exception (78-106)
infrahub_sdk/graphql/utils.py (2)
  • insert_fragments_inline (13-21)
  • remove_fragment_import (24-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: unit-tests (3.12)
  • GitHub Check: unit-tests (3.13)
  • GitHub Check: unit-tests (3.11)
  • GitHub Check: unit-tests (3.9)
  • GitHub Check: unit-tests (3.10)
  • GitHub Check: integration-tests-latest-infrahub
  • GitHub Check: documentation
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (4)
infrahub_sdk/ctl/graphql.py (4)

1-36: LGTM!

The imports are well-organized and all dependencies appear necessary for the CLI functionality. The plugin configuration in ARIADNE_PLUGINS is appropriate for controlling the code generation pipeline.


38-54: LGTM!

The function correctly handles both single files and directories, with appropriate validation and error handling. The recursive glob pattern ensures all .gql files are discovered.


57-74: LGTM!

The function properly validates the query file and performs schema validation. Excluding NoUnusedFragmentsRule is appropriate when fragments may be shared across files. Error messages are clear and helpful.


91-96: LGTM!

Standard Typer callback pattern for the CLI group.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
infrahub_sdk/ctl/graphql.py (2)

107-110: Unaddressed: Validate HTTP response before writing schema file.

This is a duplicate of a previous review comment that has not been addressed. Lines 107-109 still fetch the schema and write response.text directly to the file without checking the HTTP status code. If the server returns an error (e.g., 404, 500), the error response body will be written to the schema file, resulting in an invalid schema that will cause downstream failures in generate_return_types.

Apply this diff to validate the response and ensure the parent directory exists:

     client = initialize_client()
     response = await client._get(url=f"{client.address}/schema.graphql")
+    if response.status_code != 200:
+        raise ValueError(f"Failed to fetch schema: HTTP {response.status_code} - {response.text}")
+    destination.parent.mkdir(parents=True, exist_ok=True)
     destination.write_text(response.text)
     console.print(f"[green]Schema exported to {destination}")

Note on private method access: Line 107 calls client._get(), which is a private method. Consider documenting this dependency or requesting a public method from the InfrahubClient maintainers.


144-174: Unaddressed: Inconsistent logging and partial generation after parsing error.

This is a duplicate of a previous review comment that has not been addressed. Two issues remain:

  1. Inconsistent logging (line 147): Uses print() instead of console.print(), creating inconsistent output formatting compared to lines 168 and 174.

  2. Partial generation after parsing error (lines 167-169): The code catches ParsingError from add_operation and logs it, but then continues to lines 169-171 to generate fragments and write files. If some operations failed to parse, the generated types may be incomplete or incorrect, yet success messages are still printed (line 174). This creates a confusing user experience where errors are shown but the command reports success.

Apply this diff to fix both issues:

             except ValueError as exc:
-                print(f"Error generating result types for {gql_file}: {exc}")
+                console.print(f"[red]Error generating result types for {gql_file}: {exc}")
                 continue
             queries = filter_operations_definitions(definitions)
             fragments = filter_fragments_definitions(definitions)
 
             package_generator = get_package_generator(
                 schema=graphql_schema,
                 fragments=fragments,
                 settings=ClientSettings(
                     schema_path=str(schema),
                     target_package_name=directory.name,
                     queries_path=str(directory),
                     include_comments=CommentsStrategy.NONE,
                 ),
                 plugin_manager=plugin_manager,
             )
 
+            parsing_failed = False
             try:
                 for query_operation in queries:
                     package_generator.add_operation(query_operation)
             except ParsingError as exc:
                 console.print(f"[red]Unable to process {gql_file.name}: {exc}")
+                parsing_failed = True
+            
+            if parsing_failed:
+                continue
+            
             module_fragment = package_generator.fragments_generator.generate()
 
             generate_result_types(directory=directory, package=package_generator, fragment=module_fragment)
 
             for file_name in package_generator._result_types_files.keys():
                 console.print(f"[green]Generated {file_name} in {directory}")
🧹 Nitpick comments (2)
infrahub_sdk/ctl/graphql.py (2)

52-53: Simplify the file check condition.

The condition not query_path.is_dir() and query_path.is_file() is redundant. If a path is a file, it's inherently not a directory.

Apply this diff:

-    if not query_path.is_dir() and query_path.is_file():
+    if query_path.is_file():
         return [query_path]

78-90: Document ariadne-codegen private API usage

This function relies on internal PackageGenerator attributes (_result_types_files, _generated_files) and method (_add_comments_to_code). Add a comment explaining this dependency and open an issue/PR against ariadne-codegen to expose public APIs for:

  • Iterating result-type files
  • Inserting file comments (use include_comments config or get_file_comment hook)
  • Tracking generated files
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 448eac4 and c2cf606.

📒 Files selected for processing (1)
  • infrahub_sdk/ctl/graphql.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

When implementing Infrahub checks, subclass InfrahubCheck and override validate(data); do not implement or rely on a check() method

Files:

  • infrahub_sdk/ctl/graphql.py
infrahub_sdk/ctl/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

infrahub_sdk/ctl/**/*.py: Build CLI commands with Typer
Organize and keep all CLI commands within infrahub_sdk/ctl/

Files:

  • infrahub_sdk/ctl/graphql.py
🧬 Code graph analysis (1)
infrahub_sdk/ctl/graphql.py (5)
infrahub_sdk/async_typer.py (2)
  • AsyncTyper (11-31)
  • command (29-31)
infrahub_sdk/ctl/client.py (1)
  • initialize_client (10-25)
infrahub_sdk/ctl/utils.py (1)
  • catch_exception (78-106)
infrahub_sdk/graphql/utils.py (2)
  • insert_fragments_inline (13-21)
  • remove_fragment_import (24-30)
tests/unit/sdk/graphql/test_plugin.py (1)
  • graphql_schema (15-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: unit-tests (3.11)
  • GitHub Check: unit-tests (3.13)
  • GitHub Check: unit-tests (3.12)
  • GitHub Check: unit-tests (3.10)
  • GitHub Check: unit-tests (3.9)
  • GitHub Check: integration-tests-latest-infrahub
  • GitHub Check: documentation
  • GitHub Check: Cloudflare Pages

Copy link
Contributor

@ogenstad ogenstad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments, but great addition!


Generated Pydantic models from GraphQL queries offer several important benefits:

- **Type Safety**: Catch type errors at development time instead of runtime
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change:

- Catch type errors at development time instead of runtime
+ Catch type errors during development time instead of at runtime

)
if validation_errors:
raise ValueError("\n\n".join(error.message for error in validation_errors))
return queries_ast.definitions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we don't support it yet within the SDK this would be problematic with fragments spread out in multiple files. Not an issue for this PR, just highlighting if we want to do something different with regards to the API of the command because of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree we'll need to revisit this part of the code to improve the support for Fragments

"""Export the GraphQL schema to a file."""

client = initialize_client()
response = await client._get(url=f"{client.address}/schema.graphql")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest that we move this and avoid calling client._get from the CTL. Instead I's suggest that we add some client.schema.get_graphql_schema()

Do we need a branch parameter here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, done

for query_operation in queries:
package_generator.add_operation(query_operation)
except ParsingError as exc:
console.print(f"[red]Unable to process {gql_file.name}: {exc}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we ignore this error and the one above aside from displaying output. Do we want the command to fail here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, update the logic to ignore failing files and continue

class Mutation(BaseGraphQLQuery):
query_type = "mutation"

def __init__(self, *args: Any, mutation: str, input_data: dict, **kwargs: Any):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realise this is just moved code but I think we should remove*args: Any and also **kwargs: Any) to make this class easier to use..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's best to do that in a separate PR to avoid adding unexpected issues

@dgarros dgarros force-pushed the dga-20250913-graphql-return-types branch from c2cf606 to dabcbef Compare October 11, 2025 12:41
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
infrahub_sdk/schema/__init__.py (1)

719-736: Same branch parameter issue as async variant.

This sync version has the identical issue: the branch parameter is set but never used in the URL construction. Apply the same fix as suggested for the async version at lines 477-494.

infrahub_sdk/graphql/query.py (1)

49-52: Simplify constructor signature.

The use of *args and **kwargs obscures the required parameters and makes the API harder to discover and use. Consider making the signature explicit to improve clarity and IDE support.

Apply this diff to make the parameters explicit:

-    def __init__(self, *args: Any, mutation: str, input_data: dict, **kwargs: Any):
+    def __init__(self, mutation: str, input_data: dict, query: dict, variables: dict | None = None, name: str | None = None):
         self.input_data = input_data
         self.mutation = mutation
-        super().__init__(*args, **kwargs)
+        super().__init__(query=query, variables=variables, name=name)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2cf606 and dabcbef.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • .vale/styles/Infrahub/branded-terms-case-swap.yml (0 hunks)
  • changelog/+gql-command.added.md (1 hunks)
  • docs/docs/infrahubctl/infrahubctl-graphql.mdx (1 hunks)
  • docs/docs/python-sdk/guides/python-typing.mdx (1 hunks)
  • infrahub_sdk/ctl/cli_commands.py (2 hunks)
  • infrahub_sdk/ctl/graphql.py (1 hunks)
  • infrahub_sdk/graphql/__init__.py (1 hunks)
  • infrahub_sdk/graphql/constants.py (1 hunks)
  • infrahub_sdk/graphql/plugin.py (1 hunks)
  • infrahub_sdk/graphql/query.py (1 hunks)
  • infrahub_sdk/graphql/renderers.py (3 hunks)
  • infrahub_sdk/graphql/utils.py (1 hunks)
  • infrahub_sdk/schema/__init__.py (2 hunks)
  • pyproject.toml (5 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python01.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python02.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (1 hunks)
  • tests/unit/sdk/graphql/conftest.py (1 hunks)
  • tests/unit/sdk/graphql/test_plugin.py (1 hunks)
  • tests/unit/sdk/graphql/test_query.py (1 hunks)
  • tests/unit/sdk/graphql/test_renderer.py (1 hunks)
💤 Files with no reviewable changes (1)
  • .vale/styles/Infrahub/branded-terms-case-swap.yml
✅ Files skipped from review due to trivial changes (1)
  • docs/docs/infrahubctl/infrahubctl-graphql.mdx
🚧 Files skipped from review as they are similar to previous changes (11)
  • tests/fixtures/unit/test_graphql_plugin/python02.py
  • infrahub_sdk/graphql/constants.py
  • tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py
  • infrahub_sdk/graphql/renderers.py
  • infrahub_sdk/graphql/init.py
  • infrahub_sdk/ctl/graphql.py
  • tests/unit/sdk/graphql/conftest.py
  • infrahub_sdk/graphql/utils.py
  • pyproject.toml
  • tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py
  • changelog/+gql-command.added.md
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

When implementing Infrahub checks, subclass InfrahubCheck and override validate(data); do not implement or rely on a check() method

Files:

  • infrahub_sdk/ctl/cli_commands.py
  • tests/unit/sdk/graphql/test_plugin.py
  • tests/fixtures/unit/test_graphql_plugin/python01.py
  • infrahub_sdk/graphql/query.py
  • infrahub_sdk/graphql/plugin.py
  • infrahub_sdk/schema/__init__.py
  • tests/unit/sdk/graphql/test_query.py
  • tests/unit/sdk/graphql/test_renderer.py
infrahub_sdk/ctl/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

infrahub_sdk/ctl/**/*.py: Build CLI commands with Typer
Organize and keep all CLI commands within infrahub_sdk/ctl/

Files:

  • infrahub_sdk/ctl/cli_commands.py
tests/unit/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Place and write unit tests under tests/unit/ (isolated component tests)

Files:

  • tests/unit/sdk/graphql/test_plugin.py
  • tests/unit/sdk/graphql/test_query.py
  • tests/unit/sdk/graphql/test_renderer.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use the custom pytest plugin (infrahub_sdk.pytest_plugin) fixtures for clients, configuration, and Infrahub-specific testing

Files:

  • tests/unit/sdk/graphql/test_plugin.py
  • tests/fixtures/unit/test_graphql_plugin/python01.py
  • tests/unit/sdk/graphql/test_query.py
  • tests/unit/sdk/graphql/test_renderer.py
docs/**/*.{md,mdx}

📄 CodeRabbit inference engine (CLAUDE.md)

docs/**/*.{md,mdx}: Follow the Diataxis framework and classify docs as Tutorials, How-to guides, Explanation, or Reference
Structure How-to guides with required frontmatter and sections: introduction, prerequisites, step-by-step steps, validation, related resources
Structure Topics (Explanation) docs with introduction, concepts & definitions, background & context, architecture & design, connections, further reading
Use a professional, concise, informative tone with consistent structure across documents
Use proper language tags on all code blocks
Include both async and sync examples where applicable using the Tabs component
Add validation steps to guides to confirm success and expected outputs
Use callouts for warnings, tips, and important notes
Define new terms on first use and use domain-relevant terminology consistent with Infrahub’s model/UI
Conform to markdown style rules in .markdownlint.yaml and Vale styles in .vale/styles/

Files:

  • docs/docs/python-sdk/guides/python-typing.mdx
🧠 Learnings (1)
📚 Learning: 2025-08-24T13:35:12.998Z
Learnt from: CR
PR: opsmill/infrahub-sdk-python#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T13:35:12.998Z
Learning: Applies to infrahub_sdk/ctl/**/*.py : Build CLI commands with Typer

Applied to files:

  • infrahub_sdk/ctl/cli_commands.py
🧬 Code graph analysis (6)
tests/unit/sdk/graphql/test_plugin.py (1)
infrahub_sdk/graphql/plugin.py (4)
  • FutureAnnotationPlugin (12-29)
  • generate_result_types_module (24-29)
  • generate_result_types_module (57-63)
  • generate_result_types_module (80-85)
tests/fixtures/unit/test_graphql_plugin/python01.py (3)
tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (6)
  • CreateDevice (8-9)
  • CreateDeviceInfraDeviceUpsert (12-14)
  • CreateDeviceInfraDeviceUpsertObject (17-21)
  • CreateDeviceInfraDeviceUpsertObjectName (24-25)
  • CreateDeviceInfraDeviceUpsertObjectDescription (28-29)
  • CreateDeviceInfraDeviceUpsertObjectStatus (32-33)
tests/fixtures/unit/test_graphql_plugin/python02.py (2)
  • CreateDevice (6-7)
  • CreateDeviceInfraDeviceUpsert (10-12)
tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (2)
  • CreateDevice (9-10)
  • CreateDeviceInfraDeviceUpsert (13-15)
infrahub_sdk/graphql/query.py (1)
infrahub_sdk/graphql/renderers.py (3)
  • render_input_block (152-212)
  • render_query_block (83-149)
  • render_variables_to_string (69-80)
infrahub_sdk/schema/__init__.py (2)
tests/unit/sdk/conftest.py (1)
  • client (32-33)
infrahub_sdk/client.py (2)
  • _get (1037-1055)
  • _get (2848-2866)
tests/unit/sdk/graphql/test_query.py (1)
infrahub_sdk/graphql/query.py (2)
  • Mutation (46-77)
  • Query (31-43)
tests/unit/sdk/graphql/test_renderer.py (2)
infrahub_sdk/graphql/renderers.py (2)
  • render_input_block (152-212)
  • render_query_block (83-149)
tests/unit/sdk/graphql/conftest.py (4)
  • query_data_no_filter (17-26)
  • query_data_alias (30-39)
  • query_data_fragment (43-54)
  • input_data_01 (103-113)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: unit-tests (3.13)
  • GitHub Check: unit-tests (3.12)
  • GitHub Check: integration-tests-latest-infrahub
  • GitHub Check: unit-tests (3.11)
  • GitHub Check: unit-tests (3.10)
  • GitHub Check: unit-tests (3.9)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (16)
infrahub_sdk/ctl/cli_commands.py (1)

29-29: LGTM! GraphQL CLI group integration follows established patterns.

The import and registration of the new GraphQL command group are correctly implemented and consistent with existing sub-app integrations (branch, schema, repository, etc.).

Also applies to: 67-67

docs/docs/python-sdk/guides/python-typing.mdx (2)

103-104: LGTM!

The typo has been corrected from "procotols.py" to "protocols.py".


105-151: Well-structured documentation for the new feature.

The new section on generating Pydantic models from GraphQL queries is clear, well-organized, and follows a logical progression from rationale to usage. The workflow steps provide practical guidance for users.

tests/fixtures/unit/test_graphql_plugin/python01.py (2)

1-34: LGTM!

The Pydantic model structure is correct. The use of from __future__ import annotations allows forward references to work properly, and the models follow Pydantic v2 patterns with proper type hints and field aliases.


36-38: LGTM!

The model_rebuild() calls are necessary in Pydantic v2 to resolve forward references after all models are defined. The selection of models to rebuild is appropriate—focusing on those with forward reference dependencies.

tests/unit/sdk/graphql/test_query.py (1)

3-3: LGTM!

The import path update correctly reflects the module reorganization where Query and Mutation classes have been moved to infrahub_sdk.graphql.query.

tests/unit/sdk/graphql/test_renderer.py (2)

4-90: LGTM!

The render_query_block tests are comprehensive, covering basic rendering, alias handling, and fragment blocks. The use of fixtures from conftest and line-by-line assertions provide good test coverage.


93-145: LGTM!

The render_input_block tests thoroughly verify the rendering of nested structures, lists, and various data types with different indentation settings.

tests/unit/sdk/graphql/test_plugin.py (3)

14-35: LGTM!

The fixtures properly load test resources using the get_fixtures_dir() utility and explicit UTF-8 encoding, ensuring consistent file reading across platforms.


38-48: LGTM!

The test correctly verifies that the plugin is idempotent—it doesn't modify a module that already has the __future__ annotations import. The AST comparison approach is appropriate.


51-62: LGTM!

The test properly verifies that the plugin inserts the __future__ annotations import when it's missing, comparing against an expected output fixture. This provides good coverage of the plugin's primary functionality.

infrahub_sdk/graphql/query.py (2)

8-28: LGTM!

The BaseGraphQLQuery class provides a clean foundation with clear responsibilities: storing query structure, handling variable serialization, and rendering the opening line.


31-43: LGTM!

The Query class is clean and focused, properly delegating block rendering to the renderers module while maintaining consistent formatting.

infrahub_sdk/graphql/plugin.py (3)

12-29: LGTM!

The FutureAnnotationPlugin correctly checks for existing __future__ annotations import using any() to safely iterate over all imported names, preventing potential IndexError. The plugin is idempotent and properly inserts the import only when needed.


32-63: LGTM!

The StandardTypeHintPlugin properly converts legacy List type hints to modern list (PEP 585 style), with recursive handling for nested subscripts. The plugin correctly ensures the __future__ annotations import is present first.


66-85: LGTM!

The PydanticBaseModelPlugin correctly raises ValueError directly in find_base_model_index when the BaseModel import isn't found, as suggested in previous feedback. The error handling is appropriate for indicating misconfigured generated code.

@dgarros dgarros requested a review from ogenstad October 11, 2025 13:33
@dgarros dgarros force-pushed the dga-20250913-graphql-return-types branch from b1d8a7b to 0ca3ee4 Compare October 13, 2025 10:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/sdk/graphql/test_query.py (1)

248-290: Avoid unused variables in mutation header

The test declares $description and $number but they’re unused in the operation body. Most GraphQL validators error on unused variables (NoUnusedVariables). Consider limiting variables to those actually referenced to keep examples executable.

♻️ Duplicate comments (1)
infrahub_sdk/graphql/query.py (1)

49-53: Avoid *args/**kwargs in Mutation.init (ack duplicate)

Explicit params improve readability and static checking. Consider def __init__(self, query: dict, *, mutation: str, input_data: dict, variables: dict | None = None, name: str | None = None).

🧹 Nitpick comments (10)
pyproject.toml (4)

73-87: Extras wiring looks good; ensure dev/tests install ariadne-codegen if unit tests import it.

If unit tests import ariadne-codegen (e.g., plugin/renderers tests), include it in tests extras or dev group to avoid CI misses.

Option A — add to tests extra:

# outside the selected lines
[tool.poetry.extras]
tests = ["Jinja2", "pytest", "pyyaml", "rich", "ariadne-codegen"]

Option B — add to dev group:

# outside the selected lines
[tool.poetry.group.dev.dependencies]
ariadne-codegen = ">=0.15.3,<0.16"

Also, update docs to install CLI with codegen:

  • pip install "infrahub-sdk[ctl]"

135-135: Ruff config: duplicated controls for the same path; choose one.

You both exclude the path and add per-file-ignores for it; the exclude makes the per-file-ignores ineffective. Prefer keeping lint coverage with targeted ignores.

Preferred fix (keep per-file-ignores, remove exclude):

 [tool.ruff]
 exclude = [
   ".git",
   ".tox",
   ".venv",
   "env",
   "_build",
   "build",
   "dist",
   "examples",
-  "tests/fixtures/unit/test_graphql_plugin",
 ]

Alternative (keep exclude, drop per-file-ignores):

-"tests/fixtures/unit/test_graphql_plugin/*.py" = [
-    "FA100", # Add `from __future__ import annotations` to simplify `typing.Optional`
-]

Also applies to: 281-283


49-49: Align graphql-core minimum with ariadne-codegen’s needs.

If ariadne-codegen requires graphql-core ≥3.2.x, our current spec (>=3.1,<3.3) may resolve to an incompatible 3.1.x.

If confirmed, update (outside these lines):

-graphql-core = ">=3.1,<3.3"
+graphql-core = ">=3.2,<3.3"

49-49: Pin ariadne-codegen to a compatible version range
ariadne-codegen 0.15.3 supports Python 3.13, Pydantic v2, and graphql-core 3.x—avoid brittle exact pins:

-ariadne-codegen = { version = "0.15.3", optional = true }
+ariadne-codegen = { version = ">=0.15.3,<0.16.0", optional = true }
tests/unit/sdk/graphql/conftest.py (1)

6-13: Deduplicate enums across tests

MyStrEnum/MyIntEnum are also defined in tests/unit/sdk/graphql/test_query.py. Prefer importing these from conftest to avoid divergence.

tests/unit/sdk/graphql/test_query.py (1)

16-86: Query rendering assertions look solid

Covers no-vars and filter/vars formatting well; outputs match renderer semantics.

Consider adding tests for alias and inline fragment rendering using the provided fixtures to broaden coverage.

tests/unit/sdk/graphql/test_plugin.py (1)

38-62: Add coverage for other plugins

Only FutureAnnotationPlugin is tested. Please add unit tests for:

  • StandardTypeHintPlugin (e.g., List[...] → list[...], nested subscripts)
  • PydanticBaseModelPlugin (replacing from base_model import BaseModel with from pydantic import BaseModel)
infrahub_sdk/graphql/renderers.py (1)

191-209: Optional: make trailing commas configurable for lists

render_input_block always appends a trailing comma to list items. Some GraphQL servers/parsers may be stricter. Consider a flag to disable trailing commas while keeping current default.

Example change:

-def render_input_block(data: dict, offset: int = 4, indentation: int = 4, convert_enum: bool = False) -> list[str]:
+def render_input_block(
+    data: dict,
+    offset: int = 4,
+    indentation: int = 4,
+    convert_enum: bool = False,
+    allow_trailing_commas: bool = True,
+) -> list[str]:
@@
-        elif isinstance(value, list):
+        elif isinstance(value, list):
             lines.append(f"{offset_str}{key}: " + "[")
-            for item in value:
+            for idx, item in enumerate(value):
+                is_last = idx == len(value) - 1
                 if isinstance(item, dict):
                     lines.append(f"{offset_str}{' ' * indentation}" + "{")
@@
-                    lines.append(f"{offset_str}{' ' * indentation}" + "},")
+                    lines.append(f"{offset_str}{' ' * indentation}" + ("}," if allow_trailing_commas or not is_last else "}"))
                 else:
-                    lines.append(
-                        f"{offset_str}{' ' * indentation}{convert_to_graphql_as_string(value=item, convert_enum=convert_enum)},"
-                    )
+                    val = convert_to_graphql_as_string(value=item, convert_enum=convert_enum)
+                    lines.append(f"{offset_str}{' ' * indentation}{val}" + ("," if allow_trailing_commas or not is_last else ""))

Please confirm your target GraphQL servers accept trailing commas; if not, the above toggle will help.

infrahub_sdk/graphql/plugin.py (2)

33-55: Broaden List[...] normalization to cover typing.List[...]

Current check only handles List[...] when used via direct import. Also handle typing.List[...] (ast.Attribute).

-        if isinstance(subscript.value, ast.Name) and subscript.value.id == "List":
-            subscript.value.id = "list"
+        if isinstance(subscript.value, ast.Name) and subscript.value.id == "List":
+            subscript.value.id = "list"
+        elif isinstance(subscript.value, ast.Attribute) and subscript.value.attr == "List":
+            # Convert `typing.List[T]` -> `list[T]`
+            subscript.value = ast.Name(id="list")

66-85: Make BaseModel import replacement idempotent and non-fatal

Failing when from base_model import BaseModel isn’t present can abort generation unnecessarily. No-op when not found.

     @classmethod
     def replace_base_model_import(cls, module: ast.Module) -> ast.Module:
-        base_model_index = cls.find_base_model_index(module)
-        module.body[base_model_index] = ast.ImportFrom(module="pydantic", names=[ast.alias(name="BaseModel")], level=0)
+        try:
+            base_model_index = cls.find_base_model_index(module)
+        except ValueError:
+            return module
+        module.body[base_model_index] = ast.ImportFrom(
+            module="pydantic", names=[ast.alias(name="BaseModel")], level=0
+        )
         return module
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1d8a7b and 0ca3ee4.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • .vale/styles/Infrahub/branded-terms-case-swap.yml (0 hunks)
  • changelog/+gql-command.added.md (1 hunks)
  • docs/docs/infrahubctl/infrahubctl-graphql.mdx (1 hunks)
  • docs/docs/python-sdk/guides/python-typing.mdx (1 hunks)
  • infrahub_sdk/ctl/cli_commands.py (2 hunks)
  • infrahub_sdk/ctl/graphql.py (1 hunks)
  • infrahub_sdk/graphql/__init__.py (1 hunks)
  • infrahub_sdk/graphql/constants.py (1 hunks)
  • infrahub_sdk/graphql/plugin.py (1 hunks)
  • infrahub_sdk/graphql/query.py (1 hunks)
  • infrahub_sdk/graphql/renderers.py (3 hunks)
  • infrahub_sdk/graphql/utils.py (1 hunks)
  • infrahub_sdk/schema/__init__.py (2 hunks)
  • pyproject.toml (5 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python01.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python02.py (1 hunks)
  • tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (1 hunks)
  • tests/unit/sdk/graphql/conftest.py (1 hunks)
  • tests/unit/sdk/graphql/test_plugin.py (1 hunks)
  • tests/unit/sdk/graphql/test_query.py (1 hunks)
  • tests/unit/sdk/graphql/test_renderer.py (1 hunks)
💤 Files with no reviewable changes (1)
  • .vale/styles/Infrahub/branded-terms-case-swap.yml
✅ Files skipped from review due to trivial changes (1)
  • docs/docs/infrahubctl/infrahubctl-graphql.mdx
🚧 Files skipped from review as they are similar to previous changes (10)
  • infrahub_sdk/graphql/constants.py
  • infrahub_sdk/ctl/cli_commands.py
  • infrahub_sdk/graphql/init.py
  • infrahub_sdk/graphql/utils.py
  • tests/fixtures/unit/test_graphql_plugin/python01.py
  • tests/unit/sdk/graphql/test_renderer.py
  • changelog/+gql-command.added.md
  • tests/fixtures/unit/test_graphql_plugin/python02.py
  • docs/docs/python-sdk/guides/python-typing.mdx
  • infrahub_sdk/ctl/graphql.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

When implementing Infrahub checks, subclass InfrahubCheck and override validate(data); do not implement or rely on a check() method

Files:

  • tests/unit/sdk/graphql/test_plugin.py
  • tests/unit/sdk/graphql/conftest.py
  • infrahub_sdk/schema/__init__.py
  • infrahub_sdk/graphql/renderers.py
  • tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py
  • infrahub_sdk/graphql/plugin.py
  • tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py
  • infrahub_sdk/graphql/query.py
  • tests/unit/sdk/graphql/test_query.py
tests/unit/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Place and write unit tests under tests/unit/ (isolated component tests)

Files:

  • tests/unit/sdk/graphql/test_plugin.py
  • tests/unit/sdk/graphql/conftest.py
  • tests/unit/sdk/graphql/test_query.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use the custom pytest plugin (infrahub_sdk.pytest_plugin) fixtures for clients, configuration, and Infrahub-specific testing

Files:

  • tests/unit/sdk/graphql/test_plugin.py
  • tests/unit/sdk/graphql/conftest.py
  • tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py
  • tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py
  • tests/unit/sdk/graphql/test_query.py
🧬 Code graph analysis (7)
tests/unit/sdk/graphql/test_plugin.py (1)
infrahub_sdk/graphql/plugin.py (4)
  • FutureAnnotationPlugin (12-29)
  • generate_result_types_module (24-29)
  • generate_result_types_module (57-63)
  • generate_result_types_module (80-85)
tests/unit/sdk/graphql/conftest.py (2)
infrahub_sdk/protocols_base.py (1)
  • Enum (111-112)
tests/unit/sdk/graphql/test_query.py (2)
  • MyStrEnum (6-8)
  • MyIntEnum (11-13)
infrahub_sdk/schema/__init__.py (1)
infrahub_sdk/client.py (2)
  • _get (1037-1055)
  • _get (2848-2866)
tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (1)
tests/fixtures/unit/test_graphql_plugin/python02.py (2)
  • CreateDevice (6-7)
  • CreateDeviceInfraDeviceUpsert (10-12)
tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (1)
tests/fixtures/unit/test_graphql_plugin/python01.py (6)
  • CreateDevice (8-9)
  • CreateDeviceInfraDeviceUpsert (12-14)
  • CreateDeviceInfraDeviceUpsertObject (17-21)
  • CreateDeviceInfraDeviceUpsertObjectName (24-25)
  • CreateDeviceInfraDeviceUpsertObjectDescription (28-29)
  • CreateDeviceInfraDeviceUpsertObjectStatus (32-33)
infrahub_sdk/graphql/query.py (1)
infrahub_sdk/graphql/renderers.py (3)
  • render_input_block (152-212)
  • render_query_block (83-149)
  • render_variables_to_string (69-80)
tests/unit/sdk/graphql/test_query.py (1)
infrahub_sdk/graphql/query.py (2)
  • Mutation (46-77)
  • Query (31-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: unit-tests (3.9)
  • GitHub Check: unit-tests (3.10)
  • GitHub Check: unit-tests (3.12)
  • GitHub Check: unit-tests (3.13)
  • GitHub Check: integration-tests-latest-infrahub
  • GitHub Check: unit-tests (3.11)
🔇 Additional comments (4)
infrahub_sdk/schema/__init__.py (1)

477-494: LGTM! Async variant correctly implements branch-aware schema fetching.

The async implementation properly includes the branch parameter in the URL (line 487) and validates the response. The previous issue flagged in past reviews has been successfully addressed.

tests/fixtures/unit/test_graphql_plugin/python01_after_annotation.py (1)

1-38: LGTM: correct forward-ref and model structure

Models, aliases, and model_rebuild calls look correct for the fixture.

tests/fixtures/unit/test_graphql_plugin/python02_after_annotation.py (1)

1-19: LGTM: annotated variant matches expectations

Forward annotations and rebuild calls are appropriate for the test.

infrahub_sdk/graphql/query.py (1)

17-29: First-line rendering is clear and minimal

Name and variables are composed correctly; spacing matches tests.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ca3ee4 and d490fd8.

📒 Files selected for processing (1)
  • infrahub_sdk/schema/__init__.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

When implementing Infrahub checks, subclass InfrahubCheck and override validate(data); do not implement or rely on a check() method

Files:

  • infrahub_sdk/schema/__init__.py
🧬 Code graph analysis (1)
infrahub_sdk/schema/__init__.py (2)
tests/unit/sdk/conftest.py (1)
  • client (32-33)
infrahub_sdk/client.py (2)
  • _get (1037-1055)
  • _get (2848-2866)

Comment on lines +477 to +494
async def get_graphql_schema(self, branch: str | None = None) -> str:
"""Get the GraphQL schema as a string.
Args:
branch: The branch to get the schema for. Defaults to default_branch.
Returns:
The GraphQL schema as a string.
"""
branch = branch or self.client.default_branch
url = f"{self.client.address}/schema.graphql?branch={branch}"

response = await self.client._get(url=url)

if response.status_code != 200:
raise ValueError(f"Failed to fetch GraphQL schema: HTTP {response.status_code} - {response.text}")

return response.text
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

URL-encode the branch parameter and align error handling with existing patterns.

The branch parameter is directly interpolated into the URL without encoding. Branch names containing special characters (spaces, ?, &, etc.) will produce malformed URLs and cause request failures.

Additionally, the error handling differs from the established pattern in _parse_schema_response (lines 197-213). When the server returns a 400 status (invalid branch), the code should raise BranchNotFoundError for consistency.

Apply this diff to fix both issues:

+from urllib.parse import urlencode
+
 async def get_graphql_schema(self, branch: str | None = None) -> str:
     """Get the GraphQL schema as a string.

     Args:
         branch: The branch to get the schema for. Defaults to default_branch.

     Returns:
         The GraphQL schema as a string.
     """
     branch = branch or self.client.default_branch
-    url = f"{self.client.address}/schema.graphql?branch={branch}"
+    query_params = urlencode([("branch", branch)])
+    url = f"{self.client.address}/schema.graphql?{query_params}"

     response = await self.client._get(url=url)

+    if response.status_code == 400:
+        raise BranchNotFoundError(
+            identifier=branch, 
+            message=f"The requested branch was not found on the server [{branch}]"
+        )
+
     if response.status_code != 200:
         raise ValueError(f"Failed to fetch GraphQL schema: HTTP {response.status_code} - {response.text}")

     return response.text

Note: urlencode is already imported at line 10.

🤖 Prompt for AI Agents
In infrahub_sdk/schema/__init__.py around lines 477 to 494, the branch is
interpolated into the URL without URL-encoding and the error handling should
match the existing _parse_schema_response behavior; use urlencode to encode the
branch query parameter (e.g., build the query string with urlencode({"branch":
branch})) when constructing the url, and change the response error handling to
raise BranchNotFoundError when response.status_code == 400 and otherwise raise a
ValueError including status and response.text to keep parity with the
established pattern.

Comment on lines +719 to +736
def get_graphql_schema(self, branch: str | None = None) -> str:
"""Get the GraphQL schema as a string.
Args:
branch: The branch to get the schema for. Defaults to default_branch.
Returns:
The GraphQL schema as a string.
"""
branch = branch or self.client.default_branch
url = f"{self.client.address}/schema.graphql?branch={branch}"

response = self.client._get(url=url)

if response.status_code != 200:
raise ValueError(f"Failed to fetch GraphQL schema: HTTP {response.status_code} - {response.text}")

return response.text
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

URL-encode the branch parameter and align error handling (sync variant).

Same issues as the async variant: the branch parameter needs URL encoding, and error handling should match the established pattern.

Apply this diff:

 def get_graphql_schema(self, branch: str | None = None) -> str:
     """Get the GraphQL schema as a string.

     Args:
         branch: The branch to get the schema for. Defaults to default_branch.

     Returns:
         The GraphQL schema as a string.
     """
     branch = branch or self.client.default_branch
-    url = f"{self.client.address}/schema.graphql?branch={branch}"
+    query_params = urlencode([("branch", branch)])
+    url = f"{self.client.address}/schema.graphql?{query_params}"

     response = self.client._get(url=url)

+    if response.status_code == 400:
+        raise BranchNotFoundError(
+            identifier=branch,
+            message=f"The requested branch was not found on the server [{branch}]"
+        )
+
     if response.status_code != 200:
         raise ValueError(f"Failed to fetch GraphQL schema: HTTP {response.status_code} - {response.text}")

     return response.text

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In infrahub_sdk/schema/__init__.py around lines 719-736, URL-encode the branch
parameter when building the schema URL and replace the ad-hoc status_code check
with the client's established sync error handler to match the async variant;
specifically, encode branch with urllib.parse.quote_plus (or similar) when
interpolating into the URL and call the client's existing response-error routine
(e.g., self.client._raise_for_status(response) or the equivalent used elsewhere)
instead of manually raising ValueError so error handling/formatting stays
consistent.

@dgarros dgarros merged commit b61c7bd into develop Oct 13, 2025
20 checks passed
@dgarros dgarros deleted the dga-20250913-graphql-return-types branch October 13, 2025 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants