diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6323f2c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install wheel + pip install -e .[test,dev] + - name: Pytest + run: | + pytest + - name: Linters + run: | + pylint example tests + mypy example --ignore-missing-imports --check-untyped-defs + black --check . diff --git a/README.md b/README.md index 62b49c2..1c9ad83 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # ariadne-graphql-modules-v2-example -An example GraphQL API implemented with Ariadne GraphQL Modules v2 + +An example GraphQL API implemented with Ariadne GraphQL Modules v2. + +This API aims to use ALL features from GraphQL Modules v2. It can also be used as a reference for other developers. diff --git a/example/__init__.py b/example/__init__.py new file mode 100644 index 0000000..77a7988 --- /dev/null +++ b/example/__init__.py @@ -0,0 +1,6 @@ +from ariadne.asgi import GraphQL + +from .schema import schema + + +app = GraphQL(schema, debug=True) diff --git a/example/database.py b/example/database.py new file mode 100644 index 0000000..7e2b55f --- /dev/null +++ b/example/database.py @@ -0,0 +1,55 @@ +from typing import Any + +from .fixture import get_data + + +class DataBase: + _data: dict[str, dict[int, Any]] + _id: int + + def __init__(self, data: dict[str, dict[int, Any]], counter: int = 0): + self._data = data + self._id = counter + + async def get_row(self, table: str, **kwargs) -> Any: + assert kwargs, "use kwargs to filter" + + for row in self._data[table].values(): + for attr, value in kwargs.items(): + if getattr(row, attr) != value: + continue + + return row + + return None + + async def get_all(self, table: str, **kwargs) -> list[Any]: + if not kwargs: + return list(self._data[table].values()) + + results: list[Any] = [] + for row in self._data[table].values(): + for attr, value in kwargs.items(): + if getattr(row, attr) != value: + continue + + results.append(row) + + return results + + async def insert(self, table: str, obj: Any): + assert obj.id is None, "obj.id attr must be None" + + self._id += 1 + obj.id = self._id + + self._data[table][obj.id] = obj + + async def update(self, table: str, obj: Any): + self._data[table][obj.id] = obj + + async def delete(self, table: str, id: int): + self._data[table].pop(id, None) + + +db = DataBase(get_data(), 1000) diff --git a/example/enums/__init__.py b/example/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/enums/groupfilter.py b/example/enums/groupfilter.py new file mode 100644 index 0000000..c4db544 --- /dev/null +++ b/example/enums/groupfilter.py @@ -0,0 +1,7 @@ +from enum import StrEnum + + +class GroupFilter(StrEnum): + ALL = "all" + ADMIN = "admin" + MEMBER = "member" diff --git a/example/fixture.py b/example/fixture.py new file mode 100644 index 0000000..2d99852 --- /dev/null +++ b/example/fixture.py @@ -0,0 +1,47 @@ +from typing import Any + +from .models.group import Group +from .models.user import User + + +def get_data() -> dict[str, dict[int, Any]]: + return { + "groups": { + 1: Group( + id=1, + name="Admins", + is_admin=True, + ), + 2: Group( + id=2, + name="Members", + is_admin=False, + ), + }, + "users": { + 1: User( + id=1, + username="JohnDoe", + email="johndoe@example.com", + group_id=1, + ), + 2: User( + id=2, + username="Alice", + email="alice@example.com", + group_id=1, + ), + 3: User( + id=3, + username="Bob", + email="b0b@example.com", + group_id=2, + ), + 4: User( + id=4, + username="Mia", + email="mia@example.com", + group_id=2, + ), + }, + } diff --git a/example/models/__init__.py b/example/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/models/group.py b/example/models/group.py new file mode 100644 index 0000000..dc9eaea --- /dev/null +++ b/example/models/group.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class Group: + id: int + name: str + is_admin: bool diff --git a/example/models/user.py b/example/models/user.py new file mode 100644 index 0000000..44381cf --- /dev/null +++ b/example/models/user.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class User: + id: int + username: str + email: str + group_id: int diff --git a/example/schema.py b/example/schema.py new file mode 100644 index 0000000..3161ea9 --- /dev/null +++ b/example/schema.py @@ -0,0 +1,42 @@ +from ariadne_graphql_modules import GraphQLObject, make_executable_schema +from graphql import GraphQLResolveInfo + +from .database import db +from .enums.groupfilter import GroupFilter +from .types.group import GroupType +from .types.user import UserType + + +class Query(GraphQLObject): + hello: str + + @GraphQLObject.resolver("hello") + @staticmethod + def resolve_hello(obj, info: GraphQLResolveInfo) -> str: + return "Hello world!" + + @GraphQLObject.field(args={"filter_": GraphQLObject.argument("filter")}) + async def groups(obj, info: GraphQLResolveInfo, filter_: GroupFilter) -> list[GroupType]: + if filter_ == GroupFilter.ADMIN: + return await db.get_all("groups", is_admin=True) + + if filter_ == GroupFilter.MEMBER: + return await db.get_all("groups", is_admin=False) + + return await db.get_all("groups") + + @GraphQLObject.field() + async def group(obj, info: GraphQLResolveInfo, id: str) -> GroupType | None: + try: + id_int = int(id) + except (TypeError, ValueError): + return None + + return await db.get_row("groups", id=id_int) + + @GraphQLObject.field() + async def users(obj, info: GraphQLResolveInfo) -> list[UserType]: + return await db.get_all("users") + + +schema = make_executable_schema(Query, convert_names_case=True) diff --git a/example/types/__init__.py b/example/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/types/group.py b/example/types/group.py new file mode 100644 index 0000000..25f6e03 --- /dev/null +++ b/example/types/group.py @@ -0,0 +1,21 @@ +from typing import TYPE_CHECKING, Annotated + +from ariadne_graphql_modules import GraphQLID, GraphQLObject, deferred +from graphql import GraphQLResolveInfo + +from ..database import db +from ..models.group import Group + +if TYPE_CHECKING: + from .user import UserType + + +class GroupType(GraphQLObject): + id: GraphQLID + name: str + is_admin: bool + + @GraphQLObject.field(graphql_type=list[Annotated["UserType", deferred(".user")]]) + @staticmethod + async def users(obj: Group, info: GraphQLResolveInfo): + return await db.get_all("users", group_id=obj.id) diff --git a/example/types/user.py b/example/types/user.py new file mode 100644 index 0000000..b719fee --- /dev/null +++ b/example/types/user.py @@ -0,0 +1,23 @@ +from typing import TYPE_CHECKING, Annotated + +from ariadne_graphql_modules import GraphQLID, GraphQLObject, deferred +from graphql import GraphQLResolveInfo + +from ..database import db +from ..models.group import Group +from ..models.user import User + +if TYPE_CHECKING: + from .group import GroupType + + +class UserType(GraphQLObject): + id: GraphQLID + username: str + email: str + group: Annotated["GroupType", deferred(".group")] + + @GraphQLObject.resolver("group") + @staticmethod + async def resolve_group(user: User, info: GraphQLResolveInfo) -> Group: + return await db.get_row("groups", id=user.group_id) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..737e979 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[project] +name = "ariadne-graphql-modules-v2-example" +version = "0.1.0" +description = "An example GraphQL API implemented using the Ariadne GraphQL Modules v2." +authors = [{ name = "Rafał Pitoń", email = "rafio.xudb@gmail.com" }] +readme = "README.md" +license = { file = "LICENSE" } +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Environment :: Web Environment", + "Programming Language :: Python", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "ariadne>=0.23.0", + "ariadne-graphql-modules@git+https://github.com/mirumee/ariadne-graphql-modules@next-api", +] + +[project.optional-dependencies] +dev = ["black", "mypy", "pylint"] +test = [ + "pytest", + "pytest-asyncio", + "pytest-benchmark", + "pytest-cov", + "pytest-mock", + "freezegun", + "syrupy", + "werkzeug", + "httpx", + "opentracing", + "opentelemetry-api", + "python-multipart>=0.0.5", + "aiodataloader", + "graphql-sync-dataloaders;python_version>\"3.7\"", +] + +[tool.black] +line-length = 88 +target-version = ['py36', 'py37', 'py38'] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | snapshots +)/ +''' + +[tool.pytest.ini_options] +asyncio_mode = "strict" +testpaths = ["tests"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..bcfafe2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +import pytest +from graphql import graphql + +from example.schema import schema + + +@pytest.fixture +def exec_query(): + async def exec_query_( + document: str, + variables: dict | None = None, + operation: str | None = None, + ): + return await graphql( + schema, + document, + variable_values=variables, + operation_name=operation, + ) + + return exec_query_ diff --git a/tests/test_query.py b/tests/test_query.py new file mode 100644 index 0000000..7632ad6 --- /dev/null +++ b/tests/test_query.py @@ -0,0 +1,7 @@ +import pytest + + +@pytest.mark.asyncio +async def test_query_hello_field(exec_query): + result = await exec_query("{ hello }") + assert result.data == {"hello": "Hello world!"}