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

Uploaded Source

Built Distributions

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

Uploaded CPython 3.12 Windows x86-64

koerce-0.2.3-cp312-cp312-win32.whl (349.6 kB view details)

Uploaded CPython 3.12 Windows x86

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

Uploaded CPython 3.12 musllinux: musl 1.2+ i686

koerce-0.2.3-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.3-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.3-cp312-cp312-macosx_14_0_arm64.whl (481.8 kB view details)

Uploaded CPython 3.12 macOS 14.0+ ARM64

koerce-0.2.3-cp312-cp312-macosx_13_0_x86_64.whl (527.3 kB view details)

Uploaded CPython 3.12 macOS 13.0+ x86-64

koerce-0.2.3-cp311-cp311-win_amd64.whl (445.8 kB view details)

Uploaded CPython 3.11 Windows x86-64

koerce-0.2.3-cp311-cp311-win32.whl (365.1 kB view details)

Uploaded CPython 3.11 Windows x86

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

Uploaded CPython 3.11 musllinux: musl 1.2+ i686

koerce-0.2.3-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.3-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.3-cp311-cp311-macosx_14_0_arm64.whl (482.0 kB view details)

Uploaded CPython 3.11 macOS 14.0+ ARM64

koerce-0.2.3-cp311-cp311-macosx_13_0_x86_64.whl (543.4 kB view details)

Uploaded CPython 3.11 macOS 13.0+ x86-64

koerce-0.2.3-cp310-cp310-win_amd64.whl (442.3 kB view details)

Uploaded CPython 3.10 Windows x86-64

koerce-0.2.3-cp310-cp310-win32.whl (365.3 kB view details)

Uploaded CPython 3.10 Windows x86

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

Uploaded CPython 3.10 musllinux: musl 1.2+ i686

koerce-0.2.3-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.3-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.3-cp310-cp310-macosx_14_0_arm64.whl (481.0 kB view details)

Uploaded CPython 3.10 macOS 14.0+ ARM64

koerce-0.2.3-cp310-cp310-macosx_13_0_x86_64.whl (541.9 kB view details)

Uploaded CPython 3.10 macOS 13.0+ x86-64

File details

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

File metadata

  • Download URL: koerce-0.2.3.tar.gz
  • Upload date:
  • Size: 65.0 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.3.tar.gz
Algorithm Hash digest
SHA256 bb80f84e75892004b5657d0e1312c94fa41976acd1f5ab5d8ea3700f206f5b03
MD5 e4ee9be9c069a334bb4d90ba88e48c6b
BLAKE2b-256 07bda28d28b141b8a2b6c75f13a5c99080e9b72fcbbe9ff296a56ac27f3cbb8e

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.3-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.3-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 28d20241e45ac5f21a60f2a93f61392904041e2733af23d531777a55ea340369
MD5 c98cbf027d8d3b79ddd4d3304789b88a
BLAKE2b-256 fa0c0274b5e9366d866550771625766889ba48970f380e0b45997b76bd14c44a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.3-cp312-cp312-win32.whl
  • Upload date:
  • Size: 349.6 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.3-cp312-cp312-win32.whl
Algorithm Hash digest
SHA256 1b95b12421e1e9ec4061d87e7994af28ebf0315433311b6d88a99785fc3962cd
MD5 87b9d9d3c466b93b3d64a8e2c365fecb
BLAKE2b-256 c155591c527eafc8088e614060c302889713e02d7df0a555d7d43cbcab87735c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp312-cp312-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 2d2e1f531c46ed70397ae653d28a20e637b8c6d371233e48569d126eaded181f
MD5 15bb8bb4ad9b6fda1fb1b05dc572d9a1
BLAKE2b-256 1462bec9eb8c3f4f42e138c6a093397af6ba774a9fdb9a45f423d56994a48780

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp312-cp312-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 be69e7a5a8034917f3be20674b2a35aceb5ce70897ef25588c85465dcae60290
MD5 651e09dc8acc00554d15896bb3bd9ec3
BLAKE2b-256 d820449f43b25ae432a69d873e215862a2094b1174262c214b83010186cf034b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 eaa0ff84ff490154436f70e68e82b2582374c552680176a004affec42eacdb35
MD5 100784899c7c187d48d591abc75d4255
BLAKE2b-256 9f3f1a19ca9eab1092becbf8e85ba50b7beb8d66a565b636214de4cef6ab6b8f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 a4bdf0109c54489ec8a3869619694e05b61c75351d554b6e069e6f6e102c89a3
MD5 a717a0470311942ece07daf101e74b68
BLAKE2b-256 f25f51950fb1ef2f9b2b80fc2eff90a028305ebbf1b5f17fa9081bfbeeda1306

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp312-cp312-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 215aa4a292ef01fde43df39868d7b2524de5884ceaa5fa3e462e1644ca693ead
MD5 216821ab36efa48dd210c397f3580305
BLAKE2b-256 8e827e0d044fb6bc9c1d1ea918b649b6ce189c008d58a82d683d6640b05e4f90

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp312-cp312-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 c78bcfb8a8df9059426e15974893f7928665710a46b681d14b7322fde8e84583
MD5 9c1a3ddf087e1b567bc035c28e52036b
BLAKE2b-256 4eee785cc82fbbdcc4eeb61c75ee82361f1e0815035244b6f9350992f48f23cf

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.3-cp311-cp311-win_amd64.whl
  • Upload date:
  • Size: 445.8 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.3-cp311-cp311-win_amd64.whl
