Skip to main content

Python Pattern Matching

Project description

Performant Python Pattern Matching and Object Validation

Reusable pattern matching for Python, implemented in Cython. I originally developed this system for the Ibis Project but hopefully it can be useful for others as well.

The implementation aims to be as quick as possible, the pure python implementation is already quite fast but taking advantage of Cython allows to mitigate the overhead of the Python interpreter. I have also tried to use PyO3 but it had higher overhead than Cython. The current implementation uses the pure python mode of cython allowing quick iteration and testing, and then it can be cythonized and compiled to an extension module giving a significant speedup. Benchmarks shows more than 2x speedup over pydantic's model validation which is written in Rust.

Installation

The package is published to PyPI, so it can be installed using pip:

pip install koerce

Library components

The library contains three main components which can be used independently or together:

1. Deferred object builders

These allow delayed evaluation of python expressions given a context:

In [1]: from koerce import var, resolve

In [2]: a, b = var("a"), var("b")

In [3]: expr = (a + 1) * b["field"]

In [4]: expr
Out[4]: (($a + 1) * $b['field'])

In [5]: resolve(expr, {"a": 2, "b": {"field": 3}})
Out[5]: 9

The syntax sugar provided by the deferred objects allows the definition of complex object transformations in a concise and natural way.

2. Pattern matchers which operate on various Python objects

Patterns are the heart of the library, they allow searching and replacing specific structures in Python objects. The library provides an extensible yet simple way to define patterns and match values against them.

In [1]: from koerce import match, NoMatch, Anything

In [2]: context = {}

In [3]: match([1, 2, 3, int, "a" @ Anything()], [1, 2, 3, 4, 5], context)
Out[3]: [1, 2, 3, 4, 5]

In [4]: context
Out[4]: {'a': 5}

Note that from koerce import koerce function can be used instead of match() to avoid confusion with the built-in python match.

from dataclasses import dataclass
from koerce import Object, match

@dataclass
class B:
    x: int
    y: int
    z: float

match(Object(B, y=1, z=2), B(1, 1, 2))
# B(x=1, y=1, z=2)

where the Object pattern checks whether the passed object is an instance of B and value.y == 1 and value.z == 2 ignoring the x field.

Patterns are also able to capture values as variables making the matching process more flexible:

from koerce import var

x = var("x")

# `+x` means to capture that object argument as variable `x`
# then the `z` argument must match that captured value
match(Object(B, +x, z=x), B(1, 2, 1))
# it is a match because x and z are equal: B(x=1, y=2, z=1)

match(Object(B, +x, z=x), B(1, 2, 0))
# is is a NoMatch because x and z are unequal

Patterns also suitable for match and replace tasks because they can produce new values:

# >> operator constructs a `Replace` pattern where the right
# hand side is a deferred object
match(Object(B, +x, z=x) >> (x, x + 1), B(1, 2, 1))
# result: (1, 2)

Patterns are also composable and can be freely combined using overloaded operators:

In [1]: from koerce import match, Is, Eq, NoMatch

In [2]: pattern = Is(int) | Is(str)
   ...: assert match(pattern, 1) == 1
   ...: assert match(pattern, "1") == "1"
   ...: assert match(pattern, 3.14) is NoMatch

In [3]: pattern = Is(int) | Eq(1)
   ...: assert match(pattern, 1) == 1
   ...: assert match(pattern, None) is NoMatch

Patterns can also be constructed from python typehints:

In [1]: from koerce import match, CoercionError

In [2]: class Ordinary:
   ...:     def __init__(self, x, y):
   ...:         self.x = x
   ...:         self.y = y
   ...:
   ...:
   ...: class Coercible(Ordinary):
   ...:
   ...:     @classmethod
   ...:     def __coerce__(cls, value):
   ...:         if isinstance(value, tuple):
   ...:             return Coercible(value[0], value[1])
   ...:         else:
   ...:             raise CoercionError("Cannot coerce value to Coercible")
   ...:

In [3]: match(Ordinary, Ordinary(1, 2))
Out[3]: <__main__.Ordinary at 0x105194fe0>

In [4]: match(Ordinary, (1, 2))
Out[4]: koerce.patterns.NoMatch

