strawchemy


Namestrawchemy JSON
Version 0.9.0 PyPI version JSON
download
home_pageNone
SummaryGenerate GraphQL API from SQLAlchemy models
upload_time2025-03-21 21:32:26
maintainerNone
docs_urlNone
authorNone
requires_python>=3.9
licenseMIT
keywords api sql graphql sqlalchemy strawberry
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Strawchemy

[![🔂 Tests and linting](https://github.com/gazorby/strawchemy/actions/workflows/ci.yaml/badge.svg)](https://github.com/gazorby/strawchemy/actions/workflows/ci.yaml) [![codecov](https://codecov.io/gh/gazorby/strawchemy/graph/badge.svg?token=BCU8SX1MJ7)](https://codecov.io/gh/gazorby/strawchemy) [![PyPI Downloads](https://static.pepy.tech/badge/strawchemy)](https://pepy.tech/projects/strawchemy)

Generates GraphQL types, inputs, queries and resolvers directly from SQLAlchemy models.

## Features

- 🔄 **Automatic Type Generation**: Generate strawberry types from SQLAlchemy models

- 🧠 **Smart Resolvers**: Automatically generates single, optimized database queries for a given GraphQL request

- 🔍 **Comprehensive Filtering**: Rich filtering capabilities on most data types, including PostGIS geo columns

- 📄 **Pagination Support**: Built-in offset-based pagination

- 📊 **Aggregation Queries**: Support for aggregation functions like count, sum, avg, min, max, and statistical functions

- ⚡ **Sync/Async Support**: Works with both synchronous and asynchronous SQLAlchemy sessions

- 🛢 **Database Support**: Currently only PostgreSQL is officially supported and tested (using [asyncpg](https://github.com/MagicStack/asyncpg) or [psycopg3 sync/async](https://www.psycopg.org/psycopg3/))

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Mapping SQLAlchemy Models](#mapping-sqlalchemy-models)
- [Resolver Generation](#resolver-generation)
- [Pagination](#pagination)
- [Filtering](#filtering)
- [Aggregations](#aggregations)
- [Configuration](#configuration)
- [Contributing](#contributing)
- [License](#license)

## Installation

Strawchemy is available on PyPi

```console
pip install strawchemy
```

Strawchemy has the following optional dependencies:

- `geo` : Enable Postgis support through [geoalchemy2](https://github.com/geoalchemy/geoalchemy2)

To install these dependencies along with strawchemy:

```console
pip install strawchemy[geo]
```

## Quick Start

```python
import strawberry
from strawchemy import Strawchemy
from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

# Initialize the strawchemy mapper
strawchemy = Strawchemy()


# Define SQLAlchemy models
class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")


class Post(Base):
    __tablename__ = "post"

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    content: Mapped[str]
    author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
    author: Mapped[User] = relationship("User", back_populates="posts")


# Map models to GraphQL types
@strawchemy.type(User, include="all")
class UserType:
    pass


@strawchemy.type(Post, include="all")
class PostType:
    pass


# Create filter inputs
@strawchemy.filter_input(User, include="all")
class UserFilter:
    pass


@strawchemy.filter_input(Post, include="all")
class PostFilter:
    pass


# Create order by inputs
@strawchemy.order_by_input(User, include="all")
class UserOrderBy:
    pass


@strawchemy.order_by_input(Post, include="all")
class PostOrderBy:
    pass


# Define GraphQL query fields
@strawberry.type
class Query:
    users: list[UserType] = strawchemy.field(filter_input=UserFilter, order_by=UserOrderBy, pagination=True)
    posts: list[PostType] = strawchemy.field(filter_input=PostFilter, order_by=PostOrderBy, pagination=True)

# Create schema
schema = strawberry.Schema(query=Query)
```

```graphql
{
  # Users with pagination, filtering, and ordering
  users(
    offset: 0
    limit: 10
    filter: { name: { contains: "John" } }
    orderBy: { name: ASC }
  ) {
    id
    name
    posts {
      id
      title
      content
    }
  }

  # Posts with exact title match
  posts(filter: { title: { eq: "Introduction to GraphQL" } }) {
    id
    title
    content
    author {
      id
      name
    }
  }
}
```

## Mapping SQLAlchemy Models

Strawchemy provides an easy way to map SQLAlchemy models to GraphQL types using the `@strawchemy.type` decorator. You can include/exclude specific fields or have strawchemy map all columns/relationships of the model and it's children.

<details>
<summary>Mapping example</summary>

Include columns and relationships

```python
import strawberry
from strawchemy import Strawchemy

# Assuming these models are defined as in the Quick Start example
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey

strawchemy = Strawchemy()


class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")


@strawchemy.type(User, include="all")
class UserType:
    pass
```

Including/excluding specific fields

```python
class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    password: Mapped[str]


# Include specific fields
@strawchemy.type(User, include=["id", "name"])
class UserType:
    pass


# Exclude specific fields
@strawchemy.type(User, exclude=["password"])
class UserType:
    pass


# Include all fields
@strawchemy.type(User, include="all")
class UserType:
    pass
```

Add a custom fields

```python
from strawchemy import ModelInstance

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    first_name: Mapped[str]
    last_name: Mapped[str]


@strawchemy.type(User, include="all")
class UserType:
    instance: ModelInstance[User]

    @strawchemy.field
    def full_name(self) -> str:
        return f"{self.instance.first_name} {self.instance.last_name}"
```

See the [custom resolvers](#custom-resolvers) for more details

</details>

### Type override

By default, strawchemy generates strawberry types when visiting the model and the following relationships, but only if you have not already defined a type with the same name using the @strawchemy.type decorator, otherwise you will see an error.

To explicitly tell strawchemy to use your type, you need to define it with `@strawchemy.type(override=True)`.

<details>
<summary>Using the Override Parameter</summary>

```python
from strawchemy import Strawchemy

strawchemy = Strawchemy()

# Define models
class Color(Base):
    __tablename__ = "color"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    fruits: Mapped[list["Fruit"]] = relationship("Fruit", back_populates="color")

class Fruit(Base):
    __tablename__ = "fruit"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    color_id: Mapped[int] = mapped_column(ForeignKey("color.id"))
    color: Mapped[Color] = relationship("Color", back_populates="fruits")

# Define a type with override=True
@strawchemy.type(Color, include="all", override=True)
class ColorType:
    fruits: auto
    name: int

# Define another type that uses the same model
@strawchemy.type(Fruit, include="all", override=True)
class FruitType:
    name: int
    color: auto  # This will use the ColorType defined above

# Define a query that uses these types
@strawberry.type
class Query:
    fruit: FruitType = strawchemy.field()
```

The `override` parameter is useful in the following scenarios:

1. **Type Reuse**: When you need to use the same type in multiple places where the same model is referenced.
2. **Auto-generated Type Override**: When you want to override the default auto-generated type for a model.
3. **Custom Type Names**: When you want to use a custom name for your type but still have it recognized as the type for a specific model.

Without setting `override=True`, you would get an error like:

```
Type `FruitType` cannot be auto generated because it's already declared.
You may want to set `override=True` on the existing type to use it everywhere.
```

This happens when Strawchemy tries to auto-generate a type for a model that already has a type defined for it.

You can also use `override=True` with input types:

```python
@strawchemy.order_by_input(Fruit, include="all", override=True)
class FruitOrderBy:
    # Custom order by fields
    override: bool = True
```

</details>
</details>

## Resolver Generation

Strawchemy automatically generates resolvers for your GraphQL fields. You can use the `strawchemy.field()` function to generate fields that query your database

<details>
<summary>Resolvers example</summary>

```python
@strawberry.type
class Query:
    # Simple field that returns a list of users
    users: list[UserType] = strawchemy.field()
    # Field with filtering, ordering, and pagination
    filtered_users: list[UserType] = strawchemy.field(filter_input=UserFilter, order_by=UserOrderBy, pagination=True)
    # Field that returns a single user by ID
    user: UserType = strawchemy.field()
```

</details>

### Custom Resolvers

While Strawchemy automatically generates resolvers for most use cases, you can also create custom resolvers for more complex scenarios. There are two main approaches to creating custom resolvers:

#### 1. Using Repository Directly

When using `strawchemy.field()` as a function, strawchemy creates a resolver that delegates data fetching to the `StrawchemySyncRepository` or `StrawchemyAsyncRepository` classes depending on the SQLAlchemy session type.
You can create custom resolvers by using the `@strawchemy.field` as a decorator and working directly with the repository:

<details>
<summary>Custom resolvers using repository</summary>

```python
from sqlalchemy import select, true
from strawchemy import StrawchemySyncRepository

@strawberry.type
class Query:
    @strawchemy.field
    def red_color(self, info: strawberry.Info) -> ColorType:
        # Create a repository with a predefined filter
        repo = StrawchemySyncRepository(ColorType, info, filter_statement=select(Color).where(Color.name == "Red"))
        # Return a single result (will raise an exception if not found)
        return repo.get_one()

    @strawchemy.field
    def get_color_by_name(self, info: strawberry.Info, color: str) -> ColorType | None:
        # Create a repository with a custom filter statement
        repo = StrawchemySyncRepository(ColorType, info, filter_statement=select(Color).where(Color.name == color))
        # Return a single result or None if not found
        return repo.get_one_or_none()

    @strawchemy.field
    def get_color_by_id(self, info: strawberry.Info, id: str) -> ColorType | None:
        repo = StrawchemySyncRepository(ColorType, info)
        # Return a single result or None if not found
        return repo.get_by_id(id=id)

    @strawchemy.field
    def public_colors(self, info: strawberry.Info) -> ColorType:
        repo = StrawchemySyncRepository(ColorType, info, filter_statement=select(Color).where(Color.public.is_(true())))
        # Return a list of results
        return repo.list()
```

For async resolvers, use `StrawchemyAsyncRepository` which is the async variant of `StrawchemySyncRepository`:

```python
from strawchemy import StrawchemyAsyncRepository

@strawberry.type
class Query:
    @strawchemy.field
    async def get_color(self, info: strawberry.Info, color: str) -> ColorType | None:
        repo = StrawchemyAsyncRepository(ColorType, info, filter_statement=select(Color).where(Color.name == color))
        return await repo.get_one_or_none()
```

The repository provides several methods for fetching data:

- `get_one()`: Returns a single result, raises an exception if not found
- `get_one_or_none()`: Returns a single result or None if not found
- `get_by_id()`: Returns a single result filtered on primary key
- `list()`: Returns a list of results

</details>

#### 2. Using Query Hooks

Strawchemy provides query hooks that allow you to customize query behavior. These hooks can be applied at both the field level and the type level:

<details>
<summary>Using query hooks</summary>

```python
from strawchemy import ModelInstance, QueryHook
from strawchemy.sqlalchemy.hook import QueryHookResult
from sqlalchemy import Select
from sqlalchemy.orm.util import AliasedClass
from strawberry import Info

@strawchemy.type(Fruit, exclude={"color"})
class FruitTypeWithDescription:
    instance: ModelInstance[Fruit]

    # Use QueryHook with always_load parameter to ensure columns are loaded
    @strawchemy.field(query_hook=QueryHook(always_load=[Fruit.name, Fruit.adjectives]))
    def description(self) -> str:
        return f"The {self.instance.name} is {', '.join(self.instance.adjectives)}"

# Custom query hook function
def user_fruit_filter(
    statement: Select[tuple[Fruit]], alias: AliasedClass[Fruit], info: Info
) -> QueryHookResult[Fruit]:
    # Add a custom filter based on context
    if info.context.role == "user":
        return QueryHookResult(statement=statement.where(alias.name == "Apple"))
    return QueryHookResult(statement=statement)

# Type-level query hook
@strawchemy.type(Fruit, exclude={"color"}, query_hook=user_fruit_filter)
class FilteredFruitType:
    pass
```

> You must set a `ModelInstance` typed attribute if you want to access the model instance values.
> The `instance` attribute is matched by the `ModelInstance[Fruit]` type hint, so you can give it any name you want.

Query hooks provide powerful ways to:

- Ensure specific columns are always loaded, even if not directly requested in the GraphQL query. (useful to expose hybrid properties in the schema)
- Apply custom filters based on context (e.g., user role)
- Modify the underlying SQLAlchemy query for optimization or security

</details>

## Pagination

Strawchemy supports offset-based pagination out of the box.

<details>
<summary>Pagination example:</summary>

Enable pagination on fields:

```python
from strawchemy.types import DefaultOffsetPagination

@strawberry.type
class Query:
    # Enable pagination with default settings
    users: list[UserType] = strawchemy.field(pagination=True)
    # Customize pagination defaults
    users_custom_pagination: list[UserType] = strawchemy.field(pagination=DefaultOffsetPagination(limit=20))
```

In your GraphQL queries, you can use the `offset` and `limit` parameters:

```graphql
{
  users(offset: 0, limit: 10) {
    id
    name
  }
}
```

You can also enable pagination for nested relationships:

```python
@strawchemy.type(User, include="all", child_pagination=True)
class UserType:
    pass
```

Then in your GraphQL queries:

```graphql
{
  users {
    id
    name
    posts(offset: 0, limit: 5) {
      id
      title
    }
  }
}
```

</details>

## Filtering

Strawchemy provides powerful filtering capabilities.

<details>
<summary>Filtering example</summary>

First, create a filter input type:

```python
@strawchemy.filter_input(User, include="all")
class UserFilter:
    pass
```

Then use it in your field:

```python
@strawberry.type
class Query:
    users: list[UserType] = strawchemy.field(filter_input=UserFilter)
```

Now you can use various filter operations in your GraphQL queries:

```graphql
{
  # Equality filter
  users(filter: { name: { eq: "John" } }) {
    id
    name
  }

  # Comparison filters
  users(filter: { age: { gt: 18, lte: 30 } }) {
    id
    name
    age
  }

  # String filters
  users(filter: { name: { contains: "oh", ilike: "%OHN%" } }) {
    id
    name
  }

  # Logical operators
  users(filter: { _or: [{ name: { eq: "John" } }, { name: { eq: "Jane" } }] }) {
    id
    name
  }
  # Nested filters
  users(filter: { posts: { title: { contains: "GraphQL" } } }) {
    id
    name
    posts {
      id
      title
    }
  }

  # Compare interval component
  tasks(filter: { duration: { days: { gt: 2 } } }) {
    id
    name
    duration
  }

  # Direct interval comparison
  tasks(filter: { duration: { gt: "P2DT5H" } }) {
    id
    name
    duration
  }
}
```

</details>

Strawchemy supports a wide range of filter operations:

| Data Type/Category                      | Filter Operations                                                                                                                                                                |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Common to most types**                | `eq`, `neq`, `isNull`, `in`, `nin`                                                                                                                                               |
| **Numeric types (Int, Float, Decimal)** | `gt`, `gte`, `lt`, `lte`                                                                                                                                                         |
| **String**                              | order filter, plus `like`, `nlike`, `ilike`, `nilike`, `regexp`, `iregexp`, `nregexp`, `inregexp`, `startswith`, `endswith`, `contains`, `istartswith`, `iendswith`, `icontains` |
| **JSON**                                | `contains`, `containedIn`, `hasKey`, `hasKeyAll`, `hasKeyAny`                                                                                                                    |
| **Array**                               | `contains`, `containedIn`, `overlap`                                                                                                                                             |
| **Date**                                | order filters on plain dates, plus `year`, `month`, `day`, `weekDay`, `week`, `quarter`, `isoYear` and `isoWeekDay` filters                                                      |
| **DateTime**                            | All Date filters plus `hour`, `minute`, `second`                                                                                                                                 |
| **Time**                                | order filters on plain times, plus `hour`, `minute` and `second` filters                                                                                                         |
| **Interval**                            | order filters on plain intervals, plus `days`, `hours`, `minutes` and `seconds` filters                                                                                          |
| **Logical**                             | `_and`, `_or`, `_not`                                                                                                                                                            |

### Geo Filters

Strawchemy supports spatial filtering capabilities for geometry fields using [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946). To use geo filters, you need to have PostGIS installed and enabled in your PostgreSQL database.

<details>
<summary>Geo filters example</summary>

Define models and types:

```python
class GeoModel(Base):
    __tablename__ = "geo"

    id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
    # Define geometry columns using GeoAlchemy2
    point: Mapped[WKBElement | None] = mapped_column(Geometry("POINT", srid=4326), nullable=True)
    polygon: Mapped[WKBElement | None] = mapped_column(Geometry("POLYGON", srid=4326), nullable=True)

@strawchemy.type(GeoModel, include="all")
class GeoType: ...

@strawchemy.filter_input(GeoModel, include="all")
class GeoFieldsFilter: ...

@strawberry.type
class Query:
geo: list[GeoType] = strawchemy.field(filter_input=GeoFieldsFilter)

```

Then you can use the following geo filter operations in your GraphQL queries:

```graphql
{
  # Find geometries that contain a point
  geo(
    filter: {
      polygon: { containsGeometry: { type: "Point", coordinates: [0.5, 0.5] } }
    }
  ) {
    id
    polygon
  }

  # Find geometries that are within a polygon
  geo(
    filter: {
      point: {
        withinGeometry: {
          type: "Polygon"
          coordinates: [[[0, 0], [0, 2], [2, 2], [2, 0], [0, 0]]]
        }
      }
    }
  ) {
    id
    point
  }

  # Find records with null geometry
  geo(filter: { point: { isNull: true } }) {
    id
  }
}
```

</details>

Strawchemy supports the following geo filter operations:

- **containsGeometry**: Filters for geometries that contain the specified GeoJSON geometry
- **withinGeometry**: Filters for geometries that are within the specified GeoJSON geometry
- **isNull**: Filters for null or non-null geometry values

These filters work with all geometry types supported by PostGIS, including:

- `Point`
- `LineString`
- `Polygon`
- `MultiPoint`
- `MultiLineString`
- `MultiPolygon`
- `Geometry` (generic geometry type)

## Aggregations

Strawchemy automatically exposes aggregation fields for list relationships.

When you define a model with a list relationship, the corresponding GraphQL type will include an aggregation field for that relationship, named `<field_name>Aggregate`.

<details>
<summary> Basic aggregation example:</summary>

With the folliing model definitions:

```python
class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")


class Post(Base):
    __tablename__ = "post"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    content: Mapped[str]
    author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
    author: Mapped[User] = relationship("User", back_populates="posts")
```

And the corresponding GraphQL types:

```python
@strawchemy.type(User, include="all")
class UserType:
    pass


@strawchemy.type(Post, include="all")
class PostType:
    pass
```

You can query aggregations on the `posts` relationship:

```graphql
{
  users {
    id
    name
    postsAggregate {
      count
      min {
        title
      }
      max {
        title
      }
      # Other aggregation functions are also available
    }
  }
}
```

</details>

### Filtering by relationship aggregations

You can also filter entities based on aggregations of their related entities.

<details>
<summary>Aggregation filtering example</summary>

Define types with filters:

```python
@strawchemy.filter_input(User, include="all")
class UserFilter:
    pass


@strawberry.type
class Query:
    users: list[UserType] = strawchemy.field(filter_input=UserFilter)
```

For example, to find users who have more than 5 posts:

```graphql
{
  users(
    filter: {
      postsAggregate: { count: { arguments: [id], predicate: { gt: 5 } } }
    }
  ) {
    id
    name
    postsAggregate {
      count
    }
  }
}
```

You can use various predicates for filtering:

```graphql
# Users with exactly 3 posts
users(filter: {
  postsAggregate: {
    count: {
      arguments: [id]
      predicate: { eq: 3 }
    }
  }
})

# Users with posts containing "GraphQL" in the title
users(filter: {
  postsAggregate: {
    maxString: {
      arguments: [title]
      predicate: { contains: "GraphQL" }
    }
  }
})

# Users with an average post length greater than 1000 characters
users(filter: {
  postsAggregate: {
    avg: {
      arguments: [contentLength]
      predicate: { gt: 1000 }
    }
  }
})
```

</details>

#### Distinct aggregations

<details>
<summary>Distinct aggregation filtering example</summary>

You can also use the `distinct` parameter to count only distinct values:

```graphql
{
  users(
    filter: {
      postsAggregate: {
        count: { arguments: [category], predicate: { gt: 2 }, distinct: true }
      }
    }
  ) {
    id
    name
  }
}
```

This would find users who have posts in more than 2 distinct categories.

</details>

### Root aggregations

Strawchemy supports query level aggregations.

<details>
<summary>Root aggregations example:</summary>

First, create an aggregation type:

```python
@strawchemy.aggregation_type(User, include="all")
class UserAggregationType:
    pass
```

Then set up the root aggregations on the field:

```python
@strawberry.type
class Query:
    users_aggregations: UserAggregationType = strawchemy.field(root_aggregations=True)
```

Now you can use aggregation functions on the result of your query:

```graphql
{
  usersAggregations {
    aggregations {
      # Basic aggregations
      count

      sum {
        age
      }

      avg {
        age
      }

      min {
        age
        createdAt
      }
      max {
        age
        createdAt
      }

      # Statistical aggregations
      stddev {
        age
      }
      variance {
        age
      }
    }
    # Access the actual data
    nodes {
      id
      name
      age
    }
  }
}
```

</details>

## Configuration

Strawchemy can be configured when initializing the mapper.

### Configuration Options

| Option                     | Type                                                        | Default                  | Description                                                                                                                                                           |
| -------------------------- | ----------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_getter`           | `Callable[[Info], Session]`                                 | `default_session_getter` | Function to retrieve SQLAlchemy session from strawberry `Info` object. By default, it retrieves the session from `info.context.session`.                              |
| `auto_snake_case`          | `bool`                                                      | `True`                   | Automatically convert snake cased names to camel case in GraphQL schema.                                                                                              |
| `repository_type`          | `type[Repository] \| "auto"`                                | `"auto"`                 | Repository class to use for auto resolvers. When set to `"auto"`, Strawchemy will automatically choose between sync and async repositories based on the session type. |
| `filter_overrides`         | `OrderedDict[tuple[type, ...], type[SQLAlchemyFilterBase]]` | `None`                   | Override default filters with custom filters. This allows you to provide custom filter implementations for specific column types.                                     |
| `execution_options`        | `dict[str, Any]`                                            | `None`                   | SQLAlchemy execution options for repository operations. These options are passed to the SQLAlchemy `execution_options()` method.                                      |
| `pagination_default_limit` | `int`                                                       | `100`                    | Default pagination limit when `pagination=True`.                                                                                                                      |
| `pagination`               | `bool`                                                      | `False`                  | Enable/disable pagination on list resolvers by default.                                                                                                               |
| `default_id_field_name`    | `str`                                                       | `"id"`                   | Name for primary key fields arguments on primary key resolvers.                                                                                                       |
| `dialect`                  | `Literal["postgresql"]`                                     | `"postgresql"`           | Database dialect to use. Currently, only PostgreSQL is supported.                                                                                                     |

### Example

```python
from strawchemy import Strawchemy

# Custom session getter function
def get_session_from_context(info):
    return info.context.db_session

# Initialize with custom configuration
strawchemy = Strawchemy(
    session_getter=get_session_from_context,
    auto_snake_case=True,
    pagination=True,
    pagination_default_limit=50,
    default_id_field_name="pk",
)
```

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project.

## License

This project is licensed under the terms of the license included in the [LICENCE](LICENCE) file.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "strawchemy",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": null,
    "keywords": "API, SQL, graphql, sqlalchemy, strawberry",
    "author": null,
    "author_email": "gazorby <gazorby@pm.me>",
    "download_url": "https://files.pythonhosted.org/packages/4b/94/aa357d352742f9a9776300bbefb5ccff56a82f2cbb1b8410046db6a16e38/strawchemy-0.9.0.tar.gz",
    "platform": null,
    "description": "# Strawchemy\n\n[![\ud83d\udd02 Tests and linting](https://github.com/gazorby/strawchemy/actions/workflows/ci.yaml/badge.svg)](https://github.com/gazorby/strawchemy/actions/workflows/ci.yaml) [![codecov](https://codecov.io/gh/gazorby/strawchemy/graph/badge.svg?token=BCU8SX1MJ7)](https://codecov.io/gh/gazorby/strawchemy) [![PyPI Downloads](https://static.pepy.tech/badge/strawchemy)](https://pepy.tech/projects/strawchemy)\n\nGenerates GraphQL types, inputs, queries and resolvers directly from SQLAlchemy models.\n\n## Features\n\n- \ud83d\udd04 **Automatic Type Generation**: Generate strawberry types from SQLAlchemy models\n\n- \ud83e\udde0 **Smart Resolvers**: Automatically generates single, optimized database queries for a given GraphQL request\n\n- \ud83d\udd0d **Comprehensive Filtering**: Rich filtering capabilities on most data types, including PostGIS geo columns\n\n- \ud83d\udcc4 **Pagination Support**: Built-in offset-based pagination\n\n- \ud83d\udcca **Aggregation Queries**: Support for aggregation functions like count, sum, avg, min, max, and statistical functions\n\n- \u26a1 **Sync/Async Support**: Works with both synchronous and asynchronous SQLAlchemy sessions\n\n- \ud83d\udee2 **Database Support**: Currently only PostgreSQL is officially supported and tested (using [asyncpg](https://github.com/MagicStack/asyncpg) or [psycopg3 sync/async](https://www.psycopg.org/psycopg3/))\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Mapping SQLAlchemy Models](#mapping-sqlalchemy-models)\n- [Resolver Generation](#resolver-generation)\n- [Pagination](#pagination)\n- [Filtering](#filtering)\n- [Aggregations](#aggregations)\n- [Configuration](#configuration)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Installation\n\nStrawchemy is available on PyPi\n\n```console\npip install strawchemy\n```\n\nStrawchemy has the following optional dependencies:\n\n- `geo` : Enable Postgis support through [geoalchemy2](https://github.com/geoalchemy/geoalchemy2)\n\nTo install these dependencies along with strawchemy:\n\n```console\npip install strawchemy[geo]\n```\n\n## Quick Start\n\n```python\nimport strawberry\nfrom strawchemy import Strawchemy\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship\n\n# Initialize the strawchemy mapper\nstrawchemy = Strawchemy()\n\n\n# Define SQLAlchemy models\nclass Base(DeclarativeBase):\n    pass\n\n\nclass User(Base):\n    __tablename__ = \"user\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str]\n    posts: Mapped[list[\"Post\"]] = relationship(\"Post\", back_populates=\"author\")\n\n\nclass Post(Base):\n    __tablename__ = \"post\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    title: Mapped[str]\n    content: Mapped[str]\n    author_id: Mapped[int] = mapped_column(ForeignKey(\"user.id\"))\n    author: Mapped[User] = relationship(\"User\", back_populates=\"posts\")\n\n\n# Map models to GraphQL types\n@strawchemy.type(User, include=\"all\")\nclass UserType:\n    pass\n\n\n@strawchemy.type(Post, include=\"all\")\nclass PostType:\n    pass\n\n\n# Create filter inputs\n@strawchemy.filter_input(User, include=\"all\")\nclass UserFilter:\n    pass\n\n\n@strawchemy.filter_input(Post, include=\"all\")\nclass PostFilter:\n    pass\n\n\n# Create order by inputs\n@strawchemy.order_by_input(User, include=\"all\")\nclass UserOrderBy:\n    pass\n\n\n@strawchemy.order_by_input(Post, include=\"all\")\nclass PostOrderBy:\n    pass\n\n\n# Define GraphQL query fields\n@strawberry.type\nclass Query:\n    users: list[UserType] = strawchemy.field(filter_input=UserFilter, order_by=UserOrderBy, pagination=True)\n    posts: list[PostType] = strawchemy.field(filter_input=PostFilter, order_by=PostOrderBy, pagination=True)\n\n# Create schema\nschema = strawberry.Schema(query=Query)\n```\n\n```graphql\n{\n  # Users with pagination, filtering, and ordering\n  users(\n    offset: 0\n    limit: 10\n    filter: { name: { contains: \"John\" } }\n    orderBy: { name: ASC }\n  ) {\n    id\n    name\n    posts {\n      id\n      title\n      content\n    }\n  }\n\n  # Posts with exact title match\n  posts(filter: { title: { eq: \"Introduction to GraphQL\" } }) {\n    id\n    title\n    content\n    author {\n      id\n      name\n    }\n  }\n}\n```\n\n## Mapping SQLAlchemy Models\n\nStrawchemy provides an easy way to map SQLAlchemy models to GraphQL types using the `@strawchemy.type` decorator. You can include/exclude specific fields or have strawchemy map all columns/relationships of the model and it's children.\n\n<details>\n<summary>Mapping example</summary>\n\nInclude columns and relationships\n\n```python\nimport strawberry\nfrom strawchemy import Strawchemy\n\n# Assuming these models are defined as in the Quick Start example\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship\nfrom sqlalchemy import ForeignKey\n\nstrawchemy = Strawchemy()\n\n\nclass Base(DeclarativeBase):\n    pass\n\nclass User(Base):\n    __tablename__ = \"user\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str]\n    posts: Mapped[list[\"Post\"]] = relationship(\"Post\", back_populates=\"author\")\n\n\n@strawchemy.type(User, include=\"all\")\nclass UserType:\n    pass\n```\n\nIncluding/excluding specific fields\n\n```python\nclass User(Base):\n    __tablename__ = \"user\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str]\n    password: Mapped[str]\n\n\n# Include specific fields\n@strawchemy.type(User, include=[\"id\", \"name\"])\nclass UserType:\n    pass\n\n\n# Exclude specific fields\n@strawchemy.type(User, exclude=[\"password\"])\nclass UserType:\n    pass\n\n\n# Include all fields\n@strawchemy.type(User, include=\"all\")\nclass UserType:\n    pass\n```\n\nAdd a custom fields\n\n```python\nfrom strawchemy import ModelInstance\n\nclass User(Base):\n    __tablename__ = \"user\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    first_name: Mapped[str]\n    last_name: Mapped[str]\n\n\n@strawchemy.type(User, include=\"all\")\nclass UserType:\n    instance: ModelInstance[User]\n\n    @strawchemy.field\n    def full_name(self) -> str:\n        return f\"{self.instance.first_name} {self.instance.last_name}\"\n```\n\nSee the [custom resolvers](#custom-resolvers) for more details\n\n</details>\n\n### Type override\n\nBy default, strawchemy generates strawberry types when visiting the model and the following relationships, but only if you have not already defined a type with the same name using the @strawchemy.type decorator, otherwise you will see an error.\n\nTo explicitly tell strawchemy to use your type, you need to define it with `@strawchemy.type(override=True)`.\n\n<details>\n<summary>Using the Override Parameter</summary>\n\n```python\nfrom strawchemy import Strawchemy\n\nstrawchemy = Strawchemy()\n\n# Define models\nclass Color(Base):\n    __tablename__ = \"color\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str]\n    fruits: Mapped[list[\"Fruit\"]] = relationship(\"Fruit\", back_populates=\"color\")\n\nclass Fruit(Base):\n    __tablename__ = \"fruit\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str]\n    color_id: Mapped[int] = mapped_column(ForeignKey(\"color.id\"))\n    color: Mapped[Color] = relationship(\"Color\", back_populates=\"fruits\")\n\n# Define a type with override=True\n@strawchemy.type(Color, include=\"all\", override=True)\nclass ColorType:\n    fruits: auto\n    name: int\n\n# Define another type that uses the same model\n@strawchemy.type(Fruit, include=\"all\", override=True)\nclass FruitType:\n    name: int\n    color: auto  # This will use the ColorType defined above\n\n# Define a query that uses these types\n@strawberry.type\nclass Query:\n    fruit: FruitType = strawchemy.field()\n```\n\nThe `override` parameter is useful in the following scenarios:\n\n1. **Type Reuse**: When you need to use the same type in multiple places where the same model is referenced.\n2. **Auto-generated Type Override**: When you want to override the default auto-generated type for a model.\n3. **Custom Type Names**: When you want to use a custom name for your type but still have it recognized as the type for a specific model.\n\nWithout setting `override=True`, you would get an error like:\n\n```\nType `FruitType` cannot be auto generated because it's already declared.\nYou may want to set `override=True` on the existing type to use it everywhere.\n```\n\nThis happens when Strawchemy tries to auto-generate a type for a model that already has a type defined for it.\n\nYou can also use `override=True` with input types:\n\n```python\n@strawchemy.order_by_input(Fruit, include=\"all\", override=True)\nclass FruitOrderBy:\n    # Custom order by fields\n    override: bool = True\n```\n\n</details>\n</details>\n\n## Resolver Generation\n\nStrawchemy automatically generates resolvers for your GraphQL fields. You can use the `strawchemy.field()` function to generate fields that query your database\n\n<details>\n<summary>Resolvers example</summary>\n\n```python\n@strawberry.type\nclass Query:\n    # Simple field that returns a list of users\n    users: list[UserType] = strawchemy.field()\n    # Field with filtering, ordering, and pagination\n    filtered_users: list[UserType] = strawchemy.field(filter_input=UserFilter, order_by=UserOrderBy, pagination=True)\n    # Field that returns a single user by ID\n    user: UserType = strawchemy.field()\n```\n\n</details>\n\n### Custom Resolvers\n\nWhile Strawchemy automatically generates resolvers for most use cases, you can also create custom resolvers for more complex scenarios. There are two main approaches to creating custom resolvers:\n\n#### 1. Using Repository Directly\n\nWhen using `strawchemy.field()` as a function, strawchemy creates a resolver that delegates data fetching to the `StrawchemySyncRepository` or `StrawchemyAsyncRepository` classes depending on the SQLAlchemy session type.\nYou can create custom resolvers by using the `@strawchemy.field` as a decorator and working directly with the repository:\n\n<details>\n<summary>Custom resolvers using repository</summary>\n\n```python\nfrom sqlalchemy import select, true\nfrom strawchemy import StrawchemySyncRepository\n\n@strawberry.type\nclass Query:\n    @strawchemy.field\n    def red_color(self, info: strawberry.Info) -> ColorType:\n        # Create a repository with a predefined filter\n        repo = StrawchemySyncRepository(ColorType, info, filter_statement=select(Color).where(Color.name == \"Red\"))\n        # Return a single result (will raise an exception if not found)\n        return repo.get_one()\n\n    @strawchemy.field\n    def get_color_by_name(self, info: strawberry.Info, color: str) -> ColorType | None:\n        # Create a repository with a custom filter statement\n        repo = StrawchemySyncRepository(ColorType, info, filter_statement=select(Color).where(Color.name == color))\n        # Return a single result or None if not found\n        return repo.get_one_or_none()\n\n    @strawchemy.field\n    def get_color_by_id(self, info: strawberry.Info, id: str) -> ColorType | None:\n        repo = StrawchemySyncRepository(ColorType, info)\n        # Return a single result or None if not found\n        return repo.get_by_id(id=id)\n\n    @strawchemy.field\n    def public_colors(self, info: strawberry.Info) -> ColorType:\n        repo = StrawchemySyncRepository(ColorType, info, filter_statement=select(Color).where(Color.public.is_(true())))\n        # Return a list of results\n        return repo.list()\n```\n\nFor async resolvers, use `StrawchemyAsyncRepository` which is the async variant of `StrawchemySyncRepository`:\n\n```python\nfrom strawchemy import StrawchemyAsyncRepository\n\n@strawberry.type\nclass Query:\n    @strawchemy.field\n    async def get_color(self, info: strawberry.Info, color: str) -> ColorType | None:\n        repo = StrawchemyAsyncRepository(ColorType, info, filter_statement=select(Color).where(Color.name == color))\n        return await repo.get_one_or_none()\n```\n\nThe repository provides several methods for fetching data:\n\n- `get_one()`: Returns a single result, raises an exception if not found\n- `get_one_or_none()`: Returns a single result or None if not found\n- `get_by_id()`: Returns a single result filtered on primary key\n- `list()`: Returns a list of results\n\n</details>\n\n#### 2. Using Query Hooks\n\nStrawchemy provides query hooks that allow you to customize query behavior. These hooks can be applied at both the field level and the type level:\n\n<details>\n<summary>Using query hooks</summary>\n\n```python\nfrom strawchemy import ModelInstance, QueryHook\nfrom strawchemy.sqlalchemy.hook import QueryHookResult\nfrom sqlalchemy import Select\nfrom sqlalchemy.orm.util import AliasedClass\nfrom strawberry import Info\n\n@strawchemy.type(Fruit, exclude={\"color\"})\nclass FruitTypeWithDescription:\n    instance: ModelInstance[Fruit]\n\n    # Use QueryHook with always_load parameter to ensure columns are loaded\n    @strawchemy.field(query_hook=QueryHook(always_load=[Fruit.name, Fruit.adjectives]))\n    def description(self) -> str:\n        return f\"The {self.instance.name} is {', '.join(self.instance.adjectives)}\"\n\n# Custom query hook function\ndef user_fruit_filter(\n    statement: Select[tuple[Fruit]], alias: AliasedClass[Fruit], info: Info\n) -> QueryHookResult[Fruit]:\n    # Add a custom filter based on context\n    if info.context.role == \"user\":\n        return QueryHookResult(statement=statement.where(alias.name == \"Apple\"))\n    return QueryHookResult(statement=statement)\n\n# Type-level query hook\n@strawchemy.type(Fruit, exclude={\"color\"}, query_hook=user_fruit_filter)\nclass FilteredFruitType:\n    pass\n```\n\n> You must set a `ModelInstance` typed attribute if you want to access the model instance values.\n> The `instance` attribute is matched by the `ModelInstance[Fruit]` type hint, so you can give it any name you want.\n\nQuery hooks provide powerful ways to:\n\n- Ensure specific columns are always loaded, even if not directly requested in the GraphQL query. (useful to expose hybrid properties in the schema)\n- Apply custom filters based on context (e.g., user role)\n- Modify the underlying SQLAlchemy query for optimization or security\n\n</details>\n\n## Pagination\n\nStrawchemy supports offset-based pagination out of the box.\n\n<details>\n<summary>Pagination example:</summary>\n\nEnable pagination on fields:\n\n```python\nfrom strawchemy.types import DefaultOffsetPagination\n\n@strawberry.type\nclass Query:\n    # Enable pagination with default settings\n    users: list[UserType] = strawchemy.field(pagination=True)\n    # Customize pagination defaults\n    users_custom_pagination: list[UserType] = strawchemy.field(pagination=DefaultOffsetPagination(limit=20))\n```\n\nIn your GraphQL queries, you can use the `offset` and `limit` parameters:\n\n```graphql\n{\n  users(offset: 0, limit: 10) {\n    id\n    name\n  }\n}\n```\n\nYou can also enable pagination for nested relationships:\n\n```python\n@strawchemy.type(User, include=\"all\", child_pagination=True)\nclass UserType:\n    pass\n```\n\nThen in your GraphQL queries:\n\n```graphql\n{\n  users {\n    id\n    name\n    posts(offset: 0, limit: 5) {\n      id\n      title\n    }\n  }\n}\n```\n\n</details>\n\n## Filtering\n\nStrawchemy provides powerful filtering capabilities.\n\n<details>\n<summary>Filtering example</summary>\n\nFirst, create a filter input type:\n\n```python\n@strawchemy.filter_input(User, include=\"all\")\nclass UserFilter:\n    pass\n```\n\nThen use it in your field:\n\n```python\n@strawberry.type\nclass Query:\n    users: list[UserType] = strawchemy.field(filter_input=UserFilter)\n```\n\nNow you can use various filter operations in your GraphQL queries:\n\n```graphql\n{\n  # Equality filter\n  users(filter: { name: { eq: \"John\" } }) {\n    id\n    name\n  }\n\n  # Comparison filters\n  users(filter: { age: { gt: 18, lte: 30 } }) {\n    id\n    name\n    age\n  }\n\n  # String filters\n  users(filter: { name: { contains: \"oh\", ilike: \"%OHN%\" } }) {\n    id\n    name\n  }\n\n  # Logical operators\n  users(filter: { _or: [{ name: { eq: \"John\" } }, { name: { eq: \"Jane\" } }] }) {\n    id\n    name\n  }\n  # Nested filters\n  users(filter: { posts: { title: { contains: \"GraphQL\" } } }) {\n    id\n    name\n    posts {\n      id\n      title\n    }\n  }\n\n  # Compare interval component\n  tasks(filter: { duration: { days: { gt: 2 } } }) {\n    id\n    name\n    duration\n  }\n\n  # Direct interval comparison\n  tasks(filter: { duration: { gt: \"P2DT5H\" } }) {\n    id\n    name\n    duration\n  }\n}\n```\n\n</details>\n\nStrawchemy supports a wide range of filter operations:\n\n| Data Type/Category                      | Filter Operations                                                                                                                                                                |\n| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Common to most types**                | `eq`, `neq`, `isNull`, `in`, `nin`                                                                                                                                               |\n| **Numeric types (Int, Float, Decimal)** | `gt`, `gte`, `lt`, `lte`                                                                                                                                                         |\n| **String**                              | order filter, plus `like`, `nlike`, `ilike`, `nilike`, `regexp`, `iregexp`, `nregexp`, `inregexp`, `startswith`, `endswith`, `contains`, `istartswith`, `iendswith`, `icontains` |\n| **JSON**                                | `contains`, `containedIn`, `hasKey`, `hasKeyAll`, `hasKeyAny`                                                                                                                    |\n| **Array**                               | `contains`, `containedIn`, `overlap`                                                                                                                                             |\n| **Date**                                | order filters on plain dates, plus `year`, `month`, `day`, `weekDay`, `week`, `quarter`, `isoYear` and `isoWeekDay` filters                                                      |\n| **DateTime**                            | All Date filters plus `hour`, `minute`, `second`                                                                                                                                 |\n| **Time**                                | order filters on plain times, plus `hour`, `minute` and `second` filters                                                                                                         |\n| **Interval**                            | order filters on plain intervals, plus `days`, `hours`, `minutes` and `seconds` filters                                                                                          |\n| **Logical**                             | `_and`, `_or`, `_not`                                                                                                                                                            |\n\n### Geo Filters\n\nStrawchemy supports spatial filtering capabilities for geometry fields using [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946). To use geo filters, you need to have PostGIS installed and enabled in your PostgreSQL database.\n\n<details>\n<summary>Geo filters example</summary>\n\nDefine models and types:\n\n```python\nclass GeoModel(Base):\n    __tablename__ = \"geo\"\n\n    id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)\n    # Define geometry columns using GeoAlchemy2\n    point: Mapped[WKBElement | None] = mapped_column(Geometry(\"POINT\", srid=4326), nullable=True)\n    polygon: Mapped[WKBElement | None] = mapped_column(Geometry(\"POLYGON\", srid=4326), nullable=True)\n\n@strawchemy.type(GeoModel, include=\"all\")\nclass GeoType: ...\n\n@strawchemy.filter_input(GeoModel, include=\"all\")\nclass GeoFieldsFilter: ...\n\n@strawberry.type\nclass Query:\ngeo: list[GeoType] = strawchemy.field(filter_input=GeoFieldsFilter)\n\n```\n\nThen you can use the following geo filter operations in your GraphQL queries:\n\n```graphql\n{\n  # Find geometries that contain a point\n  geo(\n    filter: {\n      polygon: { containsGeometry: { type: \"Point\", coordinates: [0.5, 0.5] } }\n    }\n  ) {\n    id\n    polygon\n  }\n\n  # Find geometries that are within a polygon\n  geo(\n    filter: {\n      point: {\n        withinGeometry: {\n          type: \"Polygon\"\n          coordinates: [[[0, 0], [0, 2], [2, 2], [2, 0], [0, 0]]]\n        }\n      }\n    }\n  ) {\n    id\n    point\n  }\n\n  # Find records with null geometry\n  geo(filter: { point: { isNull: true } }) {\n    id\n  }\n}\n```\n\n</details>\n\nStrawchemy supports the following geo filter operations:\n\n- **containsGeometry**: Filters for geometries that contain the specified GeoJSON geometry\n- **withinGeometry**: Filters for geometries that are within the specified GeoJSON geometry\n- **isNull**: Filters for null or non-null geometry values\n\nThese filters work with all geometry types supported by PostGIS, including:\n\n- `Point`\n- `LineString`\n- `Polygon`\n- `MultiPoint`\n- `MultiLineString`\n- `MultiPolygon`\n- `Geometry` (generic geometry type)\n\n## Aggregations\n\nStrawchemy automatically exposes aggregation fields for list relationships.\n\nWhen you define a model with a list relationship, the corresponding GraphQL type will include an aggregation field for that relationship, named `<field_name>Aggregate`.\n\n<details>\n<summary> Basic aggregation example:</summary>\n\nWith the folliing model definitions:\n\n```python\nclass User(Base):\n    __tablename__ = \"user\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str]\n    posts: Mapped[list[\"Post\"]] = relationship(\"Post\", back_populates=\"author\")\n\n\nclass Post(Base):\n    __tablename__ = \"post\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    title: Mapped[str]\n    content: Mapped[str]\n    author_id: Mapped[int] = mapped_column(ForeignKey(\"user.id\"))\n    author: Mapped[User] = relationship(\"User\", back_populates=\"posts\")\n```\n\nAnd the corresponding GraphQL types:\n\n```python\n@strawchemy.type(User, include=\"all\")\nclass UserType:\n    pass\n\n\n@strawchemy.type(Post, include=\"all\")\nclass PostType:\n    pass\n```\n\nYou can query aggregations on the `posts` relationship:\n\n```graphql\n{\n  users {\n    id\n    name\n    postsAggregate {\n      count\n      min {\n        title\n      }\n      max {\n        title\n      }\n      # Other aggregation functions are also available\n    }\n  }\n}\n```\n\n</details>\n\n### Filtering by relationship aggregations\n\nYou can also filter entities based on aggregations of their related entities.\n\n<details>\n<summary>Aggregation filtering example</summary>\n\nDefine types with filters:\n\n```python\n@strawchemy.filter_input(User, include=\"all\")\nclass UserFilter:\n    pass\n\n\n@strawberry.type\nclass Query:\n    users: list[UserType] = strawchemy.field(filter_input=UserFilter)\n```\n\nFor example, to find users who have more than 5 posts:\n\n```graphql\n{\n  users(\n    filter: {\n      postsAggregate: { count: { arguments: [id], predicate: { gt: 5 } } }\n    }\n  ) {\n    id\n    name\n    postsAggregate {\n      count\n    }\n  }\n}\n```\n\nYou can use various predicates for filtering:\n\n```graphql\n# Users with exactly 3 posts\nusers(filter: {\n  postsAggregate: {\n    count: {\n      arguments: [id]\n      predicate: { eq: 3 }\n    }\n  }\n})\n\n# Users with posts containing \"GraphQL\" in the title\nusers(filter: {\n  postsAggregate: {\n    maxString: {\n      arguments: [title]\n      predicate: { contains: \"GraphQL\" }\n    }\n  }\n})\n\n# Users with an average post length greater than 1000 characters\nusers(filter: {\n  postsAggregate: {\n    avg: {\n      arguments: [contentLength]\n      predicate: { gt: 1000 }\n    }\n  }\n})\n```\n\n</details>\n\n#### Distinct aggregations\n\n<details>\n<summary>Distinct aggregation filtering example</summary>\n\nYou can also use the `distinct` parameter to count only distinct values:\n\n```graphql\n{\n  users(\n    filter: {\n      postsAggregate: {\n        count: { arguments: [category], predicate: { gt: 2 }, distinct: true }\n      }\n    }\n  ) {\n    id\n    name\n  }\n}\n```\n\nThis would find users who have posts in more than 2 distinct categories.\n\n</details>\n\n### Root aggregations\n\nStrawchemy supports query level aggregations.\n\n<details>\n<summary>Root aggregations example:</summary>\n\nFirst, create an aggregation type:\n\n```python\n@strawchemy.aggregation_type(User, include=\"all\")\nclass UserAggregationType:\n    pass\n```\n\nThen set up the root aggregations on the field:\n\n```python\n@strawberry.type\nclass Query:\n    users_aggregations: UserAggregationType = strawchemy.field(root_aggregations=True)\n```\n\nNow you can use aggregation functions on the result of your query:\n\n```graphql\n{\n  usersAggregations {\n    aggregations {\n      # Basic aggregations\n      count\n\n      sum {\n        age\n      }\n\n      avg {\n        age\n      }\n\n      min {\n        age\n        createdAt\n      }\n      max {\n        age\n        createdAt\n      }\n\n      # Statistical aggregations\n      stddev {\n        age\n      }\n      variance {\n        age\n      }\n    }\n    # Access the actual data\n    nodes {\n      id\n      name\n      age\n    }\n  }\n}\n```\n\n</details>\n\n## Configuration\n\nStrawchemy can be configured when initializing the mapper.\n\n### Configuration Options\n\n| Option                     | Type                                                        | Default                  | Description                                                                                                                                                           |\n| -------------------------- | ----------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `session_getter`           | `Callable[[Info], Session]`                                 | `default_session_getter` | Function to retrieve SQLAlchemy session from strawberry `Info` object. By default, it retrieves the session from `info.context.session`.                              |\n| `auto_snake_case`          | `bool`                                                      | `True`                   | Automatically convert snake cased names to camel case in GraphQL schema.                                                                                              |\n| `repository_type`          | `type[Repository] \\| \"auto\"`                                | `\"auto\"`                 | Repository class to use for auto resolvers. When set to `\"auto\"`, Strawchemy will automatically choose between sync and async repositories based on the session type. |\n| `filter_overrides`         | `OrderedDict[tuple[type, ...], type[SQLAlchemyFilterBase]]` | `None`                   | Override default filters with custom filters. This allows you to provide custom filter implementations for specific column types.                                     |\n| `execution_options`        | `dict[str, Any]`                                            | `None`                   | SQLAlchemy execution options for repository operations. These options are passed to the SQLAlchemy `execution_options()` method.                                      |\n| `pagination_default_limit` | `int`                                                       | `100`                    | Default pagination limit when `pagination=True`.                                                                                                                      |\n| `pagination`               | `bool`                                                      | `False`                  | Enable/disable pagination on list resolvers by default.                                                                                                               |\n| `default_id_field_name`    | `str`                                                       | `\"id\"`                   | Name for primary key fields arguments on primary key resolvers.                                                                                                       |\n| `dialect`                  | `Literal[\"postgresql\"]`                                     | `\"postgresql\"`           | Database dialect to use. Currently, only PostgreSQL is supported.                                                                                                     |\n\n### Example\n\n```python\nfrom strawchemy import Strawchemy\n\n# Custom session getter function\ndef get_session_from_context(info):\n    return info.context.db_session\n\n# Initialize with custom configuration\nstrawchemy = Strawchemy(\n    session_getter=get_session_from_context,\n    auto_snake_case=True,\n    pagination=True,\n    pagination_default_limit=50,\n    default_id_field_name=\"pk\",\n)\n```\n\n## Contributing\n\nContributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project.\n\n## License\n\nThis project is licensed under the terms of the license included in the [LICENCE](LICENCE) file.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Generate GraphQL API from SQLAlchemy models",
    "version": "0.9.0",
    "project_urls": {
        "Changelog": "https://github.com/gazorby/strawchemy/blob/main/CHANGELOG.md",
        "Documentation": "https://github.com/gazorby/strawchemy",
        "Homepage": "https://github.com/gazorby/strawchemy",
        "Issues": "https://github.com/gazorby/strawchemy/issues",
        "Repository": "https://github.com/gazorby/strawchemy"
    },
    "split_keywords": [
        "api",
        " sql",
        " graphql",
        " sqlalchemy",
        " strawberry"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "5e64a9f26bc428f018bbca1ec0b920523331004f40592b4d55c9cc79a6b609aa",
                "md5": "e1c322e78af1e96c9e01bbaa53d532dd",
                "sha256": "1a08a62cfe4909c2181aaebbfbd3aa0e03c447ce0fffa759d5e71101b9075d44"
            },
            "downloads": -1,
            "filename": "strawchemy-0.9.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "e1c322e78af1e96c9e01bbaa53d532dd",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 115125,
            "upload_time": "2025-03-21T21:32:24",
            "upload_time_iso_8601": "2025-03-21T21:32:24.736491Z",
            "url": "https://files.pythonhosted.org/packages/5e/64/a9f26bc428f018bbca1ec0b920523331004f40592b4d55c9cc79a6b609aa/strawchemy-0.9.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "4b94aa357d352742f9a9776300bbefb5ccff56a82f2cbb1b8410046db6a16e38",
                "md5": "63a690203f06b8ad6c59c3502a85fa8a",
                "sha256": "f241711745acddc59a268623e7c16a661e620dfac9b3846b12483c6c51124186"
            },
            "downloads": -1,
            "filename": "strawchemy-0.9.0.tar.gz",
            "has_sig": false,
            "md5_digest": "63a690203f06b8ad6c59c3502a85fa8a",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 414348,
            "upload_time": "2025-03-21T21:32:26",
            "upload_time_iso_8601": "2025-03-21T21:32:26.525649Z",
            "url": "https://files.pythonhosted.org/packages/4b/94/aa357d352742f9a9776300bbefb5ccff56a82f2cbb1b8410046db6a16e38/strawchemy-0.9.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-03-21 21:32:26",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "gazorby",
    "github_project": "strawchemy",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "strawchemy"
}
        
Elapsed time: 1.22501s