Skip to content

Commit

Permalink
Merge pull request #22 from BeatsuDev/feat/pydantic-to-query-builder
Browse files Browse the repository at this point in the history
feat: add a from_pydantic method that creates a QueryBuilder from a pydantic BaseModel
  • Loading branch information
BeatsuDev authored Sep 15, 2024
2 parents bbf9642 + abced50 commit 649bdd4
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/code_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install package dependencies
run: pip install pydantic

- name: Install code quality checking dependencies
run: |
python -m pip install --upgrade pip
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/testing_and_coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install package dependencies
run: pip install pydantic

- name: Install testing dependencies
run: |
pip install pytest
run: pip install pytest

- name: Run pytest
run: python -m pytest tests
Expand All @@ -40,9 +42,11 @@ jobs:
with:
python-version: 3.12

- name: Install package dependencies
run: pip install pydantic

- name: Install testing and coverage dependencies
run: |
pip install pytest pytest-cov
run: pip install pytest pytest-cov

- name: Run pytest
run: python -m pytest --cov=gqlrequests --cov-report=xml tests
Expand Down
1 change: 1 addition & 0 deletions gqlrequests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

from . import query_creator
from .builder import QueryBuilder
from .pydantic_converter import from_pydantic
9 changes: 9 additions & 0 deletions gqlrequests/pydantic_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import Type

from pydantic import BaseModel

from .builder import QueryBuilder


def from_pydantic(model: Type[BaseModel]) -> Type[QueryBuilder]:
return type(model.__name__, (QueryBuilder,), {"__annotations__": model.__annotations__})
19 changes: 15 additions & 4 deletions gqlrequests/query_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@
import enum
import inspect
import sys
from typing import TYPE_CHECKING, List, Dict, Tuple, Type, _GenericAlias # type: ignore
from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Union, _GenericAlias # type: ignore

from pydantic import BaseModel

import gqlrequests

if sys.version_info >= (3, 9):
from typing import GenericAlias # type: ignore

if TYPE_CHECKING:
from gqlrequests.builder import QueryBuilder # pragma: no cover
from gqlrequests.builder import QueryBuilder


class FieldTypeEnum(enum.Enum):
PRIMITIVE = 1
ENUM = 2
QUERY_BUILDER_CLASS = 3
QUERY_BUILDER_INSTANCE = 4
PYDANTIC_MODEL = 5

Primitives = int | float | str | bool
ValidFieldTypes = Primitives | enum.EnumType | QueryBuilder | Type[QueryBuilder] | List["ValidFieldTypes"]
Primitives = Union[int, float, str, bool]
# Pipe operator union does not support deferred string type evaluation apparently
ValidFieldTypes = Union[Primitives, enum.EnumMeta, "QueryBuilder", Type["QueryBuilder"], Type[BaseModel], List["ValidFieldTypes"]]

def generate_function_query_string(func_name: str, args: Dict[str, Primitives], fields: Dict[str, ValidFieldTypes], indent_size: int = 4, start_indents: int = 0) -> str:
"""Generates a GraphQL query string for a function with arguments."""
Expand Down Expand Up @@ -66,6 +70,9 @@ def generate_fields(fields: Dict[str, ValidFieldTypes], indent_size: int = 4, st
else:
string_output += whitespaces + field + " " + field_type.build(indent_size, len(whitespaces)) # type: ignore

elif field_type_type == FieldTypeEnum.PYDANTIC_MODEL:
string_output += whitespaces + field + " " + generate_query_string(field_type.__annotations__, indent_size, len(whitespaces))

else:
# This error should already be caught in the resolve_type function
raise ValueError(f"Invalid field type: {field_type}") # pragma: no cover
Expand Down Expand Up @@ -108,4 +115,8 @@ def resolve_type(type_hint: ValidFieldTypes) -> Tuple[FieldTypeEnum, ValidFieldT
if not inspect.isclass(type_hint) and isinstance(type_hint, gqlrequests.builder.QueryBuilder):
return (FieldTypeEnum.QUERY_BUILDER_INSTANCE, type_hint)

# BaseModel
if inspect.isclass(type_hint) and issubclass(type_hint, BaseModel):
return (FieldTypeEnum.PYDANTIC_MODEL, type_hint)

raise ValueError(f"Invalid field type: {type_hint}")
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
long_description_content_type="text/markdown",
packages=["gqlrequests"],
package_data={"gqlrequests": ["py.typed"]},
install_requires=[],
install_requires=["pydantic"],
license="MIT",
version=__version__,
description="A Python library for making GraphQL requests easier!",
Expand Down
56 changes: 56 additions & 0 deletions tests/test_pydantic_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import gqlrequests

from typing import List
from pydantic import BaseModel

class EveryTypeModel(BaseModel):
id: int
age: int
money: float
name: str
company: bool

def test_create_everytype():
correct_string = """
{
id
age
money
name
company
}
"""[1:]
EveryType = gqlrequests.from_pydantic(EveryTypeModel)
assert EveryType().build() == correct_string


class NestedTypeModel(BaseModel):
nested: EveryTypeModel

def test_create_nestedtype():
correct_string = """
{
nested {
id
age
money
name
company
}
}
"""[1:]
NestedType = gqlrequests.from_pydantic(NestedTypeModel)
assert NestedType().build() == correct_string


class ListTypeModel(BaseModel):
nested: List[int]

def test_create_listtype():
correct_string = """
{
nested
}
"""[1:]
ListType = gqlrequests.from_pydantic(ListTypeModel)
assert ListType().build() == correct_string

0 comments on commit 649bdd4

Please sign in to comment.