In [5]: match(Coercible, (1, 2))
Out[5]: <__main__.Coercible at 0x109ebb320>

The pattern creation logic also handles generic types by doing lightweight type parameter inference. The implementation is quite compact, available under Pattern.from_typehint().

3. A high-level validation system for dataclass-like objects

This abstraction is similar to what attrs or pydantic provide but there are some differences (TODO listing them).

In [1]: from typing import Optional
   ...: from koerce import Annotable
   ...:
   ...:
   ...: class MyClass(Annotable):
   ...:     x: int
   ...:     y: float
   ...:     z: Optional[list[str]] = None
   ...:

In [2]: MyClass(1, 2.0, ["a", "b"])
Out[2]: MyClass(x=1, y=2.0, z=['a', 'b'])

In [3]: MyClass(1, 2, ["a", "b"])
Out[3]: MyClass(x=1, y=2.0, z=['a', 'b'])

In [4]: MyClass("invalid", 2, ["a", "b"])
Out[4]: # raises validation error

Annotable object are mutable by default, but can be made immutable by passing immutable=True to the Annotable base class. Often it is useful to make immutable objects hashable as well, which can be done by passing hashable=True to the Annotable base class, in this case the hash is precomputed during initialization and stored in the object making the dictionary lookups cheap.

In [1]: from typing import Optional
   ...: from koerce import Annotable
   ...:
   ...:
   ...: class MyClass(Annotable, immutable=True, hashable=True):
   ...:     x: int
   ...:     y: float
   ...:     z: Optional[tuple[str, ...]] = None
   ...:

In [2]: a = MyClass(1, 2.0, ["a", "b"])

In [3]: a
Out[3]: MyClass(x=1, y=2.0, z=('a', 'b'))

In [4]: a.x = 2
AttributeError: Attribute 'x' cannot be assigned to immutable instance of type <class '__main__.MyClass'>

In [5]: {a: 1}
Out[5]: {MyClass(x=1, y=2.0, z=('a', 'b')): 1}

Available Pattern matchers

It is an incompletee list of the matchers, for more details and examples see koerce/patterns.py and koerce/tests/test_patterns.py.

Anything and Nothing

In [1]: from koerce import match, Anything, Nothing

In [2]: match(Anything(), "a")
Out[2]: 'a'

In [3]: match(Anything(), 1)
Out[3]: 1

In [4]: match(Nothing(), 1)
Out[4]: koerce._internal.NoMatch

Eq for equality matching

In [1]: from koerce import Eq, match, var

In [2]: x = var("x")

In [3]: match(Eq(1), 1)
Out[3]: 1

In [4]: match(Eq(1), 2)
Out[4]: koerce._internal.NoMatch

In [5]: match(Eq(x), 2, context={"x": 2})
Out[5]: 2

In [6]: match(Eq(x), 2, context={"x": 3})
Out[6]: koerce._internal.NoMatch

Is for instance matching

Couple simple cases are below:

In [1]: from koerce import match, Is

In [2]: class A: pass

In [3]: match(Is(A), A())
Out[3]: <__main__.A at 0x1061070e0>

In [4]: match(Is(A), "A")
Out[4]: koerce._internal.NoMatch

In [5]: match(Is(int), 1)
Out[5]: 1

In [6]: match(Is(int), 3.14)
Out[6]: koerce._internal.NoMatch

In [7]: from typing import Optional

In [8]: match(Is(Optional[int]), 1)
Out[8]: 1

In [9]: match(Is(Optional[int]), None)

Generic types are also supported by checking types of attributes / properties:

from koerce import match, Is, NoMatch
from typing import Generic, TypeVar, Any
from dataclasses import dataclass


T = TypeVar("T", covariant=True)
S = TypeVar("S", covariant=True)

@dataclass
class My(Generic[T, S]):
    a: T
    b: S
    c: str


MyAlias = My[T, str]

b_int = My(1, 2, "3")
b_float = My(1, 2.0, "3")
b_str = My("1", "2", "3")

# b_int.a must be an instance of int
# b_int.b must be an instance of Any
assert match(My[int, Any], b_int) is b_int

# both b_int.a and b_int.b must be an instance of int
assert match(My[int, int], b_int) is b_int

# b_int.b should be an instance of a float but it isn't
assert match(My[int, float], b_int) is NoMatch

