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.1.tar.gz (64.8 kB view details)

Uploaded Source

Built Distributions

koerce-0.2.1-cp312-cp312-win_amd64.whl (424.6 kB view details)

Uploaded CPython 3.12 Windows x86-64

koerce-0.2.1-cp312-cp312-win32.whl (349.2 kB view details)

Uploaded CPython 3.12 Windows x86

koerce-0.2.1-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.1-cp312-cp312-musllinux_1_2_i686.whl (3.1 MB view details)

Uploaded CPython 3.12 musllinux: musl 1.2+ i686

koerce-0.2.1-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.1-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.1-cp312-cp312-macosx_14_0_arm64.whl (481.0 kB view details)

Uploaded CPython 3.12 macOS 14.0+ ARM64

koerce-0.2.1-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.1-cp311-cp311-win_amd64.whl (445.6 kB view details)

Uploaded CPython 3.11 Windows x86-64

koerce-0.2.1-cp311-cp311-win32.whl (364.7 kB view details)

Uploaded CPython 3.11 Windows x86

koerce-0.2.1-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.1-cp311-cp311-musllinux_1_2_i686.whl (3.2 MB view details)

Uploaded CPython 3.11 musllinux: musl 1.2+ i686

koerce-0.2.1-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.1-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.1-cp311-cp311-macosx_14_0_arm64.whl (481.1 kB view details)

Uploaded CPython 3.11 macOS 14.0+ ARM64

koerce-0.2.1-cp311-cp311-macosx_13_0_x86_64.whl (542.9 kB view details)

Uploaded CPython 3.11 macOS 13.0+ x86-64

koerce-0.2.1-cp310-cp310-win_amd64.whl (441.9 kB view details)

Uploaded CPython 3.10 Windows x86-64

koerce-0.2.1-cp310-cp310-win32.whl (365.0 kB view details)

Uploaded CPython 3.10 Windows x86

koerce-0.2.1-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.1-cp310-cp310-musllinux_1_2_i686.whl (2.8 MB view details)

Uploaded CPython 3.10 musllinux: musl 1.2+ i686

koerce-0.2.1-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.1-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.1-cp310-cp310-macosx_14_0_arm64.whl (480.3 kB view details)

Uploaded CPython 3.10 macOS 14.0+ ARM64

koerce-0.2.1-cp310-cp310-macosx_13_0_x86_64.whl (541.1 kB view details)

Uploaded CPython 3.10 macOS 13.0+ x86-64

File details

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

File metadata

  • Download URL: koerce-0.2.1.tar.gz
  • Upload date:
  • Size: 64.8 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.1.tar.gz
Algorithm Hash digest
SHA256 98025ebfc800f1acae64e040221d6932519f07de4b477a806dd1f17ce7406442
MD5 67542b965587e25268388a609e5c3630
BLAKE2b-256 1a7438463b4e62a34a6a9be6314ef2c3d8171f9b26668ff8dcf0a312497b5284

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.1-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 424.6 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.1-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 bead53cd8ed914743bc3dd74781569e40ee16926388f51cb749a875cc4a55f90
MD5 909adfcfe22bc39e95be4ecfeeb8562a
BLAKE2b-256 c2027e5afa722c5430571dea5e56b410de802745943820621d804fe27f47fd1f

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.1-cp312-cp312-win32.whl
  • Upload date:
  • Size: 349.2 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.1-cp312-cp312-win32.whl
Algorithm Hash digest
SHA256 b8cc5d16f342b3c5430fbb966c0e2c04bdf3091f332de129fd8ce816b96049bf
MD5 1b4d4bb1eb61086c6e0075fed3ce59b4
BLAKE2b-256 5673ccd7818df769b8dacb8e255a8fbf70cdd43d406d7dde278e3e3e0d7e2597

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 5aa12f1893cfa97d5d380d16350b2783a699a86dd4c59ec456cd00d1110219f5
MD5 dd690f4c43c30f0466c561612dd172f6
BLAKE2b-256 e17febfea477fe9f774e9fcfc98acce0340c236f3126f316774d237656a49e9d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp312-cp312-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 1a43069e4aea2c51d3a36fb873037db3aff114d02405f5413abe93979722b9b6
MD5 543396a546a7704173a402f35fa04c3c
BLAKE2b-256 a9613faeb1c2c01f00ee2c7cbfd3eba1a8436579941a8a923f07ae18b3f269d7

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 518b27f72f80b5e33a965f5cd123f04ad2a7b43c5758f83b3602e54ad938d5a5
MD5 89e18303c056841d76d475bf85bf567d
BLAKE2b-256 0129dddd604157b0f76bf2feee58c839c417c4570d5e20357c3489003ae77af6

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 cf0fdde7879f401abf22468c85129c7f2d0063428fac84dc99cc16e5d22b52be
MD5 ad6e990bb6a6867e55fb4c8b353bba13
BLAKE2b-256 ad0f06c2a7b44ecd831de3e4125788375c87400dde4d9f51b0a3a303d0ffb63a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp312-cp312-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 e6243fa693507f88e28c9261d48b89ca99153d6a0ded840a196ee158514b8b9c
MD5 09667a16812fa09f5e3c0a3fa04aac8e
BLAKE2b-256 c3bd71783af8cdab7747e114c7aec2b98344080e3c468eae4cedebd9b92e8f3a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp312-cp312-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 4cf3fc98e9e35398cc5919049c199f7f8242ae4f90e019900a788f64c90f87a1
MD5 53f397ba10d7b255a6139687a5829d4d
BLAKE2b-256 210493567f550a5321bf04d086e13df3a9e6de44d8e28a6967f66cc2175e8569

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.1-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 445.6 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.1-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 6b1e1c2d96e9f3a4f36758efc69ceaa68ac977e6445ca18e58697da1e65bc268
MD5 9b532c52fb1ced1c36b7724f102df243
BLAKE2b-256 f0f3ef1281f817aa0b04dd91ac678271cd2b6692415140076b316792b1580440

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.1-cp311-cp311-win32.whl
  • Upload date:
  • Size: 364.7 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.1-cp311-cp311-win32.whl
