Skip to main content

Data validation and transformation library for Python. Successor to CleanCat.

Project description

CleanChausie

CleanChausie is a data validation and transformation library for Python. It is a successor to CleanCat.

Interested in working on projects like this? Close is looking for great engineers to join our team.

Key features:

  • Operate on/with type-checked objects that have good IDE/autocomplete support
  • Annotation-based declarations for simple fields
  • Composable/reusable fields and field validation logic
  • Support (but not require) passing around a context (to avoid global state)
    • Context pattern is compatible with explicit sqlalchemy-based session management. i.e. pass in a session when validating
  • Cleanly support intra-schema field dependencies (i.e. one field can depend on the validated value of another)
  • Explicit nullability/omission parameters
  • Errors returned for multiple fields at a time, with field attribution

Installation

CleanChausie requires Python 3.8+. To install, run python3 -m pip install cleanchausie.

CleanChausie by example

A basic example in Flask

This shows:

  • Annotation-based declarations for simple fields.
  • Type-checked objects (successful validation results in initialized instances of the schema)
from typing import List
from cleanchausie.fields import (
    EmailField, ListField, URLField, ValidationError, field
)
from cleanchausie.schema import Schema
from flask import app, request, jsonify

class JobApplication(Schema):
    first_name: str
    last_name: str
    email: str = field(EmailField())
    urls: List[str] = field(ListField(URLField(default_scheme='http://')))

@app.route('/job_application', methods=['POST'])
def test_view():
    result = JobApplication.clean(request.json)
    if isinstance(result, ValidationError):
        return jsonify({'errors': [{'msg': e.msg, 'field': e.field} for e in result.errors] }), 400

    # Now "result" has the validated data, in the form of a `JobApplication` instance.
    assert isinstance(result, JobApplication)
    name = f'{result.first_name} {result.last_name}'

Explicit nullability

TODO: revisit omission defaults so that they match the annotation

from typing import Optional, Union
from cleanchausie.consts import OMITTED
from cleanchausie.fields import field, StrField, Omittable, Required
from cleanchausie.schema import Schema

class NullabilityExample(Schema):
    # auto defined based on annotations
    nonnull_required: str
    nullable_omittable: Optional[str]

    # manually specified
    nonnull_omittable: Union[str, OMITTED] = field(StrField(), nullability=Omittable(allow_none=False))
    nullable_required: Optional[str] = field(StrField(), nullability=Required(allow_none=True))

Composable/reusable fields

from typing import Union
from cleanchausie.fields import field, Field, StrField, IntField, Error
from cleanchausie.schema import Schema

@field(parents=(StrField(),))
def trimmed_string(value: str) -> str:
    return value.strip()

def max_val(max_value: int) -> Field:
    @field()
    def _max_val(value: int) -> Union[int, Error]:
        if value > max_value:
            return Error(msg=f'value is above allowed max of {max_value}')
        return value
    return _max_val

def min_val(min_value: int) -> Field:
    @field()
    def _min_val(value: int) -> Union[int, Error]:
        if value < min_value:
            return Error(msg=f'value is below allowed min of {min_value}')
        return value
    return _min_val

def constrained_int(min: int, max: int) -> Field:
    return field(parents=(IntField(), min_val(min), max_val(max)))

class ReusableFieldsExampleSchema(Schema):
    first_name: str = trimmed_string
    age: int = field(parents=(IntField(), min_val(0)))
    score: int = constrained_int(min=0, max=100)

Context support

import attrs
from cleanchausie.fields import field, StrField
from cleanchausie.schema import Schema

class MyModel:  # some ORM model
    id: str
    created_by: 'User'

@attrs.frozen
class Context:
    authenticated_user: 'User'  # the User making a request
    session: 'Session'  # active ORM Session

class ContextExampleSchema(Schema):
    @field(parents=(StrField(),), accepts=('id',))
    def obj(self, value: str, context: Context) -> MyModel:
        return (
            context.session
            .query(MyModel)
            .filter(MyModel.created_by == context.authenticated_user.id)
            .filter(MyModel.id == value)
        )

with atomic() as session:
    result = ContextExampleSchema.clean(
        data={'id': 'mymodel_primarykey'},
        context=Context(authenticated_user=EXAMPLE_USER, session=session)
    )
assert isinstance(result, ContextExampleSchema)
assert isinstance(result.obj, MyModel)

Intra-schema field dependencies

from cleanchausie.fields import field
from cleanchausie.schema import Schema

class DependencyExampleSchema(Schema):
    a: str
    b: str
    
    @field()
    def a_and_b(self, a: str, b: str) -> str:
        return f'{a}::{b}'


result = DependencyExampleSchema.clean(
    data={'a': 'foo', 'b': 'bar'},
)
assert isinstance(result, DependencyExampleSchema)
assert result.a_and_b == 'foo::bar'

Per-field errors

from cleanchausie.fields import (
    Error, ValidationError, field
)
from cleanchausie.schema import Schema

class PerFieldErrorExampleSchema(Schema):
    first_name: str
    last_name: str

result = PerFieldErrorExampleSchema.clean({})
assert isinstance(result, ValidationError)
assert result.errors == [
    Error(msg='This field is required.', field=('last_name',)),
    Error(msg='This field is required.', field=('first_name',))
]

Release process

  • Make sure to thoroughly review and test the code changes.
  • Make sure you have releaserabbit installed.
  • From the project's root run releaserabbit with the desired version increment - major, minor or patch.
  • A version bump should happen automatically, and the new version should be uploaded to PyPI.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cleanchausie-1.1.0.tar.gz (21.2 kB view details)

Uploaded Source

File details

Details for the file cleanchausie-1.1.0.tar.gz.

File metadata

  • Download URL: cleanchausie-1.1.0.tar.gz
  • Upload date:
  • Size: 21.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.8

File hashes

Hashes for cleanchausie-1.1.0.tar.gz
Algorithm Hash digest
SHA256 7daa6b677b81004db29025a7493cc921f6019e1b555112a686aa91342c3bd415
MD5 be8d94f2cef822f0d4aaa6988c75117d
BLAKE2b-256 47b63909256bbdccd0b71d40be98e7dbab68a6576a9cefd108ae45f2f1558f26

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page