# now b_float.b is actually a float so it is a match
assert match(My[int, float], b_float) is b_float

# type aliases are also supported
assert match(MyAlias[str], b_str) is b_str

As patterns attempting to coerce the value as the given type

from koerce import match, As, NoMatch
from typing import Generic, TypeVar, Any
from dataclasses import dataclass

class MyClass:
    pass

class MyInt(int):
    @classmethod
    def __coerce__(cls, other):
        return MyInt(int(other))


class MyNumber(Generic[T]):
    value: T

    def __init__(self, value):
        self.value = value

    @classmethod
    def __coerce__(cls, other, T):
        return cls(T(other))


assert match(As(int), 1.0) == 1
assert match(As(str), 1.0) == "1.0"
assert match(As(float), 1.0) == 1.0
assert match(As(MyClass), "myclass") is NoMatch

# by implementing the coercible protocol objects can be transparently
# coerced to the given type
assert match(As(MyInt), 3.14) == MyInt(3)

# coercible protocol also supports generic types where the `__coerce__`
# method should be implemented on one of the base classes and the
# type parameters are passed as keyword arguments to `cls.__coerce__()`
assert match(As(MyNumber[float]), 8).value == 8.0

As and Is can be omitted because match() tries to convert its first argument to a pattern using the koerce.pattern() function:

from koerce import pattern

assert pattern(int, allow_coercion=False) == Is(int)
assert pattern(int, allow_coercion=True) == As(int)

assert match(int, 1, allow_coercion=False) == 1
assert match(int, 1.1, allow_coercion=False) is NoMatch
assert match(int, 1.1, allow_coercion=True) == 1

# default is allow_coercion=False
assert match(int, 1.1) is NoMatch

If patterns for conditionals

Allows conditional matching based on the value of the object, or other variables in the context:

from koerce import match, If, Is, var, NoMatch, Capture

x = var("x")

pattern = Capture(x) & If(x > 0)
assert match(pattern, 1) == 1
assert match(pattern, -1) is NoMatch

Custom for user defined matching logic

A function passed to either match() or pattern() is treated as a Custom pattern:

from koerce import match, Custom, NoMatch, NoMatchError

def is_even(value):
    if value % 2:
        raise NoMatchError("Value is not even")
    else:
        return value

assert match(is_even, 2) == 2
assert match(is_even, 3) is NoMatch

Capture to record values in the context

A capture pattern can be defined several ways:

from koerce import Capture, Is, var

x = var("x")

Capture("x")  # captures anything as "x" in the context
Capture(x)  # same as above but using a variable
Capture("x", Is(int))  # captures only integers as "x" in the context
Capture("x", Is(int) | Is(float))  # captures integers and floats as "x" in the context
"x" @ Is(int)  # syntax sugar for Capture("x", Is(int))
+x  # syntax sugar for Capture(x, Anything())
from koerce import match, Capture, var

# context is a mutable dictionary passed along the matching process
context = {}
assert match("x" @ Is(int), 1, context) == 1
assert context["x"] == 1

Replace for replacing matched values

Allows replacing matched values with new ones:

from koerce import match, Replace, var

x = var("x")

pattern = Replace(Capture(x), x + 1)
assert match(pattern, 1) == 2
assert match(pattern, 2) == 3

there is a syntax sugar for Replace patterns, the example above can be written as:

from koerce import match, Replace, var

x = var("x")

assert match(+x >> x + 1, 1) == 2
assert match(+x >> x + 1, 2) == 3

replace patterns are especially useful when matching objects:

from dataclasses import dataclass
from koerce import match, Replace, var, namespace

x = var("x")

@dataclass
class A:
    x: int
    y: int

@dataclass
class B:
    x: int
    y: int
    z: float


p, d = namespace(__name__)
x, y = var("x"), var("y")

# if value is an instance of A then capture A.0 as x and A.1 as y
# then construct a new B object with arguments x=x, y=1, z=y
pattern = p.A(+x, +y) >> d.B(x=x, y=1, z=y)
value = A(1, 2)
expected = B(x=1, y=1, z=2)
assert match(pattern, value) == expected

replacemenets can also be used in nested structures:

from koerce import match, Replace, var, namespace, NoMatch