Algorithm Hash digest
SHA256 8ff958835e511fade12d5cfd403cca115f65c8c19a748a6beae4292cfd73a817
MD5 0399326ce6d4cef776015d48fdfa280c
BLAKE2b-256 b80ea17b7513af278a15d4b5355c8c7d295efd1ff794efb62edb0e40af5b8b87

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.3-cp311-cp311-win32.whl
  • Upload date:
  • Size: 365.1 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.3-cp311-cp311-win32.whl
Algorithm Hash digest
SHA256 de6716ca77fd9c081bffab5f1859eb665ab394fe68824bd41ea1ab0c7a11c766
MD5 e54eb9b3e79b5df0873baa10ab6a446d
BLAKE2b-256 f476598b847f39cf99eab967aa13a2ef024b3ac0bf652fc14b62ebffb7750de8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 c5598ac88e1cf193bda4d93c9853dac38016ab09e3e88ccffeac71583feffb8c
MD5 eec4e4754e52911e4f449ae4c87b4329
BLAKE2b-256 dc7e856a877f376033365461e4b325a9d41a08243ce632317b191b0e91370aa4

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp311-cp311-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 f36ff22157fc3761b466213cd9f4d3669cb8e4646dbb83212a8ada928d536664
MD5 5da449764583546ad95caabfd69c9602
BLAKE2b-256 588a0e59ff0fce35fdfa02db258d349c98e81fa9c727569f37d55b35f34fd029

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 634d0bd67556238bbd800e83a36eb16fdba838e1a5f279c4781bf845bf14cb17
MD5 8dfe9b806aba60bd932cdf4e0d8834d5
BLAKE2b-256 2bbd810423747ecfbb76dfbe968890f53c4140074608330a7347078b3a2d2872

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 94cc43e2eeb6d43553bea7b288c63a91f2f95af667d71f4d7e4c3b2867d3bc9d
MD5 7a4c2811a25e218e0f8ae0a40ed40778
BLAKE2b-256 bf5fb5a917354533c1fe453f9e15e8b250d9a3435df00c7930f73d50e7dc7feb

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp311-cp311-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 e60b95b5cbe21d21d7f0f5c0c9eefa1ecbc4270dc45de50776b5ef84ecc8c137
MD5 300fca7d2a7e612f0650cb929ddaf543
BLAKE2b-256 a072a8ffb9a830e4c136901f314de84e251296146170cbc50c46c6891e9dbf86

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp311-cp311-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 64c883eac1497cb7941d63b249e568d09b61b6e2f9c60d0a74ed705bdc27e5fb
MD5 1bba1b60d53ae6443162a2b4d749a5ef
BLAKE2b-256 7a71824fb94db5aebb9ee5a2860beb609019aaa31c4e513e108e3e4f90e9ce15

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.3-cp310-cp310-win_amd64.whl
  • Upload date:
  • Size: 442.3 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.3-cp310-cp310-win_amd64.whl
Algorithm Hash digest
SHA256 a5b2158bbc2c1415c5bc35c6e5754d8805ade3f18700d71bb2446e6868138f47
MD5 6768fb0e8b7bb4c86a98f4c082e764d8
BLAKE2b-256 018858a923eca4354f5f4f2de9caaf75538737e59a195a7329913f9663907d8f

See more details on using hashes here.

File details

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

File metadata

  • Download URL: koerce-0.2.3-cp310-cp310-win32.whl
  • Upload date:
  • Size: 365.3 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.3-cp310-cp310-win32.whl
Algorithm Hash digest
SHA256 b124db4932499fd3b5769a284ddf736d047b70ed88d0ff67f322c7c0c690bc60
MD5 ea03e0abe261d9948de4f17512f19a1a
BLAKE2b-256 67f30a90d56ec23adac926c7af952077d2e1bcbe822246c2e789b7a14120ed29

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp310-cp310-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 3791fa6143021cd3bd5818fefda988184b70c0818f15d9111a907181b5ba1b7c
MD5 62da549d851710149dbc895f13a3616d
BLAKE2b-256 8678290919d9408c9202dd5ab064826496ae896152b4f6fcd24a6f6493e00ed8

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp310-cp310-musllinux_1_2_i686.whl
Algorithm Hash digest
SHA256 164ceb45faab9bff5fcba23d9b557375c0e6d9891eba7bccd47859ef1351d920
MD5 a4980fbb9e8f5b03ff293a326bc221f0
BLAKE2b-256 cb9725826887c15b7b2ca8a8e6a58242306a7e16dce1f49c02e817f9bf10fb36

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 2037d186f0b673d4ed6200901c99722bdd9af236de2320389b0baade79f2aeda
MD5 e728d522152eae64f91e52c1cf24f71e
BLAKE2b-256 6c72a3491ee6124a23179a68e2a65d69862bc884a0bc1d568ed527e88e12fd28

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 cbf01b72845a2bc147bdda49d275fafb6ec41f1443d9360f03127975696b0190
MD5 25d098165e09f98e5aeabcc880dcc02e
BLAKE2b-256 bb78bbe57e647602aaba90b0f652d84904adeafc92c7abf20e3cae62c76a760f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp310-cp310-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 4a0807b115ad87a9045717f54557e8c88ab8794ce455a8dcd4b63e1fc2212431
MD5 668380dd48e07b06eff6793c9b4e900c
BLAKE2b-256 4b94c0613922f3088955d1259476bafb3da4da91dc295fe7e4b982f8ff930ec5

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for koerce-0.2.3-cp310-cp310-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 2e4a166d37035cbf44f5637a53593a7a1b8842684ef1e69a3931972465393f51
MD5 9213ae3abe963c6027911f68a744daea
BLAKE2b-256 b671d0b1dc53a04597fd1baae01e0a3a172acc2d5dc23a9622dcb5680f72a44d

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