Algorithm Hash digest
SHA256 305f264660f8e50767067532b5fefa2ffe4e65bed7e1100221a4255a55fb7f9b
MD5 38c2492fa055f999602ebdf3e65b0fb4
BLAKE2b-256 f4d375fe20d91465cd22f2cded93b6f4dc0f2f10951d060b3d9897974522fb45

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 2166966e4ae98a9dec9f9fdb230021f8ca84437c7c09cd30ad932bc8a869a991
MD5 bffb18c85cec546dfca6347360c6d641
BLAKE2b-256 953125a1bbcc7d43e84fa557b01db67bbf98c6212f7be035572a3f5c05c212ae

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp311-cp311-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 6fbbfec1d4a37817de46c897723ca21a96feecfc92e1e7451c882d0b60d0ec6c
MD5 b573de772188f46bc0427d870db6b453
BLAKE2b-256 bcede2630159a9fd77afcda9991467dbca62c98ba32a358c21caa9daed0e8bcb

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f22dddde86572e6345c41f4136a7929c69ca69fc8fc8b01b091deb1c33722a8b
MD5 2b9bbb88be461d8b57ffa39cce5a4274
BLAKE2b-256 22ad052606bfd0df04b52a90f98d9adcda68c146fb7466e118d68716f02334bd

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 5c162e95036a21320cf52324bb456196aaa95a0b05394f832edba664221e7db2
MD5 9ea3525fa93c13c9a31ab6d626cc9868
BLAKE2b-256 5aff539bb5e8f586492e3bbd7e7f47b973c107e2616ffa54e901f346ac8efa0c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp311-cp311-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 d8e9f432481559933cc2771b609e385ef105ee2f7676de5d34d2d3139be3b3e2
MD5 12684f22e2791d7b5958dfba20c66c5b
BLAKE2b-256 b0a2421e08bdd3e95a554dddeb03a7b02cc5f9d6576282da1bac3b6c679a6052

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp311-cp311-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 9ccdddbcf1e2ddb7b0cf90b1bd1a172c80d8d700400118951e972d5d64d628fe
MD5 e06f89c41abd71bee3f387238193e9b8
BLAKE2b-256 3a1fa347d19c0e6ba85d910a929dfbb2ea946437f288c6b3dc3dd890952ac179

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.1-cp310-cp310-win_amd64.whl
  • Upload date:
  • Size: 441.9 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.1-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 a7f30d87495340b7c46a969b98cb8f1121c42c2775fe538fdecf275caf86a545
MD5 5818a2139884eaef0ad4c62e48e28fe8
BLAKE2b-256 dd29c39d9f4f14c95e5199ca66a1a4e3fcc8bc04560cb702f1e97219a4bab189

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.1-cp310-cp310-win32.whl
  • Upload date:
  • Size: 365.0 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.1-cp310-cp310-win32.whl
Algorithm Hash digest
SHA256 b090fb3e9d8939980fef416561edb526266c62c1d699b18dad0b28c9afe599a2
MD5 a6c84a1a4376fa21599e7c801a827df7
BLAKE2b-256 ae3d79ec9588dbe99ece6b154b73a24a94fa025872c881c3942e48aa0ec0d988

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 0ccb650d50947efef6f58ac94407964c465af7439143a37790491e90b319b470
MD5 803c9a94cc759e53ff1a55a5c2f53b5a
BLAKE2b-256 002a6e8aebea62518b392f683c4f5dd3b7a76dddcf977f740db1d3029aceb9c8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp310-cp310-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 cc82807c02f47955b105302ca14e1b9877a83b6d0514b791f7555325c39a4878
MD5 133234cbf00ef1f1bce422c72039692d
BLAKE2b-256 9e1b7f480ae757e4a112d7a5771a03a9e143f4f184543e7c238e8d37bb9b39c4

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 7c82506640f48f312612b5e2839213304856642459e4105f3bd6925098e5631e
MD5 ebc28531efab7c30dd28eefc98b619f0
BLAKE2b-256 24ec8e0b13e2ad31440ca5fe6cea8ca5e0929b6697fff2c77b706a689a425f89

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 17f62fc4e222d943a927351e0148deec161c11871251f069670876fb6fd45f2d
MD5 d5d0498e0f2850ac35deb501438cb2a9
BLAKE2b-256 b73c9d89e714a0216a7efd68478b571480eea4c52deac2da9c49972f47aa317e

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp310-cp310-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 bbf0d28d1f35f4ec3d67e0aed4bfc09b017405bbbc8083c6f84115817df9709b
MD5 b44bacc328c41085439fb1cd1571e817
BLAKE2b-256 a281eb3722eb65c72fd0cac0b9425afb608babbb4d5b0604784c68470bfc5dba

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.1-cp310-cp310-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 ed43607019f63785eb461478305bc97ba3a5f31f8aeaa252577e4c41e3158497
MD5 087c8f98b3220399405c6eefdc45facc
BLAKE2b-256 10aeef3b0c6ee1600544dc6305ae66bdce6b3f3413741fb535d0a8b2884df192

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