@dataclass
class Foo:
    value: str

@dataclass
class Bar:
    foo: Foo
    value: int

p, d = namespace(__name__)

pattern = p.Bar(p.Foo("a") >> d.Foo("b"))
value = Bar(Foo("a"), 123)
expected = Bar(Foo("b"), 123)

assert match(pattern, value) == expected
assert match(pattern, Bar(Foo("c"), 123)) is NoMatch

SequenceOf / ListOf / TupleOf

from koerce import Is, NoMatch, match, ListOf, TupleOf

pattern = ListOf(str)
assert match(pattern, ["foo", "bar"]) == ["foo", "bar"]
assert match(pattern, [1, 2]) is NoMatch
assert match(pattern, 1) is NoMatch

MappingOf / DictOf / FrozenDictOf

from koerce import DictOf, Is, match

pattern = DictOf(Is(str), Is(int))
assert match(pattern, {"a": 1, "b": 2}) == {"a": 1, "b": 2}
assert match(pattern, {"a": 1, "b": "2"}) is NoMatch

PatternList

from koerce import match, NoMatch, SomeOf, ListOf, pattern

four = [1, 2, 3, 4]
three = [1, 2, 3]

assert match([1, 2, 3, SomeOf(int, at_least=1)], four) == four
assert match([1, 2, 3, SomeOf(int, at_least=1)], three) is NoMatch

integer = pattern(int, allow_coercion=False)
floating = pattern(float, allow_coercion=False)

assert match([1, 2, *floating], [1, 2, 3]) is NoMatch
assert match([1, 2, *floating], [1, 2, 3.0]) == [1, 2, 3.0]
assert match([1, 2, *floating], [1, 2, 3.0, 4.0]) == [1, 2, 3.0, 4.0]

PatternMap

from koerce import match, NoMatch, Is, As

pattern = {
    "a": Is(int),
    "b": As(int),
    "c": Is(str),
    "d": ListOf(As(int)),
}
value = {
    "a": 1,
    "b": 2.0,
    "c": "three",
    "d": (4.0, 5.0, 6.0),
}
assert match(pattern, value) == {
    "a": 1,
    "b": 2,
    "c": "three",
    "d": [4, 5, 6],
}
assert match(pattern, {"a": 1, "b": 2, "c": "three"}) is NoMatch

Annotable objects

Annotable objects are similar to dataclasses but with some differences:

  • Annotable objects are mutable by default, but can be made immutable by passing immutable=True to the Annotable base class.
  • Annotable objects can be made hashable by passing hashable=True to the Annotable base class, in this case the hash is precomputed during initialization and stored in the object making the dictionary lookups cheap.
  • Validation strictness can be controlled by passing allow_coercion=False. When allow_coercion=True the annotations are treated as As patterns allowing the values to be coerced to the given type. When allow_coercion=False the annotations are treated as Is patterns and the values must be exactly of the given type. The default is allow_coercion=True.
  • Annotable objects support inheritance, the annotations are inherited from the base classes and the signatures are merged providing a seamless experience.
  • Annotable objects can be called with either or both positional and keyword arguments, the positional arguments are matched to the annotations in order and the keyword arguments are matched to the annotations by name.
from typing import Optional
from koerce import Annotable

class MyBase(Annotable):
    x: int
    y: float
    z: Optional[str] = None

class MyClass(MyBase):
    a: str
    b: bytes
    c: tuple[str, ...] = ("a", "b")
    x: int = 1


print(MyClass.__signature__)
# (y: float, a: str, b: bytes, c: tuple = ('a', 'b'), x: int = 1, z: Optional[str] = None)

print(MyClass(2.0, "a", b"b"))
# MyClass(y=2.0, a='a', b=b'b', c=('a', 'b'), x=1, z=None)

print(MyClass(2.0, "a", b"b", c=("c", "d")))
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=1, z=None)

print(MyClass(2.0, "a", b"b", c=("c", "d"), x=2))
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=2, z=None)

print(MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z"))
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=2, z='z')

MyClass()
# TypeError: missing a required argument: 'y'

MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z", invalid="invalid")
# TypeError: got an unexpected keyword argument 'invalid'

MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z", y=3.0)
# TypeError: multiple values for argument 'y'

MyClass("asd", "a", b"b")
# ValidationError

Performance

koerce's performance is at least comparable to pydantic's performance. pydantic-core is written in rust using the PyO3 bindings making it a pretty performant library. There is a quicker validation / serialization library from Jim Crist-Harif called msgspec implemented in hand-crafted C directly using python's C API.

koerce is not exactly like pydantic or msgpec but they are good candidates to benchmark against:

koerce/tests/test_y.py::test_pydantic PASSED
koerce/tests/test_y.py::test_msgspec PASSED
koerce/tests/test_y.py::test_annotated PASSED


------------------------------------------------------------------------------------------- benchmark: 3 tests ------------------------------------------------------------------------------------------
Name (time in ns)            Min                   Max                  Mean              StdDev                Median                IQR            Outliers  OPS (Kops/s)            Rounds  Iterations
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_msgspec            230.2801 (1.0)      6,481.4200 (1.60)       252.1706 (1.0)       97.0572 (1.0)        238.1600 (1.0)       5.0002 (1.0)      485;1616    3,965.5694 (1.0)       20000          50
test_annotated          525.6401 (2.28)     4,038.5600 (1.0)        577.7090 (2.29)     132.9966 (1.37)       553.9799 (2.33)     34.9300 (6.99)      662;671    1,730.9752 (0.44)      20000          50
test_pydantic         1,185.0201 (5.15)     6,027.9400 (1.49)     1,349.1259 (5.35)     320.3790 (3.30)     1,278.5601 (5.37)     75.5100 (15.10)   1071;1424      741.2206 (0.19)      20000          50

I tried to used the most performant API of both msgspec and pydantic receiving the arguments as a dictionary.

I am planning to make more thorough comparisons, but the model-like annotation API of koerce is roughly twice as fast as pydantic but half as fast as msgspec. Considering the implementations it also makes sense, PyO3 possible has a higher overhead than Cython has but neither of those can match the performance of hand crafted python C-API code.

This performance result could be slightly improved but has two huge advantage of the other two libraries:

  1. It is implemented in pure python with cython decorators, so it can be used even without compiling it. It could also enable JIT compilers like PyPy or the new copy and patch JIT compiler coming with CPython 3.13 to optimize hot paths better.
  2. Development an be done in pure python make it much easier to contribute to. No one needs to learn Rust or python's C API in order to fix bugs or contribute new features.

TODO:

The README is under construction, planning to improve it:

  • Example of validating functions by using @annotated decorator
  • Explain allow_coercible flag
  • Proper error messages for each pattern

Development

  • The project uses poetry for dependency management and packaging.
  • Python version support follows https://numpy.org/neps/nep-0029-deprecation_policy.html
  • The wheels are built using cibuildwheel project.
  • The implementation is in pure python with cython annotations.
  • The project uses ruff for code formatting.
  • The project uses pytest for testing.

More detailed developer guide is coming soon.

References

The project was mostly inspired by the following projects:

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

koerce-0.2.2.tar.gz (64.9 kB view details)

Uploaded Source

Built Distributions

koerce-0.2.2-cp312-cp312-win_amd64.whl (424.8 kB view details)

Uploaded CPython 3.12 Windows x86-64

koerce-0.2.2-cp312-cp312-win32.whl (349.3 kB view details)

Uploaded CPython 3.12 Windows x86

koerce-0.2.2-cp312-cp312-musllinux_1_2_x86_64.whl (3.2 MB view details)

Uploaded CPython 3.12 musllinux: musl 1.2+ x86-64

koerce-0.2.2-cp312-cp312-musllinux_1_2_i686.whl (3.1 MB view details)

Uploaded CPython 3.12 musllinux: musl 1.2+ i686

koerce-0.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.6 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64

koerce-0.2.2-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (3.3 MB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.2.2-cp312-cp312-macosx_14_0_arm64.whl (481.1 kB view details)

Uploaded CPython 3.12 macOS 14.0+ ARM64

koerce-0.2.2-cp312-cp312-macosx_13_0_x86_64.whl (526.7 kB view details)

Uploaded CPython 3.12 macOS 13.0+ x86-64

koerce-0.2.2-cp311-cp311-win_amd64.whl (445.7 kB view details)

Uploaded CPython 3.11 Windows x86-64

koerce-0.2.2-cp311-cp311-win32.whl (364.8 kB view details)

Uploaded CPython 3.11 Windows x86

koerce-0.2.2-cp311-cp311-musllinux_1_2_x86_64.whl (3.3 MB view details)

Uploaded CPython 3.11 musllinux: musl 1.2+ x86-64

koerce-0.2.2-cp311-cp311-musllinux_1_2_i686.whl (3.2 MB view details)

Uploaded CPython 3.11 musllinux: musl 1.2+ i686

koerce-0.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.7 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64

koerce-0.2.2-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (3.4 MB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.2.2-cp311-cp311-macosx_14_0_arm64.whl (481.1 kB view details)

Uploaded CPython 3.11 macOS 14.0+ ARM64

koerce-0.2.2-cp311-cp311-macosx_13_0_x86_64.whl (543.0 kB view details)

Uploaded CPython 3.11 macOS 13.0+ x86-64

koerce-0.2.2-cp310-cp310-win_amd64.whl (442.0 kB view details)

Uploaded CPython 3.10 Windows x86-64

koerce-0.2.2-cp310-cp310-win32.whl (365.1 kB view details)

Uploaded CPython 3.10 Windows x86

koerce-0.2.2-cp310-cp310-musllinux_1_2_x86_64.whl (2.9 MB view details)

Uploaded CPython 3.10 musllinux: musl 1.2+ x86-64

koerce-0.2.2-cp310-cp310-musllinux_1_2_i686.whl (2.8 MB view details)

Uploaded CPython 3.10 musllinux: musl 1.2+ i686

koerce-0.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.3 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ x86-64

koerce-0.2.2-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (3.0 MB view details)

Uploaded CPython 3.10 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.2.2-cp310-cp310-macosx_14_0_arm64.whl (480.4 kB view details)

Uploaded CPython 3.10 macOS 14.0+ ARM64

koerce-0.2.2-cp310-cp310-macosx_13_0_x86_64.whl (541.2 kB view details)

Uploaded CPython 3.10 macOS 13.0+ x86-64

File details

Details for the file koerce-0.2.2.tar.gz.

File metadata

  • Download URL: koerce-0.2.2.tar.gz
  • Upload date:
  • Size: 64.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.5

File hashes

Hashes for koerce-0.2.2.tar.gz
Algorithm Hash digest
SHA256 57fb722003ace595670e0027c3fdb81ff46b8463436addc5918a6c284a8f76f0
MD5 6c812b9a282c52e9b3a36f196ad86dba
BLAKE2b-256 3772da66c7d13986316725a26c2e1dc0da5979d181d3ff0d61b955d6ffae4d3e

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: koerce-0.2.2-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 424.8 kB
  • Tags: CPython 3.12, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.5

File hashes

Hashes for koerce-0.2.2-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 cd09168857468e23a924e39575222d138e4372c8d56dec20c3d13f791420add3
MD5 1e26419f87852b1a6a247552d516f89f
BLAKE2b-256 104a9be8c7a30c53d03f457d648e08cd06594bc9004a6de4dcf5c4fd72e99681

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp312-cp312-win32.whl.

File metadata

  • Download URL: koerce-0.2.2-cp312-cp312-win32.whl
  • Upload date:
  • Size: 349.3 kB
  • Tags: CPython 3.12, Windows x86
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.5

File hashes

Hashes for koerce-0.2.2-cp312-cp312-win32.whl
Algorithm Hash digest
SHA256 f0887cf8f2d416ce2d16812618d4200f0dca0982c4421de1d3523408aea91ea8
MD5 43f89ed8a6b9ebdf8cb1951a411c1960
BLAKE2b-256 ff22f5fb59558345177d3312e8aecb63b73554d618230b25d2f8993bcaaf64c3

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp312-cp312-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp312-cp312-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 7484cd048e8e3513b0226b955612cf1fe8eacee5d203ff7044dc52e5dd63d68a
MD5 3799864223a5e416c2baf4739142aab6
BLAKE2b-256 0f51df77974f56944c934b2bc892f65d6b7842cf353da347f223defe33d552e3

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp312-cp312-musllinux_1_2_i686.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp312-cp312-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 3addee78d110f97e7fb8de3d4c821e4b38cad03edc3030a743c7c186f2597b2f
MD5 20f29a09850fac09c181445101e385c4
BLAKE2b-256 46e53c9fead81e963d295d9e9d1a9a8d66b7552631fb8589b8d8f362a3563eea

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f9ea5b51a23a8bc0fdbdb40199d82870436231758fc58de8f3a7ec82faeeee70
MD5 79f973e26d9325e61ea2b0eaa839af94
BLAKE2b-256 09bc3704b750e7605e42d834798d309c38c8091f554dde4a9dd436812f6c47c5

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 1ca507a478ef0eeede8fb5fef53b23d40ee292ef5b00b9495c82514183680517
MD5 03053e2a3b4d157ba5dd076784d5a446
BLAKE2b-256 ff3cae070b05ad5d50b50da913f110a71db81d7ecb56758273a35093a79be48f

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp312-cp312-macosx_14_0_arm64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp312-cp312-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 675d007c194eb0fce4e8fb442f2177a4c49ba62f76986a47dd1b0b253adfc182
MD5 8483c1270b1a94e29fee339fe78c4401
BLAKE2b-256 204c870e44769ac74d21ca4cb50ca84dfe2c66ed26978d7547ac755e9c509d2c

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp312-cp312-macosx_13_0_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp312-cp312-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 558cf7f34afbfdc68d0d7f6b97a2ed3774a2006045d164a7646624f6ba7f2e51
MD5 917a6efb5203d4dd2c0c73b1ec4917f0
BLAKE2b-256 48e9b934da2eb3f09668027bcca47d7b68379c550004113fb1e60d779e01913e

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp311-cp311-win_amd64.whl.

File metadata

  • Download URL: koerce-0.2.2-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 445.7 kB
  • Tags: CPython 3.11, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.5

File hashes

Hashes for koerce-0.2.2-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 2cc932082db0f8da657215b5c1937bdbe3ef0dd33f348f0d739345729dbfee65
MD5 045edb15e25f3df2c90e4184c9744c55
BLAKE2b-256 b4c53a2d55e1011d4f320d44c26d759c3feee8f2a26e383257224ef22c47e66e

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp311-cp311-win32.whl.

File metadata

  • Download URL: koerce-0.2.2-cp311-cp311-win32.whl
  • Upload date:
  • Size: 364.8 kB
  • Tags: CPython 3.11, Windows x86
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.5

File hashes

Hashes for koerce-0.2.2-cp311-cp311-win32.whl
Algorithm Hash digest
SHA256 748d80aff28ff249d189fd09900d8409b8581cbdfac27bda46b415f2c27da08b
MD5 919f593fc8375923c013eea5e884e987
BLAKE2b-256 1ed020ce283dcf0b315db72498e14e46ad5246c6370e17143f7b18c7a2b99bf0

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp311-cp311-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 04c67c98a66715afd7aa396696e448548f628a545b91550ba2f93ed1dab834e4
MD5 7efa4d42c5918c2610f3e72fc4cd5057
BLAKE2b-256 7d50d86d305e59276fad53f44ff52748d7de21f75ada80e298f001611a3f1dde

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp311-cp311-musllinux_1_2_i686.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp311-cp311-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 822642d97690dcf5316de494dfedd0e754617b776ad752164d83c2082d04d42e
MD5 63e8d4e642958e815475cdbd17a91463
BLAKE2b-256 749f4359e97ece8b90c873b74e9307e69842d1e754e9b4b84223efcff9634ec1

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f451417f83005040fdc1dec54d28af0a77bd2310eef8105c3841e282d406990b
MD5 90c54215c065bf71e4cc46f14da7d79a
BLAKE2b-256 20513cfa10d86d9b7d9b265294e379c7344badf573c7518256afe5dbd9249758

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 392de9cdda8dda8757a60c757f28c2829b1359a780be0b5046683bc053648257
MD5 21eed27bbf17bd47103c775f28297800
BLAKE2b-256 b06ab9757f12f8dd9ee51ab45711f1352e713a623600685a1f8e40fbf73070cf

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp311-cp311-macosx_14_0_arm64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp311-cp311-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 5fafbc61c863b5b41c469975b249aeb4901b4a4cee5f73c0379e895a0daf8866
MD5 69b9286a671f746fe0b3f2bd79a06110
BLAKE2b-256 b052549fd32f04fa58f8aed6f62f8c69fc6ff1c06e6492af00d9592654c52a42

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp311-cp311-macosx_13_0_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp311-cp311-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 a2ec76ac63db6ccdc2fefb4fa25ec37589fdc06b4f9a0c747eb31e40367f9bea
MD5 41e14af9faa27758f3fcb0188ea352ad
BLAKE2b-256 aab7d773e3f9d65c0238170ff69c70a1b7d9bd7f3a013f88fb39bc46fc87b1e1

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp310-cp310-win_amd64.whl.

File metadata

  • Download URL: koerce-0.2.2-cp310-cp310-win_amd64.whl
  • Upload date:
  • Size: 442.0 kB
  • Tags: CPython 3.10, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.5

File hashes

Hashes for koerce-0.2.2-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 b73eb4de8635d7c18b04c2b9de0540e30d5ee5c2dc17b36ee36dec3dcb88ca9a
MD5 c716a98ea3a30399b5f875b9c9d72961
BLAKE2b-256 b9e94507b885abc798c464dfb82fa971b2c111a48fb979c60f08ef91fee96918

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp310-cp310-win32.whl.

File metadata

  • Download URL: koerce-0.2.2-cp310-cp310-win32.whl
  • Upload date:
  • Size: 365.1 kB
  • Tags: CPython 3.10, Windows x86
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.5

File hashes

Hashes for koerce-0.2.2-cp310-cp310-win32.whl
Algorithm Hash digest
SHA256 37d830b2c1009b680913b5ed7df8cc828e726fc017cc32406ca0b46bfc4e08a4
MD5 344df4a7267278ace6306132cf7f6eea
BLAKE2b-256 61790d03f4283bc33689d97b49fbce9d190eca1851c86af7c3ad61cf6d17eefb

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp310-cp310-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp310-cp310-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 be7a699e054b14d6031d6e670cddb9c68068d2cbfe9ea56c6adfda56fccecfc0
MD5 86be4afc1824828dbc6184d37991956a
BLAKE2b-256 54aefb5898fbc650b309632826ceb0d6c0649ed7ac0bf715bb0f2bb17c18b403

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp310-cp310-musllinux_1_2_i686.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp310-cp310-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 1a28dd87c72918a860e3b7ad29c5f25afba169d3213a264e7a032a4c53c9721b
MD5 fc679ba9e2b0b160ef215d2b922a0281
BLAKE2b-256 a013e7feef692d449263e1dc85384f5e862c28939d69d47d8dad745144b37207

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 2681b7aee62a766acf6c2f57a0bde9c2c978358c1ee592f4578ea1fbf42fcbbc
MD5 c82a3b1d8aefce75934c8d800aa98a8e
BLAKE2b-256 47b46b3e5fc92fb87eb8b64cba701485a59148818e7fd73bc3064d9867123273

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 dd818b37b142e5d227178db5727e6bccaf6184ddd6c1ff9b81965e4edee4a2b7
MD5 08daab1a09d1a34d953fbaf7eee9ad12
BLAKE2b-256 4f85362a6b612d7a07599b4c98783f9845534dee985b29fccbcd2ce5c38722be

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp310-cp310-macosx_14_0_arm64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp310-cp310-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 d7fc4424cbc31b42ed8f9380efb3c228694299167dbf10222e8d7a70d98aceef
MD5 830c5a99110f31f4b6c6fc39322cbc46
BLAKE2b-256 f3225c35f863236575195ae7c1507afa87df960e2aa8b150916d42c6f4f45026

See more details on using hashes here.

File details

Details for the file koerce-0.2.2-cp310-cp310-macosx_13_0_x86_64.whl.

File metadata

File hashes

Hashes for koerce-0.2.2-cp310-cp310-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 a62fe7442fce608ac3cdd2d81090fc1b13e7d06a2513bfd6bac06d79b0abf171
MD5 407a4492fbbb52f36c4ae3c94960545c
BLAKE2b-256 fd9fbdac566ab756b04c7cdaa13d05c0beab88ed00d65d7d39cf036c29e6623b

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